Sending Emails
Inject the Service
import { Injectable } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
@Injectable()
export class NotificationService {
constructor(private readonly mailerService: MailerService) {}
}sendMail() accepts the same fields as nodemailer email message.
Plain Text / HTML
Send a simple email without templates:
await this.mailerService.sendMail({
to: 'user@example.com',
from: 'noreply@example.com',
subject: 'Hello World',
text: 'Plain text content',
html: '<b>HTML content</b>',
});Context Placeholders
When context is set, {{key}} placeholders are replaced in the subject, in an inline html string (only when no template is used) and in a textTemplate file. Only the context's own properties are used; unknown keys are left as they are.
In html, {{key}} is HTML-escaped. Use {{{key}}} to insert markup you trust:
await this.mailerService.sendMail({
to: 'user@example.com',
subject: 'Welcome {{name}}',
html: '<p>Hi {{name}}</p>{{{signature}}}',
context: {
name: user.name, // escaped
signature: '<b>The Team</b>', // inserted as-is
},
});Changed in 3.0
Before 3.0, {{key}} was inserted into html without escaping. If you relied on that to inject markup, switch those placeholders to {{{key}}}.
Safe HTML with the html Tag
For HTML built in code, the html tagged template escapes every interpolated value, so user data can't inject markup. Arrays are joined, null, undefined and false render nothing, and nested html values (or trusted strings wrapped in unsafeHtml()) are kept as they are:
import { html, unsafeHtml } from '@nestjs-modules/mailer';
await this.mailerService.sendMail({
to: user.email,
subject: 'Your order',
html: html`
<p>Hi ${user.name},</p>
<ul>${order.items.map((item) => html`<li>${item.name} × ${item.quantity}</li>`)}</ul>
${unsafeHtml(footerFromOurOwnTemplate)}
`,
});html returns a SafeHtml value (check one with isSafeHtml()). sendMail(), send() and mail classes accept it as html, and also accept @nestjs/mail's own SafeHtml. Use unsafeHtml() only for markup you control, never for user input.
Plain-Text Part
An email with HTML but no text gets a plain-text part derived from the final HTML (after templates and plugins ran): paragraphs and line breaks kept, links written as text (url), list items as - item, styles and scripts dropped, entities decoded. Spam filters and text-only clients prefer emails that have one.
It is on by default; an explicit text (or textTemplate) is always kept, and raw messages are left alone. Turn it off with textFromHtml: false. The converter is exported as htmlToText(html).
With Templates
Use your configured template engine. The file extension (.hbs, .pug, .ejs) is appended automatically:
await this.mailerService.sendMail({
to: 'user@example.com',
subject: 'Welcome!',
template: 'welcome', // resolves to templates/welcome.hbs
context: { // data passed to the template
name: 'John Doe',
code: 'cf1a3f828287',
},
});With Absolute Path
You can use an absolute path to the template file:
await this.mailerService.sendMail({
to: 'user@example.com',
subject: 'Welcome!',
template: `${__dirname}/welcome`,
context: {
name: 'John Doe',
},
});TIP
When using absolute paths, make sure template files are copied to the dist folder at build time. See Copy Templates to dist.
Attachments
Send emails with file attachments:
await this.mailerService.sendMail({
to: 'user@example.com',
subject: 'Invoice',
html: '<p>Please find your invoice attached.</p>',
attachments: [
{
filename: 'invoice.pdf',
path: '/absolute/path/to/invoice.pdf',
},
],
});Multiple Attachments
await this.mailerService.sendMail({
to: 'user@example.com',
subject: 'Report',
template: 'report',
context: { title: 'Monthly Report' },
attachments: [
// File from disk
{ filename: 'report.pdf', path: '/path/to/report.pdf' },
// Buffer
{ filename: 'data.csv', content: Buffer.from('id,name\n1,John') },
// String content
{ filename: 'notes.txt', content: 'Some notes here' },
// URL (nodemailer will fetch it)
{ filename: 'logo.png', path: 'https://example.com/logo.png' },
],
});Inline Images (Embedded)
Embed images directly in the HTML using cid:
await this.mailerService.sendMail({
to: 'user@example.com',
subject: 'Newsletter',
html: '<img src="cid:logo" alt="Logo" />',
attachments: [
{
filename: 'logo.png',
path: '/path/to/logo.png',
cid: 'logo',
},
],
});See Nodemailer attachments docs for all options.
Custom Headers
await this.mailerService.sendMail({
to: 'user@example.com',
subject: 'Hello',
html: '<p>Hello!</p>',
headers: {
'X-Custom-Header': 'custom-value',
'X-Priority': '1',
},
});Reply-To and Threading
await this.mailerService.sendMail({
to: 'user@example.com',
replyTo: 'support@example.com',
subject: 'Re: Your ticket',
html: '<p>We received your request.</p>',
inReplyTo: '<original-message-id@example.com>',
references: ['<original-message-id@example.com>'],
});Using a Specific Transporter
When you have multiple transporters configured, specify which one to use:
await this.mailerService.sendMail({
transporterName: 'secondary',
to: 'user@example.com',
subject: 'Sent via secondary SMTP',
html: '<p>Hello!</p>',
});See Multiple Transporters for setup instructions.
Delivery Options
v4 adds per-message options that control how an email is delivered. They are not passed to nodemailer:
| Option | Description |
|---|---|
retry | Overrides the module's retries: a number of attempts, false, or options (Reliability) |
timeout | Per-attempt timeout in ms, overriding sendTimeout (Reliability) |
failover | Named transporters to try when the selected one fails, or false (Reliability) |
signal | An AbortSignal that cancels the send and the wait between retries (Reliability) |
idempotencyKey | Derives a stable Message-ID so a redelivery can be deduplicated (Reliability) |
unsubscribe | Adds one-click List-Unsubscribe headers (Delivery rules) |
Mail Classes
For emails you send from several places, a mail class keeps the subject, template and context in one typed provider:
await this.mailerService.send(WelcomeMail, { to: user.email, data: user });