Skip to content

Email Service & Adapters

Grant uses the Adapter Pattern for email delivery — a single IEmailAdapter interface with swappable backends. Switch providers by changing one environment variable.

Architecture

Adapters

AdapterUse CaseSetupDeliverability
ConsoleDevelopment — logs to stdoutNoneN/A
SMTPSelf-hosted, Gmail, Office 365Host/port/credentialsMedium
MailgunProduction at scaleAPI key + domainHigh
MailjetProduction, marketingAPI key + secretHigh
SESAWS environmentsAWS credentials + regionHigh

Interface

All adapters implement:

typescript
interface IEmailAdapter {
  send(message: EmailMessage): Promise<void>;
  close?(): Promise<void>;
}

interface EmailMessage {
  to: string | string[];
  subject: string;
  html: string;
  from?: string;
  cc?: string | string[];
  bcc?: string | string[];
  replyTo?: string;
  attachments?: Array<{ filename: string; content: Buffer | string; contentType?: string }>;
}

The EmailService wraps adapters with use-case methods (sendInvitation, sendOtp) that build HTML content and delegate to adapter.send(). Email sending is fire-and-forget — failures are logged but never thrown.

Configuration

VariableDefaultDescription
EMAIL_STRATEGYconsoleconsole, smtp, mailgun, mailjet, or ses
EMAIL_FROMnoreply@example.comDefault sender address
EMAIL_FROM_NAMEGrantSender display name

SMTP-specific:

VariableDescription
SMTP_HOSTSMTP server hostname
SMTP_PORTSMTP port (587 for TLS, 465 for SSL)
SMTP_SECUREtrue for port 465, false otherwise
SMTP_USERSMTP username
SMTP_PASSWORDSMTP password

Mailgun-specific:

VariableDescription
MAILGUN_API_KEYMailgun API key
MAILGUN_DOMAINSending domain (e.g., mg.yourdomain.com)

Mailjet-specific:

VariableDescription
MAILJET_API_KEYMailjet API key
MAILJET_API_SECRETMailjet API secret

Usage

Handlers access the email service through context:

typescript
// Send organization invitation (fire-and-forget)
this.services.email
  .sendInvitation({
    to: email,
    organizationName: organization.name,
    inviterName: inviter.name,
    invitationUrl: `${config.security.frontendUrl}/invitations/${token}`,
    roleName: role.name,
  })
  .catch((error) => logger.error({ err: error }, 'Failed to send invitation email'));

The email service is created once at startup via the EmailAdapterFactory and injected into the service layer.

Extending

To add a new provider, implement IEmailAdapter and register it in EmailAdapterFactory. See packages/@grantjs/email/src/ for the existing adapter implementations.


Related:

Released under the MIT License.