Commit Graph
20 Commits
Author SHA1 Message Date
dsql 5f23abc9c7 fix: tolerate NUL-bearing charset in part decode
a MIME part charset param containing a NUL character (e.g. malformed/adversarial
.eml input) makes codec lookup raise ValueError instead of the LookupError/TypeError
already handled here, escaping uncaught through extract_code. Catch ValueError too
and fall back to utf-8 like the existing bad-charset path, mirroring
decode_header_value's existing (UnicodeDecodeError, LookupError, ValueError) pattern.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-06 00:15:05 -04:00
dsql 0d88764510 refactor: derive __version__ from package metadata (single source)
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-03 16:59:12 -04:00
dsql 0038f03b9e docs: expand module contract, compress private docstrings
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-03 16:47:32 -04:00
dsql f1e52ff1ac docs: compress prose/module docstrings, em-dash->hyphen (de-bloat wave 1)
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-03 00:12:22 -04:00
dsql d478ed0d4c fix: warn on dropped OAuth token, skip wasted final backoff, honor external cancel, fix digit-run split (v0.1.8)
aiomail-7: log a truncated body when a 200 token response has no access_token
instead of silently discarding it. aiomail-8: connect() no longer sleeps after
the last failed retry attempt. aiomail-9: _discard_mail now distinguishes its
own task.cancel() from an external cancellation via asyncio.shield, so
connect() no longer resists cancellation. aiomail-10: the digit-run fallback
now matches contiguous \d+ runs instead of mashing digits across punctuation
within a whitespace token (e.g. a date). aiomail-11: README's dynamic-matching
example uses email.utils.parseaddr since a real From header is not a bare
address. Also compresses essay docstrings to one-line-plus-nuance with zero
behavior change (re-verified against the same negative controls).

Signed-off-by: disqualifier <dev@disqualifier.me>
v0.1.8
2026-07-02 23:30:26 -04:00
dsql fd789c2ba2 fix: decode RFC2047 subjects, OR TO/FROM in to-mode query, tz-aware Date, drop LIST completion line (v0.1.7)
Subject headers were matched/scanned raw (never RFC2047-decoded), so any
provider that base64/quoted-printable encodes non-ASCII subjects silently
failed subject filtering and code extraction. match_field='to' only
server-queried TO, so an older TO-matching message permanently hid a
forwarded-From match the client-side check was documented to accept.
_age_seconds treated a naive datetime (from a "-0000" Date header) as local
time instead of UTC, corrupting max_age freshness on non-UTC hosts. get_folders
appended aioimaplib's tagged LIST completion text as a phantom folder name on
every call against any RFC-compliant server.

Also corrects __init__.py's __version__, which was left at 0.1.5 through the
0.1.6 release.

Signed-off-by: disqualifier <dev@disqualifier.me>
v0.1.7
2026-07-02 17:02:56 -04:00
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>
v0.1.6
2026-07-02 16:40:50 -04:00
dsql b00f122b74 chore: ignore .claude/ dir (CLAUDE.md now lives under .claude/)
Signed-off-by: disqualifier <dev@disqualifier.me>
v0.1.5
2026-06-29 21:55:13 -04:00
dsql 3da833f2fc fix: revert OTP-in-logs (spent on arrival, not a secret); F1 ContentTypeError, F5 callable None-guard
revert the M-1 log change — a single-use OTP is consumed on arrival, not a live secret,
so log the code value again. keep the oauth error-body truncate.

F1: oauth token fetch uses resp.json(content_type=None) so a 200 with text/plain doesn't
ContentTypeError and discard a valid token. F5: as_predicate coalesces None for the
callable branch like the string/regex branches. drop a redundant digits.isdigit().

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 21:34:35 -04:00
dsql f940641a5a fix: never log the OTP code value (secret-in-logs); correct false test claim (v0.1.5)
M-1: retrieve.py logged the live single-use code at INFO ('found code %s', 'code %s
skipped too old'), shipping the secret to any aggregation/retention sink the host wires
(our /srv/logs -> loki/grafana path). drop the code value from both lines — log that a
code was found/retrieved and where, never the value. also truncate the oauth token-endpoint
error body to 200 chars so a token response can't be dumped whole.

aiomail-F3: CLAUDE.md claimed an '8-case tested' suite that does not exist in the repo;
corrected to describe the manual throwaway-venv exercise + the real flake8 check.

verified by execution: code retrieved, value absent from logs; control confirms the old
line carried it.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:46:51 -04:00
dsql 0cf23805dd docs: pin install line to release, note unpinned-latest option
Signed-off-by: disqualifier <dev@disqualifier.me>
v0.1.4
2026-06-29 18:13:31 -04:00
dsql 75e6550311 docs: show unpinned install line; note tag-pinning for reproducibility
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 18:07:16 -04:00
dsql a44bf11be6 fix: dead is_throttled, orphan connect-task, server-defined folder delimiter (v0.1.4)
- remove is_throttled(): read a non-existent .resp -> always False (dead) (L2)
- cancel/await aioimaplib's fire-and-forget create_connection task on a failed connect
  so a refused host doesn't log 'Task exception was never retrieved' per retry (L3)
- get_folders() parses the server-announced LIST delimiter instead of hardcoding '/',
  so '.'/NIL-delimited servers (Gmail/Dovecot) return correct names (L4)
- mark the dead aioimaplib-2.0.x tuple branch + the non-aioimaplib authenticate
  fallback as cross-version escape hatches (nits).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 17:57:37 -04:00
dsql e349638700 fix: fetch() selects message body by structure, not length (v0.1.3)
select the literal payload by isinstance bytearray instead of len>20. aioimaplib
stores the message body as the only bytearray in the response; every other line
(including the '<id> FETCH (...' header) is plain bytes. the length heuristic
matched the header line first for any 2+ digit message id or BODY[]/UID fetch,
returning a blank Message and silently breaking OTP retrieval on real mailboxes.

Signed-off-by: disqualifier <dev@disqualifier.me>
v0.1.3
2026-06-29 17:09:08 -04:00
dsql a4abe354eb fix: add match_field=from|to to restore recipient-primary OTP matching
the clean lib matched senders by From only; the original imap_tool.py matched primarily by TO (the per-user alias the code was sent to) with a HEADER FROM forwarded fallback. added match_field="from"|"to" to retrieve_otp: "from" (default) is byte-identical to current behavior, "to" searches TO primary and accepts a forwarded From match, restoring the alias flow. server query + client-side predicate both honor it. bump to v0.1.2.

Signed-off-by: disqualifier <dev@disqualifier.me>
v0.1.2
2026-06-29 03:25:18 -04:00
dsql 6ac8957583 fix: pass XOAUTH2 token as str, not bytes (was corrupting the Bearer value)
aioimaplib's mail.xoauth2(user, token) builds the SASL string by f-string interpolating the token, so a bytes token injects the b'...' repr into auth=Bearer and breaks every XOAUTH2 login. dropped the .encode() (token is already str via _resolve_token/_as_str). corrected the inline comment and the CLAUDE.md note that both wrongly claimed bytes was required — that false note propagated the bug through prior review passes.

Signed-off-by: disqualifier <dev@disqualifier.me>
v0.1.1
2026-06-28 18:45:25 -04:00
dsql 7934688595 fix: clean up the IMAP object on a failed connect; module-level asyncio import
on a failed connect attempt the IMAP4 object was dropped without logout(), leaking the socket aioimaplib held; now it is logged out (best-effort) before nulling. also moved the asyncio import in oauth.py from inside the retry loop to module top.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-28 17:18:28 -04:00
dsql ba7ae48a87 fix: bytes-token XOAUTH2 + tolerant SEARCH parse (v0.1.1)
- a token provider (or static token) returning bytes crashed token.encode() in the
  XOAUTH2 path. coerce to str at the source (_resolve_token via _as_str) so both
  the .encode() and SASL-fallback entrypoints get a str.
- client.search() did int(x) on every SEARCH token unguarded; a malformed/non-
  numeric token aborted the whole search. skip non-numeric tokens (log at debug)
  instead of crashing.

verified by execution: bytes static + async-provider tokens authenticate without
crashing (both xoauth2 and SASL-fallback paths); guarded search skips garbage.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-27 21:51:27 -04:00
dsql 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>
v0.1.0
2026-06-24 21:36:43 -04:00
dsql c08d13c02a init: async IMAP OTP retrieval
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-24 20:10:03 -04:00