From 8c8aa770bf039214070a1679f8d4427049a56d11 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Sun, 10 May 2020 16:04:59 +0200 Subject: [PATCH] feat(transport): Start async implementation (sendmail, file and stub transports) --- .github/workflows/test.yml | 5 +++ Cargo.toml | 4 +++ benches/transport_smtp.rs | 4 +-- src/lib.rs | 29 +++++++++++++++++ src/transport/file/mod.rs | 48 ++++++++++++++++++++++++++-- src/transport/sendmail/mod.rs | 59 ++++++++++++++++++++++++++++++----- src/transport/stub/mod.rs | 21 +++++++++++++ tests/transport_file.rs | 31 +++++++++++++++++- tests/transport_sendmail.rs | 23 ++++++++++++-- tests/transport_stub.rs | 24 ++++++++++++-- 10 files changed, 230 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce4f282..0052719 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,11 @@ jobs: with: command: test args: --no-default-features --features=builder,smtp-transport,file-transport,sendmail-transport + - run: rm target/debug/deps/liblettre-* + - uses: actions-rs/cargo@v1 + with: + command: test + args: --features=async check: name: Check diff --git a/Cargo.toml b/Cargo.toml index afd4197..739086b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,9 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" } maintenance = { status = "actively-developed" } [dependencies] +async-attributes = { version = "1.1", optional = true } +async-std = { version = "1.5", optional = true, features = ["unstable"] } +async-trait = { version = "0.1", optional = true } base64 = { version = "0.12", optional = true } bufstream = { version = "0.1", optional = true } hostname = { version = "0.3", optional = true } @@ -50,6 +53,7 @@ harness = false name = "transport_smtp" [features] +async = ["async-std", "async-trait", "async-attributes"] builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"] default = ["file-transport", "smtp-transport", "rustls-tls", "hostname", "r2d2", "sendmail-transport", "builder"] file-transport = ["serde", "serde_json"] diff --git a/benches/transport_smtp.rs b/benches/transport_smtp.rs index dfa24db..90fe0d3 100644 --- a/benches/transport_smtp.rs +++ b/benches/transport_smtp.rs @@ -2,7 +2,7 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use lettre::{Message, SmtpTransport, Transport}; fn bench_simple_send(c: &mut Criterion) { - let sender = SmtpTransport::new("127.0.0.1").port(2525); + let sender = SmtpTransport::builder("127.0.0.1").port(2525).build(); c.bench_function("send email", move |b| { b.iter(|| { @@ -20,7 +20,7 @@ fn bench_simple_send(c: &mut Criterion) { } fn bench_reuse_send(c: &mut Criterion) { - let sender = SmtpTransport::new("127.0.0.1").port(2525); + let sender = SmtpTransport::builder("127.0.0.1").port(2525).build(); c.bench_function("send email with connection reuse", move |b| { b.iter(|| { let email = Message::builder() diff --git a/src/lib.rs b/src/lib.rs index 2774525..e35f293 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,6 +151,35 @@ pub trait Transport { fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result; } +#[cfg(feature = "async")] +pub mod r#async { + + use super::*; + use async_trait::async_trait; + + #[async_trait] + pub trait Transport { + /// Result types for the transport + type Ok: fmt::Debug; + type Error: StdError; + + /// Sends the email + #[cfg(feature = "builder")] + // TODO take &Message + async fn send(&self, message: Message) -> Result { + let raw = message.formatted(); + let envelope = message.envelope(); + self.send_raw(&envelope, &raw).await + } + + async fn send_raw( + &self, + envelope: &Envelope, + email: &[u8], + ) -> Result; + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/transport/file/mod.rs b/src/transport/file/mod.rs index c3d6fd7..bc2a28e 100644 --- a/src/transport/file/mod.rs +++ b/src/transport/file/mod.rs @@ -78,9 +78,7 @@ impl Transport for FileTransport { fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result { let email_id = Uuid::new_v4(); - - let mut file = self.path.clone(); - file.push(format!("{}.json", email_id)); + let file = self.path.join(format!("{}.json", email_id)); let serialized = match str::from_utf8(email) { // Serialize as UTF-8 string if possible @@ -100,3 +98,47 @@ impl Transport for FileTransport { Ok(email_id.to_string()) } } + +#[cfg(feature = "async")] +pub mod r#async { + use super::{FileTransport, Id, SerializableEmail}; + use crate::{r#async::Transport, transport::file::error::Error, Envelope}; + use async_std::fs::File; + use async_std::prelude::*; + use async_trait::async_trait; + use std::str; + use uuid::Uuid; + + #[async_trait] + impl Transport for FileTransport { + type Ok = Id; + type Error = Error; + + async fn send_raw( + &self, + envelope: &Envelope, + email: &[u8], + ) -> Result { + let email_id = Uuid::new_v4(); + let file = self.path.join(format!("{}.json", email_id)); + + let serialized = match str::from_utf8(email) { + // Serialize as UTF-8 string if possible + Ok(m) => serde_json::to_string(&SerializableEmail { + envelope: envelope.clone(), + message: Some(m), + raw_message: None, + }), + Err(_) => serde_json::to_string(&SerializableEmail { + envelope: envelope.clone(), + message: None, + raw_message: Some(email), + }), + }?; + + let mut file = File::create(file.as_path()).await?; + file.write_all(serialized.as_bytes()).await?; + Ok(email_id.to_string()) + } + } +} diff --git a/src/transport/sendmail/mod.rs b/src/transport/sendmail/mod.rs index b952857..e8f3f32 100644 --- a/src/transport/sendmail/mod.rs +++ b/src/transport/sendmail/mod.rs @@ -56,6 +56,17 @@ impl SendmailTransport { command: command.into(), } } + + fn command(&self, envelope: &Envelope) -> Command { + let mut c = Command::new(&self.command); + c.arg("-i") + .arg("-f") + .arg(envelope.from().map(|f| f.as_ref()).unwrap_or("\"\"")) + .args(envelope.to()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()); + c + } } impl Transport for SendmailTransport { @@ -64,14 +75,7 @@ impl Transport for SendmailTransport { fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result { // Spawn the sendmail command - let mut process = Command::new(&self.command) - .arg("-i") - .arg("-f") - .arg(envelope.from().map(|f| f.as_ref()).unwrap_or("\"\"")) - .args(envelope.to()) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn()?; + let mut process = self.command(envelope).spawn()?; process.stdin.as_mut().unwrap().write_all(email)?; let output = process.wait_with_output()?; @@ -83,3 +87,42 @@ impl Transport for SendmailTransport { } } } + +#[cfg(feature = "async")] +pub mod r#async { + use super::SendmailTransport; + use crate::{r#async::Transport, transport::sendmail::error::Error, Envelope}; + use async_trait::async_trait; + use std::io::Write; + + #[async_trait] + impl Transport for SendmailTransport { + type Ok = (); + type Error = Error; + + // TODO: Convert to real async, once async-std has a process implementation. + async fn send_raw( + &self, + envelope: &Envelope, + email: &[u8], + ) -> Result { + let mut command = self.command(envelope); + let email = email.to_vec(); + + let output = async_std::task::spawn_blocking(move || { + // Spawn the sendmail command + let mut process = command.spawn()?; + + process.stdin.as_mut().unwrap().write_all(&email)?; + process.wait_with_output() + }) + .await?; + + if output.status.success() { + Ok(()) + } else { + Err(Error::Client(String::from_utf8(output.stderr)?)) + } + } + } +} diff --git a/src/transport/stub/mod.rs b/src/transport/stub/mod.rs index 3419b5d..dc5c3f6 100644 --- a/src/transport/stub/mod.rs +++ b/src/transport/stub/mod.rs @@ -73,3 +73,24 @@ impl Transport for StubTransport { self.response } } + +#[cfg(feature = "async")] +pub mod r#async { + use super::StubTransport; + use crate::{r#async::Transport, transport::stub::Error, Envelope}; + use async_trait::async_trait; + + #[async_trait] + impl Transport for StubTransport { + type Ok = (); + type Error = Error; + + async fn send_raw( + &self, + _envelope: &Envelope, + _email: &[u8], + ) -> Result { + self.response + } + } +} diff --git a/tests/transport_file.rs b/tests/transport_file.rs index a7086dc..b390930 100644 --- a/tests/transport_file.rs +++ b/tests/transport_file.rs @@ -1,7 +1,7 @@ #[cfg(test)] #[cfg(feature = "file-transport")] mod test { - use lettre::{transport::file::FileTransport, Message, Transport}; + use lettre::{transport::file::FileTransport, Message}; use std::{ env::temp_dir, fs::{remove_file, File}, @@ -10,6 +10,7 @@ mod test { #[test] fn file_transport() { + use lettre::Transport; let sender = FileTransport::new(temp_dir()); let email = Message::builder() .from("NoBody ".parse().unwrap()) @@ -33,4 +34,32 @@ mod test { "{\"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(); } + + #[cfg(feature = "async")] + #[async_attributes::test] + async fn file_transport_async() { + use lettre::r#async::Transport; + let sender = FileTransport::new(temp_dir()); + let email = Message::builder() + .from("NoBody ".parse().unwrap()) + .reply_to("Yuin ".parse().unwrap()) + .to("Hei ".parse().unwrap()) + .subject("Happy new year") + .date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) + .body("Be happy!") + .unwrap(); + + let result = sender.send(email).await; + let id = result.unwrap(); + + let file = temp_dir().join(format!("{}.json", id)); + let mut f = File::open(file.clone()).unwrap(); + let mut buffer = String::new(); + let _ = f.read_to_string(&mut buffer); + + assert_eq!( + buffer, + "{\"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 30205fc..2512c1e 100644 --- a/tests/transport_sendmail.rs +++ b/tests/transport_sendmail.rs @@ -1,10 +1,11 @@ #[cfg(test)] #[cfg(feature = "sendmail-transport")] mod test { - use lettre::{transport::sendmail::SendmailTransport, Message, Transport}; + use lettre::{transport::sendmail::SendmailTransport, Message}; #[test] - fn sendmail_transport_simple() { + fn sendmail_transport() { + use lettre::Transport; let sender = SendmailTransport::new(); let email = Message::builder() .from("NoBody ".parse().unwrap()) @@ -18,4 +19,22 @@ mod test { println!("{:?}", result); assert!(result.is_ok()); } + + #[cfg(feature = "async")] + #[async_attributes::test] + async fn sendmail_transport_async() { + use lettre::r#async::Transport; + let sender = SendmailTransport::new(); + let email = Message::builder() + .from("NoBody ".parse().unwrap()) + .reply_to("Yuin ".parse().unwrap()) + .to("Hei ".parse().unwrap()) + .subject("Happy new year") + .date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) + .body("Be happy!") + .unwrap(); + + let result = sender.send(email).await; + assert!(result.is_ok()); + } } diff --git a/tests/transport_stub.rs b/tests/transport_stub.rs index 4d57348..56aaaaa 100644 --- a/tests/transport_stub.rs +++ b/tests/transport_stub.rs @@ -1,7 +1,8 @@ -use lettre::{transport::stub::StubTransport, Message, Transport}; +use lettre::{transport::stub::StubTransport, Message}; #[test] fn stub_transport() { + use lettre::Transport; let sender_ok = StubTransport::new_ok(); let sender_ko = StubTransport::new_error(); let email = Message::builder() @@ -12,6 +13,25 @@ fn stub_transport() { .body("Be happy!") .unwrap(); - sender_ok.send(&email.clone()).unwrap(); + sender_ok.send(&email).unwrap(); sender_ko.send(&email).unwrap_err(); } + +#[cfg(feature = "async")] +#[async_attributes::test] +async fn stub_transport_async() { + use lettre::r#async::Transport; + let sender_ok = StubTransport::new_ok(); + let sender_ko = StubTransport::new_error(); + let email = Message::builder() + .from("NoBody ".parse().unwrap()) + .reply_to("Yuin ".parse().unwrap()) + .to("Hei ".parse().unwrap()) + .subject("Happy new year") + .date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) + .body("Be happy!") + .unwrap(); + + sender_ok.send(email.clone()).await.unwrap(); + sender_ko.send(email).await.unwrap_err(); +}