Skip to content
intermediate Phase 14 · Email & Notifications

Email Sending & Templates

Send transactional emails with Nodemailer, SendGrid, or AWS SES. Build email templates.

1h
0 problems
Topic Progress 0%

Introduction to Email Sending

Email Sending in Modern Applications

Email remains the backbone of user communication in web applications. From password resets and order confirmations to marketing newsletters and system alerts, reliable email delivery is critical for user trust and engagement.

Why Email Matters

Transactional emails have open rates of 40-50%, far exceeding marketing emails at 20%. Users expect immediate, reliable delivery for password resets, account notifications, and purchase receipts. A failed email delivery can mean a lost customer or a security vulnerability.

Email Delivery Landscape

Modern email sending involves multiple layers: SMTP servers handle transport, DNS records (SPF, DKIM, DMARC) authenticate your domain, and inbox providers (Gmail, Outlook) apply filtering algorithms. Understanding this stack helps you diagnose delivery issues and maintain high deliverability rates.

Service Comparison

Service Free Tier Best For Complexity
Nodemailer Unlimited (self-hosted) Development, small apps Low
SendGrid 100 emails/day Growing apps, analytics Medium
AWS SES 62,000/month (EC2) High volume, cost-sensitive High
Mailgun 1,000/month Developer-friendly APIs Medium

Nodemailer Fundamentals

Sending Emails with Nodemailer

Nodemailer is the most popular Node.js library for sending emails. It supports SMTP, direct transport, and integration with major email providers.

Basic SMTP Configuration

```javascript
const nodemailer = require('nodemailer');

// Create transporter with SMTP configuration
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
},
tls: {
rejectUnauthorized: false
}
});

// Verify connection configuration
async function verifyConnection() {
try {
await transporter.verify();
console.log('SMTP server is ready to take our messages');
} catch (error) {
console.error('SMTP connection failed:', error.message);
}
}
```

Sending HTML Emails

```javascript
async function sendWelcomeEmail(userEmail, userName) {
const mailOptions = {
from: '"MyApp" noreply@myapp.com',
to: userEmail,
subject: 'Welcome to MyApp!',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #333;">Welcome, ${userName}!

Thank you for joining our platform. Here's what you can do next:



  • Complete your profile

  • Explore the dashboard

  • Connect with other users


<a href="https://myapp.com/dashboard\" style="
display: inline-block;
padding: 12px 24px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
">Go to Dashboard

`,
// Attachments are supported
attachments: [
{
filename: 'getting-started.pdf',
path: './docs/getting-started.pdf'
}
]
};

const info = await transporter.sendMail(mailOptions);
console.log('Message sent: %s', info.messageId);
return info;
}
```

Error Handling and Retry Logic

```javascript
const retry = require('async-retry');

async function sendWithRetry(mailOptions, maxRetries = 3) {
return retry(
async (bail) => {
try {
const info = await transporter.sendMail(mailOptions);
return info;
} catch (error) {
// Don't retry on permanent failures
if (error.responseCode === 550 || error.code === 'EINVALID') {
bail(error);
return;
}
throw error;
}
},
{
retries: maxRetries,
minTimeout: 1000,
maxTimeout: 5000,
onRetry: (error, attempt) => {
console.log(`Retry attempt ${attempt} for email: ${error.message}`);
}
}
);
}
```

SendGrid Integration

SendGrid API Integration

SendGrid provides a robust API for email delivery with built-in analytics, template management, and deliverability optimization. It handles the complexities of email infrastructure so you can focus on your application.

Setting Up SendGrid

```javascript
const sgMail = require('@sendgrid/mail');

sgMail.setApiKey(process.env.SENDGRID_API_KEY);

// Configure sender identity
const senderConfig = {
email: 'noreply@myapp.com',
name: 'MyApp'
};

// Send a simple email
async function sendPasswordReset(email, resetToken) {
const msg = {
to: email,
from: senderConfig,
templateId: 'd-abc123def456',
dynamicTemplateData: {
resetLink: `https://myapp.com/reset?token=\${resetToken}\`,
expirationTime: '24 hours'
}
};

try {
const response = await sgMail.send(msg);
console.log('Email sent successfully');
return response;
} catch (error) {
console.error('SendGrid error:', error.response.body.errors);
throw error;
}
}
```

SendGrid with Handlebars Templates

```javascript
// SendGrid supports dynamic templates with handlebars syntax
const orderConfirmation = {
to: customerEmail,
from: senderConfig,
templateId: 'd-order-confirmation',
dynamicTemplateData: {
customerName: 'John Doe',
orderNumber: 'ORD-2024-001',
orderDate: new Date().toLocaleDateString(),
items: [
{ name: 'Product A', quantity: 2, price: 29.99 },
{ name: 'Product B', quantity: 1, price: 49.99 }
],
subtotal: 109.97,
shipping: 9.99,
total: 119.96
}
};

await sgMail.send(orderConfirmation);
```

Batch Sending and Rate Limiting

```javascript
const pLimit = require('p-limit');

// SendGrid allows 100 messages per API call
const BATCH_SIZE = 100;
const concurrencyLimit = pLimit(5); // 5 concurrent API calls

async function sendBatchEmails(recipients, templateId, templateData) {
const batches = [];

for (let i = 0; i < recipients.length; i += BATCH_SIZE) {
batches.push(recipients.slice(i, i + BATCH_SIZE));
}

const results = await Promise.all(
batches.map((batch, index) =>
concurrencyLimit(async () => {
const messages = batch.map(recipient => ({
to: recipient.email,
from: senderConfig,
templateId,
dynamicTemplateData: {
...templateData,
recipientName: recipient.name
}
}));

    return sgMail.send(messages);
  })
)

);

return results.flat();
}
```

AWS Simple Email Service

AWS SES for Scalable Email

Amazon Simple Email Service (SES) offers cost-effective email sending at scale. At $0.10 per 1,000 emails, it's ideal for high-volume applications. SES requires domain verification and proper authentication setup.

SES Configuration

```javascript
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');

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
}
});

// Send email with SES
async function sendSESEmail(to, subject, htmlBody) {
const command = new SendEmailCommand({
Source: 'noreply@myapp.com',
Destination: {
ToAddresses: Array.isArray(to) ? to : [to]
},
Message: {
Subject: {
Data: subject,
Charset: 'UTF-8'
},
Body: {
Html: {
Data: htmlBody,
Charset: 'UTF-8'
},
Text: {
Data: htmlBody.replace(/<[^>]*>/g, ''),
Charset: 'UTF-8'
}
}
},
ReplyToAddresses: ['support@myapp.com']
});

try {
const response = await sesClient.send(command);
console.log('Email sent via SES:', response.MessageId);
return response;
} catch (error) {
console.error('SES error:', error.name, error.message);
throw error;
}
}
```

Domain Verification and DNS Setup

To send emails from your domain via SES, you must verify it:

```

SPF Record (TXT)

myapp.com. IN TXT "v=spf1 include:amazonses.com ~all"

DKIM Record (CNAME) - provided by SES

sesdomainkey.myapp.com. IN CNAME abc123.dkim.amazonses.com

DMARC Record (TXT)

_dmarc.myapp.com. IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@myapp.com"
```

SES Templates with HTML

```javascript
const {
CreateTemplateCommand,
SendTemplatedEmailCommand
} = require('@aws-sdk/client-ses');

// Create a reusable template
async function createEmailTemplate() {
const template = {
TemplateName: 'WelcomeTemplate',
Subject: 'Welcome to {{appName}}, {{userName}}!',
HtmlPart: `

<body style="font-family: sans-serif;">

Hello {{userName}},


Welcome to {{appName}}! Your account is ready.


<a href="{{dashboardUrl}}" style="
display: inline-block;
padding: 10px 20px;
background: #007bff;
color: white;
text-decoration: none;
border-radius: 5px;
">Go to Dashboard


`,
TextPart: 'Hello {{userName}}, Welcome to {{appName}}!'
};

const command = new CreateTemplateCommand(template);
return sesClient.send(command);
}

// Send using template
async function sendTemplatedEmail(to, templateData) {
const command = new SendTemplatedEmailCommand({
Source: 'noreply@myapp.com',
Destination: { ToAddresses: [to] },
Template: 'WelcomeTemplate',
TemplateData: JSON.stringify(templateData)
});

return sesClient.send(command);
}
```

Email Templates and Best Practices

Email Template Design

Effective email templates must work across dozens of email clients. Use inline CSS, table-based layouts, and MJML for reliable rendering.

Responsive HTML Template Structure

```html

Email
\"Logo\"

Hello, {{name}}

{{content}}

{{#if actionUrl}} {{actionText}} {{/if}}

© 2024 MyApp. All rights reserved. Unsubscribe

\`\`\`

MJML for Responsive Emails

```javascript
const mjml = require('mjml');

function generateEmailTemplate(data) {
const mjmlTemplate = `



<mj-all font-family="Arial, sans-serif" />
<mj-text font-size="16px" color="#333" />



<mj-section background-color="#007bff">

<mj-text align="center" color="white" font-size="24px">
Welcome, ${data.name}!





${data.message}
<mj-button href="${data.actionUrl}" background-color="#007bff">
${data.actionText}





<mj-text font-size="12px" color="#999">
© 2024 MyApp. All rights reserved.





`;

const { html } = mjml(mjmlTemplate, { minify: true });
return html;
}
```

Email Delivery Checklist

  • Authentication: Set up SPF, DKIM, and DMARC records
  • Testing: Send test emails to Gmail, Outlook, Yahoo, and Apple Mail
  • Tracking: Add UTM parameters for campaign tracking
  • Unsubscribe: Include one-click unsubscribe headers (RFC 8058)
  • Bounce Handling: Process bounce and complaint notifications via webhooks
  • Rate Limiting: Respect provider limits (SendGrid: 100/sec, SES: 14/sec in sandbox)

Quiz

1. Which DNS records are required for proper email authentication with SendGrid or AWS SES?

Question 1 options

2. What is the correct approach to handle email delivery failures in a production application?

Question 2 options

3. Why do email templates use table-based layouts and inline CSS instead of modern CSS Flexbox/Grid?

Question 3 options

Flashcards

Question

What is the difference between Nodemailer and SendGrid?

Answer

Nodemailer is a Node.js library that sends emails directly via SMTP servers you configure. SendGrid is a cloud-based email service with its own API, providing deliverability tools, analytics, template management, and built-in rate limiting. Use Nodemailer for development or simple setups, and SendGrid when you need reliable delivery at scale with tracking.

Question

What are SPF, DKIM, and DMARC?

Answer

SPF (Sender Policy Framework) is a DNS TXT record that lists servers authorized to send email for your domain. DKIM (DomainKeys Identified Mail) adds a cryptographic signature to verify the email wasn't tampered with in transit. DMARC (Domain-based Message Authentication, Reporting & Conformance) tells receivers what to do with emails that fail SPF/DKIM checks (reject, quarantine, or allow).

Question

Why does AWS SES require sandbox mode for new accounts?

Answer

AWS SES starts in sandbox mode to prevent abuse. In sandbox, you can only send to verified email addresses. To exit sandbox, you must request production access by demonstrating a legitimate use case and confirming you've implemented proper email authentication (SPF, DKIM). This protects Amazon's sending reputation and prevents spam from new accounts.

Revision Notes

Key Takeaways

  • 1. Nodemailer is ideal for development and SMTP-based sending; use SendGrid or SES for production at scale
  • 2. Always configure SPF, DKIM, and DMARC DNS records to ensure email authentication and deliverability
  • 3. Use table-based layouts with inline CSS or MJML for cross-client email compatibility
  • 4. Implement retry logic with exponential backoff, distinguishing temporary failures (retry) from permanent ones (don't retry)
  • 5. SES is most cost-effective for high-volume sending ($0.10/1000 emails) but requires AWS infrastructure knowledge
  • 6. Keep unsubscribe links and process bounce/complaint webhooks to maintain sender reputation

Interview Tips

  • Explain the email delivery pipeline: your app → SMTP/API → DNS authentication → recipient server → inbox/spam/bounce
  • Know the differences between transactional emails (password resets, receipts) vs marketing emails (newsletters, promotions)
  • Discuss email deliverability factors: sender reputation, authentication records, content quality, engagement metrics
  • Describe how to handle email failures gracefully: retry logic, dead letter queues, user notifications, monitoring alerts
  • Be able to explain why email templates use table layouts and inline CSS (Outlook compatibility)
  • Know when to use each provider: Nodemailer for dev, SendGrid for mid-scale with analytics, SES for high-volume cost optimization

Cheat Sheet

Email Provider Selection: Nodemailer (dev/simple SMTP), SendGrid (mid-scale, analytics), SES (high-volume, cheapest). DNS Setup: SPF (TXT - authorized senders), DKIM (CNAME - cryptographic signing), DMARC (TXT - failure policy). Template Best Practices: Inline CSS, table layouts, max 600px width, plain text fallback. Error Handling: Exponential backoff retry, distinguish 5xx (temporary) from 4xx (permanent) errors, circuit breaker pattern. Security: Never log email content with PII, use environment variables for API keys, implement rate limiting per user.