docs: expand adaptive Mail guidance

This commit is contained in:
2026-07-21 17:29:08 +02:00
parent 0f1327638e
commit 9d1d9bfb58
2 changed files with 177 additions and 5 deletions

View File

@@ -196,6 +196,7 @@ manifest = ModuleManifest(
links=( links=(
DocumentationLink(label="Mail profiles", href="/api/v1/mail/profiles", kind="api"), 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="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"), DocumentationLink(label="Public mail module documentation", href="https://govplan.add-ideas.de/modules/mail", kind="public"),
), ),
related_modules=("campaigns",), related_modules=("campaigns",),
@@ -203,7 +204,7 @@ manifest = ModuleManifest(
configuration_keys=("mail_profile_policy",), configuration_keys=("mail_profile_policy",),
metadata={ metadata={
"kind": "reference", "kind": "reference",
"route": "/mail", "route": "/settings?section=mail-profiles",
"screen": "Mail profiles and policy", "screen": "Mail profiles and policy",
"section": "Effective profile policy", "section": "Effective profile policy",
"related_topic_ids": [ "related_topic_ids": [
@@ -228,15 +229,15 @@ manifest = ModuleManifest(
), ),
), ),
links=( 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="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"), related_modules=("campaigns", "access"),
unlocks=("Reusable, governed delivery identities without cross-module secret duplication.",), unlocks=("Reusable, governed delivery identities without cross-module secret duplication.",),
metadata={ metadata={
"kind": "reference", "kind": "reference",
"route": "/mail", "route": "/settings?section=mail-profiles",
"screen": "Mail profiles", "screen": "Mail profiles",
"section": "Profile ownership, credentials, and consumers", "section": "Profile ownership, credentials, and consumers",
"related_topic_ids": [ "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,), documentation_providers=(documentation_topics,),
) )

View File

@@ -24,11 +24,29 @@ class MailManifestTests(unittest.TestCase):
], ],
) )
topics = {topic.id: topic for topic in manifest.documentation} 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"] ownership = topics["mail.profile-ownership-and-consumers"]
self.assertEqual(ownership.metadata["kind"], "reference") 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"]) 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: def test_retirement_scrubs_credentials_before_dropping_mail_tables(self) -> None:
events: list[str] = [] events: list[str] = []