"""Tests for the message-level derivations extracted out of gmail-bulk-import. The derivations themselves have been exercised by that job's suite (and by 225 030 real messages) since June; what is new here is that they are now shared, and that `eml_ref` takes `source` as a parameter instead of hardcoding `gmail/`. These tests pin the properties the poller depends on: ids identical to the ones already in the table, and archive paths that agree with `save_eml` for BOTH sources. """ from __future__ import annotations import email import email.policy from datetime import datetime, timezone from pathlib import Path from kb_mail.archive import save_eml from kb_mail.message import EPOCH, eml_ref, message_id, parse_attachments, parse_date def _msg(raw: bytes): return email.message_from_bytes(raw, policy=email.policy.compat32) class TestMessageId: def test_strips_angle_brackets(self): assert message_id(_msg(b"Message-ID: \r\n\r\nbody")) == "abc@example.com" def test_missing_header_falls_back_to_a_content_hash(self): mid = message_id(_msg(b"Subject: no id\r\n\r\nbody")) assert mid.startswith("sha256-") assert len(mid) == len("sha256-") + 32 def test_content_hash_is_stable_for_identical_bytes(self): raw = b"Subject: no id\r\n\r\nbody" assert message_id(_msg(raw)) == message_id(_msg(raw)) def test_eight_bit_header_bytes_do_not_raise(self): # compat32 hands back an email.header.Header here, not a str — an unguarded # .strip() on it killed a full bulk-import run once. mid = message_id(_msg(b"Message-ID: <\xc4\x85bc@example.com>\r\n\r\nbody")) assert mid class TestParseDate: def test_parses_to_utc(self): ts = parse_date(_msg(b"Date: Mon, 15 Jun 2026 14:00:00 +0200\r\n\r\nbody")) assert ts == datetime(2026, 6, 15, 12, 0, tzinfo=timezone.utc) def test_missing_date_is_epoch(self): assert parse_date(_msg(b"Subject: x\r\n\r\nbody")) == EPOCH def test_unparseable_date_is_epoch(self): assert parse_date(_msg(b"Date: not a date at all\r\n\r\nbody")) == EPOCH def test_naive_date_is_assumed_utc(self): ts = parse_date(_msg(b"Date: Mon, 15 Jun 2026 14:00:00 -0000\r\n\r\nbody")) assert ts.tzinfo is timezone.utc class TestParseAttachments: def test_inline_text_is_not_an_attachment(self): assert parse_attachments(_msg(b"Content-Type: text/plain\r\n\r\nhello")) == [] def test_attachment_manifest_carries_sha256_and_size(self): raw = ( b'Content-Type: multipart/mixed; boundary="b"\r\n\r\n--b\r\n' b"Content-Type: text/plain\r\n\r\nhello\r\n--b\r\n" b"Content-Type: application/pdf\r\n" b'Content-Disposition: attachment; filename="doc.pdf"\r\n' b"Content-Transfer-Encoding: base64\r\n\r\nJVBERg==\r\n--b--\r\n" ) [att] = parse_attachments(_msg(raw)) assert att["type"] == "attachment" assert att["filename"] == "doc.pdf" assert att["size"] == 4 # decoded payload, not the base64 text assert len(att["sha256"]) == 64 class TestEmlRef: def test_layout_is_source_year_month(self): ts = datetime(2026, 6, 15, tzinfo=timezone.utc) assert eml_ref("abc@example.com", "fastmail", ts) == "fastmail/2026/06/abc@example.com.eml" def test_unsafe_characters_are_translated(self): ts = datetime(2026, 6, 15, tzinfo=timezone.utc) assert eml_ref("", "gmail", ts) == "gmail/2026/06/a_b_c.eml" async def test_agrees_with_save_eml_for_a_new_source(self, tmp_path: Path): # The two must not drift: on FileExistsError the caller fills raw_ref from eml_ref # for a file save_eml wrote on an earlier run. ts = datetime(2026, 6, 15, tzinfo=timezone.utc) written = await save_eml(tmp_path, "id/with:chars", "fastmail", ts, b"raw") assert written == eml_ref("id/with:chars", "fastmail", ts) assert (tmp_path / written).read_bytes() == b"raw"