Skip to content

fix(smtp): unpack multipart/related containers with explicit preferencelist - #171

Open
choilund wants to merge 1 commit into
YoRyan:mainfrom
choilund:fix/multipart-related-body-parsing
Open

choilund wants to merge 1 commit into
YoRyan:mainfrom
choilund:fix/multipart-related-body-parsing

Conversation

@choilund

@choilund choilund commented Oct 2, 2026

Copy link
Copy Markdown

Summary

Fixes KeyError: 'multipart/related' when parsing incoming emails that contain rich HTML with inline images or nested multipart structures (such as notifications sent by Vaultwarden, Apple Mail, Outlook, and NVR software).

Resolves #135
Relates to #9, #136

Problem

In Python's email.message.EmailMessage.get_body(), the default preference list is:

def get_body(self, preferencelist=('related', 'html', 'plain')):

When an incoming email contains an HTML body with inline images or attachments, the MIME structure wraps it in a multipart/related container. Because 'related' is the first preference, msg.get_body() returns the multipart container envelope itself rather than the leaf body part.

Attempting to read content from this container raises KeyError: 'multipart/related' because standard library content managers only extract content from leaf MIME parts. Furthermore, when multipart/related is nested inside multipart/alternative, the default preference causes get_body() to bypass alternatives (issue #135).

Solution

Pass preferencelist=('html', 'plain') to msg.get_body() in src/mailrise/smtp.py:

py_body_part = msg.get_body(preferencelist=('html', 'plain'))
  • Standard Library Traversal: Directly instructs Python's MIME parser to traverse multipart/related containers and return the actual leaf text payload (text/html or text/plain).
  • 100% Backwards Compatible: Retains Mailrise's default behavior of preferring HTML when present and falling back to plain text, preserving existing contracts and passing all existing unit tests in tests/test_smtp.py.
  • Safe Fallback: Retains the existing _getmultiparttext helper as an additional fallback for non-standard MIME hierarchies.
  • No Breaking Config Shifts: Unlike PR Prefer plain text body when available #136, this does not flip default behavior to plain text or alter config schemas, making it a safe, drop-in bugfix.

Testing

  • Added test_multipart_nested_related() to tests/test_smtp.py covering an email with nested multipart/alternative containing text/plain and a multipart/related HTML part with inline image attachments.
  • Verified that all assertions in tests/test_smtp.py pass.

CodeDok pushed a commit to CodeDok/mailrise that referenced this pull request Oct 5, 2026
Upgrade to Apprise v2.0.1+ and require Python 3.10+.

Security:
- Reply 535 to failed SMTP logins instead of leaving the client hanging (YoRyan#101)
- Compare passwords in constant time and hide whether a username exists
- Lock out IP addresses after repeated failed logins
- Warn when SMTP auth is enabled without TLS; add smtp.auth.require_tls
- Don't leak router exception details to SMTP clients
- Add smtp.data_size_limit and smtp.max_recipients
- Sanitize attachment filenames and keep attachment files in a private
  temporary directory; escape control characters in logged email fields
- Load the configuration with YAML's safe loader
- Docker: apply Debian security updates, drop pip, root-owned code (YoRyan#170)

Fixes and features:
- Fix crash on multipart/related HTML emails (YoRyan#135, YoRyan#171)
- Refuse unparseable messages instead of crashing
- Report non-Apprise notification exceptions as failures
- Add sender-based routing (YoRyan#145, YoRyan#157) and dotted usernames in config
  keys (YoRyan#164)
- Add PUID/PGID/TZ Docker build arguments (YoRyan#149)
- Fix bdist_wheel build error with recent setuptools (YoRyan#160)

CI: test on Python 3.10-3.14, scan the image with Trivy, rebuild weekly,
and publish images only to ghcr.io/codedok/mailrise.

BREAKING CHANGE: requires Python 3.10+ and Apprise 2; the Docker image runs
as UID/GID 1000 and is no longer published to Docker Hub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Not parsing text/plain alternative body

1 participant