feat: provide governed notification email delivery

This commit is contained in:
2026-07-30 17:42:07 +02:00
parent b8029c24d6
commit 0cb6719b91
4 changed files with 203 additions and 2 deletions
+102
View File
@@ -1,10 +1,13 @@
from __future__ import annotations
from dataclasses import dataclass
from email.message import EmailMessage
from email.utils import formatdate, make_msgid
from typing import Any
from sqlalchemy.orm import Session
from govoplan_core.core.mail import NotificationMailDeliveryRequest
from govoplan_core.core.modules import ModuleContext
from govoplan_mail.backend.mail_profiles import (
MailProfileError,
@@ -554,3 +557,102 @@ def delivery_outbox_capability(context: ModuleContext):
configure_runtime(settings=context.settings)
return MailDeliveryOutboxCapability()
class MailNotificationDeliveryCapability:
"""Submit notification email to the Mail-owned durable outbox."""
def submit_notification_mail(
self,
session: Session,
request: NotificationMailDeliveryRequest,
) -> dict[str, object]:
profile_id = str(request.mail_profile_id or "").strip()
from_address = str(request.from_address or "").strip()
if not profile_id or not from_address:
return {
"status": "paused",
"provider": "mail.notificationDelivery",
"error": (
"Notification email requires a Mail profile and sender "
"selected by tenant policy."
),
}
try:
transport = campaign_profile_delivery_summary(
session,
tenant_id=request.tenant_id,
profile_id=profile_id,
smtp_server_id=request.smtp_server_id,
smtp_credential_id=request.smtp_credential_id,
)
except MailProfileError:
return {
"status": "paused",
"provider": "mail.notificationDelivery",
"error": (
"The selected notification Mail profile is unavailable or "
"blocked by effective policy."
),
}
if not transport.get("smtp_available"):
return {
"status": "paused",
"provider": "mail.notificationDelivery",
"error": "The selected notification Mail profile has no usable SMTP transport.",
}
message = EmailMessage()
message["Date"] = formatdate(localtime=True)
message["Message-ID"] = make_msgid()
message["Subject"] = request.subject
message["From"] = from_address
message["To"] = request.recipient
message.set_content(request.body_text)
if request.body_html:
message.add_alternative(request.body_html, subtype="html")
from govoplan_mail.backend.delivery_outbox import submit_delivery_command
command = submit_delivery_command(
session,
tenant_id=request.tenant_id,
command_type="notification",
source_module="notifications",
source_resource_type="notification",
source_resource_id=request.notification_id,
source_version_id=None,
idempotency_key=f"notification:{request.notification_id}",
profile_id=profile_id,
smtp_server_id=(
str(transport.get("smtp_server_id"))
if transport.get("smtp_server_id")
else request.smtp_server_id
),
smtp_credential_id=(
str(transport.get("smtp_credential_id"))
if transport.get("smtp_credential_id")
else request.smtp_credential_id
),
message_bytes=bytes(message),
envelope_from=from_address,
envelope_recipients=[request.recipient],
from_header=from_address,
expected_smtp_transport_revision=str(
transport["smtp_transport_revision"]
),
)
return {
"status": "accepted",
"provider": "mail.delivery_outbox",
"external_message_id": command["id"],
"delivery_status": command["status"],
"duplicate": bool(command.get("duplicate")),
}
def notification_delivery_capability(
context: ModuleContext,
) -> MailNotificationDeliveryCapability:
configure_runtime(settings=context.settings)
return MailNotificationDeliveryCapability()
+1 -1
View File
@@ -7,7 +7,7 @@ from collections import Counter
from datetime import datetime, timedelta, timezone
from typing import Any
from sqlalchemy import and_, or_, select
from sqlalchemy import or_, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
+9 -1
View File
@@ -6,7 +6,10 @@ 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_DELIVERY_OUTBOX
from govoplan_core.core.mail import (
CAPABILITY_MAIL_DELIVERY_OUTBOX,
CAPABILITY_MAIL_NOTIFICATION_DELIVERY,
)
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
from govoplan_core.core.modules import (
DocumentationCondition,
@@ -180,6 +183,7 @@ manifest = ModuleManifest(
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"),
),
requires_interfaces=(
ModuleInterfaceRequirement(
@@ -253,6 +257,10 @@ manifest = ModuleManifest(
"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),
},
documentation=(
DocumentationTopic(