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<Local>) -> String {
+    let fmt = "%A, %B %_d, %Y at %r %Z";
+
+    // With a DateTime<Local>, `%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::<Tz>() {
+            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<String>) -> 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<Local>, 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),
         }),
     )?;