#!/usr/bin/env python3 """Daily digest of email organizer activity — sends summary to admin@thevish.io via Proton Bridge.""" import imaplib import smtplib import sqlite3 import ssl from collections import defaultdict from datetime import datetime, timedelta from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from pathlib import Path from zoneinfo import ZoneInfo # ── config ─────────────────────────────────────────────────────────────────── ACCOUNTS = [ { "name": "lzbellina92@gmail.com", "db": Path(__file__).parent / "gmail-organizer" / "processed.db", }, { "name": "your-email@example.com", "db": Path(__file__).parent / "gmail-organizer-dvish" / "processed.db", }, { "name": "admin@thevish.io", "db": Path(__file__).parent / "proton-organizer" / "processed.db", }, ] SMTP_HOST = "127.0.0.1" SMTP_PORT = 1025 SMTP_USER = "admin@thevish.io" SMTP_PASS = "REDACTED_PASSWORD" SEND_TO = "admin@thevish.io" SEND_FROM = "admin@thevish.io" IMAP_HOST = "127.0.0.1" IMAP_PORT = 1143 DIGEST_FOLDER = "Folders/Digests" # ── gather stats ───────────────────────────────────────────────────────────── def get_stats(db_path: Path, since: str) -> dict: """Query processed.db for entries since a given ISO timestamp.""" if not db_path.exists(): return {"total": 0, "categories": {}, "items": []} conn = sqlite3.connect(db_path) rows = conn.execute( "SELECT message_id, category, processed_at FROM processed WHERE processed_at >= ? ORDER BY processed_at DESC", (since,), ).fetchall() conn.close() categories = defaultdict(int) items = [] for msg_id, category, ts in rows: categories[category] += 1 items.append({"category": category, "time": ts}) return { "total": len(rows), "categories": dict(categories), "items": items, } def get_sender_cache_stats(db_path: Path) -> int: """Count entries in sender cache.""" if not db_path.exists(): return 0 conn = sqlite3.connect(db_path) try: row = conn.execute("SELECT COUNT(*) FROM sender_cache").fetchone() return row[0] if row else 0 except sqlite3.OperationalError: return 0 finally: conn.close() # ── format ─────────────────────────────────────────────────────────────────── def build_html(account_stats: list[dict], hours: int) -> str: """Build an HTML email body.""" total_all = sum(a["stats"]["total"] for a in account_stats) html_parts = [ "", f"

Email Organizer Digest

", f"

Last {hours} hours — {total_all} emails classified across {len(account_stats)} accounts.

", ] if total_all == 0: html_parts.append("

No new emails were classified in this period.

") else: for a in account_stats: stats = a["stats"] if stats["total"] == 0: continue html_parts.append(f"

{a['name']} ({stats['total']} classified)

") html_parts.append("") html_parts.append("") for cat, count in sorted(stats["categories"].items(), key=lambda x: -x[1]): html_parts.append(f"") html_parts.append("
CategoryCount
{cat}{count}
") if a.get("sender_cache"): html_parts.append(f"

Sender cache: {a['sender_cache']} known senders

") html_parts.append("
") now = datetime.now(tz=ZoneInfo("America/Los_Angeles")) html_parts.append(f"

Generated {now.strftime('%Y-%m-%d %H:%M %Z')} by email-digest.py

") html_parts.append("") return "\n".join(html_parts) def build_text(account_stats: list[dict], hours: int) -> str: """Build a plain-text email body.""" total_all = sum(a["stats"]["total"] for a in account_stats) lines = [ f"Email Organizer Digest — Last {hours} hours", f"Total: {total_all} emails classified across {len(account_stats)} accounts", "", ] if total_all == 0: lines.append("No new emails were classified in this period.") else: for a in account_stats: stats = a["stats"] if stats["total"] == 0: continue lines.append(f"--- {a['name']} ({stats['total']}) ---") for cat, count in sorted(stats["categories"].items(), key=lambda x: -x[1]): lines.append(f" {cat:>15}: {count}") lines.append("") return "\n".join(lines) # ── send ───────────────────────────────────────────────────────────────────── def file_to_digests(msg_bytes: bytes): """File a copy of the message into the Digests folder via Proton Bridge IMAP.""" ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE imap = imaplib.IMAP4(IMAP_HOST, IMAP_PORT) imap.starttls(ctx) imap.login(SMTP_USER, SMTP_PASS) # Create folder if it doesn't exist status, folders = imap.list() folder_exists = any(DIGEST_FOLDER.encode() in f for f in (folders or [])) if not folder_exists: imap.create(DIGEST_FOLDER) print(f"Created IMAP folder: {DIGEST_FOLDER}") # Append the message to the Digests folder now = imaplib.Time2Internaldate(datetime.now(tz=ZoneInfo("UTC"))) imap.append(DIGEST_FOLDER, None, now, msg_bytes) imap.logout() print(f"Filed digest to {DIGEST_FOLDER} folder") def send_email(subject: str, html_body: str, text_body: str): """Send email via Proton Bridge SMTP, then file into Digests folder.""" msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = SEND_FROM msg["To"] = SEND_TO msg["Date"] = datetime.now(tz=ZoneInfo("America/Los_Angeles")).strftime( "%a, %d %b %Y %H:%M:%S %z" ) msg.attach(MIMEText(text_body, "plain")) msg.attach(MIMEText(html_body, "html")) ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: server.starttls(context=ctx) server.login(SMTP_USER, SMTP_PASS) server.send_message(msg) print(f"Digest sent to {SEND_TO}") # File into Digests folder via IMAP try: file_to_digests(msg.as_bytes()) except Exception as e: print(f"Warning: failed to file digest to IMAP folder: {e}") # ── main ───────────────────────────────────────────────────────────────────── def main(): import argparse parser = argparse.ArgumentParser(description="Daily email organizer digest") parser.add_argument("--hours", type=int, default=24, help="Look back N hours (default: 24)") parser.add_argument("--dry-run", action="store_true", help="Print digest without sending") args = parser.parse_args() since = (datetime.now(tz=ZoneInfo("UTC")) - timedelta(hours=args.hours)).isoformat() account_stats = [] for acct in ACCOUNTS: stats = get_stats(acct["db"], since) sender_cache = get_sender_cache_stats(acct["db"]) account_stats.append({ "name": acct["name"], "stats": stats, "sender_cache": sender_cache, }) total = sum(a["stats"]["total"] for a in account_stats) now = datetime.now(tz=ZoneInfo("America/Los_Angeles")) subject = f"Email Digest: {total} classified — {now.strftime('%b %d')}" html_body = build_html(account_stats, args.hours) text_body = build_text(account_stats, args.hours) if args.dry_run: print(text_body) return send_email(subject, html_body, text_body) if __name__ == "__main__": main()