docs(notifications): EWS transport docs; close Q12 as superseded; design-doc corrections from execution reviews
This commit is contained in:
@@ -53,6 +53,37 @@ The SMTP configuration is defined centrally and used by the central Email delive
|
||||
- **Connection timeout**: Maximum time to wait for SMTP connection (default: 30 seconds).
|
||||
- **Max concurrent connections**: Maximum simultaneous SMTP connections from the central cluster (default: 5).
|
||||
- **Retry settings**: Max retry count, fixed time between retries. The Notification Outbox reuses these for delivery retry of transient failures.
|
||||
- **Transport**: The submission mechanism — `Smtp` (default) or `Ews`. Null/empty means `Smtp`, so every pre-existing row keeps its behavior. See EWS Transport below.
|
||||
|
||||
### EWS Transport (on-prem Exchange)
|
||||
|
||||
Email can be submitted through **Exchange Web Services** (SOAP over HTTPS) instead of SMTP, for networks where authenticated SMTP submission is not offered. The transport is chosen per configuration row via **Transport** = `Ews`; the SMTP/MailKit path is untouched and remains the default. Nothing changes for sites or scripts — delivery stays central-only and `Notify.To(...).Send(...)` behaves identically. Design rationale: [`docs/plans/2026-08-10-ews-email-transport-design.md`](../plans/2026-08-10-ews-email-transport-design.md).
|
||||
|
||||
**Configuration model (field reuse).** No new entity or table — the same `SmtpConfiguration` row carries both transports:
|
||||
|
||||
- **Host** — under `Ews`, the **full EWS endpoint URL**, an absolute `https://` URI (e.g. `https://mail.example.com/ews/exchange.asmx`), not a mail-server hostname.
|
||||
- **Authentication mode** — must be `basic`. There is no OAuth2 or NTLM/Negotiate path on the EWS transport.
|
||||
- **Credentials** — `username:password`, split on the **first** `:` only so a password may contain colons; the username may be domain-qualified (`domain\user`). Stored encrypted at rest via `EncryptedStringConverter`, as on the SMTP path, and projected away from every read path (`notification smtp list`, management responses, audit afterState).
|
||||
- **From address**, **Connection timeout**, **Retry settings** — same meaning; the timeout becomes the per-request HTTP timeout.
|
||||
- **Port**, **TLS mode**, **OAuth2 authority/scope**, **Max concurrent connections** — **unused** under `Ews`. The Central UI hides them and the CLI ignores them for this transport.
|
||||
|
||||
**Auth posture.** HTTP Basic over HTTPS only, sent as an explicit per-request `Authorization` header (never set on the shared, pooled `HttpClient`, which would leak the credential between callers). HTTPS is enforced in **three** places — the management write gate, the delivery adapter, and again inside the sender as defense in depth — so a Basic credential can never leave the process over cleartext even if a row is edited around a higher layer. **Revisit trigger:** if Basic is disabled on the EWS virtual directory, an auth-mode knob (Negotiate/NTLM) becomes the documented follow-on; Linux containers would then also need `gss-ntlmssp` in the image.
|
||||
|
||||
**Message semantics.** One `CreateItem` SOAP request per notification, `RequestServerVersion Exchange2013`, `MessageDisposition="SendOnly"`, recipients in **`BccRecipients` only**, body `BodyType="Text"`. BCC-only preserves the SMTP path's recipient handling (recipients cannot see each other — see Recipient Handling (Email) below). `SendOnly` deliberately leaves no Sent-Items copy: the central `Notifications` table is the audit record of what was sent, and a mailbox copy would grow unboundedly. All user content is XML-escaped. No SDK is used — a hand-rolled envelope over `HttpClient`, matching the no-SDK house pattern set by the SMS adapter.
|
||||
|
||||
**Error classification (EWS).** Classification lives inside the EWS sender, which throws typed transient/permanent exceptions that the Email adapter maps to the same three-way `DeliveryOutcome` as SMTP (there is no standalone classifier type). **What Exchange said beats what HTTP reported**: a parsed response code decides the outcome even on an HTTP 500, and only an unparseable body falls back to the status code.
|
||||
|
||||
- **Transient** (retry, then park after `MaxRetries`): connect/DNS/socket failures and request timeouts; HTTP 408/429/5xx on an unparseable body; and the availability-shaped response codes `ErrorServerBusy`, `ErrorInternalServerTransientError`, `ErrorTimeoutExpired`, `ErrorMailboxStoreUnavailable`, `ErrorInsufficientResources`.
|
||||
- **Permanent** (park immediately): HTTP 401/403 (retrying a bad credential burns a domain account's lockout budget), 404/410/405 (wrong URL or endpoint), SOAP faults, recipient-shaped and schema response codes, and configuration defects found before the send (non-https or relative endpoint, non-Basic auth type, credentials not in `username:password` form). **Unclassified defaults to permanent**, matching the SMTP adapter's stance.
|
||||
- Every surfaced message runs through `CredentialRedactor`, which masks both the packed `username:password` and its base64 Basic-auth encoding; attempts log the endpoint host and a recipient **count** only.
|
||||
|
||||
**Response parsing.** EWS responses are parsed with DTD processing **prohibited** and no external entity resolver, so a hostile or malfunctioning endpoint cannot drive entity expansion or external-entity retrieval from the delivery path. A malformed body is reported as unparseable rather than throwing.
|
||||
|
||||
**Validation layering.** The delivery adapter is the **authoritative** check — it re-validates the EWS shape at delivery time and parks the notification with a clear reason if the row is unusable, so no path can produce a silently broken send:
|
||||
|
||||
- **Management write gate** (`ManagementActor`, covering the CLI and the management API) rejects an unknown transport, and for `Ews` requires an absolute https `Host`, `basic` auth mode, and `username:password` credentials. This catches the common mistake of flipping an existing SMTP row — whose credentials are a bare password — to `Ews`.
|
||||
- **Central UI** `/notifications/smtp` writes through the notification repository directly rather than through the management gate, so the page mirrors the same https + Basic + credential-form rules client-side before saving.
|
||||
- **Transport bundle import** deliberately does **not** gate EWS shape: an imported row is applied as data, and an unusable one surfaces as a parked notification with the adapter's error. Accepted — import is an environment-migration path where the operator remaps values anyway.
|
||||
|
||||
## SMS Configuration
|
||||
|
||||
@@ -117,6 +148,7 @@ Each `Deliver(...)` call returns one of `success | transient failure | permanent
|
||||
- **Permanent failures** (SMTP 5xx permanent errors, e.g., mailbox not found): The Notification Outbox moves the row to `Parked` with the error in `LastError`. The notification will never deliver, and an operator can review or discard it on the Central UI Notification Outbox page.
|
||||
- Retries exhausted on a transient failure also result in a `Parked` row.
|
||||
- A script observes failures only by calling `Notify.Status(id)` and seeing a `Parked` status — not as a synchronous exception.
|
||||
- The bullets above describe the SMTP transport. The EWS transport produces the same three outcomes from its own classification rules — see EWS Transport above.
|
||||
|
||||
### No Rate Limiting
|
||||
- No application-level rate limiting. If the delivery endpoint enforces sending limits (e.g., Microsoft 365 throttling or Twilio rate limits), those manifest as transient failures and are retried naturally by the Notification Outbox.
|
||||
|
||||
Reference in New Issue
Block a user