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