1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-29 22:01:14 +03:00

Add AWS SES backend to send email

This commit is contained in:
Quentin Gliech
2022-01-24 16:31:53 +01:00
parent 7b487e184a
commit 1355be8fb8
8 changed files with 627 additions and 173 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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"

View File

@ -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};

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}