Skip to content

Commit eb5f51e

Browse files
committed
fix: add STARTTLS support for SMTP verification
1 parent 2c1c4b6 commit eb5f51e

File tree

4 files changed

+169
-17
lines changed

4 files changed

+169
-17
lines changed

.gitignore

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/target/
2+
**/*.rs.bk
3+
Cargo.lock
4+
*.pdb
5+
6+
*.json
7+
/input/
8+
/output/
9+
/results/
10+
results.json
11+
input.json
12+
13+
*.log
14+
/logs/
15+
/log/
16+
17+
/.idea/
18+
/.vscode/
19+
*.swp
20+
*.swo
21+
*~
22+
.project
23+
.classpath
24+
.c9/
25+
*.launch
26+
.settings/
27+
*.sublime-workspace
28+
.history/
29+
30+
.DS_Store
31+
.DS_Store?
32+
._*
33+
.Spotlight-V100
34+
.Trashes
35+
ehthumbs.db
36+
Thumbs.db
37+
38+
*.bak
39+
*.backup
40+
*.old
41+
*.orig
42+
43+
/test-data/
44+
/fixtures/
45+
46+
*.prof
47+
*.dmp
48+
*.dump
49+
50+
/dev/
51+
/.dev/
52+
/local/

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! # Email Sleuth RS
22
//!
33
//! A Rust application to discover and verify professional email addresses
4-
//! based on contact names and company websites. Inspired by a similar Python tool.
4+
//! based on contact names and company websites.
55
//! This serves as the main entry point for the application.
66
77
#![warn(missing_docs, unreachable_pub, rust_2018_idioms)]

src/sleuth.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ impl EmailSleuth {
4343
/// Finds and verifies email addresses for a given contact.
4444
///
4545
/// This is the main entry point for the finding logic corresponding to the
46-
/// Python class's `find_email` method.
4746
///
4847
/// # Arguments
4948
/// * `contact` - A validated contact with required information.

src/smtp.rs

Lines changed: 116 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ use rand::Rng;
1111
use std::net::ToSocketAddrs;
1212
use std::str::FromStr;
1313
use std::time::Duration;
14-
1514
/// Performs the SMTP RCPT TO check for a single email address.
16-
/// This attempts to replicate the logic from the Python script's _verify_smtp function.
1715
/// Uses lower-level SmtpConnection for command control.
1816
///
1917
/// # Arguments
@@ -64,18 +62,89 @@ async fn verify_smtp_email(
6462

6563
let helo_name = lettre::transport::smtp::extension::ClientId::Domain("localhost".to_string());
6664

65+
// First attempt: Try without TLS (standard connection)
66+
let connect_result = try_verify_with_connection(
67+
socket_addr,
68+
&helo_name,
69+
&sender_address,
70+
&recipient_address,
71+
email,
72+
domain,
73+
mail_server,
74+
false,
75+
)
76+
.await;
77+
78+
// If the first attempt failed with a STARTTLS requirement, retry with TLS
79+
match &connect_result {
80+
Ok(result) => {
81+
// Check if the error message indicates STARTTLS is needed
82+
let msg = result.message.to_lowercase();
83+
if msg.contains("starttls")
84+
|| msg.contains("tls required")
85+
|| (msg.contains("530") && msg.contains("5.7.0"))
86+
{
87+
tracing::info!(target: "smtp_task",
88+
"Server appears to require STARTTLS, retrying with TLS enabled");
89+
90+
// Try again with TLS enabled
91+
return try_verify_with_connection(
92+
socket_addr,
93+
&helo_name,
94+
&sender_address,
95+
&recipient_address,
96+
email,
97+
domain,
98+
mail_server,
99+
true,
100+
)
101+
.await;
102+
}
103+
}
104+
Err(e) => {
105+
tracing::error!(target: "smtp_task",
106+
"Error during verification attempt: {}", e);
107+
}
108+
}
109+
110+
connect_result
111+
}
112+
113+
/// Tries to verify an email using a specific connection type (with or without TLS)
114+
async fn try_verify_with_connection(
115+
socket_addr: std::net::SocketAddr,
116+
helo_name: &lettre::transport::smtp::extension::ClientId,
117+
sender_address: &Address,
118+
recipient_address: &Address,
119+
email: &str,
120+
domain: &str,
121+
mail_server: &str,
122+
use_tls: bool,
123+
) -> Result<SmtpVerificationResult> {
124+
let tls_parameters = if use_tls {
125+
Some(
126+
lettre::transport::smtp::client::TlsParameters::new(mail_server.to_string()).map_err(
127+
|e| AppError::SmtpTls(format!("Failed to create TLS parameters: {}", e)),
128+
)?,
129+
)
130+
} else {
131+
None
132+
};
133+
67134
let mut smtp_conn = match SmtpConnection::connect(
68135
socket_addr,
69136
Some(CONFIG.smtp_timeout),
70-
&helo_name,
71-
None,
137+
helo_name,
138+
tls_parameters.as_ref(),
72139
None,
73140
) {
74141
Ok(conn) => conn,
75142
Err(e) => {
76-
tracing::warn!(target: "smtp_task", "SMTP connection failed for {}: {}", mail_server, e);
77-
78143
let err_string = e.to_string();
144+
tracing::warn!(target: "smtp_task",
145+
"SMTP connection failed for {} (TLS={}): {}",
146+
mail_server, use_tls, e);
147+
79148
if err_string.contains("timed out") || err_string.contains("connection refused") {
80149
tracing::error!(target: "smtp_task",
81150
"Port 25 appears to be blocked by your ISP or network. Consider using a different network or VPN.");
@@ -89,33 +158,53 @@ async fn verify_smtp_email(
89158
}
90159
};
91160

161+
tracing::debug!(target: "smtp_task",
162+
"Established {} connection to {}:{}",
163+
if use_tls { "TLS" } else { "plaintext" },
164+
mail_server,
165+
socket_addr.port());
166+
92167
match smtp_conn.command(Ehlo::new(helo_name.clone())) {
93168
Ok(_) => {
94-
tracing::debug!(target: "smtp_task", "Initial EHLO successful");
169+
tracing::debug!(target: "smtp_task", "EHLO successful");
95170
}
96171
Err(e) => {
97-
tracing::warn!(target: "smtp_task", "Initial EHLO failed: {}", e);
172+
tracing::warn!(target: "smtp_task", "EHLO failed: {}", e);
98173
return Ok(handle_smtp_error(&e, mail_server));
99174
}
100175
}
101176

102-
tracing::debug!(target: "smtp_task", "SMTP connection established to {}:{}", mail_server, socket_addr.port());
103-
104177
tracing::debug!(target: "smtp_task", "Sending MAIL FROM:<{}>...", &CONFIG.smtp_sender_email);
105178
match smtp_conn.command(Mail::new(Some(sender_address.clone()), vec![])) {
106179
Ok(response) => {
107180
if response.is_positive() {
108181
tracing::debug!(target: "smtp_task", "MAIL FROM accepted by {}: {:?}", mail_server, response);
109182
} else {
183+
let message = response.message().collect::<Vec<&str>>().join(" ");
110184
tracing::error!(target: "smtp_task",
111-
"SMTP sender '{}' rejected by {}: {:?}",
112-
&CONFIG.smtp_sender_email, mail_server, response
185+
"SMTP sender '{}' rejected by {}: {} {:?}",
186+
&CONFIG.smtp_sender_email, mail_server, response.code(), message
113187
);
188+
189+
// Check if server requires STARTTLS but we didn't detect it
190+
if message.to_lowercase().contains("starttls")
191+
|| (response.code().to_string().starts_with("530") && message.contains("5.7.0"))
192+
{
193+
tracing::warn!(target: "smtp_task",
194+
"Server requires STARTTLS but current connection doesn't support it");
195+
smtp_conn.quit().ok();
196+
return Ok(SmtpVerificationResult::inconclusive_retry(format!(
197+
"Server requires STARTTLS: {} {}",
198+
response.code(),
199+
message
200+
)));
201+
}
202+
114203
smtp_conn.quit().ok();
115204
return Ok(SmtpVerificationResult::inconclusive_no_retry(format!(
116205
"MAIL FROM rejected: {} {}",
117206
response.code(),
118-
response.message().collect::<Vec<&str>>().join(" ")
207+
message
119208
)));
120209
}
121210
}
@@ -238,9 +327,9 @@ async fn verify_smtp_email(
238327
];
239328
let message_lower = target_message.to_lowercase();
240329

241-
let code_value = u16::from(target_code);
330+
let code_value = target_code.to_string();
242331

243-
if [550, 551, 553].contains(&code_value)
332+
if ["550", "551", "553"].contains(&code_value.as_str())
244333
|| rejection_phrases.iter().any(|p| message_lower.contains(p))
245334
{
246335
SmtpVerificationResult::conclusive(
@@ -282,6 +371,18 @@ fn handle_smtp_error(
282371
) -> SmtpVerificationResult {
283372
let err_string = error.to_string();
284373

374+
// Check if the error is related to STARTTLS requirement
375+
if err_string.contains("STARTTLS")
376+
|| err_string.contains("starttls")
377+
|| (err_string.contains("530") && err_string.contains("5.7.0"))
378+
{
379+
tracing::warn!(target: "smtp_task", "Server {} requires STARTTLS: {}", server, error);
380+
return SmtpVerificationResult::inconclusive_retry(format!(
381+
"SMTP requires TLS encryption: {}",
382+
err_string
383+
));
384+
}
385+
285386
if err_string.contains("550")
286387
&& (err_string.contains("does not exist")
287388
|| err_string.contains("no such user")

0 commit comments

Comments
 (0)