check_if_email_exists/smtp/parser.rs
1// Reacher - Email Verification
2// Copyright (C) 2018-2023 Reacher
3
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published
6// by the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Parse the SMTP responses to get information about the email address.
18
19use super::error::SmtpError;
20use crate::EmailAddress;
21use async_smtp::error::Error as AsyncSmtpError;
22
23/// is_invalid checks for SMTP responses meaning that the email is invalid,
24/// i.e. that the mailbox doesn't exist.
25pub fn is_invalid(e: &str, email: &EmailAddress) -> bool {
26 // 550 Address rejected
27 // 550 5.1.1 : Recipient address rejected
28 // 550 5.1.1 : Recipient address rejected: User unknown in virtual alias table
29 // 550 5.1.1 <EMAIL: Recipient address rejected: User unknown in relay recipient table
30 e.contains("address rejected")
31 // 550 5.1.1 : Unrouteable address
32 || e.contains("unrouteable")
33 // 550 5.1.1 : The email account that you tried to reach does not exist
34 || e.contains("does not exist")
35 // 550 invalid address
36 // 550 User not local or invalid address – Relay denied
37 || e.contains("invalid address")
38 // 5.1.1 Invalid email address
39 || e.contains("invalid email address")
40 // 550 Invalid recipient
41 || e.contains("invalid recipient")
42 || e.contains("may not exist")
43 || e.contains("recipient invalid")
44 // 550 5.1.1 : Recipient rejected
45 || e.contains("recipient rejected")
46 // permanent: 5.1.1 Unknown recipient address
47 || e.contains("unknown recipient address")
48 // 554 Unknown Recipient (#5.1.1) (on @parkwayhonda.com)
49 || e.contains("unknown recipient")
50 || e.contains("undeliverable")
51 // 550 User unknown
52 // 550 5.1.1 <EMAIL> User unknown
53 // 550 recipient address rejected: user unknown in local recipient table
54 || e.contains("user unknown")
55 // 550 Unknown user
56 || e.contains("unknown user")
57 // 5.1.1 Recipient unknown <EMAIL>
58 || e.contains("recipient unknown")
59 // 550 5.1.1 No such user - pp
60 // 550 No such user here
61 || e.contains("no such user")
62 // permanent: 5.1.1 MXIN501 mailbox <EMAIL> unknown (on @virginmedia.com)
63 || e.contains(format!("mailbox {email} unknown").as_str())
64 // 550 5.1.1 : Mailbox not found
65 // 550 Unknown address error ‘MAILBOX NOT FOUND’
66 || e.contains("mailbox not found")
67 // 550 5.1.1 : Invalid mailbox
68 || e.contains("invalid mailbox")
69 // 550 5.1.1 Sorry, no mailbox here by that name
70 || e.contains("no mailbox")
71 // 5.2.0 No such mailbox
72 || e.contains("no such mailbox")
73 // 550 Requested action not taken: mailbox unavailable
74 || e.contains("mailbox unavailable")
75 // 5.5.0 Requested actions not taken as the mailbox is unavailable (on @etu.uca.fr)
76 || e.contains("mailbox is unavailable")
77 // 550 5.1.1 Is not a valid mailbox
78 || e.contains("not a valid mailbox")
79 // No such recipient here
80 || e.contains("no such recipient")
81 // 554 delivery error: This user doesn’t have an account
82 || e.contains("have an account")
83 // permanent: Unknown local part <USER> in <USER@flabeg.com> (on @flabeg.com)
84 || e.contains("unknown local part")
85 // 5.1.1 RCP-P1 Domain facebook.com no longer available https://www.facebook.com/postmaster/response_codes?ip=3.80.111.155#RCP-P1
86 || e.contains("no longer available")
87 // permanent: RCPT (<EMAIL>) dosn't exist (on @hgy.ooo, @stigpods.com.cn)
88 || e.contains("dosn't exist") // sic! typo is intentional
89 // 5.1.1 <EMAIL>: Email address could not be found, or was misspelled (G8) (on @biotech-calendar.com, @invoicefactoring.com)
90 || e.contains("could not be found")
91 // No such person at this address (on @aconsa.com.mx)
92 || e.contains("no such person")
93 // Callout verification failed: 550 No Such User Here (on @medipro.co.uk)
94 || e.contains("no such user")
95 // 5.1.1 <EMAIL> Address Error (on @lucidity.co.za)
96 || e.contains("address error")
97 // E-mail address is not handled by this system (on @kaimayfair.co.uk)
98 || e.contains("address is not handled")
99 // permanent: 5.1.1 recipient is not exist (on @sim.com)
100 || e.contains("recipient is not exist")
101 // permanent: 5.1.1 <EMAIL> Recipient not found. (on @4polymer.com)
102 || e.contains("recipient not found")
103 // permanent: 5.7.1 Email doesn't exist. Please forward it or send it to contact@magency.f; 5.7.1 r - gcdp 38308e7fff4ca-30344241c74si13773751fa.513 - gsmtp"
104 || e.contains("email doesn't exist")
105 // permanent: verify address failed, User not found ***@salemall.vn (on @salemall.vn)
106 || e.contains("verify address failed")
107 // transient: unable to verify user (on @computan.net)
108 || e.contains("unable to verify user")
109 // permanent: 5.1.1 Utilisateur inconnu (on @sante.fr)
110 || e.contains("utilisateur inconnu")
111 // permanent: 5.1.1
112 || e.contains("permanent: 5.1.1")
113 // permanent: 5.7.1
114 || e.contains("permanent: 5.7.1")
115}
116
117/// Check that the mailbox has a full inbox.
118pub fn is_full_inbox(e: &str) -> bool {
119 e.contains("insufficient")
120 // https://answers.microsoft.com/en-us/outlook_com/forum/all/how-do-i-interpret-the-delivery-failure-message/2f1bf9c0-8b03-4f8f-aacc-5f6ba60a73f3
121 || e.contains("mailbox full")
122 // https://answers.microsoft.com/en-us/outlook_com/forum/all/how-do-i-interpret-the-delivery-failure-message/2f1bf9c0-8b03-4f8f-aacc-5f6ba60a73f3
123 || e.contains("quote exceeded")
124 || e.contains("over quota")
125 // 550 user has too many messages on the server
126 || e.contains("too many messages")
127 // transient: 4.2.2 The recipient's inbox is out of storage space. Please direct the; 4.2.2 recipient to; 4.2.2 https://support.google.com/mail/?p=OverQuotaTemp 41be03b00d2f7-801d5c00011si4287631a12.311 - gsmtp (on @gmail.com)
128 || e.contains("out of storage space")
129}
130
131/// Check if the email account has been disabled or blocked by the email
132/// provider.
133pub fn is_disabled_account(e: &str) -> bool {
134 // 554 The email account that you tried to reach is disabled. Learn more at https://support.google.com/mail/?p=DisabledUser"
135 e.contains("disabled")
136 // 554 delivery error: Sorry your message to <EMAIL> cannot be delivered. This account has been disabled or discontinued
137 || e.contains("discontinued")
138 //550 5.2.1 RACT MY.IP: Mailbox is inactive: <USER@hanmail.net><CRLF> (on hanmail.net)
139 || e.contains("inactive")
140}
141
142/// Check if the error is an IO "incomplete" error.
143pub fn is_err_io_errors(e: &SmtpError) -> bool {
144 match e {
145 SmtpError::AsyncSmtpError(AsyncSmtpError::Io(err)) => err.to_string() == "incomplete",
146 _ => false,
147 }
148}
149
150/// Check if the IP is blacklisted.
151pub fn is_err_ip_blacklisted(e: &SmtpError) -> bool {
152 let e = match e {
153 SmtpError::AsyncSmtpError(AsyncSmtpError::Transient(r) | AsyncSmtpError::Permanent(r)) => {
154 // TODO We can use .to_string() after:
155 // https://github.com/async-email/async-smtp/pull/53
156 r.message.join("; ").to_lowercase()
157 }
158 _ => {
159 return false;
160 }
161 };
162
163 // Permanent errors
164
165 // 5.7.1 IP address blacklisted by recipient
166 // 5.7.1 Service unavailable; Client host [147.75.45.223] is blacklisted. Visit https://www.sophos.com/en-us/threat-center/ip-lookup.aspx?ip=147.75.45.223 to request delisting
167 // 5.3.0 <EMAIL>... Mail from 147.75.45.223 rejected by Abusix blacklist (on @helsinki.fi)
168 e.contains("blacklist")
169 // Rejected because 23.129.64.213 is in a black list at b.barracudacentral.org
170 || e.contains("black list")
171 // 5.7.1 Recipient not authorized, your IP has been found on a block list
172 // gmx.net (mxgmx117) Nemesis ESMTP Service not available; No SMTP service; IP address is block listed.; For explanation visit https://www.gmx.net/mail/senderguidelines?c=bl (on @gmx.net, @web.de)
173 || e.contains("block list")
174 // Unable to add <EMAIL> because host 23.129.64.184 is listed on zen.spamhaus.org
175 // 5.7.1 Service unavailable, Client host [23.129.64.184] blocked using Spamhaus.
176 // 5.7.1 Email cannot be delivered. Reason: Email detected as Spam by spam filters.
177 || e.contains("spam")
178 // host 23.129.64.216 is listed at combined.mail.abusix.zone (127.0.0.12,
179 || e.contains("abusix")
180 // 5.7.1 Relaying denied. IP name possibly forged [45.154.35.252]
181 // 5.7.1 Relaying denied: You must check for new mail before sending mail. [23.129.64.216]
182 || e.contains("relaying denied")
183 // 5.7.1 <unknown[23.129.64.100]>: Client host rejected: Access denied
184 || e.contains("access denied")
185 // sorry, mail from your location [5.79.109.48] is administratively denied (#5.7.1)
186 || e.contains("administratively denied")
187 // 5.7.606 Access denied, banned sending IP [23.129.64.216]
188 || e.contains("banned")
189 // Blocked - see https://ipcheck.proofpoint.com/?ip=23.129.64.192
190 // 5.7.1 Mail from 23.129.64.183 has been blocked by Trend Micro Email Reputation Service.
191 || e.contains("blocked")
192 // Connection rejected by policy [7.3] 38206, please visit https://support.symantec.com/en_US/article.TECH246726.html for more details about this error message.
193 || e.contains("connection rejected")
194 // csi.mimecast.org Poor Reputation Sender. - https://community.mimecast.com/docs/DOC-1369#550 [6ATVl4DjOvSA6XNsWGoUFw.us31]
195 // Your access to this mail system has been rejected due to the sending MTA\'s poor reputation. If you believe that this failure is in error, please contact the intended recipient via alternate means.
196 || e.contains("poor reputation")
197 // JunkMail rejected - (gmail.com) [193.218.118.140]:46615 is in an RBL: http://www.barracudanetworks.com/reputation/?pr=1&ip=193.218.118.140
198 || e.contains("junkmail")
199 // mailfi01.lmco.com ESMTP 550 5.7.0 Mail from 18.234.87.196 refused by Proofpoint Reputation Services. SENDER please see and take action: https://support.proofpoint.com/dnsbl-lookup.cgi?18.234.87.196" (on @lmco.com)
200 || e.contains("refused by proofpoint")
201 // resimta-h1p-037598.sys.comcast.net resimta-h1p-037598.sys.comcast.net 5.135.185.166 found on one or more DNSBLs, see http://postmaster.comcast.net/smtp-error-codes.php#BL000001 (on @comcast.net)
202 || e.contains("dnsbl")
203 // smtp-fw-9107.amazon.com; SBRS score too low: http://www.senderbase.org/ (on @amazon.com)
204 || e.contains("sbrs score too low")
205 // https://www.spamhaus.org/sbl/query/SBLCSShttps://www.spamhaus.org/query/ip/3.238.201.74 (on @knollridges.com.ph)
206 || e.contains("spamhaus")
207
208 // Transient errors
209
210 // Blocked - see https://www.spamcop.net/bl.shtml?23.129.64.211
211 || e.contains("blocked")
212 // 4.7.1 <EMAIL>: Relay access denied
213 || e.contains("access denied")
214 // relay not permitted!
215 || e.contains("relay not permitted")
216 // 23.129.64.216 is not yet authorized to deliver mail from
217 || e.contains("not yet authorized")
218}
219
220/// Check if the IP needs a reverse DNS.
221pub fn is_err_needs_rdns(e: &SmtpError) -> bool {
222 let e = match e {
223 SmtpError::AsyncSmtpError(AsyncSmtpError::Transient(r) | AsyncSmtpError::Permanent(r)) => {
224 // TODO We can use .to_string() after:
225 // https://github.com/async-email/async-smtp/pull/53
226 r.message.join("; ").to_lowercase()
227 }
228 _ => {
229 return false;
230 }
231 };
232
233 // 4.7.25 Client host rejected: cannot find your hostname, [147.75.45.223]
234 // 4.7.1 Client host rejected: cannot find your reverse hostname, [147.75.45.223]
235 // 5.7.1 Client host rejected: cannot find your reverse hostname, [23.129.64.184]
236 e.contains("cannot find your reverse hostname")
237 // You dont seem to have a reverse dns entry. Come back later. You are greylisted for 20 minutes. See http://www.fsf.org/about/systems/greylisting
238 || e.contains("reverse dns entry")
239}
240
241#[cfg(test)]
242mod tests {
243
244 use super::{is_err_ip_blacklisted, is_invalid};
245 use crate::EmailAddress;
246 use crate::SmtpError::AsyncSmtpError;
247 use async_smtp::{
248 error::Error,
249 response::{Category, Code, Detail, Response, Severity},
250 };
251 use std::str::FromStr;
252
253 #[test]
254 fn test_is_invalid() {
255 let email = EmailAddress::from_str("foo@bar.baz").unwrap();
256
257 assert!(!is_invalid(
258 "554 5.7.1 <mta.voipdir.net[]>: Client host rejected: Access denied",
259 &email
260 ));
261
262 assert!(is_invalid(
263 "RCPT (***@stigpods.com.cn) dosn't exist",
264 &email
265 ));
266
267 assert!(is_invalid(
268 "permanent: 5.1.1 MXIN501 mailbox foo@bar.baz unknown (on @virginmedia.com)",
269 &email
270 ));
271 }
272
273 #[test]
274 fn test_is_err_ip_blacklisted() {
275 let err = Error::Permanent(Response::new(
276 Code::new(
277 Severity::PermanentNegativeCompletion,
278 Category::Information,
279 Detail::Zero,
280 ),
281 vec![
282 "gmx.net (mxgmx117) Nemesis ESMTP Service not available".to_string(),
283 "No SMTP service".to_string(),
284 "IP address is block listed.".to_string(),
285 "For explanation visit https://www.gmx.net/mail/senderguidelines?c=bl".to_string(),
286 ],
287 ));
288
289 assert!(is_err_ip_blacklisted(&AsyncSmtpError(err)))
290 }
291}