diff options
Diffstat (limited to 'rfc2822/src/lib.rs')
-rw-r--r-- | rfc2822/src/lib.rs | 898 |
1 files changed, 898 insertions, 0 deletions
diff --git a/rfc2822/src/lib.rs b/rfc2822/src/lib.rs new file mode 100644 index 00000000..0008a928 --- /dev/null +++ b/rfc2822/src/lib.rs @@ -0,0 +1,898 @@ +extern crate failure; +extern crate lalrpop_util; + +use lalrpop_util::ParseError; + +#[macro_use] mod macros; +#[macro_use] mod trace; +mod strings; +#[macro_use] mod component; +use component::{ + Component +}; +mod lexer; + +// We expose a number of productions for testing purposes. +// Unfortunately, lalrpop doesn't understand the #[cfg(test)] +// attribute. So, to avoid warnings, we allow unused imports and dead +// code when not testing. +#[cfg(test)] +mod grammar; +#[cfg(not(test))] +#[allow(unused_imports, dead_code)] +mod grammar; + +const TRACE : bool = false; + +pub type Result<T> = ::std::result::Result<T, Error>; +pub type Error = failure::Error; + +// A failure needs to have a 'static life time. lexer::Tokens don't. +// Convert tokens into strings. +// +// Unfortunately, we can't implement From, because we don't define the +// ParseError in this crate. +fn parse_error_downcast<'a>(e: ParseError<usize, lexer::Token<'a>, Error>) + -> ParseError<usize, String, Error> +{ + match e { + ParseError::UnrecognizedToken { + token: Some((start, t, end)), + expected, + } => ParseError::UnrecognizedToken { + token: Some((start, t.into(), end)), + expected, + }, + ParseError::UnrecognizedToken { + token: None, + expected, + } => ParseError::UnrecognizedToken { + token: None, + expected, + }, + + ParseError::ExtraToken { + token: (start, t, end), + } => ParseError::ExtraToken { + token: (start, t.into(), end), + }, + + ParseError::InvalidToken { location } + => ParseError::InvalidToken { location }, + + ParseError::User { error } + => ParseError::User { error }, + } +} + +/// A parsed RFC 2822 `addr-spec`. +/// +/// The address must not include angle brackets. That is, this parser +/// recognizes addresses of the form: +/// +/// ```text +/// email@example.org +/// ``` +/// +/// But not: +/// +/// ```text +/// email@example.org +/// ``` +/// +/// RFC 2822 comments are ignored. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct AddrSpec { + components: Vec<Component>, +} + +impl AddrSpec { + /// Create an RFC 2822 `addr-spec`. + /// + /// Not yet exported as this function does *not* do any escaping. + #[allow(dead_code)] + fn new<S>(address: S) + -> Result<Self> + where S: AsRef<str> + Eq + std::fmt::Debug, + { + let address = address.as_ref(); + + // Make sure the input is valid. + let a = match Self::parse(address) { + Err(err) => return Err(err.into()), + Ok(a) => a, + }; + + assert_eq!(a.address(), address); + + Ok(a) + } + + /// Parses a string that allegedly contains an [RFC 2822 + /// `addr-spec`]. + /// + /// [RFC 2822 `addr-spec`]: https://tools.ietf.org/html/rfc2822#section-3.4 + pub fn parse<S>(input: S) -> Result<Self> + where S: AsRef<str> + { + let lexer = lexer::Lexer::new(input.as_ref()); + let components = match grammar::AddrSpecParser::new().parse(lexer) { + Ok(components) => components, + Err(err) => return Err(parse_error_downcast(err).into()), + }; + + Ok(Self { + components, + }) + } + + /// Returns the address. + pub fn address(&self) -> &str { + for c in self.components.iter() { + if let Component::Address(t) = c { + return &t[..]; + } + } + // An addr-spec always has an Address. + unreachable!(); + } +} + +/// A parsed [RFC 2822 `name-addr`]. +/// +/// `name-addr`s are typically of the form: +/// +/// ```text +/// First Last (Comment) <email@example.org> +/// ``` +/// +/// The name and comment are optional, but the comment is only allowed +/// if there is also a name. +/// +/// Note: this does not recognize bare addresses. That is the angle +/// brackets are required and the following is not recognized as a +/// `name-addr`: +/// +/// ```text +/// email@example.org +/// ``` +/// +/// [RFC 2822 `name-addr`]: https://tools.ietf.org/html/rfc2822#section-3.4 +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct NameAddr { + components: Vec<Component>, +} + +impl NameAddr { + /// Create an RFC 2822 `name-addr`. + /// + /// Not yet exported as this function does *not* do any escaping. + #[allow(dead_code)] + fn new<S>(name: Option<S>, comment: Option<S>, address: Option<S>) + -> Result<Self> + where S: AsRef<str> + Eq + std::fmt::Debug, + { + let mut s = if let Some(ref name) = name { + String::from(name.as_ref()) + } else { + String::new() + }; + + if let Some(ref comment) = comment { + if name.is_some() { + s.push(' '); + } + s.push_str(&format!("({})", comment.as_ref())[..]); + } + + if let Some(ref address) = address { + if name.is_some() || comment.is_some() { + s.push(' '); + } + s.push_str(&format!("<{}>", address.as_ref())[..]); + } + + // Make sure the input is valid. + let na = match Self::parse(s) { + Err(err) => return Err(err.into()), + Ok(na) => na, + }; + + if let Some(name_reparsed) = na.name() { + assert!(name.is_some()); + assert_eq!(name_reparsed, name.unwrap().as_ref()); + } else { + assert!(name.is_none()); + } + if let Some(comment_reparsed) = na.comment() { + assert!(comment.is_some()); + assert_eq!(comment_reparsed, comment.unwrap().as_ref()); + } else { + assert!(comment.is_none()); + } + if let Some(address_reparsed) = na.address() { + assert!(address.is_some()); + assert_eq!(address_reparsed, address.unwrap().as_ref()); + } else { + assert!(address.is_none()); + } + + Ok(na) + } + + /// Parses a string that allegedly contains an [RFC 2822 + /// `name-addr`]. + /// + /// [RFC 2822 `name-addr`]: https://tools.ietf.org/html/rfc2822#section-3.4 + pub fn parse<S>(input: S) -> Result<Self> + where S: AsRef<str> + { + let lexer = lexer::Lexer::new(input.as_ref()); + let components = match grammar::NameAddrParser::new().parse(lexer) { + Ok(components) => components, + Err(err) => return Err(parse_error_downcast(err).into()), + }; + + Ok(Self { + components, + }) + } + + /// Returns the [display name]. + /// + /// [display name]: https://tools.ietf.org/html/rfc2822#section-3.4 + pub fn name(&self) -> Option<&str> { + for c in self.components.iter() { + if let Component::Text(t) = c { + return Some(&t[..]); + } + } + None + } + + /// Returns the first comment. + pub fn comment(&self) -> Option<&str> { + for c in self.components.iter() { + if let Component::Comment(t) = c { + return Some(&t[..]); + } + } + None + } + + /// Returns the address. + pub fn address(&self) -> Option<&str> { + for c in self.components.iter() { + if let Component::Address(t) = c { + return Some(&t[..]); + } + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! c { + ( $parser:expr, $t:ty) => { + fn c<S>(input: S, expected: Option<$t>) + where S: AsRef<str> + { + let input = input.as_ref().to_string(); + eprintln!("\n\ninput: '{:?}'", input); + + let lexer = lexer::Lexer::new(&input[..]); + let result = $parser.parse(lexer); + + if let Some(expected) = expected { + if let Ok(result) = result { + assert_eq!(result, expected, + "Parsing: '{:?}'\n got: '{:?}'\nexpected: '{:?}'", + input, result, expected); + } else { + panic!("Parsing: '{:?}': {:?}", input, result); + } + } else { + assert!(result.is_err(), "Parsing '{:?}'\n got: '{:?}'\nexpected: '{:?}'", + input, result, expected); + } + } + }; + } + + // comment = "(" *([FWS] ccontent) [FWS] ")" + // ccontent = ctext / quoted-pair / comment + // ctext = NO-WS-CTL / ; Non white space controls + // %d33-39 / ; The rest of the US-ASCII + // %d42-91 / ; characters not including "(", + // %d93-126 ; ")", or "\" + // quoted-pair = ("\" text) / obs-qp + // text = %d1-9 / ; Characters excluding CR and LF + // %d11 / + // %d12 / + // %d14-127 / + // obs-text + + // The comment production parses a single comment. + // + // A comment can contain pretty much anything, but any parenthesis + // need to be balanced (or escaped). + #[test] + fn comment_parser() { + c!(grammar::CommentParser::new(), Component); + + // A comment must be surrounded by (). + c("foobar", None); + c("(foobar)", + Some(Component::Comment("foobar".into()))); + c("(foo bar)", + Some(Component::Comment("foo bar".into()))); + // Unbalanced parenthesis are not allowed. + c("((foobar)", None); + // Unless they are escaped. + c("(\\(foobar)", + Some(Component::Comment("(foobar".into()))); + c("((foobar))", + Some(Component::Comment("(foobar)".into()))); + c("((fo()ob()ar))", + Some(Component::Comment("(fo()ob()ar)".into()))); + + // The comment parser doesn't remove leading or trailing + // whitespace. + c(" \r\n ((abc))", None); + c("((abc)) ", None); + + // Folding whitespace is compressed to a single space. + c("( a)", + Some(Component::Comment(" a".into()))); + c("(a )", + Some(Component::Comment("a ".into()))); + c("( a )", + Some(Component::Comment(" a ".into()))); + c("( a b )", + Some(Component::Comment(" a b ".into()))); + + c("( \r\n a b)", + Some(Component::Comment(" a b".into()))); + c("(a \r\n b )", + Some(Component::Comment("a b ".into()))); + c("(a b \r\n )", + Some(Component::Comment("a b ".into()))); + c("( a b )", + Some(Component::Comment(" a b ".into()))); + c("( \r\n a \r\n b \r\n )", + Some(Component::Comment(" a b ".into()))); + + c("( a \r\n bc \r\n d )", + Some(Component::Comment(" a bc d ".into()))); + c("(( a \r\n bc \r\n d ))", + Some(Component::Comment("( a bc d )".into()))); + + // The crlf in folding white space must be followed by a + // space. + c("(foo\r\n)", None); + c("(foo\r\n )", + Some(Component::Comment("foo ".into()))); + + // Multiple folding white spaces in a row are not allowed. + c("(( \r\n \r\n a ))", None); + c("(( abcd \r\n \r\n a ))", None); + c("(( abcd \r\n \r\n ))", None); + } + + // CFWS = *([FWS] comment) (([FWS] comment) / FWS) + // + // The CFWS production allows for multiple comments preceded, + // separated and followed by folding whitespace. + #[test] + fn cfws_parser() { + c!(grammar::CfwsParser::new(), Vec<Component>); + + // A comment must be surrounded by (). + c("foobar", None); + c("(foobar)", + Some(vec![Component::Comment("foobar".into())])); + // Unbalanced parenthesis are not allowed. + c("((foobar)", None); + c("((foobar))", + Some(vec![Component::Comment("(foobar)".into())])); + c("((fo()ob()ar))", + Some(vec![Component::Comment("(fo()ob()ar)".into())])); + + // Folding white space before and after is okay. It appears + // as a single space character. + c(" \r\n ((abc))", + Some(vec![ + Component::WS, + Component::Comment("(abc)".into()), + ])); + c("((abc)) \r\n ", + Some(vec![ + Component::Comment("(abc)".into()), + Component::WS, + ])); + + c("((a \r\n bc \r\n d))", + Some(vec![Component::Comment("(a bc d)".into())])); + + // Multiple comments are also allowed. + c("((foobar buz)) (bam)\r\n (quuz) \r\n ", + Some(vec![ + Component::Comment("(foobar buz)".into()), + Component::WS, + Component::Comment("bam".into()), + Component::WS, + Component::Comment("quuz".into()), + Component::WS, + ])); + c("(xy(z)zy) ((foobar)) (bam)", + Some(vec![ + Component::Comment("xy(z)zy".into()), + Component::WS, + Component::Comment("(foobar)".into()), + Component::WS, + Component::Comment("bam".into()) + ])); + + // Adjacent comments don't have any spaces between them. + c("((foobar buz))(bam)(quuz)", + Some(vec![ + Component::Comment("(foobar buz)".into()), + Component::Comment("bam".into()), + Component::Comment("quuz".into()), + ])); + + } + + // atom = [CFWS] 1*atext [CFWS] + // + // Note: our atom parser also allows for dots. + // + // An atom is a sequence of characters. + // + // They may be preceded or followed by a CFWS (zero or more + // comments and folding white space). + // + // Note: no spaces are allowed in the atext! They can't even be + // escaped. + #[test] + fn atom_parser() { + c!(grammar::AtomParser::new(), Vec<Component>); + + c("foobar", Some(vec![Component::Text("foobar".into())])); + + // "Any character except controls, SP, and specials." + for &s in ["a", "1", "ß", "ü", "é", "あ", "fooゔヲ", "ℝ💣東京", + "!", "#", "$", "%", "&", "'", "*", "+", "-", + "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", + // Extension: + "."] + .into_iter() + { + c(s, Some(vec![Component::Text(s.to_string())])) + } + + for &s in ["\x02", " ", + "(", ")", "<", ">", "[", "]", ":", ";", + "@", "\\", ",", "\""] + .into_iter() + { + c(s, None) + } + + // No internal white space. + c("foo bar", None); + + // If a CFWS precedes an atom, any comments are retained, but + // trailing white space is removed. + c("\r\n foobar \r\n ", + Some(vec![Component::Text("foobar".into())])); + c(" \r\n foobar ", + Some(vec![Component::Text("foobar".into())])); + + c("(some comment)foobar", + Some(vec![ + Component::Comment("some comment".into()), + Component::Text("foobar".into()) + ])); + c("(some comment) foobar", + Some(vec![ + Component::Comment("some comment".into()), + Component::Text("foobar".into()) + ])); + c("(so\r\n m \r\n e co\r\n mme \r\n nt\r\n ) \r\n foobar", + Some(vec![ + Component::Comment("so m e co mme nt ".into()), + Component::Text("foobar".into()) + ])); + + c("(a)(b)(c)foobar(d)(e)", + Some(vec![ + Component::Comment("a".into()), + Component::Comment("b".into()), + Component::Comment("c".into()), + Component::Text("foobar".into()), + Component::Comment("d".into()), + Component::Comment("e".into()) + ])); + c(" \r\n (a)\r\n (b)\r\n (c)\r\n foobar \r\n (d)(e) \r\n ", + Some(vec![ + Component::WS, + Component::Comment("a".into()), + Component::WS, + Component::Comment("b".into()), + Component::WS, + Component::Comment("c".into()), + // Whitespace between a comment and an atom is elided. + Component::Text("foobar".into()), + Component::Comment("d".into()), + Component::Comment("e".into()), + Component::WS, + ])); + } + + // quoted-string = [CFWS] + // DQUOTE *([FWS] qcontent) [FWS] DQUOTE + // [CFWS] + // + // qcontent = qtext / quoted-pair + // + // qtext = NO-WS-CTL / ; Non white space controls + // %d33 / ; The rest of the US-ASCII + // %d35-91 / ; characters not including "\" + // %d93-126 ; or the quote character + // + // quoted-pair = ("\" text) / obs-qp + #[test] + fn quoted_string_parser() { + c!(grammar::QuotedStringParser::new(), Vec<Component>); + + c("\"foobar\"", Some(vec![Component::Text("foobar".into())])); + c("\" foobar\"", Some(vec![Component::Text(" foobar".into())])); + c("\"foobar \"", Some(vec![Component::Text("foobar ".into())])); + c("\"foo bar bam \"", + Some(vec![Component::Text("foo bar bam ".into())])); + c("\" foo bar bam \"", + Some(vec![Component::Text(" foo bar bam ".into())])); + c("\r\n \"(some comment)\"", + Some(vec![ + Component::WS, + Component::Text("(some comment)".into()), + ])); + c("\"(some comment)\" \r\n ", + Some(vec![ + Component::Text("(some comment)".into()), + Component::WS, + ])); + + + c("\"\\f\\o\\o\\b\\a\\r\"", + Some(vec![Component::Text("foobar".into())])); + + // comments in a quoted string aren't. + c("(not a comment)\"foobar\"", + Some(vec![ + Component::Comment("not a comment".into()), + Component::Text("foobar".into()) + ])); + c("\"(not a comment)foobar\"", + Some(vec![Component::Text("(not a comment)foobar".into())])); + c("\"))((((not a comment)foobar\"", + Some(vec![Component::Text("))((((not a comment)foobar".into())])); + } + + // word = atom / quoted-string + #[test] + fn word_parser() { + c!(grammar::WordParser::new(), Vec<Component>); + + c("foobar", Some(vec![Component::Text("foobar".into())])); + c("\"foobar\"", Some(vec![Component::Text("foobar".into())])); + c("\"\\f\\o\\o\\b\\a\\r\"", Some(vec![Component::Text("foobar".into())])); + } + + // phrase = 1*word / obs-phrase + #[test] + fn phrase_parser() { + c!(grammar::PhraseParser::new(), Vec<Component>); + + c("foobar", Some(vec![Component::Text("foobar".into())])); + c("foobar bam", Some(vec![Component::Text("foobar bam".into())])); + c("foobar bam", Some(vec![Component::Text("foobar bam".into())])); + c(" foobar bam ", + Some(vec![ + Component::WS, + Component::Text("foobar bam".into()), + Component::WS, + ])); + + c("\"foobar\"", Some(vec![Component::Text("foobar".into())])); + c("\"foobar\" \"bam\"", Some(vec![Component::Text("foobar bam".into())])); + c("\"foobar\" \"bam\"", Some(vec![Component::Text("foobar bam".into())])); + c(" \"foobar\" \"bam\" ", + Some(vec![ + Component::WS, + Component::Text("foobar bam".into()), + Component::WS, + ])); + + c("\"foobar\"\"bam\"", + Some(vec![Component::Text("foobarbam".into())])); + c("\"foobar\"quuz\"bam\"", + Some(vec![Component::Text("foobarquuzbam".into())])); + c("\"foobar\"quuz \"bam\"", + Some(vec![Component::Text("foobarquuz bam".into())])); + c("\"foobar\"quuz \"bam\"", + Some(vec![Component::Text("foobarquuz bam".into())])); + + c("\"foobar\"", Some(vec![Component::Text("foobar".into())])); + // Just a comment is not allowed. + c("(foobar)", None); + c("(foobar) quux", + Some(vec![ + Component::Comment("foobar".into()), + Component::WS, + Component::Text("quux".into()) + ])); + c("xyzzy (foobar) quux", + Some(vec![Component::Text("xyzzy".into()), + Component::WS, + Component::Comment("foobar".into()), + Component::WS, + Component::Text("quux".into())])); + c("foobar (comment) \"quoted string\"", + Some(vec![ + Component::Text("foobar".into()), + Component::WS, + Component::Comment("comment".into()), + Component::WS, + Component::Text("quoted string".into())])); + c("foobar (comment) \" quoted string\"", + Some(vec![ + Component::Text("foobar".into()), + Component::WS, + Component::Comment("comment".into()), + Component::WS, + Component::Text(" quoted string".into())])); + c("foobar bam quuz", + Some(vec![Component::Text("foobar bam quuz".into())])); + } + + // dot-atom = [CFWS] dot-atom-text [CFWS] + // dot-atom-text = 1*atext *("." 1*atext) + #[test] + fn dot_atom_parser() { + c!(grammar::DotAtomParser::new(), Vec<Component>); + + c("f", + Some(vec![Component::Text("f".into())])); + c("foo", + Some(vec![Component::Text("foo".into())])); + c("f.o", + Some(vec![Component::Text("f.o".into())])); + c("foo.bar", + Some(vec![Component::Text("foo.bar".into())])); + + c("foo.", None); + c("foo.bar.", None); + c("foo..bar", None); + c(".foo.bar", None); + c(".", None); + c("..", None); + + // Internal space is not allowed. + c("foo bar", None); + + // But lead and trailing space is okay (and it is removed). + c(" f", + Some(vec![Component::Text("f".into())])); + c(" f", + Some(vec![Component::Text("f".into())])); + c("f ", + Some(vec![Component::Text("f".into())])); + c("f ", + Some(vec![Component::Text("f".into())])); + + // Comments are also okay. + c("(comment) f", + Some(vec![ + Component::Comment("comment".into()), + Component::Text("f".into()), + ])); + c(" (comment) f", + Some(vec![ + Component::WS, + Component::Comment("comment".into()), + Component::Text("f".into()), + ])); + c(" f (comment) ", + Some(vec![ + Component::Text("f".into()), + Component::Comment("comment".into()), + Component::WS, + ])); + c(" f (comment)", + Some(vec![ + Component::Text("f".into()), + Component::Comment("comment".into()), + ])); + } + + + // domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS] + #[test] + fn domain_literal_parser() { + c!(grammar::DomainLiteralParser::new(), Vec<Component>); + + c("[foo]", + Some(vec![Component::Text("[foo]".into())])); + c("[\\[foo\\[]", + Some(vec![Component::Text("[[foo[]".into())])); + c("[foo.bar.com quux:biz]", + Some(vec![Component::Text("[foo.bar.com quux:biz]".into())])); + c(" \r\n [\r\n foo.bar.com \r\n quux:biz\r\n ]\r\n ", + Some(vec![ + Component::WS, + Component::Text("[ foo.bar.com quux:biz ]".into()), + Component::WS, + ])); + } + + // angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr + // addr-spec = local-part "@" domain + // local-part = dot-atom / quoted-string / obs-local-part + // dot-atom = [CFWS] dot-atom-text [CFWS] + // dot-atom-text = 1*atext *("." 1*atext) + // domain = dot-atom / domain-literal / obs-domain + // domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS] + #[test] + fn angle_addr_parser() { + c!(grammar::AngleAddrParser::new(), Vec<Component>); + + c("[foo]", None); + + // Normal email addresses. + c("<foo@bar.com>", Some(vec![Component::Address("foo@bar.com".into())])); + c("<foo@bar>", Some(vec![Component::Address("foo@bar".into())])); + c("<foo.bar@x>", Some(vec![Component::Address("foo.bar@x".into())])); + c("<foo@bar>", Some(vec![Component::Address("foo@bar".into())])); + + c("<foo@@bar>", None); + c("<f@oo@bar>", None); + + // Quote the local part. + c("<\"foo\"@bar.com>", + Some(vec![Component::Address("foo@bar.com".into())])); + c("<\"f\\\"oo\"@bar.com>", + Some(vec![Component::Address("f\"oo@bar.com".into())])); + // The whole thing has to be quoted. + c("<\"foo\".bar@x>", None); + + c("<foo@[bar.com]>", + Some(vec![Component::Address("foo@[bar.com]".into())])); + c("<foo@[bar]>", + Some(vec![Component::Address("foo@[bar]".into())])); + c("<foo.bar@[x]>", + Some(vec![Component::Address("foo.bar@[x]".into())])); + + c("<foo.bar@x.>", None); + + // White space is ignored at the beginning and ending of the + // local part, but not in the middle. + c("< \r\n foo.bar@x>", + Some(vec![Component::Address("foo.bar@x".into())])); + c("< \r\n foo.bar \r\n @x>", + Some(vec![Component::Address("foo.bar@x".into())])); + c("< (quuz) \r\n foo.bar@x>", + Some(vec![ + Component::WS, + Component::Comment("quuz".into()), + Component::Address("foo.bar@x".into()), + ])); + c("< \r\n foo.bar \r\n @x>", + Some(vec![Component::Address("foo.bar@x".into())])); + c("<f \r\n oo.bar@x>", None); + + c("<foo.bar@x \r\n >", + Some(vec![Component::Address("foo.bar@x".into())])); + c("<f \r\n oo.bar@x \r\n y>", None); + + c(" <foo.bar@x> ", + Some(vec![ + Component::WS, + Component::Address("foo.bar@x".into()), + Component::WS, + ])); + + // And don't forget comments... + c("< (Hello!) foo.bar@x \r\n >", + Some(vec![ + Component::WS, + Component::Comment("Hello!".into()), + Component::Address("foo.bar@x".into())])); + // Comments in the local part are always moved left. + c("< (Hello!) foo.bar (bye?) \r\n @x \r\n >", + Some(vec![ + Component::WS, + Component::Comment("Hello!".into()), + Component::WS, + Component::Comment("bye?".into()), + Component::Address("foo.bar@x".into()), + ])); + c("< (Hello!) foo.bar@x \r\n >", + Some(vec![ |