use mail_parser::{
Address, AddressList, AddressRun, Charset, ContentType, DateTime, DecodeProblems, Encoding,
Greeting, Group, Header, HeaderForm, HeaderIter, HeaderKey, HeaderName, HeaderValue, Headers,
Host, Mailbox, Message, MessageBuffers, MessageParser, MessagePart, MessageRef, NamedHeaders,
ParsedValue, PartFlags, PartKind, PartRole, Protocol, Received, Source, TextList, TlsVersion,
};
use std::fmt::Debug;
const MULTIPART: &[u8] = b"From: Ann <ann@example.com>\r\n\
To: team: bob@example.com;\r\n\
Subject: Report\r\n\
Content-Type: multipart/mixed; boundary=\"b\"\r\n\
\r\n\
--b\r\n\
Content-Type: text/plain\r\n\
\r\n\
Hello\r\n\
--b\r\n\
Content-Type: application/pdf; name=report.pdf\r\n\
\r\n\
%PDF\r\n\
--b--\r\n";
fn send_sync<T: Send + Sync>() {}
fn copy<T: Copy + Debug>() {}
fn clone<T: Clone + Debug>() {}
fn eq<T: Eq + Debug>() {}
#[test]
fn public_types_are_send_and_sync() {
send_sync::<Message<'static>>();
send_sync::<MessageParser>();
send_sync::<MessageBuffers>();
send_sync::<MessageRef<'static>>();
send_sync::<MessagePart<'static>>();
send_sync::<Headers<'static>>();
send_sync::<Header<'static>>();
send_sync::<NamedHeaders<'static, 'static>>();
send_sync::<HeaderIter<'static>>();
send_sync::<HeaderKey<'static>>();
send_sync::<HeaderValue<'static>>();
send_sync::<HeaderName<'static>>();
send_sync::<AddressList<'static>>();
send_sync::<Address<'static>>();
send_sync::<Mailbox<'static>>();
send_sync::<Group<'static>>();
send_sync::<AddressRun<'static>>();
send_sync::<TextList<'static>>();
send_sync::<ContentType<'static>>();
send_sync::<Received<'static>>();
send_sync::<Host<'static>>();
send_sync::<PartKind<'static>>();
send_sync::<ParsedValue<'static>>();
}
#[test]
fn views_and_small_values_are_copy() {
copy::<MessageRef<'static>>();
copy::<MessagePart<'static>>();
copy::<Headers<'static>>();
copy::<Header<'static>>();
copy::<HeaderValue<'static>>();
copy::<AddressList<'static>>();
copy::<Address<'static>>();
copy::<Mailbox<'static>>();
copy::<Group<'static>>();
copy::<AddressRun<'static>>();
copy::<TextList<'static>>();
copy::<ContentType<'static>>();
copy::<Received<'static>>();
copy::<Host<'static>>();
copy::<PartKind<'static>>();
copy::<PartRole>();
copy::<PartFlags>();
copy::<DecodeProblems>();
copy::<Source>();
copy::<Encoding>();
copy::<HeaderForm>();
copy::<Charset>();
copy::<DateTime>();
copy::<Protocol>();
copy::<Greeting>();
copy::<TlsVersion>();
clone::<Message<'static>>();
clone::<MessageParser>();
clone::<MessageBuffers>();
clone::<ParsedValue<'static>>();
clone::<NamedHeaders<'static, 'static>>();
clone::<HeaderName<'static>>();
}
#[test]
fn values_compare_by_content() {
eq::<HeaderValue<'static>>();
eq::<AddressList<'static>>();
eq::<TextList<'static>>();
eq::<ContentType<'static>>();
eq::<Received<'static>>();
eq::<MessageParser>();
eq::<ParsedValue<'static>>();
let parser = MessageParser::new();
let first = parser.parse(MULTIPART).expect("message");
let copy = MULTIPART.to_vec();
let second = parser.parse(©).expect("message");
for name in [
HeaderName::From,
HeaderName::To,
HeaderName::Subject,
HeaderName::ContentType,
] {
let value = first.headers().value(name.clone());
assert!(value.is_some_and(|value| !value.is_empty()));
assert_eq!(value, second.headers().value(name));
}
assert_ne!(
first.headers().value(HeaderName::From),
first.headers().value(HeaderName::To)
);
let received = b" from a (b [10.0.0.1]) by c with ESMTPS id 1; Sat, 20 Nov 2021 14:22:01 -0800";
let parsed = HeaderForm::Received.parse(received);
let again = HeaderForm::Received.parse(received);
assert_eq!(parsed, again);
assert_eq!(parsed.value(), again.value());
assert_ne!(parsed, HeaderForm::Raw.parse(received));
assert_eq!(
parsed
.value()
.as_received()
.and_then(|received| received.with()),
Some(Protocol::ESMTPS)
);
}
fn shorten_message<'short, 'long: 'short>(message: Message<'long>) -> Message<'short> {
message
}
fn shorten_part<'short, 'long: 'short>(part: MessagePart<'long>) -> MessagePart<'short> {
part
}
fn shorten_value<'short, 'long: 'short>(value: HeaderValue<'long>) -> HeaderValue<'short> {
value
}
fn shorten_name<'short, 'long: 'short>(name: HeaderName<'long>) -> HeaderName<'short> {
name
}
#[test]
fn message_is_covariant() {
let parser = MessageParser::new();
let owned: Message<'static> = parser.parse_owned(MULTIPART.to_vec()).expect("message");
let local = b"Subject: local\r\n\r\nbody".to_vec();
let mut message = parser.parse(&local).expect("message");
assert_eq!(message.subject(), Some("local"));
message = owned;
assert_eq!(message.subject(), Some("Report"));
let message = shorten_message(message);
let part = shorten_part(message.root_part());
assert_eq!(part.children().len(), 2);
let value = shorten_value(
part.headers()
.value(HeaderName::Subject)
.unwrap_or_default(),
);
assert_eq!(value.as_text(), Some("Report"));
assert!(shorten_name(HeaderName::Other("X-Local".into())).is_other());
}
#[test]
fn owned_messages_move_across_threads() {
let message = MessageParser::new()
.parse(MULTIPART)
.expect("message")
.into_owned();
let subject = std::thread::spawn(move || message.subject().map(str::to_string))
.join()
.expect("thread");
assert_eq!(subject.as_deref(), Some("Report"));
}
#[test]
fn headers_only_messages() {
let parser = MessageParser::new();
let message = parser.parse_headers(MULTIPART).expect("message");
let root = message.root_part();
assert_eq!(message.parts().len(), 1);
assert!(matches!(root.kind(), PartKind::Multipart));
assert_eq!(root.children().len(), 0);
assert_eq!(root.role(), PartRole::Container);
assert_eq!(root.offset_end() as usize, MULTIPART.len());
assert_eq!(message.text_body().len(), 0);
assert_eq!(message.html_body().len(), 0);
assert_eq!(message.attachments().len(), 0);
assert_eq!(message.other_parts().count(), 0);
assert!(!message.has_attachments());
assert_eq!(message.subject(), Some("Report"));
let single = b"Subject: plain\r\n\r\nHello\r\n";
let message = parser.parse_headers(single).expect("message");
let root = message.root_part();
assert!(matches!(root.kind(), PartKind::Text));
assert_eq!(root.role(), PartRole::Dropped);
assert_eq!(
message
.other_parts()
.map(|part| part.id())
.collect::<Vec<_>>(),
[0]
);
assert!(!message.has_attachments());
assert_eq!(root.text().as_deref(), Some("Hello\r\n"));
}
#[test]
fn buffers_stay_usable_after_inputs_that_do_not_parse() {
let parser = MessageParser::new();
let mut buffers = MessageBuffers::new();
let mut subjects = Vec::new();
for raw in [
MULTIPART,
b"".as_slice(),
b"Subject: second\r\n\r\nbody".as_slice(),
] {
match parser.parse_with(raw, &mut buffers) {
Some(message) => {
subjects.push(message.subject().map(str::to_string));
buffers = message.into_buffers();
}
None => subjects.push(None),
}
}
assert_eq!(
subjects,
[Some("Report".to_string()), None, Some("second".to_string())]
);
}
#[test]
fn debug_output_shows_content() {
let message = MessageParser::new().parse(MULTIPART).expect("message");
let headers = format!("{:?}", message.headers());
assert!(headers.contains("name: \"Subject\", value: Text(\"Report\")"));
assert!(headers.len() < 2_000);
assert_eq!(
format!("{:?}", message.from().expect("from")),
"[Mailbox(Mailbox { name: Some(\"Ann\"), address: Some(\"ann@example.com\") })]"
);
assert_eq!(
format!("{:?}", message.to().expect("to")),
"[Group(Group { name: Some(\"team\"), members: [Mailbox { name: None, address: Some(\"bob@example.com\") }] })]"
);
assert_eq!(
format!("{:?}", message.content_type().expect("content type")),
"ContentType { type: \"multipart\", subtype: Some(\"mixed\"), attributes: {\"boundary\": \"b\"} }"
);
let parsed = HeaderForm::MessageIds.parse(b" <a@b> <c@d>");
assert_eq!(
format!("{parsed:?}"),
"ParsedValue { value: TextList([\"a@b\", \"c@d\"]) }"
);
assert_eq!(
format!("{:?}", MessageBuffers::new()),
"MessageBuffers { .. }"
);
}
#[test]
fn headers_iterate_in_for_loops() {
let message = MessageParser::new().parse(MULTIPART).expect("message");
let headers = message.headers();
let mut names = Vec::new();
for header in headers {
names.push(header.raw_name());
}
assert_eq!(names, ["From", "To", "Subject", "Content-Type"]);
let by_ref: Vec<_> = (&headers)
.into_iter()
.map(|header| header.raw_name())
.collect();
assert_eq!(by_ref, names);
let iter: HeaderIter<'_> = headers.iter();
assert_eq!(iter.len(), 4);
assert_eq!(
headers.iter().rev().map(|header| header.raw_name()).next(),
Some("Content-Type")
);
assert_eq!(
format!("{:?}", headers.iter()),
"HeaderIter { remaining: 4, .. }"
);
}
#[test]
fn all_received_lists_every_trace_field() {
let raw = b"Received: from a.example by b.example; Tue, 1 Jul 2003 10:52:37 +0200\r\n\
Received: from c.example by d.example; Tue, 1 Jul 2003 10:52:36 +0200\r\n\
Subject: trace\r\n\r\nbody\r\n";
let message = MessageParser::new().parse(&raw[..]).expect("message");
let hosts: Vec<_> = message
.all_received()
.map(|received| received.from().map(|host| host.to_string()))
.collect();
assert_eq!(
hosts,
[Some("a.example".to_string()), Some("c.example".to_string())]
);
}
#[test]
fn header_keys_find_what_names_find() {
let message = MessageParser::new().parse(MULTIPART).expect("message");
let headers = message.headers();
for name in [
HeaderName::Subject,
HeaderName::from("subject"),
HeaderName::Other("SUBJECT".into()),
HeaderName::from("x-missing"),
HeaderName::from("Content-Type"),
] {
let key: HeaderKey<'_> = name.key();
let copy = key;
assert_eq!(copy, key);
assert_eq!(
headers
.all_key(key)
.map(|h| h.raw_value())
.collect::<Vec<_>>(),
headers
.all(name.clone())
.map(|h| h.raw_value())
.collect::<Vec<_>>()
);
assert_eq!(
headers.get_key(key).map(|h| h.raw_value()),
headers.get(name.clone()).map(|h| h.raw_value())
);
}
let content_type = HeaderName::from("content-type");
let key = content_type.key();
for part in message.parts() {
assert_eq!(
part.headers().get_key(key).is_some(),
part.headers().get("Content-Type").is_some()
);
}
}
#[test]
fn known_transfer_encoding_matches_the_decode_problem() {
for (field, known) in [
("", true),
("Content-Transfer-Encoding: 8bit\r\n", true),
("Content-Transfer-Encoding: BASE64 (comment)\r\n", true),
("Content-Transfer-Encoding: x-uuencode\r\n", false),
] {
let raw = format!("Subject: s\r\n{field}\r\nbody\r\n");
let message = MessageParser::new()
.parse(raw.as_bytes())
.expect("a message");
let part = message.root_part();
assert_eq!(part.has_known_transfer_encoding(), known, "{field:?}");
assert_eq!(
part.decoded_checked()
.1
.contains(DecodeProblems::UNKNOWN_TRANSFER_ENCODING),
!known,
"{field:?}"
);
}
}