mcpField
E
email-gateway

email-gateway

github.com/jezweb開発1.1k342 回インストール3 週間前に更新
リポジトリ

email-gateway について

email-gatewayは、Resend、SendGrid、Mailgun、SMTP2Goを使ったメール送信のスキルです。各サービスのAPIに加え、テンプレート、バッチ送信、添付ファイル、Webhook、リトライ、移行の方法を扱います。

主な機能

  1. 014つのメールサービスで送信
  2. 02トランザクションメールと一括送信のパターン
  3. 03ResendでのReact Emailテンプレート
  4. 04添付ファイルとイベントWebhook
  5. 05プロバイダー抽象化、リトライ、レート制限への対応

活用シーン

  1. 01パスワードリセットなどのメール送信
  2. 02サービスAPIを使ったメールの一括配信
  3. 03Webhookによる配信イベントの追跡
  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 テンプレート1日100通、月3,000通
SendGridエンタープライズ規模ダイナミックテンプレート1日100通(無期限)
Mailgun開発者向け Webhookイベントトラッキング1日100通
SMTP2Go信頼性の高いリレー、オーストラリアシンプルな APIトライアルで月1,000通
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✅ ネイティブ対応❌❌❌
ダイナミックテンプレート✅✅✅✅
一括送信1リクエストあたり50件1リクエストあたり1,000件1リクエストあたり1,000件1リクエストあたり100件
Webhook✅✅✅✅
SMTP✅✅✅✅ メイン
IP ウォームアップ管理型手動手動管理型
専用 IPエンタープライズ$90+/月$80+/月カスタム
分析基本高度高度良好
A/B テスト❌✅✅❌
レート制限(無料枠)
プロバイダー1日あたり月あたり超過料金
Resend1003,0001,000通あたり$1
SendGrid100無期限10,000通で$15
Mailgun100無期限10,000通で$15
SMTP2Go約33トライアルで1,00010,000通で$10
API 制限
プロバイダーリクエスト/秒バーストRetry After ヘッダー
Resend10あり✅
SendGrid600あり✅
Mailgun変動あり✅
SMTP2Go10制限あり✅
メッセージ制限
プロバイダー最大サイズ添付ファイル最大宛先数
Resend40 MB合計40 MB1リクエストあたり50件
SendGrid20 MB合計20 MB1リクエストあたり1,000件
Mailgun25 MB合計25 MB1リクエストあたり1,000件
SMTP2Go50 MB合計50 MB1リクエストあたり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 シークレット(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(パーソナライズは最大1,000件):

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 を確認してください

よくあるエラー:

  • ドメインが認証されていません
  • リージョンが間違っています(米国と 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)に準拠させてください

コメント