Files
Fuchs_Intranet/Fuchs/Docs/Decisions/0028-mailer-push-com-multipart-and-attachment-confirmation.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

4.7 KiB

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

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.