Sanitized mirror from private repository - 2026-04-05 12:15:35 UTC
This commit is contained in:
201
scripts/email-digest.py
Normal file
201
scripts/email-digest.py
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Daily digest of email organizer activity — sends summary to admin@thevish.io via Proton Bridge."""
|
||||
|
||||
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"
|
||||
|
||||
|
||||
# ── 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 = [
|
||||
"<html><body>",
|
||||
f"<h2>Email Organizer Digest</h2>",
|
||||
f"<p>Last {hours} hours — {total_all} emails classified across {len(account_stats)} accounts.</p>",
|
||||
]
|
||||
|
||||
if total_all == 0:
|
||||
html_parts.append("<p><em>No new emails were classified in this period.</em></p>")
|
||||
else:
|
||||
for a in account_stats:
|
||||
stats = a["stats"]
|
||||
if stats["total"] == 0:
|
||||
continue
|
||||
html_parts.append(f"<h3>{a['name']} ({stats['total']} classified)</h3>")
|
||||
html_parts.append("<table border='1' cellpadding='6' cellspacing='0' style='border-collapse:collapse;'>")
|
||||
html_parts.append("<tr><th>Category</th><th>Count</th></tr>")
|
||||
for cat, count in sorted(stats["categories"].items(), key=lambda x: -x[1]):
|
||||
html_parts.append(f"<tr><td>{cat}</td><td>{count}</td></tr>")
|
||||
html_parts.append("</table>")
|
||||
|
||||
if a.get("sender_cache"):
|
||||
html_parts.append(f"<p><small>Sender cache: {a['sender_cache']} known senders</small></p>")
|
||||
|
||||
html_parts.append("<hr>")
|
||||
now = datetime.now(tz=ZoneInfo("America/Los_Angeles"))
|
||||
html_parts.append(f"<p><small>Generated {now.strftime('%Y-%m-%d %H:%M %Z')} by email-digest.py</small></p>")
|
||||
html_parts.append("</body></html>")
|
||||
|
||||
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 send_email(subject: str, html_body: str, text_body: str):
|
||||
"""Send email via Proton Bridge SMTP."""
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = SEND_FROM
|
||||
msg["To"] = SEND_TO
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
# ── 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()
|
||||
Reference in New Issue
Block a user