Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166#!/usr/bin/env python3"""otp-mail — read Iris's Fuser login code out of her mailbox, over IMAP.
python3 otp-mail.py mark -> {"INBOX": 42, "[Gmail]/Spam": 3} python3 otp-mail.py wait --after '<json>' --wait 120
Why python and not node: panda has no mail client and no npm tree — no hermes,no mbsync, no himalaya, nothing to `npm i`. It DOES have /usr/bin/python3, whosestdlib ships `imaplib`. So the whole mailbox side of the login is 120 lines ofstandard library and zero dependencies, on a machine we never have to provision.
The interesting part is `mark`. A login code is only good for the request thatasked for it, so reading the *newest* Fuser mail is not enough — a code from tenminutes ago is still the newest one until the new mail lands, and typing it gets"This login code is incorrect" and burns the attempt. That is a real failure, nota theoretical one.
We could compare timestamps, but that trusts two clocks (ours and Gmail's) toagree. IMAP already hands us something better: UIDNEXT — the UID the *next*message to arrive will get. So we mark UIDNEXT before pressing Continue, andafterwards accept only a message whose UID is >= that mark. "Arrived after weasked" becomes an integer comparison, and no clock is involved.
Spam is polled too: Fuser's own dialog tells you to check your spam folder, andan unattended run cannot.
The password comes in through the environment (IMAP_PASSWORD), never argv —argv is world-readable in `ps`. Nothing here ever prints it."""
import emailimport imaplibimport jsonimport osimport reimport sysimport timefrom email.header import decode_header
BOXES = ["INBOX", "[Gmail]/Spam"]SENDER = os.environ.get("OTP_FROM", "fuser.studio")CODE_RE = re.compile(r"\b(\d{6})\b")
def connect(): host = os.environ.get("IMAP_HOST", "imap.gmail.com") port = int(os.environ.get("IMAP_PORT", "993")) user = os.environ["IMAP_USER"] password = os.environ["IMAP_PASSWORD"] M = imaplib.IMAP4_SSL(host, port) M.login(user, password) return M
def uidnext(M, box): """The UID the next message into `box` will be given. Our high-water mark.""" typ, data = M.status(f'"{box}"', "(UIDNEXT)") if typ != "OK": return None m = re.search(rb"UIDNEXT (\d+)", data[0]) return int(m.group(1)) if m else None
def subject_of(msg): raw = msg.get("Subject", "") out = [] for part, enc in decode_header(raw): if isinstance(part, bytes): out.append(part.decode(enc or "utf-8", errors="replace")) else: out.append(part) return "".join(out)
def body_of(msg): chunks = [] for part in msg.walk(): if part.get_content_type() in ("text/plain", "text/html"): try: chunks.append( part.get_payload(decode=True).decode( part.get_content_charset() or "utf-8", errors="replace" ) ) except Exception: pass return "\n".join(chunks)
def look(M, box, after): """Newest Fuser mail in `box` with UID >= after, and the code in it.""" typ, _ = M.select(f'"{box}"', readonly=True) if typ != "OK": return None # `UID after:*` is an IMAP idiom that always returns at least the highest # UID in the box — even when that UID is below `after` — so the filter # below is load-bearing, not belt-and-braces. typ, data = M.uid("SEARCH", None, f"UID {after}:*", "FROM", f'"{SENDER}"') if typ != "OK" or not data or not data[0]: return None uids = [int(u) for u in data[0].split() if int(u) >= after] if not uids: return None
for uid in sorted(uids, reverse=True): # newest first typ, d = M.uid("FETCH", str(uid), "(INTERNALDATE RFC822)") if typ != "OK" or not d or not isinstance(d[0], tuple): continue msg = email.message_from_bytes(d[0][1]) subject = subject_of(msg) # The code is in the subject line ("Your Fuser login code is 123456", # services/auth/realEmailSender.ts), which is why we never have to parse # the React-rendered HTML body. Kept as a fallback in case it moves. hit = CODE_RE.search(subject) or CODE_RE.search(body_of(msg)) if hit: return { "code": hit.group(1), "uid": uid, "box": box, "subject": subject, "from": msg.get("From", ""), "date": msg.get("Date", ""), } return None
def main(): cmd = sys.argv[1] if len(sys.argv) > 1 else "wait" args = sys.argv[2:]
def arg(name, default=None): return args[args.index(name) + 1] if name in args else default
M = connect() try: if cmd == "mark": print(json.dumps({box: uidnext(M, box) for box in BOXES})) return
after = json.loads(arg("--after", "{}")) deadline = time.time() + float(arg("--wait", "120")) every = float(arg("--every", "3"))
while True: for box in BOXES: mark = after.get(box) if mark is None: continue hit = look(M, box, mark) if hit: print(json.dumps(hit)) return if time.time() >= deadline: print(json.dumps({"error": "no login code arrived in time"})) sys.exit(2) time.sleep(every) finally: try: M.logout() except Exception: pass
if __name__ == "__main__": main()