diff --git a/Cargo.toml b/Cargo.toml index 338d8ec..a0acd43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/benches/transport_smtp.rs b/benches/transport_smtp.rs index 665f7c0..d2dd2c6 100644 --- a/benches/transport_smtp.rs +++ b/benches/transport_smtp.rs @@ -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()); }) }); diff --git a/examples/smtp.rs b/examples/smtp.rs index b46fae4..6aa5abc 100644 --- a/examples/smtp.rs +++ b/examples/smtp.rs @@ -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"); diff --git a/examples/smtp_gmail.rs b/examples/smtp_gmail.rs index a7b23ba..b52590d 100644 --- a/examples/smtp_gmail.rs +++ b/examples/smtp_gmail.rs @@ -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"); diff --git a/src/address.rs b/src/address.rs index fd779d7..4f7da17 100644 --- a/src/address.rs +++ b/src/address.rs @@ -65,7 +65,6 @@ static LITERAL_RE: Lazy = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\ impl Address { /// Create email address from parts - #[inline] pub fn new, D: Into>(user: U, domain: D) -> Result { (user, domain).try_into() } diff --git a/src/error.rs b/src/error.rs index 6c8c6c4..0daa895 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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(), }) } diff --git a/src/lib.rs b/src/lib.rs index f04b60b..0663d67 100644 --- a/src/lib.rs +++ b/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) -> 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)] diff --git a/src/message/encoder.rs b/src/message/encoder.rs index 16a3ab7..599a214 100644 --- a/src/message/encoder.rs +++ b/src/message/encoder.rs @@ -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 { - Source(E), - Coding, -} - -impl Error for EncoderError where E: Debug + Display {} - -impl Display for EncoderError -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; - - /// Encode end of stream - /// - /// This proposed to use for stateful encoders like *base64*. - fn finish_chunk(&mut self) -> Result { - Ok(Bytes::new()) - } - /// Encode all data - fn encode_all(&mut self, source: &dyn Buf) -> Result { - 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; } /// 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 { - if chunk.bytes().iter().all(u8::is_ascii) { - self.line_wrapper.encode_chunk(chunk) + fn encode(&mut self, input: &[u8]) -> Vec { + 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 { - Ok(quoted_printable::encode(chunk.bytes()).into()) + fn encode(&mut self, input: &[u8]) -> Vec { + 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 { - 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 { - 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 { + 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 { - 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 { + 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 { - Ok(chunk.bytes().into()) + fn encode(&mut self, input: &[u8]) -> Vec { + input.into() } } @@ -261,27 +137,23 @@ pub fn codec(encoding: Option<&ContentTransferEncoding>) -> Box, email: Address) -> Self { Mailbox { name, email } } @@ -110,32 +109,27 @@ pub struct Mailboxes(Vec); 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 { self.into() } /// Iterate over mailboxes - #[inline] pub fn iter(&self) -> Iter { self.0.iter() } diff --git a/src/message/mimebody.rs b/src/message/mimebody.rs index 42eb7ea..1bb2137 100644 --- a/src/message/mimebody.rs +++ b/src/message/mimebody.rs @@ -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 { +pub enum Part { /// Single part with content /// - Single(SinglePart), + Single(SinglePart), /// Multiple parts of content /// - Multi(MultiPart), + Multi(MultiPart), } -impl Display for Part -where - B: AsRef, -{ - 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) { + 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 { + let mut out = Vec::new(); + self.format(&mut out); + out + } +} + /// Parts of multipart body /// -pub type Parts = Vec>; +pub type Parts = Vec; /// Creates builder for single part /// @@ -55,18 +57,16 @@ impl SinglePartBuilder { } /// Set the header to singlepart - #[inline] pub fn header(mut self, header: H) -> Self { self.headers.set(header); self } /// Build singlepart using body - #[inline] - pub fn body(self, body: T) -> SinglePart { + pub fn body>>(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 { +pub struct SinglePart { headers: Headers, - body: B, + body: Vec, } -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 SinglePart { - /// 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 + pub fn body_ref(&self) -> &[u8] { + &self.body } - /// Read the body from singlepart - #[inline] - pub fn body_ref(&self) -> &B { - &self.body + /// Get message content formatted for SMTP + pub fn formatted(&self) -> Vec { + let mut out = Vec::new(); + self.format(&mut out); + out } } -impl Display for SinglePart -where - B: AsRef, -{ - 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) { + 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::(); + 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(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(self) -> MultiPart { + 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(self, part: Part) -> MultiPart { + pub fn part(self, part: Part) -> MultiPart { self.build().part(part) } /// Creates multipart using singlepart - #[inline] - pub fn singlepart(self, part: SinglePart) -> MultiPart { + pub fn singlepart(self, part: SinglePart) -> MultiPart { self.build().singlepart(part) } /// Creates multipart using multipart - #[inline] - pub fn multipart(self, part: MultiPart) -> MultiPart { + 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 { +pub struct MultiPart { headers: Headers, - parts: Parts, + 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 MultiPart { /// Add part to multipart - #[inline] - pub fn part(mut self, part: Part) -> 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) -> 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) -> 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::().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 { + pub fn parts(&self) -> &Parts { &self.parts } /// Get a mutable reference to the parts - #[inline] - pub fn parts_mut(&mut self) -> &mut Parts { + pub fn parts_mut(&mut self) -> &mut Parts { &mut self.parts } + + /// Get message content formatted for SMTP + pub fn formatted(&self) -> Vec { + let mut out = Vec::new(); + self.format(&mut out); + out + } } -impl Display for MultiPart -where - B: AsRef, -{ - 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) { + 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 = 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 = 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 = 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 = 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 = 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("

Текст письма в уникоде

"))); - 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 = 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", diff --git a/src/message/mod.rs b/src/message/mod.rs index 3179e37..75e97a6 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -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); +} + /// 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(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>(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 /// ``. - #[inline] pub fn message_id(self, id: Option) -> 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::().is_none() { - self.date_now() + new = if new.headers.get::().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(self, body: T, split: bool) -> Result, EmailError> { + /// Create message from body + fn build(self, body: Body) -> Result { 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(self, body: T) -> Result, EmailError> { - self.build(body, true) + // In theory having a body is optional + + /// Plain ASCII body + /// + /// *WARNING*: Generally not what you want + pub fn body>(self, body: T) -> Result { + // 998 chars by line + // CR and LF MUST only occur together as CRLF; they MUST NOT appear + // independently in the body. + let body = body.into(); + + if !&body.is_ascii() { + return Err(EmailError::NonAsciiChars); + } + + self.build(Body::Raw(body)) } - /// Create message using mime body ([`MultiPart`](::MultiPart) or [`SinglePart`](::SinglePart)) - // FIXME restrict usage on MIME? - #[inline] - pub fn mime_body(self, body: T) -> Result, EmailError> { - self.mime_1_0().build(body, false) + /// Create message using mime body ([`MultiPart`](::MultiPart)) + pub fn multipart(self, part: MultiPart) -> Result { + self.mime_1_0().build(Body::Mime(Part::Multi(part))) + } + + /// Create message using mime body ([`SinglePart`](::SinglePart) + pub fn singlepart(self, part: SinglePart) -> Result { + self.mime_1_0().build(Body::Mime(Part::Single(part))) } } /// Email message which can be formatted #[derive(Clone, Debug)] -pub struct Message { +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 Message { /// 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 { + let mut out = Vec::new(); + self.format(&mut out); + out + } +} + +impl EmailFormat for Message { + fn format(&self, out: &mut Vec) { + 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 Display for Message -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?= \r\n", diff --git a/src/message/utf8_b.rs b/src/message/utf8_b.rs index 606aafe..71f7695 100644 --- a/src/message/utf8_b.rs +++ b/src/message/utf8_b.rs @@ -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 diff --git a/src/transport/file/mod.rs b/src/transport/file/mod.rs index af19d2a..423cd44 100644 --- a/src/transport/file/mod.rs +++ b/src/transport/file/mod.rs @@ -36,7 +36,7 @@ struct SerializableEmail { message: Vec, } -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 { diff --git a/src/transport/sendmail/mod.rs b/src/transport/sendmail/mod.rs index 7a30b2c..1b1a6c2 100644 --- a/src/transport/sendmail/mod.rs +++ b/src/transport/sendmail/mod.rs @@ -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 { diff --git a/src/transport/smtp/mod.rs b/src/transport/smtp/mod.rs index 089ef58..b6adfc2 100644 --- a/src/transport/smtp/mod.rs +++ b/src/transport/smtp/mod.rs @@ -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 diff --git a/src/transport/stub/mod.rs b/src/transport/stub/mod.rs index dcdf6dc..7ca4046 100644 --- a/src/transport/stub/mod.rs +++ b/src/transport/stub/mod.rs @@ -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 { diff --git a/tests/transport_file.rs b/tests/transport_file.rs index 2104a12..0c9eeaf 100644 --- a/tests/transport_file.rs +++ b/tests/transport_file.rs @@ -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 \\r\\nReply-To: Yuin \\r\\nTo: Hei \\r\\nSubject: Happy new year\\r\\nDate: Tue, 15 Nov 1994 08:12:31 GMT\\r\\n\\r\\nBe happy!\"}"); remove_file(file).unwrap(); } } diff --git a/tests/transport_sendmail.rs b/tests/transport_sendmail.rs index 447640c..c3d626a 100644 --- a/tests/transport_sendmail.rs +++ b/tests/transport_sendmail.rs @@ -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()); } diff --git a/tests/transport_smtp.rs b/tests/transport_smtp.rs index a830f9c..d0d6ac7 100644 --- a/tests/transport_smtp.rs +++ b/tests/transport_smtp.rs @@ -15,7 +15,7 @@ mod test { SmtpClient::new("127.0.0.1:2525", ClientSecurity::None) .unwrap() .transport() - .send(email) + .send(&email) .unwrap(); } } diff --git a/tests/transport_stub.rs b/tests/transport_stub.rs index ddd1723..0cfce42 100644 --- a/tests/transport_stub.rs +++ b/tests/transport_stub.rs @@ -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(); } diff --git a/website/src/creating-messages/email.md b/website/src/creating-messages/email.md index 6550b7b..e4092a1 100644 --- a/website/src/creating-messages/email.md +++ b/website/src/creating-messages/email.md @@ -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 ".parse().unwrap()) .reply_to("Yuin ".parse().unwrap()) .to("Hei ".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> = Message::builder() +let m = Message::builder() .from("NoBody ".parse().unwrap()) .reply_to("Yuin ".parse().unwrap()) .to("Hei ".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> = Message::builder() +let m = Message::builder() .from("NoBody ".parse().unwrap()) .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new year") - .mime_body( + .multipart( MultiPart::mixed() .multipart( MultiPart::alternative() diff --git a/website/src/sending-messages/smtp.md b/website/src/sending-messages/smtp.md index 79dd3c6..301bef5 100644 --- a/website/src/sending-messages/smtp.md +++ b/website/src/sending-messages/smtp.md @@ -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()); diff --git a/website/src/sending-messages/stub.md b/website/src/sending-messages/stub.md index 42d2539..1d75456 100644 --- a/website/src/sending-messages/stub.md +++ b/website/src/sending-messages/stub.md @@ -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()); } ```