From abce86bd24802ec8fa16b18e44fc5b50db91cc25 Mon Sep 17 00:00:00 2001 From: kay ozaki Date: Tue, 28 Jul 2026 08:13:06 +0100 Subject: [PATCH] add check_trailing_bits: false While testing message parsing code, there's a chance that the sender isn't fully compliant with base64. This leads to failure such as below base64 decode: non-zero trailing bits at 20211 b='i' in HRtbD4NCi== As long as it's not destructive, its more convenient to be able to support non-RFC messages so we can extract message artifacts for its decision making. closes: https://github.com/KumoCorp/kumomta/pull/558 --- crates/mailparsing/src/mimepart.rs | 18 +++++++++++++++++- docs/changelog/main.md | 6 ++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/mailparsing/src/mimepart.rs b/crates/mailparsing/src/mimepart.rs index 07a2230b..8f29d48c 100644 --- a/crates/mailparsing/src/mimepart.rs +++ b/crates/mailparsing/src/mimepart.rs @@ -16,13 +16,17 @@ use std::str::FromStr; use std::sync::Arc; /// Define our own because data_encoding::BASE64_MIME, despite its name, -/// is not RFC2045 compliant, and will not ignore spaces +/// is not RFC2045 compliant, and will not ignore spaces. +/// check_trailing_bits is disabled because real-world MIME producers emit +/// final quantums whose discarded padding bits are non-zero; a strict decoder +/// would reject those otherwise-decodable bodies. const BASE64_RFC2045: data_encoding::Encoding = data_encoding_macro::new_encoding! { symbols: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", padding: '=', ignore: " \r\n\t", wrap_width: 76, wrap_separator: "\r\n", + check_trailing_bits: false, }; #[derive(Debug, Clone, PartialEq)] @@ -2282,4 +2286,16 @@ Body\r } ); } + + /// Test the use case of check_trailing_bits being false. + /// This accepts a final quantum whose discarded padding bits are non-zero, + /// which a strict base64 decoder would reject. + /// For reference, valid base64 is aHRtbD4NCg== + #[test] + fn check_trailing_bits_test() { + // The low 4 trailing bits before `==` are non-zero in this sample. + // With check_trailing_bits=false we should still accept and decode it. + let decoded = BASE64_RFC2045.decode(b"aHRtbD4NCi==").unwrap(); + assert_eq!(decoded, b"html>\r\n"); + } } diff --git a/docs/changelog/main.md b/docs/changelog/main.md index 9e8e035c..2730c56c 100644 --- a/docs/changelog/main.md +++ b/docs/changelog/main.md @@ -202,6 +202,11 @@ startup, that would otherwise lead to rocksdb corrupting itself on the restart *after* the permissions were broken. + * Base64-encoded MIME bodies whose final quantum has non-zero discarded + padding bits are now decoded rather than rejected, matching the + leniency other mail software applies to such non-canonical encodings. + Thanks to @kayozaki! #558 + ## Fixes * An SMTP command line containing bytes that are not valid UTF-8 is now @@ -234,3 +239,4 @@ after it was first persisted are now written back to the meta spool, rather than being silently dropped on the next reload from spool. Thanks to @alexanderwburch! #570 +