diff --git a/app/services/imap_client.py b/app/services/imap_client.py index 1cb3e80..9fd463e 100644 --- a/app/services/imap_client.py +++ b/app/services/imap_client.py @@ -79,13 +79,32 @@ def _parse_date(msg: Message) -> datetime | None: def _has_attachment(msg: Message) -> bool: - if not msg.is_multipart(): - return False - for part in msg.walk(): - disposition = str(part.get("Content-Disposition") or "") + """Erkennt Anhänge robuster als nur per Content-Disposition: attachment. + Manche Absender (z.B. CPS ORMS) hängen PDFs an, ohne diesen Header zu setzen — + der Dateiname steckt dann nur im filename-Parameter. Zusätzliches Kriterium: + ein Part, der einen Dateinamen hat UND kein reiner Text- oder Multipart-Container + ist, wird als Anhang gewertet. Damit werden PDF/Office/Bilder-Anhänge sicher + erkannt, ohne dass Inline-CSS oder HTML-Body fälschlich als Anhang zählt.""" + def _is_attachment_part(part: Message) -> bool: + disposition = str(part.get("Content-Disposition") or "").lower() if "attachment" in disposition: return True - return False + filename = part.get_filename() + if not filename: + return False + ctype = (part.get_content_type() or "").lower() + # text/* und multipart/* sind normalerweise Body-Teile, keine Anhänge + if ctype.startswith("text/") or ctype.startswith("multipart/"): + return False + return True + + if msg.is_multipart(): + for part in msg.walk(): + if _is_attachment_part(part): + return True + return False + # Single-part Mail kann auch ein Anhang sein (selten, aber möglich) + return _is_attachment_part(msg) def _extract_body(msg: Message) -> str: