from typing import List, Optional from database import ( Message, SessionLocal, Thread, create_db_tables, get_thread_messages, get_threads_requiring_reply, ingest_emails, ) from zoho_client import ZohoClient def ingest_action( account_email: str, days_back: int = 7, max_results: int = 50 ) -> None: create_db_tables() client = ZohoClient(email=account_email) inbox = client.fetch_folder_emails( folder="INBOX", max_results=max_results, days_back=days_back ) sent = client.fetch_folder_emails( folder="Sent", max_results=max_results, days_back=days_back ) client.close() db = SessionLocal() try: ingest_emails( db, account_email=account_email, emails=inbox, default_folder="INBOX" ) ingest_emails( db, account_email=account_email, emails=sent, default_folder="Sent" ) threads: List[Thread] = get_threads_requiring_reply(db, account_email) print(f"Threads requiring reply for {account_email}: {len(threads)}") for t in threads: print( f"- Thread #{t.id} | Subject: {t.subject!r} | requires_reply={t.requires_reply}" ) finally: db.close() def list_threads_action( account_email: Optional[str] = None, limit: int = 20, only_requiring: bool = False ) -> None: create_db_tables() db = SessionLocal() try: q = db.query(Thread).order_by(Thread.updated_at.desc()) if account_email: q = q.filter(Thread.account_email == account_email.lower()) if only_requiring: q = q.filter(Thread.requires_reply.is_(True)) threads = q.limit(limit).all() print( f"Showing {len(threads)} threads" + (f" for {account_email}" if account_email else "") ) for t in threads: count = db.query(Message).filter(Message.thread_id == t.id).count() print( f"- id={t.id} msgs={count} requires_reply={t.requires_reply} subject={t.subject!r}" ) finally: db.close() def show_thread_action(thread_id: int) -> None: create_db_tables() db = SessionLocal() try: thread = db.query(Thread).filter(Thread.id == thread_id).one_or_none() if not thread: print(f"Thread {thread_id} not found") return print( f"Thread #{thread.id} subject={thread.subject!r} account={thread.account_email} requires_reply={thread.requires_reply}" ) messages: List[Message] = get_thread_messages(db, thread.id) for i, m in enumerate(messages, 1): direction = "IN" if m.is_incoming else "OUT" snippet = (m.body or "").strip().replace("\n", " ") if len(snippet) > 140: snippet = snippet[:140] + "..." print( f"[{i}] {m.date_sent} [{direction}] {m.folder} | from={m.from_email} -> to={m.to_email}\n" f" subject={m.subject!r}\n" f" message_id={m.message_id} in_reply_to={m.in_reply_to}\n" f" body={snippet}" ) finally: db.close() if __name__ == "__main__": import argparse import os parser = argparse.ArgumentParser(description="Email alerts utility") sub = parser.add_subparsers(dest="cmd", required=False) p_ingest = sub.add_parser("ingest", help="Fetch INBOX and Sent and ingest into DB") p_ingest.add_argument( "--account", dest="account", default=os.getenv("ZOHO_EMAIL", "") ) p_ingest.add_argument("--days-back", dest="days_back", type=int, default=7) p_ingest.add_argument("--max-results", dest="max_results", type=int, default=50) p_list = sub.add_parser("list-threads", help="List threads") p_list.add_argument("--account", dest="account", default=None) p_list.add_argument("--limit", dest="limit", type=int, default=20) p_list.add_argument("--only-requiring", dest="only_req", action="store_true") p_show = sub.add_parser("show-thread", help="Print all messages in a thread") p_show.add_argument("thread_id", type=int) args = parser.parse_args() if args.cmd == "ingest": acct = args.account or os.getenv("ZOHO_EMAIL", "") if not acct: raise SystemExit("Provide --account or set ZOHO_EMAIL") ingest_action(acct, days_back=args.days_back, max_results=args.max_results) elif args.cmd == "list-threads": list_threads_action( account_email=args.account, limit=args.limit, only_requiring=args.only_req ) elif args.cmd == "show-thread": show_thread_action(args.thread_id) else: # Default behavior: run ingest using env and then list requiring-reply threads acct = os.getenv("ZOHO_EMAIL", "") if not acct: parser.print_help() raise SystemExit(0) ingest_action(acct)