# OpenSpec Change: enterprise-server-emailer - **Change ID**: enterprise-server-emailer - **Package**: `@suaveplan/emailer` - **Category**: server/services - **Target path**: `server/services/emailer` - **Status**: PROPOSED - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/emailer.md` --- ## Summary Deliver `@suaveplan/emailer` on the genesis branch from scratch at enterprise quality. The package provides transactional email sending with a pluggable `EmailProvider` interface. It ships an `Emailer` facade, an `InMemoryProvider` for testing, an SMTP provider using `nodemailer`, HTML + text body composition via `@suaveplan/templates`, bounce/unsubscribe suppression via `@suaveplan/suppression`, idempotency via `@suaveplan/idempotency`, retry + circuit breaker via `@suaveplan/retry`, and OTel spans. Supports attachments, CC/BCC, reply-to, inline images, and batch sends with rate-limit awareness. --- ## Current-state audit (genesis) Does not exist on genesis — fresh build. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | Typed `EMAILER.*` error codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike` | | `@suaveplan/retry` (`workspace:^`) | Retry + circuit breaker | | `@suaveplan/suppression` (`workspace:^`) | Bounce/unsubscribe suppression | | `@suaveplan/idempotency` (`workspace:^`) | Dedup retried sends | | `@suaveplan/templates` (`workspace:^`) | HTML + text body templating | | `@suaveplan/logger` (`workspace:^`) | Structured logs | | `zod` (`catalog:forms`) | Options + message schemas | | `@opentelemetry/api` (`catalog:observability`) | Spans + counters | | `nodemailer` (`catalog:services`) | SMTP transport | | `tslib` (`catalog:build`) | Runtime helpers | --- ## Public API surface ```ts class Emailer { constructor(options: EmailerOptions) send(message: EmailMessage): Promise sendBatch(messages: EmailMessage[]): Promise } function createEmailer(options: EmailerOptions): Emailer interface EmailProvider { name: string send(message: EmailMessage): Promise } interface EmailMessage { to: string | string[] from: string subject: string html?: string text?: string templateId?: string templateContext?: Record cc?: string | string[] bcc?: string | string[] replyTo?: string attachments?: EmailAttachment[] idempotencyKey?: string } interface EmailAttachment { filename: string content: Uint8Array | string contentType?: string inline?: boolean cid?: string } interface EmailSendResult { messageId: string provider: string status: 'queued' | 'sent' | 'failed' accepted: string[] rejected: string[] error?: string } interface EmailerOptions { provider: EmailProvider suppressionStore?: SuppressionStore fromDefault?: string maxRetries?: number logger?: LoggerLike } type EmailerErrorCode = | 'EMAILER_SEND_FAILED' | 'EMAILER_INVALID_ADDRESS' | 'EMAILER_SUPPRESSED' | 'EMAILER_TEMPLATE_NOT_FOUND' | 'EMAILER_PROVIDER_UNAVAILABLE' | 'EMAILER_ATTACHMENT_TOO_LARGE' function registerEmailerErrorCodes(): void ``` --- ## File layout ```text server/services/emailer/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts constants/index.ts types/ emailer.types.ts emailer.types.md emailer/ emailer/ emailer.ts emailer.md emailer.types.ts emailer.test.ts index.ts providers/ in-memory-provider/ in-memory-provider.ts in-memory-provider.md in-memory-provider.test.ts index.ts smtp-provider/ smtp-provider.ts smtp-provider.md smtp-provider.types.ts smtp-provider.test.ts index.ts tests/ integration.test.ts ``` --- ## Impact Downstream: auth verification emails, order confirmations, notifications. **Prerequisites on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/retry`, `@suaveplan/suppression`, `@suaveplan/idempotency`, `@suaveplan/templates`, `@suaveplan/logger`.