Email automation remains a cornerstone of business operations, from customer communications to internal notifications. This guide explores building production-ready email workflows with n8n that handle complex scenarios including template rendering, attachment processing, and multi-channel fallbacks.
Email Node Configuration
SMTP Integration Setup
Configure reliable SMTP sending with proper authentication:
// n8n SMTP Credentials Configuration
{
"name": "Production SMTP",
"type": "smtp",
"data": {
"host": "smtp.sendgrid.net",
"port": 587,
"secure": false,
"user": "apikey",
"password": "={{ $credentials.sendgridApiKey }}"
}
}IMAP Email Trigger
Monitor inboxes for incoming emails:
// IMAP Email Trigger Node Configuration
{
"parameters": {
"mailbox": "INBOX",
"postProcessAction": "markAsRead",
"options": {
"customEmailRules": [
{
"key": "from",
"value": "*@example.com"
}
],
"downloadAttachments": true,
"forceReconnect": true
}
},
"credentials": {
"imap": {
"id": "imap-credentials-id",
"name": "Support Inbox"
}
}
}Template Engine Implementation
Dynamic Email Templates
Create reusable templates with variable substitution:
// Function Node: Template Renderer
const templates = {
welcome: {
subject: "Welcome to {{company}}, {{firstName}}!",
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #2563eb;">Welcome, {{firstName}}!</h1>
<p>Thank you for joining {{company}}. We're excited to have you on board.</p>
<div style="background: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3>Getting Started</h3>
<ul>
{{#each onboardingSteps}}
<li>{{this}}</li>
{{/each}}
</ul>
</div>
<a href="{{dashboardUrl}}" style="display: inline-block; background: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">
Go to Dashboard
</a>
<p style="margin-top: 30px; color: #6b7280; font-size: 14px;">
Need help? Reply to this email or visit our <a href="{{supportUrl}}">support center</a>.
</p>
</div>
`
},
orderConfirmation: {
subject: "Order #{{orderId}} Confirmed",
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1>Order Confirmed!</h1>
<p>Hi {{firstName}}, your order has been confirmed.</p>
<table style="width: 100%; border-collapse: collapse;">
<tr style="background: #f3f4f6;">
<th style="padding: 10px; text-align: left;">Item</th>
<th style="padding: 10px; text-align: right;">Qty</th>
<th style="padding: 10px; text-align: right;">Price</th>
</tr>
{{#each items}}
<tr>
<td style="padding: 10px; border-bottom: 1px solid #e5e7eb;">{{name}}</td>
<td style="padding: 10px; border-bottom: 1px solid #e5e7eb; text-align: right;">{{quantity}}</td>
<td style="padding: 10px; border-bottom: 1px solid #e5e7eb; text-align: right;">{{price}}</td>
</tr>
{{/each}}
<tr>
<td colspan="2" style="padding: 10px; text-align: right; font-weight: bold;">Total:</td>
<td style="padding: 10px; text-align: right; font-weight: bold;">{{total}}</td>
</tr>
</table>
</div>
`
}
};
// Simple template engine
function renderTemplate(templateName, data) {
let template = templates[templateName];
if (!template) {
throw new Error(`Template '${templateName}' not found`);
}
let subject = template.subject;
let html = template.html;
// Replace simple variables
Object.entries(data).forEach(([key, value]) => {
if (typeof value === 'string' || typeof value === 'number') {
const regex = new RegExp(`{{${key}}}`, 'g');
subject = subject.replace(regex, value);
html = html.replace(regex, value);
}
});
// Handle arrays with {{#each}}
Object.entries(data).forEach(([key, value]) => {
if (Array.isArray(value)) {
const eachRegex = new RegExp(`{{#each ${key}}}([\\s\\S]*?){{/each}}`, 'g');
html = html.replace(eachRegex, (match, template) => {
return value.map(item => {
if (typeof item === 'object') {
let rendered = template;
Object.entries(item).forEach(([k, v]) => {
rendered = rendered.replace(new RegExp(`{{${k}}}`, 'g'), v);
});
return rendered;
}
return template.replace(/{{this}}/g, item);
}).join('');
});
}
});
return { subject, html };
}
// Render the template
const templateData = $input.first().json;
const rendered = renderTemplate(templateData.templateName, templateData.variables);
return [{
json: {
to: templateData.to,
subject: rendered.subject,
html: rendered.html,
replyTo: templateData.replyTo || 'noreply@example.com'
}
}];Attachment Handling
Processing Email Attachments
Handle incoming attachments securely:
// Function Node: Attachment Processor
const emailData = $input.first().json;
const attachments = emailData.attachments || [];
const processedAttachments = [];
const allowedMimeTypes = [
'application/pdf',
'image/jpeg',
'image/png',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv'
];
const maxSizeBytes = 10 * 1024 * 1024; // 10MB
for (const attachment of attachments) {
// Validate mime type
if (!allowedMimeTypes.includes(attachment.mimeType)) {
console.log(`Rejected attachment: ${attachment.filename} - Invalid type: ${attachment.mimeType}`);
continue;
}
// Validate size
const sizeBytes = Buffer.from(attachment.content, 'base64').length;
if (sizeBytes > maxSizeBytes) {
console.log(`Rejected attachment: ${attachment.filename} - Too large: ${sizeBytes} bytes`);
continue;
}
// Sanitize filename
const sanitizedFilename = attachment.filename
.replace(/[^a-zA-Z0-9.-]/g, '_')
.substring(0, 255);
processedAttachments.push({
filename: sanitizedFilename,
mimeType: attachment.mimeType,
content: attachment.content,
sizeBytes: sizeBytes
});
}
return [{
json: {
...emailData,
processedAttachments,
attachmentCount: processedAttachments.length
}
}];Creating Email Attachments
Generate and attach files dynamically:
// Function Node: Generate PDF Attachment
const orderData = $input.first().json;
// Generate invoice content (simplified - use a proper PDF library in production)
const invoiceHtml = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.header { border-bottom: 2px solid #2563eb; padding-bottom: 20px; }
.items { margin-top: 30px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #e5e7eb; }
.total { font-weight: bold; font-size: 18px; }
</style>
</head>
<body>
<div class="header">
<h1>Invoice #${orderData.invoiceNumber}</h1>
<p>Date: ${new Date().toLocaleDateString()}</p>
</div>
<div class="items">
<table>
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
${orderData.items.map(item =>
`<tr><td>${item.name}</td><td>${item.qty}</td><td>$${item.price}</td></tr>`
).join('')}
</table>
<p class="total">Total: $${orderData.total}</p>
</div>
</body>
</html>
`;
// In production, convert HTML to PDF using a service
return [{
json: {
...orderData,
attachment: {
filename: `invoice-${orderData.invoiceNumber}.html`,
content: Buffer.from(invoiceHtml).toString('base64'),
contentType: 'text/html'
}
}
}];Multi-Channel Orchestration
Email with SMS Fallback
Implement reliable delivery with fallbacks:
// Workflow: Multi-Channel Notification
// Node 1: Determine Channel Priority
const notification = $input.first().json;
const userPreferences = notification.userPreferences;
const channels = [];
// Build channel priority based on urgency and preferences
if (notification.urgency === 'critical') {
// Critical: SMS first, then email
if (userPreferences.smsEnabled) channels.push('sms');
channels.push('email');
if (userPreferences.pushEnabled) channels.push('push');
} else if (notification.urgency === 'high') {
// High: Email and push simultaneously
channels.push('email');
if (userPreferences.pushEnabled) channels.push('push');
} else {
// Normal: Email only
channels.push('email');
}
return [{
json: {
...notification,
channelPriority: channels,
currentChannel: 0,
deliveryAttempts: []
}
}];// Node 2: Send via Current Channel (Switch Node routes to appropriate sender)
// After sending, track delivery status
const result = $input.first().json;
const deliveryAttempt = {
channel: result.channel,
timestamp: new Date().toISOString(),
success: result.success,
messageId: result.messageId,
error: result.error || null
};
result.deliveryAttempts.push(deliveryAttempt);
// Check if we need fallback
if (!result.success && result.currentChannel < result.channelPriority.length - 1) {
// Move to next channel
result.currentChannel++;
result.needsRetry = true;
} else {
result.needsRetry = false;
result.finalStatus = result.success ? 'delivered' : 'failed';
}
return [{ json: result }];Email Queue Management
Rate-Limited Email Queue
Implement sending queue with rate limiting:
// Function Node: Email Queue Manager
const emailBatch = $input.all();
const rateLimit = 100; // emails per minute
const delayBetweenEmails = 60000 / rateLimit; // ms between emails
const queuedEmails = emailBatch.map((email, index) => ({
json: {
...email.json,
queuePosition: index,
scheduledSendTime: new Date(Date.now() + (index * delayBetweenEmails)).toISOString(),
queueId: `queue_${Date.now()}_${index}`,
status: 'queued'
}
}));
return queuedEmails;Bounce and Complaint Handling
Process email bounces and maintain list hygiene:
// Function Node: Bounce Handler
const webhookData = $input.first().json;
let action = 'none';
let severity = 'info';
switch (webhookData.eventType) {
case 'bounce':
if (webhookData.bounceType === 'Permanent') {
// Hard bounce - remove from list
action = 'unsubscribe';
severity = 'high';
} else {
// Soft bounce - increment counter
action = 'increment_soft_bounce';
severity = 'medium';
}
break;
case 'complaint':
// Spam complaint - immediate removal
action = 'unsubscribe';
severity = 'critical';
break;
case 'unsubscribe':
action = 'unsubscribe';
severity = 'info';
break;
}
return [{
json: {
email: webhookData.email,
eventType: webhookData.eventType,
action,
severity,
timestamp: new Date().toISOString(),
rawEvent: webhookData
}
}];Email Analytics and Tracking
Open and Click Tracking
Implement tracking pixel and link rewriting:
// Function Node: Add Tracking to Email
const email = $input.first().json;
const trackingDomain = 'https://track.example.com';
const emailId = email.emailId || `email_${Date.now()}`;
// Add tracking pixel for opens
const trackingPixel = `<img src="${trackingDomain}/open/${emailId}" width="1" height="1" style="display:none;" />`;
// Rewrite links for click tracking
let trackedHtml = email.html;
const linkRegex = /<a\s+([^>]*href=["'])([^"']+)(["'][^>]*)>/gi;
trackedHtml = trackedHtml.replace(linkRegex, (match, before, url, after) => {
// Don't track mailto or tel links
if (url.startsWith('mailto:') || url.startsWith('tel:')) {
return match;
}
const encodedUrl = encodeURIComponent(url);
const trackedUrl = `${trackingDomain}/click/${emailId}?url=${encodedUrl}`;
return `<a ${before}${trackedUrl}${after}>`;
});
// Add tracking pixel before closing body tag
trackedHtml = trackedHtml.replace('</body>', `${trackingPixel}</body>`);
return [{
json: {
...email,
emailId,
html: trackedHtml,
tracking: {
openPixelUrl: `${trackingDomain}/open/${emailId}`,
enabled: true
}
}
}];Error Handling and Retry Logic
Robust Email Sending with Retries
// Function Node: Email Sender with Retry Logic
const email = $input.first().json;
const maxRetries = 3;
const retryDelays = [1000, 5000, 15000]; // Exponential backoff
async function sendWithRetry(emailData, attempt = 0) {
try {
// Simulate sending (in production, this calls the email service)
const result = await sendEmail(emailData);
return {
success: true,
messageId: result.messageId,
attempt: attempt + 1
};
} catch (error) {
if (attempt < maxRetries - 1) {
// Determine if error is retryable
const retryableErrors = ['ECONNRESET', 'ETIMEDOUT', 'RATE_LIMIT'];
const isRetryable = retryableErrors.some(e =>
error.message.includes(e) || error.code === e
);
if (isRetryable) {
await new Promise(resolve => setTimeout(resolve, retryDelays[attempt]));
return sendWithRetry(emailData, attempt + 1);
}
}
return {
success: false,
error: error.message,
attempt: attempt + 1
};
}
}
// For n8n, we structure as output
return [{
json: {
...email,
sendConfig: {
maxRetries,
retryDelays,
retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'RATE_LIMIT']
}
}
}];Production Workflow Example
Complete Transactional Email Workflow
# Workflow: Transactional Email System
nodes:
- name: Webhook Trigger
type: n8n-nodes-base.webhook
parameters:
path: send-email
method: POST
authentication: headerAuth
- name: Validate Request
type: n8n-nodes-base.function
parameters:
functionCode: |
const data = $input.first().json;
const required = ['to', 'templateName', 'variables'];
const missing = required.filter(f => !data[f]);
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(', ')}`);
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.to)) {
throw new Error('Invalid email format');
}
return [{ json: data }];
- name: Render Template
type: n8n-nodes-base.function
# Template rendering code here
- name: Check Suppression List
type: n8n-nodes-base.postgres
parameters:
operation: select
table: email_suppressions
options:
where:
email: '={{ $json.to }}'
- name: Route Based on Suppression
type: n8n-nodes-base.if
parameters:
conditions:
- leftValue: '={{ $json.length }}'
operation: equal
rightValue: 0
- name: Send Email
type: n8n-nodes-base.emailSend
parameters:
fromEmail: '={{ $env.FROM_EMAIL }}'
toEmail: '={{ $json.to }}'
subject: '={{ $json.subject }}'
html: '={{ $json.html }}'
- name: Log Success
type: n8n-nodes-base.postgres
parameters:
operation: insert
table: email_logs
columns: to, template, status, message_id, sent_at
- name: Handle Failure
type: n8n-nodes-base.function
parameters:
functionCode: |
// Log failure and trigger alert if needed
const error = $input.first().json;
return [{
json: {
status: 'failed',
error: error.message,
timestamp: new Date().toISOString()
}
}];Best Practices Summary
- Always validate email addresses before sending
- Use templates for consistent, maintainable email content
- Implement tracking for opens and clicks
- Handle bounces to maintain list hygiene
- Rate limit to avoid provider throttling
- Log everything for debugging and compliance
- Use fallback channels for critical notifications
- Sanitize attachments to prevent security issues
Email automation with n8n provides the flexibility to build sophisticated workflows while maintaining the reliability needed for production systems. The key is combining proper error handling with comprehensive logging to ensure deliverability and maintain sender reputation.