UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

254 lines (228 loc) 6.68 kB
import { $atom, $env, $hook, $state, type Static, z } from "alepha"; import { EmailError, type EmailProvider, type EmailSendOptions, } from "alepha/email"; import { $logger } from "alepha/logger"; import type { Transporter } from "nodemailer"; import nodemailer from "nodemailer"; // --------------------------------------------------------------------------------------------------------------------- /** * Environment variables for nodemailer configuration */ const envSchema = z.object({ EMAIL_HOST: z .text({ description: "SMTP server host", }) .optional(), EMAIL_PORT: z.number().describe("SMTP server port").default(587), EMAIL_USER: z .text({ description: "SMTP authentication username", }) .optional(), EMAIL_PASS: z .text({ description: "SMTP authentication password", }) .optional(), EMAIL_FROM: z .text({ description: "Default from email address", }) .optional(), EMAIL_SECURE: z .boolean() .describe("Use secure connection (TLS)") .default(false), }); // --------------------------------------------------------------------------------------------------------------------- /** * Nodemailer connection pooling and rate limiting options */ export const nodemailerEmailOptions = $atom({ name: "alepha.email.nodemailer.options", schema: z.object({ pool: z.boolean().describe("Enable connection pooling").optional(), maxConnections: z .number() .describe("Maximum number of connections in pool") .optional(), maxMessages: z .number() .describe("Maximum messages per connection") .optional(), rateDelta: z .number() .describe("Time in milliseconds between message sends") .optional(), rateLimit: z .number() .describe("Maximum number of messages per rateDelta") .optional(), }), default: {}, }); export type NodemailerEmailProviderOptions = Static< typeof nodemailerEmailOptions.schema >; declare module "alepha" { interface State { [nodemailerEmailOptions.key]: NodemailerEmailProviderOptions; } } // --------------------------------------------------------------------------------------------------------------------- /** * Email provider using Nodemailer for SMTP transport. * * Configuration is provided via environment variables: * - EMAIL_HOST: SMTP server host * - EMAIL_PORT: SMTP server port (default: 587) * - EMAIL_USER: SMTP authentication username * - EMAIL_PASS: SMTP authentication password * - EMAIL_FROM: Default from email address * - EMAIL_SECURE: Use secure connection (default: false) * * Advanced pooling/rate limiting options can be configured via atom: * @see {@link nodemailerEmailOptions} * * @example * ```typescript * // Configure via environment variables * // EMAIL_HOST=smtp.example.com * // EMAIL_PORT=587 * // EMAIL_USER=user@example.com * // EMAIL_PASS=secret * // EMAIL_FROM=noreply@example.com * * // Optionally configure pooling via atom * alepha.state.set(nodemailerEmailOptions.key, { * pool: true, * maxConnections: 5, * rateLimit: 10, * }); * ``` */ export class NodemailerEmailProvider implements EmailProvider { protected readonly env = $env(envSchema); protected readonly log = $logger(); protected readonly options = $state(nodemailerEmailOptions); protected transporter: Transporter | null = null; protected get host(): string { const host = this.env.EMAIL_HOST; if (!host) { throw new EmailError( "Email host not configured. Set EMAIL_HOST env var.", ); } return host; } protected get port(): number { return this.env.EMAIL_PORT; } protected get secure(): boolean { return this.env.EMAIL_SECURE; } protected get user(): string | undefined { return this.env.EMAIL_USER; } protected get pass(): string | undefined { return this.env.EMAIL_PASS; } protected get fromAddress(): string { const from = this.env.EMAIL_FROM; if (!from) { throw new EmailError( "Email from address not configured. Set EMAIL_FROM env var.", ); } return from; } protected getTransporter(): Transporter { if (!this.transporter) { this.transporter = this.createTransporter(); } return this.transporter; } public async send(options: EmailSendOptions): Promise<void> { const { to, subject, body } = options; this.log.debug("Sending email via Nodemailer", { to, subject }); try { const result = await this.getTransporter().sendMail({ from: this.fromAddress, to, subject, html: body, }); this.log.info("Email sent successfully", { to, subject, messageId: result.messageId, response: result.response, }); } catch (error) { const message = `Failed to send email via Nodemailer: ${error instanceof Error ? error.message : String(error)}`; this.log.error(message, { to, subject }); throw new EmailError(message, error instanceof Error ? error : undefined); } } protected createTransporter(): Transporter { const transporterConfig = { host: this.host, port: this.port, secure: this.secure, auth: this.user && this.pass ? { user: this.user, pass: this.pass, } : undefined, pool: this.options.pool, maxConnections: this.options.maxConnections, maxMessages: this.options.maxMessages, rateDelta: this.options.rateDelta, rateLimit: this.options.rateLimit, }; this.log.debug("Creating Nodemailer transporter", { host: transporterConfig.host, port: transporterConfig.port, secure: transporterConfig.secure, user: transporterConfig.auth?.user, pool: transporterConfig.pool, }); return nodemailer.createTransport(transporterConfig); } /** * Verify the connection to the email server. */ public async verify(): Promise<boolean> { try { await this.getTransporter().verify(); this.log.info("Email server connection verified"); return true; } catch (error) { this.log.error("Email server connection failed", { error }); return false; } } /** * Close the transporter connection. */ public close(): void { if (this.transporter) { this.transporter.close(); this.transporter = null; } } protected readonly onStart = $hook({ on: "start", handler: () => this.verify(), }); protected readonly onStop = $hook({ on: "stop", handler: () => this.close(), }); }