Add bounce mailbox processing

This commit is contained in:
2026-07-31 22:48:07 +02:00
parent 2721ee6542
commit fe309dbff5
13 changed files with 1770 additions and 7 deletions
+62 -2
View File
@@ -7,6 +7,7 @@ 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,
)
@@ -46,6 +47,8 @@ _mail_table_retirement_provider = drop_table_retirement_provider(
mail_models.MailDeliveryReconciliation,
mail_models.MailDeliveryAttempt,
mail_models.MailDeliveryCommand,
mail_models.MailBounceObservation,
mail_models.MailBounceSource,
label="Mail",
)
@@ -111,6 +114,16 @@ PERMISSIONS = (
"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",
@@ -132,6 +145,8 @@ ROLE_TEMPLATES = (
"mail:secret:manage",
"mail:delivery:diagnostic",
"mail:delivery:reconcile",
"mail:bounce:read",
"mail:bounce:manage",
),
),
RoleTemplate(
@@ -184,6 +199,7 @@ manifest = ModuleManifest(
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"),
),
requires_interfaces=(
ModuleInterfaceRequirement(
@@ -208,7 +224,7 @@ manifest = ModuleManifest(
permissions=PERMISSIONS,
route_factory=_mail_router,
role_templates=ROLE_TEMPLATES,
nav_items=(NavItem(path="/mail", label="Mail", icon="mail", required_any=("mail:mailbox:read",), order=50),),
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",
@@ -219,8 +235,15 @@ manifest = ModuleManifest(
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",), order=50),),
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),
@@ -248,6 +271,8 @@ manifest = ModuleManifest(
mail_models.MailDeliveryReconciliation,
mail_models.MailDeliveryAttempt,
mail_models.MailDeliveryCommand,
mail_models.MailBounceObservation,
mail_models.MailBounceSource,
label="Mail",
),
),
@@ -261,6 +286,10 @@ manifest = ModuleManifest(
"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(),
},
documentation=(
DocumentationTopic(
@@ -300,6 +329,37 @@ manifest = ModuleManifest(
],
},
),
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."
),
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", "audit"),
metadata={
"kind": "workflow",
"route": "/mail/bounces",
"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",