From b1d78cb0ca432cdae694615e4d3b454948fdd9c8 Mon Sep 17 00:00:00 2001 From: yuriyryabikov <22548029+kurok@users.noreply.github.com> Date: Fri, 12 Jun 2026 22:04:25 +0100 Subject: [PATCH] Propagate body decode errors instead of swallowing them The MIME parts loop in src/mail_parser.rs decoded each part body with `get_body_raw().unwrap_or_default()` and `get_body().unwrap_or_default()`. When mailparse failed to decode a body (broken base64/quoted-printable or an undecodable charset) the error was silently discarded and an empty value stored, hiding corruption from the caller. Propagate the `MailParseError` from `get_body_raw`/`get_body` up through `Mail::new` instead. The PyO3 layer already maps `MailParseError` to the Python `ParseError`, so callers now see decode failures rather than empty bodies. Valid messages (including valid base64) are unaffected. Add tests/test_decode_errors.py covering broken-base64 text bodies and attachments raising ParseError, plus valid base64 and plain messages still parsing. Closes #24 Signed-off-by: yuriyryabikov <22548029+kurok@users.noreply.github.com> --- src/mail_parser.rs | 12 ++++-- tests/test_decode_errors.py | 75 +++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 tests/test_decode_errors.py diff --git a/src/mail_parser.rs b/src/mail_parser.rs index eead781..44723ad 100644 --- a/src/mail_parser.rs +++ b/src/mail_parser.rs @@ -63,17 +63,23 @@ impl<'a> Mail { let attachment_name = mail.ctype.params.get("name"); let mime = mail.ctype.mimetype.as_str(); + // Propagate body decode failures instead of swallowing them with + // `unwrap_or_default()`. A broken transfer encoding (e.g. invalid + // base64/quoted-printable) or an undecodable charset would otherwise + // be silently turned into an empty body, hiding corruption from the + // caller. `get_body_raw`/`get_body` return `MailParseError`, which the + // PyO3 layer surfaces to Python as `ParseError`. attachments.push(Attachment { mimetype: mime.to_string(), - content: mail.get_body_raw().unwrap_or_default(), + content: mail.get_body_raw()?, filename: attachment_name.cloned().unwrap_or_default(), }); if attachment_name.is_none() { if mime == "text/plain" { - text_plain.push(mail.get_body().unwrap_or_default()) + text_plain.push(mail.get_body()?) } else if mime == "text/html" { - text_html.push(mail.get_body().unwrap_or_default()) + text_html.push(mail.get_body()?) } } } diff --git a/tests/test_decode_errors.py b/tests/test_decode_errors.py new file mode 100644 index 0000000..5f55ffa --- /dev/null +++ b/tests/test_decode_errors.py @@ -0,0 +1,75 @@ +"""Body-decode error tests for parse_email (issue #24). + +Previously `src/mail_parser.rs` decoded part bodies with +`get_body_raw().unwrap_or_default()` / `get_body().unwrap_or_default()`, which +silently turned a failed transfer-encoding decode (e.g. invalid base64) into an +empty body. That hid corruption from the caller. + +The fix propagates those `MailParseError`s through `Mail::new`; the PyO3 layer +maps them to `ParseError`. These tests assert broken encodings now surface as +`ParseError` while valid messages (including valid base64) still parse. +""" + +import base64 + +import pytest + +from fast_mail_parser import ParseError, parse_email + + +def test_broken_base64_text_body_raises(): + """A text/plain body declaring base64 with invalid base64 raises ParseError. + + This exercises the `get_body()` path. Before the fix the body was silently + stored as empty; now the decode error is surfaced. + """ + message = ( + b"Subject: broken\r\n" + b"Content-Type: text/plain\r\n" + b"Content-Transfer-Encoding: base64\r\n\r\n" + b"!!!!not_valid_base64!!!!\r\n" + ) + with pytest.raises(ParseError): + parse_email(message) + + +def test_broken_base64_attachment_raises(): + """A named attachment with invalid base64 raises ParseError. + + This exercises the `get_body_raw()` path used for every part's content. + """ + message = ( + b"Subject: bad-attach\r\n" + b'Content-Type: application/octet-stream; name="x.bin"\r\n' + b"Content-Transfer-Encoding: base64\r\n\r\n" + b"@@@not-base64@@@\r\n" + ) + with pytest.raises(ParseError): + parse_email(message) + + +def test_valid_base64_body_still_parses(): + """A valid base64-encoded text body still decodes (no regression).""" + encoded = base64.b64encode(b"hello base64 body").decode("ascii") + message = ( + b"Subject: ok\r\n" + b"Content-Type: text/plain\r\n" + b"Content-Transfer-Encoding: base64\r\n\r\n" + + encoded.encode("ascii") + + b"\r\n" + ) + mail = parse_email(message) + assert mail.subject == "ok" + assert any("hello base64 body" in part for part in mail.text_plain) + + +def test_plain_message_still_parses(): + """A normal, unencoded message still parses unaffected (no regression).""" + message = ( + b"Subject: hello\r\n" + b"Content-Type: text/plain\r\n\r\n" + b"plain body\r\n" + ) + mail = parse_email(message) + assert mail.subject == "hello" + assert any("plain body" in part for part in mail.text_plain)