@nestbolt/notifications
Mail Channel
Send email notifications using the MailMessage fluent builder API -- subjects, greetings, content lines, call-to-action buttons, attachments, and recipient control.
The mail channel sends email notifications via nodemailer. It automatically generates both HTML and plain-text versions of each email from a fluent builder API.
Prerequisites
Install nodemailer as a peer dependency:
npm install nodemailer
npm install -D @types/nodemailerConfigure the mail channel in your module setup:
NotificationModule.forRoot({
channels: {
database: true,
mail: {
transport: {
host: "smtp.example.com",
port: 587,
secure: false,
auth: {
user: "user@example.com",
pass: "password",
},
},
defaults: {
from: "noreply@example.com",
},
},
},
});Recipient Resolution
When the mail channel sends a notification, it determines the recipient's email address in this order:
notifiable.routeNotificationFor("mail")-- if this method exists on the entity and returns a non-empty string, that value is used.notifiable.email-- falls back to theemailproperty on the entity.
If neither is available, the channel throws a NotificationFailedException.
@Entity("users")
@Notifiable()
export class User extends NotifiableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column()
email!: string;
@Column({ nullable: true })
notificationEmail?: string;
routeNotificationFor(channel: string): string | undefined {
if (channel === "mail") {
// Use a dedicated notification email if set, otherwise fall back to primary
return this.notificationEmail ?? this.email;
}
return undefined;
}
}The MailMessage Builder
The MailMessage class provides a fluent API for composing email content. Every method returns this, allowing you to chain calls.
subject(value: string)
Sets the email subject line:
new MailMessage().subject("Your order has shipped!");greeting(value: string)
Sets the opening greeting, rendered as an <h1> in HTML and a plain-text line:
new MailMessage().greeting("Hello John");line(value: string)
Adds a content line. Each line is rendered as a <p> tag in HTML and a separate paragraph in plain text. You can call line() multiple times to add multiple paragraphs:
new MailMessage()
.line("Your order has been confirmed.")
.line("We will notify you when it ships.")
.line("Expected delivery: 3-5 business days.");action(text: string, url: string)
Adds a call-to-action link. In HTML, it renders as an <a> tag. In plain text, it renders as "text: url":
new MailMessage().action("View Order", "https://example.com/orders/123");Only one action can be set per message. Calling action() again replaces the previous one.
salutation(value: string)
Sets the closing salutation, rendered as a <p> tag in HTML:
new MailMessage().salutation("Best regards");from(value: string)
Overrides the sender address for this specific notification. If not set, the defaults.from value from the module configuration is used:
new MailMessage().from("billing@example.com");replyTo(value: string)
Sets the Reply-To header:
new MailMessage().replyTo("support@example.com");cc(value: string | string[])
Adds one or more CC recipients. Can be called multiple times -- addresses accumulate:
new MailMessage()
.cc("manager@example.com")
.cc(["team-lead@example.com", "qa@example.com"]);bcc(value: string | string[])
Adds one or more BCC recipients. Can be called multiple times -- addresses accumulate:
new MailMessage()
.bcc("archive@example.com")
.bcc(["compliance@example.com"]);attach(attachment: MailAttachment)
Attaches a file to the email. Can be called multiple times to attach multiple files:
new MailMessage()
.attach({ filename: "invoice.pdf", path: "/tmp/invoices/INV-001.pdf" })
.attach({ filename: "terms.pdf", path: "/tmp/terms.pdf" });The MailAttachment interface:
interface MailAttachment {
filename: string;
path?: string;
content?: string | Buffer;
contentType?: string;
}| Property | Type | Description |
|---|---|---|
filename | string | The filename shown to the recipient. |
path | string | Path to the file on disk. |
content | string | Buffer | Inline file content (alternative to path). |
contentType | string | MIME type override (e.g., "application/pdf"). |
You can provide either path or content, but not both.
Getter Methods
The MailMessage class also provides getter methods for reading the composed values:
| Method | Return Type | Description |
|---|---|---|
getSubject() | string | The email subject. |
getGreeting() | string | The greeting text. |
getSalutation() | string | The salutation text. |
getLines() | string[] | All content lines. |
getAction() | MailAction | null | The action (text and url). |
getAttachments() | MailAttachment[] | All attachments. |
getFrom() | string | The from address. |
getReplyTo() | string | The reply-to address. |
getCc() | string[] | All CC addresses. |
getBcc() | string[] | All BCC addresses. |
The MailAction interface:
interface MailAction {
text: string;
url: string;
}HTML and Plain Text Output
The MailMessage automatically generates both HTML and plain-text versions of the email.
toHtml()
Returns the HTML version. The structure is:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"></head>
<body>
<h1>Hello John</h1>
<p>Your order has been confirmed.</p>
<p>We will notify you when it ships.</p>
<p><a href="https://example.com/orders/123">View Order</a></p>
<p>Best regards</p>
</body>
</html>All text content is HTML-escaped to prevent XSS. The characters &, <, >, ", and ' are automatically escaped.
toPlainText()
Returns the plain-text version:
Hello John
Your order has been confirmed.
We will notify you when it ships.
View Order: https://example.com/orders/123
Best regardsFull Examples
Welcome Email
export class WelcomeNotification extends Notification {
constructor(private user: { name: string }) {
super();
}
via(): string[] {
return ["database", "mail"];
}
toDatabase() {
return { message: `Welcome, ${this.user.name}!`, type: "welcome" };
}
toMail() {
return new MailMessage()
.subject("Welcome to Our Platform")
.greeting(`Hello ${this.user.name}`)
.line("Thank you for creating an account.")
.line("We are excited to have you on board.")
.action("Get Started", "https://example.com/dashboard")
.salutation("The Team");
}
}Password Reset Email
export class PasswordResetNotification extends Notification {
constructor(private resetUrl: string) {
super();
}
via(): string[] {
return ["mail"];
}
toMail() {
return new MailMessage()
.subject("Reset Your Password")
.greeting("Hello")
.line("You are receiving this email because we received a password reset request for your account.")
.action("Reset Password", this.resetUrl)
.line("This link will expire in 60 minutes.")
.line("If you did not request a password reset, no further action is required.")
.salutation("Regards");
}
}Invoice Email with Attachment
export class InvoiceNotification extends Notification {
constructor(
private invoice: {
id: string;
amount: number;
pdfPath: string;
customerName: string;
},
) {
super();
}
via(): string[] {
return ["database", "mail"];
}
toDatabase() {
return {
message: `Invoice #${this.invoice.id} generated.`,
invoiceId: this.invoice.id,
amount: this.invoice.amount,
};
}
toMail() {
return new MailMessage()
.subject(`Invoice #${this.invoice.id}`)
.from("billing@example.com")
.replyTo("billing-support@example.com")
.greeting(`Hello ${this.invoice.customerName}`)
.line(`Your invoice #${this.invoice.id} for $${this.invoice.amount} is attached.`)
.line("Payment is due within 30 days.")
.action("Pay Online", `https://example.com/invoices/${this.invoice.id}/pay`)
.salutation("Thank you for your business")
.attach({
filename: `invoice-${this.invoice.id}.pdf`,
path: this.invoice.pdfPath,
});
}
}Email with CC and BCC
export class ContractSignedNotification extends Notification {
constructor(
private contract: { id: string; title: string },
private signerName: string,
) {
super();
}
via(): string[] {
return ["mail"];
}
toMail() {
return new MailMessage()
.subject(`Contract signed: ${this.contract.title}`)
.greeting(`Hello ${this.signerName}`)
.line(`The contract "${this.contract.title}" has been signed successfully.`)
.line("All parties will receive a copy for their records.")
.action("View Contract", `https://example.com/contracts/${this.contract.id}`)
.cc("legal@example.com")
.cc(["manager@example.com", "hr@example.com"])
.bcc("archive@example.com")
.salutation("Best regards");
}
}Email with Inline Content Attachment
toMail() {
const csvContent = "Name,Email,Role\nJohn,john@example.com,Admin\n";
return new MailMessage()
.subject("User Report")
.greeting("Hello")
.line("Please find the user report attached.")
.salutation("Regards")
.attach({
filename: "report.csv",
content: csvContent,
contentType: "text/csv",
});
}