@@ -209,6 +209,36 @@ fn extract_reply_to_address(original: &OriginalMessage) -> String {
209209 }
210210}
211211
212+ /// Split an RFC 5322 mailbox list on commas, respecting quoted strings.
213+ /// `"Doe, John" <john@example.com>, alice@example.com` →
214+ /// `["\"Doe, John\" <john@example.com>", "alice@example.com"]`
215+ fn split_mailbox_list ( header : & str ) -> Vec < & str > {
216+ let mut result = Vec :: new ( ) ;
217+ let mut in_quotes = false ;
218+ let mut start = 0 ;
219+
220+ for ( i, ch) in header. char_indices ( ) {
221+ match ch {
222+ '"' => in_quotes = !in_quotes,
223+ ',' if !in_quotes => {
224+ let token = header[ start..i] . trim ( ) ;
225+ if !token. is_empty ( ) {
226+ result. push ( token) ;
227+ }
228+ start = i + 1 ;
229+ }
230+ _ => { }
231+ }
232+ }
233+
234+ let token = header[ start..] . trim ( ) ;
235+ if !token. is_empty ( ) {
236+ result. push ( token) ;
237+ }
238+
239+ result
240+ }
241+
212242/// Extract the bare email address from a header value like
213243/// `"Alice <alice@example.com>"` → `"alice@example.com"` or
214244/// `"alice@example.com"` → `"alice@example.com"`.
@@ -227,46 +257,32 @@ fn build_reply_all_recipients(
227257 remove : Option < & str > ,
228258) -> ReplyRecipients {
229259 let to = extract_reply_to_address ( original) ;
230- let to_emails: Vec < String > = to
231- . split ( ',' )
232- . map ( |s| extract_email ( s. trim ( ) ) . to_lowercase ( ) )
260+ let to_emails: Vec < String > = split_mailbox_list ( & to )
261+ . iter ( )
262+ . map ( |s| extract_email ( s) . to_lowercase ( ) )
233263 . filter ( |s| !s. is_empty ( ) )
234264 . collect ( ) ;
235265
236266 // Combine original To and Cc for the CC field (excluding the reply-to recipients)
237267 let mut cc_addrs: Vec < & str > = Vec :: new ( ) ;
238268
239269 if !original. to . is_empty ( ) {
240- for addr in original. to . split ( ',' ) {
241- let addr = addr. trim ( ) ;
242- if !addr. is_empty ( ) {
243- cc_addrs. push ( addr) ;
244- }
245- }
270+ cc_addrs. extend ( split_mailbox_list ( & original. to ) ) ;
246271 }
247272 if !original. cc . is_empty ( ) {
248- for addr in original. cc . split ( ',' ) {
249- let addr = addr. trim ( ) ;
250- if !addr. is_empty ( ) {
251- cc_addrs. push ( addr) ;
252- }
253- }
273+ cc_addrs. extend ( split_mailbox_list ( & original. cc ) ) ;
254274 }
255275
256276 // Add extra CC if provided
257277 if let Some ( extra) = extra_cc {
258- for addr in extra. split ( ',' ) {
259- let addr = addr. trim ( ) ;
260- if !addr. is_empty ( ) {
261- cc_addrs. push ( addr) ;
262- }
263- }
278+ cc_addrs. extend ( split_mailbox_list ( extra) ) ;
264279 }
265280
266281 // Remove addresses if requested (exact email match)
267282 let remove_set: Vec < String > = remove
268283 . map ( |r| {
269- r. split ( ',' )
284+ split_mailbox_list ( r)
285+ . iter ( )
270286 . map ( |s| extract_email ( s) . to_lowercase ( ) )
271287 . collect ( )
272288 } )
@@ -277,8 +293,7 @@ fn build_reply_all_recipients(
277293 . filter ( |addr| {
278294 let email = extract_email ( addr) . to_lowercase ( ) ;
279295 // Filter out the reply-to recipients (already in To) and removed addresses
280- !to_emails. iter ( ) . any ( |t| t == & email)
281- && !remove_set. iter ( ) . any ( |r| r == & email)
296+ !to_emails. iter ( ) . any ( |t| t == & email) && !remove_set. iter ( ) . any ( |r| r == & email)
282297 } )
283298 . collect ( ) ;
284299
@@ -624,8 +639,7 @@ mod tests {
624639 date : "" . to_string ( ) ,
625640 snippet : "" . to_string ( ) ,
626641 } ;
627- let recipients =
628- build_reply_all_recipients ( & original, None , Some ( "ann@example.com" ) ) ;
642+ let recipients = build_reply_all_recipients ( & original, None , Some ( "ann@example.com" ) ) ;
629643 let cc = recipients. cc . unwrap ( ) ;
630644 // joann@example.com should remain, ann@example.com should be removed
631645 assert_eq ! ( cc, "joann@example.com" ) ;
@@ -655,7 +669,10 @@ mod tests {
655669
656670 #[ test]
657671 fn test_extract_email_malformed_no_closing_bracket ( ) {
658- assert_eq ! ( extract_email( "Alice <alice@example.com" ) , "Alice <alice@example.com" ) ;
672+ assert_eq ! (
673+ extract_email( "Alice <alice@example.com" ) ,
674+ "Alice <alice@example.com"
675+ ) ;
659676 }
660677
661678 #[ test]
@@ -702,11 +719,8 @@ mod tests {
702719 date : "" . to_string ( ) ,
703720 snippet : "" . to_string ( ) ,
704721 } ;
705- let recipients = build_reply_all_recipients (
706- & original,
707- None ,
708- Some ( "Carol <carol@example.com>" ) ,
709- ) ;
722+ let recipients =
723+ build_reply_all_recipients ( & original, None , Some ( "Carol <carol@example.com>" ) ) ;
710724 let cc = recipients. cc . unwrap ( ) ;
711725 assert_eq ! ( cc, "bob@example.com" ) ;
712726 }
@@ -725,8 +739,7 @@ mod tests {
725739 date : "" . to_string ( ) ,
726740 snippet : "" . to_string ( ) ,
727741 } ;
728- let recipients =
729- build_reply_all_recipients ( & original, Some ( "extra@example.com" ) , None ) ;
742+ let recipients = build_reply_all_recipients ( & original, Some ( "extra@example.com" ) , None ) ;
730743 let cc = recipients. cc . unwrap ( ) ;
731744 assert ! ( cc. contains( "bob@example.com" ) ) ;
732745 assert ! ( cc. contains( "extra@example.com" ) ) ;
@@ -793,4 +806,77 @@ mod tests {
793806 assert ! ( !cc. contains( "list@example.com" ) ) ;
794807 assert ! ( !cc. contains( "owner@example.com" ) ) ;
795808 }
809+
810+ #[ test]
811+ fn test_split_mailbox_list_simple ( ) {
812+ let addrs = split_mailbox_list ( "alice@example.com, bob@example.com" ) ;
813+ assert_eq ! ( addrs, vec![ "alice@example.com" , "bob@example.com" ] ) ;
814+ }
815+
816+ #[ test]
817+ fn test_split_mailbox_list_quoted_comma ( ) {
818+ let addrs =
819+ split_mailbox_list ( r#""Doe, John" <john@example.com>, alice@example.com"# ) ;
820+ assert_eq ! (
821+ addrs,
822+ vec![ r#""Doe, John" <john@example.com>"# , "alice@example.com" ]
823+ ) ;
824+ }
825+
826+ #[ test]
827+ fn test_split_mailbox_list_single ( ) {
828+ let addrs = split_mailbox_list ( "alice@example.com" ) ;
829+ assert_eq ! ( addrs, vec![ "alice@example.com" ] ) ;
830+ }
831+
832+ #[ test]
833+ fn test_split_mailbox_list_empty ( ) {
834+ let addrs = split_mailbox_list ( "" ) ;
835+ assert ! ( addrs. is_empty( ) ) ;
836+ }
837+
838+ #[ test]
839+ fn test_reply_all_with_quoted_comma_display_name ( ) {
840+ let original = OriginalMessage {
841+ thread_id : "t1" . to_string ( ) ,
842+ message_id_header : "" . to_string ( ) ,
843+ references : "" . to_string ( ) ,
844+ from : "sender@example.com" . to_string ( ) ,
845+ reply_to : "" . to_string ( ) ,
846+ to : r#""Doe, John" <john@example.com>, alice@example.com"# . to_string ( ) ,
847+ cc : "" . to_string ( ) ,
848+ subject : "" . to_string ( ) ,
849+ date : "" . to_string ( ) ,
850+ snippet : "" . to_string ( ) ,
851+ } ;
852+ let recipients = build_reply_all_recipients ( & original, None , None ) ;
853+ let cc = recipients. cc . unwrap ( ) ;
854+ // Both addresses should be preserved intact
855+ assert ! ( cc. contains( "john@example.com" ) ) ;
856+ assert ! ( cc. contains( "alice@example.com" ) ) ;
857+ }
858+
859+ #[ test]
860+ fn test_remove_with_quoted_comma_display_name ( ) {
861+ let original = OriginalMessage {
862+ thread_id : "t1" . to_string ( ) ,
863+ message_id_header : "" . to_string ( ) ,
864+ references : "" . to_string ( ) ,
865+ from : "sender@example.com" . to_string ( ) ,
866+ reply_to : "" . to_string ( ) ,
867+ to : r#""Doe, John" <john@example.com>, alice@example.com"# . to_string ( ) ,
868+ cc : "" . to_string ( ) ,
869+ subject : "" . to_string ( ) ,
870+ date : "" . to_string ( ) ,
871+ snippet : "" . to_string ( ) ,
872+ } ;
873+ let recipients = build_reply_all_recipients (
874+ & original,
875+ None ,
876+ Some ( "john@example.com" ) ,
877+ ) ;
878+ let cc = recipients. cc . unwrap ( ) ;
879+ assert ! ( !cc. contains( "john@example.com" ) ) ;
880+ assert ! ( cc. contains( "alice@example.com" ) ) ;
881+ }
796882}
0 commit comments