mcpField
E
email-gateway

email-gateway

github.com/jezweb开发1.1k342 次安装3 周前更新
代码仓库

关于 email-gateway

email-gateway 是一项通过 Resend、SendGrid、Mailgun 或 SMTP2Go 发送邮件的技能,介绍各家 API 以及模板、批量发送、附件、Webhook、重试和迁移模式。

主要功能

  1. 01使用四种邮件服务发送
  2. 02事务邮件与批量发送模式
  3. 03通过 Resend 使用 React Email 模板
  4. 04附件和事件 Webhook
  5. 05服务商抽象、重试与速率限制处理

适用场景

  1. 01发送密码重置等事务邮件
  2. 02通过服务商 API 批量发送邮件
  3. 03使用 Webhook 跟踪投递事件
  4. 04通过共享抽象切换服务商

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill email-gateway

安装后在 Claude Code 中输入/email-gateway确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
references/
common-errors.md
provider-comparison.md
rate-limits.md
webhook-events.md
scripts/
test-provider.sh
templates/
mailgun-send.ts
mailgun-webhooks.ts
resend-basic.ts
resend-react-email.tsx
sendgrid-dynamic-template.ts
sendgrid-transactional.ts
smtp2go-bulk.ts
smtp2go-send.ts

详细介绍

邮件网关(多服务提供商)

状态:已可投入生产 ✅ 最后更新:2026-01-10 服务提供商:Resend、SendGrid、Mailgun、SMTP2Go


快速开始

根据需求选择服务提供商:

服务提供商最适合主要功能免费额度
Resend现代应用、React EmailJSX 模板每天 100 封,每月 3k 封
SendGrid企业级规模动态模板永久每天 100 封
Mailgun面向开发者的 Webhook事件跟踪每天 100 封
SMTP2Go可靠的中继服务、AU简洁 API试用期每月 1k 封
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({
    from: '[email protected]',
    to: '[email protected]',
    subject: 'Welcome!',
    html: '<h1>Hello World</h1>',
  }),
});

const data = await response.json();
// { id: "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794" }
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: '[email protected]' }],
    }],
    from: { email: '[email protected]' },
    subject: 'Welcome!',
    content: [{
      type: 'text/html',
      value: '<h1>Hello World</h1>',
    }],
  }),
});

// Returns 202 on success (no body)
Mailgun
const formData = new FormData();
formData.append('from', '[email protected]');
formData.append('to', '[email protected]');
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: "<[email protected]>", message: "Queued. Thank you." }
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: ['<[email protected]>'],
    sender: '[email protected]',
    subject: 'Welcome!',
    html_body: '<h1>Hello World</h1>',
  }),
});

const data = await response.json();
// { data: { succeeded: 1, failed: 0, email_id: "..." } }

服务提供商对比

功能
功能ResendSendGridMailgunSMTP2Go
React Email✅ 原生支持❌❌❌
动态模板✅✅✅✅
批量发送每次请求 50 封每次请求 1000 封每次请求 1000 封每次请求 100 封
Webhook✅✅✅✅
SMTP✅✅✅✅ 主要方式
IP 预热托管手动手动托管
专用 IP企业版$90+/月$80+/月定制
分析基础高级高级良好
A/B 测试❌✅✅❌
速率限制(免费额度)
服务提供商每日每月超额费用
Resend1003,000$1/1k
SendGrid100永久10k 封 $15
Mailgun100永久10k 封 $15
SMTP2Go~33试用期 1,00010k 封 $10
API 限制
服务提供商请求/秒突发请求Retry After 标头
Resend10是✅
SendGrid600是✅
Mailgun不定是✅
SMTP2Go10有限✅
邮件限制
服务提供商最大大小附件最多收件人数
Resend40 MB总计 40 MB每次请求 50 人
SendGrid20 MB总计 20 MB每次请求 1000 人
Mailgun25 MB总计 25 MB每次请求 1000 人
SMTP2Go50 MB总计 50 MB每次请求 100 人

配置

环境变量
# Resend
RESEND_API_KEY=re_xxxxxxxxx

# SendGrid
SENDGRID_API_KEY=SG.xxxxxxxxx

# Mailgun
MAILGUN_API_KEY=xxxxxxxx-xxxxxxxx-xxxxxxxx
MAILGUN_DOMAIN=mg.yourdomain.com
MAILGUN_REGION=us  # or eu

# SMTP2Go
SMTP2GO_API_KEY=api-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Wrangler Secrets(Cloudflare Workers)
# Set 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

# Deploy to activate
npx wrangler deploy
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;
  };
}

常见模式

1. 事务性邮件

密码重置:

// 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: '[email protected]',
      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. 批量发送

Resend(最多 50 位收件人):

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: '[email protected]',
        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 个个性化设置):

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: '[email protected]' },
      template_id: templateId,
    }),
  });

  if (!response.ok) {
    const error = await response.text();
    return { success: false, error };
  }

  return { success: true };
}
3. React Email 模板(仅限 Resend)

安装 React Email:

npm install react-email @react-email/components

创建模板:

// 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):

import { Resend } from 'resend';
import WelcomeEmail from './emails/welcome';

const resend = new Resend(process.env.RESEND_API_KEY);

await resend.emails.send({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Welcome!',
  react: WelcomeEmail({ name: 'Alice', confirmUrl: 'https://...' }),
});

通过 Workers 发送(先渲染为 HTML):

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: '[email protected]',
    to: '[email protected]',
    subject: 'Welcome!',
    html,
  }),
});
4. 动态模板

SendGrid:

// 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: '[email protected]' }],
      dynamic_template_data: {
        name: 'Alice',
        confirmationCode: 'ABC123',
      },
    }],
    from: { email: '[email protected]' },
    template_id: 'd-xxxxxxxxxxxxxxxxxxxxxxxx',
  }),
});

Mailgun:

// 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', '[email protected]');
formData.append('to', '[email protected]');
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. 附件

Resend:

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: '[email protected]',
    to: '[email protected]',
    subject: 'Your Invoice',
    html: '<p>Attached is your invoice.</p>',
    attachments: [{
      filename: 'invoice.pdf',
      content: base64Content,
    }],
  }),
});

SendGrid:

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: '[email protected]' }],
    }],
    from: { email: '[email protected]' },
    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(使用带有 File 的 FormData):

const formData = new FormData();
formData.append('from', '[email protected]');
formData.append('to', '[email protected]');
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,
  }
);
6. Webhook(事件跟踪)

Resend Webhook:

事件:email.sent、email.delivered、email.delivery_delayed、email.bounced、email.complained、email.opened、email.clicked

// 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 Webhook:

// 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 Webhook:

// 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 Webhook:

// 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 错误
状态错误原因解决方法
401未授权API 密钥无效检查 RESEND_API_KEY
422验证错误邮箱格式无效发送前验证邮箱
429超出速率限制请求过多实现指数退避
500内部错误Resend 服务异常采用退避策略重试

常见验证错误:

  • 必须提供 to 字段
  • 邮箱格式无效
  • from 域名尚未验证
  • 附件大小超过 40 MB

错误响应格式:

{
  "statusCode": 422,
  "message": "Validation error",
  "name": "validation_error"
}
SendGrid 错误
状态错误原因解决方法
400请求错误JSON 格式不正确检查请求结构
401未授权API 密钥无效检查 SENDGRID_API_KEY
413请求负载过大邮件 > 20 MB减小附件大小
429请求过多速率限制实现退避机制

常见错误:

  • 缺少 personalizations
  • 模板 ID 无效
  • 发件人地址未验证
  • 附件编码问题

错误响应格式:

{
  "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 错误
状态错误原因解决方法
400请求错误参数无效检查 FormData 字段
401未授权API 密钥无效检查 MAILGUN_API_KEY
402需要付款配额超出限制升级套餐
404未找到域名无效检查 MAILGUN_DOMAIN

常见错误:

  • 域名未验证
  • 区域错误(US 与 EU)
  • 模板变量无效
  • 收件人地址语法错误

错误响应格式:

{
  "message": "Domain not found: invalid.domain.com"
}
SMTP2Go 错误
状态错误原因解决方法
401未授权API 密钥无效检查 SMTP2GO_API_KEY
422验证错误邮件格式无效验证收件人
429速率限制请求过多实现退避机制

常见错误:

  • 发件人域名未验证
  • 收件人格式无效(必须使用尖括号:<[email protected]>)
  • API 密钥未激活

错误响应格式:

{
  "data": {
    "error": "Invalid sender email address",
    "error_code": "E_ApiResponseCodes_INVALID_SENDER_ADDRESS"
  }
}

速率限制与重试

指数退避模式
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),
  })
);
速率限制跟踪
// 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 };
}

在服务提供商之间迁移

服务提供商抽象
// 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: '[email protected]',
  to: '[email protected]',
  subject: 'Welcome',
  html: '<h1>Hello</h1>',
});

测试

测试服务提供商连接
export async function testEmailProvider(
  provider: 'resend' | 'sendgrid' | 'mailgun' | 'smtp2go',
  env: Env
): Promise<{ success: boolean; error?: string }> {
  const testEmail = {
    from: '[email protected]',
    to: '[email protected]',
    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 };
  }
}

快速参考

API 端点
服务提供商端点
Resendhttps://api.resend.com/emails
SendGridhttps://api.sendgrid.com/v3/mail/send
Mailgun UShttps://api.mailgun.net/v3/{domain}/messages
Mailgun EUhttps://api.eu.mailgun.net/v3/{domain}/messages
SMTP2Gohttps://api.smtp2go.com/v3/email/send
身份验证标头
服务提供商标头格式
ResendAuthorizationBearer {API_KEY}
SendGridAuthorizationBearer {API_KEY}
MailgunAuthorizationBasic {base64(api:API_KEY)}
SMTP2Go正文字段api_key: {API_KEY}
Webhook 事件
事件类型ResendSendGridMailgunSMTP2Go
已送达email.delivereddelivereddelivereddelivered
退信email.bouncedbouncefailedbounce
垃圾邮件email.complainedspamreportcomplainedspam
已打开email.openedopenopenedopen
已点击email.clickedclickclickedclick
支持链接

生产环境注意事项:

  • 正式上线前务必验证发件人域名
  • 设置 DKIM/SPF/DMARC 记录以提高送达率
  • 高发送量(>100k/月)时使用专用 IP
  • 实现 webhook 处理程序以管理退信/投诉
  • 通过服务提供商控制面板监控发件人信誉
  • 确保退订机制符合规范(CAN-SPAM、GDPR)

评论

支持 Markdown