1use crate::mimepart::AttachmentOptions;
2use crate::{HeaderMap, MailParsingError, MimePart};
3
4#[derive(Default)]
5pub struct MessageBuilder<'a> {
6 text: Option<String>,
7 html: Option<String>,
8 headers: HeaderMap<'a>,
9 inline: Vec<MimePart<'a>>,
10 attached: Vec<MimePart<'a>>,
11 stable_content: bool,
12}
13
14impl<'a> MessageBuilder<'a> {
15 pub fn new() -> Self {
16 Self::default()
17 }
18
19 pub fn set_stable_content(&mut self, v: bool) {
20 self.stable_content = v;
21 }
22
23 pub fn text_plain(&mut self, text: &str) {
24 self.text.replace(text.to_string());
25 }
26
27 pub fn text_html(&mut self, html: &str) {
28 self.html.replace(html.to_string());
29 }
30
31 pub fn attach(&mut self, content_type: &str, data: &[u8], opts: Option<&AttachmentOptions>) {
32 let is_inline = opts.map(|opt| opt.inline).unwrap_or(false);
33
34 let part = MimePart::new_binary(content_type, data, opts);
35
36 if is_inline {
37 self.inline.push(part);
38 } else {
39 self.attached.push(part);
40 }
41 }
42
43 pub fn attach_part(&mut self, part: MimePart<'a>) {
44 let is_inline = part
45 .headers()
46 .content_disposition()
47 .ok()
48 .and_then(|opt_cd| opt_cd.map(|cd| cd.value == "inline"))
49 .unwrap_or(false);
50 if is_inline {
51 self.inline.push(part);
52 } else {
53 self.attached.push(part);
54 }
55 }
56
57 pub fn build(self) -> Result<MimePart<'a>, MailParsingError> {
58 let text = self.text.as_deref().map(MimePart::new_text_plain);
59 let html = self.html.as_deref().map(MimePart::new_html);
60
61 let content_node = match (text, html) {
62 (Some(t), Some(h)) => MimePart::new_multipart(
63 "multipart/alternative",
64 vec![t, h],
65 if self.stable_content {
66 Some("ma-boundary")
67 } else {
68 None
69 },
70 ),
71 (Some(t), None) => t,
72 (None, Some(h)) => h,
73 (None, None) => {
74 return Err(MailParsingError::BuildError(
75 "no text or html part was specified",
76 ))
77 }
78 };
79
80 let content_node = if !self.inline.is_empty() {
81 let mut parts = Vec::with_capacity(self.inline.len() + 1);
82 parts.push(content_node);
83 parts.extend(self.inline);
84 MimePart::new_multipart(
85 "multipart/related",
86 parts,
87 if self.stable_content {
88 Some("mr-boundary")
89 } else {
90 None
91 },
92 )
93 } else {
94 content_node
95 };
96
97 let mut root = if !self.attached.is_empty() {
98 let mut parts = Vec::with_capacity(self.attached.len() + 1);
99 parts.push(content_node);
100 parts.extend(self.attached);
101 MimePart::new_multipart(
102 "multipart/mixed",
103 parts,
104 if self.stable_content {
105 Some("mm-boundary")
106 } else {
107 None
108 },
109 )
110 } else {
111 content_node
112 };
113
114 root.headers_mut().headers.extend(self.headers.headers);
115
116 if root.headers().mime_version()?.is_none() {
117 root.headers_mut().set_mime_version("1.0");
118 }
119
120 if root.headers().date()?.is_none() {
121 if self.stable_content {
122 root.headers_mut().set_date(
123 chrono::DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200")
124 .expect("test date to be valid"),
125 );
126 } else {
127 root.headers_mut().set_date(chrono::Utc::now());
128 };
129 }
130
131 Ok(root)
136 }
137}
138
139impl<'a> std::ops::Deref for MessageBuilder<'a> {
140 type Target = HeaderMap<'a>;
141 fn deref(&self) -> &HeaderMap<'a> {
142 &self.headers
143 }
144}
145
146impl<'a> std::ops::DerefMut for MessageBuilder<'a> {
147 fn deref_mut(&mut self) -> &mut HeaderMap<'a> {
148 &mut self.headers
149 }
150}
151
152#[cfg(test)]
153mod test {
154 use super::*;
155
156 #[test]
157 fn basic() {
158 let mut b = MessageBuilder::new();
159 b.set_stable_content(true);
160 b.set_subject("Hello there! 🍉");
161 b.text_plain("This is the body! 👻");
162 b.text_html("<b>this is html 🚀</b>");
163 let msg = b.build().unwrap();
164 k9::snapshot!(
165 msg.to_message_string(),
166 r#"
167Content-Type: multipart/alternative;\r
168\tboundary="ma-boundary"\r
169Subject: =?UTF-8?q?Hello_there!_=F0=9F=8D=89?=\r
170Mime-Version: 1.0\r
171Date: Tue, 1 Jul 2003 10:52:37 +0200\r
172\r
173--ma-boundary\r
174Content-Type: text/plain;\r
175\tcharset="utf-8"\r
176Content-Transfer-Encoding: quoted-printable\r
177\r
178This is the body! =F0=9F=91=BB\r
179--ma-boundary\r
180Content-Type: text/html;\r
181\tcharset="utf-8"\r
182Content-Transfer-Encoding: quoted-printable\r
183\r
184<b>this is html =F0=9F=9A=80</b>\r
185--ma-boundary--\r
186
187"#
188 );
189 }
190}