diff --git a/dev/mail-testbed/README.md b/dev/mail-testbed/README.md index 59b41d2..096ee18 100644 --- a/dev/mail-testbed/README.md +++ b/dev/mail-testbed/README.md @@ -44,6 +44,57 @@ If the smoke is started immediately after `docker compose up -d`, GreenMail may bind the SMTP/IMAP ports before the services are fully ready. The smoke retries login and folder setup for `GOVOPLAN_MAIL_TEST_READY_TIMEOUT_SECONDS`. +## Campaign Acceptance + +The transport smoke proves the Mail adapters. The Campaign acceptance runner +proves the public composition: it creates an isolated temporary Core database, +creates a Mail-owned encrypted profile through the API, materializes the +credential-free [`greenmail-delivery`](../../examples/greenmail-delivery/campaign.json) +fixture with only that profile reference, validates/builds it, sends the exact +generated EML through Campaign once, appends it once, and cross-checks Campaign +report/audit state with one unique-subject message in the GreenMail INBOX and +Sent folders. Provider mailbox verification is not a byte-for-byte comparison +after provider-side header or storage transformations. + +```bash +cd /mnt/DATA/git/govoplan-campaign/dev/mail-testbed +set -a +. ./.env +set +a +/mnt/DATA/git/govoplan/.venv/bin/python run_campaign_acceptance.py \ + --evidence /tmp/govoplan-campaign-greenmail-evidence.json +``` + +The default run also uses controlled loopback protocol endpoints to prove that +an SMTP connection loss before transmission is temporary, an explicit SMTP +authentication rejection is permanent, and an IMAP authentication rejection +after SMTP acceptance leaves the send accepted while the append fails. A +second ordinary send must be rejected before another provider effect. + +The bounded JSON contains no endpoint, account, address, credential, profile, +campaign, version, or job identifiers. It records module versions, the fixture +hash, normalized classifications/counts, the Mail-profile boundary, required +audit actions, provider mailbox increments, and coverage flags. Runtime version +declarations identify the exercised composition; they do not claim that the +sources are clean, tagged, signed, or release-provenanced. The evidence names +them `declared_module_versions` and keeps `source_artifact_provenance` false; +exact commit/artifact provenance belongs to the package and release gate. + +This runner is restricted to literal loopback IP addresses and the synchronous +Campaign delivery mode. Hostnames such as `localhost` and every non-loopback +address fail before profile creation, avoiding a DNS change between validation +and connection. It is local target-like evidence, not approval of an +institution's SMTP/IMAP service. A post-DATA connection loss +with ambiguous SMTP outcome, an explicit temporary SMTP response, partial +recipient refusal, and a real worker restart remain separate drills and are +explicitly `false` in the coverage projection. Use `--success-only` only when +testing the success journey without the local failure endpoints. + +Starting the maintained test bed requires a working Docker CLI, Compose plugin, +daemon/socket access, and permission to pull `greenmail/standalone:2.1.9`. The +Campaign runner needs only the already-running loopback endpoints; it neither +starts Docker nor claims that it did. + ## Use With A Campaign Use the same settings in a campaign mail profile: diff --git a/docs/CAMPAIGN_DELIVERY_RUNBOOK.md b/docs/CAMPAIGN_DELIVERY_RUNBOOK.md index 60d3ea1..2c99a43 100644 --- a/docs/CAMPAIGN_DELIVERY_RUNBOOK.md +++ b/docs/CAMPAIGN_DELIVERY_RUNBOOK.md @@ -104,6 +104,19 @@ bed where possible: - IMAP append failure after SMTP acceptance. - Worker restart with queued, claimed, and sending jobs. +For the maintained loopback baseline, run +`dev/mail-testbed/run_campaign_acceptance.py`. It proves the public Campaign +path for SMTP acceptance, IMAP append, repeat-send blocking, an SMTP connection +failure before transmission, an explicit SMTP authentication rejection, and an +IMAP authentication rejection after SMTP acceptance. Its evidence is an +allowlisted classification/count projection; raw provider diagnostics and +transport/account identifiers are deliberately excluded. + +The runner does not simulate a connection loss after SMTP DATA and does not +restart a real worker. Keep those two checklist items open until a controlled +fault proxy/target provider and supervised worker environment can produce the +corresponding `outcome_unknown`, restart, retry, and reconciliation evidence. + ## Reporting Checks - Partial delivery must show accepted, failed, and unknown counts separately. diff --git a/docs/EXAMPLE_CAMPAIGNS_AND_RELEASE_CHECKLIST.md b/docs/EXAMPLE_CAMPAIGNS_AND_RELEASE_CHECKLIST.md index e512852..273ddab 100644 --- a/docs/EXAMPLE_CAMPAIGNS_AND_RELEASE_CHECKLIST.md +++ b/docs/EXAMPLE_CAMPAIGNS_AND_RELEASE_CHECKLIST.md @@ -26,7 +26,9 @@ schema and are safe to run in non-production environments. - campaign with blocked recipients or attachment errors that must not be sent - mock delivery campaign that captures SMTP and IMAP append messages in the mail development mailbox -- real non-production delivery campaign against the GreenMail test bed +- [`greenmail-delivery`](../examples/greenmail-delivery/campaign.json), a + credential-free real-delivery Campaign materialized with a temporary + Mail-owned profile by the loopback acceptance runner ## Fixture Rules @@ -53,6 +55,13 @@ Before tagging a campaign release: - Run the mock delivery example when the mail development mailbox capability is enabled. - Run the GreenMail SMTP/IMAP smoke for a non-production real delivery path. +- Run `dev/mail-testbed/run_campaign_acceptance.py` and retain its bounded JSON + projection. It must show one SMTP acceptance, one IMAP append, no duplicate + effect from a repeated ordinary send, matching Campaign report/audit state, + and no resolved transport material in Campaign JSON or its execution + snapshot. +- Do not interpret the local failure drills as post-DATA ambiguity or worker + restart proof; the evidence coverage flags must keep those gaps explicit. - Confirm reusable mail profile selection is revalidated after campaign owner transfer. - Confirm every inline SMTP/IMAP field is rejected on import/write, omitted diff --git a/examples/README.md b/examples/README.md index 09e7d5f..6cd88fa 100644 --- a/examples/README.md +++ b/examples/README.md @@ -19,7 +19,7 @@ campaign schema and do not require production data. | `warnings-review` | core, access, campaigns | Require explicit review before queueing jobs with warnings. | | `blocked-send` | core, access, campaigns | Confirm blocked recipients or missing attachments cannot be queued. | | `mock-delivery` | core, access, campaigns, mail with dev capability | Capture messages in the development mailbox. | -| `greenmail-delivery` | core, access, campaigns, mail | Send no-attachment, normal attachment, and ZIP attachment variants through `dev/mail-testbed`. | +| [`greenmail-delivery`](greenmail-delivery/campaign.json) | core, access, audit, campaigns, mail | Run a credential-free Campaign through a Mail-owned profile, GreenMail SMTP/IMAP, report/audit checks, repeat-send protection, and bounded failure drills. | ## Fixture Rules @@ -44,6 +44,7 @@ Before a release tag: 3. Validate every committed example fixture against the current campaign schema. 4. Build exact messages for each fixture. 5. Run the mock-delivery example when the dev mailbox capability is enabled. -6. Run `dev/mail-testbed/run_transport_smoke.py`. -7. Execute the delivery checklist in +6. Run `dev/mail-testbed/run_transport_smoke.py` for low-level transport and attachment variants. +7. Run `dev/mail-testbed/run_campaign_acceptance.py` for the Campaign journey and bounded evidence. +8. Execute the delivery checklist in `docs/EXAMPLE_CAMPAIGNS_AND_RELEASE_CHECKLIST.md`.