Skip to content

Mail

Transactional email sending with pluggable transport backends. The contract provides a concrete MailService that applies config defaults (from, replyTo) and delegates to an abstract MailTransport sub-contract — the same pattern as webhook (WebhookService + WebhookDelivery).

Contract (@modularityjs/mail)

typescript
class MailService {
  send(message: MailMessage): Promise<MailSendResult>;
}

abstract class MailTransport {
  abstract send(message: MailMessage): Promise<MailSendResult>;
}

MailService is concrete — it composes config defaults and delegates to MailTransport. Drivers implement MailTransport.

Setup

typescript
import { MailModule } from '@modularityjs/mail';
import { MailNodemailerModule } from '@modularityjs/mail-nodemailer';

const modules = [
  MailModule.forRoot({ from: 'noreply@example.com' }),
  MailNodemailerModule.forRoot({
    host: 'smtp.example.com',
    port: 587,
    auth: { user: 'app@example.com', pass: 'secret' },
  }),
];

Config

FieldTypeDefaultDescription
fromstringDefault sender address
replyTostringDefault reply-to address

Per-message from and replyTo override the config defaults.

Drivers

Nodemailer (@modularityjs/mail-nodemailer)

SMTP and other transports via nodemailer. Covers SMTP, SES, sendmail, and other backends through nodemailer's transport system.

typescript
MailNodemailerModule.forRoot({
  host: 'smtp.example.com',
  port: 587,
  secure: false,
  auth: { user: 'app@example.com', pass: 'secret' },
});
FieldTypeDefaultDescription
hoststringSMTP server hostname
portnumber587SMTP port
securebooleanfalseUse TLS
auth{ user: string; pass: string }SMTP authentication

Memory (@modularityjs/mail-memory)

In-memory transport for testing. Stores sent messages in an array with getSent() and clear().

typescript
import {
  MailMemoryModule,
  MemoryMailTransport,
} from '@modularityjs/mail-memory';

const modules = [
  MailModule.forRoot({ from: 'test@example.com' }),
  MailMemoryModule,
];

// In tests — get the transport from the container
const transport = container.get(MemoryMailTransport);
expect(transport.getSent()).toHaveLength(1);
transport.clear();

Sending Mail

Plain text / HTML

typescript
await mailService.send({
  to: 'user@example.com',
  subject: 'Order confirmed',
  html: '<h1>Thank you for your order!</h1>',
  text: 'Thank you for your order!',
});

With template engine

Compose TemplateEngine.render() + MailService.send() — no extension package needed:

typescript
@Injectable()
class OrderMailer {
  constructor(
    @Inject(MailService) private readonly mail: MailService,
    @Inject(TemplateEngine) private readonly templates: TemplateEngine,
  ) {}

  async sendConfirmation(order: Order): Promise<void> {
    const html = await this.templates.render('order-confirmation', {
      orderNumber: order.number,
      total: order.total,
    });
    await this.mail.send({
      to: order.customerEmail,
      subject: `Order ${order.number} confirmed`,
      html,
    });
  }
}

Multiple recipients, CC, BCC

typescript
await mailService.send({
  to: ['alice@example.com', 'bob@example.com'],
  cc: { name: 'Manager', address: 'manager@example.com' },
  bcc: 'audit@example.com',
  subject: 'Team update',
  text: 'Weekly report attached.',
});

Attachments

typescript
await mailService.send({
  to: 'user@example.com',
  subject: 'Your invoice',
  html: '<p>Please find your invoice attached.</p>',
  attachments: [
    {
      filename: 'invoice.pdf',
      content: pdfBuffer,
      contentType: 'application/pdf',
    },
  ],
});

Named addresses

Recipients accept both strings and MailAddress objects:

typescript
await mailService.send({
  to: { name: 'Alice', address: 'alice@example.com' },
  from: { name: 'MyApp', address: 'noreply@myapp.com' },
  subject: 'Hello',
  text: 'Hi Alice!',
});

Queue Extension (@modularityjs/mail-queue)

Async mail delivery via the queue system. Publishes MailMessage to a queue topic instead of sending synchronously. Follows the same pattern as @modularityjs/webhook-queue.

typescript
import { MailQueueModule, MAIL_SEND_TOPIC } from '@modularityjs/mail-queue';

Web process — enqueues mail

typescript
const modules = [
  RedisModule.forRoot({ host: 'localhost', port: 6379 }),
  MailModule.forRoot({ from: 'noreply@example.com' }),
  MailQueueModule, // overrides MailTransport with queue publisher
  QueueModule,
  QueueRedisModule,
];

Worker process — sends via SMTP

typescript
const modules = [
  RedisModule.forRoot({ host: 'localhost', port: 6379 }),
  MailModule.forRoot({ from: 'noreply@example.com' }),
  MailNodemailerModule.forRoot({ host: 'smtp.example.com', ... }),
  QueueModule,
  QueueRedisModule,
  AppMailConsumerModule, // your @Consume({ topic: 'mail.send', name: 'mail-send-handler' }) handler
];

The consumer listens to MAIL_SEND_TOPIC ('mail.send') and sends via the real MailService (which resolves to nodemailer in the worker process).

Custom Transport

Implement MailTransport for any backend:

typescript
@Injectable()
class SesMailTransport extends MailTransport {
  async send(message: MailMessage): Promise<MailSendResult> {
    // Use AWS SES SDK directly
    const result = await ses.sendEmail({ ... });
    return { messageId: result.MessageId, accepted: [...], rejected: [] };
  }
}

@Module({
  name: 'mail-ses',
  imports: [MailModule],
  providers: [SesMailTransport],
  preferences: [{ provide: MailTransport, useClass: SesMailTransport }],
})
class MailSesModule {}