Skip to content

Email

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

MAIL_HOST=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USERNAME=apikey
MAIL_PASSWORD=your-sendgrid-api-key

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

// After successful registration
await emailNotifier.sendWelcomeEmail(user);

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.