Nodemailer-based email system for transactional notifications and user communications.
Overview
- Nodemailer for email delivery
- SMTP configuration for external email services
- Connection pooling for performance
- Notification tracking in database
- Cache integration for unseen count
- Template support for consistent formatting
Email Configuration
Nodemailer Setup
// shared/notifications/email.ts
import nodemailer from 'nodemailer';
import { logger } from '@/shared/logging/file';
export class EmailNotifier extends Notifier {
transporter: nodemailer.Transporter;
constructor() {
super();
this.transporter = nodemailer.createTransport({
service: process.env.MAIL_HOST,
host: process.env.MAIL_HOST,
port: 587,
secure: false,
auth: {
user: process.env.MAIL_USERNAME,
pass: process.env.MAIL_PASSWORD,
},
tls: {
rejectUnauthorized: false,
},
pool: true, // Use connection pool
});
this.transporter.on('error', (error) => {
logger.error(`Email transporter error: ${error.message}`);
this.transporter.close();
});
}
async notify(
subject: string,
message: string,
to: User,
type: string,
relatedObjectId?: string,
): Promise<void> {
const mailOptions = {
from: process.env.MAIL_USERNAME,
to: to.email,
subject,
text: message,
};
this.transporter.sendMail(mailOptions, (error, info) => {
if (error) {
logger.error(`Email send failed: ${error.message}`);
} else {
logger.info(`Email sent: ${info.response}`);
}
});
// Store notification in database
await db.Notification.create({
id: this.nextId(),
userId: to.id,
medium: to.email,
subject,
message,
date: new Date(),
type,
isSeen: false,
isArchived: false,
relatedObjectId: relatedObjectId || null,
});
// Update cache
const unseenCount = await this.countUnseen(to.id);
await this.cache.set(`notifications:unseen_count:${to.id}`, unseenCount + 1);
}
close(): void {
logger.debug('Closing email transporter');
this.transporter.close();
}
}
Environment Variables
# Email Configuration
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-password
MAIL_FROM=noreply@2krika.com
# Email Settings
MAIL_SECURE=false
MAIL_TLS_REJECT_UNAUTHORIZED=false
Email Providers
Gmail
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=your-gmail@gmail.com
MAIL_PASSWORD=your-app-password # Generate from Google Account settings
Note: Enable "Less secure app access" or use App Password for Gmail.
SendGrid
Mailgun
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=your-mailgun-username
MAIL_PASSWORD=your-mailgun-password
AWS SES
MAIL_HOST=email-smtp.us-east-1.amazonaws.com
MAIL_PORT=587
MAIL_USERNAME=your-ses-smtp-username
MAIL_PASSWORD=your-ses-smtp-password
Notification System
Notification Model
// domain/shared/notification.entity.ts
export interface NotificationProps {
id: string;
userId: string;
medium: string; // Email address
subject: string;
message: string;
date: Date;
type: string; // 'order', 'payment', 'kyc', etc.
isSeen: boolean;
isArchived: boolean;
relatedObjectId?: string; // Order ID, Payment ID, etc.
}
Notification Repository
export class EmailNotifier {
async getNotifications(
userId: string,
page: number = 1,
pageSize: number = 20,
): Promise<Paginated<Notification>> {
const offset = (page - 1) * pageSize;
const { rows, count } = await db.Notification.findAndCountAll({
where: { userId, isArchived: false },
order: [['date', 'DESC']],
limit: pageSize,
offset,
});
return {
page,
pageSize,
count,
results: rows.map(this.mapToNotification),
};
}
async countUnseen(userId: string): Promise<number> {
// Check cache first
const cacheKey = `notifications:unseen_count:${userId}`;
let count = await this.cache.get<number>(cacheKey);
if (count === undefined || count === null) {
count = await db.Notification.count({
where: { userId, isSeen: false, isArchived: false },
});
await this.cache.set(cacheKey, count);
}
return count;
}
async markAsSeen(userId: string, notificationId: string): Promise<void> {
await db.Notification.update(
{ isSeen: true },
{ where: { id: notificationId, userId } }
);
}
async markAsArchived(userId: string, notificationId: string): Promise<void> {
await db.Notification.update(
{ isArchived: true },
{ where: { id: notificationId, userId } }
);
}
}
Email Templates
Template Structure
export interface EmailTemplate {
subject: string;
text: string;
html?: string;
}
export class EmailTemplates {
static welcome(user: User): EmailTemplate {
return {
subject: 'Welcome to 2KRIKA!',
text: `
Hi ${user.firstName},
Welcome to 2KRIKA! We're excited to have you on board.
Get started by:
1. Completing your profile
2. Browsing available services
3. Posting your first service
Best regards,
The 2KRIKA Team
`,
html: `
<h1>Welcome to 2KRIKA!</h1>
<p>Hi ${user.firstName},</p>
<p>We're excited to have you on board.</p>
<p>Get started by:</p>
<ul>
<li>Completing your profile</li>
<li>Browsing available services</li>
<li>Posting your first service</li>
</ul>
<p>Best regards,<br>The 2KRIKA Team</p>
`,
};
}
static orderConfirmation(order: Order, user: User): EmailTemplate {
return {
subject: `Order Confirmation - ${order.id}`,
text: `
Hi ${user.firstName},
Your order has been confirmed!
Order ID: ${order.id}
Service: ${order.service.name}
Price: ${order.price}
Status: ${order.status}
You can track your order status in your dashboard.
Best regards,
The 2KRIKA Team
`,
};
}
static paymentReceived(payment: Payment, user: User): EmailTemplate {
return {
subject: `Payment Received - ${payment.reference}`,
text: `
Hi ${user.firstName},
We've received your payment!
Payment Reference: ${payment.reference}
Amount: ${payment.amount}
Status: ${payment.status}
Your order will be processed shortly.
Best regards,
The 2KRIKA Team
`,
};
}
static kycApproved(user: User): EmailTemplate {
return {
subject: 'KYC Verification Approved',
text: `
Hi ${user.firstName},
Great news! Your KYC verification has been approved.
You can now:
- Offer services on the platform
- Receive payments
- Access all premium features
Best regards,
The 2KRIKA Team
`,
};
}
static kycRejected(user: User, reason: string): EmailTemplate {
return {
subject: 'KYC Verification Update',
text: `
Hi ${user.firstName},
We were unable to approve your KYC verification.
Reason: ${reason}
Please update your information and resubmit.
Best regards,
The 2KRIKA Team
`,
};
}
}
Using Templates
export class EmailNotifier {
async sendWelcomeEmail(user: User): Promise<void> {
const template = EmailTemplates.welcome(user);
await this.notify(
template.subject,
template.text,
user,
'welcome',
);
}
async sendOrderConfirmation(order: Order, user: User): Promise<void> {
const template = EmailTemplates.orderConfirmation(order, user);
await this.notify(
template.subject,
template.text,
user,
'order',
order.id,
);
}
}
Email Use Cases
1. User Registration
2. Order Lifecycle
// Order created
await emailNotifier.sendOrderConfirmation(order, buyer);
// Order accepted
await emailNotifier.notify(
'Order Accepted',
`Your order ${order.id} has been accepted by the seller.`,
buyer,
'order',
order.id,
);
// Order completed
await emailNotifier.notify(
'Order Completed',
`Your order ${order.id} has been completed!`,
buyer,
'order',
order.id,
);
3. Payment Events
// Payment successful
await emailNotifier.sendPaymentReceived(payment, user);
// Payment failed
await emailNotifier.notify(
'Payment Failed',
`Your payment for order ${order.id} failed. Please try again.`,
user,
'payment',
payment.id,
);
4. KYC Updates
// KYC approved
await emailNotifier.sendKycApproved(user);
// KYC rejected
await emailNotifier.sendKycRejected(user, rejectionReason);
5. Password Reset
await emailNotifier.notify(
'Password Reset Request',
`Click here to reset your password: ${resetLink}`,
user,
'password_reset',
);
Advanced Features
HTML Email Templates
import { renderToString } from 'react-dom/server';
export class EmailNotifier {
async sendHtmlEmail(
to: User,
subject: string,
htmlContent: string,
): Promise<void> {
const mailOptions = {
from: process.env.MAIL_USERNAME,
to: to.email,
subject,
html: htmlContent,
};
await this.transporter.sendMail(mailOptions);
}
}
Email with Attachments
async sendEmailWithAttachment(
to: User,
subject: string,
message: string,
attachments: Array<{ filename: string; path: string }>,
): Promise<void> {
const mailOptions = {
from: process.env.MAIL_USERNAME,
to: to.email,
subject,
text: message,
attachments,
};
await this.transporter.sendMail(mailOptions);
}
// Usage
await emailNotifier.sendEmailWithAttachment(
user,
'Invoice',
'Please find your invoice attached.',
[{ filename: 'invoice.pdf', path: '/path/to/invoice.pdf' }],
);
Batch Emails
async sendBulkEmails(
users: User[],
subject: string,
message: string,
): Promise<void> {
const promises = users.map(user =>
this.notify(subject, message, user, 'bulk')
);
await Promise.all(promises);
}
Error Handling
Retry Logic
async sendEmailWithRetry(
mailOptions: any,
maxRetries: number = 3,
): Promise<void> {
for (let i = 0; i < maxRetries; i++) {
try {
await this.transporter.sendMail(mailOptions);
return;
} catch (error) {
logger.error(`Email send attempt ${i + 1} failed: ${error.message}`);
if (i === maxRetries - 1) {
throw error; // Final attempt failed
}
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
}
Email Queue
For high-volume emails, use a queue:
import Bull from 'bull';
const emailQueue = new Bull('email', {
redis: { host: 'localhost', port: 6379 },
});
// Add to queue
emailQueue.add({
to: user.email,
subject: 'Subject',
message: 'Message',
});
// Process queue
emailQueue.process(async (job) => {
const { to, subject, message } = job.data;
await emailNotifier.sendEmail(to, subject, message);
});
Best Practices
✅ Do
- Use templates – Consistent formatting
- Track notifications – Store in database
- Handle errors – Log and retry
- Use connection pooling – Better performance
- Validate email addresses – Before sending
- Provide unsubscribe – For marketing emails
- Use HTML sparingly – Many clients block it
❌ Don't
- Don't send spam – Respect user preferences
- Don't expose credentials – Use environment variables
- Don't send large attachments – Use links instead
- Don't forget rate limits – Email providers have limits
- Don't send without confirmation – For sensitive actions
- Don't ignore bounces – Track and handle
Testing
Mock Email Notifier
// tests/mocks/email.mock.ts
export class MockEmailNotifier extends Notifier {
private emails: Array<{
to: string;
subject: string;
message: string;
}> = [];
async notify(
subject: string,
message: string,
to: User,
type: string,
): Promise<void> {
this.emails.push({
to: to.email,
subject,
message,
});
}
getEmails() {
return this.emails;
}
clear() {
this.emails = [];
}
}
Email Tests
describe('EmailNotifier', () => {
let emailNotifier: MockEmailNotifier;
let user: User;
beforeEach(() => {
emailNotifier = new MockEmailNotifier();
user = UserFactory.create({ email: 'test@example.com' });
});
it('should send welcome email', async () => {
await emailNotifier.sendWelcomeEmail(user);
const emails = emailNotifier.getEmails();
expect(emails).toHaveLength(1);
expect(emails[0].to).toBe('test@example.com');
expect(emails[0].subject).toContain('Welcome');
});
it('should send order confirmation', async () => {
const order = OrderFactory.create({ userId: user.id });
await emailNotifier.sendOrderConfirmation(order, user);
const emails = emailNotifier.getEmails();
expect(emails[0].subject).toContain('Order Confirmation');
expect(emails[0].message).toContain(order.id);
});
});
Email Preview (Development)
Use Ethereal Email for testing:
// Development only
if (process.env.NODE_ENV === 'development') {
const testAccount = await nodemailer.createTestAccount();
this.transporter = nodemailer.createTransport({
host: 'smtp.ethereal.email',
port: 587,
secure: false,
auth: {
user: testAccount.user,
pass: testAccount.pass,
},
});
// Get preview URL
this.transporter.sendMail(mailOptions, (error, info) => {
console.log('Preview URL: %s', nodemailer.getTestMessageUrl(info));
});
}
Summary
The email system provides:
- Nodemailer integration for reliable email delivery
- Multiple provider support (Gmail, SendGrid, SES, etc.)
- Notification tracking in database
- Template system for consistent emails
- Connection pooling for performance
- Error handling with retry logic
- Cache integration for unseen counts
This comprehensive email infrastructure supports all transactional and notification needs of the application.