email-gateway について
email-gatewayは、Resend、SendGrid、Mailgun、SMTP2Goを使ったメール送信のスキルです。各サービスのAPIに加え、テンプレート、バッチ送信、添付ファイル、Webhook、リトライ、移行の方法を扱います。
主な機能
- 014つのメールサービスで送信
- 02トランザクションメールと一括送信のパターン
- 03ResendでのReact Emailテンプレート
- 04添付ファイルとイベントWebhook
- 05プロバイダー抽象化、リトライ、レート制限への対応
活用シーン
- 01パスワードリセットなどのメール送信
- 02サービスAPIを使ったメールの一括配信
- 03Webhookによる配信イベントの追跡
- 04共通の抽象化を通じたサービス切り替え
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill email-gatewayインストール後、Claude Code で/email-gatewayを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonreferences/common-errors.mdprovider-comparison.mdrate-limits.mdwebhook-events.mdscripts/test-provider.shtemplates/mailgun-send.tsmailgun-webhooks.tsresend-basic.tsresend-react-email.tsxsendgrid-dynamic-template.tssendgrid-transactional.tssmtp2go-bulk.tssmtp2go-send.ts詳細
メールゲートウェイ(マルチプロバイダー)
ステータス:本番環境対応済み ✅ 最終更新日:2026-01-10 プロバイダー:Resend、SendGrid、Mailgun、SMTP2Go
クイックスタート
ニーズに応じてプロバイダーを選択してください。
| プロバイダー | 最適な用途 | 主な機能 | 無料枠 |
|---|---|---|---|
| Resend | モダンなアプリ、React Email | JSX テンプレート | 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: "..." } }
プロバイダーの比較
機能
| 機能 | Resend | SendGrid | Mailgun | SMTP2Go |
|---|---|---|---|---|
| React Email | ✅ ネイティブ対応 | ❌ | ❌ | ❌ |
| ダイナミックテンプレート | ✅ | ✅ | ✅ | ✅ |
| 一括送信 | 1リクエストあたり50件 | 1リクエストあたり1,000件 | 1リクエストあたり1,000件 | 1リクエストあたり100件 |
| Webhook | ✅ | ✅ | ✅ | ✅ |
| SMTP | ✅ | ✅ | ✅ | ✅ メイン |
| IP ウォームアップ | 管理型 | 手動 | 手動 | 管理型 |
| 専用 IP | エンタープライズ | $90+/月 | $80+/月 | カスタム |
| 分析 | 基本 | 高度 | 高度 | 良好 |
| A/B テスト | ❌ | ✅ | ✅ | ❌ |
レート制限(無料枠)
| プロバイダー | 1日あたり | 月あたり | 超過料金 |
|---|---|---|---|
| Resend | 100 | 3,000 | 1,000通あたり$1 |
| SendGrid | 100 | 無期限 | 10,000通で$15 |
| Mailgun | 100 | 無期限 | 10,000通で$15 |
| SMTP2Go | 約33 | トライアルで1,000 | 10,000通で$10 |
API 制限
| プロバイダー | リクエスト/秒 | バースト | Retry After ヘッダー |
|---|---|---|---|
| Resend | 10 | あり | ✅ |
| SendGrid | 600 | あり | ✅ |
| Mailgun | 変動 | あり | ✅ |
| SMTP2Go | 10 | 制限あり | ✅ |
メッセージ制限
| プロバイダー | 最大サイズ | 添付ファイル | 最大宛先数 |
|---|---|---|---|
| Resend | 40 MB | 合計40 MB | 1リクエストあたり50件 |
| SendGrid | 20 MB | 合計20 MB | 1リクエストあたり1,000件 |
| Mailgun | 25 MB | 合計25 MB | 1リクエストあたり1,000件 |
| SMTP2Go | 50 MB | 合計50 MB | 1リクエストあたり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 エンドポイント
| プロバイダー | エンドポイント |
|---|---|
| Resend | https://api.resend.com/emails |
| SendGrid | https://api.sendgrid.com/v3/mail/send |
| Mailgun US | https://api.mailgun.net/v3/{domain}/messages |
| Mailgun EU | https://api.eu.mailgun.net/v3/{domain}/messages |
| SMTP2Go | https://api.smtp2go.com/v3/email/send |
認証ヘッダー
| プロバイダー | ヘッダー | 形式 |
|---|---|---|
| Resend | Authorization | Bearer {API_KEY} |
| SendGrid | Authorization | Bearer {API_KEY} |
| Mailgun | Authorization | Basic {base64(api:API_KEY)} |
| SMTP2Go | 本文フィールド | api_key: {API_KEY} |
Webhook イベント
| イベントタイプ | Resend | SendGrid | Mailgun | SMTP2Go |
|---|---|---|---|---|
| 配信済み | email.delivered | delivered | delivered | delivered |
| バウンス | email.bounced | bounce | failed | bounce |
| スパム | email.complained | spamreport | complained | spam |
| 開封 | email.opened | open | opened | open |
| クリック | email.clicked | click | clicked | click |
サポートリンク
- 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 レコードを設定してください
- 大量のメールを送信する場合(>100k/月)は、専用 IP を使用してください
- バウンスや苦情を管理するための Webhook ハンドラーを実装してください
- プロバイダーのダッシュボードで送信者の評価を監視してください
- 配信停止の仕組みを法令(CAN-SPAM、GDPR)に準拠させてください