1use once_cell::sync::Lazy;
7use std::collections::HashMap;
8
9pub type Hint = (&'static str, &'static str);
11
12pub static SMTP_ENHANCED: Lazy<HashMap<&'static str, Hint>> = Lazy::new(|| {
14 let mut m = HashMap::new();
15 m.insert(
17 "5.7.0",
18 (
19 "Authentication required, or chosen AUTH mechanism not permitted.",
20 "Enable SMTP AUTH for the mailbox; verify LOGIN / PLAIN / XOAUTH2 is allowed.",
21 ),
22 );
23 m.insert("5.7.1", ("Relay access denied - server refuses to forward this message.",
24 "Either the sender is not authenticated, the recipient is external and relaying is disabled, or a transport rule is blocking the message."));
25 m.insert("5.7.3", ("Authentication unsuccessful.",
26 "Bad password, MFA enabled without app-password, or Basic/Legacy auth disabled on the tenant (O365: 'SMTP AUTH disabled')."));
27 m.insert(
28 "5.7.8",
29 (
30 "Authentication credentials invalid.",
31 "Username/password rejected by the SASL layer.",
32 ),
33 );
34 m.insert("5.7.57", ("Client was not authenticated to send anonymous mail during MAIL FROM.",
35 "Force STARTTLS + AUTH before MAIL FROM. In O365 this is the typical error when the client connects without AUTH."));
36 m.insert("5.7.60", ("SendAsDenied - authenticated user is not allowed to send AS this From: address.",
37 "Grant the mailbox 'Send As' (or 'Send on Behalf') rights to the authenticated account, or change the From: header to match the login."));
38 m.insert(
39 "5.7.64",
40 (
41 "TenantAttribution; Relay Access Denied.",
42 "Authenticated SMTP submission requires a licensed mailbox in O365.",
43 ),
44 );
45 m.insert(
46 "5.7.124",
47 (
48 "The user is not authorised to send mail.",
49 "Disabled mailbox, blocked by Conditional Access, or licence missing.",
50 ),
51 );
52 m.insert(
53 "5.7.135",
54 (
55 "Authentication unsuccessful, the user credentials have expired.",
56 "Reset the password / refresh the OAuth token.",
57 ),
58 );
59 m.insert(
60 "5.7.139",
61 (
62 "Authentication unsuccessful, the request did not meet the criteria.",
63 "Conditional Access policy denied the login (location, device, MFA).",
64 ),
65 );
66 m.insert(
67 "5.7.500",
68 (
69 "Access denied, sending domain disabled.",
70 "The sender's domain is blocked for outbound mail on this tenant.",
71 ),
72 );
73 m.insert(
74 "5.7.501",
75 (
76 "Access denied, banned sender.",
77 "Sender address is on a tenant blocklist.",
78 ),
79 );
80 m.insert(
81 "5.7.508",
82 (
83 "Access denied, rate or traffic threshold exceeded.",
84 "Throttled; wait and retry, or ask admin to raise the limit.",
85 ),
86 );
87 m.insert(
88 "5.7.511",
89 (
90 "Access denied, banned sender.",
91 "Sender flagged as spam source.",
92 ),
93 );
94 m.insert(
95 "5.7.606",
96 (
97 "Access denied, banned sending IP.",
98 "The submitting IP is on a Microsoft blocklist.",
99 ),
100 );
101 m.insert(
102 "5.7.708",
103 (
104 "Service refused. Source IP has bad reputation.",
105 "Submit from a different IP or request delisting.",
106 ),
107 );
108 m.insert(
109 "5.7.750",
110 (
111 "Client blocked from sending from unregistered domains.",
112 "Verify the sender domain in the tenant or use an accepted domain.",
113 ),
114 );
115 m.insert(
116 "5.1.0",
117 (
118 "Sender address rejected.",
119 "From/MAIL FROM not accepted; usually format or domain policy.",
120 ),
121 );
122 m.insert(
123 "5.1.1",
124 (
125 "Bad destination mailbox - recipient does not exist.",
126 "Check the recipient address.",
127 ),
128 );
129 m.insert(
130 "5.1.7",
131 (
132 "Invalid sender address (malformed).",
133 "Fix the MAIL FROM syntax.",
134 ),
135 );
136 m.insert(
137 "5.1.8",
138 (
139 "Sender domain not allowed.",
140 "Domain not accepted by the server.",
141 ),
142 );
143 m.insert(
144 "5.1.10",
145 (
146 "Recipient address rejected - user unknown.",
147 "Typo or non-existent recipient.",
148 ),
149 );
150 m.insert(
151 "5.4.1",
152 (
153 "Recipient address rejected: access denied.",
154 "Recipient mailbox refuses messages from this sender / domain.",
155 ),
156 );
157 m.insert(
158 "5.2.1",
159 (
160 "Mailbox disabled, not accepting messages.",
161 "Recipient mailbox suspended.",
162 ),
163 );
164 m.insert("5.2.2", ("Mailbox full.", "Recipient quota exceeded."));
165 m.insert(
166 "5.3.4",
167 ("Message too big for system.", "Reduce message size."),
168 );
169 m.insert(
171 "4.7.0",
172 (
173 "Temporary authentication failure / throttling.",
174 "Try again later; could also be tarpit for repeated bad logins.",
175 ),
176 );
177 m.insert(
178 "4.4.2",
179 (
180 "Connection dropped.",
181 "Network glitch or server restart; retry.",
182 ),
183 );
184 m.insert(
185 "4.3.2",
186 (
187 "System not accepting network messages.",
188 "Server maintenance.",
189 ),
190 );
191 m
192});
193
194pub const IMAP_HINTS: &[(&str, &str)] = &[
196 (
197 "AUTHENTICATIONFAILED",
198 "IMAP login rejected - bad password, MFA without app-password, or Basic Auth disabled.",
199 ),
200 ("LOGIN failed", "IMAP login rejected by server."),
201 (
202 "[ALERT]",
203 "Server returned an ALERT response - read it; admin-defined message.",
204 ),
205 ("[UNAVAILABLE]", "Mailbox/server temporarily unavailable."),
206 (
207 "[PRIVACYREQUIRED]",
208 "Server requires TLS before LOGIN - use STARTTLS or implicit SSL.",
209 ),
210 (
211 "[CLIENTBUG]",
212 "Client did something the server considers wrong; usually missing STARTTLS or wrong state.",
213 ),
214 (
215 "LOGINDISABLED",
216 "Plain LOGIN is disabled on this server - use STARTTLS/SSL or XOAUTH2.",
217 ),
218];
219
220pub const CLIENT_BOUNCE_HINTS: &[(&str, &str)] = &[
228 (
234 "Send mail as",
235 "Gmail's 'Send mail as' upstream login failed. Settings > See all settings > Accounts and Import > Send mail as > Edit info > re-enter the SMTP password (or an app-password if the source account has 2FA).",
236 ),
237 (
238 "Mail sturen als",
239 "Gmail 'Mail sturen als' upstream-login mislukt. Instellingen > Alle instellingen weergeven > Accounts en import > Mail sturen als > Gegevens bewerken > voer het SMTP-wachtwoord opnieuw in (of een app-wachtwoord als het bronaccount 2FA gebruikt).",
240 ),
241];
242
243pub const POP_HINTS: &[(&str, &str)] = &[
244 (
245 "authentication failed",
246 "POP3 login rejected - bad credentials or POP disabled for this mailbox.",
247 ),
248 ("Logon failure", "POP3 login rejected."),
249 (
250 "not implemented",
251 "Server does not support the issued command.",
252 ),
253 (
254 "disabled",
255 "POP3 access is disabled for this account / tenant.",
256 ),
257];
258
259pub fn smtp_hints_for(msg: &str) -> Vec<String> {
261 let mut out = Vec::new();
262 for esc in extract_enhanced_codes(msg) {
263 if let Some((what, fix)) = SMTP_ENHANCED.get(esc.as_str()) {
264 out.push(format!(" ESC {esc}: {what}"));
265 out.push(format!(" -> Action: {fix}"));
266 }
267 }
268 let lower = msg.to_lowercase();
272 for (needle, hint) in CLIENT_BOUNCE_HINTS {
273 if lower.contains(&needle.to_lowercase()) {
274 out.push(format!(" -> {hint}"));
275 }
276 }
277 out
278}
279
280pub fn imap_hints_for(msg: &str) -> Vec<String> {
281 let lower = msg.to_lowercase();
282 IMAP_HINTS
283 .iter()
284 .filter(|(needle, _)| lower.contains(&needle.to_lowercase()))
285 .map(|(_, hint)| format!(" -> {hint}"))
286 .collect()
287}
288
289pub fn pop_hints_for(msg: &str) -> Vec<String> {
290 let lower = msg.to_lowercase();
291 POP_HINTS
292 .iter()
293 .filter(|(needle, _)| lower.contains(&needle.to_lowercase()))
294 .map(|(_, hint)| format!(" -> {hint}"))
295 .collect()
296}
297
298fn extract_enhanced_codes(s: &str) -> Vec<String> {
301 let mut out = Vec::new();
302 let bytes = s.as_bytes();
303 let mut i = 0;
304 while i < bytes.len() {
305 let prev_is_boundary = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
307 if prev_is_boundary && matches!(bytes[i], b'2' | b'4' | b'5') {
308 let mut j = i + 1;
310 if j < bytes.len() && bytes[j] == b'.' {
311 j += 1;
312 let mid_start = j;
313 while j < bytes.len() && bytes[j].is_ascii_digit() && j - mid_start < 3 {
314 j += 1;
315 }
316 if j > mid_start && j < bytes.len() && bytes[j] == b'.' {
317 j += 1;
318 let tail_start = j;
319 while j < bytes.len() && bytes[j].is_ascii_digit() && j - tail_start < 3 {
320 j += 1;
321 }
322 if j > tail_start && (j == bytes.len() || !bytes[j].is_ascii_alphanumeric()) {
323 out.push(s[i..j].to_string());
324 i = j;
325 continue;
326 }
327 }
328 }
329 }
330 i += 1;
331 }
332 out
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
342 fn extracts_codes_with_boundaries() {
343 let v = extract_enhanced_codes("535 5.7.139 Authentication unsuccessful; 4.7.0 throttle");
344 assert_eq!(v, vec!["5.7.139", "4.7.0"]);
345 }
346
347 #[test]
348 fn ignores_version_strings_and_partials() {
349 assert!(extract_enhanced_codes("running 1.2.3 release").is_empty());
351 assert!(extract_enhanced_codes("see 5.7 spec").is_empty());
353 assert!(extract_enhanced_codes("v5.7.60suffix").is_empty());
355 }
356
357 #[test]
358 fn extracts_at_start_and_end_of_string() {
359 assert_eq!(extract_enhanced_codes("5.1.1"), vec!["5.1.1"]);
360 assert_eq!(extract_enhanced_codes("foo 4.4.2"), vec!["4.4.2"]);
361 }
362
363 #[test]
366 fn hint_includes_send_as_for_5_7_60() {
367 let h = smtp_hints_for("550 5.7.60 SendAsDenied");
368 let joined = h.join("\n");
369 assert!(joined.contains("SendAsDenied"));
370 assert!(joined.contains("Send As"));
371 assert!(joined.contains("ESC 5.7.60"));
372 }
373
374 #[test]
375 fn hint_includes_basic_auth_for_5_7_139() {
376 let h = smtp_hints_for(
377 "535 5.7.139 Authentication unsuccessful, basic authentication is disabled",
378 );
379 let joined = h.join("\n");
380 assert!(joined.contains("5.7.139"));
381 assert!(joined.contains("Conditional Access"));
382 }
383
384 #[test]
385 fn hint_for_unknown_code_is_empty() {
386 let h = smtp_hints_for("550 5.9.999 Made up code");
389 assert!(h.is_empty(), "expected no hints, got {h:?}");
390 }
391
392 #[test]
393 fn hint_recognises_gmail_send_as_bounce_english() {
394 let h = smtp_hints_for(
395 "You're sending this message from a different address or alias using the 'Send mail as' feature.",
396 );
397 let joined = h.join("\n");
398 assert!(joined.contains("Send mail as"));
399 assert!(joined.contains("Accounts and Import"));
400 }
401
402 #[test]
403 fn hint_recognises_gmail_send_as_bounce_dutch() {
404 let h = smtp_hints_for(
406 "Je verzendt dit bericht vanaf een ander adres of een alias met de functie 'Mail sturen als'. De instellingen voor het account dat je gebruikt voor 'Mail sturen als' zijn niet correct of zijn verouderd.",
407 );
408 let joined = h.join("\n");
409 assert!(joined.contains("Mail sturen als"));
410 assert!(joined.contains("Accounts en import"));
411 }
412
413 #[test]
414 fn hint_collects_multiple_codes_in_one_reply() {
415 let h = smtp_hints_for("550 5.7.60 SendAsDenied; also see 5.1.1 for the recipient");
416 let joined = h.join("\n");
417 assert!(joined.contains("5.7.60"));
418 assert!(joined.contains("5.1.1"));
419 }
420
421 #[test]
424 fn imap_hint_for_authenticationfailed() {
425 let h = imap_hints_for("a1 NO [AUTHENTICATIONFAILED] LOGIN failed");
426 assert!(!h.is_empty());
427 assert!(h.iter().any(|s| s.contains("bad password")));
428 }
429
430 #[test]
431 fn imap_hint_for_logindisabled() {
432 let h = imap_hints_for("* CAPABILITY IMAP4rev1 LOGINDISABLED STARTTLS");
433 assert!(h
434 .iter()
435 .any(|s| s.contains("STARTTLS") || s.contains("XOAUTH2")));
436 }
437
438 #[test]
441 fn pop_hint_for_authentication_failed() {
442 let h = pop_hints_for("-ERR authentication failed");
443 assert!(!h.is_empty());
444 assert!(h
445 .iter()
446 .any(|s| s.contains("POP disabled") || s.contains("bad credentials")));
447 }
448
449 #[test]
450 fn pop_hint_for_disabled() {
451 let h = pop_hints_for("-ERR POP is disabled for this account");
452 assert!(h.iter().any(|s| s.contains("disabled")));
453 }
454}