feat(accountsetup): add AOL provider with app-password help and IMAP/SMTP presets #190
@@ -8,18 +8,19 @@ private const val SMTP_SUBMISSION_PORT = 587
|
||||
private const val SMTPS_PORT = 465
|
||||
|
||||
/**
|
||||
* Preconfigured IMAP/SMTP presets for the app-password vendors (Gmail, Yahoo, iCloud).
|
||||
* Preconfigured IMAP/SMTP presets for the app-password vendors (Gmail, Yahoo, iCloud, AOL).
|
||||
*
|
||||
* These mirror [Account.outlook]: each entry knows its servers so onboarding only has to collect an
|
||||
* email + app password (see the app-password setup screen). Outlook is intentionally NOT here — it
|
||||
* uses interactive OAuth, not an app password.
|
||||
*
|
||||
* Port / security rationale (verified against current vendor docs):
|
||||
* - IMAP is implicit TLS on 993 for all three: none of these vendors document a STARTTLS IMAP
|
||||
* - IMAP is implicit TLS on 993 for all four: none of these vendors document a STARTTLS IMAP
|
||||
* endpoint, so 993/[MailSecurity.SSL_TLS] is the only correct choice.
|
||||
* - SMTP biases toward STARTTLS on 587 where the vendor documents it (Gmail, iCloud), matching the
|
||||
* epic's "prefer STARTTLS where supported" guidance. Yahoo documents implicit TLS on 465 as its
|
||||
* outgoing server, so it keeps 465/[MailSecurity.SSL_TLS].
|
||||
* epic's "prefer STARTTLS where supported" guidance. Yahoo and AOL each document only implicit
|
||||
* TLS on 465 for their outgoing server (no STARTTLS/587 alternative is documented), so both keep
|
||||
* 465/[MailSecurity.SSL_TLS].
|
||||
* - [MailSecurity.NONE] is never used — every path here is encrypted end to end.
|
||||
*/
|
||||
enum class MailProvider(
|
||||
@@ -32,7 +33,7 @@ enum class MailProvider(
|
||||
/**
|
||||
* Setup instructions for the provider's two-factor prerequisite, or null when there isn't one.
|
||||
* Gmail and iCloud both refuse to issue app passwords until two-factor auth is on, so both
|
||||
* link their own setup article; Yahoo gates nothing on it.
|
||||
* link their own setup article; Yahoo and AOL gate nothing on it.
|
||||
*/
|
||||
val twoFactorHelpUrl: String? = null,
|
||||
private val imapHost: String,
|
||||
@@ -91,6 +92,20 @@ enum class MailProvider(
|
||||
smtpPort = SMTP_SUBMISSION_PORT,
|
||||
smtpSecurity = MailSecurity.STARTTLS,
|
||||
),
|
||||
AOL(
|
||||
key = "aol",
|
||||
displayName = "AOL Mail",
|
||||
// AOL's own "Create and manage 3rd-party app passwords" article. Unlike Gmail/iCloud, it
|
||||
// never lists two-step verification as a prerequisite for generating an app password (nor
|
||||
// does AOL's separate two-step-verification article call itself one), so there is no
|
||||
// twoFactorHelpUrl below — same as Yahoo (issue #156).
|
||||
appPasswordHelpUrl = "https://help.aol.com/articles/Create-and-manage-app-password",
|
||||
imapHost = "imap.aol.com",
|
||||
smtpHost = "smtp.aol.com",
|
||||
// AOL documents smtp.aol.com:465 with implicit SSL/TLS as its outgoing server (issue #154).
|
||||
smtpPort = SMTPS_PORT,
|
||||
smtpSecurity = MailSecurity.SSL_TLS,
|
||||
),
|
||||
;
|
||||
|
||||
/**
|
||||
|
||||
@@ -268,16 +268,18 @@ private fun providerIntro(provider: MailProvider): Int = when (provider) {
|
||||
MailProvider.GMAIL -> R.string.app_password_intro_gmail
|
||||
MailProvider.YAHOO -> R.string.app_password_intro_yahoo
|
||||
MailProvider.ICLOUD -> R.string.app_password_intro_icloud
|
||||
MailProvider.AOL -> R.string.app_password_intro_aol
|
||||
}
|
||||
|
||||
/**
|
||||
* Button copy for the two-factor prerequisite link, in each provider's own terminology — Google
|
||||
* calls it "2-Step Verification", Apple "Two-Factor Authentication". Only reached for providers
|
||||
* that expose [MailProvider.twoFactorHelpUrl]; Yahoo has none, so its branch here is unused.
|
||||
* that expose [MailProvider.twoFactorHelpUrl]; Yahoo and AOL have none, so their branches here are
|
||||
* unused.
|
||||
*/
|
||||
private fun twoFactorHelpLabel(provider: MailProvider): Int = when (provider) {
|
||||
MailProvider.ICLOUD -> R.string.app_password_2fa_help_icloud
|
||||
MailProvider.GMAIL, MailProvider.YAHOO -> R.string.app_password_2fa_help
|
||||
MailProvider.GMAIL, MailProvider.YAHOO, MailProvider.AOL -> R.string.app_password_2fa_help
|
||||
}
|
||||
|
||||
private fun MailSecurity.label(): String = when (this) {
|
||||
|
||||
@@ -189,12 +189,13 @@
|
||||
<string name="account_setup_other">Other (IMAP/SMTP)</string>
|
||||
<string name="account_setup_subtitle">Choose your email provider to get started.</string>
|
||||
|
||||
<!-- App-password guided setup (Gmail/Yahoo/iCloud) -->
|
||||
<!-- App-password guided setup (Gmail/Yahoo/iCloud/AOL) -->
|
||||
<string name="app_password_title">Connect %1$s</string>
|
||||
<string name="app_password_unknown_provider">Unknown email provider.</string>
|
||||
<string name="app_password_intro_gmail">To connect Gmail, create an app password in your Google Account. Gmail requires 2-Step Verification to be turned on before you can create one.</string>
|
||||
<string name="app_password_intro_yahoo">To connect Yahoo Mail, generate an app password from your Yahoo Account security settings.</string>
|
||||
<string name="app_password_intro_icloud">To connect iCloud Mail, create an app-specific password from your Apple ID account page.</string>
|
||||
<string name="app_password_intro_aol">To connect AOL Mail, generate an app password from your AOL Account security settings.</string>
|
||||
<string name="app_password_what_is">An app password is a one-off password that lets an app sign in to your account without your main password or a two-factor code.</string>
|
||||
<string name="app_password_warning">Store this app password carefully — it grants full access to your email. LibreMail keeps it only on this device.</string>
|
||||
<string name="app_password_open_page">Create an app password for %1$s</string>
|
||||
|
||||
@@ -54,6 +54,19 @@ class MailProviderTest {
|
||||
assertEquals(MailSecurity.STARTTLS, account.smtp.security)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aol preset uses documented imap and implicit-tls smtp endpoints`() {
|
||||
val account = MailProvider.AOL.createAccount("user@aol.com")
|
||||
|
||||
assertEquals("imap.aol.com", account.imap.host)
|
||||
assertEquals(993, account.imap.port)
|
||||
assertEquals(MailSecurity.SSL_TLS, account.imap.security)
|
||||
|
||||
assertEquals("smtp.aol.com", account.smtp.host)
|
||||
assertEquals(465, account.smtp.port)
|
||||
assertEquals(MailSecurity.SSL_TLS, account.smtp.security)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no provider ever uses insecure transport`() {
|
||||
MailProvider.entries.forEach { provider ->
|
||||
@@ -82,7 +95,7 @@ class MailProviderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `gmail and icloud link two-factor setup help over https, yahoo does not`() {
|
||||
fun `gmail and icloud link two-factor setup help over https, yahoo and aol do not`() {
|
||||
// Gmail's app-passwords page rejects accounts without 2-Step Verification, so its setup
|
||||
// screen must offer the setup article as a way out (issue #98).
|
||||
val gmailUrl = assertNotNull(
|
||||
@@ -100,6 +113,11 @@ class MailProviderTest {
|
||||
// which never list two-step verification as a prerequisite for generating an app password
|
||||
// (issue #155) — so it must not grow the extra link.
|
||||
assertNull(MailProvider.YAHOO.twoFactorHelpUrl)
|
||||
|
||||
// AOL gates nothing on two-factor either — its app-password article never lists two-step
|
||||
// verification as a prerequisite, and its separate two-step-verification article doesn't
|
||||
// call itself one either (issue #156) — so it must not grow the extra link.
|
||||
assertNull(MailProvider.AOL.twoFactorHelpUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -115,6 +133,14 @@ class MailProviderTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aol app-password help points at AOL's dedicated app-password article`() {
|
||||
assertEquals(
|
||||
"https://help.aol.com/articles/Create-and-manage-app-password",
|
||||
MailProvider.AOL.appPasswordHelpUrl,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createAccount trims the email and derives a stable id and display name`() {
|
||||
val account = MailProvider.GMAIL.createAccount(" User@Gmail.com ")
|
||||
@@ -136,6 +162,7 @@ class MailProviderTest {
|
||||
assertEquals(MailProvider.GMAIL, MailProvider.fromKey("gmail"))
|
||||
assertEquals(MailProvider.YAHOO, MailProvider.fromKey("YAHOO"))
|
||||
assertEquals(MailProvider.ICLOUD, MailProvider.fromKey("iCloud"))
|
||||
assertEquals(MailProvider.AOL, MailProvider.fromKey("AOL"))
|
||||
assertNull(MailProvider.fromKey("outlook"))
|
||||
assertNull(MailProvider.fromKey(""))
|
||||
}
|
||||
@@ -163,6 +190,9 @@ class MailProviderTest {
|
||||
assertEquals(MailProvider.GMAIL, MailProvider.forImapHost("imap.googlemail.com"))
|
||||
assertEquals(MailProvider.GMAIL, MailProvider.forImapHost("IMAP.GMAIL.COM"))
|
||||
assertEquals(MailProvider.ICLOUD, MailProvider.forImapHost("imap.mail.me.com"))
|
||||
// A manually configured AOL account (set up before this preset existed) must still resolve
|
||||
// to the AOL brand (issue #156).
|
||||
assertEquals(MailProvider.AOL, MailProvider.forImapHost("imap.aol.com"))
|
||||
assertNull(MailProvider.forImapHost("imap.example.org"))
|
||||
}
|
||||
|
||||
@@ -176,6 +206,7 @@ class MailProviderTest {
|
||||
fun `brandFor centralizes host to brand matching, including Outlook`() {
|
||||
assertEquals("Gmail", MailProvider.brandFor(account("imap.googlemail.com")))
|
||||
assertEquals("iCloud Mail", MailProvider.brandFor(account("imap.mail.me.com")))
|
||||
assertEquals("AOL Mail", MailProvider.brandFor(account("imap.aol.com")))
|
||||
// A manually configured Office 365 host is Outlook even without the OAuth auth type.
|
||||
assertEquals(MailProvider.OUTLOOK_BRAND, MailProvider.brandFor(account("outlook.office365.com")))
|
||||
// The OAuth auth type brands as Outlook regardless of host.
|
||||
|
||||
Reference in New Issue
Block a user