Files
2026-05-27 23:29:53 +01:00

4.1 KiB

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

class Emailer {
  constructor(options: EmailerOptions)
  send(message: EmailMessage): Promise<EmailSendResult>
  sendBatch(messages: EmailMessage[]): Promise<EmailSendResult[]>
}

function createEmailer(options: EmailerOptions): Emailer

interface EmailProvider {
  name: string
  send(message: EmailMessage): Promise<EmailSendResult>
}

interface EmailMessage {
  to: string | string[]
  from: string
  subject: string
  html?: string
  text?: string
  templateId?: string
  templateContext?: Record<string, unknown>
  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

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.