fix(hausmeister): Booking.com Marketing-Mails filtern, Mail-Dedup-Race beheben

Genius-/Rabatt-Mails von noreply@booking.com waren als wichtig markiert und
wurden stündlich erneut gemeldet, weil seen_mails durch State-Overwrite verloren ging.
This commit is contained in:
Homelab Cursor 2026-08-02 22:01:11 +02:00
parent 0c5a9d73a6
commit c19a4f654f
2 changed files with 37 additions and 5 deletions

View file

@ -199,6 +199,23 @@ MARKETING_FROM_PATTERNS = [
"promotion@", "promo@", "no-reply@mail.",
]
# Booking.com: noreply@ mischt echte Buchungsmails mit Genius-/Rabatt-Spam.
BOOKING_REAL_SUBJECT = [
"confirmation", "confirmed", "bestaetigung", "bestätigung", "buchung",
"reservation", "reservierung", "booking number", "buchungsnummer",
"cancelled", "canceled", "storn", "modified", "geändert", "geaendert",
"new message", "neue nachricht", "guest message", "nachricht von",
"payment", "zahlung", "invoice", "rechnung", "receipt", "beleg",
"check-in", "checkin", "pin code", "türcode", "tuercode", "door code",
"cancellation", "refund", "erstattung",
]
BOOKING_MARKETING_SUBJECT = [
"genius", "discount", "rabatt", "deal", "angebot", "% off", "%off",
"extending", "fancy", "inspired", "wishlist", "save on", "sparen",
"reminder:", "ready to use", "special offer", "last chance",
"you might like", "entdecken", "explore", "trip ideas",
]
def _is_marketing_mail(frm_lower: str) -> bool:
"""Werbemails von sonst wichtigen Absendern (z.B. booking.com campaign@)."""
@ -210,6 +227,19 @@ def _is_spam_sender(frm_lower: str) -> bool:
return any(s in frm_lower for s in SPAM_SENDERS)
def _is_booking_noise(frm_lower: str, subj_lower: str) -> bool:
"""Booking.com Marketing ausfiltern; echte Buchungs-/Gast-Mails behalten."""
if "booking.com" not in frm_lower:
return False
if any(p in subj_lower for p in BOOKING_REAL_SUBJECT):
return False
if any(p in subj_lower for p in BOOKING_MARKETING_SUBJECT):
return True
if "noreply@" in frm_lower or "campaign@" in frm_lower or "email.campaign@" in frm_lower:
return True
return False
def get_important_mails(hours: int = 24) -> list[dict]:
"""Mails von wichtigen Absendern (Bank, Hoster, etc.), Spam-Sender ausgefiltert."""
m = _connect()
@ -226,10 +256,13 @@ def get_important_mails(hours: int = 24) -> list[dict]:
parsed = _parse_mail(msg_data)
if parsed:
frm_lower = parsed["from"].lower()
subj_lower = (parsed.get("subject") or "").lower()
if _is_spam_sender(frm_lower):
continue
if _is_marketing_mail(frm_lower):
continue
if _is_booking_noise(frm_lower, subj_lower):
continue
if any(s in frm_lower for s in IMPORTANT_SENDERS):
results.append(parsed)
results.reverse()

View file

@ -409,8 +409,9 @@ def check_all(state: dict | None = None) -> list[str]:
mail_client.init(cfg)
important = mail_client.get_important_mails(hours=1)
if important and "error" not in important[0]:
state = _load_alert_state()
seen = state.get("seen_mails", {})
# Am gemeinsamen state-Objekt arbeiten (kein Reload/Zwischen-Save),
# sonst ueberschreibt run_check_and_alert() die seen_mails wieder.
seen = state.setdefault("seen_mails", {})
now = datetime.now(timezone.utc).timestamp()
new_mails = []
@ -422,9 +423,7 @@ def check_all(state: dict | None = None) -> list[str]:
new_mails.append(m)
seen[fp] = now
seen = {k: v for k, v in seen.items() if now - v < 172800}
state["seen_mails"] = seen
_save_alert_state(state)
state["seen_mails"] = {k: v for k, v in seen.items() if now - v < 172800}
if new_mails:
senders = [m["from"][:30] for m in new_mails]