From a4a0e917580cf465ffd1aa8ae6723860aab23c6f Mon Sep 17 00:00:00 2001 From: ufoscout Date: Tue, 28 May 2019 13:16:13 +0200 Subject: Allow access to encoded body --- .gitattributes | 1 + src/body.rs | 152 ++++++++++++++++++++++++++ src/lib.rs | 205 ++++++++++++++++++++++++++++++----- tests/files/test_email_01.txt | 86 +++++++++++++++ tests/files/test_email_01_sample.pdf | 198 +++++++++++++++++++++++++++++++++ 5 files changed, 616 insertions(+), 26 deletions(-) create mode 100644 src/body.rs create mode 100644 tests/files/test_email_01.txt create mode 100644 tests/files/test_email_01_sample.pdf diff --git a/.gitattributes b/.gitattributes index f795e12..36e2df7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ target/doc/* linguist-generated=true +tests/files/test_email_01_sample.pdf -text -diff diff --git a/src/body.rs b/src/body.rs new file mode 100644 index 0000000..ed1b617 --- /dev/null +++ b/src/body.rs @@ -0,0 +1,152 @@ +use charset::{decode_ascii, Charset}; +use {MailParseError, ParsedContentType}; + +/// Represents the body of an email (or mail subpart) +pub enum Body<'a> { + /// A body with 'base64' Content-Transfer-Encoding. + Base64(EncodedBody<'a>), + /// A body with 'quoted-printable' Content-Transfer-Encoding. + QuotedPrintable(EncodedBody<'a>), + /// A body with '7bit' Content-Transfer-Encoding. + SevenBit(TextBody<'a>), + /// A body with '8bit' Content-Transfer-Encoding. + EightBit(TextBody<'a>), + /// A body with 'binary' Content-Transfer-Encoding. + Binary(BinaryBody<'a>), +} + +impl<'a> Body<'a> { + pub fn new( + body: &'a [u8], + ctype: &'a ParsedContentType, + transfer_encoding: &Option, + ) -> Body<'a> { + transfer_encoding + .as_ref() + .map(|encoding| match encoding.as_ref() { + "base64" => Body::Base64(EncodedBody { + decoder: decode_base64, + body, + ctype, + }), + "quoted-printable" => Body::QuotedPrintable(EncodedBody { + decoder: decode_quoted_printable, + body, + ctype, + }), + "7bit" => Body::SevenBit(TextBody { body, ctype }), + "8bit" => Body::EightBit(TextBody { body, ctype }), + "binary" => Body::Binary(BinaryBody { body, ctype }), + _ => Body::get_default(body, ctype), + }) + .unwrap_or_else(|| Body::get_default(body, ctype)) + } + + fn get_default(body: &'a [u8], ctype: &'a ParsedContentType) -> Body<'a> { + Body::SevenBit(TextBody { body, ctype }) + } +} + +/// Struct that holds the encoded body representation of the message (or message subpart). +pub struct EncodedBody<'a> { + decoder: fn(&[u8]) -> Result, MailParseError>, + ctype: &'a ParsedContentType, + body: &'a [u8], +} + +impl<'a> EncodedBody<'a> { + /// Get the body Content-Type + pub fn get_content_type(&self) -> &'a ParsedContentType { + self.ctype + } + + /// Get the raw body of the message exactly as it is written in the message (or message subpart). + pub fn get_raw(&self) -> &'a [u8] { + self.body + } + + /// Get the decoded body of the message (or message subpart). + pub fn get_decoded(&self) -> Result, MailParseError> { + (self.decoder)(self.body) + } + + /// Get the body of the message as a Rust string. + /// This function tries to decode the body and then converts + /// the result into a Rust UTF-8 string using the charset in the Content-Type + /// (or "us-ascii" if the charset was missing or not recognized). + /// This operation returns a valid result only if the decoded body + /// has a text format. + pub fn get_decoded_as_string(&self) -> Result { + get_body_as_string(&self.get_decoded()?, &self.ctype) + } +} + +/// Struct that holds the textual body representation of the message (or message subpart). +pub struct TextBody<'a> { + ctype: &'a ParsedContentType, + body: &'a [u8], +} + +impl<'a> TextBody<'a> { + /// Get the body Content-Type + pub fn get_content_type(&self) -> &'a ParsedContentType { + self.ctype + } + + /// Get the raw body of the message exactly as it is written in the message (or message subpart). + pub fn get_raw(&self) -> &'a [u8] { + self.body + } + + /// Get the body of the message as a Rust string. + /// This function converts the body into a Rust UTF-8 string using the charset + /// in the Content-Type + /// (or "us-ascii" if the charset was missing or not recognized). + pub fn get_as_string(&self) -> Result { + get_body_as_string(self.body, &self.ctype) + } +} + +/// Struct that holds a binary body representation of the message (or message subpart). +pub struct BinaryBody<'a> { + ctype: &'a ParsedContentType, + body: &'a [u8], +} + +impl<'a> BinaryBody<'a> { + /// Get the body Content-Type + pub fn get_content_type(&self) -> &'a ParsedContentType { + self.ctype + } + + /// Get the raw body of the message exactly as it is written in the message (or message subpart). + pub fn get_raw(&self) -> &'a [u8] { + self.body + } +} + +fn decode_base64(body: &[u8]) -> Result, MailParseError> { + let cleaned = body + .iter() + .filter(|c| !c.is_ascii_whitespace()) + .cloned() + .collect::>(); + Ok(base64::decode(&cleaned)?) +} + +fn decode_quoted_printable(body: &[u8]) -> Result, MailParseError> { + Ok(quoted_printable::decode( + body, + quoted_printable::ParseMode::Robust, + )?) +} + +fn get_body_as_string(body: &[u8], ctype: &ParsedContentType) -> Result { + let cow = if let Some(charset) = Charset::for_label(ctype.charset.as_bytes()) { + let (cow, _, _) = charset.decode(body); + cow + } else { + decode_ascii(body) + }; + Ok(cow.into_owned()) +} diff --git a/src/lib.rs b/src/lib.rs index c727c58..7af26bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,12 +7,13 @@ use std::error; use std::fmt; use std::ops::Deref; -use charset::decode_ascii; use charset::decode_latin1; use charset::Charset; +pub mod body; mod dateparse; +use body::Body; pub use dateparse::dateparse; /// An error type that represents the different kinds of errors that may be @@ -650,14 +651,13 @@ impl<'a> ParsedMail<'a> { /// assert_eq!(p.get_body().unwrap(), "This is the body"); /// ``` pub fn get_body(&self) -> Result { - let decoded = self.get_body_raw()?; - let cow = if let Some(charset) = Charset::for_label(self.ctype.charset.as_bytes()) { - let (cow, _, _) = charset.decode(&decoded); - cow - } else { - decode_ascii(&decoded) - }; - Ok(cow.into_owned()) + match self.get_body_encoded()? { + Body::Base64(body) | Body::QuotedPrintable(body) => body.get_decoded_as_string(), + Body::SevenBit(body) | Body::EightBit(body) => body.get_as_string(), + Body::Binary(_) => Err(MailParseError::Generic( + "Message body of type binary body cannot be parsed into a string", + )), + } } /// Get the body of the message as a Rust Vec. This function tries to @@ -675,27 +675,59 @@ impl<'a> ParsedMail<'a> { /// assert_eq!(p.get_body_raw().unwrap(), b"This is the body"); /// ``` pub fn get_body_raw(&self) -> Result, MailParseError> { - let transfer_coding = self + match self.get_body_encoded()? { + Body::Base64(body) | Body::QuotedPrintable(body) => body.get_decoded(), + Body::SevenBit(body) | Body::EightBit(body) => Ok(Vec::::from(body.get_raw())), + Body::Binary(body) => Ok(Vec::::from(body.get_raw())), + } + } + + /// Get the body of the message. + /// This function returns original the body without attempting to + /// unapply the Content-Transfer-Encoding. + /// + /// # Examples + /// ``` + /// use mailparse::parse_mail; + /// use mailparse::body::Body; + /// + /// let mail = parse_mail(b"Content-Transfer-Encoding: base64\r\n\r\naGVsbG 8gd\r\n29ybGQ=").unwrap(); + /// + /// match mail.get_body_encoded().unwrap() { + /// Body::Base64(body) => { + /// assert_eq!(body.get_raw(), b"aGVsbG 8gd\r\n29ybGQ="); + /// assert_eq!(body.get_decoded().unwrap(), b"hello world"); + /// assert_eq!(body.get_decoded_as_string().unwrap(), "hello world"); + /// }, + /// _ => assert!(false), + /// }; + /// + /// + /// // An email whose body encoding is not known upfront + /// let another_mail = parse_mail(b"").unwrap(); + /// + /// match another_mail.get_body_encoded().unwrap() { + /// Body::Base64(body) | Body::QuotedPrintable(body) => { + /// println!("mail body encoded: {:?}", body.get_raw()); + /// println!("mail body decoded: {:?}", body.get_decoded().unwrap()); + /// println!("mail body decoded as string: {}", body.get_decoded_as_string().unwrap()); + /// }, + /// Body::SevenBit(body) | Body::EightBit(body) => { + /// println!("mail body: {:?}", body.get_raw()); + /// println!("mail body as string: {}", body.get_as_string().unwrap()); + /// }, + /// Body::Binary(body) => { + /// println!("mail body binary: {:?}", body.get_raw()); + /// } + /// } + /// ``` + pub fn get_body_encoded(&'a self) -> Result, MailParseError> { + let transfer_encoding = self .headers .get_first_value("Content-Transfer-Encoding")? .map(|s| s.to_lowercase()); - let decoded = match transfer_coding { - Some(ref enc) if enc == "base64" => { - let cleaned = self - .body - .iter() - .filter(|c| !c.is_ascii_whitespace()) - .cloned() - .collect::>(); - base64::decode(&cleaned)? - } - Some(ref enc) if enc == "quoted-printable" => { - quoted_printable::decode(self.body, quoted_printable::ParseMode::Robust)? - } - _ => Vec::::from(self.body), - }; - Ok(decoded) + Ok(Body::new(self.body, &self.ctype, &transfer_encoding)) } /// Returns a struct containing a parsed representation of the @@ -1265,4 +1297,125 @@ mod tests { let parsed = parse_param_content(r#"Content-Type: application/octet-stream; name=""#); assert_eq!(parsed.params["name"], "\""); } + + #[test] + fn test_default_content_encoding() { + let mail = parse_mail(b"Content-Type: text/plain; charset=UTF-7\r\n\r\n+JgM-").unwrap(); + let body = mail.get_body_encoded().unwrap(); + match body { + Body::SevenBit(body) => { + assert_eq!(body.get_raw(), b"+JgM-"); + assert_eq!(body.get_as_string().unwrap(), "\u{2603}"); + } + _ => assert!(false), + }; + } + + #[test] + fn test_7bit_content_encoding() { + let mail = parse_mail(b"Content-Type: text/plain; charset=UTF-7\r\nContent-Transfer-Encoding: 7bit\r\n\r\n+JgM-").unwrap(); + let body = mail.get_body_encoded().unwrap(); + match body { + Body::SevenBit(body) => { + assert_eq!(body.get_raw(), b"+JgM-"); + assert_eq!(body.get_as_string().unwrap(), "\u{2603}"); + } + _ => assert!(false), + }; + } + + #[test] + fn test_8bit_content_encoding() { + let mail = parse_mail(b"Content-Type: text/plain; charset=UTF-7\r\nContent-Transfer-Encoding: 8bit\r\n\r\n+JgM-").unwrap(); + let body = mail.get_body_encoded().unwrap(); + match body { + Body::EightBit(body) => { + assert_eq!(body.get_raw(), b"+JgM-"); + assert_eq!(body.get_as_string().unwrap(), "\u{2603}"); + } + _ => assert!(false), + }; + } + + #[test] + fn test_quoted_printable_content_encoding() { + let mail = parse_mail( + b"Content-Type: text/plain; charset=UTF-7\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n+JgM-", + ).unwrap(); + match mail.get_body_encoded().unwrap() { + Body::QuotedPrintable(body) => { + assert_eq!(body.get_raw(), b"+JgM-"); + assert_eq!(body.get_decoded().unwrap(), b"+JgM-"); + assert_eq!(body.get_decoded_as_string().unwrap(), "\u{2603}"); + } + _ => assert!(false), + }; + } + + #[test] + fn test_base64_content_encoding() { + let mail = + parse_mail(b"Content-Transfer-Encoding: base64\r\n\r\naGVsbG 8gd\r\n29ybGQ=").unwrap(); + match mail.get_body_encoded().unwrap() { + Body::Base64(body) => { + assert_eq!(body.get_raw(), b"aGVsbG 8gd\r\n29ybGQ="); + assert_eq!(body.get_decoded().unwrap(), b"hello world"); + assert_eq!(body.get_decoded_as_string().unwrap(), "hello world"); + } + _ => assert!(false), + }; + } + + #[test] + fn test_binary_content_encoding() { + let mail = parse_mail(b"Content-Transfer-Encoding: binary\r\n\r\n######").unwrap(); + let body = mail.get_body_encoded().unwrap(); + match body { + Body::Binary(body) => { + assert_eq!(body.get_raw(), b"######"); + } + _ => assert!(false), + }; + } + + #[test] + fn test_body_content_encoding_with_multipart() { + let mail_filepath = "./tests/files/test_email_01.txt"; + let mail = std::fs::read(mail_filepath) + .expect(&format!("Unable to open the file [{}]", mail_filepath)); + let mail = parse_mail(&mail).unwrap(); + + let subpart_0 = mail.subparts.get(0).unwrap(); + match subpart_0.get_body_encoded().unwrap() { + Body::SevenBit(body) => { + assert_eq!( + body.get_as_string().unwrap().trim(), + "Test with attachments" + ); + } + _ => assert!(false), + }; + + let subpart_1 = mail.subparts.get(1).unwrap(); + match subpart_1.get_body_encoded().unwrap() { + Body::Base64(body) => { + let pdf_filepath = "./tests/files/test_email_01_sample.pdf"; + let original_pdf = std::fs::read(pdf_filepath) + .expect(&format!("Unable to open the file [{}]", pdf_filepath)); + assert_eq!(body.get_decoded().unwrap(), original_pdf); + } + _ => assert!(false), + }; + + let subpart_2 = mail.subparts.get(2).unwrap(); + match subpart_2.get_body_encoded().unwrap() { + Body::Base64(body) => { + assert_eq!( + body.get_decoded_as_string().unwrap(), + "txt file context for email collector\n1234567890987654321\n" + ); + } + _ => assert!(false), + }; + } } diff --git a/tests/files/test_email_01.txt b/tests/files/test_email_01.txt new file mode 100644 index 0000000..286c089 --- /dev/null +++ b/tests/files/test_email_01.txt @@ -0,0 +1,86 @@ +Subject: Test with attachments +Content-Type: multipart/mixed; + boundary="------------E5401F4DD68F2F7A872C2A83" +Content-Language: en-US + +This is a multi-part message in MIME format. +--------------E5401F4DD68F2F7A872C2A83 +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 7bit + +Test with attachments + +--------------E5401F4DD68F2F7A872C2A83 +Content-Type: application/pdf; + name="sample.pdf" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="sample.pdf" + +JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YWxvZw0KL091dGxp +bmVzIDIgMCBSDQovUGFnZXMgMyAwIFINCj4+DQplbmRvYmoNCg0KMiAwIG9iag0KPDwNCi9U +eXBlIC9PdXRsaW5lcw0KL0NvdW50IDANCj4+DQplbmRvYmoNCg0KMyAwIG9iag0KPDwNCi9U +eXBlIC9QYWdlcw0KL0NvdW50IDINCi9LaWRzIFsgNCAwIFIgNiAwIFIgXSANCj4+DQplbmRv +YmoNCg0KNCAwIG9iag0KPDwNCi9UeXBlIC9QYWdlDQovUGFyZW50IDMgMCBSDQovUmVzb3Vy +Y2VzIDw8DQovRm9udCA8PA0KL0YxIDkgMCBSIA0KPj4NCi9Qcm9jU2V0IDggMCBSDQo+Pg0K +L01lZGlhQm94IFswIDAgNjEyLjAwMDAgNzkyLjAwMDBdDQovQ29udGVudHMgNSAwIFINCj4+ +DQplbmRvYmoNCg0KNSAwIG9iag0KPDwgL0xlbmd0aCAxMDc0ID4+DQpzdHJlYW0NCjIgSg0K +QlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBBIFNp +bXBsZSBQREYgRmlsZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42 +MDgwIFRkDQooIFRoaXMgaXMgYSBzbWFsbCBkZW1vbnN0cmF0aW9uIC5wZGYgZmlsZSAtICkg +VGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjY0LjcwNDAgVGQNCigganVzdCBm +b3IgdXNlIGluIHRoZSBWaXJ0dWFsIE1lY2hhbmljcyB0dXRvcmlhbHMuIE1vcmUgdGV4dC4g +QW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NTIuNzUyMCBU +ZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0 +LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDYyOC44NDgwIFRkDQooIEFu +ZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRl +eHQuIEFuZCBtb3JlICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjE2Ljg5 +NjAgVGQNCiggdGV4dC4gQW5kIG1vcmUgdGV4dC4gQm9yaW5nLCB6enp6ei4gQW5kIG1vcmUg +dGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5 +LjI1MDAgNjA0Ljk0NDAgVGQNCiggbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9y +ZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9G +MSAwMDEwIFRmDQo2OS4yNTAwIDU5Mi45OTIwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBt +b3JlIHRleHQuICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNTY5LjA4ODAg +VGQNCiggQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5k +IG1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUw +MCA1NTcuMTM2MCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBF +dmVuIG1vcmUuIENvbnRpbnVlZCBvbiBwYWdlIDIgLi4uKSBUag0KRVQNCmVuZHN0cmVhbQ0K +ZW5kb2JqDQoNCjYgMCBvYmoNCjw8DQovVHlwZSAvUGFnZQ0KL1BhcmVudCAzIDAgUg0KL1Jl +c291cmNlcyA8PA0KL0ZvbnQgPDwNCi9GMSA5IDAgUiANCj4+DQovUHJvY1NldCA4IDAgUg0K +Pj4NCi9NZWRpYUJveCBbMCAwIDYxMi4wMDAwIDc5Mi4wMDAwXQ0KL0NvbnRlbnRzIDcgMCBS +DQo+Pg0KZW5kb2JqDQoNCjcgMCBvYmoNCjw8IC9MZW5ndGggNjc2ID4+DQpzdHJlYW0NCjIg +Sg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBT +aW1wbGUgUERGIEZpbGUgMiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4 +OC42MDgwIFRkDQooIC4uLmNvbnRpbnVlZCBmcm9tIHBhZ2UgMS4gWWV0IG1vcmUgdGV4dC4g +QW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBU +Zg0KNjkuMjUwMCA2NzYuNjU2MCBUZA0KKCBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0 +LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSApIFRqDQpFVA0KQlQN +Ci9GMSAwMDEwIFRmDQo2OS4yNTAwIDY2NC43MDQwIFRkDQooIHRleHQuIE9oLCBob3cgYm9y +aW5nIHR5cGluZyB0aGlzIHN0dWZmLiBCdXQgbm90IGFzIGJvcmluZyBhcyB3YXRjaGluZyAp +IFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY1Mi43NTIwIFRkDQooIHBhaW50 +IGRyeS4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5k +IG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NDAuODAw +MCBUZA0KKCBCb3JpbmcuICBNb3JlLCBhIGxpdHRsZSBtb3JlIHRleHQuIFRoZSBlbmQsIGFu +ZCBqdXN0IGFzIHdlbGwuICkgVGoNCkVUDQplbmRzdHJlYW0NCmVuZG9iag0KDQo4IDAgb2Jq +DQpbL1BERiAvVGV4dF0NCmVuZG9iag0KDQo5IDAgb2JqDQo8PA0KL1R5cGUgL0ZvbnQNCi9T +dWJ0eXBlIC9UeXBlMQ0KL05hbWUgL0YxDQovQmFzZUZvbnQgL0hlbHZldGljYQ0KL0VuY29k +aW5nIC9XaW5BbnNpRW5jb2RpbmcNCj4+DQplbmRvYmoNCg0KMTAgMCBvYmoNCjw8DQovQ3Jl +YXRvciAoUmF2ZSBcKGh0dHA6Ly93d3cubmV2cm9uYS5jb20vcmF2ZVwpKQ0KL1Byb2R1Y2Vy +IChOZXZyb25hIERlc2lnbnMpDQovQ3JlYXRpb25EYXRlIChEOjIwMDYwMzAxMDcyODI2KQ0K +Pj4NCmVuZG9iag0KDQp4cmVmDQowIDExDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAw +MTkgMDAwMDAgbg0KMDAwMDAwMDA5MyAwMDAwMCBuDQowMDAwMDAwMTQ3IDAwMDAwIG4NCjAw +MDAwMDAyMjIgMDAwMDAgbg0KMDAwMDAwMDM5MCAwMDAwMCBuDQowMDAwMDAxNTIyIDAwMDAw +IG4NCjAwMDAwMDE2OTAgMDAwMDAgbg0KMDAwMDAwMjQyMyAwMDAwMCBuDQowMDAwMDAyNDU2 +IDAwMDAwIG4NCjAwMDAwMDI1NzQgMDAwMDAgbg0KDQp0cmFpbGVyDQo8PA0KL1NpemUgMTEN +Ci9Sb290IDEgMCBSDQovSW5mbyAxMCAwIFINCj4+DQoNCnN0YXJ0eHJlZg0KMjcxNA0KJSVF +T0YNCg== +--------------E5401F4DD68F2F7A872C2A83 +Content-Type: text/plain; charset=UTF-8; + name="sample.txt" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="sample.txt" + +dHh0IGZpbGUgY29udGV4dCBmb3IgZW1haWwgY29sbGVjdG9yCjEyMzQ1Njc4OTA5ODc2NTQz +MjEK +--------------E5401F4DD68F2F7A872C2A83-- diff --git a/tests/files/test_email_01_sample.pdf b/tests/files/test_email_01_sample.pdf new file mode 100644 index 0000000..dbf091d --- /dev/null +++ b/tests/files/test_email_01_sample.pdf @@ -0,0 +1,198 @@ +%PDF-1.3 +%âãÏÓ + +1 0 obj +<< +/Type /Catalog +/Outlines 2 0 R +/Pages 3 0 R +>> +endobj + +2 0 obj +<< +/Type /Outlines +/Count 0 +>> +endobj + +3 0 obj +<< +/Type /Pages +/Count 2 +/Kids [ 4 0 R 6 0 R ] +>> +endobj + +4 0 obj +<< +/Type /Page +/Parent 3 0 R +/Resources << +/Font << +/F1 9 0 R +>> +/ProcSet 8 0 R +>> +/MediaBox [0 0 612.0000 792.0000] +/Contents 5 0 R +>> +endobj + +5 0 obj +<< /Length 1074 >> +stream +2 J +BT +0 0 0 rg +/F1 0027 Tf +57.3750 722.2800 Td +( A Simple PDF File ) Tj +ET +BT +/F1 0010 Tf +69.2500 688.6080 Td +( This is a small demonstration .pdf file - ) Tj +ET +BT +/F1 0010 Tf +69.2500 664.7040 Td +( just for use in the Virtual Mechanics tutorials. More text. And more ) Tj +ET +BT +/F1 0010 Tf +69.2500 652.7520 Td +( text. And more text. And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 628.8480 Td +( And more text. And more text. And more text. And more text. And more ) Tj +ET +BT +/F1 0010 Tf +69.2500 616.8960 Td +( text. And more text. Boring, zzzzz. And more text. And more text. And ) Tj +ET +BT +/F1 0010 Tf +69.2500 604.9440 Td +( more text. And more text. And more text. And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 592.9920 Td +( And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 569.0880 Td +( And more text. And more text. And more text. And more text. And more ) Tj +ET +BT +/F1 0010 Tf +69.2500 557.1360 Td +( text. And more text. And more text. Even more. Continued on page 2 ...) Tj +ET +endstream +endobj + +6 0 obj +<< +/Type /Page +/Parent 3 0 R +/Resources << +/Font << +/F1 9 0 R +>> +/ProcSet 8 0 R +>> +/MediaBox [0 0 612.0000 792.0000] +/Contents 7 0 R +>> +endobj + +7 0 obj +<< /Length 676 >> +stream +2 J +BT +0 0 0 rg +/F1 0027 Tf +57.3750 722.2800 Td +( Simple PDF File 2 ) Tj +ET +BT +/F1 0010 Tf +69.2500 688.6080 Td +( ...continued from page 1. Yet more text. And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 676.6560 Td +( And more text. And more text. And more text. And more text. And more ) Tj +ET +BT +/F1 0010 Tf +69.2500 664.7040 Td +( text. Oh, how boring typing this stuff. But not as boring as watching ) Tj +ET +BT +/F1 0010 Tf +69.2500 652.7520 Td +( paint dry. And more text. And more text. And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 640.8000 Td +( Boring. More, a little more text. The end, and just as well. ) Tj +ET +endstream +endobj + +8 0 obj +[/PDF /Text] +endobj + +9 0 obj +<< +/Type /Font +/Subtype /Type1 +/Name /F1 +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +>> +endobj + +10 0 obj +<< +/Creator (Rave \(http://www.nevrona.com/rave\)) +/Producer (Nevrona Designs) +/CreationDate (D:20060301072826) +>> +endobj + +xref +0 11 +0000000000 65535 f +0000000019 00000 n +0000000093 00000 n +0000000147 00000 n +0000000222 00000 n +0000000390 00000 n +0000001522 00000 n +0000001690 00000 n +0000002423 00000 n +0000002456 00000 n +0000002574 00000 n + +trailer +<< +/Size 11 +/Root 1 0 R +/Info 10 0 R +>> + +startxref +2714 +%%EOF -- cgit v1.2.3