reuse email validation

This commit is contained in:
Brandon Thomas committed 2026-01-31 10:51:49 -05:00
1 parent 654bd160ef
commit d1ada07bf3
9 files changed
+100 -91

No files matched your search

@@ -1,34 +0,0 @@
use md5::{Digest, Md5};
pub fn email_to_gravatar(email_address: &str) -> String {
let email = email_address.trim().to_lowercase();
let mut hasher = Md5::new();
hasher.update(email);
let hash = hasher.finalize();
let gravatar_hash = format!("{:x}", hash);
gravatar_hash
}
#[cfg(test)]
pub mod tests {
use crate::email::email_to_gravatar::email_to_gravatar;
#[test]
fn test_gravatar() {
// Email 1
assert_eq!(email_to_gravatar("example@example.com"), "23463b99b62a72f26ed677cc556c44e8".to_string());
assert_eq!(email_to_gravatar("EXAMPLE@EXAMPLE.COM"), "23463b99b62a72f26ed677cc556c44e8".to_string());
assert_eq!(email_to_gravatar(" example@example.com \n "), "23463b99b62a72f26ed677cc556c44e8".to_string());
// Email 2
assert_eq!(email_to_gravatar("echelon@gmail.com"), "37e7e18f19d8f654a1ace405c846a9de".to_string());
assert_eq!(email_to_gravatar("ECHELON@GMAIL.COM"), "37e7e18f19d8f654a1ace405c846a9de".to_string());
assert_eq!(email_to_gravatar(" ECHELON@GMAIL.COM "), "37e7e18f19d8f654a1ace405c846a9de".to_string());
// Misc
assert_eq!(email_to_gravatar(""), "d41d8cd98f00b204e9800998ecf8427e".to_string());
assert_eq!(email_to_gravatar(" "), "d41d8cd98f00b204e9800998ecf8427e".to_string());
}
}
@@ -0,0 +1,34 @@
use md5::{Digest, Md5};
pub fn email_to_gravatar_hash(email_address: &str) -> String {
let email = email_address.trim().to_lowercase();
let mut hasher = Md5::new();
hasher.update(email);
let hash = hasher.finalize();
let gravatar_hash = format!("{:x}", hash);
gravatar_hash
}
#[cfg(test)]
pub mod tests {
use crate::email::email_to_gravatar_hash::email_to_gravatar_hash;
#[test]
fn test_gravatar() {
// Email 1
assert_eq!(email_to_gravatar_hash("example@example.com"), "23463b99b62a72f26ed677cc556c44e8".to_string());
assert_eq!(email_to_gravatar_hash("EXAMPLE@EXAMPLE.COM"), "23463b99b62a72f26ed677cc556c44e8".to_string());
assert_eq!(email_to_gravatar_hash(" example@example.com \n "), "23463b99b62a72f26ed677cc556c44e8".to_string());
// Email 2
assert_eq!(email_to_gravatar_hash("echelon@gmail.com"), "37e7e18f19d8f654a1ace405c846a9de".to_string());
assert_eq!(email_to_gravatar_hash("ECHELON@GMAIL.COM"), "37e7e18f19d8f654a1ace405c846a9de".to_string());
assert_eq!(email_to_gravatar_hash(" ECHELON@GMAIL.COM "), "37e7e18f19d8f654a1ace405c846a9de".to_string());
// Misc
assert_eq!(email_to_gravatar_hash(""), "d41d8cd98f00b204e9800998ecf8427e".to_string());
assert_eq!(email_to_gravatar_hash(" "), "d41d8cd98f00b204e9800998ecf8427e".to_string());
}
}
+2 -2
View File
@@ -1,3 +1,3 @@
pub mod email_to_gravatar;
pub mod email_to_gravatar_hash;
pub mod generate_random_synthetic_email;
pub mod validate_email_address;
pub mod validate_email_address_format;
Whitespace-only changes.
@@ -0,0 +1,55 @@
/// Basic email format validation
pub fn validate_email_address_format(email: &str) -> Result<(), String> {
let email = email.trim();
if email.is_empty() {
return Err("email address cannot be empty".to_string());
}
if email.len() > 255 {
return Err("email address is too long".to_string());
}
// Basic format check: must contain exactly one @ with content on both sides
let at_count = email.matches('@').count();
if at_count != 1 {
return Err("email address must contain exactly one @".to_string());
}
let parts: Vec<&str> = email.split('@').collect();
if parts.len() != 2 {
return Err("invalid email format".to_string());
}
let local = parts[0];
let domain = parts[1];
if local.is_empty() {
return Err("email address must have content before @".to_string());
}
if domain.is_empty() {
return Err("email address must have a domain after @".to_string());
}
// Domain must contain at least one dot
if !domain.contains('.') {
return Err("email domain must contain a dot".to_string());
}
// Domain cannot start or end with a dot
if domain.starts_with('.') || domain.ends_with('.') {
return Err("email domain cannot start or end with a dot".to_string());
}
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn test_invalid_empty() {
}
}
@@ -18,7 +18,7 @@ use sqlx::{Acquire, MySql};
use std::collections::HashMap;
use stripe_checkout::checkout_session::{CreateCheckoutSession, CreateCheckoutSessionAutomaticTax, CreateCheckoutSessionLineItems, CreateCheckoutSessionSavedPaymentMethodOptions, CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters, CreateCheckoutSessionSavedPaymentMethodOptionsPaymentMethodSave, CreateCheckoutSessionSubscriptionData};
use stripe_shared::{CheckoutSession, CheckoutSessionMode, PriceId};
use users::email::email_to_gravatar::email_to_gravatar;
use users::email::email_to_gravatar_hash::email_to_gravatar_hash;
use users::email::generate_random_synthetic_email::generate_random_synthetic_email;
use users::username::generate_random_username::generate_random_username;
@@ -57,7 +57,7 @@ pub (super) async fn user_creation_case(
// NB: We try a few times to make sure we don't hit an email/username collision.
let user_email_address = generate_random_synthetic_email();
let user_email_gravatar_hash = email_to_gravatar(&user_email_address);
let user_email_gravatar_hash = email_to_gravatar_hash(&user_email_address);
let display_name = generate_random_username();
let username = display_name.trim().to_lowercase();
@@ -28,7 +28,7 @@ use password::bcrypt_hash_password::bcrypt_hash_password;
use sqlx::MySqlPool;
use tokens::tokens::user_sessions::UserSessionToken;
use user_input_common::check_for_slurs::contains_slurs;
use users::email::email_to_gravatar::email_to_gravatar;
use users::email::email_to_gravatar_hash::email_to_gravatar_hash;
use utoipa::ToSchema;
#[derive(ToSchema, Deserialize)]
@@ -185,7 +185,7 @@ pub async fn create_account_handler(
let email_address = request.email_address.trim().to_lowercase();
let email_gravatar_hash = email_to_gravatar(&email_address);
let email_gravatar_hash = email_to_gravatar_hash(&email_address);
let ip_address = get_request_ip(&http_request);
@@ -16,11 +16,11 @@ use artcraft_api_defs::users::edit_email::{EditEmailRequest, EditEmailResponse};
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::state::server_state::ServerState;
use hashing::md5::email_to_gravatar_hash::email_to_gravatar_hash;
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::queries::users::user::update::update_email::{update_email, UpdateEmailArgs, UpdateEmailError};
use mysql_queries::utils::transactor::Transactor;
use users::email::email_to_gravatar_hash::email_to_gravatar_hash;
use users::email::validate_email_address_format::validate_email_address_format;
#[derive(Debug, ToSchema)]
pub enum EditEmailError {
@@ -56,52 +56,6 @@ impl fmt::Display for EditEmailError {
}
}
/// Basic email format validation
fn validate_email(email: &str) -> Result<(), String> {
let email = email.trim();
if email.is_empty() {
return Err("email address cannot be empty".to_string());
}
if email.len() > 255 {
return Err("email address is too long".to_string());
}
// Basic format check: must contain exactly one @ with content on both sides
let at_count = email.matches('@').count();
if at_count != 1 {
return Err("email address must contain exactly one @".to_string());
}
let parts: Vec<&str> = email.split('@').collect();
if parts.len() != 2 {
return Err("invalid email format".to_string());
}
let local = parts[0];
let domain = parts[1];
if local.is_empty() {
return Err("email address must have content before @".to_string());
}
if domain.is_empty() {
return Err("email address must have a domain after @".to_string());
}
// Domain must contain at least one dot
if !domain.contains('.') {
return Err("email domain must contain a dot".to_string());
}
// Domain cannot start or end with a dot
if domain.starts_with('.') || domain.ends_with('.') {
return Err("email domain cannot start or end with a dot".to_string());
}
Ok(())
}
/// Edit email address of the current user.
#[utoipa::path(
@@ -126,7 +80,7 @@ pub async fn edit_email_handler(
{
let email_address = request.email_address.trim().to_lowercase();
if let Err(reason) = validate_email(&email_address) {
if let Err(reason) = validate_email_address_format(&email_address) {
return Err(EditEmailError::BadInput(format!("bad email: {}", &reason)));
}
@@ -13,7 +13,7 @@ use mysql_queries::queries::users::user::create::create_account_from_google_sso:
use mysql_queries::utils::transactor::Transactor;
use sqlx::pool::PoolConnection;
use sqlx::{Acquire, MySql};
use users::email::email_to_gravatar::email_to_gravatar;
use users::email::email_to_gravatar_hash::email_to_gravatar_hash;
use users::username::generate_random_username::generate_random_username;
pub struct CreateArgs<'a> {
@@ -40,7 +40,7 @@ pub async fn handle_new_sso_account_for_new_user(
let user_feature_flags = studio_feature_flags();
let ip_address = get_request_ip(&args.http_request);
let user_email_gravatar_hash = email_to_gravatar(&args.user_email_address);
let user_email_gravatar_hash = email_to_gravatar_hash(&args.user_email_address);
let mut maybe_source = get_request_signup_source_enum(&args.http_request);