summaryrefslogtreecommitdiffstats
path: root/rfc2822/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rfc2822/src/lib.rs')
-rw-r--r--rfc2822/src/lib.rs898
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![