Initial resurrection kit — 81 scripts, 62 references, configs, systemd units, crons, Docker compose files, Caddy config, master README
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user