1use std::error::Error;
8use lettre::transport::smtp::authentication::Credentials;
9use lettre::{Message, AsyncSmtpTransport, AsyncTransport, Tokio1Executor};
10use crate::app::Config;
11
12#[derive(Debug, Clone)]
14pub struct MailAttachment {
15 pub name: String,
16 pub body: Vec<u8>,
17 pub content_type: String,
18}
19
20#[derive(Debug, Clone)]
22pub struct Mailer {
23 to: Vec<String>,
24 cc: Vec<String>,
25 bcc: Vec<String>,
26 subject: Option<String>,
27 html_body: Option<String>,
28 text_body: Option<String>,
29 from_name: Option<String>,
30 from_address: Option<String>,
31 attachments: Vec<MailAttachment>,
32}
33
34impl Default for Mailer {
35 fn default() -> Self {
36 Self::new()
37 }
38}
39
40impl Mailer {
41 pub fn new() -> Self {
43 Self {
44 to: Vec::new(),
45 cc: Vec::new(),
46 bcc: Vec::new(),
47 subject: None,
48 html_body: None,
49 text_body: None,
50 from_name: None,
51 from_address: None,
52 attachments: Vec::new(),
53 }
54 }
55
56 pub fn to(mut self, to: impl Into<String>) -> Self {
58 self.to.push(to.into());
59 self
60 }
61
62 pub fn cc(mut self, cc: impl Into<String>) -> Self {
64 self.cc.push(cc.into());
65 self
66 }
67
68 pub fn bcc(mut self, bcc: impl Into<String>) -> Self {
70 self.bcc.push(bcc.into());
71 self
72 }
73
74 pub fn subject(mut self, subject: impl Into<String>) -> Self {
76 self.subject = Some(subject.into());
77 self
78 }
79
80 pub fn html(mut self, body: impl Into<String>) -> Self {
82 self.html_body = Some(body.into());
83 self
84 }
85
86 pub fn text(mut self, body: impl Into<String>) -> Self {
88 self.text_body = Some(body.into());
89 self
90 }
91
92 pub fn from(mut self, name: impl Into<String>, address: impl Into<String>) -> Self {
94 self.from_name = Some(name.into());
95 self.from_address = Some(address.into());
96 self
97 }
98
99 pub fn attach(mut self, name: impl Into<String>, body: Vec<u8>, content_type: impl Into<String>) -> Self {
101 self.attachments.push(MailAttachment {
102 name: name.into(),
103 body,
104 content_type: content_type.into(),
105 });
106 self
107 }
108
109 pub async fn send(self) -> Result<(), Box<dyn Error + Send + Sync>> {
111 let config = Config::load();
112
113 let from_name = self.from_name.unwrap_or(config.mail_from_name);
114 let from_address = self.from_address.unwrap_or(config.mail_from_address);
115
116 let mut builder = Message::builder()
117 .from(format!("{} <{}>", from_name, from_address).parse()?);
118
119 if self.to.is_empty() {
120 return Err("At least one recipient ('to') must be specified".into());
121 }
122
123 for recipient in self.to {
124 builder = builder.to(recipient.parse()?);
125 }
126
127 for cc_rec in self.cc {
128 builder = builder.cc(cc_rec.parse()?);
129 }
130
131 for bcc_rec in self.bcc {
132 builder = builder.bcc(bcc_rec.parse()?);
133 }
134
135 if let Some(sub) = self.subject {
136 builder = builder.subject(sub);
137 }
138
139 let email = match (self.html_body, self.text_body) {
141 (Some(html), Some(text)) => {
142 let alt = lettre::message::MultiPart::alternative()
143 .singlepart(lettre::message::SinglePart::plain(text))
144 .singlepart(lettre::message::SinglePart::html(html));
145 if !self.attachments.is_empty() {
146 let mut mixed = lettre::message::MultiPart::mixed().multipart(alt);
147 for att in self.attachments {
148 let mime = att.content_type.parse::<lettre::message::header::ContentType>()?;
149 let single_part = lettre::message::SinglePart::builder()
150 .header(mime)
151 .header(lettre::message::header::ContentDisposition::attachment(&att.name))
152 .body(att.body);
153 mixed = mixed.singlepart(single_part);
154 }
155 builder.multipart(mixed)?
156 } else {
157 builder.multipart(alt)?
158 }
159 }
160 (Some(html), None) => {
161 if !self.attachments.is_empty() {
162 let alt = lettre::message::MultiPart::alternative()
163 .singlepart(lettre::message::SinglePart::html(html));
164 let mut mixed = lettre::message::MultiPart::mixed().multipart(alt);
165 for att in self.attachments {
166 let mime = att.content_type.parse::<lettre::message::header::ContentType>()?;
167 let single_part = lettre::message::SinglePart::builder()
168 .header(mime)
169 .header(lettre::message::header::ContentDisposition::attachment(&att.name))
170 .body(att.body);
171 mixed = mixed.singlepart(single_part);
172 }
173 builder.multipart(mixed)?
174 } else {
175 builder
176 .header(lettre::message::header::ContentType::TEXT_HTML)
177 .body(html)?
178 }
179 }
180 (None, Some(text)) => {
181 if !self.attachments.is_empty() {
182 let alt = lettre::message::MultiPart::alternative()
183 .singlepart(lettre::message::SinglePart::plain(text));
184 let mut mixed = lettre::message::MultiPart::mixed().multipart(alt);
185 for att in self.attachments {
186 let mime = att.content_type.parse::<lettre::message::header::ContentType>()?;
187 let single_part = lettre::message::SinglePart::builder()
188 .header(mime)
189 .header(lettre::message::header::ContentDisposition::attachment(&att.name))
190 .body(att.body);
191 mixed = mixed.singlepart(single_part);
192 }
193 builder.multipart(mixed)?
194 } else {
195 builder
196 .header(lettre::message::header::ContentType::TEXT_PLAIN)
197 .body(text)?
198 }
199 }
200 (None, None) => {
201 return Err("Email body (HTML or Text) is required".into());
202 }
203 };
204
205 let creds = Credentials::new(config.mail_username, config.mail_password);
207
208 let mailer = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&config.mail_host)?
209 .port(config.mail_port)
210 .credentials(creds)
211 .build();
212
213 mailer.send(email).await?;
214
215 Ok(())
216 }
217}
218
219pub struct MailService;
221
222impl MailService {
223 pub async fn send_email(
225 to: &str,
226 subject: &str,
227 body: &str,
228 ) -> Result<(), Box<dyn Error + Send + Sync>> {
229 Mailer::new()
230 .to(to)
231 .subject(subject)
232 .html(body)
233 .send()
234 .await
235 }
236}