675 lines
25 KiB
Python
Executable File
675 lines
25 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Boys' Email Monitor — polls garrison@iamgmb.com and greyson@iamgmb.com inboxes,
|
|
logs new emails, tracks sender frequency, and sends daily summary emails.
|
|
|
|
Usage:
|
|
# Collect new emails (run every 60 min via cron)
|
|
python3 boys-mail-monitor.py collect
|
|
|
|
# Send daily summary (run at 7 PM ET via cron)
|
|
python3 boys-mail-monitor.py summary
|
|
|
|
# Test send (dry run summary to sho'nuff)
|
|
python3 boys-mail-monitor.py test-summary
|
|
"""
|
|
import imaplib
|
|
import email
|
|
import json
|
|
import os
|
|
import smtplib
|
|
import sys
|
|
import logging
|
|
import re
|
|
import random
|
|
|
|
# ── Sho'Nuff's signature ──────────────────────────
|
|
SHONUFF_TITLES = [
|
|
"Germaine's AI Ops Engineer",
|
|
"Germaine's Baddest AI Mofo Low Down Around This Town",
|
|
"Germaine's One-and-Only Digital Master",
|
|
"Germaine's Converse-Kicking Assistant",
|
|
"Keeper of Germaine's Teeth (Catches Bullets)",
|
|
"Germaine's AI Problem Child",
|
|
"Germaine's 24/7 Co-Pilot",
|
|
"Germaine's Digital Henchman",
|
|
"Germaine's Glow Up Coordinator",
|
|
"Germaine's Digital Sidekick",
|
|
"Germaine's AI Bodyguard",
|
|
"Germaine's Chaos Coordinator",
|
|
"Germaine's Full-Time Menace",
|
|
]
|
|
SHONUFF_CLOSINGS = [
|
|
"Who's the Master?",
|
|
"Kiss my Converse!",
|
|
"Am I the meanest?",
|
|
"Am I the prettiest?",
|
|
"Am I the baddest mofo low down around this town?",
|
|
"All right, Leroy. Who's the one-and-only Master?",
|
|
"YOU'LL... NEVER... USE... THIS... FOOT... AGAIN!",
|
|
"Catches bullets with his teeth?",
|
|
]
|
|
|
|
def shonuff_title():
|
|
return random.choice(SHONUFF_TITLES)
|
|
|
|
def shonuff_closing():
|
|
return random.choice(SHONUFF_CLOSINGS)
|
|
from datetime import datetime, timezone, timedelta
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.utils import parsedate_to_datetime, formataddr
|
|
from html import escape
|
|
|
|
# ── Configuration ──────────────────────────────────────────────────────────
|
|
|
|
BASE_DIR = "/root/.hermes"
|
|
DATA_DIR = os.path.join(BASE_DIR, "data")
|
|
SCRIPTS_DIR = os.path.join(BASE_DIR, "scripts")
|
|
LOG_FILE = "/var/log/boys-mail-monitor.log"
|
|
MAIL_LOG = os.path.join(DATA_DIR, "boys-mail.json")
|
|
SENDERS_DB = os.path.join(DATA_DIR, "boys-senders.json")
|
|
|
|
# IMAP — Boys' accounts
|
|
BOYS_ACCOUNTS = {
|
|
"Garrison": {
|
|
"host": "heracles.mxrouting.net",
|
|
"port": 993,
|
|
"user": "garrison@iamgmb.com",
|
|
"pw": "Baby.g2015!",
|
|
},
|
|
"Greyson": {
|
|
"host": "heracles.mxrouting.net",
|
|
"port": 993,
|
|
"user": "greyson@iamgmb.com",
|
|
"pw": "Boogie.woogie20",
|
|
},
|
|
}
|
|
|
|
# SMTP — Sho'Nuff's outbound
|
|
SMTP_HOST = "mail.germainebrown.com"
|
|
SMTP_PORT = 2525
|
|
SMTP_USER = "shonuff@germainebrown.com"
|
|
SMTP_PASS = "Catches.bullets1985"
|
|
SMTP_FROM = "shonuff@germainebrown.com"
|
|
SMTP_FROM_NAME = "Sho'Nuff Brown"
|
|
BCC_ADDR = "g@germainebrown.com"
|
|
|
|
# Recipient email addresses — PLACEHOLDERS until Germaine provides them
|
|
# Tina gets Garrison's summaries, Kate gets Greyson's summaries
|
|
RECIPIENTS = {
|
|
"Garrison": "tinamichelle1008@gmail.com",
|
|
"Greyson": "katherineeubank@gmail.com",
|
|
}
|
|
|
|
MOM_NAMES = {
|
|
"Garrison": "Tina",
|
|
"Greyson": "Kate",
|
|
}
|
|
|
|
HIGH_VOLUME_THRESHOLD = 3
|
|
|
|
# ── Logging ────────────────────────────────────────────────────────────────
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
handlers=[
|
|
logging.FileHandler(LOG_FILE),
|
|
logging.StreamHandler(),
|
|
],
|
|
)
|
|
log = logging.getLogger("boys-mail")
|
|
|
|
|
|
# ── Data helpers ───────────────────────────────────────────────────────────
|
|
|
|
def load_json(path, default):
|
|
if os.path.exists(path):
|
|
try:
|
|
with open(path) as f:
|
|
return json.load(f)
|
|
except (json.JSONDecodeError, OSError):
|
|
log.warning("Corrupt JSON %s, reinitializing", path)
|
|
return default
|
|
|
|
|
|
def save_json(path, data):
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
with open(path, "w") as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
|
|
def safe_str(val):
|
|
if val is None:
|
|
return ""
|
|
if isinstance(val, str):
|
|
return val
|
|
try:
|
|
return str(val)
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def extract_sender_name(from_header):
|
|
"""Extract a display name from a From header like 'John Doe <john@example.com>'."""
|
|
if not from_header:
|
|
return "Unknown Sender"
|
|
name, addr = email.utils.parseaddr(from_header)
|
|
return name if name else addr
|
|
|
|
|
|
def extract_sender_email(from_header):
|
|
"""Extract just the email address from a From header."""
|
|
if not from_header:
|
|
return "unknown@unknown"
|
|
name, addr = email.utils.parseaddr(from_header)
|
|
return addr.lower() if addr else from_header.lower().strip()
|
|
|
|
|
|
def parse_email_date(date_str):
|
|
"""Try to parse an email date string."""
|
|
try:
|
|
dt = parsedate_to_datetime(date_str)
|
|
return dt
|
|
except Exception:
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
def format_time_for_summary(email_date):
|
|
"""Format a datetime to something like '2:30 PM'."""
|
|
if isinstance(email_date, str):
|
|
email_date = parse_email_date(email_date)
|
|
# Convert to ET
|
|
et = email_date - timedelta(hours=4) # EDT roughly
|
|
return et.strftime("%I:%M %p").lstrip("0")
|
|
|
|
|
|
def format_date_header(date_obj):
|
|
"""Format a date like 'Wednesday, July 8, 2026'."""
|
|
if isinstance(date_obj, str):
|
|
date_obj = datetime.now(timezone.utc)
|
|
# Convert to ET
|
|
et = date_obj
|
|
return et.strftime("%A, %B %d, %Y")
|
|
|
|
|
|
def get_day_name(date_obj):
|
|
"""Get day name like 'Wednesday'."""
|
|
if isinstance(date_obj, str):
|
|
date_obj = datetime.now(timezone.utc)
|
|
return date_obj.strftime("%A")
|
|
|
|
|
|
# ── IMAP Polling ───────────────────────────────────────────────────────────
|
|
|
|
def poll_inbox(child_name, config, mail_log_data, senders_data):
|
|
"""Poll a single child's inbox for UNSEEN messages. Logs new ones."""
|
|
child_lower = child_name.lower()
|
|
new_count = 0
|
|
|
|
try:
|
|
conn = imaplib.IMAP4_SSL(config["host"], config["port"])
|
|
conn.login(config["user"], config["pw"])
|
|
conn.select("INBOX")
|
|
|
|
status, data = conn.search(None, "UNSEEN")
|
|
if status != "OK" or not data[0]:
|
|
conn.logout()
|
|
log.info("%s: No UNSEEN messages", child_name)
|
|
return 0
|
|
|
|
for num in data[0].split():
|
|
try:
|
|
# BODY.PEEK so we don't mark as seen — boys leave UNSEEN
|
|
status, msg_data = conn.fetch(num, "(BODY.PEEK[] BODY.PEEK[HEADER])")
|
|
if status != "OK":
|
|
status, msg_data = conn.fetch(num, "(RFC822)")
|
|
if status != "OK":
|
|
continue
|
|
raw = email.message_from_bytes(msg_data[0][1])
|
|
|
|
from_hdr = safe_str(raw.get("From", "")).strip()
|
|
subject = safe_str(raw.get("Subject", "(no subject)"))
|
|
date_str = safe_str(raw.get("Date", ""))
|
|
msg_id = safe_str(raw.get("Message-ID", str(num)))
|
|
|
|
sender_email = extract_sender_email(from_hdr)
|
|
sender_name = extract_sender_name(from_hdr)
|
|
|
|
# Build entry
|
|
entry = {
|
|
"child": child_name,
|
|
"from": from_hdr,
|
|
"from_name": sender_name,
|
|
"from_email": sender_email,
|
|
"subject": subject,
|
|
"date": date_str,
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"uid": num.decode(),
|
|
}
|
|
|
|
# Add to mail log
|
|
mail_log_data.append(entry)
|
|
|
|
# Update sender tracking
|
|
if sender_email not in senders_data:
|
|
senders_data[sender_email] = {
|
|
"name": sender_name,
|
|
"first_seen": entry["timestamp"],
|
|
"last_seen": entry["timestamp"],
|
|
"count": 0,
|
|
"child": child_name,
|
|
}
|
|
senders_data[sender_email]["last_seen"] = entry["timestamp"]
|
|
senders_data[sender_email]["count"] += 1
|
|
# Update name if we now have a better one
|
|
if sender_name != "Unknown Sender" and sender_name:
|
|
senders_data[sender_email]["name"] = sender_name
|
|
|
|
new_count += 1
|
|
log.info(
|
|
"%s: New email from %s — %s",
|
|
child_name, sender_email, subject[:60],
|
|
)
|
|
|
|
except Exception as e:
|
|
log.error("%s: Error processing message %s: %s", child_name, num, e)
|
|
continue
|
|
|
|
conn.logout()
|
|
|
|
except Exception as e:
|
|
log.error("%s: IMAP connection error: %s", child_name, e)
|
|
return 0
|
|
|
|
return new_count
|
|
|
|
|
|
def do_collect():
|
|
"""Collect new emails from both inboxes."""
|
|
log.info("=" * 50)
|
|
log.info("Boys' Mail Collection Run")
|
|
log.info("=" * 50)
|
|
|
|
mail_log = load_json(MAIL_LOG, [])
|
|
senders = load_json(SENDERS_DB, {})
|
|
total = 0
|
|
|
|
for child_name, config in BOYS_ACCOUNTS.items():
|
|
try:
|
|
count = poll_inbox(child_name, config, mail_log, senders)
|
|
total += count
|
|
except Exception as e:
|
|
log.error("Failed to poll %s: %s", child_name, e)
|
|
|
|
# Trim mail log — keep entries from the last 30 days using the script's timestamp
|
|
cutoff_ts = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat()
|
|
mail_log = [e for e in mail_log if e.get("timestamp", "") >= cutoff_ts]
|
|
|
|
save_json(MAIL_LOG, mail_log)
|
|
save_json(SENDERS_DB, senders)
|
|
|
|
log.info("Total new emails collected: %d", total)
|
|
log.info("Total tracked senders: %d", len(senders))
|
|
|
|
# Flag high-volume senders
|
|
for email_addr, info in senders.items():
|
|
if info["count"] >= HIGH_VOLUME_THRESHOLD and not info.get("flagged"):
|
|
info["flagged"] = True
|
|
log.warning(
|
|
"HIGH VOLUME: %s (%s) — %d emails for %s",
|
|
info["name"], email_addr, info["count"], info["child"],
|
|
)
|
|
save_json(SENDERS_DB, senders)
|
|
|
|
return total
|
|
|
|
|
|
# ── Email Builder ──────────────────────────────────────────────────────────
|
|
|
|
def build_summary_html(child_name, today_emails, senders_for_child):
|
|
"""Build the full HTML summary email body manually (no send-shonuff.py)."""
|
|
mom_name = MOM_NAMES.get(child_name, "Mom")
|
|
today = datetime.now(timezone.utc)
|
|
date_str = format_date_header(today)
|
|
|
|
# ── New Emails Table ───────────────────────────────────────────────
|
|
if not today_emails:
|
|
emails_section = (
|
|
'<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;'
|
|
'padding:16px;margin:16px 0;text-align:center;">'
|
|
'<p style="font-size:14px;color:#166534;margin:0;">✅ No new emails today</p>'
|
|
'</div>'
|
|
)
|
|
else:
|
|
rows = ""
|
|
for em in today_emails:
|
|
sender_display = em.get("from_name", em.get("from", "Unknown"))
|
|
subj = escape(em.get("subject", "(no subject)"))
|
|
t = format_time_for_summary(em.get("date", ""))
|
|
rows += (
|
|
"<tr>"
|
|
f'<td style="padding:8px 10px;border:1px solid #e5e7eb;">{escape(sender_display)}</td>'
|
|
f'<td style="padding:8px 10px;border:1px solid #e5e7eb;">{subj}</td>'
|
|
f'<td style="padding:8px 10px;border:1px solid #e5e7eb;white-space:nowrap;">{t}</td>'
|
|
"</tr>"
|
|
)
|
|
|
|
emails_section = f"""
|
|
<h3 style="font-size:14px;font-weight:600;margin:20px 0 8px;">📨 New Emails</h3>
|
|
<table style="width:100%;border-collapse:collapse;font-size:13px;">
|
|
<tr style="background:#f3f4f6;">
|
|
<th style="padding:8px 10px;border:1px solid #e5e7eb;text-align:left;font-weight:600;">From</th>
|
|
<th style="padding:8px 10px;border:1px solid #e5e7eb;text-align:left;font-weight:600;">Subject</th>
|
|
<th style="padding:8px 10px;border:1px solid #e5e7eb;text-align:left;font-weight:600;">Time</th>
|
|
</tr>
|
|
{rows}
|
|
</table>
|
|
"""
|
|
|
|
# ── Sender Frequency Section ───────────────────────────────────────
|
|
if not senders_for_child:
|
|
frequency_section = ""
|
|
else:
|
|
# Sort by count descending
|
|
sorted_senders = sorted(
|
|
senders_for_child.items(), key=lambda x: x[1]["count"], reverse=True
|
|
)
|
|
freq_rows = ""
|
|
for sender_email, info in sorted_senders:
|
|
name = info.get("name", sender_email)
|
|
count = info["count"]
|
|
note = ""
|
|
bg = ""
|
|
if count >= HIGH_VOLUME_THRESHOLD:
|
|
note = "⚠️ High volume — potential subscription"
|
|
bg = ' style="background:#fef3c7;"'
|
|
|
|
freq_rows += (
|
|
f"<tr{bg}>"
|
|
f'<td style="padding:8px 10px;border:1px solid #e5e7eb;">{escape(name)}<br><span style="font-size:11px;color:#999;">{escape(sender_email)}</span></td>'
|
|
f'<td style="padding:8px 10px;border:1px solid #e5e7eb;text-align:center;font-weight:600;">{count}</td>'
|
|
f'<td style="padding:8px 10px;border:1px solid #e5e7eb;font-size:12px;">{note}</td>'
|
|
"</tr>"
|
|
)
|
|
|
|
frequency_section = f"""
|
|
<h3 style="font-size:14px;font-weight:600;margin:20px 0 8px;">📊 This Week's Top Senders</h3>
|
|
<table style="width:100%;border-collapse:collapse;font-size:13px;">
|
|
<tr style="background:#f3f4f6;">
|
|
<th style="padding:8px 10px;border:1px solid #e5e7eb;text-align:left;font-weight:600;">Sender</th>
|
|
<th style="padding:8px 10px;border:1px solid #e5e7eb;text-align:center;font-weight:600;">Count</th>
|
|
<th style="padding:8px 10px;border:1px solid #e5e7eb;text-align:left;font-weight:600;">Note</th>
|
|
</tr>
|
|
{freq_rows}
|
|
</table>
|
|
"""
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html>
|
|
<head><meta charset="utf-8"></head>
|
|
<body style="font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;color:#1a1a1a;max-width:560px;margin:0 auto;padding:20px;">
|
|
|
|
<!-- Red divider title bar -->
|
|
<div style="border-bottom:2px solid #dc2626;padding-bottom:12px;margin-bottom:20px;">
|
|
<h2 style="margin:0;font-size:18px;font-weight:600;">📬 {escape(child_name)}'s Email Summary</h2>
|
|
<p style="color:#666;font-size:12px;margin:4px 0 0;">{escape(date_str)}</p>
|
|
</div>
|
|
|
|
<!-- Body content -->
|
|
<p style="font-size:14px;line-height:1.6;color:#333;">Hey {escape(mom_name)}!</p>
|
|
|
|
<p style="font-size:14px;line-height:1.6;">Here's what came through today:</p>
|
|
|
|
{emails_section}
|
|
|
|
{frequency_section}
|
|
|
|
<!-- Circular signature -->
|
|
<p style="font-size:12px;color:#888;font-style:italic;margin:0 0 12px;">_{shonuff_closing()}_</p>
|
|
<hr style="border:none;height:2px;width:40px;background:#cc0000;margin:0 0 12px 0;border-radius:2px;">
|
|
<table cellpadding="0" cellspacing="0" border="0" style="font-family:'Inter',-apple-system,sans-serif;color:#2c2c2c;font-size:13px;line-height:1.6;">
|
|
<tr>
|
|
<td style="padding-right:14px;vertical-align:middle;width:80px;">
|
|
<img src="https://core.itpropartner.com/shonuff-signature.png" alt="SB" width="72" height="auto" style="border-radius:50%;display:block;max-width:72px;">
|
|
</td>
|
|
<td style="vertical-align:middle;">
|
|
<div style="font-size:15px;font-weight:700;color:#1a1a1a;letter-spacing:-0.2px;">Sho'Nuff Brown</div>
|
|
<div style="font-size:12px;color:#888;margin-bottom:1px;">{shonuff_title()}</div>
|
|
<div style="font-size:12px;color:#aaa;margin-bottom:6px;">iAmGMB</div>
|
|
<table cellpadding="0" cellspacing="0" border="0" style="font-size:12px;color:#999;line-height:1.7;">
|
|
<tr><td style="padding-right:4px;vertical-align:top;white-space:nowrap;color:#bbb;">@</td><td><a href="mailto:shonuff@germainebrown.com" style="color:#555;text-decoration:none;border-bottom:1px dotted #ccc;">shonuff@germainebrown.com</a></td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<p style="font-size:11px;color:#aaa;">Reply to adjust how often you get these summaries, request {escape(child_name)}'s login credentials for your phone, or flag senders to unsubscribe.</p>
|
|
<p style="font-size:11px;color:#aaa;">Webmail: <a href="http://webmail.iamgmb.com" style="color:#1d4ed8;">webmail.iamgmb.com</a></p>
|
|
</body>
|
|
</html>"""
|
|
|
|
return html
|
|
|
|
|
|
def build_plain_text(child_name, today_emails, senders_for_child):
|
|
"""Build a plain-text fallback version."""
|
|
mom_name = MOM_NAMES.get(child_name, "Mom")
|
|
lines = [f"{child_name}'s Email Summary", "=" * 40, ""]
|
|
lines.append(f"Hey {mom_name},")
|
|
lines.append("")
|
|
lines.append("Here's what came through today:")
|
|
lines.append("")
|
|
|
|
if not today_emails:
|
|
lines.append("✅ No new emails today")
|
|
else:
|
|
lines.append("📨 New Emails:")
|
|
lines.append("-" * 40)
|
|
for em in today_emails:
|
|
sender = em.get("from_name", em.get("from", "Unknown"))
|
|
subj = em.get("subject", "(no subject)")
|
|
t = format_time_for_summary(em.get("date", ""))
|
|
lines.append(f" From: {sender}")
|
|
lines.append(f" Re: {subj}")
|
|
lines.append(f" Time: {t}")
|
|
lines.append("")
|
|
|
|
if senders_for_child:
|
|
lines.append("📊 Top Senders This Week:")
|
|
lines.append("-" * 40)
|
|
sorted_senders = sorted(
|
|
senders_for_child.items(), key=lambda x: x[1]["count"], reverse=True
|
|
)
|
|
for sender_email, info in sorted_senders:
|
|
name = info.get("name", sender_email)
|
|
count = info["count"]
|
|
note = " ⚠️ HIGH VOLUME" if count >= HIGH_VOLUME_THRESHOLD else ""
|
|
lines.append(f" {name} ({sender_email}): {count}{note}")
|
|
|
|
lines.append("")
|
|
lines.append("--")
|
|
lines.append("Sho'Nuff Brown — Operations Engineer")
|
|
lines.append("shonuff@germainebrown.com")
|
|
lines.append("")
|
|
lines.append(f"Reply to adjust frequency, request {child_name}'s login credentials for your phone, or flag senders to unsubscribe.")
|
|
lines.append("Webmail: http://webmail.iamgmb.com")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def send_email(child_name, today_emails, senders_for_child, to_addr):
|
|
"""
|
|
Send a daily summary email for a child.
|
|
Builds HTML manually (no send-shonuff.py wrapper).
|
|
"""
|
|
html = build_summary_html(child_name, today_emails, senders_for_child)
|
|
plain = build_plain_text(child_name, today_emails, senders_for_child)
|
|
|
|
subject = f"📬 {child_name}'s Email Summary — {datetime.now(timezone.utc).strftime('%b %d')}"
|
|
|
|
msg = MIMEMultipart("alternative")
|
|
msg["From"] = f"{SMTP_FROM_NAME} <{SMTP_FROM}>"
|
|
msg["To"] = to_addr
|
|
msg["Bcc"] = BCC_ADDR
|
|
msg["Subject"] = subject
|
|
|
|
msg.attach(MIMEText(plain, "plain"))
|
|
msg.attach(MIMEText(html, "html"))
|
|
|
|
try:
|
|
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as s:
|
|
s.starttls()
|
|
s.login(SMTP_USER, SMTP_PASS)
|
|
s.send_message(msg)
|
|
log.info(
|
|
"Summary sent for %s to %s (BCC: %s)",
|
|
child_name, to_addr, BCC_ADDR,
|
|
)
|
|
return True
|
|
except Exception as e:
|
|
log.error("Failed to send summary for %s: %s", child_name, e)
|
|
return False
|
|
|
|
|
|
# ── Summary Generation ─────────────────────────────────────────────────────
|
|
|
|
def do_summary():
|
|
"""Generate and send daily email summaries for both children."""
|
|
log.info("=" * 50)
|
|
log.info("Boys' Daily Summary Run")
|
|
log.info("=" * 50)
|
|
|
|
mail_log = load_json(MAIL_LOG, [])
|
|
senders = load_json(SENDERS_DB, {})
|
|
|
|
# Get today's date in ET
|
|
today = datetime.now(timezone.utc) - timedelta(hours=4) # EDT
|
|
today_str = today.strftime("%Y-%m-%d")
|
|
|
|
log.info("Processing summaries for date: %s", today_str)
|
|
log.info("Total mail log entries: %d", len(mail_log))
|
|
log.info("Total tracked senders: %d", len(senders))
|
|
|
|
results = []
|
|
|
|
for child_name in BOYS_ACCOUNTS:
|
|
mom_name = MOM_NAMES.get(child_name, "Mom")
|
|
to_addr = RECIPIENTS.get(child_name)
|
|
|
|
# Filter today's emails for this child
|
|
today_emails = [
|
|
e for e in mail_log
|
|
if e.get("child") == child_name
|
|
and parse_email_date(e.get("date", "")).strftime("%Y-%m-%d") == today_str
|
|
]
|
|
|
|
# Filter senders for this child
|
|
senders_for_child = {
|
|
email_addr: info
|
|
for email_addr, info in senders.items()
|
|
if info.get("child") == child_name
|
|
}
|
|
|
|
log.info(
|
|
"%s: %d emails today, %d tracked senders, sending to %s",
|
|
child_name, len(today_emails), len(senders_for_child), to_addr,
|
|
)
|
|
|
|
if to_addr and "@" in to_addr and "example.com" not in to_addr:
|
|
success = send_email(child_name, today_emails, senders_for_child, to_addr)
|
|
results.append((child_name, to_addr, success, len(today_emails)))
|
|
else:
|
|
log.warning(
|
|
"%s: No valid recipient email configured for %s (%s). "
|
|
"Germaine needs to provide it!",
|
|
child_name, mom_name, to_addr,
|
|
)
|
|
# Print the summary to log anyway for verification
|
|
log.info(
|
|
"Would have sent to %s:\n%s",
|
|
to_addr,
|
|
build_summary_html(child_name, today_emails, senders_for_child),
|
|
)
|
|
results.append((child_name, to_addr, False, len(today_emails)))
|
|
|
|
log.info("Summary run complete: %s", results)
|
|
return results
|
|
|
|
|
|
def do_test_summary():
|
|
"""Send a test summary to Sho'Nuff himself to verify formatting."""
|
|
log.info("=" * 50)
|
|
log.info("Boys' Mail — Test Summary (sent to shonuff inbox)")
|
|
log.info("=" * 50)
|
|
|
|
mail_log = load_json(MAIL_LOG, [])
|
|
senders = load_json(SENDERS_DB, {})
|
|
|
|
today = datetime.now(timezone.utc) - timedelta(hours=4)
|
|
today_str = today.strftime("%Y-%m-%d")
|
|
|
|
for child_name in BOYS_ACCOUNTS:
|
|
today_emails = [
|
|
e for e in mail_log
|
|
if e.get("child") == child_name
|
|
and parse_email_date(e.get("date", "")).strftime("%Y-%m-%d") == today_str
|
|
]
|
|
senders_for_child = {
|
|
email_addr: info
|
|
for email_addr, info in senders.items()
|
|
if info.get("child") == child_name
|
|
}
|
|
|
|
# Send test to sho'nuff
|
|
subject = f"📬 TEST: {child_name}'s Email Summary — {today.strftime('%b %d')}"
|
|
html = build_summary_html(child_name, today_emails, senders_for_child)
|
|
plain = build_plain_text(child_name, today_emails, senders_for_child)
|
|
|
|
msg = MIMEMultipart("alternative")
|
|
msg["From"] = f"{SMTP_FROM_NAME} <{SMTP_FROM}>"
|
|
msg["To"] = SMTP_FROM
|
|
msg["Subject"] = subject
|
|
|
|
msg.attach(MIMEText(plain, "plain"))
|
|
msg.attach(MIMEText(html, "html"))
|
|
|
|
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as s:
|
|
s.starttls()
|
|
s.login(SMTP_USER, SMTP_PASS)
|
|
s.send_message(msg)
|
|
|
|
log.info("Test summary for %s sent to %s", child_name, SMTP_FROM)
|
|
|
|
print("✅ Test summaries sent to shonuff@germainebrown.com — check your inbox.")
|
|
|
|
|
|
# ── Main CLI ───────────────────────────────────────────────────────────────
|
|
|
|
def main():
|
|
os.makedirs(DATA_DIR, exist_ok=True)
|
|
|
|
if len(sys.argv) < 2:
|
|
print("Usage: boys-mail-monitor.py [collect|summary|test-summary]")
|
|
print(" collect — Poll inboxes for new emails")
|
|
print(" summary — Send daily summary emails")
|
|
print(" test-summary — Send test summaries to shonuff inbox")
|
|
return
|
|
|
|
command = sys.argv[1]
|
|
|
|
if command == "collect":
|
|
do_collect()
|
|
elif command == "summary":
|
|
do_summary()
|
|
elif command == "test-summary":
|
|
do_test_summary()
|
|
else:
|
|
print(f"Unknown command: {command}")
|
|
print("Usage: boys-mail-monitor.py [collect|summary|test-summary]")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|