Mailer
@voltage/mailer sends emails from NestJS using JSX templates rendered at send time. Email content is defined in injectable factory classes, making it easy to inject services for translation, URL generation, or data fetching. When CSS inlining is enabled, Tailwind utility classes are inlined directly into the HTML so they survive email client rendering.
Installation
Section titled “Installation”yarn add @voltage/mailer @voltage/jsx @voltage/logger @voltage/zod nodemailer zodBasic usage
Section titled “Basic usage”forRoot()
Section titled “forRoot()”import { MailerModule } from '@voltage/mailer';import { createTransport } from 'nodemailer';
@Module({ imports: [ MailerModule.forRoot({ transport: createTransport({ host: 'smtp.example.com', port: 587 }), defaultSender: 'noreply@example.com', juice: true, }), ],})export class AppModule {}forRootAsync()
Section titled “forRootAsync()”MailerModule.forRootAsync({ inject: [AppConfiguration], useFactory: (config: AppConfiguration) => ({ transport: createTransport(config.mailer.transport), defaultSender: config.mailer.defaultSender, juice: true, }),})forFeature()
Section titled “forFeature()”Register custom mail factory classes in the module that owns them:
@Module({ imports: [MailerModule.forFeature([WelcomeMail, PasswordResetMail])],})export class MailModule {}Defining a mail
Section titled “Defining a mail”Implement MailFactory<Params> and return a Mail object from create(). Mail is Nodemailer’s SendMailOptions with JSX allowed on any property:
import { Injectable } from '@nestjs/common';import { MailFactory, Mail } from '@voltage/mailer';import { Intl } from '@voltage/intl';
interface WelcomeMailParams { from: string; to: string; user: User;}
@Injectable()export class WelcomeMail implements MailFactory<WelcomeMailParams> { constructor(private readonly intl: Intl) {}
create(params: WelcomeMailParams): Mail { const { user } = params;
return { subject: this.intl.formatMessage(messages.subject, { name: user.firstName }), html: ( <EmailLayout> <h1>{this.intl.formatMessage(messages.heading)}</h1> <p>{this.intl.formatMessage(messages.body)}</p> </EmailLayout> ), text: this.intl.formatMessage(messages.body), }; }}The from and to fields are required on Params but handled by the mailer — to is set per recipient and from falls back to defaultSender when not provided.
Sending
Section titled “Sending”Inject Mailer and call send() with the factory class and an envelope:
import { Mailer } from '@voltage/mailer';
@Injectable()export class AuthService { constructor(private readonly mailer: Mailer) {}
async register(user: User) { this.mailer.send(WelcomeMail, { recipients: [user.email], params: { user }, }); }}send() iterates over recipients and calls factory.create() once per address. Errors are caught and logged per recipient — a failure for one address does not abort the rest. The call itself is non-blocking; you do not need to await it unless you want to surface errors.
To override the sender for a specific email, pass from in params:
this.mailer.send(WelcomeMail, { recipients: [user.email], params: { user, from: 'support@example.com' },});Dynamic envelopes
Section titled “Dynamic envelopes”When building the envelope itself requires DI — for example, fetching recipients from the database — implement MailEnvelopeFactory<T> and pass the class to send() instead of an inline object:
@Injectable()export class WeeklyDigestEnvelope implements MailEnvelopeFactory<WeeklyDigestMail> { constructor(private readonly query: QueryHelper) {}
async create(): Promise<MailEnvelope<WeeklyDigestMail>> { const users = await this.query.for(User).getMany( where(equals('newsletterOptIn', true)), );
return { recipients: users.map((u) => u.email), params: { users }, }; }}this.mailer.send(WeeklyDigestMail, WeeklyDigestEnvelope);The mailer resolves WeeklyDigestEnvelope from the DI container and calls create() before dispatching.
JSX templates
Section titled “JSX templates”JSX elements are detected automatically and rendered to HTML before the email is sent. Use injectable components for shared layouts. Without CSS inlining, use inline style attributes — the only styling method guaranteed to work across all email clients:
@Injectable()export class EmailLayout extends InjectableComponent { render(props: { children: Children }) { const { children } = props;
return ( <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;"> <div style="padding: 24px; border-bottom: 1px solid #e5e7eb;"> <img src="https://example.com/logo.png" alt="Logo" style="height: 32px;" /> </div> <div style="padding: 24px;">{children}</div> </div> ); }}CSS inlining
Section titled “CSS inlining”When juice: true is set, Tailwind utility classes are compiled and inlined as style attributes automatically. This lets you write templates with class names instead of inline styles:
@Injectable()export class EmailLayout extends InjectableComponent { constructor(private readonly intl: Intl) { super(); }
render(props: { children: Children }) { const { children } = props;
return ( <div class="max-w-lg mx-auto font-sans bg-white"> <div class="px-8 py-6 border-b border-gray-200"> <img src="https://example.com/logo.png" alt="Logo" class="h-8" /> </div> <div class="px-8 py-6">{children}</div> <div class="px-8 py-4 bg-gray-50 text-sm text-gray-500 text-center"> {this.intl.formatMessage(messages.footer)} </div> </div> ); }}@Injectable()export class WelcomeMail implements MailFactory<WelcomeMailParams> { create(params: WelcomeMailParams): Mail { const { user } = params;
return { subject: `Welcome, ${user.firstName}`, html: ( <EmailLayout> <h1 class="text-2xl font-bold text-gray-900 mb-4"> Hey {user.firstName}, welcome aboard! </h1> <p class="text-gray-700 mb-6"> Your account is ready. Click below to get started. </p> <a href={params.url} class="inline-block bg-blue-600 text-white font-semibold px-6 py-3 rounded" > Get started </a> </EmailLayout> ), text: `Hey ${user.firstName}, welcome aboard! Visit ${params.url} to get started.`, }; }}@voltage/tailwind is used for Tailwind compilation when registered. The mailer also ships mail-friendly CSS overrides applied on top of the compiled output.
SimpleMail
Section titled “SimpleMail”SimpleMail is a built-in factory for one-off emails that don’t need a dedicated class. Pass subject, html, and text directly as params:
this.mailer.send(SimpleMail, { recipients: ['user@example.com'], params: { subject: 'Hello', html: <p>This is a quick email.</p>, text: 'This is a quick email.', },});Configuration
Section titled “Configuration”interface MailerConfiguration { /** Nodemailer transport instance (created via nodemailer.createTransport). */ transport: Mail['transport']; /** Default from address used when params.from is not provided. */ defaultSender: string; /** * Whether to inline CSS into the HTML before sending. * Requires juice to be installed. When @voltage/tailwind is registered, * Tailwind is compiled first and its output is also inlined. * Defaults to false. */ juice?: boolean;}