mailtutan_lib/models/
message.rs

1use anyhow::{Context, Result};
2use chrono::Local;
3use mail_parser;
4use serde::Serialize;
5use uuid::Uuid;
6
7#[derive(Serialize, Debug, Default, Clone)]
8pub struct Message {
9    pub id: Option<usize>,
10    pub sender: String,
11    pub recipients: Vec<String>,
12    pub subject: String,
13    pub created_at: Option<String>,
14    pub attachments: Vec<Attachment>,
15    #[serde(skip_serializing)]
16    pub source: Vec<u8>,
17    pub formats: Vec<String>,
18    #[serde(skip_serializing)]
19    pub html: Option<String>,
20    #[serde(skip_serializing)]
21    pub plain: Option<String>,
22}
23
24#[derive(Serialize, Debug, Default, Clone)]
25pub struct MessageEvent {
26    #[serde(rename = "type")]
27    pub event_type: String,
28    pub message: Message,
29}
30
31#[derive(Serialize, Debug, Default, Clone)]
32pub struct Attachment {
33    pub cid: String,
34    #[serde(rename = "type")]
35    pub file_type: String,
36    pub filename: String,
37    #[serde(skip_serializing)]
38    pub body: Vec<u8>,
39}
40
41impl Message {
42    pub fn from(data: &Vec<u8>) -> Result<Self> {
43        use mail_parser::HeaderValue;
44
45        let message = mail_parser::Message::parse(data.as_ref()).context("parse message")?;
46
47        let sender = {
48            if let HeaderValue::Address(addr) = message.from() {
49                format!(
50                    "{} {}",
51                    addr.name.as_ref().context("parse sender name")?,
52                    addr.address.as_ref().context("parse sender address")?
53                )
54            } else {
55                "".to_owned()
56            }
57        };
58
59        let recipients = {
60            let mut list: Vec<String> = vec![];
61
62            if let HeaderValue::Address(addr) = message.to() {
63                list.push(format!(
64                    "{}",
65                    addr.address.as_ref().context("parse recipient address")?
66                ));
67            }
68
69            list
70        };
71        let subject = message.subject().unwrap_or("").to_string();
72
73        let mut formats = vec!["source".to_owned()];
74        let mut html: Option<String> = None;
75        let mut plain: Option<String> = None;
76
77        if message.html_body_count() > 0 {
78            formats.push("html".to_owned());
79            html = Some(message.body_html(0).unwrap().to_string());
80        }
81
82        if message.text_body_count() > 0 {
83            formats.push("plain".to_owned());
84            plain = Some(message.body_text(0).unwrap().to_string());
85        }
86
87        use mail_parser::MimeHeaders;
88
89        let attachments = message
90            .attachments()
91            .map(|attachment| Attachment {
92                filename: attachment
93                    .attachment_name()
94                    .unwrap_or("unknown")
95                    .to_string(),
96                file_type: attachment.content_type().unwrap().ctype().to_string(),
97                body: attachment.contents().to_vec(),
98                cid: Uuid::new_v4().to_string(),
99            })
100            .collect();
101
102        Ok(Self {
103            id: None,
104            sender,
105            recipients,
106            subject,
107            created_at: Some(Local::now().format("%Y-%m-%d %H:%M:%S").to_string()),
108            attachments,
109            source: data.to_owned(),
110            formats,
111            html,
112            plain,
113        })
114    }
115}
116
117#[cfg(test)]
118mod test {
119    use super::*;
120
121    #[test]
122    fn test_subject() {
123        let data = concat!(
124            "From: Private Person <me@fromdomain.com>\n",
125            "To: A Test User <test@todomain.com>\n",
126            "Subject: SMTP e-mail test\n",
127            "\n",
128            "This is a test e-mail message.\n"
129        )
130        .as_bytes()
131        .to_vec();
132
133        let message = Message::from(&data).unwrap();
134        assert_eq!(message.subject, "SMTP e-mail test");
135    }
136
137    #[test]
138    fn test_felan() {
139        let data = concat!(
140            "Subject: This is a test email\n",
141            "Content-Type: multipart/alternative; boundary=foobar\n",
142            "Date: Sun, 02 Oct 2016 07:06:22 -0700 (PDT)\n",
143            "\n",
144            "--foobar\n",
145            "Content-Type: text/plain; charset=utf-8\n",
146            "Content-Transfer-Encoding: quoted-printable\n",
147            "\n",
148            "This is the plaintext version, in utf-8. Proof by Euro: =E2=82=AC\n",
149            "--foobar\n",
150            "Content-Type: text/html\n",
151            "Content-Transfer-Encoding: base64\n",
152            "\n",
153            "PGh0bWw+PGJvZHk+VGhpcyBpcyB0aGUgPGI+SFRNTDwvYj4gdmVyc2lvbiwgaW4g \n",
154            "dXMtYXNjaWkuIFByb29mIGJ5IEV1cm86ICZldXJvOzwvYm9keT48L2h0bWw+Cg== \n",
155            "--foobar--\n",
156            "After the final boundary stuff gets ignored.\n"
157        )
158        .as_bytes()
159        .to_vec();
160
161        let message = Message::from(&data).unwrap();
162        assert_eq!(message.subject, "This is a test email");
163    }
164
165    #[test]
166    fn test_subject_is_not_found() {
167        let data = concat!(
168            "Content-Type: multipart/alternative; boundary=foobar\n",
169            "Date: Sun, 02 Oct 2016 07:06:22 -0700 (PDT)\n",
170            "\n",
171            "--foobar\n",
172            "Content-Type: text/plain; charset=utf-8\n",
173            "Content-Transfer-Encoding: quoted-printable\n",
174            "\n",
175            "This is the plaintext version, in utf-8. Proof by Euro: =E2=82=AC\n",
176            "--foobar\n",
177            "Content-Type: text/html\n",
178            "Content-Transfer-Encoding: base64\n",
179            "\n",
180            "PGh0bWw+PGJvZHk+VGhpcyBpcyB0aGUgPGI+SFRNTDwvYj4gdmVyc2lvbiwgaW4g \n",
181            "dXMtYXNjaWkuIFByb29mIGJ5IEV1cm86ICZldXJvOzwvYm9keT48L2h0bWw+Cg== \n",
182            "--foobar--\n",
183            "After the final boundary stuff gets ignored.\n"
184        )
185        .as_bytes()
186        .to_vec();
187
188        let message = Message::from(&data).unwrap();
189        assert_eq!(message.subject, "");
190    }
191}