• v0.28.3 3daa875c5a

    0.28.3 — the rest of the shared mail layer leaves the tenant
    All checks were successful
    CI / gate (push) Successful in 1m25s
    CI / image (push) Successful in 14s
    Stable

    joeyr released this 2026-08-21 18:40:14 -05:00 | 22 commits to main since this release

    The rest of the shared mail layer follows the parsers out. 0.28.2 moved the seven
    modules that were already separate files; this moves what was still buried inside
    icloudmail/client.py and is no more about iCloud than they were. Found by starting the
    Gmail client and discovering how much of it would have been a second copy. No behaviour
    changed.

    Changed

    • The eight mail errors moved to mcp_gateway/mail/errors.py. MailError and its
      subclasses describe facts about mail — a folder outside the allowlist, a handle whose
      UIDVALIDITY has moved — not facts about a provider. Imported rather than re-declared
      per tenant so there is exactly one MailError in the process: two would make
      except MailError catch half of what it was written to catch, and the missed half would
      surface as an unhandled exception rather than a tool error.

    • The address, MIME and unsubscribe helpers moved to mcp_gateway/mail/parts.py
      _address, _unsubscribe, _has_attachments, _split_addresses,
      _nth_attachment_bytes, _first_part, _strip_html. Each takes what a FETCH already
      returned and answers a question about it. _has_attachments walks a BODYSTRUCTURE the
      same way whichever server sent it.

    • _body and _attachments became body_of and attachments_of there, and the
      tenant's methods delegate. The only thing tying either to a tenant was a cap read from
      config, so the cap is a parameter now and nothing provider-shaped is left.

    client.py, smtp.py and tools.py still hold everything that is iCloud — the
    COPYUID workaround, the missing SPECIAL-USE, the junk-folder fallback. Those are the
    tenant, and none of them should follow.

    Notes

    • icloudmail/client.py is ~300 lines smaller and every name it used to export is
      still importable from it, so nothing outside had to change. One test now imports
      _strip_html from mail.parts instead, because that is where the behaviour lives and a
      test should name the module that owns it.

    • Why now rather than in 0.28.2: these were not obviously shared until there was a
      second consumer. Extracting on suspicion would have been designing an abstraction with
      one caller; extracting when the second client is being written is the first moment the
      shape is known.

    • 1084 tests green, unchanged.

    Downloads