From cc3b172e56602d42470591ecbe89892edaded9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 27 Jul 2026 18:24:42 +0200 Subject: [PATCH] normalize email from --- .env.example | 5 +++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- src/auth/mod.rs | 22 +++++++++++++--------- src/config.rs | 30 ++++++++++++++++++++++++++++-- src/state.rs | 8 ++++++++ 7 files changed, 57 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index 3fdffb1..9140781 100644 --- a/.env.example +++ b/.env.example @@ -70,8 +70,13 @@ ACCOUNT_CONFIRMATION_REQUIRED=false SHARE_CONFIRMATION_REQUIRED=true # smtp mailing +# port 465 → tls +# port 587 → starttls PUBLIC_URL=https://pad.example.com # SMTP_HOST=smtp.example.com +#SMTP_SECURITY=none +SMTP_SECURITY=starttls +#SMTP_SECURITY=tls SMTP_PORT=587 SMTP_USERNAME= SMTP_PASSWORD= diff --git a/Cargo.lock b/Cargo.lock index c979b1b..7088245 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2581,7 +2581,7 @@ dependencies = [ [[package]] name = "rustpad" -version = "0.1.8" +version = "0.1.9" dependencies = [ "argon2", "aws-config", diff --git a/Cargo.toml b/Cargo.toml index 4a893ba..f572cc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustpad" -version = "0.1.8" +version = "0.1.9" edition = "2024" rust-version = "1.94" description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL" diff --git a/README.md b/README.md index 8c3b82d..fe10e6c 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Migrations are stored in `migrations/sqlite`, `migrations/postgres`, and `migrat Nicknames can be used anonymously while they remain unregistered. Registering a nickname reserves it and requires a valid login session before it can be used in editor WebSocket connections. -Configure `PUBLIC_URL`, `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, and `SMTP_FROM` to enable password-reset emails. `SMTP_FROM` accepts both `RustPad ` and a value wrapped in one matching pair of single or double quotes, as may be passed literally by container env-file implementations. Reset links expire after 30 minutes and can be used only once. +Configure `PUBLIC_URL`, `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURITY`, `SMTP_USERNAME`, `SMTP_PASSWORD`, and `SMTP_FROM` to enable password-reset emails. `SMTP_FROM` accepts both `RustPad ` and a value wrapped in one matching pair of single or double quotes, as may be passed literally by container env-file implementations. `SMTP_SECURITY` accepts `none` (plain SMTP, typically an internal relay on port 25), `starttls`, or `tls` (implicit TLS, commonly port 465). When omitted, it defaults to `tls` for port 465 and `starttls` for other ports. Reset links expire after 30 minutes and can be used only once. ## Diagnostics and logging diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 1971ba9..462e0e8 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -25,7 +25,7 @@ use tracing::{debug, info, warn}; use crate::{ queries, - state::{SharedState, SmtpConfig}, + state::{SharedState, SmtpConfig, SmtpSecurity}, }; const MIN_PASSWORD: usize = 8; @@ -2098,15 +2098,19 @@ async fn send_share_invitation( } async fn send_message(smtp: &SmtpConfig, message: Message, label: &str) -> Result<(), AuthError> { - let mut builder = if smtp.port == 465 { - AsyncSmtpTransport::::relay(&smtp.host) - } else { - AsyncSmtpTransport::::starttls_relay(&smtp.host) + let mut builder = match smtp.security { + SmtpSecurity::None => AsyncSmtpTransport::::builder_dangerous(&smtp.host), + SmtpSecurity::StartTls => AsyncSmtpTransport::::starttls_relay(&smtp.host) + .map_err(|error| { + tracing::error!(error = %error, host = %smtp.host, port = smtp.port, security = ?smtp.security, "invalid SMTP configuration"); + AuthError::internal("Invalid SMTP configuration.") + })?, + SmtpSecurity::Tls => AsyncSmtpTransport::::relay(&smtp.host) + .map_err(|error| { + tracing::error!(error = %error, host = %smtp.host, port = smtp.port, security = ?smtp.security, "invalid SMTP configuration"); + AuthError::internal("Invalid SMTP configuration.") + })?, } - .map_err(|error| { - tracing::error!(error = %error, host = %smtp.host, port = smtp.port, "invalid SMTP configuration"); - AuthError::internal("Invalid SMTP configuration.") - })? .port(smtp.port); if !smtp.username.is_empty() { diff --git a/src/config.rs b/src/config.rs index e94fad6..c7f49c3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -121,6 +121,10 @@ impl Config { Some(crate::state::SmtpConfig { host, port: values.get("SMTP_PORT", "587").parse()?, + security: parse_smtp_security(&values.get( + "SMTP_SECURITY", + if values.get("SMTP_PORT", "587") == "465" { "tls" } else { "starttls" }, + ))?, username: values.get("SMTP_USERNAME", ""), password: values.get("SMTP_PASSWORD", ""), from: normalize_smtp_from( @@ -187,6 +191,15 @@ impl Config { +fn parse_smtp_security(value: &str) -> Result> { + match value.trim().to_ascii_lowercase().as_str() { + "none" | "plain" => Ok(crate::state::SmtpSecurity::None), + "starttls" => Ok(crate::state::SmtpSecurity::StartTls), + "tls" | "ssl" | "smtps" => Ok(crate::state::SmtpSecurity::Tls), + _ => Err("SMTP_SECURITY must be one of: none, starttls, tls".into()), + } +} + fn normalize_smtp_from(value: String) -> Result> { let trimmed = value.trim(); if trimmed.is_empty() { @@ -225,7 +238,7 @@ const KNOWN_CONFIG_KEYS: &[&str] = &[ "FRONTEND_LOG_LEVEL", "ANONYMOUS_ACCESS_TOKEN_TTL_DAYS", "USER_SESSION_TTL_DAYS", "UNCONFIRMED_ACCOUNT_TTL_DAYS", "AUTHORIZATION_TYPE", "S3_ENDPOINT", "S3_REGION", "S3_BUCKET", "S3_ACCESS_KEY", "S3_SECRET_KEY", - "S3_FORCE_PATH_STYLE", "SMTP_HOST", "SMTP_PORT", "SMTP_USERNAME", "SMTP_PASSWORD", + "S3_FORCE_PATH_STYLE", "SMTP_HOST", "SMTP_PORT", "SMTP_SECURITY", "SMTP_USERNAME", "SMTP_PASSWORD", "SMTP_FROM", "PUBLIC_URL", "LDAP_URL", "LDAP_STARTTLS", "LDAP_BIND_DN", "LDAP_BIND_PASSWORD", "LDAP_BASE_DN", "LDAP_USER_FILTER", "LDAP_USERNAME_ATTRIBUTE", "LDAP_EMAIL_ATTRIBUTE", "LDAP_DISPLAY_NAME_ATTRIBUTE", "LDAP_EXTERNAL_ID_ATTRIBUTE", @@ -387,7 +400,20 @@ fn parse_yaml_scalar(value: &str) -> Result { #[cfg(test)] mod tests { - use super::normalize_smtp_from; + use super::{normalize_smtp_from, parse_smtp_security}; + use crate::state::SmtpSecurity; + + #[test] + fn smtp_security_accepts_supported_modes() { + assert_eq!(parse_smtp_security("none").unwrap(), SmtpSecurity::None); + assert_eq!(parse_smtp_security("starttls").unwrap(), SmtpSecurity::StartTls); + assert_eq!(parse_smtp_security("tls").unwrap(), SmtpSecurity::Tls); + } + + #[test] + fn smtp_security_rejects_unknown_mode() { + assert!(parse_smtp_security("auto").is_err()); + } #[test] fn smtp_from_accepts_unquoted_value() { diff --git a/src/state.rs b/src/state.rs index 8bed166..2aa518c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -11,10 +11,18 @@ use tokio::sync::{RwLock, broadcast}; const CHANNEL_CAPACITY: usize = 256; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SmtpSecurity { + None, + StartTls, + Tls, +} + #[derive(Debug, Clone)] pub struct SmtpConfig { pub host: String, pub port: u16, + pub security: SmtpSecurity, pub username: String, pub password: String, pub from: String,