1use crate::{Attachment, Result, MailError};
2use lettre::Address;
3
4#[derive(Debug, Clone)]
6pub struct Message {
7 pub(crate) from: Option<String>,
8 pub(crate) to: Vec<String>,
9 pub(crate) cc: Vec<String>,
10 pub(crate) bcc: Vec<String>,
11 pub(crate) reply_to: Option<String>,
12 pub(crate) subject: Option<String>,
13 pub(crate) text: Option<String>,
14 pub(crate) html: Option<String>,
15 pub(crate) attachments: Vec<Attachment>,
16}
17
18impl Message {
19 pub fn new() -> Self {
20 Self {
21 from: None,
22 to: Vec::new(),
23 cc: Vec::new(),
24 bcc: Vec::new(),
25 reply_to: None,
26 subject: None,
27 text: None,
28 html: None,
29 attachments: Vec::new(),
30 }
31 }
32
33 pub fn from(mut self, from: impl Into<String>) -> Self {
35 self.from = Some(from.into());
36 self
37 }
38
39 pub fn to(mut self, to: impl Into<String>) -> Self {
41 self.to.push(to.into());
42 self
43 }
44
45 pub fn cc(mut self, cc: impl Into<String>) -> Self {
47 self.cc.push(cc.into());
48 self
49 }
50
51 pub fn bcc(mut self, bcc: impl Into<String>) -> Self {
53 self.bcc.push(bcc.into());
54 self
55 }
56
57 pub fn reply_to(mut self, reply_to: impl Into<String>) -> Self {
59 self.reply_to = Some(reply_to.into());
60 self
61 }
62
63 pub fn subject(mut self, subject: impl Into<String>) -> Self {
65 self.subject = Some(subject.into());
66 self
67 }
68
69 pub fn text(mut self, text: impl Into<String>) -> Self {
71 self.text = Some(text.into());
72 self
73 }
74
75 pub fn html(mut self, html: impl Into<String>) -> Self {
77 self.html = Some(html.into());
78 self
79 }
80
81 pub fn attach(mut self, attachment: Attachment) -> Self {
83 self.attachments.push(attachment);
84 self
85 }
86
87 pub(crate) fn validate(&self) -> Result<()> {
89 if self.from.is_none() {
90 return Err(MailError::MissingField("from".to_string()));
91 }
92 if self.to.is_empty() {
93 return Err(MailError::MissingField("to".to_string()));
94 }
95 if self.subject.is_none() {
96 return Err(MailError::MissingField("subject".to_string()));
97 }
98 if self.text.is_none() && self.html.is_none() {
99 return Err(MailError::MissingField("text or html".to_string()));
100 }
101
102 if let Some(from) = &self.from {
104 from.parse::<Address>()
105 .map_err(|_| MailError::InvalidAddress(format!("from: {from}")))?;
106 }
107 for to in &self.to {
108 to.parse::<Address>()
109 .map_err(|_| MailError::InvalidAddress(format!("to: {to}")))?;
110 }
111 for cc in &self.cc {
112 cc.parse::<Address>()
113 .map_err(|_| MailError::InvalidAddress(format!("cc: {cc}")))?;
114 }
115 for bcc in &self.bcc {
116 bcc.parse::<Address>()
117 .map_err(|_| MailError::InvalidAddress(format!("bcc: {bcc}")))?;
118 }
119 if let Some(reply_to) = &self.reply_to {
120 reply_to
121 .parse::<Address>()
122 .map_err(|_| MailError::InvalidAddress(format!("reply_to: {reply_to}")))?;
123 }
124
125 Ok(())
126 }
127}
128
129impl Default for Message {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::Message;
138
139 #[test]
140 fn validate_rejects_bad_recipient() {
141 let msg = Message::new()
142 .from("sender@example.com")
143 .to("bad-address")
144 .subject("hello")
145 .text("body");
146 assert!(msg.validate().is_err());
147 }
148}