feat: Implement mailer service for sending emails

This commit is contained in:
Ginger
2026-03-19 11:13:42 -04:00
committed by Ellis Git
parent aa79072411
commit bb7fd9efc1
15 changed files with 378 additions and 2 deletions
+1
View File
@@ -123,6 +123,7 @@ blurhash.workspace = true
blurhash.optional = true
recaptcha-verify = { version = "0.2.0", default-features = false }
yansi.workspace = true
lettre.workspace = true
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
sd-notify.workspace = true
+48
View File
@@ -0,0 +1,48 @@
use askama::Template;
use ruma::UserId;
pub trait MessageTemplate: Template {
fn subject(&self) -> String;
}
#[derive(Template)]
#[template(path = "mail/change_email.txt.j2")]
pub struct ChangeEmail<'a> {
user_id: &'a UserId,
verification_link: &'a str,
}
impl MessageTemplate for ChangeEmail<'_> {
fn subject(&self) -> String { "Verify your email address".to_owned() }
}
#[derive(Template)]
#[template(path = "mail/new_account.txt.j2")]
pub struct NewAccount<'a> {
server_name: &'a str,
verification_link: &'a str,
}
impl MessageTemplate for NewAccount<'_> {
fn subject(&self) -> String { "Create your new Matrix account".to_owned() }
}
#[derive(Template)]
#[template(path = "mail/password_reset.txt.j2")]
pub struct PasswordReset<'a> {
display_name: &'a str,
user_id: &'a UserId,
verification_link: &'a str,
}
impl MessageTemplate for PasswordReset<'_> {
fn subject(&self) -> String { format!("Password reset request for {}", &self.user_id) }
}
#[derive(Template)]
#[template(path = "mail/test.txt.j2")]
pub struct Test;
impl MessageTemplate for Test {
fn subject(&self) -> String { "Test message".to_owned() }
}
+102
View File
@@ -0,0 +1,102 @@
use std::sync::Arc;
use conduwuit::{Err, Result, err, info};
use lettre::{
AsyncSmtpTransport, AsyncTransport, Tokio1Executor,
message::{Mailbox, MessageBuilder},
};
use crate::{Args, mailer::messages::MessageTemplate};
pub mod messages;
type Transport = AsyncSmtpTransport<Tokio1Executor>;
type TransportError = lettre::transport::smtp::Error;
pub struct Service {
transport: Option<(Mailbox, Transport)>,
}
#[async_trait::async_trait]
impl crate::Service for Service {
fn build(args: Args<'_>) -> Result<Arc<Self>> {
let transport = args
.server
.config
.smtp
.as_ref()
.map(|config| {
Ok((config.sender.clone(), Transport::from_url(&config.connection_uri)?.build()))
})
.transpose()
.map_err(|err: TransportError| err!("Failed to set up SMTP transport: {err}"))?;
Ok(Arc::new(Self { transport }))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
async fn worker(self: Arc<Self>) -> Result<()> {
if let Some((_, ref transport)) = self.transport {
match transport.test_connection().await {
| Ok(true) => {
info!("SMTP connection test successful");
Ok(())
},
| Ok(false) => {
Err!("SMTP connection test failed")
},
| Err(err) => {
Err!("SMTP connection test failed: {err}")
},
}
} else {
info!("SMTP is not configured, email functionality will be unavailable");
Ok(())
}
}
}
impl Service {
/// Returns a mailer which allows email to be sent, if SMTP is configured.
#[must_use]
pub fn mailer(&self) -> Option<Mailer<'_>> {
self.transport
.as_ref()
.map(|(sender, transport)| Mailer { sender, transport })
}
}
pub struct Mailer<'a> {
sender: &'a Mailbox,
transport: &'a Transport,
}
impl Mailer<'_> {
/// Sends an email.
pub async fn send<Template: MessageTemplate>(
&self,
recipient: Mailbox,
message: Template,
) -> Result<()> {
let subject = message.subject();
let body = message
.render()
.map_err(|err| err!("Failed to render message template: {err}"))?;
let message = MessageBuilder::new()
.from(self.sender.clone())
.to(recipient)
.subject(subject)
.date_now()
.body(body)
.expect("should have been able to construct message");
self.transport
.send(message)
.await
.map_err(|err: TransportError| err!("Failed to send message: {err}"))?;
Ok(())
}
}
+1
View File
@@ -21,6 +21,7 @@ pub mod federation;
pub mod firstrun;
pub mod globals;
pub mod key_backups;
pub mod mailer;
pub mod media;
pub mod moderation;
pub mod password_reset;
+3 -1
View File
@@ -9,7 +9,7 @@ use tokio::sync::Mutex;
use crate::{
account_data, admin, announcements, antispam, appservice, client, config, emergency,
federation, firstrun, globals, key_backups,
federation, firstrun, globals, key_backups, mailer,
manager::Manager,
media, moderation, password_reset, presence, pusher, registration_tokens, resolver, rooms,
sending, server_keys,
@@ -28,6 +28,7 @@ pub struct Services {
pub key_backups: Arc<key_backups::Service>,
pub media: Arc<media::Service>,
pub password_reset: Arc<password_reset::Service>,
pub mailer: Arc<mailer::Service>,
pub presence: Arc<presence::Service>,
pub pusher: Arc<pusher::Service>,
pub registration_tokens: Arc<registration_tokens::Service>,
@@ -83,6 +84,7 @@ impl Services {
key_backups: build!(key_backups::Service),
media: build!(media::Service),
password_reset: build!(password_reset::Service),
mailer: build!(mailer::Service),
presence: build!(presence::Service),
pusher: build!(pusher::Service),
registration_tokens: build!(registration_tokens::Service),
+3
View File
@@ -0,0 +1,3 @@
{%- block content %}{% endblock %}
Message sent by Continuwuity {{ env!("CARGO_PKG_VERSION") }}. 🐈
@@ -0,0 +1,10 @@
{% extends "_base.txt.j2" %}
{% block content -%}
Hello!
Somebody, probably you, tried to associate this email address with the Matrix account {{ user_id }}.
If that's your account, and this is your email address, click this link to proceed:
{{ verification_link }}
Otherwise, you can ignore this email. The above link will expire in one hour.
{%- endblock %}
@@ -0,0 +1,10 @@
{% extends "_base.txt.j2" %}
{% block content -%}
Hello!
Somebody, probably you, tried to create a Matrix account on {{ server_name }} using this email address.
Use the link below to proceed with creating your account:
{{ verification_link }}
If you are not trying to create an account, you can ignore this email. The above link will expire in one hour.
{%- endblock %}
@@ -0,0 +1,10 @@
{% extends "_base.txt.j2" %}
{% block content -%}
Hello {{ display_name }} ({{ user_id }}),
Somebody, probably you, tried to reset your Matrix account's password.
If you requested for your password to be reset, click this link to proceed:
{{ verification_link }}
Otherwise, you can ignore this email. The above link will expire in one hour.
{%- endblock %}
+5
View File
@@ -0,0 +1,5 @@
{% extends "_base.txt.j2" %}
{% block content -%}
If you're seeing this, SMTP is configured correctly. :3
{%- endblock %}