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        err,
115    )]
116    pub async fn send_verification_email(
117        &self,
118        to: Mailbox,
119        context: &WithLanguage<EmailVerificationContext>,
120    ) -> Result<(), Error> {
121        let message = self.prepare_verification_email(to, context)?;
122        self.transport.send(message).await?;
123        Ok(())
124    }
125
126    /// Send the recovery email to a user
127    ///
128    /// # Errors
129    ///
130    /// Will return `Err` if the email failed rendering or failed sending
131    #[tracing::instrument(
132        name = "email.recovery.send",
133        skip_all,
134        fields(
135            email.to = %to,
136            email.language = %context.language(),
137            user.id = %context.user().id,
138            user_recovery_session.id = %context.session().id,
139        ),
140        err,
141    )]
142    pub async fn send_recovery_email(
143        &self,
144        to: Mailbox,
145        context: &WithLanguage<EmailRecoveryContext>,
146    ) -> Result<(), Error> {
147        let message = self.prepare_recovery_email(to, context)?;
148        self.transport.send(message).await?;
149        Ok(())
150    }
151
152    /// Test the connetion to the mail server
153    ///
154    /// # Errors
155    ///
156    /// Returns an error if the connection failed
157    #[tracing::instrument(name = "email.test_connection", skip_all, err)]
158    pub async fn test_connection(&self) -> Result<(), crate::transport::Error> {
159        self.transport.test_connection().await
160    }
161}