from __future__ import annotations import unittest from unittest.mock import patch from sqlalchemy import Column, String, Table, create_engine from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.pool import StaticPool from govoplan_core.db.base import Base from govoplan_mail.backend.bounce_processing import ( SqlMailBounceProcessingProvider, parse_delivery_status, ) from govoplan_mail.backend.db.models import ( MailBounceObservation, MailBounceSource, MailDeliveryCommand, MailServerProfile, ) from govoplan_mail.backend.delivery_outbox import submit_delivery_command DSN = b"""From: Mail Delivery Subsystem To: sender@example.test Date: Fri, 31 Jul 2026 12:00:00 +0000 Subject: Delivery Status Notification (Failure) MIME-Version: 1.0 Content-Type: multipart/report; report-type=delivery-status; boundary="dsn" --dsn Content-Type: text/plain; charset=utf-8 Delivery failed. --dsn Content-Type: message/delivery-status Reporting-MTA: dns; mx.example.test Original-Message-ID: Final-Recipient: rfc822; recipient@example.test Action: failed Status: 5.1.1 Remote-MTA: dns; destination.example.test Diagnostic-Code: smtp; 550 mailbox unavailable Last-Attempt-Date: Fri, 31 Jul 2026 11:59:00 +0000 --dsn Content-Type: message/rfc822 Message-ID: From: sender@example.test To: recipient@example.test Subject: Original Body --dsn-- """ class MailBounceProcessingTests(unittest.TestCase): def setUp(self) -> None: self.engine = create_engine( "sqlite+pysqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) access_users = Base.metadata.tables.get("access_users") if access_users is None: access_users = Table( "access_users", Base.metadata, Column("id", String(36), primary_key=True), ) Base.metadata.create_all( self.engine, tables=[ access_users, MailServerProfile.__table__, MailDeliveryCommand.__table__, MailBounceSource.__table__, MailBounceObservation.__table__, ], ) self.SessionLocal = sessionmaker( bind=self.engine, class_=Session, expire_on_commit=False, ) with self.SessionLocal() as session: session.add( MailServerProfile( id="profile-1", tenant_id="tenant-1", scope_type="tenant", scope_id="tenant-1", name="Delivery", slug="delivery", smtp_config={"host": "smtp.example.test", "port": 25}, ) ) session.commit() self.audit_delivery = patch( "govoplan_mail.backend.delivery_outbox.audit_event" ) self.audit_bounce = patch( "govoplan_mail.backend.bounce_processing.audit_event" ) self.audit_delivery.start() self.audit_bounce.start() self.addCleanup(self.audit_delivery.stop) self.addCleanup(self.audit_bounce.stop) self.addCleanup(self.engine.dispose) def test_parser_extracts_structured_recipient_outcome(self) -> None: reports = parse_delivery_status(DSN) self.assertEqual(1, len(reports)) self.assertEqual("recipient@example.test", reports[0]["recipient"]) self.assertEqual("failed", reports[0]["action"]) self.assertEqual("5.1.1", reports[0]["status_code"]) self.assertEqual( "", reports[0]["original_message_id"], ) def test_processing_correlates_and_is_idempotent(self) -> None: provider = SqlMailBounceProcessingProvider() with self.SessionLocal() as session: command = submit_delivery_command( session, tenant_id="tenant-1", command_type="campaign_report", source_module="campaigns", source_resource_type="campaign", source_resource_id="campaign-1", source_version_id="version-1", idempotency_key="delivery-1", profile_id="profile-1", message_bytes=( b"Message-ID: \r\n" b"Subject: Original\r\n\r\nBody" ), envelope_from="sender@example.test", envelope_recipients=["recipient@example.test"], from_header="sender@example.test", expected_smtp_transport_revision="revision-1", ) first = provider.process_raw_message( session, tenant_id="tenant-1", profile_id="profile-1", folder="INBOX", uid="42", raw_message=DSN, ) second = provider.process_raw_message( session, tenant_id="tenant-1", profile_id="profile-1", folder="INBOX", uid="42", raw_message=DSN, ) self.assertEqual(command["id"], first[0].command_id) self.assertTrue(first[0].matched) self.assertTrue(first[0].permanent) self.assertEqual(first[0].id, second[0].id) self.assertEqual(1, session.query(MailBounceObservation).count()) self.assertEqual( "pending", session.get(MailDeliveryCommand, command["id"]).status, ) def test_ordinary_mail_is_not_misclassified(self) -> None: self.assertEqual( (), parse_delivery_status( b"From: person@example.test\r\nSubject: Hello\r\n\r\nNot a DSN" ), ) if __name__ == "__main__": unittest.main()