normalize email from
This commit is contained in:
@@ -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. Reset links expire after 30 minutes and can be used only once.
|
||||
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.
|
||||
|
||||
## Diagnostics and logging
|
||||
|
||||
|
||||
+86
-1
@@ -1,3 +1,4 @@
|
||||
use lettre::message::Mailbox;
|
||||
use std::{collections::HashMap, env, net::IpAddr, path::Path};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -122,7 +123,9 @@ impl Config {
|
||||
port: values.get("SMTP_PORT", "587").parse()?,
|
||||
username: values.get("SMTP_USERNAME", ""),
|
||||
password: values.get("SMTP_PASSWORD", ""),
|
||||
from: values.required("SMTP_FROM", "SMTP_HOST is set")?,
|
||||
from: normalize_smtp_from(
|
||||
values.required("SMTP_FROM", "SMTP_HOST is set")?,
|
||||
)?,
|
||||
public_url: values.required("PUBLIC_URL", "SMTP_HOST is set")?,
|
||||
})
|
||||
} else {
|
||||
@@ -171,6 +174,9 @@ impl Config {
|
||||
return Err("STATIC_DIR and FILES_DIR cannot be empty".into());
|
||||
}
|
||||
if let Some(smtp) = &self.smtp {
|
||||
smtp.from
|
||||
.parse::<Mailbox>()
|
||||
.map_err(|error| format!("SMTP_FROM is not a valid mailbox: {error}"))?;
|
||||
if !(smtp.public_url.starts_with("http://") || smtp.public_url.starts_with("https://")) {
|
||||
return Err("PUBLIC_URL must start with http:// or https://".into());
|
||||
}
|
||||
@@ -180,6 +186,37 @@ impl Config {
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn normalize_smtp_from(value: String) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("SMTP_FROM cannot be empty".into());
|
||||
}
|
||||
|
||||
if trimmed.parse::<Mailbox>().is_ok() {
|
||||
return Ok(trimmed.to_owned());
|
||||
}
|
||||
|
||||
let unquoted = match (trimmed.as_bytes().first(), trimmed.as_bytes().last()) {
|
||||
(Some(b'"'), Some(b'"')) | (Some(b'\''), Some(b'\'')) if trimmed.len() >= 2 => {
|
||||
trimmed[1..trimmed.len() - 1].trim()
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("SMTP_FROM is not a valid mailbox: {trimmed}").into());
|
||||
}
|
||||
};
|
||||
|
||||
if unquoted.is_empty() {
|
||||
return Err("SMTP_FROM cannot be empty".into());
|
||||
}
|
||||
|
||||
unquoted
|
||||
.parse::<Mailbox>()
|
||||
.map_err(|error| format!("SMTP_FROM is not a valid mailbox: {error}"))?;
|
||||
|
||||
Ok(unquoted.to_owned())
|
||||
}
|
||||
|
||||
const KNOWN_CONFIG_KEYS: &[&str] = &[
|
||||
"APP_HOST", "APP_PORT", "DATABASE_URL", "DATABASE_MAX_CONNECTIONS",
|
||||
"STATIC_DIR", "FILES_DIR", "STORAGE_DRIVER", "UPLOAD_MAX_SIZE_MB",
|
||||
@@ -346,3 +383,51 @@ fn parse_yaml_scalar(value: &str) -> Result<String, String> {
|
||||
}
|
||||
Ok(value.to_owned())
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::normalize_smtp_from;
|
||||
|
||||
#[test]
|
||||
fn smtp_from_accepts_unquoted_value() {
|
||||
assert_eq!(
|
||||
normalize_smtp_from("RustPad <rustpad@notes.example>".to_owned()).unwrap(),
|
||||
"RustPad <rustpad@notes.example>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smtp_from_removes_matching_double_quotes() {
|
||||
assert_eq!(
|
||||
normalize_smtp_from(" \"RustPad <rustpad@notes.example>\" ".to_owned()).unwrap(),
|
||||
"RustPad <rustpad@notes.example>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smtp_from_removes_matching_single_quotes() {
|
||||
assert_eq!(
|
||||
normalize_smtp_from(" 'RustPad <rustpad@notes.example>' ".to_owned()).unwrap(),
|
||||
"RustPad <rustpad@notes.example>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smtp_from_preserves_valid_quoted_display_name() {
|
||||
assert_eq!(
|
||||
normalize_smtp_from("\"Rust, Pad\" <rustpad@notes.example>".to_owned()).unwrap(),
|
||||
"\"Rust, Pad\" <rustpad@notes.example>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smtp_from_rejects_unmatched_quotes() {
|
||||
assert!(normalize_smtp_from("\"RustPad <rustpad@notes.example>".to_owned()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smtp_from_rejects_invalid_mailbox() {
|
||||
assert!(normalize_smtp_from("RustPad".to_owned()).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user