@@ -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