normalize email from
This commit is contained in:
@@ -70,8 +70,13 @@ ACCOUNT_CONFIRMATION_REQUIRED=false
|
|||||||
SHARE_CONFIRMATION_REQUIRED=true
|
SHARE_CONFIRMATION_REQUIRED=true
|
||||||
|
|
||||||
# smtp mailing
|
# smtp mailing
|
||||||
|
# port 465 → tls
|
||||||
|
# port 587 → starttls
|
||||||
PUBLIC_URL=https://pad.example.com
|
PUBLIC_URL=https://pad.example.com
|
||||||
# SMTP_HOST=smtp.example.com
|
# SMTP_HOST=smtp.example.com
|
||||||
|
#SMTP_SECURITY=none
|
||||||
|
SMTP_SECURITY=starttls
|
||||||
|
#SMTP_SECURITY=tls
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_USERNAME=
|
SMTP_USERNAME=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
|
|||||||
Generated
+1
-1
@@ -2581,7 +2581,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.1.8"
|
version = "0.1.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"aws-config",
|
"aws-config",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.1.8"
|
version = "0.1.9"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.94"
|
rust-version = "1.94"
|
||||||
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
||||||
|
|||||||
@@ -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.
|
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 <no-reply@example.com>` 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 <no-reply@example.com>` 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
|
## Diagnostics and logging
|
||||||
|
|
||||||
|
|||||||
+13
-9
@@ -25,7 +25,7 @@ use tracing::{debug, info, warn};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
queries,
|
queries,
|
||||||
state::{SharedState, SmtpConfig},
|
state::{SharedState, SmtpConfig, SmtpSecurity},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MIN_PASSWORD: usize = 8;
|
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> {
|
async fn send_message(smtp: &SmtpConfig, message: Message, label: &str) -> Result<(), AuthError> {
|
||||||
let mut builder = if smtp.port == 465 {
|
let mut builder = match smtp.security {
|
||||||
AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.host)
|
SmtpSecurity::None => AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&smtp.host),
|
||||||
} else {
|
SmtpSecurity::StartTls => AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&smtp.host)
|
||||||
AsyncSmtpTransport::<Tokio1Executor>::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::<Tokio1Executor>::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);
|
.port(smtp.port);
|
||||||
|
|
||||||
if !smtp.username.is_empty() {
|
if !smtp.username.is_empty() {
|
||||||
|
|||||||
+28
-2
@@ -121,6 +121,10 @@ impl Config {
|
|||||||
Some(crate::state::SmtpConfig {
|
Some(crate::state::SmtpConfig {
|
||||||
host,
|
host,
|
||||||
port: values.get("SMTP_PORT", "587").parse()?,
|
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", ""),
|
username: values.get("SMTP_USERNAME", ""),
|
||||||
password: values.get("SMTP_PASSWORD", ""),
|
password: values.get("SMTP_PASSWORD", ""),
|
||||||
from: normalize_smtp_from(
|
from: normalize_smtp_from(
|
||||||
@@ -187,6 +191,15 @@ impl Config {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fn parse_smtp_security(value: &str) -> Result<crate::state::SmtpSecurity, Box<dyn std::error::Error>> {
|
||||||
|
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<String, Box<dyn std::error::Error>> {
|
fn normalize_smtp_from(value: String) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
let trimmed = value.trim();
|
let trimmed = value.trim();
|
||||||
if trimmed.is_empty() {
|
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",
|
"FRONTEND_LOG_LEVEL", "ANONYMOUS_ACCESS_TOKEN_TTL_DAYS", "USER_SESSION_TTL_DAYS",
|
||||||
"UNCONFIRMED_ACCOUNT_TTL_DAYS", "AUTHORIZATION_TYPE",
|
"UNCONFIRMED_ACCOUNT_TTL_DAYS", "AUTHORIZATION_TYPE",
|
||||||
"S3_ENDPOINT", "S3_REGION", "S3_BUCKET", "S3_ACCESS_KEY", "S3_SECRET_KEY",
|
"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",
|
"SMTP_FROM", "PUBLIC_URL", "LDAP_URL", "LDAP_STARTTLS", "LDAP_BIND_DN",
|
||||||
"LDAP_BIND_PASSWORD", "LDAP_BASE_DN", "LDAP_USER_FILTER", "LDAP_USERNAME_ATTRIBUTE",
|
"LDAP_BIND_PASSWORD", "LDAP_BASE_DN", "LDAP_USER_FILTER", "LDAP_USERNAME_ATTRIBUTE",
|
||||||
"LDAP_EMAIL_ATTRIBUTE", "LDAP_DISPLAY_NAME_ATTRIBUTE", "LDAP_EXTERNAL_ID_ATTRIBUTE",
|
"LDAP_EMAIL_ATTRIBUTE", "LDAP_DISPLAY_NAME_ATTRIBUTE", "LDAP_EXTERNAL_ID_ATTRIBUTE",
|
||||||
@@ -387,7 +400,20 @@ fn parse_yaml_scalar(value: &str) -> Result<String, String> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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]
|
#[test]
|
||||||
fn smtp_from_accepts_unquoted_value() {
|
fn smtp_from_accepts_unquoted_value() {
|
||||||
|
|||||||
@@ -11,10 +11,18 @@ use tokio::sync::{RwLock, broadcast};
|
|||||||
|
|
||||||
const CHANNEL_CAPACITY: usize = 256;
|
const CHANNEL_CAPACITY: usize = 256;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SmtpSecurity {
|
||||||
|
None,
|
||||||
|
StartTls,
|
||||||
|
Tls,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SmtpConfig {
|
pub struct SmtpConfig {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
pub security: SmtpSecurity,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub from: String,
|
pub from: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user