--- status: Active lastUpdated: 2026-09-17 applyTo: - "Fuchs/Services/IComService.cs" - "Fuchs/Services/ProcessWebComService.cs" - "Fuchs/Services/ProcessWebComSettings.cs" - "Fuchs/Services/FuchsMailerClient.cs" - "Fuchs/Services/MailerAttachmentPolicy.cs" - "Fuchs.Tests/ProcessWebComServiceTests.cs" - "Fuchs.Tests/FuchsMailerClientTests.cs" relatedDecisions: - "0028-mailer-push-com-multipart-and-attachment-confirmation.md" - "0029-outbound-mail-via-ocore-pwmailer-client.md" --- # ProcessWeb Mailer interface (outbound email/SMS) ## Summary All outbound email and SMS leaves Fuchs through `IComService` → `ProcessWebComService`, which is a thin adapter over the OCORE_web **`IPWMailerClientService`** (`OCORE_web/comm/pwmailer/`). The client owns the HTTP contract of the ProcessWeb Mailer API; Fuchs owns what is specific to Fuchs — the recipient-override safety net, the signature, the attachment limits, the `fds__emaillog` audit row and telemetry. The authoritative, server-side contract is the Mailer's own function catalog: > **https://processweb.de/api/mailer/fc** — subject to change as features are > added, but kept backward compatible. The OCORE_web client is kept in sync > with it (its header says so and names the server-side files); read both > before changing anything here. ## How it works ### Layering | Layer | File | Responsibility | |-------|------|----------------| | Adapter | `Fuchs/Services/ProcessWebComService.cs` | `IComService`: validate recipient, apply `Fuchs:Email:OverrideRecipient`, append signature, build a `PWMailerMessage`, interpret the `PWMailerResult`, write `fds__emaillog`, emit metrics | | Fuchs client | `Fuchs/Services/FuchsMailerClient.cs` | `PWMailerClientService` subclass; overrides `ValidateMessage` to add the attachment limits (`MailerAttachmentPolicy`) on top of the contract's base rules | | OCORE client | `OCORE_web/comm/pwmailer/PWMailerClientService*.cs` | endpoints, envelope, JSON vs multipart (unnamed envelope field + one file part per attachment), `attachmentCount`, Basic auth, token resolution, `x-pcw-excd` error headers, retries/timeouts, typed results | ### Configuration `Fuchs:Mailer` (`ProcessWebComSettings`) stays the single config section. In `Program.cs` it is mapped onto `PWMailerClientOptions`: - `BaseUrl` (`https://processweb.de`) + `/api/mailer` → `PWMailerClientOptions.BaseUrl` - `AccountId` → `Account`, `ServerId` → `ServerId`, `Token` → `Token` - `TokenSecretName = "Fuchs--Mailer--Token"` — the same Key Vault secret OCORE secret management already maps into `Fuchs:Mailer:Token`. In production the configuration value is therefore already the plain token; the name only matters when the `MANAGED_BY_KEYVAULT` placeholder is still in place (e.g. locally), where the client loads it on demand. - `Attachments` (`MaxFileBytes` 10 MB, `MaxMessageBytes` 20 MB, `MaxCount` 10 — the contract defaults) feed `FuchsMailerClient`'s pre-validation. Registration: `AddPWMailerClient()` (singleton, named `HttpClient` `PWMailerClient`). `ProcessWebComService` stays scoped. ### Sending an email 1. `SendEmailAsync` validates the address, applies the override, appends the signature, and builds `PWMailerMessage { To, Cc, Subject, Body, Reference, ComType = "e", Attachments }`. `AttachmentCount` is left `null`, so the client sends the real count and the Mailer cross-checks it (400 on mismatch). 2. `FuchsMailerClient.ValidateMessage` runs *inside* `PushAsync` before any bytes go out: base rules (server id, recipient, subject, body, empty file, missing extension) plus Fuchs limits (count, per-file size, total size, forbidden extensions — `MailerAttachmentPolicy.ForbiddenExtensions` is the contract's quoted subset; the server's list is longer and authoritative). A violation returns a result with `StatusCode 0`, `Attempts 0` and the `"; "`-joined rule messages as `Error`. 3. The client posts JSON (no attachments) or multipart (with) to `POST /api/mailer/push_com`, never retrying a push. 4. The adapter interprets the `PWMailerResult`: | Result | `SendEmailAsync` | audit `fds__emaillog` | `fuchs.emails.failed{reason}` | |--------|------------------|-----------------------|-------------------------------| | `Success` and (no attachments or `AttachmentsProcessed == sent`) | `true` | `guid = comStackID` | — (`fuchs.emails.sent`) | | `Success` but `AttachmentsProcessed != sent` | `false` | counts in log, `guid = comStackID` | `attachments-mismatch` | | `IsConflict` (409: identical message still pending at the Mailer — it will be sent, nothing new queued) | `true` | "Duplicate of a still-pending entry (409) …" | — (`fuchs.emails.sent`) | | `StatusCode 0`, `Attempts 0`, `Exception is ArgumentException` (refused by pre-validation) | `false` | one line per rule | `validation` | | `StatusCode 0`, `Attempts 0`, no exception (options unusable / token not resolvable, e.g. Key Vault outage) | `false` | `Mailer client not usable: …` (Error log) | `client-config` | | any other failure | `false` | `API error {status}: {error}` | `api-error` | | client threw | `false` | exception detail | `api-error` | `reference` (`inv_` etc.) is the audit key and part of the Mailer's idempotency key (server + reference + content, while the first entry is still pending). A resend of the identical mail within that window answers 409 and is reported as **success** — the first entry is delivered; nothing is sent twice. A resend after delivery started is a new entry. Callers keep reusing `inv_` (e.g. `req/resend`); no per-attempt reference is needed. ### SMS and verification - `SendSmsAsync` pushes `PWMailerMessage { ComType = "s", To = mobile, Body }` — the base validation requires no subject for `comType "s"`. - `VerifyCredentialsAsync` → `IPWMailerClientService.VerifyAsync(serverId)` (`GET /api/mailer/verify[?serverid=…]`), mapped to `MailerCredentialsCheckResult` for the Admin system-status view. ### Observability `fuchs.emails.sent` / `fuchs.emails.failed{reason}` with reasons `invalid-email`, `disabled`, `validation`, `client-config`, `attachments-mismatch`, `api-error`; `fuchs.email.send.duration{success}`. The OCORE client additionally logs every failed call at Warning and exposes `RequestSending`/`ResponseReceived`/`CallCompleted`/`CallFailed` events (not subscribed by Fuchs today). ## Key files - `Fuchs/Services/IComService.cs` — interface + `MailerCredentialsCheckResult`. - `Fuchs/Services/ProcessWebComService.cs` — the adapter. - `Fuchs/Services/FuchsMailerClient.cs` — `ValidateMessage` override. - `Fuchs/Services/MailerAttachmentPolicy.cs` — limit rules (pure). - `Fuchs/Services/ProcessWebComSettings.cs` — `Fuchs:Mailer` incl. `Attachments`. - `Fuchs/Program.cs` — options mapping + `AddPWMailerClient()`. - `OCORE_web/OCORE_web/comm/pwmailer/` — the client (submodule). - `Fuchs.Tests/ProcessWebComServiceTests.cs`, `Fuchs.Tests/FuchsMailerClientTests.cs`; the wire format is tested in `OCORE_web/OCORE_webTests/comm/PWMailerClientServiceTests.cs`. ## Related decisions - [0028 — push_com multipart attachments and attachment confirmation](../Decisions/0028-mailer-push-com-multipart-and-attachment-confirmation.md) - [0029 — Outbound mail via the OCORE_web PWMailer client](../Decisions/0029-outbound-mail-via-ocore-pwmailer-client.md)