Files
ElyOffice/crates/ely-mail-engine/tests/mime_tree.rs
T
ZacharyZhang-NYandClaude Opus 5.5 fe90221c11 C12.2: read and write MIME by the tree
The reader walks the MIME tree with its container context: alternative
picks versions, related finds its root by start, attached multiparts
stay whole, digest children are messages. Nesting deeper than 64
levels, counting enclosed messages, is refused, and over-deep parse
results are taken apart iteratively so dropping them cannot overflow
the stack. Composite parts with a Content-ID are cid targets.

The writer keeps attachment parameters and Content-IDs, writes
multipart and message attachments unencoded, and makes its own
Message-ID. SMTP adds BODY=8BITMIME for 8-bit bodies and SMTPUTF8
when any header block at any depth is 8-bit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 17:35:05 -04:00

214 lines
5.9 KiB
Rust

use ely_mail_engine::{Draft, Mailbox, Part, build, read};
/// Lines joined with CRLF, as servers send them.
fn crlf(lines: &[&str]) -> Vec<u8> {
lines.join("\r\n").into_bytes()
}
fn part(name: Option<&str>, kind: &str, id: Option<&str>, bytes: &[u8]) -> Part {
Part {
name: name.map(str::to_owned),
content_type: kind.to_owned(),
params: Vec::new(),
content_id: id.map(str::to_owned),
bytes: bytes.to_vec(),
}
}
#[test]
fn text_attachments_keep_their_own_bytes() {
let mail = read(&crlf(&[
"Content-Type: multipart/mixed; boundary=b",
"",
"--b",
"Content-Type: text/plain",
"",
"body",
"--b",
"Content-Type: text/plain; charset=iso-8859-1",
"Content-Transfer-Encoding: quoted-printable",
"Content-Disposition: attachment; filename=qp.txt",
"",
"Caf=E9",
"--b",
"Content-Type: text/plain; charset=iso-8859-1",
"Content-Disposition: attachment; filename=raw.txt",
"",
"Caf\u{e9}",
"--b",
"Content-Type: text/plain; charset=iso-8859-1",
"Content-Transfer-Encoding: base64",
"Content-Disposition: attachment; filename=b64.txt",
"",
"Q2Fm6Q==",
"--b--",
"",
]))
.unwrap();
assert_eq!(mail.text.as_deref(), Some("body"));
let bytes: Vec<&[u8]> = mail
.attachments
.iter()
.map(|p| p.bytes.as_slice())
.collect();
assert_eq!(bytes, [b"Caf\xe9", "Caf\u{e9}".as_bytes(), b"Caf\xe9"]);
}
#[test]
fn a_built_latin1_text_attachment_reads_back_as_built() {
let latin1 = part(Some("cafe.txt"), "text/plain", None, b"Caf\xe9\r\n");
let draft = Draft {
from: Mailbox {
name: None,
email: "a@example.com".into(),
},
to: Vec::new(),
cc: Vec::new(),
subject: "s".into(),
text: "t".into(),
html: None,
attachments: vec![latin1.clone()],
in_reply_to: None,
references: Vec::new(),
date: 0,
};
let back = read(&build(&draft).unwrap().bytes).unwrap();
assert_eq!(back.attachments, [latin1]);
}
#[test]
fn a_related_root_named_by_start_can_come_after_its_image() {
let mail = read(&crlf(&[
"Content-Type: multipart/related; boundary=r; type=\"text/html\"; start=\"<root@example.com>\"",
"",
"--r",
"Content-Type: image/png",
"Content-ID: <img@example.com>",
"Content-Transfer-Encoding: base64",
"",
"iVBORw==",
"--r",
"Content-Type: text/html",
"Content-ID: <root@example.com>",
"",
"<img src=\"cid:img@example.com\">",
"--r--",
"",
]))
.unwrap();
assert_eq!(
mail.html.as_deref(),
Some("<img src=\"cid:img@example.com\">")
);
assert_eq!(
mail.inline,
[part(None, "image/png", Some("img@example.com"), b"\x89PNG")]
);
assert!(mail.attachments.is_empty());
}
#[test]
fn a_related_image_marked_attachment_is_still_a_cid_target() {
let mail = read(&crlf(&[
"Content-Type: multipart/related; boundary=r",
"",
"--r",
"Content-Type: text/html",
"",
"<img src=\"cid:img@example.com\">",
"--r",
"Content-Type: image/png",
"Content-ID: <img@example.com>",
"Content-Disposition: attachment; filename=img.png",
"Content-Transfer-Encoding: base64",
"",
"iVBORw==",
"--r--",
"",
]))
.unwrap();
assert_eq!(
mail.inline,
[part(
Some("img.png"),
"image/png",
Some("img@example.com"),
b"\x89PNG"
)]
);
assert!(mail.attachments.is_empty());
}
#[test]
fn an_attached_multipart_is_one_attachment_and_no_body() {
let entity = [
"Content-Type: multipart/alternative; boundary=in",
"Content-Disposition: attachment; filename=alternatives.mime",
"",
"--in",
"Content-Type: text/plain",
"",
"private attachment",
"--in--",
];
let mut lines = vec![
"Content-Type: multipart/mixed; boundary=out",
"",
"--out",
"Content-Type: text/plain",
"",
"body",
"--out",
];
lines.extend(entity);
lines.extend(["--out--", ""]);
let mail = read(&crlf(&lines)).unwrap();
assert_eq!(mail.text.as_deref(), Some("body"));
assert_eq!(mail.attachments.len(), 1);
let attached = &mail.attachments[0];
assert_eq!(attached.name.as_deref(), Some("alternatives.mime"));
assert_eq!(attached.content_type, "multipart/alternative");
assert_eq!(attached.bytes, crlf(&entity));
}
#[test]
fn alternatives_of_one_type_are_chosen_not_joined() {
let mail = read(&crlf(&[
"Content-Type: multipart/alternative; boundary=a",
"",
"--a",
"Content-Type: text/plain; charset=us-ascii",
"",
"Cafe",
"--a",
"Content-Type: text/plain; charset=utf-8",
"",
"Café",
"--a",
"Content-Type: text/calendar",
"",
"BEGIN:VCALENDAR",
"--a--",
"",
]))
.unwrap();
assert_eq!(mail.text.as_deref(), Some("Café"));
assert_eq!(
mail.attachments,
[part(None, "text/calendar", None, b"BEGIN:VCALENDAR")]
);
}
#[test]
fn a_digest_child_without_a_type_is_a_message() {
let enclosed = "From: x@example.com\r\nSubject: inner\r\n\r\nhello";
let mut lines = vec!["Content-Type: multipart/digest; boundary=d", "", "--d", ""];
lines.push(enclosed);
lines.extend(["--d--", ""]);
let mail = read(&crlf(&lines)).unwrap();
assert_eq!(mail.attachments.len(), 1);
assert_eq!(mail.attachments[0].content_type, "message/rfc822");
let inner = read(&mail.attachments[0].bytes).unwrap();
assert_eq!(inner.subject.as_deref(), Some("inner"));
}