melib/email/parser: Add rfc5322 compliant parser for MessageID

master
Manos Pitsidianakis 2020-09-03 14:44:03 +03:00
parent 5d107db8b8
commit d14f26569e
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
6 changed files with 299 additions and 268 deletions

View File

@ -1253,7 +1253,7 @@ pub fn envelope(input: &[u8]) -> IResult<&[u8], Envelope> {
}
if let Some(in_reply_to) = in_reply_to {
env.set_in_reply_to(&in_reply_to);
env.push_references(&in_reply_to);
env.push_references(env.in_reply_to().unwrap().clone());
}
if let Some(message_id) = message_id {

View File

@ -246,10 +246,10 @@ impl std::convert::From<EmailObject> for crate::Envelope {
}
if let Some(ref in_reply_to) = t.in_reply_to {
env.set_in_reply_to(in_reply_to[0].as_bytes());
env.push_references(in_reply_to[0].as_bytes());
env.push_references(env.in_reply_to().unwrap().clone());
}
if let Some(v) = t.headers.get("References") {
let parse_result = crate::email::parser::address::references(v.as_bytes());
let parse_result = crate::email::parser::address::msg_id_list(v.as_bytes());
if let Ok((_, v)) = parse_result {
for v in v {
env.push_references(v);

View File

@ -144,7 +144,7 @@ pub fn over_article(input: &str) -> IResult<&str, (UID, Envelope)> {
if let Some(references) = references {
{
if let Ok((_, r)) =
crate::email::parser::address::references(references.as_bytes())
crate::email::parser::address::msg_id_list(references.as_bytes())
{
for v in r {
env.push_references(v);

View File

@ -227,8 +227,6 @@ impl Envelope {
return Err(MeliError::new(error_msg));
}
};
let mut in_reply_to = None;
for (name, value) in headers {
let name: HeaderName = name.try_into()?;
if name == "to" {
@ -260,7 +258,7 @@ impl Envelope {
self.set_message_id(value);
} else if name == "references" {
{
let parse_result = parser::address::references(value);
let parse_result = parser::address::msg_id_list(value);
if let Ok((_, value)) = parse_result {
for v in value {
self.push_references(v);
@ -270,7 +268,6 @@ impl Envelope {
self.set_references(value);
} else if name == "in-reply-to" {
self.set_in_reply_to(value);
in_reply_to = Some(value);
} else if name == "date" {
let parse_result = parser::encodings::phrase(value, false);
if let Ok((_, value)) = parse_result {
@ -318,7 +315,7 @@ impl Envelope {
*
* if self.message_id.is_none() ...
*/
if let Some(ref mut x) = in_reply_to {
if let Some(x) = self.in_reply_to.clone() {
self.push_references(x);
}
if let Ok(d) = parser::generic::date(&self.date.as_bytes()) {
@ -520,15 +517,16 @@ impl Envelope {
self.to = new_val;
}
pub fn set_in_reply_to(&mut self, new_val: &[u8]) {
// FIXME msg_id_list
let new_val = new_val.trim();
let slice = match parser::address::message_id(new_val) {
let val = match parser::address::msg_id(new_val) {
Ok(v) => v.1,
Err(_) => {
self.in_reply_to = None;
self.in_reply_to = Some(MessageID::new(new_val, new_val));
return;
}
};
self.in_reply_to = Some(MessageID::new(new_val, slice));
self.in_reply_to = Some(val);
}
pub fn set_subject(&mut self, new_val: Vec<u8>) {
let mut new_val = String::from_utf8(new_val)
@ -546,25 +544,16 @@ impl Envelope {
}
pub fn set_message_id(&mut self, new_val: &[u8]) {
let new_val = new_val.trim();
match parser::address::message_id(new_val) {
Ok((_, slice)) => {
self.message_id = MessageID::new(new_val, slice);
match parser::address::msg_id(new_val) {
Ok((_, val)) => {
self.message_id = val;
}
Err(_) => {
self.message_id = MessageID::new(new_val, new_val);
}
}
}
pub fn push_references(&mut self, new_val: &[u8]) {
let new_val = new_val.trim();
let slice = match parser::address::message_id(new_val) {
Ok(v) => v.1,
Err(e) => {
debug!(e);
return;
}
};
let new_ref = MessageID::new(new_val, slice);
pub fn push_references(&mut self, new_ref: MessageID) {
match self.references {
Some(ref mut s) => {
if s.refs.contains(&new_ref) {

View File

@ -318,15 +318,15 @@ impl StrBuild for MessageID {
#[test]
fn test_strbuilder() {
let m_id = b"<20170825132332.6734-1@el13635@mail.ntua.gr>";
let (_, slice) = parser::address::message_id(m_id).unwrap();
let m_id = b"<20170825132332.6734-1@mail.ntua.gr>";
let (_, val) = parser::address::msg_id(m_id).unwrap();
assert_eq!(
MessageID::new(m_id, slice),
val,
MessageID(
m_id.to_vec(),
StrBuilder {
offset: 1,
length: 43,
length: 35,
}
)
);

View File

@ -27,7 +27,7 @@ use nom::{
combinator::peek,
combinator::{map, opt},
error::{context, ErrorKind},
multi::{many0, many1, separated_list, separated_nonempty_list},
multi::{many0, many1, separated_nonempty_list},
number::complete::le_u8,
sequence::{delimited, pair, preceded, separated_pair, terminated},
};
@ -618,9 +618,225 @@ pub mod generic {
let s = "this is\n\ta folded name";
assert_eq!(
&unstructured(s.as_bytes()).unwrap(),
"this is a\tfolded name",
"this is\ta folded name",
);
}
///`atom = [CFWS] 1*atext [CFWS]`
pub fn atom(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (input, opt_space) = opt(cfws)(input)?;
let mut i = 0;
while i < input.len() {
//&& !input[i].is_ascii_whitespace() {
match input[i] {
b'(' | b')' | b'<' | b'>' | b'[' | b']' | b':' | b';' | b'@' | b'\\' | b','
| b'.' | b'\r' | b'\n' | b'"' => break,
_ => {}
}
i += 1;
}
if i == 0 {
return Err(nom::Err::Error(
(input, "atom(): starts with whitespace or empty").into(),
));
}
while i + 1 > 0 {
if input[i - 1] == b' ' || input[i - 1] == b'\t' {
i -= 1;
} else {
break;
}
}
let (rest, opt_space2) = opt(cfws)(&input[i..])?;
let ret = if opt_space.is_some() || opt_space2.is_some() {
let mut ret = Vec::with_capacity(i + 2);
if let Some(opt_space) = opt_space {
ret.extend_from_slice(&opt_space);
}
ret.extend_from_slice(&input[..i]);
if let Some(opt_space) = opt_space2 {
ret.extend_from_slice(&opt_space);
}
Cow::Owned(ret)
} else {
Cow::Borrowed(&input[..i])
};
Ok((rest, ret))
}
///`quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS]`
pub fn quoted_string(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (input, opt_space) = opt(cfws)(input)?;
if !input.starts_with(b"\"") {
return Err(nom::Err::Error(
(input, "quoted_string(): doesn't start with DQUOTE").into(),
));
}
let input = &input[1..];
let mut i = 0;
while i < input.len() && input[i] != b'"' {
if opt_space.is_some() || (input[i..].starts_with(b"\\") && i + 1 < input.len()) {
let mut ret = if let Some(opt_space) = opt_space {
let mut r = Vec::with_capacity(2 * i);
r.extend_from_slice(&opt_space);
r
} else {
Vec::with_capacity(2 * i)
};
ret.extend_from_slice(&input[..i]);
i += 1;
ret.push(input[i]);
i += 1;
while i < input.len() && input[i] != b'"' {
if input[i..].starts_with(b"\\") && i + 1 < input.len() {
i += 1;
}
ret.push(input[i]);
i += 1;
}
if i < input.len() {
// skip DQUOTE
i += 1;
} else {
return Err(nom::Err::Error(
(input, "quoted_string(): unclosed DQUOTE").into(),
));
}
let (rest, opt_sp) = opt(cfws)(&input[i..])?;
if let Some(opt_sp) = opt_sp {
ret.extend_from_slice(&opt_sp);
}
let ret = Cow::Owned(ret);
return Ok((rest, ret));
}
i += 1;
}
let ret = Cow::Borrowed(&input[..i]);
if i < input.len() {
// skip DQUOTE
i += 1;
} else {
return Err(nom::Err::Error(
(input, "quoted_string(): unclosed DQUOTE").into(),
));
}
let (rest, opt_sp) = opt(cfws)(&input[i..])?;
if let Some(opt_sp) = opt_sp {
let mut ret = ret.to_vec();
ret.extend_from_slice(&opt_sp);
Ok((rest, Cow::Owned(ret)))
} else {
Ok((rest, ret))
}
}
///`word = atom / quoted-string`
pub fn word(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
alt((quoted_string, atom))(input)
}
///`phrase = 1*word / obs-phrase`
pub fn phrase2(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
let (rest, words) = many1(word)(input)?;
let len = words.iter().map(|v| v.len()).sum::<usize>();
let mut ret = words
.into_iter()
.fold(Vec::with_capacity(len), |mut acc, el| {
acc.extend_from_slice(&el);
acc
});
let right_wsp_padding = ret.len() - ret.rtrim().len();
for _ in 0..right_wsp_padding {
ret.pop();
}
Ok((rest, ret))
}
#[test]
fn test_phrase() {
let s = b"\"Jeffrey \\\"fejj\\\" Stedfast\""; // <fejj@helixcode.com>"
assert_eq!(to_str!(&phrase2(s).unwrap().1), "Jeffrey \"fejj\" Stedfast");
}
///dot-atom-text = 1*atext *("." 1*atext)
pub fn dot_atom_text(mut input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let mut ret = vec![];
let mut at_least_one = false;
while let Ok((_input, atext_r)) = atext(input) {
at_least_one = true;
ret.push(atext_r);
input = _input;
}
if !at_least_one {
return Err(nom::Err::Error(
(input, "dot_atom(): starts with at least one atext").into(),
));
}
loop {
if !input.starts_with(b".") {
break;
}
ret.push(b'.');
input = &input[1..];
let mut at_least_one = false;
while let Ok((_input, atext_r)) = atext(input) {
at_least_one = true;
ret.push(atext_r);
input = _input;
}
if !at_least_one {
return Err(nom::Err::Error(
(input, "dot_atom(): DOT followed with at least one atext").into(),
));
}
}
Ok((input, ret.into()))
}
///`atext = ALPHA / DIGIT / ; Printable US-ASCII "!" / "#" / ; characters not including "$" / "%" / ; specials. Used for atoms. "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"`
pub fn atext(input: &[u8]) -> IResult<&[u8], u8> {
if input.is_empty() {
return Err(nom::Err::Error((input, "atext(): empty input").into()));
}
if input[0].is_ascii_alphanumeric()
|| [
b'!', b'#', b'$', b'%', b'&', b'\'', b'*', b'+', b'-', b'/', b'=', b'?', b'^',
b'_', b'`', b'{', b'|', b'}', b'~',
]
.contains(&input[0])
{
Ok((&input[1..], input[0]))
} else {
return Err(nom::Err::Error((input, "atext(): invalid byte").into()));
}
}
///dot-atom = [CFWS] dot-atom-text [CFWS]
pub fn dot_atom(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (input, _) = opt(cfws)(input)?;
let (input, ret) = dot_atom_text(input)?;
let (input, _) = opt(cfws)(input)?;
Ok((input, ret.into()))
}
///```text
///dtext = %d33-90 / ; Printable US-ASCII
/// %d94-126 / ; characters not including
/// obs-dtext ; "[", "]", or "\"
///```
pub fn dtext(input: &[u8]) -> IResult<&[u8], u8> {
if input.is_empty() {
return Err(nom::Err::Error((input, "dtext(): empty input").into()));
}
if (input[0] >= 33 && input[0] <= 90) || (input[0] > 94 && input[0] < 126) {
Ok((&input[1..], input[0]))
} else {
Err(nom::Err::Error((input, "dtext(): out of range").into()))
}
}
}
pub mod headers {
@ -1453,7 +1669,9 @@ pub mod encodings {
pub mod address {
use super::*;
use crate::email::address::*;
use crate::email::parser::generic::cfws;
use crate::email::parser::generic::{
atom, cfws, dot_atom, dot_atom_text, dtext, phrase2, quoted_string,
};
pub fn display_addr(input: &[u8]) -> IResult<&[u8], Address> {
if input.is_empty() || input.len() < 3 {
Err(nom::Err::Error((input, "display_addr(): EOF").into()))
@ -1565,202 +1783,9 @@ pub mod address {
let (input, _) = opt(cfws)(input)?;
Ok((input, addr_spec))
}
///`atom = [CFWS] 1*atext [CFWS]`
fn atom(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (input, opt_space) = opt(cfws)(input)?;
let mut i = 0;
while i < input.len() {
//&& !input[i].is_ascii_whitespace() {
match input[i] {
b'(' | b')' | b'<' | b'>' | b'[' | b']' | b':' | b';' | b'@' | b'\\' | b','
| b'.' | b'\r' | b'\n' | b'"' => break,
_ => {}
}
i += 1;
}
if i == 0 {
return Err(nom::Err::Error(
(input, "atom(): starts with whitespace or empty").into(),
));
}
while i + 1 > 0 {
if input[i - 1] == b' ' || input[i - 1] == b'\t' {
i -= 1;
} else {
break;
}
}
let (rest, opt_space2) = opt(cfws)(&input[i..])?;
let ret = if opt_space.is_some() || opt_space2.is_some() {
let mut ret = Vec::with_capacity(i + 2);
if let Some(opt_space) = opt_space {
ret.extend_from_slice(&opt_space);
}
ret.extend_from_slice(&input[..i]);
if let Some(opt_space) = opt_space2 {
ret.extend_from_slice(&opt_space);
}
Cow::Owned(ret)
} else {
Cow::Borrowed(&input[..i])
};
Ok((rest, ret))
}
///`quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS]`
fn quoted_string(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (input, opt_space) = opt(cfws)(input)?;
if !input.starts_with(b"\"") {
return Err(nom::Err::Error(
(input, "quoted_string(): doesn't start with DQUOTE").into(),
));
}
let input = &input[1..];
let mut i = 0;
while i < input.len() && input[i] != b'"' {
if opt_space.is_some() || (input[i..].starts_with(b"\\") && i + 1 < input.len()) {
let mut ret = if let Some(opt_space) = opt_space {
let mut r = Vec::with_capacity(2 * i);
r.extend_from_slice(&opt_space);
r
} else {
Vec::with_capacity(2 * i)
};
ret.extend_from_slice(&input[..i]);
i += 1;
ret.push(input[i]);
i += 1;
while i < input.len() && input[i] != b'"' {
if input[i..].starts_with(b"\\") && i + 1 < input.len() {
i += 1;
}
ret.push(input[i]);
i += 1;
}
if i < input.len() {
// skip DQUOTE
i += 1;
} else {
return Err(nom::Err::Error(
(input, "quoted_string(): unclosed DQUOTE").into(),
));
}
let (rest, opt_sp) = opt(cfws)(&input[i..])?;
if let Some(opt_sp) = opt_sp {
ret.extend_from_slice(&opt_sp);
}
let ret = Cow::Owned(ret);
return Ok((rest, ret));
}
i += 1;
}
let ret = Cow::Borrowed(&input[..i]);
if i < input.len() {
// skip DQUOTE
i += 1;
} else {
return Err(nom::Err::Error(
(input, "quoted_string(): unclosed DQUOTE").into(),
));
}
let (rest, opt_sp) = opt(cfws)(&input[i..])?;
if let Some(opt_sp) = opt_sp {
let mut ret = ret.to_vec();
ret.extend_from_slice(&opt_sp);
Ok((rest, Cow::Owned(ret)))
} else {
Ok((rest, ret))
}
}
///`word = atom / quoted-string`
fn word(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
alt((quoted_string, atom))(input)
}
///`phrase = 1*word / obs-phrase`
fn phrase(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
let (rest, words) = many1(word)(input)?;
let len = words.iter().map(|v| v.len()).sum::<usize>();
let mut ret = words
.into_iter()
.fold(Vec::with_capacity(len), |mut acc, el| {
acc.extend_from_slice(&el);
acc
});
let right_wsp_padding = ret.len() - ret.rtrim().len();
for _ in 0..right_wsp_padding {
ret.pop();
}
Ok((rest, ret))
}
#[test]
fn test_phrase() {
let s = b"\"Jeffrey \\\"fejj\\\" Stedfast\""; // <fejj@helixcode.com>"
assert_eq!(to_str!(&phrase(s).unwrap().1), "Jeffrey \"fejj\" Stedfast");
}
///`addr-spec = local-part "@" domain`
pub fn addr_spec(input: &[u8]) -> IResult<&[u8], Address> {
///`atext = ALPHA / DIGIT / ; Printable US-ASCII "!" / "#" / ; characters not including "$" / "%" / ; specials. Used for atoms. "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"`
fn atext(input: &[u8]) -> IResult<&[u8], u8> {
if input.is_empty() {
return Err(nom::Err::Error((input, "atext(): empty input").into()));
}
if input[0].is_ascii_alphanumeric()
|| [
b'!', b'#', b'$', b'%', b'&', b'\'', b'*', b'+', b'-', b'/', b'=', b'?', b'^',
b'_', b'`', b'{', b'|', b'}', b'~',
]
.contains(&input[0])
{
Ok((&input[1..], input[0]))
} else {
return Err(nom::Err::Error((input, "atext(): invalid byte").into()));
}
}
///dot-atom-text = 1*atext *("." 1*atext)
///dot-atom = [CFWS] dot-atom-text [CFWS]
fn dot_atom(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (mut input, _) = opt(cfws)(input)?;
let mut ret = vec![];
let mut at_least_one = false;
while let Ok((_input, atext_r)) = atext(input) {
at_least_one = true;
ret.push(atext_r);
input = _input;
}
if !at_least_one {
return Err(nom::Err::Error(
(input, "dot_atom(): starts with at least one atext").into(),
));
}
loop {
if !input.starts_with(b".") {
break;
}
ret.push(b'.');
input = &input[1..];
let mut at_least_one = false;
while let Ok((_input, atext_r)) = atext(input) {
at_least_one = true;
ret.push(atext_r);
input = _input;
}
if !at_least_one {
return Err(nom::Err::Error(
(input, "dot_atom(): DOT followed with at least one atext").into(),
));
}
}
let (input, _) = opt(cfws)(input)?;
Ok((input, ret.into()))
}
///`obs-domain = atom *("." atom)`
fn obs_domain(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (mut input, atom_) = context("obs_domain", atom)(input)?;
@ -1819,21 +1844,6 @@ pub mod address {
Ok((input, ret_s.into()))
}
///```text
///dtext = %d33-90 / ; Printable US-ASCII
/// %d94-126 / ; characters not including
/// obs-dtext ; "[", "]", or "\"
///```
fn dtext(input: &[u8]) -> IResult<&[u8], u8> {
if input.is_empty() {
return Err(nom::Err::Error((input, "dtext(): empty input").into()));
}
if (input[0] >= 33 && input[0] <= 90) || (input[0] > 94 && input[0] < 126) {
Ok((&input[1..], input[0]))
} else {
Err(nom::Err::Error((input, "dtext(): out of range").into()))
}
}
let (input, local_part) = context("addr_spec()", local_part)(input)?;
let (input, _) = context("addr_spec()", tag("@"))(input)?;
let (input, domain) = context("addr_spec()", domain)(input)?;
@ -1849,7 +1859,7 @@ pub mod address {
///`display-name = phrase`
pub fn display_name(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
let (rest, ret) = phrase(input)?;
let (rest, ret) = phrase2(input)?;
if let Ok((_, ret)) = crate::email::parser::encodings::phrase(&ret, true) {
Ok((rest, ret))
} else {
@ -1965,36 +1975,44 @@ pub mod address {
))
}
pub fn message_id(input: &[u8]) -> IResult<&[u8], &[u8]> {
delimited(tag("<"), take_until(">"), tag(">"))(input.ltrim())
//complete!(delimited!(ws!(tag!("<")), take_until1!(">"), tag!(">")))
}
fn message_id_peek(input: &[u8]) -> IResult<&[u8], &[u8]> {
let input_length = input.len();
if input.is_empty() {
Err(nom::Err::Error(
(input, "message_id_peek(): found EOF").into(),
))
} else if input_length == 2 || input[0] != b'<' {
Err(nom::Err::Error(
(input, "message_id_peek(): expected '<'").into(),
))
} else {
for (i, &x) in input.iter().take(input_length).enumerate().skip(1) {
if x == b'>' {
return Ok((&input[i + 1..], &input[0..=i]));
}
}
Err(nom::Err::Error(
(input, "message_id_peek(): expected closing '>'").into(),
))
///`msg-id = [CFWS] "<" id-left "@" id-right ">" [CFWS]`
pub fn msg_id(input: &[u8]) -> IResult<&[u8], MessageID> {
///`no-fold-literal = "[" *dtext "]"`
pub fn no_fold_literal(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let orig_input = input;
let (input, _) = tag("[")(input)?;
let (input, ret) = many0(dtext)(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, Cow::Borrowed(&orig_input[0..ret.len() + 1])))
}
///`id-left = dot-atom-text / obs-id-left`
pub fn id_left(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
dot_atom_text(input)
}
///`id-right = dot-atom-text / no-fold-literal / obs-id-right`
pub fn id_right(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
alt((dot_atom_text, no_fold_literal))(input)
}
let (input, _) = opt(cfws)(input)?;
let orig_input = input;
let (input, _) = tag("<")(input)?;
let (input, id_left_) = id_left(input)?;
let (input, _) = tag("@")(input)?;
let (input, id_right_) = id_right(input)?;
let (input, _) = tag(">")(input)?;
let (input, _) = opt(cfws)(input)?;
Ok((
input,
MessageID::new(
&orig_input[..3 + id_left_.len() + id_right_.len()],
&orig_input[1..2 + id_left_.len() + id_right_.len()],
),
))
}
pub fn references(input: &[u8]) -> IResult<&[u8], Vec<&[u8]>> {
separated_list(is_a(" \n\t\r"), message_id_peek)(input)
// separated_list!(complete!(is_a!(" \n\t\r")), message_id_peek));
pub fn msg_id_list(input: &[u8]) -> IResult<&[u8], Vec<MessageID>> {
many0(msg_id)(input)
}
use smallvec::SmallVec;
@ -2463,4 +2481,28 @@ border=3D=220=22>
</tr>"#)
);
}
#[test]
fn test_msg_id() {
let s = "Message-ID: <1234@local.machine.example>\r\n";
let (rest, (_header_name, value)) = headers::header(s.as_bytes()).unwrap();
assert!(rest.is_empty());
let a = msg_id(value).unwrap().1;
assert_eq!(a.val(), b"<1234@local.machine.example>");
let s = "Message-ID: <testabcd.1234@silly.test>\r\n";
let (rest, (_header_name, value)) = headers::header(s.as_bytes()).unwrap();
assert!(rest.is_empty());
let b = msg_id(value).unwrap().1;
assert_eq!(b.val(), b"<testabcd.1234@silly.test>");
let s = "References: <1234@local.machine.example>\r\n";
let (rest, (_header_name, value)) = headers::header(s.as_bytes()).unwrap();
assert!(rest.is_empty());
assert_eq!(&msg_id_list(value).unwrap().1, &[a.clone()]);
let s = "References: <1234@local.machine.example> <3456@example.net>\r\n";
let (rest, (_header_name, value)) = headers::header(s.as_bytes()).unwrap();
assert!(rest.is_empty());
let s = b"<3456@example.net>";
let c = msg_id(s).unwrap().1;
assert_eq!(&msg_id_list(value).unwrap().1, &[a, c]);
}
}