mailtutan_lib/models/
message.rs1use 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}