Files
aiomail/README.md
T
dsql a340067048 fix: reconnect-on-stale re-selects folder; lock connect() against races
ensure_connection() reconnected via connect(), which only reaches IMAP
state AUTH, not SELECTED - every search/fetch/store after a mid-pass
link drop silently failed (swallowed into []/None/False) because
aioimaplib rejects those commands outside SELECTED. Track the
currently-selected folder and re-select it after reconnecting; note
pre-reconnect sequence-number ids are invalid post-reconnect unless
use_uid=True.

Also add an asyncio.Lock around connect()/close()/ensure_connection():
concurrent callers on one instance used to race inside connect(),
where task B's `await self.close()` tore down task A's mid-handshake
connection, leaking sockets and orphaning server sessions. Superseded
connections are now always logged out instead of silently overwritten.

Bump to 0.1.6, update README install pins.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-02 16:40:50 -04:00

94 lines
3.2 KiB
Markdown

# 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.6
# OAuth token providers (Microsoft / Google) need the extra:
aiomail[oauth] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.6
```
Direct:
```bash
pip install "aiomail @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.6"
pip install "aiomail[oauth] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiomail.git@v0.1.6"
```
Requires `aioimaplib` and `beautifulsoup4` (pulled transitively). The `oauth`
extra adds `aiohttp` for the refresh-token providers.
Drop the `@v0.1.6` suffix from the line above to install the latest unpinned.
## 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.**