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