1use crate::tls::Security;
16
17#[derive(Debug, Clone, Copy)]
19pub struct ServerSpec {
20 pub host: &'static str,
21 pub port: u16,
22 pub security: Security,
23}
24
25#[derive(Debug, Clone, Copy)]
27pub struct Provider {
28 pub name: &'static str,
30 pub note: Option<&'static str>,
33 pub smtp: ServerSpec,
34 pub imap: ServerSpec,
35 pub pop: Option<ServerSpec>,
39}
40
41const STARTTLS: Security = Security::StartTls;
43const SSL: Security = Security::Implicit;
44
45pub const PROVIDERS: &[Provider] = &[
48 Provider {
49 name: "Outlook.com / Hotmail (consumer)",
50 note: None,
51 smtp: ServerSpec { host: "smtp-mail.outlook.com", port: 587, security: STARTTLS },
52 imap: ServerSpec { host: "outlook.office365.com", port: 993, security: SSL },
53 pop: Some(ServerSpec { host: "outlook.office365.com", port: 995, security: SSL }),
54 },
55 Provider {
56 name: "Microsoft 365 / Office 365 (work)",
57 note: Some("Tenant may have SMTP AUTH disabled - see the 5.7.139 hint."),
58 smtp: ServerSpec { host: "smtp.office365.com", port: 587, security: STARTTLS },
59 imap: ServerSpec { host: "outlook.office365.com", port: 993, security: SSL },
60 pop: Some(ServerSpec { host: "outlook.office365.com", port: 995, security: SSL }),
61 },
62 Provider {
63 name: "Gmail / Google Workspace",
64 note: Some("Requires a Google App Password if 2-Step Verification is on."),
65 smtp: ServerSpec { host: "smtp.gmail.com", port: 587, security: STARTTLS },
66 imap: ServerSpec { host: "imap.gmail.com", port: 993, security: SSL },
67 pop: Some(ServerSpec { host: "pop.gmail.com", port: 995, security: SSL }),
68 },
69 Provider {
70 name: "Yahoo Mail",
71 note: Some("Generate an App Password at account-security; the regular password is rejected."),
72 smtp: ServerSpec { host: "smtp.mail.yahoo.com", port: 587, security: STARTTLS },
73 imap: ServerSpec { host: "imap.mail.yahoo.com", port: 993, security: SSL },
74 pop: Some(ServerSpec { host: "pop.mail.yahoo.com", port: 995, security: SSL }),
75 },
76 Provider {
77 name: "iCloud / Apple Mail",
78 note: Some("Use an app-specific password from appleid.apple.com (2FA is required)."),
79 smtp: ServerSpec { host: "smtp.mail.me.com", port: 587, security: STARTTLS },
80 imap: ServerSpec { host: "imap.mail.me.com", port: 993, security: SSL },
81 pop: None,
82 },
83 Provider {
84 name: "Proton Mail (Bridge)",
85 note: Some("Requires Proton Bridge running locally; password is the Bridge-generated one, not your Proton account password."),
86 smtp: ServerSpec { host: "127.0.0.1", port: 1025, security: STARTTLS },
87 imap: ServerSpec { host: "127.0.0.1", port: 1143, security: STARTTLS },
88 pop: None,
89 },
90 Provider {
91 name: "Fastmail",
92 note: Some("Generate an App Password under Settings > Privacy & Security."),
93 smtp: ServerSpec { host: "smtp.fastmail.com", port: 587, security: STARTTLS },
94 imap: ServerSpec { host: "imap.fastmail.com", port: 993, security: SSL },
95 pop: Some(ServerSpec { host: "pop.fastmail.com", port: 995, security: SSL }),
96 },
97 Provider {
98 name: "Zoho Mail",
99 note: None,
100 smtp: ServerSpec { host: "smtp.zoho.com", port: 587, security: STARTTLS },
101 imap: ServerSpec { host: "imap.zoho.com", port: 993, security: SSL },
102 pop: Some(ServerSpec { host: "pop.zoho.com", port: 995, security: SSL }),
103 },
104 Provider {
105 name: "AOL Mail",
106 note: Some("Requires an App Password if 2-step verification is on."),
107 smtp: ServerSpec { host: "smtp.aol.com", port: 587, security: STARTTLS },
108 imap: ServerSpec { host: "imap.aol.com", port: 993, security: SSL },
109 pop: Some(ServerSpec { host: "pop.aol.com", port: 995, security: SSL }),
110 },
111 Provider {
112 name: "GMX / Mail.com",
113 note: None,
114 smtp: ServerSpec { host: "mail.gmx.com", port: 587, security: STARTTLS },
115 imap: ServerSpec { host: "imap.gmx.com", port: 993, security: SSL },
116 pop: Some(ServerSpec { host: "pop.gmx.com", port: 995, security: SSL }),
117 },
118 Provider {
119 name: "Yandex Mail",
120 note: None,
121 smtp: ServerSpec { host: "smtp.yandex.com", port: 465, security: SSL },
122 imap: ServerSpec { host: "imap.yandex.com", port: 993, security: SSL },
123 pop: Some(ServerSpec { host: "pop.yandex.com", port: 995, security: SSL }),
124 },
125];
126
127pub fn by_name(name: &str) -> Option<&'static Provider> {
129 PROVIDERS.iter().find(|p| p.name == name)
130}
131
132pub fn detect(smtp_host: &str, imap_host: &str, pop_host: &str) -> Option<&'static Provider> {
136 PROVIDERS.iter().find(|p| {
137 p.smtp.host == smtp_host
138 && p.imap.host == imap_host
139 && p.pop.map(|s| s.host).unwrap_or(pop_host) == pop_host
140 })
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn names_are_unique() {
149 let mut seen = std::collections::HashSet::new();
153 for p in PROVIDERS {
154 assert!(seen.insert(p.name), "duplicate provider name: {}", p.name);
155 }
156 }
157
158 #[test]
159 fn outlook_consumer_is_first_for_back_compat() {
160 assert_eq!(
163 PROVIDERS.first().map(|p| p.name),
164 Some("Outlook.com / Hotmail (consumer)")
165 );
166 }
167
168 #[test]
169 fn every_provider_has_smtp_and_imap() {
170 for p in PROVIDERS {
171 assert!(!p.smtp.host.is_empty(), "{} has empty smtp.host", p.name);
172 assert!(p.smtp.port > 0, "{} has invalid smtp.port", p.name);
173 assert!(!p.imap.host.is_empty(), "{} has empty imap.host", p.name);
174 assert!(p.imap.port > 0, "{} has invalid imap.port", p.name);
175 }
176 }
177
178 #[test]
179 fn by_name_round_trip() {
180 for p in PROVIDERS {
181 let back = by_name(p.name).expect("known provider");
182 assert_eq!(back.smtp.host, p.smtp.host);
183 }
184 assert!(by_name("definitely not a real provider").is_none());
185 }
186
187 #[test]
188 fn detect_finds_outlook_defaults() {
189 let p = detect(
190 "smtp-mail.outlook.com",
191 "outlook.office365.com",
192 "outlook.office365.com",
193 );
194 assert_eq!(p.map(|p| p.name), Some("Outlook.com / Hotmail (consumer)"));
195 }
196
197 #[test]
198 fn detect_returns_none_for_custom_setup() {
199 assert!(detect(
200 "smtp.example.invalid",
201 "imap.example.invalid",
202 "pop.example.invalid"
203 )
204 .is_none());
205 }
206}