Skip to content

Events ​

The mailer publishes an event at every step of a send (before, after, on error, on each retry and failover, when recipients are suppressed, and for queued jobs), so you can log deliveries, record metrics or alert on failures without wrapping MailerService. In v4 events are published three ways, and you can use any of them: an RxJS stream that needs no extra package, @nestjs/event-emitter listeners, and node:diagnostics_channel for APM tools.

Available events ​

MailerEventNameWhen
BEFORE_SENDmailer.before_sendBefore the first attempt
AFTER_SENDmailer.after_sendAfter a successful send
SEND_ERRORmailer.send_errorWhen the send fails for good (after retries and failover)
RETRYmailer.retryA transient failure is about to be retried
FAILOVERmailer.failoverThe transporter failed and the next one in failover is tried
SUPPRESSEDmailer.suppressedsuppress dropped one or more recipients
QUEUEDmailer.queuedAn email was added to the queue
QUEUE_COMPLETEDmailer.queue.completedA queued email was sent
QUEUE_FAILEDmailer.queue.failedA queued email failed

Payload ​

Every event carries a MailerEventPayload:

FieldEventsDescription
mailOptionsallThe mail options being sent
timestampallWhen the event happened
resultAFTER_SEND, QUEUE_COMPLETEDThe nodemailer result (messageId, accepted, …)
errorSEND_ERROR, RETRY, FAILOVER, QUEUE_FAILEDThe error. On SEND_ERROR it is the typed error the caller receives
attemptAFTER_SEND, SEND_ERROR, RETRYThe attempt that just finished, 1 for the first
permanentSEND_ERRORWhether retrying could not help (see what is retried)
transporterNameAFTER_SEND, SEND_ERROR, RETRY, FAILOVERThe named transporter involved (undefined for the default one). For FAILOVER, the one tried next
durationMsAFTER_SEND, SEND_ERRORHow long the send took, retries included
suppressedSUPPRESSEDThe addresses that were dropped

RxJS stream ​

MailerEventService.events$ emits every event as a MailerEventRecord (the payload plus its event name). It needs no extra package:

typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import { MailerEvent, MailerEventService } from '@nestjs-modules/mailer';
import { filter } from 'rxjs';

@Injectable()
export class MailMetrics implements OnModuleInit {
  constructor(private readonly mailerEvents: MailerEventService) {}

  onModuleInit() {
    this.mailerEvents.events$
      .pipe(filter((record) => record.event === MailerEvent.SEND_ERROR))
      .subscribe((record) => {
        console.error(
          `Email failed after ${record.attempt} attempt(s)`,
          record.permanent ? '(permanent)' : '(transient)',
          record.error?.message,
        );
      });
  }
}

@nestjs/event-emitter ​

Install the package and import EventEmitterModule; the mailer finds EventEmitter2 automatically:

bash
pnpm add @nestjs/event-emitter
typescript
import { Module } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { MailerModule } from '@nestjs-modules/mailer';

@Module({
  imports: [
    EventEmitterModule.forRoot(),
    MailerModule.forRoot({ transport: process.env.SMTP_URL }),
  ],
})
export class AppModule {}
typescript
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { MailerEvent, MailerEventPayload } from '@nestjs-modules/mailer';

@Injectable()
export class EmailEventListener {
  @OnEvent(MailerEvent.AFTER_SEND)
  handleSent(payload: MailerEventPayload) {
    console.log('Sent to', payload.mailOptions.to, 'in', payload.durationMs, 'ms');
    console.log('Message ID:', payload.result?.messageId);
  }

  @OnEvent(MailerEvent.RETRY)
  handleRetry(payload: MailerEventPayload) {
    console.warn(`Attempt ${payload.attempt} failed, retrying:`, payload.error?.message);
  }

  @OnEvent(MailerEvent.SEND_ERROR)
  handleError(payload: MailerEventPayload) {
    console.error('Email failed:', payload.error?.message);
  }
}

INFO

In v3 the EventEmitter2 instance was never actually injected, so @OnEvent() listeners did not fire. v4 fixes this: they now receive every event.

Another emitter ​

To publish to a different emitter, provide anything with an emit(event, payload) method under the MAILER_EVENT_EMITTER token, where the mailer module can see it. With forRootAsync(), use extraProviders:

typescript
import { Module } from '@nestjs/common';
import { EventEmitter } from 'node:events';
import { MAILER_EVENT_EMITTER, MailerModule } from '@nestjs-modules/mailer';

export const mailEvents = new EventEmitter();

@Module({
  imports: [
    MailerModule.forRootAsync({
      useFactory: () => ({ transport: process.env.SMTP_URL }),
      extraProviders: [{ provide: MAILER_EVENT_EMITTER, useValue: mailEvents }],
    }),
  ],
})
export class AppModule {}

diagnostics_channel ​

Each event is also published to the node:diagnostics_channel channel nestjs-modules:<event name> (for example nestjs-modules:mailer.after_send), with the same record as events$. APM and tracing tools can subscribe without touching your Nest app. mailerChannelName() returns the channel name of an event:

typescript
import { subscribe } from 'node:diagnostics_channel';
import { MailerEvent, MailerEventRecord, mailerChannelName } from '@nestjs-modules/mailer';

subscribe(mailerChannelName(MailerEvent.AFTER_SEND), (message) => {
  const record = message as MailerEventRecord;
  console.log('mail sent', record.durationMs);
});

Use cases ​

Delivery log ​

typescript
@OnEvent(MailerEvent.AFTER_SEND)
async logEmail(payload: MailerEventPayload) {
  await this.emailLogService.create({
    to: payload.mailOptions.to,
    subject: payload.mailOptions.subject,
    template: payload.mailOptions.template,
    sentAt: payload.timestamp,
    messageId: payload.result?.messageId,
    transporter: payload.transporterName ?? 'default',
  });
}

Metrics ​

typescript
@OnEvent(MailerEvent.AFTER_SEND)
trackSuccess(payload: MailerEventPayload) {
  this.metrics.histogram('emails.duration_ms', payload.durationMs);
}

@OnEvent(MailerEvent.SEND_ERROR)
trackFailure(payload: MailerEventPayload) {
  this.metrics.increment('emails.failed', { permanent: String(payload.permanent) });
}

TIP

Emitting never throws: a failing @nestjs/event-emitter listener is logged as a warning and does not fail the send. MailerEventService.isAvailable() tells you whether an emitter is wired; events$ and diagnostics channels work regardless.

Released under the MIT License.