117 lines
3.3 KiB
Python
117 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Check shonuff@germainebrown.com inbox, find new replies, extract content.
|
|
Outputs JSON for the cron agent to summarize.
|
|
Silent if nothing new.
|
|
"""
|
|
import imaplib, email, json, os, hashlib
|
|
from email.utils import parsedate_to_datetime
|
|
from datetime import datetime, timezone
|
|
|
|
HOST = "mail.germainebrown.com"
|
|
PORT = 993
|
|
USER = "shonuff@germainebrown.com"
|
|
PW = "Catches.bullets1985"
|
|
SEEN_FILE = "/root/.hermes/scripts/.shonuff-processed.json"
|
|
TRUSTED = ["g@germainebrown.com"]
|
|
|
|
def load_seen():
|
|
if os.path.exists(SEEN_FILE):
|
|
return set(json.load(open(SEEN_FILE)))
|
|
return set()
|
|
|
|
def save_seen(seen):
|
|
json.dump(list(seen), open(SEEN_FILE, "w"))
|
|
|
|
def safe_str(val):
|
|
if val is None: return ""
|
|
if isinstance(val, str): return val
|
|
try: return str(val)
|
|
except: return ""
|
|
|
|
def get_text_body(msg):
|
|
"""Extract plain text from email, favoring text/plain."""
|
|
body = ""
|
|
if msg.is_multipart():
|
|
for part in msg.walk():
|
|
ct = part.get_content_type()
|
|
if ct == "text/plain":
|
|
payload = part.get_payload(decode=True)
|
|
if payload:
|
|
body += payload.decode("utf-8", errors="replace")
|
|
else:
|
|
payload = msg.get_payload(decode=True)
|
|
if payload:
|
|
body = payload.decode("utf-8", errors="replace")
|
|
# Strip excessive whitespace
|
|
import re
|
|
body = re.sub(r'\s+', ' ', body).strip()
|
|
return body[:2000] # limit to 2000 chars
|
|
|
|
def main():
|
|
seen = load_seen()
|
|
new_messages = []
|
|
|
|
try:
|
|
conn = imaplib.IMAP4_SSL(HOST, PORT)
|
|
conn.login(USER, PW)
|
|
conn.select("INBOX")
|
|
|
|
status, data = conn.search(None, "UNSEEN")
|
|
if status != "OK" or not data[0]:
|
|
conn.logout()
|
|
save_seen(seen)
|
|
return # silent
|
|
|
|
for num in data[0].split():
|
|
status, msg_data = conn.fetch(num, "(RFC822)")
|
|
if status != "OK":
|
|
continue
|
|
raw = email.message_from_bytes(msg_data[0][1])
|
|
sender = safe_str(raw.get("From", "")).strip()
|
|
subj = safe_str(raw.get("Subject", "(no subject)"))
|
|
msg_id = safe_str(raw.get("Message-ID", str(num)))
|
|
date = safe_str(raw.get("Date", ""))
|
|
|
|
# Skip Germaine
|
|
if any(t.lower() in sender.lower() for t in TRUSTED):
|
|
continue
|
|
|
|
# Skip self (shonuff's own Sent copies or BCC'd messages)
|
|
if "shonuff@germainebrown.com" in sender.lower():
|
|
continue
|
|
|
|
# Skip already processed
|
|
h = hashlib.md5(msg_id.encode()).hexdigest()
|
|
if h in seen:
|
|
continue
|
|
seen.add(h)
|
|
|
|
body = get_text_body(raw)
|
|
|
|
new_messages.append({
|
|
"from": sender,
|
|
"subject": subj,
|
|
"date": date,
|
|
"body_preview": body[:500],
|
|
"uid": num.decode(),
|
|
})
|
|
|
|
# Mark as read so we don't re-process
|
|
conn.store(num, "+FLAGS", "\\Seen")
|
|
|
|
conn.logout()
|
|
save_seen(seen)
|
|
|
|
except Exception as e:
|
|
print(json.dumps({"error": str(e)}))
|
|
return
|
|
|
|
if not new_messages:
|
|
return # silent
|
|
|
|
# Output JSON for the cron agent
|
|
print(json.dumps({"messages": new_messages}, indent=2))
|
|
|
|
if __name__ == "__main__":
|
|
main()
|