From c08d13c02a7e977fb924183578dbedd1c2f80ff8 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Wed, 24 Jun 2026 20:10:03 -0400 Subject: [PATCH] init: async IMAP OTP retrieval Signed-off-by: disqualifier --- .gitignore | 16 ++++++++++ README.md | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6ff830 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# claude +CLAUDE.md + +# python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +.eggs/ + +# env +.venv/ +venv/ +.env +.pytest_cache/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3de9c1a --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# aiomail + +Async IMAP one-time-code retrieval. Reads OTP / login codes out of IMAP mailboxes +**you own**, with password or OAuth2 (XOAUTH2) auth and dynamic sender / subject / +code matching. + +This reads codes from email; it does not generate them (that is `pyotp`'s job). + +## Install + +`requirements.txt`: + +``` +aiomail @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.0 +# OAuth token providers (Microsoft / Google) need the extra: +aiomail[oauth] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.0 +``` + +Direct: + +```bash +pip install "aiomail @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.0" +pip install "aiomail[oauth] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.0" +``` + +Requires `aioimaplib` and `beautifulsoup4` (pulled transitively). The `oauth` +extra adds `aiohttp` for the refresh-token providers. + +## Password auth + +```python +from aiomail import IMAPClient, PasswordAuth, retrieve_otp + +client = IMAPClient(PasswordAuth(user, password), host="outlook.office365.com") +async with client: + code = await retrieve_otp(client, sender="no-reply@privy.io", subject="login code") +``` + +## OAuth2 auth + +Pass a static access token, or a token provider that refreshes one on connect: + +```python +from aiomail import IMAPClient, OAuth2Auth, retrieve_otp +from aiomail.oauth import MicrosoftTokenProvider + +auth = OAuth2Auth(user, token_provider=MicrosoftTokenProvider(client_id, refresh_token)) +client = IMAPClient(auth, host="outlook.office365.com") +async with client: + code = await retrieve_otp(client, sender="no-reply@privy.io") +``` + +`GoogleTokenProvider(client_id, refresh_token, client_secret)` is also provided. +Credentials are always supplied by you — nothing is hardcoded. + +## Dynamic matching + +`sender` and `subject` accept a substring, a compiled regex, or a callable: + +```python +import re +await retrieve_otp(client, sender="uber.com") # substring +await retrieve_otp(client, sender=re.compile(r"no-?reply@.*\.io")) # regex +await retrieve_otp(client, sender=lambda f: f.endswith("@x.com")) # callable +``` + +Code extraction is tunable too — `patterns` (regexes, first group wins) and +`lengths` (standalone digit-run fallback): + +```python +from aiomail import extract_code +extract_code(message, patterns=[r"PIN[:\s]+(\d{6})"], lengths=(6,)) +``` + +## Scope + +`retrieve_otp` walks `folders` newest-first, filters by sender/subject, extracts a +code, and applies `max_age` (seconds; `None` disables). Provider quirks (folder +names, code lengths, freshness) are parameters, not hardcoded branches. + +For mailboxes / accounts you own and control. + +## Verification status + +Pure logic (`extract_code`, `as_predicate`, `retrieve_otp`) and the IMAP entrypoints +are verified against the installed `aioimaplib` API. The **live-server paths are not +fully tested**: end-to-end XOAUTH2 login against real Outlook/Gmail, the +Microsoft/Google refresh-token exchange (scopes may need adjusting to your app +registration), and the iCloud `(BODY[])` fetch. Password IMAP and the matching logic +work; **confirm OAuth end-to-end against your own mailbox before relying on it in +production.**