Add password_file config variant for email and database

This commit is contained in:
networkException
2026-07-26 17:57:40 +02:00
parent 19614ff89f
commit bdfeee2ceb
6 changed files with 100 additions and 18 deletions
+1 -1
View File
@@ -177,7 +177,7 @@ impl Options {
homeserver_connection_from_config(&config.matrix, http_client.clone()).await?;
if !self.no_worker {
let mailer = mailer_from_config(&config.email, &templates)?;
let mailer = mailer_from_config(&config.email, &templates).await?;
test_mailer_in_background(&mailer, Duration::from_secs(30));
info!("Starting task worker");
+1 -1
View File
@@ -64,7 +64,7 @@ impl Options {
)
.await?;
let mailer = mailer_from_config(&config.email, &templates)?;
let mailer = mailer_from_config(&config.email, &templates).await?;
test_mailer_in_background(&mailer, Duration::from_secs(30));
let http_client = mas_http::reqwest_client();
+24 -7
View File
@@ -59,7 +59,7 @@ pub async fn password_manager_from_config(
PasswordManager::new(config.minimum_complexity(), schemes)
}
pub fn mailer_from_config(
pub async fn mailer_from_config(
config: &EmailConfig,
templates: &Templates,
) -> Result<Mailer, anyhow::Error> {
@@ -83,10 +83,15 @@ pub fn mailer_from_config(
.mode()
.context("invalid email configuration: missing mode")?;
let credentials = match (config.username(), config.password()) {
let password = config
.password()
.await
.context("invalid email configuration: unable to read password file")?;
let credentials = match (config.username(), password) {
(Some(username), Some(password)) => Some(mas_email::SmtpCredentials::new(
username.to_owned(),
password.to_owned(),
password.clone(),
)),
(None, None) => None,
_ => {
@@ -287,7 +292,7 @@ pub async fn templates_from_config(
.with_context(|| format!("Failed to load the templates at {}", config.path))
}
fn database_connect_options_from_config(
async fn database_connect_options_from_config(
config: &DatabaseConfig,
opts: &DatabaseConnectOptions,
) -> Result<PgConnectOptions, anyhow::Error> {
@@ -317,6 +322,15 @@ fn database_connect_options_from_config(
opts = opts.password(password);
}
if let Some(password_file) = config.password_file.as_deref() {
opts = opts.password(
tokio::fs::read_to_string(password_file)
.await
.context("could not read database password file")?
.trim(),
);
}
if let Some(database) = config.database.as_deref() {
opts = opts.database(database);
}
@@ -386,7 +400,8 @@ fn database_connect_options_from_config(
/// Create a database connection pool from the configuration
#[tracing::instrument(name = "db.connect", skip_all)]
pub async fn database_pool_from_config(config: &DatabaseConfig) -> Result<PgPool, anyhow::Error> {
let options = database_connect_options_from_config(config, &DatabaseConnectOptions::default())?;
let options =
database_connect_options_from_config(config, &DatabaseConnectOptions::default()).await?;
PgPoolOptions::new()
.max_connections(config.max_connections.into())
.min_connections(config.min_connections)
@@ -424,7 +439,8 @@ impl Default for DatabaseConnectOptions {
pub async fn database_connection_from_config(
config: &DatabaseConfig,
) -> Result<PgConnection, anyhow::Error> {
database_connect_options_from_config(config, &DatabaseConnectOptions::default())?
database_connect_options_from_config(config, &DatabaseConnectOptions::default())
.await?
.connect()
.await
.context("could not connect to the database")
@@ -437,7 +453,8 @@ pub async fn database_connection_from_config_with_options(
config: &DatabaseConfig,
options: &DatabaseConnectOptions,
) -> Result<PgConnection, anyhow::Error> {
database_connect_options_from_config(config, options)?
database_connect_options_from_config(config, options)
.await?
.connect()
.await
.context("could not connect to the database")
+19 -1
View File
@@ -47,6 +47,7 @@ impl Default for DatabaseConfig {
socket: None,
username: None,
password: None,
password_file: None,
database: None,
ssl_mode: None,
ssl_ca: None,
@@ -137,6 +138,15 @@ pub struct DatabaseConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
/// Path to the password to be used if the server demands password
/// authentication
///
/// This must not be specified if the `password` or `uri` option is
/// specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<String>")]
pub password_file: Option<Utf8PathBuf>,
/// The database name
///
/// This must not be specified if `uri` is specified.
@@ -242,14 +252,22 @@ impl ConfigurationSection for DatabaseConfig {
|| self.socket.is_some()
|| self.username.is_some()
|| self.password.is_some()
|| self.password_file.is_some()
|| self.database.is_some();
if self.uri.is_some() && has_split_options {
return Err(annotate(figment::error::Error::from(
"uri must not be specified if host, port, socket, username, password, or database are specified".to_owned(),
"uri must not be specified if host, port, socket, username, password, password_file, or database are specified".to_owned(),
)).into());
}
if self.password.is_some() && self.password_file.is_some() {
return Err(annotate(figment::error::Error::from(
"password must not be specified if password_file is specified".to_owned(),
))
.into());
}
if self.ssl_ca.is_some() && self.ssl_ca_file.is_some() {
return Err(annotate(figment::error::Error::from(
"ssl_ca must not be specified if ssl_ca_file is specified".to_owned(),
+39 -6
View File
@@ -8,6 +8,7 @@
use std::{num::NonZeroU16, str::FromStr};
use camino::Utf8PathBuf;
use lettre::message::Mailbox;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::Error};
@@ -86,17 +87,25 @@ pub struct EmailConfig {
/// SMTP transport: Username for use to authenticate when connecting to the
/// SMTP server
///
/// Must be set if the `password` field is set
/// Must be set if the `password` or `password_file` field is set
#[serde(skip_serializing_if = "Option::is_none")]
username: Option<String>,
/// SMTP transport: Password for use to authenticate when connecting to the
/// SMTP server
///
/// Must be set if the `username` field is set
/// Must be set if the `username` but not `password_file` field is set
#[serde(skip_serializing_if = "Option::is_none")]
password: Option<String>,
/// SMTP transport: Path to the password for use to authenticate when
/// connecting to the SMTP server
///
/// Must be set if the `username` but not `password` field is set
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<String>")]
password_file: Option<Utf8PathBuf>,
/// Sendmail transport: Command to use to send emails
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(default = "default_sendmail_command")]
@@ -135,9 +144,21 @@ impl EmailConfig {
}
/// Password for use to authenticate when connecting to the SMTP server
#[must_use]
pub fn password(&self) -> Option<&str> {
self.password.as_deref()
///
/// # Errors
///
/// Returns an error if the password is a file and it could not be read.
pub async fn password(&self) -> Result<Option<String>, anyhow::Error> {
if let Some(password_file) = &self.password_file {
return Ok(Some(
tokio::fs::read_to_string(password_file)
.await?
.trim()
.to_owned(),
));
}
Ok(self.password.clone())
}
/// Command to use to send emails
@@ -158,6 +179,7 @@ impl Default for EmailConfig {
port: None,
username: None,
password: None,
password_file: None,
command: None,
}
}
@@ -183,6 +205,10 @@ impl ConfigurationSection for EmailConfig {
error_on_field(figment::error::Error::missing_field(field), field)
};
let duplicate_field = |field: &'static str, duplicate: &'static str| {
error_on_field(figment::error::Error::duplicate_field(duplicate), field)
};
let unexpected_field = |field: &'static str, expected_fields: &'static [&'static str]| {
error_on_field(
figment::error::Error::unknown_field(field, expected_fields),
@@ -202,7 +228,10 @@ impl ConfigurationSection for EmailConfig {
return Err(error_on_field(figment::error::Error::custom(e), "reply_to").into());
}
match (self.username.is_some(), self.password.is_some()) {
match (
self.username.is_some(),
self.password.is_some() || self.password_file.is_some(),
) {
(true, true) | (false, false) => {}
(true, false) => {
return Err(missing_field("password").into());
@@ -212,6 +241,10 @@ impl ConfigurationSection for EmailConfig {
}
}
if self.password.is_some() && self.password_file.is_some() {
return Err(duplicate_field("password", "password_file").into());
}
if self.mode.is_none() {
return Err(missing_field("mode").into());
}
+16 -2
View File
@@ -1135,6 +1135,13 @@
"null"
]
},
"password_file": {
"description": "Path to the password to be used if the server demands password\nauthentication\n\nThis must not be specified if the `password` or `uri` option is\nspecified.",
"type": [
"string",
"null"
]
},
"database": {
"description": "The database name\n\nThis must not be specified if `uri` is specified.",
"type": [
@@ -1575,14 +1582,21 @@
"maximum": 65535
},
"username": {
"description": "SMTP transport: Username for use to authenticate when connecting to the\nSMTP server\n\nMust be set if the `password` field is set",
"description": "SMTP transport: Username for use to authenticate when connecting to the\nSMTP server\n\nMust be set if the `password` or `password_file` field is set",
"type": [
"string",
"null"
]
},
"password": {
"description": "SMTP transport: Password for use to authenticate when connecting to the\nSMTP server\n\nMust be set if the `username` field is set",
"description": "SMTP transport: Password for use to authenticate when connecting to the\nSMTP server\n\nMust be set if the `username` but not `password_file` field is set",
"type": [
"string",
"null"
]
},
"password_file": {
"description": "SMTP transport: Path to the password for use to authenticate when\nconnecting to the SMTP server\n\nMust be set if the `username` but not `password` field is set",
"type": [
"string",
"null"