Files
rustpad/src/config/smtp.rs
T
2026-08-05 09:53:28 +02:00

84 lines
2.6 KiB
Rust

/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use lettre::message::Mailbox;
use crate::state::{SmtpConfig, SmtpSecurity};
use super::values::ConfigValues;
pub(super) fn load_smtp(
values: &ConfigValues,
) -> Result<Option<SmtpConfig>, Box<dyn std::error::Error>> {
let Some(host) = values.optional("SMTP_HOST") else {
return Ok(None);
};
let port = values.get("SMTP_PORT", "587").parse()?;
let security = match values.optional("SMTP_SECURITY") {
Some(value) => parse_smtp_security(&value)?,
None => smtp_security_for_port(port),
};
Ok(Some(SmtpConfig {
host,
port,
security,
username: values.get("SMTP_USERNAME", ""),
password: values.get("SMTP_PASSWORD", ""),
from: normalize_smtp_from(values.required("SMTP_FROM", "SMTP_HOST is set")?)?,
public_url: values.required("PUBLIC_URL", "SMTP_HOST is set")?,
}))
}
fn smtp_security_for_port(port: u16) -> SmtpSecurity {
match port {
465 => SmtpSecurity::Tls,
587 => SmtpSecurity::StartTls,
_ => SmtpSecurity::None,
}
}
fn parse_smtp_security(value: &str) -> Result<SmtpSecurity, Box<dyn std::error::Error>> {
match value.trim().to_ascii_lowercase().as_str() {
"none" | "plain" => Ok(SmtpSecurity::None),
"starttls" => Ok(SmtpSecurity::StartTls),
"tls" | "ssl" | "smtps" => Ok(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>> {
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())
}
#[cfg(test)]
#[path = "../tests/config_smtp.rs"]
mod tests;