119 lines
2.9 KiB
Python
119 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Check IMAP inbox for bounce-back / delivery-failure emails.
|
|
|
|
Silent (exit 0 with no output) when nothing to report.
|
|
Outputs summary when bounces are found.
|
|
"""
|
|
|
|
import imaplib
|
|
import email
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
HOST = "mail.germainebrown.com"
|
|
PORT = 993
|
|
USER = "g@germainebrown.com"
|
|
PW_FILE = "/root/.config/himalaya/g-germainebrown.pass"
|
|
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():
|
|
try:
|
|
pw = open(PW_FILE).read().strip()
|
|
except FileNotFoundError:
|
|
print("[SILENT]")
|
|
return
|
|
|
|
since = (datetime.now(timezone.utc) - timedelta(hours=CHECK_HOURS)).strftime("%d-%b-%Y")
|
|
|
|
try:
|
|
conn = imaplib.IMAP4_SSL(HOST, PORT)
|
|
conn.login(USER, pw)
|
|
conn.select("INBOX")
|
|
except Exception as e:
|
|
print(f"ERROR: IMAP connection failed: {e}")
|
|
sys.exit(1)
|
|
|
|
status, data = conn.search(None, f'(SINCE {since})')
|
|
if status != "OK":
|
|
print("[SILENT]")
|
|
conn.logout()
|
|
return
|
|
|
|
bounces = []
|
|
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):
|
|
subject = raw.get("Subject", "(no subject)")
|
|
sender = raw.get("From", "(unknown)")
|
|
date = raw.get("Date", "")
|
|
bounces.append({
|
|
"subject": subject,
|
|
"from": sender,
|
|
"date": date,
|
|
"uid": num.decode(),
|
|
})
|
|
|
|
conn.logout()
|
|
|
|
if not bounces:
|
|
return
|
|
|
|
print(f"📬 {len(bounces)} bounce-back(s) detected in the last {CHECK_HOURS}h:\n")
|
|
for b in bounces:
|
|
print(f" • {b['subject']}")
|
|
print(f" From: {b['from']}")
|
|
print(f" Date: {b['date']}")
|
|
print()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|