1use crate::address::*;
2use crate::prelude::*;
3use std::borrow::Cow;
4
5#[derive(Debug)]
29pub struct Email<'a> {
30 #[cfg(not(feature = "mime"))]
32 pub body: Option<Cow<'a, str>>,
33
34 #[cfg(feature = "from")]
35 pub from: Vec<Mailbox<'a>>,
38
39 #[cfg(feature = "sender")]
40 pub sender: Mailbox<'a>,
44
45 #[cfg(feature = "subject")]
46 pub subject: Option<Cow<'a, str>>,
48
49 #[cfg(feature = "date")]
50 pub date: DateTime,
53
54 #[cfg(feature = "to")]
55 pub to: Option<Vec<Address<'a>>>,
56
57 #[cfg(feature = "cc")]
58 pub cc: Option<Vec<Address<'a>>>,
59
60 #[cfg(feature = "bcc")]
61 pub bcc: Option<Vec<Address<'a>>>,
62
63 #[cfg(feature = "message-id")]
64 pub message_id: Option<(Cow<'a, str>, Cow<'a, str>)>,
65
66 #[cfg(feature = "in-reply-to")]
67 pub in_reply_to: Option<Vec<(Cow<'a, str>, Cow<'a, str>)>>,
68
69 #[cfg(feature = "references")]
70 pub references: Option<Vec<(Cow<'a, str>, Cow<'a, str>)>>,
71
72 #[cfg(feature = "reply-to")]
73 pub reply_to: Option<Vec<Address<'a>>>,
74
75 #[cfg(feature = "comments")]
76 pub comments: Vec<Cow<'a, str>>,
77
78 #[cfg(feature = "keywords")]
79 pub keywords: Vec<Vec<Cow<'a, str>>>,
80
81 #[cfg(feature = "trace")]
82 pub trace: Vec<(
83 Option<Option<EmailAddress<'a>>>,
84 Vec<(Vec<crate::parsing::fields::ReceivedToken<'a>>, DateTime)>,
85 Vec<crate::parsing::fields::TraceField<'a>>,
86 )>,
87
88 #[cfg(feature = "mime")]
89 pub mime_entity: RawEntity<'a>,
90
91 pub unknown_fields: Vec<(&'a str, Cow<'a, str>)>,
94}
95
96impl<'a> Email<'a> {
97 pub fn parse(data: &'a [u8]) -> Result<Email<'a>, Error> {
99 let (fields, body) = crate::parse_message(data)?;
100
101 #[cfg(feature = "from")]
102 let mut from = None;
103 #[cfg(feature = "sender")]
104 let mut sender = None;
105 #[cfg(feature = "subject")]
106 let mut subject = None;
107 #[cfg(feature = "date")]
108 let mut date = None;
109 #[cfg(feature = "to")]
110 let mut to = None;
111 #[cfg(feature = "cc")]
112 let mut cc = None;
113 #[cfg(feature = "bcc")]
114 let mut bcc = None;
115 #[cfg(feature = "message-id")]
116 let mut message_id = None;
117 #[cfg(feature = "in-reply-to")]
118 let mut in_reply_to = None;
119 #[cfg(feature = "references")]
120 let mut references = None;
121 #[cfg(feature = "reply-to")]
122 let mut reply_to = None;
123 #[cfg(feature = "comments")]
124 let mut comments = Vec::new();
125 #[cfg(feature = "keywords")]
126 let mut keywords = Vec::new();
127 #[cfg(feature = "trace")]
128 let mut trace = Vec::new();
129 #[cfg(feature = "mime")]
130 let mut mime_version = None;
131 #[cfg(feature = "mime")]
132 let mut content_type = None;
133 #[cfg(feature = "mime")]
134 let mut content_transfer_encoding = None;
135 #[cfg(feature = "mime")]
136 let mut content_id = None;
137 #[cfg(feature = "mime")]
138 let mut content_description = None;
139 #[cfg(feature = "content-disposition")]
140 let mut content_disposition = None;
141
142 let mut unknown_fields = Vec::new();
143
144 for field in fields {
145 match field {
146 #[cfg(feature = "from")]
147 Field::From(mailboxes) => {
148 if from.is_none() {
149 from = Some(mailboxes)
150 } else {
151 return Err(Error::DuplicateHeader("From"));
152 }
153 }
154 #[cfg(feature = "sender")]
155 Field::Sender(mailbox) => {
156 if sender.is_none() {
157 sender = Some(mailbox)
158 } else {
159 return Err(Error::DuplicateHeader("Sender"));
160 }
161 }
162 #[cfg(feature = "subject")]
163 Field::Subject(data) => {
164 if subject.is_none() {
165 subject = Some(data)
166 } else {
167 return Err(Error::DuplicateHeader("Subject"));
168 }
169 }
170 #[cfg(feature = "date")]
171 Field::Date(data) => {
172 if date.is_none() {
173 date = Some(data)
174 } else {
175 return Err(Error::DuplicateHeader("Date"));
176 }
177 }
178 #[cfg(feature = "to")]
179 Field::To(addresses) => {
180 if to.is_none() {
181 to = Some(addresses)
182 } else {
183 return Err(Error::DuplicateHeader("To"));
184 }
185 }
186 #[cfg(feature = "cc")]
187 Field::Cc(addresses) => {
188 if cc.is_none() {
189 cc = Some(addresses)
190 } else {
191 return Err(Error::DuplicateHeader("Cc"));
192 }
193 }
194 #[cfg(feature = "bcc")]
195 Field::Bcc(addresses) => {
196 if bcc.is_none() {
197 bcc = Some(addresses)
198 } else {
199 return Err(Error::DuplicateHeader("Bcc"));
200 }
201 }
202 #[cfg(feature = "message-id")]
203 Field::MessageId(id) => {
204 if message_id.is_none() {
205 message_id = Some(id)
206 } else {
207 return Err(Error::DuplicateHeader("Message-ID"));
208 }
209 }
210 #[cfg(feature = "in-reply-to")]
211 Field::InReplyTo(ids) => {
212 if in_reply_to.is_none() {
213 in_reply_to = Some(ids)
214 } else {
215 return Err(Error::DuplicateHeader("In-Reply-To"));
216 }
217 }
218 #[cfg(feature = "references")]
219 Field::References(ids) => {
220 if references.is_none() {
221 references = Some(ids)
222 } else {
223 return Err(Error::DuplicateHeader("References"));
224 }
225 }
226 #[cfg(feature = "reply-to")]
227 Field::ReplyTo(mailboxes) => {
228 if reply_to.is_none() {
229 reply_to = Some(mailboxes)
230 } else {
231 return Err(Error::DuplicateHeader("Reply-To"));
232 }
233 }
234 #[cfg(feature = "comments")]
235 Field::Comments(data) => comments.push(data),
236 #[cfg(feature = "keywords")]
237 Field::Keywords(mut data) => {
238 keywords.append(&mut data);
239 }
240 #[cfg(feature = "trace")]
241 Field::Trace {
242 return_path,
243 received,
244 fields,
245 } => {
246 trace.push((return_path, received, fields));
247 }
248 #[cfg(feature = "mime")]
249 Field::MimeVersion(major, minor) => {
250 if mime_version.is_none() {
251 mime_version = Some((major, minor))
252 } else {
253 return Err(Error::DuplicateHeader("Mime-Version"));
254 }
255 }
256 #[cfg(feature = "mime")]
257 Field::ContentType {
258 mime_type,
259 subtype,
260 parameters,
261 } => {
262 if content_type.is_none() {
263 content_type = Some((mime_type, subtype, parameters))
264 } else {
265 return Err(Error::DuplicateHeader("Content-Type"));
266 }
267 }
268 #[cfg(feature = "mime")]
269 Field::ContentTransferEncoding(encoding) => {
270 if content_transfer_encoding.is_none() {
271 content_transfer_encoding = Some(encoding)
272 } else {
273 return Err(Error::DuplicateHeader("Content-Transfer-Encoding"));
274 }
275 }
276 #[cfg(feature = "mime")]
277 Field::ContentId(id) => {
278 if content_id.is_none() {
279 content_id = Some(id)
280 } else {
281 return Err(Error::DuplicateHeader("Content-Id"));
282 }
283 }
284 #[cfg(feature = "mime")]
285 Field::ContentDescription(description) => {
286 if content_description.is_none() {
287 content_description = Some(description)
288 } else {
289 return Err(Error::DuplicateHeader("Content-Description"));
290 }
291 }
292 #[cfg(feature = "content-disposition")]
293 Field::ContentDisposition(disposition) => {
294 if content_disposition.is_none() {
295 content_disposition = Some(disposition)
296 } else {
297 return Err(Error::DuplicateHeader("Content-Disposition"));
298 }
299 }
300 Field::Unknown { name, value } => {
301 unknown_fields.push((name, value));
302 }
303 }
304 }
305
306 #[cfg(feature = "from")]
307 let from = from.ok_or(Error::MissingHeader("From"))?;
308 #[cfg(feature = "date")]
309 let date = date.ok_or(Error::MissingHeader("Date"))?;
310
311 #[cfg(feature = "sender")]
312 let sender = match sender {
313 Some(sender) => sender,
314 None => {
315 if from.len() == 1 {
316 from[0].clone()
317 } else {
318 return Err(Error::MissingHeader("Sender"));
319 }
320 }
321 };
322
323 #[cfg(feature = "mime")]
324 let (content_type, body) = (
325 content_type.unwrap_or((
326 ContentType::Text,
327 Cow::Borrowed("plain"),
328 vec![(Cow::Borrowed("charset"), Cow::Borrowed("us-ascii"))]
329 .into_iter()
330 .collect(),
331 )),
332 if let Some(body) = body {
333 Some(crate::parsing::mime::entity::decode_value(
334 Cow::Borrowed(body),
335 content_transfer_encoding.unwrap_or(ContentTransferEncoding::SevenBit),
336 )?)
337 } else {
338 None
339 },
340 );
341
342 Ok(Email {
343 #[cfg(not(feature = "mime"))]
344 body,
345 #[cfg(feature = "from")]
346 from,
347 #[cfg(feature = "sender")]
348 sender,
349 #[cfg(feature = "subject")]
350 subject,
351 #[cfg(feature = "date")]
352 date,
353 #[cfg(feature = "to")]
354 to,
355 #[cfg(feature = "cc")]
356 cc,
357 #[cfg(feature = "bcc")]
358 bcc,
359 #[cfg(feature = "message-id")]
360 message_id,
361 #[cfg(feature = "in-reply-to")]
362 in_reply_to,
363 #[cfg(feature = "references")]
364 references,
365 #[cfg(feature = "reply-to")]
366 reply_to,
367 #[cfg(feature = "trace")]
368 trace,
369 #[cfg(feature = "comments")]
370 comments,
371 #[cfg(feature = "keywords")]
372 keywords,
373 #[cfg(feature = "mime")]
374 mime_entity: RawEntity {
375 mime_type: content_type.0,
376 subtype: content_type.1,
377 description: content_description,
378 id: content_id,
379 parameters: content_type.2,
380 #[cfg(feature = "content-disposition")]
381 disposition: content_disposition,
382 value: body.unwrap_or(Cow::Borrowed(b"")),
383 additional_headers: Vec::new(),
384 },
385 unknown_fields,
386 })
387 }
388}
389
390impl<'a> std::convert::TryFrom<&'a [u8]> for Email<'a> {
391 type Error = crate::error::Error;
392
393 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
394 Self::parse(value)
395 }
396}
397
398#[cfg(test)]
399mod test {
400 use super::*;
401
402 #[test]
403 fn test_full_email() {
404 }
414
415 #[test]
416 fn test_field_number() {
417 assert!(Email::parse(
418 b"\
420 From: Mubelotix <mubelotix@mubelotix.dev>\r\n\
421 \r\n\
422 Hey!\r\n",
423 )
424 .is_err());
425
426 assert!(Email::parse(
427 b"\
429 From: Mubelotix <mubelotix@mubelotix.dev>\r\n\
430 Date: 5 May 2003 18:58:34 +0000\r\n\
431 Date: 6 May 2003 18:58:34 +0000\r\n\
432 \r\n\
433 Hey!\r\n",
434 )
435 .is_err());
436
437 assert!(Email::parse(
438 b"\
440 Date: 5 May 2003 18:58:34 +0000\r\n\
441 \r\n\
442 Hey!\r\n",
443 )
444 .is_err());
445
446 assert!(Email::parse(
447 b"\
449 From: Mubelotix <mubelotix@mubelotix.dev>\r\n\
450 From: Someone <jack@gmail.com>\r\n\
451 Date: 5 May 2003 18:58:34 +0000\r\n\
452 \r\n\
453 Hey!\r\n",
454 )
455 .is_err());
456 }
457}