fix: preserve inline and positional attachments when parsing multipart emails

linuztx committed Mar 23, 2026 at 14:01 UTC 67a07e4b9641545ee5fe082fe6726bdc1977aab9
1 file changed +23 -12
plugins/_email_integration/helpers/imap_client.py
+23 -12
@@ -321,6 +321,7 @@ async def _parse_body(
321 body = ""
322 attachments: list[str] = []
323 cid_map: dict[str, str] = {}
324 + body_parts: list[str] = []
325
326 if email_msg.is_multipart():
327 for part in email_msg.walk():
@@ -341,17 +342,27 @@ async def _parse_body(
342 cid = part.get("Content-ID")
343 if cid:
344 cid_map[cid.strip("<>")] = path
344 -
345 - elif content_type == "text/plain" and not body:
346 - charset = part.get_content_charset() or "utf-8"
347 - payload = part.get_payload(decode=True)
348 - body = payload.decode(charset, errors="ignore") if isinstance(payload, bytes) else ""
349 -
350 - elif content_type == "text/html" and not body:
351 - charset = part.get_content_charset() or "utf-8"
352 - payload = part.get_payload(decode=True)
353 - html = payload.decode(charset, errors="ignore") if isinstance(payload, bytes) else ""
354 - body = _html_to_text(html, cid_map)
345 +
346 + if not cid and body_parts:
347 + body_parts.append(f"\n[attachment://{path}]\n")
348 +
349 + elif content_type == "text/plain":
350 + if not body:
351 + charset = part.get_content_charset() or "utf-8"
352 + payload = part.get_payload(decode=True)
353 + body = payload.decode(charset, errors="ignore") if isinstance(payload, bytes) else ""
354 + body_parts.append(body)
355 +
356 + elif content_type == "text/html":
357 + if not body:
358 + charset = part.get_content_charset() or "utf-8"
359 + payload = part.get_payload(decode=True)
360 + html = payload.decode(charset, errors="ignore") if isinstance(payload, bytes) else ""
361 + body = _html_to_text(html, cid_map)
362 + body_parts.append(body)
363 +
364 + if len(body_parts) > 1:
365 + body = "".join(body_parts)
366 else:
367 content_type = email_msg.get_content_type()
368 charset = email_msg.get_content_charset() or "utf-8"
@@ -394,7 +405,7 @@ def _html_to_text(html_content: str, cid_map: dict[str, str] | None = None) -> s
405 if src.startswith("cid:"):
406 cid = src[4:]
407 if cid in cid_map:
397 - img.replace_with(soup.new_string(f"[file://{cid_map[cid]}]"))
408 + img.replace_with(soup.new_string(f"[attachment://{cid_map[cid]}]"))
409 html_content = str(soup)
410
411 h = html2text.HTML2Text()