You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
Add AWS SES backend to send email
This commit is contained in:
359
Cargo.lock
generated
359
Cargo.lock
generated
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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<Credentials>,
|
||||
},
|
||||
AwsSes,
|
||||
}
|
||||
|
||||
impl Default for EmailTransportConfig {
|
||||
|
@ -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"
|
||||
|
@ -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<MailTransportInner>,
|
||||
}
|
||||
|
||||
enum MailTransportInner {
|
||||
Blackhole,
|
||||
Smtp(AsyncSmtpTransport<Tokio1Executor>),
|
||||
}
|
||||
|
||||
impl TryFrom<&EmailTransportConfig> for MailTransport {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(config: &EmailTransportConfig) -> Result<Self, Self::Error> {
|
||||
let inner = match config {
|
||||
EmailTransportConfig::Blackhole => MailTransportInner::Blackhole,
|
||||
EmailTransportConfig::Smtp {
|
||||
mode,
|
||||
hostname,
|
||||
credentials,
|
||||
port,
|
||||
} => {
|
||||
let mut t = match mode {
|
||||
EmailSmtpMode::Plain => {
|
||||
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(hostname)
|
||||
}
|
||||
EmailSmtpMode::StartTls => {
|
||||
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(hostname)?
|
||||
}
|
||||
EmailSmtpMode::Tls => AsyncSmtpTransport::<Tokio1Executor>::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<Self::Ok, Self::Error> {
|
||||
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<Message> {
|
||||
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};
|
||||
|
88
crates/email/src/mailer.rs
Normal file
88
crates/email/src/mailer.rs
Normal file
@ -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<Message> {
|
||||
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(())
|
||||
}
|
||||
}
|
53
crates/email/src/transport/aws_ses.rs
Normal file
53
crates/email/src/transport/aws_ses.rs
Normal file
@ -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<Self::Ok, Self::Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
121
crates/email/src/transport/mod.rs
Normal file
121
crates/email/src/transport/mod.rs
Normal file
@ -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<TransportInner>,
|
||||
}
|
||||
|
||||
enum TransportInner {
|
||||
Blackhole,
|
||||
Smtp(AsyncSmtpTransport<Tokio1Executor>),
|
||||
AwsSes(aws_ses::Transport),
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
pub async fn from_config(config: &EmailTransportConfig) -> Result<Self, anyhow::Error> {
|
||||
let inner = match config {
|
||||
EmailTransportConfig::Blackhole => TransportInner::Blackhole,
|
||||
EmailTransportConfig::Smtp {
|
||||
mode,
|
||||
hostname,
|
||||
credentials,
|
||||
port,
|
||||
} => {
|
||||
let mut t = match mode {
|
||||
EmailSmtpMode::Plain => {
|
||||
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(hostname)
|
||||
}
|
||||
EmailSmtpMode::StartTls => {
|
||||
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(hostname)?
|
||||
}
|
||||
EmailSmtpMode::Tls => AsyncSmtpTransport::<Tokio1Executor>::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<Self::Ok, Self::Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user