From 9d1d9bfb58f552b7fe8be48aa10f1b9a769d123d Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 21 Jul 2026 17:29:08 +0200 Subject: [PATCH] docs: expand adaptive Mail guidance --- src/govoplan_mail/backend/manifest.py | 162 +++++++++++++++++++++++++- tests/test_manifest.py | 20 +++- 2 files changed, 177 insertions(+), 5 deletions(-) diff --git a/src/govoplan_mail/backend/manifest.py b/src/govoplan_mail/backend/manifest.py index 6741390..754d404 100644 --- a/src/govoplan_mail/backend/manifest.py +++ b/src/govoplan_mail/backend/manifest.py @@ -196,6 +196,7 @@ manifest = ModuleManifest( links=( DocumentationLink(label="Mail profiles", href="/api/v1/mail/profiles", kind="api"), DocumentationLink(label="Tenant mail policy", href="/api/v1/mail/policies/tenant", kind="api"), + DocumentationLink(label="Mail handbook", href="govoplan-mail/docs/MAIL_HANDBOOK.md", kind="repository"), DocumentationLink(label="Public mail module documentation", href="https://govplan.add-ideas.de/modules/mail", kind="public"), ), related_modules=("campaigns",), @@ -203,7 +204,7 @@ manifest = ModuleManifest( configuration_keys=("mail_profile_policy",), metadata={ "kind": "reference", - "route": "/mail", + "route": "/settings?section=mail-profiles", "screen": "Mail profiles and policy", "section": "Effective profile policy", "related_topic_ids": [ @@ -228,15 +229,15 @@ manifest = ModuleManifest( ), ), links=( - DocumentationLink(label="Mail profiles", href="/mail", kind="runtime"), + DocumentationLink(label="Mail profiles", href="/settings?section=mail-profiles", kind="runtime"), DocumentationLink(label="Mail profiles API", href="/api/v1/mail/profiles", kind="api"), - DocumentationLink(label="Campaigns", href="/campaigns", kind="runtime"), + DocumentationLink(label="Mail handbook", href="govoplan-mail/docs/MAIL_HANDBOOK.md", kind="repository"), ), related_modules=("campaigns", "access"), unlocks=("Reusable, governed delivery identities without cross-module secret duplication.",), metadata={ "kind": "reference", - "route": "/mail", + "route": "/settings?section=mail-profiles", "screen": "Mail profiles", "section": "Profile ownership, credentials, and consumers", "related_topic_ids": [ @@ -247,6 +248,159 @@ manifest = ModuleManifest( ], }, ), + DocumentationTopic( + id="mail.workflow.choose-and-test-profile", + title="Choose and test a reusable Mail profile", + summary="Select a profile visible in the current context, test its SMTP or IMAP connection, and let the consuming task store only its stable reference.", + body="Profile tests verify current connection and authentication using Mail-owned credentials. They do not prove later Campaign policy authorization, deliverability, recipient acceptance, or future availability. Use the picker rather than entering identifiers or copying transport settings into another module.", + layer="configured", + documentation_types=("user",), + audience=("mail_user", "campaign_manager"), + order=41, + conditions=( + DocumentationCondition( + required_modules=("mail",), + required_scopes=("mail:profile:read", "mail:profile:use", "mail:profile:test"), + ), + ), + links=( + DocumentationLink(label="Mail profiles", href="/settings?section=mail-profiles", kind="runtime"), + DocumentationLink(label="Mail handbook", href="govoplan-mail/docs/MAIL_HANDBOOK.md", kind="repository"), + ), + related_modules=("campaigns",), + unlocks=("Reusable SMTP/IMAP identities that consumers can reference without handling credentials.",), + metadata={ + "kind": "workflow", + "route": "/settings?section=mail-profiles", + "screen": "Mail profiles", + "prerequisites": [ + "Mail is installed and you may read, use, and test profiles visible in the current context.", + "A profile administrator has configured credentials and effective policy.", + ], + "steps": [ + "Open Mail profiles and choose a visible active profile.", + "Review its safe scope, SMTP/IMAP availability, and sender identity without expecting credential values.", + "Run the relevant SMTP or IMAP connectivity/authentication test against a non-production target first.", + "Return to the consuming task and select the same profile through its picker.", + ], + "outcome": "The consuming task references an available Mail-owned profile and contains no copied transport configuration or credentials.", + "verification": "Reload both surfaces, confirm only the stable reference is retained by the consumer, and perform the consumer's own contextual policy validation.", + "related_topic_ids": [ + "mail.profile-ownership-and-consumers", + "mail.reference.campaign-delivery-contract", + ], + }, + ), + DocumentationTopic( + id="mail.workflow.read-mailbox", + title="Read a permitted mailbox without changing it", + summary="Choose an IMAP-enabled profile, browse folders, and inspect bounded message content through the read-only mailbox surface.", + body="Mailbox access requires both mailbox-read and profile-use authority for a profile visible in the actor's scope. Listing folders or messages must not mark mail read, move it, delete it, or expose unbounded content.", + layer="configured", + documentation_types=("user",), + audience=("mail_user",), + order=42, + conditions=( + DocumentationCondition( + required_modules=("mail",), + required_scopes=("mail:mailbox:read", "mail:profile:use"), + ), + ), + links=( + DocumentationLink(label="Mail", href="/mail", kind="runtime"), + DocumentationLink(label="Mail handbook", href="govoplan-mail/docs/MAIL_HANDBOOK.md", kind="repository"), + ), + unlocks=("Read-only access to an authorized operational mailbox without broadening profile administration rights.",), + metadata={ + "kind": "workflow", + "route": "/mail", + "screen": "Mail", + "prerequisites": [ + "An active visible profile has IMAP configured.", + "You may both use that profile and read its mailbox.", + ], + "steps": [ + "Open Mail and choose an authorized IMAP-enabled profile.", + "Select a folder and page through its bounded message index.", + "Open only the message needed for the task and close it when finished.", + ], + "outcome": "The required message was inspected without changing provider mailbox state.", + "verification": "Refresh the provider mailbox independently and confirm no read, move, delete, reply, or flag mutation was caused by GovOPlaN.", + "related_topic_ids": [ + "mail.workflow.choose-and-test-profile", + "mail.reference.credentials-egress-retirement", + ], + }, + ), + DocumentationTopic( + id="mail.reference.credentials-egress-retirement", + title="Protect Mail credentials, network egress, and retirement", + summary="Keep secrets Mail-owned, pin every SMTP/IMAP peer, bound responses, and delete owned credentials immediately with non-secret audit evidence.", + body="Private-network connector access is deployment-wide, but every allowed hostname still resolves to an approved peer that is pinned at socket creation. Unsupported transports fail before connection. Deleting a profile immediately scrubs its owned encrypted SMTP/IMAP passwords and records non-secret audit when secrets existed; a scrub or audit failure rolls the action back. Destructive module retirement applies the same rule before table drop.", + layer="evidence", + documentation_types=("admin",), + audience=("mail_admin", "platform_operator", "security_reviewer", "release_reviewer"), + order=43, + conditions=( + DocumentationCondition( + required_modules=("mail",), + any_scopes=("mail:secret:manage", "mail:profile:write", "system:settings:read"), + ), + ), + links=( + DocumentationLink(label="Mail handbook", href="govoplan-mail/docs/MAIL_HANDBOOK.md", kind="repository"), + DocumentationLink(label="Mail profiles API", href="/api/v1/mail/profiles", kind="api"), + ), + related_modules=("audit",), + unlocks=("A testable Mail trust boundary with fail-closed egress and auditable secret disposal.",), + metadata={ + "kind": "reference", + "route": "/settings?section=mail-profiles", + "screen": "Mail profiles", + "section": "Credentials, network egress, deletion, and retirement", + "verification": "Test DNS rebinding and denied addresses, prove the connected peer is pinned, inject credential-scrub and audit failures, repeat deletion for idempotency, and run the retirement preflight against a snapshot.", + "related_topic_ids": [ + "mail.workflow.choose-and-test-profile", + "mail.reference.campaign-delivery-contract", + "campaigns.reference.composition-assurance", + ], + }, + ), + DocumentationTopic( + id="mail.reference.campaign-delivery-contract", + title="Integrate Campaign through the Mail delivery contract", + summary="Campaign freezes a Mail profile reference and opaque revision; Mail re-authorizes, revision-checks, resolves credentials, and performs the effect in one call.", + body="The mail.campaign_delivery 0.2 contract never returns decrypted credentials or resolved SMTP/IMAP configuration. Mail compares the expected random transport revision before decrypting protocol-specific credentials and returns only bounded sanitized outcomes. Campaign owns durable recipient jobs, retry, and reconciliation. Live report emailing remains disabled until the planned Mail-owned idempotent outbox and attempt ledger are implemented.", + layer="available", + documentation_types=("admin", "user"), + audience=("integrator", "campaign_manager", "campaign_sender", "release_reviewer"), + order=44, + conditions=( + DocumentationCondition( + required_modules=("mail", "campaigns"), + required_scopes=("mail:profile:use", "campaigns:campaign:read"), + ), + ), + links=( + DocumentationLink(label="Campaigns", href="/campaigns", kind="runtime"), + DocumentationLink(label="Mail handbook", href="govoplan-mail/docs/MAIL_HANDBOOK.md", kind="repository"), + DocumentationLink(label="Campaign Mail profile boundary", href="govoplan-campaign/docs/MAIL_PROFILE_BOUNDARY.md", kind="repository"), + ), + related_modules=("campaigns", "audit"), + unlocks=("Profile-backed campaign delivery without cross-module credential or configuration copies.",), + metadata={ + "kind": "reference", + "route": "/campaigns/{campaign_id}/mail-settings", + "screen": "Campaign Mail settings", + "section": "Mail-owned profile and transport boundary", + "verification": "Prove stale revisions fail before credential decryption, SMTP never decrypts IMAP credentials, IMAP never decrypts SMTP credentials, provider details are sanitized, and the interface/version gate passes.", + "related_topic_ids": [ + "mail.profile-ownership-and-consumers", + "campaigns.mail-profile-user-journey", + "campaigns.workflow.retry-and-reconcile", + ], + }, + ), ), documentation_providers=(documentation_topics,), ) diff --git a/tests/test_manifest.py b/tests/test_manifest.py index ea166e8..da493ec 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -24,11 +24,29 @@ class MailManifestTests(unittest.TestCase): ], ) topics = {topic.id: topic for topic in manifest.documentation} + self.assertTrue( + { + "mail.workflow.choose-and-test-profile", + "mail.workflow.read-mailbox", + "mail.reference.credentials-egress-retirement", + "mail.reference.campaign-delivery-contract", + }.issubset(topics) + ) ownership = topics["mail.profile-ownership-and-consumers"] self.assertEqual(ownership.metadata["kind"], "reference") - self.assertEqual(ownership.metadata["route"], "/mail") + self.assertEqual(ownership.metadata["route"], "/settings?section=mail-profiles") self.assertIn("campaigns.mail-profile-user-journey", ownership.metadata["related_topic_ids"]) + mailbox = topics["mail.workflow.read-mailbox"] + self.assertEqual(mailbox.metadata["route"], "/mail") + self.assertEqual( + mailbox.conditions[0].required_scopes, + ("mail:mailbox:read", "mail:profile:use"), + ) + + campaign_contract = topics["mail.reference.campaign-delivery-contract"] + self.assertEqual(campaign_contract.conditions[0].required_modules, ("mail", "campaigns")) + def test_retirement_scrubs_credentials_before_dropping_mail_tables(self) -> None: events: list[str] = []