Skip to main content

mas_email/
mailer.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8//! Send emails to users
9
10use lettre::{
11    AsyncTransport, Message,
12    message::{Mailbox, MessageBuilder, MultiPart},
13};
14use mas_templates::{
15    EmailInviteContext, EmailRecoveryContext, EmailVerificationContext, Templates, WithLanguage,
16};
17use thiserror::Error;
18
19use crate::MailTransport;
20
21/// Helps sending mails to users
22#[derive(Clone)]
23pub struct Mailer {
24    templates: Templates,
25    transport: MailTransport,
26    from: Mailbox,
27    reply_to: Mailbox,
28}
29
30#[derive(Debug, Error)]
31#[error(transparent)]
32pub enum Error {
33    Transport(#[from] crate::transport::Error),
34    Templates(#[from] mas_templates::TemplateError),
35    Content(#[from] lettre::error::Error),
36}
37
38impl Mailer {
39    /// Constructs a new [`Mailer`]
40    #[must_use]
41    pub fn new(
42        templates: Templates,
43        transport: MailTransport,
44        from: Mailbox,
45        reply_to: Mailbox,
46    ) -> Self {
47        Self {
48            templates,
49            transport,
50            from,
51            reply_to,
52        }
53    }
54
55    fn base_message(&self) -> MessageBuilder {
56        Message::builder()
57            .from(self.from.clone())
58            .reply_to(self.reply_to.clone())
59            // By passing `None`, lettre generates a random message ID
60            // with a random UUID and the hostname for us
61            .message_id(None)
62    }
63
64    fn prepare_verification_email(
65        &self,
66        to: Mailbox,
67        context: &WithLanguage<EmailVerificationContext>,
68    ) -> Result<Message, Error> {
69        let plain = self.templates.render_email_verification_txt(context)?;
70
71        let html = self.templates.render_email_verification_html(context)?;
72
73        let multipart = MultiPart::alternative_plain_html(plain, html);
74
75        let subject = self.templates.render_email_verification_subject(context)?;
76
77        let message = self
78            .base_message()
79            .subject(subject.trim())
80            .to(to)
81            .multipart(multipart)?;
82
83        Ok(message)
84    }
85
86    fn prepare_recovery_email(
87        &self,
88        to: Mailbox,
89        context: &WithLanguage<EmailRecoveryContext>,
90    ) -> Result<Message, Error> {
91        let plain = self.templates.render_email_recovery_txt(context)?;
92
93        let html = self.templates.render_email_recovery_html(context)?;
94
95        let multipart = MultiPart::alternative_plain_html(plain, html);
96
97        let subject = self.templates.render_email_recovery_subject(context)?;
98
99        let message = self
100            .base_message()
101            .subject(subject.trim())
102            .to(to)
103            .multipart(multipart)?;
104
105        Ok(message)
106    }
107
108    fn prepare_invite_email(
109        &self,
110        to: Mailbox,
111        context: &WithLanguage<EmailInviteContext>,
112    ) -> Result<Message, Error> {
113        let plain = self.templates.render_email_invite_txt(context)?;
114
115        let html = self.templates.render_email_invite_html(context)?;
116
117        let multipart = MultiPart::alternative_plain_html(plain, html);
118
119        let subject = self.templates.render_email_invite_subject(context)?;
120
121        let message = self
122            .base_message()
123            .subject(subject.trim())
124            .to(to)
125            .multipart(multipart)?;
126
127        Ok(message)
128    }
129
130    /// Send the verification email to a user
131    ///
132    /// # Errors
133    ///
134    /// Will return `Err` if the email failed rendering or failed sending
135    #[tracing::instrument(
136        name = "email.verification.send",
137        skip_all,
138        fields(
139            email.to = %to,
140            email.language = %context.language(),
141        ),
142    )]
143    pub async fn send_verification_email(
144        &self,
145        to: Mailbox,
146        context: &WithLanguage<EmailVerificationContext>,
147    ) -> Result<(), Error> {
148        let message = self.prepare_verification_email(to, context)?;
149        self.transport.send(message).await?;
150        Ok(())
151    }
152
153    /// Send the recovery email to a user
154    ///
155    /// # Errors
156    ///
157    /// Will return `Err` if the email failed rendering or failed sending
158    #[tracing::instrument(
159        name = "email.recovery.send",
160        skip_all,
161        fields(
162            email.to = %to,
163            email.language = %context.language(),
164            user.id = %context.user().id,
165            user_recovery_session.id = %context.session().id,
166        ),
167    )]
168    pub async fn send_recovery_email(
169        &self,
170        to: Mailbox,
171        context: &WithLanguage<EmailRecoveryContext>,
172    ) -> Result<(), Error> {
173        let message = self.prepare_recovery_email(to, context)?;
174        self.transport.send(message).await?;
175        Ok(())
176    }
177
178    /// Send an invite email
179    ///
180    /// # Errors
181    ///
182    /// Will return `Err` if the email failed rendering or failed sending
183    #[tracing::instrument(
184        name = "email.invite.send",
185        skip_all,
186        fields(
187            email.to = %to,
188            email.language = %context.language(),
189        ),
190    )]
191    pub async fn send_invite_email(
192        &self,
193        to: Mailbox,
194        context: &WithLanguage<EmailInviteContext>,
195    ) -> Result<(), Error> {
196        let message = self.prepare_invite_email(to, context)?;
197        self.transport.send(message).await?;
198        Ok(())
199    }
200
201    /// Test the connetion to the mail server
202    ///
203    /// # Errors
204    ///
205    /// Returns an error if the connection failed
206    #[tracing::instrument(name = "email.test_connection", skip_all)]
207    pub async fn test_connection(&self) -> Result<(), crate::transport::Error> {
208        self.transport.test_connection().await
209    }
210}