diff --git a/benches/transport_smtp.rs b/benches/transport_smtp.rs index 13b8684..9288807 100644 --- a/benches/transport_smtp.rs +++ b/benches/transport_smtp.rs @@ -13,7 +13,7 @@ fn bench_simple_send(c: &mut Criterion) { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new year") - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); let result = black_box(sender.send(&email)); assert!(result.is_ok()); @@ -32,7 +32,7 @@ fn bench_reuse_send(c: &mut Criterion) { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new year") - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); let result = black_box(sender.send(&email)); assert!(result.is_ok()); diff --git a/examples/smtp.rs b/examples/smtp.rs index e865e05..9314edb 100644 --- a/examples/smtp.rs +++ b/examples/smtp.rs @@ -8,7 +8,7 @@ fn main() { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new year") - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); // Open a local connection on port 25 diff --git a/examples/smtp_selfsigned.rs b/examples/smtp_selfsigned.rs index 8cf1927..378b032 100644 --- a/examples/smtp_selfsigned.rs +++ b/examples/smtp_selfsigned.rs @@ -14,7 +14,7 @@ fn main() { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new year") - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); // Use a custom certificate stored on disk to securely verify the server's certificate diff --git a/examples/smtp_starttls.rs b/examples/smtp_starttls.rs index 7a1c7e0..5b83705 100644 --- a/examples/smtp_starttls.rs +++ b/examples/smtp_starttls.rs @@ -8,7 +8,7 @@ fn main() { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new year") - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); diff --git a/examples/smtp_tls.rs b/examples/smtp_tls.rs index ca394f0..f77b5c2 100644 --- a/examples/smtp_tls.rs +++ b/examples/smtp_tls.rs @@ -8,7 +8,7 @@ fn main() { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new year") - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); diff --git a/examples/tokio02_smtp_starttls.rs b/examples/tokio02_smtp_starttls.rs index ca11f5a..37e2d3f 100644 --- a/examples/tokio02_smtp_starttls.rs +++ b/examples/tokio02_smtp_starttls.rs @@ -17,7 +17,7 @@ async fn main() { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new async year") - .body("Be happy with async!") + .body(String::from("Be happy with async!")) .unwrap(); let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); diff --git a/examples/tokio02_smtp_tls.rs b/examples/tokio02_smtp_tls.rs index 412f361..6d40c80 100644 --- a/examples/tokio02_smtp_tls.rs +++ b/examples/tokio02_smtp_tls.rs @@ -17,7 +17,7 @@ async fn main() { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new async year") - .body("Be happy with async!") + .body(String::from("Be happy with async!")) .unwrap(); let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); diff --git a/examples/tokio03_smtp_starttls.rs b/examples/tokio03_smtp_starttls.rs index 131e33b..4dc4e4b 100644 --- a/examples/tokio03_smtp_starttls.rs +++ b/examples/tokio03_smtp_starttls.rs @@ -17,7 +17,7 @@ async fn main() { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new async year") - .body("Be happy with async!") + .body(String::from("Be happy with async!")) .unwrap(); let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); diff --git a/examples/tokio03_smtp_tls.rs b/examples/tokio03_smtp_tls.rs index 344460a..8d13071 100644 --- a/examples/tokio03_smtp_tls.rs +++ b/examples/tokio03_smtp_tls.rs @@ -17,7 +17,7 @@ async fn main() { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new async year") - .body("Be happy with async!") + .body(String::from("Be happy with async!")) .unwrap(); let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); diff --git a/src/message/body.rs b/src/message/body.rs index 9196184..1d620e7 100644 --- a/src/message/body.rs +++ b/src/message/body.rs @@ -1,95 +1,79 @@ -use std::borrow::Cow; use std::io::{self, Write}; +use std::ops::Deref; use crate::message::header::ContentTransferEncoding; -/// A [`SinglePart`][super::SinglePart] body. +/// A [`Message`][super::Message] or [`SinglePart`][super::SinglePart] body. #[derive(Debug, Clone)] -pub struct Body(BodyInner); +pub struct Body { + buf: Vec, + encoding: ContentTransferEncoding, +} +/// Either a `Vec` or a `String`. +/// +/// If the content is valid utf-8 a `String` should be passed, as it +/// makes for a more efficient `Content-Transfer-Encoding` to be choosen. #[derive(Debug, Clone)] -enum BodyInner { +pub enum MaybeString { Binary(Vec), String(String), } impl Body { - /// Returns the length of this `Body` in bytes. + /// Encode the supplied `buf`, making it ready to be sent as a body. + /// + /// Automatically chooses the most efficient encoding between + /// `7bit`, `quoted-printable` and `base64`. + pub fn new>(buf: B) -> Self { + let buf: MaybeString = buf.into(); + + let encoding = buf.encoding(); + Self::new_impl(buf.into(), encoding) + } + + /// Encode the supplied `buf`, using the provided `encoding`. + /// + /// Generally [`Body::new`] should be used. + /// + /// Returns an [`Err`] with the supplied `buf` untouched in case + /// the choosen encoding wouldn't have worked. + pub fn new_with_encoding>( + buf: B, + encoding: ContentTransferEncoding, + ) -> Result> { + let buf: MaybeString = buf.into(); + + if !buf.is_encoding_ok(encoding) { + return Err(buf.into()); + } + + Ok(Self::new_impl(buf.into(), encoding)) + } + + /// Builds a new `Body` using a pre-encoded buffer. + /// + /// **Generally not you want.** + /// + /// `buf` shouldn't contain non-ascii characters, lines longer than 1000 characters or nul bytes. #[inline] - pub fn len(&self) -> usize { - match &self.0 { - BodyInner::Binary(b) => b.len(), - BodyInner::String(s) => s.len(), - } + pub fn dangerous_pre_encoded(buf: Vec, encoding: ContentTransferEncoding) -> Self { + Self { buf, encoding } } - /// 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> { + /// Encodes the supplied `buf` using the provided `encoding` + fn new_impl(buf: Vec, encoding: ContentTransferEncoding) -> Self { 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::SevenBit + | ContentTransferEncoding::EightBit + | ContentTransferEncoding::Binary => Self { buf, encoding }, ContentTransferEncoding::QuotedPrintable => { - let encoded = quoted_printable::encode_to_str(self); - Cow::Owned(Body(BodyInner::String(encoded))) + let encoded = quoted_printable::encode(buf); + + Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable) } ContentTransferEncoding::Base64 => { - let base64_len = self.len() * 4 / 3 + 4; + let base64_len = buf.len() * 4 / 3 + 4; let base64_endings_len = base64_len + base64_len / LINE_MAX_LENGTH; let mut out = Vec::with_capacity(base64_endings_len); @@ -102,7 +86,7 @@ impl Body { // 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(); + let mut buf: &[u8] = buf.as_ref(); while !buf.is_empty() { match writer.write(buf) { Ok(0) => { @@ -118,32 +102,145 @@ impl Body { } } - Cow::Owned(Body(BodyInner::Binary(out))) + Self::dangerous_pre_encoded(out, ContentTransferEncoding::Base64) + } + } + } + + /// Returns the length of this `Body` in bytes. + #[inline] + pub fn len(&self) -> usize { + self.buf.len() + } + + /// Returns `true` if this `Body` has a length of zero, `false` otherwise. + #[inline] + pub fn is_empty(&self) -> bool { + self.buf.is_empty() + } + + /// Returns the `Content-Transfer-Encoding` of this `Body`. + #[inline] + pub fn encoding(&self) -> ContentTransferEncoding { + self.encoding + } + + /// Consumes `Body` and returns the inner `Vec` + #[inline] + pub fn into_vec(self) -> Vec { + self.buf + } +} + +impl MaybeString { + /// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString` + /// + /// If the `MaybeString` 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 `MaybeString` 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 { + Self::String(s) if is_7bit_encoded(s.as_ref()) => ContentTransferEncoding::SevenBit, + // TODO: consider when base64 would be a better option because of output size + Self::String(_) => ContentTransferEncoding::QuotedPrintable, + Self::Binary(_) => ContentTransferEncoding::Base64, + } + } + + /// Returns whether the provided `encoding` would encode this `MaybeString` into + /// a valid email body. + fn is_encoding_ok(&self, encoding: ContentTransferEncoding) -> bool { + match encoding { + ContentTransferEncoding::SevenBit => is_7bit_encoded(&self), + ContentTransferEncoding::EightBit => is_8bit_encoded(&self), + ContentTransferEncoding::Binary => true, + ContentTransferEncoding::QuotedPrintable => { + // TODO: check + + true + } + ContentTransferEncoding::Base64 => { + // TODO: check + + true } } } } -impl From> for Body { - #[inline] - fn from(b: Vec) -> Self { - Self(BodyInner::Binary(b)) +/// A trait for [`MessageBuilder::body`][super::MessageBuilder::body] and +/// [`SinglePartBuilder::body`][super::SinglePartBuilder::body], +/// which can either take something that can be encoded into [`Body`] +/// or a pre-encoded [`Body`] +pub trait IntoBody { + fn into_body(self, encoding: Option) -> Body; +} + +impl IntoBody for T +where + T: Into, +{ + fn into_body(self, encoding: Option) -> Body { + match encoding { + Some(encoding) => Body::new_with_encoding(self, encoding).expect("invalid encoding"), + None => Body::new(self), + } } } -impl From for Body { - #[inline] - fn from(s: String) -> Self { - Self(BodyInner::String(s)) +impl IntoBody for Body { + fn into_body(self, encoding: Option) -> Body { + let _ = encoding; + + self } } 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(), + self.buf.as_ref() + } +} + +impl From> for MaybeString { + #[inline] + fn from(b: Vec) -> Self { + Self::Binary(b) + } +} + +impl From for MaybeString { + #[inline] + fn from(s: String) -> Self { + Self::String(s) + } +} + +impl From for Vec { + #[inline] + fn from(s: MaybeString) -> Self { + match s { + MaybeString::Binary(b) => b, + MaybeString::String(s) => s.into(), + } + } +} + +impl Deref for MaybeString { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &Self::Target { + match self { + Self::Binary(b) => b.as_ref(), + Self::String(s) => s.as_ref(), } } } @@ -152,7 +249,7 @@ impl AsRef<[u8]> for Body { /// 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 { +fn is_7bit_encoded(buf: &[u8]) -> bool { buf.is_ascii() && !contains_too_long_lines(buf) } @@ -221,43 +318,31 @@ mod test { #[test] fn seven_bit_detect() { - let input = Body::from(String::from("Hello, world!")); + let encoded = Body::new(String::from("Hello, world!")); - let encoding = input.encoding(); - assert_eq!(encoding, ContentTransferEncoding::SevenBit); + assert_eq!(encoded.encoding(), ContentTransferEncoding::SevenBit); + assert_eq!(encoded.as_ref(), b"Hello, world!"); } #[test] fn seven_bit_encode() { - let input = Body::from(String::from("Hello, world!")); + let encoded = Body::new_with_encoding( + String::from("Hello, world!"), + ContentTransferEncoding::SevenBit, + ) + .unwrap(); - let output = input.encode(ContentTransferEncoding::SevenBit); - assert_eq!(output.as_ref().as_ref(), b"Hello, world!"); + assert_eq!(encoded.encoding(), ContentTransferEncoding::SevenBit); + assert_eq!(encoded.as_ref(), b"Hello, world!"); } #[test] fn seven_bit_too_long_detect() { - let input = Body::from("Hello, world!".repeat(100)); + let encoded = Body::new("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!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable); assert_eq!( - output.as_ref().as_ref(), + encoded.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", @@ -283,63 +368,127 @@ mod test { } #[test] - #[should_panic] - fn seven_bit_invalid() { - let input = Body::from(String::from("Привет, мир!")); + fn seven_bit_too_long_fail() { + let result = Body::new_with_encoding( + "Hello, world!".repeat(100), + ContentTransferEncoding::SevenBit, + ); - let _ = input.encode(ContentTransferEncoding::SevenBit); + assert!(result.is_err()); + } + + #[test] + fn seven_bit_too_long_encode_quotedprintable() { + let encoded = Body::new_with_encoding( + "Hello, world!".repeat(100), + ContentTransferEncoding::QuotedPrintable, + ) + .unwrap(); + + assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable); + assert_eq!( + encoded.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] + fn seven_bit_invalid() { + let result = Body::new_with_encoding( + String::from("Привет, мир!"), + ContentTransferEncoding::SevenBit, + ); + + assert!(result.is_err()); } #[test] fn eight_bit_encode() { - let input = Body::from(String::from("Привет, мир!")); + let encoded = Body::new_with_encoding( + String::from("Привет, мир!"), + ContentTransferEncoding::EightBit, + ) + .unwrap(); - let out = input.encode(ContentTransferEncoding::EightBit); - assert_eq!(out.as_ref().as_ref(), "Привет, мир!".as_bytes()); + assert_eq!(encoded.encoding(), ContentTransferEncoding::EightBit); + assert_eq!(encoded.as_ref(), "Привет, мир!".as_bytes()); } #[test] - #[should_panic] fn eight_bit_too_long_fail() { - let input = Body::from("Привет, мир!".repeat(200)); + let result = Body::new_with_encoding( + "Привет, мир!".repeat(200), + ContentTransferEncoding::EightBit, + ); - let _ = input.encode(ContentTransferEncoding::EightBit); + assert!(result.is_err()); } #[test] fn quoted_printable_detect() { - let input = Body::from(String::from("Привет, мир!")); + let encoded = Body::new(String::from("Привет, мир!")); - let encoding = input.encoding(); - assert_eq!(encoding, ContentTransferEncoding::QuotedPrintable); + assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable); + assert_eq!( + encoded.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_ascii() { - let input = Body::from(String::from("Hello, world!")); + let encoded = Body::new_with_encoding( + String::from("Hello, world!"), + ContentTransferEncoding::QuotedPrintable, + ) + .unwrap(); - let output = input.encode(ContentTransferEncoding::QuotedPrintable); - assert_eq!(output.as_ref().as_ref(), b"Hello, world!"); + assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable); + assert_eq!(encoded.as_ref(), b"Hello, world!"); } #[test] fn quoted_printable_encode_utf8() { - let input = Body::from(String::from("Привет, мир!")); + let encoded = Body::new_with_encoding( + String::from("Привет, мир!"), + ContentTransferEncoding::QuotedPrintable, + ) + .unwrap(); - let output = input.encode(ContentTransferEncoding::QuotedPrintable); + assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable); assert_eq!( - output.as_ref().as_ref(), + encoded.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 encoded = Body::new(String::from("Текст письма в уникоде")); - let output = input.encode(ContentTransferEncoding::QuotedPrintable); + assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable); assert_eq!( - output.as_ref().as_ref(), + encoded.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" @@ -350,26 +499,34 @@ mod test { #[test] fn base64_detect() { - let input = Body::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + let input = Body::new(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 encoded = Body::new_with_encoding( + vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + ContentTransferEncoding::Base64, + ) + .unwrap(); - let output = input.encode(ContentTransferEncoding::Base64); - assert_eq!(output.as_ref().as_ref(), b"AAECAwQFBgcICQ=="); + assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64); + assert_eq!(encoded.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 encoded = Body::new_with_encoding( + vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20), + ContentTransferEncoding::Base64, + ) + .unwrap(); - let output = input.encode(ContentTransferEncoding::Base64); + assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64); assert_eq!( - output.as_ref().as_ref(), + encoded.as_ref(), concat!( "AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUG\r\n", "BwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQID\r\n", @@ -382,19 +539,25 @@ mod test { #[test] fn base64_encode_ascii() { - let input = Body::from(String::from("Hello World!")); + let encoded = Body::new_with_encoding( + String::from("Hello World!"), + ContentTransferEncoding::Base64, + ) + .unwrap(); - let output = input.encode(ContentTransferEncoding::Base64); - assert_eq!(output.as_ref().as_ref(), b"SGVsbG8gV29ybGQh"); + assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64); + assert_eq!(encoded.as_ref(), b"SGVsbG8gV29ybGQh"); } #[test] fn base64_encode_ascii_wrapping() { - let input = Body::from("Hello World!".repeat(20)); + let encoded = + Body::new_with_encoding("Hello World!".repeat(20), ContentTransferEncoding::Base64) + .unwrap(); - let output = input.encode(ContentTransferEncoding::Base64); + assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64); assert_eq!( - output.as_ref().as_ref(), + encoded.as_ref(), concat!( "SGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29y\r\n", "bGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8g\r\n", diff --git a/src/message/mimebody.rs b/src/message/mimebody.rs index 814eae2..38d6ca2 100644 --- a/src/message/mimebody.rs +++ b/src/message/mimebody.rs @@ -1,6 +1,6 @@ use crate::message::{ header::{ContentTransferEncoding, ContentType, Header, Headers}, - Body, EmailFormat, + EmailFormat, IntoBody, }; use mime::Mime; use rand::Rng; @@ -68,10 +68,15 @@ impl SinglePartBuilder { } /// Build singlepart using body - pub fn body>(self, body: T) -> SinglePart { + pub fn body(mut self, body: T) -> SinglePart { + let maybe_encoding = self.headers.get::().copied(); + let body = body.into_body(maybe_encoding); + + self.headers.set(body.encoding()); + SinglePart { headers: self.headers, - body: body.into(), + body: body.into_vec(), } } } @@ -101,7 +106,7 @@ impl Default for SinglePartBuilder { #[derive(Debug, Clone)] pub struct SinglePart { headers: Headers, - body: Body, + body: Vec, } impl SinglePart { @@ -111,57 +116,26 @@ impl SinglePart { SinglePartBuilder::new() } - /// Creates a singlepart builder using 7bit encoding - /// - /// 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] + #[doc(hidden)] + #[deprecated = "Replaced by SinglePart::builder(), which chooses the best Content-Transfer-Encoding based on the provided body"] pub fn seven_bit() -> SinglePartBuilder { Self::builder().header(ContentTransferEncoding::SevenBit) } - /// Creates a singlepart builder using quoted-printable encoding - /// - /// 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] + #[doc(hidden)] + #[deprecated = "Replaced by SinglePart::builder(), which chooses the best Content-Transfer-Encoding based on the provided body"] pub fn quoted_printable() -> SinglePartBuilder { Self::builder().header(ContentTransferEncoding::QuotedPrintable) } - /// Creates a singlepart builder using base64 encoding - /// - /// 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] + #[doc(hidden)] + #[deprecated = "Replaced by SinglePart::builder(), which chooses the best Content-Transfer-Encoding based on the provided body"] pub fn base64() -> SinglePartBuilder { Self::builder().header(ContentTransferEncoding::Base64) } - /// Creates a singlepart builder using 8bit encoding - /// - /// 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] + #[doc(hidden)] + #[deprecated = "Replaced by SinglePart::builder(), which chooses the best Content-Transfer-Encoding based on the provided body"] pub fn eight_bit() -> SinglePartBuilder { Self::builder().header(ContentTransferEncoding::EightBit) } @@ -174,7 +148,7 @@ impl SinglePart { /// Read the body from singlepart #[inline] - pub fn body(&self) -> &Body { + pub fn raw_body(&self) -> &[u8] { &self.body } @@ -190,15 +164,7 @@ impl EmailFormat for SinglePart { fn format(&self, out: &mut Vec) { out.extend_from_slice(self.headers.to_string().as_bytes()); out.extend_from_slice(b"\r\n"); - - let encoding = self - .headers - .get::() - .copied() - .unwrap_or_else(|| self.body.encoding()); - let encoded = self.body.encode(encoding); - - out.extend_from_slice(encoded.as_ref().as_ref()); + out.extend_from_slice(&self.body); out.extend_from_slice(b"\r\n"); } } @@ -629,11 +595,13 @@ mod test { "\r\n", "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "Content-Type: application/pgp-encrypted\r\n", + "Content-Transfer-Encoding: 7bit\r\n", "\r\n", "Version: 1\r\n", "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n", "Content-Disposition: inline; filename=\"encrypted.asc\"\r\n", + "Content-Transfer-Encoding: 7bit\r\n", "\r\n", "-----BEGIN PGP MESSAGE-----\r\n", "wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n", @@ -688,11 +656,13 @@ mod test { "\r\n", "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "Content-Type: text/plain\r\n", + "Content-Transfer-Encoding: 7bit\r\n", "\r\n", "Test email for signature\r\n", "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n", "Content-Disposition: attachment; filename=\"signature.asc\"\r\n", + "Content-Transfer-Encoding: 7bit\r\n", "\r\n", "-----BEGIN PGP SIGNATURE-----\r\n", "\r\n", diff --git a/src/message/mod.rs b/src/message/mod.rs index 67640bf..e09a074 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -22,7 +22,7 @@ //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") -//! .body("Be happy!")?; +//! .body(String::from("Be happy!"))?; //! # Ok(()) //! # } //! ``` @@ -184,7 +184,7 @@ //! //! ``` -pub use body::Body; +pub use body::{Body, IntoBody, MaybeString}; pub use mailbox::*; pub use mimebody::*; @@ -196,13 +196,16 @@ mod mailbox; mod mimebody; mod utf8_b; +use std::convert::TryFrom; +use std::time::SystemTime; + +use uuid::Uuid; + use crate::{ address::Envelope, - message::header::{EmailDate, Header, Headers, MailboxesHeader}, + message::header::{ContentTransferEncoding, EmailDate, Header, Headers, MailboxesHeader}, Error as EmailError, }; -use std::{convert::TryFrom, time::SystemTime}; -use uuid::Uuid; const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost"; @@ -410,23 +413,17 @@ impl MessageBuilder { }) } - // In theory having a body is optional - - /// Plain US-ASCII body + /// Create [`Message`] using a [`Vec`], [`String`], or [`Body`] 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 { - let body = body.into(); + /// Automatically gets encoded with `7bit`, `quoted-printable` or `base64` + /// `Content-Transfer-Encoding`, based on the most efficient and valid encoding + /// for `body`. + pub fn body(mut self, body: T) -> Result { + let maybe_encoding = self.headers.get::().copied(); + let body = body.into_body(maybe_encoding); - if !self::body::is_7bit_encoded(body.as_ref()) { - return Err(EmailError::NonAsciiChars); - } - - self.build(MessageBody::Raw(body)) + self.headers.set(body.encoding()); + self.build(MessageBody::Raw(body.into_vec())) } /// Create message using mime body ([`MultiPart`][self::MultiPart]) @@ -451,7 +448,7 @@ pub struct Message { #[derive(Clone, Debug)] enum MessageBody { Mime(Part), - Raw(String), + Raw(Vec), } impl Message { @@ -485,7 +482,7 @@ impl EmailFormat for Message { MessageBody::Mime(p) => p.format(out), MessageBody::Raw(r) => { out.extend_from_slice(b"\r\n"); - out.extend(r.as_bytes()) + out.extend_from_slice(&r) } } } @@ -503,7 +500,9 @@ mod test { #[test] fn email_missing_originator() { - assert!(Message::builder().body("Happy new year!").is_err()); + assert!(Message::builder() + .body(String::from("Happy new year!")) + .is_err()); } #[test] @@ -511,7 +510,7 @@ mod test { assert!(Message::builder() .from("NoBody ".parse().unwrap()) .to("NoBody ".parse().unwrap()) - .body("Happy new year!") + .body(String::from("Happy new year!")) .is_ok()); } @@ -520,7 +519,7 @@ mod test { assert!(Message::builder() .from("NoBody ".parse().unwrap()) .from("AnyBody ".parse().unwrap()) - .body("Happy new year!") + .body(String::from("Happy new year!")) .is_err()); } @@ -541,7 +540,7 @@ mod test { vec!["Pony O.P. ".parse().unwrap()].into(), )) .header(header::Subject("яңа ел белән!".into())) - .body("Happy new year!") + .body(String::from("Happy new year!")) .unwrap(); assert_eq!( @@ -551,6 +550,7 @@ mod test { "From: =?utf-8?b?0JrQsNC4?= \r\n", "To: Pony O.P. \r\n", "Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n", + "Content-Transfer-Encoding: 7bit\r\n", "\r\n", "Happy new year!" ) @@ -570,7 +570,7 @@ mod test { .multipart( MultiPart::related() .singlepart( - SinglePart::eight_bit() + SinglePart::builder() .header(header::ContentType( "text/html; charset=utf8".parse().unwrap(), )) @@ -579,7 +579,7 @@ mod test { )), ) .singlepart( - SinglePart::base64() + SinglePart::builder() .header(header::ContentType("image/png".parse().unwrap())) .header(header::ContentDisposition { disposition: header::DispositionType::Inline, diff --git a/src/transport/file/mod.rs b/src/transport/file/mod.rs index 635c18b..f9ed1bb 100644 --- a/src/transport/file/mod.rs +++ b/src/transport/file/mod.rs @@ -19,7 +19,7 @@ //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") -//! .body("Be happy!")?; +//! .body(String::from("Be happy!"))?; //! //! let result = sender.send(&email); //! assert!(result.is_ok()); @@ -51,7 +51,7 @@ //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") -//! .body("Be happy!")?; +//! .body(String::from("Be happy!"))?; //! //! let result = sender.send(&email); //! assert!(result.is_ok()); @@ -79,7 +79,7 @@ //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") -//! .body("Be happy!")?; +//! .body(String::from("Be happy!"))?; //! //! let result = sender.send(email).await; //! assert!(result.is_ok()); @@ -104,7 +104,7 @@ //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") -//! .body("Be happy!")?; +//! .body(String::from("Be happy!"))?; //! //! let result = sender.send(email).await; //! assert!(result.is_ok()); diff --git a/src/transport/sendmail/mod.rs b/src/transport/sendmail/mod.rs index ac9b57e..6cb377c 100644 --- a/src/transport/sendmail/mod.rs +++ b/src/transport/sendmail/mod.rs @@ -14,7 +14,7 @@ //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") -//! .body("Be happy!")?; +//! .body(String::from("Be happy!"))?; //! //! let sender = SendmailTransport::new(); //! let result = sender.send(&email); @@ -40,7 +40,7 @@ //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") -//! .body("Be happy!")?; +//! .body(String::from("Be happy!"))?; //! //! let sender = SendmailTransport::new(); //! let result = sender.send(email).await; @@ -63,7 +63,7 @@ //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") -//! .body("Be happy!")?; +//! .body(String::from("Be happy!"))?; //! //! let sender = SendmailTransport::new(); //! let result = sender.send(email).await; diff --git a/src/transport/smtp/mod.rs b/src/transport/smtp/mod.rs index 8320df1..ce577c2 100644 --- a/src/transport/smtp/mod.rs +++ b/src/transport/smtp/mod.rs @@ -40,7 +40,7 @@ //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") -//! .body("Be happy!")?; +//! .body(String::from("Be happy!"))?; //! //! // Create TLS transport on port 465 //! let sender = SmtpTransport::relay("smtp.example.com") diff --git a/src/transport/stub/mod.rs b/src/transport/stub/mod.rs index 008aee9..0353d19 100644 --- a/src/transport/stub/mod.rs +++ b/src/transport/stub/mod.rs @@ -19,7 +19,7 @@ //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") -//! .body("Be happy!")?; +//! .body(String::from("Be happy!"))?; //! //! let mut sender = StubTransport::new_ok(); //! let result = sender.send(&email); diff --git a/testdata/email_with_png.eml b/testdata/email_with_png.eml index a300e17..56734ea 100644 --- a/testdata/email_with_png.eml +++ b/testdata/email_with_png.eml @@ -7,15 +7,15 @@ MIME-Version: 1.0 Content-Type: multipart/related; boundary="kve7WTqtIDDNFzED8Dccv2dHCJYXcEwETTAaIYCAcfn6lxvHldUc21nczexPKCfoDVec" --kve7WTqtIDDNFzED8Dccv2dHCJYXcEwETTAaIYCAcfn6lxvHldUc21nczexPKCfoDVec -Content-Transfer-Encoding: 8bit Content-Type: text/html; charset=utf8 +Content-Transfer-Encoding: 7bit

Hello, world!

--kve7WTqtIDDNFzED8Dccv2dHCJYXcEwETTAaIYCAcfn6lxvHldUc21nczexPKCfoDVec -Content-Transfer-Encoding: base64 Content-Type: image/png Content-Disposition: inline Content-ID: <123> +Content-Transfer-Encoding: base64 iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAACXBIWXMAASdGAAEnRgHWSSfaAAAA GXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAIABJREFUeJzs3WmMlfXd//HvzLAO diff --git a/tests/transport_file.rs b/tests/transport_file.rs index 9b01850..f0996cb 100644 --- a/tests/transport_file.rs +++ b/tests/transport_file.rs @@ -20,7 +20,7 @@ mod test { .to("Hei ".parse().unwrap()) .subject("Happy new year") .date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); let result = sender.send(&email); @@ -31,7 +31,17 @@ mod test { assert_eq!( eml, - "From: NoBody \r\nReply-To: Yuin \r\nTo: Hei \r\nSubject: Happy new year\r\nDate: Tue, 15 Nov 1994 08:12:31 GMT\r\n\r\nBe happy!"); + concat!( + "From: NoBody \r\n", + "Reply-To: Yuin \r\n", + "To: Hei \r\n", + "Subject: Happy new year\r\n", + "Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", + "Content-Transfer-Encoding: 7bit\r\n", + "\r\n", + "Be happy!" + ) + ); remove_file(eml_file).unwrap(); } @@ -46,7 +56,7 @@ mod test { .to("Hei ".parse().unwrap()) .subject("Happy new year") .date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); let result = sender.send(&email); @@ -60,7 +70,17 @@ mod test { assert_eq!( eml, - "From: NoBody \r\nReply-To: Yuin \r\nTo: Hei \r\nSubject: Happy new year\r\nDate: Tue, 15 Nov 1994 08:12:31 GMT\r\n\r\nBe happy!"); + concat!( + "From: NoBody \r\n", + "Reply-To: Yuin \r\n", + "To: Hei \r\n", + "Subject: Happy new year\r\n", + "Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", + "Content-Transfer-Encoding: 7bit\r\n", + "\r\n", + "Be happy!" + ) + ); remove_file(eml_file).unwrap(); assert_eq!( @@ -82,7 +102,7 @@ mod test { .to("Hei ".parse().unwrap()) .subject("Happy new year") .date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); let result = sender.send(email).await; @@ -93,7 +113,17 @@ mod test { assert_eq!( eml, - "From: NoBody \r\nReply-To: Yuin \r\nTo: Hei \r\nSubject: Happy new year\r\nDate: Tue, 15 Nov 1994 08:12:31 GMT\r\n\r\nBe happy!"); + concat!( + "From: NoBody \r\n", + "Reply-To: Yuin \r\n", + "To: Hei \r\n", + "Subject: Happy new year\r\n", + "Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", + "Content-Transfer-Encoding: 7bit\r\n", + "\r\n", + "Be happy!" + ) + ); remove_file(eml_file).unwrap(); } @@ -109,7 +139,7 @@ mod test { .to("Hei ".parse().unwrap()) .subject("Happy new year") .date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); let result = sender.send(email).await; @@ -120,7 +150,17 @@ mod test { assert_eq!( eml, - "From: NoBody \r\nReply-To: Yuin \r\nTo: Hei \r\nSubject: Happy new year\r\nDate: Tue, 15 Nov 1994 08:12:31 GMT\r\n\r\nBe happy!"); + concat!( + "From: NoBody \r\n", + "Reply-To: Yuin \r\n", + "To: Hei \r\n", + "Subject: Happy new year\r\n", + "Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", + "Content-Transfer-Encoding: 7bit\r\n", + "\r\n", + "Be happy!" + ) + ); remove_file(eml_file).unwrap(); } } diff --git a/tests/transport_sendmail.rs b/tests/transport_sendmail.rs index 99c68d3..15535c6 100644 --- a/tests/transport_sendmail.rs +++ b/tests/transport_sendmail.rs @@ -15,7 +15,7 @@ mod test { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new year") - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); let result = sender.send(&email); @@ -35,7 +35,7 @@ mod test { .to("Hei ".parse().unwrap()) .subject("Happy new year") .date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); let result = sender.send(email).await; @@ -54,7 +54,7 @@ mod test { .to("Hei ".parse().unwrap()) .subject("Happy new year") .date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); let result = sender.send(email).await; diff --git a/tests/transport_smtp.rs b/tests/transport_smtp.rs index 49620d4..bcfaa41 100644 --- a/tests/transport_smtp.rs +++ b/tests/transport_smtp.rs @@ -10,7 +10,7 @@ mod test { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new year") - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); SmtpTransport::builder_dangerous("127.0.0.1") .port(2525) diff --git a/tests/transport_stub.rs b/tests/transport_stub.rs index 01691fa..2266f00 100644 --- a/tests/transport_stub.rs +++ b/tests/transport_stub.rs @@ -16,7 +16,7 @@ mod test { .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new year") - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); sender_ok.send(&email).unwrap(); @@ -36,7 +36,7 @@ mod test { .to("Hei ".parse().unwrap()) .subject("Happy new year") .date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); sender_ok.send(email.clone()).await.unwrap(); @@ -56,7 +56,7 @@ mod test { .to("Hei ".parse().unwrap()) .subject("Happy new year") .date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) - .body("Be happy!") + .body(String::from("Be happy!")) .unwrap(); sender_ok.send(email.clone()).await.unwrap();