Skip to main content

smtp_test_tool/
providers.rs

1//! Built-in mail-provider presets.
2//!
3//! Selecting a preset overwrites the SMTP / IMAP / POP3 host, port, and
4//! security fields on the active profile - nothing else.  Credentials,
5//! the chosen profile name, theme, and other settings are left alone.
6//!
7//! The list is deliberately small and curated.  When you have to add an
8//! eleventh entry, ask whether one of the existing ten can go first.
9//!
10//! Sources verified against each provider's official documentation at
11//! the time of writing; ports/hosts are stable across years for these
12//! services.  If a value rots, users can always pick "Custom" and edit
13//! the host field directly.
14
15use crate::tls::Security;
16
17/// One protocol endpoint (SMTP / IMAP / POP3).
18#[derive(Debug, Clone, Copy)]
19pub struct ServerSpec {
20    pub host: &'static str,
21    pub port: u16,
22    pub security: Security,
23}
24
25/// A named bundle of endpoints for one mail provider.
26#[derive(Debug, Clone, Copy)]
27pub struct Provider {
28    /// Human-readable name shown in the UI dropdown.
29    pub name: &'static str,
30    /// Optional clarification shown under the dropdown - app-password
31    /// requirements, "needs Proton Bridge running", etc.
32    pub note: Option<&'static str>,
33    pub smtp: ServerSpec,
34    pub imap: ServerSpec,
35    /// Some providers do not expose POP3 at all (iCloud, Proton Bridge,
36    /// Microsoft 365 work mailboxes in many tenants).  `None` means
37    /// "leave the POP3 fields alone and disable the POP3 test by default".
38    pub pop: Option<ServerSpec>,
39}
40
41/// Standard submission + secure-access ports.
42const STARTTLS: Security = Security::StartTls;
43const SSL: Security = Security::Implicit;
44
45/// The ten built-in providers, in roughly descending popularity, plus a
46/// note about Proton Bridge because it's the most surprising entry.
47pub 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
127/// Find a provider by exact name match (case-sensitive).
128pub fn by_name(name: &str) -> Option<&'static Provider> {
129    PROVIDERS.iter().find(|p| p.name == name)
130}
131
132/// Best-effort reverse lookup: which provider does the current Profile
133/// resemble?  Returns `Some(provider)` when ALL three protocol hosts
134/// match exactly, else `None` (meaning the user has a custom setup).
135pub 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        // A duplicate name would silently make by_name() return the
150        // first match while the dropdown shows two entries - both are
151        // user-hostile.
152        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        // v0.1.0 shipped Outlook.com defaults; the first entry of the
161        // list is the default fallback if a config has no provider hint.
162        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}