1use 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#[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 #[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 #[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 #[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 #[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}