use base64::{
alphabet,
engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig},
Engine as _,
};
const BASE64_EMAIL: GeneralPurpose = GeneralPurpose::new(
&alphabet::STANDARD,
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
);
use crate::{
error::ParseError,
message::DecodedBodyValue,
part::{ParsedPart, TransferEncoding},
};
pub fn decode_body_value(
raw: &[u8],
part: &ParsedPart,
max_bytes: Option<usize>,
) -> Result<DecodedBodyValue, ParseError> {
let (offset_u32, length_u32) = part.body_range;
let offset = offset_u32 as usize;
let length = length_u32 as usize;
let end = offset.checked_add(length).ok_or(ParseError::InvalidRange {
offset: offset_u32,
length: length_u32,
available: raw.len() as u64,
})?;
if end > raw.len() {
return Err(ParseError::InvalidRange {
offset: offset_u32,
length: length_u32,
available: raw.len() as u64,
});
}
let body_bytes = &raw[offset..end];
let mut is_encoding_problem = false;
let mut input_was_limited = false;
let decoded: Vec<u8> = match part.transfer_encoding {
TransferEncoding::Base64 => {
let max_b64_chars = max_bytes
.map(|n| n.saturating_mul(4).div_ceil(3).next_multiple_of(4))
.unwrap_or(usize::MAX);
let mut stripped = Vec::with_capacity(max_b64_chars.min(body_bytes.len()));
for &b in body_bytes {
if b == b'\r' || b == b'\n' {
continue;
}
if stripped.len() >= max_b64_chars {
input_was_limited = true;
break;
}
stripped.push(b);
}
match BASE64_EMAIL.decode(&stripped) {
Ok(v) => v,
Err(_) => {
is_encoding_problem = true;
Vec::new()
}
}
}
TransferEncoding::QuotedPrintable => {
let (decoded, limited, enc_err) = qp_with_limit(body_bytes, max_bytes);
input_was_limited = limited;
is_encoding_problem = enc_err;
decoded
}
TransferEncoding::UUEncode => decode_uuencode(
body_bytes,
max_bytes,
&mut is_encoding_problem,
&mut input_was_limited,
),
TransferEncoding::Identity
| TransferEncoding::SevenBit
| TransferEncoding::EightBit
| TransferEncoding::Binary => {
let truncated = max_bytes.map_or(body_bytes, |n| {
let limit = n.min(body_bytes.len());
input_was_limited = limit < body_bytes.len();
&body_bytes[..limit]
});
truncated.to_vec()
}
};
let (truncated_bytes, is_truncated) = match max_bytes {
Some(n) if decoded.len() > n => (decoded[..n].to_vec(), true),
_ => (decoded, input_was_limited),
};
let charset = part.charset.as_deref().unwrap_or("utf-8");
let enc = encoding_rs::Encoding::for_label(charset.as_bytes()).unwrap_or(encoding_rs::UTF_8);
let (cow, _, had_errors) = enc.decode(&truncated_bytes);
is_encoding_problem |= had_errors;
let value = cow.into_owned();
Ok(DecodedBodyValue {
value,
is_truncated,
is_encoding_problem,
})
}
fn qp_with_limit(body: &[u8], max_bytes: Option<usize>) -> (Vec<u8>, bool, bool) {
let mut input_was_limited = false;
let qp_input = max_bytes.map_or(body, |n| {
let limit = n.saturating_mul(4).min(body.len());
input_was_limited = limit < body.len();
&body[..limit]
});
let decoded_preview =
match quoted_printable::decode(qp_input, quoted_printable::ParseMode::Robust) {
Ok(v) => v,
Err(_) => return (Vec::new(), false, true),
};
if input_was_limited {
if let Some(n) = max_bytes {
if decoded_preview.len() <= n {
match quoted_printable::decode(body, quoted_printable::ParseMode::Robust) {
Ok(v) => return (v, false, false),
Err(_) => return (Vec::new(), false, true),
}
}
}
}
(decoded_preview, input_was_limited, false)
}
fn decode_uuencode(
body: &[u8],
max_bytes: Option<usize>,
is_encoding_problem: &mut bool,
input_was_limited: &mut bool,
) -> Vec<u8> {
match uuencoding::decode_limited(body, max_bytes) {
Err(_) => {
*is_encoding_problem = true;
Vec::new()
}
Ok(block) => {
if block.is_truncated {
if block.was_limit_hit {
*input_was_limited = true;
} else {
*is_encoding_problem = true;
}
}
block.data
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::part::{ParsedPart, TransferEncoding};
fn make_part(
body_bytes: &[u8],
transfer_encoding: TransferEncoding,
charset: Option<&str>,
) -> (Vec<u8>, ParsedPart) {
let prefix = b"fake-header: x\r\n\r\n";
let mut raw: Vec<u8> = prefix.to_vec();
let offset = raw.len();
raw.extend_from_slice(body_bytes);
let length = body_bytes.len();
let part = ParsedPart {
part_id: "1".to_owned(),
content_type: "text/plain".to_owned(),
charset: charset.map(str::to_owned),
transfer_encoding,
disposition: None,
filename: None,
cid: None,
header_range: (0u32, offset as u32),
body_range: (offset as u32, length as u32),
children: vec![],
is_encoding_problem: false,
};
(raw, part)
}
#[test]
fn test_base64_body() {
let b64 = b"SGVsbG8sIFdvcmxkIQ==";
let (raw, part) = make_part(b64, TransferEncoding::Base64, Some("utf-8"));
let result = decode_body_value(&raw, &part, None).unwrap();
assert_eq!(result.value, "Hello, World!");
assert!(!result.is_truncated);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_base64_missing_padding_tolerant() {
let b64_no_pad = b"SGVsbG8sIFdvcmxkIQ";
let (raw, part) = make_part(b64_no_pad, TransferEncoding::Base64, Some("utf-8"));
let result = decode_body_value(&raw, &part, None).unwrap();
assert_eq!(result.value, "Hello, World!");
assert!(
!result.is_encoding_problem,
"missing padding must not be an error"
);
}
#[test]
fn test_base64_invalid_chars_sets_encoding_problem() {
let b64_bad = b"!!!not-base64!!!";
let (raw, part) = make_part(b64_bad, TransferEncoding::Base64, Some("utf-8"));
let result = decode_body_value(&raw, &part, None).unwrap();
assert!(result.is_encoding_problem);
assert!(result.value.is_empty());
}
#[test]
fn test_quoted_printable_body() {
let qp = b"caf=C3=A9";
let (raw, part) = make_part(qp, TransferEncoding::QuotedPrintable, Some("utf-8"));
let result = decode_body_value(&raw, &part, None).unwrap();
assert_eq!(result.value, "caf\u{e9}"); assert!(!result.is_truncated);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_latin1_charset() {
let latin1 = b"\xe9";
let (raw, part) = make_part(latin1, TransferEncoding::Identity, Some("iso-8859-1"));
let result = decode_body_value(&raw, &part, None).unwrap();
assert_eq!(result.value, "\u{e9}"); assert!(!result.is_truncated);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_max_bytes_truncation() {
let body = b"Hello, World!";
let (raw, part) = make_part(body, TransferEncoding::Identity, Some("utf-8"));
let result = decode_body_value(&raw, &part, Some(5)).unwrap();
assert_eq!(result.value, "Hello");
assert!(result.is_truncated);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_base64_is_truncated_multiple_of_3() {
let b64 = b"SGVsbG8sIFdvcmxkIQ==";
let (raw, part) = make_part(b64, TransferEncoding::Base64, Some("utf-8"));
let result = decode_body_value(&raw, &part, Some(3)).unwrap();
assert!(
result.is_truncated,
"max_bytes=3 (multiple of 3) on 13-byte body: is_truncated must be true"
);
let result = decode_body_value(&raw, &part, Some(6)).unwrap();
assert!(
result.is_truncated,
"max_bytes=6 (multiple of 3) on 13-byte body: is_truncated must be true"
);
let result = decode_body_value(&raw, &part, Some(9)).unwrap();
assert!(
result.is_truncated,
"max_bytes=9 (multiple of 3) on 13-byte body: is_truncated must be true"
);
let result = decode_body_value(&raw, &part, Some(13)).unwrap();
assert!(
!result.is_truncated,
"max_bytes=13 (exact body length): is_truncated must be false"
);
}
#[test]
fn test_base64_max_bytes_non_multiple_of_4() {
let b64 = b"SGVsbG8sIFdvcmxkIQ==";
let (raw, part) = make_part(b64, TransferEncoding::Base64, Some("utf-8"));
for n in 1usize..=10 {
let result = decode_body_value(&raw, &part, Some(n)).unwrap();
assert!(
!result.is_encoding_problem,
"max_bytes={n}: unexpected encoding problem (base64 pre-truncation not a multiple of 4?)"
);
assert!(
!result.value.is_empty(),
"max_bytes={n}: expected non-empty result"
);
}
}
#[test]
fn test_qp_soft_linebreak_no_false_truncation() {
let mut body = b"a".to_vec();
for _ in 0..20 {
body.extend_from_slice(b"=\r\n");
}
let (raw, part) = make_part(&body, TransferEncoding::QuotedPrintable, Some("utf-8"));
let result = decode_body_value(&raw, &part, Some(2)).unwrap();
assert_eq!(result.value, "a", "decoded value must be 'a'");
assert!(
!result.is_truncated,
"is_truncated must be false: full body decodes to 1 byte, which fits in max_bytes=2"
);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_qp_exact_max_bytes_no_false_truncation() {
let mut body = b"ab".to_vec();
for _ in 0..20 {
body.extend_from_slice(b"=\r\n");
}
let (raw, part) = make_part(&body, TransferEncoding::QuotedPrintable, Some("utf-8"));
let result = decode_body_value(&raw, &part, Some(2)).unwrap();
assert_eq!(result.value, "ab", "decoded value must be 'ab'");
assert!(
!result.is_truncated,
"is_truncated must be false: full body decodes to exactly max_bytes=2 bytes"
);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_qp_real_truncation_not_suppressed() {
let body = b"=41=42=43";
let (raw, part) = make_part(body, TransferEncoding::QuotedPrintable, Some("utf-8"));
let result = decode_body_value(&raw, &part, Some(2)).unwrap();
assert_eq!(result.value.as_bytes(), b"AB", "decoded value must be 'AB'");
assert!(
result.is_truncated,
"is_truncated must be true: full body decodes to 3 bytes, exceeds max_bytes=2"
);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_uuencode_hello_world() {
let uu_body = b"begin 644 test.txt\n-2&5L;&\\L(%=O<FQD(0 \n \nend\n";
let (raw, part) = make_part(uu_body, TransferEncoding::UUEncode, Some("utf-8"));
let result = decode_body_value(&raw, &part, None).unwrap();
assert_eq!(result.value, "Hello, World!");
assert!(!result.is_truncated);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_uuencode_hello() {
let uu_body = b"begin 644 hello.txt\n%2&5L;&\\ \n \nend\n";
let (raw, part) = make_part(uu_body, TransferEncoding::UUEncode, Some("utf-8"));
let result = decode_body_value(&raw, &part, None).unwrap();
assert_eq!(result.value, "Hello");
assert!(!result.is_truncated);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_uuencode_empty() {
let uu_body = b"begin 644 empty.txt\n \nend\n";
let (raw, part) = make_part(uu_body, TransferEncoding::UUEncode, Some("utf-8"));
let result = decode_body_value(&raw, &part, None).unwrap();
assert_eq!(result.value, "");
assert!(!result.is_truncated);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_uuencode_crlf_line_endings() {
let uu_body = b"begin 644 test.txt\r\n-2&5L;&\\L(%=O<FQD(0 \r\n \r\nend\r\n";
let (raw, part) = make_part(uu_body, TransferEncoding::UUEncode, Some("utf-8"));
let result = decode_body_value(&raw, &part, None).unwrap();
assert_eq!(result.value, "Hello, World!");
assert!(!result.is_truncated);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_uuencode_content_before_begin_skipped() {
let uu_body =
b"Some MIME preamble\r\nMore garbage\r\nbegin 644 test.txt\n-2&5L;&\\L(%=O<FQD(0 \n \nend\n";
let (raw, part) = make_part(uu_body, TransferEncoding::UUEncode, Some("utf-8"));
let result = decode_body_value(&raw, &part, None).unwrap();
assert_eq!(result.value, "Hello, World!");
assert!(!result.is_truncated);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_uuencode_max_bytes_truncation() {
let uu_body = b"begin 644 test.txt\n-2&5L;&\\L(%=O<FQD(0 \n \nend\n";
let (raw, part) = make_part(uu_body, TransferEncoding::UUEncode, Some("utf-8"));
let result = decode_body_value(&raw, &part, Some(5)).unwrap();
assert_eq!(result.value, "Hello");
assert!(result.is_truncated);
assert!(!result.is_encoding_problem);
}
#[test]
fn test_uuencode_no_begin_line_is_encoding_problem() {
let uu_body = b"this has no begin line\njust garbage\n";
let (raw, part) = make_part(uu_body, TransferEncoding::UUEncode, None);
let result = decode_body_value(&raw, &part, None).unwrap();
assert!(result.is_encoding_problem);
}
#[test]
fn test_uuencode_null_byte_in_encoded_payload_no_panic() {
let uu_body = b"begin 644 f\n#\x00\x00\x00\x00\n \nend\n";
let (raw, part) = make_part(uu_body, TransferEncoding::UUEncode, None);
let _result = decode_body_value(&raw, &part, None).unwrap();
}
#[test]
fn test_uuencode_backtick_end_marker() {
let uu_body = b"begin 644 hi.txt\n\"2&D \n`\nend\n";
let (raw, part) = make_part(uu_body, TransferEncoding::UUEncode, None);
let result = decode_body_value(&raw, &part, None).unwrap();
assert_eq!(result.value.as_bytes(), b"Hi");
assert!(!result.is_encoding_problem);
}
#[test]
fn test_uuencode_full_45_byte_line() {
let uu_body =
b"begin 644 test.bin\nM $\" P0%!@<(\"0H+# T.#Q 1$A,4%187&!D:&QP=\'A\\@(2(C)\"4F)R@I*BLL\n \nend\n";
let (raw, part) = make_part(uu_body, TransferEncoding::UUEncode, None);
let result = decode_body_value(&raw, &part, None).unwrap();
assert!(!result.is_encoding_problem, "unexpected encoding problem");
let decoded = result.value.as_bytes();
assert_eq!(decoded.len(), 45, "expected 45 decoded bytes");
for (i, &b) in decoded.iter().enumerate() {
assert_eq!(
b, i as u8,
"decoded[{i}] = {b:#04x}, expected {:#04x}",
i as u8
);
}
}
#[test]
fn test_uuencode_two_line_decode() {
let uu_body = b"begin 644 test48.bin\n\
M $\" P0%!@<(\"0H+# T.#Q 1$A,4%187&!D:&QP=\'A\\@(2(C)\"4F)R@I*BLL\n\
#+2XO\n \nend\n";
let (raw, part) = make_part(uu_body, TransferEncoding::UUEncode, None);
let result = decode_body_value(&raw, &part, None).unwrap();
assert!(!result.is_encoding_problem, "unexpected encoding problem");
let decoded = result.value.as_bytes();
assert_eq!(decoded.len(), 48, "expected 48 decoded bytes");
for (i, &b) in decoded.iter().enumerate() {
assert_eq!(
b, i as u8,
"decoded[{i}] = {b:#04x}, expected {:#04x}",
i as u8
);
}
}
}