@@ -57,10 +57,17 @@ impl Header for AutoSubmitted {
5757 }
5858}
5959
60- /// IRMA/Yivi attribute identifier for the signer's full name. When this
61- /// attribute appears in `FileState.sender_attributes` we render the name
62- /// in place of the bare email everywhere the sender is shown in the body.
63- const FULLNAME_ATYPE : & str = "pbdf.gemeente.personalData.fullname" ;
60+ /// Suffix that identifies the signer's full-name attribute across IRMA
61+ /// schemes — prod (`pbdf.gemeente.personalData.fullname`) and demo
62+ /// (`irma-demo.gemeente.personalData.fullname`) both end with this. When
63+ /// such an attribute appears in `FileState.sender_attributes` we render
64+ /// the disclosed name in place of the bare email everywhere the sender
65+ /// is shown in the body.
66+ const FULLNAME_ATYPE_SUFFIX : & str = ".gemeente.personalData.fullname" ;
67+
68+ fn is_fullname_atype ( atype : & str ) -> bool {
69+ atype. ends_with ( FULLNAME_ATYPE_SUFFIX )
70+ }
6471
6572/// Embedded PostGuard logo, served inline via a `Content-ID: <pg-logo>`
6673/// MIME part rather than fetched from postguard.eu. Removes the
@@ -157,11 +164,6 @@ struct EmailTextTemplate<'a> {
157164 sender_attributes : & ' a [ ( String , String ) ] ,
158165}
159166
160- /// Resolve the display string and remaining attribute pills for the
161- /// sender. When the signer disclosed their full name, the name takes the
162- /// place of the bare email everywhere it would otherwise appear in the
163- /// body, and is removed from the attribute pill list so it doesn't render
164- /// twice.
165167/// Assemble the MIME body: a `multipart/alternative` whose HTML branch is
166168/// itself a `multipart/related` carrying the HTML part plus the PostGuard
167169/// logo as an inline image referenced via `cid:pg-logo`. This shape avoids
@@ -180,12 +182,19 @@ fn build_body(html: String, text: String) -> Result<MultiPart, Box<dyn std::erro
180182 . multipart ( related) )
181183}
182184
185+ /// Resolve the display string and remaining attribute pills for the
186+ /// sender. When the signer disclosed their full name, the name takes the
187+ /// place of the bare email everywhere it would otherwise appear in the
188+ /// body, and is removed from the attribute pill list so it doesn't render
189+ /// twice. An empty disclosed value is treated as not disclosed, so we
190+ /// fall through to the email instead of rendering a blank.
183191fn sender_display ( state : & FileState ) -> ( String , Vec < ( String , String ) > ) {
184192 let mut attrs = state. sender_attributes . clone ( ) ;
185193 let name = attrs
186194 . iter ( )
187- . position ( |( t, _) | t == FULLNAME_ATYPE )
188- . map ( |i| attrs. remove ( i) . 1 ) ;
195+ . position ( |( t, _) | is_fullname_atype ( t) )
196+ . map ( |i| attrs. remove ( i) . 1 )
197+ . filter ( |n| !n. is_empty ( ) ) ;
189198 let display = name
190199 . or_else ( || state. sender . clone ( ) )
191200 . unwrap_or_else ( || "Someone" . to_string ( ) ) ;
@@ -359,12 +368,15 @@ pub async fn send_email(
359368 . from ( config. email_from ( ) ) // checked in config
360369 . to ( recipient. clone ( ) )
361370 . subject ( subject) ;
362- if let Some ( reply_to) = state
363- . sender
364- . as_deref ( )
365- . and_then ( |s| s. parse :: < Mailbox > ( ) . ok ( ) )
366- {
367- builder = builder. reply_to ( reply_to) ;
371+ if let Some ( sender) = state. sender . as_deref ( ) {
372+ match sender. parse :: < Mailbox > ( ) {
373+ Ok ( mailbox) => builder = builder. reply_to ( mailbox) ,
374+ Err ( e) => log:: warn!(
375+ "Skipping Reply-To: sender `{}` did not parse as Mailbox: {}" ,
376+ sender,
377+ e
378+ ) ,
379+ }
368380 }
369381 let email = builder. multipart ( build_body ( html, text) ?) ?;
370382
@@ -497,14 +509,37 @@ mod tests {
497509 #[ test]
498510 fn sender_display_promotes_disclosed_name ( ) {
499511 let state = filestate_with_attrs ( vec ! [
500- ( FULLNAME_ATYPE . to_owned( ) , "Jan Jansen" . to_owned( ) ) ,
512+ (
513+ "pbdf.gemeente.personalData.fullname" . to_owned( ) ,
514+ "Jan Jansen" . to_owned( ) ,
515+ ) ,
501516 ( "orgName" . to_owned( ) , "Acme" . to_owned( ) ) ,
502517 ] ) ;
503518 let ( display, remaining) = sender_display ( & state) ;
504519 assert_eq ! ( display, "Jan Jansen" ) ;
505520 assert_eq ! ( remaining, vec![ ( "orgName" . to_owned( ) , "Acme" . to_owned( ) ) ] ) ;
506521 }
507522
523+ #[ test]
524+ fn sender_display_promotes_disclosed_name_from_demo_scheme ( ) {
525+ let state = filestate_with_attrs ( vec ! [ (
526+ "irma-demo.gemeente.personalData.fullname" . to_owned( ) ,
527+ "Jan Jansen" . to_owned( ) ,
528+ ) ] ) ;
529+ let ( display, _) = sender_display ( & state) ;
530+ assert_eq ! ( display, "Jan Jansen" ) ;
531+ }
532+
533+ #[ test]
534+ fn sender_display_treats_empty_disclosed_name_as_not_disclosed ( ) {
535+ let state = filestate_with_attrs ( vec ! [ (
536+ "pbdf.gemeente.personalData.fullname" . to_owned( ) ,
537+ String :: new( ) ,
538+ ) ] ) ;
539+ let ( display, _) = sender_display ( & state) ;
540+ assert_eq ! ( display, "sender@example.com" ) ;
541+ }
542+
508543 #[ test]
509544 fn sender_display_falls_back_to_email_when_no_name_disclosed ( ) {
510545 let state = filestate_with_attrs ( vec ! [ ( "orgName" . to_owned( ) , "Acme" . to_owned( ) ) ] ) ;
@@ -513,6 +548,15 @@ mod tests {
513548 assert_eq ! ( remaining, vec![ ( "orgName" . to_owned( ) , "Acme" . to_owned( ) ) ] ) ;
514549 }
515550
551+ #[ test]
552+ fn sender_display_uses_someone_when_no_sender_and_no_name ( ) {
553+ let mut state = filestate_with_attrs ( vec ! [ ] ) ;
554+ state. sender = None ;
555+ let ( display, remaining) = sender_display ( & state) ;
556+ assert_eq ! ( display, "Someone" ) ;
557+ assert ! ( remaining. is_empty( ) ) ;
558+ }
559+
516560 #[ test]
517561 fn x_postguard_header_round_trips ( ) {
518562 let parsed = XPostGuard :: parse ( X_POSTGUARD_VERSION ) . expect ( "parse" ) ;
0 commit comments