use once_cell::sync::Lazy;
use std::collections::HashMap;
pub type Hint = (&'static str, &'static str);
pub static SMTP_ENHANCED: Lazy<HashMap<&'static str, Hint>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert(
"5.7.0",
(
"Authentication required, or chosen AUTH mechanism not permitted.",
"Enable SMTP AUTH for the mailbox; verify LOGIN / PLAIN / XOAUTH2 is allowed.",
),
);
m.insert("5.7.1", ("Relay access denied - server refuses to forward this message.",
"Either the sender is not authenticated, the recipient is external and relaying is disabled, or a transport rule is blocking the message."));
m.insert("5.7.3", ("Authentication unsuccessful.",
"Bad password, MFA enabled without app-password, or Basic/Legacy auth disabled on the tenant (O365: 'SMTP AUTH disabled')."));
m.insert(
"5.7.8",
(
"Authentication credentials invalid.",
"Username/password rejected by the SASL layer.",
),
);
m.insert("5.7.57", ("Client was not authenticated to send anonymous mail during MAIL FROM.",
"Force STARTTLS + AUTH before MAIL FROM. In O365 this is the typical error when the client connects without AUTH."));
m.insert("5.7.60", ("SendAsDenied - authenticated user is not allowed to send AS this From: address.",
"Grant the mailbox 'Send As' (or 'Send on Behalf') rights to the authenticated account, or change the From: header to match the login."));
m.insert(
"5.7.64",
(
"TenantAttribution; Relay Access Denied.",
"Authenticated SMTP submission requires a licensed mailbox in O365.",
),
);
m.insert(
"5.7.124",
(
"The user is not authorised to send mail.",
"Disabled mailbox, blocked by Conditional Access, or licence missing.",
),
);
m.insert(
"5.7.135",
(
"Authentication unsuccessful, the user credentials have expired.",
"Reset the password / refresh the OAuth token.",
),
);
m.insert(
"5.7.139",
(
"Authentication unsuccessful, the request did not meet the criteria.",
"Conditional Access policy denied the login (location, device, MFA).",
),
);
m.insert(
"5.7.500",
(
"Access denied, sending domain disabled.",
"The sender's domain is blocked for outbound mail on this tenant.",
),
);
m.insert(
"5.7.501",
(
"Access denied, banned sender.",
"Sender address is on a tenant blocklist.",
),
);
m.insert(
"5.7.508",
(
"Access denied, rate or traffic threshold exceeded.",
"Throttled; wait and retry, or ask admin to raise the limit.",
),
);
m.insert(
"5.7.511",
(
"Access denied, banned sender.",
"Sender flagged as spam source.",
),
);
m.insert(
"5.7.606",
(
"Access denied, banned sending IP.",
"The submitting IP is on a Microsoft blocklist.",
),
);
m.insert(
"5.7.708",
(
"Service refused. Source IP has bad reputation.",
"Submit from a different IP or request delisting.",
),
);
m.insert(
"5.7.750",
(
"Client blocked from sending from unregistered domains.",
"Verify the sender domain in the tenant or use an accepted domain.",
),
);
m.insert(
"5.1.0",
(
"Sender address rejected.",
"From/MAIL FROM not accepted; usually format or domain policy.",
),
);
m.insert(
"5.1.1",
(
"Bad destination mailbox - recipient does not exist.",
"Check the recipient address.",
),
);
m.insert(
"5.1.7",
(
"Invalid sender address (malformed).",
"Fix the MAIL FROM syntax.",
),
);
m.insert(
"5.1.8",
(
"Sender domain not allowed.",
"Domain not accepted by the server.",
),
);
m.insert(
"5.1.10",
(
"Recipient address rejected - user unknown.",
"Typo or non-existent recipient.",
),
);
m.insert(
"5.4.1",
(
"Recipient address rejected: access denied.",
"Recipient mailbox refuses messages from this sender / domain.",
),
);
m.insert(
"5.2.1",
(
"Mailbox disabled, not accepting messages.",
"Recipient mailbox suspended.",
),
);
m.insert("5.2.2", ("Mailbox full.", "Recipient quota exceeded."));
m.insert(
"5.3.4",
("Message too big for system.", "Reduce message size."),
);
m.insert(
"4.7.0",
(
"Temporary authentication failure / throttling.",
"Try again later; could also be tarpit for repeated bad logins.",
),
);
m.insert(
"4.4.2",
(
"Connection dropped.",
"Network glitch or server restart; retry.",
),
);
m.insert(
"4.3.2",
(
"System not accepting network messages.",
"Server maintenance.",
),
);
m
});
pub const IMAP_HINTS: &[(&str, &str)] = &[
(
"AUTHENTICATIONFAILED",
"IMAP login rejected - bad password, MFA without app-password, or Basic Auth disabled.",
),
("LOGIN failed", "IMAP login rejected by server."),
(
"[ALERT]",
"Server returned an ALERT response - read it; admin-defined message.",
),
("[UNAVAILABLE]", "Mailbox/server temporarily unavailable."),
(
"[PRIVACYREQUIRED]",
"Server requires TLS before LOGIN - use STARTTLS or implicit SSL.",
),
(
"[CLIENTBUG]",
"Client did something the server considers wrong; usually missing STARTTLS or wrong state.",
),
(
"LOGINDISABLED",
"Plain LOGIN is disabled on this server - use STARTTLS/SSL or XOAUTH2.",
),
];
pub const CLIENT_BOUNCE_HINTS: &[(&str, &str)] = &[
(
"Send mail as",
"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).",
),
(
"Mail sturen als",
"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).",
),
];
pub const POP_HINTS: &[(&str, &str)] = &[
(
"authentication failed",
"POP3 login rejected - bad credentials or POP disabled for this mailbox.",
),
("Logon failure", "POP3 login rejected."),
(
"not implemented",
"Server does not support the issued command.",
),
(
"disabled",
"POP3 access is disabled for this account / tenant.",
),
];
pub fn smtp_hints_for(msg: &str) -> Vec<String> {
let mut out = Vec::new();
for esc in extract_enhanced_codes(msg) {
if let Some((what, fix)) = SMTP_ENHANCED.get(esc.as_str()) {
out.push(format!(" ESC {esc}: {what}"));
out.push(format!(" -> Action: {fix}"));
}
}
let lower = msg.to_lowercase();
for (needle, hint) in CLIENT_BOUNCE_HINTS {
if lower.contains(&needle.to_lowercase()) {
out.push(format!(" -> {hint}"));
}
}
out
}
pub fn imap_hints_for(msg: &str) -> Vec<String> {
let lower = msg.to_lowercase();
IMAP_HINTS
.iter()
.filter(|(needle, _)| lower.contains(&needle.to_lowercase()))
.map(|(_, hint)| format!(" -> {hint}"))
.collect()
}
pub fn pop_hints_for(msg: &str) -> Vec<String> {
let lower = msg.to_lowercase();
POP_HINTS
.iter()
.filter(|(needle, _)| lower.contains(&needle.to_lowercase()))
.map(|(_, hint)| format!(" -> {hint}"))
.collect()
}
fn extract_enhanced_codes(s: &str) -> Vec<String> {
let mut out = Vec::new();
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let prev_is_boundary = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
if prev_is_boundary && matches!(bytes[i], b'2' | b'4' | b'5') {
let mut j = i + 1;
if j < bytes.len() && bytes[j] == b'.' {
j += 1;
let mid_start = j;
while j < bytes.len() && bytes[j].is_ascii_digit() && j - mid_start < 3 {
j += 1;
}
if j > mid_start && j < bytes.len() && bytes[j] == b'.' {
j += 1;
let tail_start = j;
while j < bytes.len() && bytes[j].is_ascii_digit() && j - tail_start < 3 {
j += 1;
}
if j > tail_start && (j == bytes.len() || !bytes[j].is_ascii_alphanumeric()) {
out.push(s[i..j].to_string());
i = j;
continue;
}
}
}
}
i += 1;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_codes_with_boundaries() {
let v = extract_enhanced_codes("535 5.7.139 Authentication unsuccessful; 4.7.0 throttle");
assert_eq!(v, vec!["5.7.139", "4.7.0"]);
}
#[test]
fn ignores_version_strings_and_partials() {
assert!(extract_enhanced_codes("running 1.2.3 release").is_empty());
assert!(extract_enhanced_codes("see 5.7 spec").is_empty());
assert!(extract_enhanced_codes("v5.7.60suffix").is_empty());
}
#[test]
fn extracts_at_start_and_end_of_string() {
assert_eq!(extract_enhanced_codes("5.1.1"), vec!["5.1.1"]);
assert_eq!(extract_enhanced_codes("foo 4.4.2"), vec!["4.4.2"]);
}
#[test]
fn hint_includes_send_as_for_5_7_60() {
let h = smtp_hints_for("550 5.7.60 SendAsDenied");
let joined = h.join("\n");
assert!(joined.contains("SendAsDenied"));
assert!(joined.contains("Send As"));
assert!(joined.contains("ESC 5.7.60"));
}
#[test]
fn hint_includes_basic_auth_for_5_7_139() {
let h = smtp_hints_for(
"535 5.7.139 Authentication unsuccessful, basic authentication is disabled",
);
let joined = h.join("\n");
assert!(joined.contains("5.7.139"));
assert!(joined.contains("Conditional Access"));
}
#[test]
fn hint_for_unknown_code_is_empty() {
let h = smtp_hints_for("550 5.9.999 Made up code");
assert!(h.is_empty(), "expected no hints, got {h:?}");
}
#[test]
fn hint_recognises_gmail_send_as_bounce_english() {
let h = smtp_hints_for(
"You're sending this message from a different address or alias using the 'Send mail as' feature.",
);
let joined = h.join("\n");
assert!(joined.contains("Send mail as"));
assert!(joined.contains("Accounts and Import"));
}
#[test]
fn hint_recognises_gmail_send_as_bounce_dutch() {
let h = smtp_hints_for(
"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.",
);
let joined = h.join("\n");
assert!(joined.contains("Mail sturen als"));
assert!(joined.contains("Accounts en import"));
}
#[test]
fn hint_collects_multiple_codes_in_one_reply() {
let h = smtp_hints_for("550 5.7.60 SendAsDenied; also see 5.1.1 for the recipient");
let joined = h.join("\n");
assert!(joined.contains("5.7.60"));
assert!(joined.contains("5.1.1"));
}
#[test]
fn imap_hint_for_authenticationfailed() {
let h = imap_hints_for("a1 NO [AUTHENTICATIONFAILED] LOGIN failed");
assert!(!h.is_empty());
assert!(h.iter().any(|s| s.contains("bad password")));
}
#[test]
fn imap_hint_for_logindisabled() {
let h = imap_hints_for("* CAPABILITY IMAP4rev1 LOGINDISABLED STARTTLS");
assert!(h
.iter()
.any(|s| s.contains("STARTTLS") || s.contains("XOAUTH2")));
}
#[test]
fn pop_hint_for_authentication_failed() {
let h = pop_hints_for("-ERR authentication failed");
assert!(!h.is_empty());
assert!(h
.iter()
.any(|s| s.contains("POP disabled") || s.contains("bad credentials")));
}
#[test]
fn pop_hint_for_disabled() {
let h = pop_hints_for("-ERR POP is disabled for this account");
assert!(h.iter().any(|s| s.contains("disabled")));
}
}