Async IMAP wrapper for OTP/login-code retrieval with password & OAuth2 auth
Go to file
disqualifier 5f7ed74306 add package: pyproject + src
async IMAP one-time-code retrieval consolidated from 5 forks. three
injected layers: auth (PasswordAuth/OAuth2Auth XOAUTH2), client
(IMAPClient connect/retry/folders/search/fetch/mark_seen), retrieve
(retrieve_otp newest-first with sender/subject/code matching via
substring/regex/callable). config-free, emit-only logging. optional
[oauth] extra adds aiohttp refresh-token providers (Microsoft/Google).

fixed vs forks: dropped nonexistent uid_fetch/uid_store for aioimaplib's
uid() dispatcher, xoauth2 token now bytes, guarded XOAUTH2 fallback,
use_uid decoupled from use_ssl. src/ multi-module, hatchling.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-24 21:36:43 -04:00
src/aiomail add package: pyproject + src 2026-06-24 21:36:43 -04:00
.gitignore init: async IMAP OTP retrieval 2026-06-24 20:10:03 -04:00
pyproject.toml add package: pyproject + src 2026-06-24 21:36:43 -04:00
README.md init: async IMAP OTP retrieval 2026-06-24 20:10:03 -04:00

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:

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

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:

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:

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):

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.