# EWS Email Transport Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans (or subagent-driven-development in-session) to implement this plan task-by-task.

**Goal:** Add on-prem Exchange EWS (SOAP over HTTPS, Basic auth) as a second selectable email transport beside SMTP in the central Notification Outbox, per `docs/plans/2026-08-10-ews-email-transport-design.md`.

**Architecture:** One additive `Transport` discriminator (`Smtp` default / `Ews`) on the existing `SmtpConfiguration` row; `EmailNotificationDeliveryAdapter` branches after config load to a new no-SDK `EwsSoapMailSender` (one `CreateItem` SOAP POST, BCC-only, `SendOnly`, explicit Basic header). Everything else — outbox lifecycle, retry/park, recipient resolution, KPIs, UI/CLI surface, bundle SecretsBlock — is reused.

**Tech Stack:** .NET 10, xUnit, EF Core (MS SQL, one additive migration), `IHttpClientFactory`, hand-rolled SOAP (no EWS SDK). Solution: `ZB.MOM.WW.ScadaBridge.slnx`.

**Execution environment:** Work in a dedicated worktree (the `EnterWorktree` tool is broken here — create manually):
```bash
git -C /Users/dohertj2/Desktop/ScadaBridge worktree add /Users/dohertj2/Desktop/ScadaBridge-wt-ews -b feature/ews-email-transport main
cd /Users/dohertj2/Desktop/ScadaBridge-wt-ews   # ALWAYS use absolute paths; Bash cwd can drift
```
All file paths below are worktree-relative. Run tests with explicit `--filter` (targeted-tests rule); full `dotnet build ZB.MOM.WW.ScadaBridge.slnx` at the end, not per task.

**Never** put the contents of `email_details.txt` (untracked, git-ignored) in code, tests, or docs. Test fixtures use fake hosts/credentials only.

---

### Task 1: EmailTransport enum + parser (NotificationService)

**Classification:** small
**Estimated implement time:** ~3 min
**Parallelizable with:** Task 2, Task 3

**Files:**
- Create: `src/ZB.MOM.WW.ScadaBridge.NotificationService/EmailTransport.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests/EmailTransportParserTests.cs`

Mirror the existing `SmtpTlsMode`/`SmtpTlsModeParser` pattern (`src/ZB.MOM.WW.ScadaBridge.NotificationService/SmtpTlsMode.cs`) exactly — read it first.

**Step 1: Write the failing tests**

```csharp
using ZB.MOM.WW.ScadaBridge.NotificationService;

namespace ZB.MOM.WW.ScadaBridge.NotificationService.Tests;

public class EmailTransportParserTests
{
    [Theory]
    [InlineData(null, EmailTransport.Smtp)]        // null/empty = legacy rows = SMTP
    [InlineData("", EmailTransport.Smtp)]
    [InlineData("Smtp", EmailTransport.Smtp)]
    [InlineData("smtp", EmailTransport.Smtp)]
    [InlineData("SMTP", EmailTransport.Smtp)]
    [InlineData("Ews", EmailTransport.Ews)]
    [InlineData("ews", EmailTransport.Ews)]
    [InlineData("EWS", EmailTransport.Ews)]
    public void Parse_recognizes_transports_case_insensitively(string? input, EmailTransport expected)
        => Assert.Equal(expected, EmailTransportParser.Parse(input));

    [Theory]
    [InlineData("Graph")]
    [InlineData("smtps")]
    public void Parse_rejects_unknown_transport(string input)
    {
        var ex = Assert.Throws<ArgumentException>(() => EmailTransportParser.Parse(input));
        Assert.Contains(input, ex.Message);
    }
}
```

**Step 2: Run to verify failure**
`dotnet test tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests --filter EmailTransportParserTests -v minimal` → compile error (type missing).

**Step 3: Implement**

```csharp
namespace ZB.MOM.WW.ScadaBridge.NotificationService;

/// <summary>Email delivery transport selected by <c>SmtpConfiguration.Transport</c>.</summary>
public enum EmailTransport
{
    /// <summary>Direct SMTP submission via MailKit (the original path).</summary>
    Smtp,
    /// <summary>On-prem Exchange Web Services — one CreateItem SOAP call over HTTPS.</summary>
    Ews,
}

/// <summary>
/// Parses the stored transport string. Null/empty maps to <see cref="EmailTransport.Smtp"/>
/// (every pre-EWS configuration row is an SMTP row); an unknown value throws
/// <see cref="ArgumentException"/> — a configuration error retrying cannot fix, which the
/// delivery adapter surfaces as a permanent failure (mirrors <c>SmtpTlsModeParser</c>).
/// </summary>
public static class EmailTransportParser
{
    public static EmailTransport Parse(string? value)
    {
        if (string.IsNullOrWhiteSpace(value)) return EmailTransport.Smtp;
        return value.Trim().ToLowerInvariant() switch
        {
            "smtp" => EmailTransport.Smtp,
            "ews" => EmailTransport.Ews,
            _ => throw new ArgumentException(
                $"Unknown email transport '{value}'. Expected one of: Smtp, Ews."),
        };
    }
}
```

**Step 4: Run tests** → PASS.

**Step 5: Commit** — `git add` the two files; `git commit -m "feat(notifications): EmailTransport enum + parser for the EWS transport discriminator"`.

---

### Task 2: SmtpConfiguration.Transport entity field + EF mapping + migration

**Classification:** high-risk (EF migration)
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 1, Task 3

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/SmtpConfiguration.cs`
- Modify: `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/NotificationConfiguration.cs` (class `SmtpConfigurationConfiguration`, ~line 103)
- Create: migration via CLI (see Step 3 — **build first**, never `--no-build`)

**Step 1: Add the property** (after `TlsMode`, before `FromAddress`):

```csharp
    /// <summary>
    /// Gets or sets the email delivery transport ("Smtp" or "Ews"). Null/empty means Smtp
    /// (legacy rows). Under Ews, <see cref="Host"/> holds the full EWS endpoint URL,
    /// <see cref="AuthType"/> must be Basic, and Port/TlsMode/OAuth2* are unused.
    /// </summary>
    public string? Transport { get; set; }
```

**Step 2: EF mapping** — in `SmtpConfigurationConfiguration.Configure`, after the `TlsMode` property block:

```csharp
        // Email transport discriminator ("Smtp"/"Ews"); null = Smtp for pre-EWS rows.
        builder.Property(s => s.Transport)
            .HasMaxLength(50);
```

**Step 3: Migration** — from the worktree root:
```bash
dotnet build src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase   # NEVER scaffold off a stale DLL
dotnet ef migrations add AddSmtpConfigurationTransport \
  --project src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase \
  --startup-project src/ZB.MOM.WW.ScadaBridge.Host
```
Verify the generated migration adds exactly one nullable `nvarchar(50)` column `Transport` to `SmtpConfigurations` — if it is empty, the build was stale: delete the files and redo (see `ef-migrations-add-no-build-stale` gotcha).

**Step 4: Build + existing ConfigurationDatabase tests**
`dotnet test tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests -v minimal` → PASS.

**Step 5: Commit** — `feat(notifications): additive Transport column on SmtpConfigurations`.

---

### Task 3: EWS SOAP envelope builder + response parser (pure)

**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 1, Task 2

**Files:**
- Create: `src/ZB.MOM.WW.ScadaBridge.NotificationService/Ews/EwsSoapEnvelope.cs`
- Create: `src/ZB.MOM.WW.ScadaBridge.NotificationService/Ews/EwsResponseParser.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests/Ews/EwsSoapEnvelopeTests.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests/Ews/EwsResponseParserTests.cs`

**Step 1: Failing tests.** Envelope: builds `CreateItem` with `MessageDisposition="SendOnly"`, `RequestServerVersion Version="Exchange2013"`, subject/body XML-escaped (`<`, `&`, `"` cases), plain-text `BodyType="Text"`, all recipients under `BccRecipients` (none in `ToRecipients` — assert the element is absent), `From` mailbox present, element order Subject → Body → BccRecipients → From (EWS `MessageType` is a schema `sequence`; wrong order = `ErrorSchemaValidation`). Parser: `(Success, "NoError")` for a success body; `(Error, "ErrorServerBusy")` from a `ResponseClass="Error"` message; SOAP 1.1 `<soap:Fault>` body → fault detail `ResponseCode` extracted (`ErrorSchemaValidation`); garbage input → `EwsParseResult.Unparseable`.

Use small inline XML literals for canned responses; namespaces: `soap=http://schemas.xmlsoap.org/soap/envelope/`, `m=…/2006/messages`, `t=…/2006/types`.

**Step 2: Run to verify failure.**

**Step 3: Implement.** `EwsSoapEnvelope.BuildCreateItem(string from, IReadOnlyList<string> bcc, string subject, string body)` returning `string` — build with `XDocument`/`XElement` (escaping for free), structure:

```xml
<soap:Envelope xmlns:soap="…" xmlns:t="…/types" xmlns:m="…/messages">
  <soap:Header><t:RequestServerVersion Version="Exchange2013"/></soap:Header>
  <soap:Body>
    <m:CreateItem MessageDisposition="SendOnly">
      <m:Items><t:Message>
        <t:Subject>…</t:Subject>
        <t:Body BodyType="Text">…</t:Body>
        <t:BccRecipients><t:Mailbox><t:EmailAddress>…</t:EmailAddress></t:Mailbox>…</t:BccRecipients>
        <t:From><t:Mailbox><t:EmailAddress>…</t:EmailAddress></t:Mailbox></t:From>
      </t:Message></m:Items>
    </m:CreateItem>
  </soap:Body>
</soap:Envelope>
```

`EwsResponseParser.Parse(string responseBody)` returning `EwsParseResult` record: `Kind` (`Success` | `Error` | `Fault` | `Unparseable`) + `ResponseCode` (string?) + `MessageText` (string?). Wrap `XDocument.Parse` in try/catch → `Unparseable`. Look for `CreateItemResponseMessage` `ResponseClass` attribute + `m:ResponseCode`; else `soap:Fault` → `detail//t:ResponseCode` (fall back to `faultstring`).

**Step 4: Run tests** → PASS. **Step 5: Commit** — `feat(notifications): EWS CreateItem envelope builder + response parser`.

---

### Task 4: EwsSoapMailSender + DI registration

**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** none (needs Task 3)

**Files:**
- Create: `src/ZB.MOM.WW.ScadaBridge.NotificationService/Ews/IEwsMailSender.cs` (+ `EwsSendRequest` record, `EwsTransientException`, `EwsPermanentException`)
- Create: `src/ZB.MOM.WW.ScadaBridge.NotificationService/Ews/EwsSoapMailSender.cs`
- Modify: `src/ZB.MOM.WW.ScadaBridge.NotificationService/ServiceCollectionExtensions.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests/Ews/EwsSoapMailSenderTests.cs`

**Contract:**

```csharp
/// <summary>One-shot EWS mail submission (CreateItem, SendOnly, BCC-only).</summary>
public interface IEwsMailSender
{
    /// <summary>
    /// Sends via EWS. Throws <see cref="EwsPermanentException"/> for failures retrying cannot
    /// fix (auth 401/403, bad URL 404, schema faults, recipient rejections) and
    /// <see cref="EwsTransientException"/> for availability-shaped failures (network/DNS/timeout,
    /// HTTP 5xx/408/429, ErrorServerBusy-class response codes).
    /// </summary>
    Task SendAsync(EwsSendRequest request, CancellationToken cancellationToken = default);
}

public sealed record EwsSendRequest(
    Uri Endpoint,                       // absolute https EWS URL (from SmtpConfiguration.Host)
    string Username,                    // may be "domain\\user"
    string Password,
    string FromAddress,
    IReadOnlyList<string> BccRecipients,
    string Subject,
    string Body,
    int TimeoutSeconds);                // non-positive = HttpClient default
```

**Implementation notes (Step 3):**
- `public const string HttpClientName = "EwsMail";` on the sender; ctor takes `IHttpClientFactory` + `ILogger<EwsSoapMailSender>`.
- **Explicit Basic header** (deterministic — the live probe proved Basic enabled): `new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}")))` per request message — never on the shared client (pooled handler).
- POST `StringContent(envelope, Encoding.UTF8, "text/xml")` (SOAP 1.1). Timeout via linked CTS (mirror `SmsNotificationDeliveryAdapter`, which leaves `HttpClient.Timeout` at default).
- Classification (transient set chosen in design §4.3):
  - `HttpRequestException`, `TaskCanceledException` when NOT the caller's token → wrap in `EwsTransientException`. Caller-cancelled `OperationCanceledException` propagates unchanged.
  - HTTP 401/403/404/405/410 → `EwsPermanentException`; 408/429/5xx with **unparseable** body → `EwsTransientException`; other unparseable non-success → permanent.
  - Parsed body (any HTTP status): `ResponseCode` in `{ErrorServerBusy, ErrorInternalServerTransientError, ErrorTimeoutExpired, ErrorMailboxStoreUnavailable, ErrorInsufficientResources}` → transient; `Success`/`NoError` → return; any other `Error`/`Fault` code → permanent (message includes the code).
- **Never log or embed the password**: exception messages carry status/response code/`MessageText` only; the Authorization header value must not appear in any message (add a test asserting the thrown message does not contain the base64 credential).

**Tests (Step 1, failing first):** stub `HttpMessageHandler` (capture request; canned responses). Assert: Basic header matches expected base64 of `user:pass`; content type `text/xml`; envelope contains the BCC recipient; success body → no throw; `ErrorServerBusy` body → `EwsTransientException`; 401 → `EwsPermanentException`; `ErrorSchemaValidation` fault (HTTP 500) → **permanent** (the critical case distinguishing parsed-fault-500 from bare-500); bare 503 → transient; socket failure (handler throws `HttpRequestException`) → transient; credential never in exception message.

**DI (in `AddNotificationService`):** `services.AddHttpClient(EwsSoapMailSender.HttpClientName);` + `services.TryAddSingleton<IEwsMailSender, EwsSoapMailSender>();` (stateless; factory-per-request handler).

**Steps 4/5:** filtered test run PASS → commit `feat(notifications): no-SDK EWS SOAP mail sender with typed transient/permanent classification`.

---

### Task 5: EmailNotificationDeliveryAdapter transport branch

**Classification:** high-risk (delivery path + outcome contract)
**Estimated implement time:** ~5 min
**Parallelizable with:** none (needs Tasks 1, 2, 4)

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Delivery/EmailNotificationDeliveryAdapter.cs`
- Modify: `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/ServiceCollectionExtensions.cs` (doc-comment only: note the EWS seam also comes from `AddNotificationService`)
- Test: `tests/ZB.MOM.WW.ScadaBridge.NotificationOutbox.Tests/Delivery/EmailNotificationDeliveryAdapterEwsTests.cs` (new file; leave the existing SMTP adapter tests untouched — they must stay green unmodified, proving the SMTP path is untouched)

**Step 1 (failing tests):** with a config row `Transport="Ews"`, `Host="https://ews.example.test/ews/exchange.asmx"`, `AuthType="basic"`, `Credentials=@"dom\svc:pw"`:
- happy path → `IEwsMailSender` (mock) receives endpoint/user `dom\svc`/pw/from/bcc/subject/body; outcome `Success`.
- sender throws `EwsTransientException` → `Transient`; `EwsPermanentException` → `Permanent`.
- config errors → `Permanent` WITHOUT calling the sender: non-absolute/non-https `Host`; `AuthType="oauth2"`; missing/`:`-less `Credentials`; unknown `Transport="Graph"`.
- `Transport=null` and `Transport="Smtp"` still drive the SMTP path (existing `_smtpClientFactory` invoked, sender mock untouched).

**Step 2:** run new test file → fails.

**Step 3:** Adapter changes — ctor gains `IEwsMailSender? ewsMailSender = null` (optional, matching the `OAuth2TokenService?` pattern so existing test ctor calls compile — but UPDATE the DI doc comment). In `DeliverAsync`, after the SMTP-config load + multiple-row warning and BEFORE the TLS-mode parse, branch:

```csharp
        EmailTransport transport;
        try { transport = EmailTransportParser.Parse(smtpConfig.Transport); }
        catch (ArgumentException ex) { return DeliveryOutcome.Permanent(ex.Message); }

        if (transport == EmailTransport.Ews)
        {
            return await DeliverViaEwsAsync(smtpConfig, recipients, notification, cancellationToken);
        }
        // existing SMTP path continues unchanged below
```

`DeliverViaEwsAsync` (private): validate addresses via the existing `EmailAddressValidator.ValidateAddresses`; validate endpoint (`Uri.TryCreate` absolute + `https`), auth type `basic`, credentials split on FIRST `':'` (password may contain `:`? split-limit 2 like the SMTP path); null `_ewsMailSender` → `Permanent("EWS transport configured but no EWS sender is registered")`; call `SendAsync`; catch `EwsPermanentException` → `Permanent`, `EwsTransientException` → `Transient`, caller-cancelled OCE rethrow, anything else → `Permanent` (default-permanent stance) — every message through `CredentialRedactor.Scrub(msg, smtpConfig.Credentials)`.

**Step 4:** `dotnet test tests/ZB.MOM.WW.ScadaBridge.NotificationOutbox.Tests --filter EmailNotificationDeliveryAdapter -v minimal` → ALL pass (new + pre-existing SMTP tests).

**Step 5:** Commit — `feat(notifications): EWS branch in the email delivery adapter`.

---

### Task 6: Management command + validation + public shape

**Classification:** standard
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 9

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/NotificationCommands.cs:11`
- Modify: `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs` (`SmtpConfigPublicShape` ~2114, `HandleUpdateSmtpConfig` ~2142)
- Test: `tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs` (extend existing UpdateSmtpConfig coverage)

**Changes:**
- `UpdateSmtpConfigCommand` gains a TRAILING optional param `string? Transport = null` (additive-only message evolution; Newtonsoft deserializes missing → null → preserve-if-null. Do NOT reorder existing params).
- `HandleUpdateSmtpConfig`: `if (cmd.Transport is not null) config.Transport = cmd.Transport;` plus validation BEFORE save: parse via `EmailTransportParser.Parse(cmd.Transport ?? config.Transport)` → `ManagementCommandException` on `ArgumentException`; when the effective transport is Ews, require `config.Host` (post-assignment) to be an absolute `https` URI and effective `AuthType` to be `basic` — else `ManagementCommandException` with a message naming the offending field. (NotificationService is already referenced? **Check**: if `ManagementService.csproj` does not reference `ZB.MOM.WW.ScadaBridge.NotificationService`, inline the parse — a local `switch` on the lowercased string — instead of adding a project reference; keeping the dependency graph flat wins over DRY here.)
- `SmtpConfigPublicShape` adds `c.Transport`.
- Tests (failing first): update-with-`Transport="Ews"`+https host+basic → persisted, response shape carries Transport; `Transport="Graph"` → `ManagementCommandException`; Ews with `http://` host → rejected; omitted Transport preserves stored value. RequiredRoleMatrix is unchanged (no new command — do not touch the frozen matrix).

**Run:** `dotnet test tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests --filter UpdateSmtpConfig -v minimal` → PASS. Commit — `feat(management): Transport on UpdateSmtpConfigCommand with EWS shape validation`.

---

### Task 7: CLI --transport flag

**Classification:** small
**Estimated implement time:** ~3 min
**Parallelizable with:** Task 8 (after Task 6)

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/NotificationCommands.cs` (BuildSmtp ~127, options block ~160, `BuildUpdateSmtpConfigCommand` ~207)
- Test: `tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/SmtpUpdateCommandTests.cs`

Add static `SmtpTransportOption`:

```csharp
    private static readonly Option<string?> SmtpTransportOption = CreateTransportOption();

    private static Option<string?> CreateTransportOption()
    {
        var option = new Option<string?>("--transport")
        {
            Description = "Email transport: Smtp or Ews (optional; preserves existing if omitted). " +
                          "Under Ews, --server is the full EWS URL (https://…/ews/exchange.asmx), " +
                          "--auth-mode must be basic, and --port/--tls-mode are unused.",
        };
        option.AcceptOnlyFromAmong("Smtp", "Ews");
        return option;
    }
```

Wire into `updateCmd.Add(...)` and `BuildUpdateSmtpConfigCommand` (pass as the new trailing arg). Tests failing-first in `SmtpUpdateCommandTests` (mirror existing option tests): `--transport Ews` maps through; omitted → null; `--transport Graph` → parse error.

**Run:** `dotnet test tests/ZB.MOM.WW.ScadaBridge.CLI.Tests --filter SmtpUpdate -v minimal` → PASS. Commit — `feat(cli): notification smtp update --transport smtp|ews`.

---

### Task 8: Central UI transport selector

**Classification:** small
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 7 (after Task 6)

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Notifications/SmtpConfiguration.razor`

Read the whole page first (272 lines). Changes, following its existing patterns exactly (Bootstrap, `@bind`, no component libs):
- Read view: a "Transport" row (badge, `@(smtp.Transport ?? "Smtp")`) above Host; when transport is Ews, relabel the Host row "EWS URL" and hide the Port/TLS rows; OAuth2 rows stay gated on AuthType as today. (The management list response now carries `Transport` — the page's row model needs the property added where `OAuth2Authority` etc. are declared.)
- Edit form: `<select @bind="_transport">` with `Smtp`/`Ews` options, default from the loaded row (`?? "Smtp"`); when `_transport == "Ews"` hide Port + TLS + auth-type select (force-display `basic`, set `_authType = "basic"` on switch to Ews), show Host input labelled "EWS URL" with placeholder `https://mail.example.com/ews/exchange.asmx`; credentials placeholder `domain\username:password`. Pass `_transport` as the new `UpdateSmtpConfigCommand` trailing arg where the page builds the command (~line 237 area).
- Playwright: `SmtpConfigTests` exist but are env-gated (live cluster) — do NOT add new Playwright coverage in this task; server-side validation is covered by Task 6.

**Verify:** `dotnet build src/ZB.MOM.WW.ScadaBridge.CentralUI` → 0 errors. Commit — `feat(ui): EWS transport selector on /notifications/smtp`.

---

### Task 9: Transport bundle carriage (Transport + the pre-existing OAuth2 drop)

**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 6

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs` (`SmtpConfigDto` ~288)
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs` (serialize ~168–189, deserialize ~478–490)
- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/EntitySerializerTests.cs`

**Discovered defect this task also fixes:** `OAuth2Authority`/`OAuth2Scope` are NOT carried by `SmtpConfigDto` today — an exported bundle silently drops them on import (exactly the T8 silent-data-loss class). Fix alongside `Transport`.

- `SmtpConfigDto`: add THREE trailing nullable params (additive — old bundles deserialize them as null; serializer's WhenWritingNull keeps them out of old-shaped JSON): `string? OAuth2Authority = null, string? OAuth2Scope = null, string? Transport = null`.
- Serializer: map all three from the entity. Deserializer: restore all three in the object initializer.
- Tests failing-first (extend the existing SMTP round-trip test at ~line 53): a config with `Transport="Ews"`, `OAuth2Authority`/`OAuth2Scope` set survives export→import with all three intact; and a JSON payload WITHOUT the new properties (old bundle) deserializes with all three null.
- `schemaVersion` stays (additive rule); do not bump `bundleFormatVersion`.

**Run:** `dotnet test tests/ZB.MOM.WW.ScadaBridge.Transport.Tests --filter EntitySerializer -v minimal` → PASS. Commit — `fix(transport): carry Transport + OAuth2 authority/scope on SmtpConfigDto (OAuth2 fields were silently dropped)`.

---

### Task 10: Docs sweep

**Classification:** trivial
**Estimated implement time:** ~5 min
**Parallelizable with:** none (last code-adjacent task; needs all prior merged)

**Files:**
- Modify: `docs/requirements/Component-NotificationService.md` — EWS transport section under the SMTP config section (~line 44): config model (field reuse table), Basic-over-HTTPS posture, BCC-only + SendOnly semantics, transient/permanent classification, the no-NTLM decision + its revisit trigger (Basic disabled on the EWS vdir).
- Modify: `docs/plans/questions.md:13` + `docs/plans/phase-7-integrations.md:466` — Q12 status → "Closed 2026-08-10 as superseded — production mail is on-prem Exchange EWS; see docs/plans/2026-08-10-ews-email-transport-design.md".
- Modify: `docs/deployment/production-checklist.md:46` — add the EWS variant lines (outbound HTTPS 443 to the Exchange CAS; service-account password rotation; Basic requires TLS) alongside the SMTP items.
- Modify: `docs/test_infra/test_infra_smtp.md:125` — note the OAuth2 gap is superseded by EWS for on-prem deployments (pointer to the design doc).
- Modify: `docs/plans/2026-08-10-ews-email-transport-design.md` §4.1 — correct "same storage treatment" note: Credentials ARE encrypted at rest via `EncryptedStringConverter` (ConfigurationDatabase), so no asymmetry with SmsConfiguration exists; and note the Task 9 OAuth2-drop fix.
- Modify: `CLAUDE.md` — External Integrations bullet: one line stating email delivery supports SMTP or on-prem Exchange EWS via `SmtpConfiguration.Transport`, EWS = no-SDK CreateItem/Basic/BCC/SendOnly.
- Modify: `README.md` component table row for Notification Service ONLY IF it names SMTP specifically (check first).

Commit — `docs(notifications): EWS transport docs; close Q12 as superseded`.

---

### Task 11: Integration verify + merge back

**Classification:** standard
**Estimated implement time:** ~5 min (plus build/test wall time)
**Parallelizable with:** none

From the worktree root:
1. `dotnet build ZB.MOM.WW.ScadaBridge.slnx` → 0 errors / 0 warnings.
2. Targeted suites (full-suite is a milestone-end exception — this is the milestone end for the feature, but per the targeted-tests rule run the impacted + consumer set, not the world):
   `dotnet test tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests tests/ZB.MOM.WW.ScadaBridge.NotificationOutbox.Tests tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests tests/ZB.MOM.WW.ScadaBridge.CLI.Tests tests/ZB.MOM.WW.ScadaBridge.Transport.Tests tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests -v minimal` → green (known env-only Playwright exclusions don't run here).
3. Verify every task's commit is on the branch (`git log --oneline main..HEAD` — expect ~10 commits).
4. **Do not merge to main or push** — report done; merging is the finishing-a-development-branch flow with the user.

**Known live-gate residue (manual, user-run, after merge):** configure `Transport=Ews` against `webmail.zimmer.com` with the dev/test credentials from the untracked `email_details.txt`, send to a single-recipient test list, verify Delivered + received mail. Not automatable from CI (real server, real creds).

---

## Dependency graph

```
T1 ─┬────────────► T5 ─► T11
T2 ─┤            ▲
T3 ─► T4 ────────┘
T2 ─► T6 ─► T7 ─► T11
          └► T8 ─► T11
T2 ─► T9 ─► T11
T10 after T1–T9
```
Wave 1: T1, T2, T3 (parallel — file-disjoint). Wave 2: T4, T6, T9. Wave 3: T5, T7, T8. Wave 4: T10, then T11.
