- 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.
4.7 KiB
status, date, applyTo, supersededBy
| status | date | applyTo | supersededBy | ||||
|---|---|---|---|---|---|---|---|
| Accepted | 2026-09-17 |
|
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
- Request shape follows the contract exactly. No attachments →
application/jsonenvelope. With attachments →multipart/form-data: envelope in an unnamed field (name="", set via an explicitContent-DispositionbecauseMultipartFormDataContent.Addrejects an empty name), then one file part per attachment with real bytes. Field names as in the contract (comType, notcomtype). Base URLhttps://processweb.de, paths/api/mailer/push_comand/api/mailer/verify. attachmentCountis always sent so the Mailer itself rejects a request whose file parts don't match.- A 200 is not success when attachments were sent unless
attachmentsProcessedequals 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, reasonattachments-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. - 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 underFuchs: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. comStackIDis the audit key, falling back to theMessage-Idheader; a local id only when the Mailer returned neither.- 409 is a distinct failure (
duplicate): the Mailer already holds a message under thisreferenceand did not queue this one.
Consequences
IComService.SendEmailAsyncreturningtruenow means "accepted and every attachment confirmed processed" — strictly stronger than before. Existing callers (req/sconf, reminders, admin test mail) need no change; they already treatfalseas not-sent.- A caller passing a 0-byte attachment now gets
falseinstead 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.mddescribes 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/verifyendpoint's body differs from the legacyfc?fn=verifyone, onlyVerifyCredentialsAsync'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.