Files
homelab-optimized/scripts/email-digest.py
Gitea Mirror Bot d72af152e3
Some checks failed
Documentation / Deploy to GitHub Pages (push) Has been cancelled
Documentation / Build Docusaurus (push) Has been cancelled
Sanitized mirror from private repository - 2026-04-16 07:19:56 UTC
2026-04-16 07:19:56 +00:00

240 lines
8.4 KiB
Python

#!/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 = [
"<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 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()