mas_email/
mailer.rs

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