message: more body improvements (#519)

This commit is contained in:
Paolo Barbolini
2020-12-18 14:49:59 +01:00
committed by GitHub
parent f06b8f3823
commit ad9699827e
21 changed files with 437 additions and 264 deletions

View File

@@ -13,7 +13,7 @@ fn bench_simple_send(c: &mut Criterion) {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".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 <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".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());

View File

@@ -8,7 +8,7 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.body(String::from("Be happy!"))
.unwrap();
// Open a local connection on port 25

View File

@@ -14,7 +14,7 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".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

View File

@@ -8,7 +8,7 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".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());

View File

@@ -8,7 +8,7 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".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());

View File

@@ -17,7 +17,7 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".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());

View File

@@ -17,7 +17,7 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".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());

View File

@@ -17,7 +17,7 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".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());

View File

@@ -17,7 +17,7 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".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());

View File

@@ -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<u8>,
encoding: ContentTransferEncoding,
}
/// Either a `Vec<u8>` 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<u8>),
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<B: Into<MaybeString>>(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<B: Into<MaybeString>>(
buf: B,
encoding: ContentTransferEncoding,
) -> Result<Self, Vec<u8>> {
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<u8>, 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<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> {
/// Encodes the supplied `buf` using the provided `encoding`
fn new_impl(buf: Vec<u8>, 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<u8>`
#[inline]
pub fn into_vec(self) -> Vec<u8> {
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<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 {
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<Vec<u8>> for Body {
#[inline]
fn from(b: Vec<u8>) -> 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<ContentTransferEncoding>) -> Body;
}
impl<T> IntoBody for T
where
T: Into<MaybeString>,
{
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body {
match encoding {
Some(encoding) => Body::new_with_encoding(self, encoding).expect("invalid encoding"),
None => Body::new(self),
}
}
}
impl From<String> for Body {
#[inline]
fn from(s: String) -> Self {
Self(BodyInner::String(s))
impl IntoBody for Body {
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> 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<Vec<u8>> for MaybeString {
#[inline]
fn from(b: Vec<u8>) -> Self {
Self::Binary(b)
}
}
impl From<String> for MaybeString {
#[inline]
fn from(s: String) -> Self {
Self::String(s)
}
}
impl From<MaybeString> for Vec<u8> {
#[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",

View File

@@ -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<T: Into<Body>>(self, body: T) -> SinglePart {
pub fn body<T: IntoBody>(mut self, body: T) -> SinglePart {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().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<u8>,
}
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<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
out.extend_from_slice(b"\r\n");
let encoding = self
.headers
.get::<ContentTransferEncoding>()
.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",

View File

@@ -22,7 +22,7 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".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<u8>`], [`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<T: Into<String>>(self, body: T) -> Result<Message, EmailError> {
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<T: IntoBody>(mut self, body: T) -> Result<Message, EmailError> {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().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<u8>),
}
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 <nobody@domain.tld>".parse().unwrap())
.to("NoBody <nobody@domain.tld>".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 <nobody@domain.tld>".parse().unwrap())
.from("AnyBody <anybody@domain.tld>".parse().unwrap())
.body("Happy new year!")
.body(String::from("Happy new year!"))
.is_err());
}
@@ -541,7 +540,7 @@ mod test {
vec!["Pony O.P. <pony@domain.tld>".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?= <kayo@example.com>\r\n",
"To: Pony O.P. <pony@domain.tld>\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,

View File

@@ -19,7 +19,7 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".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 <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".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 <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".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 <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")?;
//! .body(String::from("Be happy!"))?;
//!
//! let result = sender.send(email).await;
//! assert!(result.is_ok());

View File

@@ -14,7 +14,7 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".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 <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".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 <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")?;
//! .body(String::from("Be happy!"))?;
//!
//! let sender = SendmailTransport::new();
//! let result = sender.send(email).await;

View File

@@ -40,7 +40,7 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".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")

View File

@@ -19,7 +19,7 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")?;
//! .body(String::from("Be happy!"))?;
//!
//! let mut sender = StubTransport::new_ok();
//! let result = sender.send(&email);

View File

@@ -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
<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>
--kve7WTqtIDDNFzED8Dccv2dHCJYXcEwETTAaIYCAcfn6lxvHldUc21nczexPKCfoDVec
Content-Transfer-Encoding: base64
Content-Type: image/png
Content-Disposition: inline
Content-ID: <123>
Content-Transfer-Encoding: base64
iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAACXBIWXMAASdGAAEnRgHWSSfaAAAA
GXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAIABJREFUeJzs3WmMlfXd//HvzLAO

View File

@@ -20,7 +20,7 @@ mod test {
.to("Hei <hei@domain.tld>".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 <nobody@domain.tld>\r\nReply-To: Yuin <yuin@domain.tld>\r\nTo: Hei <hei@domain.tld>\r\nSubject: Happy new year\r\nDate: Tue, 15 Nov 1994 08:12:31 GMT\r\n\r\nBe happy!");
concat!(
"From: NoBody <nobody@domain.tld>\r\n",
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\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 <hei@domain.tld>".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 <nobody@domain.tld>\r\nReply-To: Yuin <yuin@domain.tld>\r\nTo: Hei <hei@domain.tld>\r\nSubject: Happy new year\r\nDate: Tue, 15 Nov 1994 08:12:31 GMT\r\n\r\nBe happy!");
concat!(
"From: NoBody <nobody@domain.tld>\r\n",
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\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 <hei@domain.tld>".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 <nobody@domain.tld>\r\nReply-To: Yuin <yuin@domain.tld>\r\nTo: Hei <hei@domain.tld>\r\nSubject: Happy new year\r\nDate: Tue, 15 Nov 1994 08:12:31 GMT\r\n\r\nBe happy!");
concat!(
"From: NoBody <nobody@domain.tld>\r\n",
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\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 <hei@domain.tld>".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 <nobody@domain.tld>\r\nReply-To: Yuin <yuin@domain.tld>\r\nTo: Hei <hei@domain.tld>\r\nSubject: Happy new year\r\nDate: Tue, 15 Nov 1994 08:12:31 GMT\r\n\r\nBe happy!");
concat!(
"From: NoBody <nobody@domain.tld>\r\n",
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\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();
}
}

View File

@@ -15,7 +15,7 @@ mod test {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".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 <hei@domain.tld>".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 <hei@domain.tld>".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;

View File

@@ -10,7 +10,7 @@ mod test {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.body(String::from("Be happy!"))
.unwrap();
SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)

View File

@@ -16,7 +16,7 @@ mod test {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".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 <hei@domain.tld>".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 <hei@domain.tld>".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();