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)
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
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
| Field | Type | Default | Description |
|---|---|---|---|
from | string | — | Default sender address |
replyTo | string | — | Default 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.
MailNodemailerModule.forRoot({
host: 'smtp.example.com',
port: 587,
secure: false,
auth: { user: 'app@example.com', pass: 'secret' },
});| Field | Type | Default | Description |
|---|---|---|---|
host | string | — | SMTP server hostname |
port | number | 587 | SMTP port |
secure | boolean | false | Use 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().
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
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:
@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
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
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:
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.
import { MailQueueModule, MAIL_SEND_TOPIC } from '@modularityjs/mail-queue';Web process — enqueues mail
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
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:
@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 {}