Files
homelab-optimized/scripts/email-digest.py
Gitea Mirror Bot 2be8f1fe17
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m1s
Documentation / Deploy to GitHub Pages (push) Has been skipped
Sanitized mirror from private repository - 2026-04-05 08:31:50 UTC
2026-04-05 08:31:50 +00:00

202 lines
7.2 KiB
Python

#!/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 &mdash; {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()