AWS SES Integration
AWS Simple Email Service (SES) is Amazon's scalable email platform. BookWish uses SES for transactional emails and email notifications.
Overview
BookWish uses AWS SES for:
- Transactional Emails: Order confirmations, shipping notifications
- Account Emails: Welcome emails, password resets
- Notification Emails: Wishlist matches, stock alerts
- Bulk Emails: Newsletters, announcements (future)
- High Deliverability: Excellent email delivery rates
- Cost-Effective: $0.10 per 1,000 emails
Implementation
Location: /backend/src/integrations/ses.ts
Configuration
Required environment variables:
AWS_REGION=us-east-1
AWS_SES_ACCESS_KEY_ID=AKIA...
AWS_SES_SECRET_ACCESS_KEY=...
AWS_SES_EMAIL_SOURCE=noreply@bookwish.io
The integration uses the AWS SDK v3 for Node.js.
Features
1. Send Email
Send a single email to one or more recipients.
Basic Email
import { sendEmail } from '../integrations/ses';
await sendEmail({
to: 'customer@example.com',
subject: 'Order Confirmation',
htmlBody: '<h1>Thank you for your order!</h1><p>Order #1234 has been received.</p>',
textBody: 'Thank you for your order! Order #1234 has been received.'
});
Email with Reply-To
await sendEmail({
to: 'customer@example.com',
subject: 'Wishlist Match Found',
htmlBody: '<h1>Great news!</h1><p>A book on your wishlist is now available.</p>',
textBody: 'Great news! A book on your wishlist is now available.',
replyTo: 'support@bookwish.io'
});
Multiple Recipients
await sendEmail({
to: ['user1@example.com', 'user2@example.com', 'user3@example.com'],
subject: 'New Feature Announcement',
htmlBody: '<h1>New Feature!</h1><p>Check out our new reading challenges.</p>',
textBody: 'New Feature! Check out our new reading challenges.'
});
2. Batch Emails
Send multiple emails with different content to different recipients.
Send Batch
import { sendBatchEmails } from '../integrations/ses';
await sendBatchEmails([
{
to: 'user1@example.com',
subject: 'Your Order Shipped',
htmlBody: '<p>Order #123 has shipped.</p>',
textBody: 'Order #123 has shipped.'
},
{
to: 'user2@example.com',
subject: 'Your Order Shipped',
htmlBody: '<p>Order #456 has shipped.</p>',
textBody: 'Order #456 has shipped.'
},
// ... more emails
]);
Batch sends use Promise.allSettled() so individual failures don't stop the batch.
Email Payload Structure
SESEmailPayload
interface SESEmailPayload {
to: string | string[]; // Recipient(s)
subject: string; // Email subject
htmlBody: string; // HTML email body
textBody?: string; // Plain text fallback
replyTo?: string; // Reply-to address
}
Email Templates
BookWish uses email templates for consistency:
Order Confirmation
const emailHtml = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.header { background: #2563eb; color: white; padding: 20px; }
.content { padding: 20px; }
</style>
</head>
<body>
<div class="header">
<h1>Order Confirmation</h1>
</div>
<div class="content">
<p>Hi ${userName},</p>
<p>Thank you for your order!</p>
<h3>Order Details</h3>
<ul>
${orderItems.map(item => `<li>${item.title} - $${item.price}</li>`).join('')}
</ul>
<p><strong>Total: $${total}</strong></p>
</div>
</body>
</html>
`;
await sendEmail({
to: userEmail,
subject: `Order Confirmation #${orderId}`,
htmlBody: emailHtml,
textBody: `Order #${orderId} confirmed. Total: $${total}`
});
Wishlist Match
const emailHtml = `
<!DOCTYPE html>
<html>
<body>
<h1>Wishlist Match Found!</h1>
<p>Good news! A book on your wishlist is now available:</p>
<div style="border: 1px solid #ddd; padding: 15px; margin: 20px 0;">
<img src="${book.coverUrl}" alt="${book.title}" style="max-width: 200px;">
<h2>${book.title}</h2>
<p>by ${book.authors.join(', ')}</p>
<p><strong>$${book.price}</strong> at ${store.name}</p>
<a href="${bookUrl}" style="background: #2563eb; color: white; padding: 10px 20px; text-decoration: none; display: inline-block;">View Book</a>
</div>
</body>
</html>
`;
await sendEmail({
to: userEmail,
subject: `Wishlist Match: ${book.title}`,
htmlBody: emailHtml
});
AWS SES Setup
1. Verify Email Address
Before sending, verify sender email in SES Console:
# Using AWS CLI
aws ses verify-email-identity --email-address noreply@bookwish.io --region us-east-1
Or verify in AWS Console:
- Go to SES Console
- Navigate to "Verified identities"
- Click "Create identity"
- Enter email address
- Verify via email link
2. Domain Verification (Production)
For production, verify entire domain:
- Add DNS records provided by SES
- Enable DKIM signing
- Set up SPF records
- Configure DMARC
Example DNS records:
# DKIM
bookwish._domainkey.bookwish.io CNAME xxxxx.dkim.amazonses.com
# SPF
bookwish.io TXT "v=spf1 include:amazonses.com ~all"
# DMARC
_dmarc.bookwish.io TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@bookwish.io"
3. Move Out of Sandbox
AWS SES starts in sandbox mode:
- Can only send to verified emails
- Daily sending limit: 200 emails
- Sending rate: 1 email/second
Request production access:
- Go to SES Console
- Click "Request production access"
- Fill out use case form
- Wait for approval (typically 24 hours)
Error Handling
Email Send Failures
try {
await sendEmail(payload);
} catch (error) {
logger.error('ses.send_failed', { error: String(error) });
// Handle error (retry, notify admin, etc.)
}
Common Errors
- MessageRejected: Email content flagged as spam
- MailFromDomainNotVerified: Sender domain not verified
- ConfigurationSetDoesNotExist: Invalid configuration set
- AccountSendingPaused: Account suspended
- InvalidParameterValue: Invalid email format
Bounces and Complaints
Set up SNS topics for bounce/complaint handling:
// In SES Console, configure SNS topics for:
// - Bounces (email doesn't exist)
// - Complaints (user marked as spam)
// - Deliveries (optional)
Handle bounce notifications:
app.post('/webhooks/ses/bounce', async (req, res) => {
const notification = JSON.parse(req.body);
if (notification.notificationType === 'Bounce') {
const bouncedEmails = notification.bounce.bouncedRecipients;
for (const recipient of bouncedRecipients) {
// Mark email as bounced in database
await markEmailBounced(recipient.emailAddress);
}
}
res.status(200).send('OK');
});
Email Service Layer
Higher-level email service (/backend/src/services/email.service.ts) wraps SES:
Send Order Confirmation
export async function sendOrderConfirmation(order: Order) {
const user = await getUser(order.userId);
const htmlBody = generateOrderConfirmationHtml(order, user);
const textBody = generateOrderConfirmationText(order, user);
await sendEmail({
to: user.email,
subject: `Order Confirmation #${order.id}`,
htmlBody,
textBody,
replyTo: 'orders@bookwish.io'
});
}
Send Wishlist Match Notification
export async function sendWishlistMatch(userId: string, book: Book, store: Store) {
const user = await getUser(userId);
const htmlBody = generateWishlistMatchHtml(book, store);
await sendEmail({
to: user.email,
subject: `Wishlist Match: ${book.title}`,
htmlBody
});
}
Best Practices
- Always Include Text Version: Provide plain text fallback for HTML emails
- Reply-To Address: Set reply-to for transactional emails
- Verify Domain: Use verified domain for production
- Handle Bounces: Monitor and handle bounce notifications
- Rate Limiting: Respect SES sending limits
- Email Validation: Validate email addresses before sending
- Unsubscribe Links: Include unsubscribe for marketing emails
- Test Thoroughly: Test emails in multiple clients
HTML Email Guidelines
Responsive Design
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
@media only screen and (max-width: 600px) {
.content { padding: 10px !important; }
}
</style>
</head>
<body>
<!-- Email content -->
</body>
</html>
Inline CSS
Email clients often strip <style> tags. Use inline styles:
<p style="color: #333; font-size: 16px; line-height: 1.5;">
Your email content here.
</p>
Avoid JavaScript
Email clients block JavaScript. Don't use it.
Test in Multiple Clients
Test emails in:
- Gmail
- Outlook
- Apple Mail
- Mobile clients
Monitoring
Track Email Metrics
// Log email sends
logger.info('email.sent', {
to: payload.to,
subject: payload.subject,
type: 'order_confirmation'
});
// Track deliverability
const delivered = await getDeliveredCount();
const bounced = await getBouncedCount();
const deliveryRate = delivered / (delivered + bounced);
Important Metrics
- Delivery rate
- Bounce rate
- Complaint rate
- Open rate (requires tracking pixels)
- Click rate (requires tracked links)
Cost Optimization
SES pricing (us-east-1):
- First 62,000 emails/month: FREE (via EC2)
- Beyond free tier: $0.10 per 1,000 emails
- Attachments: $0.12 per GB
Tips to optimize:
- Use free tier efficiently
- Batch emails when possible
- Implement email preferences (reduce sends)
- Clean up bounced emails
- Consider SNS for high-volume notifications
Rate Limits
Default SES limits (sandbox):
- Sending quota: 200 emails/day
- Sending rate: 1 email/second
Production limits (after approval):
- Sending quota: 50,000+ emails/day (increases automatically)
- Sending rate: 14+ emails/second (increases automatically)
Testing
Local Testing
Use SES in sandbox mode:
# Send test email to verified address
AWS_REGION=us-east-1 node -e "
const { sendEmail } = require('./dist/integrations/ses');
await sendEmail({
to: 'verified@example.com',
subject: 'Test',
htmlBody: '<p>Test email</p>'
});
"
Email Simulator
Use AWS's email simulator addresses:
// These addresses simulate different scenarios in sandbox mode
await sendEmail({
to: 'success@simulator.amazonses.com', // Success
subject: 'Test',
htmlBody: '<p>Test</p>'
});
// Other simulator addresses:
// - bounce@simulator.amazonses.com (hard bounce)
// - complaint@simulator.amazonses.com (spam complaint)
// - suppressionlist@simulator.amazonses.com (on suppression list)
Initialization
SES client is initialized on first use:
// In integrations/ses.ts
let sesClient: SESClient | null = null;
export function initializeSES(): void {
if (sesClient) return;
sesClient = new SESClient({
region: env.AWS_REGION,
credentials: {
accessKeyId: env.AWS_SES_ACCESS_KEY_ID,
secretAccessKey: env.AWS_SES_SECRET_ACCESS_KEY
}
});
}
// Call during server startup
initializeSES();
Limitations
- Sandbox Restrictions: Limited recipients until production approved
- Rate Limits: Sending rate limits apply
- Attachment Size: 10 MB maximum per email
- Email Size: 10 MB maximum total size
- Recipient Limit: 50 recipients per email
- Sending Pause: AWS may pause sending if quality issues detected