├── .gitignore ├── README.md ├── .travis.yml ├── Cargo.toml ├── .travis.sh ├── src ├── lib.rs ├── results.rs ├── rfc2045.rs ├── rfc2047.rs ├── mimeheaders.rs ├── rfc822.rs ├── header.rs ├── address.rs ├── rfc5322.rs └── message.rs ├── examples ├── parse_email.rs └── printing.rs └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | 7 | # Executables 8 | *.exe 9 | 10 | # Generated by Cargo 11 | /target/ 12 | Cargo.lock 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rust-mime 2 | ========= 3 | 4 | [![Build Status](https://travis-ci.org/niax/rust-email.svg?branch=master)](https://travis-ci.org/niax/rust-email) 5 | 6 | Rust implementation of RFC5322 (among others) messages. 7 | 8 | [Documentation](http://niax.github.io/rust-email/email/) 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | 7 | env: 8 | global: 9 | - secure: "K6TAj7O5B1JM5kHwZIjZOIb1RezvZluuVFVlE70zcCw6psohdyPcFwthKSQ5DCA//AYrfPJUwA3TO2znM2VNE+7SJNlGC490WwycL6aAtHLo526S6IS00kQnwAQOJ5kIiYI24wbkG+sMJP8l3O3sMpZzCyLwMqv4pFaXHVOehGU=" 10 | 11 | script: sh .travis.sh script 12 | after_success: sh .travis.sh after_success 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "email" 3 | version = "0.0.21" 4 | authors = ["Nicholas Hollett "] 5 | description = "Implementation of RFC 5322 email messages" 6 | repository = "https://github.com/niax/rust-email" 7 | license = "MIT" 8 | edition = "2018" 9 | build = "build.rs" 10 | 11 | [dependencies] 12 | encoding_rs = "0.8" 13 | chrono = "0.4.9" 14 | lazy_static = "1.4.0" 15 | base64 = "0.12.0" 16 | rand = "0.7.2" 17 | 18 | [features] 19 | nightly = [] 20 | 21 | [build-dependencies] 22 | version_check = "0.9.1" 23 | -------------------------------------------------------------------------------- /.travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | script() { 5 | cargo build -v 6 | if [ "$TRAVIS_RUST_VERSION" = "nightly" ]; then 7 | cargo test -v 8 | cargo doc -v --no-deps 9 | fi 10 | } 11 | 12 | after_success() { 13 | if ([ "$TRAVIS_BRANCH" = master ] && 14 | [ "$TRAVIS_RUST_VERSION" = "nightly" ] && 15 | [ "$TRAVIS_PULL_REQUEST" = false ]); then 16 | sudo pip install ghp-import 17 | ghp-import -n target/doc 18 | git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages 2>/dev/null 19 | fi 20 | } 21 | 22 | $@ 23 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(all(feature = "nightly", test), feature(test))] 2 | 3 | extern crate base64; 4 | extern crate chrono; 5 | extern crate encoding_rs as encoding; 6 | extern crate rand; 7 | 8 | #[macro_use] 9 | extern crate lazy_static; 10 | 11 | pub use crate::address::{Address, Mailbox}; 12 | pub use crate::header::{FromHeader, Header, HeaderIter, HeaderMap, ToFoldedHeader, ToHeader}; 13 | pub use crate::message::{MimeMessage, MimeMultipartType}; 14 | 15 | mod address; 16 | mod header; 17 | mod message; 18 | pub mod mimeheaders; 19 | pub mod results; 20 | pub mod rfc2045; 21 | pub mod rfc2047; 22 | pub mod rfc5322; 23 | pub mod rfc822; 24 | -------------------------------------------------------------------------------- /src/results.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | #[derive(Debug)] 5 | pub struct ParsingError { 6 | desc: String, // FIXME: Too basic 7 | } 8 | 9 | impl fmt::Display for ParsingError { 10 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 11 | self.description().fmt(f) 12 | } 13 | } 14 | 15 | impl Error for ParsingError { 16 | fn description(&self) -> &str { 17 | &self.desc[..] 18 | } 19 | 20 | fn cause(&self) -> Option<&dyn Error> { 21 | None 22 | } 23 | } 24 | 25 | impl ParsingError { 26 | pub fn new(desc: String) -> Self { 27 | ParsingError { desc } 28 | } 29 | } 30 | 31 | pub type ParsingResult = Result; 32 | -------------------------------------------------------------------------------- /examples/parse_email.rs: -------------------------------------------------------------------------------- 1 | extern crate email; 2 | 3 | use email::MimeMessage; 4 | use std::env; 5 | use std::fs::File; 6 | use std::io::Read; 7 | use std::path::Path; 8 | 9 | fn main() { 10 | let args: Vec<_> = env::args().collect(); 11 | assert!(args.len() > 1); 12 | let msg_path = Path::new(&args[1]); 13 | 14 | let mut file = File::open(&msg_path).expect("can't open file"); 15 | let raw_msg_bytes = { 16 | let mut rv: Vec = vec![]; 17 | file.read_to_end(&mut rv).expect("can't read from file"); 18 | rv 19 | }; 20 | let raw_msg = String::from_utf8_lossy(&raw_msg_bytes); 21 | 22 | println!("INPUT:"); 23 | println!("{}", &raw_msg); 24 | 25 | let msg = MimeMessage::parse(&raw_msg[..]).unwrap(); 26 | 27 | println!("PARSED:"); 28 | println!("{}", msg.as_string()); 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nicholas Hollett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /examples/printing.rs: -------------------------------------------------------------------------------- 1 | extern crate email; 2 | 3 | use email::{Address, Header, MimeMessage}; 4 | 5 | fn main() { 6 | let parts = vec![ 7 | MimeMessage::new("First part".to_string()), 8 | MimeMessage::new("Second part".to_string()), 9 | ]; 10 | 11 | let mut message = MimeMessage::new("Parent".to_string()); 12 | 13 | for part in parts.into_iter() { 14 | message.children.push(part); 15 | } 16 | 17 | message.headers.insert( 18 | Header::new_with_value( 19 | "To".to_string(), 20 | vec![ 21 | Address::new_mailbox_with_name( 22 | "John Doe".to_string(), 23 | "john@example.org".to_string(), 24 | ), 25 | Address::new_mailbox_with_name( 26 | "Joe Blogs".to_string(), 27 | "joe@example.org".to_string(), 28 | ), 29 | Address::new_mailbox_with_name( 30 | "Mr Black".to_string(), 31 | "mafia_black@example.org".to_string(), 32 | ), 33 | ], 34 | ) 35 | .unwrap(), 36 | ); 37 | 38 | message.update_headers(); 39 | 40 | println!("{}", message.as_string()); 41 | } 42 | -------------------------------------------------------------------------------- /src/rfc2045.rs: -------------------------------------------------------------------------------- 1 | //! Module for dealing with RFC2045 style headers. 2 | use super::rfc5322::Rfc5322Parser; 3 | 4 | use std::collections::HashMap; 5 | 6 | /// Parser over RFC 2045 style headers. 7 | /// 8 | /// Things of the style `value; param1=foo; param2="bar"` 9 | pub struct Rfc2045Parser<'s> { 10 | parser: Rfc5322Parser<'s>, 11 | } 12 | 13 | impl<'s> Rfc2045Parser<'s> { 14 | /// Create a new parser over `s` 15 | pub fn new(s: &str) -> Rfc2045Parser { 16 | Rfc2045Parser { 17 | parser: Rfc5322Parser::new(s), 18 | } 19 | } 20 | 21 | fn consume_token(&mut self) -> Option { 22 | let token = self.parser.consume_while(|c| { 23 | match c { 24 | // Not any tspecials 25 | '(' | ')' | '<' | '>' | '@' | ',' | ';' | ':' | '\\' | '\"' | '/' | '[' | ']' 26 | | '?' | '=' => false, 27 | '!'..='~' => true, 28 | _ => false, 29 | } 30 | }); 31 | 32 | if !token.is_empty() { 33 | Some(token) 34 | } else { 35 | None 36 | } 37 | } 38 | 39 | /// Consume up to all of the input into the value and a hashmap 40 | /// over parameters to values. 41 | pub fn consume_all(&mut self) -> (String, HashMap) { 42 | let value = self.parser.consume_while(|c| c != ';'); 43 | 44 | // Find the parameters 45 | let mut params = HashMap::new(); 46 | while !self.parser.eof() { 47 | // Eat the ; and any whitespace 48 | if self.parser.consume_char() != Some(';') { 49 | break; 50 | } 51 | 52 | // RFC ignorant mail systems may append a ';' without a parameter after. 53 | // This violates the RFC but does happen, so deal with it. 54 | if self.parser.eof() { 55 | break; 56 | } 57 | 58 | self.parser.consume_linear_whitespace(); 59 | 60 | let attribute = self.consume_token(); 61 | self.parser.consume_linear_whitespace(); 62 | 63 | if self.parser.consume_char() != Some('=') { 64 | break; 65 | } 66 | 67 | self.parser.consume_linear_whitespace(); 68 | // Value can be token or quoted-string 69 | let value = if self.parser.peek() == '"' { 70 | self.parser.consume_quoted_string() 71 | } else { 72 | self.consume_token() 73 | }; 74 | 75 | if let (Some(attrib), Some(val)) = (attribute, value) { 76 | params.insert(attrib, val); 77 | } 78 | } 79 | 80 | (value, params) 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::*; 87 | 88 | use std::collections::HashMap; 89 | 90 | struct ParserTestCase<'s> { 91 | input: &'s str, 92 | output: (&'s str, Vec<(&'s str, &'s str)>), 93 | name: &'s str, 94 | } 95 | 96 | #[test] 97 | pub fn test_foo() { 98 | let tests = vec![ 99 | ParserTestCase { 100 | input: "foo/bar", 101 | output: ("foo/bar", vec![]), 102 | name: "Basic value", 103 | }, 104 | ParserTestCase { 105 | input: "foo/bar; foo=bar", 106 | output: ("foo/bar", vec![("foo", "bar")]), 107 | name: "Basic value with parameter", 108 | }, 109 | ParserTestCase { 110 | input: "foo/bar; foo=\"bar\"", 111 | output: ("foo/bar", vec![("foo", "bar")]), 112 | name: "Basic value with quoted parameter", 113 | }, 114 | ParserTestCase { 115 | input: "foo/bar; foo=\"bar\"; baz=qux", 116 | output: ("foo/bar", vec![("foo", "bar"), ("baz", "qux")]), 117 | name: "Multiple values", 118 | }, 119 | ParserTestCase { 120 | input: "foo/bar; foo = \"bar\"; baz=qux", 121 | output: ("foo/bar", vec![("foo", "bar"), ("baz", "qux")]), 122 | name: "Parameter with space", 123 | }, 124 | ]; 125 | 126 | for test in tests.into_iter() { 127 | let (expected_value, expected_param_list) = test.output; 128 | let mut expected_params = HashMap::new(); 129 | for &(param_name, param_value) in expected_param_list.iter() { 130 | expected_params.insert(param_name.to_string(), param_value.to_string()); 131 | } 132 | 133 | let mut parser = Rfc2045Parser::new(test.input); 134 | let (value, parameters) = parser.consume_all(); 135 | 136 | assert!(value == expected_value.to_string(), test.name); 137 | assert!(parameters == expected_params, test.name); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/rfc2047.rs: -------------------------------------------------------------------------------- 1 | //! Module for decoding RFC 2047 strings 2 | // use for to_ascii_lowercase 3 | use base64::decode; 4 | 5 | use encoding::Encoding; 6 | 7 | /// Decode an RFC 2047 string (`s`) into a Rust String. 8 | /// 9 | /// Will accept either "Q" encoding (RFC 2047 Section 4.2) or 10 | /// "B" encoding (BASE64) 11 | /// [unstable] 12 | pub fn decode_rfc2047(s: &str) -> Option { 13 | let parts: Vec<&str> = s.split('?').collect(); 14 | if parts.len() != 5 || parts[0] != "=" || parts[4] != "=" { 15 | None 16 | } else { 17 | let charset = parts[1].to_ascii_lowercase(); 18 | let encoding = parts[2].to_ascii_lowercase(); 19 | let content = parts[3]; 20 | 21 | let bytes = match &encoding[..] { 22 | "q" => decode_q_encoding(content), 23 | "b" => decode_base64_encoding(content), 24 | _ => panic!("Unknown encoding type"), 25 | }; 26 | 27 | // XXX: Relies on WHATWG labels, rather than MIME labels for 28 | // charset. Consider adding mapping upstream. 29 | let decoder = Encoding::for_label(charset[..].as_bytes()); 30 | 31 | match (bytes, decoder) { 32 | (Ok(b), Some(d)) => { 33 | let (x, ..) = d.decode(&b); 34 | Some(x.into_owned()) 35 | }, 36 | _ => None, 37 | } 38 | } 39 | } 40 | 41 | pub fn decode_q_encoding(s: &str) -> Result, String> { 42 | let mut result = Vec::new(); 43 | let mut char_iter = s.chars(); 44 | 45 | loop { 46 | match char_iter.next() { 47 | Some('=') => { 48 | let mut hex_string = String::new(); 49 | match char_iter.next().unwrap() { 50 | '\r' => { 51 | // Possible continuation - expect the next character to be a newline 52 | if char_iter.next().unwrap() == '\n' { 53 | continue; 54 | } else { 55 | return Err("Invalid line endings in text".to_string()); 56 | } 57 | } 58 | '\n' => continue, // treat unix line endings similar to CRLF 59 | c => { 60 | hex_string.push(c); 61 | hex_string.push(char_iter.next().unwrap()); 62 | } 63 | } 64 | let hex_string_slice = &hex_string[..]; 65 | match u8::from_str_radix(hex_string_slice, 16) { 66 | Ok(char_val) => result.push(char_val), 67 | Err(e) => return Err(format!("'{}' is not a hex number: {}", hex_string, e)), 68 | } 69 | } 70 | Some(c) => { 71 | result.push(c as u8); 72 | } 73 | None => break, 74 | }; 75 | } 76 | 77 | Ok(result) 78 | } 79 | 80 | fn decode_base64_encoding(s: &str) -> Result, String> { 81 | match decode(s) { 82 | Ok(bytes) => Ok(bytes), 83 | Err(_) => Err("Failed to base64 decode".to_string()), 84 | } 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use super::*; 90 | 91 | struct DecodeTest<'s> { 92 | input: &'s str, 93 | output: &'s str, 94 | } 95 | 96 | struct DecodeByteTest<'s> { 97 | input: &'s str, 98 | output: &'s [u8], 99 | } 100 | 101 | #[test] 102 | fn test_decode() { 103 | let tests = [ 104 | DecodeTest { 105 | input: "=?ISO-8859-1?Q?Test=20text?=", 106 | output: "Test text", 107 | }, 108 | DecodeTest { 109 | input: "=?ISO-8859-1?b?VGVzdCB0ZXh0?=", 110 | output: "Test text", 111 | }, 112 | DecodeTest { 113 | input: "=?utf-8?b?44GT44KT44Gr44Gh44Gv44CC?=", 114 | output: "こんにちは。", 115 | }, 116 | ]; 117 | 118 | for t in tests.iter() { 119 | assert_eq!(decode_rfc2047(t.input).unwrap(), t.output.to_string()); 120 | } 121 | } 122 | 123 | #[test] 124 | fn test_multiline_quoted_printable_decode() { 125 | let tests = [ 126 | // Test with CRLF line endings 127 | DecodeByteTest { 128 | input: "Python 2=2E=\r\n6", 129 | output: &[80, 121, 116, 104, 111, 110, 32, 50, 46, 54], 130 | }, 131 | // Test with Unix line endings 132 | DecodeByteTest { 133 | input: "Python 2=2E=\n6", 134 | output: &[80, 121, 116, 104, 111, 110, 32, 50, 46, 54], 135 | }, 136 | ]; 137 | 138 | for t in tests.iter() { 139 | assert_eq!(decode_q_encoding(t.input).unwrap(), t.output.to_vec()); 140 | } 141 | } 142 | 143 | #[test] 144 | fn test_decode_failure() { 145 | let tests = [ 146 | // Invalid base64 147 | "=?ISO-8859-1?b?-?=", 148 | // Not valid RFC 2047 149 | "=?Doesn't end with equals", 150 | // Unknown charset 151 | "=?NOCHARSET?q?foo?=", 152 | ]; 153 | 154 | for t in tests.iter() { 155 | println!("{}", t); 156 | assert!(decode_rfc2047(*t).is_none()); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/mimeheaders.rs: -------------------------------------------------------------------------------- 1 | use super::header::{FromHeader, ToHeader}; 2 | use super::results::{ParsingError, ParsingResult}; 3 | use super::rfc2045::Rfc2045Parser; 4 | use super::rfc2047::decode_q_encoding; 5 | 6 | use base64; 7 | use std::collections::HashMap; 8 | 9 | /// Content-Type string, major/minor as the first and second elements 10 | /// respectively. 11 | pub type MimeContentType = (String, String); 12 | 13 | /// Special header type for the Content-Type header. 14 | pub struct MimeContentTypeHeader { 15 | /// The content type presented by this header 16 | pub content_type: MimeContentType, 17 | /// Parameters of this header 18 | pub params: HashMap, 19 | } 20 | 21 | impl FromHeader for MimeContentTypeHeader { 22 | fn from_header(value: String) -> ParsingResult { 23 | let mut parser = Rfc2045Parser::new(&value[..]); 24 | let (value, params) = parser.consume_all(); 25 | 26 | let mime_parts: Vec<&str> = value[..].splitn(2, '/').collect(); 27 | 28 | if mime_parts.len() == 2 { 29 | Ok(MimeContentTypeHeader { 30 | content_type: (mime_parts[0].to_string(), mime_parts[1].to_string()), 31 | params, 32 | }) 33 | } else { 34 | Err(ParsingError::new(format!("Invalid mimetype: {}", value))) 35 | } 36 | } 37 | } 38 | 39 | impl ToHeader for MimeContentTypeHeader { 40 | fn to_header(value: MimeContentTypeHeader) -> ParsingResult { 41 | let (mime_major, mime_minor) = value.content_type; 42 | let mut result = format!("{}/{}", mime_major, mime_minor); 43 | for (key, val) in value.params.iter() { 44 | result = format!("{}; {}={}", result, key, val); 45 | } 46 | Ok(result) 47 | } 48 | } 49 | 50 | /// Special header type for the Content-Transfer-Encoding header. 51 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 52 | pub enum MimeContentTransferEncoding { 53 | /// Message content is not encoded in any way. 54 | Identity, 55 | /// Content transfered using the quoted-printable encoding. 56 | /// 57 | /// This encoding is defined in RFC 2045 Section 6.7 58 | QuotedPrintable, 59 | /// Content transfered as BASE64 60 | /// 61 | /// This encoding is defined in RFC 2045 Section 6.8 62 | Base64, 63 | } 64 | 65 | // Util function for MimeContentTransferEncoding::decode 66 | fn byte_in_base64_alphabet(b: char) -> bool { 67 | match b { 68 | 'A'..='Z' => true, 69 | 'a'..='z' => true, 70 | '0'..='9' => true, 71 | '+' | '/' | '=' => true, 72 | _ => false, 73 | } 74 | } 75 | 76 | impl MimeContentTransferEncoding { 77 | /// Decode the input string with this transfer encoding. 78 | /// 79 | /// Note that this will return a clone of the input's bytes if the 80 | /// transfer encoding is the Identity encoding. 81 | /// [unstable] 82 | pub fn decode(self, input: &str) -> Option> { 83 | match self { 84 | MimeContentTransferEncoding::Identity => Some(input.as_bytes().to_vec()), 85 | MimeContentTransferEncoding::QuotedPrintable => decode_q_encoding(&input[..]).ok(), 86 | MimeContentTransferEncoding::Base64 => { 87 | // As per RFC 2045 section 6.8, all bytes not part of the Base64 Alphabet Table are 88 | // to be ignored. 89 | let mut buf = input.to_owned(); 90 | buf.retain(byte_in_base64_alphabet); 91 | base64::decode(&buf).ok() 92 | } 93 | } 94 | } 95 | } 96 | 97 | impl FromHeader for MimeContentTransferEncoding { 98 | fn from_header(value: String) -> ParsingResult { 99 | // XXX: Used to be into_ascii_lowercase, which is more memory-efficient. Unfortunately that 100 | // API was unstable at the time, so we copy the string here 101 | let lower = value.to_ascii_lowercase(); 102 | match &lower[..] { 103 | "7bit" | "8bit" | "binary" => Ok(MimeContentTransferEncoding::Identity), 104 | "quoted-printable" => Ok(MimeContentTransferEncoding::QuotedPrintable), 105 | "base64" => Ok(MimeContentTransferEncoding::Base64), 106 | x => Err(ParsingError::new(format!("Invalid encoding: {}", x))), 107 | } 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use super::super::header::Header; 114 | use super::*; 115 | 116 | use std::collections::HashMap; 117 | 118 | struct ContentTypeParseTestResult<'a> { 119 | major_type: &'a str, 120 | minor_type: &'a str, 121 | params: Vec<(&'a str, &'a str)>, 122 | } 123 | 124 | struct ContentTypeParseTest<'a> { 125 | input: &'a str, 126 | result: Option>, 127 | } 128 | 129 | #[test] 130 | fn test_content_type_parse() { 131 | let tests = vec![ 132 | ContentTypeParseTest { 133 | input: "text/plain", 134 | result: Some(ContentTypeParseTestResult { 135 | major_type: "text", 136 | minor_type: "plain", 137 | params: vec![], 138 | }), 139 | }, 140 | ContentTypeParseTest { 141 | input: "text/plain; charset=us-ascii", 142 | result: Some(ContentTypeParseTestResult { 143 | major_type: "text", 144 | minor_type: "plain", 145 | params: vec![("charset", "us-ascii")], 146 | }), 147 | }, 148 | ContentTypeParseTest { 149 | input: "application/octet-stream; charset=us-ascii; param=value", 150 | result: Some(ContentTypeParseTestResult { 151 | major_type: "application", 152 | minor_type: "octet-stream", 153 | params: vec![("charset", "us-ascii"), ("param", "value")], 154 | }), 155 | }, 156 | ContentTypeParseTest { 157 | input: "text/plain; charset=\"windows-1251\";", 158 | result: Some(ContentTypeParseTestResult { 159 | major_type: "text", 160 | minor_type: "plain", 161 | params: vec![("charset", "windows-1251")], 162 | }), 163 | }, 164 | ]; 165 | 166 | for test in tests.into_iter() { 167 | let header = Header::new("Content-Type".to_string(), test.input.to_string()); 168 | let parsed_header: Option = header.get_value().ok(); 169 | 170 | let result = match (parsed_header, test.result) { 171 | (Some(given_result), Some(expected_result)) => { 172 | let (given_major, given_minor) = given_result.content_type; 173 | let mut expected_params = HashMap::new(); 174 | for &(param_name, param_value) in expected_result.params.iter() { 175 | expected_params.insert(param_name.to_string(), param_value.to_string()); 176 | } 177 | given_major == expected_result.major_type.to_string() 178 | && given_minor == expected_result.minor_type.to_string() 179 | && given_result.params == expected_params 180 | } 181 | (None, None) => true, 182 | (_, _) => false, 183 | }; 184 | assert!(result, format!("Content-Type parse: '{}'", test.input)); 185 | } 186 | } 187 | 188 | #[test] 189 | fn test_content_transfer_parse() { 190 | let tests = vec![ 191 | ("base64", Some(MimeContentTransferEncoding::Base64)), 192 | ( 193 | "quoted-printable", 194 | Some(MimeContentTransferEncoding::QuotedPrintable), 195 | ), 196 | ("7bit", Some(MimeContentTransferEncoding::Identity)), 197 | ("8bit", Some(MimeContentTransferEncoding::Identity)), 198 | ("binary", Some(MimeContentTransferEncoding::Identity)), 199 | // Check for case insensitivity 200 | ("BASE64", Some(MimeContentTransferEncoding::Base64)), 201 | // Check for fail case 202 | ("lkasjdl", None), 203 | ]; 204 | 205 | for (test, expected) in tests.into_iter() { 206 | let header = Header::new("Content-Transfer-Encoding".to_string(), test.to_string()); 207 | let parsed: Option = header.get_value().ok(); 208 | assert_eq!(parsed, expected); 209 | } 210 | } 211 | 212 | struct ContentTransferDecodeTest<'s> { 213 | encoding: MimeContentTransferEncoding, 214 | input: &'s str, 215 | output: Option>, 216 | } 217 | 218 | #[test] 219 | fn test_content_transfer_decode() { 220 | let tests = vec![ 221 | ContentTransferDecodeTest { 222 | encoding: MimeContentTransferEncoding::Identity, 223 | input: "foo", 224 | output: Some(vec![102, 111, 111]), 225 | }, 226 | ContentTransferDecodeTest { 227 | encoding: MimeContentTransferEncoding::QuotedPrintable, 228 | input: "foo=\r\nbar\r\nbaz", 229 | output: Some(vec![ 230 | 102, 111, 111, 98, 97, 114, 13, 10, // foobar 231 | 98, 97, 122, // baz 232 | ]), 233 | }, 234 | ContentTransferDecodeTest { 235 | encoding: MimeContentTransferEncoding::Base64, 236 | input: "Zm9vCmJhcgpi\r\nYXoKcXV4Cg==", 237 | output: Some(vec![ 238 | 102, 111, 111, 10, // foo 239 | 98, 97, 114, 10, // bar 240 | 98, 97, 122, 10, // baz 241 | 113, 117, 120, 10, // qux 242 | ]), 243 | }, 244 | // Bad base64 content 245 | ContentTransferDecodeTest { 246 | encoding: MimeContentTransferEncoding::Base64, 247 | input: "/?#", 248 | output: None, 249 | }, 250 | ]; 251 | 252 | for test in tests.into_iter() { 253 | let result = test.encoding.decode(&test.input.to_string()); 254 | assert_eq!(result, test.output); 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/rfc822.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use chrono::offset::TimeZone; 4 | use chrono::{DateTime, FixedOffset}; 5 | 6 | use super::results::{ParsingError, ParsingResult}; 7 | use super::rfc5322::Rfc5322Parser; 8 | 9 | static DAYS_OF_WEEK: [&str; 7] = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; 10 | 11 | static MONTHS: [&str; 12] = [ 12 | "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec", 13 | ]; 14 | 15 | // Lazily build TZ_DATA when we need it. 16 | lazy_static! { 17 | static ref TZ_DATA: HashMap<&'static str, i32> = { 18 | let mut map = HashMap::new(); 19 | map.insert("Z", 0); // Zulu 20 | map.insert("UT", 0); 21 | map.insert("GMT", 0); 22 | map.insert("PST", -28800); // UTC-8 23 | map.insert("PDT", -25200); // UTC-7 24 | map.insert("MST", -25200); // UTC-7 25 | map.insert("MDT", -21600); // UTC-6 26 | map.insert("CST", -21600); // UTC-6 27 | map.insert("CDT", -18000); // UTC-5 28 | map.insert("EST", -18000); // UTC-5 29 | map.insert("EDT", -14400); // UTC-4 30 | map 31 | }; 32 | } 33 | 34 | /// Parser for RFC822 style dates, as defined by Section 5. 35 | /// 36 | /// Note that this also supports the additions as specified in 37 | /// RFC5322 Section 3.3 while still being backward compatible. 38 | /// [unstable] 39 | pub struct Rfc822DateParser<'s> { 40 | parser: Rfc5322Parser<'s>, 41 | } 42 | 43 | impl<'s> Rfc822DateParser<'s> { 44 | /// [unstable] 45 | pub fn new(s: &'s str) -> Rfc822DateParser<'s> { 46 | Rfc822DateParser { 47 | parser: Rfc5322Parser::new(s), 48 | } 49 | } 50 | 51 | #[inline] 52 | fn consume_u32(&mut self) -> Option { 53 | match self.parser.consume_word(false) { 54 | Some(s) => match s.parse() { 55 | // FIXME 56 | Ok(x) => Some(x), 57 | Err(_) => None, 58 | }, 59 | None => None, 60 | } 61 | } 62 | 63 | fn consume_time(&mut self) -> ParsingResult<(u32, u32, u32)> { 64 | let hour = match self.consume_u32() { 65 | Some(x) => x, 66 | None => { 67 | return Err(ParsingError::new( 68 | "Failed to parse time: Expected hour, a number.".to_string(), 69 | )) 70 | } 71 | }; 72 | 73 | self.parser.assert_char(':')?; 74 | self.parser.consume_char(); 75 | 76 | let minute = match self.consume_u32() { 77 | Some(x) => x, 78 | None => { 79 | return Err(ParsingError::new( 80 | "Failed to parse time: Expected minute.".to_string(), 81 | )) 82 | } 83 | }; 84 | 85 | // Seconds are optional, only try to parse if we see the next seperator. 86 | let second = match self.parser.assert_char(':') { 87 | Ok(_) => { 88 | self.parser.consume_char(); 89 | self.consume_u32() 90 | } 91 | Err(_) => None, 92 | } 93 | .unwrap_or(0); 94 | 95 | Ok((hour, minute, second)) 96 | } 97 | 98 | fn consume_timezone_offset(&mut self) -> ParsingResult { 99 | match self.parser.consume_word(false) { 100 | Some(s) => { 101 | // from_str doesn't like leading '+' to indicate positive, 102 | // so strip it off if it's there. 103 | let mut s_slice = &s[..]; 104 | s_slice = if s_slice.starts_with('+') { 105 | &s_slice[1..] 106 | } else { 107 | s_slice 108 | }; 109 | // Try to parse zone as an int 110 | match s_slice.parse::() { 111 | Ok(i) => { 112 | let offset_hours = i / 100; 113 | let offset_mins = i % 100; 114 | Ok(offset_hours * 3600 + offset_mins * 60) 115 | } 116 | Err(_) => { 117 | // Isn't an int, so try to use the strings->TZ hash. 118 | match TZ_DATA.get(s_slice) { 119 | Some(offset) => Ok(*offset), 120 | None => { 121 | Err(ParsingError::new(format!("Invalid timezone: {}", s_slice))) 122 | } 123 | } 124 | } 125 | } 126 | } 127 | None => Err(ParsingError::new("Expected timezone offset.".to_string())), 128 | } 129 | } 130 | 131 | /// Consume a DateTime from the input. 132 | /// 133 | /// If successful, returns a DateTime with a fixed offset based on the 134 | /// timezone parsed. You may wish to deal with this in UTC, in which case 135 | /// you may want something like 136 | /// 137 | /// ``` 138 | /// use email::rfc822::Rfc822DateParser; 139 | /// use chrono::Utc; 140 | /// 141 | /// let mut p = Rfc822DateParser::new("Thu, 18 Dec 2014 21:07:22 +0100"); 142 | /// let d = p.consume_datetime().unwrap(); 143 | /// let as_utc = d.with_timezone(&Utc); 144 | /// 145 | /// assert_eq!(d, as_utc); 146 | /// ``` 147 | /// [unstable] 148 | pub fn consume_datetime(&mut self) -> ParsingResult> { 149 | // Handle the optional day "," 150 | self.parser.push_position(); 151 | let day_of_week = self.parser.consume_word(false); 152 | if let Some(day_of_week) = day_of_week { 153 | // XXX: Used to be into_ascii_lowercase, which is more memory-efficient. Unfortunately that 154 | // API was unstable at the time, so we copy the string here 155 | let lower_dow = day_of_week.to_ascii_lowercase(); 156 | if DAYS_OF_WEEK.contains(&&lower_dow[..]) { 157 | // Lose the "," 158 | self.parser.consume_while(|c| c == ',' || c.is_whitespace()); 159 | } else { 160 | // What we read doesn't look like a day, so ignore it, 161 | // go back to the start and continue on. 162 | self.parser.pop_position(); 163 | }; 164 | } else { 165 | // We don't have a leading day "," so go back to the start. 166 | self.parser.pop_position(); 167 | } 168 | 169 | let day_of_month = match self.consume_u32() { 170 | Some(x) => x, 171 | None => { 172 | return Err(ParsingError::new( 173 | "Expected day of month, a number.".to_string(), 174 | )) 175 | } 176 | }; 177 | 178 | self.parser.consume_linear_whitespace(); 179 | let month = self.consume_month()?; 180 | self.parser.consume_linear_whitespace(); 181 | 182 | let year = match self.consume_u32() { 183 | Some(i) => { 184 | // See RFC5322 4.3 for justification of obsolete year format handling. 185 | match i { 186 | // 2 digit year between 0 and 49 is assumed to be in the 2000s 187 | 0..=49 => i + 2000, 188 | // 2 digit year greater than 50 and 3 digit years are added to 1900 189 | 50..=999 => i + 1900, 190 | _ => i, 191 | } 192 | } 193 | None => return Err(ParsingError::new("Expected year.".to_string())), 194 | }; 195 | self.parser.consume_linear_whitespace(); 196 | 197 | let time = self.consume_time()?; 198 | self.parser.consume_linear_whitespace(); 199 | 200 | let tz_offset = self.consume_timezone_offset()?; 201 | 202 | let (hour, minute, second) = time; 203 | 204 | Ok(FixedOffset::east(tz_offset) 205 | .ymd(year as i32, month, day_of_month) 206 | .and_hms(hour, minute, second)) 207 | } 208 | 209 | fn consume_month(&mut self) -> ParsingResult { 210 | match self.parser.consume_word(false) { 211 | Some(s) => { 212 | // XXX: Used to be into_ascii_lowercase, which is more memory-efficient. Unfortunately that 213 | // API was unstable at the time, so we copy the string here 214 | let lower_month = s.to_ascii_lowercase(); 215 | // Add one because months are 1 indexed, array is 0 indexed. 216 | for (i, month) in MONTHS.iter().enumerate() { 217 | if month == &&lower_month[..] { 218 | return Ok((i + 1) as u32); 219 | }; 220 | } 221 | Err(ParsingError::new(format!("Invalid month: {}", lower_month))) 222 | } 223 | None => Err(ParsingError::new("Expected month.".to_string())), 224 | } 225 | } 226 | } 227 | 228 | #[cfg(test)] 229 | mod tests { 230 | use super::*; 231 | use chrono::offset::TimeZone; 232 | use chrono::{DateTime, FixedOffset}; 233 | 234 | #[test] 235 | fn test_time_parse() { 236 | struct TimeParseTest<'s> { 237 | input: &'s str, 238 | result: Option>, 239 | } 240 | 241 | let edt = FixedOffset::east(-14400); // UTC-0400 242 | let cet = FixedOffset::east(3600); // UTC+0100 243 | let napal = FixedOffset::east(20700); // UTC+0545 244 | let utc = FixedOffset::east(0); // UTC+0000 245 | let tests = vec![ 246 | TimeParseTest { 247 | input: "Mon, 20 Jun 1982 10:01:59 EDT", 248 | result: Some(edt.ymd(1982, 6, 20).and_hms(10, 1, 59)), 249 | }, 250 | TimeParseTest { 251 | // Check the 2 digit date parsing logic, >=50 252 | input: "Mon, 20 Jun 82 10:01:59 EDT", 253 | result: Some(edt.ymd(1982, 6, 20).and_hms(10, 1, 59)), 254 | }, 255 | TimeParseTest { 256 | // Check the 2 digit date parsing logic, <50 257 | input: "Mon, 20 Jun 02 10:01:59 EDT", 258 | result: Some(edt.ymd(2002, 6, 20).and_hms(10, 1, 59)), 259 | }, 260 | TimeParseTest { 261 | // Check the optional seconds 262 | input: "Mon, 20 Jun 1982 10:01 EDT", 263 | result: Some(edt.ymd(1982, 6, 20).and_hms(10, 1, 0)), 264 | }, 265 | TimeParseTest { 266 | // Check different TZ parsing 267 | input: "Mon, 20 Jun 1982 10:01:59 +0100", 268 | result: Some(cet.ymd(1982, 6, 20).and_hms(10, 1, 59)), 269 | }, 270 | TimeParseTest { 271 | input: "Mon, 20 Jun 1982 10:01:59 -0400", 272 | result: Some(edt.ymd(1982, 6, 20).and_hms(10, 1, 59)), 273 | }, 274 | TimeParseTest { 275 | // Test for wierd minute offsets in TZ 276 | input: "Mon, 20 Jun 1982 10:01:59 +0545", 277 | result: Some(napal.ymd(1982, 6, 20).and_hms(10, 1, 59)), 278 | }, 279 | TimeParseTest { 280 | // Test for being able to skip day of week 281 | input: "09 Jan 2012 21:20:00 +0000", 282 | result: Some(utc.ymd(2012, 1, 9).and_hms(21, 20, 00)), 283 | }, 284 | ]; 285 | 286 | for test in tests.into_iter() { 287 | let mut parser = Rfc822DateParser::new(test.input); 288 | assert_eq!(parser.consume_datetime().ok(), test.result); 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/header.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::Entry; 2 | use std::collections::HashMap; 3 | use std::fmt; 4 | use std::ops::Deref; 5 | use std::slice::Iter as SliceIter; 6 | use std::sync::Arc; 7 | 8 | use chrono::{DateTime, FixedOffset, Utc}; 9 | 10 | use super::results::{ParsingError, ParsingResult}; 11 | use super::rfc2047::decode_rfc2047; 12 | use super::rfc822::Rfc822DateParser; 13 | 14 | /// Trait for converting from RFC822 Header values into 15 | /// Rust types. 16 | pub trait FromHeader: Sized { 17 | /// Parse the `value` of the header. 18 | /// 19 | /// Returns None if the value failed to be parsed 20 | fn from_header(value: String) -> ParsingResult; 21 | } 22 | 23 | /// Trait for converting from a Rust type into a Header value. 24 | pub trait ToHeader { 25 | /// Turn the `value` into a String suitable for being used in 26 | /// a message header. 27 | /// 28 | /// Returns None if the value cannot be stringified. 29 | fn to_header(value: Self) -> ParsingResult; 30 | } 31 | 32 | /// Trait for converting from a Rust time into a Header value 33 | /// that handles its own folding. 34 | /// 35 | /// Be mindful that this trait does not mean that the value will 36 | /// not be folded later, rather that the type returns a value that 37 | /// should not be folded, given that the header value starts so far 38 | /// in to a line. 39 | /// [unstable] 40 | pub trait ToFoldedHeader { 41 | fn to_folded_header(start_pos: usize, value: Self) -> ParsingResult; 42 | } 43 | 44 | impl ToFoldedHeader for T { 45 | fn to_folded_header(_: usize, value: T) -> ParsingResult { 46 | // We ignore the start_position because the thing will fold anyway. 47 | ToHeader::to_header(value) 48 | } 49 | } 50 | 51 | impl FromHeader for String { 52 | fn from_header(value: String) -> ParsingResult { 53 | #[derive(Debug, Clone, Copy)] 54 | enum ParseState { 55 | Normal(usize), 56 | SeenEquals(usize), 57 | SeenQuestion(usize, usize), 58 | } 59 | 60 | let mut state = ParseState::Normal(0); 61 | let mut decoded = String::new(); 62 | 63 | let value_slice = &value[..]; 64 | 65 | for (pos, c) in value.char_indices() { 66 | state = match (state, c) { 67 | (ParseState::SeenQuestion(start_pos, 4), '=') => { 68 | let next_pos = pos + c.len_utf8(); 69 | // Go to decode if we've seen enough ? 70 | let part_decoded = decode_rfc2047(&value_slice[start_pos..next_pos]); 71 | let to_push = match part_decoded { 72 | Some(ref s) => &s[..], 73 | // Decoding failed, push the undecoded string in. 74 | None => &value_slice[start_pos..pos], 75 | }; 76 | decoded.push_str(to_push); 77 | // Revert us to normal state, but starting at the next character. 78 | ParseState::Normal(next_pos) 79 | } 80 | (ParseState::SeenQuestion(start_pos, count), '?') => { 81 | ParseState::SeenQuestion(start_pos, count + 1) 82 | } 83 | (ParseState::SeenQuestion(start_pos, count), _) => { 84 | if count > 4 { 85 | // This isn't a RFC2047 sequence, so go back to a normal. 86 | ParseState::Normal(start_pos) 87 | } else { 88 | state 89 | } 90 | } 91 | (ParseState::SeenEquals(start_pos), '?') => ParseState::SeenQuestion(start_pos, 1), 92 | (ParseState::SeenEquals(start_pos), _) => { 93 | // This isn't a RFC2047 sequence, so go back to a normal. 94 | ParseState::Normal(start_pos) 95 | } 96 | (ParseState::Normal(start_pos), '=') => { 97 | if start_pos != pos { 98 | // Push all up to the =, if there is stuff to push. 99 | decoded.push_str(&value_slice[start_pos..pos]); 100 | } 101 | ParseState::SeenEquals(pos) 102 | } 103 | (ParseState::Normal(_), _) => state, 104 | }; 105 | } 106 | 107 | // Don't forget to push on whatever we have left 108 | let last_start = match state { 109 | ParseState::Normal(start_pos) => start_pos, 110 | ParseState::SeenEquals(start_pos) => start_pos, 111 | ParseState::SeenQuestion(start_pos, _) => start_pos, 112 | }; 113 | decoded.push_str(&value_slice[last_start..]); 114 | 115 | Ok(decoded) 116 | } 117 | } 118 | 119 | impl FromHeader for DateTime { 120 | fn from_header(value: String) -> ParsingResult> { 121 | let mut parser = Rfc822DateParser::new(&value[..]); 122 | parser.consume_datetime() 123 | } 124 | } 125 | 126 | impl FromHeader for DateTime { 127 | fn from_header(value: String) -> ParsingResult> { 128 | let dt: ParsingResult> = FromHeader::from_header(value); 129 | dt.map(|i| i.with_timezone(&Utc)) 130 | } 131 | } 132 | 133 | impl ToHeader for String { 134 | fn to_header(value: String) -> ParsingResult { 135 | Ok(value) 136 | } 137 | } 138 | 139 | impl<'a> ToHeader for &'a str { 140 | fn to_header(value: &'a str) -> ParsingResult { 141 | Ok(value.to_string()) 142 | } 143 | } 144 | 145 | /// Represents an RFC 822 Header 146 | /// [unstable] 147 | #[derive(PartialEq, Eq, Clone, Debug, Hash)] 148 | pub struct Header { 149 | /// The name of this header 150 | pub name: String, 151 | value: String, 152 | } 153 | 154 | impl, T: Into> From<(S, T)> for Header { 155 | fn from(header: (S, T)) -> Self { 156 | let (name, value) = header; 157 | Header::new(name.into(), value.into()) 158 | } 159 | } 160 | 161 | impl Header { 162 | /// Creates a new Header for the given `name` and `value` 163 | /// [unstable] 164 | pub fn new(name: String, value: String) -> Header { 165 | Header { name, value } 166 | } 167 | 168 | /// Creates a new Header for the given `name` and `value`, 169 | /// as converted through the `ToHeader` or `ToFoldedHeader` trait. 170 | /// 171 | /// Returns None if the value failed to be converted. 172 | /// [unstable] 173 | pub fn new_with_value(name: String, value: T) -> ParsingResult
{ 174 | let header_len = name.len() + 2; 175 | ToFoldedHeader::to_folded_header(header_len, value) 176 | .map(|val| Header::new(name.clone(), val)) 177 | } 178 | 179 | /// Get the value represented by this header, as parsed 180 | /// into whichever type `T` 181 | /// [unstable] 182 | pub fn get_value(&self) -> ParsingResult { 183 | FromHeader::from_header(self.value.clone()) 184 | } 185 | } 186 | 187 | impl fmt::Display for Header { 188 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 189 | write!(fmt, "{}: {}", self.name, self.value) 190 | } 191 | } 192 | 193 | /// [unstable] 194 | pub struct HeaderIter<'s> { 195 | iter: SliceIter<'s, Arc
>, 196 | } 197 | 198 | impl<'s> HeaderIter<'s> { 199 | /// [unstable] 200 | fn new(iter: SliceIter<'s, Arc
>) -> HeaderIter<'s> { 201 | HeaderIter { iter } 202 | } 203 | } 204 | 205 | impl<'s> Iterator for HeaderIter<'s> { 206 | type Item = &'s Header; 207 | 208 | fn next(&mut self) -> Option<&'s Header> { 209 | match self.iter.next() { 210 | Some(s) => Some(s.deref()), 211 | None => None, 212 | } 213 | } 214 | } 215 | 216 | /// A collection of Headers 217 | /// [unstable] 218 | #[derive(Eq, PartialEq, Debug, Clone)] 219 | pub struct HeaderMap { 220 | // We store headers "twice" inside the HeaderMap. 221 | // 222 | // The first is as an ordered list of headers, 223 | // which is used to iterate over. 224 | ordered_headers: Vec>, 225 | // The second is as a mapping between header names 226 | // and all of the headers with that name. 227 | // 228 | // This allows quick retrival of a header by name. 229 | headers: HashMap>>, 230 | } 231 | 232 | impl HeaderMap { 233 | /// [unstable] 234 | pub fn new() -> HeaderMap { 235 | HeaderMap { 236 | ordered_headers: Vec::new(), 237 | headers: HashMap::new(), 238 | } 239 | } 240 | 241 | /// Adds a header to the collection 242 | /// [unstable] 243 | pub fn insert(&mut self, header: Header) { 244 | let header_name = header.name.clone(); 245 | let rc = Arc::new(header); 246 | // Add to the ordered list of headers 247 | self.ordered_headers.push(rc.clone()); 248 | 249 | // and to the mapping between header names and values. 250 | match self.headers.entry(header_name) { 251 | Entry::Occupied(mut entry) => { 252 | entry.get_mut().push(rc); 253 | } 254 | Entry::Vacant(entry) => { 255 | // There haven't been any headers with this name 256 | // as of yet, so make a new list and push it in. 257 | let mut header_list = Vec::new(); 258 | header_list.push(rc); 259 | entry.insert(header_list); 260 | } 261 | }; 262 | } 263 | 264 | pub fn replace(&mut self, header: Header) { 265 | let header_name = header.name.clone(); 266 | let rc = Arc::new(header); 267 | // Remove existing 268 | let mut i = 0; 269 | let mut have_inserted = false; 270 | while i < self.ordered_headers.len() { 271 | if self.ordered_headers[i].name == header_name { 272 | if have_inserted { 273 | // Just remove the header, as we've already updated 274 | self.ordered_headers.remove(i); 275 | } else { 276 | // Update the header in-place 277 | self.ordered_headers[i] = rc.clone(); 278 | have_inserted = true; 279 | } 280 | } else { 281 | i += 1; 282 | } 283 | } 284 | let mut header_list = Vec::new(); 285 | header_list.push(rc.clone()); 286 | // Straight up replace the header in the map 287 | self.headers.insert(header_name, header_list); 288 | } 289 | 290 | /// Get an Iterator over the collection of headers. 291 | /// [unstable] 292 | pub fn iter(&self) -> HeaderIter { 293 | HeaderIter::new(self.ordered_headers.iter()) 294 | } 295 | 296 | /// Get the last value of the header with `name` 297 | /// [unstable] 298 | pub fn get(&self, name: String) -> Option<&Header> { 299 | self.headers 300 | .get(&name) 301 | .map(|headers| headers.last().unwrap()) 302 | .map(|rc| rc.deref()) 303 | } 304 | 305 | /// Get the last value of the header with `name`, as a decoded type. 306 | /// [unstable] 307 | pub fn get_value(&self, name: String) -> ParsingResult { 308 | match self.get(name) { 309 | Some(ref header) => header.get_value(), 310 | None => Err(ParsingError::new("Couldn't find header value.".to_string())), 311 | } 312 | } 313 | 314 | /// [unstable] 315 | /// Get the number of headers within this map. 316 | pub fn len(&self) -> usize { 317 | self.ordered_headers.len() 318 | } 319 | 320 | pub fn is_empty(&self) -> bool { 321 | self.len() == 0 322 | } 323 | 324 | /// [unstable] 325 | /// Find a list of headers of `name`, `None` if there 326 | /// are no headers with that name. 327 | pub fn find(&self, name: &str) -> Option> { 328 | self.headers 329 | .get(name) 330 | .map(|rcs| rcs.iter().map(|rc| rc.deref()).collect()) 331 | } 332 | } 333 | 334 | impl Default for HeaderMap { 335 | fn default() -> Self { 336 | HeaderMap::new() 337 | } 338 | } 339 | 340 | #[cfg(test)] 341 | mod tests { 342 | use super::*; 343 | use std::collections::HashSet; 344 | 345 | use chrono::offset::TimeZone; 346 | use chrono::{DateTime, FixedOffset, Utc}; 347 | 348 | static SAMPLE_HEADERS: [(&'static str, &'static str); 4] = [ 349 | ("Test", "Value"), 350 | ("Test", "Value 2"), 351 | ("Test-2", "Value 3"), 352 | ("Test-Multiline", "Foo\nBar"), 353 | ]; 354 | 355 | fn make_sample_headers() -> Vec
{ 356 | SAMPLE_HEADERS 357 | .iter() 358 | .map(|&(name, value)| Header::new(name.to_string(), value.to_string())) 359 | .collect() 360 | } 361 | 362 | #[test] 363 | fn test_header_to_string() { 364 | let header = Header::new("Test".to_string(), "Value".to_string()); 365 | assert_eq!(header.to_string(), "Test: Value".to_string()); 366 | } 367 | 368 | #[test] 369 | fn test_string_get_value() { 370 | struct HeaderTest<'s> { 371 | input: &'s str, 372 | result: Option<&'s str>, 373 | } 374 | 375 | let tests = vec![ 376 | HeaderTest { 377 | input: "Value", 378 | result: Some("Value"), 379 | }, 380 | HeaderTest { 381 | input: "=?ISO-8859-1?Q?Test=20text?=", 382 | result: Some("Test text"), 383 | }, 384 | HeaderTest { 385 | input: "=?ISO-8859-1?Q?Multiple?= =?utf-8?b?ZW5jb2Rpbmdz?=", 386 | result: Some("Multiple encodings"), 387 | }, 388 | HeaderTest { 389 | input: "Some things with =?utf-8?b?ZW5jb2Rpbmdz?=, other things without.", 390 | result: Some("Some things with encodings, other things without."), 391 | }, 392 | HeaderTest { 393 | input: "Encoding =?utf-8?q?fail", 394 | result: Some("Encoding =?utf-8?q?fail"), 395 | }, 396 | ]; 397 | 398 | for test in tests.into_iter() { 399 | let header = Header::new("Test".to_string(), test.input.to_string()); 400 | let string_value = header.get_value::().ok(); 401 | assert_eq!(string_value, test.result.map(|s| { s.to_string() })); 402 | } 403 | } 404 | 405 | #[test] 406 | fn test_datetime_get_value() { 407 | let header = Header::new( 408 | "Date".to_string(), 409 | "Wed, 17 Dec 2014 09:35:07 +0100".to_string(), 410 | ); 411 | let dt_value = header.get_value::>().unwrap(); 412 | assert_eq!( 413 | dt_value, 414 | FixedOffset::east(3600).ymd(2014, 12, 17).and_hms(9, 35, 7) 415 | ); 416 | } 417 | 418 | #[test] 419 | fn test_datetime_utc_get_value() { 420 | let header = Header::new( 421 | "Date".to_string(), 422 | "Wed, 17 Dec 2014 09:35:07 +0100".to_string(), 423 | ); 424 | let dt_value = header.get_value::>().unwrap(); 425 | assert_eq!(dt_value, Utc.ymd(2014, 12, 17).and_hms(8, 35, 7)); 426 | } 427 | 428 | #[test] 429 | fn test_to_header_string() { 430 | let header = Header::new_with_value("Test".to_string(), "Value".to_string()).unwrap(); 431 | let header_value = header.get_value::().unwrap(); 432 | assert_eq!(header_value, "Value".to_string()); 433 | } 434 | 435 | #[test] 436 | fn test_to_header_str() { 437 | let header = Header::new_with_value("Test".to_string(), "Value").unwrap(); 438 | let header_value = header.get_value::().unwrap(); 439 | assert_eq!(header_value, "Value".to_string()); 440 | } 441 | 442 | #[test] 443 | fn test_header_map_len() { 444 | let mut headers = HeaderMap::new(); 445 | for (i, header) in make_sample_headers().into_iter().enumerate() { 446 | headers.insert(header); 447 | assert_eq!(headers.len(), i + 1); 448 | } 449 | } 450 | #[test] 451 | fn test_header_map_iter() { 452 | let mut headers = HeaderMap::new(); 453 | let mut expected_headers = HashSet::new(); 454 | for header in make_sample_headers().into_iter() { 455 | headers.insert(header.clone()); 456 | expected_headers.insert(header); 457 | } 458 | 459 | let mut count = 0; 460 | // Ensure all the headers returned are expected 461 | for header in headers.iter() { 462 | assert!(expected_headers.contains(header)); 463 | count += 1; 464 | } 465 | // And that there is the right number of them 466 | assert_eq!(count, expected_headers.len()); 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /src/address.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::str::FromStr; 3 | 4 | use super::header::{FromHeader, ToFoldedHeader}; 5 | use super::results::{ParsingError, ParsingResult}; 6 | use super::rfc5322::{Rfc5322Parser, MIME_LINE_LENGTH}; 7 | 8 | /// Represents an RFC 5322 Address 9 | #[derive(PartialEq, Eq, Debug, Clone)] 10 | pub enum Address { 11 | /// A "regular" email address 12 | Mailbox(Mailbox), 13 | /// A named group of mailboxes 14 | Group(String, Vec), 15 | } 16 | 17 | impl Address { 18 | /// Shortcut function to make a new Mailbox with the given address 19 | /// [unstable] 20 | pub fn new_mailbox(address: String) -> Address { 21 | Address::Mailbox(Mailbox::new(address)) 22 | } 23 | 24 | /// Shortcut function to make a new Mailbox with the address and given-name 25 | /// [unstable] 26 | pub fn new_mailbox_with_name(name: String, address: String) -> Address { 27 | Address::Mailbox(Mailbox::new_with_name(name, address)) 28 | } 29 | 30 | /// Shortcut function to make a new Group with a collection of mailboxes 31 | /// [unstable] 32 | pub fn new_group(name: String, mailboxes: Vec) -> Address { 33 | Address::Group(name, mailboxes) 34 | } 35 | } 36 | 37 | impl fmt::Display for Address { 38 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 39 | match *self { 40 | Address::Mailbox(ref mbox) => mbox.fmt(fmt), 41 | Address::Group(ref name, ref mboxes) => { 42 | let mut mailbox_list = String::new(); 43 | for mbox in mboxes.iter() { 44 | if !mailbox_list.is_empty() { 45 | // Insert the separator if there's already things in this list 46 | mailbox_list.push_str(", "); 47 | } 48 | mailbox_list.push_str(&mbox.to_string()[..]); 49 | } 50 | write!(fmt, "{}: {};", name, mailbox_list) 51 | } 52 | } 53 | } 54 | } 55 | 56 | /// Represents an RFC 5322 mailbox 57 | #[derive(PartialEq, Eq, Debug, Clone)] 58 | pub struct Mailbox { 59 | /// The given name for this address 60 | pub name: Option, 61 | /// The mailbox address 62 | pub address: String, 63 | } 64 | 65 | impl Mailbox { 66 | /// Create a new Mailbox without a display name 67 | pub fn new(address: String) -> Mailbox { 68 | Mailbox { 69 | name: None, 70 | address, 71 | } 72 | } 73 | 74 | /// Create a new Mailbox with a display name 75 | pub fn new_with_name(name: String, address: String) -> Mailbox { 76 | Mailbox { 77 | name: Some(name), 78 | address, 79 | } 80 | } 81 | } 82 | 83 | impl fmt::Display for Mailbox { 84 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 85 | match self.name { 86 | Some(ref name) => write!(fmt, "\"{}\" <{}>", name, self.address), 87 | None => write!(fmt, "<{}>", self.address), 88 | } 89 | } 90 | } 91 | 92 | impl<'a> From<&'a str> for Mailbox { 93 | fn from(mailbox: &'a str) -> Mailbox { 94 | Mailbox::new(mailbox.into()) 95 | } 96 | } 97 | 98 | impl From for Mailbox { 99 | fn from(mailbox: String) -> Mailbox { 100 | Mailbox::new(mailbox) 101 | } 102 | } 103 | 104 | impl, T: Into> From<(S, T)> for Mailbox { 105 | fn from(header: (S, T)) -> Mailbox { 106 | let (address, alias) = header; 107 | Mailbox::new_with_name(alias.into(), address.into()) 108 | } 109 | } 110 | 111 | impl FromStr for Mailbox { 112 | type Err = ParsingError; 113 | 114 | fn from_str(s: &str) -> ParsingResult { 115 | AddressParser::new(s).parse_mailbox() 116 | } 117 | } 118 | 119 | impl FromHeader for Vec
{ 120 | fn from_header(value: String) -> ParsingResult> { 121 | AddressParser::new(&value[..]).parse_address_list() 122 | } 123 | } 124 | 125 | impl ToFoldedHeader for Vec
{ 126 | fn to_folded_header(start_pos: usize, value: Vec
) -> ParsingResult { 127 | let mut header = String::new(); 128 | 129 | let mut line_len = start_pos; 130 | 131 | for addr in value.iter() { 132 | let addr_str = format!("{}, ", addr); 133 | 134 | if line_len + addr_str.len() > MIME_LINE_LENGTH { 135 | // Adding this would cause a wrap, so wrap before! 136 | header.push_str("\r\n\t"); 137 | line_len = 0; 138 | } 139 | line_len += addr_str.len(); 140 | header.push_str(&addr_str[..]); 141 | } 142 | 143 | // Clear up the final ", " 144 | let real_len = header.len() - 2; 145 | header.truncate(real_len); 146 | 147 | Ok(header) 148 | } 149 | } 150 | 151 | pub struct AddressParser<'s> { 152 | p: Rfc5322Parser<'s>, 153 | } 154 | 155 | impl<'s> AddressParser<'s> { 156 | pub fn new(s: &str) -> AddressParser { 157 | AddressParser { 158 | p: Rfc5322Parser::new(s), 159 | } 160 | } 161 | 162 | pub fn parse_address_list(&mut self) -> ParsingResult> { 163 | let mut result = Vec::new(); 164 | let mut expected_separator: char; 165 | 166 | while !self.p.eof() { 167 | self.p.push_position(); 168 | 169 | match self.parse_group() { 170 | Ok(x) => { 171 | // Is a group 172 | result.push(x); 173 | expected_separator = ';'; 174 | } 175 | Err(e) => { 176 | // If we failed to parse as group, try again as mailbox 177 | self.p.pop_position(); 178 | result.push(Address::Mailbox(match self.parse_mailbox() { 179 | Ok(x) => x, 180 | Err(e2) => { 181 | return Err(ParsingError::new(format!( 182 | "Failed to parse as group: {}\n\ 183 | Failed to parse as mailbox: {}", 184 | e, e2 185 | ))) 186 | } 187 | })); 188 | expected_separator = ','; 189 | } 190 | }; 191 | 192 | self.p.consume_linear_whitespace(); 193 | if !self.p.eof() && self.p.peek() == expected_separator { 194 | // Clear the separator 195 | self.p.consume_char(); 196 | } 197 | } 198 | 199 | Ok(result) 200 | } 201 | 202 | pub fn parse_group(&mut self) -> ParsingResult
{ 203 | let name = match self.p.consume_phrase(false) { 204 | Some(x) => x, 205 | None => { 206 | return Err(ParsingError::new(format!( 207 | "Couldn't find group name: {}", 208 | self.p.peek_to_end() 209 | ))) 210 | } 211 | }; 212 | 213 | self.p.assert_char(':')?; 214 | self.p.consume_char(); 215 | 216 | let mut mailboxes = Vec::new(); 217 | 218 | while !self.p.eof() && self.p.peek() != ';' { 219 | mailboxes.push(self.parse_mailbox()?); 220 | 221 | if !self.p.eof() && self.p.peek() == ',' { 222 | self.p.consume_char(); 223 | } 224 | } 225 | 226 | Ok(Address::Group(name, mailboxes)) 227 | } 228 | 229 | pub fn parse_mailbox(&mut self) -> ParsingResult { 230 | // Push the current position of the parser so we can back out later 231 | self.p.push_position(); 232 | match self.parse_name_addr() { 233 | Ok(result) => Ok(result), 234 | Err(_) => { 235 | // Revert back to our original position to try to parse an addr-spec 236 | self.p.pop_position(); 237 | Ok(Mailbox::new(self.parse_addr_spec()?)) 238 | } 239 | } 240 | } 241 | 242 | fn parse_name_addr(&mut self) -> ParsingResult { 243 | // Find display-name 244 | let display_name = self.p.consume_phrase(false); 245 | self.p.consume_linear_whitespace(); 246 | 247 | self.p.assert_char('<')?; 248 | self.p.consume_char(); 249 | 250 | let addr = self.parse_addr_spec()?; 251 | if self.p.consume_char() != Some('>') { 252 | // Fail because we should have a closing RANGLE here (to match the opening one) 253 | Err(ParsingError::new( 254 | "Missing '>' at end while parsing address header.".to_string(), 255 | )) 256 | } else { 257 | Ok(match display_name { 258 | Some(name) => Mailbox::new_with_name(name, addr), 259 | None => Mailbox::new(addr), 260 | }) 261 | } 262 | } 263 | 264 | fn parse_addr_spec(&mut self) -> ParsingResult { 265 | // local-part is a phrase, but allows dots in atoms 266 | let local_part = match self.p.consume_phrase(true) { 267 | Some(x) => x, 268 | None => { 269 | return Err(ParsingError::new( 270 | "Couldn't find local part while parsing address.".to_owned(), 271 | )) 272 | } 273 | }; 274 | 275 | self.p.assert_char('@')?; 276 | self.p.consume_char(); 277 | 278 | let domain = self.parse_domain()?; 279 | Ok(format!("{}@{}", local_part, domain)) 280 | } 281 | 282 | fn parse_domain(&mut self) -> ParsingResult { 283 | // TODO: support domain-literal 284 | match self.p.consume_atom(true) { 285 | Some(x) => Ok(x), 286 | None => Err(ParsingError::new("Failed to parse domain.".to_string())), 287 | } 288 | } 289 | } 290 | 291 | #[cfg(test)] 292 | mod tests { 293 | use super::*; 294 | 295 | use super::super::header::Header; 296 | 297 | #[test] 298 | fn test_address_to_string() { 299 | let addr = Mailbox::new("foo@example.org".to_string()); 300 | assert_eq!(addr.to_string(), "".to_string()); 301 | 302 | let name_addr = 303 | Mailbox::new_with_name("Joe Blogs".to_string(), "foo@example.org".to_string()); 304 | assert_eq!( 305 | name_addr.to_string(), 306 | "\"Joe Blogs\" ".to_string() 307 | ); 308 | } 309 | 310 | #[test] 311 | fn test_address_from_string() { 312 | let addr = "\"Joe Blogs\" " 313 | .parse::() 314 | .unwrap(); 315 | assert_eq!(addr.name.unwrap(), "Joe Blogs".to_string()); 316 | assert_eq!(addr.address, "joe@example.org".to_string()); 317 | 318 | assert!("Not an address".parse::().is_err()); 319 | } 320 | 321 | #[test] 322 | fn test_address_parsing() { 323 | let mut parser = AddressParser::new("\"Joe Blogs\" "); 324 | let mut addr = parser.parse_mailbox().unwrap(); 325 | assert_eq!(addr.name.unwrap(), "Joe Blogs".to_string()); 326 | assert_eq!(addr.address, "joe@example.org".to_string()); 327 | 328 | parser = AddressParser::new("joe@example.org"); 329 | addr = parser.parse_mailbox().unwrap(); 330 | assert_eq!(addr.name, None); 331 | assert_eq!(addr.address, "joe@example.org".to_string()); 332 | } 333 | 334 | #[test] 335 | fn test_invalid_address_parsing() { 336 | for a in vec![", \"John Doe\" ;".to_string() 358 | ); 359 | } 360 | 361 | #[test] 362 | fn test_address_group_parsing() { 363 | let mut parser = 364 | AddressParser::new("A Group:\"Joe Blogs\" ,john@example.org;"); 365 | let addr = parser.parse_group().unwrap(); 366 | match addr { 367 | Address::Group(name, mboxes) => { 368 | assert_eq!(name, "A Group".to_string()); 369 | assert_eq!( 370 | mboxes, 371 | vec![ 372 | Mailbox::new_with_name( 373 | "Joe Blogs".to_string(), 374 | "joe@example.org".to_string() 375 | ), 376 | Mailbox::new("john@example.org".to_string()), 377 | ] 378 | ); 379 | } 380 | _ => assert!(false), 381 | } 382 | } 383 | 384 | #[test] 385 | fn test_address_list_parsing() { 386 | let mut parser = 387 | AddressParser::new("\"Joe Blogs\" , \"John Doe\" "); 388 | assert_eq!( 389 | parser.parse_address_list().unwrap(), 390 | vec![ 391 | Address::new_mailbox_with_name( 392 | "Joe Blogs".to_string(), 393 | "joe@example.org".to_string() 394 | ), 395 | Address::new_mailbox_with_name( 396 | "John Doe".to_string(), 397 | "john@example.org".to_string() 398 | ), 399 | ] 400 | ); 401 | } 402 | 403 | #[test] 404 | fn test_address_list_parsing_groups() { 405 | let mut parser = AddressParser::new("A Group:\"Joe Blogs\" , \"John Doe\" ; , "); 406 | assert_eq!( 407 | parser.parse_address_list().unwrap(), 408 | vec![ 409 | Address::new_group( 410 | "A Group".to_string(), 411 | vec![ 412 | Mailbox::new_with_name( 413 | "Joe Blogs".to_string(), 414 | "joe@example.org".to_string() 415 | ), 416 | Mailbox::new_with_name( 417 | "John Doe".to_string(), 418 | "john@example.org".to_string() 419 | ), 420 | ] 421 | ), 422 | Address::new_mailbox("third@example.org".to_string()), 423 | Address::new_mailbox("fourth@example.org".to_string()), 424 | ] 425 | ); 426 | } 427 | 428 | #[test] 429 | fn test_from_header_parsing() { 430 | let header = Header::new( 431 | "From:".to_string(), 432 | "\"Joe Blogs\" , \"John Doe\" ".to_string(), 433 | ); 434 | let addresses: Vec
= header.get_value().unwrap(); 435 | assert_eq!( 436 | addresses, 437 | vec![ 438 | Address::new_mailbox_with_name( 439 | "Joe Blogs".to_string(), 440 | "joe@example.org".to_string() 441 | ), 442 | Address::new_mailbox_with_name( 443 | "John Doe".to_string(), 444 | "john@example.org".to_string() 445 | ), 446 | ] 447 | ); 448 | } 449 | 450 | #[test] 451 | fn test_to_header_generation() { 452 | let addresses = vec![ 453 | Address::new_mailbox_with_name("Joe Blogs".to_string(), "joe@example.org".to_string()), 454 | Address::new_mailbox_with_name("John Doe".to_string(), "john@example.org".to_string()), 455 | ]; 456 | 457 | let header = Header::new_with_value("From:".to_string(), addresses).unwrap(); 458 | assert_eq!( 459 | header.get_value::().unwrap(), 460 | "\"Joe Blogs\" , \"John Doe\" ".to_string() 461 | ); 462 | } 463 | 464 | #[test] 465 | fn test_to_header_line_wrap() { 466 | let addresses = vec![ 467 | Address::new_mailbox_with_name("Joe Blogs".to_string(), "joe@example.org".to_string()), 468 | Address::new_mailbox_with_name("John Doe".to_string(), "john@example.org".to_string()), 469 | Address::new_mailbox_with_name( 470 | "Mr Black".to_string(), 471 | "mafia_black@example.org".to_string(), 472 | ), 473 | ]; 474 | 475 | let header = Header::new_with_value("To".to_string(), addresses).unwrap(); 476 | assert_eq!( 477 | &header.to_string()[..], 478 | "To: \"Joe Blogs\" , \"John Doe\" , \r\n\ 479 | \t\"Mr Black\" " 480 | ); 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /src/rfc5322.rs: -------------------------------------------------------------------------------- 1 | //! Module with helpers for dealing with RFC 5322 2 | 3 | use super::header::{Header, HeaderMap}; 4 | use super::results::{ParsingError, ParsingResult}; 5 | use super::rfc2047::decode_rfc2047; 6 | 7 | pub const MIME_LINE_LENGTH: usize = 78; 8 | 9 | trait Rfc5322Character { 10 | /// Is considered a special character by RFC 5322 Section 3.2.3 11 | fn is_special(&self) -> bool; 12 | /// Is considered to be a VCHAR by RFC 5234 Appendix B.1 13 | fn is_vchar(&self) -> bool; 14 | /// Is considered to be field text as defined by RFC 5322 Section 3.6.8 15 | fn is_ftext(&self) -> bool; 16 | 17 | fn is_atext(&self) -> bool { 18 | self.is_vchar() && !self.is_special() 19 | } 20 | } 21 | 22 | impl Rfc5322Character for char { 23 | fn is_ftext(&self) -> bool { 24 | match *self { 25 | '!'..='9' | ';'..='~' => true, 26 | _ => false, 27 | } 28 | } 29 | 30 | fn is_special(&self) -> bool { 31 | match *self { 32 | '(' | ')' | '<' | '>' | '[' | ']' | ':' | ';' | '@' | '\\' | ',' | '.' | '\"' | ' ' => { 33 | true 34 | } 35 | _ => false, 36 | } 37 | } 38 | 39 | fn is_vchar(&self) -> bool { 40 | match *self { 41 | '!'..='~' => true, 42 | _ => false, 43 | } 44 | } 45 | } 46 | 47 | /// RFC 5322 base parser for parsing 48 | /// `atom`, `dot-atom`, `quoted-string`, `phrase`, `message` 49 | /// 50 | /// This should prove useful for parsing other things that appear in RFC 5322, 51 | /// as most are based off these core items. 52 | /// 53 | /// It also implements a stack for tracking the position. 54 | /// This allows the simple implementation of backtracking, by pushing the position 55 | /// before a test and popping it if the test should fail. 56 | /// [unstable] 57 | pub struct Rfc5322Parser<'s> { 58 | s: &'s str, 59 | pos: usize, 60 | pos_stack: Vec, 61 | } 62 | 63 | impl<'s> Rfc5322Parser<'s> { 64 | /// Make a new parser, initialized with the given string. 65 | /// [unstable] 66 | pub fn new(source: &'s str) -> Rfc5322Parser<'s> { 67 | Rfc5322Parser { 68 | s: source, 69 | pos: 0, 70 | pos_stack: Vec::new(), 71 | } 72 | } 73 | 74 | /// Push the current position onto the stack. 75 | /// [unstable] 76 | pub fn push_position(&mut self) { 77 | self.pos_stack.push(self.pos); 78 | } 79 | 80 | /// Move the position back to the last entry pushed 81 | /// [unstable] 82 | pub fn pop_position(&mut self) { 83 | match self.pos_stack.pop() { 84 | Some(pos) => { 85 | self.pos = pos; 86 | } 87 | None => panic!("Popped position stack too far"), 88 | } 89 | } 90 | 91 | /// Consume a message from the input. 92 | /// 93 | /// Returns as a map of the headers and the body text. 94 | /// 95 | /// A message is defined as: 96 | /// 97 | /// `fields = *field 98 | /// body = text 99 | /// message = fields CRLF body` 100 | /// [unstable] 101 | pub fn consume_message(&mut self) -> Option<(HeaderMap, String)> { 102 | let mut headers = HeaderMap::new(); 103 | while !self.eof() { 104 | let header = self.consume_header(); 105 | if let Some(header) = header { 106 | headers.insert(header); 107 | } else { 108 | // Check end of headers as marked by CRLF 109 | if !self.eof() && self.peek_linebreak() { 110 | assert!(self.consume_linebreak()); 111 | } 112 | 113 | break; 114 | } 115 | } 116 | 117 | // Whatever remains is the body 118 | let body = self.s[self.pos..].to_string(); 119 | self.pos = self.s.len(); 120 | 121 | Some((headers, body)) 122 | } 123 | 124 | /// Consume a header from the input. 125 | /// 126 | /// A header is defined as: 127 | /// 128 | /// `ftext = "!".."9" / ";".."~" 129 | /// field-name = 1*ftext 130 | /// field = field-name *LWSP ":" unstructured` 131 | /// [unstable] 132 | pub fn consume_header(&mut self) -> Option
{ 133 | let last_pos = self.pos; 134 | // Parse field-name 135 | let field_name = self.consume_while(|c| c.is_ftext()); 136 | self.consume_linear_whitespace(); 137 | if field_name.is_empty() || self.eof() || self.peek() != ':' { 138 | // Fail to parse if we didn't see a field, we're at the end of input 139 | // or we haven't just seen a ":" 140 | self.pos = last_pos; 141 | None 142 | } else { 143 | // Consume the ":" and any leading whitespace 144 | self.consume_char(); 145 | self.consume_linear_whitespace(); 146 | let field_value = self.consume_unstructured(); 147 | 148 | // don't just panic!() 149 | if !self.consume_linebreak() { 150 | return None; 151 | }; 152 | 153 | Some(Header::new(field_name, field_value)) 154 | } 155 | } 156 | 157 | /// Consume an unstructured from the input. 158 | /// [unstable] 159 | pub fn consume_unstructured(&mut self) -> String { 160 | let mut result = String::new(); 161 | while !self.eof() { 162 | if self.peek_linebreak() { 163 | // Check for folding whitespace, if it wasn't, then 164 | // we're done parsing 165 | if !self.consume_folding_whitespace() { 166 | break; 167 | } 168 | } 169 | 170 | let fragment = &self.consume_while(|c| c.is_vchar() || c == ' ' || c == '\t')[..]; 171 | 172 | if fragment.is_empty() { 173 | // If the text no longer matches the expected format, then we 174 | // should stop. 175 | break; 176 | } 177 | 178 | result.push_str(fragment); 179 | } 180 | result 181 | } 182 | 183 | /// Consume folding whitespace. 184 | /// 185 | /// This is a CRLF followed by one or more whitespace character. 186 | /// 187 | /// Returns true if whitespace was consume 188 | /// [unstable] 189 | pub fn consume_folding_whitespace(&mut self) -> bool { 190 | // Remember where we were, in case this isn't folding whitespace 191 | let current_position = self.pos; 192 | let is_fws = if !self.eof() && self.consume_linebreak() { 193 | match self.consume_char() { 194 | Some(' ') | Some('\t') => true, 195 | _ => false, 196 | } 197 | } else { 198 | false 199 | }; 200 | 201 | if is_fws { 202 | // This was a folding whitespace, so consume all linear whitespace 203 | self.consume_linear_whitespace(); 204 | } else { 205 | // Reset back if we didn't see a folding whitespace 206 | self.pos = current_position; 207 | } 208 | 209 | is_fws 210 | } 211 | 212 | /// Consume a word from the input. 213 | /// 214 | /// A word is defined as: 215 | /// 216 | /// `word = atom / quoted-string` 217 | /// 218 | /// If `allow_dot_atom` is true, then `atom` can be a `dot-atom` in this phrase. 219 | /// [unstable] 220 | pub fn consume_word(&mut self, allow_dot_atom: bool) -> Option { 221 | let p = self.peek(); 222 | if p == '"' { 223 | // Word is a quoted string 224 | self.consume_quoted_string() 225 | } else { 226 | // Word is an atom (or not a word) 227 | self.consume_atom(allow_dot_atom) 228 | } 229 | } 230 | 231 | /// Consume a phrase from the input. 232 | /// 233 | /// A phrase is defined as: 234 | /// 235 | /// `phrase = 1*word` 236 | /// 237 | /// If `allow_dot_atom` is true, then `atom` can be a `dot-atom` in this phrase. 238 | /// [unstable] 239 | pub fn consume_phrase(&mut self, allow_dot_atom: bool) -> Option { 240 | let mut phrase = String::new(); 241 | 242 | while !self.eof() { 243 | self.consume_linear_whitespace(); 244 | 245 | let word = match self.consume_word(allow_dot_atom) { 246 | Some(x) => x, 247 | None => break, // If it's not a word, it's no longer 248 | // in a phrase, so stop. 249 | }; 250 | 251 | let w_slice = &word[..]; 252 | // RFC 2047 encoded words start with =?, end with ?= 253 | let decoded_word = if w_slice.starts_with("=?") && w_slice.ends_with("?=") { 254 | match decode_rfc2047(w_slice) { 255 | Some(w) => w, 256 | None => w_slice.to_string(), 257 | } 258 | } else { 259 | w_slice.to_string() 260 | }; 261 | 262 | // Make sure we put a leading space on, if this isn't the first insertion 263 | if !phrase.is_empty() { 264 | phrase.push_str(" "); 265 | } 266 | phrase.push_str(&decoded_word[..]); 267 | } 268 | 269 | if !phrase.is_empty() { 270 | Some(phrase) 271 | } else { 272 | None 273 | } 274 | } 275 | 276 | /// Consume a quoted string from the input 277 | /// [unstable] 278 | pub fn consume_quoted_string(&mut self) -> Option { 279 | if self.peek() != '"' { 280 | // Fail if we were called wrong 281 | None 282 | } else { 283 | let mut quoted_string = String::new(); 284 | let mut inside_escape = false; 285 | let mut terminated = false; 286 | // Consume the leading " 287 | self.consume_char(); 288 | while !terminated && !self.eof() { 289 | match self.peek() { 290 | '\\' if !inside_escape => { 291 | // If we were not already being escaped, consume the 292 | // escape character and mark that we're being escaped. 293 | self.consume_char(); 294 | inside_escape = true; 295 | } 296 | '"' if !inside_escape => { 297 | // If this is a DQUOTE and we haven't seen an escape character, 298 | // consume it and mark that we should break from the loop 299 | self.consume_char(); 300 | terminated = true; 301 | } 302 | _ => { 303 | // Any old character gets pushed in 304 | if let Some(c) = self.consume_char() { 305 | quoted_string.push(c); 306 | // Clear any escape character state we have 307 | inside_escape = false; 308 | } 309 | // TODO: Should this return a Result<> instead of an Option<>? 310 | else { 311 | return None; 312 | } 313 | } 314 | } 315 | } 316 | 317 | if inside_escape || !terminated { 318 | // Return an error state if we're still expecting a character 319 | None 320 | } else { 321 | Some(quoted_string) 322 | } 323 | } 324 | } 325 | 326 | /// Consume an atom from the input. 327 | /// 328 | /// If `allow_dot` is true, then also allow '.' to be considered as an 329 | /// atext character. 330 | /// [unstable] 331 | pub fn consume_atom(&mut self, allow_dot: bool) -> Option { 332 | if self.eof() || !self.peek().is_atext() { 333 | None 334 | } else { 335 | Some(self.consume_while(|c| c.is_atext() || (allow_dot && c == '.'))) 336 | } 337 | } 338 | 339 | /// Consume LWSP (Linear whitespace) 340 | /// [unstable] 341 | pub fn consume_linear_whitespace(&mut self) { 342 | self.consume_while(|c| c == '\t' || c == ' '); 343 | } 344 | 345 | /// Consume a single character from the input. 346 | #[inline] 347 | /// [unstable] 348 | pub fn consume_char(&mut self) -> Option { 349 | if self.eof() { 350 | return None; 351 | } 352 | let c = self.peek(); 353 | self.pos += c.len_utf8(); 354 | Some(c) 355 | } 356 | 357 | // Consume a linebreak: \r\n, \r or \n 358 | /// [unstable] 359 | pub fn consume_linebreak(&mut self) -> bool { 360 | if self.eof() { 361 | return false; 362 | } 363 | 364 | let start_pos = self.pos; 365 | 366 | match self.consume_char() { 367 | Some('\r') => { 368 | // Try to consume a single \n following the \r 369 | if !self.eof() && self.peek() == '\n' { 370 | self.consume_char(); 371 | } 372 | true 373 | } 374 | Some('\n') => true, 375 | _ => { 376 | self.pos = start_pos; 377 | false 378 | } 379 | } 380 | } 381 | 382 | // Peek at the current character and determine whether it's (part of) a linebreak 383 | /// [unstable] 384 | pub fn peek_linebreak(&mut self) -> bool { 385 | match self.peek() { 386 | '\r' | '\n' => true, 387 | _ => false, 388 | } 389 | } 390 | 391 | /// Consume a set of characters, each passed to `test` until this function 392 | /// returns false. 393 | /// 394 | /// The position after calling this function will be pointing to the character 395 | /// which caused a false result from `test`. 396 | /// 397 | /// Returns the string of characters that returned true for the test function. 398 | #[inline] 399 | /// [unstable] 400 | pub fn consume_while bool>(&mut self, test: F) -> String { 401 | let start_pos = self.pos; 402 | while !self.eof() && test(self.peek()) { 403 | self.consume_char(); 404 | } 405 | self.s[start_pos..self.pos].to_string() 406 | } 407 | 408 | /// Peek at the current character. 409 | /// 410 | /// Note that this does not do any bounds checking. 411 | #[inline] 412 | /// [unstable] 413 | pub fn peek(&self) -> char { 414 | self.s[self.pos..] 415 | .chars() 416 | .next() 417 | .unwrap_or(char::REPLACEMENT_CHARACTER) 418 | } 419 | 420 | /// Check that `!self.eof() && self.peek() == c` 421 | #[inline] 422 | /// [unstable] 423 | pub fn assert_char(&self, c: char) -> ParsingResult<()> { 424 | self.assert_not_eof()?; 425 | 426 | let actual_c = self.peek(); 427 | if c == actual_c { 428 | Ok(()) 429 | } else { 430 | Err(ParsingError::new(format!( 431 | "Expected {}, got {}", 432 | c, actual_c 433 | ))) 434 | } 435 | } 436 | 437 | /// Check that we have not reached the end of the input. 438 | #[inline] 439 | /// [unstable] 440 | pub fn assert_not_eof(&self) -> ParsingResult<()> { 441 | if self.eof() { 442 | Err(ParsingError::new("Reached EOF.".to_string())) 443 | } else { 444 | Ok(()) 445 | } 446 | } 447 | 448 | /// Get the unconsumed string. Should only be used for debugging purposes! 449 | #[inline] 450 | /// [unstable] 451 | pub fn peek_to_end(&self) -> &str { 452 | &self.s[self.pos..] 453 | } 454 | 455 | /// Returns true if we have reached the end of the input. 456 | #[inline] 457 | /// [unstable] 458 | pub fn eof(&self) -> bool { 459 | self.pos >= self.s.len() 460 | } 461 | } 462 | 463 | /// Type for constructing RFC 5322 messages 464 | pub struct Rfc5322Builder { 465 | result: String, 466 | } 467 | 468 | impl Rfc5322Builder { 469 | /// Make a new builder, with an empty string 470 | pub fn new() -> Rfc5322Builder { 471 | Rfc5322Builder { 472 | result: "".to_string(), 473 | } 474 | } 475 | 476 | pub fn result(&self) -> &String { 477 | &self.result 478 | } 479 | 480 | pub fn emit_raw(&mut self, s: &str) { 481 | self.result.push_str(s); 482 | } 483 | 484 | pub fn emit_folded(&mut self, s: &str) { 485 | let mut cur_len = 0; 486 | let mut last_space = 0; 487 | let mut last_cut = 0; 488 | 489 | for (pos, c) in s.char_indices() { 490 | match c { 491 | ' ' => { 492 | last_space = pos; 493 | } 494 | '\r' => { 495 | cur_len = 0; 496 | } 497 | '\n' => { 498 | cur_len = 0; 499 | } 500 | _ => {} 501 | } 502 | 503 | cur_len += 1; 504 | // We've reached our line length, so 505 | if cur_len >= MIME_LINE_LENGTH && last_space > 0 { 506 | // Emit the string from the last place we cut it to the 507 | // last space that we saw 508 | self.emit_raw(&s[last_cut..last_space]); 509 | // ... and get us ready to put out the continuation 510 | self.emit_raw("\r\n\t"); 511 | 512 | // Reset our counters 513 | cur_len = 0; 514 | last_cut = last_space + s[last_space..].chars().next().unwrap().len_utf8(); 515 | last_space = 0; 516 | } 517 | } 518 | 519 | // Finally, emit everything left in the string 520 | self.emit_raw(&s[last_cut..]); 521 | } 522 | } 523 | 524 | impl Default for Rfc5322Builder { 525 | fn default() -> Self { 526 | Rfc5322Builder::new() 527 | } 528 | } 529 | 530 | #[cfg(test)] 531 | mod tests { 532 | use super::*; 533 | 534 | struct PhraseTestCase<'s> { 535 | input: &'s str, 536 | output: &'s str, 537 | name: &'s str, 538 | } 539 | 540 | #[test] 541 | fn test_parser() { 542 | let mut parser = Rfc5322Parser::new(""); 543 | assert!(parser.consume_message().is_some()); 544 | 545 | let mut parser = Rfc5322Parser::new("\r\n"); 546 | assert!(parser.consume_message().is_some()); 547 | 548 | let mut parser = Rfc5322Parser::new("From: Garbage@-\r\n"); 549 | assert!(parser.consume_message().is_some()); 550 | 551 | let mut parser = Rfc5322Parser::new("From: Garbage@"); 552 | assert!(parser.consume_message().is_some()); 553 | 554 | let mut parser = Rfc5322Parser::new("From: Garnage@-"); 555 | assert!(parser.consume_message().is_some()); 556 | } 557 | 558 | #[test] 559 | fn test_consume_phrase() { 560 | let tests = [ 561 | PhraseTestCase { 562 | input: "\"test phrase\"", output: "test phrase", 563 | name: "Simple quoted-string" 564 | }, 565 | PhraseTestCase { 566 | input: "\"test \\\"phrase\\\"\"", output: "test \"phrase\"", 567 | name: "quoted-string with escape character" 568 | }, 569 | PhraseTestCase { 570 | input: "\"=?utf-8?q?encoded=20q-string?=\"", output: "encoded q-string", 571 | name: "Encoded quoted-string" 572 | }, 573 | PhraseTestCase { 574 | input: "atom test", output: "atom test", 575 | name: "Collection of atoms" 576 | }, 577 | PhraseTestCase { 578 | input: "=?utf-8?q?encoded=20atom?=", output: "encoded atom", 579 | name: "Encoded atom" 580 | }, 581 | PhraseTestCase { 582 | input: "Mix of atoms \"and quoted strings\"", output: "Mix of atoms and quoted strings", 583 | name: "Mix of atoms and quoted strings" 584 | }, 585 | PhraseTestCase { 586 | input: "=?utf-8?q?encoded=20atoms?= mixed with \"unencoded\" \"=?utf-8?b?YW5kIGVuY29kZWQgcS1zdHJpbmdz?=\"", 587 | output: "encoded atoms mixed with unencoded and encoded q-strings", 588 | name: "Mix of atoms, q-strings of differing encodings" 589 | }, 590 | PhraseTestCase { 591 | input: "\"John Smith\" ", output: "John Smith", 592 | name: "Stop consuming phrase at \"special\" character", 593 | } 594 | ]; 595 | 596 | for t in tests.iter() { 597 | let mut p = Rfc5322Parser::new(t.input); 598 | let phrase = p.consume_phrase(false); 599 | assert!(phrase.is_some(), format!("{} returned Some", t.name)); 600 | let test_name = format!("{} == {} for {}", phrase.clone().unwrap(), t.output, t.name); 601 | assert!(phrase.unwrap() == t.output.to_string(), test_name); 602 | } 603 | } 604 | 605 | struct MessageTestCase<'s> { 606 | input: &'s str, 607 | headers: Vec<(&'s str, &'s str)>, 608 | body: &'s str, 609 | } 610 | 611 | #[test] 612 | fn test_consume_message() { 613 | let tests = vec![ 614 | MessageTestCase { 615 | input: "From: \"Joe Blogs\" \r\n\r\nBody", 616 | headers: vec![ 617 | ("From", "\"Joe Blogs\" "), 618 | ], 619 | body: "Body", 620 | }, 621 | // Support parsing messages with \n instead of \r\n 622 | MessageTestCase { 623 | input: "From: \"Joe Blogs\" \n\nBody", 624 | headers: vec![ 625 | ("From", "\"Joe Blogs\" "), 626 | ], 627 | body: "Body", 628 | }, 629 | MessageTestCase { 630 | input: "From: \"Joe Blogs\" \r\n\r\nMultiline\r\nBody", 631 | headers: vec![ 632 | ("From", "\"Joe Blogs\" "), 633 | ], 634 | body: "Multiline\r\nBody", 635 | }, 636 | MessageTestCase { 637 | input: "From: \"Joe Blogs\" \r\nTo: \"John Doe\" \r\n\r\nMultiple headers", 638 | headers: vec![ 639 | ("From", "\"Joe Blogs\" "), 640 | ("To", "\"John Doe\" "), 641 | ], 642 | body: "Multiple headers", 643 | }, 644 | MessageTestCase { 645 | input: "Folded-Header: Some content that is \r\n\t wrapped with a tab.\r\n\r\nFolding whitespace test", 646 | headers: vec![ 647 | ("Folded-Header", "Some content that is wrapped with a tab."), 648 | ], 649 | body: "Folding whitespace test", 650 | }, 651 | MessageTestCase { 652 | input: "Folded-Header: Some content that is \r\n wrapped with spaces.\r\n\r\nFolding whitespace test", 653 | headers: vec![ 654 | ("Folded-Header", "Some content that is wrapped with spaces."), 655 | ], 656 | body: "Folding whitespace test", 657 | }, 658 | ]; 659 | 660 | for test in tests.iter() { 661 | let mut p = Rfc5322Parser::new(test.input); 662 | let message = p.consume_message(); 663 | match message { 664 | Some((headers, body)) => { 665 | assert_eq!(body, test.body.to_string()); 666 | for &(header_title, header_value) in test.headers.iter() { 667 | let matching_headers = headers.find(&header_title.to_string()).unwrap(); 668 | assert!( 669 | matching_headers 670 | .iter() 671 | .filter(|h| { 672 | let val: String = h.get_value().unwrap(); 673 | val == header_value.to_string() 674 | }) 675 | .count() 676 | > 0 677 | ); 678 | } 679 | } 680 | None => panic!("Failed to parse message"), 681 | }; 682 | } 683 | } 684 | 685 | #[test] 686 | fn test_builder_folding() { 687 | struct BuildFoldTest<'s> { 688 | input: &'s str, 689 | expected: &'s str, 690 | } 691 | 692 | let tests = vec![ 693 | BuildFoldTest { 694 | input: "A long line that should get folded on a space at some point around here, possibly at this point.", 695 | expected: "A long line that should get folded on a space at some point around here,\r\n\ 696 | \tpossibly at this point.", 697 | }, 698 | BuildFoldTest { 699 | input: "A long line that should get folded on a space at some point around here, possibly at this point. And yet more content that will get folded onto another line.", 700 | expected: "A long line that should get folded on a space at some point around here,\r\n\ 701 | \tpossibly at this point. And yet more content that will get folded onto another\r\n\ 702 | \tline.", 703 | }, 704 | ]; 705 | 706 | for test in tests.into_iter() { 707 | let mut gen = Rfc5322Builder::new(); 708 | gen.emit_folded(test.input); 709 | assert_eq!(gen.result(), &test.expected.to_string()); 710 | } 711 | } 712 | } 713 | -------------------------------------------------------------------------------- /src/message.rs: -------------------------------------------------------------------------------- 1 | use super::header::{Header, HeaderMap}; 2 | use super::mimeheaders::{MimeContentTransferEncoding, MimeContentType, MimeContentTypeHeader}; 3 | use super::results::{ParsingError, ParsingResult}; 4 | use super::rfc5322::{Rfc5322Builder, Rfc5322Parser}; 5 | 6 | use std::collections::HashMap; 7 | 8 | use encoding::Encoding; 9 | 10 | use rand::distributions::Alphanumeric; 11 | use rand::{thread_rng, Rng}; 12 | 13 | const BOUNDARY_LENGTH: usize = 30; 14 | 15 | /// Marks the type of a multipart message 16 | #[derive(Eq, PartialEq, Debug, Clone, Copy)] 17 | pub enum MimeMultipartType { 18 | /// Entries which are independent. 19 | /// 20 | /// This value is the default. 21 | /// 22 | /// As defined by Section 5.1.3 of RFC 2046 23 | Mixed, 24 | /// Entries which are interchangeable, such that the system can choose 25 | /// whichever is "best" for its use. 26 | /// 27 | /// As defined by Section 5.1.4 of RFC 2046 28 | Alternative, 29 | /// Entries are (typically) a collection of messages. 30 | /// 31 | /// As defined by Section 5.1.5 of RFC 2046 32 | Digest, 33 | /// Two entries, the first of which explains the decryption process for 34 | /// the second body part. 35 | /// 36 | /// As defined by Section 2.2 of RFC 1847 37 | Encrypted, 38 | /// Entry order does not matter, and could be displayed simultaneously. 39 | /// 40 | /// As defined by Section 5.1.6 of RFC 2046 41 | Parallel, 42 | /// Two entries, the first of which is the content, the second is a 43 | /// digital signature of the first, including MIME headers. 44 | /// 45 | /// As defined by Section 2.1 of RFC 1847 46 | Signed, 47 | } 48 | 49 | impl MimeMultipartType { 50 | /// Returns the appropriate `MimeMultipartType` for the given MimeContentType 51 | pub fn from_content_type(ct: MimeContentType) -> Option { 52 | let (major, minor) = ct; 53 | match (&major[..], &minor[..]) { 54 | ("multipart", "alternative") => Some(MimeMultipartType::Alternative), 55 | ("multipart", "digest") => Some(MimeMultipartType::Digest), 56 | ("multipart", "encrypted") => Some(MimeMultipartType::Encrypted), 57 | ("multipart", "parallel") => Some(MimeMultipartType::Parallel), 58 | ("multipart", "signed") => Some(MimeMultipartType::Signed), 59 | ("multipart", "mixed") | ("multipart", _) => Some(MimeMultipartType::Mixed), 60 | _ => None, 61 | } 62 | } 63 | 64 | /// Returns a MimeContentType that represents this multipart type. 65 | pub fn to_content_type(self) -> MimeContentType { 66 | let multipart = "multipart".to_string(); 67 | match self { 68 | MimeMultipartType::Mixed => (multipart, "mixed".to_string()), 69 | MimeMultipartType::Alternative => (multipart, "alternative".to_string()), 70 | MimeMultipartType::Digest => (multipart, "digest".to_string()), 71 | MimeMultipartType::Encrypted => (multipart, "encrypted".to_string()), 72 | MimeMultipartType::Parallel => (multipart, "parallel".to_string()), 73 | MimeMultipartType::Signed => (multipart, "signed".to_string()), 74 | } 75 | } 76 | } 77 | 78 | /// Represents a MIME message 79 | /// [unstable] 80 | #[derive(Eq, PartialEq, Debug, Clone)] 81 | pub struct MimeMessage { 82 | /// The headers for this message 83 | pub headers: HeaderMap, 84 | 85 | /// The content of this message 86 | /// 87 | /// Keep in mind that this is the undecoded form, so may be quoted-printable 88 | /// or base64 encoded. 89 | pub body: String, 90 | 91 | /// The MIME multipart message type of this message, or `None` if the message 92 | /// is not a multipart message. 93 | pub message_type: Option, 94 | 95 | /// Any additional parameters of the MIME multipart header, not including the boundary. 96 | pub message_type_params: Option>, 97 | 98 | /// The sub-messages of this message 99 | pub children: Vec, 100 | 101 | /// The boundary used for MIME multipart messages 102 | /// 103 | /// This will always be set, even if the message only has a single part 104 | pub boundary: String, 105 | } 106 | 107 | impl MimeMessage { 108 | fn random_boundary() -> String { 109 | let mut rng = thread_rng(); 110 | std::iter::repeat(()) 111 | .map(|()| rng.sample(Alphanumeric)) 112 | .take(BOUNDARY_LENGTH) 113 | .collect() 114 | } 115 | 116 | /// [unstable] 117 | pub fn new(body: String) -> MimeMessage { 118 | let mut message = MimeMessage::new_blank_message(); 119 | message.body = body; 120 | message.update_headers(); 121 | message 122 | } 123 | 124 | pub fn new_with_children( 125 | body: String, 126 | message_type: MimeMultipartType, 127 | children: Vec, 128 | ) -> MimeMessage { 129 | let mut message = MimeMessage::new_blank_message(); 130 | message.body = body; 131 | message.message_type = Some(message_type); 132 | message.children = children; 133 | message.update_headers(); 134 | message 135 | } 136 | 137 | pub fn new_with_boundary( 138 | body: String, 139 | message_type: MimeMultipartType, 140 | children: Vec, 141 | boundary: String, 142 | ) -> MimeMessage { 143 | let mut message = MimeMessage::new_blank_message(); 144 | message.body = body; 145 | message.message_type = Some(message_type); 146 | message.children = children; 147 | message.boundary = boundary; 148 | message.update_headers(); 149 | message 150 | } 151 | 152 | pub fn new_with_boundary_and_params( 153 | body: String, 154 | message_type: MimeMultipartType, 155 | children: Vec, 156 | boundary: String, 157 | message_type_params: Option>, 158 | ) -> MimeMessage { 159 | let mut message = MimeMessage::new_blank_message(); 160 | message.body = body; 161 | message.message_type = Some(message_type); 162 | message.children = children; 163 | message.boundary = boundary; 164 | message.message_type_params = message_type_params; 165 | message.update_headers(); 166 | message 167 | } 168 | 169 | pub fn new_blank_message() -> MimeMessage { 170 | MimeMessage { 171 | headers: HeaderMap::new(), 172 | body: "".to_string(), 173 | message_type: None, 174 | message_type_params: None, 175 | children: Vec::new(), 176 | 177 | boundary: MimeMessage::random_boundary(), 178 | } 179 | } 180 | 181 | /// Update the headers on this message based on the internal state. 182 | /// 183 | /// When certain properties of the message are modified, the headers 184 | /// used to represent them are not automatically updated. 185 | /// Call this if these are changed. 186 | pub fn update_headers(&mut self) { 187 | if !self.children.is_empty() && self.message_type.is_none() { 188 | // This should be a multipart message, so make it so! 189 | self.message_type = Some(MimeMultipartType::Mixed); 190 | } 191 | 192 | if let Some(message_type) = self.message_type { 193 | // We are some form of multi-part message, so update our 194 | // Content-Type header. 195 | let mut params = match &self.message_type_params { 196 | Some(p) => p.clone(), 197 | None => HashMap::new(), 198 | }; 199 | params.insert("boundary".to_string(), self.boundary.clone()); 200 | let ct_header = MimeContentTypeHeader { 201 | content_type: message_type.to_content_type(), 202 | params, 203 | }; 204 | self.headers 205 | .replace(Header::new_with_value("Content-Type".to_string(), ct_header).unwrap()); 206 | } 207 | } 208 | 209 | /// Parse `s` into a MimeMessage. 210 | /// 211 | /// Recurses down into each message, supporting an unlimited depth of messages. 212 | /// 213 | /// Be warned that each sub-message that fails to be parsed will be thrown away. 214 | /// [unstable] 215 | pub fn parse(s: &str) -> ParsingResult { 216 | let mut parser = Rfc5322Parser::new(s); 217 | match parser.consume_message() { 218 | Some((headers, body)) => MimeMessage::from_headers(headers, body), 219 | None => Err(ParsingError::new( 220 | "Couldn't parse MIME message.".to_string(), 221 | )), 222 | } 223 | } 224 | 225 | pub fn as_string(&self) -> String { 226 | let mut builder = Rfc5322Builder::new(); 227 | 228 | for header in self.headers.iter() { 229 | builder.emit_folded(&header.to_string()[..]); 230 | builder.emit_raw("\r\n"); 231 | } 232 | builder.emit_raw("\r\n"); 233 | 234 | self.as_string_without_headers_internal(builder) 235 | } 236 | 237 | pub fn as_string_without_headers(&self) -> String { 238 | let builder = Rfc5322Builder::new(); 239 | 240 | self.as_string_without_headers_internal(builder) 241 | } 242 | 243 | fn as_string_without_headers_internal(&self, mut builder: Rfc5322Builder) -> String { 244 | builder.emit_raw(&format!("{}\r\n", self.body)[..]); 245 | 246 | if !self.children.is_empty() { 247 | for part in self.children.iter() { 248 | builder.emit_raw(&format!("--{}\r\n{}\r\n", self.boundary, part.as_string())[..]); 249 | } 250 | 251 | builder.emit_raw(&format!("--{}--\r\n", self.boundary)[..]); 252 | } 253 | 254 | builder.result().clone() 255 | } 256 | 257 | /// Decode the body of this message, as a series of bytes 258 | pub fn decoded_body_bytes(&self) -> Option> { 259 | let transfer_encoding: MimeContentTransferEncoding = self 260 | .headers 261 | .get_value("Content-Transfer-Encoding".to_string()) 262 | .unwrap_or(MimeContentTransferEncoding::Identity); 263 | transfer_encoding.decode(&self.body) 264 | } 265 | 266 | /// Decode the body of this message, as a string. 267 | /// 268 | /// This takes into account any charset as set on the `Content-Type` header, 269 | /// decoding the bytes with this character set. 270 | pub fn decoded_body_string(&self) -> ParsingResult { 271 | let bytes = match self.decoded_body_bytes() { 272 | // FIXME 273 | Some(x) => x, 274 | None => { 275 | return Err(ParsingError::new( 276 | "Unable to get decoded body bytes.".to_string(), 277 | )) 278 | } 279 | }; 280 | 281 | let content_type: Result = 282 | self.headers.get_value("Content-Type".to_string()); 283 | let charset = match &content_type { 284 | Ok(ct) => ct.params.get("charset").map(String::as_str), 285 | Err(_) => None, 286 | } 287 | .unwrap_or_else(|| "us-ascii"); 288 | 289 | match Encoding::for_label(charset.as_bytes()) { 290 | Some(decoder) => { 291 | let (x, ..) = decoder.decode(&bytes); 292 | Ok(x.into_owned()) 293 | } 294 | None => Err(ParsingError::new(format!("Invalid encoding: {}", charset))), 295 | } 296 | } 297 | 298 | // Make a message from a header map and body, parsing out any multi-part 299 | // messages that are discovered by looking at the Content-Type header. 300 | fn from_headers(headers: HeaderMap, body: String) -> ParsingResult { 301 | let content_type = { 302 | let header = headers.get("Content-Type".to_string()); 303 | match header { 304 | Some(h) => h.get_value(), 305 | None => Ok(MimeContentTypeHeader { 306 | content_type: ("text".to_string(), "plain".to_string()), 307 | params: HashMap::new(), 308 | }), 309 | } 310 | }?; 311 | 312 | // Pull out the major mime type and the boundary (if it exists) 313 | let (mime_type, sub_mime_type) = content_type.content_type; 314 | let boundary = content_type.params.get(&"boundary".to_string()); 315 | 316 | let mut message = match (&mime_type[..], boundary) { 317 | // Only consider a multipart message if we have a boundary, otherwise don't 318 | // bother and just assume it's a single message. 319 | ("multipart", Some(boundary)) => { 320 | // Pull apart the message on the boundary. 321 | let mut parts = MimeMessage::split_boundary(&body, boundary); 322 | // Pop off the first message, as it's part of the parent. 323 | let pre_body = if parts.is_empty() { 324 | "".to_string() 325 | } else { 326 | parts.remove(0) 327 | }; 328 | // Parse out each of the child parts, recursively downwards. 329 | // Filtering out and unwrapping None as we go. 330 | let message_parts: Vec = parts 331 | .iter() 332 | .filter_map(|part| match MimeMessage::parse(&part[..]) { 333 | Ok(x) => Some(x), 334 | Err(_) => None, 335 | }) 336 | .collect(); 337 | // It should be safe to unwrap the multipart type here because we know the main 338 | // mimetype is "multipart" 339 | let multipart_type = 340 | MimeMultipartType::from_content_type((mime_type, sub_mime_type)).unwrap(); 341 | 342 | // Extract any extra Content-Type parameters, but leave boundary out (we'll calculate it 343 | // ourselves later, and leaving it in is confusing) 344 | let mut content_type_params = content_type.params.clone(); 345 | content_type_params.remove(&"boundary".to_string()); 346 | let optional_params = if content_type_params.len() > 0 { 347 | Some(content_type_params) 348 | } else { 349 | None 350 | }; 351 | 352 | MimeMessage::new_with_boundary_and_params( 353 | pre_body, 354 | multipart_type, 355 | message_parts, 356 | boundary.clone(), 357 | optional_params, 358 | ) 359 | } 360 | _ => MimeMessage::new(body), 361 | }; 362 | 363 | message.headers = headers; 364 | Ok(message) 365 | } 366 | 367 | // Split `body` up on the `boundary` string. 368 | fn split_boundary(body: &str, boundary: &str) -> Vec { 369 | #[derive(Debug)] 370 | enum ParseState { 371 | Distinguished, 372 | DistinguishedEnd, 373 | Normal, 374 | SeenCr, 375 | SeenLf, 376 | SeenDash, 377 | ReadBoundary, 378 | BoundaryEnd, 379 | } 380 | 381 | // Start in a state where we're at the beginning of a line. 382 | let mut state = ParseState::SeenLf; 383 | 384 | // Initialize starting positions 385 | let mut boundary_start = 0; 386 | let mut boundary_end = 0; 387 | 388 | let mut parts = Vec::new(); 389 | 390 | let body_slice = &body[..]; 391 | 392 | let mut done = false; 393 | 394 | for (pos, c) in body.char_indices() { 395 | state = match (state, c) { 396 | (ParseState::ReadBoundary, '-') => ParseState::Distinguished, 397 | (ParseState::Distinguished, '-') => ParseState::DistinguishedEnd, 398 | (ParseState::BoundaryEnd, _) => { 399 | // We're now out of a boundary, so remember where the end is, 400 | // so we can slice from the end of this boundary to the start of the next. 401 | boundary_end = pos; 402 | if c == '\n' { 403 | ParseState::BoundaryEnd 404 | } else { 405 | ParseState::Normal 406 | } 407 | } 408 | (ParseState::DistinguishedEnd, '\r') | (ParseState::DistinguishedEnd, '\n') => { 409 | let read_boundary = body_slice[(boundary_start + 1)..(pos - 2)].trim(); 410 | if read_boundary == boundary { 411 | // Boundary matches, push the part 412 | // The part is from the last boundary's end to this boundary's beginning 413 | let part = &body_slice[boundary_end..(boundary_start - 1)]; 414 | parts.push(part.to_string()); 415 | done = true; 416 | break; 417 | } else { 418 | // This isn't our boundary, so leave it. 419 | ParseState::Normal 420 | } 421 | } 422 | (ParseState::ReadBoundary, '\r') | (ParseState::ReadBoundary, '\n') => { 423 | let read_boundary = body_slice[(boundary_start + 1)..pos].trim(); 424 | if read_boundary == boundary { 425 | // Boundary matches, push the part 426 | // The part is from the last boundary's end to this boundary's beginning 427 | let part = &body_slice[boundary_end..(boundary_start - 1)]; 428 | parts.push(part.to_string()); 429 | // This is our boundary, so consume boundary end 430 | ParseState::BoundaryEnd 431 | } else { 432 | // This isn't our boundary, so leave it. 433 | ParseState::Normal 434 | } 435 | } 436 | (ParseState::ReadBoundary, _) => ParseState::ReadBoundary, 437 | (_, '\n') => ParseState::SeenLf, 438 | (_, '\r') => ParseState::SeenCr, 439 | (ParseState::SeenDash, '-') => { 440 | boundary_start = pos; 441 | ParseState::ReadBoundary 442 | } 443 | (ParseState::SeenLf, '-') => ParseState::SeenDash, 444 | (ParseState::Normal, _) => ParseState::Normal, 445 | (_, _) => ParseState::Normal, 446 | }; 447 | } 448 | 449 | if !done { 450 | // Push in the final part of the message (what remains) 451 | let final_part = &body_slice[boundary_end..]; 452 | if !final_part.trim().is_empty() { 453 | parts.push(final_part.to_string()); 454 | } 455 | } 456 | 457 | parts 458 | } 459 | } 460 | 461 | #[cfg(test)] 462 | mod tests { 463 | use super::super::header::{Header, HeaderMap}; 464 | use super::*; 465 | 466 | #[derive(Debug)] 467 | struct MessageTestResult<'s> { 468 | headers: Vec<(&'s str, &'s str)>, 469 | message_type_params: Vec<(&'s str, &'s str)>, 470 | body: &'s str, 471 | children: Vec>, 472 | } 473 | 474 | impl<'s> MessageTestResult<'s> { 475 | fn headers(&self) -> HeaderMap { 476 | let mut headers = HeaderMap::new(); 477 | for &(name, value) in self.headers.iter() { 478 | let header = Header::new(name.to_string(), value.to_string()); 479 | headers.insert(header); 480 | } 481 | 482 | headers 483 | } 484 | 485 | fn matches(&self, other: &MimeMessage) -> bool { 486 | let headers = self.headers(); 487 | 488 | let message_type_params = if self.message_type_params.len() > 0 { 489 | let mut params = HashMap::new(); 490 | for &(name, value) in self.message_type_params.iter() { 491 | params.insert(name.to_string(), value.to_string()); 492 | } 493 | Some(params) 494 | } else { 495 | None 496 | }; 497 | 498 | let header_match = headers == other.headers; 499 | let message_type_params_match = message_type_params == other.message_type_params; 500 | let body_match = self.body.to_string() == other.body; 501 | 502 | let mut children_match = self.children.len() == other.children.len(); 503 | if children_match { 504 | for (index, child) in self.children.iter().enumerate() { 505 | if !child.matches(&other.children[index]) { 506 | children_match = false; 507 | break; 508 | } 509 | } 510 | } 511 | 512 | if !children_match { 513 | println!("Children do not match!"); 514 | } 515 | if !header_match { 516 | println!( 517 | "Headers do not match! Have: {:#?} Expected: {:#?}", 518 | other.headers, headers 519 | ); 520 | } 521 | if !message_type_params_match { 522 | println!( 523 | "Content-Type params do not match! Have: {:#?} Expected: {:#?}", 524 | other.message_type_params, message_type_params 525 | ); 526 | } 527 | if !body_match { 528 | println!( 529 | "Body does not match!\nHave:\n{} \nExpected:\n{}", 530 | other.body, self.body 531 | ); 532 | } 533 | 534 | header_match && message_type_params_match && body_match && children_match 535 | } 536 | } 537 | 538 | struct ParseTest<'s> { 539 | input: &'s str, 540 | output: Option>, 541 | name: &'s str, 542 | } 543 | 544 | #[test] 545 | fn test_message_parse() { 546 | let tests = vec![ 547 | ParseTest { 548 | input: "From: joe@example.org\r\nTo: john@example.org\r\n\r\nHello!", 549 | output: Some(MessageTestResult { 550 | headers: vec![ 551 | ("From", "joe@example.org"), 552 | ("To", "john@example.org"), 553 | ], 554 | message_type_params: vec![], 555 | body: "Hello!", 556 | children: vec![], 557 | }), 558 | name: "Simple single part message parse", 559 | }, 560 | ParseTest { 561 | input: "From: joe@example.org\r\n\ 562 | To: john@example.org\r\n\ 563 | Content-Type: multipart/alternative; boundary=foo\r\n\ 564 | \r\n\ 565 | Parent\r\n\ 566 | --foo\r\n\ 567 | Hello!\r\n\ 568 | --foo\r\n\ 569 | Other\r\n", 570 | output: Some(MessageTestResult { 571 | headers: vec![ 572 | ("From", "joe@example.org"), 573 | ("To", "john@example.org"), 574 | ("Content-Type", "multipart/alternative; boundary=foo"), 575 | ], 576 | message_type_params: vec![], 577 | body: "Parent\r\n", 578 | children: vec![ 579 | MessageTestResult { 580 | headers: vec![], 581 | message_type_params: vec![], 582 | body: "Hello!\r\n", 583 | children: vec![], 584 | }, 585 | MessageTestResult { 586 | headers: vec![], 587 | message_type_params: vec![], 588 | body: "Other\r\n", 589 | children: vec![], 590 | }, 591 | ], 592 | }), 593 | name: "Simple multipart message parse", 594 | }, 595 | ParseTest { 596 | input: "From: joe@example.org\n\ 597 | To: john@example.org\n\ 598 | Content-Type: multipart/alternative; boundary=\"foo\"\n\ 599 | \n\ 600 | \n\ 601 | Parent\n\ 602 | --foo\n\ 603 | Hello!\n\ 604 | --foo\n\ 605 | Other\n", 606 | output: Some(MessageTestResult { 607 | headers: vec![ 608 | ("From", "joe@example.org"), 609 | ("To", "john@example.org"), 610 | ("Content-Type", "multipart/alternative; boundary=\"foo\""), 611 | ], 612 | message_type_params: vec![], 613 | body: "\nParent\n", 614 | children: vec![ 615 | MessageTestResult { 616 | headers: vec![], 617 | message_type_params: vec![], 618 | body: "Hello!\n", 619 | children: vec![], 620 | }, 621 | MessageTestResult { 622 | headers: vec![], 623 | message_type_params: vec![], 624 | body: "Other\n", 625 | children: vec![], 626 | }, 627 | ], 628 | }), 629 | name: "Unix line ending multipart message parse", 630 | }, 631 | ParseTest { 632 | input: "From: joe@example.org\r\n\ 633 | To: john@example.org\r\n\ 634 | Content-Type: multipart/mixed; boundary=foo\r\n\ 635 | \r\n\ 636 | Parent\r\n\ 637 | --foo\r\n\ 638 | Content-Type: multipart/alternative; boundary=bar\r\n\ 639 | \r\n\ 640 | --bar\r\n\ 641 | Hello!\r\n\ 642 | --bar\r\n\ 643 | Other\r\n\ 644 | --foo\r\n\ 645 | Outside\r\n\ 646 | --foo\r\n", 647 | output: Some(MessageTestResult { 648 | headers: vec![ 649 | ("From", "joe@example.org"), 650 | ("To", "john@example.org"), 651 | ("Content-Type", "multipart/mixed; boundary=foo"), 652 | ], 653 | message_type_params: vec![], 654 | body: "Parent\r\n", 655 | children: vec![ 656 | MessageTestResult { 657 | headers: vec![ 658 | ("Content-Type", "multipart/alternative; boundary=bar"), 659 | ], 660 | message_type_params: vec![], 661 | body: "", 662 | children: vec![ 663 | MessageTestResult { 664 | headers: vec![ ], 665 | message_type_params: vec![], 666 | body: "Hello!\r\n", 667 | children: vec![], 668 | }, 669 | MessageTestResult { 670 | headers: vec![], 671 | message_type_params: vec![], 672 | body: "Other\r\n", 673 | children: vec![], 674 | }, 675 | ], 676 | }, 677 | MessageTestResult { 678 | headers: vec![], 679 | message_type_params: vec![], 680 | body: "Outside\r\n", 681 | children: vec![], 682 | }, 683 | ], 684 | }), 685 | name: "Deeply nested multipart test", 686 | }, 687 | ParseTest { 688 | input: "From: joe@example.org\n\ 689 | To: john@example.org\n\ 690 | Content-Type: multipart/alternative; boundary=\"foo\"\n\ 691 | \n\ 692 | \n\ 693 | Parent\n\ 694 | --foo\n\ 695 | Hello!\n\ 696 | --foo\n\ 697 | Other\n\ 698 | --foo--\n\ 699 | Outside\n", 700 | output: Some(MessageTestResult { 701 | headers: vec![ 702 | ("From", "joe@example.org"), 703 | ("To", "john@example.org"), 704 | ("Content-Type", "multipart/alternative; boundary=\"foo\""), 705 | ], 706 | message_type_params: vec![], 707 | body: "\nParent\n", 708 | children: vec![ 709 | MessageTestResult { 710 | headers: vec![], 711 | message_type_params: vec![], 712 | body: "Hello!\n", 713 | children: vec![], 714 | }, 715 | MessageTestResult { 716 | headers: vec![], 717 | message_type_params: vec![], 718 | body: "Other\n", 719 | children: vec![], 720 | }, 721 | ], 722 | }), 723 | name: "Distinguished boundary", 724 | }, 725 | ParseTest { 726 | input: "From: joe@example.org\n\ 727 | To: john@example.org\n\ 728 | Content-Type: multipart/encrypted; boundary=\"boundary_encrypted\"; protocol=\"application/pgp-encrypted\"\n\ 729 | \n\ 730 | \n\ 731 | This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\n\ 732 | --boundary_encrypted\n\ 733 | Content-Type: application/octet-stream; name=\"encrypted.asc\"\n\ 734 | Content-Disposition: OpenPGP encrypted message\n\ 735 | Content-Disposition: inline; filename=\"encrypted.asc\"\n\ 736 | \n\ 737 | -----BEGIN PGP MESSAGE-----\n\ 738 | -----END PGP MESSAGE-----\n\ 739 | \n\ 740 | --boundary_encrypted--\n\ 741 | \n", 742 | output: Some(MessageTestResult { 743 | headers: vec![ 744 | ("From", "joe@example.org"), 745 | ("To", "john@example.org"), 746 | ("Content-Type", "multipart/encrypted; boundary=\"boundary_encrypted\"; protocol=\"application/pgp-encrypted\""), 747 | ], 748 | message_type_params: vec![ 749 | ("protocol", "application/pgp-encrypted"), 750 | ], 751 | body: "\nThis is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\n", 752 | children: vec![ 753 | MessageTestResult { 754 | headers: vec![ 755 | ("Content-Type", "application/octet-stream; name=\"encrypted.asc\""), 756 | ("Content-Disposition", "OpenPGP encrypted message"), 757 | ("Content-Disposition", "inline; filename=\"encrypted.asc\""), 758 | ], 759 | message_type_params: vec![], 760 | body: "-----BEGIN PGP MESSAGE-----\n-----END PGP MESSAGE-----\n\n", 761 | children: vec![], 762 | }, 763 | ], 764 | }), 765 | name: "PGP Sample Message", 766 | }, 767 | ]; 768 | 769 | for test in tests.into_iter() { 770 | println!("--- Next test: {}", test.name); 771 | let message = MimeMessage::parse(test.input); 772 | let result = match (test.output, message) { 773 | (Some(ref expected), Ok(ref given)) => expected.matches(given), 774 | (None, Err(_)) => true, 775 | (_, _) => false, 776 | }; 777 | assert!(result, test.name); 778 | } 779 | } 780 | 781 | struct BodyDecodingTestResult<'s> { 782 | body: &'s str, 783 | headers: Vec<(&'s str, &'s str)>, 784 | result: Option<&'s str>, 785 | } 786 | 787 | #[test] 788 | fn test_body_string_decoding() { 789 | let tests = vec![ 790 | BodyDecodingTestResult { 791 | body: "foo=\r\nbar\r\nbaz", 792 | headers: vec![ 793 | ("Content-Type", "text/plain"), 794 | ("Content-Transfer-Encoding", "quoted-printable"), 795 | ], 796 | result: Some("foobar\r\nbaz"), 797 | }, 798 | BodyDecodingTestResult { 799 | body: "foo=\r\nbar\r\nbaz", 800 | headers: vec![ 801 | ("Content-Type", "text/plain"), 802 | ("Content-Transfer-Encoding", "7bit"), 803 | ], 804 | result: Some("foo=\r\nbar\r\nbaz"), 805 | }, 806 | ]; 807 | 808 | for test in tests.into_iter() { 809 | let mut headers = HeaderMap::new(); 810 | for (name, value) in test.headers.into_iter() { 811 | headers.insert(Header::new(name.to_string(), value.to_string())); 812 | } 813 | let mut message = MimeMessage::new(test.body.to_string()); 814 | message.headers = headers; 815 | let expected = test.result.map(|s| s.to_string()); 816 | assert_eq!(message.decoded_body_string().ok(), expected); 817 | } 818 | } 819 | 820 | struct MultipartParseTest<'s> { 821 | mime_type: (&'s str, &'s str), 822 | result: Option, 823 | } 824 | 825 | #[test] 826 | fn test_multipart_type_type_parsing() { 827 | let tests = vec![ 828 | MultipartParseTest { 829 | mime_type: ("multipart", "mixed"), 830 | result: Some(MimeMultipartType::Mixed), 831 | }, 832 | MultipartParseTest { 833 | mime_type: ("multipart", "alternative"), 834 | result: Some(MimeMultipartType::Alternative), 835 | }, 836 | MultipartParseTest { 837 | mime_type: ("multipart", "digest"), 838 | result: Some(MimeMultipartType::Digest), 839 | }, 840 | MultipartParseTest { 841 | mime_type: ("multipart", "parallel"), 842 | result: Some(MimeMultipartType::Parallel), 843 | }, 844 | // Test fallback on multipart/mixed 845 | MultipartParseTest { 846 | mime_type: ("multipart", "potato"), 847 | result: Some(MimeMultipartType::Mixed), 848 | }, 849 | // Test failure state 850 | MultipartParseTest { 851 | mime_type: ("text", "plain"), 852 | result: None, 853 | }, 854 | ]; 855 | 856 | for test in tests.into_iter() { 857 | let (major_type, minor_type) = test.mime_type; 858 | assert_eq!( 859 | MimeMultipartType::from_content_type(( 860 | major_type.to_string(), 861 | minor_type.to_string() 862 | )), 863 | test.result 864 | ); 865 | } 866 | } 867 | 868 | #[test] 869 | fn test_multipart_type_to_content_type() { 870 | let multipart = "multipart".to_string(); 871 | 872 | assert_eq!( 873 | MimeMultipartType::Mixed.to_content_type(), 874 | (multipart.clone(), "mixed".to_string()) 875 | ); 876 | assert_eq!( 877 | MimeMultipartType::Alternative.to_content_type(), 878 | (multipart.clone(), "alternative".to_string()) 879 | ); 880 | assert_eq!( 881 | MimeMultipartType::Digest.to_content_type(), 882 | (multipart.clone(), "digest".to_string()) 883 | ); 884 | assert_eq!( 885 | MimeMultipartType::Parallel.to_content_type(), 886 | (multipart.clone(), "parallel".to_string()) 887 | ); 888 | } 889 | 890 | #[test] 891 | fn test_boundary_generation() { 892 | let message = MimeMessage::new("Body".to_string()); 893 | // This is random, so we can only really check that it's the expected length 894 | assert_eq!(message.boundary.len(), super::BOUNDARY_LENGTH); 895 | } 896 | } 897 | 898 | #[cfg(all(feature = "nightly", test))] 899 | mod bench { 900 | extern crate test; 901 | use self::test::Bencher; 902 | 903 | use super::*; 904 | 905 | macro_rules! bench_parser { 906 | ($name:ident, $test:expr) => { 907 | #[bench] 908 | fn $name(b: &mut Bencher) { 909 | let s = $test; 910 | b.iter(|| { 911 | let _ = MimeMessage::parse(s); 912 | }); 913 | } 914 | }; 915 | } 916 | 917 | bench_parser!( 918 | bench_simple, 919 | "From: joe@example.org\r\nTo: john@example.org\r\n\r\nHello!" 920 | ); 921 | bench_parser!( 922 | bench_simple_multipart, 923 | "From: joe@example.org\r\n\ 924 | To: john@example.org\r\n\ 925 | Content-Type: multipart/alternative; boundary=foo\r\n\ 926 | \r\n\ 927 | Parent\r\n\ 928 | --foo\r\n\ 929 | Hello!\r\n\ 930 | --foo\r\n\ 931 | Other\r\n\ 932 | --foo" 933 | ); 934 | bench_parser!( 935 | bench_deep_multipart, 936 | "From: joe@example.org\r\n\ 937 | To: john@example.org\r\n\ 938 | Content-Type: multipart/mixed; boundary=foo\r\n\ 939 | \r\n\ 940 | Parent\r\n\ 941 | --foo\r\n\ 942 | Content-Type: multipart/alternative; boundary=bar\r\n\ 943 | \r\n\ 944 | --bar\r\n\ 945 | Hello!\r\n\ 946 | --bar\r\n\ 947 | Other\r\n\ 948 | --foo\r\n\ 949 | Outside\r\n\ 950 | --foo\r\n" 951 | ); 952 | } 953 | --------------------------------------------------------------------------------