email-gateway
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseEmail Gateway (Multi-Provider)
多提供商邮件网关
Status: Production Ready ✅
Last Updated: 2026-01-10
Providers: Resend, SendGrid, Mailgun, SMTP2Go
状态:已就绪可用于生产环境 ✅
最后更新:2026-01-10
支持的提供商:Resend、SendGrid、Mailgun、SMTP2Go
Quick Start
快速开始
Choose your provider based on needs:
| Provider | Best For | Key Feature | Free Tier |
|---|---|---|---|
| Resend | Modern apps, React Email | JSX templates | 100/day, 3k/month |
| SendGrid | Enterprise scale | Dynamic templates | 100/day forever |
| Mailgun | Developer webhooks | Event tracking | 100/day |
| SMTP2Go | Reliable relay, AU | Simple API | 1k/month trial |
根据需求选择合适的提供商:
| 提供商 | 最佳适用场景 | 核心特性 | 免费额度 |
|---|---|---|---|
| Resend | 现代应用、React Email | JSX模板 | 每日100封,每月3000封 |
| SendGrid | 企业级规模 | 动态模板 | 永久每日100封 |
| Mailgun | 开发者Webhook | 事件追踪 | 每日100封 |
| SMTP2Go | 可靠中继、澳大利亚地区 | 简单API | 试用期每月1000封 |
Resend (Recommended for New Projects)
Resend(推荐用于新项目)
typescript
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@yourdomain.com',
to: 'user@example.com',
subject: 'Welcome!',
html: '<h1>Hello World</h1>',
}),
});
const data = await response.json();
// { id: "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794" }typescript
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@yourdomain.com',
to: 'user@example.com',
subject: 'Welcome!',
html: '<h1>Hello World</h1>',
}),
});
const data = await response.json();
// { id: "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794" }SendGrid (Enterprise)
SendGrid(企业级)
typescript
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{
to: [{ email: 'user@example.com' }],
}],
from: { email: 'noreply@yourdomain.com' },
subject: 'Welcome!',
content: [{
type: 'text/html',
value: '<h1>Hello World</h1>',
}],
}),
});
// Returns 202 on success (no body)typescript
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{
to: [{ email: 'user@example.com' }],
}],
from: { email: 'noreply@yourdomain.com' },
subject: 'Welcome!',
content: [{
type: 'text/html',
value: '<h1>Hello World</h1>',
}],
}),
});
// Returns 202 on success (no body)Mailgun
Mailgun
typescript
const formData = new FormData();
formData.append('from', 'noreply@yourdomain.com');
formData.append('to', 'user@example.com');
formData.append('subject', 'Welcome!');
formData.append('html', '<h1>Hello World</h1>');
const response = await fetch(
`https://api.mailgun.net/v3/${env.MAILGUN_DOMAIN}/messages`,
{
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`api:${env.MAILGUN_API_KEY}`)}`,
},
body: formData,
}
);
const data = await response.json();
// { id: "<20111114174239.25659.5817@samples.mailgun.org>", message: "Queued. Thank you." }typescript
const formData = new FormData();
formData.append('from', 'noreply@yourdomain.com');
formData.append('to', 'user@example.com');
formData.append('subject', 'Welcome!');
formData.append('html', '<h1>Hello World</h1>');
const response = await fetch(
`https://api.mailgun.net/v3/${env.MAILGUN_DOMAIN}/messages`,
{
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`api:${env.MAILGUN_API_KEY}`)}`,
},
body: formData,
}
);
const data = await response.json();
// { id: "<20111114174239.25659.5817@samples.mailgun.org>", message: "Queued. Thank you." }SMTP2Go
SMTP2Go
typescript
const response = await fetch('https://api.smtp2go.com/v3/email/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
api_key: env.SMTP2GO_API_KEY,
to: ['<user@example.com>'],
sender: 'noreply@yourdomain.com',
subject: 'Welcome!',
html_body: '<h1>Hello World</h1>',
}),
});
const data = await response.json();
// { data: { succeeded: 1, failed: 0, email_id: "..." } }typescript
const response = await fetch('https://api.smtp2go.com/v3/email/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
api_key: env.SMTP2GO_API_KEY,
to: ['<user@example.com>'],
sender: 'noreply@yourdomain.com',
subject: 'Welcome!',
html_body: '<h1>Hello World</h1>',
}),
});
const data = await response.json();
// { data: { succeeded: 1, failed: 0, email_id: "..." } }Provider Comparison
提供商对比
Features
功能特性
| Feature | Resend | SendGrid | Mailgun | SMTP2Go |
|---|---|---|---|---|
| React Email | ✅ Native | ❌ | ❌ | ❌ |
| Dynamic Templates | ✅ | ✅ | ✅ | ✅ |
| Batch Sending | 50/request | 1000/request | 1000/request | 100/request |
| Webhooks | ✅ | ✅ | ✅ | ✅ |
| SMTP | ✅ | ✅ | ✅ | ✅ Primary |
| IP Warmup | Managed | Manual | Manual | Managed |
| Dedicated IPs | Enterprise | $90+/mo | $80+/mo | Custom |
| Analytics | Basic | Advanced | Advanced | Good |
| A/B Testing | ❌ | ✅ | ✅ | ❌ |
| 功能 | Resend | SendGrid | Mailgun | SMTP2Go |
|---|---|---|---|---|
| React Email | ✅ 原生支持 | ❌ | ❌ | ❌ |
| 动态模板 | ✅ | ✅ | ✅ | ✅ |
| 批量发送上限 | 50收件人/请求 | 1000收件人/请求 | 1000收件人/请求 | 100收件人/请求 |
| Webhook | ✅ | ✅ | ✅ | ✅ |
| SMTP协议 | ✅ | ✅ | ✅ | ✅ 主要协议 |
| IP预热 | 托管式 | 手动 | 手动 | 托管式 |
| 独立IP | 企业版提供 | 90美元+/月 | 80美元+/月 | 定制化 |
| 数据分析 | 基础版 | 高级版 | 高级版 | 良好 |
| A/B测试 | ❌ | ✅ | ✅ | ❌ |
Rate Limits (Free Tier)
免费额度限制
| Provider | Daily | Monthly | Overage Cost |
|---|---|---|---|
| Resend | 100 | 3,000 | $1/1k |
| SendGrid | 100 | Forever | $15 for 10k |
| Mailgun | 100 | Forever | $15 for 10k |
| SMTP2Go | ~33 | 1,000 trial | $10 for 10k |
| 提供商 | 每日限额 | 每月限额 | 超额费用 |
|---|---|---|---|
| Resend | 100封 | 3000封 | 1美元/1000封 |
| SendGrid | 100封 | 永久免费 | 15美元/10000封 |
| Mailgun | 100封 | 永久免费 | 15美元/10000封 |
| SMTP2Go | 约33封 | 试用期1000封 | 10美元/10000封 |
API Limits
API请求限制
| Provider | Requests/sec | Burst | Retry After Header |
|---|---|---|---|
| Resend | 10 | Yes | ✅ |
| SendGrid | 600 | Yes | ✅ |
| Mailgun | Varies | Yes | ✅ |
| SMTP2Go | 10 | Limited | ✅ |
| 提供商 | 请求数/秒 | 突发请求支持 | 重试等待头 |
|---|---|---|---|
| Resend | 10 | 是 | ✅ |
| SendGrid | 600 | 是 | ✅ |
| Mailgun | 可变 | 是 | ✅ |
| SMTP2Go | 10 | 有限支持 | ✅ |
Message Limits
消息内容限制
| Provider | Max Size | Attachments | Max Recipients |
|---|---|---|---|
| Resend | 40 MB | 40 MB total | 50/request |
| SendGrid | 20 MB | 20 MB total | 1000/request |
| Mailgun | 25 MB | 25 MB total | 1000/request |
| SMTP2Go | 50 MB | 50 MB total | 100/request |
| 提供商 | 最大大小 | 附件限制 | 最大收件人数 |
|---|---|---|---|
| Resend | 40 MB | 总大小40 MB | 50人/请求 |
| SendGrid | 20 MB | 总大小20 MB | 1000人/请求 |
| Mailgun | 25 MB | 总大小25 MB | 1000人/请求 |
| SMTP2Go | 50 MB | 总大小50 MB | 100人/请求 |
Configuration
配置说明
Environment Variables
环境变量
bash
undefinedbash
undefinedResend
Resend
RESEND_API_KEY=re_xxxxxxxxx
RESEND_API_KEY=re_xxxxxxxxx
SendGrid
SendGrid
SENDGRID_API_KEY=SG.xxxxxxxxx
SENDGRID_API_KEY=SG.xxxxxxxxx
Mailgun
Mailgun
MAILGUN_API_KEY=xxxxxxxx-xxxxxxxx-xxxxxxxx
MAILGUN_DOMAIN=mg.yourdomain.com
MAILGUN_REGION=us # or eu
MAILGUN_API_KEY=xxxxxxxx-xxxxxxxx-xxxxxxxx
MAILGUN_DOMAIN=mg.yourdomain.com
MAILGUN_REGION=us # 或 eu
SMTP2Go
SMTP2Go
SMTP2GO_API_KEY=api-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
undefinedSMTP2GO_API_KEY=api-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
undefinedWrangler Secrets (Cloudflare Workers)
Wrangler密钥配置(Cloudflare Workers)
bash
undefinedbash
undefinedSet secrets
设置密钥
echo "re_xxxxxxxxx" | npx wrangler secret put RESEND_API_KEY
echo "SG.xxxxxxxxx" | npx wrangler secret put SENDGRID_API_KEY
echo "xxxxxxxx-xxxxxxxx-xxxxxxxx" | npx wrangler secret put MAILGUN_API_KEY
echo "api-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | npx wrangler secret put SMTP2GO_API_KEY
echo "re_xxxxxxxxx" | npx wrangler secret put RESEND_API_KEY
echo "SG.xxxxxxxxx" | npx wrangler secret put SENDGRID_API_KEY
echo "xxxxxxxx-xxxxxxxx-xxxxxxxx" | npx wrangler secret put MAILGUN_API_KEY
echo "api-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | npx wrangler secret put SMTP2GO_API_KEY
Deploy to activate
部署生效
npx wrangler deploy
undefinednpx wrangler deploy
undefinedTypeScript Types
TypeScript类型定义
typescript
// Resend
interface ResendEmail {
from: string;
to: string | string[];
subject: string;
html?: string;
text?: string;
cc?: string | string[];
bcc?: string | string[];
replyTo?: string | string[];
headers?: Record<string, string>;
attachments?: Array<{
filename: string;
content: string; // base64
}>;
tags?: Record<string, string>;
scheduledAt?: string; // ISO 8601
}
interface ResendResponse {
id: string;
}
// SendGrid
interface SendGridEmail {
personalizations: Array<{
to: Array<{ email: string; name?: string }>;
cc?: Array<{ email: string; name?: string }>;
bcc?: Array<{ email: string; name?: string }>;
subject?: string;
dynamic_template_data?: Record<string, unknown>;
}>;
from: { email: string; name?: string };
subject?: string;
content?: Array<{
type: 'text/plain' | 'text/html';
value: string;
}>;
template_id?: string;
attachments?: Array<{
content: string; // base64
filename: string;
type?: string;
disposition?: 'inline' | 'attachment';
}>;
}
// Mailgun
interface MailgunEmail {
from: string;
to: string | string[];
subject: string;
html?: string;
text?: string;
cc?: string | string[];
bcc?: string | string[];
'h:Reply-To'?: string;
template?: string;
'h:X-Mailgun-Variables'?: string; // JSON
attachment?: File | File[];
inline?: File | File[];
'o:tag'?: string | string[];
'o:tracking'?: 'yes' | 'no';
'o:tracking-clicks'?: 'yes' | 'no' | 'htmlonly';
'o:tracking-opens'?: 'yes' | 'no';
}
interface MailgunResponse {
id: string;
message: string;
}
// SMTP2Go
interface SMTP2GoEmail {
api_key: string;
to: string[];
sender: string;
subject: string;
html_body?: string;
text_body?: string;
custom_headers?: Array<{
header: string;
value: string;
}>;
attachments?: Array<{
filename: string;
fileblob: string; // base64
mimetype?: string;
}>;
}
interface SMTP2GoResponse {
data: {
succeeded: number;
failed: number;
failures?: string[];
email_id?: string;
};
}typescript
// Resend
interface ResendEmail {
from: string;
to: string | string[];
subject: string;
html?: string;
text?: string;
cc?: string | string[];
bcc?: string | string[];
replyTo?: string | string[];
headers?: Record<string, string>;
attachments?: Array<{
filename: string;
content: string; // base64
}>;
tags?: Record<string, string>;
scheduledAt?: string; // ISO 8601
}
interface ResendResponse {
id: string;
}
// SendGrid
interface SendGridEmail {
personalizations: Array<{
to: Array<{ email: string; name?: string }>;
cc?: Array<{ email: string; name?: string }>;
bcc?: Array<{ email: string; name?: string }>;
subject?: string;
dynamic_template_data?: Record<string, unknown>;
}>;
from: { email: string; name?: string };
subject?: string;
content?: Array<{
type: 'text/plain' | 'text/html';
value: string;
}>;
template_id?: string;
attachments?: Array<{
content: string; // base64
filename: string;
type?: string;
disposition?: 'inline' | 'attachment';
}>;
}
// Mailgun
interface MailgunEmail {
from: string;
to: string | string[];
subject: string;
html?: string;
text?: string;
cc?: string | string[];
bcc?: string | string[];
'h:Reply-To'?: string;
template?: string;
'h:X-Mailgun-Variables'?: string; // JSON
attachment?: File | File[];
inline?: File | File[];
'o:tag'?: string | string[];
'o:tracking'?: 'yes' | 'no';
'o:tracking-clicks'?: 'yes' | 'no' | 'htmlonly';
'o:tracking-opens'?: 'yes' | 'no';
}
interface MailgunResponse {
id: string;
message: string;
}
// SMTP2Go
interface SMTP2GoEmail {
api_key: string;
to: string[];
sender: string;
subject: string;
html_body?: string;
text_body?: string;
custom_headers?: Array<{
header: string;
value: string;
}>;
attachments?: Array<{
filename: string;
fileblob: string; // base64
mimetype?: string;
}>;
}
interface SMTP2GoResponse {
data: {
succeeded: number;
failed: number;
failures?: string[];
email_id?: string;
};
}Common Patterns
常见使用模式
1. Transactional Emails
1. 事务性邮件
Password Reset:
typescript
// templates/password-reset.ts
export async function sendPasswordReset(
provider: 'resend' | 'sendgrid' | 'mailgun' | 'smtp2go',
to: string,
resetToken: string,
env: Env
): Promise<{ success: boolean; id?: string; error?: string }> {
const resetUrl = `https://yourapp.com/reset-password?token=${resetToken}`;
const html = `
<h1>Reset Your Password</h1>
<p>Click the link below to reset your password:</p>
<a href="${resetUrl}">Reset Password</a>
<p>This link expires in 1 hour.</p>
`;
switch (provider) {
case 'resend':
return sendViaResend(to, 'Reset Your Password', html, env);
case 'sendgrid':
return sendViaSendGrid(to, 'Reset Your Password', html, env);
case 'mailgun':
return sendViaMailgun(to, 'Reset Your Password', html, env);
case 'smtp2go':
return sendViaSMTP2Go(to, 'Reset Your Password', html, env);
}
}
async function sendViaResend(
to: string,
subject: string,
html: string,
env: Env
): Promise<{ success: boolean; id?: string; error?: string }> {
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@yourdomain.com',
to,
subject,
html,
}),
});
if (!response.ok) {
const error = await response.text();
return { success: false, error };
}
const data = await response.json();
return { success: true, id: data.id };
}密码重置:
typescript
// templates/password-reset.ts
export async function sendPasswordReset(
provider: 'resend' | 'sendgrid' | 'mailgun' | 'smtp2go',
to: string,
resetToken: string,
env: Env
): Promise<{ success: boolean; id?: string; error?: string }> {
const resetUrl = `https://yourapp.com/reset-password?token=${resetToken}`;
const html = `
<h1>Reset Your Password</h1>
<p>Click the link below to reset your password:</p>
<a href="${resetUrl}">Reset Password</a>
<p>This link expires in 1 hour.</p>
`;
switch (provider) {
case 'resend':
return sendViaResend(to, 'Reset Your Password', html, env);
case 'sendgrid':
return sendViaSendGrid(to, 'Reset Your Password', html, env);
case 'mailgun':
return sendViaMailgun(to, 'Reset Your Password', html, env);
case 'smtp2go':
return sendViaSMTP2Go(to, 'Reset Your Password', html, env);
}
}
async function sendViaResend(
to: string,
subject: string,
html: string,
env: Env
): Promise<{ success: boolean; id?: string; error?: string }> {
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@yourdomain.com',
to,
subject,
html,
}),
});
if (!response.ok) {
const error = await response.text();
return { success: false, error };
}
const data = await response.json();
return { success: true, id: data.id };
}2. Batch Sending
2. 批量发送
Resend (max 50 recipients):
typescript
async function sendBatchResend(
recipients: string[],
subject: string,
html: string,
env: Env
): Promise<Array<{ email: string; id?: string; error?: string }>> {
const results: Array<{ email: string; id?: string; error?: string }> = [];
// Chunk into groups of 50
for (let i = 0; i < recipients.length; i += 50) {
const chunk = recipients.slice(i, i + 50);
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@yourdomain.com',
to: chunk,
subject,
html,
}),
});
if (response.ok) {
const data = await response.json();
chunk.forEach(email => results.push({ email, id: data.id }));
} else {
const error = await response.text();
chunk.forEach(email => results.push({ email, error }));
}
}
return results;
}SendGrid (max 1000 personalizations):
typescript
async function sendBatchSendGrid(
recipients: Array<{ email: string; name?: string; data?: Record<string, unknown> }>,
templateId: string,
env: Env
): Promise<{ success: boolean; error?: string }> {
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: recipients.map(r => ({
to: [{ email: r.email, name: r.name }],
dynamic_template_data: r.data || {},
})),
from: { email: 'noreply@yourdomain.com' },
template_id: templateId,
}),
});
if (!response.ok) {
const error = await response.text();
return { success: false, error };
}
return { success: true };
}Resend(最多50收件人):
typescript
async function sendBatchResend(
recipients: string[],
subject: string,
html: string,
env: Env
): Promise<Array<{ email: string; id?: string; error?: string }>> {
const results: Array<{ email: string; id?: string; error?: string }> = [];
// 按50人一组拆分
for (let i = 0; i < recipients.length; i += 50) {
const chunk = recipients.slice(i, i + 50);
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@yourdomain.com',
to: chunk,
subject,
html,
}),
});
if (response.ok) {
const data = await response.json();
chunk.forEach(email => results.push({ email, id: data.id }));
} else {
const error = await response.text();
chunk.forEach(email => results.push({ email, error }));
}
}
return results;
}SendGrid(最多1000个个性化设置):
typescript
async function sendBatchSendGrid(
recipients: Array<{ email: string; name?: string; data?: Record<string, unknown> }>,
templateId: string,
env: Env
): Promise<{ success: boolean; error?: string }> {
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: recipients.map(r => ({
to: [{ email: r.email, name: r.name }],
dynamic_template_data: r.data || {},
})),
from: { email: 'noreply@yourdomain.com' },
template_id: templateId,
}),
});
if (!response.ok) {
const error = await response.text();
return { success: false, error };
}
return { success: true };
}3. React Email Templates (Resend Only)
3. React Email模板(仅Resend支持)
Install React Email:
bash
npm install react-email @react-email/componentsCreate Template:
tsx
// emails/welcome.tsx
import {
Html,
Head,
Body,
Container,
Heading,
Text,
Button,
} from '@react-email/components';
interface WelcomeEmailProps {
name: string;
confirmUrl: string;
}
export default function WelcomeEmail({ name, confirmUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Body style={{ fontFamily: 'Arial, sans-serif' }}>
<Container>
<Heading>Welcome, {name}!</Heading>
<Text>Thanks for signing up. Please confirm your email address:</Text>
<Button href={confirmUrl} style={{ background: '#000', color: '#fff' }}>
Confirm Email
</Button>
</Container>
</Body>
</Html>
);
}Send via Resend SDK (Node.js):
typescript
import { Resend } from 'resend';
import WelcomeEmail from './emails/welcome';
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: 'noreply@yourdomain.com',
to: 'user@example.com',
subject: 'Welcome!',
react: WelcomeEmail({ name: 'Alice', confirmUrl: 'https://...' }),
});Send via Workers (render to HTML first):
typescript
import { render } from '@react-email/render';
import WelcomeEmail from './emails/welcome';
const html = render(WelcomeEmail({ name: 'Alice', confirmUrl: 'https://...' }));
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@yourdomain.com',
to: 'user@example.com',
subject: 'Welcome!',
html,
}),
});安装React Email:
bash
npm install react-email @react-email/components创建模板:
tsx
// emails/welcome.tsx
import {
Html,
Head,
Body,
Container,
Heading,
Text,
Button,
} from '@react-email/components';
interface WelcomeEmailProps {
name: string;
confirmUrl: string;
}
export default function WelcomeEmail({ name, confirmUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Body style={{ fontFamily: 'Arial, sans-serif' }}>
<Container>
<Heading>Welcome, {name}!</Heading>
<Text>Thanks for signing up. Please confirm your email address:</Text>
<Button href={confirmUrl} style={{ background: '#000', color: '#fff' }}>
Confirm Email
</Button>
</Container>
</Body>
</Html>
);
}通过Resend SDK发送(Node.js):
typescript
import { Resend } from 'resend';
import WelcomeEmail from './emails/welcome';
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: 'noreply@yourdomain.com',
to: 'user@example.com',
subject: 'Welcome!',
react: WelcomeEmail({ name: 'Alice', confirmUrl: 'https://...' }),
});通过Workers发送(先渲染为HTML):
typescript
import { render } from '@react-email/render';
import WelcomeEmail from './emails/welcome';
const html = render(WelcomeEmail({ name: 'Alice', confirmUrl: 'https://...' }));
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@yourdomain.com',
to: 'user@example.com',
subject: 'Welcome!',
html,
}),
});4. Dynamic Templates
4. 动态模板
SendGrid:
typescript
// 1. Create template in SendGrid dashboard with handlebars
// Subject: Welcome {{name}}!
// Body: <h1>Hi {{name}}</h1><p>Your code: {{confirmationCode}}</p>
// 2. Send with template ID
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{
to: [{ email: 'user@example.com' }],
dynamic_template_data: {
name: 'Alice',
confirmationCode: 'ABC123',
},
}],
from: { email: 'noreply@yourdomain.com' },
template_id: 'd-xxxxxxxxxxxxxxxxxxxxxxxx',
}),
});Mailgun:
typescript
// 1. Create template in Mailgun dashboard or via API
// Use {{name}} and {{confirmationCode}} variables
// 2. Send with template name
const formData = new FormData();
formData.append('from', 'noreply@yourdomain.com');
formData.append('to', 'user@example.com');
formData.append('subject', 'Welcome');
formData.append('template', 'welcome-template');
formData.append('h:X-Mailgun-Variables', JSON.stringify({
name: 'Alice',
confirmationCode: 'ABC123',
}));
const response = await fetch(
`https://api.mailgun.net/v3/${env.MAILGUN_DOMAIN}/messages`,
{
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`api:${env.MAILGUN_API_KEY}`)}`,
},
body: formData,
}
);SendGrid:
typescript
// 1. 在SendGrid控制台创建模板,使用handlebars语法
// 主题: Welcome {{name}}!
// 内容: <h1>Hi {{name}}</h1><p>Your code: {{confirmationCode}}</p>
// 2. 使用模板ID发送
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{
to: [{ email: 'user@example.com' }],
dynamic_template_data: {
name: 'Alice',
confirmationCode: 'ABC123',
},
}],
from: { email: 'noreply@yourdomain.com' },
template_id: 'd-xxxxxxxxxxxxxxxxxxxxxxxx',
}),
});Mailgun:
typescript
// 1. 在Mailgun控制台或通过API创建模板
// 使用{{name}}和{{confirmationCode}}变量
// 2. 使用模板名称发送
const formData = new FormData();
formData.append('from', 'noreply@yourdomain.com');
formData.append('to', 'user@example.com');
formData.append('subject', 'Welcome');
formData.append('template', 'welcome-template');
formData.append('h:X-Mailgun-Variables', JSON.stringify({
name: 'Alice',
confirmationCode: 'ABC123',
}));
const response = await fetch(
`https://api.mailgun.net/v3/${env.MAILGUN_DOMAIN}/messages`,
{
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`api:${env.MAILGUN_API_KEY}`)}`,
},
body: formData,
}
);5. Attachments
5. 附件处理
Resend:
typescript
const fileBuffer = await file.arrayBuffer();
const base64Content = btoa(String.fromCharCode(...new Uint8Array(fileBuffer)));
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@yourdomain.com',
to: 'user@example.com',
subject: 'Your Invoice',
html: '<p>Attached is your invoice.</p>',
attachments: [{
filename: 'invoice.pdf',
content: base64Content,
}],
}),
});SendGrid:
typescript
const fileBuffer = await file.arrayBuffer();
const base64Content = btoa(String.fromCharCode(...new Uint8Array(fileBuffer)));
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{
to: [{ email: 'user@example.com' }],
}],
from: { email: 'noreply@yourdomain.com' },
subject: 'Your Invoice',
content: [{ type: 'text/html', value: '<p>Attached is your invoice.</p>' }],
attachments: [{
content: base64Content,
filename: 'invoice.pdf',
type: 'application/pdf',
disposition: 'attachment',
}],
}),
});Mailgun (uses FormData with File):
typescript
const formData = new FormData();
formData.append('from', 'noreply@yourdomain.com');
formData.append('to', 'user@example.com');
formData.append('subject', 'Your Invoice');
formData.append('html', '<p>Attached is your invoice.</p>');
formData.append('attachment', file); // File object directly
const response = await fetch(
`https://api.mailgun.net/v3/${env.MAILGUN_DOMAIN}/messages`,
{
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`api:${env.MAILGUN_API_KEY}`)}`,
},
body: formData,
}
);Resend:
typescript
const fileBuffer = await file.arrayBuffer();
const base64Content = btoa(String.fromCharCode(...new Uint8Array(fileBuffer)));
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@yourdomain.com',
to: 'user@example.com',
subject: 'Your Invoice',
html: '<p>Attached is your invoice.</p>',
attachments: [{
filename: 'invoice.pdf',
content: base64Content,
}],
}),
});SendGrid:
typescript
const fileBuffer = await file.arrayBuffer();
const base64Content = btoa(String.fromCharCode(...new Uint8Array(fileBuffer)));
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{
to: [{ email: 'user@example.com' }],
}],
from: { email: 'noreply@yourdomain.com' },
subject: 'Your Invoice',
content: [{ type: 'text/html', value: '<p>Attached is your invoice.</p>' }],
attachments: [{
content: base64Content,
filename: 'invoice.pdf',
type: 'application/pdf',
disposition: 'attachment',
}],
}),
});Mailgun(使用FormData和File对象):
typescript
const formData = new FormData();
formData.append('from', 'noreply@yourdomain.com');
formData.append('to', 'user@example.com');
formData.append('subject', 'Your Invoice');
formData.append('html', '<p>Attached is your invoice.</p>');
formData.append('attachment', file); // 直接传入File对象
const response = await fetch(
`https://api.mailgun.net/v3/${env.MAILGUN_DOMAIN}/messages`,
{
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`api:${env.MAILGUN_API_KEY}`)}`,
},
body: formData,
}
);6. Webhooks (Event Tracking)
6. Webhook(事件追踪)
Resend Webhooks:
Events: , , , , , ,
email.sentemail.deliveredemail.delivery_delayedemail.bouncedemail.complainedemail.openedemail.clickedtypescript
// Verify webhook signature
import { createHmac } from 'crypto';
export async function verifyResendWebhook(
request: Request,
secret: string
): Promise<boolean> {
const signature = request.headers.get('svix-signature');
const timestamp = request.headers.get('svix-timestamp');
const body = await request.text();
if (!signature || !timestamp) return false;
const signedContent = `${timestamp}.${body}`;
const expectedSignature = createHmac('sha256', secret)
.update(signedContent)
.digest('base64');
return signature.includes(expectedSignature);
}
// Handle webhook
export async function handleResendWebhook(request: Request, env: Env) {
const isValid = await verifyResendWebhook(request, env.RESEND_WEBHOOK_SECRET);
if (!isValid) {
return new Response('Invalid signature', { status: 401 });
}
const event = await request.json();
switch (event.type) {
case 'email.bounced':
// Mark email as invalid
await markEmailInvalid(event.data.email);
break;
case 'email.complained':
// Unsubscribe user
await unsubscribeUser(event.data.email);
break;
}
return new Response('OK');
}SendGrid Webhooks:
typescript
// Verify webhook signature (requires express-style body parser)
import { EventWebhook, EventWebhookHeader } from '@sendgrid/eventwebhook';
export async function verifySendGridWebhook(
request: Request,
publicKey: string
): Promise<boolean> {
const signature = request.headers.get(EventWebhookHeader.SIGNATURE());
const timestamp = request.headers.get(EventWebhookHeader.TIMESTAMP());
const body = await request.text();
const eventWebhook = new EventWebhook();
const ecPublicKey = eventWebhook.convertPublicKeyToECDSA(publicKey);
return eventWebhook.verifySignature(
ecPublicKey,
body,
signature!,
timestamp!
);
}
// Handle webhook
export async function handleSendGridWebhook(request: Request, env: Env) {
const events = await request.json();
for (const event of events) {
switch (event.event) {
case 'bounce':
await markEmailInvalid(event.email);
break;
case 'spamreport':
await unsubscribeUser(event.email);
break;
}
}
return new Response('OK');
}Mailgun Webhooks:
typescript
// Verify webhook signature
import { createHmac } from 'crypto';
export function verifyMailgunWebhook(
timestamp: string,
token: string,
signature: string,
signingKey: string
): boolean {
const encoded = createHmac('sha256', signingKey)
.update(`${timestamp}${token}`)
.digest('hex');
return encoded === signature;
}
// Handle webhook
export async function handleMailgunWebhook(request: Request, env: Env) {
const data = await request.json();
const isValid = verifyMailgunWebhook(
data.signature.timestamp,
data.signature.token,
data.signature.signature,
env.MAILGUN_WEBHOOK_SIGNING_KEY
);
if (!isValid) {
return new Response('Invalid signature', { status: 401 });
}
switch (data['event-data'].event) {
case 'failed':
if (data['event-data'].severity === 'permanent') {
await markEmailInvalid(data['event-data'].recipient);
}
break;
case 'complained':
await unsubscribeUser(data['event-data'].recipient);
break;
}
return new Response('OK');
}SMTP2Go Webhooks:
typescript
// Verify webhook signature
import { createHmac } from 'crypto';
export function verifySMTP2GoWebhook(
body: string,
signature: string,
secret: string
): boolean {
const expectedSignature = createHmac('sha256', secret)
.update(body)
.digest('hex');
return expectedSignature === signature;
}
// Handle webhook
export async function handleSMTP2GoWebhook(request: Request, env: Env) {
const signature = request.headers.get('X-Smtp2go-Signature');
const body = await request.text();
if (!signature || !verifySMTP2GoWebhook(body, signature, env.SMTP2GO_WEBHOOK_SECRET)) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
switch (event.event) {
case 'bounce':
await markEmailInvalid(event.email);
break;
case 'spam':
await unsubscribeUser(event.email);
break;
}
return new Response('OK');
}Resend Webhook:
事件类型: , , , , , ,
email.sentemail.deliveredemail.delivery_delayedemail.bouncedemail.complainedemail.openedemail.clickedtypescript
// 验证Webhook签名
import { createHmac } from 'crypto';
export async function verifyResendWebhook(
request: Request,
secret: string
): Promise<boolean> {
const signature = request.headers.get('svix-signature');
const timestamp = request.headers.get('svix-timestamp');
const body = await request.text();
if (!signature || !timestamp) return false;
const signedContent = `${timestamp}.${body}`;
const expectedSignature = createHmac('sha256', secret)
.update(signedContent)
.digest('base64');
return signature.includes(expectedSignature);
}
// 处理Webhook
export async function handleResendWebhook(request: Request, env: Env) {
const isValid = await verifyResendWebhook(request, env.RESEND_WEBHOOK_SECRET);
if (!isValid) {
return new Response('Invalid signature', { status: 401 });
}
const event = await request.json();
switch (event.type) {
case 'email.bounced':
// 标记邮箱为无效
await markEmailInvalid(event.data.email);
break;
case 'email.complained':
// 取消用户订阅
await unsubscribeUser(event.data.email);
break;
}
return new Response('OK');
}SendGrid Webhook:
typescript
// 验证Webhook签名(需要express风格的body解析器)
import { EventWebhook, EventWebhookHeader } from '@sendgrid/eventwebhook';
export async function verifySendGridWebhook(
request: Request,
publicKey: string
): Promise<boolean> {
const signature = request.headers.get(EventWebhookHeader.SIGNATURE());
const timestamp = request.headers.get(EventWebhookHeader.TIMESTAMP());
const body = await request.text();
const eventWebhook = new EventWebhook();
const ecPublicKey = eventWebhook.convertPublicKeyToECDSA(publicKey);
return eventWebhook.verifySignature(
ecPublicKey,
body,
signature!,
timestamp!
);
}
// 处理Webhook
export async function handleSendGridWebhook(request: Request, env: Env) {
const events = await request.json();
for (const event of events) {
switch (event.event) {
case 'bounce':
await markEmailInvalid(event.email);
break;
case 'spamreport':
await unsubscribeUser(event.email);
break;
}
}
return new Response('OK');
}Mailgun Webhook:
typescript
// 验证Webhook签名
import { createHmac } from 'crypto';
export function verifyMailgunWebhook(
timestamp: string,
token: string,
signature: string,
signingKey: string
): boolean {
const encoded = createHmac('sha256', signingKey)
.update(`${timestamp}${token}`)
.digest('hex');
return encoded === signature;
}
// 处理Webhook
export async function handleMailgunWebhook(request: Request, env: Env) {
const data = await request.json();
const isValid = verifyMailgunWebhook(
data.signature.timestamp,
data.signature.token,
data.signature.signature,
env.MAILGUN_WEBHOOK_SIGNING_KEY
);
if (!isValid) {
return new Response('Invalid signature', { status: 401 });
}
switch (data['event-data'].event) {
case 'failed':
if (data['event-data'].severity === 'permanent') {
await markEmailInvalid(data['event-data'].recipient);
}
break;
case 'complained':
await unsubscribeUser(data['event-data'].recipient);
break;
}
return new Response('OK');
}SMTP2Go Webhook:
typescript
// 验证Webhook签名
import { createHmac } from 'crypto';
export function verifySMTP2GoWebhook(
body: string,
signature: string,
secret: string
): boolean {
const expectedSignature = createHmac('sha256', secret)
.update(body)
.digest('hex');
return expectedSignature === signature;
}
// 处理Webhook
export async function handleSMTP2GoWebhook(request: Request, env: Env) {
const signature = request.headers.get('X-Smtp2go-Signature');
const body = await request.text();
if (!signature || !verifySMTP2GoWebhook(body, signature, env.SMTP2GO_WEBHOOK_SECRET)) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
switch (event.event) {
case 'bounce':
await markEmailInvalid(event.email);
break;
case 'spam':
await unsubscribeUser(event.email);
break;
}
return new Response('OK');
}Error Handling
错误处理
Resend Errors
Resend错误
| Status | Error | Cause | Fix |
|---|---|---|---|
| 401 | Unauthorized | Invalid API key | Check RESEND_API_KEY |
| 422 | Validation error | Invalid email format | Validate emails before sending |
| 429 | Rate limit exceeded | Too many requests | Implement exponential backoff |
| 500 | Internal error | Resend service issue | Retry with backoff |
Common validation errors:
- field required
to - Invalid email format
- domain not verified
from - Attachment size exceeds 40 MB
Error response format:
json
{
"statusCode": 422,
"message": "Validation error",
"name": "validation_error"
}| 状态码 | 错误类型 | 原因 | 修复方案 |
|---|---|---|---|
| 401 | 未授权 | API密钥无效 | 检查RESEND_API_KEY配置 |
| 422 | 验证错误 | 邮箱格式无效 | 发送前验证邮箱格式 |
| 429 | 请求超限 | 请求次数过多 | 实现指数退避重试机制 |
| 500 | 内部错误 | Resend服务故障 | 带退避机制重试 |
常见验证错误:
- 缺少字段
to - 邮箱格式无效
- 域名未验证
from - 附件大小超过40 MB
错误响应格式:
json
{
"statusCode": 422,
"message": "Validation error",
"name": "validation_error"
}SendGrid Errors
SendGrid错误
| Status | Error | Cause | Fix |
|---|---|---|---|
| 400 | Bad request | Malformed JSON | Check request structure |
| 401 | Unauthorized | Invalid API key | Check SENDGRID_API_KEY |
| 413 | Payload too large | Message > 20 MB | Reduce attachment size |
| 429 | Too many requests | Rate limit | Implement backoff |
Common errors:
- Missing
personalizations - Invalid template ID
- Unverified sender address
- Attachment encoding issues
Error response format:
json
{
"errors": [
{
"message": "The from email does not contain a valid address.",
"field": "from.email",
"help": "http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#message.from.email"
}
]
}| 状态码 | 错误类型 | 原因 | 修复方案 |
|---|---|---|---|
| 400 | 请求错误 | JSON格式错误 | 检查请求结构 |
| 401 | 未授权 | API密钥无效 | 检查SENDGRID_API_KEY配置 |
| 413 | 请求过大 | 消息大小超过20 MB | 减小附件大小 |
| 429 | 请求超限 | 请求次数过多 | 实现退避重试机制 |
常见错误:
- 缺少字段
personalizations - 模板ID无效
- 发件人地址未验证
- 附件编码问题
错误响应格式:
json
{
"errors": [
{
"message": "The from email does not contain a valid address.",
"field": "from.email",
"help": "http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#message.from.email"
}
]
}Mailgun Errors
Mailgun错误
| Status | Error | Cause | Fix |
|---|---|---|---|
| 400 | Bad request | Invalid parameters | Check FormData fields |
| 401 | Unauthorized | Invalid API key | Check MAILGUN_API_KEY |
| 402 | Payment required | Quota exceeded | Upgrade plan |
| 404 | Not found | Invalid domain | Check MAILGUN_DOMAIN |
Common errors:
- Domain not verified
- Wrong region (US vs EU)
- Invalid template variables
- Recipient address syntax
Error response format:
json
{
"message": "Domain not found: invalid.domain.com"
}| 状态码 | 错误类型 | 原因 | 修复方案 |
|---|---|---|---|
| 400 | 请求错误 | 参数无效 | 检查FormData字段 |
| 401 | 未授权 | API密钥无效 | 检查MAILGUN_API_KEY配置 |
| 402 | 支付要求 | 额度超限 | 升级服务套餐 |
| 404 | 未找到 | 域名无效 | 检查MAILGUN_DOMAIN配置 |
常见错误:
- 域名未验证
- 区域选择错误(美国vs欧盟)
- 模板变量无效
- 收件人地址语法错误
错误响应格式:
json
{
"message": "Domain not found: invalid.domain.com"
}SMTP2Go Errors
SMTP2Go错误
| Status | Error | Cause | Fix |
|---|---|---|---|
| 401 | Unauthorized | Invalid API key | Check SMTP2GO_API_KEY |
| 422 | Validation error | Invalid email format | Validate recipients |
| 429 | Rate limit | Too many requests | Implement backoff |
Common errors:
- Sender domain not verified
- Invalid recipient format (must use angle brackets: )
<email@domain.com> - API key not activated
Error response format:
json
{
"data": {
"error": "Invalid sender email address",
"error_code": "E_ApiResponseCodes_INVALID_SENDER_ADDRESS"
}
}| 状态码 | 错误类型 | 原因 | 修复方案 |
|---|---|---|---|
| 401 | 未授权 | API密钥无效 | 检查SMTP2GO_API_KEY配置 |
| 422 | 验证错误 | 邮箱格式无效 | 验证收件人格式 |
| 429 | 请求超限 | 请求次数过多 | 实现退避重试机制 |
常见错误:
- 发件人域名未验证
- 收件人格式无效(必须使用尖括号:)
<email@domain.com> - API密钥未激活
错误响应格式:
json
{
"data": {
"error": "Invalid sender email address",
"error_code": "E_ApiResponseCodes_INVALID_SENDER_ADDRESS"
}
}Rate Limiting & Retry
请求限制与重试
Exponential Backoff Pattern
指数退避重试模式
typescript
async function sendWithRetry(
sendFn: () => Promise<Response>,
maxRetries = 3
): Promise<Response> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await sendFn();
if (response.ok) {
return response;
}
// Check if retryable
if (response.status === 429 || response.status >= 500) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// Non-retryable error
return response;
} catch (error) {
lastError = error as Error;
if (attempt < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
}
throw lastError || new Error('Max retries exceeded');
}
// Usage
const response = await sendWithRetry(() =>
fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(email),
})
);typescript
async function sendWithRetry(
sendFn: () => Promise<Response>,
maxRetries = 3
): Promise<Response> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await sendFn();
if (response.ok) {
return response;
}
// 检查是否可重试
if (response.status === 429 || response.status >= 500) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// 不可重试错误
return response;
} catch (error) {
lastError = error as Error;
if (attempt < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
}
throw lastError || new Error('Max retries exceeded');
}
// 使用示例
const response = await sendWithRetry(() =>
fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(email),
})
);Rate Limit Tracking
请求限制追踪
typescript
// Use KV to track rate limits per provider
interface RateLimitState {
count: number;
resetAt: number;
}
async function checkRateLimit(
provider: string,
kv: KVNamespace
): Promise<{ allowed: boolean; resetAt?: number }> {
const key = `rate-limit:${provider}`;
const stateJson = await kv.get(key);
const state: RateLimitState = stateJson ? JSON.parse(stateJson) : null;
const now = Date.now();
if (!state || now > state.resetAt) {
// Reset window
const limits: Record<string, number> = {
resend: 10,
sendgrid: 600,
mailgun: 100,
smtp2go: 10,
};
const newState: RateLimitState = {
count: 1,
resetAt: now + 1000, // 1 second window
};
await kv.put(key, JSON.stringify(newState), { expirationTtl: 60 });
return { allowed: true };
}
const limits: Record<string, number> = {
resend: 10,
sendgrid: 600,
mailgun: 100,
smtp2go: 10,
};
if (state.count >= limits[provider]) {
return { allowed: false, resetAt: state.resetAt };
}
state.count++;
await kv.put(key, JSON.stringify(state), { expirationTtl: 60 });
return { allowed: true };
}typescript
// 使用KV存储追踪各提供商的请求限制
interface RateLimitState {
count: number;
resetAt: number;
}
async function checkRateLimit(
provider: string,
kv: KVNamespace
): Promise<{ allowed: boolean; resetAt?: number }> {
const key = `rate-limit:${provider}`;
const stateJson = await kv.get(key);
const state: RateLimitState = stateJson ? JSON.parse(stateJson) : null;
const now = Date.now();
if (!state || now > state.resetAt) {
// 重置时间窗口
const limits: Record<string, number> = {
resend: 10,
sendgrid: 600,
mailgun: 100,
smtp2go: 10,
};
const newState: RateLimitState = {
count: 1,
resetAt: now + 1000, // 1秒时间窗口
};
await kv.put(key, JSON.stringify(newState), { expirationTtl: 60 });
return { allowed: true };
}
const limits: Record<string, number> = {
resend: 10,
sendgrid: 600,
mailgun: 100,
smtp2go: 10,
};
if (state.count >= limits[provider]) {
return { allowed: false, resetAt: state.resetAt };
}
state.count++;
await kv.put(key, JSON.stringify(state), { expirationTtl: 60 });
return { allowed: true };
}Migration Between Providers
提供商迁移
Provider Abstraction
提供商抽象封装
typescript
// types.ts
export interface EmailProvider {
send(email: EmailMessage): Promise<EmailResult>;
sendBatch(emails: EmailMessage[]): Promise<EmailResult[]>;
}
export interface EmailMessage {
from: string;
to: string | string[];
subject: string;
html?: string;
text?: string;
attachments?: Attachment[];
tags?: Record<string, string>;
}
export interface EmailResult {
success: boolean;
id?: string;
error?: string;
}
export interface Attachment {
filename: string;
content: string; // base64
}
// providers/resend.ts
export class ResendProvider implements EmailProvider {
constructor(private apiKey: string) {}
async send(email: EmailMessage): Promise<EmailResult> {
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: email.from,
to: email.to,
subject: email.subject,
html: email.html,
text: email.text,
attachments: email.attachments,
tags: email.tags,
}),
});
if (!response.ok) {
return { success: false, error: await response.text() };
}
const data = await response.json();
return { success: true, id: data.id };
}
async sendBatch(emails: EmailMessage[]): Promise<EmailResult[]> {
return Promise.all(emails.map(email => this.send(email)));
}
}
// providers/sendgrid.ts
export class SendGridProvider implements EmailProvider {
constructor(private apiKey: string) {}
async send(email: EmailMessage): Promise<EmailResult> {
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{
to: Array.isArray(email.to)
? email.to.map(e => ({ email: e }))
: [{ email: email.to }],
}],
from: { email: email.from },
subject: email.subject,
content: email.html
? [{ type: 'text/html', value: email.html }]
: [{ type: 'text/plain', value: email.text || '' }],
attachments: email.attachments?.map(a => ({
content: a.content,
filename: a.filename,
})),
}),
});
if (!response.ok) {
return { success: false, error: await response.text() };
}
return { success: true };
}
async sendBatch(emails: EmailMessage[]): Promise<EmailResult[]> {
// SendGrid supports batch via personalizations
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: emails.map(email => ({
to: Array.isArray(email.to)
? email.to.map(e => ({ email: e }))
: [{ email: email.to }],
subject: email.subject,
})),
from: { email: emails[0].from },
content: emails[0].html
? [{ type: 'text/html', value: emails[0].html }]
: [{ type: 'text/plain', value: emails[0].text || '' }],
}),
});
if (!response.ok) {
return emails.map(() => ({ success: false, error: await response.text() }));
}
return emails.map(() => ({ success: true }));
}
}
// Usage
const provider = env.EMAIL_PROVIDER === 'resend'
? new ResendProvider(env.RESEND_API_KEY)
: new SendGridProvider(env.SENDGRID_API_KEY);
await provider.send({
from: 'noreply@yourdomain.com',
to: 'user@example.com',
subject: 'Welcome',
html: '<h1>Hello</h1>',
});typescript
// types.ts
export interface EmailProvider {
send(email: EmailMessage): Promise<EmailResult>;
sendBatch(emails: EmailMessage[]): Promise<EmailResult[]>;
}
export interface EmailMessage {
from: string;
to: string | string[];
subject: string;
html?: string;
text?: string;
attachments?: Attachment[];
tags?: Record<string, string>;
}
export interface EmailResult {
success: boolean;
id?: string;
error?: string;
}
export interface Attachment {
filename: string;
content: string; // base64
}
// providers/resend.ts
export class ResendProvider implements EmailProvider {
constructor(private apiKey: string) {}
async send(email: EmailMessage): Promise<EmailResult> {
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: email.from,
to: email.to,
subject: email.subject,
html: email.html,
text: email.text,
attachments: email.attachments,
tags: email.tags,
}),
});
if (!response.ok) {
return { success: false, error: await response.text() };
}
const data = await response.json();
return { success: true, id: data.id };
}
async sendBatch(emails: EmailMessage[]): Promise<EmailResult[]> {
return Promise.all(emails.map(email => this.send(email)));
}
}
// providers/sendgrid.ts
export class SendGridProvider implements EmailProvider {
constructor(private apiKey: string) {}
async send(email: EmailMessage): Promise<EmailResult> {
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{
to: Array.isArray(email.to)
? email.to.map(e => ({ email: e }))
: [{ email: email.to }],
}],
from: { email: email.from },
subject: email.subject,
content: email.html
? [{ type: 'text/html', value: email.html }]
: [{ type: 'text/plain', value: email.text || '' }],
attachments: email.attachments?.map(a => ({
content: a.content,
filename: a.filename,
})),
}),
});
if (!response.ok) {
return { success: false, error: await response.text() };
}
return { success: true };
}
async sendBatch(emails: EmailMessage[]): Promise<EmailResult[]> {
// SendGrid通过personalizations支持批量发送
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: emails.map(email => ({
to: Array.isArray(email.to)
? email.to.map(e => ({ email: e }))
: [{ email: email.to }],
subject: email.subject,
})),
from: { email: emails[0].from },
content: emails[0].html
? [{ type: 'text/html', value: emails[0].html }]
: [{ type: 'text/plain', value: emails[0].text || '' }],
}),
});
if (!response.ok) {
return emails.map(() => ({ success: false, error: await response.text() }));
}
return emails.map(() => ({ success: true }));
}
}
// 使用示例
const provider = env.EMAIL_PROVIDER === 'resend'
? new ResendProvider(env.RESEND_API_KEY)
: new SendGridProvider(env.SENDGRID_API_KEY);
await provider.send({
from: 'noreply@yourdomain.com',
to: 'user@example.com',
subject: 'Welcome',
html: '<h1>Hello</h1>',
});Testing
测试
Test Provider Connectivity
测试提供商连通性
typescript
export async function testEmailProvider(
provider: 'resend' | 'sendgrid' | 'mailgun' | 'smtp2go',
env: Env
): Promise<{ success: boolean; error?: string }> {
const testEmail = {
from: 'test@yourdomain.com',
to: 'test@yourdomain.com',
subject: 'Test Email',
html: '<p>This is a test email.</p>',
};
try {
switch (provider) {
case 'resend': {
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(testEmail),
});
if (!response.ok) {
return { success: false, error: await response.text() };
}
return { success: true };
}
case 'sendgrid': {
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{ to: [{ email: testEmail.to }] }],
from: { email: testEmail.from },
subject: testEmail.subject,
content: [{ type: 'text/html', value: testEmail.html }],
}),
});
if (!response.ok) {
return { success: false, error: await response.text() };
}
return { success: true };
}
case 'mailgun': {
const formData = new FormData();
formData.append('from', testEmail.from);
formData.append('to', testEmail.to);
formData.append('subject', testEmail.subject);
formData.append('html', testEmail.html);
const response = await fetch(
`https://api.mailgun.net/v3/${env.MAILGUN_DOMAIN}/messages`,
{
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`api:${env.MAILGUN_API_KEY}`)}`,
},
body: formData,
}
);
if (!response.ok) {
return { success: false, error: await response.text() };
}
return { success: true };
}
case 'smtp2go': {
const response = await fetch('https://api.smtp2go.com/v3/email/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
api_key: env.SMTP2GO_API_KEY,
to: [`<${testEmail.to}>`],
sender: testEmail.from,
subject: testEmail.subject,
html_body: testEmail.html,
}),
});
if (!response.ok) {
return { success: false, error: await response.text() };
}
const data = await response.json();
if (data.data.failed > 0) {
return { success: false, error: data.data.failures?.join(', ') };
}
return { success: true };
}
}
} catch (error) {
return { success: false, error: (error as Error).message };
}
}typescript
export async function testEmailProvider(
provider: 'resend' | 'sendgrid' | 'mailgun' | 'smtp2go',
env: Env
): Promise<{ success: boolean; error?: string }> {
const testEmail = {
from: 'test@yourdomain.com',
to: 'test@yourdomain.com',
subject: 'Test Email',
html: '<p>This is a test email.</p>',
};
try {
switch (provider) {
case 'resend': {
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(testEmail),
});
if (!response.ok) {
return { success: false, error: await response.text() };
}
return { success: true };
}
case 'sendgrid': {
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{ to: [{ email: testEmail.to }] }],
from: { email: testEmail.from },
subject: testEmail.subject,
content: [{ type: 'text/html', value: testEmail.html }],
}),
});
if (!response.ok) {
return { success: false, error: await response.text() };
}
return { success: true };
}
case 'mailgun': {
const formData = new FormData();
formData.append('from', testEmail.from);
formData.append('to', testEmail.to);
formData.append('subject', testEmail.subject);
formData.append('html', testEmail.html);
const response = await fetch(
`https://api.mailgun.net/v3/${env.MAILGUN_DOMAIN}/messages`,
{
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`api:${env.MAILGUN_API_KEY}`)}`,
},
body: formData,
}
);
if (!response.ok) {
return { success: false, error: await response.text() };
}
return { success: true };
}
case 'smtp2go': {
const response = await fetch('https://api.smtp2go.com/v3/email/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
api_key: env.SMTP2GO_API_KEY,
to: [`<${testEmail.to}>`],
sender: testEmail.from,
subject: testEmail.subject,
html_body: testEmail.html,
}),
});
if (!response.ok) {
return { success: false, error: await response.text() };
}
const data = await response.json();
if (data.data.failed > 0) {
return { success: false, error: data.data.failures?.join(', ') };
}
return { success: true };
}
}
} catch (error) {
return { success: false, error: (error as Error).message };
}
}Quick Reference
快速参考
API Endpoints
API端点
| Provider | Endpoint |
|---|---|
| Resend | |
| SendGrid | |
| Mailgun US | |
| Mailgun EU | |
| SMTP2Go | |
| 提供商 | 端点地址 |
|---|---|
| Resend | |
| SendGrid | |
| Mailgun美国 | |
| Mailgun欧盟 | |
| SMTP2Go | |
Authentication Headers
认证头
| Provider | Header | Format |
|---|---|---|
| Resend | | |
| SendGrid | | |
| Mailgun | | |
| SMTP2Go | Body field | |
| 提供商 | 请求头 | 格式 |
|---|---|---|
| Resend | | |
| SendGrid | | |
| Mailgun | | |
| SMTP2Go | 请求体字段 | |
Webhook Events
Webhook事件
| Event Type | Resend | SendGrid | Mailgun | SMTP2Go |
|---|---|---|---|---|
| Delivered | | | | |
| Bounced | | | | |
| Spam | | | | |
| Opened | | | | |
| Clicked | | | | |
| 事件类型 | Resend | SendGrid | Mailgun | SMTP2Go |
|---|---|---|---|---|
| 已送达 | | | | |
| 退信 | | | | |
| 垃圾邮件投诉 | | | | |
| 已打开 | | | | |
| 已点击 | | | | |
Support Links
支持链接
- Resend: https://resend.com/docs
- SendGrid: https://www.twilio.com/docs/sendgrid
- Mailgun: https://documentation.mailgun.com
- SMTP2Go: https://developers.smtp2go.com
Production Notes:
- Always verify sender domains before production
- Set up DKIM/SPF/DMARC records for deliverability
- Use dedicated IPs for high-volume sending (>100k/month)
- Implement webhook handlers for bounce/complaint management
- Monitor sender reputation via provider dashboards
- Keep unsubscribe mechanisms compliant (CAN-SPAM, GDPR)
- Resend: https://resend.com/docs
- SendGrid: https://www.twilio.com/docs/sendgrid
- Mailgun: https://documentation.mailgun.com
- SMTP2Go: https://developers.smtp2go.com
生产环境注意事项:
- 生产前务必验证发件人域名
- 配置DKIM/SPF/DMARC记录以提升送达率
- 高发送量场景(>10万/月)使用独立IP
- 实现Webhook处理器处理退信和投诉
- 通过提供商控制台监控发件人信誉
- 确保退订机制符合法规要求(CAN-SPAM、GDPR)