Skip to content

Mail Component

MailComponent wires a pluggable email transport (Nodemailer, Mailgun, or your own) into MailService, adding template rendering, batch sending, and an independent queue executor for verification-code/token flows.

In one example

Bind MailKeys.MAIL_OPTIONS, register MailComponent, then inject IMailService anywhere to send:

typescript
import { BaseApplication, ValueOrPromise } from '@venizia/ignis';
import { MailComponent, MailKeys, MailProviders } from '@venizia/ignis/mail';

export class Application extends BaseApplication {
  preConfigure(): ValueOrPromise<void> {
    // MAIL_OPTIONS is the only binding MailComponent requires
    this.bind({ key: MailKeys.MAIL_OPTIONS }).toValue({
      provider: MailProviders.NODEMAILER,
      from: 'noreply@example.com',
      config: {
        host: 'smtp.gmail.com',
        port: 465,
        secure: true,
        auth: { user: process.env.APP_ENV_MAIL_USER, pass: process.env.APP_ENV_MAIL_PASS },
      },
    });

    this.component(MailComponent);
  }
}
typescript
import { BaseService, inject } from '@venizia/ignis';
import { MailKeys, type IMailService } from '@venizia/ignis/mail';

export class UserService extends BaseService {
  constructor(@inject({ key: MailKeys.MAIL_SERVICE }) private mailService: IMailService) {
    super({ scope: UserService.name });
  }

  async sendWelcomeEmail(email: string) {
    return this.mailService.send({ to: email, subject: 'Welcome!', html: '<h1>Welcome!</h1>' });
  }
}

MailKeys.MAIL_QUEUE_EXECUTOR_CONFIG is optional -- omit it and MailComponent binds a direct executor (no queue) by default.

How it works

  • One required binding. MailComponent.binding() throws Mail options not configured if MailKeys.MAIL_OPTIONS is not bound before registration. Every other binding -- queue executor config, verification generators -- is optional with a working default.
  • Transport is a discriminated union. TMailOptions.provider selects NodemailerTransportHelper, MailgunTransportHelper, or a custom object you supply that implements IMailTransport (send() + verify()). MailTransportProvider is the factory that switches on it and throws for an unsupported provider string.
  • MailService is the one sending entry point. send(), sendBatch(), sendTemplate(), and verify() all validate first, then call the transport directly and synchronously, then normalize failures into MailErrorCodes. A validation error (400) passes through unchanged; only a throwing transport gets wrapped as SEND_FAILED (500) -- the built-in Nodemailer and Mailgun transports never throw, they return { success: false, error } instead.
  • The queue executor is a separate subsystem, not a mail queue. IMailQueueExecutor (direct / internal-queue / bullmq) only exposes enqueueVerificationEmail() and setProcessor() -- it never touches MailService. You must call setProcessor() with your own function (typically one that calls mailService.send() internally) before enqueueVerificationEmail() does anything.
  • Templates are a simple substitution engine. TemplateEngineService stores templates in an in-memory Map and replaces {{variable}} placeholders (dot-notation for nested values). A missing value is logged and left as the literal placeholder text, not blanked out.
  • Startup logging never leaks credentials. MailComponent logs only mailOptions.provider and queueExecutorConfig.type -- never the SMTP password, OAuth2 secret, API key, or Redis password nested inside them.

Common tasks

Send an email

Inject IMailService via MailKeys.MAIL_SERVICE and call send().

typescript
const result = await this.mailService.send({
  to: 'user@example.com',
  subject: 'Welcome!',
  html: '<h1>Welcome!</h1>',
  text: 'Welcome!',
});

Send a batch of emails

sendBatch() runs each message through send() with bounded concurrency (default 5).

typescript
const results = await this.mailService.sendBatch(messages, { concurrency: 5 });

Send a registered template

Register a template on IMailTemplateEngine, then send it through IMailService.

typescript
this.templateEngine.registerTemplate({
  name: 'welcome-email',
  content: '<h1>Welcome {{userName}}!</h1>',
  options: { subject: 'Welcome to {{appName}}' },
});

await this.mailService.sendTemplate({
  templateName: 'welcome-email',
  data: { userName: 'Jane', appName: 'My App' },
  recipients: 'user@example.com',
});

Switch to Mailgun

config must carry username, key, and domain -- the transport validates them eagerly, on construction.

typescript
{
  provider: MailProviders.MAILGUN,
  from: 'noreply@example.com',
  config: { username: 'api', key: process.env.MAILGUN_API_KEY, domain: 'mg.example.com' },
}

Queue verification emails

Get the queue executor instance, register a processor, then enqueue.

typescript
const executor = this.application.get<IMailQueueExecutor>({
  key: MailKeys.MAIL_QUEUE_EXECUTOR_INSTANCE,
});

executor.setProcessor(async email => {
  await this.mailService.send({ to: email, subject: 'Verify', html: '...' });
  return { success: true, message: 'Sent', expiresInMinutes: 10 };
});

await executor.enqueueVerificationEmail('user@example.com');

Generate a verification code and token

MAIL_VERIFICATION_DATA_GENERATOR composes a numeric code and a base64url token in one call.

typescript
const data = this.verificationGenerator.generateVerificationData({
  codeLength: 6,
  tokenBytes: 32,
  codeExpiryMinutes: 10,
  tokenExpiryHours: 24,
});

See also

Files: