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

5.0 KiB

Tasks: enterprise-server-emailer

Change ID: enterprise-server-emailer Package: @suaveplan/emailer Target path: server/services/emailer Prerequisites on Verdaccio: @suaveplan/error, @suaveplan/types, @suaveplan/retry, @suaveplan/suppression, @suaveplan/idempotency, @suaveplan/templates, @suaveplan/logger


0. Pre-flight (non-negotiable)

  • 0.1 Confirm clean working tree (git status reports nothing) and no in-flight worktrees with uncommitted work (git worktree list then git status in each)
  • 0.2 If any worktrees exist under .claude/worktrees/, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (git worktree remove) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
  • 0.3 Confirm the package set in MANIFEST.md matches find packages -maxdepth 3 -name package.json count. Drift here means a prior change archived without regenerating the manifest.

1. Scaffold

  • Create directory tree per proposal.md.
  • package.json at 0.1.0, "sideEffects": false, exports "." + emailer + providers/in-memory + providers/smtp.
  • Deps: all workspace + catalog deps listed in proposal.
  • DevDeps: vitest catalog:testing, @vitest/coverage-istanbul catalog:testing, @suaveplan/testing workspace:^.
  • tsconfig.json extending library-node.json; vitest.config.ts with createTestConfig().
  • Scripts: lint, typecheck, test, test:coverage, build, docs:check.

2. Types and errors

  • Define all interfaces and Zod schemas (EmailMessageSchema, EmailerOptionsSchema, EmailAttachmentSchema) in src/types/emailer.types.ts.
  • Write emailer.types.md (≥200 words, both examples, API reference).
  • src/error-codes.ts: six codes; registerEmailerErrorCodes(); eager call.

3. InMemory provider

  • Implement InMemoryProvider in src/providers/in-memory-provider/in-memory-provider.ts:
    • send(message): store in internal sent[]; return { messageId: uuid, status: 'sent', accepted: message.to, rejected: [] }.
    • getSent() / clearSent(): testing helpers.
  • Write in-memory-provider.test.ts: send + getSent; clearSent; attachment round-trip.
  • Write in-memory-provider.md (≥200 words).

4. SMTP provider

  • Implement SmtpProvider in src/providers/smtp-provider/smtp-provider.ts:
    • Constructor creates nodemailer Transporter.
    • send(message): build Mail.Options from EmailMessage; call transporter.sendMail(); map result.
    • Attachment handling: convert Uint8Array to Buffer for nodemailer.
    • Throw EMAILER_PROVIDER_UNAVAILABLE on transport error; EMAILER_SEND_FAILED on send error.
    • Guard nodemailer import; throw if unavailable.
  • smtp-provider.types.ts with SmtpProviderOptions.
  • Write smtp-provider.test.ts (≤500 lines): skip if SKIP_SMTP=1; use smtp-tester or nodemailer-mock; send and verify received; attachments; { timeout: 15000 }.
  • Write smtp-provider.md (≥200 words).

5. Emailer facade

  • Implement Emailer in src/emailer/emailer/emailer.ts:
    • send(message):
      1. Validate to addresses (basic @ check); throw EMAILER_INVALID_ADDRESS on invalid.
      2. Validate attachment sizes against configurable limit (default 25 MB each); throw EMAILER_ATTACHMENT_TOO_LARGE.
      3. Check suppression for each recipient; build filtered accepted list; throw EMAILER_SUPPRESSED if all suppressed.
      4. If templateId provided: render via @suaveplan/templates; populate html/text.
      5. Idempotency guard when idempotencyKey present.
      6. provider.send() with @suaveplan/retry; throw EMAILER_SEND_FAILED on exhaustion.
      7. OTel span emailer.send; counter emailer.sends_total with status.
    • sendBatch(messages): parallel with Promise.allSettled.
  • Write emailer.types.ts.
  • Write emailer.test.ts (≤500 lines): successful send; suppressed recipient; invalid address; template rendering; attachment too large; retry; idempotency dedup; batch with partial failures.
  • Write emailer.md (≥200 words, simple + advanced, API reference).

6. Integration tests

  • tests/integration.test.ts: Emailer + InMemoryProvider + real @suaveplan/suppression store; add address to suppression; send batch; verify sent/suppressed split; { timeout: 10000 }.

7. Docs + verify + publish

  • Root README.md: quick-start with InMemoryProvider, SMTP setup, suppression workflow, template usage, API table.
  • bunx turbo lint --filter=@suaveplan/emailer — zero errors.
  • bunx turbo typecheck --filter=@suaveplan/emailer — zero errors.
  • bunx turbo test --filter=@suaveplan/emailer — all pass.
  • bunx turbo test:coverage --filter=@suaveplan/emailer — 100%.
  • bun scripts/publish.ts --package @suaveplan/emailer.
  • Update CLAUDE.md; archive to openspec/archive/2026-04-24-enterprise-server-emailer/.