1use std::path::PathBuf;
2
3#[derive(Debug, Clone)]
17pub struct Alternative {
18 content_type: String,
20 content: Vec<u8>,
22}
23
24impl Alternative {
25 pub fn new(content_type: impl Into<String>, content: Vec<u8>) -> Self {
36 Self {
37 content_type: content_type.into(),
38 content,
39 }
40 }
41
42 pub fn html(content: impl Into<String>) -> Self {
53 let content_str = content.into();
54 Self::new("text/html", content_str.into_bytes())
55 }
56
57 pub fn plain(content: impl Into<String>) -> Self {
68 let content_str = content.into();
69 Self::new("text/plain", content_str.into_bytes())
70 }
71
72 pub fn content_type(&self) -> &str {
74 &self.content_type
75 }
76
77 pub fn content(&self) -> &[u8] {
79 &self.content
80 }
81
82 pub fn content_as_string(&self) -> Option<&str> {
84 std::str::from_utf8(&self.content).ok()
85 }
86}
87
88#[derive(Debug, Clone)]
109pub struct Attachment {
110 filename: String,
112 content: Vec<u8>,
114 mime_type: String,
116 content_id: Option<String>,
118 inline: bool,
120}
121
122impl Attachment {
123 pub fn new(filename: impl Into<String>, content: Vec<u8>) -> Self {
138 let filename_str = filename.into();
139 let mime_type = Self::detect_mime_type(&filename_str);
140
141 Self {
142 filename: filename_str,
143 content,
144 mime_type,
145 content_id: None,
146 inline: false,
147 }
148 }
149
150 pub fn from_path(path: PathBuf, filename: impl Into<String>) -> std::io::Result<Self> {
166 let content = std::fs::read(&path)?;
168
169 let filename_str = filename.into();
170 let mime_type = Self::detect_mime_type(&filename_str);
171
172 Ok(Self {
173 filename: filename_str,
174 content,
175 mime_type,
176 content_id: None,
177 inline: false,
178 })
179 }
180
181 pub fn inline(
194 filename: impl Into<String>,
195 content: Vec<u8>,
196 content_id: impl Into<String>,
197 ) -> Self {
198 let filename_str = filename.into();
199 let mime_type = Self::detect_mime_type(&filename_str);
200
201 Self {
202 filename: filename_str,
203 content,
204 mime_type,
205 content_id: Some(content_id.into()),
206 inline: true,
207 }
208 }
209
210 pub fn with_mime_type(&mut self, mime_type: impl Into<String>) -> &mut Self {
222 self.mime_type = mime_type.into();
223 self
224 }
225
226 pub fn as_inline(&mut self, content_id: impl Into<String>) -> &mut Self {
238 self.content_id = Some(content_id.into());
239 self.inline = true;
240 self
241 }
242
243 pub fn filename(&self) -> &str {
245 &self.filename
246 }
247
248 pub fn content(&self) -> &[u8] {
250 &self.content
251 }
252
253 pub fn mime_type(&self) -> &str {
255 &self.mime_type
256 }
257
258 pub fn content_id(&self) -> Option<&str> {
260 self.content_id.as_deref()
261 }
262
263 pub fn is_inline(&self) -> bool {
265 self.inline
266 }
267
268 fn detect_mime_type(filename: &str) -> String {
270 mime_guess::from_path(filename)
271 .first()
272 .map(|mime| mime.to_string())
273 .unwrap_or_else(|| "application/octet-stream".to_string())
274 }
275}
276
277#[derive(Debug, Clone)]
283pub struct EmailMessage {
284 subject: String,
285 body: String,
286 from_email: String,
287 to: Vec<String>,
288 cc: Vec<String>,
289 bcc: Vec<String>,
290 reply_to: Vec<String>,
291 html_body: Option<String>,
292 alternatives: Vec<Alternative>,
293 attachments: Vec<Attachment>,
294 headers: Vec<(String, String)>,
295}
296
297impl EmailMessage {
298 pub fn builder() -> EmailMessageBuilder {
300 EmailMessageBuilder::default()
301 }
302
303 pub fn subject(&self) -> &str {
305 &self.subject
306 }
307
308 pub fn body(&self) -> &str {
310 &self.body
311 }
312
313 pub fn from_email(&self) -> &str {
315 &self.from_email
316 }
317
318 pub fn to(&self) -> &[String] {
320 &self.to
321 }
322
323 pub fn cc(&self) -> &[String] {
325 &self.cc
326 }
327
328 pub fn bcc(&self) -> &[String] {
330 &self.bcc
331 }
332
333 pub fn reply_to(&self) -> &[String] {
335 &self.reply_to
336 }
337
338 pub fn html_body(&self) -> Option<&str> {
340 self.html_body.as_deref()
341 }
342
343 pub fn alternatives(&self) -> &[Alternative] {
345 &self.alternatives
346 }
347
348 pub fn attachments(&self) -> &[Attachment] {
350 &self.attachments
351 }
352
353 pub fn headers(&self) -> &[(String, String)] {
355 &self.headers
356 }
357
358 pub async fn send(
360 &self,
361 backend: &dyn crate::backends::EmailBackend,
362 ) -> crate::EmailResult<()> {
363 backend.send_messages(std::slice::from_ref(self)).await?;
364 Ok(())
365 }
366
367 pub async fn send_with_backend(
369 &self,
370 backend: &dyn crate::backends::EmailBackend,
371 ) -> crate::EmailResult<()> {
372 backend.send_messages(std::slice::from_ref(self)).await?;
373 Ok(())
374 }
375}
376
377#[derive(Default)]
378pub struct EmailMessageBuilder {
379 subject: String,
380 body: String,
381 from_email: String,
382 to: Vec<String>,
383 cc: Vec<String>,
384 bcc: Vec<String>,
385 reply_to: Vec<String>,
386 html_body: Option<String>,
387 alternatives: Vec<Alternative>,
388 attachments: Vec<Attachment>,
389 headers: Vec<(String, String)>,
390}
391
392impl EmailMessageBuilder {
393 pub fn subject(mut self, subject: impl Into<String>) -> Self {
394 self.subject = subject.into();
395 self
396 }
397
398 pub fn body(mut self, body: impl Into<String>) -> Self {
399 self.body = body.into();
400 self
401 }
402
403 pub fn from(mut self, from: impl Into<String>) -> Self {
404 self.from_email = from.into();
405 self
406 }
407
408 pub fn from_email(mut self, from: impl Into<String>) -> Self {
409 self.from_email = from.into();
410 self
411 }
412
413 pub fn to(mut self, to: Vec<String>) -> Self {
414 self.to = to;
415 self
416 }
417
418 pub fn cc(mut self, cc: Vec<String>) -> Self {
419 self.cc = cc;
420 self
421 }
422
423 pub fn bcc(mut self, bcc: Vec<String>) -> Self {
424 self.bcc = bcc;
425 self
426 }
427
428 pub fn reply_to(mut self, reply_to: Vec<String>) -> Self {
429 self.reply_to = reply_to;
430 self
431 }
432
433 pub fn html(mut self, html: impl Into<String>) -> Self {
434 self.html_body = Some(html.into());
435 self
436 }
437
438 pub fn attachment(mut self, attachment: Attachment) -> Self {
439 self.attachments.push(attachment);
440 self
441 }
442
443 pub fn alternative(mut self, alternative: Alternative) -> Self {
444 self.alternatives.push(alternative);
445 self
446 }
447
448 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
449 self.headers.push((name.into(), value.into()));
450 self
451 }
452
453 pub fn build(self) -> crate::EmailResult<EmailMessage> {
459 use crate::validation::{
460 check_header_injection, validate_email, validate_email_list, validate_header_name,
461 };
462
463 if !self.from_email.is_empty() {
465 validate_email(&self.from_email)?;
466 }
467
468 validate_email_list(&self.to)?;
470 validate_email_list(&self.cc)?;
471 validate_email_list(&self.bcc)?;
472 validate_email_list(&self.reply_to)?;
473
474 check_header_injection(&self.subject)?;
476
477 for (name, value) in &self.headers {
479 validate_header_name(name)?;
480 check_header_injection(value)?;
481 }
482
483 Ok(EmailMessage {
484 subject: self.subject,
485 body: self.body,
486 from_email: self.from_email,
487 to: self.to,
488 cc: self.cc,
489 bcc: self.bcc,
490 reply_to: self.reply_to,
491 html_body: self.html_body,
492 alternatives: self.alternatives,
493 attachments: self.attachments,
494 headers: self.headers,
495 })
496 }
497}