diff --git a/Cargo.lock b/Cargo.lock index 95a6e1b7..a3797cd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2420,6 +2420,7 @@ dependencies = [ "figment", "indoc", "lettre", + "mas-email", "mas-iana", "mas-jose", "mas-keystore", @@ -2461,7 +2462,6 @@ dependencies = [ "aws-sdk-sesv2", "aws-types", "lettre", - "mas-config", "mas-templates", "tokio", "tracing", @@ -2483,7 +2483,6 @@ dependencies = [ "indoc", "lettre", "mas-axum-utils", - "mas-config", "mas-data-model", "mas-email", "mas-http", @@ -2707,7 +2706,6 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "mas-config", "mas-data-model", "mas-router", "oauth2-types", diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 0b62ff80..b336456c 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -23,7 +23,8 @@ use clap::Parser; use futures::stream::{StreamExt, TryStreamExt}; use hyper::Server; use mas_config::RootConfig; -use mas_email::{MailTransport, Mailer}; +use mas_email::Mailer; +use mas_handlers::MatrixHomeserver; use mas_http::ServerLayer; use mas_policy::PolicyFactory; use mas_router::UrlBuilder; @@ -148,7 +149,7 @@ impl Options { let listener = TcpListener::bind(addr).context("could not bind address")?; // Connect to the mail server - let mail_transport = MailTransport::from_config(&config.email.transport).await?; + let mail_transport = config.email.transport.to_transport().await?; mail_transport.test_connection().await?; // Connect to the database @@ -203,7 +204,7 @@ impl Options { let policy_factory = Arc::new(policy_factory); // Load and compile the templates - let templates = Templates::load_from_config(&config.templates) + let templates = Templates::load(config.templates.path.clone(), config.templates.builtin) .await .context("could not load templates")?; @@ -218,7 +219,7 @@ impl Options { let static_files = mas_static_files::service(&config.http.web_root); - let matrix_config = config.matrix.clone(); + let homeserver = MatrixHomeserver::new(config.matrix.homeserver.clone()); // Explicitely the config to properly zeroize secret keys drop(config); @@ -242,7 +243,7 @@ impl Options { &encrypter, &mailer, &url_builder, - &matrix_config, + &homeserver, &policy_factory, ) .fallback(static_files) diff --git a/crates/cli/src/commands/templates.rs b/crates/cli/src/commands/templates.rs index 2c6e4023..26ba8c7b 100644 --- a/crates/cli/src/commands/templates.rs +++ b/crates/cli/src/commands/templates.rs @@ -62,7 +62,7 @@ impl Options { path: Some(path.to_string()), builtin: !skip_builtin, }; - let templates = Templates::load_from_config(&config).await?; + let templates = Templates::load(config.path.clone(), config.builtin).await?; templates.check_render().await?; Ok(()) diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 81cd585a..f8f7b44f 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -32,3 +32,4 @@ indoc = "1.0.7" mas-jose = { path = "../jose" } mas-keystore = { path = "../keystore" } mas-iana = { path = "../iana" } +mas-email = { path = "../email" } diff --git a/crates/config/src/sections/email.rs b/crates/config/src/sections/email.rs index 75f57a67..6db56057 100644 --- a/crates/config/src/sections/email.rs +++ b/crates/config/src/sections/email.rs @@ -14,8 +14,10 @@ use std::num::NonZeroU16; +use anyhow::Context; use async_trait::async_trait; use lettre::{message::Mailbox, Address}; +use mas_email::MailTransport; use schemars::{ gen::SchemaGenerator, schema::{InstanceType, Schema, SchemaObject}, @@ -51,7 +53,7 @@ pub struct Credentials { } /// Encryption mode to use -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum EmailSmtpMode { /// Plain text @@ -62,6 +64,16 @@ pub enum EmailSmtpMode { Tls, } +impl From<&EmailSmtpMode> for mas_email::SmtpMode { + fn from(value: &EmailSmtpMode) -> Self { + match value { + EmailSmtpMode::Plain => Self::Plain, + EmailSmtpMode::StartTls => Self::StartTls, + EmailSmtpMode::Tls => Self::Tls, + } + } +} + /// What backend should be used when sending emails #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(tag = "transport", rename_all = "snake_case")] @@ -156,3 +168,30 @@ impl ConfigurationSection<'_> for EmailConfig { Self::default() } } + +impl EmailTransportConfig { + /// Create a [`lettre::Transport`] out of this config + /// + /// # Errors + /// + /// Returns an error if the transport could not be created + pub async fn to_transport(&self) -> Result { + match self { + Self::Blackhole => Ok(MailTransport::blackhole()), + Self::Smtp { + mode, + hostname, + credentials, + port, + } => { + let credentials = credentials + .clone() + .map(|c| mas_email::SmtpCredentials::new(c.username, c.password)); + MailTransport::smtp(mode.into(), hostname, port.as_ref().copied(), credentials) + .context("failed to build SMTP transport") + } + EmailTransportConfig::Sendmail { command } => Ok(MailTransport::sendmail(command)), + EmailTransportConfig::AwsSes => Ok(MailTransport::aws_ses().await), + } + } +} diff --git a/crates/email/Cargo.toml b/crates/email/Cargo.toml index 3664444d..ac9ad74f 100644 --- a/crates/email/Cargo.toml +++ b/crates/email/Cargo.toml @@ -15,7 +15,6 @@ aws-config = "0.47.0" aws-types = "0.47.0" mas-templates = { path = "../templates" } -mas-config = { path = "../config" } [dependencies.lettre] version = "0.10.1" diff --git a/crates/email/src/lib.rs b/crates/email/src/lib.rs index 5adc8aa2..754d09df 100644 --- a/crates/email/src/lib.rs +++ b/crates/email/src/lib.rs @@ -26,7 +26,9 @@ mod mailer; mod transport; +pub use lettre::transport::smtp::authentication::Credentials as SmtpCredentials; + pub use self::{ mailer::Mailer, - transport::{aws_ses::Transport as AwsSesTransport, Transport as MailTransport}, + transport::{aws_ses::Transport as AwsSesTransport, SmtpMode, Transport as MailTransport}, }; diff --git a/crates/email/src/transport/mod.rs b/crates/email/src/transport/mod.rs index e75ef188..bb1773b7 100644 --- a/crates/email/src/transport/mod.rs +++ b/crates/email/src/transport/mod.rs @@ -14,7 +14,7 @@ //! Email transport backends -use std::sync::Arc; +use std::{ffi::OsString, num::NonZeroU16, sync::Arc}; use async_trait::async_trait; use lettre::{ @@ -25,10 +25,20 @@ use lettre::{ }, AsyncTransport, Tokio1Executor, }; -use mas_config::{EmailSmtpMode, EmailTransportConfig}; pub mod aws_ses; +/// Encryption mode to use +#[derive(Debug, Clone, Copy)] +pub enum SmtpMode { + /// Plain text + Plain, + /// StartTLS (starts as plain text then upgrade to TLS) + StartTls, + /// TLS + Tls, +} + /// A wrapper around many [`AsyncTransport`]s #[derive(Default, Clone)] pub struct Transport { @@ -43,52 +53,56 @@ enum TransportInner { } impl Transport { - /// Construct a transport from a user configration + fn new(inner: TransportInner) -> Self { + let inner = Arc::new(inner); + Self { inner } + } + + /// Construct a blackhole transport + #[must_use] + pub fn blackhole() -> Self { + Self::new(TransportInner::Blackhole) + } + + /// Construct a SMTP transport /// /// # Errors /// - /// Will return `Err` on invalid confiuration - pub async fn from_config(config: &EmailTransportConfig) -> Result { - let inner = match config { - EmailTransportConfig::Blackhole => TransportInner::Blackhole, - EmailTransportConfig::Smtp { - mode, - hostname, - credentials, - port, - } => { - let mut t = match mode { - EmailSmtpMode::Plain => { - AsyncSmtpTransport::::builder_dangerous(hostname) - } - EmailSmtpMode::StartTls => { - AsyncSmtpTransport::::starttls_relay(hostname)? - } - EmailSmtpMode::Tls => AsyncSmtpTransport::::relay(hostname)?, - }; - - if let Some(credentials) = credentials { - t = t.credentials(Credentials::new( - credentials.username.clone(), - credentials.password.clone(), - )); - } - - if let Some(port) = port { - t = t.port((*port).into()); - } - - TransportInner::Smtp(t.build()) - } - EmailTransportConfig::Sendmail { command } => { - TransportInner::Sendmail(AsyncSendmailTransport::new_with_command(command)) - } - EmailTransportConfig::AwsSes => { - TransportInner::AwsSes(aws_ses::Transport::from_env().await) - } + /// Returns an error if the underlying SMTP transport could not be built + pub fn smtp( + mode: SmtpMode, + hostname: &str, + port: Option, + credentials: Option, + ) -> Result { + let mut t = match mode { + SmtpMode::Plain => AsyncSmtpTransport::::builder_dangerous(hostname), + SmtpMode::StartTls => AsyncSmtpTransport::::starttls_relay(hostname)?, + SmtpMode::Tls => AsyncSmtpTransport::::relay(hostname)?, }; - let inner = Arc::new(inner); - Ok(Self { inner }) + + if let Some(credentials) = credentials { + t = t.credentials(credentials); + } + + if let Some(port) = port { + t = t.port(port.into()); + } + + Ok(Self::new(TransportInner::Smtp(t.build()))) + } + + /// Construct a Sendmail transport + #[must_use] + pub fn sendmail(command: impl Into) -> Self { + Self::new(TransportInner::Sendmail( + AsyncSendmailTransport::new_with_command(command), + )) + } + + /// Construct a AWS SES transport + pub async fn aws_ses() -> Self { + Self::new(TransportInner::AwsSes(aws_ses::Transport::from_env().await)) } } diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index 54a78273..14b3b027 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -52,7 +52,6 @@ headers = "0.3.7" oauth2-types = { path = "../oauth2-types" } mas-axum-utils = { path = "../axum-utils" } -mas-config = { path = "../config" } mas-data-model = { path = "../data-model" } mas-email = { path = "../email" } mas-http = { path = "../http" } diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index 51658ee8..c827dcee 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -15,7 +15,6 @@ use axum::{response::IntoResponse, Extension, Json}; use chrono::{Duration, Utc}; use hyper::StatusCode; -use mas_config::MatrixConfig; use mas_data_model::{CompatSession, CompatSsoLoginState, Device, TokenType}; use mas_storage::{ compat::{ @@ -31,7 +30,7 @@ use serde_with::{serde_as, skip_serializing_none, DurationMilliSeconds}; use sqlx::{PgPool, Postgres, Transaction}; use thiserror::Error; -use super::MatrixError; +use super::{MatrixError, MatrixHomeserver}; #[derive(Debug, Serialize)] #[serde(tag = "type")] @@ -199,7 +198,7 @@ impl IntoResponse for RouteError { #[tracing::instrument(skip_all, err)] pub(crate) async fn post( Extension(pool): Extension, - Extension(config): Extension, + Extension(homeserver): Extension, Json(input): Json, ) -> Result { let mut txn = pool.begin().await?; @@ -216,7 +215,7 @@ pub(crate) async fn post( } }; - let user_id = format!("@{}:{}", session.user.username, config.homeserver); + let user_id = format!("@{}:{}", session.user.username, homeserver); // If the client asked for a refreshable token, make it expire let expires_in = if input.refresh_token { diff --git a/crates/handlers/src/compat/mod.rs b/crates/handlers/src/compat/mod.rs index 3ae2030c..df26cc49 100644 --- a/crates/handlers/src/compat/mod.rs +++ b/crates/handlers/src/compat/mod.rs @@ -22,6 +22,22 @@ pub(crate) mod login_sso_redirect; pub(crate) mod logout; pub(crate) mod refresh; +#[derive(Debug, Clone)] +pub struct MatrixHomeserver(String); + +impl MatrixHomeserver { + #[must_use] + pub const fn new(hs: String) -> Self { + Self(hs) + } +} + +impl std::fmt::Display for MatrixHomeserver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + #[derive(Debug, Serialize)] struct MatrixError { errcode: &'static str, diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 629f0c90..ee19643a 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -30,7 +30,6 @@ use axum::{ }; use headers::HeaderName; use hyper::header::{ACCEPT, ACCEPT_LANGUAGE, AUTHORIZATION, CONTENT_LANGUAGE, CONTENT_TYPE}; -use mas_config::MatrixConfig; use mas_email::Mailer; use mas_http::CorsLayerExt; use mas_keystore::{Encrypter, Keystore}; @@ -46,6 +45,8 @@ mod health; mod oauth2; mod views; +pub use compat::MatrixHomeserver; + #[must_use] #[allow( clippy::too_many_lines, @@ -60,7 +61,7 @@ pub fn router( encrypter: &Encrypter, mailer: &Mailer, url_builder: &UrlBuilder, - matrix_config: &MatrixConfig, + homeserver: &MatrixHomeserver, policy_factory: &Arc, ) -> Router where @@ -239,33 +240,28 @@ where .layer(Extension(encrypter.clone())) .layer(Extension(url_builder.clone())) .layer(Extension(mailer.clone())) - .layer(Extension(matrix_config.clone())) + .layer(Extension(homeserver.clone())) .layer(Extension(policy_factory.clone())) } #[cfg(test)] async fn test_router(pool: &PgPool) -> Result { - use mas_config::TemplatesConfig; use mas_email::MailTransport; - let templates_config = TemplatesConfig::default(); - let templates = Templates::load_from_config(&templates_config).await?; + let templates = Templates::load(None, true).await?; // TODO: add test keys to the store let key_store = Keystore::default(); let encrypter = Encrypter::new(&[0x42; 32]); - let transport = MailTransport::default(); + let transport = MailTransport::blackhole(); let mailbox = "server@example.com".parse()?; let mailer = Mailer::new(&templates, &transport, &mailbox, &mailbox); let url_builder = UrlBuilder::new("https://example.com/".parse()?); - let matrix_config = MatrixConfig { - homeserver: "example.com".to_owned(), - }; - + let homeserver = MatrixHomeserver::new("example.com".to_owned()); let policy_factory = PolicyFactory::load_default(serde_json::json!({})).await?; let policy_factory = Arc::new(policy_factory); @@ -276,7 +272,7 @@ async fn test_router(pool: &PgPool) -> Result { &encrypter, &mailer, &url_builder, - &matrix_config, + &homeserver, &policy_factory, )) } diff --git a/crates/templates/Cargo.toml b/crates/templates/Cargo.toml index aceeb9d7..2fbde560 100644 --- a/crates/templates/Cargo.toml +++ b/crates/templates/Cargo.toml @@ -25,5 +25,4 @@ url = "2.2.2" oauth2-types = { path = "../oauth2-types" } mas-data-model = { path = "../data-model" } -mas-config = { path = "../config" } mas-router = { path = "../router" } diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 3ab7e974..3261c55a 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -33,7 +33,6 @@ use std::{ }; use anyhow::{bail, Context as _}; -use mas_config::TemplatesConfig; use mas_data_model::StorageBackend; use serde::Serialize; use tera::{Context, Error as TeraError, Tera}; @@ -63,7 +62,8 @@ pub use self::{ #[derive(Debug, Clone)] pub struct Templates { tera: Arc>, - config: TemplatesConfig, + path: Option, + builtin: bool, } /// There was an issue while loading the templates @@ -90,7 +90,7 @@ pub enum TemplateLoadingError { impl Templates { /// List directories to watch pub async fn watch_roots(&self) -> Vec { - Self::roots(self.config.path.as_deref(), self.config.builtin) + Self::roots(self.path.as_deref(), self.builtin) .await .into_iter() .filter_map(Result::ok) @@ -133,17 +133,17 @@ impl Templates { Ok(tera) } - /// Load the templates from [the config][`TemplatesConfig`] - pub async fn load_from_config(config: &TemplatesConfig) -> Result { - let tera = Self::load(config.path.as_deref(), config.builtin).await?; - + /// Load the templates from the given config + pub async fn load(path: Option, builtin: bool) -> Result { + let tera = Self::load_(path.as_deref(), builtin).await?; Ok(Self { tera: Arc::new(RwLock::new(tera)), - config: config.clone(), + path, + builtin, }) } - async fn load(path: Option<&str>, builtin: bool) -> Result { + async fn load_(path: Option<&str>, builtin: bool) -> Result { let mut teras = Vec::new(); let roots = Self::roots(path, builtin).await; @@ -202,7 +202,7 @@ impl Templates { /// Reload the templates on disk pub async fn reload(&self) -> anyhow::Result<()> { // Prepare the new Tera instance - let new_tera = Self::load(self.config.path.as_deref(), self.config.builtin).await?; + let new_tera = Self::load_(self.path.as_deref(), self.builtin).await?; // Swap it *self.tera.write().await = new_tera; @@ -378,12 +378,7 @@ mod tests { #[tokio::test] async fn check_builtin_templates() { - let config = TemplatesConfig { - path: None, - builtin: true, - }; - - let templates = Templates::load_from_config(&config).await.unwrap(); + let templates = Templates::load(None, true).await.unwrap(); templates.check_render().await.unwrap(); } }