Skip to content

Commit a2084b3

Browse files
committed
Recognize comments in the remaining addrparse states
3e29988 added comment handling to the Unquoted and AfterBracketedAddr states and noted that "in general comment support is still lacking". RFC 5322 allows CFWS between any two tokens of an address, but the other states push '(' into whatever string they are accumulating, so a comment ends up in the addr or the display name: addrparse("(ab) x@y.com") -> addr "(ab) x@y.com" addrparse("\"Foo\" (c) <x@y.com>") -> display_name "Foo (c)" Comment bodies containing '<', ',', ':' or ';' were worse than that: they derailed the parse into a hard error. Handle '(' in Initial, AfterQuotedName and NameWithEncodedWord as well. The QuotedName state is deliberately left alone, since a paren inside a quoted-string is qtext (RFC 5322 3.2.4), as is BracketedAddr, whose contents this parser passes through verbatim.
1 parent 1a0171b commit a2084b3

1 file changed

Lines changed: 167 additions & 0 deletions

File tree

src/addrparse.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,9 @@ fn addrparse_inner(
327327
));
328328
}
329329
return Ok(MailAddrList(result));
330+
} else if c == '(' {
331+
comment_return = Some(AddrParseState::Initial);
332+
state = AddrParseState::Comment;
330333
} else {
331334
state = AddrParseState::Unquoted;
332335
addr = Some(String::new());
@@ -418,6 +421,9 @@ fn addrparse_inner(
418421
.collect(),
419422
)));
420423
name = None;
424+
} else if c == '(' {
425+
comment_return = Some(AddrParseState::AfterQuotedName);
426+
state = AddrParseState::Comment;
421427
} else {
422428
// I think technically not valid, but this occurs in real-world corpus, so
423429
// handle gracefully
@@ -540,6 +546,9 @@ fn addrparse_inner(
540546
.collect(),
541547
)));
542548
addr = None;
549+
} else if c == '(' {
550+
comment_return = Some(AddrParseState::NameWithEncodedWord);
551+
state = AddrParseState::Comment;
543552
} else {
544553
addr.as_mut().unwrap().push(c);
545554
}
@@ -738,6 +747,164 @@ mod tests {
738747
);
739748
}
740749

750+
#[test]
751+
fn parse_comments() {
752+
fn single(name: Option<&str>, addr: &str) -> MailAddrList {
753+
MailAddrList(vec![MailAddr::Single(
754+
SingleInfo::new(name.map(String::from), addr.to_string()).unwrap(),
755+
)])
756+
}
757+
758+
// A comment before an unbracketed address; the case from issue #65, but leading
759+
// rather than trailing.
760+
assert_eq!(addrparse("(ab) x@y.com").unwrap(), single(None, "x@y.com"));
761+
assert_eq!(
762+
addrparse("(a)(b) x@y.com").unwrap(),
763+
single(None, "x@y.com")
764+
);
765+
assert_eq!(
766+
addrparse(" (c) x@y.com").unwrap(),
767+
single(None, "x@y.com")
768+
);
769+
770+
// ...before the other three ways an address can start.
771+
assert_eq!(addrparse("(c) <x@y.com>").unwrap(), single(None, "x@y.com"));
772+
assert_eq!(addrparse("(c)<x@y.com>").unwrap(), single(None, "x@y.com"));
773+
assert_eq!(
774+
addrparse("(c) Foo <x@y.com>").unwrap(),
775+
single(Some("Foo"), "x@y.com")
776+
);
777+
assert_eq!(
778+
addrparse(r#"(c) "Foo" <x@y.com>"#).unwrap(),
779+
single(Some("Foo"), "x@y.com")
780+
);
781+
782+
// A comment between a quoted display name and what follows it.
783+
assert_eq!(
784+
addrparse(r#""Foo" (c) <x@y.com>"#).unwrap(),
785+
single(Some("Foo"), "x@y.com")
786+
);
787+
// The doubled space is pre-existing and not comment-specific: master produces it
788+
// for `Foo (c) Bar <x@y.com>` too.
789+
assert_eq!(
790+
addrparse(r#""Foo" (c) Bar <x@y.com>"#).unwrap(),
791+
single(Some("Foo Bar"), "x@y.com")
792+
);
793+
794+
// Comments in a list, and either side of a group.
795+
assert_eq!(
796+
addrparse("a@b.com, (c) x@y.com").unwrap(),
797+
MailAddrList(vec![
798+
MailAddr::Single(SingleInfo::new(None, "a@b.com".to_string()).unwrap()),
799+
MailAddr::Single(SingleInfo::new(None, "x@y.com".to_string()).unwrap()),
800+
])
801+
);
802+
assert_eq!(
803+
addrparse("(c) grp: x@y.com;").unwrap(),
804+
MailAddrList(vec![MailAddr::Group(GroupInfo::new(
805+
"grp".to_string(),
806+
vec![SingleInfo::new(None, "x@y.com".to_string()).unwrap()]
807+
))])
808+
);
809+
assert_eq!(
810+
addrparse("grp: (c) x@y.com;").unwrap(),
811+
MailAddrList(vec![MailAddr::Group(GroupInfo::new(
812+
"grp".to_string(),
813+
vec![SingleInfo::new(None, "x@y.com".to_string()).unwrap()]
814+
))])
815+
);
816+
assert_eq!(
817+
addrparse(r#""grp" (c): x@y.com;"#).unwrap(),
818+
MailAddrList(vec![MailAddr::Group(GroupInfo::new(
819+
"grp".to_string(),
820+
vec![SingleInfo::new(None, "x@y.com".to_string()).unwrap()]
821+
))])
822+
);
823+
824+
// ctext covers everything but "(", ")" and "\", so none of these terminate the
825+
// comment or split the address list. Several of them used to be hard errors.
826+
for body in [
827+
"a@b", "a<b", "a>b", "a,b", "a:b", "a;b", "a\"b", "", " c ",
828+
] {
829+
assert_eq!(
830+
addrparse(&format!("({}) x@y.com", body)).unwrap(),
831+
single(None, "x@y.com"),
832+
"comment body {:?}",
833+
body
834+
);
835+
}
836+
837+
// A comment is not an address, so a header made only of comments is empty rather
838+
// than an error.
839+
assert_eq!(addrparse("(c)").unwrap(), MailAddrList(vec![]));
840+
assert_eq!(addrparse("x@y.com, (c)").unwrap(), single(None, "x@y.com"));
841+
assert_eq!(
842+
addrparse("grp: x@y.com; (c)").unwrap(),
843+
MailAddrList(vec![MailAddr::Group(GroupInfo::new(
844+
"grp".to_string(),
845+
vec![SingleInfo::new(None, "x@y.com".to_string()).unwrap()]
846+
))])
847+
);
848+
// ...but an unterminated one is still an error, as it already was in the trailing
849+
// position.
850+
assert!(addrparse("(c x@y.com").is_err());
851+
assert!(addrparse("x@y.com (c").is_err());
852+
853+
// Parentheses inside a quoted-string are qtext, not a comment.
854+
assert_eq!(
855+
addrparse(r#""F(o)o" <x@y.com>"#).unwrap(),
856+
single(Some("F(o)o"), "x@y.com")
857+
);
858+
assert_eq!(
859+
addrparse(r#""(c)" <x@y.com>"#).unwrap(),
860+
single(Some("(c)"), "x@y.com")
861+
);
862+
// A stray ")" outside a comment stays put.
863+
assert_eq!(
864+
addrparse("Fo)o <x@y.com>").unwrap(),
865+
single(Some("Fo)o"), "x@y.com")
866+
);
867+
assert_eq!(addrparse(")x@y.com").unwrap(), single(None, ")x@y.com"));
868+
869+
// The contents of an angle-addr are still passed through verbatim.
870+
assert_eq!(
871+
addrparse("<(c)x@y.com>").unwrap(),
872+
single(None, "(c)x@y.com")
873+
);
874+
}
875+
876+
#[test]
877+
fn parse_comments_with_encoded_words() {
878+
let cases = [
879+
("From: =?UTF-8?B?Rm9v?= (c) <x@y.com>", "Foo"),
880+
("From: (c) =?UTF-8?B?Rm9v?= <x@y.com>", "Foo"),
881+
(
882+
"From: =?UTF-8?B?Rm9v?= (c) =?UTF-8?B?QmFy?= <x@y.com>",
883+
"Foo Bar",
884+
),
885+
("From: \"=?utf-8?q?G=C3=B6tz?= C\" (x) <g@c.de>", "Götz C"),
886+
];
887+
for (header, name) in cases {
888+
let (parsed, _) = crate::parse_header(header.as_bytes()).unwrap();
889+
let addrs = addrparse_header(&parsed).unwrap();
890+
assert_eq!(
891+
addrs.extract_single_info().unwrap().display_name,
892+
Some(name.to_string()),
893+
"header {:?}",
894+
header
895+
);
896+
}
897+
898+
let (parsed, _) = crate::parse_header(b"From: =?UTF-8?B?Z3Jw?= (c) : x@y.com;").unwrap();
899+
assert_eq!(
900+
addrparse_header(&parsed).unwrap(),
901+
MailAddrList(vec![MailAddr::Group(GroupInfo::new(
902+
"grp".to_string(),
903+
vec![SingleInfo::new(None, "x@y.com".to_string()).unwrap()]
904+
))])
905+
);
906+
}
907+
741908
#[test]
742909
fn parse_multi() {
743910
assert_eq!(

0 commit comments

Comments
 (0)