Skip to content

Commit 64aea4d

Browse files
committed
fix(email): address dobby review on disclosed-name flow
- Treat empty disclosed fullname as not disclosed; fall through to the email instead of rendering a blank where the sender appears. - Match the fullname attribute by `.gemeente.personalData.fullname` suffix so the `irma-demo` scheme used in staging also routes through this path. (Previously only `pbdf.gemeente.personalData.fullname` matched.) - Log a warning when `state.sender` is `Some(...)` but doesn't parse as a `Mailbox` — the Reply-To deliverability fix shouldn't silently drop. - Move the `sender_display` docstring from above `build_body` to above `sender_display`. - Extend the `pg-core not found in Cargo.lock` panic in build.rs to mention that PG_CORE_VERSION feeds the X-PostGuard header consumed by the Outlook add-in's OnMessageRead filter. - New tests: demo-scheme name detection, empty-name fallback, and the `Someone` fallback (sender=None, no name).
1 parent 7f03943 commit 64aea4d

2 files changed

Lines changed: 67 additions & 19 deletions

File tree

build.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ fn main() {
2121
None
2222
}
2323
})
24-
.expect("pg-core entry not found in Cargo.lock");
24+
.expect(
25+
"pg-core entry not found in Cargo.lock — PG_CORE_VERSION feeds the \
26+
X-PostGuard mail header that the Outlook add-in's OnMessageRead \
27+
launch event filters on (see src/email.rs::XPostGuard).",
28+
);
2529

2630
println!("cargo:rustc-env=PG_CORE_VERSION={}", version);
2731
}

src/email.rs

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
183191
fn 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

Comments
 (0)