Repository: lettre/lettre (language: rust)

Issue:
Emails sent to file aren't in plain text; hard to verify contents
**Describe the bug**

When using `FileTransport` to send to a tmp file, the contents of the email are displayed in bytes rather than plain text. This makes it difficult to easily manually verify that an email contains what you want it to contain, especially when developing.

This changed in [this commit](https://github.com/lettre/lettre/commit/f07fe8687d7bb3e9d7b87ced60ef5cf063757f0f#diff-501dc37ff125121e2d8a968da995316aL36-R37) as demonstrated by that test.

Is there a reason the email couldn't be printed as plain text in the file?

**To Reproduce**

Inspired by the test linked to above, using lettre at 3995ea2:

```rust
#[test]
fn email_file_should_contain_plain_text() {
    use lettre::{Email, EmailAddress, Envelope, FileTransport, Transport};
    use std::fs;
    use std::path::Path;

    let email = Email::new(
        Envelope::new(
            Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
            vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
        )
        .unwrap(),
        "id".to_string(),
        "Hello file".to_string().into_bytes(),
    );
    let mut sender = FileTransport::new(Path::new("/tmp"));
    let message_id = email.message_id().to_string();

    let result = sender.send(email);
    assert!(result.is_ok());

    let file = format!("/tmp/{}.json", message_id);
    let contents = fs::read_to_string(file).unwrap();

    assert_eq!(
        contents,
        "{\"envelope\":\
         {\"forward_path\":[\"root@localhost\"],\"reverse_path\":\"user@localhost\"},\
         \"message_id\":\"id\",\"message\":\"Hello file\"}"
    );
}
```

**Expected behavior**

I expected to be able to read the contents of the email in the file without any additional processing.

**Environment (please complete the following information):**

- Lettre version: 0.9.2, test written above is against lettre master 3995ea2
- OS: macOS
- Rust: 1.39.0 stable

**Additional context**

This makes [developing crates.io more difficult](https://github.com/rust-lang/crates.io/issues/1998).



Discussion / hints:
Thanks for reporting, this was not an intended change, I'll have a look.
I think this is as simple as updating the serializable message struct to use a string, no?
I opened #402 to change how the message is represented in the file. 

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 338d8eca2..a0acd4388 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,11 +19,10 @@ maintenance = { status = "actively-developed" }
 [dependencies]
 base64 = { version = "0.12", optional = true }
 bufstream = { version = "0.1", optional = true }
-# TODO to 0.5
-bytes = { version = "0.4", optional = true }
 hostname = { version = "0.3", optional = true }
 hyperx = { version = "1", optional = true, features = ["headers"] }
 idna = "0.2"
+line-wrap = "0.1"
 log = "0.4"
 uuid = { version = "0.8", features = ["v4"] }
 mime = { version = "0.3", optional = true }
@@ -50,7 +49,7 @@ harness = false
 name = "transport_smtp"
 
 [features]
-builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable", "bytes"]
+builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"]
 connection-pool = ["r2d2"]
 default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "native-tls", "builder"]
 file-transport = ["serde", "serde_json"]
diff --git a/benches/transport_smtp.rs b/benches/transport_smtp.rs
index 665f7c002..d2dd2c696 100644
--- a/benches/transport_smtp.rs
+++ b/benches/transport_smtp.rs
@@ -17,7 +17,7 @@ fn bench_simple_send(c: &mut Criterion) {
                 .subject("Happy new year")
                 .body("Be happy!")
                 .unwrap();
-            let result = black_box(sender.send(email));
+            let result = black_box(sender.send(&email));
             assert!(result.is_ok());
         })
     });
@@ -37,7 +37,7 @@ fn bench_reuse_send(c: &mut Criterion) {
                 .subject("Happy new year")
                 .body("Be happy!")
                 .unwrap();
-            let result = black_box(sender.send(email));
+            let result = black_box(sender.send(&email));
             assert!(result.is_ok());
         })
     });
diff --git a/examples/smtp.rs b/examples/smtp.rs
index b46fae4f1..6aa5abc2e 100644
--- a/examples/smtp.rs
+++ b/examples/smtp.rs
@@ -16,7 +16,7 @@ fn main() {
     // Open a local connection on port 25
     let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
     // Send the email
-    let result = mailer.send(email);
+    let result = mailer.send(&email);
 
     if result.is_ok() {
         println!("Email sent");
diff --git a/examples/smtp_gmail.rs b/examples/smtp_gmail.rs
index a7b23ba07..b52590dd3 100644
--- a/examples/smtp_gmail.rs
+++ b/examples/smtp_gmail.rs
@@ -23,7 +23,7 @@ fn main() {
         .transport();
 
     // Send the email
-    let result = mailer.send(email);
+    let result = mailer.send(&email);
 
     if result.is_ok() {
         println!("Email sent");
diff --git a/src/address.rs b/src/address.rs
index fd779d7df..4f7da1755 100644
--- a/src/address.rs
+++ b/src/address.rs
@@ -65,7 +65,6 @@ static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\
 
 impl Address {
     /// Create email address from parts
-    #[inline]
     pub fn new<U: Into<String>, D: Into<String>>(user: U, domain: D) -> Result<Self, AddressError> {
         (user, domain).try_into()
     }
diff --git a/src/error.rs b/src/error.rs
index 6c8c6c4fc..0daa895c0 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -23,6 +23,8 @@ pub enum Error {
     CannotParseFilename,
     /// IO error
     Io(std::io::Error),
+    /// Non-ASCII chars
+    NonAsciiChars,
 }
 
 impl Display for Error {
@@ -35,6 +37,7 @@ impl Display for Error {
             Error::EmailMissingLocalPart => "missing local part in email address".to_string(),
             Error::EmailMissingDomain => "missing domain in email address".to_string(),
             Error::CannotParseFilename => "could not parse attachment filename".to_string(),
+            Error::NonAsciiChars => "contains non-ASCII chars".to_string(),
             Error::Io(e) => e.to_string(),
         })
     }
diff --git a/src/lib.rs b/src/lib.rs
index f04b60b3e..0663d67bb 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -9,7 +9,8 @@
     trivial_casts,
     trivial_numeric_casts,
     unstable_features,
-    unused_import_braces
+    unused_import_braces,
+    unsafe_code
 )]
 
 pub mod address;
@@ -23,7 +24,7 @@ use crate::error::Error;
 #[cfg(feature = "builder")]
 pub use crate::message::{
     header::{self, Headers},
-    Mailboxes, Message,
+    EmailFormat, Mailboxes, Message,
 };
 #[cfg(feature = "file-transport")]
 pub use crate::transport::file::FileTransport;
@@ -37,7 +38,6 @@ pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
 pub use crate::transport::smtp::{ClientSecurity, SmtpClient, SmtpTransport};
 #[cfg(feature = "builder")]
 use std::convert::TryFrom;
-use std::fmt::Display;
 
 /// Simple email envelope representation
 ///
@@ -109,27 +109,20 @@ impl TryFrom<&Headers> for Envelope {
     }
 }
 
-// FIXME generate random log id
-
 /// Transport method for emails
-pub trait Transport<'a, B> {
+pub trait Transport<'a> {
     /// Result type for the transport
     type Result;
 
     /// Sends the email
     /// FIXME not mut
 
-    // email = message (bytes) + envelope
-    fn send(&mut self, message: Message<B>) -> Self::Result
-    where
-        B: Display,
-    {
-        self.send_raw(message.envelope(), message.to_string().as_bytes())
+    fn send(&mut self, message: &Message) -> Self::Result {
+        let raw = message.formatted();
+        self.send_raw(message.envelope(), &raw)
     }
 
     fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result;
-
-    // TODO allow sending generic data
 }
 
 #[cfg(test)]
diff --git a/src/message/encoder.rs b/src/message/encoder.rs
index 16a3ab7ae..9b6e3776d 100644
--- a/src/message/encoder.rs
+++ b/src/message/encoder.rs
@@ -1,61 +1,16 @@
 use crate::message::header::ContentTransferEncoding;
-use bytes::{Buf, BufMut, Bytes, BytesMut, IntoBuf};
-use std::{
-    cmp::min,
-    error::Error,
-    fmt::{Debug, Display, Formatter, Result as FmtResult},
-};
-
-/// Content encoding error
-#[derive(Debug, Clone)]
-pub enum EncoderError<E> {
-    Source(E),
-    Coding,
-}
-
-impl<E> Error for EncoderError<E> where E: Debug + Display {}
-
-impl<E> Display for EncoderError<E>
-where
-    E: Display,
-{
-    fn fmt(&self, f: &mut Formatter) -> FmtResult {
-        match self {
-            EncoderError::Source(error) => write!(f, "Source error: {}", error),
-            EncoderError::Coding => f.write_str("Coding error"),
-        }
-    }
-}
+use line_wrap::{crlf, line_wrap, LineEnding};
+use std::io::Write;
 
 /// Encoder trait
 pub trait EncoderCodec: Send {
-    /// Encode chunk of data
-    fn encode_chunk(&mut self, input: &dyn Buf) -> Result<Bytes, ()>;
-
-    /// Encode end of stream
-    ///
-    /// This proposed to use for stateful encoders like *base64*.
-    fn finish_chunk(&mut self) -> Result<Bytes, ()> {
-        Ok(Bytes::new())
-    }
-
     /// Encode all data
-    fn encode_all(&mut self, source: &dyn Buf) -> Result<Bytes, ()> {
-        let chunk = self.encode_chunk(source)?;
-        let end = self.finish_chunk()?;
-
-        Ok(if end.is_empty() {
-            chunk
-        } else {
-            let mut chunk = chunk.try_mut().unwrap();
-            chunk.put(end);
-            chunk.freeze()
-        })
-    }
+    fn encode(&mut self, input: &[u8]) -> Vec<u8>;
 }
 
 /// 7bit codec
 ///
+/// WARNING: Panics when passed non-ascii chars
 struct SevenBitCodec {
     line_wrapper: EightBitCodec,
 }
@@ -69,11 +24,11 @@ impl SevenBitCodec {
 }
 
 impl EncoderCodec for SevenBitCodec {
-    fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
-        if chunk.bytes().iter().all(u8::is_ascii) {
-            self.line_wrapper.encode_chunk(chunk)
+    fn encode(&mut self, input: &[u8]) -> Vec<u8> {
+        if input.iter().all(u8::is_ascii) {
+            self.line_wrapper.encode(input)
         } else {
-            Err(())
+            panic!("")
         }
     }
 }
@@ -89,8 +44,8 @@ impl QuotedPrintableCodec {
 }
 
 impl EncoderCodec for QuotedPrintableCodec {
-    fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
-        Ok(quoted_printable::encode(chunk.bytes()).into())
+    fn encode(&mut self, input: &[u8]) -> Vec<u8> {
+        quoted_printable::encode(input)
     }
 }
 
@@ -98,75 +53,20 @@ impl EncoderCodec for QuotedPrintableCodec {
 ///
 struct Base64Codec {
     line_wrapper: EightBitCodec,
-    last_padding: Bytes,
 }
 
 impl Base64Codec {
     pub fn new() -> Self {
         Base64Codec {
+            // TODO probably 78, 76 is for qp
             line_wrapper: EightBitCodec::new().with_limit(78 - 2),
-            last_padding: Bytes::new(),
         }
     }
 }
 
 impl EncoderCodec for Base64Codec {
-    fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
-        let in_len = self.last_padding.len() + chunk.remaining();
-        let out_len = in_len * 4 / 3;
-
-        let mut out = BytesMut::with_capacity(out_len);
-
-        let chunk = if self.last_padding.is_empty() {
-            chunk.bytes()[..].into_buf()
-        } else {
-            let mut src = BytesMut::with_capacity(3);
-            let len = min(chunk.remaining(), 3 - self.last_padding.len());
-
-            src.put(&self.last_padding);
-            src.put(&chunk.bytes()[..len]);
-
-            // encode beginning
-            unsafe {
-                let len = base64::encode_config_slice(&src, base64::STANDARD, out.bytes_mut());
-                out.advance_mut(len);
-            }
-
-            chunk.bytes()[len..].into_buf()
-        };
-
-        let len = chunk.remaining() - (chunk.remaining() % 3);
-        let chunk = if len > 0 {
-            // encode chunk
-            unsafe {
-                let len = base64::encode_config_slice(
-                    &chunk.bytes()[..len],
-                    base64::STANDARD,
-                    out.bytes_mut(),
-                );
-                out.advance_mut(len);
-            }
-            chunk.bytes()[len..].into_buf()
-        } else {
-            chunk.bytes()[..].into_buf()
-        };
-
-        // update last padding
-        self.last_padding = chunk.bytes().into();
-
-        self.line_wrapper.encode_chunk(&out.freeze().into_buf())
-    }
-
-    fn finish_chunk(&mut self) -> Result<Bytes, ()> {
-        let mut out = BytesMut::with_capacity(4);
-
-        unsafe {
-            let len =
-                base64::encode_config_slice(&self.last_padding, base64::STANDARD, out.bytes_mut());
-            out.advance_mut(len);
-        }
-
-        self.line_wrapper.encode_chunk(&out.freeze().into_buf())
+    fn encode(&mut self, input: &[u8]) -> Vec<u8> {
+        self.line_wrapper.encode(base64::encode(input).as_bytes())
     }
 }
 
@@ -174,7 +74,6 @@ impl EncoderCodec for Base64Codec {
 ///
 struct EightBitCodec {
     max_length: usize,
-    line_bytes: usize,
 }
 
 const DEFAULT_MAX_LINE_LENGTH: usize = 1000 - 2;
@@ -183,7 +82,6 @@ impl EightBitCodec {
     pub fn new() -> Self {
         EightBitCodec {
             max_length: DEFAULT_MAX_LINE_LENGTH,
-            line_bytes: 0,
         }
     }
 
@@ -194,37 +92,15 @@ impl EightBitCodec {
 }
 
 impl EncoderCodec for EightBitCodec {
-    fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
-        let mut out = BytesMut::with_capacity(chunk.remaining() + 20);
-        let mut src = chunk.bytes()[..].into_buf();
-        while src.has_remaining() {
-            let line_break = src.bytes().iter().position(|b| *b == b'\n');
-            let mut split_pos = if let Some(line_break) = line_break {
-                line_break
-            } else {
-                src.remaining()
-            };
-            let max_length = self.max_length - self.line_bytes;
-            if split_pos < max_length {
-                // advance line bytes
-                self.line_bytes += split_pos;
-            } else {
-                split_pos = max_length;
-                // reset line bytes
-                self.line_bytes = 0;
-            };
-            let has_remaining = split_pos < src.remaining();
-            //let mut taken = src.take(split_pos);
-            out.reserve(split_pos + if has_remaining { 2 } else { 0 });
-            //out.put(&mut taken);
-            out.put(&src.bytes()[..split_pos]);
-            if has_remaining {
-                out.put_slice(b"\r\n");
-            }
-            src.advance(split_pos);
-            //src = taken.into_inner();
-        }
-        Ok(out.freeze())
+    fn encode(&mut self, input: &[u8]) -> Vec<u8> {
+        let ending = &crlf();
+
+        let mut out = vec![0_u8; input.len() + input.len() / self.max_length * ending.len()];
+        let mut writer: &mut [u8] = out.as_mut();
+        writer.write_all(input).unwrap();
+
+        line_wrap(&mut out, input.len(), self.max_length, ending);
+        out
     }
 }
 
@@ -239,8 +115,8 @@ impl BinaryCodec {
 }
 
 impl EncoderCodec for BinaryCodec {
-    fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
-        Ok(chunk.bytes().into())
+    fn encode(&mut self, input: &[u8]) -> Vec<u8> {
+        input.into()
     }
 }
 
@@ -261,27 +137,23 @@ pub fn codec(encoding: Option<&ContentTransferEncoding>) -> Box<dyn EncoderCodec
 
 #[cfg(test)]
 mod test {
-    use super::{
-        Base64Codec, BinaryCodec, EightBitCodec, EncoderCodec, QuotedPrintableCodec, SevenBitCodec,
-    };
-    use bytes::IntoBuf;
-    use std::str::from_utf8;
+    use super::*;
 
     #[test]
     fn seven_bit_encode() {
         let mut c = SevenBitCodec::new();
 
         assert_eq!(
-            c.encode_chunk(&"Hello, world!".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("Hello, world!".into()))
+            &String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
+            "Hello, world!"
         );
+    }
 
-        assert_eq!(
-            c.encode_chunk(&"Hello, мир!".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Err(())
-        );
+    #[test]
+    #[should_panic]
+    fn seven_bit_encode_panic() {
+        let mut c = SevenBitCodec::new();
+        c.encode("Hello, мир!".as_bytes());
     }
 
     #[test]
@@ -289,16 +161,12 @@ mod test {
         let mut c = QuotedPrintableCodec::new();
 
         assert_eq!(
-            c.encode_chunk(&"Привет, мир!".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok(
-                "=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".into()
-            ))
+            &String::from_utf8(c.encode("Привет, мир!".as_bytes())).unwrap(),
+            "=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!"
         );
 
-        assert_eq!(c.encode_chunk(&"Текст письма в уникоде".into_buf())
-                   .map(|s| from_utf8(&s).map(|s| String::from(s))),
-                   Ok(Ok("=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5".into())));
+        assert_eq!(&String::from_utf8(c.encode("Текст письма в уникоде".as_bytes())).unwrap(),
+                   "=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5");
     }
 
     #[test]
@@ -306,112 +174,48 @@ mod test {
         let mut c = Base64Codec::new();
 
         assert_eq!(
-            c.encode_all(&"Привет, мир!".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("0J/RgNC40LLQtdGCLCDQvNC40YAh".into()))
+            &String::from_utf8(c.encode("Привет, мир!".as_bytes())).unwrap(),
+            "0J/RgNC40LLQtdGCLCDQvNC40YAh"
         );
 
         assert_eq!(
-            c.encode_all(&"Текст письма в уникоде подлиннее.".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok(concat!(
-                "0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQ\r\n",
-                "vtC00LUg0L/QvtC00LvQuNC90L3QtdC1Lg=="
+            &String::from_utf8(c.encode("Текст письма в уникоде подлиннее.".as_bytes())).unwrap(),
+            concat!(
+                "0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQ",
+                "vtC00LUg0L/QvtC00LvQuNC90L3Q\r\ntdC1Lg=="
             )
-            .into()))
         );
-    }
-
-    #[test]
-    fn base64_encode_all() {
-        let mut c = Base64Codec::new();
 
         assert_eq!(
-            c.encode_all(
-                &"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую."
-                    .into_buf()
-            ).map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok(
+            &String::from_utf8(c.encode(
+                "Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую.".as_bytes()
+                    )).unwrap(),
+
                 concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
                         "0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
                         "viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
-                        "udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRji4=").into()
-            ))
+                        "udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRji4=")
         );
-
-        let mut c = Base64Codec::new();
-
         assert_eq!(
-            c.encode_all(
-                &"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую это."
-                    .into_buf()
-            ).map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok(
+            &String::from_utf8(c.encode(
+                "Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую это.".as_bytes()
+            )).unwrap(),
+
                 concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
                         "0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
                         "viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
                         "udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRjiDRjdGC\r\n",
-                        "0L4u").into()
-            ))
+                        "0L4u")
         );
     }
 
     #[test]
-    fn base64_encode_chunked() {
+    fn base64_encodeed() {
         let mut c = Base64Codec::new();
 
         assert_eq!(
-            c.encode_chunk(&"Chunk.".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("Q2h1bmsu".into()))
-        );
-
-        assert_eq!(
-            c.finish_chunk()
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("".into()))
-        );
-
-        let mut c = Base64Codec::new();
-
-        assert_eq!(
-            c.encode_chunk(&"Chunk".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("Q2h1".into()))
-        );
-
-        assert_eq!(
-            c.finish_chunk()
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("bms=".into()))
-        );
-
-        let mut c = Base64Codec::new();
-
-        assert_eq!(
-            c.encode_chunk(&"Chun".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("Q2h1".into()))
-        );
-
-        assert_eq!(
-            c.finish_chunk()
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("bg==".into()))
-        );
-
-        let mut c = Base64Codec::new();
-
-        assert_eq!(
-            c.encode_chunk(&"Chu".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("Q2h1".into()))
-        );
-
-        assert_eq!(
-            c.finish_chunk()
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("".into()))
+            &String::from_utf8(c.encode("Chunk.".as_bytes())).unwrap(),
+            "Q2h1bmsu"
         );
     }
 
@@ -420,15 +224,13 @@ mod test {
         let mut c = EightBitCodec::new();
 
         assert_eq!(
-            c.encode_chunk(&"Hello, world!".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("Hello, world!".into()))
+            &String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
+            "Hello, world!"
         );
 
         assert_eq!(
-            c.encode_chunk(&"Hello, мир!".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("Hello, мир!".into()))
+            &String::from_utf8(c.encode("Hello, мир!".as_bytes())).unwrap(),
+            "Hello, мир!"
         );
     }
 
@@ -437,15 +239,13 @@ mod test {
         let mut c = BinaryCodec::new();
 
         assert_eq!(
-            c.encode_chunk(&"Hello, world!".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("Hello, world!".into()))
+            &String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
+            "Hello, world!"
         );
 
         assert_eq!(
-            c.encode_chunk(&"Hello, мир!".into_buf())
-                .map(|s| from_utf8(&s).map(|s| String::from(s))),
-            Ok(Ok("Hello, мир!".into()))
+            &String::from_utf8(c.encode("Hello, мир!".as_bytes())).unwrap(),
+            "Hello, мир!"
         );
     }
 }
diff --git a/src/message/mailbox/types.rs b/src/message/mailbox/types.rs
index 88ab125c7..305d20c3e 100644
--- a/src/message/mailbox/types.rs
+++ b/src/message/mailbox/types.rs
@@ -25,7 +25,6 @@ pub struct Mailbox {
 
 impl Mailbox {
     /// Create new mailbox using email address and addressee name
-    #[inline]
     pub fn new(name: Option<String>, email: Address) -> Self {
         Mailbox { name, email }
     }
@@ -110,32 +109,27 @@ pub struct Mailboxes(Vec<Mailbox>);
 
 impl Mailboxes {
     /// Create mailboxes list
-    #[inline]
     pub fn new() -> Self {
         Mailboxes(Vec::new())
     }
 
     /// Add mailbox to a list
-    #[inline]
     pub fn with(mut self, mbox: Mailbox) -> Self {
         self.0.push(mbox);
         self
     }
 
     /// Add mailbox to a list
-    #[inline]
     pub fn push(&mut self, mbox: Mailbox) {
         self.0.push(mbox);
     }
 
     /// Extract first mailbox
-    #[inline]
     pub fn into_single(self) -> Option<Mailbox> {
         self.into()
     }
 
     /// Iterate over mailboxes
-    #[inline]
     pub fn iter(&self) -> Iter<Mailbox> {
         self.0.iter()
     }
diff --git a/src/message/mimebody.rs b/src/message/mimebody.rs
index 42eb7ea48..1bb21375e 100644
--- a/src/message/mimebody.rs
+++ b/src/message/mimebody.rs
@@ -1,43 +1,45 @@
 use crate::message::{
     encoder::codec,
     header::{ContentTransferEncoding, ContentType, Header, Headers},
+    EmailFormat,
 };
-use bytes::{Bytes, IntoBuf};
 use mime::Mime;
-use std::{
-    fmt::{Display, Error as FmtError, Formatter, Result as FmtResult},
-    str::from_utf8,
-};
 use textnonce::TextNonce;
 
 /// MIME part variants
 ///
 #[derive(Debug, Clone)]
-pub enum Part<B = Bytes> {
+pub enum Part {
     /// Single part with content
     ///
-    Single(SinglePart<B>),
+    Single(SinglePart),
 
     /// Multiple parts of content
     ///
-    Multi(MultiPart<B>),
+    Multi(MultiPart),
 }
 
-impl<B> Display for Part<B>
-where
-    B: AsRef<str>,
-{
-    fn fmt(&self, f: &mut Formatter) -> FmtResult {
-        match *self {
-            Part::Single(ref part) => part.fmt(f),
-            Part::Multi(ref part) => part.fmt(f),
+impl EmailFormat for Part {
+    fn format(&self, out: &mut Vec<u8>) {
+        match self {
+            Part::Single(part) => part.format(out),
+            Part::Multi(part) => part.format(out),
         }
     }
 }
 
+impl Part {
+    /// Get message content formatted for SMTP
+    pub fn formatted(&self) -> Vec<u8> {
+        let mut out = Vec::new();
+        self.format(&mut out);
+        out
+    }
+}
+
 /// Parts of multipart body
 ///
-pub type Parts<B = Bytes> = Vec<Part<B>>;
+pub type Parts = Vec<Part>;
 
 /// Creates builder for single part
 ///
@@ -55,18 +57,16 @@ impl SinglePartBuilder {
     }
 
     /// Set the header to singlepart
-    #[inline]
     pub fn header<H: Header>(mut self, header: H) -> Self {
         self.headers.set(header);
         self
     }
 
     /// Build singlepart using body
-    #[inline]
-    pub fn body<T>(self, body: T) -> SinglePart<T> {
+    pub fn body<T: Into<Vec<u8>>>(self, body: T) -> SinglePart {
         SinglePart {
             headers: self.headers,
-            body,
+            body: body.into(),
         }
     }
 }
@@ -94,12 +94,12 @@ impl Default for SinglePartBuilder {
 /// ```
 ///
 #[derive(Debug, Clone)]
-pub struct SinglePart<B = Bytes> {
+pub struct SinglePart {
     headers: Headers,
-    body: B,
+    body: Vec<u8>,
 }
 
-impl SinglePart<()> {
+impl SinglePart {
     /// Creates a default builder for singlepart
     pub fn builder() -> SinglePartBuilder {
         SinglePartBuilder::new()
@@ -129,7 +129,6 @@ impl SinglePart<()> {
     /// Creates a singlepart builder with 8-bit encoding
     ///
     /// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::EightBit)`.
-    #[inline]
     pub fn eight_bit() -> SinglePartBuilder {
         Self::builder().header(ContentTransferEncoding::EightBit)
     }
@@ -137,55 +136,38 @@ impl SinglePart<()> {
     /// Creates a singlepart builder with binary encoding
     ///
     /// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Binary)`.
-    #[inline]
     pub fn binary() -> SinglePartBuilder {
         Self::builder().header(ContentTransferEncoding::Binary)
     }
-}
-
-impl<B> SinglePart<B> {
-    /// Get the transfer encoding
-    #[inline]
-    pub fn encoding(&self) -> Option<&ContentTransferEncoding> {
-        self.headers.get()
-    }
 
     /// Get the headers from singlepart
-    #[inline]
     pub fn headers(&self) -> &Headers {
         &self.headers
     }
 
-    /// Get a mutable reference to the headers
-    #[inline]
-    pub fn headers_mut(&mut self) -> &mut Headers {
-        &mut self.headers
-    }
-
     /// Read the body from singlepart
-    #[inline]
-    pub fn body_ref(&self) -> &B {
+    pub fn body_ref(&self) -> &[u8] {
         &self.body
     }
+
+    /// Get message content formatted for SMTP
+    pub fn formatted(&self) -> Vec<u8> {
+        let mut out = Vec::new();
+        self.format(&mut out);
+        out
+    }
 }
 
-impl<B> Display for SinglePart<B>
-where
-    B: AsRef<str>,
-{
-    fn fmt(&self, f: &mut Formatter) -> FmtResult {
-        self.headers.fmt(f)?;
-        "\r\n".fmt(f)?;
+impl EmailFormat for SinglePart {
+    fn format(&self, out: &mut Vec<u8>) {
+        out.extend_from_slice(self.headers.to_string().as_bytes());
+        out.extend_from_slice(b"\r\n");
 
-        let body = self.body.as_ref();
-        let mut encoder = codec(self.encoding());
-        let result = encoder
-            .encode_all(&body.into_buf())
-            .map_err(|_| FmtError::default())?;
-        let body = from_utf8(&result).map_err(|_| FmtError::default())?;
+        let encoding = self.headers.get::<ContentTransferEncoding>();
+        let mut encoder = codec(encoding);
 
-        body.fmt(f)?;
-        "\r\n".fmt(f)
+        out.extend_from_slice(&encoder.encode(&self.body));
+        out.extend_from_slice(b"\r\n");
     }
 }
 
@@ -255,7 +237,6 @@ pub struct MultiPartBuilder {
 
 impl MultiPartBuilder {
     /// Creates default multipart builder
-    #[inline]
     pub fn new() -> Self {
         Self {
             headers: Headers::new(),
@@ -263,14 +244,12 @@ impl MultiPartBuilder {
     }
 
     /// Set a header
-    #[inline]
     pub fn header<H: Header>(mut self, header: H) -> Self {
         self.headers.set(header);
         self
     }
 
     /// Set `Content-Type` header using [`MultiPartKind`]
-    #[inline]
     pub fn kind(self, kind: MultiPartKind) -> Self {
         self.header(ContentType(kind.into()))
     }
@@ -286,8 +265,7 @@ impl MultiPartBuilder {
     }
 
     /// Creates multipart without parts
-    #[inline]
-    pub fn build<B>(self) -> MultiPart<B> {
+    pub fn build(self) -> MultiPart {
         MultiPart {
             headers: self.headers,
             parts: Vec::new(),
@@ -295,20 +273,17 @@ impl MultiPartBuilder {
     }
 
     /// Creates multipart using part
-    #[inline]
-    pub fn part<B>(self, part: Part<B>) -> MultiPart<B> {
+    pub fn part(self, part: Part) -> MultiPart {
         self.build().part(part)
     }
 
     /// Creates multipart using singlepart
-    #[inline]
-    pub fn singlepart<B>(self, part: SinglePart<B>) -> MultiPart<B> {
+    pub fn singlepart(self, part: SinglePart) -> MultiPart {
         self.build().singlepart(part)
     }
 
     /// Creates multipart using multipart
-    #[inline]
-    pub fn multipart<B>(self, part: MultiPart<B>) -> MultiPart<B> {
+    pub fn multipart(self, part: MultiPart) -> MultiPart {
         self.build().multipart(part)
     }
 }
@@ -322,14 +297,13 @@ impl Default for MultiPartBuilder {
 /// Multipart variant with parts
 ///
 #[derive(Debug, Clone)]
-pub struct MultiPart<B = Bytes> {
+pub struct MultiPart {
     headers: Headers,
-    parts: Parts<B>,
+    parts: Parts,
 }
 
-impl MultiPart<()> {
+impl MultiPart {
     /// Creates multipart builder
-    #[inline]
     pub fn builder() -> MultiPartBuilder {
         MultiPartBuilder::new()
     }
@@ -337,7 +311,6 @@ impl MultiPart<()> {
     /// Creates mixed multipart builder
     ///
     /// Shortcut for `MultiPart::builder().kind(MultiPartKind::Mixed)`
-    #[inline]
     pub fn mixed() -> MultiPartBuilder {
         MultiPart::builder().kind(MultiPartKind::Mixed)
     }
@@ -345,7 +318,6 @@ impl MultiPart<()> {
     /// Creates alternative multipart builder
     ///
     /// Shortcut for `MultiPart::builder().kind(MultiPartKind::Alternative)`
-    #[inline]
     pub fn alternative() -> MultiPartBuilder {
         MultiPart::builder().kind(MultiPartKind::Alternative)
     }
@@ -353,97 +325,90 @@ impl MultiPart<()> {
     /// Creates related multipart builder
     ///
     /// Shortcut for `MultiPart::builder().kind(MultiPartKind::Related)`
-    #[inline]
     pub fn related() -> MultiPartBuilder {
         MultiPart::builder().kind(MultiPartKind::Related)
     }
-}
 
-impl<B> MultiPart<B> {
     /// Add part to multipart
-    #[inline]
-    pub fn part(mut self, part: Part<B>) -> Self {
+    pub fn part(mut self, part: Part) -> Self {
         self.parts.push(part);
         self
     }
 
     /// Add single part to multipart
-    #[inline]
-    pub fn singlepart(mut self, part: SinglePart<B>) -> Self {
+    pub fn singlepart(mut self, part: SinglePart) -> Self {
         self.parts.push(Part::Single(part));
         self
     }
 
     /// Add multi part to multipart
-    #[inline]
-    pub fn multipart(mut self, part: MultiPart<B>) -> Self {
+    pub fn multipart(mut self, part: MultiPart) -> Self {
         self.parts.push(Part::Multi(part));
         self
     }
 
     /// Get the boundary of multipart contents
-    #[inline]
     pub fn boundary(&self) -> String {
         let content_type = &self.headers.get::<ContentType>().unwrap().0;
         content_type.get_param("boundary").unwrap().as_str().into()
     }
 
     /// Get the headers from the multipart
-    #[inline]
     pub fn headers(&self) -> &Headers {
         &self.headers
     }
 
     /// Get a mutable reference to the headers
-    #[inline]
     pub fn headers_mut(&mut self) -> &mut Headers {
         &mut self.headers
     }
 
     /// Get the parts from the multipart
-    #[inline]
-    pub fn parts(&self) -> &Parts<B> {
+    pub fn parts(&self) -> &Parts {
         &self.parts
     }
 
     /// Get a mutable reference to the parts
-    #[inline]
-    pub fn parts_mut(&mut self) -> &mut Parts<B> {
+    pub fn parts_mut(&mut self) -> &mut Parts {
         &mut self.parts
     }
+
+    /// Get message content formatted for SMTP
+    pub fn formatted(&self) -> Vec<u8> {
+        let mut out = Vec::new();
+        self.format(&mut out);
+        out
+    }
 }
 
-impl<B> Display for MultiPart<B>
-where
-    B: AsRef<str>,
-{
-    fn fmt(&self, f: &mut Formatter) -> FmtResult {
-        self.headers.fmt(f)?;
-        "\r\n".fmt(f)?;
+impl EmailFormat for MultiPart {
+    fn format(&self, out: &mut Vec<u8>) {
+        out.extend_from_slice(self.headers.to_string().as_bytes());
+        out.extend_from_slice(b"\r\n");
 
         let boundary = self.boundary();
 
         for part in &self.parts {
-            "--".fmt(f)?;
-            boundary.fmt(f)?;
-            "\r\n".fmt(f)?;
-            part.fmt(f)?;
+            out.extend_from_slice(b"--");
+            out.extend_from_slice(boundary.as_bytes());
+            out.extend_from_slice(b"\r\n");
+            part.format(out);
         }
 
-        "--".fmt(f)?;
-        boundary.fmt(f)?;
-        "--\r\n".fmt(f)
+        out.extend_from_slice(b"--");
+        out.extend_from_slice(boundary.as_bytes());
+        out.extend_from_slice(b"--\r\n");
     }
 }
 
 #[cfg(test)]
 mod test {
-    use super::{MultiPart, Part, SinglePart};
+    use super::*;
     use crate::message::header;
 
     #[test]
     fn single_part_binary() {
-        let part: SinglePart<String> = SinglePart::builder()
+        let part = SinglePart::builder()
             .header(header::ContentType(
                 "text/plain; charset=utf8".parse().unwrap(),
             ))
@@ -451,7 +416,7 @@ mod test {
             .body(String::from("Текст письма в уникоде"));
 
         assert_eq!(
-            format!("{}", part),
+            String::from_utf8(part.formatted()).unwrap(),
             concat!(
                 "Content-Type: text/plain; charset=utf8\r\n",
                 "Content-Transfer-Encoding: binary\r\n",
@@ -463,7 +428,7 @@ mod test {
 
     #[test]
     fn single_part_quoted_printable() {
-        let part: SinglePart<String> = SinglePart::builder()
+        let part = SinglePart::builder()
             .header(header::ContentType(
                 "text/plain; charset=utf8".parse().unwrap(),
             ))
@@ -471,7 +436,7 @@ mod test {
             .body(String::from("Текст письма в уникоде"));
 
         assert_eq!(
-            format!("{}", part),
+            String::from_utf8(part.formatted()).unwrap(),
             concat!(
                 "Content-Type: text/plain; charset=utf8\r\n",
                 "Content-Transfer-Encoding: quoted-printable\r\n",
@@ -484,7 +449,7 @@ mod test {
 
     #[test]
     fn single_part_base64() {
-        let part: SinglePart<String> = SinglePart::builder()
+        let part = SinglePart::builder()
             .header(header::ContentType(
                 "text/plain; charset=utf8".parse().unwrap(),
             ))
@@ -492,7 +457,7 @@ mod test {
             .body(String::from("Текст письма в уникоде"));
 
         assert_eq!(
-            format!("{}", part),
+            String::from_utf8(part.formatted()).unwrap(),
             concat!(
                 "Content-Type: text/plain; charset=utf8\r\n",
                 "Content-Transfer-Encoding: base64\r\n",
@@ -504,7 +469,7 @@ mod test {
 
     #[test]
     fn multi_part_mixed() {
-        let part: MultiPart<String> = MultiPart::mixed()
+        let part = MultiPart::mixed()
             .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
             .part(Part::Single(
                 SinglePart::builder()
@@ -531,7 +496,7 @@ mod test {
                     .body(String::from("int main() { return 0; }")),
             );
 
-        assert_eq!(format!("{}", part),
+        assert_eq!(String::from_utf8(part.formatted()).unwrap(),
                    concat!("Content-Type: multipart/mixed;",
                            " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
                            "\r\n",
@@ -551,7 +516,7 @@ mod test {
 
     #[test]
     fn multi_part_alternative() {
-        let part: MultiPart<String> = MultiPart::alternative()
+        let part = MultiPart::alternative()
             .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
             .part(Part::Single(SinglePart::builder()
                              .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
@@ -562,7 +527,7 @@ mod test {
                              .header(header::ContentTransferEncoding::Binary)
                              .body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
 
-        assert_eq!(format!("{}", part),
+        assert_eq!(String::from_utf8(part.formatted()).unwrap(),
                    concat!("Content-Type: multipart/alternative;",
                            " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
                            "\r\n",
@@ -581,7 +546,7 @@ mod test {
 
     #[test]
     fn multi_part_mixed_related() {
-        let part: MultiPart<String> = MultiPart::mixed()
+        let part = MultiPart::mixed()
             .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
             .multipart(MultiPart::related()
                             .boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh")
@@ -603,7 +568,7 @@ mod test {
                              .header(header::ContentTransferEncoding::Binary)
                              .body(String::from("int main() { return 0; }")));
 
-        assert_eq!(format!("{}", part),
+        assert_eq!(String::from_utf8(part.formatted()).unwrap(),
                    concat!("Content-Type: multipart/mixed;",
                            " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
                            "\r\n",
diff --git a/src/message/mod.rs b/src/message/mod.rs
index 3179e37e5..75e97a602 100644
--- a/src/message/mod.rs
+++ b/src/message/mod.rs
@@ -16,16 +16,16 @@ use crate::{
     message::header::{EmailDate, Header, Headers, MailboxesHeader},
     Envelope, Error as EmailError,
 };
-use bytes::Bytes;
-use std::{
-    convert::TryFrom,
-    fmt::{Display, Formatter, Result as FmtResult},
-    time::SystemTime,
-};
+use std::{convert::TryFrom, time::SystemTime};
 use uuid::Uuid;
 
 const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
 
+pub trait EmailFormat {
+    // Use a writer?
+    fn format(&self, out: &mut Vec<u8>);
+}
+
 /// A builder for messages
 #[derive(Debug, Clone)]
 pub struct MessageBuilder {
@@ -34,7 +34,6 @@ pub struct MessageBuilder {
 
 impl MessageBuilder {
     /// Creates a new default message builder
-    #[inline]
     pub fn new() -> Self {
         Self {
             headers: Headers::new(),
@@ -42,7 +41,6 @@ impl MessageBuilder {
     }
 
     /// Set custom header to message
-    #[inline]
     pub fn header<H: Header>(mut self, header: H) -> Self {
         self.headers.set(header);
         self
@@ -61,7 +59,6 @@ impl MessageBuilder {
     /// Add `Date` header to message
     ///
     /// Shortcut for `self.header(header::Date(date))`.
-    #[inline]
     pub fn date(self, date: EmailDate) -> Self {
         self.header(header::Date(date))
     }
@@ -69,7 +66,6 @@ impl MessageBuilder {
     /// Set `Date` header using current date/time
     ///
     /// Shortcut for `self.date(SystemTime::now())`.
-    #[inline]
     pub fn date_now(self) -> Self {
         self.date(SystemTime::now().into())
     }
@@ -77,7 +73,6 @@ impl MessageBuilder {
     /// Set `Subject` header to message
     ///
     /// Shortcut for `self.header(header::Subject(subject.into()))`.
-    #[inline]
     pub fn subject<S: Into<String>>(self, subject: S) -> Self {
         self.header(header::Subject(subject.into()))
     }
@@ -85,8 +80,9 @@ impl MessageBuilder {
     /// Set `Mime-Version` header to 1.0
     ///
     /// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
-    #[inline]
-    pub fn mime_1_0(self) -> Self {
+    ///
+    /// Not exposed as it is set by body methods
+    fn mime_1_0(self) -> Self {
         self.header(header::MIME_VERSION_1_0)
     }
 
@@ -95,7 +91,6 @@ impl MessageBuilder {
     /// https://tools.ietf.org/html/rfc5322#section-3.6.2
     ///
     /// Shortcut for `self.header(header::Sender(mbox))`.
-    #[inline]
     pub fn sender(self, mbox: Mailbox) -> Self {
         self.header(header::Sender(mbox))
     }
@@ -105,7 +100,6 @@ impl MessageBuilder {
     /// https://tools.ietf.org/html/rfc5322#section-3.6.2
     ///
     /// Shortcut for `self.mailbox(header::From(mbox))`.
-    #[inline]
     pub fn from(self, mbox: Mailbox) -> Self {
         self.mailbox(header::From(mbox.into()))
     }
@@ -115,7 +109,6 @@ impl MessageBuilder {
     /// https://tools.ietf.org/html/rfc5322#section-3.6.2
     ///
     /// Shortcut for `self.mailbox(header::ReplyTo(mbox))`.
-    #[inline]
     pub fn reply_to(self, mbox: Mailbox) -> Self {
         self.mailbox(header::ReplyTo(mbox.into()))
     }
@@ -123,7 +116,6 @@ impl MessageBuilder {
     /// Set or add mailbox to `To` header
     ///
     /// Shortcut for `self.mailbox(header::To(mbox))`.
-    #[inline]
     pub fn to(self, mbox: Mailbox) -> Self {
         self.mailbox(header::To(mbox.into()))
     }
@@ -131,7 +123,6 @@ impl MessageBuilder {
     /// Set or add mailbox to `Cc` header
     ///
     /// Shortcut for `self.mailbox(header::Cc(mbox))`.
-    #[inline]
     pub fn cc(self, mbox: Mailbox) -> Self {
         self.mailbox(header::Cc(mbox.into()))
     }
@@ -139,21 +130,18 @@ impl MessageBuilder {
     /// Set or add mailbox to `Bcc` header
     ///
     /// Shortcut for `self.mailbox(header::Bcc(mbox))`.
-    #[inline]
     pub fn bcc(self, mbox: Mailbox) -> Self {
         self.mailbox(header::Bcc(mbox.into()))
     }
 
     /// Set or add message id to [`In-Reply-To`
     /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
-    #[inline]
     pub fn in_reply_to(self, id: String) -> Self {
         self.header(header::InReplyTo(id))
     }
 
     /// Set or add message id to [`References`
     /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
-    #[inline]
     pub fn references(self, id: String) -> Self {
         self.header(header::References(id))
     }
@@ -165,7 +153,6 @@ impl MessageBuilder {
     ///
     /// If `None` is provided, an id will be generated in the
     /// `<UUID@HOSTNAME>`.
-    #[inline]
     pub fn message_id(self, id: Option<String>) -> Self {
         match id {
             Some(i) => self.header(header::MessageId(i)),
@@ -188,107 +175,123 @@ impl MessageBuilder {
 
     /// Set [User-Agent
     /// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004)
-    #[inline]
     pub fn user_agent(self, id: String) -> Self {
         self.header(header::UserAgent(id))
     }
 
     fn insert_missing_headers(self) -> Self {
+        let mut new = self;
+
         // Insert Date if missing
-        if self.headers.get::<header::Date>().is_none() {
-            self.date_now()
+        new = if new.headers.get::<header::Date>().is_none() {
+            new.date_now()
         } else {
-            self
-        }
+            new
+        };
+
         // TODO insert sender if needed?
+        new
     }
 
     // TODO: High-level methods for attachments and embedded files
 
-    /// Create message by joining content
-    #[inline]
-    fn build<T>(self, body: T, split: bool) -> Result<Message<T>, EmailError> {
+    /// Create message from body
+    fn build(self, body: Body) -> Result<Message, EmailError> {
         let res = self.insert_missing_headers();
+
         let envelope = Envelope::try_from(&res.headers)?;
         Ok(Message {
             headers: res.headers,
-            split,
             body,
             envelope,
         })
     }
 
-    /// Create message using body
-    #[inline]
-    pub fn body<T>(self, body: T) -> Result<Message<T>, EmailError> {
-        self.build(body, true)
+    // In theory having a body is optional
+
+    /// Plain ASCII body
+    ///
+    /// *WARNING*: Generally not what you want
+    pub fn body<T: Into<String>>(self, body: T) -> Result<Message, EmailError> {
+        // 998 chars by line
+        // CR and LF MUST only occur together as CRLF; they MUST NOT appear
+        //  independently in the body.
+        let body = body.into();
+
+        if !&body.is_ascii() {
+            return Err(EmailError::NonAsciiChars);
+        }
+
+        self.build(Body::Raw(body))
+    }
+
+    /// Create message using mime body ([`MultiPart`](::MultiPart))
+    pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
+        self.mime_1_0().build(Body::Mime(Part::Multi(part)))
     }
 
-    /// Create message using mime body ([`MultiPart`](::MultiPart) or [`SinglePart`](::SinglePart))
-    // FIXME restrict usage on MIME?
-    #[inline]
-    pub fn mime_body<T>(self, body: T) -> Result<Message<T>, EmailError> {
-        self.mime_1_0().build(body, false)
+    /// Create message using mime body ([`SinglePart`](::SinglePart)
+    pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
+        self.mime_1_0().build(Body::Mime(Part::Single(part)))
     }
 }
 
 /// Email message which can be formatted
 #[derive(Clone, Debug)]
-pub struct Message<B = Bytes> {
+pub struct Message {
     headers: Headers,
-    split: bool,
-    body: B,
+    body: Body,
     envelope: Envelope,
 }
 
-impl Message<()> {
+#[derive(Clone, Debug)]
+enum Body {
+    Mime(Part),
+    Raw(String),
+}
+
+impl Message {
     /// Create a new message builder without headers
-    #[inline]
     pub fn builder() -> MessageBuilder {
         MessageBuilder::new()
     }
-}
 
-impl<B> Message<B> {
     /// Get the headers from the Message
-    #[inline]
     pub fn headers(&self) -> &Headers {
         &self.headers
     }
 
-    /// Read the body
-    #[inline]
-    pub fn body_ref(&self) -> &B {
-        &self.body
-    }
-
-    /// Try to extract envelope data from `Message` headers
-    #[inline]
+    /// Get `Message` envelope
     pub fn envelope(&self) -> &Envelope {
         &self.envelope
     }
-}
 
-impl Default for MessageBuilder {
-    fn default() -> Self {
-        MessageBuilder::new()
+    /// Get message content formatted for SMTP
+    pub fn formatted(&self) -> Vec<u8> {
+        let mut out = Vec::new();
+        self.format(&mut out);
+        out
     }
 }
 
-impl<B> Display for Message<B>
-where
-    B: Display,
-{
-    fn fmt(&self, f: &mut Formatter) -> FmtResult {
-        self.headers.fmt(f)?;
-        if self.split {
-            f.write_str("\r\n")?;
+impl EmailFormat for Message {
+    fn format(&self, out: &mut Vec<u8>) {
+        out.extend_from_slice(self.headers.to_string().as_bytes());
+        match &self.body {
+            Body::Mime(p) => p.format(out),
+            Body::Raw(r) => {
+                out.extend_from_slice(b"\r\n");
+                out.extend(r.as_bytes())
+            }
         }
-        self.body.fmt(f)
     }
 }
 
-// An email is Message + Envelope
+impl Default for MessageBuilder {
+    fn default() -> Self {
+        MessageBuilder::new()
+    }
+}
 
 #[cfg(test)]
 mod test {
@@ -315,7 +318,7 @@ mod test {
             .unwrap();
 
         assert_eq!(
-            format!("{}", email),
+            String::from_utf8(email.formatted()).unwrap(),
             concat!(
                 "Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
                 "From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
diff --git a/src/message/utf8_b.rs b/src/message/utf8_b.rs
index 606aafe35..71f7695d8 100644
--- a/src/message/utf8_b.rs
+++ b/src/message/utf8_b.rs
@@ -1,5 +1,7 @@
 use std::str::from_utf8;
 
+// https://tools.ietf.org/html/rfc1522
+
 fn allowed_char(c: char) -> bool {
     c >= 1 as char && c <= 9 as char
         || c == 11 as char
diff --git a/src/transport/file/mod.rs b/src/transport/file/mod.rs
index af19d2a64..042c50bc2 100644
--- a/src/transport/file/mod.rs
+++ b/src/transport/file/mod.rs
@@ -8,6 +8,7 @@ use std::{
     fs::File,
     io::prelude::*,
     path::{Path, PathBuf},
+    str,
 };
 use uuid::Uuid;
 
@@ -31,12 +32,13 @@ impl FileTransport {
 
 #[derive(PartialEq, Eq, Clone, Debug)]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-struct SerializableEmail {
+struct SerializableEmail<'a> {
     envelope: Envelope,
-    message: Vec<u8>,
+    raw_message: Option<&'a [u8]>,
+    message: Option<&'a str>,
 }
 
-impl<'a, B> Transport<'a, B> for FileTransport {
+impl<'a> Transport<'a> for FileTransport {
     type Result = FileResult;
 
     fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result {
@@ -45,10 +47,19 @@ impl<'a, B> Transport<'a, B> for FileTransport {
         let mut file = self.path.clone();
         file.push(format!("{}.json", email_id));
 
-        let serialized = serde_json::to_string(&SerializableEmail {
-            envelope: envelope.clone(),
-            message: email.to_vec(),
-        })?;
+        let serialized = match str::from_utf8(email) {
+            // Serialize as UTF-8 string if possible
+            Ok(m) => serde_json::to_string(&SerializableEmail {
+                envelope: envelope.clone(),
+                message: Some(m),
+                raw_message: None,
+            }),
+            Err(_) => serde_json::to_string(&SerializableEmail {
+                envelope: envelope.clone(),
+                message: None,
+                raw_message: Some(email),
+            }),
+        }?;
 
         File::create(file.as_path())?.write_all(serialized.as_bytes())?;
         Ok(email_id.to_string())
diff --git a/src/transport/sendmail/mod.rs b/src/transport/sendmail/mod.rs
index 7a30b2c90..1b1a6c25d 100644
--- a/src/transport/sendmail/mod.rs
+++ b/src/transport/sendmail/mod.rs
@@ -36,7 +36,7 @@ impl SendmailTransport {
     }
 }
 
-impl<'a, B> Transport<'a, B> for SendmailTransport {
+impl<'a> Transport<'a> for SendmailTransport {
     type Result = SendmailResult;
 
     fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result {
diff --git a/src/transport/smtp/mod.rs b/src/transport/smtp/mod.rs
index 089ef585b..b6adfc267 100644
--- a/src/transport/smtp/mod.rs
+++ b/src/transport/smtp/mod.rs
@@ -437,7 +437,7 @@ impl<'a> SmtpTransport {
     }
 }
 
-impl<'a, B> Transport<'a, B> for SmtpTransport {
+impl<'a> Transport<'a> for SmtpTransport {
     type Result = SmtpResult;
 
     /// Sends an email
diff --git a/src/transport/stub/mod.rs b/src/transport/stub/mod.rs
index dcdf6dc4d..21c13a130 100644
--- a/src/transport/stub/mod.rs
+++ b/src/transport/stub/mod.rs
@@ -5,7 +5,6 @@
 use crate::Envelope;
 use crate::Transport;
 use log::info;
-use std::fmt::Display;
 
 /// This transport logs the message envelope and returns the given response
 #[derive(Debug, Clone, Copy)]
@@ -28,10 +27,7 @@ impl StubTransport {
 /// SMTP result type
 pub type StubResult = Result<(), ()>;
 
-impl<'a, B> Transport<'a, B> for StubTransport
-where
-    B: Display,
-{
+impl<'a> Transport<'a> for StubTransport {
     type Result = StubResult;
 
     fn send_raw(&mut self, envelope: &Envelope, _email: &[u8]) -> Self::Result {
diff --git a/website/src/creating-messages/email.md b/website/src/creating-messages/email.md
index 6550b7bac..e4092a1ae 100644
--- a/website/src/creating-messages/email.md
+++ b/website/src/creating-messages/email.md
@@ -14,7 +14,7 @@ The easiest way how we can create email message with simple string.
 # extern crate lettre;
 use lettre::message::Message;
 
-let m: Message<&str> = Message::builder()
+let m = Message::builder()
     .from("NoBody <nobody@domain.tld>".parse().unwrap())
     .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
     .to("Hei <hei@domain.tld>".parse().unwrap())
@@ -44,14 +44,14 @@ The more complex way is using MIME contents.
 
 ```rust
 # extern crate lettre;
-use lettre::message::{header, Message, SinglePart};
+use lettre::message::{header, Message, SinglePart, Part};
 
-let m: Message<SinglePart<&str>> = Message::builder()
+let m = Message::builder()
     .from("NoBody <nobody@domain.tld>".parse().unwrap())
     .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
     .to("Hei <hei@domain.tld>".parse().unwrap())
     .subject("Happy new year")
-    .mime_body(
+    .singlepart(
         SinglePart::builder()
             .header(header::ContentType(
                 "text/plain; charset=utf8".parse().unwrap(),
@@ -82,14 +82,14 @@ And more advanced way of building message by using multipart MIME contents.
 
 ```rust
 # extern crate lettre;
-use lettre::message::{header, Message, MultiPart, SinglePart};
+use lettre::message::{header, Message, MultiPart, SinglePart, Part};
 
-let m: Message<MultiPart<&str>> = Message::builder()
+let m = Message::builder()
     .from("NoBody <nobody@domain.tld>".parse().unwrap())
     .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
     .to("Hei <hei@domain.tld>".parse().unwrap())
     .subject("Happy new year")
-    .mime_body(
+    .multipart(
         MultiPart::mixed()
         .multipart(
             MultiPart::alternative()
diff --git a/website/src/sending-messages/smtp.md b/website/src/sending-messages/smtp.md
index 79dd3c64e..301bef5f4 100644
--- a/website/src/sending-messages/smtp.md
+++ b/website/src/sending-messages/smtp.md
@@ -37,7 +37,7 @@ fn main() {
     let mut mailer =
     SmtpClient::new_unencrypted_localhost().unwrap().transport();
     // Send the email
-    let result = mailer.send(email);
+    let result = mailer.send(&email);
 
     assert!(result.is_ok());
 }
@@ -88,11 +88,11 @@ fn main() {
         // Enable connection reuse
         .connection_reuse(ConnectionReuseParameters::ReuseUnlimited).transport();
 
-    let result_1 = mailer.send(email_1);
+    let result_1 = mailer.send(&email_1);
     assert!(result_1.is_ok());
 
     // The second email will use the same connection
-    let result_2 = mailer.send(email_2);
+    let result_2 = mailer.send(&email_2);
     assert!(result_2.is_ok());
 
     // Explicitly close the SMTP transaction as we enabled connection reuse
@@ -145,7 +145,7 @@ fn main() {
         .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
         .transport();
 
-    let result = mailer.send(email);
+    let result = mailer.send(&email);
 
     assert!(result.is_ok());
 
diff --git a/website/src/sending-messages/stub.md b/website/src/sending-messages/stub.md
index 42d2539a3..1d7545601 100644
--- a/website/src/sending-messages/stub.md
+++ b/website/src/sending-messages/stub.md
@@ -19,7 +19,7 @@ fn main() {
         .unwrap();
 
     let mut sender = StubTransport::new_positive();
-    let result = sender.send(email);
+    let result = sender.send(&email);
     assert!(result.is_ok());
 }
 ```


Test patch:
diff --git a/tests/transport_file.rs b/tests/transport_file.rs
index 2104a122a..0c9eeaf79 100644
--- a/tests/transport_file.rs
+++ b/tests/transport_file.rs
@@ -20,7 +20,7 @@ mod test {
             .body("Be happy!")
             .unwrap();
 
-        let result = sender.send(email);
+        let result = sender.send(&email);
         let id = result.unwrap();
 
         let file = temp_dir().join(format!("{}.json", id));
@@ -30,7 +30,7 @@ mod test {
 
         assert_eq!(
             buffer,
-            "{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"message\":[70,114,111,109,58,32,78,111,66,111,100,121,32,60,110,111,98,111,100,121,64,100,111,109,97,105,110,46,116,108,100,62,13,10,82,101,112,108,121,45,84,111,58,32,89,117,105,110,32,60,121,117,105,110,64,100,111,109,97,105,110,46,116,108,100,62,13,10,84,111,58,32,72,101,105,32,60,104,101,105,64,100,111,109,97,105,110,46,116,108,100,62,13,10,83,117,98,106,101,99,116,58,32,72,97,112,112,121,32,110,101,119,32,121,101,97,114,13,10,68,97,116,101,58,32,84,117,101,44,32,49,53,32,78,111,118,32,49,57,57,52,32,48,56,58,49,50,58,51,49,32,71,77,84,13,10,13,10,66,101,32,104,97,112,112,121,33]}");
+            "{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"raw_message\":null,\"message\":\"From: NoBody <nobody@domain.tld>\\r\\nReply-To: Yuin <yuin@domain.tld>\\r\\nTo: Hei <hei@domain.tld>\\r\\nSubject: Happy new year\\r\\nDate: Tue, 15 Nov 1994 08:12:31 GMT\\r\\n\\r\\nBe happy!\"}");
         remove_file(file).unwrap();
     }
 }
diff --git a/tests/transport_sendmail.rs b/tests/transport_sendmail.rs
index 447640c37..c3d626adf 100644
--- a/tests/transport_sendmail.rs
+++ b/tests/transport_sendmail.rs
@@ -14,7 +14,7 @@ mod test {
             .body("Be happy!")
             .unwrap();
 
-        let result = sender.send(email);
+        let result = sender.send(&email);
         println!("{:?}", result);
         assert!(result.is_ok());
     }
diff --git a/tests/transport_smtp.rs b/tests/transport_smtp.rs
index a830f9cd6..d0d6ac7df 100644
--- a/tests/transport_smtp.rs
+++ b/tests/transport_smtp.rs
@@ -15,7 +15,7 @@ mod test {
         SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
             .unwrap()
             .transport()
-            .send(email)
+            .send(&email)
             .unwrap();
     }
 }
diff --git a/tests/transport_stub.rs b/tests/transport_stub.rs
index ddd172375..0cfce4201 100644
--- a/tests/transport_stub.rs
+++ b/tests/transport_stub.rs
@@ -12,6 +12,6 @@ fn stub_transport() {
         .body("Be happy!")
         .unwrap();
 
-    sender_ok.send(email.clone()).unwrap();
-    sender_ko.send(email).unwrap_err();
+    sender_ok.send(&email.clone()).unwrap();
+    sender_ko.send(&email).unwrap_err();
 }