server-mail
@mmm/server-mail
Shared NestJS mail module with selectable mail providers.
Installation
pnpm add @mmm/server-mailUsage
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:
logoheroheadlinecontentfooter
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); },});