use crate::tls::Security;
#[derive(Debug, Clone, Copy)]
pub struct ServerSpec {
pub host: &'static str,
pub port: u16,
pub security: Security,
}
#[derive(Debug, Clone, Copy)]
pub struct Provider {
pub name: &'static str,
pub note: Option<&'static str>,
pub smtp: ServerSpec,
pub imap: ServerSpec,
pub pop: Option<ServerSpec>,
}
const STARTTLS: Security = Security::StartTls;
const SSL: Security = Security::Implicit;
pub const PROVIDERS: &[Provider] = &[
Provider {
name: "Outlook.com / Hotmail (consumer)",
note: None,
smtp: ServerSpec { host: "smtp-mail.outlook.com", port: 587, security: STARTTLS },
imap: ServerSpec { host: "outlook.office365.com", port: 993, security: SSL },
pop: Some(ServerSpec { host: "outlook.office365.com", port: 995, security: SSL }),
},
Provider {
name: "Microsoft 365 / Office 365 (work)",
note: Some("Tenant may have SMTP AUTH disabled - see the 5.7.139 hint."),
smtp: ServerSpec { host: "smtp.office365.com", port: 587, security: STARTTLS },
imap: ServerSpec { host: "outlook.office365.com", port: 993, security: SSL },
pop: Some(ServerSpec { host: "outlook.office365.com", port: 995, security: SSL }),
},
Provider {
name: "Gmail / Google Workspace",
note: Some("Requires a Google App Password if 2-Step Verification is on."),
smtp: ServerSpec { host: "smtp.gmail.com", port: 587, security: STARTTLS },
imap: ServerSpec { host: "imap.gmail.com", port: 993, security: SSL },
pop: Some(ServerSpec { host: "pop.gmail.com", port: 995, security: SSL }),
},
Provider {
name: "Yahoo Mail",
note: Some("Generate an App Password at account-security; the regular password is rejected."),
smtp: ServerSpec { host: "smtp.mail.yahoo.com", port: 587, security: STARTTLS },
imap: ServerSpec { host: "imap.mail.yahoo.com", port: 993, security: SSL },
pop: Some(ServerSpec { host: "pop.mail.yahoo.com", port: 995, security: SSL }),
},
Provider {
name: "iCloud / Apple Mail",
note: Some("Use an app-specific password from appleid.apple.com (2FA is required)."),
smtp: ServerSpec { host: "smtp.mail.me.com", port: 587, security: STARTTLS },
imap: ServerSpec { host: "imap.mail.me.com", port: 993, security: SSL },
pop: None,
},
Provider {
name: "Proton Mail (Bridge)",
note: Some("Requires Proton Bridge running locally; password is the Bridge-generated one, not your Proton account password."),
smtp: ServerSpec { host: "127.0.0.1", port: 1025, security: STARTTLS },
imap: ServerSpec { host: "127.0.0.1", port: 1143, security: STARTTLS },
pop: None,
},
Provider {
name: "Fastmail",
note: Some("Generate an App Password under Settings > Privacy & Security."),
smtp: ServerSpec { host: "smtp.fastmail.com", port: 587, security: STARTTLS },
imap: ServerSpec { host: "imap.fastmail.com", port: 993, security: SSL },
pop: Some(ServerSpec { host: "pop.fastmail.com", port: 995, security: SSL }),
},
Provider {
name: "Zoho Mail",
note: None,
smtp: ServerSpec { host: "smtp.zoho.com", port: 587, security: STARTTLS },
imap: ServerSpec { host: "imap.zoho.com", port: 993, security: SSL },
pop: Some(ServerSpec { host: "pop.zoho.com", port: 995, security: SSL }),
},
Provider {
name: "AOL Mail",
note: Some("Requires an App Password if 2-step verification is on."),
smtp: ServerSpec { host: "smtp.aol.com", port: 587, security: STARTTLS },
imap: ServerSpec { host: "imap.aol.com", port: 993, security: SSL },
pop: Some(ServerSpec { host: "pop.aol.com", port: 995, security: SSL }),
},
Provider {
name: "GMX / Mail.com",
note: None,
smtp: ServerSpec { host: "mail.gmx.com", port: 587, security: STARTTLS },
imap: ServerSpec { host: "imap.gmx.com", port: 993, security: SSL },
pop: Some(ServerSpec { host: "pop.gmx.com", port: 995, security: SSL }),
},
Provider {
name: "Yandex Mail",
note: None,
smtp: ServerSpec { host: "smtp.yandex.com", port: 465, security: SSL },
imap: ServerSpec { host: "imap.yandex.com", port: 993, security: SSL },
pop: Some(ServerSpec { host: "pop.yandex.com", port: 995, security: SSL }),
},
];
pub fn by_name(name: &str) -> Option<&'static Provider> {
PROVIDERS.iter().find(|p| p.name == name)
}
pub fn detect(smtp_host: &str, imap_host: &str, pop_host: &str) -> Option<&'static Provider> {
PROVIDERS.iter().find(|p| {
p.smtp.host == smtp_host
&& p.imap.host == imap_host
&& p.pop.map(|s| s.host).unwrap_or(pop_host) == pop_host
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_are_unique() {
let mut seen = std::collections::HashSet::new();
for p in PROVIDERS {
assert!(seen.insert(p.name), "duplicate provider name: {}", p.name);
}
}
#[test]
fn outlook_consumer_is_first_for_back_compat() {
assert_eq!(
PROVIDERS.first().map(|p| p.name),
Some("Outlook.com / Hotmail (consumer)")
);
}
#[test]
fn every_provider_has_smtp_and_imap() {
for p in PROVIDERS {
assert!(!p.smtp.host.is_empty(), "{} has empty smtp.host", p.name);
assert!(p.smtp.port > 0, "{} has invalid smtp.port", p.name);
assert!(!p.imap.host.is_empty(), "{} has empty imap.host", p.name);
assert!(p.imap.port > 0, "{} has invalid imap.port", p.name);
}
}
#[test]
fn by_name_round_trip() {
for p in PROVIDERS {
let back = by_name(p.name).expect("known provider");
assert_eq!(back.smtp.host, p.smtp.host);
}
assert!(by_name("definitely not a real provider").is_none());
}
#[test]
fn detect_finds_outlook_defaults() {
let p = detect(
"smtp-mail.outlook.com",
"outlook.office365.com",
"outlook.office365.com",
);
assert_eq!(p.map(|p| p.name), Some("Outlook.com / Hotmail (consumer)"));
}
#[test]
fn detect_returns_none_for_custom_setup() {
assert!(detect(
"smtp.example.invalid",
"imap.example.invalid",
"pop.example.invalid"
)
.is_none());
}
}