refactor: Message body encoder
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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
408
src/message/body.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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, мир!"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user