feat(builder): Improve Message representation

* Remove bytes dependency and rely directly on bytes vec
* Allow encoding non-utf-8 strings
* Use Vec<u8> instead of Display for email formatting
This commit is contained in:
Alexis Mousset
2020-04-26 19:04:52 +02:00
parent 4e500ded50
commit 7f22a98f2f
23 changed files with 268 additions and 509 deletions

View File

@@ -19,11 +19,10 @@ maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
base64 = { version = "0.12", optional = true } base64 = { version = "0.12", optional = true }
bufstream = { version = "0.1", optional = true } bufstream = { version = "0.1", optional = true }
# TODO to 0.5
bytes = { version = "0.4", optional = true }
hostname = { version = "0.3", optional = true } hostname = { version = "0.3", optional = true }
hyperx = { version = "1", optional = true, features = ["headers"] } hyperx = { version = "1", optional = true, features = ["headers"] }
idna = "0.2" idna = "0.2"
line-wrap = "0.1"
log = "0.4" log = "0.4"
uuid = { version = "0.8", features = ["v4"] } uuid = { version = "0.8", features = ["v4"] }
mime = { version = "0.3", optional = true } mime = { version = "0.3", optional = true }
@@ -50,7 +49,7 @@ harness = false
name = "transport_smtp" name = "transport_smtp"
[features] [features]
builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable", "bytes"] builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"]
connection-pool = ["r2d2"] connection-pool = ["r2d2"]
default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "native-tls", "builder"] default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "native-tls", "builder"]
file-transport = ["serde", "serde_json"] file-transport = ["serde", "serde_json"]

View File

@@ -17,7 +17,7 @@ fn bench_simple_send(c: &mut Criterion) {
.subject("Happy new year") .subject("Happy new year")
.body("Be happy!") .body("Be happy!")
.unwrap(); .unwrap();
let result = black_box(sender.send(email)); let result = black_box(sender.send(&email));
assert!(result.is_ok()); assert!(result.is_ok());
}) })
}); });
@@ -37,7 +37,7 @@ fn bench_reuse_send(c: &mut Criterion) {
.subject("Happy new year") .subject("Happy new year")
.body("Be happy!") .body("Be happy!")
.unwrap(); .unwrap();
let result = black_box(sender.send(email)); let result = black_box(sender.send(&email));
assert!(result.is_ok()); assert!(result.is_ok());
}) })
}); });

View File

@@ -16,7 +16,7 @@ fn main() {
// Open a local connection on port 25 // Open a local connection on port 25
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport(); let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
// Send the email // Send the email
let result = mailer.send(email); let result = mailer.send(&email);
if result.is_ok() { if result.is_ok() {
println!("Email sent"); println!("Email sent");

View File

@@ -23,7 +23,7 @@ fn main() {
.transport(); .transport();
// Send the email // Send the email
let result = mailer.send(email); let result = mailer.send(&email);
if result.is_ok() { if result.is_ok() {
println!("Email sent"); println!("Email sent");

View File

@@ -65,7 +65,6 @@ static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\
impl Address { impl Address {
/// Create email address from parts /// Create email address from parts
#[inline]
pub fn new<U: Into<String>, D: Into<String>>(user: U, domain: D) -> Result<Self, AddressError> { pub fn new<U: Into<String>, D: Into<String>>(user: U, domain: D) -> Result<Self, AddressError> {
(user, domain).try_into() (user, domain).try_into()
} }

View File

@@ -23,6 +23,8 @@ pub enum Error {
CannotParseFilename, CannotParseFilename,
/// IO error /// IO error
Io(std::io::Error), Io(std::io::Error),
/// Non-ASCII chars
NonAsciiChars,
} }
impl Display for Error { impl Display for Error {
@@ -35,6 +37,7 @@ impl Display for Error {
Error::EmailMissingLocalPart => "missing local part in email address".to_string(), Error::EmailMissingLocalPart => "missing local part in email address".to_string(),
Error::EmailMissingDomain => "missing domain in email address".to_string(), Error::EmailMissingDomain => "missing domain in email address".to_string(),
Error::CannotParseFilename => "could not parse attachment filename".to_string(), Error::CannotParseFilename => "could not parse attachment filename".to_string(),
Error::NonAsciiChars => "contains non-ASCII chars".to_string(),
Error::Io(e) => e.to_string(), Error::Io(e) => e.to_string(),
}) })
} }

View File

@@ -9,7 +9,8 @@
trivial_casts, trivial_casts,
trivial_numeric_casts, trivial_numeric_casts,
unstable_features, unstable_features,
unused_import_braces unused_import_braces,
unsafe_code
)] )]
pub mod address; pub mod address;
@@ -23,7 +24,7 @@ use crate::error::Error;
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
pub use crate::message::{ pub use crate::message::{
header::{self, Headers}, header::{self, Headers},
Mailboxes, Message, EmailFormat, Mailboxes, Message,
}; };
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
pub use crate::transport::file::FileTransport; pub use crate::transport::file::FileTransport;
@@ -37,7 +38,6 @@ pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
pub use crate::transport::smtp::{ClientSecurity, SmtpClient, SmtpTransport}; pub use crate::transport::smtp::{ClientSecurity, SmtpClient, SmtpTransport};
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt::Display;
/// Simple email envelope representation /// Simple email envelope representation
/// ///
@@ -109,27 +109,20 @@ impl TryFrom<&Headers> for Envelope {
} }
} }
// FIXME generate random log id
/// Transport method for emails /// Transport method for emails
pub trait Transport<'a, B> { pub trait Transport<'a> {
/// Result type for the transport /// Result type for the transport
type Result; type Result;
/// Sends the email /// Sends the email
/// FIXME not mut /// FIXME not mut
// email = message (bytes) + envelope fn send(&mut self, message: &Message) -> Self::Result {
fn send(&mut self, message: Message<B>) -> Self::Result let raw = message.formatted();
where self.send_raw(message.envelope(), &raw)
B: Display,
{
self.send_raw(message.envelope(), message.to_string().as_bytes())
} }
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result; fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result;
// TODO allow sending generic data
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,61 +1,16 @@
use crate::message::header::ContentTransferEncoding; use crate::message::header::ContentTransferEncoding;
use bytes::{Buf, BufMut, Bytes, BytesMut, IntoBuf}; use std::io::Write;
use std::{ use line_wrap::{line_wrap, crlf, LineEnding};
cmp::min,
error::Error,
fmt::{Debug, Display, Formatter, Result as FmtResult},
};
/// Content encoding error
#[derive(Debug, Clone)]
pub enum EncoderError<E> {
Source(E),
Coding,
}
impl<E> Error for EncoderError<E> where E: Debug + Display {}
impl<E> Display for EncoderError<E>
where
E: Display,
{
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self {
EncoderError::Source(error) => write!(f, "Source error: {}", error),
EncoderError::Coding => f.write_str("Coding error"),
}
}
}
/// Encoder trait /// Encoder trait
pub trait EncoderCodec: Send { pub trait EncoderCodec: Send {
/// Encode chunk of data
fn encode_chunk(&mut self, input: &dyn Buf) -> Result<Bytes, ()>;
/// Encode end of stream
///
/// This proposed to use for stateful encoders like *base64*.
fn finish_chunk(&mut self) -> Result<Bytes, ()> {
Ok(Bytes::new())
}
/// Encode all data /// Encode all data
fn encode_all(&mut self, source: &dyn Buf) -> Result<Bytes, ()> { fn encode(&mut self, input: &[u8]) -> Vec<u8>;
let chunk = self.encode_chunk(source)?;
let end = self.finish_chunk()?;
Ok(if end.is_empty() {
chunk
} else {
let mut chunk = chunk.try_mut().unwrap();
chunk.put(end);
chunk.freeze()
})
}
} }
/// 7bit codec /// 7bit codec
/// ///
/// WARNING: Panics when passed non-ascii chars
struct SevenBitCodec { struct SevenBitCodec {
line_wrapper: EightBitCodec, line_wrapper: EightBitCodec,
} }
@@ -69,11 +24,11 @@ impl SevenBitCodec {
} }
impl EncoderCodec for SevenBitCodec { impl EncoderCodec for SevenBitCodec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> { fn encode(&mut self, input: &[u8]) -> Vec<u8> {
if chunk.bytes().iter().all(u8::is_ascii) { if input.iter().all(u8::is_ascii) {
self.line_wrapper.encode_chunk(chunk) self.line_wrapper.encode(input)
} else { } else {
Err(()) panic!("")
} }
} }
} }
@@ -89,8 +44,8 @@ impl QuotedPrintableCodec {
} }
impl EncoderCodec for QuotedPrintableCodec { impl EncoderCodec for QuotedPrintableCodec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> { fn encode(&mut self, input: &[u8]) -> Vec<u8> {
Ok(quoted_printable::encode(chunk.bytes()).into()) quoted_printable::encode(input)
} }
} }
@@ -98,75 +53,20 @@ impl EncoderCodec for QuotedPrintableCodec {
/// ///
struct Base64Codec { struct Base64Codec {
line_wrapper: EightBitCodec, line_wrapper: EightBitCodec,
last_padding: Bytes,
} }
impl Base64Codec { impl Base64Codec {
pub fn new() -> Self { pub fn new() -> Self {
Base64Codec { Base64Codec {
// TODO probably 78, 76 is for qp
line_wrapper: EightBitCodec::new().with_limit(78 - 2), line_wrapper: EightBitCodec::new().with_limit(78 - 2),
last_padding: Bytes::new(),
} }
} }
} }
impl EncoderCodec for Base64Codec { impl EncoderCodec for Base64Codec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> { fn encode(&mut self, input: &[u8]) -> Vec<u8> {
let in_len = self.last_padding.len() + chunk.remaining(); self.line_wrapper.encode(base64::encode(input).as_bytes())
let out_len = in_len * 4 / 3;
let mut out = BytesMut::with_capacity(out_len);
let chunk = if self.last_padding.is_empty() {
chunk.bytes()[..].into_buf()
} else {
let mut src = BytesMut::with_capacity(3);
let len = min(chunk.remaining(), 3 - self.last_padding.len());
src.put(&self.last_padding);
src.put(&chunk.bytes()[..len]);
// encode beginning
unsafe {
let len = base64::encode_config_slice(&src, base64::STANDARD, out.bytes_mut());
out.advance_mut(len);
}
chunk.bytes()[len..].into_buf()
};
let len = chunk.remaining() - (chunk.remaining() % 3);
let chunk = if len > 0 {
// encode chunk
unsafe {
let len = base64::encode_config_slice(
&chunk.bytes()[..len],
base64::STANDARD,
out.bytes_mut(),
);
out.advance_mut(len);
}
chunk.bytes()[len..].into_buf()
} else {
chunk.bytes()[..].into_buf()
};
// update last padding
self.last_padding = chunk.bytes().into();
self.line_wrapper.encode_chunk(&out.freeze().into_buf())
}
fn finish_chunk(&mut self) -> Result<Bytes, ()> {
let mut out = BytesMut::with_capacity(4);
unsafe {
let len =
base64::encode_config_slice(&self.last_padding, base64::STANDARD, out.bytes_mut());
out.advance_mut(len);
}
self.line_wrapper.encode_chunk(&out.freeze().into_buf())
} }
} }
@@ -174,7 +74,6 @@ impl EncoderCodec for Base64Codec {
/// ///
struct EightBitCodec { struct EightBitCodec {
max_length: usize, max_length: usize,
line_bytes: usize,
} }
const DEFAULT_MAX_LINE_LENGTH: usize = 1000 - 2; const DEFAULT_MAX_LINE_LENGTH: usize = 1000 - 2;
@@ -183,7 +82,6 @@ impl EightBitCodec {
pub fn new() -> Self { pub fn new() -> Self {
EightBitCodec { EightBitCodec {
max_length: DEFAULT_MAX_LINE_LENGTH, max_length: DEFAULT_MAX_LINE_LENGTH,
line_bytes: 0,
} }
} }
@@ -194,37 +92,15 @@ impl EightBitCodec {
} }
impl EncoderCodec for EightBitCodec { impl EncoderCodec for EightBitCodec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> { fn encode(&mut self, input: &[u8]) -> Vec<u8> {
let mut out = BytesMut::with_capacity(chunk.remaining() + 20); let ending = &crlf();
let mut src = chunk.bytes()[..].into_buf();
while src.has_remaining() { let mut out = vec![0_u8; input.len() + input.len() / self.max_length * ending.len()];
let line_break = src.bytes().iter().position(|b| *b == b'\n'); let mut writer: &mut [u8] = out.as_mut();
let mut split_pos = if let Some(line_break) = line_break { writer.write_all(input).unwrap();
line_break
} else { line_wrap(&mut out, input.len(), self.max_length, ending);
src.remaining() out
};
let max_length = self.max_length - self.line_bytes;
if split_pos < max_length {
// advance line bytes
self.line_bytes += split_pos;
} else {
split_pos = max_length;
// reset line bytes
self.line_bytes = 0;
};
let has_remaining = split_pos < src.remaining();
//let mut taken = src.take(split_pos);
out.reserve(split_pos + if has_remaining { 2 } else { 0 });
//out.put(&mut taken);
out.put(&src.bytes()[..split_pos]);
if has_remaining {
out.put_slice(b"\r\n");
}
src.advance(split_pos);
//src = taken.into_inner();
}
Ok(out.freeze())
} }
} }
@@ -239,8 +115,8 @@ impl BinaryCodec {
} }
impl EncoderCodec for BinaryCodec { impl EncoderCodec for BinaryCodec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> { fn encode(&mut self, input: &[u8]) -> Vec<u8> {
Ok(chunk.bytes().into()) input.into()
} }
} }
@@ -261,27 +137,23 @@ pub fn codec(encoding: Option<&ContentTransferEncoding>) -> Box<dyn EncoderCodec
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{ use super::*;
Base64Codec, BinaryCodec, EightBitCodec, EncoderCodec, QuotedPrintableCodec, SevenBitCodec,
};
use bytes::IntoBuf;
use std::str::from_utf8;
#[test] #[test]
fn seven_bit_encode() { fn seven_bit_encode() {
let mut c = SevenBitCodec::new(); let mut c = SevenBitCodec::new();
assert_eq!( assert_eq!(
c.encode_chunk(&"Hello, world!".into_buf()) &String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
.map(|s| from_utf8(&s).map(|s| String::from(s))), "Hello, world!"
Ok(Ok("Hello, world!".into()))
); );
}
assert_eq!( #[test]
c.encode_chunk(&"Hello, мир!".into_buf()) #[should_panic]
.map(|s| from_utf8(&s).map(|s| String::from(s))), fn seven_bit_encode_panic() {
Err(()) let mut c = SevenBitCodec::new();
); c.encode("Hello, мир!".as_bytes());
} }
#[test] #[test]
@@ -289,16 +161,13 @@ mod test {
let mut c = QuotedPrintableCodec::new(); let mut c = QuotedPrintableCodec::new();
assert_eq!( assert_eq!(
c.encode_chunk(&"Привет, мир!".into_buf()) &String::from_utf8(c.encode("Привет, мир!".as_bytes())).unwrap(),
.map(|s| from_utf8(&s).map(|s| String::from(s))), "=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!"
Ok(Ok(
"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".into()
))
); );
assert_eq!(c.encode_chunk(&"Текст письма в уникоде".into_buf()) assert_eq!(&String::from_utf8(c.encode("Текст письма в уникоде".as_bytes())).unwrap(),
.map(|s| from_utf8(&s).map(|s| String::from(s))), "=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");
Ok(Ok("=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".into())));
} }
#[test] #[test]
@@ -306,112 +175,51 @@ mod test {
let mut c = Base64Codec::new(); let mut c = Base64Codec::new();
assert_eq!( assert_eq!(
c.encode_all(&"Привет, мир!".into_buf()) &String::from_utf8(c.encode("Привет, мир!".as_bytes())).unwrap(),
.map(|s| from_utf8(&s).map(|s| String::from(s))), "0J/RgNC40LLQtdGCLCDQvNC40YAh"
Ok(Ok("0J/RgNC40LLQtdGCLCDQvNC40YAh".into()))
); );
assert_eq!( assert_eq!(
c.encode_all(&"Текст письма в уникоде подлиннее.".into_buf()) &String::from_utf8(c.encode("Текст письма в уникоде подлиннее.".as_bytes())).unwrap(),
.map(|s| from_utf8(&s).map(|s| String::from(s))), concat!(
Ok(Ok(concat!( "0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQ",
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQ\r\n", "vtC00LUg0L/QvtC00LvQuNC90L3Q\r\ntdC1Lg=="
"vtC00LUg0L/QvtC00LvQuNC90L3QtdC1Lg=="
) )
.into()))
); );
}
#[test]
fn base64_encode_all() {
let mut c = Base64Codec::new();
assert_eq!( assert_eq!(
c.encode_all( &String::from_utf8(c.encode(
&"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую." "Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую.".as_bytes()
.into_buf() )).unwrap(),
).map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok(
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n", concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n", "0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n", "viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRji4=").into() "udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRji4=")
))
); );
let mut c = Base64Codec::new();
assert_eq!( assert_eq!(
c.encode_all( &String::from_utf8(c.encode(
&"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую это." "Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую это.".as_bytes()
.into_buf()
).map(|s| from_utf8(&s).map(|s| String::from(s))), )).unwrap(),
Ok(Ok(
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n", concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n", "0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n", "viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRjiDRjdGC\r\n", "udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRjiDRjdGC\r\n",
"0L4u").into() "0L4u")
))
); );
} }
#[test] #[test]
fn base64_encode_chunked() { fn base64_encodeed() {
let mut c = Base64Codec::new(); let mut c = Base64Codec::new();
assert_eq!( assert_eq!(
c.encode_chunk(&"Chunk.".into_buf()) &String::from_utf8(c.encode("Chunk.".as_bytes())).unwrap(),
.map(|s| from_utf8(&s).map(|s| String::from(s))), "Q2h1bmsu"
Ok(Ok("Q2h1bmsu".into()))
);
assert_eq!(
c.finish_chunk()
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("".into()))
);
let mut c = Base64Codec::new();
assert_eq!(
c.encode_chunk(&"Chunk".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Q2h1".into()))
);
assert_eq!(
c.finish_chunk()
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("bms=".into()))
);
let mut c = Base64Codec::new();
assert_eq!(
c.encode_chunk(&"Chun".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Q2h1".into()))
);
assert_eq!(
c.finish_chunk()
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("bg==".into()))
);
let mut c = Base64Codec::new();
assert_eq!(
c.encode_chunk(&"Chu".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Q2h1".into()))
);
assert_eq!(
c.finish_chunk()
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("".into()))
); );
} }
@@ -420,15 +228,13 @@ mod test {
let mut c = EightBitCodec::new(); let mut c = EightBitCodec::new();
assert_eq!( assert_eq!(
c.encode_chunk(&"Hello, world!".into_buf()) &String::from_utf8(c.encode("Hello, world!".as_bytes())
.map(|s| from_utf8(&s).map(|s| String::from(s))), ).unwrap(), "Hello, world!"
Ok(Ok("Hello, world!".into()))
); );
assert_eq!( assert_eq!(
c.encode_chunk(&"Hello, мир!".into_buf()) &String::from_utf8(c.encode("Hello, мир!".as_bytes())
.map(|s| from_utf8(&s).map(|s| String::from(s))), ).unwrap(), "Hello, мир!"
Ok(Ok("Hello, мир!".into()))
); );
} }
@@ -437,15 +243,13 @@ mod test {
let mut c = BinaryCodec::new(); let mut c = BinaryCodec::new();
assert_eq!( assert_eq!(
c.encode_chunk(&"Hello, world!".into_buf()) &String::from_utf8(c.encode("Hello, world!".as_bytes())
.map(|s| from_utf8(&s).map(|s| String::from(s))), ).unwrap(), "Hello, world!"
Ok(Ok("Hello, world!".into()))
); );
assert_eq!( assert_eq!(
c.encode_chunk(&"Hello, мир!".into_buf()) &String::from_utf8(c.encode("Hello, мир!".as_bytes())
.map(|s| from_utf8(&s).map(|s| String::from(s))), ).unwrap(), "Hello, мир!"
Ok(Ok("Hello, мир!".into()))
); );
} }
} }

View File

@@ -25,7 +25,6 @@ pub struct Mailbox {
impl Mailbox { impl Mailbox {
/// Create new mailbox using email address and addressee name /// Create new mailbox using email address and addressee name
#[inline]
pub fn new(name: Option<String>, email: Address) -> Self { pub fn new(name: Option<String>, email: Address) -> Self {
Mailbox { name, email } Mailbox { name, email }
} }
@@ -110,32 +109,27 @@ pub struct Mailboxes(Vec<Mailbox>);
impl Mailboxes { impl Mailboxes {
/// Create mailboxes list /// Create mailboxes list
#[inline]
pub fn new() -> Self { pub fn new() -> Self {
Mailboxes(Vec::new()) Mailboxes(Vec::new())
} }
/// Add mailbox to a list /// Add mailbox to a list
#[inline]
pub fn with(mut self, mbox: Mailbox) -> Self { pub fn with(mut self, mbox: Mailbox) -> Self {
self.0.push(mbox); self.0.push(mbox);
self self
} }
/// Add mailbox to a list /// Add mailbox to a list
#[inline]
pub fn push(&mut self, mbox: Mailbox) { pub fn push(&mut self, mbox: Mailbox) {
self.0.push(mbox); self.0.push(mbox);
} }
/// Extract first mailbox /// Extract first mailbox
#[inline]
pub fn into_single(self) -> Option<Mailbox> { pub fn into_single(self) -> Option<Mailbox> {
self.into() self.into()
} }
/// Iterate over mailboxes /// Iterate over mailboxes
#[inline]
pub fn iter(&self) -> Iter<Mailbox> { pub fn iter(&self) -> Iter<Mailbox> {
self.0.iter() self.0.iter()
} }

View File

@@ -1,43 +1,45 @@
use crate::message::{ use crate::message::{
encoder::codec, encoder::codec,
header::{ContentTransferEncoding, ContentType, Header, Headers}, header::{ContentTransferEncoding, ContentType, Header, Headers},
EmailFormat,
}; };
use bytes::{Bytes, IntoBuf};
use mime::Mime; use mime::Mime;
use std::{
fmt::{Display, Error as FmtError, Formatter, Result as FmtResult},
str::from_utf8,
};
use textnonce::TextNonce; use textnonce::TextNonce;
/// MIME part variants /// MIME part variants
/// ///
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Part<B = Bytes> { pub enum Part {
/// Single part with content /// Single part with content
/// ///
Single(SinglePart<B>), Single(SinglePart),
/// Multiple parts of content /// Multiple parts of content
/// ///
Multi(MultiPart<B>), Multi(MultiPart),
} }
impl<B> Display for Part<B> impl EmailFormat for Part {
where fn format(&self, out: &mut Vec<u8>) {
B: AsRef<str>, match self {
{ Part::Single(part) => part.format(out),
fn fmt(&self, f: &mut Formatter) -> FmtResult { Part::Multi(part) => part.format(out),
match *self {
Part::Single(ref part) => part.fmt(f),
Part::Multi(ref part) => part.fmt(f),
} }
} }
} }
impl Part {
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
}
/// Parts of multipart body /// Parts of multipart body
/// ///
pub type Parts<B = Bytes> = Vec<Part<B>>; pub type Parts = Vec<Part>;
/// Creates builder for single part /// Creates builder for single part
/// ///
@@ -55,18 +57,16 @@ impl SinglePartBuilder {
} }
/// Set the header to singlepart /// Set the header to singlepart
#[inline]
pub fn header<H: Header>(mut self, header: H) -> Self { pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header); self.headers.set(header);
self self
} }
/// Build singlepart using body /// Build singlepart using body
#[inline] pub fn body<T: Into<Vec<u8>>>(self, body: T) -> SinglePart {
pub fn body<T>(self, body: T) -> SinglePart<T> {
SinglePart { SinglePart {
headers: self.headers, headers: self.headers,
body, body: body.into(),
} }
} }
} }
@@ -94,12 +94,12 @@ impl Default for SinglePartBuilder {
/// ``` /// ```
/// ///
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SinglePart<B = Bytes> { pub struct SinglePart {
headers: Headers, headers: Headers,
body: B, body: Vec<u8>,
} }
impl SinglePart<()> { impl SinglePart {
/// Creates a default builder for singlepart /// Creates a default builder for singlepart
pub fn builder() -> SinglePartBuilder { pub fn builder() -> SinglePartBuilder {
SinglePartBuilder::new() SinglePartBuilder::new()
@@ -129,7 +129,6 @@ impl SinglePart<()> {
/// Creates a singlepart builder with 8-bit encoding /// Creates a singlepart builder with 8-bit encoding
/// ///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::EightBit)`. /// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::EightBit)`.
#[inline]
pub fn eight_bit() -> SinglePartBuilder { pub fn eight_bit() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::EightBit) Self::builder().header(ContentTransferEncoding::EightBit)
} }
@@ -137,55 +136,38 @@ impl SinglePart<()> {
/// Creates a singlepart builder with binary encoding /// Creates a singlepart builder with binary encoding
/// ///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Binary)`. /// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Binary)`.
#[inline]
pub fn binary() -> SinglePartBuilder { pub fn binary() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::Binary) Self::builder().header(ContentTransferEncoding::Binary)
} }
}
impl<B> SinglePart<B> {
/// Get the transfer encoding
#[inline]
pub fn encoding(&self) -> Option<&ContentTransferEncoding> {
self.headers.get()
}
/// Get the headers from singlepart /// Get the headers from singlepart
#[inline]
pub fn headers(&self) -> &Headers { pub fn headers(&self) -> &Headers {
&self.headers &self.headers
} }
/// Get a mutable reference to the headers /// Read the body from singlepart
#[inline] pub fn body_ref(&self) -> &[u8] {
pub fn headers_mut(&mut self) -> &mut Headers { &self.body
&mut self.headers
} }
/// Read the body from singlepart /// Get message content formatted for SMTP
#[inline] pub fn formatted(&self) -> Vec<u8> {
pub fn body_ref(&self) -> &B { let mut out = Vec::new();
&self.body self.format(&mut out);
out
} }
} }
impl<B> Display for SinglePart<B> impl EmailFormat for SinglePart {
where fn format(&self, out: &mut Vec<u8>) {
B: AsRef<str>, out.extend_from_slice(self.headers.to_string().as_bytes());
{ out.extend_from_slice(b"\r\n");
fn fmt(&self, f: &mut Formatter) -> FmtResult {
self.headers.fmt(f)?;
"\r\n".fmt(f)?;
let body = self.body.as_ref(); let encoding = self.headers.get::<ContentTransferEncoding>();
let mut encoder = codec(self.encoding()); let mut encoder = codec(encoding);
let result = encoder
.encode_all(&body.into_buf())
.map_err(|_| FmtError::default())?;
let body = from_utf8(&result).map_err(|_| FmtError::default())?;
body.fmt(f)?; out.extend_from_slice(&encoder.encode(&self.body));
"\r\n".fmt(f) out.extend_from_slice(b"\r\n");
} }
} }
@@ -255,7 +237,6 @@ pub struct MultiPartBuilder {
impl MultiPartBuilder { impl MultiPartBuilder {
/// Creates default multipart builder /// Creates default multipart builder
#[inline]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
headers: Headers::new(), headers: Headers::new(),
@@ -263,14 +244,12 @@ impl MultiPartBuilder {
} }
/// Set a header /// Set a header
#[inline]
pub fn header<H: Header>(mut self, header: H) -> Self { pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header); self.headers.set(header);
self self
} }
/// Set `Content-Type` header using [`MultiPartKind`] /// Set `Content-Type` header using [`MultiPartKind`]
#[inline]
pub fn kind(self, kind: MultiPartKind) -> Self { pub fn kind(self, kind: MultiPartKind) -> Self {
self.header(ContentType(kind.into())) self.header(ContentType(kind.into()))
} }
@@ -286,8 +265,7 @@ impl MultiPartBuilder {
} }
/// Creates multipart without parts /// Creates multipart without parts
#[inline] pub fn build(self) -> MultiPart {
pub fn build<B>(self) -> MultiPart<B> {
MultiPart { MultiPart {
headers: self.headers, headers: self.headers,
parts: Vec::new(), parts: Vec::new(),
@@ -295,20 +273,17 @@ impl MultiPartBuilder {
} }
/// Creates multipart using part /// Creates multipart using part
#[inline] pub fn part(self, part: Part) -> MultiPart {
pub fn part<B>(self, part: Part<B>) -> MultiPart<B> {
self.build().part(part) self.build().part(part)
} }
/// Creates multipart using singlepart /// Creates multipart using singlepart
#[inline] pub fn singlepart(self, part: SinglePart) -> MultiPart {
pub fn singlepart<B>(self, part: SinglePart<B>) -> MultiPart<B> {
self.build().singlepart(part) self.build().singlepart(part)
} }
/// Creates multipart using multipart /// Creates multipart using multipart
#[inline] pub fn multipart(self, part: MultiPart) -> MultiPart {
pub fn multipart<B>(self, part: MultiPart<B>) -> MultiPart<B> {
self.build().multipart(part) self.build().multipart(part)
} }
} }
@@ -322,14 +297,13 @@ impl Default for MultiPartBuilder {
/// Multipart variant with parts /// Multipart variant with parts
/// ///
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MultiPart<B = Bytes> { pub struct MultiPart {
headers: Headers, headers: Headers,
parts: Parts<B>, parts: Parts,
} }
impl MultiPart<()> { impl MultiPart {
/// Creates multipart builder /// Creates multipart builder
#[inline]
pub fn builder() -> MultiPartBuilder { pub fn builder() -> MultiPartBuilder {
MultiPartBuilder::new() MultiPartBuilder::new()
} }
@@ -337,7 +311,6 @@ impl MultiPart<()> {
/// Creates mixed multipart builder /// Creates mixed multipart builder
/// ///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Mixed)` /// Shortcut for `MultiPart::builder().kind(MultiPartKind::Mixed)`
#[inline]
pub fn mixed() -> MultiPartBuilder { pub fn mixed() -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Mixed) MultiPart::builder().kind(MultiPartKind::Mixed)
} }
@@ -345,7 +318,6 @@ impl MultiPart<()> {
/// Creates alternative multipart builder /// Creates alternative multipart builder
/// ///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Alternative)` /// Shortcut for `MultiPart::builder().kind(MultiPartKind::Alternative)`
#[inline]
pub fn alternative() -> MultiPartBuilder { pub fn alternative() -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Alternative) MultiPart::builder().kind(MultiPartKind::Alternative)
} }
@@ -353,97 +325,90 @@ impl MultiPart<()> {
/// Creates related multipart builder /// Creates related multipart builder
/// ///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Related)` /// Shortcut for `MultiPart::builder().kind(MultiPartKind::Related)`
#[inline]
pub fn related() -> MultiPartBuilder { pub fn related() -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Related) MultiPart::builder().kind(MultiPartKind::Related)
} }
}
impl<B> MultiPart<B> {
/// Add part to multipart /// Add part to multipart
#[inline] pub fn part(mut self, part: Part) -> Self {
pub fn part(mut self, part: Part<B>) -> Self {
self.parts.push(part); self.parts.push(part);
self self
} }
/// Add single part to multipart /// Add single part to multipart
#[inline] pub fn singlepart(mut self, part: SinglePart) -> Self {
pub fn singlepart(mut self, part: SinglePart<B>) -> Self {
self.parts.push(Part::Single(part)); self.parts.push(Part::Single(part));
self self
} }
/// Add multi part to multipart /// Add multi part to multipart
#[inline] pub fn multipart(mut self, part: MultiPart) -> Self {
pub fn multipart(mut self, part: MultiPart<B>) -> Self {
self.parts.push(Part::Multi(part)); self.parts.push(Part::Multi(part));
self self
} }
/// Get the boundary of multipart contents /// Get the boundary of multipart contents
#[inline]
pub fn boundary(&self) -> String { pub fn boundary(&self) -> String {
let content_type = &self.headers.get::<ContentType>().unwrap().0; let content_type = &self.headers.get::<ContentType>().unwrap().0;
content_type.get_param("boundary").unwrap().as_str().into() content_type.get_param("boundary").unwrap().as_str().into()
} }
/// Get the headers from the multipart /// Get the headers from the multipart
#[inline]
pub fn headers(&self) -> &Headers { pub fn headers(&self) -> &Headers {
&self.headers &self.headers
} }
/// Get a mutable reference to the headers /// Get a mutable reference to the headers
#[inline]
pub fn headers_mut(&mut self) -> &mut Headers { pub fn headers_mut(&mut self) -> &mut Headers {
&mut self.headers &mut self.headers
} }
/// Get the parts from the multipart /// Get the parts from the multipart
#[inline] pub fn parts(&self) -> &Parts {
pub fn parts(&self) -> &Parts<B> {
&self.parts &self.parts
} }
/// Get a mutable reference to the parts /// Get a mutable reference to the parts
#[inline] pub fn parts_mut(&mut self) -> &mut Parts {
pub fn parts_mut(&mut self) -> &mut Parts<B> {
&mut self.parts &mut self.parts
} }
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
} }
impl<B> Display for MultiPart<B> impl EmailFormat for MultiPart {
where fn format(&self, out: &mut Vec<u8>) {
B: AsRef<str>, out.extend_from_slice(self.headers.to_string().as_bytes());
{ out.extend_from_slice(b"\r\n");
fn fmt(&self, f: &mut Formatter) -> FmtResult {
self.headers.fmt(f)?;
"\r\n".fmt(f)?;
let boundary = self.boundary(); let boundary = self.boundary();
for part in &self.parts { for part in &self.parts {
"--".fmt(f)?; out.extend_from_slice(b"--");
boundary.fmt(f)?; out.extend_from_slice(boundary.as_bytes());
"\r\n".fmt(f)?; out.extend_from_slice(b"\r\n");
part.fmt(f)?; part.format(out);
} }
"--".fmt(f)?; out.extend_from_slice(b"--");
boundary.fmt(f)?; out.extend_from_slice(boundary.as_bytes());
"--\r\n".fmt(f) out.extend_from_slice(b"--\r\n");
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{MultiPart, Part, SinglePart}; use super::*;
use crate::message::header; use crate::message::header;
#[test] #[test]
fn single_part_binary() { fn single_part_binary() {
let part: SinglePart<String> = SinglePart::builder() let part = SinglePart::builder()
.header(header::ContentType( .header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(), "text/plain; charset=utf8".parse().unwrap(),
)) ))
@@ -451,7 +416,7 @@ mod test {
.body(String::from("Текст письма в уникоде")); .body(String::from("Текст письма в уникоде"));
assert_eq!( assert_eq!(
format!("{}", part), String::from_utf8(part.formatted()).unwrap(),
concat!( concat!(
"Content-Type: text/plain; charset=utf8\r\n", "Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: binary\r\n", "Content-Transfer-Encoding: binary\r\n",
@@ -463,7 +428,7 @@ mod test {
#[test] #[test]
fn single_part_quoted_printable() { fn single_part_quoted_printable() {
let part: SinglePart<String> = SinglePart::builder() let part = SinglePart::builder()
.header(header::ContentType( .header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(), "text/plain; charset=utf8".parse().unwrap(),
)) ))
@@ -471,7 +436,7 @@ mod test {
.body(String::from("Текст письма в уникоде")); .body(String::from("Текст письма в уникоде"));
assert_eq!( assert_eq!(
format!("{}", part), String::from_utf8(part.formatted()).unwrap(),
concat!( concat!(
"Content-Type: text/plain; charset=utf8\r\n", "Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n", "Content-Transfer-Encoding: quoted-printable\r\n",
@@ -484,7 +449,7 @@ mod test {
#[test] #[test]
fn single_part_base64() { fn single_part_base64() {
let part: SinglePart<String> = SinglePart::builder() let part = SinglePart::builder()
.header(header::ContentType( .header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(), "text/plain; charset=utf8".parse().unwrap(),
)) ))
@@ -492,7 +457,7 @@ mod test {
.body(String::from("Текст письма в уникоде")); .body(String::from("Текст письма в уникоде"));
assert_eq!( assert_eq!(
format!("{}", part), String::from_utf8(part.formatted()).unwrap(),
concat!( concat!(
"Content-Type: text/plain; charset=utf8\r\n", "Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: base64\r\n", "Content-Transfer-Encoding: base64\r\n",
@@ -504,7 +469,7 @@ mod test {
#[test] #[test]
fn multi_part_mixed() { fn multi_part_mixed() {
let part: MultiPart<String> = MultiPart::mixed() let part = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single( .part(Part::Single(
SinglePart::builder() SinglePart::builder()
@@ -531,7 +496,7 @@ mod test {
.body(String::from("int main() { return 0; }")), .body(String::from("int main() { return 0; }")),
); );
assert_eq!(format!("{}", part), assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/mixed;", concat!("Content-Type: multipart/mixed;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n", " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n", "\r\n",
@@ -551,7 +516,7 @@ mod test {
#[test] #[test]
fn multi_part_alternative() { fn multi_part_alternative() {
let part: MultiPart<String> = MultiPart::alternative() let part = MultiPart::alternative()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(SinglePart::builder() .part(Part::Single(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap())) .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
@@ -562,7 +527,7 @@ mod test {
.header(header::ContentTransferEncoding::Binary) .header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>"))); .body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
assert_eq!(format!("{}", part), assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/alternative;", concat!("Content-Type: multipart/alternative;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n", " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n", "\r\n",
@@ -581,7 +546,7 @@ mod test {
#[test] #[test]
fn multi_part_mixed_related() { fn multi_part_mixed_related() {
let part: MultiPart<String> = MultiPart::mixed() let part = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.multipart(MultiPart::related() .multipart(MultiPart::related()
.boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh") .boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh")
@@ -603,7 +568,7 @@ mod test {
.header(header::ContentTransferEncoding::Binary) .header(header::ContentTransferEncoding::Binary)
.body(String::from("int main() { return 0; }"))); .body(String::from("int main() { return 0; }")));
assert_eq!(format!("{}", part), assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/mixed;", concat!("Content-Type: multipart/mixed;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n", " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n", "\r\n",

View File

@@ -16,16 +16,16 @@ use crate::{
message::header::{EmailDate, Header, Headers, MailboxesHeader}, message::header::{EmailDate, Header, Headers, MailboxesHeader},
Envelope, Error as EmailError, Envelope, Error as EmailError,
}; };
use bytes::Bytes; use std::{convert::TryFrom, time::SystemTime};
use std::{
convert::TryFrom,
fmt::{Display, Formatter, Result as FmtResult},
time::SystemTime,
};
use uuid::Uuid; use uuid::Uuid;
const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost"; const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
pub trait EmailFormat {
// Use a writer?
fn format(&self, out: &mut Vec<u8>);
}
/// A builder for messages /// A builder for messages
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MessageBuilder { pub struct MessageBuilder {
@@ -34,7 +34,6 @@ pub struct MessageBuilder {
impl MessageBuilder { impl MessageBuilder {
/// Creates a new default message builder /// Creates a new default message builder
#[inline]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
headers: Headers::new(), headers: Headers::new(),
@@ -42,7 +41,6 @@ impl MessageBuilder {
} }
/// Set custom header to message /// Set custom header to message
#[inline]
pub fn header<H: Header>(mut self, header: H) -> Self { pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header); self.headers.set(header);
self self
@@ -61,7 +59,6 @@ impl MessageBuilder {
/// Add `Date` header to message /// Add `Date` header to message
/// ///
/// Shortcut for `self.header(header::Date(date))`. /// Shortcut for `self.header(header::Date(date))`.
#[inline]
pub fn date(self, date: EmailDate) -> Self { pub fn date(self, date: EmailDate) -> Self {
self.header(header::Date(date)) self.header(header::Date(date))
} }
@@ -69,7 +66,6 @@ impl MessageBuilder {
/// Set `Date` header using current date/time /// Set `Date` header using current date/time
/// ///
/// Shortcut for `self.date(SystemTime::now())`. /// Shortcut for `self.date(SystemTime::now())`.
#[inline]
pub fn date_now(self) -> Self { pub fn date_now(self) -> Self {
self.date(SystemTime::now().into()) self.date(SystemTime::now().into())
} }
@@ -77,7 +73,6 @@ impl MessageBuilder {
/// Set `Subject` header to message /// Set `Subject` header to message
/// ///
/// Shortcut for `self.header(header::Subject(subject.into()))`. /// Shortcut for `self.header(header::Subject(subject.into()))`.
#[inline]
pub fn subject<S: Into<String>>(self, subject: S) -> Self { pub fn subject<S: Into<String>>(self, subject: S) -> Self {
self.header(header::Subject(subject.into())) self.header(header::Subject(subject.into()))
} }
@@ -85,8 +80,9 @@ impl MessageBuilder {
/// Set `Mime-Version` header to 1.0 /// Set `Mime-Version` header to 1.0
/// ///
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`. /// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
#[inline] ///
pub fn mime_1_0(self) -> Self { /// Not exposed as it is set by body methods
fn mime_1_0(self) -> Self {
self.header(header::MIME_VERSION_1_0) self.header(header::MIME_VERSION_1_0)
} }
@@ -95,7 +91,6 @@ impl MessageBuilder {
/// https://tools.ietf.org/html/rfc5322#section-3.6.2 /// https://tools.ietf.org/html/rfc5322#section-3.6.2
/// ///
/// Shortcut for `self.header(header::Sender(mbox))`. /// Shortcut for `self.header(header::Sender(mbox))`.
#[inline]
pub fn sender(self, mbox: Mailbox) -> Self { pub fn sender(self, mbox: Mailbox) -> Self {
self.header(header::Sender(mbox)) self.header(header::Sender(mbox))
} }
@@ -105,7 +100,6 @@ impl MessageBuilder {
/// https://tools.ietf.org/html/rfc5322#section-3.6.2 /// https://tools.ietf.org/html/rfc5322#section-3.6.2
/// ///
/// Shortcut for `self.mailbox(header::From(mbox))`. /// Shortcut for `self.mailbox(header::From(mbox))`.
#[inline]
pub fn from(self, mbox: Mailbox) -> Self { pub fn from(self, mbox: Mailbox) -> Self {
self.mailbox(header::From(mbox.into())) self.mailbox(header::From(mbox.into()))
} }
@@ -115,7 +109,6 @@ impl MessageBuilder {
/// https://tools.ietf.org/html/rfc5322#section-3.6.2 /// https://tools.ietf.org/html/rfc5322#section-3.6.2
/// ///
/// Shortcut for `self.mailbox(header::ReplyTo(mbox))`. /// Shortcut for `self.mailbox(header::ReplyTo(mbox))`.
#[inline]
pub fn reply_to(self, mbox: Mailbox) -> Self { pub fn reply_to(self, mbox: Mailbox) -> Self {
self.mailbox(header::ReplyTo(mbox.into())) self.mailbox(header::ReplyTo(mbox.into()))
} }
@@ -123,7 +116,6 @@ impl MessageBuilder {
/// Set or add mailbox to `To` header /// Set or add mailbox to `To` header
/// ///
/// Shortcut for `self.mailbox(header::To(mbox))`. /// Shortcut for `self.mailbox(header::To(mbox))`.
#[inline]
pub fn to(self, mbox: Mailbox) -> Self { pub fn to(self, mbox: Mailbox) -> Self {
self.mailbox(header::To(mbox.into())) self.mailbox(header::To(mbox.into()))
} }
@@ -131,7 +123,6 @@ impl MessageBuilder {
/// Set or add mailbox to `Cc` header /// Set or add mailbox to `Cc` header
/// ///
/// Shortcut for `self.mailbox(header::Cc(mbox))`. /// Shortcut for `self.mailbox(header::Cc(mbox))`.
#[inline]
pub fn cc(self, mbox: Mailbox) -> Self { pub fn cc(self, mbox: Mailbox) -> Self {
self.mailbox(header::Cc(mbox.into())) self.mailbox(header::Cc(mbox.into()))
} }
@@ -139,21 +130,18 @@ impl MessageBuilder {
/// Set or add mailbox to `Bcc` header /// Set or add mailbox to `Bcc` header
/// ///
/// Shortcut for `self.mailbox(header::Bcc(mbox))`. /// Shortcut for `self.mailbox(header::Bcc(mbox))`.
#[inline]
pub fn bcc(self, mbox: Mailbox) -> Self { pub fn bcc(self, mbox: Mailbox) -> Self {
self.mailbox(header::Bcc(mbox.into())) self.mailbox(header::Bcc(mbox.into()))
} }
/// Set or add message id to [`In-Reply-To` /// Set or add message id to [`In-Reply-To`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4) /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
#[inline]
pub fn in_reply_to(self, id: String) -> Self { pub fn in_reply_to(self, id: String) -> Self {
self.header(header::InReplyTo(id)) self.header(header::InReplyTo(id))
} }
/// Set or add message id to [`References` /// Set or add message id to [`References`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4) /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
#[inline]
pub fn references(self, id: String) -> Self { pub fn references(self, id: String) -> Self {
self.header(header::References(id)) self.header(header::References(id))
} }
@@ -165,7 +153,6 @@ impl MessageBuilder {
/// ///
/// If `None` is provided, an id will be generated in the /// If `None` is provided, an id will be generated in the
/// `<UUID@HOSTNAME>`. /// `<UUID@HOSTNAME>`.
#[inline]
pub fn message_id(self, id: Option<String>) -> Self { pub fn message_id(self, id: Option<String>) -> Self {
match id { match id {
Some(i) => self.header(header::MessageId(i)), Some(i) => self.header(header::MessageId(i)),
@@ -188,85 +175,116 @@ impl MessageBuilder {
/// Set [User-Agent /// Set [User-Agent
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004) /// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004)
#[inline]
pub fn user_agent(self, id: String) -> Self { pub fn user_agent(self, id: String) -> Self {
self.header(header::UserAgent(id)) self.header(header::UserAgent(id))
} }
fn insert_missing_headers(self) -> Self { fn insert_missing_headers(self) -> Self {
let mut new = self;
// Insert Date if missing // Insert Date if missing
if self.headers.get::<header::Date>().is_none() { new = if new.headers.get::<header::Date>().is_none() {
self.date_now() new.date_now()
} else { } else {
self new
} };
// TODO insert sender if needed? // TODO insert sender if needed?
new
} }
// TODO: High-level methods for attachments and embedded files // TODO: High-level methods for attachments and embedded files
/// Create message by joining content /// Create message from body
#[inline] fn build(self, body: Body) -> Result<Message, EmailError> {
fn build<T>(self, body: T, split: bool) -> Result<Message<T>, EmailError> {
let res = self.insert_missing_headers(); let res = self.insert_missing_headers();
let envelope = Envelope::try_from(&res.headers)?; let envelope = Envelope::try_from(&res.headers)?;
Ok(Message { Ok(Message {
headers: res.headers, headers: res.headers,
split,
body, body,
envelope, envelope,
}) })
} }
/// Create message using body // In theory having a body is optional
#[inline]
pub fn body<T>(self, body: T) -> Result<Message<T>, EmailError> { /// Plain ASCII body
self.build(body, true) ///
/// *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() {
return Err(EmailError::NonAsciiChars);
}
self.build(Body::Raw(body))
} }
/// Create message using mime body ([`MultiPart`](::MultiPart) or [`SinglePart`](::SinglePart)) /// Create message using mime body ([`MultiPart`](::MultiPart))
// FIXME restrict usage on MIME? pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
#[inline] self.mime_1_0().build(Body::Mime(Part::Multi(part)))
pub fn mime_body<T>(self, body: T) -> Result<Message<T>, EmailError> { }
self.mime_1_0().build(body, false)
/// Create message using mime body ([`SinglePart`](::SinglePart)
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
self.mime_1_0().build(Body::Mime(Part::Single(part)))
} }
} }
/// Email message which can be formatted /// Email message which can be formatted
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Message<B = Bytes> { pub struct Message {
headers: Headers, headers: Headers,
split: bool, body: Body,
body: B,
envelope: Envelope, envelope: Envelope,
} }
impl Message<()> { #[derive(Clone, Debug)]
enum Body {
Mime(Part),
Raw(String),
}
impl Message {
/// Create a new message builder without headers /// Create a new message builder without headers
#[inline]
pub fn builder() -> MessageBuilder { pub fn builder() -> MessageBuilder {
MessageBuilder::new() MessageBuilder::new()
} }
}
impl<B> Message<B> {
/// Get the headers from the Message /// Get the headers from the Message
#[inline]
pub fn headers(&self) -> &Headers { pub fn headers(&self) -> &Headers {
&self.headers &self.headers
} }
/// Read the body /// Get `Message` envelope
#[inline]
pub fn body_ref(&self) -> &B {
&self.body
}
/// Try to extract envelope data from `Message` headers
#[inline]
pub fn envelope(&self) -> &Envelope { pub fn envelope(&self) -> &Envelope {
&self.envelope &self.envelope
} }
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
}
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) => {
out.extend_from_slice(b"\r\n");
out.extend(r.as_bytes())
}
}
}
} }
impl Default for MessageBuilder { impl Default for MessageBuilder {
@@ -275,21 +293,6 @@ impl Default for MessageBuilder {
} }
} }
impl<B> Display for Message<B>
where
B: Display,
{
fn fmt(&self, f: &mut Formatter) -> FmtResult {
self.headers.fmt(f)?;
if self.split {
f.write_str("\r\n")?;
}
self.body.fmt(f)
}
}
// An email is Message + Envelope
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::message::{header, mailbox::Mailbox, Message}; use crate::message::{header, mailbox::Mailbox, Message};
@@ -315,7 +318,7 @@ mod test {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
format!("{}", email), String::from_utf8(email.formatted()).unwrap(),
concat!( concat!(
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", "Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n", "From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",

View File

@@ -1,5 +1,7 @@
use std::str::from_utf8; use std::str::from_utf8;
// https://tools.ietf.org/html/rfc1522
fn allowed_char(c: char) -> bool { fn allowed_char(c: char) -> bool {
c >= 1 as char && c <= 9 as char c >= 1 as char && c <= 9 as char
|| c == 11 as char || c == 11 as char

View File

@@ -36,7 +36,7 @@ struct SerializableEmail {
message: Vec<u8>, message: Vec<u8>,
} }
impl<'a, B> Transport<'a, B> for FileTransport { impl<'a> Transport<'a> for FileTransport {
type Result = FileResult; type Result = FileResult;
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result { fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result {

View File

@@ -36,7 +36,7 @@ impl SendmailTransport {
} }
} }
impl<'a, B> Transport<'a, B> for SendmailTransport { impl<'a> Transport<'a> for SendmailTransport {
type Result = SendmailResult; type Result = SendmailResult;
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result { fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result {

View File

@@ -437,7 +437,7 @@ impl<'a> SmtpTransport {
} }
} }
impl<'a, B> Transport<'a, B> for SmtpTransport { impl<'a> Transport<'a> for SmtpTransport {
type Result = SmtpResult; type Result = SmtpResult;
/// Sends an email /// Sends an email

View File

@@ -5,7 +5,7 @@
use crate::Envelope; use crate::Envelope;
use crate::Transport; use crate::Transport;
use log::info; use log::info;
use std::fmt::Display;
/// This transport logs the message envelope and returns the given response /// This transport logs the message envelope and returns the given response
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -28,10 +28,7 @@ impl StubTransport {
/// SMTP result type /// SMTP result type
pub type StubResult = Result<(), ()>; pub type StubResult = Result<(), ()>;
impl<'a, B> Transport<'a, B> for StubTransport impl<'a> Transport<'a> for StubTransport {
where
B: Display,
{
type Result = StubResult; type Result = StubResult;
fn send_raw(&mut self, envelope: &Envelope, _email: &[u8]) -> Self::Result { fn send_raw(&mut self, envelope: &Envelope, _email: &[u8]) -> Self::Result {

View File

@@ -20,7 +20,7 @@ mod test {
.body("Be happy!") .body("Be happy!")
.unwrap(); .unwrap();
let result = sender.send(email); let result = sender.send(&email);
let id = result.unwrap(); let id = result.unwrap();
let file = temp_dir().join(format!("{}.json", id)); let file = temp_dir().join(format!("{}.json", id));
@@ -30,7 +30,7 @@ mod test {
assert_eq!( assert_eq!(
buffer, buffer,
"{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"message\":[70,114,111,109,58,32,78,111,66,111,100,121,32,60,110,111,98,111,100,121,64,100,111,109,97,105,110,46,116,108,100,62,13,10,82,101,112,108,121,45,84,111,58,32,89,117,105,110,32,60,121,117,105,110,64,100,111,109,97,105,110,46,116,108,100,62,13,10,84,111,58,32,72,101,105,32,60,104,101,105,64,100,111,109,97,105,110,46,116,108,100,62,13,10,83,117,98,106,101,99,116,58,32,72,97,112,112,121,32,110,101,119,32,121,101,97,114,13,10,68,97,116,101,58,32,84,117,101,44,32,49,53,32,78,111,118,32,49,57,57,52,32,48,56,58,49,50,58,51,49,32,71,77,84,13,10,13,10,66,101,32,104,97,112,112,121,33]}"); "{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"raw_message\":null,\"message\":\"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!\"}");
remove_file(file).unwrap(); remove_file(file).unwrap();
} }
} }

View File

@@ -14,7 +14,7 @@ mod test {
.body("Be happy!") .body("Be happy!")
.unwrap(); .unwrap();
let result = sender.send(email); let result = sender.send(&email);
println!("{:?}", result); println!("{:?}", result);
assert!(result.is_ok()); assert!(result.is_ok());
} }

View File

@@ -15,7 +15,7 @@ mod test {
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None) SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
.unwrap() .unwrap()
.transport() .transport()
.send(email) .send(&email)
.unwrap(); .unwrap();
} }
} }

View File

@@ -12,6 +12,6 @@ fn stub_transport() {
.body("Be happy!") .body("Be happy!")
.unwrap(); .unwrap();
sender_ok.send(email.clone()).unwrap(); sender_ok.send(&email.clone()).unwrap();
sender_ko.send(email).unwrap_err(); sender_ko.send(&email).unwrap_err();
} }

View File

@@ -14,7 +14,7 @@ The easiest way how we can create email message with simple string.
# extern crate lettre; # extern crate lettre;
use lettre::message::Message; use lettre::message::Message;
let m: Message<&str> = Message::builder() let m = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
@@ -44,14 +44,14 @@ The more complex way is using MIME contents.
```rust ```rust
# extern crate lettre; # extern crate lettre;
use lettre::message::{header, Message, SinglePart}; use lettre::message::{header, Message, SinglePart, Part};
let m: Message<SinglePart<&str>> = Message::builder() let m = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.mime_body( .singlepart(
SinglePart::builder() SinglePart::builder()
.header(header::ContentType( .header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(), "text/plain; charset=utf8".parse().unwrap(),
@@ -82,14 +82,14 @@ And more advanced way of building message by using multipart MIME contents.
```rust ```rust
# extern crate lettre; # extern crate lettre;
use lettre::message::{header, Message, MultiPart, SinglePart}; use lettre::message::{header, Message, MultiPart, SinglePart, Part};
let m: Message<MultiPart<&str>> = Message::builder() let m = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.mime_body( .multipart(
MultiPart::mixed() MultiPart::mixed()
.multipart( .multipart(
MultiPart::alternative() MultiPart::alternative()

View File

@@ -37,7 +37,7 @@ fn main() {
let mut mailer = let mut mailer =
SmtpClient::new_unencrypted_localhost().unwrap().transport(); SmtpClient::new_unencrypted_localhost().unwrap().transport();
// Send the email // Send the email
let result = mailer.send(email); let result = mailer.send(&email);
assert!(result.is_ok()); assert!(result.is_ok());
} }
@@ -88,11 +88,11 @@ fn main() {
// Enable connection reuse // Enable connection reuse
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited).transport(); .connection_reuse(ConnectionReuseParameters::ReuseUnlimited).transport();
let result_1 = mailer.send(email_1); let result_1 = mailer.send(&email_1);
assert!(result_1.is_ok()); assert!(result_1.is_ok());
// The second email will use the same connection // The second email will use the same connection
let result_2 = mailer.send(email_2); let result_2 = mailer.send(&email_2);
assert!(result_2.is_ok()); assert!(result_2.is_ok());
// Explicitly close the SMTP transaction as we enabled connection reuse // Explicitly close the SMTP transaction as we enabled connection reuse
@@ -145,7 +145,7 @@ fn main() {
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited) .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.transport(); .transport();
let result = mailer.send(email); let result = mailer.send(&email);
assert!(result.is_ok()); assert!(result.is_ok());

View File

@@ -19,7 +19,7 @@ fn main() {
.unwrap(); .unwrap();
let mut sender = StubTransport::new_positive(); let mut sender = StubTransport::new_positive();
let result = sender.send(email); let result = sender.send(&email);
assert!(result.is_ok()); assert!(result.is_ok());
} }
``` ```