mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat: Implement mailer service for sending emails
This commit is contained in:
@@ -84,6 +84,7 @@ libc.workspace = true
|
||||
libloading.workspace = true
|
||||
libloading.optional = true
|
||||
log.workspace = true
|
||||
lettre.workspace = true
|
||||
num-traits.workspace = true
|
||||
rand.workspace = true
|
||||
rand_core = { version = "0.6.4", features = ["getrandom"] }
|
||||
|
||||
@@ -16,6 +16,7 @@ use either::{
|
||||
};
|
||||
use figment::providers::{Env, Format, Toml};
|
||||
pub use figment::{Figment, value::Value as FigmentValue};
|
||||
use lettre::message::Mailbox;
|
||||
use regex::RegexSet;
|
||||
use ruma::{
|
||||
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
|
||||
@@ -756,6 +757,9 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub well_known: WellKnownConfig,
|
||||
|
||||
/// display: nested
|
||||
pub smtp: Option<SmtpConfig>,
|
||||
|
||||
/// Enable OpenTelemetry OTLP tracing export. This replaces the deprecated
|
||||
/// Jaeger exporter. Traces will be sent via OTLP to a collector (such as
|
||||
/// Jaeger) that supports the OpenTelemetry Protocol.
|
||||
@@ -2440,6 +2444,34 @@ pub struct DraupnirConfig {
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.smtp",
|
||||
optional = "true"
|
||||
)]
|
||||
pub struct SmtpConfig {
|
||||
/// A `smtp://`` URI which will be used to connect to a mail server. Setting
|
||||
/// this option enables features which depend on the ability to send email,
|
||||
/// such as self-service password resets.
|
||||
///
|
||||
/// For most modern mail servers, format the URI like this:
|
||||
/// `smtps://username:password@hostname:port`
|
||||
///
|
||||
/// For a guide on the accepted URI syntax, consult Lettre's documentation:
|
||||
/// https://docs.rs/lettre/latest/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url
|
||||
pub connection_uri: String,
|
||||
|
||||
/// The outgoing address which will be used for sending emails.
|
||||
///
|
||||
/// For a syntax guide, see https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
|
||||
///
|
||||
/// ...or if you don't want to read the RFC, for some reason:
|
||||
/// - `Name <address@domain.org>` to specify a sender name
|
||||
/// - `address@domain.org` to not use a name
|
||||
pub sender: Mailbox,
|
||||
}
|
||||
|
||||
const DEPRECATED_KEYS: &[&str] = &[
|
||||
"cache_capacity",
|
||||
"conduit_cache_capacity_modifier",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% extends "_base.txt.j2" %}
|
||||
|
||||
{% block content -%}
|
||||
If you're seeing this, SMTP is configured correctly. :3
|
||||
{%- endblock %}
|
||||
Reference in New Issue
Block a user