1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
// check-if-email-exists
// Copyright (C) 2018-2022 Reacher

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.

// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use crate::misc::{MiscDetails, MiscError};
use crate::mx::{MxDetails, MxError};
use crate::smtp::{SmtpDetails, SmtpError};
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.
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct CheckEmailInputProxy {
	/// Use the specified SOCKS5 proxy host to perform email verification.
	pub host: String,
	/// Use the specified SOCKS5 proxy port to perform email verification.
	pub port: u16,
	/// Username to pass to proxy authentication.
	pub username: Option<String>,
	/// Password to pass to proxy authentication.
	pub password: Option<String>,
}

/// Define how to apply TLS to a SMTP client connection. Will be converted into
/// async_smtp::ClientSecurity.
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
pub enum SmtpSecurity {
	/// Insecure connection only (for testing purposes).
	None,
	/// Start with insecure connection and use `STARTTLS` when available.
	Opportunistic,
	/// Start with insecure connection and require `STARTTLS`.
	Required,
	/// Use TLS wrapped connection.
	Wrapper,
}

impl SmtpSecurity {
	pub fn to_client_security(self, tls_params: ClientTlsParameters) -> ClientSecurity {
		match self {
			Self::None => ClientSecurity::None,
			Self::Opportunistic => ClientSecurity::Opportunistic(tls_params),
			Self::Required => ClientSecurity::Required(tls_params),
			Self::Wrapper => ClientSecurity::Wrapper(tls_params),
		}
	}
}

/// Builder pattern for the input argument into the main `email_exists`
/// function.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CheckEmailInput {
	/// The email to validate.
	pub to_emails: Vec<String>,
	/// Email to use in the `MAIL FROM:` SMTP command.
	///
	/// Defaults to "user@example.org".
	pub from_email: String,
	/// Name to use in the `EHLO:` SMTP command.
	///
	/// Defaults to "localhost" (note: "localhost" is not a FQDN).
	pub hello_name: String,
	/// Perform the email verification via the specified SOCK5 proxy. The usage of a
	/// proxy is optional.
	pub proxy: Option<CheckEmailInputProxy>,
	/// SMTP port to use for email validation. Generally, ports 25, 465, 587
	/// and 2525 are used.
	///
	/// Defaults to 25.
	pub smtp_port: u16,
	/// Add optional timeout for the SMTP verification step.
	pub smtp_timeout: Option<Duration>,
	/// For Yahoo email addresses, use Yahoo's API instead of connecting
	/// directly to their SMTP servers.
	///
	/// Defaults to true.
	pub yahoo_use_api: bool,
	/// Number of retries of SMTP connections to do.
	///
	/// Defaults to 2 to avoid greylisting.
	pub retries: usize,
	/// How to apply TLS to a SMTP client connection.
	///
	/// Defaults to Opportunistic.
	pub smtp_security: SmtpSecurity,
}

impl Default for CheckEmailInput {
	fn default() -> Self {
		CheckEmailInput {
			to_emails: vec![],
			from_email: "user@example.org".into(),
			hello_name: "localhost".into(),
			proxy: None,
			smtp_port: 25,
			smtp_security: SmtpSecurity::None,
			smtp_timeout: None,
			yahoo_use_api: true,
			retries: 2,
		}
	}
}

impl CheckEmailInput {
	/// Create a new CheckEmailInput.
	pub fn new(to_emails: Vec<String>) -> CheckEmailInput {
		CheckEmailInput {
			to_emails,
			..Default::default()
		}
	}

	/// Set the email to use in the `MAIL FROM:` SMTP command. Defaults to
	/// `user@example.org` if not explicitly set.
	#[deprecated(since = "0.8.24", note = "Please use set_from_email instead")]
	pub fn from_email(&mut self, email: String) -> &mut CheckEmailInput {
		self.from_email = email;
		self
	}

	/// Set the email to use in the `MAIL FROM:` SMTP command. Defaults to
	/// `user@example.org` if not explicitly set.
	pub fn set_from_email(&mut self, email: String) -> &mut CheckEmailInput {
		self.from_email = email;
		self
	}

	/// Set the name to use in the `EHLO:` SMTP command. Defaults to `localhost`
	/// if not explicitly set.
	#[deprecated(since = "0.8.24", note = "Please use set_hello_name instead")]
	pub fn hello_name(&mut self, name: String) -> &mut CheckEmailInput {
		self.hello_name = name;
		self
	}

	/// Set the name to use in the `EHLO:` SMTP command. Defaults to `localhost`
	/// if not explicitly set.
	pub fn set_hello_name(&mut self, name: String) -> &mut CheckEmailInput {
		self.hello_name = name;
		self
	}

	/// Use the specified SOCK5 proxy to perform email verification.
	#[deprecated(since = "0.8.24", note = "Please use set_proxy instead")]
	pub fn proxy(&mut self, proxy_host: String, proxy_port: u16) -> &mut CheckEmailInput {
		self.proxy = Some(CheckEmailInputProxy {
			host: proxy_host,
			port: proxy_port,
			..Default::default()
		});
		self
	}

	/// Use the specified SOCK5 proxy to perform email verification.
	pub fn set_proxy(&mut self, proxy: CheckEmailInputProxy) -> &mut CheckEmailInput {
		self.proxy = Some(proxy);
		self
	}

	/// Set the number of SMTP retries to do.
	pub fn set_retries(&mut self, retries: usize) -> &mut CheckEmailInput {
		self.retries = retries;
		self
	}

	/// Add optional timeout for the SMTP verification step.
	#[deprecated(since = "0.8.24", note = "Please use set_smtp_timeout instead")]
	pub fn smtp_timeout(&mut self, duration: Duration) -> &mut CheckEmailInput {
		self.smtp_timeout = Some(duration);
		self
	}

	/// Change the SMTP port.
	pub fn set_smtp_port(&mut self, port: u16) -> &mut CheckEmailInput {
		self.smtp_port = port;
		self
	}

	/// Set the SMTP client security to use for TLS.
	pub fn set_smtp_security(&mut self, smtp_security: SmtpSecurity) -> &mut CheckEmailInput {
		self.smtp_security = smtp_security;
		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);
		self
	}

	/// Set whether to use Yahoo's API or connecting directly to their SMTP
	/// servers. Defaults to true.
	#[deprecated(since = "0.8.24", note = "Please use set_yahoo_use_api instead")]
	pub fn yahoo_use_api(&mut self, use_api: bool) -> &mut CheckEmailInput {
		self.yahoo_use_api = use_api;
		self
	}

	/// Set whether to use Yahoo's API or connecting directly to their SMTP
	/// servers. Defaults to true.
	pub fn set_yahoo_use_api(&mut self, use_api: bool) -> &mut CheckEmailInput {
		self.yahoo_use_api = use_api;
		self
	}
}

/// An enum to describe how confident we are that the recipient address is
/// real.
#[derive(Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Reachable {
	/// The email is safe to send.
	Safe,
	/// The email address appears to exist, but has quality issues that may
	/// result in low engagement or a bounce. Emails are classified as risky
	/// when one of the following happens:
	/// - catch-all email,
	/// - disposable email,
	/// - role-based address,
	/// - full inbox.
	Risky,
	/// Emails that don't exist or are syntactically incorrect. Do not send to
	/// these emails.
	Invalid,
	/// We're unable to get a valid response from the recipient's email server.
	Unknown,
}

/// The result of the [check_email](check_email) function.
#[derive(Debug)]
pub struct CheckEmailOutput {
	/// Input by the user.
	pub input: String,
	pub is_reachable: Reachable,
	/// Misc details about the email address.
	pub misc: Result<MiscDetails, MiscError>,
	/// Details about the MX host.
	pub mx: Result<MxDetails, MxError>,
	/// Details about the SMTP responses of the email.
	pub smtp: Result<SmtpDetails, SmtpError>,
	/// Details about the email address.
	pub syntax: SyntaxDetails,
}

impl Default for CheckEmailOutput {
	fn default() -> Self {
		CheckEmailOutput {
			input: String::default(),
			is_reachable: Reachable::Unknown,
			misc: Ok(MiscDetails::default()),
			mx: Ok(MxDetails::default()),
			smtp: Ok(SmtpDetails::default()),
			syntax: SyntaxDetails::default(),
		}
	}
}

// Implement a custom serialize.
impl Serialize for CheckEmailOutput {
	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
	where
		S: Serializer,
	{
		// This is just used internally to get the nested error field.
		#[derive(Serialize)]
		struct MyError<E> {
			error: E,
		}

		let mut map = serializer.serialize_map(Some(1))?;
		map.serialize_entry("input", &self.input)?;
		map.serialize_entry("is_reachable", &self.is_reachable)?;
		match &self.misc {
			Ok(t) => map.serialize_entry("misc", &t)?,
			Err(error) => map.serialize_entry("misc", &MyError { error })?,
		}
		match &self.mx {
			Ok(t) => map.serialize_entry("mx", &t)?,
			Err(error) => map.serialize_entry("mx", &MyError { error })?,
		}
		match &self.smtp {
			Ok(t) => map.serialize_entry("smtp", &t)?,
			Err(error) => map.serialize_entry("smtp", &MyError { error })?,
		}
		map.serialize_entry("syntax", &self.syntax)?;
		map.end()
	}
}