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

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ fn main() {
.transport();
// Send the email
let result = mailer.send(email);
let result = mailer.send(&email);
if result.is_ok() {
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 {
/// Create email address from parts
#[inline]
pub fn new<U: Into<String>, D: Into<String>>(user: U, domain: D) -> Result<Self, AddressError> {
(user, domain).try_into()
}

View File

@@ -23,6 +23,8 @@ pub enum Error {
CannotParseFilename,
/// IO error
Io(std::io::Error),
/// Non-ASCII chars
NonAsciiChars,
}
impl Display for Error {
@@ -35,6 +37,7 @@ impl Display for Error {
Error::EmailMissingLocalPart => "missing local part in email address".to_string(),
Error::EmailMissingDomain => "missing domain in email address".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(),
})
}

View File

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

View File

@@ -1,61 +1,16 @@
use crate::message::header::ContentTransferEncoding;
use bytes::{Buf, BufMut, Bytes, BytesMut, IntoBuf};
use std::{
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"),
}
}
}
use std::io::Write;
use line_wrap::{line_wrap, crlf, LineEnding};
/// Encoder trait
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
fn encode_all(&mut self, source: &dyn Buf) -> Result<Bytes, ()> {
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()
})
}
fn encode(&mut self, input: &[u8]) -> Vec<u8>;
}
/// 7bit codec
///
/// WARNING: Panics when passed non-ascii chars
struct SevenBitCodec {
line_wrapper: EightBitCodec,
}
@@ -69,11 +24,11 @@ impl SevenBitCodec {
}
impl EncoderCodec for SevenBitCodec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
if chunk.bytes().iter().all(u8::is_ascii) {
self.line_wrapper.encode_chunk(chunk)
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
if input.iter().all(u8::is_ascii) {
self.line_wrapper.encode(input)
} else {
Err(())
panic!("")
}
}
}
@@ -89,8 +44,8 @@ impl QuotedPrintableCodec {
}
impl EncoderCodec for QuotedPrintableCodec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
Ok(quoted_printable::encode(chunk.bytes()).into())
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
quoted_printable::encode(input)
}
}
@@ -98,75 +53,20 @@ impl EncoderCodec for QuotedPrintableCodec {
///
struct Base64Codec {
line_wrapper: EightBitCodec,
last_padding: Bytes,
}
impl Base64Codec {
pub fn new() -> Self {
Base64Codec {
// TODO probably 78, 76 is for qp
line_wrapper: EightBitCodec::new().with_limit(78 - 2),
last_padding: Bytes::new(),
}
}
}
impl EncoderCodec for Base64Codec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
let in_len = self.last_padding.len() + chunk.remaining();
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())
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
self.line_wrapper.encode(base64::encode(input).as_bytes())
}
}
@@ -174,7 +74,6 @@ impl EncoderCodec for Base64Codec {
///
struct EightBitCodec {
max_length: usize,
line_bytes: usize,
}
const DEFAULT_MAX_LINE_LENGTH: usize = 1000 - 2;
@@ -183,7 +82,6 @@ impl EightBitCodec {
pub fn new() -> Self {
EightBitCodec {
max_length: DEFAULT_MAX_LINE_LENGTH,
line_bytes: 0,
}
}
@@ -194,37 +92,15 @@ impl EightBitCodec {
}
impl EncoderCodec for EightBitCodec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
let mut out = BytesMut::with_capacity(chunk.remaining() + 20);
let mut src = chunk.bytes()[..].into_buf();
while src.has_remaining() {
let line_break = src.bytes().iter().position(|b| *b == b'\n');
let mut split_pos = if let Some(line_break) = line_break {
line_break
} else {
src.remaining()
};
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())
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
let ending = &crlf();
let mut out = vec![0_u8; input.len() + input.len() / self.max_length * ending.len()];
let mut writer: &mut [u8] = out.as_mut();
writer.write_all(input).unwrap();
line_wrap(&mut out, input.len(), self.max_length, ending);
out
}
}
@@ -239,8 +115,8 @@ impl BinaryCodec {
}
impl EncoderCodec for BinaryCodec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
Ok(chunk.bytes().into())
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
input.into()
}
}
@@ -261,27 +137,23 @@ pub fn codec(encoding: Option<&ContentTransferEncoding>) -> Box<dyn EncoderCodec
#[cfg(test)]
mod test {
use super::{
Base64Codec, BinaryCodec, EightBitCodec, EncoderCodec, QuotedPrintableCodec, SevenBitCodec,
};
use bytes::IntoBuf;
use std::str::from_utf8;
use super::*;
#[test]
fn seven_bit_encode() {
let mut c = SevenBitCodec::new();
assert_eq!(
c.encode_chunk(&"Hello, world!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Hello, world!".into()))
&String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
"Hello, world!"
);
}
assert_eq!(
c.encode_chunk(&"Hello, мир!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Err(())
);
#[test]
#[should_panic]
fn seven_bit_encode_panic() {
let mut c = SevenBitCodec::new();
c.encode("Hello, мир!".as_bytes());
}
#[test]
@@ -289,16 +161,13 @@ mod test {
let mut c = QuotedPrintableCodec::new();
assert_eq!(
c.encode_chunk(&"Привет, мир!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok(
"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".into()
))
&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!(c.encode_chunk(&"Текст письма в уникоде".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
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())));
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]
@@ -306,112 +175,51 @@ mod test {
let mut c = Base64Codec::new();
assert_eq!(
c.encode_all(&"Привет, мир!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("0J/RgNC40LLQtdGCLCDQvNC40YAh".into()))
&String::from_utf8(c.encode("Привет, мир!".as_bytes())).unwrap(),
"0J/RgNC40LLQtdGCLCDQvNC40YAh"
);
assert_eq!(
c.encode_all(&"Текст письма в уникоде подлиннее.".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok(concat!(
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQ\r\n",
"vtC00LUg0L/QvtC00LvQuNC90L3QtdC1Lg=="
&String::from_utf8(c.encode("Текст письма в уникоде подлиннее.".as_bytes())).unwrap(),
concat!(
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQ",
"vtC00LUg0L/QvtC00LvQuNC90L3Q\r\ntdC1Lg=="
)
.into()))
);
}
#[test]
fn base64_encode_all() {
let mut c = Base64Codec::new();
);
assert_eq!(
c.encode_all(
&"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую."
.into_buf()
).map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok(
&String::from_utf8(c.encode(
"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую.".as_bytes()
)).unwrap(),
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRji4=").into()
))
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRji4=")
);
let mut c = Base64Codec::new();
assert_eq!(
c.encode_all(
&"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую это."
.into_buf()
).map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok(
&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").into()
))
"0L4u")
);
}
#[test]
fn base64_encode_chunked() {
fn base64_encodeed() {
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("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()))
&String::from_utf8(c.encode("Chunk.".as_bytes())).unwrap(),
"Q2h1bmsu"
);
}
@@ -420,15 +228,13 @@ mod test {
let mut c = EightBitCodec::new();
assert_eq!(
c.encode_chunk(&"Hello, world!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Hello, world!".into()))
&String::from_utf8(c.encode("Hello, world!".as_bytes())
).unwrap(), "Hello, world!"
);
assert_eq!(
c.encode_chunk(&"Hello, мир!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Hello, мир!".into()))
&String::from_utf8(c.encode("Hello, мир!".as_bytes())
).unwrap(), "Hello, мир!"
);
}
@@ -437,15 +243,13 @@ mod test {
let mut c = BinaryCodec::new();
assert_eq!(
c.encode_chunk(&"Hello, world!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Hello, world!".into()))
&String::from_utf8(c.encode("Hello, world!".as_bytes())
).unwrap(), "Hello, world!"
);
assert_eq!(
c.encode_chunk(&"Hello, мир!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Hello, мир!".into()))
&String::from_utf8(c.encode("Hello, мир!".as_bytes())
).unwrap(), "Hello, мир!"
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ struct SerializableEmail {
message: Vec<u8>,
}
impl<'a, B> Transport<'a, B> for FileTransport {
impl<'a> Transport<'a> for FileTransport {
type Result = FileResult;
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;
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;
/// Sends an email

View File

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

View File

@@ -20,7 +20,7 @@ mod test {
.body("Be happy!")
.unwrap();
let result = sender.send(email);
let result = sender.send(&email);
let id = result.unwrap();
let file = temp_dir().join(format!("{}.json", id));
@@ -30,7 +30,7 @@ mod test {
assert_eq!(
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();
}
}

View File

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

View File

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

View File

@@ -12,6 +12,6 @@ fn stub_transport() {
.body("Be happy!")
.unwrap();
sender_ok.send(email.clone()).unwrap();
sender_ko.send(email).unwrap_err();
sender_ok.send(&email.clone()).unwrap();
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;
use lettre::message::Message;
let m: Message<&str> = Message::builder()
let m = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
@@ -44,14 +44,14 @@ The more complex way is using MIME contents.
```rust
# 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())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.mime_body(
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
@@ -82,14 +82,14 @@ And more advanced way of building message by using multipart MIME contents.
```rust
# 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())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.mime_body(
.multipart(
MultiPart::mixed()
.multipart(
MultiPart::alternative()

View File

@@ -37,7 +37,7 @@ fn main() {
let mut mailer =
SmtpClient::new_unencrypted_localhost().unwrap().transport();
// Send the email
let result = mailer.send(email);
let result = mailer.send(&email);
assert!(result.is_ok());
}
@@ -88,11 +88,11 @@ fn main() {
// Enable connection reuse
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited).transport();
let result_1 = mailer.send(email_1);
let result_1 = mailer.send(&email_1);
assert!(result_1.is_ok());
// 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());
// Explicitly close the SMTP transaction as we enabled connection reuse
@@ -145,7 +145,7 @@ fn main() {
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.transport();
let result = mailer.send(email);
let result = mailer.send(&email);
assert!(result.is_ok());

View File

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