Playwright Tests / test (push) Canceled after 0s
- 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.
92 lines
4.7 KiB
Markdown
92 lines
4.7 KiB
Markdown
---
|
|
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.
|