Skip to content

Requirements: Sending Library (Phase 1)

Functional requirements for the common-module lib/infra/email package. Behavioral definitions live in the Design §9; each requirement cites its design anchor rather than restating it.

  • REQ-LIB-001 — The library SHALL provide an EmailMessage value type (from, to, cc, bcc, subject, htmlBody, textBody, replyTo, attachments, custom headers) whose construction is a smart constructor returning Result<EmailMessage>, rejecting header-bound fields that fail sanitizeHeader (Design §9.4 — Header hygiene).
  • REQ-LIB-002 — The library SHALL provide fromHeader(displayName, address) producing an RFC 5322-compliant From header, behavior-identical to the pre-lift operations implementation (operations/src/main/kotlin/cards/arda/operations/shopaccess/email/service/EmailSendService.kt).
  • REQ-LIB-003 — The library SHALL provide an EmailServer value type (apiBaseUrl defaulting to the Postmark API base, serverToken, messageStream defaulting to outbound) with a Result-returning smart constructor; the token SHALL be excluded from toString (Design §8 — EmailServer).
  • REQ-LIB-004 — The library SHALL provide EmailSenderFactory.from(server, retryPolicy, engine): Result<EmailSender>; the factory SHALL carry no knowledge of any specific server identity, domain, or account — the EmailServer value is its only server input (DQ-005 A′, Design §9.3).
  • REQ-LIB-005EmailSender.send(message) SHALL classify every send into the sealed SendEmailOutcome: Sent(messageId) (2xx), Rejected(errorCode, reason) (4xx except 429, never retried), RateLimited(retryAfter) (429), TransportFailure(AppError) (5xx / transport) — (Design §9.4 — Outcome classification).
  • REQ-LIB-006 — Transient outcomes SHALL be retried inside the sender per SendRetryPolicy (default: 3 attempts, exponential backoff 500 ms × 2 capped at 10 s, honoring Retry-After); permanent rejections SHALL short-circuit (Design §9.4 — Retry with backoff).
  • REQ-LIB-007 — The Postmark implementation SHALL serialize requests wire-identically to the pre-lift operations client (PostmarkSendEmailRequest: nulls omitted, MessageStream set, Cc/Bcc semantics preserved) and authenticate per call via X-Postmark-Server-Token from the EmailServer value.
  • REQ-LIB-008 — The factory SHALL expose optional lifecycle hooks (onSendSuccess / onTransientFailure / onPermanentFailure) so consumers can attach metrics and tenant-lifecycle behavior without the library depending on them (Design §8 — PostmarkEmailSender).
  • REQ-LIB-009ConfigurationProvider SHALL bind an OPTIONAL top-level email {} HOCON block via EmailConfig.fromConfig(cfg): Result<EmailConfig>: absent block → null capability (WARN); present but malformed (including blank serverToken) → boot failure with AppError.GeneralValidation — mirroring globalDsConfiguration (Design §9.2).
  • REQ-LIB-010EmailConfig SHALL expose server: EmailServer and MAY carry an optional uninterpreted from { domain, localPart, displayName } convenience sub-block for consumers (Design §8 — EmailConfig binding).
  • REQ-LIB-011 — All fallible library operations SHALL use the monadic error channel (Result + typed AppError leaves; no getOrNull/throwing in prod paths), and the HTTP layer SHALL be built on the existing httpClient(logger, engine) seam — no new HTTP stack, no new runtime dependencies.


Copyright: (c) Arda Systems 2025-2026, All rights reserved