--- status: Accepted date: 2026-09-17 applyTo: - "Fuchs/Services/IComService.cs" - "Fuchs/Services/ProcessWebComService.cs" - "Fuchs/Services/ProcessWebComSettings.cs" - "Fuchs/Services/MailerAttachmentPolicy.cs" supersededBy: "" --- # 0028 — Mailer push_com: multipart attachments, count confirmation, contract pre-validation ## Context The ProcessWeb Mailer API (`push_com`) was extended to accept attachments. Its contract — https://processweb.de/api/mailer/fc — specifies `multipart/form-data` with the JSON envelope in an unnamed form field and one file part per attachment, an optional `attachmentCount` safety check, and a 200 response reporting `attachmentsReceived`/`attachmentsProcessed`. `ProcessWebComService` predated that: it base64-inlined attachments as `communication.attachments[]` in a JSON body. That property is not part of the contract, so the Mailer accepted the message and dropped the files — a 200 for an invoice mail whose PDF never arrived, with no error anywhere. The client also fabricated the audit `guid` (`Guid.NewGuid()`), treated a 409 (idempotency duplicate) as a generic API error, and called the legacy `/mailer/fc?fn=verify` and `api.processweb.de` paths. ## Decision 1. **Request shape follows the contract exactly.** No attachments → `application/json` envelope. With attachments → `multipart/form-data`: envelope in an unnamed field (`name=""`, set via an explicit `Content-Disposition` because `MultipartFormDataContent.Add` rejects an empty name), then one file part per attachment with real bytes. Field names as in the contract (`comType`, not `comtype`). Base URL `https://processweb.de`, paths `/api/mailer/push_com` and `/api/mailer/verify`. 2. **`attachmentCount` is always sent** so the Mailer itself rejects a request whose file parts don't match. 3. **A 200 is not success when attachments were sent unless `attachmentsProcessed` equals the number sent.** Anything else — fewer processed, or a body that isn't the contract JSON — is reported to the caller as a failed send (`false`, reason `attachments-mismatch`, counts in the audit log). Callers gate "mark as sent" on that boolean, so an invoice whose PDF was not processed is never flagged as sent. 4. **Contract limits are pre-validated client-side** (`MailerAttachmentPolicy`: count, per-file and per-message size, empty file, missing/forbidden extension) with the contract's defaults configurable under `Fuchs:Mailer:Attachments`. A violation fails before any bytes are sent, with the exact rule in the audit log. Empty files are a violation, not silently skipped as before. 5. **`comStackID` is the audit key**, falling back to the `Message-Id` header; a local id only when the Mailer returned neither. 6. **409 is a distinct failure** (`duplicate`): the Mailer already holds a message under this `reference` and did not queue this one. ## Consequences - `IComService.SendEmailAsync` returning `true` now means "accepted *and* every attachment confirmed processed" — strictly stronger than before. Existing callers (`req/sconf`, reminders, admin test mail) need no change; they already treat `false` as not-sent. - A caller passing a 0-byte attachment now gets `false` instead of a mail without the file. That is intended: an empty PDF is an upstream failure that should surface, not be hidden by a successful-looking send. - The forbidden-extension list in the client is the contract's quoted subset; the server's longer list stays authoritative and still answers 400 for the rest. - The concept doc `Docs/Concepts/processweb-mailer-interface.md` describes the current client behaviour and must be kept in sync when the contract evolves (it is declared backward compatible, so additions rather than breaks are expected). - The verify response is still parsed as `{ ok, account, serverid }`; if the `/api/mailer/verify` endpoint's body differs from the legacy `fc?fn=verify` one, only `VerifyCredentialsAsync`'s DTO needs adjusting. ## Alternatives considered - **Keep inline base64 and ask the Mailer to accept it too.** Rejected: the contract is the Mailer's; two accepted shapes would just be a second thing to keep compatible, and base64 inflates every PDF by a third. - **Treat 200 as success regardless of `attachmentsProcessed`.** Rejected — that is exactly the silent-drop failure mode this change exists to close. - **Treat 409 as success ("already sent").** Rejected: a resend under the same reference would then report success without anything being queued; the caller must see it and decide (e.g. use a new reference). - **Skip client-side validation and rely on the server's 400.** Rejected: the 400 body is an opaque string in the audit log; a named rule is what support needs, and it costs nothing.