#!/usr/bin/env python3 """ List unread emails via IMAP (e.g. Proton Mail Bridge). Read-only; does not mark as read. Only lists messages sent to the configured alias (MAIL_FILTER_TO, default ai.support.lecoffreio@4nkweb.com). Output is for the agent: each mail with UID, Message-ID, From, To, Subject, Date, body. Usage: ./gitea-issues/mail-list-unread.sh """ from __future__ import annotations import email import imaplib import sys from email.header import decode_header from pathlib import Path # Add gitea-issues to path for mail_common sys.path.insert(0, str(Path(__file__).resolve().parent)) from mail_common import load_imap_config, repo_root, imap_ssl_context def decode_header_value(header: str | None) -> str: if not header: return "" from email.header import decode_header as dh parts = dh(header) result = [] for part, charset in parts: if isinstance(part, bytes): result.append(part.decode(charset or "utf-8", errors="replace")) else: result.append(part) return "".join(result) def get_text_body(msg: email.message.Message) -> str: if msg.is_multipart(): for part in msg.walk(): if part.get_content_type() == "text/plain": payload = part.get_payload(decode=True) if payload: return payload.decode(part.get_content_charset() or "utf-8", errors="replace") return "" payload = msg.get_payload(decode=True) if not payload: return "" return payload.decode(msg.get_content_charset() or "utf-8", errors="replace") def is_sent_to_alias(msg: email.message.Message, filter_to: str) -> bool: """True if any To/Delivered-To/X-Original-To/Cc header contains the filter address.""" if not filter_to: return True headers_to_check = ("To", "Delivered-To", "X-Original-To", "Cc", "Envelope-To") for name in headers_to_check: value = msg.get(name) if value: decoded = decode_header_value(value).lower() if filter_to in decoded: return True return False def main() -> None: cfg = load_imap_config() if not cfg["user"] or not cfg["password"]: root = repo_root() env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env" print("[gitea-issues] ERROR: IMAP_USER and IMAP_PASSWORD required.", file=sys.stderr) print(f"[gitea-issues] Set env or create {env_path}", file=sys.stderr) sys.exit(1) mail = imaplib.IMAP4(cfg["host"], int(cfg["port"])) if cfg["use_starttls"]: mail.starttls(imap_ssl_context(cfg.get("ssl_verify", True))) mail.login(cfg["user"], cfg["password"]) mail.select("INBOX") _, nums = mail.search(None, "UNSEEN") ids = nums[0].split() if not ids: print("[gitea-issues] No unread messages.") mail.logout() return shown = 0 for uid in ids: uid_s = uid.decode("ascii") _, data = mail.fetch(uid, "(RFC822)") if not data or not data[0]: continue msg = email.message_from_bytes(data[0][1]) if not is_sent_to_alias(msg, cfg.get("filter_to", "")): continue mid = msg.get("Message-ID", "").strip() from_ = decode_header_value(msg.get("From")) to_ = decode_header_value(msg.get("To")) subj = decode_header_value(msg.get("Subject")) date_h = decode_header_value(msg.get("Date")) body = get_text_body(msg) print("--- MAIL", f"UID={uid_s}", "---") print("Message-ID:", mid or "(none)") print("From:", from_) print("To:", to_ or "") print("Subject:", subj) print("Date:", date_h or "") print("Body:") print(body or "(empty)") print("--- END MAIL ---") shown += 1 if shown == 0: print("[gitea-issues] No unread messages sent to the configured alias (MAIL_FILTER_TO).") mail.logout() if __name__ == "__main__": main()