1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-06 06:02:40 +03:00

Flatten the email config

This commit is contained in:
Quentin Gliech
2024-03-21 13:53:38 +01:00
parent bf50469da1
commit 6d77d0ed25
5 changed files with 293 additions and 161 deletions

View File

@@ -16,7 +16,7 @@ use std::time::Duration;
use anyhow::Context; use anyhow::Context;
use mas_config::{ use mas_config::{
BrandingConfig, DatabaseConfig, EmailConfig, EmailSmtpMode, EmailTransportConfig, BrandingConfig, DatabaseConfig, EmailConfig, EmailSmtpMode, EmailTransportKind,
PasswordsConfig, PolicyConfig, TemplatesConfig, PasswordsConfig, PolicyConfig, TemplatesConfig,
}; };
use mas_email::{MailTransport, Mailer}; use mas_email::{MailTransport, Mailer};
@@ -61,17 +61,28 @@ pub fn mailer_from_config(
) -> Result<Mailer, anyhow::Error> { ) -> Result<Mailer, anyhow::Error> {
let from = config.from.parse()?; let from = config.from.parse()?;
let reply_to = config.reply_to.parse()?; let reply_to = config.reply_to.parse()?;
let transport = match &config.transport { let transport = match config.transport() {
EmailTransportConfig::Blackhole => MailTransport::blackhole(), EmailTransportKind::Blackhole => MailTransport::blackhole(),
EmailTransportConfig::Smtp { EmailTransportKind::Smtp => {
mode, // This should have been set ahead of time
hostname, let hostname = config
credentials, .hostname()
port, .context("invalid configuration: missing hostname")?;
} => {
let credentials = credentials let mode = config
.clone() .mode()
.map(|c| mas_email::SmtpCredentials::new(c.username, c.password)); .context("invalid configuration: missing mode")?;
let credentials = match (config.username(), config.password()) {
(Some(username), Some(password)) => Some(mas_email::SmtpCredentials::new(
username.to_owned(),
password.to_owned(),
)),
(None, None) => None,
_ => {
anyhow::bail!("invalid configuration: missing username or password");
}
};
let mode = match mode { let mode = match mode {
EmailSmtpMode::Plain => mas_email::SmtpMode::Plain, EmailSmtpMode::Plain => mas_email::SmtpMode::Plain,
@@ -79,12 +90,10 @@ pub fn mailer_from_config(
EmailSmtpMode::Tls => mas_email::SmtpMode::Tls, EmailSmtpMode::Tls => mas_email::SmtpMode::Tls,
}; };
MailTransport::smtp(mode, hostname, port.as_ref().copied(), credentials) MailTransport::smtp(mode, hostname, config.port(), credentials)
.context("failed to build SMTP transport")? .context("failed to build SMTP transport")?
} }
EmailTransportConfig::Sendmail { command } => MailTransport::sendmail(command), EmailTransportKind::Sendmail => MailTransport::sendmail(config.command()),
#[allow(deprecated)]
EmailTransportConfig::AwsSes => anyhow::bail!("AWS SESv2 backend has been removed"),
}; };
Ok(Mailer::new(templates.clone(), transport, from, reply_to)) Ok(Mailer::new(templates.clone(), transport, from, reply_to))

View File

@@ -19,7 +19,7 @@ use std::num::NonZeroU16;
use async_trait::async_trait; use async_trait::async_trait;
use rand::Rng; use rand::Rng;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{de::Error, Deserialize, Serialize};
use super::ConfigurationSection; use super::ConfigurationSection;
@@ -47,55 +47,27 @@ pub enum EmailSmtpMode {
} }
/// What backend should be used when sending emails /// What backend should be used when sending emails
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default)]
#[serde(tag = "transport", rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum EmailTransportConfig { pub enum EmailTransportKind {
/// Don't send emails anywhere /// Don't send emails anywhere
#[default]
Blackhole, Blackhole,
/// Send emails via an SMTP relay /// Send emails via an SMTP relay
Smtp { Smtp,
/// Connection mode to the relay
mode: EmailSmtpMode,
/// Hostname to connect to
#[schemars(with = "crate::schema::Hostname")]
hostname: String,
/// Port to connect to. Default is 25 for plain, 465 for TLS and 587 for
/// StartTLS
#[serde(default, skip_serializing_if = "Option::is_none")]
port: Option<NonZeroU16>,
/// Set of credentials to use
#[serde(flatten, default)]
credentials: Option<Credentials>,
},
/// Send emails by calling sendmail /// Send emails by calling sendmail
Sendmail { Sendmail,
/// Command to execute
#[serde(default = "default_sendmail_command")]
command: String,
},
/// Send emails via the AWS SESv2 API
#[deprecated(note = "The AWS SESv2 backend has be removed.")]
AwsSes,
}
impl Default for EmailTransportConfig {
fn default() -> Self {
Self::Blackhole
}
} }
fn default_email() -> String { fn default_email() -> String {
r#""Authentication Service" <root@localhost>"#.to_owned() r#""Authentication Service" <root@localhost>"#.to_owned()
} }
fn default_sendmail_command() -> String { #[allow(clippy::unnecessary_wraps)]
"sendmail".to_owned() fn default_sendmail_command() -> Option<String> {
Some("sendmail".to_owned())
} }
/// Configuration related to sending emails /// Configuration related to sending emails
@@ -112,8 +84,85 @@ pub struct EmailConfig {
pub reply_to: String, pub reply_to: String,
/// What backend should be used when sending emails /// What backend should be used when sending emails
#[serde(flatten, default)] transport: EmailTransportKind,
pub transport: EmailTransportConfig,
/// SMTP transport: Connection mode to the relay
#[serde(skip_serializing_if = "Option::is_none")]
mode: Option<EmailSmtpMode>,
/// SMTP transport: Hostname to connect to
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<crate::schema::Hostname>")]
hostname: Option<String>,
/// SMTP transport: Port to connect to. Default is 25 for plain, 465 for TLS
/// and 587 for StartTLS
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1, max = 65535))]
port: Option<NonZeroU16>,
/// SMTP transport: Username for use to authenticate when connecting to the
/// SMTP server
///
/// Must be set if the `password` 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
#[serde(skip_serializing_if = "Option::is_none")]
password: Option<String>,
/// Sendmail transport: Command to use to send emails
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(default = "default_sendmail_command")]
command: Option<String>,
}
impl EmailConfig {
/// What backend should be used when sending emails
#[must_use]
pub fn transport(&self) -> EmailTransportKind {
self.transport
}
/// Connection mode to the relay
#[must_use]
pub fn mode(&self) -> Option<EmailSmtpMode> {
self.mode
}
/// Hostname to connect to
#[must_use]
pub fn hostname(&self) -> Option<&str> {
self.hostname.as_deref()
}
/// Port to connect to
#[must_use]
pub fn port(&self) -> Option<NonZeroU16> {
self.port
}
/// Username for use to authenticate when connecting to the SMTP server
#[must_use]
pub fn username(&self) -> Option<&str> {
self.username.as_deref()
}
/// Password for use to authenticate when connecting to the SMTP server
#[must_use]
pub fn password(&self) -> Option<&str> {
self.password.as_deref()
}
/// Command to use to send emails
#[must_use]
pub fn command(&self) -> Option<&str> {
self.command.as_deref()
}
} }
impl Default for EmailConfig { impl Default for EmailConfig {
@@ -121,7 +170,13 @@ impl Default for EmailConfig {
Self { Self {
from: default_email(), from: default_email(),
reply_to: default_email(), reply_to: default_email(),
transport: EmailTransportConfig::Blackhole, transport: EmailTransportKind::Blackhole,
mode: None,
hostname: None,
port: None,
username: None,
password: None,
command: None,
} }
} }
} }
@@ -137,6 +192,98 @@ impl ConfigurationSection for EmailConfig {
Ok(Self::default()) Ok(Self::default())
} }
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::error::Error> {
let metadata = figment.find_metadata(Self::PATH.unwrap());
let error_on_field = |mut error: figment::error::Error, field: &'static str| {
error.metadata = metadata.cloned();
error.profile = Some(figment::Profile::Default);
error.path = vec![Self::PATH.unwrap().to_owned(), field.to_owned()];
error
};
let missing_field = |field: &'static str| {
error_on_field(figment::error::Error::missing_field(field), field)
};
let unexpected_field = |field: &'static str, expected_fields: &'static [&'static str]| {
error_on_field(
figment::error::Error::unknown_field(field, expected_fields),
field,
)
};
match self.transport {
EmailTransportKind::Blackhole => {}
EmailTransportKind::Smtp => {
match (self.username.is_some(), self.password.is_some()) {
(true, true) | (false, false) => {}
(true, false) => {
return Err(missing_field("password"));
}
(false, true) => {
return Err(missing_field("username"));
}
}
if self.mode.is_none() {
return Err(missing_field("mode"));
}
if self.hostname.is_none() {
return Err(missing_field("hostname"));
}
if self.command.is_some() {
return Err(unexpected_field(
"command",
&[
"from",
"reply_to",
"transport",
"mode",
"hostname",
"port",
"username",
"password",
],
));
}
}
EmailTransportKind::Sendmail => {
let expected_fields = &["from", "reply_to", "transport", "command"];
if self.command.is_none() {
return Err(missing_field("command"));
}
if self.mode.is_some() {
return Err(unexpected_field("mode", expected_fields));
}
if self.hostname.is_some() {
return Err(unexpected_field("hostname", expected_fields));
}
if self.port.is_some() {
return Err(unexpected_field("port", expected_fields));
}
if self.username.is_some() {
return Err(unexpected_field("username", expected_fields));
}
if self.password.is_some() {
return Err(unexpected_field("password", expected_fields));
}
}
}
Ok(())
}
fn test() -> Self { fn test() -> Self {
Self::default() Self::default()
} }

View File

@@ -35,7 +35,7 @@ pub use self::{
branding::BrandingConfig, branding::BrandingConfig,
clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig}, clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
database::DatabaseConfig, database::DatabaseConfig,
email::{EmailConfig, EmailSmtpMode, EmailTransportConfig}, email::{EmailConfig, EmailSmtpMode, EmailTransportKind},
experimental::ExperimentalConfig, experimental::ExperimentalConfig,
http::{ http::{
BindConfig as HttpBindConfig, HttpConfig, ListenerConfig as HttpListenerConfig, BindConfig as HttpBindConfig, HttpConfig, ListenerConfig as HttpListenerConfig,

View File

@@ -92,10 +92,13 @@ impl Transport {
/// Construct a Sendmail transport /// Construct a Sendmail transport
#[must_use] #[must_use]
pub fn sendmail(command: impl Into<OsString>) -> Self { pub fn sendmail(command: Option<impl Into<OsString>>) -> Self {
Self::new(TransportInner::Sendmail( let transport = if let Some(command) = command {
AsyncSendmailTransport::new_with_command(command), AsyncSendmailTransport::new_with_command(command)
)) } else {
AsyncSendmailTransport::new()
};
Self::new(TransportInner::Sendmail(transport))
} }
} }

View File

@@ -1348,106 +1348,9 @@
"EmailConfig": { "EmailConfig": {
"description": "Configuration related to sending emails", "description": "Configuration related to sending emails",
"type": "object", "type": "object",
"oneOf": [
{
"description": "Don't send emails anywhere",
"type": "object",
"required": [ "required": [
"transport" "transport"
], ],
"properties": {
"transport": {
"type": "string",
"enum": [
"blackhole"
]
}
}
},
{
"description": "Send emails via an SMTP relay",
"type": "object",
"required": [
"hostname",
"mode",
"transport"
],
"properties": {
"transport": {
"type": "string",
"enum": [
"smtp"
]
},
"mode": {
"description": "Connection mode to the relay",
"allOf": [
{
"$ref": "#/definitions/EmailSmtpMode"
}
]
},
"hostname": {
"description": "Hostname to connect to",
"allOf": [
{
"$ref": "#/definitions/Hostname"
}
]
},
"port": {
"description": "Port to connect to. Default is 25 for plain, 465 for TLS and 587 for StartTLS",
"type": "integer",
"format": "uint16",
"minimum": 1.0
},
"username": {
"description": "Username for use to authenticate when connecting to the SMTP server",
"type": "string"
},
"password": {
"description": "Password for use to authenticate when connecting to the SMTP server",
"type": "string"
}
}
},
{
"description": "Send emails by calling sendmail",
"type": "object",
"required": [
"transport"
],
"properties": {
"transport": {
"type": "string",
"enum": [
"sendmail"
]
},
"command": {
"description": "Command to execute",
"default": "sendmail",
"type": "string"
}
}
},
{
"description": "Send emails via the AWS SESv2 API",
"deprecated": true,
"type": "object",
"required": [
"transport"
],
"properties": {
"transport": {
"type": "string",
"enum": [
"aws_ses"
]
}
}
}
],
"properties": { "properties": {
"from": { "from": {
"description": "Email address to use as From when sending emails", "description": "Email address to use as From when sending emails",
@@ -1460,9 +1363,79 @@
"default": "\"Authentication Service\" <root@localhost>", "default": "\"Authentication Service\" <root@localhost>",
"type": "string", "type": "string",
"format": "email" "format": "email"
},
"transport": {
"description": "What backend should be used when sending emails",
"allOf": [
{
"$ref": "#/definitions/EmailTransportKind"
}
]
},
"mode": {
"description": "SMTP transport: Connection mode to the relay",
"allOf": [
{
"$ref": "#/definitions/EmailSmtpMode"
}
]
},
"hostname": {
"description": "SMTP transport: Hostname to connect to",
"allOf": [
{
"$ref": "#/definitions/Hostname"
}
]
},
"port": {
"description": "SMTP transport: Port to connect to. Default is 25 for plain, 465 for TLS and 587 for StartTLS",
"type": "integer",
"format": "uint16",
"maximum": 65535.0,
"minimum": 1.0
},
"username": {
"description": "SMTP transport: Username for use to authenticate when connecting to the SMTP server\n\nMust be set if the `password` field is set",
"type": "string"
},
"password": {
"description": "SMTP transport: Password for use to authenticate when connecting to the SMTP server\n\nMust be set if the `username` field is set",
"type": "string"
},
"command": {
"description": "Sendmail transport: Command to use to send emails",
"default": "sendmail",
"type": "string"
} }
} }
}, },
"EmailTransportKind": {
"description": "What backend should be used when sending emails",
"oneOf": [
{
"description": "Don't send emails anywhere",
"type": "string",
"enum": [
"blackhole"
]
},
{
"description": "Send emails via an SMTP relay",
"type": "string",
"enum": [
"smtp"
]
},
{
"description": "Send emails by calling sendmail",
"type": "string",
"enum": [
"sendmail"
]
}
]
},
"EmailSmtpMode": { "EmailSmtpMode": {
"description": "Encryption mode to use", "description": "Encryption mode to use",
"oneOf": [ "oneOf": [