From 9a71103fe79c435cfa79dc7940c3f8d7c5c34c6c Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Tue, 16 Jul 2019 00:42:07 +0200 Subject: [PATCH] hkp: welcome email on upload of previously unknown key --- dist/templates/email/welcome-html.hbs | 27 ++++++++++++++ dist/templates/email/welcome-txt.hbs | 16 ++++++++ src/mail.rs | 26 +++++++++++++ src/web/hkp.rs | 54 +++++++++++++++++++++++---- src/web/mod.rs | 4 ++ src/web/vks.rs | 35 +++++++++++++---- src/web/vks_web.rs | 2 +- 7 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 dist/templates/email/welcome-html.hbs create mode 100644 dist/templates/email/welcome-txt.hbs diff --git a/dist/templates/email/welcome-html.hbs b/dist/templates/email/welcome-html.hbs new file mode 100644 index 0000000..175d6b3 --- /dev/null +++ b/dist/templates/email/welcome-html.hbs @@ -0,0 +1,27 @@ + + + + + Your key upload on {{domain}} + + +

+ Hi, +

+ this is an automated message from {{domain}}. If you didn't + upload your key there, please ignore it. +

+ OpenPGP key: {{primary_fp}} +

+ This key was just uploaded for the first time, and is now published without + identity information. If you want to allow others to find this key by e-mail + address, please follow this link: +

+ {{uri}} +

+ You can find more info at {{domain}}/about. +

+ Greetings from the keys.openpgp.org team + + + diff --git a/dist/templates/email/welcome-txt.hbs b/dist/templates/email/welcome-txt.hbs new file mode 100644 index 0000000..64a9295 --- /dev/null +++ b/dist/templates/email/welcome-txt.hbs @@ -0,0 +1,16 @@ +Hi, + +this is an automated message from {{domain}}. If you didn't upload your key +there, please ignore it. + +OpenPGP key: {{primary_fp}} + +This key was just uploaded for the first time, and is now published without +identity information. If you want to allow others to find this key by e-mail +address, please follow this link: + + {{uri}} + +You can find more info at {{base_uri}}/about + +Greetings from the keys.openpgp.org team diff --git a/src/mail.rs b/src/mail.rs index 125dde6..3b0a4f8 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -28,6 +28,14 @@ mod context { pub base_uri: String, pub domain: String, } + + #[derive(Serialize, Clone)] + pub struct Welcome { + pub primary_fp: String, + pub uri: String, + pub base_uri: String, + pub domain: String, + } } pub struct Service { @@ -107,6 +115,24 @@ impl Service { ) } + pub fn send_welcome(&self, base_uri: &str, tpk_name: String, userid: &Email, + token: &str) + -> Result<()> { + let ctx = context::Welcome { + primary_fp: tpk_name, + uri: format!("{}/upload/{}", base_uri, token), + base_uri: base_uri.to_owned(), + domain: self.domain.clone(), + }; + + self.send( + &vec![userid], + &format!("Your key upload on {}", self.domain), + "welcome", + ctx, + ) + } + fn send(&self, to: &[&Email], subject: &str, template: &str, ctx: T) -> Result<()> where T: Serialize + Clone, diff --git a/src/web/hkp.rs b/src/web/hkp.rs index bcbb434..6ffefeb 100644 --- a/src/web/hkp.rs +++ b/src/web/hkp.rs @@ -14,7 +14,9 @@ use rate_limiter::RateLimiter; use tokens; use web; +use mail; use web::{HagridState, RequestOrigin, MyResponse, vks_web}; +use web::vks::response::UploadResponse; #[derive(Debug)] pub enum Hkp { @@ -128,18 +130,41 @@ pub fn pks_add_form( db: rocket::State, tokens_stateless: rocket::State, rate_limiter: rocket::State, - cont_type: &ContentType, + mail_service: rocket::State, data: Data, ) -> MyResponse { - match vks_web::process_post_form_data(db, tokens_stateless, rate_limiter, cont_type, data) { + match vks_web::process_post_form(db, tokens_stateless, rate_limiter, data) { + Ok(UploadResponse::Ok { is_new_key, key_fpr, primary_uid, token, .. }) => { + let msg = if is_new_key && send_welcome_mail(&request_origin, &mail_service, key_fpr, primary_uid, token) { + "Upload successful. This is a new key, a welcome mail has been sent!".to_owned() + } else { + format!("Upload successful. Note that identity information will only be published after verification! see {}/about/usage#gnupg-upload", request_origin.get_base_uri()) + }; + MyResponse::plain(msg) + } Ok(_) => { - let msg = format!("Upload successful. Note that identity information will only be published with verification! see {}/about/usage#gnupg-upload", request_origin.get_base_uri()); + let msg = format!("Upload successful. Note that identity information will only be published after verification! see {}/about/usage#gnupg-upload", request_origin.get_base_uri()); MyResponse::plain(msg) } Err(err) => MyResponse::ise(err), } } +fn send_welcome_mail( + request_origin: &RequestOrigin, + mail_service: &mail::Service, + fpr: String, + primary_uid: Option, + token: String, +) -> bool { + if let Some(primary_uid) = primary_uid { + mail_service.send_welcome( + request_origin.get_base_uri(), fpr, &primary_uid, &token).is_ok() + } else { + false + } +} + #[get("/pks/lookup")] pub fn pks_lookup(state: rocket::State, db: rocket::State, @@ -307,9 +332,9 @@ mod tests { let body = response.body_string().unwrap(); eprintln!("response: {}", body); - // Check that we do not get a confirmation mail. - let confirm_mail = pop_mail(filemail_into.as_path()).unwrap(); - assert!(confirm_mail.is_none()); + // Check that we get a welcome mail + let welcome_mail = pop_mail(filemail_into.as_path()).unwrap(); + assert!(welcome_mail.is_some()); // We should not be able to look it up by email address. check_null_responses_by_email(&client, "foo@invalid.example.com"); @@ -321,6 +346,16 @@ mod tests { // And check that we can see the human-readable result page. check_hr_responses_by_fingerprint(&client, &tpk, 0); + // Upload the same key again, make sure the welcome mail is not sent again + let mut response = client.post("/pks/add") + .body(post_data.as_bytes()) + .header(ContentType::Form) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + + let welcome_mail = pop_mail(filemail_into.as_path()).unwrap(); + assert!(welcome_mail.is_none()); + assert_consistency(client.rocket()); } @@ -357,8 +392,11 @@ mod tests { .header(ContentType::Form) .dispatch(); assert_eq!(response.status(), Status::Ok); - let confirm_mail = pop_mail(filemail_into.as_path()).unwrap(); - assert!(confirm_mail.is_none()); + + // Check that there is no welcome mail (since we uploaded two) + let welcome_mail = pop_mail(filemail_into.as_path()).unwrap(); + assert!(welcome_mail.is_none()); + check_mr_responses_by_fingerprint(&client, &tpk_0, 0); check_mr_responses_by_fingerprint(&client, &tpk_1, 0); check_hr_responses_by_fingerprint(&client, &tpk_0, 0); diff --git a/src/web/mod.rs b/src/web/mod.rs index 2d0ee72..86b45f9 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -455,12 +455,16 @@ fn configure_mail_service(config: &Config) -> Result { let verify_txt = template_dir.join("email/publish-txt.hbs"); let manage_html = template_dir.join("email/manage-html.hbs"); let manage_txt = template_dir.join("email/manage-txt.hbs"); + let welcome_html = template_dir.join("email/welcome-html.hbs"); + let welcome_txt = template_dir.join("email/welcome-txt.hbs"); let mut handlebars = Handlebars::new(); handlebars.register_template_file("verify-html", verify_html)?; handlebars.register_template_file("verify-txt", verify_txt)?; handlebars.register_template_file("manage-html", manage_html)?; handlebars.register_template_file("manage-txt", manage_txt)?; + handlebars.register_template_file("welcome-html", welcome_html)?; + handlebars.register_template_file("welcome-txt", welcome_txt)?; let filemail_into = config.get_str("filemail_into") .ok().map(|p| PathBuf::from(p)); diff --git a/src/web/vks.rs b/src/web/vks.rs index f670b8c..0825cfb 100644 --- a/src/web/vks.rs +++ b/src/web/vks.rs @@ -1,6 +1,6 @@ use failure::Fallible as Result; -use database::{Database, KeyDatabase, StatefulTokens, EmailAddressStatus, TpkStatus}; +use database::{Database, KeyDatabase, StatefulTokens, EmailAddressStatus, TpkStatus, ImportResult}; use database::types::{Fingerprint,Email}; use mail; use tokens::{self, StatelessSerializable}; @@ -29,6 +29,8 @@ pub mod request { } pub mod response { + use database::types::Email; + #[derive(Debug,Serialize,Deserialize,PartialEq,Eq)] pub enum EmailStatus { #[serde(rename = "unpublished")] @@ -50,6 +52,8 @@ pub mod response { is_revoked: bool, status: HashMap, count_unparsed: usize, + is_new_key: bool, + primary_uid: Option, }, OkMulti { key_fprs: Vec }, Error(String), @@ -144,8 +148,10 @@ fn process_key_single( ) -> response::UploadResponse { let fp = Fingerprint::try_from(tpk.fingerprint()).unwrap(); - let tpk_status = match db.merge(tpk) { - Ok(import_result) => import_result.into_tpk_status(), + let (tpk_status, is_new_key) = match db.merge(tpk) { + Ok(ImportResult::New(tpk_status)) => (tpk_status, true), + Ok(ImportResult::Updated(tpk_status)) => (tpk_status, false), + Ok(ImportResult::Unchanged(tpk_status)) => (tpk_status, false), Err(_) => return UploadResponse::err(&format!( "Something went wrong processing key {}", fp)), }; @@ -163,7 +169,7 @@ fn process_key_single( let token = tokens_stateless.create(&verify_state); - show_upload_verify(rate_limiter, token, tpk_status, verify_state) + show_upload_verify(rate_limiter, token, tpk_status, verify_state, is_new_key) } pub fn request_verify( @@ -183,7 +189,7 @@ pub fn request_verify( if tpk_status.is_revoked { return show_upload_verify( - &rate_limiter, token, tpk_status, verify_state); + &rate_limiter, token, tpk_status, verify_state, false); } let emails_requested: Vec<_> = addresses.into_iter() @@ -205,7 +211,7 @@ pub fn request_verify( } } - show_upload_verify(&rate_limiter, token, tpk_status, verify_state) + show_upload_verify(&rate_limiter, token, tpk_status, verify_state, false) } fn check_tpk_state( @@ -270,10 +276,19 @@ fn show_upload_verify( token: String, tpk_status: TpkStatus, verify_state: VerifyTpkState, + is_new_key: bool, ) -> response::UploadResponse { let key_fpr = verify_state.fpr.to_string(); if tpk_status.is_revoked { - return response::UploadResponse::Ok { token, key_fpr, count_unparsed: 0, is_revoked: true, status: HashMap::new() }; + return response::UploadResponse::Ok { + token, + key_fpr, + count_unparsed: 0, + is_revoked: true, + status: HashMap::new(), + is_new_key: false, + primary_uid: None, + }; } let status: HashMap<_,_> = tpk_status.email_status @@ -292,8 +307,12 @@ fn show_upload_verify( } }) .collect(); + let primary_uid = tpk_status.email_status + .get(0) + .map(|(email, _)| email) + .cloned(); let count_unparsed = tpk_status.unparsed_uids; - response::UploadResponse::Ok { token, key_fpr, count_unparsed, is_revoked: false, status } + response::UploadResponse::Ok { token, key_fpr, count_unparsed, is_revoked: false, status, is_new_key, primary_uid } } diff --git a/src/web/vks_web.rs b/src/web/vks_web.rs index f28a72e..18f75ee 100644 --- a/src/web/vks_web.rs +++ b/src/web/vks_web.rs @@ -120,7 +120,7 @@ impl MyResponse { fn upload_response(response: UploadResponse) -> Self { match response { - UploadResponse::Ok { token, key_fpr, is_revoked, count_unparsed, status } => + UploadResponse::Ok { token, key_fpr, is_revoked, count_unparsed, status, .. } => Self::upload_ok(token, key_fpr, is_revoked, count_unparsed, status), UploadResponse::OkMulti { key_fprs } => Self::upload_ok_multi(key_fprs),