diff --git a/melib/src/backends/imap/protocol_parser.rs b/melib/src/backends/imap/protocol_parser.rs index 2182a3c5..1f3ab0a9 100644 --- a/melib/src/backends/imap/protocol_parser.rs +++ b/melib/src/backends/imap/protocol_parser.rs @@ -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 { diff --git a/melib/src/backends/jmap/objects/email.rs b/melib/src/backends/jmap/objects/email.rs index f5f529cc..5a464c44 100644 --- a/melib/src/backends/jmap/objects/email.rs +++ b/melib/src/backends/jmap/objects/email.rs @@ -246,10 +246,10 @@ impl std::convert::From 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); diff --git a/melib/src/backends/nntp/protocol_parser.rs b/melib/src/backends/nntp/protocol_parser.rs index 63df5f95..378be994 100644 --- a/melib/src/backends/nntp/protocol_parser.rs +++ b/melib/src/backends/nntp/protocol_parser.rs @@ -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); diff --git a/melib/src/email.rs b/melib/src/email.rs index f2b9c739..e950350e 100644 --- a/melib/src/email.rs +++ b/melib/src/email.rs @@ -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) { 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) { diff --git a/melib/src/email/address.rs b/melib/src/email/address.rs index 31317c1d..8cfc24ea 100644 --- a/melib/src/email/address.rs +++ b/melib/src/email/address.rs @@ -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, } ) ); diff --git a/melib/src/email/parser.rs b/melib/src/email/parser.rs index 50aaa1d0..ee25e82b 100644 --- a/melib/src/email/parser.rs +++ b/melib/src/email/parser.rs @@ -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> { + let (rest, words) = many1(word)(input)?; + let len = words.iter().map(|v| v.len()).sum::(); + 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\""; // " + 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> { - let (rest, words) = many1(word)(input)?; - let len = words.iter().map(|v| v.len()).sum::(); - 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\""; // " - 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> { - 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> { + many0(msg_id)(input) } use smallvec::SmallVec; @@ -2463,4 +2481,28 @@ border=3D=220=22> "#) ); } + + #[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: \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""); + 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]); + } }