diff --git a/Cargo.lock b/Cargo.lock index bcf2c979..1392e5e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2552,7 +2552,6 @@ dependencies = [ "quoted_printable", "rustls", "rustls-pemfile", - "serde", "socket2", "tokio", "tokio-rustls", @@ -2741,8 +2740,6 @@ dependencies = [ "chrono", "figment", "indoc", - "lettre", - "mas-email", "mas-iana", "mas-jose", "mas-keystore", diff --git a/crates/cli/src/commands/debug.rs b/crates/cli/src/commands/debug.rs index f15123d1..991541cb 100644 --- a/crates/cli/src/commands/debug.rs +++ b/crates/cli/src/commands/debug.rs @@ -12,17 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use anyhow::Context; use clap::Parser; use hyper::{Response, Uri}; use mas_config::PolicyConfig; use mas_handlers::HttpClientFactory; use mas_http::HttpServiceExt; -use mas_policy::PolicyFactory; use tokio::io::AsyncWriteExt; use tower::{Service, ServiceExt}; use tracing::info; +use crate::util::policy_factory_from_config; + #[derive(Parser, Debug)] pub(super) struct Options { #[command(subcommand)] @@ -124,19 +124,7 @@ impl Options { SC::Policy => { let config: PolicyConfig = root.load_config()?; info!("Loading and compiling the policy module"); - let policy_file = tokio::fs::File::open(&config.wasm_module) - .await - .context("failed to open OPA WASM policy file")?; - - let policy_factory = PolicyFactory::load( - policy_file, - config.data.clone().unwrap_or_default(), - config.register_entrypoint.clone(), - config.client_registration_entrypoint.clone(), - config.authorization_grant_entrypoint.clone(), - ) - .await - .context("failed to load the policy")?; + let policy_factory = policy_factory_from_config(&config).await?; let _instance = policy_factory.instantiate().await?; Ok(()) diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 506ed4b6..22eac325 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -19,10 +19,8 @@ use clap::Parser; use futures_util::stream::{StreamExt, TryStreamExt}; use itertools::Itertools; use mas_config::RootConfig; -use mas_email::Mailer; use mas_handlers::{AppState, HttpClientFactory, MatrixHomeserver}; use mas_listener::{server::Server, shutdown::ShutdownStream}; -use mas_policy::PolicyFactory; use mas_router::UrlBuilder; use mas_storage::MIGRATOR; use mas_tasks::TaskQueue; @@ -30,7 +28,7 @@ use mas_templates::Templates; use tokio::signal::unix::SignalKind; use tracing::{error, info, log::warn}; -use crate::util::password_manager_from_config; +use crate::util::{mailer_from_config, password_manager_from_config, policy_factory_from_config}; #[derive(Parser, Debug, Default)] pub(super) struct Options { @@ -106,10 +104,6 @@ impl Options { pub async fn run(&self, root: &super::Options) -> anyhow::Result<()> { let config: RootConfig = root.load_config()?; - // Connect to the mail server - let mail_transport = config.email.transport.to_transport().await?; - mail_transport.test_connection().await?; - // Connect to the database let pool = config.database.connect().await?; @@ -126,6 +120,7 @@ impl Options { queue.recuring(Duration::from_secs(15), mas_tasks::cleanup_expired(&pool)); queue.start(); + // TODO: task queue, key store, encrypter, url builder, http client // Initialize the key store let key_store = config .secrets @@ -137,19 +132,7 @@ impl Options { // Load and compile the WASM policies (and fallback to the default embedded one) info!("Loading and compiling the policy module"); - let policy_file = tokio::fs::File::open(&config.policy.wasm_module) - .await - .context("failed to open OPA WASM policy file")?; - - let policy_factory = PolicyFactory::load( - policy_file, - config.policy.data.clone().unwrap_or_default(), - config.policy.register_entrypoint.clone(), - config.policy.client_registration_entrypoint.clone(), - config.policy.authorization_grant_entrypoint.clone(), - ) - .await - .context("failed to load the policy")?; + let policy_factory = policy_factory_from_config(&config.policy).await?; let policy_factory = Arc::new(policy_factory); let url_builder = UrlBuilder::new(config.http.public_base.clone()); @@ -159,12 +142,8 @@ impl Options { .await .context("could not load templates")?; - let mailer = Mailer::new( - &templates, - &mail_transport, - &config.email.from, - &config.email.reply_to, - ); + let mailer = mailer_from_config(&config.email, &templates).await?; + mailer.test_connection().await?; let homeserver = MatrixHomeserver::new(config.matrix.homeserver.clone()); diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 9ffb44d8..ba7c07db 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -12,8 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_config::PasswordsConfig; +use anyhow::Context; +use mas_config::{EmailConfig, EmailSmtpMode, EmailTransportConfig, PasswordsConfig, PolicyConfig}; +use mas_email::{MailTransport, Mailer}; use mas_handlers::passwords::PasswordManager; +use mas_policy::PolicyFactory; +use mas_templates::Templates; pub async fn password_manager_from_config( config: &PasswordsConfig, @@ -35,3 +39,55 @@ pub async fn password_manager_from_config( PasswordManager::new(schemes) } + +pub async fn mailer_from_config( + config: &EmailConfig, + templates: &Templates, +) -> Result { + let from = config.from.parse()?; + let reply_to = config.reply_to.parse()?; + let transport = match &config.transport { + EmailTransportConfig::Blackhole => MailTransport::blackhole(), + EmailTransportConfig::Smtp { + mode, + hostname, + credentials, + port, + } => { + let credentials = credentials + .clone() + .map(|c| mas_email::SmtpCredentials::new(c.username, c.password)); + + let mode = match mode { + EmailSmtpMode::Plain => mas_email::SmtpMode::Plain, + EmailSmtpMode::StartTls => mas_email::SmtpMode::StartTls, + EmailSmtpMode::Tls => mas_email::SmtpMode::Tls, + }; + + MailTransport::smtp(mode, hostname, port.as_ref().copied(), credentials) + .context("failed to build SMTP transport")? + } + EmailTransportConfig::Sendmail { command } => MailTransport::sendmail(command), + EmailTransportConfig::AwsSes => MailTransport::aws_ses().await?, + }; + + Ok(Mailer::new(templates.clone(), transport, from, reply_to)) +} + +pub async fn policy_factory_from_config( + config: &PolicyConfig, +) -> Result { + let policy_file = tokio::fs::File::open(&config.wasm_module) + .await + .context("failed to open OPA WASM policy file")?; + + PolicyFactory::load( + policy_file, + config.data.clone().unwrap_or_default(), + config.register_entrypoint.clone(), + config.client_registration_entrypoint.clone(), + config.authorization_grant_entrypoint.clone(), + ) + .await + .context("failed to load the policy") +} diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 824ae085..bf957d8f 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -24,7 +24,6 @@ serde = { version = "1.0.150", features = ["derive"] } serde_with = { version = "2.1.0", features = ["hex", "chrono"] } serde_json = "1.0.89" sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "postgres"] } -lettre = { version = "0.10.1", default-features = false, features = ["serde", "builder"] } pem-rfc7468 = "0.6.0" rustls-pemfile = "1.0.1" @@ -36,11 +35,8 @@ indoc = "1.0.7" mas-jose = { path = "../jose" } mas-keystore = { path = "../keystore" } mas-iana = { path = "../iana" } -mas-email = { path = "../email" } [features] -native-roots = ["mas-email/native-roots"] -webpki-roots = ["mas-email/webpki-roots"] docker = [] [[bin]] diff --git a/crates/config/src/sections/email.rs b/crates/config/src/sections/email.rs index cdde5054..9da6ae68 100644 --- a/crates/config/src/sections/email.rs +++ b/crates/config/src/sections/email.rs @@ -14,10 +14,7 @@ use std::num::NonZeroU16; -use anyhow::Context; use async_trait::async_trait; -use lettre::{message::Mailbox, Address}; -use mas_email::MailTransport; use rand::Rng; use schemars::{ gen::SchemaGenerator, @@ -59,22 +56,14 @@ pub struct Credentials { pub enum EmailSmtpMode { /// Plain text Plain, + /// StartTLS (starts as plain text then upgrade to TLS) StartTls, + /// TLS 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")] @@ -118,9 +107,8 @@ impl Default for EmailTransportConfig { } } -fn default_email() -> Mailbox { - let address = Address::new("root", "localhost").unwrap(); - Mailbox::new(Some("Authentication Service".to_owned()), address) +fn default_email() -> String { + r#""Authentication Service" "#.to_owned() } fn default_sendmail_command() -> String { @@ -133,12 +121,12 @@ pub struct EmailConfig { /// Email address to use as From when sending emails #[serde(default = "default_email")] #[schemars(schema_with = "mailbox_schema")] - pub from: Mailbox, + pub from: String, /// Email address to use as Reply-To when sending emails #[serde(default = "default_email")] #[schemars(schema_with = "mailbox_schema")] - pub reply_to: Mailbox, + pub reply_to: String, /// What backend should be used when sending emails #[serde(flatten, default)] @@ -172,30 +160,3 @@ 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/src/mailer.rs b/crates/email/src/mailer.rs index 122f891f..33fa7cdf 100644 --- a/crates/email/src/mailer.rs +++ b/crates/email/src/mailer.rs @@ -35,16 +35,16 @@ impl Mailer { /// Constructs a new [`Mailer`] #[must_use] pub fn new( - templates: &Templates, - transport: &MailTransport, - from: &Mailbox, - reply_to: &Mailbox, + templates: Templates, + transport: MailTransport, + from: Mailbox, + reply_to: Mailbox, ) -> Self { Self { - templates: templates.clone(), - transport: transport.clone(), - from: from.clone(), - reply_to: reply_to.clone(), + templates, + transport, + from, + reply_to, } } @@ -110,4 +110,13 @@ impl Mailer { self.transport.send(message).await?; Ok(()) } + + /// Test the connetion to the mail server + /// + /// # Errors + /// + /// Returns an error if the connection failed + pub async fn test_connection(&self) -> Result<(), anyhow::Error> { + self.transport.test_connection().await + } } diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 2d3d1812..927d20df 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -372,8 +372,8 @@ async fn test_state(pool: PgPool) -> Result { let password_manager = PasswordManager::new([(1, Hasher::argon2id(None))])?; let transport = MailTransport::blackhole(); - let mailbox = "server@example.com".parse()?; - let mailer = Mailer::new(&templates, &transport, &mailbox, &mailbox); + let mailbox: lettre::message::Mailbox = "server@example.com".parse()?; + let mailer = Mailer::new(templates.clone(), transport, mailbox.clone(), mailbox); let homeserver = MatrixHomeserver::new("example.com".to_owned());