1use mail_builder::MessageBuilder;
6use mail_parser::{Message, MessageParser};
7use std::path::PathBuf;
8
9#[cfg(feature = "pgp")]
10use crate::pgp::Pgp;
11use crate::{
12 message::{FilterParts, MimeBodyInterpreter},
13 Error, Result,
14};
15
16use super::header;
17
18#[derive(Clone, Debug, Default, Eq, PartialEq)]
20pub enum FilterHeaders {
21 #[default]
23 All,
24
25 Include(Vec<String>),
27
28 Exclude(Vec<String>),
30}
31
32impl FilterHeaders {
33 pub fn contains(&self, header: &String) -> bool {
34 match self {
35 Self::All => false,
36 Self::Include(headers) => headers.contains(header),
37 Self::Exclude(headers) => !headers.contains(header),
38 }
39 }
40}
41
42#[derive(Clone, Debug, Default, Eq, PartialEq)]
44pub struct MimeInterpreterBuilder {
45 show_headers: FilterHeaders,
47
48 mime_body_interpreter: MimeBodyInterpreter,
50}
51
52impl MimeInterpreterBuilder {
53 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn with_show_headers(mut self, s: FilterHeaders) -> Self {
60 self.show_headers = s;
61 self
62 }
63
64 pub fn with_show_all_headers(mut self) -> Self {
66 self.show_headers = FilterHeaders::All;
67 self
68 }
69
70 pub fn with_show_only_headers(
72 mut self,
73 headers: impl IntoIterator<Item = impl ToString>,
74 ) -> Self {
75 let headers = headers.into_iter().fold(Vec::new(), |mut headers, header| {
76 let header = header.to_string();
77 if !headers.contains(&header) {
78 headers.push(header)
79 }
80 headers
81 });
82 self.show_headers = FilterHeaders::Include(headers);
83 self
84 }
85
86 pub fn with_show_additional_headers(
90 mut self,
91 headers: impl IntoIterator<Item = impl ToString>,
92 ) -> Self {
93 let next_headers = headers.into_iter().fold(Vec::new(), |mut headers, header| {
94 let header = header.to_string();
95 if !headers.contains(&header) && !self.show_headers.contains(&header) {
96 headers.push(header)
97 }
98 headers
99 });
100
101 match &mut self.show_headers {
102 FilterHeaders::All => {
103 self.show_headers = FilterHeaders::Include(next_headers);
106 }
107 FilterHeaders::Include(headers) => {
108 headers.extend(next_headers);
109 }
110 FilterHeaders::Exclude(headers) => {
111 headers.extend(next_headers);
112 }
113 };
114
115 self
116 }
117
118 pub fn with_hide_all_headers(mut self) -> Self {
120 self.show_headers = FilterHeaders::Include(Vec::new());
121 self
122 }
123
124 pub fn with_show_multiparts(mut self, b: bool) -> Self {
126 self.mime_body_interpreter = self.mime_body_interpreter.with_show_multiparts(b);
127 self
128 }
129
130 pub fn with_show_parts(mut self, visibility: bool) -> Self {
132 self.mime_body_interpreter = self.mime_body_interpreter.with_show_parts(visibility);
133 self
134 }
135
136 pub fn with_filter_parts(mut self, f: FilterParts) -> Self {
138 self.mime_body_interpreter = self.mime_body_interpreter.with_filter_parts(f);
139 self
140 }
141
142 pub fn with_show_plain_texts_signature(mut self, b: bool) -> Self {
144 self.mime_body_interpreter = self
145 .mime_body_interpreter
146 .with_show_plain_texts_signature(b);
147 self
148 }
149
150 pub fn with_show_attachments(mut self, b: bool) -> Self {
152 self.mime_body_interpreter = self.mime_body_interpreter.with_show_attachments(b);
153 self
154 }
155
156 pub fn with_show_inline_attachments(mut self, b: bool) -> Self {
158 self.mime_body_interpreter = self.mime_body_interpreter.with_show_inline_attachments(b);
159 self
160 }
161
162 pub fn with_save_attachments(mut self, b: bool) -> Self {
164 self.mime_body_interpreter = self.mime_body_interpreter.with_save_attachments(b);
165 self
166 }
167
168 pub fn with_save_attachments_dir(mut self, dir: impl Into<PathBuf>) -> Self {
173 self.mime_body_interpreter = self.mime_body_interpreter.with_save_attachments_dir(dir);
174 self
175 }
176
177 pub fn with_save_some_attachments_dir(self, dir: Option<impl Into<PathBuf>>) -> Self {
183 match dir {
184 Some(dir) => self.with_save_attachments_dir(dir),
185 None => {
186 self.with_save_attachments_dir(MimeBodyInterpreter::default_save_attachments_dir())
187 }
188 }
189 }
190
191 #[cfg(feature = "pgp")]
193 pub fn set_pgp(&mut self, pgp: impl Into<Pgp>) {
194 self.mime_body_interpreter.set_pgp(pgp);
195 }
196
197 #[cfg(feature = "pgp")]
199 pub fn with_pgp(mut self, pgp: impl Into<Pgp>) -> Self {
200 self.mime_body_interpreter.set_pgp(pgp);
201 self
202 }
203
204 #[cfg(feature = "pgp")]
206 pub fn set_some_pgp(&mut self, pgp: Option<impl Into<Pgp>>) {
207 self.mime_body_interpreter.set_some_pgp(pgp);
208 }
209
210 #[cfg(feature = "pgp")]
212 pub fn with_some_pgp(mut self, pgp: Option<impl Into<Pgp>>) -> Self {
213 self.mime_body_interpreter.set_some_pgp(pgp);
214 self
215 }
216
217 pub fn build(self) -> MimeInterpreter {
222 MimeInterpreter {
223 show_headers: self.show_headers,
224 mime_body_interpreter: self.mime_body_interpreter,
225 }
226 }
227}
228
229#[derive(Clone, Debug, Default, Eq, PartialEq)]
231pub struct MimeInterpreter {
232 show_headers: FilterHeaders,
233 mime_body_interpreter: MimeBodyInterpreter,
234}
235
236impl MimeInterpreter {
237 pub async fn from_msg(self, msg: &Message<'_>) -> Result<String> {
239 let mut mml = String::new();
240
241 match self.show_headers {
242 FilterHeaders::All => msg.headers().iter().for_each(|header| {
243 let key = header.name.as_str();
244 let val = header::display_value(key, &header.value);
245 mml.push_str(&format!("{key}: {val}\n"));
246 }),
247 FilterHeaders::Include(keys) => keys
248 .iter()
249 .filter_map(|key| msg.header(key.as_str()).map(|val| (key, val)))
250 .for_each(|(key, val)| {
251 let val = header::display_value(key, val);
252 mml.push_str(&format!("{key}: {val}\n"));
253 }),
254 FilterHeaders::Exclude(keys) => msg
255 .headers()
256 .iter()
257 .filter(|header| !keys.contains(&header.name.as_str().to_owned()))
258 .for_each(|header| {
259 let key = header.name.as_str();
260 let val = header::display_value(key, &header.value);
261 mml.push_str(&format!("{key}: {val}\n"));
262 }),
263 };
264
265 if !mml.is_empty() {
266 mml.push('\n');
267 }
268
269 let mime_body_interpreter = self.mime_body_interpreter;
270
271 #[cfg(feature = "pgp")]
272 let mime_body_interpreter = mime_body_interpreter
273 .with_pgp_sender(header::extract_first_email(msg.from()))
274 .with_pgp_recipient(header::extract_first_email(msg.to()));
275
276 let mml_body = mime_body_interpreter.interpret_msg(msg).await?;
277
278 mml.push_str(&mml_body);
279
280 Ok(mml)
281 }
282
283 pub async fn from_bytes(self, bytes: impl AsRef<[u8]>) -> Result<String> {
285 let msg = MessageParser::new()
286 .parse(bytes.as_ref())
287 .ok_or(Error::ParseRawEmailError)?;
288 self.from_msg(&msg).await
289 }
290
291 pub async fn from_msg_builder(self, builder: MessageBuilder<'_>) -> Result<String> {
293 let bytes = builder.write_to_vec().map_err(Error::BuildEmailError)?;
294 self.from_bytes(&bytes).await
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use concat_with::concat_line;
301 use mail_builder::MessageBuilder;
302
303 use super::MimeInterpreterBuilder;
304
305 fn msg_builder() -> MessageBuilder<'static> {
306 MessageBuilder::new()
307 .message_id("id@localhost")
308 .in_reply_to("reply-id@localhost")
309 .date(0_u64)
310 .from("from@localhost")
311 .to("to@localhost")
312 .subject("subject")
313 .text_body("Hello, world!")
314 }
315
316 #[tokio::test]
317 async fn all_headers() {
318 let mml = MimeInterpreterBuilder::new()
319 .with_show_all_headers()
320 .build()
321 .from_msg_builder(msg_builder())
322 .await
323 .unwrap();
324
325 let expected_mml = concat_line!(
326 "Message-ID: <id@localhost>",
327 "In-Reply-To: <reply-id@localhost>",
328 "Date: Thu, 1 Jan 1970 00:00:00 +0000",
329 "From: from@localhost",
330 "To: to@localhost",
331 "Subject: subject",
332 "MIME-Version: 1.0",
333 "Content-Type: text/plain; charset=utf-8",
334 "Content-Transfer-Encoding: 7bit",
335 "",
336 "Hello, world!",
337 );
338
339 assert_eq!(mml, expected_mml);
340 }
341
342 #[tokio::test]
343 async fn only_headers() {
344 let mml = MimeInterpreterBuilder::new()
345 .with_show_only_headers(["From", "Subject"])
346 .build()
347 .from_msg_builder(msg_builder())
348 .await
349 .unwrap();
350
351 let expected_mml = concat_line!(
352 "From: from@localhost",
353 "Subject: subject",
354 "",
355 "Hello, world!",
356 );
357
358 assert_eq!(mml, expected_mml);
359 }
360
361 #[tokio::test]
362 async fn only_headers_duplicated() {
363 let mml = MimeInterpreterBuilder::new()
364 .with_show_only_headers(["From", "Subject", "From"])
365 .build()
366 .from_msg_builder(msg_builder())
367 .await
368 .unwrap();
369
370 let expected_mml = concat_line!(
371 "From: from@localhost",
372 "Subject: subject",
373 "",
374 "Hello, world!",
375 );
376
377 assert_eq!(mml, expected_mml);
378 }
379
380 #[tokio::test]
381 async fn no_headers() {
382 let mml = MimeInterpreterBuilder::new()
383 .with_hide_all_headers()
384 .build()
385 .from_msg_builder(msg_builder())
386 .await
387 .unwrap();
388
389 let expected_mml = concat_line!("Hello, world!");
390
391 assert_eq!(mml, expected_mml);
392 }
393
394 #[tokio::test]
395 async fn mml_markup_escaped() {
396 let msg_builder = MessageBuilder::new()
397 .message_id("id@localhost")
398 .in_reply_to("reply-id@localhost")
399 .date(0_u64)
400 .from("from@localhost")
401 .to("to@localhost")
402 .subject("subject")
403 .text_body("<#part>Should be escaped.<#/part>");
404
405 let mml = MimeInterpreterBuilder::new()
406 .with_show_only_headers(["From", "Subject"])
407 .build()
408 .from_msg_builder(msg_builder)
409 .await
410 .unwrap();
411
412 let expected_mml = concat_line!(
413 "From: from@localhost",
414 "Subject: subject",
415 "",
416 "<#!part>Should be escaped.<#!/part>",
417 );
418
419 assert_eq!(mml, expected_mml);
420 }
421}