Skip to content

Commit 8e93bdd

Browse files
committed
feat(upload): add notifyRecipients toggle on /fileupload/init
The recipient notification email was sent unconditionally on every upload — there was no way for a client to upload silently. That's a problem when the encrypted payload is delivered to recipients through another channel (e.g. an email add-in delivering the message from the sender's own mailbox) and the Cryptify mail would be a duplicate. Adds an optional `notifyRecipients` field to the InitBody. Defaults to `true` to preserve the long-standing behaviour for clients that don't know about it. When `false`, the per-recipient SMTP delivery loop in `send_email` is skipped; the recipient list is still parsed and stored (so the existing validation behaviour is unchanged) and the sender confirmation, gated separately by `confirm`, is sent regardless.
1 parent 3e05dfd commit 8e93bdd

4 files changed

Lines changed: 57 additions & 25 deletions

File tree

api-description.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ paths:
6565
type: "boolean"
6666
example: true
6767
description: "Whether to send a confirmation email to the sender"
68+
notifyRecipients:
69+
type: "boolean"
70+
default: true
71+
example: true
72+
description: "Whether to email each recipient with a download link. Optional; defaults to true. Set to false to upload silently when the encrypted payload reaches recipients through another channel and a Cryptify-sent notification would be a duplicate."
6873
responses:
6974
"200":
7075
description: "Successful operation"

src/email.rs

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -215,31 +215,39 @@ pub async fn send_email(
215215
mailer_builder = mailer_builder.credentials(credentials);
216216
}
217217

218-
for recipient in state.recipients.iter() {
219-
// combine URL with mail variables into template
220-
let base = Url::parse(config.server_url())?;
221-
let mut url = base.join("/download")?;
222-
url.query_pairs_mut()
223-
.append_pair("uuid", uuid)
224-
.append_pair("recipient", &format!("{}", recipient.email));
225-
226-
let (email, subject) = email_templates(state, url.as_str());
227-
let email = Message::builder()
228-
.header(ContentType::TEXT_HTML)
229-
.header(XPostGuard(X_POSTGUARD_VERSION.to_owned()))
230-
.from(config.email_from()) // checked in config
231-
.to(recipient.clone())
232-
.subject(subject)
233-
.body(email)?;
234-
235-
// send email
236-
log::info!("Sending email to {}", recipient.email);
237-
let mailer = mailer_builder.clone().build();
238-
mailer.send(&email).map_err(|e| {
239-
log::error!("Failed to send email to {}: {}", recipient.email, e);
240-
e
241-
})?;
242-
log::info!("Email sent to {}", recipient.email);
218+
if state.notify_recipients {
219+
for recipient in state.recipients.iter() {
220+
// combine URL with mail variables into template
221+
let base = Url::parse(config.server_url())?;
222+
let mut url = base.join("/download")?;
223+
url.query_pairs_mut()
224+
.append_pair("uuid", uuid)
225+
.append_pair("recipient", &format!("{}", recipient.email));
226+
227+
let (email, subject) = email_templates(state, url.as_str());
228+
let email = Message::builder()
229+
.header(ContentType::TEXT_HTML)
230+
.header(XPostGuard(X_POSTGUARD_VERSION.to_owned()))
231+
.from(config.email_from()) // checked in config
232+
.to(recipient.clone())
233+
.subject(subject)
234+
.body(email)?;
235+
236+
// send email
237+
log::info!("Sending email to {}", recipient.email);
238+
let mailer = mailer_builder.clone().build();
239+
mailer.send(&email).map_err(|e| {
240+
log::error!("Failed to send email to {}: {}", recipient.email, e);
241+
e
242+
})?;
243+
log::info!("Email sent to {}", recipient.email);
244+
}
245+
} else {
246+
log::info!(
247+
"notify_recipients disabled — skipping notification mail for {} recipient(s) on upload {}",
248+
state.recipients.iter().count(),
249+
uuid
250+
);
243251
}
244252

245253
if state.confirm {

src/main.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ struct InitBody {
4848
#[serde(rename = "mailLang")]
4949
mail_lang: email::Language,
5050
confirm: bool,
51+
/// Whether to email each recipient with a download link. Optional;
52+
/// defaults to `true` to preserve existing client behaviour. Set to
53+
/// `false` when the encrypted payload reaches the recipients through
54+
/// another channel (e.g. an email add-in delivering the message from
55+
/// the user's own mailbox) and a Cryptify-sent notification would be
56+
/// a duplicate. The recipient list itself is still validated and
57+
/// stored — only the SMTP delivery is skipped.
58+
#[serde(rename = "notifyRecipients", default = "default_true")]
59+
notify_recipients: bool,
60+
}
61+
62+
fn default_true() -> bool {
63+
true
5164
}
5265

5366
#[derive(Serialize, Deserialize)]
@@ -121,6 +134,7 @@ async fn upload_init(
121134
sender: None,
122135
sender_attributes: Vec::new(),
123136
confirm: request.confirm,
137+
notify_recipients: request.notify_recipients,
124138
is_api_key: api_key.0,
125139
},
126140
);

src/store.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ pub struct FileState {
2424
pub sender: Option<String>,
2525
pub sender_attributes: Vec<(String, String)>,
2626
pub confirm: bool,
27+
/// When false, the recipient notification email is suppressed (the
28+
/// recipients still appear in the parsed list, but the SMTP delivery
29+
/// loop in `send_email` is skipped). The sender confirmation, if
30+
/// `confirm` is true, is sent regardless.
31+
pub notify_recipients: bool,
2732
pub is_api_key: bool,
2833
}
2934

0 commit comments

Comments
 (0)