-
Notifications
You must be signed in to change notification settings - Fork 360
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
66f7d76
commit 0e1f6b0
Showing
8 changed files
with
202 additions
and
69 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
||
|
@@ -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; | ||
|
@@ -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.") | ||
}); | ||
|
@@ -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), | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
@@ -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(); | ||
|
@@ -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(); | ||
|
@@ -191,7 +193,7 @@ fn email_deliverable( | |
}); | ||
} | ||
|
||
Err(err) | ||
Err(SmtpError::LettreError(err)) | ||
} | ||
} | ||
} | ||
|
@@ -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) | ||
|
@@ -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)?; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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}}" | ||
); | ||
} | ||
|