1#![doc = include_str!("../README.md")]
8#![deny(rust_2018_idioms)]
9#[forbid(unsafe_code)]
10pub mod encoders;
11pub mod headers;
12pub mod mime;
13
14use std::{
15 borrow::Cow,
16 io::{self, Write},
17};
18
19use headers::{
20 address::Address,
21 content_type::ContentType,
22 date::Date,
23 message_id::{generate_message_id_header, MessageId},
24 text::Text,
25 Header, HeaderType,
26};
27use mime::{BodyPart, MimePart};
28
29#[derive(Clone, Debug)]
31pub struct MessageBuilder<'x> {
32 pub headers: Vec<(Cow<'x, str>, HeaderType<'x>)>,
33 pub html_body: Option<MimePart<'x>>,
34 pub text_body: Option<MimePart<'x>>,
35 pub attachments: Option<Vec<MimePart<'x>>>,
36 pub body: Option<MimePart<'x>>,
37}
38
39impl Default for MessageBuilder<'_> {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45impl<'x> MessageBuilder<'x> {
46 pub fn new() -> Self {
48 MessageBuilder {
49 headers: Vec::new(),
50 html_body: None,
51 text_body: None,
52 attachments: None,
53 body: None,
54 }
55 }
56
57 pub fn message_id(self, value: impl Into<MessageId<'x>>) -> Self {
60 self.header("Message-ID", value.into())
61 }
62
63 pub fn in_reply_to(self, value: impl Into<MessageId<'x>>) -> Self {
65 self.header("In-Reply-To", value.into())
66 }
67
68 pub fn references(self, value: impl Into<MessageId<'x>>) -> Self {
70 self.header("References", value.into())
71 }
72
73 pub fn sender(self, value: impl Into<Address<'x>>) -> Self {
75 self.header("Sender", value.into())
76 }
77
78 pub fn from(self, value: impl Into<Address<'x>>) -> Self {
80 self.header("From", value.into())
81 }
82
83 pub fn to(self, value: impl Into<Address<'x>>) -> Self {
85 self.header("To", value.into())
86 }
87
88 pub fn cc(self, value: impl Into<Address<'x>>) -> Self {
90 self.header("Cc", value.into())
91 }
92
93 pub fn bcc(self, value: impl Into<Address<'x>>) -> Self {
95 self.header("Bcc", value.into())
96 }
97
98 pub fn reply_to(self, value: impl Into<Address<'x>>) -> Self {
100 self.header("Reply-To", value.into())
101 }
102
103 pub fn subject(self, value: impl Into<Text<'x>>) -> Self {
105 self.header("Subject", value.into())
106 }
107
108 pub fn date(self, value: impl Into<Date>) -> Self {
111 self.header("Date", value.into())
112 }
113
114 pub fn header(
116 mut self,
117 header: impl Into<Cow<'x, str>>,
118 value: impl Into<HeaderType<'x>>,
119 ) -> Self {
120 self.headers.push((header.into(), value.into()));
121 self
122 }
123
124 pub fn headers<T, U, V>(mut self, header: T, values: U) -> Self
126 where
127 T: Into<Cow<'x, str>>,
128 U: IntoIterator<Item = V>,
129 V: Into<HeaderType<'x>>,
130 {
131 let header = header.into();
132
133 for value in values {
134 self.headers.push((header.clone(), value.into()));
135 }
136
137 self
138 }
139
140 pub fn text_body(mut self, value: impl Into<Cow<'x, str>>) -> Self {
144 self.text_body = Some(MimePart::new("text/plain", BodyPart::Text(value.into())));
145 self
146 }
147
148 pub fn html_body(mut self, value: impl Into<Cow<'x, str>>) -> Self {
152 self.html_body = Some(MimePart::new("text/html", BodyPart::Text(value.into())));
153 self
154 }
155
156 pub fn attachment(
158 mut self,
159 content_type: impl Into<ContentType<'x>>,
160 filename: impl Into<Cow<'x, str>>,
161 value: impl Into<BodyPart<'x>>,
162 ) -> Self {
163 self.attachments
164 .get_or_insert_with(Vec::new)
165 .push(MimePart::new(content_type, value).attachment(filename));
166 self
167 }
168
169 pub fn inline(
171 mut self,
172 content_type: impl Into<ContentType<'x>>,
173 cid: impl Into<Cow<'x, str>>,
174 value: impl Into<BodyPart<'x>>,
175 ) -> Self {
176 self.attachments
177 .get_or_insert_with(Vec::new)
178 .push(MimePart::new(content_type, value).inline().cid(cid));
179 self
180 }
181
182 pub fn body(mut self, value: MimePart<'x>) -> Self {
184 self.body = Some(value);
185 self
186 }
187
188 pub fn write_to(self, mut output: impl Write) -> io::Result<()> {
190 let mut has_date = false;
191 let mut has_message_id = false;
192 let mut has_mime_version = false;
193
194 for (header_name, header_value) in &self.headers {
195 if !has_date && header_name == "Date" {
196 has_date = true;
197 } else if !has_message_id && header_name == "Message-ID" {
198 has_message_id = true;
199 } else if !has_mime_version && header_name == "MIME-Version" {
200 has_mime_version = true;
201 }
202
203 output.write_all(header_name.as_bytes())?;
204 output.write_all(b": ")?;
205 header_value.write_header(&mut output, header_name.len() + 2)?;
206 }
207
208 if !has_message_id {
209 output.write_all(b"Message-ID: ")?;
210
211 #[cfg(feature = "gethostname")]
212 generate_message_id_header(
213 &mut output,
214 gethostname::gethostname().to_str().unwrap_or("localhost"),
215 )?;
216
217 #[cfg(not(feature = "gethostname"))]
218 generate_message_id_header(&mut output, "localhost")?;
219
220 output.write_all(b"\r\n")?;
221 }
222
223 if !has_date {
224 output.write_all(b"Date: ")?;
225 output.write_all(Date::now().to_rfc822().as_bytes())?;
226 output.write_all(b"\r\n")?;
227 }
228
229 if !has_mime_version {
230 output.write_all(b"MIME-Version: 1.0\r\n")?;
231 }
232
233 self.write_body(output)
234 }
235
236 pub fn write_body(self, output: impl Write) -> io::Result<()> {
238 (if let Some(body) = self.body {
239 body
240 } else {
241 match (self.text_body, self.html_body, self.attachments) {
242 (Some(text), Some(html), Some(attachments)) => {
243 let mut parts = Vec::with_capacity(attachments.len() + 1);
244 parts.push(MimePart::new("multipart/alternative", vec![text, html]));
245 parts.extend(attachments);
246
247 MimePart::new("multipart/mixed", parts)
248 }
249 (Some(text), Some(html), None) => {
250 MimePart::new("multipart/alternative", vec![text, html])
251 }
252 (Some(text), None, Some(attachments)) => {
253 let mut parts = Vec::with_capacity(attachments.len() + 1);
254 parts.push(text);
255 parts.extend(attachments);
256 MimePart::new("multipart/mixed", parts)
257 }
258 (Some(text), None, None) => text,
259 (None, Some(html), Some(attachments)) => {
260 let mut parts = Vec::with_capacity(attachments.len() + 1);
261 parts.push(html);
262 parts.extend(attachments);
263 MimePart::new("multipart/mixed", parts)
264 }
265 (None, Some(html), None) => html,
266 (None, None, Some(attachments)) => MimePart::new("multipart/mixed", attachments),
267 (None, None, None) => MimePart::new("text/plain", "\n"),
268 }
269 })
270 .write_part(output)?;
271
272 Ok(())
273 }
274
275 pub fn write_to_vec(self) -> io::Result<Vec<u8>> {
277 let mut output = Vec::new();
278 self.write_to(&mut output)?;
279 Ok(output)
280 }
281
282 pub fn write_to_string(self) -> io::Result<String> {
284 let mut output = Vec::new();
285 self.write_to(&mut output)?;
286 String::from_utf8(output).map_err(io::Error::other)
287 }
288}
289
290#[cfg(test)]
291mod tests {
292
293 use mail_parser::MessageParser;
294
295 use crate::{
296 headers::{address::Address, url::URL},
297 mime::MimePart,
298 MessageBuilder,
299 };
300
301 #[test]
302 fn build_nested_message() {
303 let output = MessageBuilder::new()
304 .from(Address::new_address("John Doe".into(), "john@doe.com"))
305 .to(Address::new_address("Jane Doe".into(), "jane@doe.com"))
306 .subject("RFC 8621 Section 4.1.4 test")
307 .body(MimePart::new(
308 "multipart/mixed",
309 vec![
310 MimePart::new("text/plain", "Part A contents go here...").inline(),
311 MimePart::new(
312 "multipart/mixed",
313 vec![
314 MimePart::new(
315 "multipart/alternative",
316 vec![
317 MimePart::new(
318 "multipart/mixed",
319 vec![
320 MimePart::new(
321 "text/plain",
322 "Part B contents go here...",
323 )
324 .inline(),
325 MimePart::new(
326 "image/jpeg",
327 "Part C contents go here...".as_bytes(),
328 )
329 .inline(),
330 MimePart::new(
331 "text/plain",
332 "Part D contents go here...",
333 )
334 .inline(),
335 ],
336 ),
337 MimePart::new(
338 "multipart/related",
339 vec![
340 MimePart::new(
341 "text/html",
342 "Part E contents go here...",
343 )
344 .inline(),
345 MimePart::new(
346 "image/jpeg",
347 "Part F contents go here...".as_bytes(),
348 ),
349 ],
350 ),
351 ],
352 ),
353 MimePart::new("image/jpeg", "Part G contents go here...".as_bytes())
354 .attachment("image_G.jpg"),
355 MimePart::new(
356 "application/x-excel",
357 "Part H contents go here...".as_bytes(),
358 ),
359 MimePart::new(
360 "x-message/rfc822",
361 "Part J contents go here...".as_bytes(),
362 ),
363 ],
364 ),
365 MimePart::new("text/plain", "Part K contents go here...").inline(),
366 ],
367 ))
368 .write_to_vec()
369 .unwrap();
370 MessageParser::new().parse(&output).unwrap();
371 }
373
374 #[test]
375 fn build_message() {
376 let output = MessageBuilder::new()
377 .from(("John Doe", "john@doe.com"))
378 .to(vec![
379 ("Antoine de Saint-Exupéry", "antoine@exupery.com"),
380 ("안녕하세요 세계", "test@test.com"),
381 ("Xin chào", "addr@addr.com"),
382 ])
383 .bcc(vec![
384 (
385 "Привет, мир",
386 vec![
387 ("ASCII recipient", "addr1@addr7.com"),
388 ("ハロー・ワールド", "addr2@addr6.com"),
389 ("áéíóú", "addr3@addr5.com"),
390 ("Γειά σου Κόσμε", "addr4@addr4.com"),
391 ],
392 ),
393 (
394 "Hello world",
395 vec![
396 ("שלום עולם", "addr5@addr3.com"),
397 ("¡El ñandú comió ñoquis!", "addr6@addr2.com"),
398 ("Recipient", "addr7@addr1.com"),
399 ],
400 ),
401 ])
402 .header("List-Archive", URL::new("http://example.com/archive"))
403 .subject("Hello world!")
404 .text_body("Hello, world!\n".repeat(20))
405 .html_body("<p>¡Hola Mundo!</p>".repeat(20))
406 .inline("image/png", "cid:image", [0, 1, 2, 3, 4, 5].as_ref())
407 .attachment("text/plain", "my fíle.txt", "안녕하세요 세계".repeat(20))
408 .attachment(
409 "text/plain",
410 "ハロー・ワールド",
411 "ハロー・ワールド".repeat(20).into_bytes(),
412 )
413 .write_to_vec()
414 .unwrap();
415 MessageParser::new().parse(&output).unwrap();
416 }
417}