From caf049791b8e7107734cd1f8917d51c91a2b80ad Mon Sep 17 00:00:00 2001 From: Philipp Korber Date: Fri, 16 Nov 2018 16:15:42 +0100 Subject: refactor: prepared to be merged into different repository --- smtp/Cargo.toml | 23 ++++ smtp/README.md | 88 ++++++++++++++ smtp/src/error.rs | 116 +++++++++++++++++++ smtp/src/lib.rs | 109 ++++++++++++++++++ smtp/src/request.rs | 299 ++++++++++++++++++++++++++++++++++++++++++++++++ smtp/src/resolve_all.rs | 81 +++++++++++++ smtp/src/send_mail.rs | 139 ++++++++++++++++++++++ 7 files changed, 855 insertions(+) create mode 100644 smtp/Cargo.toml create mode 100644 smtp/README.md create mode 100644 smtp/src/error.rs create mode 100644 smtp/src/lib.rs create mode 100644 smtp/src/request.rs create mode 100644 smtp/src/resolve_all.rs create mode 100644 smtp/src/send_mail.rs (limited to 'smtp') diff --git a/smtp/Cargo.toml b/smtp/Cargo.toml new file mode 100644 index 0000000..536a92e --- /dev/null +++ b/smtp/Cargo.toml @@ -0,0 +1,23 @@ +[package] +authors = ["Philipp Korber "] +name = "mail-smtp" +version = "0.2.0-wip" +categories = [] +description = "[internal/mail-api] combines mail-core with new-tokio-smtp" +documentation = "https://docs.rs/mail-smtp" +keywords = ["mail-api"] +license = "MIT OR Apache-2.0" +readme = "./README.md" +repository = "https://github.com/1aim/mail-smtp" + +[dependencies] +futures = "0.1" +failure = "0.1.1" +mail-core = { path="../core" } +mail-headers = { path="../headers"} +mail-internals = { path="../internals" } +new-tokio-smtp = "0.8.1" + +[features] +test-with-traceing = ["mail-internals/traceing"] +extended-api = [] \ No newline at end of file diff --git a/smtp/README.md b/smtp/README.md new file mode 100644 index 0000000..b02fade --- /dev/null +++ b/smtp/README.md @@ -0,0 +1,88 @@ +# mail-smtp   + +**Allows sending `mail-core` `Mail`'s through `new-tokio-smtp`** + +--- + + +This library binds together `new-tokio-smtp` and the `mail` crates. + +It can be used to send mail given as mail crates `Mail` instances +to a Mail Submission Agent (MSA). It could, theoretically also +be used to send to an MX, but this often needs additional functionality +for reliable usage which is not part of this crate. + +For ease of use this crate re-exports some of the most commonly used +parts from `new-tokio-smtp` including `ConnectionConfig`, +`ConnectionBuilder`, all authentication commands/methods (the +`auth` module) as well as useful types (in the `misc` module). + +The `send_mails` function is the simplest way to send a batch +of mails. Nevertheless it doesn't directly accept `Mail` instances, +instead it accepts `MailRequest` instances. This is needed, as +the sender/recipient(s) specified through the `Mail` headers +and those used fro smtp mail delivery are not necessary exactly +the same (e.g. for bounce back mails and some no-reply setups). + +# Example + +```rust ,no_run +extern crate futures; +//if you use the mail facade use the re-exports from it instead +extern crate mail_core; +extern crate mail_smtp; +#[macro_use] extern crate mail_headers; + +use futures::Future; +use mail_headers::*; +use mail_headers::components::Domain; +use mail_core::{Mail, default_impl::simple_context}; +use mail_smtp::{send_mails, ConnectionConfig}; + +fn main() { + // this is normally done _once per application instance_ + // and then stored in e.g. a lazy_static. Also `Domain` + // will implement `FromStr` in the future. + let ctx = simple_context::new( + Domain::from_unchecked("example.com".to_owned(), + // This should be "world" unique for the given domain + // to assure message and content ids are world unique. + "asdkds".parse().unwrap() + ).unwrap(); + + let mut mail = Mail::plain_text("Some body").unwrap(); + mail.set_headers(headers! { + _From: ["bla@example.com"], + _To: ["blub@example.com"], + Subject: "Some Mail" + }.unwrap()).unwrap(); + + // don't use unencrypted con for anything but testing and + // simplified examples + let con_config = ConnectionConfig::build_local_unencrypted().build(); + + let fut = send_mails(con_config, vec![mail.into()], ctx); + let results = fut.wait(); +} +``` + + +## Documentation + +Documentation can be [viewed on docs.rs](https://docs.rs/mail-smtp). +(once published) + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any +additional terms or conditions. diff --git a/smtp/src/error.rs b/smtp/src/error.rs new file mode 100644 index 0000000..17f76e5 --- /dev/null +++ b/smtp/src/error.rs @@ -0,0 +1,116 @@ +//! Module containing all custom errors. +use std::{io as std_io}; + +use new_tokio_smtp::error::{ + ConnectingFailed, + LogicError, GeneralError +}; + +use mail::error::MailError; +use headers::error::HeaderValidationError; + +/// Error used when sending a mail fails. +/// +/// Failing to encode a mail before sending +/// it also counts as a `MailSendError`, as +/// it's done "on the fly" when sending a mail. +#[derive(Debug, Fail)] +pub enum MailSendError { + + /// Something is wrong with the mail instance (e.g. it can't be encoded). + /// + /// This can happen for a number of reasons including: + /// + /// 1. Missing header fields. + /// 2. Invalid header fields. + /// 2. Encoding header fields fails. + /// 3. Loading resources failed (resources like e.g. appendix, logo embedded in html mail, etc.) + #[fail(display = "{}", _0)] + Mail(MailError), + + /// Sending the mail failed. + /// + /// This can happen for a number of reasons including: + /// 1. Server rejects mail transaction because of send or receiver + /// address or body data (e.g. body to long). + /// 2. Mail address requires smtputf8 support, which is not given. + /// 3. Server rejects sending the mail for other reasons (it's + /// closing, overloaded etc.). + #[fail(display = "{}", _0)] + Smtp(LogicError), + + /// Setting up the connection failed. + /// + /// Failures can include but are not limited to: + /// + /// - Connecting with TCP failed. + /// - Starting TLS failed. + /// - Server does not want to be used (e.g. failure on sending EHLO). + /// - Authentication failed. + #[fail(display = "{}", _0)] + Connecting(ConnectingFailed), + + /// An I/O error happened while using the connection. + /// + /// This is mainly for I/O errors after the setup of the connection + /// was successful, which normally includes sending Ehlo and Auth + /// commands. + #[fail(display = "{}", _0)] + Io(std_io::Error) +} + +impl From for MailSendError { + fn from(err: MailError) -> Self { + MailSendError::Mail(err) + } +} + +impl From for MailSendError { + fn from(err: LogicError) -> Self { + MailSendError::Smtp(err) + } +} + +impl From for MailSendError { + fn from(err: std_io::Error) -> Self { + MailSendError::Io(err) + } +} + +impl From for MailSendError { + fn from(err: ConnectingFailed) -> Self { + MailSendError::Connecting(err) + } +} + +impl From for MailSendError { + fn from(err: GeneralError) -> Self { + use self::GeneralError::*; + match err { + Connecting(err) => Self::from(err), + Cmd(err) => Self::from(err), + Io(err) => Self::from(err) + } + } +} + + +#[derive(Debug, Fail)] +pub enum OtherValidationError { + + #[fail(display = "no To header was present")] + NoTo +} + +impl From for HeaderValidationError { + + fn from(ove: OtherValidationError) -> Self { + HeaderValidationError::Custom(ove.into()) + } +} + +impl From for MailError { + fn from(ove: OtherValidationError) -> Self { + MailError::from(HeaderValidationError::from(ove)) + } +} \ No newline at end of file diff --git a/smtp/src/lib.rs b/smtp/src/lib.rs new file mode 100644 index 0000000..271fa46 --- /dev/null +++ b/smtp/src/lib.rs @@ -0,0 +1,109 @@ +//! This library binds together `new-tokio-smtp` and the `mail` crates. +//! +//! It can be used to send mail given as mail crates `Mail` instances +//! to a Mail Submission Agent (MSA). It could, theoretically also +//! be used to send to an MX, but this often needs additional functionality +//! for reliable usage which is not part of this crate. +//! +//! For ease of use this crate re-exports some of the most commonly used +//! parts from `new-tokio-smtp` including `ConnectionConfig`, +//! `ConnectionBuilder`, all authentication commands/methods (the +//! `auth` module) as well as useful types (in the `misc` module). +//! +//! The `send_mails` function is the simplest way to send a batch +//! of mails. Nevertheless it doesn't directly accept `Mail` instances, +//! instead it accepts `MailRequest` instances. This is needed, as +//! the sender/recipient(s) specified through the `Mail` headers +//! and those used fro smtp mail delivery are not necessary exactly +//! the same (e.g. for bounce back mails and some no-reply setups). +//! +//! # Example +//! +//! ```no_run +//! extern crate futures; +//! //if you use the mail facade use the re-exports from it instead +//! extern crate mail_core; +//! extern crate mail_smtp; +//! #[macro_use] extern crate mail_headers; +//! +//! use futures::Future; +//! use mail_headers::{ +//! headers::*, +//! header_components::Domain +//! }; +//! use mail_core::{Mail, default_impl::simple_context}; +//! use mail_smtp::{self as smtp, ConnectionConfig}; +//! +//! # fn main() { +//! // this is normally done _once per application instance_ +//! // and then stored in e.g. a lazy_static. Also `Domain` +//! // will implement `FromStr` in the future. +//! let ctx = simple_context::new(Domain::from_unchecked("example.com".to_owned()), "asdkds".parse().unwrap()) +//! .unwrap(); +//! +//! let mut mail = Mail::plain_text("Some body"); +//! mail.insert_headers(headers! { +//! _From: ["bla@example.com"], +//! _To: ["blub@example.com"], +//! Subject: "Some Mail" +//! }.unwrap()); +//! +//! // don't use unencrypted con for anything but testing and +//! // simplified examples +//! let con_config = ConnectionConfig::build_local_unencrypted().build(); +//! +//! let fut = smtp::send(mail.into(), con_config, ctx); +//! let results = fut.wait(); +//! # } +//! ``` +//! +//! +extern crate futures; +extern crate new_tokio_smtp; +extern crate mail_core as mail; +extern crate mail_internals; +#[cfg_attr(test, macro_use)] +extern crate mail_headers as headers; +#[macro_use] +extern crate failure; + +mod resolve_all; + +pub mod error; +mod request; +mod send_mail; + +pub use self::request::MailRequest; +#[cfg(feature="extended-api")] +pub use self::request::derive_envelop_data_from_mail; + +pub use self::send_mail::{send, send_batch}; +#[cfg(feature="extended-api")] +pub use self::send_mail::encode; + +pub use new_tokio_smtp::{ConnectionConfig, ConnectionBuilder}; + +pub mod auth { + //! Module containing authentification commands/methods. + //! + //! This Module is re-exported from `new-tokio-smtp` for + //! ease of use. + + pub use new_tokio_smtp::command::auth::*; + + /// Auth command for not doing anything on auth. + //FIXME: this currently still sends the noop cmd, + // replace it with some new "NoCommand" command. + pub type NoAuth = ::new_tokio_smtp::command::Noop; +} + +pub mod misc { + //! A small collection of usefull types re-exported from `new-tokio-smtp`. + pub use new_tokio_smtp::{ + ClientId, + Domain, + AddressLiteral, + SetupTls, + DefaultTlsSetup + }; +} \ No newline at end of file diff --git a/smtp/src/request.rs b/smtp/src/request.rs new file mode 100644 index 0000000..5f68437 --- /dev/null +++ b/smtp/src/request.rs @@ -0,0 +1,299 @@ +use std::mem; + +use new_tokio_smtp::send_mail::{ + self as smtp, + MailAddress, + EnvelopData +}; + +use mail_internals::{ + MailType, + encoder::{EncodingBuffer, EncodableInHeader}, + error::EncodingError +}; +use headers::{ + headers::{Sender, _From, _To}, + header_components::Mailbox, + error::{BuildInValidationError} +}; +use mail::{ + Mail, + error::{MailError, OtherValidationError} +}; + +use ::error::{ OtherValidationError as AnotherOtherValidationError }; + +/// This type contains a mail and potentially some envelop data. +/// +/// It is needed as in some edge cases the smtp envelop data (i.e. +/// smtp from and smtp recipient) can not be correctly derived +/// from the mail. +/// +/// The default usage is to directly turn a `Mail` into a `MailRequest` +/// by either using `MailRequest::new`, `MailRequest::from` or `Mail::into`. +/// +#[derive(Clone, Debug)] +pub struct MailRequest { + mail: Mail, + envelop_data: Option +} + +impl From for MailRequest { + fn from(mail: Mail) -> Self { + MailRequest::new(mail) + } +} + + + +impl MailRequest { + + /// creates a new `MailRequest` from a `Mail` instance + pub fn new(mail: Mail) -> Self { + MailRequest { mail, envelop_data: None } + } + + /// create a new `MailRequest` and use custom smtp `EnvelopData` + /// + /// Note that envelop data comes from `new-tokio-smtp::send_mail` and + /// is not re-exported so if you happen to run into one of the view + /// cases where you need to set it manually just import it from + /// `new-tokio-smtp`. + pub fn new_with_envelop(mail: Mail, envelop: EnvelopData) -> Self { + MailRequest { mail, envelop_data: Some(envelop) } + } + + /// replace the smtp `EnvelopData` + pub fn override_envelop(&mut self, envelop: EnvelopData) -> Option { + mem::replace(&mut self.envelop_data, Some(envelop)) + } + + pub fn _into_mail_with_envelop(self) -> Result<(Mail, EnvelopData), MailError> { + let envelop = + if let Some(envelop) = self.envelop_data { envelop } + else { derive_envelop_data_from_mail(&self.mail)? }; + + Ok((self.mail, envelop)) + } + + #[cfg(not(feature="extended-api"))] + #[inline(always)] + pub(crate) fn into_mail_with_envelop(self) -> Result<(Mail, EnvelopData), MailError> { + self._into_mail_with_envelop() + } + + /// Turns this type into the contained mail an associated envelop data. + /// + /// If envelop data was explicitly set it is returned. + /// If no envelop data was explicitly given it is derived from the + /// Mail header fields using `derive_envelop_data_from_mail`. + #[cfg(feature="extended-api")] + #[inline(always)] + pub fn into_mail_with_envelop(self) -> Result<(Mail, EnvelopData), MailError> { + self._into_mail_with_envelop() + } +} + +fn mailaddress_from_mailbox(mailbox: &Mailbox) -> Result { + let email = &mailbox.email; + let needs_smtputf8 = email.check_if_internationalized(); + let mt = if needs_smtputf8 { MailType::Internationalized } else { MailType::Ascii }; + let mut buffer = EncodingBuffer::new(mt); + { + let mut writer = buffer.writer(); + email.encode(&mut writer)?; + writer.commit_partial_header(); + } + let raw: Vec = buffer.into(); + let address = String::from_utf8(raw).expect("[BUG] encoding Email produced non utf8 data"); + Ok(MailAddress::new_unchecked(address, needs_smtputf8)) +} + +/// Generates envelop data based on the given Mail. +/// +/// If a sender header is given smtp will use this +/// as smtp from else the single mailbox in from +/// is used as smtp from. +/// +/// All `To`'s are used as smtp recipients. +/// +/// **`Cc`/`Bcc` is currently no supported/has no +/// special handling** +/// +/// # Error +/// +/// An error is returned if there is: +/// +/// - No From header +/// - No To header +/// - A From header with multiple addresses but no Sender header +/// +pub fn derive_envelop_data_from_mail(mail: &Mail) + -> Result +{ + let headers = mail.headers(); + let smtp_from = + if let Some(sender) = headers.get_single(Sender) { + let sender = sender?; + //TODO double check with from field + mailaddress_from_mailbox(sender)? + } else { + let from = headers.get_single(_From) + .ok_or(OtherValidationError::NoFrom)??; + + if from.len() > 1 { + return Err(BuildInValidationError::MultiMailboxFromWithoutSender.into()); + } + + mailaddress_from_mailbox(from.first())? + }; + + let smtp_to = + if let Some(to) = headers.get_single(_To) { + let to = to?; + to.try_mapped_ref(mailaddress_from_mailbox)? + } else { + return Err(AnotherOtherValidationError::NoTo.into()); + }; + + //TODO Cc, Bcc + + Ok(EnvelopData { + from: Some(smtp_from), + to: smtp_to + }) +} + +#[cfg(test)] +mod test { + + mod derive_envelop_data_from_mail { + use super::super::derive_envelop_data_from_mail; + use mail::{ + Mail, + Resource, + file_buffer::FileBuffer + }; + use headers::{ + headers::{_From, _To, Sender}, + header_components::MediaType + }; + + fn mock_resource() -> Resource { + let mt = MediaType::parse("text/plain; charset=utf-8").unwrap(); + let fb = FileBuffer::new(mt, "abcd↓efg".to_owned().into()); + Resource::sourceless_from_buffer(fb) + } + + #[test] + fn use_sender_if_given() { + let mut mail = Mail::new_singlepart_mail(mock_resource()); + + mail.insert_headers(headers! { + Sender: "strange@caffe.test", + _From: ["ape@caffe.test", "epa@caffe.test"], + _To: ["das@ding.test"] + }.unwrap()); + + let envelop_data = derive_envelop_data_from_mail(&mail).unwrap(); + + assert_eq!( + envelop_data.from.as_ref().unwrap().as_str(), + "strange@caffe.test" + ); + } + + #[test] + fn use_from_if_no_sender_given() { + let mut mail = Mail::new_singlepart_mail(mock_resource()); + mail.insert_headers(headers! { + _From: ["ape@caffe.test"], + _To: ["das@ding.test"] + }.unwrap()); + + let envelop_data = derive_envelop_data_from_mail(&mail).unwrap(); + + assert_eq!( + envelop_data.from.as_ref().unwrap().as_str(), + "ape@caffe.test" + ); + } + + #[test] + fn fail_if_no_sender_but_multi_mailbox_from() { + let mut mail = Mail::new_singlepart_mail(mock_resource()); + mail.insert_headers(headers! { + _From: ["ape@caffe.test", "a@b.test"], + _To: ["das@ding.test"] + }.unwrap()); + + let envelop_data = derive_envelop_data_from_mail(&mail); + + //assert is_err + envelop_data.unwrap_err(); + } + + #[test] + fn use_to() { + let mut mail = Mail::new_singlepart_mail(mock_resource()); + mail.insert_headers(headers! { + _From: ["ape@caffe.test"], + _To: ["das@ding.test"] + }.unwrap()); + + let envelop_data = derive_envelop_data_from_mail(&mail).unwrap(); + + assert_eq!( + envelop_data.to.first().as_str(), + "das@ding.test" + ); + } + } + + mod mailaddress_from_mailbox { + use headers::{ + HeaderTryFrom, + header_components::{Mailbox, Email} + }; + use super::super::mailaddress_from_mailbox; + + #[test] + #[cfg_attr(not(feature="test-with-traceing"), ignore)] + fn does_not_panic_with_tracing_enabled() { + let mb = Mailbox::try_from("hy@b").unwrap(); + mailaddress_from_mailbox(&mb).unwrap(); + } + + #[test] + fn correctly_converts_mailbox() { + let mb = Mailbox::from(Email::new("tast@tost.test").unwrap()); + let address = mailaddress_from_mailbox(&mb).unwrap(); + assert_eq!(address.as_str(), "tast@tost.test"); + assert_eq!(address.needs_smtputf8(), false); + } + + #[test] + fn tracks_if_smtputf8_is_needed() { + let mb = Mailbox::from(Email::new("tüst@tost.test").unwrap()); + let address = mailaddress_from_mailbox(&mb).unwrap(); + assert_eq!(address.as_str(), "tüst@tost.test"); + assert_eq!(address.needs_smtputf8(), true); + } + + #[test] + fn puny_encodes_domain_if_smtputf8_is_not_needed() { + let mb = Mailbox::from(Email::new("tast@tüst.test").unwrap()); + let address = mailaddress_from_mailbox(&mb).unwrap(); + assert_eq!(address.as_str(), "tast@xn--tst-hoa.test"); + assert_eq!(address.needs_smtputf8(), false); + } + + #[test] + fn does_not_puny_encodes_domain_if_smtputf8_is_needed() { + let mb = Mailbox::from(Email::new("töst@tüst.test").unwrap()); + let address = mailaddress_from_mailbox(&mb).unwrap(); + assert_eq!(address.as_str(), "töst@tüst.test"); + assert_eq!(address.needs_smtputf8(), true); + } + } +} \ No newline at end of file diff --git a/smtp/src/resolve_all.rs b/smtp/src/resolve_all.rs new file mode 100644 index 0000000..69d3c8b --- /dev/null +++ b/smtp/src/resolve_all.rs @@ -0,0 +1,81 @@ +use std::mem; +use std::iter::FromIterator; + +use futures::{Future, Async, Poll}; + +pub enum AltFuse { + Future(F), + Resolved(Result) +} + +impl Future for AltFuse + where F: Future +{ + type Item = (); + //TODO[futures/v>=0.2 |rust/! type]: use Never or ! + type Error = (); + + fn poll(&mut self) -> Poll { + let result = match *self { + AltFuse::Resolved(_) => return Ok(Async::Ready(())), + AltFuse::Future(ref mut fut) => match fut.poll() { + Ok(Async::NotReady) => return Ok(Async::NotReady), + Ok(Async::Ready(val)) => Ok(val), + Err(err) => Err(err) + } + }; + + *self = AltFuse::Resolved(result); + Ok(Async::Ready(())) + } +} + + +pub struct ResolveAll + where F: Future +{ + all: Vec> +} + +impl Future for ResolveAll + where F: Future +{ + type Item = Vec>; + //TODO[futures >= 0.2/rust ! type]: use Never or ! + type Error = (); + + fn poll(&mut self) -> Poll { + let mut any_not_ready = false; + for fut in self.all.iter_mut() { + if let Ok(Async::NotReady) = fut.poll() { + any_not_ready = true; + } + } + if any_not_ready { + Ok(Async::NotReady) + } else { + let results = mem::replace(&mut self.all, Vec::new()) + .into_iter().map(|alt_fuse_fut| match alt_fuse_fut { + AltFuse::Resolved(res) => res, + AltFuse::Future(_) => unreachable!() + }) + .collect(); + Ok(Async::Ready(results)) + } + } +} + +impl FromIterator for ResolveAll + where I: Future +{ + fn from_iter(all: T) -> Self + where T: IntoIterator + { + let all = all + .into_iter() + .map(|fut| AltFuse::Future(fut)) + .collect(); + + ResolveAll { all } + } +} \ No newline at end of file diff --git a/smtp/src/send_mail.rs b/smtp/src/send_mail.rs new file mode 100644 index 0000000..3839421 --- /dev/null +++ b/smtp/src/send_mail.rs @@ -0,0 +1,139 @@ +//! Module implementing mail sending using `new-tokio-smtp::send_mail`. + +use std::iter::{once as one}; + +use futures::{ + stream::{self, Stream}, + future::{self, Future, Either} +}; + +use mail_internals::{ + MailType, + encoder::EncodingBuffer +}; +use mail::Context; + +use new_tokio_smtp::{ + ConnectionConfig, + Cmd, + SetupTls, + send_mail::MailEnvelop, + Connection, + send_mail as smtp +}; + +use ::{ + error::MailSendError, + request::MailRequest +}; + +/// Sends a given mail (request). +/// +/// - This will use the given context to encode the mail. +/// - Then it will use the connection config to open a connection to a mail +/// server (likely a Mail Submission Agent (MSA)). +/// - Following this it will send the mail to the server. +/// - After which it will close the connection again. +/// +/// You can use `MailRequest: From` (i.e. `mail.into()`) to pass in +/// a mail and derive the envelop data (from, to) from it or create your own +/// mail request if different smtp envelop data is needed. +pub fn send(mail: MailRequest, conconf: ConnectionConfig, ctx: impl Context) + -> impl Future + where A: Cmd, S: SetupTls +{ + let fut = encode(mail, ctx) + .then(move |envelop_res| Connection + ::connect_send_quit(conconf, one(envelop_res)) + .collect()) + .map(|mut results| results.pop().expect("[BUG] sending one mail expects one result")); + + fut +} + +/// Sends a batch of mails to a server. +/// +/// - This will use the given context to encode all mails. +/// - After which it will use the connection config to open a connection +/// to the server (like a Mail Submission Agent (MSA)). +/// - Then it will start sending mails. +/// - If a mail fails because of an error code but setting up the connection +/// (which includes auth) didn't fail then others mails in the input will +/// still be send +/// - If the connection is broken because setting it up failed or it was +/// interrupted, then the mail at which place it was noticed will return +/// the given error and all later mails will return a I/0-Error with the +/// `ErrorKind::NoConnection` +/// - It will return a `Stream` which when polled will send the mails +/// and return results _in the order the mails had been supplied_. So +/// for each mail there will be exactly one result. +/// - Once the stream is completed the connection will automatically be +/// closed (even if the stream is not yet dropped, it closes it the +/// moment it notices that there are no more mails to send!) +/// +pub fn send_batch( + mails: Vec, + conconf: ConnectionConfig, + ctx: C +) -> impl Stream + where A: Cmd, S: SetupTls, C: Context +{ + let iter = mails.into_iter().map(move |mail| encode(mail, ctx.clone())); + + let fut = collect_res(stream::futures_ordered(iter)) + .map(move |vec_of_res| Connection::connect_send_quit(conconf, vec_of_res)) + .flatten_stream(); + + fut +} + +//FIXME[futures/v>=0.2] use Error=Never +fn collect_res(stream: S) -> impl Future>, Error=E> + where S: Stream +{ + stream.then(|res| Ok(res)).collect() +} + +/// Turns a `MailRequest` into a future resolving to a `MailEnvelop`. +/// +/// This function is mainly used internally for `send`, `send_batch` +/// but can be used by other libraries when `send`/`send_batch` doesn't +/// quite match their use case. E.g. if they want to have a connection +/// pool and instead of `connect->send->quit` want to have something like +/// `take_from_pool->test->send->place_back_to_pool`, in which case they +/// probably would want to do something along the lines of using encode +/// then take a connection, test it, use the mail envelops with `new-tokio-smtp`'s +/// `SendAllMails` stream with a `on_completion` handler which places it +/// back in the pool. +pub fn encode(request: MailRequest, ctx: C) + -> impl Future + where C: Context +{ + let (mail, envelop_data) = + match request.into_mail_with_envelop() { + Ok(pair) => pair, + Err(e) => return Either::A(future::err(e.into())) + }; + + let fut = mail + .into_encodeable_mail(ctx.clone()) + .and_then(move |enc_mail| ctx.offload_fn(move || { + let (mail_type, requirement) = + if envelop_data.needs_smtputf8() { + (MailType::Internationalized, smtp::EncodingRequirement::Smtputf8) + } else { + (MailType::Ascii, smtp::EncodingRequirement::None) + }; + + let mut buffer = EncodingBuffer::new(mail_type); + enc_mail.encode(&mut buffer)?; + + let vec_buffer: Vec<_> = buffer.into(); + let smtp_mail = smtp::Mail::new(requirement, vec_buffer); + + Ok(smtp::MailEnvelop::from((smtp_mail, envelop_data))) + })) + .map_err(MailSendError::from); + + Either::B(fut) +} \ No newline at end of file -- cgit v1.2.3