Cloudflare Workers
postal-mime is ideal for parsing emails in Cloudflare Email Workers. This guide covers integration patterns and best practices.
Email Workers Setup
Cloudflare Email Workers allow you to programmatically process incoming emails for your domain.
Basic Email Worker
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
// Parse the incoming email
const email = await PostalMime.parse(message.raw);
console.log('From:', email.from?.address);
console.log('Subject:', email.subject);
console.log('Attachments:', email.attachments.length);
}
};
TypeScript Version
import PostalMime from 'postal-mime';
import type { Email } from 'postal-mime';
interface Env {
// Your environment bindings
}
export default {
async email(
message: ForwardableEmailMessage,
env: Env,
ctx: ExecutionContext
): Promise<void> {
const email: Email = await PostalMime.parse(message.raw);
console.log('Subject:', email.subject);
}
};
Common Patterns
Forward Based on Content
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
const email = await PostalMime.parse(message.raw);
// Forward to different addresses based on subject
if (email.subject?.toLowerCase().includes('urgent')) {
await message.forward('urgent@example.com');
} else if (email.subject?.toLowerCase().includes('support')) {
await message.forward('support@example.com');
} else {
await message.forward('general@example.com');
}
}
};
Store Attachments in R2
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
const email = await PostalMime.parse(message.raw);
// Store each attachment in R2
for (const attachment of email.attachments) {
if (attachment.filename) {
const key = `attachments/${Date.now()}-${attachment.filename}`;
await env.MY_BUCKET.put(key, attachment.content, {
httpMetadata: {
contentType: attachment.mimeType
}
});
console.log(`Stored: ${key}`);
}
}
}
};
Log to D1 Database
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
const email = await PostalMime.parse(message.raw);
// Insert email metadata into D1
await env.DB.prepare(`
INSERT INTO emails (from_address, to_address, subject, date, has_attachments)
VALUES (?, ?, ?, ?, ?)
`).bind(
email.from?.address || '',
message.to,
email.subject || '',
email.date || new Date().toISOString(),
email.attachments.length > 0 ? 1 : 0
).run();
}
};
Send Notification via Webhook
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
const email = await PostalMime.parse(message.raw);
// Send notification to webhook
await fetch(env.WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: email.from?.address,
subject: email.subject,
preview: email.text?.substring(0, 200),
attachmentCount: email.attachments.length,
timestamp: new Date().toISOString()
})
});
}
};
Handling Large Emails
Attachment Size Limits
import PostalMime from 'postal-mime';
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024; // 10MB
export default {
async email(message, env, ctx) {
const email = await PostalMime.parse(message.raw);
const oversizedAttachments = email.attachments.filter(
att => att.content.byteLength > MAX_ATTACHMENT_SIZE
);
if (oversizedAttachments.length > 0) {
console.warn('Email contains oversized attachments');
// Handle accordingly
}
}
};
Security Limits
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
try {
const email = await PostalMime.parse(message.raw, {
maxNestingDepth: 50, // Stricter limit
maxHeadersSize: 524288 // 512KB
});
// Process email...
} catch (error) {
if (error.message.includes('nesting depth') ||
error.message.includes('header size')) {
console.error('Potentially malicious email rejected');
return; // Drop the email
}
throw error;
}
}
};
Email Filtering
Spam Detection
import PostalMime from 'postal-mime';
function isLikelySpam(email) {
const spamIndicators = [
// No sender
!email.from?.address,
// Suspicious subject patterns
/urgent|winner|lottery|claim/i.test(email.subject || ''),
// Mismatched from/reply-to
email.replyTo?.[0]?.address !== email.from?.address,
// Only HTML content (no text alternative)
email.html && !email.text
];
return spamIndicators.filter(Boolean).length >= 2;
}
export default {
async email(message, env, ctx) {
const email = await PostalMime.parse(message.raw);
if (isLikelySpam(email)) {
console.log('Potential spam detected, dropping email');
return;
}
await message.forward('inbox@example.com');
}
};
Content-Based Routing
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
const email = await PostalMime.parse(message.raw);
// Check for specific attachment types
const hasInvoice = email.attachments.some(
att => att.mimeType === 'application/pdf' &&
att.filename?.toLowerCase().includes('invoice')
);
if (hasInvoice) {
await message.forward('accounting@example.com');
return;
}
// Check for calendar invites
const hasCalendarInvite = email.attachments.some(
att => att.mimeType === 'text/calendar'
);
if (hasCalendarInvite) {
await message.forward('calendar@example.com');
return;
}
await message.forward('general@example.com');
}
};
Wrangler Configuration
name = "email-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[triggers]
email = ["*@yourdomain.com"]
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "email-attachments"
[[d1_databases]]
binding = "DB"
database_name = "emails"
database_id = "your-database-id"
[vars]
WEBHOOK_URL = "https://hooks.example.com/email"
Error Handling
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
try {
const email = await PostalMime.parse(message.raw);
// Process email...
} catch (error) {
console.error('Failed to parse email:', error);
// Store raw email for later investigation
const key = `failed/${Date.now()}-${message.from}`;
await env.MY_BUCKET.put(key, message.raw);
// Optionally forward to admin
await message.forward('admin@example.com');
}
}
};
Best Practices
- Always handle parsing errors - Malformed emails are common
- Set security limits - Use stricter limits for untrusted input
- Use waitUntil for async operations - Don't block the response
export default {
async email(message, env, ctx) {
const email = await PostalMime.parse(message.raw);
// Use waitUntil for non-critical async operations
ctx.waitUntil(
saveToDatabase(email, env.DB)
);
// Forward immediately
await message.forward('inbox@example.com');
}
};
- Consider message size - Cloudflare has a 25MB limit for email messages
- Log strategically - Use console.log for debugging, it appears in Workers logs