dsql 9c96fa793a fix: select() on the requested folder despite a failed old-folder reselect
_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>
2026-07-06 00:15:55 -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.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.

S
Description
Async IMAP wrapper for OTP/login-code retrieval with password & OAuth2 auth
Readme
188 KiB
Languages
Python 100%