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,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check IMAP inbox for bounce-back / delivery-failure emails and alert if found.
|
||||
|
||||
Silent (exit 0 with no output) when nothing to report.
|
||||
Outputs summary when bounces are found.
|
||||
"""
|
||||
|
||||
import imaplib
|
||||
import email
|
||||
from email.utils import parsedate_to_datetime
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
HOST = "mail.germainebrown.com"
|
||||
PORT = 993
|
||||
ACCOUNTS = [
|
||||
{"user": "g@germainebrown.com", "pw_file": "/root/.config/himalaya/g-germainebrown.pass", "label": "Germaine"},
|
||||
{"user": "shonuff@germainebrown.com", "pw": "Catches.bullets1985", "label": "Sho'Nuff"},
|
||||
]
|
||||
CHECK_HOURS = 24
|
||||
|
||||
BOUNCE_KEYWORDS = [
|
||||
"mail delivery failed", "delivery status notification", "undelivered",
|
||||
"returned mail", "mail delivery failure", "non-delivery",
|
||||
"delivery failed", "undeliverable", "delivery report",
|
||||
"returned to sender", "message bounced",
|
||||
]
|
||||
|
||||
BOUNCE_SENDERS = [
|
||||
"mailer-daemon", "postmaster", "mail delivery system",
|
||||
]
|
||||
|
||||
|
||||
def safe_str(val):
|
||||
if val is None:
|
||||
return ""
|
||||
if isinstance(val, str):
|
||||
return val
|
||||
try:
|
||||
return str(val)
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def is_bounce(msg):
|
||||
subject = safe_str(msg.get("Subject", "")).lower()
|
||||
sender = safe_str(msg.get("From", "")).lower()
|
||||
|
||||
for kw in BOUNCE_KEYWORDS:
|
||||
if kw in subject:
|
||||
return True
|
||||
|
||||
for s in BOUNCE_SENDERS:
|
||||
if s in sender:
|
||||
return True
|
||||
|
||||
if msg.get_content_type() == "text/plain":
|
||||
payload = msg.get_payload(decode=True)
|
||||
if payload:
|
||||
body = safe_str(payload).lower()
|
||||
if "permanent error" in body or "could not be delivered" in body:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
all_bounces = []
|
||||
|
||||
since = (datetime.now(timezone.utc) - timedelta(hours=CHECK_HOURS)).strftime("%d-%b-%Y")
|
||||
|
||||
for acct in ACCOUNTS:
|
||||
try:
|
||||
if "pw" in acct:
|
||||
pw = acct["pw"]
|
||||
else:
|
||||
pw = open(acct["pw_file"]).read().strip()
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
|
||||
try:
|
||||
conn = imaplib.IMAP4_SSL(HOST, PORT)
|
||||
conn.login(acct["user"], pw)
|
||||
conn.select("INBOX")
|
||||
except Exception as e:
|
||||
print(f"ERROR: {acct['label']} IMAP failed: {e}")
|
||||
continue
|
||||
|
||||
status, data = conn.search(None, f'(SINCE {since})')
|
||||
if status != "OK":
|
||||
conn.logout()
|
||||
continue
|
||||
|
||||
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])
|
||||
if is_bounce(raw):
|
||||
all_bounces.append({
|
||||
"account": acct["label"],
|
||||
"subject": raw.get("Subject", "(no subject)"),
|
||||
"from": raw.get("From", "(unknown)"),
|
||||
"date": raw.get("Date", ""),
|
||||
})
|
||||
|
||||
conn.logout()
|
||||
|
||||
if not all_bounces:
|
||||
return
|
||||
|
||||
print(f"📬 {len(all_bounces)} bounce-back(s) detected in the last {CHECK_HOURS}h:\n")
|
||||
for b in all_bounces:
|
||||
print(f" [{b['account']}] {b['subject']}")
|
||||
print(f" From: {b['from']}")
|
||||
print(f" Date: {b['date']}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user