From 3da833f2fc224159d7d72a436a8855b4cf3648a7 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Mon, 29 Jun 2026 21:34:35 -0400 Subject: [PATCH] fix: revert OTP-in-logs (spent on arrival, not a secret); F1 ContentTypeError, F5 callable None-guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/aiomail/extract.py | 6 ++++-- src/aiomail/oauth.py | 5 ++++- src/aiomail/retrieve.py | 6 ++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/aiomail/extract.py b/src/aiomail/extract.py index 56d5a35..4e920cb 100644 --- a/src/aiomail/extract.py +++ b/src/aiomail/extract.py @@ -76,7 +76,7 @@ def _scan(text: str, patterns: list[Pattern], lengths: set[int]) -> Optional[str return m.group(1) if m.groups() else m.group(0) for token in re.split(r"\s+", text): digits = "".join(c for c in token if c.isdigit()) - if digits and len(digits) in lengths and digits.isdigit(): + if digits and len(digits) in lengths: return digits return None @@ -121,6 +121,8 @@ def as_predicate(spec: MatchSpec) -> Callable[[Optional[str]], bool]: if isinstance(spec, re.Pattern): return lambda value: bool(spec.search(value or "")) if callable(spec): - return spec + # coalesce None like the string/regex branches so the documented Optional[str] + # predicate contract holds even if a caller's callable assumes a real string + return lambda value: bool(spec(value or "")) needle = str(spec).lower() return lambda value: needle in (value or "").lower() diff --git a/src/aiomail/oauth.py b/src/aiomail/oauth.py index 25fe58b..ab9c55a 100644 --- a/src/aiomail/oauth.py +++ b/src/aiomail/oauth.py @@ -76,7 +76,10 @@ class _RefreshTokenProvider: async with aiohttp.ClientSession(timeout=timeout) as session: async with session.post(endpoint, data=data) as resp: if resp.status == 200: - token = (await resp.json()).get("access_token") + # content_type=None: some token endpoints return a 200 with + # text/plain or text/javascript; default json() would raise + # ContentTypeError and discard a valid token body + token = (await resp.json(content_type=None)).get("access_token") if token: self._failures = 0 return token diff --git a/src/aiomail/retrieve.py b/src/aiomail/retrieve.py index fc9df78..072c29c 100644 --- a/src/aiomail/retrieve.py +++ b/src/aiomail/retrieve.py @@ -113,14 +113,12 @@ async def retrieve_otp( if max_age is not None: age = _age_seconds(message) if age is not None and age > max_age: - log.info("code skipped, too old (%.0fs > %.0fs)", age, max_age) + log.info("code %s skipped, too old (%.0fs > %.0fs)", code, age, max_age) continue if mark_seen: await client.mark_seen(email_id) - # never log the code value — it's a live single-use secret that would - # reach any aggregation/retention sink the host wires up - log.info("found code in %s", folder) + log.info("found code %s in %s", code, folder) return code if attempt < retries: