refactor: Message body encoder

This commit is contained in:
Paolo Barbolini
2020-11-06 13:53:50 +01:00
parent d3c73d8bd7
commit 170e929a2b
6 changed files with 496 additions and 307 deletions

View File

@@ -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");

View File

@@ -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(),
))

408
src/message/body.rs Normal file
View File

@@ -0,0 +1,408 @@
use std::borrow::Cow;
use std::io::{self, Write};
use crate::message::header::ContentTransferEncoding;
/// A [`SinglePart`][super::SinglePart] body.
#[derive(Debug, Clone)]
pub struct Body(BodyInner);
#[derive(Debug, Clone)]
enum BodyInner {
Binary(Vec<u8>),
String(String),
}
impl Body {
/// Returns the length of this `Body` in bytes.
#[inline]
pub fn len(&self) -> usize {
match &self.0 {
BodyInner::Binary(b) => b.len(),
BodyInner::String(s) => s.len(),
}
}
/// Returns `true` if this `Body` has a length of zero, `false` otherwise.
#[inline]
pub fn is_empty(&self) -> bool {
match &self.0 {
BodyInner::Binary(b) => b.is_empty(),
BodyInner::String(s) => s.is_empty(),
}
}
/// Suggests the best `Content-Transfer-Encoding` to be used for this `Body`
///
/// If the `Body` was created from a `String` composed only of US-ASCII
/// characters, with no lines longer than 1000 characters, then 7bit
/// encoding will be used, else quoted-printable will be choosen.
///
/// If the `Body` was instead created from a `Vec<u8>`, base64 encoding is always
/// choosen.
///
/// `8bit` and `binary` encodings are never returned, as they may not be
/// supported by all SMTP servers.
pub fn encoding(&self) -> ContentTransferEncoding {
match &self.0 {
BodyInner::String(s) if is_7bit_encoded(s.as_ref()) => {
ContentTransferEncoding::SevenBit
}
// TODO: consider when base64 would be a better option because of output size
BodyInner::String(_) => ContentTransferEncoding::QuotedPrintable,
BodyInner::Binary(_) => ContentTransferEncoding::Base64,
}
}
/// Encodes this `Body` using the choosen `encoding`.
///
/// # Panic
///
/// Panics if the choosen `Content-Transfer-Encoding` would end-up
/// creating an incorrectly encoded email.
///
/// Could happen for example if `7bit` encoding is choosen when the
/// content isn't US-ASCII or contains lines longer than 1000 characters.
///
/// Never panics when using an `encoding` returned by [`encoding`][Body::encoding].
pub fn encode(&self, encoding: ContentTransferEncoding) -> Cow<'_, Body> {
match encoding {
ContentTransferEncoding::SevenBit => {
assert!(
is_7bit_encoded(self.as_ref()),
"Body isn't valid 7bit content"
);
Cow::Borrowed(self)
}
ContentTransferEncoding::EightBit => {
assert!(
is_8bit_encoded(self.as_ref()),
"Body isn't valid 8bit content"
);
Cow::Borrowed(self)
}
ContentTransferEncoding::Binary => Cow::Borrowed(self),
ContentTransferEncoding::QuotedPrintable => {
let encoded = quoted_printable::encode_to_str(self);
Cow::Owned(Body(BodyInner::String(encoded)))
}
ContentTransferEncoding::Base64 => {
let base64_len = self.len() * 4 / 3 + 4;
let base64_endings_len = base64_len + base64_len / LINE_MAX_LENGTH;
let mut out = Vec::with_capacity(base64_endings_len);
{
let writer = LineWrappingWriter::new(&mut out, LINE_MAX_LENGTH);
let mut writer = base64::write::EncoderWriter::new(writer, base64::STANDARD);
// TODO: use writer.write_all(self.as_ref()).expect("base64 encoding never fails");
// modified Write::write_all to work around base64 crate bug
// TODO: remove once https://github.com/marshallpierce/rust-base64/issues/148 is fixed
{
let mut buf: &[u8] = self.as_ref();
while !buf.is_empty() {
match writer.write(buf) {
Ok(0) => {
// ignore 0 writes
}
Ok(n) => {
buf = &buf[n..];
}
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => panic!("base64 encoding never fails: {}", e),
}
}
}
}
Cow::Owned(Body(BodyInner::Binary(out)))
}
}
}
}
impl From<Vec<u8>> for Body {
#[inline]
fn from(b: Vec<u8>) -> Self {
Self(BodyInner::Binary(b))
}
}
impl From<String> for Body {
#[inline]
fn from(s: String) -> Self {
Self(BodyInner::String(s))
}
}
impl AsRef<[u8]> for Body {
#[inline]
fn as_ref(&self) -> &[u8] {
match &self.0 {
BodyInner::Binary(b) => b.as_ref(),
BodyInner::String(s) => s.as_ref(),
}
}
}
/// Checks whether it contains only US-ASCII characters,
/// and no lines are longer than 1000 characters including the `\n` character.
///
/// Most efficient content encoding available
pub(crate) fn is_7bit_encoded(buf: &[u8]) -> bool {
buf.is_ascii() && !contains_too_long_lines(buf)
}
/// Checks that no lines are longer than 1000 characters,
/// including the `\n` character.
/// NOTE: 8bit isn't supported by all SMTP servers.
fn is_8bit_encoded(buf: &[u8]) -> bool {
!contains_too_long_lines(buf)
}
/// Checks if there are lines that are longer than 1000 characters,
/// including the `\n` character.
fn contains_too_long_lines(buf: &[u8]) -> bool {
buf.len() > 1000 && buf.split(|&b| b == b'\n').any(|line| line.len() > 999)
}
const LINE_SEPARATOR: &[u8] = b"\r\n";
const LINE_MAX_LENGTH: usize = 78 - LINE_SEPARATOR.len();
/// A `Write`r that inserts a line separator `\r\n` every `max_line_length` bytes.
struct LineWrappingWriter<'a, W> {
writer: &'a mut W,
current_line_length: usize,
max_line_length: usize,
}
impl<'a, W> LineWrappingWriter<'a, W> {
pub fn new(writer: &'a mut W, max_line_length: usize) -> Self {
Self {
writer,
current_line_length: 0,
max_line_length,
}
}
}
impl<'a, W> Write for LineWrappingWriter<'a, W>
where
W: Write,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let remaining_line_len = self.max_line_length - self.current_line_length;
let write_len = std::cmp::min(buf.len(), remaining_line_len);
self.writer.write_all(&buf[..write_len])?;
if remaining_line_len == write_len {
self.writer.write_all(LINE_SEPARATOR)?;
self.current_line_length = 0;
} else {
self.current_line_length += write_len;
}
Ok(write_len)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
#[cfg(test)]
mod test {
use super::{Body, ContentTransferEncoding};
#[test]
fn seven_bit_detect() {
let input = Body::from(String::from("Hello, world!"));
let encoding = input.encoding();
assert_eq!(encoding, ContentTransferEncoding::SevenBit);
}
#[test]
fn seven_bit_encode() {
let input = Body::from(String::from("Hello, world!"));
let output = input.encode(ContentTransferEncoding::SevenBit);
assert_eq!(output.as_ref().as_ref(), b"Hello, world!");
}
#[test]
fn seven_bit_too_long_detect() {
let input = Body::from("Hello, world!".repeat(100));
let encoding = input.encoding();
assert_eq!(encoding, ContentTransferEncoding::QuotedPrintable);
}
#[test]
#[should_panic]
fn seven_bit_too_long_fail() {
let input = Body::from("Hello, world!".repeat(100));
let _ = input.encode(ContentTransferEncoding::SevenBit);
}
#[test]
fn seven_bit_too_long_encode_quotedprintable() {
let input = Body::from("Hello, world!".repeat(100));
let output = input.encode(ContentTransferEncoding::QuotedPrintable);
assert_eq!(
output.as_ref().as_ref(),
concat!(
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
"ello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, worl=\r\n",
"d!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, w=\r\n",
"orld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello=\r\n",
", world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!He=\r\n",
"llo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world=\r\n",
"!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wo=\r\n",
"rld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello,=\r\n",
" world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hel=\r\n",
"lo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!=\r\n",
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
"ello, world!Hello, world!"
)
.as_bytes()
);
}
#[test]
#[should_panic]
fn seven_bit_invalid() {
let input = Body::from(String::from("Привет, мир!"));
let _ = input.encode(ContentTransferEncoding::SevenBit);
}
#[test]
fn eight_bit_encode() {
let input = Body::from(String::from("Привет, мир!"));
let out = input.encode(ContentTransferEncoding::EightBit);
assert_eq!(out.as_ref().as_ref(), "Привет, мир!".as_bytes());
}
#[test]
#[should_panic]
fn eight_bit_too_long_fail() {
let input = Body::from("Привет, мир!".repeat(200));
let _ = input.encode(ContentTransferEncoding::EightBit);
}
#[test]
fn quoted_printable_detect() {
let input = Body::from(String::from("Привет, мир!"));
let encoding = input.encoding();
assert_eq!(encoding, ContentTransferEncoding::QuotedPrintable);
}
#[test]
fn quoted_printable_encode_ascii() {
let input = Body::from(String::from("Hello, world!"));
let output = input.encode(ContentTransferEncoding::QuotedPrintable);
assert_eq!(output.as_ref().as_ref(), b"Hello, world!");
}
#[test]
fn quoted_printable_encode_utf8() {
let input = Body::from(String::from("Привет, мир!"));
let output = input.encode(ContentTransferEncoding::QuotedPrintable);
assert_eq!(
output.as_ref().as_ref(),
b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
);
}
#[test]
fn quoted_printable_encode_line_wrap() {
let input = Body::from(String::from("Текст письма в уникоде"));
let output = input.encode(ContentTransferEncoding::QuotedPrintable);
assert_eq!(
output.as_ref().as_ref(),
concat!(
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5"
)
.as_bytes()
);
}
#[test]
fn base64_detect() {
let input = Body::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let encoding = input.encoding();
assert_eq!(encoding, ContentTransferEncoding::Base64);
}
#[test]
fn base64_encode_bytes() {
let input = Body::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let output = input.encode(ContentTransferEncoding::Base64);
assert_eq!(output.as_ref().as_ref(), b"AAECAwQFBgcICQ==");
}
#[test]
fn base64_encode_bytes_wrapping() {
let input = Body::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20));
let output = input.encode(ContentTransferEncoding::Base64);
assert_eq!(
output.as_ref().as_ref(),
concat!(
"AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUG\r\n",
"BwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQID\r\n",
"BAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkA\r\n",
"AQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAk="
)
.as_bytes()
);
}
#[test]
fn base64_encode_ascii() {
let input = Body::from(String::from("Hello World!"));
let output = input.encode(ContentTransferEncoding::Base64);
assert_eq!(output.as_ref().as_ref(), b"SGVsbG8gV29ybGQh");
}
#[test]
fn base64_encode_ascii_wrapping() {
let input = Body::from("Hello World!".repeat(20));
let output = input.encode(ContentTransferEncoding::Base64);
assert_eq!(
output.as_ref().as_ref(),
concat!(
"SGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29y\r\n",
"bGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8g\r\n",
"V29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVs\r\n",
"bG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQh\r\n",
"SGVsbG8gV29ybGQh"
)
.as_bytes()
);
}
}

View File

@@ -1,247 +0,0 @@
use crate::message::header::ContentTransferEncoding;
/// Encoder trait
pub trait EncoderCodec: Send {
/// Encode all data
fn encode(&mut self, input: &[u8]) -> Vec<u8>;
}
/// 7bit codec
///
/// WARNING: Panics when passed non-ascii chars
struct SevenBitCodec {
line_wrapper: EightBitCodec,
}
impl SevenBitCodec {
pub fn new() -> Self {
SevenBitCodec {
line_wrapper: EightBitCodec::new(),
}
}
}
impl EncoderCodec for SevenBitCodec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
assert!(input.is_ascii(), "input must be valid ascii");
self.line_wrapper.encode(input)
}
}
/// Quoted-Printable codec
///
struct QuotedPrintableCodec();
impl QuotedPrintableCodec {
pub fn new() -> Self {
QuotedPrintableCodec()
}
}
impl EncoderCodec for QuotedPrintableCodec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
quoted_printable::encode(input)
}
}
/// Base64 codec
///
struct Base64Codec {
line_wrapper: EightBitCodec,
}
impl Base64Codec {
pub fn new() -> Self {
Base64Codec {
// TODO probably 78, 76 is for qp
line_wrapper: EightBitCodec::new().with_limit(78 - 2),
}
}
}
impl EncoderCodec for Base64Codec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
self.line_wrapper.encode(base64::encode(input).as_bytes())
}
}
/// 8bit codec
///
struct EightBitCodec {
max_length: usize,
}
const DEFAULT_MAX_LINE_LENGTH: usize = 1000 - 2;
impl EightBitCodec {
pub fn new() -> Self {
EightBitCodec {
max_length: DEFAULT_MAX_LINE_LENGTH,
}
}
pub fn with_limit(mut self, max_length: usize) -> Self {
self.max_length = max_length;
self
}
}
impl EncoderCodec for EightBitCodec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
let ending = b"\r\n";
let endings_len = input.len() / self.max_length * ending.len();
let mut out = Vec::with_capacity(input.len() + endings_len);
for chunk in input.chunks(self.max_length) {
// write the line ending after every chunk, except the last one
if !out.is_empty() {
out.extend_from_slice(ending);
}
out.extend_from_slice(chunk);
}
out
}
}
/// Binary codec
///
struct BinaryCodec;
impl BinaryCodec {
pub fn new() -> Self {
BinaryCodec
}
}
impl EncoderCodec for BinaryCodec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
input.into()
}
}
pub fn codec(encoding: Option<&ContentTransferEncoding>) -> Box<dyn EncoderCodec> {
use self::ContentTransferEncoding::*;
match encoding {
Some(SevenBit) => Box::new(SevenBitCodec::new()),
Some(QuotedPrintable) => Box::new(QuotedPrintableCodec::new()),
Some(Base64) => Box::new(Base64Codec::new()),
Some(EightBit) => Box::new(EightBitCodec::new()),
Some(Binary) | None => Box::new(BinaryCodec::new()),
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn seven_bit_encode() {
let mut c = SevenBitCodec::new();
assert_eq!(
&String::from_utf8(c.encode(b"Hello, world!")).unwrap(),
"Hello, world!"
);
}
#[test]
#[should_panic]
fn seven_bit_encode_panic() {
let mut c = SevenBitCodec::new();
c.encode("Hello, мир!".as_bytes());
}
#[test]
fn quoted_printable_encode() {
let mut c = QuotedPrintableCodec::new();
assert_eq!(
&String::from_utf8(c.encode("Привет, мир!".as_bytes())).unwrap(),
"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!"
);
assert_eq!(&String::from_utf8(c.encode("Текст письма в уникоде".as_bytes())).unwrap(),
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5");
}
#[test]
fn base64_encode() {
let mut c = Base64Codec::new();
assert_eq!(
&String::from_utf8(c.encode("Привет, мир!".as_bytes())).unwrap(),
"0J/RgNC40LLQtdGCLCDQvNC40YAh"
);
assert_eq!(
&String::from_utf8(c.encode("Текст письма в уникоде подлиннее.".as_bytes())).unwrap(),
concat!(
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQ",
"vtC00LUg0L/QvtC00LvQuNC90L3Q\r\ntdC1Lg=="
)
);
assert_eq!(
&String::from_utf8(c.encode(
"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую.".as_bytes()
)).unwrap(),
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRji4=")
);
assert_eq!(
&String::from_utf8(c.encode(
"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую это.".as_bytes()
)).unwrap(),
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRjiDRjdGC\r\n",
"0L4u")
);
}
#[test]
fn base64_encodeed() {
let mut c = Base64Codec::new();
assert_eq!(&String::from_utf8(c.encode(b"Chunk.")).unwrap(), "Q2h1bmsu");
}
#[test]
fn eight_bit_encode() {
let mut c = EightBitCodec::new();
assert_eq!(
&String::from_utf8(c.encode(b"Hello, world!")).unwrap(),
"Hello, world!"
);
assert_eq!(
&String::from_utf8(c.encode("Hello, мир!".as_bytes())).unwrap(),
"Hello, мир!"
);
}
#[test]
fn binary_encode() {
let mut c = BinaryCodec::new();
assert_eq!(
&String::from_utf8(c.encode(b"Hello, world!")).unwrap(),
"Hello, world!"
);
assert_eq!(
&String::from_utf8(c.encode("Hello, мир!".as_bytes())).unwrap(),
"Hello, мир!"
);
}
}

View File

@@ -1,7 +1,6 @@
use crate::message::{
encoder::codec,
header::{ContentTransferEncoding, ContentType, Header, Headers},
EmailFormat,
Body, EmailFormat,
};
use mime::Mime;
use rand::Rng;
@@ -69,7 +68,7 @@ impl SinglePartBuilder {
}
/// Build singlepart using body
pub fn body<T: Into<Vec<u8>>>(self, body: T) -> SinglePart {
pub fn body<T: Into<Body>>(self, body: T) -> SinglePart {
SinglePart {
headers: self.headers,
body: body.into(),
@@ -94,8 +93,7 @@ impl Default for SinglePartBuilder {
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let part = SinglePart::builder()
/// .header(header::ContentType("text/plain; charset=utf8".parse()?))
/// .header(header::ContentTransferEncoding::Binary)
/// .body("Текст письма в уникоде");
/// .body(String::from("Текст письма в уникоде"));
/// # Ok(())
/// # }
/// ```
@@ -103,61 +101,84 @@ impl Default for SinglePartBuilder {
#[derive(Debug, Clone)]
pub struct SinglePart {
headers: Headers,
body: Vec<u8>,
body: Body,
}
impl SinglePart {
/// Creates a default builder for singlepart
/// Creates a builder for singlepart
#[inline]
pub fn builder() -> SinglePartBuilder {
SinglePartBuilder::new()
}
/// Creates a singlepart builder with 7bit encoding
/// Creates a singlepart builder using 7bit encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::SevenBit)`.
/// 7bit encoding is the most efficient encoding available,
/// but it requires the body only to consist of US-ASCII characters,
/// with no lines longer than 1000 characters.
///
/// When in doubt use [`SinglePart::builder`] instead, which
/// chooses the best encoding based on the body.
///
/// # Panics
///
/// The above conditions are checked when the Body gets encoded
#[inline]
pub fn seven_bit() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::SevenBit)
}
/// Creates a singlepart builder with quoted-printable encoding
/// Creates a singlepart builder using quoted-printable encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::QuotedPrintable)`.
/// quoted-printable encoding should be used when the body may contain
/// lines longer than 1000 characters or a few non US-ASCII characters may be present.
/// If the body contains many non US-ASCII characters, [`SinglePart::base64`]
/// should be used instead, since it creates a smaller output compared
/// to quoted-printable.
///
/// When in doubt use [`SinglePart::builder`] instead, which
/// chooses the best encoding based on the body.
#[inline]
pub fn quoted_printable() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::QuotedPrintable)
}
/// Creates a singlepart builder with base64 encoding
/// Creates a singlepart builder using base64 encoding
///
/// 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<u8> {
let mut out = Vec::new();
self.format(&mut out);
@@ -170,10 +191,14 @@ impl EmailFormat for SinglePart {
out.extend_from_slice(self.headers.to_string().as_bytes());
out.extend_from_slice(b"\r\n");
let encoding = self.headers.get::<ContentTransferEncoding>();
let mut encoder = codec(encoding);
let encoding = self
.headers
.get::<ContentTransferEncoding>()
.copied()
.unwrap_or_else(|| self.body.encoding());
let encoded = self.body.encode(encoding);
out.extend_from_slice(&encoder.encode(&self.body));
out.extend_from_slice(encoded.as_ref().as_ref());
out.extend_from_slice(b"\r\n");
}
}

View File

@@ -59,9 +59,8 @@
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType(
//! "text/plain; charset=utf8".parse()?,
//! )).header(header::ContentTransferEncoding::QuotedPrintable)
//! .body("Привет, мир!"),
//! "text/plain; charset=utf8".parse()?))
//! .body(String::from("Привет, мир!")),
//! )?;
//! # Ok(())
//! # }
@@ -101,31 +100,31 @@
//! .multipart(
//! MultiPart::alternative()
//! .singlepart(
//! SinglePart::quoted_printable()
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .body("Hello, world! :)")
//! .body(String::from("Hello, world! :)"))
//! )
//! .multipart(
//! MultiPart::related()
//! .singlepart(
//! SinglePart::eight_bit()
//! SinglePart::builder()
//! .header(header::ContentType("text/html; charset=utf8".parse()?))
//! .body("<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>")
//! .body(String::from("<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>"))
//! )
//! .singlepart(
//! SinglePart::base64()
//! SinglePart::builder()
//! .header(header::ContentType("image/png".parse()?))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Inline,
//! parameters: vec![],
//! })
//! .header(header::ContentId("<123>".into()))
//! .body("<smile-raw-image-data>")
//! .body(Vec::<u8>::from("<smile-raw-image-data>"))
//! )
//! )
//! )
//! .singlepart(
//! SinglePart::seven_bit()
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Attachment,
@@ -136,7 +135,7 @@
//! )
//! ]
//! })
//! .body("int main() { return 0; }")
//! .body(String::from("int main() { return 0; }"))
//! )
//! )?;
//! # Ok(())
@@ -185,12 +184,13 @@
//!
//! ```
pub use body::Body;
pub use mailbox::*;
pub use mimebody::*;
pub use mime;
mod encoder;
mod body;
pub mod header;
mod mailbox;
mod mimebody;
@@ -375,7 +375,7 @@ impl MessageBuilder {
// TODO: High-level methods for attachments and embedded files
/// Create message from body
fn build(self, body: Body) -> Result<Message, EmailError> {
fn build(self, body: MessageBody) -> Result<Message, EmailError> {
// Check for missing required headers
// https://tools.ietf.org/html/rfc5322#section-3.6
@@ -412,30 +412,31 @@ impl MessageBuilder {
// In theory having a body is optional
/// Plain ASCII body
/// Plain US-ASCII body
///
/// Fails if is contains non ASCII characters or if it
/// contains lines longer than 1000 characters, including
/// the `\n` character.
///
/// *WARNING*: Generally not what you want
pub fn body<T: Into<String>>(self, body: T) -> Result<Message, EmailError> {
// 998 chars by line
// CR and LF MUST only occur together as CRLF; they MUST NOT appear
// independently in the body.
let body = body.into();
if !&body.is_ascii() {
if !self::body::is_7bit_encoded(body.as_ref()) {
return Err(EmailError::NonAsciiChars);
}
self.build(Body::Raw(body))
self.build(MessageBody::Raw(body))
}
/// Create message using mime body ([`MultiPart`][self::MultiPart])
pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
self.mime_1_0().build(Body::Mime(Part::Multi(part)))
self.mime_1_0().build(MessageBody::Mime(Part::Multi(part)))
}
/// Create message using mime body ([`SinglePart`][self::SinglePart])
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
self.mime_1_0().build(Body::Mime(Part::Single(part)))
self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
}
}
@@ -443,12 +444,12 @@ impl MessageBuilder {
#[derive(Clone, Debug)]
pub struct Message {
headers: Headers,
body: Body,
body: MessageBody,
envelope: Envelope,
}
#[derive(Clone, Debug)]
enum Body {
enum MessageBody {
Mime(Part),
Raw(String),
}
@@ -481,8 +482,8 @@ impl EmailFormat for Message {
fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
match &self.body {
Body::Mime(p) => p.format(out),
Body::Raw(r) => {
MessageBody::Mime(p) => p.format(out),
MessageBody::Raw(r) => {
out.extend_from_slice(b"\r\n");
out.extend(r.as_bytes())
}
@@ -573,7 +574,9 @@ mod test {
.header(header::ContentType(
"text/html; charset=utf8".parse().unwrap(),
))
.body("<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>"),
.body(String::from(
"<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
)),
)
.singlepart(
SinglePart::base64()