Skip to content

Commit

Permalink
refactor: Use futures (#78)
Browse files Browse the repository at this point in the history
BREAKING CHANGE:
- The main function `email_exists` now returns a Future:
```rust
pub async fn email_exists(to_email: &str, from_email: &str) -> SingleEmail {}
```
- The `SmtpError::SmtpError` has been renamed to `SmtpError::LettreError` to show the underlying error more correctly (i.e., coming from `lettre` crate).
- The `BlockedByISP` error has been removed. Instead, you'll see e.g. `"connection refused"`, or whatever is returned by the SMTP server:
```json
{
  // ...,
  "smtp": {
    "error": {
      "type": "LettreError",
      "message": "connection refused"
    }
  },
}
```
  • Loading branch information
amaury1093 authored Nov 16, 2019
1 parent 66f7d76 commit 0e1f6b0
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 69 deletions.
183 changes: 152 additions & 31 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ path = "src/main.rs"
check-if-email-exists = { path = "core" }
clap = { version = "2.32", features = ["yaml"] }
env_logger = "0.7"
futures = "0.3"
serde = "1.0"
serde_json = "1.0"

Expand Down
2 changes: 1 addition & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ appveyor = { repository = "amaurymartiny/check-if-email-exists", service = "gith
travis-ci = { repository = "amaurymartiny/check-if-email-exists", service = "github" }

[dependencies]
futures = "0.3"
lettre = "0.9"
log = "0.4"
mailchecker = "3.3"
native-tls = "^0.2"
rand = "0.7"
rayon = "1.2.0"
trust-dns-resolver = "0.12.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
42 changes: 20 additions & 22 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ extern crate log;
extern crate mailchecker;
extern crate native_tls;
extern crate rand;
extern crate rayon;
extern crate serde;
extern crate trust_dns_resolver;

Expand All @@ -29,9 +28,9 @@ mod smtp;
mod syntax;
mod util;

use futures::future::select_ok;
use lettre::{smtp::SMTP_PORT, EmailAddress};
use mx::{get_mx_lookup, MxDetails, MxError};
use rayon::prelude::*;
use serde::{ser::SerializeMap, Serialize, Serializer};
use smtp::{SmtpDetails, SmtpError};
use std::str::FromStr;
Expand Down Expand Up @@ -79,7 +78,7 @@ impl Serialize for SingleEmail {

/// The main function: checks email format, checks MX records, and checks SMTP
/// responses to the email inbox.
pub fn email_exists(to_email: &str, from_email: &str) -> SingleEmail {
pub async fn email_exists(to_email: &str, from_email: &str) -> SingleEmail {
let from_email = EmailAddress::from_str(from_email).unwrap_or_else(|_| {
EmailAddress::from_str("[email protected]").expect("This is a valid email. qed.")
});
Expand Down Expand Up @@ -110,31 +109,30 @@ pub fn email_exists(to_email: &str, from_email: &str) -> SingleEmail {
};
debug!("Found the following MX hosts {:?}", my_mx);

// `(host, port)` combination
// We could add ports 465 and 587 too
let combinations = my_mx
// Create one future per lookup result
let futures = my_mx
.lookup
.iter()
.map(|host| (host.exchange(), SMTP_PORT))
.collect::<Vec<_>>();

let my_smtp = combinations
// Concurrently find any combination that returns true for email_exists
.par_iter()
// Attempt to make a SMTP call to host
.flat_map(|(host, port)| {
smtp::smtp_details(
.map(|host| {
let fut = smtp::smtp_details(
&from_email,
&my_syntax.address,
host,
*port,
host.exchange(),
// We could add ports 465 and 587 too
SMTP_PORT,
my_syntax.domain.as_str(),
)
);

// https://rust-lang.github.io/async-book/04_pinning/01_chapter.html
Box::pin(fut)
})
.find_any(|_| true)
// If all smtp calls timed out/got refused/errored, we assume that the
// ISP is blocking relevant ports
.ok_or(SmtpError::BlockedByIsp);
.collect::<Vec<_>>();

// Race, return the first future that resolves
let my_smtp = match select_ok(futures).await {
Ok((details, _)) => Ok(details),
Err(err) => Err(err),
};

SingleEmail {
mx: Ok(my_mx),
Expand Down
29 changes: 17 additions & 12 deletions core/src/smtp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,9 @@ pub struct SmtpDetails {
pub enum SmtpError {
/// Skipped checking SMTP details
Skipped,
/// ISP is blocking SMTP ports
BlockedByIsp,
/// IO error when communicating with SMTP server
/// Error when communicating with SMTP server
#[serde(serialize_with = "ser_with_display")]
SmtpError(LettreSmtpError),
LettreError(LettreSmtpError),
}

/// Try to send an smtp command, close and return Err if fails.
Expand Down Expand Up @@ -123,7 +121,7 @@ struct Deliverability {
fn email_deliverable(
smtp_client: &mut InnerClient<NetworkStream>,
to_email: &EmailAddress,
) -> Result<Deliverability, LettreSmtpError> {
) -> Result<Deliverability, SmtpError> {
// "RCPT TO: [email protected]"
// FIXME Do not clone?
let to_email = to_email.clone();
Expand All @@ -138,10 +136,14 @@ fn email_deliverable(
is_disabled: false,
})
} else {
Err(LettreSmtpError::Client("Can't find 2.1.5 in RCPT command"))
Err(SmtpError::LettreError(LettreSmtpError::Client(
"Can't find 2.1.5 in RCPT command",
)))
}
}
None => Err(LettreSmtpError::Client("No response on RCPT command")),
None => Err(SmtpError::LettreError(LettreSmtpError::Client(
"No response on RCPT command",
))),
},
Err(err) => {
let err_string = err.to_string();
Expand Down Expand Up @@ -191,7 +193,7 @@ fn email_deliverable(
});
}

Err(err)
Err(SmtpError::LettreError(err))
}
}
}
Expand All @@ -200,7 +202,7 @@ fn email_deliverable(
fn email_has_catch_all(
smtp_client: &mut InnerClient<NetworkStream>,
domain: &str,
) -> Result<bool, LettreSmtpError> {
) -> Result<bool, SmtpError> {
// Create a random 15-char alphanumerical string
let random_email = rand::thread_rng()
.sample_iter(&Alphanumeric)
Expand All @@ -216,14 +218,17 @@ fn email_has_catch_all(
}

/// Get all email details we can.
pub fn smtp_details(
pub async fn smtp_details(
from_email: &EmailAddress,
to_email: &EmailAddress,
host: &Name,
port: u16,
domain: &str,
) -> Result<SmtpDetails, LettreSmtpError> {
let mut smtp_client = connect_to_host(from_email, host, port)?;
) -> Result<SmtpDetails, SmtpError> {
let mut smtp_client = match connect_to_host(from_email, host, port) {
Ok(client) => client,
Err(err) => return Err(SmtpError::LettreError(err)),
};

let is_catch_all = email_has_catch_all(&mut smtp_client, domain).unwrap_or(false);
let deliverability = email_deliverable(&mut smtp_client, to_email)?;
Expand Down
5 changes: 4 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ extern crate serde;

use check_if_email_exists::email_exists;
use clap::App;
use futures::executor::block_on;
use serde_json;

fn main() -> serde_json::Result<()> {
Expand All @@ -36,7 +37,9 @@ fn main() -> serde_json::Result<()> {
.value_of("TO_EMAIL")
.expect("TO_EMAIL is required. qed.");

let output = serde_json::to_string_pretty(&email_exists(&to_email, &from_email))?;
let result = block_on(email_exists(&to_email, &from_email));

let output = serde_json::to_string_pretty(&result)?;
println!("{}", output);

Ok(())
Expand Down
1 change: 1 addition & 0 deletions test_suite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ publish = false

[dependencies]
check-if-email-exists = { path = "../core" }
futures = "0.3"
serde = "1.0"
serde_json = "1.0"
8 changes: 6 additions & 2 deletions test_suite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,23 @@
#[cfg(test)]
mod tests {
use check_if_email_exists::email_exists;
use futures::executor::block_on;

#[test]
fn should_output_error_for_invalid_email() {
let result = block_on(email_exists("foo", "[email protected]"));
assert_eq!(
serde_json::to_string(&email_exists("foo", "[email protected]")).unwrap(),
serde_json::to_string(&result).unwrap(),
"{\"mx\":{\"error\":{\"type\":\"Skipped\"}},\"smtp\":{\"error\":{\"type\":\"Skipped\"}},\"syntax\":{\"error\":{\"type\":\"SyntaxError\",\"message\":\"invalid email address\"}}}"
);
}

#[test]
fn should_output_error_for_invalid_mx() {
let result = block_on(email_exists("[email protected]", "[email protected]"));

assert_eq!(
serde_json::to_string(&email_exists("[email protected]", "[email protected]")).unwrap(),
serde_json::to_string(&result).unwrap(),
"{\"mx\":{\"error\":{\"type\":\"ResolveError\",\"message\":\"no record found for name: bar.baz type: MX class: IN\"}},\"smtp\":{\"error\":{\"type\":\"Skipped\"}},\"syntax\":{\"address\":\"[email protected]\",\"domain\":\"bar.baz\",\"username\":\"foo\",\"valid_format\":true}}"
);
}
Expand Down

0 comments on commit 0e1f6b0

Please sign in to comment.