mas_email/
mailer.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE 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    }
57
58    fn prepare_verification_email(
59        &self,
60        to: Mailbox,
61        context: &WithLanguage<EmailVerificationContext>,
62    ) -> Result<Message, Error> {
63        let plain = self.templates.render_email_verification_txt(context)?;
64
65        let html = self.templates.render_email_verification_html(context)?;
66
67        let multipart = MultiPart::alternative_plain_html(plain, html);
68
69        let subject = self.templates.render_email_verification_subject(context)?;
70
71        let message = self
72            .base_message()
73            .subject(subject.trim())
74            .to(to)
75            .multipart(multipart)?;
76
77        Ok(message)
78    }
79
80    fn prepare_recovery_email(
81        &self,
82        to: Mailbox,
83        context: &WithLanguage<EmailRecoveryContext>,
84    ) -> Result<Message, Error> {
85        let plain = self.templates.render_email_recovery_txt(context)?;
86
87        let html = self.templates.render_email_recovery_html(context)?;
88
89        let multipart = MultiPart::alternative_plain_html(plain, html);
90
91        let subject = self.templates.render_email_recovery_subject(context)?;
92
93        let message = self
94            .base_message()
95            .subject(subject.trim())
96            .to(to)
97            .multipart(multipart)?;
98
99        Ok(message)
100    }
101
102    /// Send the verification email to a user
103    ///
104    /// # Errors
105    ///
106    /// Will return `Err` if the email failed rendering or failed sending
107    #[tracing::instrument(
108        name = "email.verification.send",
109        skip_all,
110        fields(
111            email.to = %to,
112            email.language = %context.language(),
113        ),
114    )]
115    pub async fn send_verification_email(
116        &self,
117        to: Mailbox,
118        context: &WithLanguage<EmailVerificationContext>,
119    ) -> Result<(), Error> {
120        let message = self.prepare_verification_email(to, context)?;
121        self.transport.send(message).await?;
122        Ok(())
123    }
124
125    /// Send the recovery email to a user
126    ///
127    /// # Errors
128    ///
129    /// Will return `Err` if the email failed rendering or failed sending
130    #[tracing::instrument(
131        name = "email.recovery.send",
132        skip_all,
133        fields(
134            email.to = %to,
135            email.language = %context.language(),
136            user.id = %context.user().id,
137            user_recovery_session.id = %context.session().id,
138        ),
139    )]
140    pub async fn send_recovery_email(
141        &self,
142        to: Mailbox,
143        context: &WithLanguage<EmailRecoveryContext>,
144    ) -> Result<(), Error> {
145        let message = self.prepare_recovery_email(to, context)?;
146        self.transport.send(message).await?;
147        Ok(())
148    }
149
150    /// Test the connetion to the mail server
151    ///
152    /// # Errors
153    ///
154    /// Returns an error if the connection failed
155    #[tracing::instrument(name = "email.test_connection", skip_all)]
156    pub async fn test_connection(&self) -> Result<(), crate::transport::Error> {
157        self.transport.test_connection().await
158    }
159}