From a28ebcb401be9a2b0c052d1db319a0c5d6622a3d Mon Sep 17 00:00:00 2001 From: Jeremy Lin Date: Tue, 7 Jul 2020 21:30:18 -0700 Subject: [PATCH] Use local time in email notifications for new device logins In this implementation, the `TZ` environment variable must be set in order for the formatted output to use a more user-friendly time zone abbreviation (e.g., `UTC`). Otherwise, the output uses the time zone's UTC offset (e.g., `+00:00`). --- Cargo.lock | 24 ++++++++++++++++++++++-- Cargo.toml | 5 +++-- src/api/identity.rs | 8 +++++--- src/mail.rs | 27 ++++++++++++++++++++++----- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1fb64652..2dd25a33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,6 +132,7 @@ dependencies = [ "backtrace", "chashmap", "chrono", + "chrono-tz", "data-encoding", "data-url", "diesel", @@ -275,15 +276,25 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0fee792e164f78f5fe0c296cc2eb3688a2ca2b70cdff33040922d298203f0c4" +checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6" dependencies = [ "num-integer", "num-traits", "time 0.1.43", ] +[[package]] +name = "chrono-tz" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d96be7c3e993c9ee4356442db24ba364c924b6b8331866be6b6952bfe74b9d" +dependencies = [ + "chrono", + "parse-zoneinfo", +] + [[package]] name = "clap" version = "2.33.1" @@ -1618,6 +1629,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "pear" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index 51f9ae4f..4618b202 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,8 +62,9 @@ ring = "0.16.15" # UUID generation uuid = { version = "0.8.1", features = ["v4"] } -# Date and time librar for Rust -chrono = "0.4.12" +# Date and time libraries +chrono = "0.4.13" +chrono-tz = "0.5.2" time = "0.2.16" # TOTP library diff --git a/src/api/identity.rs b/src/api/identity.rs index 2aabfe95..a0cb8cde 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -1,4 +1,4 @@ -use chrono::Utc; +use chrono::Local; use num_traits::FromPrimitive; use rocket::request::{Form, FormItems, FromForm}; use rocket::Route; @@ -97,8 +97,10 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult ) } + let now = Local::now(); + if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() { - let now = Utc::now().naive_utc(); + let now = now.naive_utc(); if user.last_verifying_at.is_none() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() > CONFIG.signups_verify_resend_time() as i64 { let resend_limit = CONFIG.signups_verify_resend_limit() as i32; if resend_limit == 0 || user.login_verify_count < resend_limit { @@ -130,7 +132,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &ip, &conn)?; if CONFIG.mail_enabled() && new_device { - if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &device.updated_at, &device.name) { + if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name) { error!("Error sending new device email: {:#?}", e); if CONFIG.require_device_email() { diff --git a/src/mail.rs b/src/mail.rs index c660b674..8a6257f2 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -1,3 +1,4 @@ +use std::env; use std::str::FromStr; use lettre::message::{header, Mailbox, Message, MultiPart, SinglePart}; @@ -12,7 +13,9 @@ use crate::api::EmptyResult; use crate::auth::{encode_jwt, generate_delete_claims, generate_invite_claims, generate_verify_email_claims}; use crate::error::Error; use crate::CONFIG; -use chrono::NaiveDateTime; + +use chrono::{DateTime, Local}; +use chrono_tz::Tz; fn mailer() -> SmtpTransport { let host = CONFIG.smtp_host().unwrap(); @@ -87,6 +90,22 @@ fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String Ok((subject, body)) } +pub fn format_datetime(dt: &DateTime) -> String { + let fmt = "%A, %B %_d, %Y at %r %Z"; + + // With a DateTime, `%Z` formats as the time zone's UTC offset + // (e.g., `+00:00`). If the `TZ` environment variable is set, try to + // format as a time zone abbreviation instead (e.g., `UTC`). + if let Ok(tz) = env::var("TZ") { + if let Ok(tz) = tz.parse::() { + return dt.with_timezone(&tz).format(fmt).to_string(); + } + } + + // Otherwise, fall back to just displaying the UTC offset. + dt.format(fmt).to_string() +} + pub fn send_password_hint(address: &str, hint: Option) -> EmptyResult { let template_name = if hint.is_some() { "email/pw_hint_some" @@ -217,19 +236,17 @@ pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult { send_email(address, &subject, &body_html, &body_text) } -pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, device: &str) -> EmptyResult { +pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &DateTime, device: &str) -> EmptyResult { use crate::util::upcase_first; let device = upcase_first(device); - let datetime = dt.format("%A, %B %_d, %Y at %H:%M").to_string(); - let (subject, body_html, body_text) = get_text( "email/new_device_logged_in", json!({ "url": CONFIG.domain(), "ip": ip, "device": device, - "datetime": datetime, + "datetime": format_datetime(dt), }), )?;