Skip to content

Mail Component Reference

Every binding key, configuration variant, interface, and internal mechanism of MailComponent. For the task-oriented walkthrough, see Usage & Examples.

Files:

Quick reference

ItemValue
Package@venizia/ignis
Subpath@venizia/ignis/mail
Component classMailComponent
RuntimesBoth (Bun, Node.js)
ComponentPurpose
MailComponentRegisters mail services, transporters, generators, and the queue executor
MailServicesend(), sendBatch(), sendTemplate(), verify()
TemplateEngineService{{variable}} substitution engine, in-memory template registry
NodemailerTransportHelperSMTP transport via nodemailer
MailgunTransportHelperMailgun HTTP API transport via mailgun.js
DirectMailExecutorHelperRuns the processor immediately, no queue
InternalQueueMailExecutorHelperIn-memory queue (SequentialQueueHelper)
BullMQMailExecutorHelperRedis-backed queue, distributed workers
MailTransportProviderFactory: TMailOptions -> IMailTransport
MailQueueExecutorProviderFactory: IMailQueueExecutorConfig -> IMailQueueExecutor
NumericCodeGeneratorCryptographically random numeric codes
RandomTokenGeneratorCryptographically random base64url tokens
DefaultVerificationDataGeneratorComposes both generators into an IVerificationData

Import paths

typescript
import {
  MailComponent,
  MailKeys,
  MailProviders,
  MailErrorCodes,
  MailDefaults,
  MailExecutorErrors,
  MailQueueExecutorTypes,
  BullMQExecutorModes,
  MailService,
  TemplateEngineService,
  NumericCodeGenerator,
  RandomTokenGenerator,
  DefaultVerificationDataGenerator,
  MailTransportProvider,
  MailQueueExecutorProvider,
} from '@venizia/ignis/mail';

import type {
  TMailOptions,
  IBaseMailOptions,
  INodemailerMailOptions,
  IMailgunMailOptions,
  ICustomMailOptions,
  IGenericMailOptions,
  IMailService,
  IMailTemplateEngine,
  IMailMessage,
  IMailSendResult,
  IMailTransport,
  IMailAttachment,
  IMailQueueExecutor,
  IMailQueueExecutorConfig,
  IMailQueueOptions,
  IMailQueueResult,
  IMailProcessorResult,
  ITemplate,
  IVerificationCodeGenerator,
  IVerificationTokenGenerator,
  IVerificationDataGenerator,
  IVerificationData,
  IVerificationGenerationOptions,
  TMailProvider,
  TNodemailerConfig,
  TMailgunConfig,
} from '@venizia/ignis/mail';

Architecture

  ┌─────────────────────────────────────────────────┐
  │               Your Application                   │
  │                                                   │
  │  preConfigure()                                   │
  │    ├── binds MailKeys.MAIL_OPTIONS  (required)   │
  │    ├── binds MailKeys.MAIL_QUEUE_EXECUTOR_CONFIG │
  │    │     (optional -- defaults to `direct`)       │
  │    └── registers MailComponent                    │
  └───────────────────────┬─────────────────────────┘


  ┌─────────────────────────────────────────────────┐
  │               MailComponent.binding()             │
  │                                                   │
  │    ├── initGenerators()   (transient bindings)    │
  │    │     ├── NumericCodeGenerator                 │
  │    │     ├── RandomTokenGenerator                 │
  │    │     └── DefaultVerificationDataGenerator     │
  │    │                                               │
  │    ├── initProviders()    (singleton bindings)     │
  │    │     ├── MailTransportProvider                 │
  │    │     └── MailQueueExecutorProvider              │
  │    │                                               │
  │    ├── initServices()     (singleton bindings)     │
  │    │     ├── MailService                            │
  │    │     └── TemplateEngineService                  │
  │    │                                               │
  │    └── createAndBindInstances()                   │
  │          ├── Transport Instance  ◄── MAIL_OPTIONS  │
  │          └── Queue Executor      ◄── QUEUE_CONFIG  │
  │                                    (or the direct   │
  │                                     default)        │
  └─────────────────────────────────────────────────┘

MailService and the queue executor are independent consumers of the transport/config -- IMailQueueExecutor never calls MailService. See How it works on the Overview for that distinction.

Source: component.ts

Binding keys

KeyConstantTypeRequiredDefault
@app/components/mail/optionsMailKeys.MAIL_OPTIONSTMailOptionsYes--
@app/components/mail/queue/executor-configMailKeys.MAIL_QUEUE_EXECUTOR_CONFIGIMailQueueExecutorConfigNo{ type: MailQueueExecutorTypes.DIRECT }
@app/components/mail/serviceMailKeys.MAIL_SERVICEIMailServiceNoMailService (singleton)
@app/components/mail/services/template-engineMailKeys.MAIL_TEMPLATE_ENGINEIMailTemplateEngineNoTemplateEngineService (singleton)
@app/components/mail/transport-providerMailKeys.MAIL_TRANSPORT_PROVIDERTGetMailTransportFnNoMailTransportProvider (singleton)
@app/components/mail/transport-instanceMailKeys.MAIL_TRANSPORT_INSTANCEIMailTransportNoCreated by the component
@app/components/mail/queue-executor-providerMailKeys.MAIL_QUEUE_EXECUTOR_PROVIDERTGetMailQueueExecutorFnNoMailQueueExecutorProvider (singleton)
@app/components/mail/queue-executor-instanceMailKeys.MAIL_QUEUE_EXECUTOR_INSTANCEIMailQueueExecutorNoCreated by the component
@app/components/mail/verification/code-generatorMailKeys.MAIL_VERIFICATION_CODE_GENERATORIVerificationCodeGeneratorNoNumericCodeGenerator (transient)
@app/components/mail/verification/token-generatorMailKeys.MAIL_VERIFICATION_TOKEN_GENERATORIVerificationTokenGeneratorNoRandomTokenGenerator (transient)
@app/components/mail/verification/data-generatorMailKeys.MAIL_VERIFICATION_DATA_GENERATORIVerificationDataGeneratorNoDefaultVerificationDataGenerator (transient)

IMPORTANT

MailKeys.MAIL_OPTIONS is the only binding MailComponent requires. It throws Mail options not configured in binding() if the key is not bound. MailKeys.MAIL_QUEUE_EXECUTOR_CONFIG is read with isOptional: true -- when it is not bound, createAndBindInstances() falls back to { type: MailQueueExecutorTypes.DIRECT } rather than failing startup.

Source: common/keys.ts

Configuration

Transport options (TMailOptions)

A discriminated union on provider, extending IBaseMailOptions:

typescript
interface IBaseMailOptions {
  from?: string;
  fromName?: string;
}

interface INodemailerMailOptions extends IBaseMailOptions {
  provider: 'nodemailer';
  config: TNodemailerConfig; // SMTPTransport | SMTPTransport.Options | string
}

interface IMailgunMailOptions extends IBaseMailOptions {
  provider: 'mailgun';
  config: TMailgunConfig; // AnyType & { domain: string } -- also requires username, key at runtime
}

interface ICustomMailOptions extends IBaseMailOptions {
  provider: 'custom';
  config: IMailTransport; // Must implement send() and verify()
}

interface IGenericMailOptions extends IBaseMailOptions {
  provider: string;
  config: Record<string, AnyType>;
}

type TMailOptions =
  | INodemailerMailOptions
  | IMailgunMailOptions
  | ICustomMailOptions
  | IGenericMailOptions;

Nodemailer (SMTP with basic auth):

typescript
{
  provider: MailProviders.NODEMAILER,
  from: 'noreply@example.com',
  fromName: 'Example App',
  config: {
    host: 'smtp.gmail.com',
    port: 465,
    secure: true,
    auth: { user: 'your-email@gmail.com', pass: 'your-app-password' },
  },
}

Nodemailer (OAuth2):

typescript
{
  provider: MailProviders.NODEMAILER,
  from: applicationEnvironment.get<string>('APP_ENV_MAIL_FROM') ?? 'noreply@example.com',
  fromName: applicationEnvironment.get<string>('APP_ENV_MAIL_FROM_NAME') ?? 'Example App',
  config: {
    host: applicationEnvironment.get<string>('APP_ENV_MAIL_HOST') ?? 'smtp.gmail.com',
    port: +(applicationEnvironment.get<number>('APP_ENV_MAIL_PORT') ?? 465),
    secure: toBoolean(applicationEnvironment.get<boolean>('APP_ENV_MAIL_SECURE') ?? true),
    auth: {
      type: 'oauth2',
      user: applicationEnvironment.get<string>('APP_ENV_MAIL_USER'),
      clientId: applicationEnvironment.get<string>('APP_ENV_MAIL_CLIENT_ID'),
      clientSecret: applicationEnvironment.get<string>('APP_ENV_MAIL_CLIENT_SECRET'),
      refreshToken: applicationEnvironment.get<string>('APP_ENV_MAIL_REFRESH_TOKEN'),
    },
  },
}

APP_ENV_MAIL_* is an app-level convention, not a framework-defined env var -- applicationEnvironment.get() reads whatever variables your wrapper component chooses to look up. A matching .env:

APP_ENV_MAIL_HOST=smtp.gmail.com
APP_ENV_MAIL_PORT=465
APP_ENV_MAIL_SECURE=true
APP_ENV_MAIL_USER=your-email@gmail.com
APP_ENV_MAIL_CLIENT_ID=your-oauth2-client-id
APP_ENV_MAIL_CLIENT_SECRET=your-oauth2-client-secret
APP_ENV_MAIL_REFRESH_TOKEN=your-oauth2-refresh-token

TIP

For Gmail OAuth2, follow Google's OAuth2 setup guide to obtain the client ID, secret, and refresh token.

Mailgun:

typescript
{
  provider: MailProviders.MAILGUN,
  from: 'noreply@example.com',
  fromName: 'Example App',
  config: {
    username: 'api',                        // required -- mailgun.js client username
    key: process.env.MAILGUN_API_KEY,        // required -- Mailgun API key
    domain: 'mg.example.com',                // required
    host: 'api.eu.mailgun.net',              // optional -- EU region
  },
}

IMPORTANT

MailgunTransportHelper validates username, key, and domain on construction and throws Invalid Mailgun configuration | Missing required keys: <keys> if any is missing -- this happens even though TMailgunConfig's only typed requirement is domain. username and key are checked at runtime, not by the type.

Custom transport:

typescript
{
  provider: MailProviders.CUSTOM,
  from: 'noreply@example.com',
  config: myTransportImplementingIMailTransport, // must have send() and verify()
}

Generic provider (extensibility escape hatch):

typescript
{
  provider: 'sendgrid',
  from: 'noreply@example.com',
  config: { apiKey: process.env.SENDGRID_API_KEY },
}

WARNING

IGenericMailOptions falls through to the default case in MailTransportProvider and throws Unsupported mail provider: <provider> -- it exists only for you to bind a custom MailTransportProvider that recognizes the provider string. It is not handled by the framework's built-in provider.

Queue executor options (IMailQueueExecutorConfig)

typescript
interface IMailQueueExecutorConfig {
  type: TConstValue<typeof MailQueueExecutorTypes>; // 'direct' | 'internal-queue' | 'bullmq'
  internalQueue?: {
    identifier: string;
  };
  bullmq?: {
    redis: IRedisSingleHelperOptions;
    queue: { identifier: string; name: string };
    mode: TConstValue<typeof BullMQExecutorModes>; // REQUIRED -- no default
  };
}
typescript
// Direct (also the implicit default when MAIL_QUEUE_EXECUTOR_CONFIG is unbound)
{ type: 'direct' }

// Internal queue (in-memory)
{ type: 'internal-queue', internalQueue: { identifier: 'mail-internal-queue' } }

// BullMQ (Redis-backed)
{
  type: 'bullmq',
  bullmq: {
    redis: { host: 'localhost', port: 6379, password: 'your-redis-password' },
    queue: { identifier: 'mail-queue', name: 'mail-queue' },
    mode: 'both', // 'queue-only' | 'worker-only' | 'both' -- required, no default
  },
}

NOTE

Choosing an executor: direct for development or low-volume apps (no queueing overhead); internal-queue for single-instance apps with moderate volume (in-memory, with retry); bullmq for distributed or high-volume systems (Redis-backed, configurable concurrency/priority/backoff).

Source: common/types.ts

Constants

ConstantValueDescription
MailDefaults.BATCH_CONCURRENCY5Default concurrent sends in sendBatch()
MailDefaults.FALLBACK_FROM'noreply@example.com'Used by getDefaultFrom() when options.from is unset
MailExecutorErrors.PROCESSOR_NOT_SET'Processor not set. Call setProcessor() first.'Thrown by all three queue executors
MailQueueExecutorTypes.DIRECT'direct'Immediate execution
MailQueueExecutorTypes.INTERNAL_QUEUE'internal-queue'In-memory queue
MailQueueExecutorTypes.BULLMQ'bullmq'Redis-backed queue
BullMQExecutorModes.QUEUE_ONLY'queue-only'Producer only (enqueue)
BullMQExecutorModes.WORKER_ONLY'worker-only'Consumer only (process)
BullMQExecutorModes.BOTH'both'Full duplex (produce + consume)

MailQueueExecutorTypes and BullMQExecutorModes both carry a SCHEME_SET/MODE_SET and a static isValid():

typescript
MailQueueExecutorTypes.isValid('bullmq');  // true
MailQueueExecutorTypes.isValid('unknown'); // false
BullMQExecutorModes.isValid('both');       // true
BullMQExecutorModes.isValid('invalid');    // false

MailErrorCodes

Built through MessageCode.build(), so every value is lower-case (ApplicationError lower-cases whatever it is handed):

ConstantValueDescription
MailErrorCodes.INVALID_CONFIGURATION'core.mail.invalid_configuration'Invalid or missing configuration (transport, template engine, subject, body)
MailErrorCodes.SEND_FAILED'core.mail.send_failed'Single email send failed
MailErrorCodes.VERIFICATION_FAILED'core.mail.verification_failed'Transport connection verification failed
MailErrorCodes.INVALID_RECIPIENT'core.mail.invalid_recipient'Missing or empty recipient address
MailErrorCodes.BATCH_SEND_FAILED'core.mail.batch_send_failed'Batch email operation failed
MailErrorCodes.TEMPLATE_NOT_FOUND'core.mail.template_not_found'Template name not found in registry

Source: common/constants.ts

IMailService interface

typescript
interface IMailService {
  send(message: IMailMessage): Promise<IMailSendResult>;

  sendBatch(
    messages: IMailMessage[],
    options?: { concurrency?: number },
  ): Promise<IMailSendResult[]>;

  sendTemplate(opts: {
    templateName: string;
    data: Record<string, any>;
    recipients: string | string[];
    options?: Partial<IMailMessage>;
  }): Promise<IMailSendResult>;

  verify(): Promise<boolean>;
}

send(message)

  1. validateMessage() throws for missing to, missing subject, or missing both text/html (400, see below).
  2. Merges message.from with getDefaultFrom() if unset.
  3. Delegates to transport.send().
  4. Returns { success, messageId, error? }.
  5. If the transport throws and the error is not already an ApplicationError, it is caught and re-thrown as MailErrorCodes.SEND_FAILED (500). An ApplicationError thrown earlier in the same try block (e.g. from validateMessage()) is re-thrown as-is, unchanged.

sendBatch(messages, options?)

Sends every message via send() with concurrency bounded by executePromiseWithLimit(), default MailDefaults.BATCH_CONCURRENCY (5). A send() that throws is caught per-message and downgraded to { success: false, error }; a failure in the batch operation itself throws MailErrorCodes.BATCH_SEND_FAILED.

sendTemplate(opts)

Renders a registered template and sends it. Subject resolution order: options.subject -> the template's own subject (rendered through the same engine) -> 'No Subject'. Throws MailErrorCodes.INVALID_CONFIGURATION ("Template engine not configured") if templateEngine was not injected. Re-throws any other error unchanged, including TEMPLATE_NOT_FOUND from the template engine.

verify()

Delegates to transport.verify(). If the transport throws, wraps it as MailErrorCodes.VERIFICATION_FAILED (500).

Protected methods (MailService)

validateMessage(message)

CheckError codeStatusMessage
to is falsy or an empty arrayINVALID_RECIPIENT400Recipient email address is required
subject is falsyINVALID_CONFIGURATION400Email subject is required
Both text and html are falsyINVALID_CONFIGURATION400Email must have either text or html content

getDefaultFrom()

  • If options.from is unset, returns MailDefaults.FALLBACK_FROM ('noreply@example.com').
  • Else if options.fromName is unset, returns options.from as-is.
  • Else returns "${fromName}" <${from}>.

Source: services/mail.service.ts

IMailMessage interface

typescript
interface IMailMessage {
  from?: string;                  // Uses getDefaultFrom() if not provided
  to: string | string[];
  cc?: string | string[];
  bcc?: string | string[];
  replyTo?: string;
  subject: string;
  text?: string;
  html?: string;
  attachments?: IMailAttachment[];
  headers?: Record<string, string>;
  requireValidate?: boolean;      // Passed through to sendTemplate()'s render() call
  [key: string]: any;             // Open-ended -- provider-specific fields pass through
}
FieldNotes
fromFalls back to getDefaultFrom(). Can be overridden per message (multi-tenant scenarios).
to, cc, bccSingle string or array. Nodemailer joins arrays with ', '; Mailgun passes to as-is (always coerced to an array), cc/bcc pass through unmodified.
replyToNodemailer passes it directly; Mailgun maps it to h:Reply-To.
subjectRendered through the template engine when set via sendTemplate().
text, htmlAt least one is required (validateMessage()).
attachmentsSee IMailAttachment below.
headersNodemailer passes them directly; Mailgun prefixes every key with h:.
requireValidatetrue makes template rendering throw on missing placeholders instead of preserving them as literal text. Defaults to false.
typescript
interface IMailAttachment {
  filename?: string;
  contentType?: string;
  path?: string;
  content?: string | Buffer | Readable;
  cid?: string;
  [key: string]: any;
}
  • File path: { filename: 'doc.pdf', path: '/path/to/doc.pdf' }
  • Buffer: { filename: 'data.txt', content: Buffer.from('...') }
  • Inline image: { filename: 'logo.png', path: '...', cid: 'logo' }
  • Mailgun maps each attachment to { filename, data: att.path ?? att.content ?? Buffer.from('') }.

Source: common/types.ts

IMailTemplateEngine interface

typescript
interface IMailTemplateEngine {
  render(opts: {
    templateData?: string;
    templateName?: string;
    data: Record<string, any>;
    requireValidate?: boolean;
  }): string;

  registerTemplate(opts: { name: string; content: string }): void;

  validateTemplateData(opts: { template: string; data: Record<string, any> }): {
    isValid: boolean;
    missingKeys: string[];
    allKeys: string[];
  };

  getTemplate(name: string): ITemplate | undefined;
  listTemplates(): ITemplate[];
  hasTemplate(name: string): boolean;
  removeTemplate(name: string): boolean;
}

TemplateEngineService also exposes clearTemplates(): void -- it resets the registry but is not part of the IMailTemplateEngine interface, so it is only reachable when you hold the concrete class, not the interface type.

MethodBehavior
render(opts)Renders by templateName (registry lookup) or raw templateData. Throws if neither is given. Throws TEMPLATE_NOT_FOUND (404) if templateName is not registered. Delegates to renderSimpleTemplate().
registerTemplate(opts)options (on the class) accepts subject/description (Partial<ITemplate>). Overwrites an existing template with the same name.
validateTemplateData(opts)Extracts every unique {{key}} via /\{\{(\s*[\w.]+\s*)\}\}/g, deduplicates, resolves nested dot-notation values. Returns isValid, missingKeys, allKeys.
getTemplate(name)Returns undefined if not registered.
listTemplates()All templates, as an array of ITemplate.
hasTemplate(name)Registry membership check.
removeTemplate(name)Logs the removal, returns true/false.
typescript
interface ITemplate {
  name: string;
  content?: string;
  render?: (data: Record<string, AnyType>) => string;
  subject?: string;
  description?: string;
}

The render field supports a custom per-template render function, though the built-in TemplateEngineService always uses content + renderSimpleTemplate() instead.

Source: services/template.service.ts

Transport layer

MailTransportProvider.value() returns a factory function that switches on TMailOptions.provider:

ProviderResult
'nodemailer'NodemailerTransportHelper, backed by nodemailer
'mailgun'MailgunTransportHelper, backed by the Mailgun HTTP API (mailgun.js)
'custom'The config value itself, validated to implement IMailTransport
Any other stringThrows Unsupported mail provider: <provider> (INVALID_CONFIGURATION, 500)

Type narrowing uses three private guards, each checking provider === X && 'config' in options:

typescript
private isNodemailerOptions(options: TMailOptions): options is INodemailerMailOptions
private isMailgunOptions(options: TMailOptions): options is IMailgunMailOptions
private isCustomOptions(options: TMailOptions): options is ICustomMailOptions

For custom, an additional isMailTransport() utility check reports specifically which of send/verify is missing.

typescript
interface IMailTransport {
  send(message: IMailMessage): Promise<IMailSendResult>;
  verify(): Promise<boolean>;
  close?(): Promise<void>;
}

Peer dependency loading

Neither transport calls a shared validateModule() helper. configure() calls require('nodemailer') / require('mailgun.js') directly -- if the package is not installed, Node's own Cannot find module '<name>' error propagates uncaught from the constructor, not a framework-formatted message.

Nodemailer (NodemailerTransportHelper, extends BaseHelper):

  • configure() builds the transporter via nodemailer.createTransport(config).
  • send() maps IMailMessage fields to Nodemailer's mail options, joining array recipients with ', '. Catches transport errors and returns { success: false, error } -- never throws.
  • verify() delegates to transporter.verify() (SMTP handshake). Catches errors and returns false -- never throws.
  • close() calls transporter.close().

Mailgun (MailgunTransportHelper, extends BaseHelper):

  • configure() calls validateConfig() before building the client: username, key, and domain must all be present in config, or it throws Invalid Mailgun configuration | Missing required keys: <keys> (INVALID_CONFIGURATION, 500). This check runs even though TMailgunConfig's type only requires domain.
  • send() converts IMailMessage to Mailgun's format: to is coerced to an array, replyTo becomes h:Reply-To, every custom header is prefixed h:. Attachments map to { filename, data: path ?? content ?? Buffer.from('') }. Catches send errors and returns { success: false, error } -- never throws.
  • verify() sends a test message to verify@<domain> with o:testmode: 'yes' (Mailgun has no dedicated verify endpoint). Catches errors and returns false -- never throws.
  • No close() -- the HTTP API is stateless.

Custom transport: set provider: MailProviders.CUSTOM and pass an object implementing IMailTransport as config. Useful for SendGrid, AWS SES, or a custom SMTP relay that the framework does not ship a helper for.

Source: helpers/transporters/

Queue executor implementations

All three implement:

typescript
interface IMailQueueExecutor {
  enqueueVerificationEmail(email: string, options?: IMailQueueOptions): Promise<IMailQueueResult>;
  setProcessor(processor: (email: string) => Promise<IMailProcessorResult>): void;
}

DirectMailExecutorHelper (extends BaseHelper) -- calls the processor immediately, no queue. Returns { queued: false, message: 'Email sent immediately (no queue)', result }. Throws MailExecutorErrors.PROCESSOR_NOT_SET if enqueueVerificationEmail() runs before setProcessor().

InternalQueueMailExecutorHelper (extends BaseHelper, constructor takes IInternalQueueMailExecutorOpts) -- wraps SequentialQueueHelper<IQueueJobPayload> from @venizia/ignis-helpers, autoDispatch: true.

  • Job IDs: job_<counter>_<timestamp>.
  • options.delay > 0 schedules the enqueue itself via setTimeout, tracked in an internal delayedJobs: Map<string, NodeJS.Timeout>.
  • Retry: on a thrown error or { success: false } from the processor, retries up to options.attempts ?? 3 times. calculateBackoff(): no backoff config -> 1000ms fixed; type: 'fixed' -> the raw delay; type: 'exponential' -> delay * 2 ** (attempt - 1).
  • close() clears every pending delayed/retry setTimeout -- without it, a live timer keeps the event loop open past shutdown.
  • No persistence across restarts.

BullMQMailExecutorHelper (extends BaseHelper, constructor takes IBullMQMailExecutorOpts) -- wraps BullMQHelper (@venizia/ignis-helpers/bullmq) and a RedisSingleHelper connection.

ModeQueue createdWorkers createdCan enqueueCan process
'queue-only'YesNo (setProcessor() skips worker creation, logs a warning)Yes -- processor not requiredNo
'worker-only'NoYesNo (throws)Yes
'both'YesYesYes -- processor requiredYes
  • setProcessor(processor, opts?) is async. It clears all existing workers, then creates opts?.numberOfWorkers ?? 1 workers, each with concurrencyPerWorker ?? 5 and lockDuration ?? 30000ms. In queue-only mode it stores the processor and returns without creating any worker.
  • enqueueVerificationEmail() throws Cannot enqueue jobs in worker-only mode... in worker-only mode; throws Queue helper not initialized... if the queue was never created (should not happen in a queue-enabled mode); throws PROCESSOR_NOT_SET only when mode !== 'queue-only' and no processor is set. Enqueue options: attempts: options?.attempts ?? 3, backoff: { type: options?.backoff?.type ?? 'exponential', delay: options?.backoff?.delay ?? 1000 }, removeOnComplete: true, removeOnFail: false.
  • Dynamic workers: addWorker(opts) (requires a processor, unique identifier, default concurrency 5, default lock duration 30000ms), removeWorker(index) (closes then splices, returns false if out of range), clearWorkers() (closes and empties, called internally by setProcessor()), getWorkerCount(), getMode().
  • close() tears down workers, then the queue, then the Redis connection -- every step runs even if an earlier one failed, and all failures are collected into one thrown error at the end.
typescript
interface IBullMQMailExecutorOpts {
  redis: IRedisSingleHelperOptions; // from @venizia/ignis-helpers
  queue: { identifier: string; name: string };
  mode: TConstValue<typeof BullMQExecutorModes>; // REQUIRED, no default
}

Source: helpers/executors/

Additional interfaces

typescript
interface IMailSendResult {
  success: boolean;
  messageId?: string;
  response?: any;
  error?: string;
}

interface IMailQueueOptions {
  priority?: number;
  delay?: number;
  attempts?: number;
  backoff?: { type: 'fixed' | 'exponential'; delay: number };
}

interface IMailQueueResult {
  jobId?: string;
  queued: boolean;   // false for direct execution, true for internal-queue and bullmq
  message: string;
  result?: IMailProcessorResult; // populated only for direct execution (synchronous processor)
}

interface IMailProcessorResult {
  success: boolean;
  message: string;
  expiresInMinutes: number;
  nextResendAt?: string;
}

interface IVerificationData {
  verificationCode: string;
  codeGeneratedAt: string;
  codeExpiresAt: string;
  codeAttempts: number;
  verificationToken: string;
  tokenGeneratedAt: string;
  tokenExpiresAt: string;
  lastCodeSentAt: string;
}

interface IVerificationGenerationOptions {
  codeLength: number;       // all four fields required, no defaults
  tokenBytes: number;
  codeExpiryMinutes: number;
  tokenExpiryHours: number;
}

interface IQueueJobPayload {
  id: string;
  email: string;
  options?: IMailQueueOptions;
  attempts: number;
  scheduledAt: number;
}

interface IInternalQueueMailExecutorOpts {
  identifier: string;
}

Source: common/types.ts

Utility functions

typescript
// type.utility.ts
function isMailTransport(value: AnyType): value is IMailTransport;
function isValidMailOptions(options: AnyType): options is TMailOptions;

// verification.utility.ts
function getExpiryTime(minutes: number): Date;         // Date `minutes` minutes from now
function getExpiryTimeInHours(hours: number): Date;     // Date `hours` hours from now
  • isMailTransport() checks that send and verify are functions, and that close is either a function or undefined.
  • isValidMailOptions() checks that provider is a string and config is truthy -- it does not validate the shape of config against the specific provider.

Source: utilities/

See also