"""Duplicate Message-ID handling in the indexer (SPEC §7.1). Real-world trigger: home.pl's welcome mailer reuses one static Message-ID for every recipient, so the same id lands in different accounts as different messages. """ from __future__ import annotations import uuid from collections.abc import AsyncIterator from datetime import UTC, datetime, timedelta from email.message import EmailMessage import pytest_asyncio from sqlalchemy import delete from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.pool import NullPool from app.config import get_settings from app.indexer.indexer import persist_message from app.models import Account, Mailbox, Message, User from app.providers.base import RemoteMessage class Dupenv: def __init__(self, session: AsyncSession, user_id: uuid.UUID) -> None: self.session = session self.user_id = user_id self.accounts: dict[str, uuid.UUID] = {} self.mailboxes: dict[str, uuid.UUID] = {} self._uid = 0 async def add_account(self, label: str) -> None: acc = Account( user_id=self.user_id, email=f"{label}@example.com", imap_config={}, smtp_config={}, secret_ct=b"", discovery_src="manual", ) self.session.add(acc) await self.session.flush() self.accounts[label] = acc.id async def add_mailbox(self, account_label: str, path: str) -> None: mb = Mailbox( account_id=self.accounts[account_label], user_id=self.user_id, path=path, special_use="inbox", ) self.session.add(mb) await self.session.flush() self.mailboxes[f"{account_label}/{path}"] = mb.id async def ingest( self, account_label: str, path: str, *, mid: str, minutes: int = 0 ) -> Message | None: self._uid += 1 m = EmailMessage() m["Subject"] = "Pierwsze kroki z pocztą w home.pl" m["From"] = "info@home.pl" m["Message-ID"] = mid m["Date"] = "Sun, 23 Nov 2025 12:47:30 +0000" m.set_content("welcome template body") remote = RemoteMessage( uid=self._uid, raw=m.as_bytes(), flags=frozenset(), internal_date=datetime(2026, 1, 1, tzinfo=UTC) + timedelta(minutes=minutes), ) result = await persist_message( self.session, user_id=self.user_id, account_id=self.accounts[account_label], mailbox_id=self.mailboxes[f"{account_label}/{path}"], mailbox_special_use="inbox", remote=remote, ) return result.message if result else None @pytest_asyncio.fixture async def dup() -> AsyncIterator[Dupenv]: engine = create_async_engine(get_settings().database_url, poolclass=NullPool) sm = async_sessionmaker(engine, expire_on_commit=False) session = sm() user = User(handle=f"dup{uuid.uuid4().hex[:8]}@test.local", password_hash="x") session.add(user) await session.flush() env = Dupenv(session, user.id) await env.add_account("a") await env.add_account("b") await env.add_mailbox("a", "INBOX") await env.add_mailbox("a", "Archive") await env.add_mailbox("b", "INBOX") try: yield env finally: await session.execute(delete(User).where(User.id == user.id)) await session.commit() await session.close() await engine.dispose() async def test_cross_account_duplicate_isolates(dup: Dupenv) -> None: # Same static Message-ID delivered to two different accounts -> two threads. a = await dup.ingest("a", "INBOX", mid="", minutes=0) b = await dup.ingest("b", "INBOX", mid="", minutes=60) assert a is not None and b is not None assert a.thread_id != b.thread_id # The second one was stored with a synthesized, non-authoritative id. assert a.message_id_hdr == "static@home.pl" assert b.message_id_hdr.endswith("@synthetic.local") async def test_same_account_duplicate_merges(dup: Dupenv) -> None: # Same message in INBOX + Archive of ONE account (Gmail-style) -> one thread. a = await dup.ingest("a", "INBOX", mid="", minutes=0) b = await dup.ingest("a", "Archive", mid="", minutes=1) assert a is not None and b is not None assert a.thread_id == b.thread_id assert b.message_id_hdr == "msg@x.com"