From f3a08f293791128b953ef2c9ea545416744871aa Mon Sep 17 00:00:00 2001 From: Guido Date: Sat, 22 Aug 2026 20:23:35 +0200 Subject: [PATCH] feat: Weitergeleitete Bank-Mails erkennen und Uebergangenes zeigen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wer Bank-Mails in ein Sammelpostfach weiterleitet, hat im From je nach Methode die eigene Adresse stehen. Geprueft werden deshalb auch Reply-To, Return-Path, Sender und X-Forwarded-For – bei automatischer Weiterleitung bleibt der Originalabsender dort erhalten. Ausserdem meldet der Abruf jetzt, welche Mails er uebergangen hat, mit Absender und Betreff. Beim Einrichten war das sonst Raten: Man sah nur "3 uebersprungen" und wusste nicht, was in den Filter gehoert. Co-Authored-By: Claude Opus 5 --- backend/app/services/mail_import.py | 30 ++++++++++++++++++--- backend/tests/test_mail_import.py | 36 ++++++++++++++++++++++++++ web/src/locales/de.json | 4 ++- web/src/locales/en.json | 4 ++- web/src/pages/settings/Postfaecher.jsx | 15 +++++++++++ 5 files changed, 84 insertions(+), 5 deletions(-) diff --git a/backend/app/services/mail_import.py b/backend/app/services/mail_import.py index 2b95a02..d7c9fbb 100644 --- a/backend/app/services/mail_import.py +++ b/backend/app/services/mail_import.py @@ -119,6 +119,20 @@ def _text_der_mail(msg) -> tuple[str, str]: return betreff, "" +def _absenderfeld(msg) -> str: + """Alles, was den ursprünglichen Absender verraten kann. + + Leitet ein Postfach automatisch weiter, bleibt ``From`` meist der + Originalabsender – verlassen kann man sich darauf aber nicht. Gmail und + andere setzen zusätzlich ``X-Forwarded-For``; bei manueller Weiterleitung + steht die eigene Adresse im ``From`` und die ursprüngliche höchstens im + ``Reply-To``. Geprüft wird deshalb alles zusammen. + """ + teile = [str(msg[k] or "") for k in + ("From", "Reply-To", "Return-Path", "X-Forwarded-For", "X-Original-From", "Sender")] + return " ".join(t for t in teile if t) + + def _passender_absender(absender: list[MailSender], von: str, betreff: str) -> MailSender | None: """Erste Zuordnung, deren Filter greifen. Ohne Filter passt sie auf alles.""" von_klein, betreff_klein = von.lower(), betreff.lower() @@ -151,6 +165,9 @@ def hole_neue(db: Session, quelle: MailSource) -> dict: verbindung = _verbinden(quelle) neu = uebersprungen = unklar = 0 + # Was durchgefallen ist, mit Absender und Betreff – sonst raet man beim + # Einrichten, was in den Filter gehoert. + ohne_zuordnung: list[dict] = [] try: code, _ = verbindung.select(f'"{quelle.folder}"') if code != "OK": @@ -180,10 +197,15 @@ def hole_neue(db: Session, quelle: MailSource) -> dict: continue betreff, text = _text_der_mail(msg) - von = str(msg["From"] or "") + von = _absenderfeld(msg) zuordnung = _passender_absender(absender, von, betreff) if zuordnung is None: uebersprungen += 1 + if len(ohne_zuordnung) < 5: + ohne_zuordnung.append({ + "from": str(msg["From"] or "")[:120], + "subject": betreff[:120], + }) continue gesendet = None @@ -228,7 +250,8 @@ def hole_neue(db: Session, quelle: MailSource) -> dict: verbindung.logout() except Exception: pass - return {"neu": neu, "uebersprungen": uebersprungen, "unklar": unklar} + return {"neu": neu, "uebersprungen": uebersprungen, "unklar": unklar, + "ohne_zuordnung": ohne_zuordnung} def _nachbereiten(verbindung: imaplib.IMAP4, quelle: MailSource, uid: bytes) -> None: @@ -253,4 +276,5 @@ def abruf_mit_fehlerbehandlung(db: Session, quelle: MailSource) -> dict: quelle.last_error = str(e)[:500] quelle.last_poll_at = datetime.now(timezone.utc) db.commit() - return {"neu": 0, "uebersprungen": 0, "unklar": 0, "fehler": str(e)} + return {"neu": 0, "uebersprungen": 0, "unklar": 0, + "ohne_zuordnung": [], "fehler": str(e)} diff --git a/backend/tests/test_mail_import.py b/backend/tests/test_mail_import.py index dcd4169..3a28d08 100644 --- a/backend/tests/test_mail_import.py +++ b/backend/tests/test_mail_import.py @@ -213,3 +213,39 @@ def test_verschieben_statt_nur_markieren(admin, fake_imap): admin.post(f"/api/mail/sources/{quelle['id']}/fetch") assert server.kopiert and "Belege" in server.kopiert[0][1] + + +def test_weitergeleitete_mail_wird_am_originalabsender_erkannt(admin, fake_imap, monkeypatch): + """Leitet man Bank-Mails in ein Sammelpostfach weiter, steht die eigene + Adresse im ``From``. Der Originalabsender bleibt aber in den + Weiterleitungs-Koepfen erhalten – daran muss die Zuordnung greifen.""" + konto = _konto(admin) + quelle = _postfach(admin) + admin.post(f"/api/mail/sources/{quelle['id']}/senders", json={ + "from_contains": "raiffeisen.ch", "account_id": konto["id"]}) + + roh = (MUSTER / "raiffeisen_zahlungsstatus.eml").read_bytes() + weitergeleitet = roh.replace( + b"From: noreply@ebanking.raiffeisen.ch", + b"From: ich@gmail.com\nX-Forwarded-For: noreply@ebanking.raiffeisen.ch ich@gmail.com", + ).replace(b" absenderAnlegen(q, d)} t={t} /> + {uebergangen.length > 0 && ( +
+ {t("mailbox.skippedTitle")} +

{t("mailbox.skippedHint")}

+
    + {uebergangen.map((m, i) => ( +
  • {m.from} — {m.subject}
  • + ))} +
+
+ )}