diff --git a/examples/basic_html.rs b/examples/basic_html.rs
index 19aca6b..7fd5261 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 02ffceb..ec92916 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 0000000..9196184
--- /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),
+ 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`, 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> for Body {
+ #[inline]
+ fn from(b: Vec) -> Self {
+ Self(BodyInner::Binary(b))
+ }
+}
+
+impl From 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 {
+ 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 5e47d45..0000000
--- 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;
-}
-
-/// 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 {
- 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 {
- 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 {
- 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 {
- 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 {
- input.into()
- }
-}
-
-pub fn codec(encoding: Option<&ContentTransferEncoding>) -> Box {
- 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/mimebody.rs b/src/message/mimebody.rs
index 0832aa3..814eae2 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>>(self, body: T) -> SinglePart {
+ pub fn body>(self, body: T) -> SinglePart {
SinglePart {
headers: self.headers,
body: body.into(),
@@ -94,8 +93,7 @@ impl Default for SinglePartBuilder {
/// # fn main() -> Result<(), Box> {
/// 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,
+ 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
///
- /// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Base64)`.
+ /// base64 encoding must be used when the body is composed of mainly
+ /// non-text data.
+ ///
+ /// 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 {
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::();
- let mut encoder = codec(encoding);
+ let encoding = self
+ .headers
+ .get::()
+ .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 e2e1bfb..67640bf 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("Hello, world! 
")
+//! .body(String::from("Hello, world! 
"))
//! )
//! .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("")
+//! .body(Vec::::from(""))
//! )
//! )
//! )
//! .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 {
+ fn build(self, body: MessageBody) -> Result {
// 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>(self, body: T) -> Result {
- // 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 {
- 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 {
- 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) {
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("Hello, world! 
"),
+ .body(String::from(
+ "Hello, world! 
",
+ )),
)
.singlepart(
SinglePart::base64()