Skip to content

Guard the provider seam the way pan_mail_pro guards its contract - #12

Merged
rutgerhofste merged 1 commit into
mainfrom
claude/mail-provider-abstraction-ehkt0f
Aug 18, 2026
Merged

rutgerhofste merged 1 commit into
mainfrom
claude/mail-provider-abstraction-ehkt0f

Conversation

@rutgerhofste

Copy link
Copy Markdown
Member

Part of a two-repo alignment with pantalytics/pan_mail_pro (see its sibling PR). Both codebases have the same architecture — provider-agnostic tools/callers over a mail-provider contract with swappable backends — and each had proven practices the other lacked. This PR ports pan_mail_pro's seam-guarding to Squirrel; the sibling PR ports Squirrel's SMTP/IMAP hardening the other way.

What changed

providers/factory.py builds from a registry. MAIL_PROVIDER_REGISTRY maps each provider name to a lazy loader — the same shape as pan_mail_pro's PROVIDER_CLIENTS. Adding Gmail/Outlook later is one entry plus a subpackage, instead of another if branch. Behaviour is unchanged; the unknown-provider error still names the supported set.

tests/test_provider_contract.py pins the seam (the counterpart of pan_mail_pro's file of the same name):

  • the registry and config.SUPPORTED_MAIL_PROVIDERS name the same backends — two lists, one meaning, held equal mechanically;
  • every registered backend implements the whole MailProvider protocol. The protocol is structural (typing.Protocol), so a missing method otherwise fails at the first call, at runtime, in a user's session — this moves that failure to make test;
  • an unknown provider raises naming the supported set;
  • the tool layer contains no reference to a concrete client (soverin, imap_tools, imaplib, smtplib, caldav). Until now that was a convention in CLAUDE.md; pan_mail_pro greps the same boundary in CI, because a convention nobody can check is a convention that is already broken somewhere.

Testing

make lint (ruff + ty) and the full unit suite pass: 226 passed, 19 integration tests deselected.

🤖 Generated with Claude Code

https://claude.ai/code/session_01St97qp2vqVGNN8piXgopG2


Generated by Claude Code

The MailProvider protocol is structural: a backend missing a method fails
at the first call, at runtime, in a user's session — never at import, and
never in CI. pan_mail_pro solved the same problem for its
mail.provider.client with a registry plus a contract test; this ports
that shape.

- providers/factory.py now builds from MAIL_PROVIDER_REGISTRY (one
  lazily-loaded entry per backend, the shape of PROVIDER_CLIENTS) instead
  of an if-branch per provider.
- tests/test_provider_contract.py pins the seam: the registry and
  config.SUPPORTED_MAIL_PROVIDERS name the same backends, every
  registered backend implements the whole protocol, an unknown provider
  fails naming the supported set — and the tool layer contains no
  reference to a concrete client, which until now was only a convention
  in CLAUDE.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01St97qp2vqVGNN8piXgopG2
@rutgerhofste
rutgerhofste marked this pull request as ready for review August 18, 2026 14:18
@rutgerhofste
rutgerhofste merged commit b08bcda into main Aug 18, 2026
3 checks passed
@rutgerhofste
rutgerhofste deleted the claude/mail-provider-abstraction-ehkt0f branch August 18, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants