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',
}
);
Using Queue (Recommended)
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 nameactivation_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 namelogin_time- Login timestampdevice- Device type (Desktop, Mobile, Tablet)browser- Browser namelocation- City/locationip_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 namereset_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 nameotp_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 namealert_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):
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:
- Enable 2FA on your Google account
- Go to Google Account > Security > 2-Step Verification > App Passwords
- Generate an App Password
- 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
- Layout (
layouts/default.hbs) - Contains the HTML structure - Template (
auth/activation.hbs) - Contains the specific email content - Rendering - Template is inserted into layout's
{{> body}}placeholder
Template Variables
All templates have access to:
app_name- Application name fromAPP_NAMEapp_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:
- SMTP credentials correct in
.env USE_MAIL=yesin environment- No firewall blocking SMTP port
- 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
- Email Templates Guide - Advanced template customization
- Queue System - Deep dive into Bull queues
- Environment Configuration - Email configuration reference
Need help? Check the GitHub repository or contact support.