mml/message/body/compiler/
mod.rs1mod parsers;
6mod tokens;
7
8use std::{ffi::OsStr, fs, ops::Deref};
9
10use async_recursion::async_recursion;
11use mail_builder::{
12 mime::{BodyPart, MimePart},
13 MessageBuilder,
14};
15use shellexpand_utils::shellexpand_path;
16#[allow(unused_imports)]
17use tracing::{debug, warn};
18
19#[cfg(feature = "pgp")]
20use crate::pgp::Pgp;
21use crate::{Error, Result};
22
23use super::{
24 ALTERNATIVE, ATTACHMENT, DISPOSITION, ENCODING, ENCODING_7BIT, ENCODING_8BIT, ENCODING_BASE64,
25 ENCODING_QUOTED_PRINTABLE, FILENAME, INLINE, MIXED, MULTIPART_BEGIN, MULTIPART_BEGIN_ESCAPED,
26 MULTIPART_END, MULTIPART_END_ESCAPED, NAME, PART_BEGIN, PART_BEGIN_ESCAPED, PART_END,
27 PART_END_ESCAPED, RECIPIENT_FILENAME, RELATED, TYPE,
28};
29#[cfg(feature = "pgp")]
30use super::{ENCRYPT, PGP_MIME, SIGN};
31
32use self::{parsers::prelude::*, tokens::Part};
33
34#[derive(Clone, Debug, Default, Eq, PartialEq)]
39pub struct MmlBodyCompiler {
40 #[cfg(feature = "pgp")]
41 pgp: Option<Pgp>,
42 #[cfg(feature = "pgp")]
43 pgp_sender: Option<String>,
44 #[cfg(feature = "pgp")]
45 pgp_recipients: Vec<String>,
46}
47
48impl<'a> MmlBodyCompiler {
49 pub fn new() -> Self {
51 Self::default()
52 }
53
54 #[cfg(feature = "pgp")]
55 pub fn set_pgp(&mut self, pgp: impl Into<Pgp>) {
56 self.pgp = Some(pgp.into());
57 }
58
59 #[cfg(feature = "pgp")]
60 pub fn with_pgp(mut self, pgp: impl Into<Pgp>) -> Self {
61 self.set_pgp(pgp);
62 self
63 }
64
65 #[cfg(feature = "pgp")]
66 pub fn set_some_pgp(&mut self, pgp: Option<impl Into<Pgp>>) {
67 self.pgp = pgp.map(Into::into);
68 }
69
70 #[cfg(feature = "pgp")]
71 pub fn with_some_pgp(mut self, pgp: Option<impl Into<Pgp>>) -> Self {
72 self.set_some_pgp(pgp);
73 self
74 }
75
76 #[cfg(feature = "pgp")]
77 pub fn with_pgp_sender(mut self, sender: Option<String>) -> Self {
78 self.pgp_sender = sender;
79 self
80 }
81
82 #[cfg(feature = "pgp")]
83 pub fn with_pgp_recipients(mut self, recipients: Vec<String>) -> Self {
84 self.pgp_recipients = recipients;
85 self
86 }
87
88 #[cfg(feature = "pgp")]
90 async fn encrypt_part(&self, clear_part: &MimePart<'a>) -> Result<MimePart<'a>> {
91 match &self.pgp {
92 None => {
93 debug!("cannot encrypt part: pgp not configured");
94 Ok(clear_part.clone())
95 }
96 Some(pgp) => {
97 let recipients = self.pgp_recipients.clone();
98
99 let mut clear_part_bytes = Vec::new();
100 clear_part
101 .clone()
102 .write_part(&mut clear_part_bytes)
103 .map_err(Error::WriteCompiledPartToVecError)?;
104
105 let encrypted_part_bytes = pgp.encrypt(recipients, clear_part_bytes).await?;
106 let encrypted_part_bytes =
107 encrypted_part_bytes
108 .into_iter()
109 .fold(Vec::new(), |mut part, b| {
110 if b == b'\n' {
111 part.push(b'\r');
112 part.push(b'\n');
113 } else {
114 part.push(b);
115 };
116 part
117 });
118 let encrypted_part = MimePart::new(
119 "multipart/encrypted; protocol=\"application/pgp-encrypted\"",
120 vec![
121 MimePart::new("application/pgp-encrypted", "Version: 1"),
122 MimePart::new("application/octet-stream", encrypted_part_bytes)
123 .transfer_encoding("7bit"),
124 ],
125 );
126
127 Ok(encrypted_part)
128 }
129 }
130 }
131
132 #[cfg(feature = "pgp")]
137 async fn try_encrypt_part(&self, clear_part: MimePart<'a>) -> MimePart<'a> {
138 match self.encrypt_part(&clear_part).await {
139 Ok(encrypted_part) => encrypted_part,
140 Err(err) => {
141 debug!("cannot encrypt email part using pgp: {err}");
142 debug!("{err:?}");
143 clear_part
144 }
145 }
146 }
147
148 #[cfg(feature = "pgp")]
150 async fn sign_part(&self, clear_part: MimePart<'a>) -> Result<MimePart<'a>> {
151 match &self.pgp {
152 None => {
153 debug!("cannot sign part: pgp not configured");
154 Ok(clear_part.clone())
155 }
156 Some(pgp) => {
157 let sender = self
158 .pgp_sender
159 .as_ref()
160 .ok_or(Error::PgpSignMissingSenderError)?;
161
162 let mut clear_part_bytes = Vec::new();
163 clear_part
164 .clone()
165 .write_part(&mut clear_part_bytes)
166 .map_err(Error::WriteCompiledPartToVecError)?;
167
168 let signature_bytes = pgp.sign(sender, clear_part_bytes).await?;
169 let signature_bytes =
170 signature_bytes.into_iter().fold(Vec::new(), |mut part, b| {
171 if b == b'\n' {
172 part.push(b'\r');
173 part.push(b'\n');
174 } else {
175 part.push(b);
176 };
177 part
178 });
179
180 let signed_part = MimePart::new(
181 "multipart/signed; protocol=\"application/pgp-signature\"; micalg=\"pgp-sha256\"",
182 vec![
183 clear_part,
184 MimePart::new("application/pgp-signature", signature_bytes)
185 .transfer_encoding("7bit"),
186 ],
187 );
188
189 Ok(signed_part)
190 }
191 }
192 }
193
194 #[cfg(feature = "pgp")]
199 async fn try_sign_part(&self, clear_part: MimePart<'a>) -> MimePart<'a> {
200 match self.sign_part(clear_part.clone()).await {
201 Ok(signed_part) => signed_part,
202 Err(err) => {
203 debug!("cannot sign email part using pgp: {err}");
204 debug!("{err:?}");
205 clear_part
206 }
207 }
208 }
209
210 fn unescape_mml_markup(text: impl AsRef<str>) -> String {
213 text.as_ref()
214 .replace(PART_BEGIN_ESCAPED, PART_BEGIN)
215 .replace(PART_END_ESCAPED, PART_END)
216 .replace(MULTIPART_BEGIN_ESCAPED, MULTIPART_BEGIN)
217 .replace(MULTIPART_END_ESCAPED, MULTIPART_END)
218 }
219
220 async fn compile_parts(&'a self, parts: Vec<Part<'a>>) -> Result<MessageBuilder> {
223 let mut builder = MessageBuilder::new();
224
225 builder = match parts.len() {
226 0 => builder.text_body(String::new()),
227 1 => builder.body(self.compile_part(parts.into_iter().next().unwrap()).await?),
228 _ => {
229 let mut compiled_parts = Vec::new();
230
231 for part in parts {
232 let part = self.compile_part(part).await?;
233 compiled_parts.push(part);
234 }
235
236 builder.body(MimePart::new("multipart/mixed", compiled_parts))
237 }
238 };
239
240 Ok(builder)
241 }
242
243 #[async_recursion]
245 async fn compile_part(&'a self, part: Part<'a>) -> Result<MimePart> {
246 match part {
247 Part::Multi(props, parts) => {
248 let no_parts = BodyPart::Multipart(Vec::new());
249
250 let mut multi_part = match props.get(TYPE) {
251 Some(&MIXED) | None => MimePart::new("multipart/mixed", no_parts),
252 Some(&ALTERNATIVE) => MimePart::new("multipart/alternative", no_parts),
253 Some(&RELATED) => MimePart::new("multipart/related", no_parts),
254 Some(unknown) => {
255 debug!("unknown multipart type {unknown}, falling back to mixed");
256 MimePart::new("multipart/mixed", no_parts)
257 }
258 };
259
260 for part in parts {
261 multi_part.add_part(self.compile_part(part).await?)
262 }
263
264 #[cfg(feature = "pgp")]
265 {
266 multi_part = match props.get(SIGN) {
267 Some(&PGP_MIME) => self.try_sign_part(multi_part).await,
268 _ => multi_part,
269 };
270
271 multi_part = match props.get(ENCRYPT) {
272 Some(&PGP_MIME) => self.try_encrypt_part(multi_part).await,
273 _ => multi_part,
274 };
275 }
276
277 Ok(multi_part)
278 }
279 Part::Single(ref props, body) => {
280 let fpath = props.get(FILENAME).map(shellexpand_path);
281
282 let mut part = match &fpath {
283 Some(fpath) => {
284 let contents = fs::read(fpath)
285 .map_err(|err| Error::ReadAttachmentError(err, fpath.clone()))?;
286 let mut ctype = Part::get_or_guess_content_type(props, &contents).into();
287 if let Some(name) = props.get(NAME) {
288 ctype = ctype.attribute("name", *name);
289 }
290 MimePart::new(ctype, contents)
291 }
292 None => {
293 let mut ctype =
294 Part::get_or_guess_content_type(props, body.as_bytes()).into();
295 if let Some(name) = props.get(NAME) {
296 ctype = ctype.attribute("name", *name);
297 }
298 MimePart::new(ctype, body)
299 }
300 };
301
302 part = match props.get(ENCODING) {
303 Some(&ENCODING_7BIT) => part.transfer_encoding(ENCODING_7BIT),
304 Some(&ENCODING_8BIT) => part.transfer_encoding(ENCODING_8BIT),
305 Some(&ENCODING_QUOTED_PRINTABLE) => {
306 part.transfer_encoding(ENCODING_QUOTED_PRINTABLE)
307 }
308 Some(&ENCODING_BASE64) => part.transfer_encoding(ENCODING_BASE64),
309 _ => part,
310 };
311
312 part = match props.get(DISPOSITION) {
313 Some(&INLINE) => part.inline(),
314 Some(&ATTACHMENT) => part.attachment(
315 props
316 .get(RECIPIENT_FILENAME)
317 .map(Deref::deref)
318 .or_else(|| match &fpath {
319 Some(fpath) => fpath.file_name().and_then(OsStr::to_str),
320 None => None,
321 })
322 .unwrap_or("noname")
323 .to_owned(),
324 ),
325 _ if fpath.is_some() => part.attachment(
326 props
327 .get(RECIPIENT_FILENAME)
328 .map(ToString::to_string)
329 .or_else(|| {
330 fpath
331 .unwrap()
332 .file_name()
333 .and_then(OsStr::to_str)
334 .map(ToString::to_string)
335 })
336 .unwrap_or_else(|| "noname".to_string()),
337 ),
338 _ => part,
339 };
340
341 #[cfg(feature = "pgp")]
342 {
343 part = match props.get(SIGN) {
344 Some(&PGP_MIME) => self.try_sign_part(part).await,
345 _ => part,
346 };
347
348 part = match props.get(ENCRYPT) {
349 Some(&PGP_MIME) => self.try_encrypt_part(part).await,
350 _ => part,
351 };
352 };
353
354 Ok(part)
355 }
356 Part::PlainText(body) => {
357 let body = Self::unescape_mml_markup(body);
358 let part = MimePart::new("text/plain", body);
359 Ok(part)
360 }
361 }
362 }
363
364 pub async fn compile(&'a self, mml_body: &'a str) -> Result<MessageBuilder> {
366 let res = parsers::parts().parse(mml_body);
367 if let Some(parts) = res.output() {
368 Ok(self.compile_parts(parts.to_owned()).await?)
369 } else {
370 let errs = res.errors().map(|err| err.clone().into_owned()).collect();
371 Err(Error::ParseMmlError(errs, mml_body.to_owned()))
372 }
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use concat_with::concat_line;
379 use std::io::prelude::*;
380 use tempfile::Builder;
381
382 use super::MmlBodyCompiler;
383
384 #[tokio::test]
385 async fn plain() {
386 let mml_body = concat_line!("Hello, world!", "");
387
388 let msg = MmlBodyCompiler::new()
389 .compile(mml_body)
390 .await
391 .unwrap()
392 .message_id("id@localhost")
393 .date(0_u64)
394 .write_to_string()
395 .unwrap();
396
397 let expected_msg = concat_line!(
398 "Message-ID: <id@localhost>\r",
399 "Date: Thu, 1 Jan 1970 00:00:00 +0000\r",
400 "MIME-Version: 1.0\r",
401 "Content-Type: text/plain; charset=\"utf-8\"\r",
402 "Content-Transfer-Encoding: 7bit\r",
403 "\r",
404 "Hello, world!\r",
405 "",
406 );
407
408 assert_eq!(msg, expected_msg);
409 }
410
411 #[tokio::test]
412 async fn html() {
413 let mml_body = concat_line!(
414 "<#part type=\"text/html\">",
415 "<h1>Hello, world!</h1>",
416 "<#/part>",
417 );
418
419 let msg = MmlBodyCompiler::new()
420 .compile(mml_body)
421 .await
422 .unwrap()
423 .message_id("id@localhost")
424 .date(0_u64)
425 .write_to_string()
426 .unwrap();
427
428 let expected_msg = concat_line!(
429 "Message-ID: <id@localhost>\r",
430 "Date: Thu, 1 Jan 1970 00:00:00 +0000\r",
431 "MIME-Version: 1.0\r",
432 "Content-Type: text/html; charset=\"utf-8\"\r",
433 "Content-Transfer-Encoding: 7bit\r",
434 "\r",
435 "<h1>Hello, world!</h1>\r",
436 "",
437 );
438
439 assert_eq!(msg, expected_msg);
440 }
441
442 #[tokio::test]
443 async fn attachment() {
444 let mut attachment = Builder::new()
445 .prefix("attachment")
446 .suffix(".txt")
447 .rand_bytes(0)
448 .tempfile()
449 .unwrap();
450 write!(attachment, "Hello, world!").unwrap();
451 let attachment_path = attachment.path().to_string_lossy();
452
453 let mml_body = format!(
454 "<#part filename={attachment_path} type=text/plain name=custom recipient-filename=/tmp/custom encoding=base64>discarded body<#/part>"
455 );
456
457 let msg = MmlBodyCompiler::new()
458 .compile(&mml_body)
459 .await
460 .unwrap()
461 .message_id("id@localhost")
462 .date(0_u64)
463 .write_to_string()
464 .unwrap();
465
466 let expected_msg = concat_line!(
467 "Message-ID: <id@localhost>\r",
468 "Date: Thu, 1 Jan 1970 00:00:00 +0000\r",
469 "MIME-Version: 1.0\r",
470 "Content-Type: text/plain; name=\"custom\"\r",
471 "Content-Transfer-Encoding: base64\r",
472 "Content-Disposition: attachment; filename=\"/tmp/custom\"\r",
473 "\r",
474 "Hello, world!",
475 );
476
477 assert_eq!(msg, expected_msg);
478 }
479}