diff --git a/src/addrparse.rs b/src/addrparse.rs index 23120e9..e578679 100644 --- a/src/addrparse.rs +++ b/src/addrparse.rs @@ -327,6 +327,9 @@ fn addrparse_inner( )); } return Ok(MailAddrList(result)); + } else if c == '(' { + comment_return = Some(AddrParseState::Initial); + state = AddrParseState::Comment; } else { state = AddrParseState::Unquoted; addr = Some(String::new()); @@ -418,6 +421,9 @@ fn addrparse_inner( .collect(), ))); name = None; + } else if c == '(' { + comment_return = Some(AddrParseState::AfterQuotedName); + state = AddrParseState::Comment; } else { // I think technically not valid, but this occurs in real-world corpus, so // handle gracefully @@ -540,6 +546,9 @@ fn addrparse_inner( .collect(), ))); addr = None; + } else if c == '(' { + comment_return = Some(AddrParseState::NameWithEncodedWord); + state = AddrParseState::Comment; } else { addr.as_mut().unwrap().push(c); } @@ -738,6 +747,164 @@ mod tests { ); } + #[test] + fn parse_comments() { + fn single(name: Option<&str>, addr: &str) -> MailAddrList { + MailAddrList(vec![MailAddr::Single( + SingleInfo::new(name.map(String::from), addr.to_string()).unwrap(), + )]) + } + + // A comment before an unbracketed address; the case from issue #65, but leading + // rather than trailing. + assert_eq!(addrparse("(ab) x@y.com").unwrap(), single(None, "x@y.com")); + assert_eq!( + addrparse("(a)(b) x@y.com").unwrap(), + single(None, "x@y.com") + ); + assert_eq!( + addrparse(" (c) x@y.com").unwrap(), + single(None, "x@y.com") + ); + + // ...before the other three ways an address can start. + assert_eq!(addrparse("(c) ").unwrap(), single(None, "x@y.com")); + assert_eq!(addrparse("(c)").unwrap(), single(None, "x@y.com")); + assert_eq!( + addrparse("(c) Foo ").unwrap(), + single(Some("Foo"), "x@y.com") + ); + assert_eq!( + addrparse(r#"(c) "Foo" "#).unwrap(), + single(Some("Foo"), "x@y.com") + ); + + // A comment between a quoted display name and what follows it. + assert_eq!( + addrparse(r#""Foo" (c) "#).unwrap(), + single(Some("Foo"), "x@y.com") + ); + // The doubled space is pre-existing and not comment-specific: master produces it + // for `Foo (c) Bar ` too. + assert_eq!( + addrparse(r#""Foo" (c) Bar "#).unwrap(), + single(Some("Foo Bar"), "x@y.com") + ); + + // Comments in a list, and either side of a group. + assert_eq!( + addrparse("a@b.com, (c) x@y.com").unwrap(), + MailAddrList(vec![ + MailAddr::Single(SingleInfo::new(None, "a@b.com".to_string()).unwrap()), + MailAddr::Single(SingleInfo::new(None, "x@y.com".to_string()).unwrap()), + ]) + ); + assert_eq!( + addrparse("(c) grp: x@y.com;").unwrap(), + MailAddrList(vec![MailAddr::Group(GroupInfo::new( + "grp".to_string(), + vec![SingleInfo::new(None, "x@y.com".to_string()).unwrap()] + ))]) + ); + assert_eq!( + addrparse("grp: (c) x@y.com;").unwrap(), + MailAddrList(vec![MailAddr::Group(GroupInfo::new( + "grp".to_string(), + vec![SingleInfo::new(None, "x@y.com".to_string()).unwrap()] + ))]) + ); + assert_eq!( + addrparse(r#""grp" (c): x@y.com;"#).unwrap(), + MailAddrList(vec![MailAddr::Group(GroupInfo::new( + "grp".to_string(), + vec![SingleInfo::new(None, "x@y.com".to_string()).unwrap()] + ))]) + ); + + // ctext covers everything but "(", ")" and "\", so none of these terminate the + // comment or split the address list. Several of them used to be hard errors. + for body in [ + "a@b", "ab", "a,b", "a:b", "a;b", "a\"b", "", " c ", + ] { + assert_eq!( + addrparse(&format!("({}) x@y.com", body)).unwrap(), + single(None, "x@y.com"), + "comment body {:?}", + body + ); + } + + // A comment is not an address, so a header made only of comments is empty rather + // than an error. + assert_eq!(addrparse("(c)").unwrap(), MailAddrList(vec![])); + assert_eq!(addrparse("x@y.com, (c)").unwrap(), single(None, "x@y.com")); + assert_eq!( + addrparse("grp: x@y.com; (c)").unwrap(), + MailAddrList(vec![MailAddr::Group(GroupInfo::new( + "grp".to_string(), + vec![SingleInfo::new(None, "x@y.com".to_string()).unwrap()] + ))]) + ); + // ...but an unterminated one is still an error, as it already was in the trailing + // position. + assert!(addrparse("(c x@y.com").is_err()); + assert!(addrparse("x@y.com (c").is_err()); + + // Parentheses inside a quoted-string are qtext, not a comment. + assert_eq!( + addrparse(r#""F(o)o" "#).unwrap(), + single(Some("F(o)o"), "x@y.com") + ); + assert_eq!( + addrparse(r#""(c)" "#).unwrap(), + single(Some("(c)"), "x@y.com") + ); + // A stray ")" outside a comment stays put. + assert_eq!( + addrparse("Fo)o ").unwrap(), + single(Some("Fo)o"), "x@y.com") + ); + assert_eq!(addrparse(")x@y.com").unwrap(), single(None, ")x@y.com")); + + // The contents of an angle-addr are still passed through verbatim. + assert_eq!( + addrparse("<(c)x@y.com>").unwrap(), + single(None, "(c)x@y.com") + ); + } + + #[test] + fn parse_comments_with_encoded_words() { + let cases = [ + ("From: =?UTF-8?B?Rm9v?= (c) ", "Foo"), + ("From: (c) =?UTF-8?B?Rm9v?= ", "Foo"), + ( + "From: =?UTF-8?B?Rm9v?= (c) =?UTF-8?B?QmFy?= ", + "Foo Bar", + ), + ("From: \"=?utf-8?q?G=C3=B6tz?= C\" (x) ", "Götz C"), + ]; + for (header, name) in cases { + let (parsed, _) = crate::parse_header(header.as_bytes()).unwrap(); + let addrs = addrparse_header(&parsed).unwrap(); + assert_eq!( + addrs.extract_single_info().unwrap().display_name, + Some(name.to_string()), + "header {:?}", + header + ); + } + + let (parsed, _) = crate::parse_header(b"From: =?UTF-8?B?Z3Jw?= (c) : x@y.com;").unwrap(); + assert_eq!( + addrparse_header(&parsed).unwrap(), + MailAddrList(vec![MailAddr::Group(GroupInfo::new( + "grp".to_string(), + vec![SingleInfo::new(None, "x@y.com".to_string()).unwrap()] + ))]) + ); + } + #[test] fn parse_multi() { assert_eq!(