From 1355be8fb86c83f6515ccb963fa66e5c8a4aabe6 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Mon, 24 Jan 2022 16:31:53 +0100 Subject: [PATCH] Add AWS SES backend to send email --- Cargo.lock | 359 +++++++++++++++++++++++++- crates/cli/src/server.rs | 2 +- crates/config/src/email.rs | 3 +- crates/email/Cargo.toml | 2 + crates/email/src/lib.rs | 172 +----------- crates/email/src/mailer.rs | 88 +++++++ crates/email/src/transport/aws_ses.rs | 53 ++++ crates/email/src/transport/mod.rs | 121 +++++++++ 8 files changed, 627 insertions(+), 173 deletions(-) create mode 100644 crates/email/src/mailer.rs create mode 100644 crates/email/src/transport/aws_ses.rs create mode 100644 crates/email/src/transport/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 4f934136..47fe17a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,6 +178,284 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "aws-config" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33df8f310ad3e5937d55b9e92e1404241b4f91b7c1da7822b89fde04caaacd2c" +dependencies = [ + "aws-http", + "aws-sdk-sso", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-types", + "bytes 1.1.0", + "hex", + "http", + "hyper", + "ring", + "tokio", + "tower", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-endpoint" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4862c0314a90e9e6bdbc9625236aeebd194e66f98066d1f1e3f2f32b867f9ecd" +dependencies = [ + "aws-smithy-http", + "aws-types", + "http", + "regex", + "tracing", +] + +[[package]] +name = "aws-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae844a9180de89ae4dcf3ea8f21230b84f181eed52335123f0e3a435da21d0f" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "aws-types", + "http", + "lazy_static", + "percent-encoding", + "tracing", +] + +[[package]] +name = "aws-sdk-sesv2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a397809fef83bfece0d724ef5f71da033c8d0d6330d03f3dd8c679b1769487a" +dependencies = [ + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-types", + "bytes 1.1.0", + "http", + "tokio-stream", + "tower", +] + +[[package]] +name = "aws-sdk-sso" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d34588414f5077e48761d2977dfd2a66a1d2df80034cff50010893b4fa584f" +dependencies = [ + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-types", + "bytes 1.1.0", + "http", + "tokio-stream", + "tower", +] + +[[package]] +name = "aws-sdk-sts" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c34df5bea9c58505f7cb4346dc56842f0300f5244827e078f359302b223386" +dependencies = [ + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-query", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes 1.1.0", + "http", + "tower", +] + +[[package]] +name = "aws-sig-auth" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba38995b2c5531e56877e0771746545ac6cd3893261d8cd5753b69bd39ceff2" +dependencies = [ + "aws-sigv4", + "aws-smithy-http", + "aws-types", + "http", + "thiserror", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23b934ee5886df8c49c47ca0a65230d2407eaa71c2a883cbdf87c5d25c458cd" +dependencies = [ + "aws-smithy-http", + "form_urlencoded", + "hex", + "http", + "once_cell", + "percent-encoding", + "regex", + "ring", + "time 0.3.5", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5fcbffea194e3b5c20471f5dc12d042a9edee49aadbb0efb25315b6dc9dd5d" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", + "tokio-stream", +] + +[[package]] +name = "aws-smithy-client" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4120423bf4bfe09332eb9c83300d2c20a7ce58f1ace34fadbc87fae4db2c6b3" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-types", + "bytes 1.1.0", + "fastrand", + "http", + "http-body", + "hyper", + "hyper-rustls 0.22.1", + "lazy_static", + "pin-project", + "pin-project-lite", + "tokio", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-http" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0cd7cf4c3eab32ccbaab8a3c2a128d5fee49c59414b8f0e1be66380ab5870a" +dependencies = [ + "aws-smithy-types", + "bytes 1.1.0", + "bytes-utils", + "futures-core", + "http", + "http-body", + "hyper", + "percent-encoding", + "pin-project", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "aws-smithy-http-tower" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af28d1e5455d9362208e12aa17221a8c27dd430e85578a0a27964c1f1eed42c0" +dependencies = [ + "aws-smithy-http", + "bytes 1.1.0", + "http", + "http-body", + "pin-project", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a73ad42c7528114053c4ace79dc2b560cb3fbeb4c677246da066bd639fb61e40" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-query" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e66f2ced4b96990a00bd774aa6fd02bd485d1fce081219637e2907c588f9dee4" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-types" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193abb2559d65d6eaeacc45dd3764cb8f821a90425f6b051a8fd17ea12cbd0d1" +dependencies = [ + "itoa 1.0.1", + "num-integer", + "ryu", + "time 0.3.5", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e222a20a9a91c396fe1689faae16499fa857de08071a8a86d2b734319eac405" +dependencies = [ + "thiserror", + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcd5d5fbab40e704ae3d0f3c602131f4c691475617270bf793cf1c47d9d24e41" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "rustc_version", + "tracing", + "zeroize", +] + [[package]] name = "backtrace" version = "0.3.63" @@ -357,6 +635,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bytes-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e314712951c43123e5920a446464929adc667a5eade7f8fb3997776c9df6e54" +dependencies = [ + "bytes 1.1.0", + "either", +] + [[package]] name = "cc" version = "1.0.72" @@ -648,6 +936,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ct-logs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8" +dependencies = [ + "sct 0.6.1", +] + [[package]] name = "darling" version = "0.13.1" @@ -1285,6 +1582,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" +dependencies = [ + "ct-logs", + "futures-util", + "hyper", + "log", + "rustls 0.19.1", + "rustls-native-certs 0.5.0", + "tokio", + "tokio-rustls 0.22.0", + "webpki 0.21.4", +] + [[package]] name = "hyper-rustls" version = "0.23.0" @@ -1612,6 +1926,8 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "aws-config", + "aws-sdk-sesv2", "lettre", "mas-config", "mas-templates", @@ -2702,7 +3018,7 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-rustls", + "hyper-rustls 0.23.0", "ipnet", "js-sys", "lazy_static", @@ -2711,7 +3027,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.20.2", - "rustls-native-certs", + "rustls-native-certs 0.6.1", "rustls-pemfile", "serde", "serde_json", @@ -2810,6 +3126,15 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustls" version = "0.19.1" @@ -2835,6 +3160,18 @@ dependencies = [ "webpki 0.22.0", ] +[[package]] +name = "rustls-native-certs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" +dependencies = [ + "openssl-probe", + "rustls 0.19.1", + "schannel", + "security-framework", +] + [[package]] name = "rustls-native-certs" version = "0.6.1" @@ -2987,6 +3324,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" + [[package]] name = "serde" version = "1.0.133" @@ -4028,6 +4371,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a1f0175e03a0973cf4afd476bef05c26e228520400eb1fd473ad417b1c00ffb" + [[package]] name = "utf-8" version = "0.7.6" @@ -4296,6 +4645,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "xmlparser" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index 9520ac6c..ded5119e 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -223,7 +223,7 @@ impl ServerCommand { let listener = TcpListener::bind(addr).context("could not bind address")?; // Connect to the mail server - let mail_transport = MailTransport::try_from(&config.email.transport)?; + let mail_transport = MailTransport::from_config(&config.email.transport).await?; mail_transport.test_connection().await?; // Connect to the database diff --git a/crates/config/src/email.rs b/crates/config/src/email.rs index 4411aa9f..08458705 100644 --- a/crates/config/src/email.rs +++ b/crates/config/src/email.rs @@ -39,7 +39,7 @@ pub enum EmailSmtpMode { } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(tag = "transport", rename_all = "lowercase")] +#[serde(tag = "transport", rename_all = "snake_case")] pub enum EmailTransportConfig { Blackhole, Smtp { @@ -52,6 +52,7 @@ pub enum EmailTransportConfig { #[serde(flatten, default)] credentials: Option, }, + AwsSes, } impl Default for EmailTransportConfig { diff --git a/crates/email/Cargo.toml b/crates/email/Cargo.toml index d425422b..48d461e9 100644 --- a/crates/email/Cargo.toml +++ b/crates/email/Cargo.toml @@ -13,6 +13,8 @@ tokio = { version = "1.15.0", features = ["macros"] } mas-templates = { path = "../templates" } mas-config = { path = "../config" } tracing = "0.1.29" +aws-sdk-sesv2 = "0.5.2" +aws-config = "0.5.2" [dependencies.lettre] version = "0.10.0-rc.4" diff --git a/crates/email/src/lib.rs b/crates/email/src/lib.rs index da6cd362..6bcdce0c 100644 --- a/crates/email/src/lib.rs +++ b/crates/email/src/lib.rs @@ -12,173 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; +mod mailer; +mod transport; -use async_trait::async_trait; -use lettre::{ - address::Envelope, - message::{Mailbox, MessageBuilder, MultiPart}, - transport::smtp::{authentication::Credentials, AsyncSmtpTransport}, - AsyncTransport, Message, Tokio1Executor, -}; -use mas_config::{EmailSmtpMode, EmailTransportConfig}; -use mas_templates::{EmailVerificationContext, Templates}; - -#[derive(Default, Clone)] -pub struct MailTransport { - inner: Arc, -} - -enum MailTransportInner { - Blackhole, - Smtp(AsyncSmtpTransport), -} - -impl TryFrom<&EmailTransportConfig> for MailTransport { - type Error = anyhow::Error; - - fn try_from(config: &EmailTransportConfig) -> Result { - let inner = match config { - EmailTransportConfig::Blackhole => MailTransportInner::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); - } - - MailTransportInner::Smtp(t.build()) - } - }; - let inner = Arc::new(inner); - Ok(Self { inner }) - } -} - -impl MailTransport { - pub async fn test_connection(&self) -> anyhow::Result<()> { - match self.inner.as_ref() { - MailTransportInner::Blackhole => {} - MailTransportInner::Smtp(t) => { - t.test_connection().await?; - } - } - - Ok(()) - } -} - -impl Default for MailTransportInner { - fn default() -> Self { - Self::Blackhole - } -} - -#[async_trait] -impl AsyncTransport for MailTransport { - type Ok = (); - type Error = anyhow::Error; - - async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result { - match self.inner.as_ref() { - MailTransportInner::Blackhole => { - tracing::warn!( - ?envelope, - "An email was supposed to be sent but no email backend is configured" - ); - } - MailTransportInner::Smtp(t) => { - t.send_raw(envelope, email).await?; - } - }; - - Ok(()) - } -} - -#[derive(Clone)] -pub struct Mailer { - templates: Templates, - transport: MailTransport, - from: Mailbox, - reply_to: Mailbox, -} - -impl Mailer { - pub fn new( - 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(), - } - } - - fn base_message(&self) -> MessageBuilder { - Message::builder() - .from(self.from.clone()) - .reply_to(self.reply_to.clone()) - } - - async fn prepare_verification_email( - &self, - to: Mailbox, - context: &EmailVerificationContext, - ) -> anyhow::Result { - let plain = self - .templates - .render_email_verification_txt(context) - .await?; - - let html = self - .templates - .render_email_verification_html(context) - .await?; - - let multipart = MultiPart::alternative_plain_html(plain, html); - - let message = self - .base_message() - // TODO: template/localize this - .subject("Verify your email address") - .to(to) - .multipart(multipart)?; - - Ok(message) - } - - pub async fn send_verification_email( - &self, - to: Mailbox, - context: &EmailVerificationContext, - ) -> anyhow::Result<()> { - let message = self.prepare_verification_email(to, context).await?; - self.transport.send(message).await?; - Ok(()) - } -} +pub use self::{mailer::Mailer, transport::Transport as MailTransport, transport::aws_ses::Transport as AwsSesTransport}; diff --git a/crates/email/src/mailer.rs b/crates/email/src/mailer.rs new file mode 100644 index 00000000..ccc675b8 --- /dev/null +++ b/crates/email/src/mailer.rs @@ -0,0 +1,88 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use lettre::{ + message::{Mailbox, MessageBuilder, MultiPart}, + AsyncTransport, Message, +}; +use mas_templates::{EmailVerificationContext, Templates}; + +use crate::MailTransport; + +#[derive(Clone)] +pub struct Mailer { + templates: Templates, + transport: MailTransport, + from: Mailbox, + reply_to: Mailbox, +} + +impl Mailer { + pub fn new( + 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(), + } + } + + fn base_message(&self) -> MessageBuilder { + Message::builder() + .from(self.from.clone()) + .reply_to(self.reply_to.clone()) + } + + async fn prepare_verification_email( + &self, + to: Mailbox, + context: &EmailVerificationContext, + ) -> anyhow::Result { + let plain = self + .templates + .render_email_verification_txt(context) + .await?; + + let html = self + .templates + .render_email_verification_html(context) + .await?; + + let multipart = MultiPart::alternative_plain_html(plain, html); + + let message = self + .base_message() + // TODO: template/localize this + .subject("Verify your email address") + .to(to) + .multipart(multipart)?; + + Ok(message) + } + + pub async fn send_verification_email( + &self, + to: Mailbox, + context: &EmailVerificationContext, + ) -> anyhow::Result<()> { + let message = self.prepare_verification_email(to, context).await?; + self.transport.send(message).await?; + Ok(()) + } +} diff --git a/crates/email/src/transport/aws_ses.rs b/crates/email/src/transport/aws_ses.rs new file mode 100644 index 00000000..5efb52eb --- /dev/null +++ b/crates/email/src/transport/aws_ses.rs @@ -0,0 +1,53 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use async_trait::async_trait; +use aws_sdk_sesv2::{ + model::{EmailContent, RawMessage}, + Blob, Client, +}; +use lettre::{address::Envelope, AsyncTransport}; + +pub struct Transport { + client: Client, +} + +impl Transport { + pub async fn from_env() -> Self { + let config = aws_config::from_env().load().await; + Self::new(&config) + } + + pub fn new(config: &aws_config::Config) -> Self { + let client = Client::new(config); + Self { client } + } +} + +#[async_trait] +impl AsyncTransport for Transport { + type Ok = (); + type Error = anyhow::Error; + + async fn send_raw(&self, _envelope: &Envelope, email: &[u8]) -> Result { + let email = Blob::new(email); + let email = RawMessage::builder().data(email).build(); + let email = EmailContent::builder().raw(email).build(); + + let req = self.client.send_email().content(email); + req.send().await?; + + Ok(()) + } +} diff --git a/crates/email/src/transport/mod.rs b/crates/email/src/transport/mod.rs new file mode 100644 index 00000000..9a113924 --- /dev/null +++ b/crates/email/src/transport/mod.rs @@ -0,0 +1,121 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use async_trait::async_trait; +use lettre::{ + address::Envelope, + transport::smtp::{authentication::Credentials, AsyncSmtpTransport}, + AsyncTransport, Tokio1Executor, +}; +use mas_config::{EmailSmtpMode, EmailTransportConfig}; + +pub mod aws_ses; + +#[derive(Default, Clone)] +pub struct Transport { + inner: Arc, +} + +enum TransportInner { + Blackhole, + Smtp(AsyncSmtpTransport), + AwsSes(aws_ses::Transport), +} + +impl Transport { + 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); + } + + TransportInner::Smtp(t.build()) + } + EmailTransportConfig::AwsSes => TransportInner::AwsSes(aws_ses::Transport::from_env().await), + }; + let inner = Arc::new(inner); + Ok(Self { inner }) + } +} + +impl Transport { + pub async fn test_connection(&self) -> anyhow::Result<()> { + match self.inner.as_ref() { + TransportInner::Blackhole => {} + TransportInner::Smtp(t) => { + t.test_connection().await?; + } + TransportInner::AwsSes(_) => {} + } + + Ok(()) + } +} + +impl Default for TransportInner { + fn default() -> Self { + Self::Blackhole + } +} + +#[async_trait] +impl AsyncTransport for Transport { + type Ok = (); + type Error = anyhow::Error; + + async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result { + match self.inner.as_ref() { + TransportInner::Blackhole => { + tracing::warn!( + ?envelope, + "An email was supposed to be sent but no email backend is configured" + ); + } + TransportInner::Smtp(t) => { + t.send_raw(envelope, email).await?; + } + TransportInner::AwsSes(t) => { + t.send_raw(envelope, email).await?; + } + }; + + Ok(()) + } +}