AWS SES (Simple Email Service)
Amazon Simple Email Service (SES) is a cloud-based email sending service designed to help digital marketers and application developers send marketing, notification, and transactional emails. In VibeReference applications, SES provides a reliable and cost-effective way to incorporate email communications.
Introduction to AWS SES
AWS SES handles the underlying infrastructure for high email deliverability and provides a robust system for sending large volumes of email. It includes:
- High Deliverability: Amazon works with ISPs to maximize delivery rates
- Scalability: Send a few emails or millions without provisioning servers
- Analytics: Track open rates, click-through rates, and other key metrics
- Cost Effectiveness: Pay only for what you send
- Flexibility: Send emails via API or SMTP
- Authentication: Support for SPF, DKIM, and DMARC
- Content Filtering: Helps prevent emails from being marked as spam
Setting Up AWS SES
Account Setup
-
Create an AWS Account: If you don't already have one, create an account at aws.amazon.com.
-
Access SES Console: Navigate to the SES service from the AWS Management Console.
-
Region Selection: Choose the AWS region closest to your main user base.
Domain Verification
Before you can send emails from your domain, you must verify ownership:
// Example of using AWS SDK to verify a domain
import { SESClient, VerifyDomainIdentityCommand } from "@aws-sdk/client-ses";
const sesClient = new SESClient({ region: "us-east-1" });
async function verifyDomain(domain: string) {
try {
const command = new VerifyDomainIdentityCommand({ Domain: domain });
const response = await sesClient.send(command);
console.log("Verification token:", response.VerificationToken);
console.log("Add this token as a TXT record to your DNS configuration.");
return response.VerificationToken;
} catch (error) {
console.error("Error verifying domain:", error);
throw error;
}
}
// Usage
verifyDomain("yourdomain.com");
DNS Configuration
After verifying your domain, you'll need to set up the following DNS records:
- TXT Record: For domain verification
- DKIM Records: Three CNAME records for DKIM signing
- SPF Record: TXT record to specify authorized email servers
- DMARC Record: TXT record to specify policy for emails that fail authentication
Example DNS records:
# Domain verification
_amazonses.yourdomain.com. TXT "verification-token-from-aws"
# DKIM records
selector1._domainkey.yourdomain.com. CNAME selector1-xxxxx._domainkey.amazonses.com.
selector2._domainkey.yourdomain.com. CNAME selector2-xxxxx._domainkey.amazonses.com.
selector3._domainkey.yourdomain.com. CNAME selector3-xxxxx._domainkey.amazonses.com.
# SPF record
yourdomain.com. TXT "v=spf1 include:amazonses.com ~all"
# DMARC record
_dmarc.yourdomain.com. TXT "v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc-reports@yourdomain.com"
Moving Out of the Sandbox
By default, new SES accounts are placed in a "sandbox" with limitations:
- You can only send emails to verified email addresses
- Daily sending quotas are limited
To move out of the sandbox:
- Navigate to the SES console
- Click on "Request Production Access"
- Fill out the form with details about your use case
- Wait for AWS to approve your request (typically 1-2 business days)
Integrating SES with VibeReference
Installation
npm install @aws-sdk/client-ses
Configuration
Create a utility file to configure the SES client:
// lib/aws/ses.ts
import { SESClient } from "@aws-sdk/client-ses";
export const sesClient = new SESClient({
region: process.env.AWS_REGION || "us-east-1",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
Add these environment variables to your project:
# .env.local
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
Sending Email with the AWS SDK
// lib/email/sendEmail.ts
import { SendEmailCommand } from "@aws-sdk/client-ses";
import { sesClient } from "../aws/ses";
interface EmailParams {
to: string | string[];
from: string;
subject: string;
html: string;
text: string;
replyTo?: string;
}
export async function sendEmail({
to,
from,
subject,
html,
text,
replyTo,
}: EmailParams) {
const recipients = Array.isArray(to) ? to : [to];
const params = {
Destination: {
ToAddresses: recipients,
},
Message: {
Body: {
Html: {
Charset: "UTF-8",
Data: html,
},
Text: {
Charset: "UTF-8",
Data: text,
},
},
Subject: {
Charset: "UTF-8",
Data: subject,
},
},
Source: from,
ReplyToAddresses: replyTo ? [replyTo] : undefined,
};
try {
const command = new SendEmailCommand(params);
const result = await sesClient.send(command);
return { success: true, messageId: result.MessageId };
} catch (error) {
console.error("Error sending email:", error);
return { success: false, error };
}
}
Create Email Templates
// lib/email/templates.ts
interface WelcomeEmailParams {
userName: string;
verificationLink: string;
}
export function welcomeEmailTemplate({ userName, verificationLink }: WelcomeEmailParams) {
return {
subject: `Welcome to VibeReference, ${userName}!`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1>Welcome to VibeReference!</h1>
<p>Hi ${userName},</p>
<p>Thank you for signing up. We're excited to have you on board!</p>
<p>Please verify your email address by clicking the button below:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${verificationLink}" style="background-color: #4F46E5; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold;">
Verify Email
</a>
</div>
<p>If you didn't create this account, you can safely ignore this email.</p>
<p>Best regards,<br>The VibeReference Team</p>
</div>
`,
text: `
Welcome to VibeReference!
Hi ${userName},
Thank you for signing up. We're excited to have you on board!
Please verify your email address by clicking the link below:
${verificationLink}
If you didn't create this account, you can safely ignore this email.
Best regards,
The VibeReference Team
`,
};
}
Using Email Templates in API Routes
// pages/api/auth/signup.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { sendEmail } from '@/lib/email/sendEmail';
import { welcomeEmailTemplate } from '@/lib/email/templates';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { email, name } = req.body;
// Create user account logic here...
// Generate verification token and link
const verificationToken = generateToken(); // Implement this function
const verificationLink = `${process.env.NEXT_PUBLIC_APP_URL}/verify?token=${verificationToken}`;
// Send welcome email
const emailTemplate = welcomeEmailTemplate({
userName: name,
verificationLink,
});
const emailResult = await sendEmail({
to: email,
from: 'welcome@yourdomain.com',
subject: emailTemplate.subject,
html: emailTemplate.html,
text: emailTemplate.text,
});
if (!emailResult.success) {
console.error('Failed to send welcome email:', emailResult.error);
}
return res.status(201).json({ success: true });
} catch (error) {
console.error('Error in signup handler:', error);
return res.status(500).json({ error: 'Failed to create account' });
}
}
Email Types and Templates
Transactional Emails
Emails triggered by user actions or system events:
- Welcome Emails: Sent when users sign up
- Password Reset: Sent when users request password recovery
- Account Verification: To verify email addresses
- Order Confirmation: For e-commerce transactions
- Receipt/Invoice: For payments and subscriptions
- Status Updates: For order processing, ticket status, etc.
Promotional Emails
Marketing and engagement emails:
- Newsletter: Regular content updates
- Product Updates: New feature announcements
- Special Offers: Discounts and promotions
- Event Invitations: Webinars, product launches, etc.
- Re-engagement: For inactive users
Best Practices for Email Deliverability
- Authentication: Implement SPF, DKIM, and DMARC
- Warm Up Your Domain: Gradually increase sending volume
- Monitor Bounce Rates: Keep bounce rates below 5%
- Handle Bounces: Process and remove bounced addresses from your list
- Honor Unsubscribe Requests: Always include and respect unsubscribe links
- Segment Your Lists: Send targeted content to interested subscribers
- Maintain List Hygiene: Regularly remove inactive subscribers
- Avoid Spam Triggers: Don't use excessive capital letters, spam words, or misleading subjects
- Test Before Sending: Preview emails on different devices and clients
Handling Email Events
AWS SES can send notifications about email events via SNS (Simple Notification Service):
// Example SES Configuration Set with SNS Notification
import {
SESClient,
CreateConfigurationSetCommand,
CreateConfigurationSetEventDestinationCommand
} from "@aws-sdk/client-ses";
async function setupEventTracking() {
const sesClient = new SESClient({ region: "us-east-1" });
// Step 1: Create a Configuration Set
const configSetCommand = new CreateConfigurationSetCommand({
ConfigurationSet: {
Name: "VibeReferenceEmailTracking",
},
});
await sesClient.send(configSetCommand);
// Step 2: Create an Event Destination (SNS Topic)
const eventDestinationCommand = new CreateConfigurationSetEventDestinationCommand({
ConfigurationSetName: "VibeReferenceEmailTracking",
EventDestination: {
Name: "EmailEventsDestination",
Enabled: true,
MatchingEventTypes: [
"send",
"reject",
"bounce",
"complaint",
"delivery",
"open",
"click",
],
SNSDestination: {
TopicARN: "arn:aws:sns:us-east-1:123456789012:email-events-topic",
},
},
});
await sesClient.send(eventDestinationCommand);
}
To use a configuration set when sending an email:
const params = {
// ... other email parameters
ConfigurationSetName: "VibeReferenceEmailTracking",
};
Handling Bounces and Complaints
Create a Lambda function to process SNS notifications:
// AWS Lambda function to process email events
export const handler = async (event) => {
try {
for (const record of event.Records) {
const message = JSON.parse(record.Sns.Message);
// Handle different notification types
switch (message.notificationType) {
case 'Bounce':
await handleBounce(message.bounce);
break;
case 'Complaint':
await handleComplaint(message.complaint);
break;
case 'Delivery':
await logDelivery(message.delivery);
break;
// Handle other event types...
}
}
return { statusCode: 200, body: 'Processed successfully' };
} catch (error) {
console.error('Error processing SNS message:', error);
return { statusCode: 500, body: 'Error processing message' };
}
};
async function handleBounce(bounce) {
// Extract email addresses that bounced
const bouncedRecipients = bounce.bouncedRecipients.map(r => r.emailAddress);
// Add to suppression list or update database
for (const email of bouncedRecipients) {
await updateBouncedEmailStatus(email, bounce.bounceType, bounce.bounceSubType);
}
}
async function handleComplaint(complaint) {
// Extract email addresses that complained
const complainingRecipients = complaint.complainedRecipients.map(r => r.emailAddress);
// Update database or suppression list
for (const email of complainingRecipients) {
await updateComplaintStatus(email, complaint.complaintFeedbackType);
}
}
// Implement these functions based on your application needs
async function updateBouncedEmailStatus(email, bounceType, bounceSubType) {
// Update database logic
}
async function updateComplaintStatus(email, feedbackType) {
// Update database logic
}
async function logDelivery(delivery) {
// Log successful delivery
}
Email Templates with React and JSX
For better maintainability, use React components for email templates:
npm install react-email @react-email/components
// emails/WelcomeEmail.tsx
import React from 'react';
import {
Html,
Head,
Body,
Container,
Section,
Heading,
Text,
Button,
Hr
} from '@react-email/components';
interface WelcomeEmailProps {
userName: string;
verificationLink: string;
}
export const WelcomeEmail: React.FC<WelcomeEmailProps> = ({
userName,
verificationLink
}) => {
return (
<Html>
<Head />
<Body style={{ fontFamily: 'Arial, sans-serif', margin: '0', padding: '0' }}>
<Container style={{ maxWidth: '600px', margin: '0 auto' }}>
<Section>
<Heading as="h1">Welcome to VibeReference!</Heading>
<Text>Hi {userName},</Text>
<Text>Thank you for signing up. We're excited to have you on board!</Text>
<Text>Please verify your email address by clicking the button below:</Text>
<Section style={{ textAlign: 'center', margin: '30px 0' }}>
<Button
href={verificationLink}
style={{
backgroundColor: '#4F46E5',
color: 'white',
padding: '12px 24px',
borderRadius: '4px',
fontWeight: 'bold',
textDecoration: 'none',
}}
>
Verify Email
</Button>
</Section>
<Text>If you didn't create this account, you can safely ignore this email.</Text>
<Hr />
<Text style={{ fontSize: '14px', color: '#666' }}>
Best regards,<br />
The VibeReference Team
</Text>
</Section>
</Container>
</Body>
</Html>
);
};
Render the React email template:
// lib/email/renderEmail.ts
import { renderAsync } from '@react-email/render';
import { WelcomeEmail } from '@/emails/WelcomeEmail';
export async function renderWelcomeEmail(props: { userName: string; verificationLink: string }) {
const html = await renderAsync(WelcomeEmail(props));
// Generate plain text version or use a library like html-to-text
const text = htmlToText(html);
return {
html,
text,
};
}
Using SES with Serverless Functions
With AWS Lambda
// AWS Lambda function to send emails
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
const sesClient = new SESClient({ region: "us-east-1" });
export const handler = async (event) => {
try {
const { to, subject, html, text } = JSON.parse(event.body);
const params = {
Destination: {
ToAddresses: [to],
},
Message: {
Body: {
Html: {
Charset: "UTF-8",
Data: html,
},
Text: {
Charset: "UTF-8",
Data: text,
},
},
Subject: {
Charset: "UTF-8",
Data: subject,
},
},
Source: "noreply@yourdomain.com",
};
const command = new SendEmailCommand(params);
const result = await sesClient.send(command);
return {
statusCode: 200,
body: JSON.stringify({
success: true,
messageId: result.MessageId,
}),
};
} catch (error) {
console.error("Error sending email:", error);
return {
statusCode: 500,
body: JSON.stringify({
success: false,
error: error.message,
}),
};
}
};
Monitoring Email Performance
Email Metrics to Track
- Delivery Rate: Percentage of emails successfully delivered
- Open Rate: Percentage of delivered emails that are opened
- Click-Through Rate (CTR): Percentage of email recipients who clicked on a link
- Bounce Rate: Percentage of emails that couldn't be delivered
- Complaint Rate: Percentage of emails marked as spam
- Unsubscribe Rate: Percentage of recipients who unsubscribe
Tracking Pixel for Opens
// Add tracking pixel to email template
function addTrackingPixel(html: string, emailId: string) {
const trackingUrl = `https://yourvibereferenceapp.com/api/email/track?id=${emailId}&event=open`;
const trackingPixel = `<img src="${trackingUrl}" width="1" height="1" alt="" style="display: none;" />`;
// Insert before closing body tag
return html.replace('</body>', `${trackingPixel}</body>`);
}
Tracking Links for Clicks
// Wrap links with tracking URLs
function wrapLinksWithTracking(html: string, emailId: string) {
const dom = new JSDOM(html);
const links = dom.window.document.querySelectorAll('a');
links.forEach((link) => {
const originalUrl = link.getAttribute('href');
if (originalUrl) {
const encodedUrl = encodeURIComponent(originalUrl);
const trackingUrl = `https://yourvibereferenceapp.com/api/email/track?id=${emailId}&event=click&url=${encodedUrl}`;
link.setAttribute('href', trackingUrl);
}
});
return dom.serialize();
}
Testing Emails Locally
For local development, use a tool like Mailhog or Ethereal:
// lib/email/sendEmailDev.ts
import nodemailer from 'nodemailer';
export async function sendEmailDev(options: {
to: string | string[];
subject: string;
html: string;
text: string;
from: string;
}) {
// Create a test account using Ethereal
const testAccount = await nodemailer.createTestAccount();
// Create a transporter
const transporter = nodemailer.createTransport({
host: 'smtp.ethereal.email',
port: 587,
secure: false,
auth: {
user: testAccount.user,
pass: testAccount.pass,
},
});
// Send mail
const info = await transporter.sendMail({
from: options.from,
to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
subject: options.subject,
text: options.text,
html: options.html,
});
console.log('Message sent: %s', info.messageId);
// Preview URL
console.log('Preview URL: %s', nodemailer.getTestMessageUrl(info));
return info;
}
Resources
- AWS SES Documentation
- AWS SES Console
- AWS SDK for JavaScript v3
- Email Deliverability Best Practices
- React Email: Framework for building email templates with React
- MJML: Framework for responsive email design
- DMARC: Email authentication protocol