Skip to main content

Mail Providers

The template implements a provider-abstracted email system using the Factory pattern. It supports Resend and Novu as production providers, with a mock provider for development and testing. The system gracefully degrades when no API keys are configured.

Architecture Overview

Source Files

FilePurpose
lib/mail/index.tsEmailService class, interfaces, exported helper functions
lib/mail/factory.tsEmailProviderFactory for provider instantiation
lib/mail/resend.tsResend API provider implementation
lib/mail/novu.tsNovu notification provider implementation
lib/mail/mock.tsConsole-logging mock provider
lib/mail/templates/HTML email templates for various events

Core Interfaces

EmailMessage

export interface EmailMessage {
from: string;
to: string | string[];
subject: string;
html: string;
text?: string;
}

EmailProvider

Every provider implements this interface:

export interface EmailProvider {
sendEmail(message: EmailMessage): Promise<any>;
getName(): string;
}

EmailServiceConfig

export interface EmailServiceConfig {
provider: string;
defaultFrom: string;
apiKeys: Record<string, string>;
domain: string;
novu?: EmailNovuConfig;
}

export interface EmailNovuConfig {
templateId?: string;
backendUrl?: string;
}

EmailProviderFactory

The factory creates provider instances based on configuration, with automatic fallback to MockEmailProvider when API keys are missing:

export class EmailProviderFactory {
static createProvider(config: EmailServiceConfig): EmailProvider {
const provider = config.provider.toLowerCase();
switch (provider) {
case "resend":
if (!config.apiKeys.resend || config.apiKeys.resend.trim() === '') {
return new MockEmailProvider(); // Graceful fallback
}
return new ResendProvider(config.apiKeys.resend, config.defaultFrom);
case "novu":
if (!config.apiKeys.novu || config.apiKeys.novu.trim() === '') {
return new MockEmailProvider();
}
return new NovuProvider(config.apiKeys.novu, config.defaultFrom, config.novu);
default:
return new MockEmailProvider();
}
}
}

Provider Implementations

ResendProvider

Wraps the official Resend SDK:

export class ResendProvider implements EmailProvider {
private resend: Resend;
private defaultFrom: string;

constructor(apiKey: string, defaultFrom: string) {
this.resend = new Resend(apiKey);
this.defaultFrom = defaultFrom;
}

async sendEmail(message: EmailMessage): Promise<CreateEmailResponse> {
return this.resend.emails.send({
from: message.from || this.defaultFrom,
to: message.to,
subject: message.subject,
html: message.html,
text: message.text,
});
}

getName(): string { return "resend"; }
}

NovuProvider

Integrates with the Novu notification infrastructure. Sends emails via Novu workflow triggers:

export class NovuProvider implements EmailProvider {
private novu: Novu;
private defaultFrom: string;
private templateId: string;

constructor(apiKey: string, defaultFrom: string, config?: EmailNovuConfig) {
this.novu = new Novu({
secretKey: apiKey,
serverURL: config?.backendUrl,
});
this.templateId = config?.templateId || "email-default";
}

async sendEmail(message: EmailMessage) {
const email = Array.isArray(message.to) ? message.to[0] : message.to;
return this.novu.trigger({
to: { subscriberId: email, email },
workflowId: this.templateId,
payload: {
subject: message.subject,
body: message.html,
preheader: message.text,
from: message.from || this.defaultFrom,
},
});
}
}

MockEmailProvider

Logs email messages to the console without sending:

export class MockEmailProvider implements EmailProvider {
async sendEmail(message: EmailMessage) {
console.log("Sending email:", message);
return Promise.resolve();
}
getName(): string { return "mock"; }
}

EmailService Class

The EmailService provides high-level email operations with availability checking:

export class EmailService {
private provider: EmailProvider | null = null;
private isAvailable: boolean = false;

constructor(config: EmailServiceConfig) {
const hasApiKey = Object.values(config.apiKeys).some(key => key && key.trim() !== '');
if (hasApiKey) {
this.provider = EmailProviderFactory.createProvider(config);
this.isAvailable = true;
}
}

public isServiceAvailable(): boolean {
return this.isAvailable && this.provider !== null;
}
}

Available Email Methods

MethodDescription
sendVerificationEmail(email, token)Email verification with link
sendPasswordResetEmail(email, token)Password reset with link
sendTwoFactorTokenEmail(email, token)2FA code delivery
sendPasswordChangeConfirmationEmail(email, userName?, ip?, ua?)Password change notification
sendAccountCreatedEmail(userName, email, companyName?)Welcome email on registration
sendNewsletterSubscriptionEmail(email)Newsletter subscription confirmation
sendNewsletterUnsubscriptionEmail(email)Newsletter unsubscribe confirmation
sendCustomEmail(message)Send arbitrary email content

Graceful Degradation

The tryEmailOperation wrapper handles unavailable email services without throwing:

async function tryEmailOperation<T>(
operation: (service: EmailService) => Promise<T>,
operationName: string
): Promise<T | EmailSkippedResult> {
const service = await mailService();
if (!service.isServiceAvailable()) {
return { skipped: true, reason: 'Email service not configured' };
}
return await operation(service);
}

This allows the application to function normally even without email configuration. The EmailSkippedResult type signals that the operation was not performed:

interface EmailSkippedResult {
skipped: true;
reason: string;
}

Configuration Resolution

Email configuration merges environment variables with content-based settings:

async function mailService() {
const config = await getCachedConfig();
return new EmailService({
provider: config.mail?.provider || emailConfig.provider,
defaultFrom: config.mail?.default_from || emailConfig.defaultFrom,
domain: config.app_url || emailConfig.domain,
novu: config.mail?.provider === "novu" ? {
templateId: config.mail?.template_id,
backendUrl: config.mail?.backend_url,
} : undefined,
});
}

Email Templates

The lib/mail/templates/ directory contains HTML templates for all email types:

TemplateFile
Account Createdaccount-created.ts
Email Verificationemail-verification.ts
Password Changepassword-change-confirmation.ts
Payment Successpayment-success.ts
Payment Failedpayment-failed.ts
Subscription Eventssubscription-events.ts
Subscription Expiredsubscription-expired.ts
Subscription Renewalsubscription-renewal-reminder.ts
Submission Decisionsubmission-decision.ts
Newsletter Welcomenewsletter-welcome.ts
Newsletter Regularnewsletter-regular.ts
Newsletter Unsubscribenewsletter-unsubscribe.ts
Admin Notificationadmin-notification.ts

Templates return objects with subject, html, and text properties for full email rendering.