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:
@@ -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"]
|
||||
|
||||
@@ -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());
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
21
src/lib.rs
21
src/lib.rs
@@ -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)]
|
||||
|
||||
@@ -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, мир!"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ mod test {
|
||||
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.transport()
|
||||
.send(email)
|
||||
.send(&email)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user