_connect_and_reselect_locked() re-selects the prior _selected_folder after reconnecting and returns False if that reselect fails, even when the underlying link came back live. select() gated entirely on that bool, so it returned False without ever sending a SELECT for the folder the caller actually asked for - retrieve_otp's per-folder loop then silently skips a folder a live connection could have selected, for that pass. Let select() proceed to its own SELECT whenever the connection is live (self._mail is not None), only bailing out when ensure_connection reflects an unreconnectable link. Signed-off-by: disqualifier <dev@disqualifier.me>
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.10
# OAuth token providers (Microsoft / Google) need the extra:
aiomail[oauth] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.10
Direct:
pip install "aiomail @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.10"
pip install "aiomail[oauth] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.10"
Requires aioimaplib and beautifulsoup4 (pulled transitively). The oauth
extra adds aiohttp for the refresh-token providers.
Drop the @v0.1.10 suffix from the line above to install the latest unpinned.
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
from email.utils import parseaddr
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: parseaddr(f)[1].endswith("@x.com")) # callable
A real From header is Name <addr@x.com>, not a bare address — parseaddr pulls
the address out before matching (a plain f.endswith(...) would never match).
Subject headers are RFC2047-decoded before matching/extraction, so providers
that encode non-ASCII subjects (=?utf-8?B?...?=) still match on plain text.
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.