Files
Fuchs_Intranet/Fuchs/Docs/Decisions/0029-outbound-mail-via-ocore-pwmailer-client.md
Stefan 249a945f5e
Playwright Tests / test (push) Canceled after 0s
feat: Implement admin-only specialties management interface
- Added MailerAttachmentPolicy for validating email attachments against server rules.
- Introduced fis.specialties.js for managing hidden maintenance actions via an admin interface.
- Created fis.specialties.scss for styling the specialties management UI.
- Added German localization for specialties management in fis.specialties_txt_de.js.
- Developed CSS for specialties management in fis.specialties.css.
- Implemented server-side stored procedure fds__admin_backupAndClearInvoiceFile for backing up and clearing invoice files.
- Created fds__invoice_file_backups table to store backup records of invoice files.
2026-09-17 21:17:23 +02:00

5.6 KiB

status, date, applyTo, supersededBy
status date applyTo supersededBy
Accepted 2026-09-17
Fuchs/Services/ProcessWebComService.cs
Fuchs/Services/FuchsMailerClient.cs
Fuchs/Services/MailerAttachmentPolicy.cs
Fuchs/Services/ProcessWebComSettings.cs
Fuchs/Program.cs

0029 — Outbound mail goes through the OCORE_web PWMailer client

Context

Right after 0028 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_<Id> 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.