From d04f84cc1e7b30e02d3717ab1af9f680cdb2c27f Mon Sep 17 00:00:00 2001 From: Amaury <1293565+amaurym@users.noreply.github.com> Date: Mon, 9 Jan 2023 00:16:54 +0100 Subject: [PATCH] feat: Set default timeout to 10s (#1251) * Add timeout * Set default timeout to 10s * Use duration in u64 (seconds) * Add user/pass to openapi * default hello_name to example.org * Fix lint --- Cargo.lock | 72 +++++++++++++++++ backend/README.md | 28 +++---- backend/openapi.json | 105 ++++++++++++++++--------- backend/src/check.rs | 10 +-- backend/src/routes/check_email/post.rs | 41 +--------- backend/tests/check_email.rs | 13 +-- core/Cargo.toml | 1 + core/src/smtp/mod.rs | 2 +- core/src/util/input_output.rs | 49 +++++++++--- 9 files changed, 203 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00ea66023..8f5480f80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -423,6 +423,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_with", "tokio", "trust-dns-proto", ] @@ -618,6 +619,41 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "data-encoding" version = "2.3.2" @@ -1202,6 +1238,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -1221,6 +1263,7 @@ checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", + "serde", ] [[package]] @@ -2163,6 +2206,34 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25bf4a5a814902cd1014dbccfa4d4560fb8432c779471e96e035602519f82eef" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap", + "serde", + "serde_json", + "serde_with_macros", + "time 0.3.14", +] + +[[package]] +name = "serde_with_macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3452b4c0f6c1e357f73fdb87cd1efabaa12acf328c7a528e252893baeb3f4aa" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha-1" version = "0.10.0" @@ -2478,6 +2549,7 @@ dependencies = [ "itoa 1.0.3", "libc", "num_threads", + "serde", "time-macros", ] diff --git a/backend/README.md b/backend/README.md index 25853afc7..72c6db051 100644 --- a/backend/README.md +++ b/backend/README.md @@ -48,20 +48,20 @@ Then send a `POST http://localhost:8080/v0/check_email` request with the followi These are the environment variables used to configure the HTTP server. To pass them to the Docker container, use the `-e {ENV_VAR}={VALUE}` flag. -| Env Var | Required? | Description | Default | -| ----------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | -| `RCH_ENABLE_BULK` | No | If set to `1`, then bulk verification endpoints will be added to the backend. | 0 | -| `DATABASE_URL` | Yes if `RCH_ENABLE_BULK==1` | Database connection string for storing results and task queue | not defined | -| `RCH_HTTP_HOST` | No | The host name to bind the HTTP server to. | `127.0.0.1` | -| `PORT` | No | The port to bind the HTTP server to, often populated by the cloud provider. | `8080` | -| `RCH_HOTMAIL_USE_HEADLESS` | No | Set to a running WebDriver process endpoint (e.g. `http://localhost:4444`) to use a headless navigator to Hotmail's password recovery page to check Hotmail/Outlook addresses. We recommend `chromedriver` as it allows parallel requests. | not defined | -| `RCH_FROM_EMAIL` | No | The email to use in the `MAIL FROM:` SMTP command. | `user@example.org` | -| `RCH_SENTRY_DSN` | No | If set, bug reports will be sent to this [Sentry](https://sentry.io) DSN. | not defined | -| `RCH_HEADER_SECRET` | No | If set, then all HTTP requests must have the `x-reacher-secret` header set to this value. This is used to protect the backend against public unwanted HTTP requests. | undefined | -| `RCH_DATABASE_MAX_CONNECTIONS` | No | (Bulk) Connections created for the database pool | 5 | -| `RCH_MINIMUM_TASK_CONCURRENCY` | No | (Bulk) Minimum number of concurrent running tasks below which more tasks are fetched | 10 | -| `RCH_MAXIMUM_CONCURRENT_TASK_FETCH` | No | (Bulk) Maximum number of tasks fetched at once | 20 | -| `RUST_LOG` | No | One of `trace,debug,warn,error,info`. 💡 PRO TIP: `RUST_LOG=debug` is very handful for debugging purposes. | not defined | +| Env Var | Required? | Description | Default | +| ----------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | +| `RCH_ENABLE_BULK` | No | If set to `1`, then bulk verification endpoints will be added to the backend. | 0 | +| `DATABASE_URL` | Yes if `RCH_ENABLE_BULK==1` | Database connection string for storing results and task queue | not defined | +| `RCH_HTTP_HOST` | No | The host name to bind the HTTP server to. | `127.0.0.1` | +| `PORT` | No | The port to bind the HTTP server to, often populated by the cloud provider. | `8080` | +| `RCH_HOTMAIL_USE_HEADLESS` | No | Set to a running WebDriver process endpoint (e.g. `http://localhost:4444`) to use a headless navigator to Hotmail's password recovery page to check Hotmail/Outlook addresses. We recommend `chromedriver` as it allows parallel requests. | not defined | +| `RCH_SMTP_TIMEOUT` | No | The default timeout of each SMTP connection, in seconds. Can be overwritten in each request using the `smtp_timeout` field. | 10s | +| `RCH_SENTRY_DSN` | No | If set, bug reports will be sent to this [Sentry](https://sentry.io) DSN. | not defined | +| `RCH_HEADER_SECRET` | No | If set, then all HTTP requests must have the `x-reacher-secret` header set to this value. This is used to protect the backend against public unwanted HTTP requests. | undefined | +| `RCH_DATABASE_MAX_CONNECTIONS` | No | (Bulk) Connections created for the database pool | 5 | +| `RCH_MINIMUM_TASK_CONCURRENCY` | No | (Bulk) Minimum number of concurrent running tasks below which more tasks are fetched | 10 | +| `RCH_MAXIMUM_CONCURRENT_TASK_FETCH` | No | (Bulk) Maximum number of tasks fetched at once | 20 | +| `RUST_LOG` | No | One of `trace,debug,warn,error,info`. 💡 PRO TIP: `RUST_LOG=debug` is very handful for debugging purposes. | not defined | ## REST API Documentation diff --git a/backend/openapi.json b/backend/openapi.json index 5b04553bf..9fc6b2a63 100644 --- a/backend/openapi.json +++ b/backend/openapi.json @@ -2,11 +2,11 @@ "openapi": "3.0.0", "info": { "title": "Reacher", - "version": "0.3.*", + "version": "0.4.*", "description": "### What is Reacher?\n\nReacher is a powerful, free and open-source email verification API service. It is provided both as a SaaS and as a self-host solution.", "license": { "name": "AGPL-3.0 OR Commercial", - "url": "https://github.com/reacherhq/backend/blob/master/LICENSE.md" + "url": "https://github.com/reacherhq/check-if-email-exists/blob/master/LICENSE.md" }, "contact": { "name": "Reacher", @@ -40,8 +40,7 @@ "is_reachable": "invalid", "misc": { "is_disposable": false, - "is_role_account": true, - "gravatar_url": null + "is_role_account": true }, "mx": { "accepts_mail": true, @@ -110,8 +109,7 @@ "is_reachable": "invalid", "misc": { "is_disposable": false, - "is_role_account": true, - "gravatar_url": null + "is_role_account": true }, "mx": { "accepts_mail": true, @@ -207,10 +205,7 @@ "description": "A human-readable description of the error." } }, - "required": [ - "type", - "message" - ] + "required": ["type", "message"] }, "MiscDetails": { "title": "MiscDetails", @@ -227,13 +222,10 @@ }, "gravatar_url": { "type": "string", - "description": "The Gravatar url of the image belonging to the given email." + "description": "URL to the email's Gravatar profile picture. It is only populated if check_gravatar is set to true in the request, and if the email has an associated Gravatar." } }, - "required": [ - "is_disposable", - "is_role_account" - ] + "required": ["is_disposable", "is_role_account"] }, "MxDetails": { "title": "MxDetails", @@ -251,10 +243,7 @@ } } }, - "required": [ - "accepts_mail", - "records" - ], + "required": ["accepts_mail", "records"], "description": "Object holding the MX details of the mail server." }, "SmtpDetails": { @@ -309,27 +298,18 @@ "description": "The username of the email, i.e. the part before the \"@\" symbol." } }, - "required": [ - "domain", - "is_valid_syntax", - "username" - ] + "required": ["domain", "is_valid_syntax", "username"] }, "Reachable": { "type": "string", "title": "Reachable", - "enum": [ - "invalid", - "unknown", - "safe", - "risky" - ], + "enum": ["invalid", "unknown", "safe", "risky"], "description": "An enum to describe how confident we are that the recipient address is real: `safe`, `risky`, `invalid` and `unknown`. Check our FAQ to know the meanings of the 4 possibilities: https://help.reacher.email/email-attributes-inside-json." }, "CheckEmailInput": { "title": "CheckEmailInput", "type": "object", - "description": "Input containing all parameters necessary for an email verification.", + "description": "Input containing all parameters necessary for an email verification, as well as some config on how to perform the verification.", "properties": { "from_email": { "type": "string", @@ -345,11 +325,53 @@ }, "proxy": { "$ref": "#/components/schemas/CheckEmailInputProxy" + }, + "smtp_port": { + "type": "number", + "description": "SMTP port to use for email validation. Generally, ports 25, 465, 587 and 2525 are used." + }, + "smtp_timeout": { + "type": "number", + "description": "Add optional timeout for the SMTP verification step, in seconds." + }, + "yahoo_use_api": { + "type": "boolean", + "description": "For Yahoo email addresses, use Yahoo's API instead of connecting directly to their SMTP servers." + }, + "gmail_use_api": { + "type": "boolean", + "description": "For Gmail email addresses, use Gmail's API instead of connecting directly to their SMTP servers." + }, + "microsoft365_use_api": { + "type": "boolean", + "description": "For Microsoft 365 email addresses, use OneDrive's API instead of connecting directly to their SMTP servers." + }, + "check_gravatar": { + "type": "boolean", + "description": "Whether to check if a gravatar image is existing for the given email." + }, + "hotmail_use_headless": { + "type": "string", + "description": "For Hotmail/Outlook email addresses, use a headless navigator connecting to the password recovery page instead of the SMTP server. This assumes you have a WebDriver compatible process running, then pass its endpoint, usually http://localhost:4444. We recommend running chromedriver (and not geckodriver) as it allows parallel requests." + }, + "retries": { + "type": "number", + "default": 2, + "description": "Number of retries of SMTP connections to do." + }, + "smtp_security": { + "type": "string", + "example": "Opportunistic", + "enum": [ + "None", + "Opportunistic", + "Required", + "Wrapper" + ], + "description": "How to apply TLS to a SMTP client connection." } }, - "required": [ - "to_email" - ] + "required": ["to_email"] }, "CheckEmailInputProxy": { "title": "CheckEmailInputProxy", @@ -370,12 +392,17 @@ "port": { "type": "integer", "description": "The proxy port." + }, + "username": { + "type": "string", + "description": "Username to pass to proxy authentication." + }, + "password": { + "type": "string", + "description": "Password to pass to proxy authentication." } }, - "required": [ - "host", - "port" - ] + "required": ["host", "port"] } }, "securitySchemes": { @@ -387,4 +414,4 @@ } } } -} \ No newline at end of file +} diff --git a/backend/src/check.rs b/backend/src/check.rs index 0194ac62c..aace8f1e9 100644 --- a/backend/src/check.rs +++ b/backend/src/check.rs @@ -17,23 +17,15 @@ //! This file contains shared logic for checking one email. use std::env; -use std::time::Duration; use check_if_email_exists::{check_email as ciee_check_email, CheckEmailInput, CheckEmailOutput}; use warp::Filter; use super::sentry_util; -/// Timeout after which we drop the `check-if-email-exists` check. We run the -/// checks twice (to avoid greylisting), so each verification takes 60s max. -const SMTP_TIMEOUT: u64 = 30; - /// Same as `check-if-email-exists`'s check email, but adds some additional /// inputs and error handling. -pub async fn check_email(mut input: CheckEmailInput) -> CheckEmailOutput { - input.set_smtp_timeout(Duration::from_secs(SMTP_TIMEOUT)); - input.set_hotmail_use_headless(env::var("RCH_HOTMAIL_USE_HEADLESS").ok()); - +pub async fn check_email(input: CheckEmailInput) -> CheckEmailOutput { let res = ciee_check_email(&input).await; sentry_util::log_unknown_errors(&res); diff --git a/backend/src/routes/check_email/post.rs b/backend/src/routes/check_email/post.rs index d74a680af..65fc67b40 100644 --- a/backend/src/routes/check_email/post.rs +++ b/backend/src/routes/check_email/post.rs @@ -16,51 +16,16 @@ //! This file implements the `POST /check_email` endpoint. -use std::env; - +use check_if_email_exists::CheckEmailInput; use check_if_email_exists::LOG_TARGET; -use check_if_email_exists::{CheckEmailInput, CheckEmailInputProxy}; -use serde::{Deserialize, Serialize}; use warp::Filter; use crate::check::{check_email, check_header}; -/// Endpoint request body. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct EndpointRequest { - from_email: Option, - hello_name: Option, - proxy: Option, - smtp_port: Option, - to_email: String, -} - -impl From for CheckEmailInput { - fn from(req: EndpointRequest) -> Self { - // Create Request for check_if_email_exists from body - let mut input = CheckEmailInput::new(req.to_email); - input - .set_from_email(req.from_email.unwrap_or_else(|| { - env::var("RCH_FROM_EMAIL").unwrap_or_else(|_| "user@example.org".into()) - })) - .set_hello_name(req.hello_name.unwrap_or_else(|| "gmail.com".into())); - - if let Some(proxy_input) = req.proxy { - input.set_proxy(proxy_input); - } - - if let Some(smtp_port) = req.smtp_port { - input.set_smtp_port(smtp_port); - } - - input - } -} - /// The main endpoint handler that implements the logic of this route. -async fn handler(body: EndpointRequest) -> Result { +async fn handler(body: CheckEmailInput) -> Result { // Run the future to check an email. - Ok(warp::reply::json(&check_email(body.into()).await)) + Ok(warp::reply::json(&check_email(body).await)) } /// Create the `POST /check_email` endpoint. diff --git a/backend/tests/check_email.rs b/backend/tests/check_email.rs index 3604acbbd..a9d217487 100644 --- a/backend/tests/check_email.rs +++ b/backend/tests/check_email.rs @@ -16,8 +16,9 @@ use std::env; +use check_if_email_exists::CheckEmailInput; use reacher_backend::check::REACHER_SECRET_HEADER; -use reacher_backend::routes::{check_email::post::EndpointRequest, create_routes}; +use reacher_backend::routes::create_routes; use warp::http::StatusCode; use warp::test::request; @@ -33,7 +34,7 @@ async fn test_input_foo_bar() { .path("/v0/check_email") .method("POST") .header(REACHER_SECRET_HEADER, "foobar") - .json(&serde_json::from_str::(r#"{"to_email": "foo@bar"}"#).unwrap()) + .json(&serde_json::from_str::(r#"{"to_email": "foo@bar"}"#).unwrap()) .reply(&create_routes(None)) .await; @@ -49,7 +50,7 @@ async fn test_input_foo_bar_baz() { .path("/v0/check_email") .method("POST") .header(REACHER_SECRET_HEADER, "foobar") - .json(&serde_json::from_str::(r#"{"to_email": "foo@bar.baz"}"#).unwrap()) + .json(&serde_json::from_str::(r#"{"to_email": "foo@bar.baz"}"#).unwrap()) .reply(&create_routes(None)) .await; @@ -64,7 +65,7 @@ async fn test_reacher_secret_missing_header() { let resp = request() .path("/v0/check_email") .method("POST") - .json(&serde_json::from_str::(r#"{"to_email": "foo@bar.baz"}"#).unwrap()) + .json(&serde_json::from_str::(r#"{"to_email": "foo@bar.baz"}"#).unwrap()) .reply(&create_routes(None)) .await; @@ -80,7 +81,7 @@ async fn test_reacher_secret_wrong_secret() { .path("/v0/check_email") .method("POST") .header(REACHER_SECRET_HEADER, "barbaz") - .json(&serde_json::from_str::(r#"{"to_email": "foo@bar.baz"}"#).unwrap()) + .json(&serde_json::from_str::(r#"{"to_email": "foo@bar.baz"}"#).unwrap()) .reply(&create_routes(None)) .await; @@ -96,7 +97,7 @@ async fn test_reacher_secret_correct_secret() { .path("/v0/check_email") .method("POST") .header(REACHER_SECRET_HEADER, "foobar") - .json(&serde_json::from_str::(r#"{"to_email": "foo@bar"}"#).unwrap()) + .json(&serde_json::from_str::(r#"{"to_email": "foo@bar"}"#).unwrap()) .reply(&create_routes(None)) .await; diff --git a/core/Cargo.toml b/core/Cargo.toml index aab311ee7..3ed49288f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -31,6 +31,7 @@ serde_json = "1.0.91" trust-dns-proto = "0.21.2" md5 = "0.7.0" levenshtein = "1.0.5" +serde_with = "2.1.0" [dev-dependencies] tokio = { version = "1.23.0" } diff --git a/core/src/smtp/mod.rs b/core/src/smtp/mod.rs index dd7597882..1bd11a01f 100644 --- a/core/src/smtp/mod.rs +++ b/core/src/smtp/mod.rs @@ -121,7 +121,7 @@ mod tests { let to_email = EmailAddress::from_str("foo@gmail.com").unwrap(); let host = Name::from_str("gmail.com").unwrap(); let mut input = CheckEmailInput::default(); - input.set_smtp_timeout(Duration::from_millis(1)); + input.set_smtp_timeout(Some(Duration::from_millis(1))); let res = runtime.block_on(check_smtp(&to_email, &host, 25, "gmail.com", &input)); match res { diff --git a/core/src/util/input_output.rs b/core/src/util/input_output.rs index b1b15b253..4b18161aa 100644 --- a/core/src/util/input_output.rs +++ b/core/src/util/input_output.rs @@ -14,13 +14,16 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use std::time::Duration; + +use async_smtp::{ClientSecurity, ClientTlsParameters}; +use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer}; +use serde_with::{serde_as, DurationSeconds}; + use crate::misc::{MiscDetails, MiscError}; use crate::mx::{MxDetails, MxError}; use crate::smtp::{SmtpDetails, SmtpError, SmtpErrorDesc}; use crate::syntax::SyntaxDetails; -use async_smtp::{ClientSecurity, ClientTlsParameters}; -use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer}; -use std::time::Duration; /// Perform the email verification via a specified proxy. The usage of a proxy /// is optional. @@ -50,6 +53,12 @@ pub enum SmtpSecurity { Wrapper, } +impl Default for SmtpSecurity { + fn default() -> Self { + Self::Opportunistic + } +} + impl SmtpSecurity { pub fn to_client_security(self, tls_params: ClientTlsParameters) -> ClientSecurity { match self { @@ -63,6 +72,7 @@ impl SmtpSecurity { /// Builder pattern for the input argument into the main `email_exists` /// function. +#[serde_as] #[derive(Debug, Clone, Deserialize, Serialize)] pub struct CheckEmailInput { /// The email to validate. @@ -70,10 +80,12 @@ pub struct CheckEmailInput { /// Email to use in the `MAIL FROM:` SMTP command. /// /// Defaults to "user@example.org". + #[serde(default)] pub from_email: String, /// Name to use in the `EHLO:` SMTP command. /// /// Defaults to "localhost" (note: "localhost" is not a FQDN). + #[serde(default)] pub hello_name: String, /// Perform the email verification via the specified SOCK5 proxy. The usage of a /// proxy is optional. @@ -82,27 +94,37 @@ pub struct CheckEmailInput { /// and 2525 are used. /// /// Defaults to 25. + #[serde(default)] pub smtp_port: u16, - /// Add optional timeout for the SMTP verification step. + /// Add timeout for the SMTP verification step. Set to None if you don't + /// want to use a timeout. + /// + /// Defaults to 10s. + #[serde_as(as = "Option")] + #[serde(default)] pub smtp_timeout: Option, /// For Yahoo email addresses, use Yahoo's API instead of connecting /// directly to their SMTP servers. /// /// Defaults to true. + #[serde(default)] pub yahoo_use_api: bool, /// For Gmail email addresses, use Gmail's API instead of connecting /// directly to their SMTP servers. /// /// Defaults to false. + #[serde(default)] pub gmail_use_api: bool, /// For Microsoft 365 email addresses, use OneDrive's API instead of /// connecting directly to their SMTP servers. /// /// Defaults to false. + #[serde(default)] pub microsoft365_use_api: bool, // Whether to check if a gravatar image is existing for the given email. // - // Defaults to false + // Defaults to false. + #[serde(default)] pub check_gravatar: bool, /// For Hotmail/Outlook email addresses, use a headless navigator /// connecting to the password recovery page instead of the SMTP server. @@ -112,14 +134,17 @@ pub struct CheckEmailInput { /// /// Defaults to None. #[cfg(feature = "headless")] + #[serde(default)] pub hotmail_use_headless: Option, /// Number of retries of SMTP connections to do. /// /// Defaults to 2 to avoid greylisting. + #[serde(default)] pub retries: usize, /// How to apply TLS to a SMTP client connection. /// /// Defaults to Opportunistic. + #[serde(default)] pub smtp_security: SmtpSecurity, } @@ -128,13 +153,13 @@ impl Default for CheckEmailInput { CheckEmailInput { to_email: "".into(), from_email: "user@example.org".into(), - hello_name: "localhost".into(), + hello_name: "example.org".into(), #[cfg(feature = "headless")] hotmail_use_headless: None, proxy: None, smtp_port: 25, - smtp_security: SmtpSecurity::Opportunistic, - smtp_timeout: None, + smtp_security: SmtpSecurity::default(), + smtp_timeout: Some(Duration::from_secs(10)), yahoo_use_api: true, gmail_use_api: false, microsoft365_use_api: false, @@ -225,9 +250,11 @@ impl CheckEmailInput { self } - /// Add optional timeout for the SMTP verification step. - pub fn set_smtp_timeout(&mut self, duration: Duration) -> &mut CheckEmailInput { - self.smtp_timeout = Some(duration); + /// Add optional timeout for the SMTP verification step. This is the + /// timeout for _each_ SMTP connection attempt, not for the whole email + /// verification process. + pub fn set_smtp_timeout(&mut self, duration: Option) -> &mut CheckEmailInput { + self.smtp_timeout = duration; self }