Skip to content

server-mail

@mmm/server-mail

Shared NestJS mail module with selectable mail providers.

Installation

Terminal window
pnpm add @mmm/server-mail

Usage

Register the module once in your application:

import { Module } from '@nestjs/common';
import { MailModule } from '@mmm/server-mail';
@Module({
imports: [
MailModule.forRoot({
provider: 'smtp',
host: process.env.SMTP_HOST!,
port: Number(process.env.SMTP_PORT ?? 587),
secure: process.env.SMTP_SECURE === 'true',
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
fromEmail: 'noreply@example.com',
fromName: 'Example App',
}),
],
})
export class AppModule {}

Inject MailService where you want to send mail:

import { Injectable } from '@nestjs/common';
import { MailService } from '@mmm/server-mail';
@Injectable()
export class InviteService {
constructor(private readonly mailService: MailService) {}
async sendInvite(email: string, inviteUrl: string) {
await this.mailService.send({
to: email,
context: { inviteUrl },
template: ({ inviteUrl }) => ({
subject: 'Your invite',
html: `<p>Open your invite: <a href="${inviteUrl}">${inviteUrl}</a></p>`,
text: `Open your invite: ${inviteUrl}`,
}),
});
}
}

to accepts a single email address or an array of email addresses.

Providers

SMTP

Uses nodemailer internally.

MailModule.forRoot({
provider: 'smtp',
host: 'smtp.example.com',
port: 587,
secure: false,
user: 'smtp-user',
pass: 'smtp-password',
fromEmail: 'noreply@example.com',
fromName: 'Example App',
});

user and pass are optional. If both are omitted, SMTP auth is disabled.

Mailjet

MailModule.forRoot({
provider: 'mailjet',
apiKey: process.env.MAILJET_API_KEY!,
apiSecret: process.env.MAILJET_API_SECRET!,
fromEmail: 'noreply@example.com',
fromName: 'Example App',
});

Local Development

The local provider logs rendered mail payloads to the console and does not send anything.

MailModule.forRoot({
provider: 'lokaldev',
fromEmail: 'dev@example.com',
});

Async Configuration

Use forRootAsync if mail options need to be loaded from another service:

MailModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
provider: 'smtp',
host: configService.getOrThrow('SMTP_HOST'),
port: Number(configService.get('SMTP_PORT') ?? 587),
secure: configService.get('SMTP_SECURE') === 'true',
user: configService.get('SMTP_USER'),
pass: configService.get('SMTP_PASS'),
fromEmail: configService.getOrThrow('MAIL_FROM_EMAIL'),
fromName: configService.get('MAIL_FROM_NAME'),
}),
});

Mail Templates

Templates are functions that receive a context object and return the rendered mail content.

type WelcomeContext = {
name: string;
};
const welcomeTemplate = ({ name }: WelcomeContext) => ({
subject: `Welcome, ${name}`,
html: `<h1>Welcome, ${name}</h1>`,
text: `Welcome, ${name}`,
});

Block Mail Template

The package includes a block-based template helper for branded emails. Every block is optional and can be filled from application code, CMS data, or any other source:

  • logo
  • hero
  • headline
  • content
  • footer
import { createBlockMailTemplate } from '@mmm/server-mail';
type InviteContext = {
firstName: string;
inviteUrl: string;
cms: {
logoUrl?: string;
heroUrl?: string;
headline?: string;
introHtml: string;
footerHtml?: string;
};
};
export const inviteTemplate = createBlockMailTemplate<InviteContext>({
subject: () => 'ServicePortal - Einladung',
previewText: ({ cms }) => cms.headline,
blocks: ({ cms, inviteUrl }) => ({
logo: cms.logoUrl
? {
src: cms.logoUrl,
alt: 'Logo',
width: 180,
}
: undefined,
hero: cms.heroUrl
? {
src: cms.heroUrl,
alt: '',
}
: undefined,
headline: cms.headline ?? 'Ihre Email',
content: {
html: `
${cms.introHtml}
<p style="text-align:center;margin:32px 0 12px;">
<a href="${inviteUrl}" style="display:inline-block;background:#006eb7;color:#ffffff;padding:14px 42px;border-radius:28px;text-decoration:none;font-weight:700;">
Hier klicken
</a>
</p>
<p style="text-align:center;color:#9aa0a6;">${inviteUrl}</p>
`,
text: `${cms.headline}\n\n${inviteUrl}`,
},
footer: cms.footerHtml
? {
html: cms.footerHtml,
links: [
{ label: 'Impressum', href: 'https://example.com/impressum' },
{ label: 'Datenschutz', href: 'https://example.com/datenschutz' },
],
}
: undefined,
}),
theme: {
accentColor: '#07376f',
footerBackgroundColor: '#07376f',
maxWidth: 720,
},
});

Use it like any other MailTemplate:

await this.mailService.send({
to: 'kunde@example.com',
template: inviteTemplate,
context: {
firstName: 'Steffen',
inviteUrl: 'https://example.com/invite/token',
cms: {
logoUrl: 'https://example.com/logo.png',
heroUrl: 'https://example.com/hero.jpg',
headline: 'Ihre Email',
introHtml: '<p>Hiermit erhalten Sie Ihren persoenlichen Zugang.</p>',
footerHtml: '<p>Automatische E-Mail des ServicePortals</p>',
},
},
});

If the default layout is too restrictive, keep using plain templates or override individual block rendering:

createBlockMailTemplate({
subject: 'Custom email',
blocks: {
headline: 'Custom headline',
content: '<p>Custom content</p>',
},
renderBlock: (blockName, block, ctx, defaults) => {
if (blockName === 'headline') {
return `<h1 style="font-size:40px;color:#111827;">${block}</h1>`;
}
return defaults[blockName](block as never);
},
});