Skip to main content

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:

  1. Go to SES Console
  2. Navigate to "Verified identities"
  3. Click "Create identity"
  4. Enter email address
  5. Verify via email link

2. Domain Verification (Production)

For production, verify entire domain:

  1. Add DNS records provided by SES
  2. Enable DKIM signing
  3. Set up SPF records
  4. 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:

  1. Go to SES Console
  2. Click "Request production access"
  3. Fill out use case form
  4. 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

  1. Always Include Text Version: Provide plain text fallback for HTML emails
  2. Reply-To Address: Set reply-to for transactional emails
  3. Verify Domain: Use verified domain for production
  4. Handle Bounces: Monitor and handle bounce notifications
  5. Rate Limiting: Respect SES sending limits
  6. Email Validation: Validate email addresses before sending
  7. Unsubscribe Links: Include unsubscribe for marketing emails
  8. 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:

  1. Use free tier efficiently
  2. Batch emails when possible
  3. Implement email preferences (reduce sends)
  4. Clean up bounced emails
  5. 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

Additional Resources