use mime_tree::{decode_body_value, parse, ParseError};
#[test]
fn test_simple_plain_text() {
let raw = b"From: alice@example.com\r\n\
To: bob@example.com\r\n\
Subject: Hello\r\n\
MIME-Version: 1.0\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
\r\n\
Hello, World!\r\n";
let msg = parse(raw).expect("parse of valid RFC 5322 message must succeed");
assert_eq!(msg.part_index.content_type, "text/plain");
assert_eq!(msg.part_index.charset, Some("utf-8".to_owned()));
assert_eq!(msg.text_body, vec!["1".to_owned()]);
assert_eq!(msg.html_body, vec!["1".to_owned()]);
assert!(msg.attachments.is_empty(), "no attachments expected");
assert!(
msg.warnings.is_empty(),
"unexpected warnings: {:?}",
msg.warnings
);
let has_from = msg.headers.iter().any(|h| h.name == "From");
assert!(has_from, "From header must be present in parsed headers");
}
#[test]
fn test_multipart_alternative() {
let raw = concat!(
"From: alice@example.com\r\n",
"MIME-Version: 1.0\r\n",
"Content-Type: multipart/alternative; boundary=\"boundary\"\r\n",
"\r\n",
"--boundary\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"\r\n",
"Plain text body\r\n",
"--boundary\r\n",
"Content-Type: text/html; charset=utf-8\r\n",
"\r\n",
"<html><body>HTML body</body></html>\r\n",
"--boundary--\r\n"
)
.as_bytes();
let msg = parse(raw).expect("parse must succeed");
assert_eq!(
msg.text_body,
vec!["1".to_owned()],
"text/plain child must be in text_body"
);
assert_eq!(
msg.html_body,
vec!["2".to_owned()],
"text/html child must be in html_body"
);
assert!(msg.attachments.is_empty(), "no attachments expected");
assert_eq!(
msg.part_index.children.len(),
2,
"root must have 2 children"
);
assert_eq!(msg.part_index.children[0].part_id, "1");
assert_eq!(msg.part_index.children[0].content_type, "text/plain");
assert_eq!(msg.part_index.children[1].part_id, "2");
assert_eq!(msg.part_index.children[1].content_type, "text/html");
}
#[test]
fn test_multipart_mixed_with_attachment() {
let raw = concat!(
"From: alice@example.com\r\n",
"MIME-Version: 1.0\r\n",
"Content-Type: multipart/mixed; boundary=\"b\"\r\n",
"\r\n",
"--b\r\n",
"Content-Type: text/plain\r\n",
"\r\n",
"Main body text\r\n",
"--b\r\n",
"Content-Type: application/pdf\r\n",
"Content-Disposition: attachment; filename=\"doc.pdf\"\r\n",
"Content-Transfer-Encoding: base64\r\n",
"\r\n",
"SGVsbG8=\r\n",
"--b--\r\n"
)
.as_bytes();
let msg = parse(raw).expect("parse must succeed");
assert_eq!(msg.text_body, vec!["1".to_owned()]);
assert_eq!(msg.html_body, vec!["1".to_owned()]);
assert_eq!(msg.attachments, vec!["2".to_owned()]);
let pdf_part = &msg.part_index.children[1];
assert_eq!(
pdf_part.disposition,
Some("attachment".to_owned()),
"disposition must be 'attachment'"
);
assert_eq!(
pdf_part.filename,
Some("doc.pdf".to_owned()),
"filename must be 'doc.pdf'"
);
}
#[test]
fn test_byte_range_validity() {
let raw = b"From: test@example.com\r\n\r\nBody content\r\n";
let msg = parse(raw).expect("parse must succeed");
let (body_off, body_len) = msg.part_index.body_range;
let (hdr_off, hdr_len) = msg.part_index.header_range;
assert!(
(body_off as usize).saturating_add(body_len as usize) <= raw.len(),
"body_range ({body_off}, {body_len}) exceeds raw.len()={}",
raw.len()
);
assert!(
(hdr_off as usize).saturating_add(hdr_len as usize) <= raw.len(),
"header_range ({hdr_off}, {hdr_len}) exceeds raw.len()={}",
raw.len()
);
let body_slice = &raw[body_off as usize..(body_off + body_len) as usize];
assert!(
std::str::from_utf8(body_slice)
.expect("body must be valid UTF-8")
.contains("Body content"),
"body slice does not contain expected text; got: {:?}",
std::str::from_utf8(body_slice)
);
}
#[test]
fn test_empty_input_error() {
let result = parse(b"");
assert!(
matches!(result, Err(ParseError::EmptyInput)),
"empty input must return EmptyInput, got: {:?}",
result
);
}
#[test]
fn test_decode_body_value_base64() {
let raw = b"From: test@example.com\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
Content-Transfer-Encoding: base64\r\n\
\r\n\
SGVsbG8sIFdvcmxkIQ==\r\n";
let msg = parse(raw).expect("parse must succeed");
let decoded = decode_body_value(raw, &msg.part_index, None)
.expect("decode_body_value must succeed for valid base64");
assert_eq!(decoded.value, "Hello, World!");
assert!(!decoded.is_truncated, "no truncation limit was applied");
assert!(
!decoded.is_encoding_problem,
"valid base64 + utf-8 must not report encoding problems"
);
}
#[test]
fn test_smime_parts_are_opaque_leaves() {
let raw = b"From: test@example.com\r\n\
Content-Type: application/pkcs7-mime; smime-type=enveloped-data\r\n\
Content-Transfer-Encoding: base64\r\n\
\r\n\
SGVsbG8=\r\n";
let msg = parse(raw).expect("parse must succeed");
assert_eq!(
msg.part_index.content_type, "application/pkcs7-mime",
"content-type must be preserved as registered IANA type"
);
assert!(
!msg.text_body.contains(&"1".to_owned()),
"S/MIME part must not appear in text_body"
);
assert!(
!msg.html_body.contains(&"1".to_owned()),
"S/MIME part must not appear in html_body"
);
}
#[test]
fn test_no_content_type_defaults_to_text_plain() {
let raw = b"From: alice@example.com\r\n\
MIME-Version: 1.0\r\n\
\r\n\
Hello, this is a bare body with no Content-Type header.\r\n";
let msg = parse(raw).expect("parse must succeed");
assert_eq!(
msg.part_index.content_type, "text/plain",
"missing Content-Type must default to text/plain per RFC 2045 §5.2"
);
assert_eq!(
msg.part_index.charset,
Some("us-ascii".to_owned()),
"missing Content-Type must default to charset=us-ascii per RFC 2045 §5.2"
);
assert!(
msg.text_body.contains(&msg.part_index.part_id),
"bare-body part must appear in text_body; text_body={:?}",
msg.text_body
);
}