aioimaplib forwards a timeout into IMAP4.uid(...) but drops it specifically for the STORE command (protocol.uid() calls self.store(*criteria, by_uid=True) without passing timeout through, so the Command never arms its internal timer). Combined with IMAP4_SSL's default conn_lost_cb=None, a connection that goes silent during a use_uid=True mark_seen call can hang the coroutine forever, unlike the non-uid store path which is already wrapped by aioimaplib itself. Wrap the uid-store call in asyncio.wait_for(self.timeout) so a stalled server times out and mark_seen returns False like the rest of this method's contract. 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.