detect attachments without Content-Disposition: attachment header
The cps datensysteme rule (from cps-datensysteme.de + subject "Rechnung" + has_attachment true + date after) was failing on real invoices from CPS ORMS. Their PDFs arrive without a Content-Disposition header — the filename lives only on the Content-Type parameter — so _has_attachment returned False and the rule never matched. Extended the check to also count a part as an attachment when it has a filename() and its content-type is neither text/* nor multipart/*. That catches PDFs, Office and image attachments regardless of whether the sender set Content-Disposition, without treating body parts as attachments. Verified against four synthetic cases including the exact CPS ORMS shape. Same helper is now applied to non-multipart mails too, in case an entire mail is a single attached file (rare but possible). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user