import asyncio from types import SimpleNamespace import pytest from pneumatube.smtp_server import GatewayHandler class _StubBridge: def __init__(self): self.forwarded = [] def forward(self, username, sender, subject, body): self.forwarded.append(username) if username == "broken": raise RuntimeError("smolmail server unreachable") @staticmethod def extract_text(message): return message.get_payload(decode=True).decode("utf-8", "replace") class _InlineWorker: """Runs synchronously: handle_DATA's per-recipient logic doesn't depend on real threading, just on worker.run's (fn, *args) -> result contract.""" async def run(self, fn, *args): return fn(*args) def _envelope(rcpt_tos): return SimpleNamespace( content=b"Subject: hi\r\n\r\nbody text\r\n", mail_from="sender@realmail.example", rcpt_tos=rcpt_tos, ) def test_handle_data_replies_once_per_recipient(): bridge = _StubBridge() handler = GatewayHandler(bridge, _InlineWorker(), config=SimpleNamespace()) envelope = _envelope(["alice@gateway.example", "broken@gateway.example"]) reply = asyncio.run(handler.handle_DATA(None, None, envelope)) lines = reply.split("\r\n") assert len(lines) == 2 assert lines[0].startswith("250") assert lines[1].startswith("451") assert bridge.forwarded == ["alice", "broken"] def test_handle_data_all_succeed(): bridge = _StubBridge() handler = GatewayHandler(bridge, _InlineWorker(), config=SimpleNamespace()) envelope = _envelope(["alice@gateway.example", "bob@gateway.example"]) reply = asyncio.run(handler.handle_DATA(None, None, envelope)) assert reply == "250 2.1.5 delivered\r\n250 2.1.5 delivered"