NestboltNestbolt

@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/nodemailer

Configure 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:

  1. notifiable.routeNotificationFor("mail") -- if this method exists on the entity and returns a non-empty string, that value is used.
  2. notifiable.email -- falls back to the email property 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;
}
PropertyTypeDescription
filenamestringThe filename shown to the recipient.
pathstringPath to the file on disk.
contentstring | BufferInline file content (alternative to path).
contentTypestringMIME 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:

MethodReturn TypeDescription
getSubject()stringThe email subject.
getGreeting()stringThe greeting text.
getSalutation()stringThe salutation text.
getLines()string[]All content lines.
getAction()MailAction | nullThe action (text and url).
getAttachments()MailAttachment[]All attachments.
getFrom()stringThe from address.
getReplyTo()stringThe 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 regards

Full 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",
    });
}