Multi-provider email sending for Cloudflare Workers and Node.js applications. Build transactional email systems with Resend (React Email support), SendGrid (enterprise scale), Mailgun (developer webhooks), or SMTP2Go (reliable relay). Includes template patterns, webhook verification, attachment handling, and error recovery. Use when sending emails via API, handling bounces/complaints, or migrating between providers.
/plugin marketplace add jezweb/claude-skills/plugin install jezweb-tooling-skills@jezweb/claude-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
README.mdreferences/common-errors.mdreferences/provider-comparison.mdreferences/rate-limits.mdreferences/webhook-events.mdscripts/test-provider.shtemplates/mailgun-send.tstemplates/mailgun-webhooks.tstemplates/resend-basic.tstemplates/resend-react-email.tsxtemplates/sendgrid-dynamic-template.tstemplates/sendgrid-transactional.tstemplates/smtp2go-bulk.tstemplates/smtp2go-send.tsStatus: Production Ready ✅ Last Updated: 2026-01-10 Providers: Resend, SendGrid, Mailgun, SMTP2Go
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 |
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" }
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)
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." }
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: "..." } }
| 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 | ❌ | ✅ | ✅ | ❌ |
| 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 |
| Provider | Requests/sec | Burst | Retry After Header |
|---|---|---|---|
| Resend | 10 | Yes | ✅ |
| SendGrid | 600 | Yes | ✅ |
| Mailgun | Varies | Yes | ✅ |
| SMTP2Go | 10 | Limited | ✅ |
| 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
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
# 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
// 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;
};
}
Password Reset:
// 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 };
}
Resend (max 50 recipients):
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):
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 };
}
Install React Email:
npm install react-email @react-email/components
Create Template:
// 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):
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):
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,
}),
});
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: 'user@example.com' }],
dynamic_template_data: {
name: 'Alice',
confirmationCode: 'ABC123',
},
}],
from: { email: 'noreply@yourdomain.com' },
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', '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,
}
);
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: 'noreply@yourdomain.com',
to: 'user@example.com',
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: '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):
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 Webhooks:
Events: 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 Webhooks:
// 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:
// 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:
// 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');
}
| 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:
to field requiredfrom domain not verifiedError response format:
{
"statusCode": 422,
"message": "Validation error",
"name": "validation_error"
}
| 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:
personalizationsError response format:
{
"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"
}
]
}
| 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:
Error response format:
{
"message": "Domain not found: invalid.domain.com"
}
| 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:
<email@domain.com>)Error response format:
{
"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: 'noreply@yourdomain.com',
to: 'user@example.com',
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: '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 };
}
}
| Provider | Endpoint |
|---|---|
| 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 |
| Provider | Header | Format |
|---|---|---|
| Resend | Authorization | Bearer {API_KEY} |
| SendGrid | Authorization | Bearer {API_KEY} |
| Mailgun | Authorization | Basic {base64(api:API_KEY)} |
| SMTP2Go | Body field | api_key: {API_KEY} |
| Event Type | Resend | SendGrid | Mailgun | SMTP2Go |
|---|---|---|---|---|
| Delivered | email.delivered | delivered | delivered | delivered |
| Bounced | email.bounced | bounce | failed | bounce |
| Spam | email.complained | spamreport | complained | spam |
| Opened | email.opened | open | opened | open |
| Clicked | email.clicked | click | clicked | click |
Production Notes:
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.