--- status: Accepted date: 2026-09-17 applyTo: - "Fuchs/Services/ProcessWebComService.cs" - "Fuchs/Services/FuchsMailerClient.cs" - "Fuchs/Services/MailerAttachmentPolicy.cs" - "Fuchs/Services/ProcessWebComSettings.cs" - "Fuchs/Program.cs" supersededBy: "" --- # 0029 — Outbound mail goes through the OCORE_web PWMailer client ## Context Right after [0028](0028-mailer-push-com-multipart-and-attachment-confirmation.md) re-implemented the Mailer API's `push_com` contract by hand in `ProcessWebComService`, OCORE_web gained a complete typed client for that API (`OCORE_web/comm/pwmailer/`, `IPWMailerClientService`): every operation, the JSON/multipart request shapes, `attachmentCount`, typed receipts, Basic auth with Key Vault/encrypted token resolution, `x-pcw-excd` error headers, retries and timeouts — declared to be kept in sync with the server-side controllers. Keeping a second implementation of the same contract in Fuchs would mean two things to update on every API change. The client validated a message's required fields and empty/extension-less attachments, but had no hook for application rules (size limits, a file-type deny-list) — `PWMailerMessage` is sealed and `PushAsync` called `Validate` directly. ## Decision 1. **`ProcessWebComService` is an adapter over `IPWMailerClientService`.** It keeps only what is Fuchs-specific: recipient validation and the `Fuchs:Email:OverrideRecipient` safety net, the signature, building the `PWMailerMessage`, interpreting the `PWMailerResult`, the `fds__emaillog` audit row and telemetry. All HTTP transport lives in the OCORE client; the hand-rolled `PostPushComAsync`/MIME guessing from 0028 is deleted. 2. **Pre-validation is an overridable hook in the client.** OCORE_web's `PWMailerClientService` gains `protected virtual ValidateMessage(PWMailerMessage)` (default: `PWMailerMessage.Validate`), called by `PushAsync` and `PushBulkAsync` before anything is sent. Fuchs overrides it in `FuchsMailerClient` to add the contract's attachment limits (`MailerAttachmentPolicy`: count, per-file/per-message size, forbidden extensions, configurable under `Fuchs:Mailer:Attachments`). A refusal is a `PWMailerResult` with `StatusCode 0` / `Attempts 0`, which the adapter reports as reason `validation` with one audit line per rule. 3. **One configuration section.** `Fuchs:Mailer` (`ProcessWebComSettings`) remains the source; `Program.cs` maps it onto `PWMailerClientOptions` (`BaseUrl + /api/mailer`, `AccountId → Account`, `Token`, `ServerId`) and sets `TokenSecretName = "Fuchs--Mailer--Token"`, i.e. the Key Vault secret OCORE secret management already maps into `Fuchs:Mailer:Token`. No second `PWMailer` section, no second secret. 4. **0028's behavioural rules stand**, with one correction: multipart with `attachmentCount`, a 200 counts as success only when `attachmentsProcessed` equals the number sent, `comStackID` is the audit key. **409 is now a success, not a `duplicate` failure** (this replaces 0028's rule 6): the client documents 409 as "duplicate of a still-pending entry with the same server/reference/content — nothing was enqueued, the earlier entry will be sent". The mail is therefore on its way; the resend paths (`req/resend`, reminders) reuse `inv_` as the reference, and a double click must not surface as a send error. The deduplication is recorded in the audit row. They are enforced partly by the client (shape, count) and partly by the adapter (processed check, audit, 409 mapping). 5. Two small OCORE_web fixes made in the same change: `PWMailerMessage.Validate` no longer demands a subject for `comType "s"` (SMS), and a call refused by validation reports `Attempts = 0` as its documentation already stated. ## Consequences - Fuchs no longer asserts the wire format in its own tests; that lives in `OCORE_web/OCORE_webTests/comm/PWMailerClientServiceTests.cs`. Fuchs tests assert the `PWMailerMessage` handed to the client and the interpretation of each `PWMailerResult` shape, plus `FuchsMailerClient`'s refusals. - OCORE_web is a submodule owned by another repository: the hook, the SMS rule and the `Attempts` fix are a commit there and a pointer bump here. - The client is a singleton with a cached token; after a token rotation `IPWMailerClientService.ResetToken()` (on the concrete type) is the way to pick it up without a restart — not wired to any admin action yet. - Empty attachments are still refused (now by the client's base rule), so an upstream render failure never produces a mail without its PDF. The flip side: a caller attaching an *optional* file must skip an empty byte array itself — the reminder's invoice copy (`IntranetController.Reminder.cs`) does, otherwise a never-stored invoice file would fail the whole reminder instead of sending it without the copy. - The client's `getCom`/`getComStatus`/`abortCom` and inbox operations are available to Fuchs for free (e.g. a delivery-status column keyed by the audited `comStackID`) — not used yet. ## Alternatives considered - **Keep the 0028 hand-rolled transport.** Rejected: duplicate contract implementation, and the OCORE client is the one declared to track the server. - **Pre-validate in the adapter only (before `PushAsync`).** Rejected: the rules belong with the client's own validation so every OCORE consumer can apply them the same way, and the user asked for an overridable hook. - **Bind a separate `PWMailer` config section.** Rejected: `Fuchs:Mailer` is already Key Vault-managed and read by the Admin status view; mapping in code avoids a second token secret and drift between two sections.