from __future__ import annotations from dataclasses import replace from pathlib import Path from sqlalchemy import inspect from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER from govoplan_core.core.mail import ( CAPABILITY_MAIL_BOUNCE_PROCESSING, CAPABILITY_MAIL_DELIVERY_OUTBOX, CAPABILITY_MAIL_NOTIFICATION_DELIVERY, CAPABILITY_MAIL_POSTBOX_BRIDGE, ) from govoplan_core.core.postbox import CAPABILITY_POSTBOX_DELIVERY from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard from govoplan_core.core.modules import ( DocumentationCondition, DocumentationConfigurationProviderRegistration, DocumentationLink, DocumentationTopic, FrontendModule, FrontendRoute, MigrationSpec, ModuleContext, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest, NavItem, PermissionDefinition, ProductAreaContribution, QuickAccessTool, RoleTemplate, ) from govoplan_core.core.provider_governance import ( ExternalProviderDeclaration, ExternalProviderStateProviderRegistration, ProviderBehaviorDeclaration, ProviderObjectDeclaration, declared_module_architecture, ) from govoplan_core.core.search import SearchSourceProviderRegistration from govoplan_core.core.views import ViewSurface from govoplan_core.db.base import Base from govoplan_mail.backend.configuration_provider import MAIL_CONFIGURATION_CAPABILITY from govoplan_mail.backend.documentation import ( documentation_configuration_states, documentation_topics, ) from govoplan_mail.backend.provider_state import ( IMAP_PROVIDER_ID, SMTP_PROVIDER_ID, imap_provider_states, smtp_provider_states, ) from govoplan_mail.backend.db import models as mail_models # noqa: F401 - populate Mail ORM metadata from govoplan_mail.backend.search_source import create_mail_search_source _mail_table_retirement_provider = drop_table_retirement_provider( mail_models.MailServerCredentialBinding, mail_models.MailServerEndpoint, mail_models.MailServerProfile, mail_models.MailProfilePolicy, mail_models.MailMailboxFolderIndex, mail_models.MailMailboxMessageIndex, mail_models.MailDeliveryReconciliation, mail_models.MailDeliveryAttempt, mail_models.MailDeliveryCommand, mail_models.MailBounceObservation, mail_models.MailBounceSource, label="Mail", ) def _mail_retirement_provider(session: object | None, module_id: str): plan = _mail_table_retirement_provider(session, module_id) base_executor = plan.destroy_data_executor if base_executor is None: return plan def executor(execute_session: object, execute_module_id: str) -> None: if not hasattr(execute_session, "get_bind") or not hasattr(execute_session, "query"): raise RuntimeError("No database session is available for Mail credential retirement.") if inspect(execute_session.get_bind()).has_table(mail_models.MailServerProfile.__tablename__): from govoplan_mail.backend.mail_profiles import delete_mail_profile_credentials_for_retirement delete_mail_profile_credentials_for_retirement(execute_session) base_executor(execute_session, execute_module_id) return replace( plan, destroy_data_warnings=( *plan.destroy_data_warnings, "Mail-owned encrypted SMTP/IMAP passwords are scrubbed with non-secret audit records immediately before tables are dropped; any scrub or audit failure blocks retirement.", ), destroy_data_executor=executor, ) def _configuration_provider(context: ModuleContext) -> object: del context from govoplan_mail.backend.configuration_provider import SqlMailConfigurationProvider return SqlMailConfigurationProvider() def _permission(scope: str, label: str, description: str) -> PermissionDefinition: module_id, resource, action = scope.split(":", 2) return PermissionDefinition( scope=scope, label=label, description=description, category="Mail", level="tenant", module_id=module_id, resource=resource, action=action, ) PERMISSIONS = ( _permission("mail:profile:read", "View mail profiles", "Inspect reusable SMTP/IMAP profile metadata."), _permission("mail:profile:use", "Use mail profiles", "Select an approved mail profile for delivery."), _permission("mail:profile:test", "Test mail profiles", "Run SMTP/IMAP connection tests."), _permission("mail:mailbox:read", "Read mailboxes", "List IMAP folders and inspect messages without mutating mailbox state."), _permission("mail:profile:write", "Manage mail profiles", "Create and edit reusable mail profiles."), _permission( "mail:profile:write_own", "Manage own mail profiles", "Create, edit, and deactivate only the current account's user-scoped mail profiles within effective policy.", ), _permission("mail:secret:manage", "Manage mail secrets", "Create or replace stored SMTP/IMAP credentials."), _permission( "mail:delivery:diagnostic", "Inspect mail delivery diagnostics", "Inspect bounded recipient-level refusal and attempt evidence for durable Mail commands.", ), _permission( "mail:delivery:reconcile", "Reconcile mail delivery outcomes", "Reconcile unknown delivery outcomes and explicitly authorize deliberate resend commands.", ), _permission( "mail:bounce:read", "View bounce processing", "Inspect bounded delivery-status observations and their correlation state.", ), _permission( "mail:bounce:manage", "Manage bounce processing", "Configure and run IMAP delivery-status watchers.", ), _permission( "mail:secret:manage_own", "Manage own mail secrets", "Create, replace, and delete credentials only for the current account's user-scoped mail profiles.", ), ) ROLE_TEMPLATES = ( RoleTemplate( slug="mail_profile_admin", name="Mail profile administrator", description="Manage reusable mail profiles and credentials.", permissions=( "mail:profile:read", "mail:profile:use", "mail:profile:test", "mail:mailbox:read", "mail:profile:write", "mail:secret:manage", "mail:delivery:diagnostic", "mail:delivery:reconcile", "mail:bounce:read", "mail:bounce:manage", ), ), RoleTemplate( slug="mail_profile_user", name="Mail profile user", description="Use and test approved mail profiles without reading secrets.", permissions=("mail:profile:read", "mail:profile:use", "mail:profile:test", "mail:mailbox:read"), ), RoleTemplate( slug="mail_profile_self_service", name="Mail profile self-service user", description="Create and manage only personal Mail profiles and credentials within effective policy.", permissions=( "mail:profile:read", "mail:profile:use", "mail:profile:test", "mail:mailbox:read", "mail:profile:write_own", "mail:secret:manage_own", ), ), ) def _mail_router(context: ModuleContext): from govoplan_mail.backend.runtime import configure_runtime configure_runtime(registry=context.registry, settings=context.settings) from fastapi import APIRouter from govoplan_mail.backend.router import router aggregate = APIRouter() aggregate.include_router(router) app_env = str(getattr(context.settings, "app_env", "dev")).lower() if app_env == "dev" and bool(getattr(context.settings, "dev_mailbox_api_enabled", False)): from govoplan_mail.backend.dev_router import router as dev_router aggregate.include_router(dev_router) return aggregate SMTP_PROVIDER = ExternalProviderDeclaration( id=SMTP_PROVIDER_ID, module_id="mail", label="SMTP message delivery", maturity="publish", operations=("discover", "read", "publish", "preview", "dry_run"), objects=( ProviderObjectDeclaration( object_type="mail_server_endpoint", field_groups=("identity", "transport", "policy", "revision"), authority_modes=("external_authoritative", "governance_overlay"), default_authority_mode="governance_overlay", ), ProviderObjectDeclaration( object_type="outbound_message", field_groups=("envelope", "content_digest", "delivery_state", "evidence"), authority_modes=("governance_overlay",), default_authority_mode="governance_overlay", ), ), behavior=ProviderBehaviorDeclaration( revision_tokens="Every command pins the selected SMTP endpoint, credential, and random transport revision.", concurrency="The expected transport revision is checked before credentials are decrypted or an effect starts.", freshness="Delivery outcomes and completion times are retained; transport freshness is not periodic state.", health="Successful deliveries and unresolved outcome-unknown commands are projected without exposing server details.", max_read_items=5000, idempotency="Tenant, command type, and idempotency key bind one canonical encrypted delivery request.", retry="Only classified pre-acceptance temporary failures are retried with bounded scheduling.", timeout_seconds=60, conflicts="A reused idempotency key with different content is rejected before delivery.", outcome_unknown="A connection failure after effect start becomes outcome_unknown and is never blindly retried.", outcome_unknown_supported=True, evidence="Encrypted command, attempt, acceptance/refusal summary, effect-start marker, and reconciliation are retained.", audit_event_types=( "mail.delivery_requested", "mail.delivery_completed", "mail.delivery_reconciled", ), correction="A reconciled new command is a separate auditable effect and does not rewrite the original outcome.", rollback="SMTP acceptance cannot be rolled back.", compensation="A follow-up message or domain correction is the only safe compensation after acceptance.", reconciliation="Operators record provider evidence and choose accepted or not-accepted before any resend.", outage="Pending commands remain durable; accepted or outcome-unknown commands are not redelivered automatically.", classifications=("confidential", "personal", "special_category"), purposes=("governed message delivery", "notification delivery"), retention="Payload, delivery evidence, and audit records follow separate configured retention policies.", secret_handling="Credentials are decrypted only inside Mail after authorization and revision validation.", ), interface_names=("mail.campaign_delivery", "mail.delivery_commands", "mail.delivery_outbox"), documentation_topic_ids=("mail.reference.campaign-delivery-contract",), ) IMAP_PROVIDER = ExternalProviderDeclaration( id=IMAP_PROVIDER_ID, module_id="mail", label="IMAP mailbox projection", maturity="read", operations=("discover", "search", "read", "preview"), objects=( ProviderObjectDeclaration( object_type="mailbox_folder", field_groups=("identity", "flags", "counts", "revision"), authority_modes=("external_authoritative", "external_mirror"), default_authority_mode="external_mirror", ), ProviderObjectDeclaration( object_type="mailbox_message", field_groups=("identity", "headers", "body_preview", "flags", "source_metadata"), authority_modes=("external_authoritative", "external_mirror"), default_authority_mode="external_mirror", ), ), behavior=ProviderBehaviorDeclaration( revision_tokens="UIDVALIDITY, UID, folder, flags, and transport revision identify mailbox observations.", concurrency="Bounded reads pin the profile transport revision and never mutate message flags.", freshness="Folder and message index timestamps state when the external mailbox was last observed.", health="Index state and bounce-source errors are projected independently of secret profile fields.", max_read_items=500, evidence="Mailbox index rows and bounce observations retain bounded source identities and observation times.", audit_event_types=("mail.mailbox.read", "mail.bounce.observed"), correction="A later mailbox refresh replaces the derived projection while source-owned history remains external.", reconciliation="UIDVALIDITY changes invalidate the affected derived index before a bounded refresh.", outage="The last derived index remains readable with stale or unknown freshness where policy permits.", classifications=("confidential", "personal", "special_category"), purposes=("mailbox access", "delivery-status processing"), retention="Derived mailbox indexes and bounce evidence follow Mail retention policy.", secret_handling="IMAP credentials remain encrypted and are never returned through mailbox or provider-state APIs.", ), documentation_topic_ids=("mail.workflow.read-mailbox",), ) manifest = ModuleManifest( id="mail", name="Mail", version="0.1.18", required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), optional_dependencies=("campaigns", "addresses", "calendar", "postbox", "search"), provides_interfaces=( ModuleInterfaceProvider(name="mail.campaign_delivery", version="0.2.0"), ModuleInterfaceProvider(name="mail.delivery_commands", version="0.1.0"), ModuleInterfaceProvider(name="mail.delivery_outbox", version="0.1.0"), ModuleInterfaceProvider(name="mail.notification_delivery", version="0.1.0"), ModuleInterfaceProvider(name="mail.bounce_processing", version="0.1.0"), ModuleInterfaceProvider(name=CAPABILITY_MAIL_POSTBOX_BRIDGE, version="1.0.0"), ), requires_interfaces=( ModuleInterfaceRequirement( name="calendar.invitations", version_min="0.2.0", version_max_exclusive="0.3.0", optional=True, ), ModuleInterfaceRequirement( name="campaigns.access", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name="campaigns.mail_policy_context", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name="addresses.lookup", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name=CAPABILITY_POSTBOX_DELIVERY, version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name="search.source", version_min="1.0.0", version_max_exclusive="2.0.0", optional=True, ), ), permissions=PERMISSIONS, route_factory=_mail_router, role_templates=ROLE_TEMPLATES, search_sources=( SearchSourceProviderRegistration( id="mail.mailbox_messages", factory=create_mail_search_source, ), ), nav_items=(NavItem(path="/mail", label="Mail", icon="mail", required_any=("mail:mailbox:read", "mail:bounce:read", "mail:bounce:manage"), order=50),), frontend=FrontendModule( module_id="mail", package_name="@govoplan/mail-webui", routes=( FrontendRoute( path="/mail", component="MailboxPage", required_any=("mail:mailbox:read",), order=50, ), FrontendRoute( path="/mail/bounces", component="MailBouncePage", required_any=("mail:bounce:read", "mail:bounce:manage"), order=51, surface_id="mail.bounce-processing", ), ), nav_items=(NavItem(path="/mail", label="Mail", icon="mail", required_any=("mail:mailbox:read", "mail:bounce:read", "mail:bounce:manage"), order=50),), view_surfaces=( ViewSurface(id="mail.admin.system-servers", module_id="mail", kind="section", label="System mail servers", order=70), ViewSurface(id="mail.admin.tenant-servers", module_id="mail", kind="section", label="Tenant mail servers", order=60), ViewSurface(id="mail.admin.group-servers", module_id="mail", kind="section", label="Group mail servers", order=20), ViewSurface(id="mail.admin.user-servers", module_id="mail", kind="section", label="User mail servers", order=20), ViewSurface(id="mail.settings.profiles", module_id="mail", kind="section", label="Personal mail profiles", order=10), ViewSurface(id="mail.quick_access.messages", module_id="mail", kind="quick_access", label="Mail Quick Access", order=80), ), product_areas=( ProductAreaContribution( id="communication", module_id="mail", label="i18n:govoplan-core.product_area.communication", icon="mail", description="i18n:govoplan-core.product_area.communication_description", surface_ids=("mail.nav.mail", "mail.route.mail"), order=40, ), ), quick_access_tools=( QuickAccessTool( id="mail.messages", module_id="mail", category_id="messages", label="i18n:govoplan-mail.mail.92379cbb", description="i18n:govoplan-mail.quick_access_description", surface_id="mail.quick_access.messages", icon="mail", full_page_path="/mail", required_any=("mail:mailbox:read",), order=10, modes=("browse", "compose"), ), ), ), migration_spec=MigrationSpec( module_id="mail", metadata=Base.metadata, script_location=str(Path(__file__).with_name("migrations") / "versions"), retirement_supported=True, retirement_provider=_mail_retirement_provider, retirement_notes="Destructive retirement first scrubs and audits Mail-owned credentials, then drops Mail-owned database tables after the installer captures a database snapshot.", ), uninstall_guard_providers=( persistent_table_uninstall_guard( mail_models.MailServerCredentialBinding, mail_models.MailServerEndpoint, mail_models.MailServerProfile, mail_models.MailProfilePolicy, mail_models.MailMailboxFolderIndex, mail_models.MailMailboxMessageIndex, mail_models.MailDeliveryReconciliation, mail_models.MailDeliveryAttempt, mail_models.MailDeliveryCommand, mail_models.MailBounceObservation, mail_models.MailBounceSource, label="Mail", ), ), capability_factories={ MAIL_CONFIGURATION_CAPABILITY: _configuration_provider, "mail.campaign_delivery": lambda context: __import__("govoplan_mail.backend.capabilities", fromlist=["campaign_capability"]).campaign_capability(context), CAPABILITY_MAIL_DELIVERY_OUTBOX: lambda context: __import__( "govoplan_mail.backend.capabilities", fromlist=["delivery_outbox_capability"], ).delivery_outbox_capability(context), CAPABILITY_MAIL_NOTIFICATION_DELIVERY: lambda context: __import__( "govoplan_mail.backend.capabilities", fromlist=["notification_delivery_capability"], ).notification_delivery_capability(context), CAPABILITY_MAIL_BOUNCE_PROCESSING: lambda context: __import__( "govoplan_mail.backend.bounce_processing", fromlist=["SqlMailBounceProcessingProvider"], ).SqlMailBounceProcessingProvider(), CAPABILITY_MAIL_POSTBOX_BRIDGE: lambda context: __import__( "govoplan_mail.backend.postbox_bridge", fromlist=["create_postbox_bridge"], ).create_postbox_bridge(context), }, documentation=( DocumentationTopic( id="mail.postbox.bridge", title="Bridge selected Mail observations into Postbox", summary="Deliver one immutable IMAP observation to an explicit function Postbox without turning Postbox into a mailbox account.", body=( "Mail exposes an optional, idempotent bridge capability. A configured caller supplies an exact Mail profile, folder, UIDVALIDITY, UID, raw observation, " "and typed Postbox target. Mail parses bounded headers, plaintext, participants, and attachment evidence; Postbox owns target resolution, " "delivery, retention, receipts, and access. The bridge stores no credentials in Postbox and never treats account login as Postbox membership." ), layer="configured", documentation_types=("admin", "user"), audience=("mail_user", "mail_admin", "administrator"), related_modules=("postbox", "idm", "organizations"), translations={ "de": { "title": "Ausgewählte Mail-Beobachtungen an Postbox übergeben", "summary": "Eine unveränderliche IMAP-Beobachtung an ein ausdrücklich bestimmtes Funktionspostfach liefern, ohne Postbox zu einem Mailkonto zu machen.", "body": ( "Mail stellt eine optionale, idempotente Bridge-Capability bereit. Ein konfigurierter Aufrufer übergibt ein exaktes Mailprofil, Ordner, UIDVALIDITY, UID, die rohe Beobachtung " "und ein typisiertes Postbox-Ziel. Mail liest begrenzte Kopfzeilen, Klartext, Beteiligte und Anlagennachweise; Postbox besitzt Zielauflösung, Zustellung, " "Aufbewahrung, Belege und Zugriff. Die Bridge speichert keine Zugangsdaten in Postbox und behandelt die Kontoanmeldung niemals als Postfachmitgliedschaft." ), } }, metadata={"kind": "guide", "help_contexts": ["mail.postbox-bridge"]}, order=36, ), DocumentationTopic( id="mail.configuration-package.smtp-profile", title="Provision SMTP profiles from deployment receipts", summary="Review and apply an idempotent Mail profile without moving plaintext credentials through a package.", body=( "The Mail configuration provider reads the validated mail.smtp deployment capability and derives authoritative endpoint metadata. " "Missing non-secret transport fields are collected by preflight; authentication is represented only by an existing credential-envelope id. " "Tenant scope is the default, system scope requires system authority, conflicting profiles are preserved unless update is explicitly reviewed, " "and an unchanged second apply is a no-op. SMTP reachability remains a separate Mail profile test." ), layer="configured", documentation_types=("admin",), audience=("mail_admin", "administrator", "operator"), related_modules=("core", "ops", "access"), conditions=( DocumentationCondition( required_modules=("mail", "access"), any_scopes=("admin:settings:read", "system:settings:read"), ), ), links=( DocumentationLink( label="Configuration packages", href="/admin?section=configuration-packages", kind="runtime", ), DocumentationLink( label="Mail handbook", href="govoplan-mail/docs/MAIL_HANDBOOK.md", kind="repository", ), ), translations={ "de": { "title": "SMTP-Profile aus Bereitstellungsnachweisen einrichten", "summary": "Ein idempotentes Mail-Profil prüfen und anwenden, ohne Klartext-Zugangsdaten durch ein Konfigurationspaket zu übertragen.", "body": ( "Der Mail-Konfigurationsprovider liest die validierte Bereitstellungsfähigkeit mail.smtp und übernimmt deren maßgebliche Endpunktdaten. " "Fehlende nicht geheime Transportangaben werden im Preflight abgefragt; Authentifizierung wird ausschließlich durch die ID eines vorhandenen Zugangsdaten-Umschlags referenziert. " "Mandantenbezug ist der Standard, Systembezug erfordert Systemberechtigung, abweichende vorhandene Profile bleiben ohne ausdrücklich geprüfte Aktualisierung unverändert, " "und eine unveränderte zweite Anwendung bleibt wirkungslos. Die SMTP-Erreichbarkeit wird weiterhin separat im Mail-Profil getestet." ), } }, metadata={ "kind": "workflow", "route": "/admin?section=configuration-packages", "help_contexts": ["admin.configuration-packages", "mail.admin.profiles"], }, order=4, ), DocumentationTopic( id="mail.quick-access-and-product-area", title="Mail in Communication and Messages", summary="Use Mail in the Communication product area and the shared Messages Quick Access drawer.", body=( "Mail contributes its full mailbox to Communication. When Quick Access is enabled, its compact provider surface " "appears inside the shared Messages drawer alongside independent Postbox and future chat contributions. " "The shared drawer does not merge channel state, credentials, custody, delivery semantics, or authorization." ), layer="configured", documentation_types=("user", "admin"), audience=("mail_user", "mail_admin", "administrator"), related_modules=("quick_access", "views", "postbox"), translations={ "de": { "title": "Mail in Kommunikation und Nachrichten", "summary": "Mail im Produktbereich Kommunikation und in der gemeinsamen Schnellzugriffseinblendung Nachrichten verwenden.", "body": ( "Mail ordnet das vollständige Postfach Kommunikation zu. Ist der Schnellzugriff aktiviert, erscheint die kompakte " "Mail-Oberfläche gemeinsam mit unabhängigen Beiträgen aus Postbox und künftig Chat unter Nachrichten. " "Die gemeinsame Darstellung führt weder Kanalzustand noch Zugangsdaten, Verwahrung oder Berechtigungen zusammen." ), } }, metadata={"kind": "reference", "help_contexts": ["mail.quick_access.messages"]}, order=37, ), DocumentationTopic( id="mail.search.mailbox-messages", title="Search authorized mailbox messages", summary="Expose bounded cached mailbox message metadata and previews to permission-aware platform Search.", body=( "When Search is installed, Mail contributes its read-only mailbox cache, never credentials or " "unbounded raw messages. Results require both mailbox-read and profile-use authority and recheck " "the current profile scope, policy, activity, tenant, user, group, or Campaign visibility before " "returning a result. Mailbox refreshes publish durable index changes; a rebuild remains available " "for reconciliation." ), layer="configured", documentation_types=("admin", "user"), audience=("mail_user", "mail_admin", "administrator"), related_modules=("search",), order=38, ), DocumentationTopic( id="mail.profiles-and-policy", title="Mail profiles and policy hierarchy", summary="Mail sending is configured through reusable SMTP/IMAP profiles and an effective policy assembled from system, tenant, owner, and campaign scope where applicable.", body="The active policy decides whether users can only choose approved profiles or whether user, group, and campaign scopes may define additional reusable profiles. Runtime documentation adds the current tenant posture when the actor may read mail profile policy.", layer="configured", documentation_types=("admin",), audience=("tenant_admin", "mail_admin", "campaign_admin"), order=39, i18n_key="mail.topic.profiles_and_policy", conditions=( DocumentationCondition( required_modules=("mail",), any_scopes=("mail:profile:read", "admin:policies:read", "system:settings:read"), configuration_keys=("mail_profile_policy",), ), ), 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",), unlocks=("Campaign-local delivery rules become visible when govoplan-campaign is installed.",), configuration_keys=("mail_profile_policy",), metadata={ "kind": "reference", "route": "/settings?section=mail-profiles", "screen": "Mail profiles and policy", "section": "Effective profile policy", "help_contexts": ["mail.profiles", "mail.admin.profiles"], "related_topic_ids": [ "mail.profile-ownership-and-consumers", "campaigns.mail-profile-governance", ], }, ), DocumentationTopic( id="mail.profile-standard-folder-mappings", title="Map standard folders for an IMAP profile", summary="Store Inbox, Sent, Drafts, Trash, Archive, and Junk mappings on the reusable Mail profile and populate them from bounded IMAP discovery.", body=( "Folder names are profile/server metadata, not Campaign settings. An authorized profile administrator may enter names directly or use folder discovery; empty mappings retain automatic behavior. Existing imap.sent_folder values are presented and persisted as the Sent mapping, while the compatibility field remains synchronized for consumers that still read it. A Campaign-specific Sent-folder override remains authoritative for that Campaign and is not rewritten by profile discovery. Folder discovery lists provider-visible names without creating, renaming, moving, or deleting remote folders." ), layer="configured", documentation_types=("admin", "user"), audience=("mail_admin", "campaign_admin", "mail_user"), order=40, conditions=( DocumentationCondition( required_modules=("mail",), required_scopes=("mail:profile:read",), any_scopes=("mail:profile:write", "mail:profile:write_own", "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",), metadata={ "kind": "workflow", "route": "/settings?section=mail-profiles", "screen": "Mail profiles", "section": "IMAP standard folder mappings", "help_contexts": ["mail.profiles", "mail.admin.profiles"], "steps": [ "Open an editable IMAP server in a reusable Mail profile.", "Choose Detect folders to load the provider-visible folder names, or enter exact names manually.", "Apply the detected mappings, review every role, and leave uncertain roles empty for automatic behavior.", "Save the profile and reload it to verify the mappings were retained.", ], "verification": "Confirm legacy Sent-only profiles show the same effective Sent mapping, and confirm a Campaign-local Sent override remains unchanged.", }, ), DocumentationTopic( id="mail.bounce-processing", title="Delivery-status and bounce processing", summary="Watch an authorized IMAP folder and correlate DSN outcomes with durable Mail commands.", body=( "Mail stores the outgoing RFC Message-ID with its durable command, parses " "bounded message/delivery-status reports without changing mailbox flags, " "and records idempotent per-recipient observations. SMTP acceptance remains " "separate from a later bounce. Unmatched reports remain visible for review; " "Mail stores only bounded diagnostics and a raw digest, not the raw bounce body. " "When Calendar is active, the same bounded source scan forwards METHOD:REPLY " "iCalendar parts to Calendar for idempotent attendee reconciliation without " "classifying the message as a bounce." ), layer="configured", documentation_types=("admin", "user"), audience=("mail_admin", "campaign_manager", "release_reviewer"), order=43, conditions=( DocumentationCondition( required_modules=("mail",), any_scopes=("mail:bounce:read", "mail:bounce:manage"), ), ), related_modules=("campaigns", "calendar", "audit"), metadata={ "kind": "workflow", "route": "/mail/bounces", "screen": "Bounce processing", "help_contexts": ["mail.bounces", "mail.bounce-processing"], "verification": ( "Send a message with a unique Message-ID, ingest a DSN twice, and " "verify one correlated observation while the SMTP acceptance remains intact." ), }, ), DocumentationTopic( id="mail.profile-ownership-and-consumers", title="Mail owns transport profiles and credentials", summary="Other modules select authorized Mail profiles by stable identifier; they do not copy SMTP/IMAP settings or secrets.", body="Mail encrypts credentials, tests connections, evaluates profile scope and policy, and performs revision-gated transport effects without returning resolved configuration. Random persisted revisions rotate when normalized transport or account identity changes, while password-only rotation remains transparent to built business intent. Campaign stores only server.mail_profile_id plus sanitized delivery evidence. Campaign-local transport fields are rejected, and legacy campaign records fail closed until an explicit profile migration preserves the source audit record and updates an editable version.", layer="available", documentation_types=("admin", "user"), audience=("mail_user", "mail_admin", "campaign_manager", "campaign_sender"), order=40, conditions=( DocumentationCondition( required_modules=("mail",), any_scopes=( "mail:profile:read", "mail:profile:use", "mail:profile:write", "mail:profile:write_own", ), ), ), links=( 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 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": "/settings?section=mail-profiles", "screen": "Mail profiles", "section": "Profile ownership, credentials, and consumers", "related_topic_ids": [ "mail.profiles-and-policy", "campaigns.mail-profile-user-journey", "campaigns.mail-profile-governance", "campaigns.mail-profile-operations", ], }, ), 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", "help_contexts": ["mail.profiles", "app.settings"], "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", "help_contexts": ["mail.list", "mail.mailbox"], "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 ordinary recipient jobs; report messages use Mail's encrypted idempotent delivery-command and attempt ledger. Every current SMTP and Sent-folder attempt passes a stable effect identifier into a Mail-owned Core recovery operation before provider contact. Effect-start evidence prevents blind redelivery, unknown outcomes require explicit reconciliation, and raw recipient refusals require Mail diagnostic authority. Mail outbox dispatch and retention scans are partitioned by tenant entitlement, so disabling Mail leaves accepted commands and evidence untouched for operator resolution.", 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_configuration_providers=( DocumentationConfigurationProviderRegistration( keys=("mail_profile_policy",), resolve=documentation_configuration_states, ), ), external_providers=(SMTP_PROVIDER, IMAP_PROVIDER), external_provider_state_providers=( ExternalProviderStateProviderRegistration( module_id="mail", provider_id=SMTP_PROVIDER_ID, provider=smtp_provider_states, ), ExternalProviderStateProviderRegistration( module_id="mail", provider_id=IMAP_PROVIDER_ID, provider=imap_provider_states, ), ), architecture=declared_module_architecture( layer="communication_participation", kind="integration", maturity="vertical_slice", documentation_ref="docs/MAIL_HANDBOOK.md", test_ref="tests/test_delivery_outbox.py", known_limits=("A complete webmail profile and recovery adoption for future provider-side mailbox mutations are not reference-ready.",), supported_authority_modes=( "external_authoritative", "external_mirror", "governance_overlay", ), owned_concepts=("mail profile", "mail delivery command", "delivery attempt", "mailbox projection"), non_owned_concepts=("campaign", "notification", "recipient address directory", "external mailbox"), target_tested_providers=(SMTP_PROVIDER_ID, IMAP_PROVIDER_ID), recovery_docs=("docs/MAIL_HANDBOOK.md",), security_docs=("docs/MAIL_HANDBOOK.md",), operations_docs=("docs/MAIL_HANDBOOK.md",), ), ) def get_manifest() -> ModuleManifest: return manifest