Use connection url to configure email (fixes #5472) (#5476)

* Use connection url for configure email (fixes #5472)

* clippy
This commit is contained in:
Nutomic 2025-03-04 18:50:19 +00:00 committed by GitHub
parent a6507c169d
commit 1e02dea397
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 19 additions and 63 deletions

View file

@ -70,16 +70,10 @@
} }
# Email sending configuration. All options except login/password are mandatory # Email sending configuration. All options except login/password are mandatory
email: { email: {
# Hostname and port of the smtp server # https://docs.rs/lettre/0.11.14/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url
smtp_server: "localhost:25" connection: "smtps://user:pass@hostname:port"
# Login name for smtp server
smtp_login: "string"
# Password to login to the smtp server
smtp_password: "string"
# Address to send emails from, eg "noreply@your-instance.com" # Address to send emails from, eg "noreply@your-instance.com"
smtp_from_address: "noreply@example.com" smtp_from_address: "noreply@example.com"
# Whether or not smtp connections should use tls. Can be none, tls, or starttls
tls_type: "none"
} }
# Parameters for automatic configuration of new instance (only used at first start) # Parameters for automatic configuration of new instance (only used at first start)
setup: { setup: {

View file

@ -79,6 +79,7 @@ lettre = { version = "0.11.12", default-features = false, features = [
"builder", "builder",
"smtp-transport", "smtp-transport",
"tokio1-rustls-tls", "tokio1-rustls-tls",
"pool",
], optional = true } ], optional = true }
markdown-it = { version = "0.6.1", optional = true } markdown-it = { version = "0.6.1", optional = true }
ts-rs = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true }

View file

@ -5,13 +5,13 @@ use crate::{
use html2text; use html2text;
use lettre::{ use lettre::{
message::{Mailbox, MultiPart}, message::{Mailbox, MultiPart},
transport::smtp::{authentication::Credentials, extension::ClientId}, transport::smtp::extension::ClientId,
Address, Address,
AsyncTransport, AsyncTransport,
Message, Message,
}; };
use rosetta_i18n::{Language, LanguageId}; use rosetta_i18n::{Language, LanguageId};
use std::str::FromStr; use std::{str::FromStr, sync::OnceLock};
use translations::Lang; use translations::Lang;
use uuid::Uuid; use uuid::Uuid;
@ -28,21 +28,16 @@ pub async fn send_email(
html: &str, html: &str,
settings: &Settings, settings: &Settings,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
static MAILER: OnceLock<AsyncSmtpTransport> = OnceLock::new();
let email_config = settings.email.clone().ok_or(LemmyErrorType::NoEmailSetup)?; let email_config = settings.email.clone().ok_or(LemmyErrorType::NoEmailSetup)?;
let domain = settings.hostname.clone();
let (smtp_server, smtp_port) = { #[expect(clippy::expect_used)]
let email_and_port = email_config.smtp_server.split(':').collect::<Vec<&str>>(); let mailer = MAILER.get_or_init(|| {
let email = *email_and_port AsyncSmtpTransport::from_url(&email_config.connection)
.first() .expect("init email transport")
.ok_or(LemmyErrorType::EmailRequired)?; .hello_name(ClientId::Domain(settings.hostname.clone()))
let port = email_and_port .build()
.get(1) });
.ok_or(LemmyErrorType::EmailSmtpServerNeedsAPort)?
.parse::<u16>()?;
(email, port)
};
// use usize::MAX as the line wrap length, since lettre handles the wrapping for us // use usize::MAX as the line wrap length, since lettre handles the wrapping for us
let plain_text = html2text::from_read(html.as_bytes(), usize::MAX)?; let plain_text = html2text::from_read(html.as_bytes(), usize::MAX)?;
@ -70,24 +65,6 @@ pub async fn send_email(
)) ))
.with_lemmy_type(LemmyErrorType::EmailSendFailed)?; .with_lemmy_type(LemmyErrorType::EmailSendFailed)?;
// don't worry about 'dangeous'. it's just that leaving it at the default configuration
// is bad.
// Set the TLS
let mut builder = match email_config.tls_type.as_str() {
"starttls" => AsyncSmtpTransport::starttls_relay(smtp_server)?.port(smtp_port),
"tls" => AsyncSmtpTransport::relay(smtp_server)?.port(smtp_port),
_ => AsyncSmtpTransport::builder_dangerous(smtp_server).port(smtp_port),
};
// Set the creds if they exist
let smtp_password = email_config.smtp_password();
if let (Some(username), Some(password)) = (email_config.smtp_login, smtp_password) {
builder = builder.credentials(Credentials::new(username, password));
}
let mailer = builder.hello_name(ClientId::Domain(domain)).build();
mailer mailer
.send(email) .send(email)
.await .await

View file

@ -74,7 +74,6 @@ pub enum LemmyErrorType {
ObjectNotLocal, ObjectNotLocal,
NoEmailSetup, NoEmailSetup,
LocalSiteNotSetup, LocalSiteNotSetup,
EmailSmtpServerNeedsAPort,
InvalidEmailAddress(String), InvalidEmailAddress(String),
RateLimitError, RateLimitError,
InvalidName, InvalidName,

View file

@ -156,28 +156,13 @@ pub struct DatabaseConfig {
#[derive(Debug, Deserialize, Serialize, Clone, Document, SmartDefault)] #[derive(Debug, Deserialize, Serialize, Clone, Document, SmartDefault)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
pub struct EmailConfig { pub struct EmailConfig {
/// Hostname and port of the smtp server /// https://docs.rs/lettre/0.11.14/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url
#[doku(example = "localhost:25")] #[default("smtp://localhost:25")]
pub smtp_server: String, #[doku(example = "smtps://user:pass@hostname:port")]
/// Login name for smtp server pub(crate) connection: String,
pub smtp_login: Option<String>,
/// Password to login to the smtp server
smtp_password: Option<String>,
#[doku(example = "noreply@example.com")]
/// Address to send emails from, eg "noreply@your-instance.com" /// Address to send emails from, eg "noreply@your-instance.com"
pub smtp_from_address: String, #[doku(example = "noreply@example.com")]
/// Whether or not smtp connections should use tls. Can be none, tls, or starttls pub(crate) smtp_from_address: String,
#[default("none")]
#[doku(example = "none")]
pub tls_type: String,
}
impl EmailConfig {
pub fn smtp_password(&self) -> Option<String> {
std::env::var("LEMMY_SMTP_PASSWORD")
.ok()
.or(self.smtp_password.clone())
}
} }
#[derive(Debug, Deserialize, Serialize, Clone, Default, Document)] #[derive(Debug, Deserialize, Serialize, Clone, Default, Document)]