Repository: lettre/lettre (language: rust)

Issue:
8bit encoding behaves incorrectly
Hi there :wave: I've been working to update Hagrid to lettre 0.10.0-alpha3. It has mostly been a smooth experience, in particular the builder API is much nicer now than it was before. Good job on that :+1: 

However :)

**Describe the bug**
The 8bit encoder indiscriminately inserts a linebreak `\r\n` every 998 bytes, regardless of existing newlines or charset. This can easily be observed [in the code](https://github.com/lettre/lettre/blob/b187885e70af400d50e3200390306b9bd2109675/src/message/encoder.rs#L90).

Adding line breaks without considering the encoding changes the content in subtle ways. In most cases it'll be a benign extra linebreak in the mail, but there are more problematic scenarios. In particular, adding a linebreak in the middle of a multi-byte character will break the entire encoding.

I noticed this when an email test case of mine was encoded as a `raw_message` rather than `message` by FileTransport. This was caused by a Japanese character ら placed on a 998 byte boundary, and its utf8 encoded form `e3 82 89` becoming `e3 82 0d 0a 89`.

**To Reproduce**
```
    #[test]
    fn eight_bit_encode_long() {
        let mut c = EightBitCodec::new();

        assert_eq!(
            &String::from_utf8(c.encode(b"Hello, worldaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!")).unwrap(),
            "Hello, worldaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!"
        );
    }
```

**Expected behavior**
I'm not sure what the *correct* behavior here would be. The 8bit encoder as is cannot actually decide where to insert line breaks, since that depends on the encoded charset. I can think of three ways to improve the situation:

* make the encoder a no-op, and leave 8bit line length up to the api user. this will lead to "malformed mails that probably still work" situations, but matches real world requirements most easily.
* throw an error when a line longer than 998 characters is encountered. simple to implement, ensures that outgoing mail is spec-compliant, but also gets in the way of what users want to do?
* make the encoder charset-sensitive, and insert line breaks before multi-byte characters. In practice this will only work for utf8, but that's probably the 99.9% case anyways.

**Environment:**
- 0.10.0-alpha.3


Discussion / hints:
Thanks for testing the alpha version. Looks like as part of #495 we'll have to decide what's the best way of fixing this.
In the meantime you should be able to work around the issue by using quoted printable or base64

Fix patch:
diff --git a/examples/basic_html.rs b/examples/basic_html.rs
index 19aca6b66..7fd5261ab 100644
--- a/examples/basic_html.rs
+++ b/examples/basic_html.rs
@@ -26,18 +26,18 @@ fn main() {
         .multipart(
             MultiPart::alternative() // This is composed of two parts.
                 .singlepart(
-                    SinglePart::eight_bit()
+                    SinglePart::builder()
                         .header(header::ContentType(
                             "text/plain; charset=utf8".parse().unwrap(),
                         ))
-                        .body("Hello from Lettre! A mailer library for Rust"), // Every message should have a plain text fallback.
+                        .body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
                 )
                 .singlepart(
-                    SinglePart::quoted_printable()
+                    SinglePart::builder()
                         .header(header::ContentType(
                             "text/html; charset=utf8".parse().unwrap(),
                         ))
-                        .body(html),
+                        .body(String::from(html)),
                 ),
         )
         .expect("failed to build email");
diff --git a/examples/maud_html.rs b/examples/maud_html.rs
index 02ffcebeb..ec9291609 100644
--- a/examples/maud_html.rs
+++ b/examples/maud_html.rs
@@ -35,14 +35,14 @@ fn main() {
         .multipart(
             MultiPart::alternative() // This is composed of two parts.
                 .singlepart(
-                    SinglePart::eight_bit()
+                    SinglePart::builder()
                         .header(header::ContentType(
                             "text/plain; charset=utf8".parse().unwrap(),
                         ))
-                        .body("Hello from Lettre! A mailer library for Rust"), // Every message should have a plain text fallback.
+                        .body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
                 )
                 .singlepart(
-                    SinglePart::quoted_printable()
+                    SinglePart::builder()
                         .header(header::ContentType(
                             "text/html; charset=utf8".parse().unwrap(),
                         ))
diff --git a/src/message/body.rs b/src/message/body.rs
new file mode 100644
index 000000000..91961844c
--- /dev/null
+++ b/src/message/body.rs
@@ -0,0 +1,408 @@
+use std::borrow::Cow;
+use std::io::{self, Write};
+
+use crate::message::header::ContentTransferEncoding;
+
+/// A [`SinglePart`][super::SinglePart] body.
+#[derive(Debug, Clone)]
+pub struct Body(BodyInner);
+
+#[derive(Debug, Clone)]
+enum BodyInner {
+    Binary(Vec<u8>),
+    String(String),
+}
+
+impl Body {
+    /// Returns the length of this `Body` in bytes.
+    #[inline]
+    pub fn len(&self) -> usize {
+        match &self.0 {
+            BodyInner::Binary(b) => b.len(),
+            BodyInner::String(s) => s.len(),
+        }
+    }
+
+    /// Returns `true` if this `Body` has a length of zero, `false` otherwise.
+    #[inline]
+    pub fn is_empty(&self) -> bool {
+        match &self.0 {
+            BodyInner::Binary(b) => b.is_empty(),
+            BodyInner::String(s) => s.is_empty(),
+        }
+    }
+
+    /// Suggests the best `Content-Transfer-Encoding` to be used for this `Body`
+    ///
+    /// If the `Body` was created from a `String` composed only of US-ASCII
+    /// characters, with no lines longer than 1000 characters, then 7bit
+    /// encoding will be used, else quoted-printable will be choosen.
+    ///
+    /// If the `Body` was instead created from a `Vec<u8>`, base64 encoding is always
+    /// choosen.
+    ///
+    /// `8bit` and `binary` encodings are never returned, as they may not be
+    /// supported by all SMTP servers.
+    pub fn encoding(&self) -> ContentTransferEncoding {
+        match &self.0 {
+            BodyInner::String(s) if is_7bit_encoded(s.as_ref()) => {
+                ContentTransferEncoding::SevenBit
+            }
+            // TODO: consider when base64 would be a better option because of output size
+            BodyInner::String(_) => ContentTransferEncoding::QuotedPrintable,
+            BodyInner::Binary(_) => ContentTransferEncoding::Base64,
+        }
+    }
+
+    /// Encodes this `Body` using the choosen `encoding`.
+    ///
+    /// # Panic
+    ///
+    /// Panics if the choosen `Content-Transfer-Encoding` would end-up
+    /// creating an incorrectly encoded email.
+    ///
+    /// Could happen for example if `7bit` encoding is choosen when the
+    /// content isn't US-ASCII or contains lines longer than 1000 characters.
+    ///
+    /// Never panics when using an `encoding` returned by [`encoding`][Body::encoding].
+    pub fn encode(&self, encoding: ContentTransferEncoding) -> Cow<'_, Body> {
+        match encoding {
+            ContentTransferEncoding::SevenBit => {
+                assert!(
+                    is_7bit_encoded(self.as_ref()),
+                    "Body isn't valid 7bit content"
+                );
+
+                Cow::Borrowed(self)
+            }
+            ContentTransferEncoding::EightBit => {
+                assert!(
+                    is_8bit_encoded(self.as_ref()),
+                    "Body isn't valid 8bit content"
+                );
+
+                Cow::Borrowed(self)
+            }
+            ContentTransferEncoding::Binary => Cow::Borrowed(self),
+            ContentTransferEncoding::QuotedPrintable => {
+                let encoded = quoted_printable::encode_to_str(self);
+                Cow::Owned(Body(BodyInner::String(encoded)))
+            }
+            ContentTransferEncoding::Base64 => {
+                let base64_len = self.len() * 4 / 3 + 4;
+                let base64_endings_len = base64_len + base64_len / LINE_MAX_LENGTH;
+
+                let mut out = Vec::with_capacity(base64_endings_len);
+                {
+                    let writer = LineWrappingWriter::new(&mut out, LINE_MAX_LENGTH);
+                    let mut writer = base64::write::EncoderWriter::new(writer, base64::STANDARD);
+
+                    // TODO: use writer.write_all(self.as_ref()).expect("base64 encoding never fails");
+
+                    // modified Write::write_all to work around base64 crate bug
+                    // TODO: remove once https://github.com/marshallpierce/rust-base64/issues/148 is fixed
+                    {
+                        let mut buf: &[u8] = self.as_ref();
+                        while !buf.is_empty() {
+                            match writer.write(buf) {
+                                Ok(0) => {
+                                    // ignore 0 writes
+                                }
+                                Ok(n) => {
+                                    buf = &buf[n..];
+                                }
+                                Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
+                                Err(e) => panic!("base64 encoding never fails: {}", e),
+                            }
+                        }
+                    }
+                }
+
+                Cow::Owned(Body(BodyInner::Binary(out)))
+            }
+        }
+    }
+}
+
+impl From<Vec<u8>> for Body {
+    #[inline]
+    fn from(b: Vec<u8>) -> Self {
+        Self(BodyInner::Binary(b))
+    }
+}
+
+impl From<String> for Body {
+    #[inline]
+    fn from(s: String) -> Self {
+        Self(BodyInner::String(s))
+    }
+}
+
+impl AsRef<[u8]> for Body {
+    #[inline]
+    fn as_ref(&self) -> &[u8] {
+        match &self.0 {
+            BodyInner::Binary(b) => b.as_ref(),
+            BodyInner::String(s) => s.as_ref(),
+        }
+    }
+}
+
+/// Checks whether it contains only US-ASCII characters,
+/// and no lines are longer than 1000 characters including the `\n` character.
+///
+/// Most efficient content encoding available
+pub(crate) fn is_7bit_encoded(buf: &[u8]) -> bool {
+    buf.is_ascii() && !contains_too_long_lines(buf)
+}
+
+/// Checks that no lines are longer than 1000 characters,
+/// including the `\n` character.
+/// NOTE: 8bit isn't supported by all SMTP servers.
+fn is_8bit_encoded(buf: &[u8]) -> bool {
+    !contains_too_long_lines(buf)
+}
+
+/// Checks if there are lines that are longer than 1000 characters,
+/// including the `\n` character.
+fn contains_too_long_lines(buf: &[u8]) -> bool {
+    buf.len() > 1000 && buf.split(|&b| b == b'\n').any(|line| line.len() > 999)
+}
+
+const LINE_SEPARATOR: &[u8] = b"\r\n";
+const LINE_MAX_LENGTH: usize = 78 - LINE_SEPARATOR.len();
+
+/// A `Write`r that inserts a line separator `\r\n` every `max_line_length` bytes.
+struct LineWrappingWriter<'a, W> {
+    writer: &'a mut W,
+    current_line_length: usize,
+    max_line_length: usize,
+}
+
+impl<'a, W> LineWrappingWriter<'a, W> {
+    pub fn new(writer: &'a mut W, max_line_length: usize) -> Self {
+        Self {
+            writer,
+            current_line_length: 0,
+            max_line_length,
+        }
+    }
+}
+
+impl<'a, W> Write for LineWrappingWriter<'a, W>
+where
+    W: Write,
+{
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        let remaining_line_len = self.max_line_length - self.current_line_length;
+        let write_len = std::cmp::min(buf.len(), remaining_line_len);
+
+        self.writer.write_all(&buf[..write_len])?;
+
+        if remaining_line_len == write_len {
+            self.writer.write_all(LINE_SEPARATOR)?;
+
+            self.current_line_length = 0;
+        } else {
+            self.current_line_length += write_len;
+        }
+
+        Ok(write_len)
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        self.writer.flush()
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::{Body, ContentTransferEncoding};
+
+    #[test]
+    fn seven_bit_detect() {
+        let input = Body::from(String::from("Hello, world!"));
+
+        let encoding = input.encoding();
+        assert_eq!(encoding, ContentTransferEncoding::SevenBit);
+    }
+
+    #[test]
+    fn seven_bit_encode() {
+        let input = Body::from(String::from("Hello, world!"));
+
+        let output = input.encode(ContentTransferEncoding::SevenBit);
+        assert_eq!(output.as_ref().as_ref(), b"Hello, world!");
+    }
+
+    #[test]
+    fn seven_bit_too_long_detect() {
+        let input = Body::from("Hello, world!".repeat(100));
+
+        let encoding = input.encoding();
+        assert_eq!(encoding, ContentTransferEncoding::QuotedPrintable);
+    }
+
+    #[test]
+    #[should_panic]
+    fn seven_bit_too_long_fail() {
+        let input = Body::from("Hello, world!".repeat(100));
+
+        let _ = input.encode(ContentTransferEncoding::SevenBit);
+    }
+
+    #[test]
+    fn seven_bit_too_long_encode_quotedprintable() {
+        let input = Body::from("Hello, world!".repeat(100));
+
+        let output = input.encode(ContentTransferEncoding::QuotedPrintable);
+        assert_eq!(
+            output.as_ref().as_ref(),
+            concat!(
+                "Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
+                "ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
+                "world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
+                "o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
+                "ello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, worl=\r\n",
+                "d!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, w=\r\n",
+                "orld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello=\r\n",
+                ", world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!He=\r\n",
+                "llo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world=\r\n",
+                "!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wo=\r\n",
+                "rld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello,=\r\n",
+                " world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hel=\r\n",
+                "lo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!=\r\n",
+                "Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
+                "ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
+                "world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
+                "o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
+                "ello, world!Hello, world!"
+            )
+            .as_bytes()
+        );
+    }
+
+    #[test]
+    #[should_panic]
+    fn seven_bit_invalid() {
+        let input = Body::from(String::from("Привет, мир!"));
+
+        let _ = input.encode(ContentTransferEncoding::SevenBit);
+    }
+
+    #[test]
+    fn eight_bit_encode() {
+        let input = Body::from(String::from("Привет, мир!"));
+
+        let out = input.encode(ContentTransferEncoding::EightBit);
+        assert_eq!(out.as_ref().as_ref(), "Привет, мир!".as_bytes());
+    }
+
+    #[test]
+    #[should_panic]
+    fn eight_bit_too_long_fail() {
+        let input = Body::from("Привет, мир!".repeat(200));
+
+        let _ = input.encode(ContentTransferEncoding::EightBit);
+    }
+
+    #[test]
+    fn quoted_printable_detect() {
+        let input = Body::from(String::from("Привет, мир!"));
+
+        let encoding = input.encoding();
+        assert_eq!(encoding, ContentTransferEncoding::QuotedPrintable);
+    }
+
+    #[test]
+    fn quoted_printable_encode_ascii() {
+        let input = Body::from(String::from("Hello, world!"));
+
+        let output = input.encode(ContentTransferEncoding::QuotedPrintable);
+        assert_eq!(output.as_ref().as_ref(), b"Hello, world!");
+    }
+
+    #[test]
+    fn quoted_printable_encode_utf8() {
+        let input = Body::from(String::from("Привет, мир!"));
+
+        let output = input.encode(ContentTransferEncoding::QuotedPrintable);
+        assert_eq!(
+            output.as_ref().as_ref(),
+            b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
+        );
+    }
+
+    #[test]
+    fn quoted_printable_encode_line_wrap() {
+        let input = Body::from(String::from("Текст письма в уникоде"));
+
+        let output = input.encode(ContentTransferEncoding::QuotedPrintable);
+        assert_eq!(
+            output.as_ref().as_ref(),
+            concat!(
+                "=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"
+            )
+            .as_bytes()
+        );
+    }
+
+    #[test]
+    fn base64_detect() {
+        let input = Body::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
+        let encoding = input.encoding();
+        assert_eq!(encoding, ContentTransferEncoding::Base64);
+    }
+
+    #[test]
+    fn base64_encode_bytes() {
+        let input = Body::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
+
+        let output = input.encode(ContentTransferEncoding::Base64);
+        assert_eq!(output.as_ref().as_ref(), b"AAECAwQFBgcICQ==");
+    }
+
+    #[test]
+    fn base64_encode_bytes_wrapping() {
+        let input = Body::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20));
+
+        let output = input.encode(ContentTransferEncoding::Base64);
+        assert_eq!(
+            output.as_ref().as_ref(),
+            concat!(
+                "AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUG\r\n",
+                "BwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQID\r\n",
+                "BAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkA\r\n",
+                "AQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAk="
+            )
+            .as_bytes()
+        );
+    }
+
+    #[test]
+    fn base64_encode_ascii() {
+        let input = Body::from(String::from("Hello World!"));
+
+        let output = input.encode(ContentTransferEncoding::Base64);
+        assert_eq!(output.as_ref().as_ref(), b"SGVsbG8gV29ybGQh");
+    }
+
+    #[test]
+    fn base64_encode_ascii_wrapping() {
+        let input = Body::from("Hello World!".repeat(20));
+
+        let output = input.encode(ContentTransferEncoding::Base64);
+        assert_eq!(
+            output.as_ref().as_ref(),
+            concat!(
+                "SGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29y\r\n",
+                "bGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8g\r\n",
+                "V29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVs\r\n",
+                "bG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQh\r\n",
+                "SGVsbG8gV29ybGQh"
+            )
+            .as_bytes()
+        );
+    }
+}
diff --git a/src/message/encoder.rs b/src/message/encoder.rs
deleted file mode 100644
index 5e47d4516..000000000
--- a/src/message/encoder.rs
+++ /dev/null
@@ -1,247 +0,0 @@
-use crate::message::header::ContentTransferEncoding;
-
-/// Encoder trait
-pub trait EncoderCodec: Send {
-    /// Encode all data
-    fn encode(&mut self, input: &[u8]) -> Vec<u8>;
-}
-
-/// 7bit codec
-///
-/// WARNING: Panics when passed non-ascii chars
-struct SevenBitCodec {
-    line_wrapper: EightBitCodec,
-}
-
-impl SevenBitCodec {
-    pub fn new() -> Self {
-        SevenBitCodec {
-            line_wrapper: EightBitCodec::new(),
-        }
-    }
-}
-
-impl EncoderCodec for SevenBitCodec {
-    fn encode(&mut self, input: &[u8]) -> Vec<u8> {
-        assert!(input.is_ascii(), "input must be valid ascii");
-
-        self.line_wrapper.encode(input)
-    }
-}
-
-/// Quoted-Printable codec
-///
-struct QuotedPrintableCodec();
-
-impl QuotedPrintableCodec {
-    pub fn new() -> Self {
-        QuotedPrintableCodec()
-    }
-}
-
-impl EncoderCodec for QuotedPrintableCodec {
-    fn encode(&mut self, input: &[u8]) -> Vec<u8> {
-        quoted_printable::encode(input)
-    }
-}
-
-/// Base64 codec
-///
-struct Base64Codec {
-    line_wrapper: EightBitCodec,
-}
-
-impl Base64Codec {
-    pub fn new() -> Self {
-        Base64Codec {
-            // TODO probably 78, 76 is for qp
-            line_wrapper: EightBitCodec::new().with_limit(78 - 2),
-        }
-    }
-}
-
-impl EncoderCodec for Base64Codec {
-    fn encode(&mut self, input: &[u8]) -> Vec<u8> {
-        self.line_wrapper.encode(base64::encode(input).as_bytes())
-    }
-}
-
-/// 8bit codec
-///
-struct EightBitCodec {
-    max_length: usize,
-}
-
-const DEFAULT_MAX_LINE_LENGTH: usize = 1000 - 2;
-
-impl EightBitCodec {
-    pub fn new() -> Self {
-        EightBitCodec {
-            max_length: DEFAULT_MAX_LINE_LENGTH,
-        }
-    }
-
-    pub fn with_limit(mut self, max_length: usize) -> Self {
-        self.max_length = max_length;
-        self
-    }
-}
-
-impl EncoderCodec for EightBitCodec {
-    fn encode(&mut self, input: &[u8]) -> Vec<u8> {
-        let ending = b"\r\n";
-        let endings_len = input.len() / self.max_length * ending.len();
-        let mut out = Vec::with_capacity(input.len() + endings_len);
-
-        for chunk in input.chunks(self.max_length) {
-            // write the line ending after every chunk, except the last one
-            if !out.is_empty() {
-                out.extend_from_slice(ending);
-            }
-
-            out.extend_from_slice(chunk);
-        }
-
-        out
-    }
-}
-
-/// Binary codec
-///
-struct BinaryCodec;
-
-impl BinaryCodec {
-    pub fn new() -> Self {
-        BinaryCodec
-    }
-}
-
-impl EncoderCodec for BinaryCodec {
-    fn encode(&mut self, input: &[u8]) -> Vec<u8> {
-        input.into()
-    }
-}
-
-pub fn codec(encoding: Option<&ContentTransferEncoding>) -> Box<dyn EncoderCodec> {
-    use self::ContentTransferEncoding::*;
-
-    match encoding {
-        Some(SevenBit) => Box::new(SevenBitCodec::new()),
-        Some(QuotedPrintable) => Box::new(QuotedPrintableCodec::new()),
-        Some(Base64) => Box::new(Base64Codec::new()),
-        Some(EightBit) => Box::new(EightBitCodec::new()),
-        Some(Binary) | None => Box::new(BinaryCodec::new()),
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use super::*;
-
-    #[test]
-    fn seven_bit_encode() {
-        let mut c = SevenBitCodec::new();
-
-        assert_eq!(
-            &String::from_utf8(c.encode(b"Hello, world!")).unwrap(),
-            "Hello, world!"
-        );
-    }
-
-    #[test]
-    #[should_panic]
-    fn seven_bit_encode_panic() {
-        let mut c = SevenBitCodec::new();
-        c.encode("Hello, мир!".as_bytes());
-    }
-
-    #[test]
-    fn quoted_printable_encode() {
-        let mut c = QuotedPrintableCodec::new();
-
-        assert_eq!(
-            &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!(&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]
-    fn base64_encode() {
-        let mut c = Base64Codec::new();
-
-        assert_eq!(
-            &String::from_utf8(c.encode("Привет, мир!".as_bytes())).unwrap(),
-            "0J/RgNC40LLQtdGCLCDQvNC40YAh"
-        );
-
-        assert_eq!(
-            &String::from_utf8(c.encode("Текст письма в уникоде подлиннее.".as_bytes())).unwrap(),
-            concat!(
-                "0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQ",
-                "vtC00LUg0L/QvtC00LvQuNC90L3Q\r\ntdC1Lg=="
-            )
-        );
-
-        assert_eq!(
-            &String::from_utf8(c.encode(
-                "Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую.".as_bytes()
-                    )).unwrap(),
-
-                concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
-                        "0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
-                        "viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
-                        "udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRji4=")
-        );
-        assert_eq!(
-            &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")
-        );
-    }
-
-    #[test]
-    fn base64_encodeed() {
-        let mut c = Base64Codec::new();
-
-        assert_eq!(&String::from_utf8(c.encode(b"Chunk.")).unwrap(), "Q2h1bmsu");
-    }
-
-    #[test]
-    fn eight_bit_encode() {
-        let mut c = EightBitCodec::new();
-
-        assert_eq!(
-            &String::from_utf8(c.encode(b"Hello, world!")).unwrap(),
-            "Hello, world!"
-        );
-
-        assert_eq!(
-            &String::from_utf8(c.encode("Hello, мир!".as_bytes())).unwrap(),
-            "Hello, мир!"
-        );
-    }
-
-    #[test]
-    fn binary_encode() {
-        let mut c = BinaryCodec::new();
-
-        assert_eq!(
-            &String::from_utf8(c.encode(b"Hello, world!")).unwrap(),
-            "Hello, world!"
-        );
-
-        assert_eq!(
-            &String::from_utf8(c.encode("Hello, мир!".as_bytes())).unwrap(),
-            "Hello, мир!"
-        );
-    }
-}
diff --git a/src/message/header/content.rs b/src/message/header/content.rs
index e74fd570c..866c6c98b 100644
--- a/src/message/header/content.rs
+++ b/src/message/header/content.rs
@@ -21,7 +21,7 @@ pub enum ContentTransferEncoding {
 
 impl Default for ContentTransferEncoding {
     fn default() -> Self {
-        ContentTransferEncoding::SevenBit
+        ContentTransferEncoding::Base64
     }
 }
 
diff --git a/src/message/mimebody.rs b/src/message/mimebody.rs
index 0832aa32b..814eae2a4 100644
--- a/src/message/mimebody.rs
+++ b/src/message/mimebody.rs
@@ -1,7 +1,6 @@
 use crate::message::{
-    encoder::codec,
     header::{ContentTransferEncoding, ContentType, Header, Headers},
-    EmailFormat,
+    Body, EmailFormat,
 };
 use mime::Mime;
 use rand::Rng;
@@ -69,7 +68,7 @@ impl SinglePartBuilder {
     }
 
     /// Build singlepart using body
-    pub fn body<T: Into<Vec<u8>>>(self, body: T) -> SinglePart {
+    pub fn body<T: Into<Body>>(self, body: T) -> SinglePart {
         SinglePart {
             headers: self.headers,
             body: body.into(),
@@ -94,8 +93,7 @@ impl Default for SinglePartBuilder {
 /// # fn main() -> Result<(), Box<dyn Error>> {
 /// let part = SinglePart::builder()
 ///      .header(header::ContentType("text/plain; charset=utf8".parse()?))
-///      .header(header::ContentTransferEncoding::Binary)
-///      .body("Текст письма в уникоде");
+///      .body(String::from("Текст письма в уникоде"));
 /// # Ok(())
 /// # }
 /// ```
@@ -103,61 +101,84 @@ impl Default for SinglePartBuilder {
 #[derive(Debug, Clone)]
 pub struct SinglePart {
     headers: Headers,
-    body: Vec<u8>,
+    body: Body,
 }
 
 impl SinglePart {
-    /// Creates a default builder for singlepart
+    /// Creates a builder for singlepart
+    #[inline]
     pub fn builder() -> SinglePartBuilder {
         SinglePartBuilder::new()
     }
 
-    /// Creates a singlepart builder with 7bit encoding
+    /// Creates a singlepart builder using 7bit encoding
     ///
-    /// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::SevenBit)`.
+    /// 7bit encoding is the most efficient encoding available,
+    /// but it requires the body only to consist of US-ASCII characters,
+    /// with no lines longer than 1000 characters.
+    ///
+    /// When in doubt use [`SinglePart::builder`] instead, which
+    /// chooses the best encoding based on the body.
+    ///
+    /// # Panics
+    ///
+    /// The above conditions are checked when the Body gets encoded
+    #[inline]
     pub fn seven_bit() -> SinglePartBuilder {
         Self::builder().header(ContentTransferEncoding::SevenBit)
     }
 
-    /// Creates a singlepart builder with quoted-printable encoding
+    /// Creates a singlepart builder using quoted-printable encoding
     ///
-    /// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::QuotedPrintable)`.
+    /// quoted-printable encoding should be used when the body may contain
+    /// lines longer than 1000 characters or a few non US-ASCII characters may be present.
+    /// If the body contains many non US-ASCII characters, [`SinglePart::base64`]
+    /// should be used instead, since it creates a smaller output compared
+    /// to quoted-printable.
+    ///
+    /// When in doubt use [`SinglePart::builder`] instead, which
+    /// chooses the best encoding based on the body.
+    #[inline]
     pub fn quoted_printable() -> SinglePartBuilder {
         Self::builder().header(ContentTransferEncoding::QuotedPrintable)
     }
 
-    /// Creates a singlepart builder with base64 encoding
+    /// Creates a singlepart builder using base64 encoding
+    ///
+    /// base64 encoding must be used when the body is composed of mainly
+    /// non-text data.
     ///
-    /// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Base64)`.
+    /// When in doubt use [`SinglePart::builder`] instead, which
+    /// chooses the best encoding based on the body.
+    #[inline]
     pub fn base64() -> SinglePartBuilder {
         Self::builder().header(ContentTransferEncoding::Base64)
     }
 
-    /// Creates a singlepart builder with 8-bit encoding
+    /// Creates a singlepart builder using 8bit encoding
     ///
-    /// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::EightBit)`.
+    /// Some SMTP servers might not support 8bit bodies.
+    ///
+    /// When in doubt use [`SinglePart::builder`] instead, which
+    /// chooses the best encoding based on the body.
+    #[inline]
     pub fn eight_bit() -> SinglePartBuilder {
         Self::builder().header(ContentTransferEncoding::EightBit)
     }
 
-    /// Creates a singlepart builder with binary encoding
-    ///
-    /// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Binary)`.
-    pub fn binary() -> SinglePartBuilder {
-        Self::builder().header(ContentTransferEncoding::Binary)
-    }
-
     /// Get the headers from singlepart
+    #[inline]
     pub fn headers(&self) -> &Headers {
         &self.headers
     }
 
     /// Read the body from singlepart
-    pub fn body_ref(&self) -> &[u8] {
+    #[inline]
+    pub fn body(&self) -> &Body {
         &self.body
     }
 
-    /// Get message content formatted for SMTP
+    /// Get message content formatted for sending
     pub fn formatted(&self) -> Vec<u8> {
         let mut out = Vec::new();
         self.format(&mut out);
@@ -170,10 +191,14 @@ impl EmailFormat for SinglePart {
         out.extend_from_slice(self.headers.to_string().as_bytes());
         out.extend_from_slice(b"\r\n");
 
-        let encoding = self.headers.get::<ContentTransferEncoding>();
-        let mut encoder = codec(encoding);
+        let encoding = self
+            .headers
+            .get::<ContentTransferEncoding>()
+            .copied()
+            .unwrap_or_else(|| self.body.encoding());
+        let encoded = self.body.encode(encoding);
 
-        out.extend_from_slice(&encoder.encode(&self.body));
+        out.extend_from_slice(encoded.as_ref().as_ref());
         out.extend_from_slice(b"\r\n");
     }
 }
diff --git a/src/message/mod.rs b/src/message/mod.rs
index e2e1bfbb1..67640bf13 100644
--- a/src/message/mod.rs
+++ b/src/message/mod.rs
@@ -59,9 +59,8 @@
 //!     .singlepart(
 //!         SinglePart::builder()
 //!             .header(header::ContentType(
-//!                 "text/plain; charset=utf8".parse()?,
-//!             )).header(header::ContentTransferEncoding::QuotedPrintable)
-//!             .body("Привет, мир!"),
+//!                 "text/plain; charset=utf8".parse()?))
+//!             .body(String::from("Привет, мир!")),
 //!     )?;
 //! # Ok(())
 //! # }
@@ -101,31 +100,31 @@
 //!         .multipart(
 //!             MultiPart::alternative()
 //!             .singlepart(
-//!                 SinglePart::quoted_printable()
+//!                 SinglePart::builder()
 //!                 .header(header::ContentType("text/plain; charset=utf8".parse()?))
-//!                 .body("Hello, world! :)")
+//!                 .body(String::from("Hello, world! :)"))
 //!             )
 //!             .multipart(
 //!                MultiPart::related()
 //!                 .singlepart(
-//!                     SinglePart::eight_bit()
+//!                     SinglePart::builder()
 //!                     .header(header::ContentType("text/html; charset=utf8".parse()?))
-//!                     .body("<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>")
+//!                     .body(String::from("<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>"))
 //!                 )
 //!                 .singlepart(
-//!                     SinglePart::base64()
+//!                     SinglePart::builder()
 //!                     .header(header::ContentType("image/png".parse()?))
 //!                     .header(header::ContentDisposition {
 //!                         disposition: header::DispositionType::Inline,
 //!                         parameters: vec![],
 //!                     })
 //!                     .header(header::ContentId("<123>".into()))
-//!                     .body("<smile-raw-image-data>")
+//!                     .body(Vec::<u8>::from("<smile-raw-image-data>"))
 //!                 )
 //!             )
 //!         )
 //!         .singlepart(
-//!             SinglePart::seven_bit()
+//!             SinglePart::builder()
 //!             .header(header::ContentType("text/plain; charset=utf8".parse()?))
 //!             .header(header::ContentDisposition {
 //!                 disposition: header::DispositionType::Attachment,
@@ -136,7 +135,7 @@
 //!                     )
 //!                 ]
 //!             })
-//!             .body("int main() { return 0; }")
+//!             .body(String::from("int main() { return 0; }"))
 //!         )
 //!     )?;
 //! # Ok(())
@@ -185,12 +184,13 @@
 //!
 //! ```
 
+pub use body::Body;
 pub use mailbox::*;
 pub use mimebody::*;
 
 pub use mime;
 
-mod encoder;
+mod body;
 pub mod header;
 mod mailbox;
 mod mimebody;
@@ -375,7 +375,7 @@ impl MessageBuilder {
     // TODO: High-level methods for attachments and embedded files
 
     /// Create message from body
-    fn build(self, body: Body) -> Result<Message, EmailError> {
+    fn build(self, body: MessageBody) -> Result<Message, EmailError> {
         // Check for missing required headers
         // https://tools.ietf.org/html/rfc5322#section-3.6
 
@@ -412,30 +412,31 @@ impl MessageBuilder {
 
     // In theory having a body is optional
 
-    /// Plain ASCII body
+    /// Plain US-ASCII body
+    ///
+    /// Fails if is contains non ASCII characters or if it
+    /// contains lines longer than 1000 characters, including
+    /// the `\n` character.
     ///
     /// *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() {
+        if !self::body::is_7bit_encoded(body.as_ref()) {
             return Err(EmailError::NonAsciiChars);
         }
 
-        self.build(Body::Raw(body))
+        self.build(MessageBody::Raw(body))
     }
 
     /// Create message using mime body ([`MultiPart`][self::MultiPart])
     pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
-        self.mime_1_0().build(Body::Mime(Part::Multi(part)))
+        self.mime_1_0().build(MessageBody::Mime(Part::Multi(part)))
     }
 
     /// Create message using mime body ([`SinglePart`][self::SinglePart])
     pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
-        self.mime_1_0().build(Body::Mime(Part::Single(part)))
+        self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
     }
 }
 
@@ -443,12 +444,12 @@ impl MessageBuilder {
 #[derive(Clone, Debug)]
 pub struct Message {
     headers: Headers,
-    body: Body,
+    body: MessageBody,
     envelope: Envelope,
 }
 
 #[derive(Clone, Debug)]
-enum Body {
+enum MessageBody {
     Mime(Part),
     Raw(String),
 }
@@ -481,8 +482,8 @@ 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) => {
+            MessageBody::Mime(p) => p.format(out),
+            MessageBody::Raw(r) => {
                 out.extend_from_slice(b"\r\n");
                 out.extend(r.as_bytes())
             }
@@ -573,7 +574,9 @@ mod test {
                             .header(header::ContentType(
                                 "text/html; charset=utf8".parse().unwrap(),
                             ))
-                            .body("<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>"),
+                            .body(String::from(
+                                "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
+                            )),
                     )
                     .singlepart(
                         SinglePart::base64()


Test patch: