n8n Automation

n8n Email Automation: Complete Workflow Implementation Guide

DeviDevs Team
9 min read
#n8n#email automation#workflow automation#SMTP#IMAP

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

  1. Always validate email addresses before sending
  2. Use templates for consistent, maintainable email content
  3. Implement tracking for opens and clicks
  4. Handle bounces to maintain list hygiene
  5. Rate limit to avoid provider throttling
  6. Log everything for debugging and compliance
  7. Use fallback channels for critical notifications
  8. 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.

Weekly AI Security & Automation Digest

Get the latest on AI Security, workflow automation, secure integrations, and custom platform development delivered weekly.

No spam. Unsubscribe anytime.