Skip to main content

Email System

Express Backend TS includes a complete email system with Handlebars templates, queue support for reliable delivery, and easy customization.

Features

  • Template Engine - Handlebars-based HTML templates with layouts
  • Beautiful Design - Pre-built responsive email templates
  • Queue Support - Background processing with Bull and Redis
  • Retry Mechanism - Automatic retry on failure (3 attempts with exponential backoff)
  • Attachments - Support for file attachments
  • Dual Mode - Development (MailDev) and Production (SMTP) configurations
  • Fast Delivery - Non-blocking async sending via queue
  • Easy Testing - MailDev integration for local development

Architecture

Trigger (Login, Register, etc.)
|
v
mailService.sendWithQueue()
|
v
Bull Queue (if USE_QUEUE=yes)
|
v
Template Rendering (Handlebars + Layout)
|
v
SMTP Transport (Nodemailer)
|
v
Email Delivered

Quick Start

Send an Email

import mailService from './utils/bases/mail.service';

// Send email immediately (blocking)
await mailService.send(
'user@example.com', // receivers
'Welcome!', // subject
'auth/activation', // template path
{ // template data
user_name: 'John Doe',
activation_code: '123456',
}
);
import mailService from './utils/bases/mail.service';

// Add email to queue (non-blocking, recommended)
await mailService.sendWithQueue(
'user@example.com',
'Welcome!',
'auth/activation',
{
user_name: 'John Doe',
activation_code: '123456',
}
);

Available Templates

All templates are located in templates/auth/ directory.

1. Activation Email

File: templates/auth/activation.hbs

Usage:

await mailService.sendWithQueue(
user.email,
'Activate Your Account',
'auth/activation',
{
user_name: `${user.first_name} ${user.last_name}`,
activation_code: otp,
}
);

Variables:

  • user_name - User's full name
  • activation_code - 6-digit OTP

2. Login Notification

File: templates/auth/login.hbs

Usage:

await mailService.sendWithQueue(
user.email,
'New Login Detected',
'auth/login',
{
user_name: `${user.first_name} ${user.last_name}`,
login_time: new Date().toLocaleString(),
device: loginHistory.device,
browser: loginHistory.browser,
location: loginHistory.location,
ip_address: loginHistory.ip,
}
);

Variables:

  • user_name - User's full name
  • login_time - Login timestamp
  • device - Device type (Desktop, Mobile, Tablet)
  • browser - Browser name
  • location - City/location
  • ip_address - IP address

3. Forgot Password

File: templates/auth/forgot-password.hbs

Usage:

await mailService.sendWithQueue(
user.email,
'Reset Your Password',
'auth/forgot-password',
{
user_name: `${user.first_name} ${user.last_name}`,
reset_link: `${process.env.APP_URL_FRONTEND}/reset-password?token=${resetToken}`,
}
);

Variables:

  • user_name - User's full name
  • reset_link - Password reset link with token

4. Register OTP

File: templates/auth/register-otp.hbs

Usage:

await mailService.sendWithQueue(
user.email,
'Verify Your Email',
'auth/register-otp',
{
user_name: `${user.first_name} ${user.last_name}`,
otp_code: otp,
}
);

Variables:

  • user_name - User's full name
  • otp_code - 6-digit OTP

5. Security Alert

File: templates/auth/security-alert.hbs

Usage:

await mailService.sendWithQueue(
user.email,
'Security Alert',
'auth/security-alert',
{
user_name: `${user.first_name} ${user.last_name}`,
alert_reason: 'Multiple failed login attempts detected',
}
);

Variables:

  • user_name - User's full name
  • alert_reason - Why alert was triggered

6. Password Changed

File: templates/auth/password-changed.hbs

Usage:

await mailService.sendWithQueue(
user.email,
'Password Changed Successfully',
'auth/password-changed',
{
user_name: `${user.first_name} ${user.last_name}`,
change_time: new Date().toLocaleString(),
}
);

Creating Custom Templates

Step 1: Create Template File

Create a new .hbs file in templates/ directory (e.g., templates/custom/welcome.hbs):

<div class="content">
<h2>Welcome, {{user_name}}!</h2>
<p>We're excited to have you on board at {{app_name}}.</p>

<ul>
{{#each features}}
<li>{{this}}</li>
{{/each}}
</ul>

<a href="{{dashboard_link}}" class="button">Get Started</a>
</div>

Step 2: Send Email with Custom Template

await mailService.sendWithQueue(
user.email,
'Welcome to Our Platform!',
'custom/welcome',
{
user_name: 'John Doe',
features: [
'Complete Authentication System',
'Redis Caching',
'File Upload',
'Email Templates',
],
dashboard_link: 'https://app.com/dashboard',
}
);

Email Service Methods

send()

Send email immediately (blocking).

mailService.send(
receivers: string | string[],
subject: string,
content: string, // template path
html?: EmailTemplateData | null, // template variables
sender?: MailSender | null, // custom sender
attachments?: any[] // file attachments
): Promise<MailResult>

Example:

await mailService.send(
['user1@example.com', 'user2@example.com'],
'Monthly Report',
'reports/monthly',
{
month: 'January',
revenue: '$50,000',
growth: '15%',
},
null,
[
{
filename: 'report.pdf',
path: './reports/january.pdf',
},
]
);

sendWithQueue()

Add email to queue (non-blocking, recommended).

mailService.sendWithQueue(
receivers: string | string[],
subject: string,
content: string,
html?: EmailTemplateData | null,
sender?: MailSender | null,
attachments?: any[]
): Promise<Job | MailResult | void>

Benefits:

  • Non-blocking - returns immediately
  • Automatic retry on failure (3 attempts with exponential backoff: 2s, 4s, 8s)
  • Survives server restarts (Redis persistence)
  • Better performance for bulk emails
  • Queue processing logs

With Attachments

File Attachments

await mailService.sendWithQueue(
user.email,
'Your Invoice',
'invoice',
{
user_name: user.name,
invoice_number: 'INV-001',
total: '$99.99',
},
null,
[
{
filename: 'invoice.pdf',
path: './invoices/INV-001.pdf',
},
{
filename: 'receipt.pdf',
path: './receipts/REC-001.pdf',
},
]
);

Buffer Attachments

const pdfBuffer = await generatePDF();

await mailService.sendWithQueue(
user.email,
'Your Report',
'report',
{ user_name: user.name },
null,
[
{
filename: 'report.pdf',
content: pdfBuffer,
contentType: 'application/pdf',
},
]
);

Configuration

Environment Variables

Development mode uses variables with _DEV suffix:

# Development Email (MailDev)
MAIL_HOST_DEV=localhost
MAIL_PORT_DEV=1025
MAIL_SERVICE_DEV=gmail
MAIL_SENSER_NAME_DEV="Development Team"
MAIL_SENSER_EMAIL_DEV="dev@localhost"
MAIL_SECURE_DEV=false
MAIL_USER_DEV=false
MAIL_PASSWORD_DEV=false

Production mode uses variables without suffix:

# Production Email (SMTP)
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_SERVICE=gmail
MAIL_SENSER_NAME="Your Company"
MAIL_SENSER_EMAIL="noreply@yourdomain.com"
MAIL_SECURE=true
MAIL_USER=your-email@gmail.com
MAIL_PASSWORD=your-app-password

The system automatically selects the correct configuration based on APP_MODE.

Template Path

TEMPLATE_PATH=templates

SMTP Providers

Gmail

MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_SECURE=false
MAIL_USER=your-email@gmail.com
MAIL_PASSWORD=your-app-password # Use App Password, not regular password

Gmail Setup:

  1. Enable 2FA on your Google account
  2. Go to Google Account > Security > 2-Step Verification > App Passwords
  3. Generate an App Password
  4. Use the App Password in MAIL_PASSWORD

SendGrid

MAIL_HOST=smtp.sendgrid.net
MAIL_PORT=587
MAIL_SECURE=false
MAIL_USER=apikey
MAIL_PASSWORD=your-sendgrid-api-key

AWS SES

MAIL_HOST=email-smtp.us-east-1.amazonaws.com
MAIL_PORT=587
MAIL_SECURE=false
MAIL_USER=your-ses-smtp-username
MAIL_PASSWORD=your-ses-smtp-password

Mailgun

MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_SECURE=false
MAIL_USER=postmaster@your-domain.mailgun.org
MAIL_PASSWORD=your-mailgun-password

Development Testing

MailDev

MailDev captures all emails locally without sending real emails:

# Install MailDev
npm install -g maildev

# Start MailDev
maildev

# Access web UI at http://localhost:1080

Configuration (in .env):

APP_MODE=DEVELOPMENT
MAIL_HOST_DEV=localhost
MAIL_PORT_DEV=1025
MAIL_SECURE_DEV=false
MAIL_USER_DEV=false
MAIL_PASSWORD_DEV=false

All emails will be captured by MailDev and visible in the web UI at http://localhost:1080.

Queue Management

View Queue Status

import mailService from './utils/bases/mail.service';

// Get queue statistics
const stats = await mailService.getQueueStats();
console.log(stats);
// {
// waiting: 10,
// active: 2,
// completed: 145,
// failed: 3,
// delayed: 5
// }

Get Failed Jobs

// Get all failed jobs
const failed = await mailService.getFailedJobs();

console.log(failed);
// [
// {
// id: '123',
// data: { type: 'mail', data: {...} },
// failedReason: 'SMTP_UNREACHABLE: Connection timeout',
// attemptsMade: 3,
// timestamp: 1234567890,
// }
// ]

Retry Failed Jobs

// Retry a specific job
await mailService.retryFailedJob('123');

// Retry all failed jobs
const retriedCount = await mailService.retryAllFailedJobs();
console.log(`Retried ${retriedCount} jobs`);

Remove Failed Jobs

// Remove a specific job
await mailService.removeFailedJob('123');

Clean Old Jobs

// Clean completed jobs older than 24 hours
await mailService.cleanCompletedJobs(24 * 60 * 60 * 1000);

// Clean failed jobs older than 7 days
await mailService.cleanFailedJobs(7 * 24 * 60 * 60 * 1000);

Template System

Layout Structure

The template system uses a layout-based approach:

templates/
├── layouts/
│ └── default.hbs # Base layout
├── partials/ # Reusable components
│ ├── header.hbs
│ └── footer.hbs
└── auth/ # Email templates
├── activation.hbs
├── login.hbs
└── ...

How It Works

  1. Layout (layouts/default.hbs) - Contains the HTML structure
  2. Template (auth/activation.hbs) - Contains the specific email content
  3. Rendering - Template is inserted into layout's {{> body}} placeholder

Template Variables

All templates have access to:

  • app_name - Application name from APP_NAME
  • app_settings - Full app configuration
  • Custom variables passed in template data

Handlebars Helpers

The system includes custom Handlebars helpers (located in utils/helpers/handlebars.helper.ts).

Best Practices

1. Always Use Queue for Production

// Good - Non-blocking, survives restarts
await mailService.sendWithQueue(/* ... */);

// Bad - Blocks request, no retry
await mailService.send(/* ... */);

2. Handle Errors Gracefully

try {
await mailService.sendWithQueue(
user.email,
'Welcome',
'auth/activation',
{ user_name: user.name }
);
} catch (error) {
console.error('Failed to queue email:', error);
// Log error but don't fail the request
}

3. Use Meaningful Subjects

// Good
subject: 'Welcome to ExpressBase - Activate Your Account'

// Bad
subject: 'Activation'

4. Test Templates Thoroughly

Always test email rendering across different clients:

  • Gmail
  • Outlook
  • Apple Mail
  • Mobile devices

Troubleshooting

Emails Not Sending

Check:

  1. SMTP credentials correct in .env
  2. USE_MAIL=yes in environment
  3. No firewall blocking SMTP port
  4. Check queue status if using queues
const stats = await mailService.getQueueStats();
console.log(stats);

"Invalid login" Error

Solution:

  • Gmail: Use App Password, not account password
  • Enable "Less secure app access" (not recommended)
  • Check 2FA settings

Emails in Spam

Solutions:

  • Set up SPF, DKIM, DMARC records for your domain
  • Use verified domain
  • Include unsubscribe link
  • Avoid spam trigger words

Template Not Found

Check:

  • Template file exists in templates/ directory
  • File extension is .hbs
  • Path is correct (e.g., auth/activation, not /auth/activation.hbs)

Queue Not Processing

Check:

# Test Redis connection
redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT} ping
# Should respond: PONG

# Check queue configuration
USE_QUEUE=yes

View queue processor logs: The queue processor logs all activity:

 Mail queue processor started
Processing mail job 123 (Attempt 1/3)
📤 Sending mail to: user@example.com
Mail sent successfully to: user@example.com

Next Steps


Need help? Check the GitHub repository or contact support.