IMAP: Add app version to ID command

This commit is contained in:
cketti 2023-11-02 17:50:39 -04:00
parent faece141b7
commit 77ff16bcf7
14 changed files with 60 additions and 24 deletions

View file

@ -10,6 +10,7 @@ import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.power.PowerManager
import com.fsck.k9.mail.ssl.TrustedSocketFactory
import com.fsck.k9.mail.store.imap.IdleRefreshManager
import com.fsck.k9.mail.store.imap.ImapClientId
import com.fsck.k9.mail.store.imap.ImapStore
import com.fsck.k9.mail.store.imap.ImapStoreConfig
import com.fsck.k9.mail.transport.smtp.SmtpTransport
@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
@Suppress("LongParameterList")
class ImapBackendFactory(
private val accountManager: AccountManager,
private val powerManager: PowerManager,
@ -27,6 +29,7 @@ class ImapBackendFactory(
private val trustedSocketFactory: TrustedSocketFactory,
private val context: Context,
private val clientIdAppName: String,
private val clientIdAppVersion: String,
) : BackendFactory {
override fun createBackend(account: Account): Backend {
val accountName = account.displayName
@ -71,7 +74,7 @@ class ImapBackendFactory(
override fun isSubscribedFoldersOnly() = account.isSubscribedFoldersOnly
override fun clientIdAppName() = clientIdAppName
override fun clientId() = ImapClientId(appName = clientIdAppName, appVersion = clientIdAppVersion)
}
}

View file

@ -29,12 +29,14 @@ val backendsModule = module {
trustedSocketFactory = get(),
context = get(),
clientIdAppName = get(named("ClientIdAppName")),
clientIdAppVersion = get(named("ClientIdAppVersion")),
)
}
single<SystemAlarmManager> { AndroidAlarmManager(context = get(), alarmManager = get()) }
single<IdleRefreshManager> { BackendIdleRefreshManager(alarmManager = get()) }
single { Pop3BackendFactory(get(), get()) }
single(named("ClientIdAppName")) { BuildConfig.CLIENT_ID_APP_NAME }
single(named("ClientIdAppVersion")) { BuildConfig.VERSION_NAME }
single<OAuth2TokenProviderFactory> { RealOAuth2TokenProviderFactory(context = get()) }
developmentModuleAdditions()

View file

@ -30,6 +30,7 @@ val featureAccountServerValidationModule = module {
trustedSocketFactory = get(),
oAuth2TokenProviderFactory = get(),
clientIdAppName = get(named("ClientIdAppName")),
clientIdAppVersion = get(named("ClientIdAppVersion")),
),
pop3Validator = Pop3ServerSettingsValidator(
trustedSocketFactory = get(),

View file

@ -46,6 +46,7 @@ class ServerValidationModuleKtTest : KoinTest {
single<LocalKeyStore> { mock() }
factory<AccountCommonExternalContract.AccountStateLoader> { mock() }
single(named("ClientIdAppName")) { "App Name" }
single(named("ClientIdAppVersion")) { "App Version" }
}
@OptIn(KoinExperimentalAPI::class)

View file

@ -0,0 +1,6 @@
package com.fsck.k9.mail.store.imap
data class ImapClientId(
val appName: String,
val appVersion: String,
)

View file

@ -16,6 +16,7 @@ class ImapServerSettingsValidator(
private val trustedSocketFactory: TrustedSocketFactory,
private val oAuth2TokenProviderFactory: OAuth2TokenProviderFactory?,
private val clientIdAppName: String,
private val clientIdAppVersion: String,
) : ServerSettingsValidator {
@Suppress("TooGenericExceptionCaught")
@ -26,7 +27,7 @@ class ImapServerSettingsValidator(
val config = object : ImapStoreConfig {
override val logLabel = "check"
override fun isSubscribedFoldersOnly() = false
override fun clientIdAppName() = clientIdAppName
override fun clientId() = ImapClientId(appName = clientIdAppName, appVersion = clientIdAppVersion)
}
val oAuth2TokenProvider = createOAuth2TokenProviderOrNull(authStateStorage)
val store = RealImapStore(serverSettings, config, trustedSocketFactory, oAuth2TokenProvider)

View file

@ -15,7 +15,7 @@ internal interface ImapSettings {
val password: String?
val clientCertificateAlias: String?
val useCompression: Boolean
val clientIdAppName: String?
val clientId: ImapClientId?
var pathPrefix: String?
var pathDelimiter: String?

View file

@ -3,5 +3,5 @@ package com.fsck.k9.mail.store.imap
interface ImapStoreConfig {
val logLabel: String
fun isSubscribedFoldersOnly(): Boolean
fun clientIdAppName(): String
fun clientId(): ImapClientId
}

View file

@ -563,10 +563,14 @@ internal class RealImapConnection(
}
private fun sendClientIdIfSupported() {
if (hasCapability(Capabilities.ID) && settings.clientIdAppName != null) {
val encodedAppName = ImapUtility.encodeString(settings.clientIdAppName)
val clientId = settings.clientId
if (hasCapability(Capabilities.ID) && clientId != null) {
val encodedAppName = ImapUtility.encodeString(clientId.appName)
val encodedAppVersion = ImapUtility.encodeString(clientId.appVersion)
try {
executeSimpleCommand("""ID ("name" $encodedAppName)""")
executeSimpleCommand("""ID ("name" $encodedAppName "version" $encodedAppVersion)""")
} catch (e: NegativeImapResponseException) {
Timber.d(e, "Ignoring negative response to ID command")
}

View file

@ -302,7 +302,7 @@ internal open class RealImapStore(
override val useCompression: Boolean = serverSettings.isUseCompression
override val clientIdAppName: String? = config.clientIdAppName().takeIf { serverSettings.isSendClientId }
override val clientId: ImapClientId? = config.clientId().takeIf { serverSettings.isSendClientId }
override var pathPrefix: String?
get() = this@RealImapStore.pathPrefix

View file

@ -24,6 +24,7 @@ private const val AUTHORIZATION_STATE = "auth state"
private const val AUTHORIZATION_TOKEN = "auth-token"
private val CLIENT_CERTIFICATE_ALIAS: String? = null
private const val CLIENT_ID = "clientId"
private const val CLIENT_VERSION = "clientVersion"
class ImapServerSettingsValidatorTest {
private val fakeTrustManager = FakeTrustManager()
@ -32,6 +33,7 @@ class ImapServerSettingsValidatorTest {
trustedSocketFactory = trustedSocketFactory,
oAuth2TokenProviderFactory = null,
clientIdAppName = CLIENT_ID,
clientIdAppVersion = CLIENT_VERSION,
)
@Test
@ -45,7 +47,7 @@ class ImapServerSettingsValidatorTest {
output("+")
expect("AHVzZXIAcGFzc3dvcmQ=")
output("2 OK [CAPABILITY IMAP4rev1 AUTH=PLAIN NAMESPACE ID] LOGIN completed")
expect("3 ID (\"name\" \"$CLIENT_ID\")")
expect("3 ID (\"name\" \"$CLIENT_ID\" \"version\" \"$CLIENT_VERSION\")")
output("* ID NIL")
output("3 OK ID completed")
expect("4 NAMESPACE")
@ -85,6 +87,7 @@ class ImapServerSettingsValidatorTest {
FakeOAuth2TokenProvider()
},
clientIdAppName = CLIENT_ID,
clientIdAppVersion = CLIENT_VERSION,
)
val server = startServer {
output("* OK IMAP4rev1 server ready")
@ -93,7 +96,7 @@ class ImapServerSettingsValidatorTest {
output("1 OK CAPABILITY Completed")
expect("2 AUTHENTICATE OAUTHBEARER bixhPXVzZXIsAWF1dGg9QmVhcmVyIGF1dGgtdG9rZW4BAQ==")
output("2 OK [CAPABILITY IMAP4rev1 SASL-IR AUTH=PLAIN AUTH=OAUTHBEARER NAMESPACE ID] LOGIN completed")
expect("3 ID (\"name\" \"$CLIENT_ID\")")
expect("3 ID (\"name\" \"$CLIENT_ID\" \"version\" \"$CLIENT_VERSION\")")
output("* ID NIL")
output("3 OK ID completed")
expect("4 NAMESPACE")

View file

@ -831,12 +831,15 @@ class RealImapConnectionTest {
fun `open() with ID capability and clientIdAppName should send ID command`() {
val server = MockImapServer().apply {
simplePreAuthAndLoginDialog(postAuthCapabilities = "ID")
expect("""3 ID ("name" "AppName")""")
expect("""3 ID ("name" "AppName" "version" "AppVersion")""")
output("""* ID ("name" "CustomImapServer" "vendor" "Company, Inc." "version" "0.1")""")
output("3 OK ID completed")
simplePostAuthenticationDialog(tag = 4)
}
val imapConnection = startServerAndCreateImapConnection(server, clientIdAppName = "AppName")
val imapConnection = startServerAndCreateImapConnection(
server,
clientId = ImapClientId(appName = "AppName", appVersion = "AppVersion"),
)
imapConnection.open()
@ -850,7 +853,10 @@ class RealImapConnectionTest {
simplePreAuthAndLoginDialog()
simplePostAuthenticationDialog(tag = 3)
}
val imapConnection = startServerAndCreateImapConnection(server, clientIdAppName = "AppName")
val imapConnection = startServerAndCreateImapConnection(
server,
clientId = ImapClientId(appName = "AppName", appVersion = "AppVersion"),
)
imapConnection.open()
@ -864,7 +870,7 @@ class RealImapConnectionTest {
simplePreAuthAndLoginDialog(postAuthCapabilities = "ID")
simplePostAuthenticationDialog(tag = 3)
}
val imapConnection = startServerAndCreateImapConnection(server, clientIdAppName = null)
val imapConnection = startServerAndCreateImapConnection(server, clientId = null)
imapConnection.open()
@ -876,12 +882,15 @@ class RealImapConnectionTest {
fun `open() with empty untagged ID response`() {
val server = MockImapServer().apply {
simplePreAuthAndLoginDialog(postAuthCapabilities = "ID")
expect("""3 ID ("name" "AppName")""")
expect("""3 ID ("name" "AppName" "version" "AppVersion")""")
output("""* ID NIL""")
output("3 OK ID completed")
simplePostAuthenticationDialog(tag = 4)
}
val imapConnection = startServerAndCreateImapConnection(server, clientIdAppName = "AppName")
val imapConnection = startServerAndCreateImapConnection(
server,
clientId = ImapClientId(appName = "AppName", appVersion = "AppVersion"),
)
imapConnection.open()
@ -893,11 +902,14 @@ class RealImapConnectionTest {
fun `open() with missing untagged ID response`() {
val server = MockImapServer().apply {
simplePreAuthAndLoginDialog(postAuthCapabilities = "ID")
expect("""3 ID ("name" "AppName")""")
expect("""3 ID ("name" "AppName" "version" "AppVersion")""")
output("3 OK ID completed")
simplePostAuthenticationDialog(tag = 4)
}
val imapConnection = startServerAndCreateImapConnection(server, clientIdAppName = "AppName")
val imapConnection = startServerAndCreateImapConnection(
server,
clientId = ImapClientId(appName = "AppName", appVersion = "AppVersion"),
)
imapConnection.open()
@ -909,11 +921,14 @@ class RealImapConnectionTest {
fun `open() with BAD response to ID command should not throw`() {
val server = MockImapServer().apply {
simplePreAuthAndLoginDialog(postAuthCapabilities = "ID")
expect("""3 ID ("name" "AppName")""")
expect("""3 ID ("name" "AppName" "version" "AppVersion")""")
output("3 BAD Server doesn't like the ID command")
simplePostAuthenticationDialog(tag = 4)
}
val imapConnection = startServerAndCreateImapConnection(server, clientIdAppName = "AppName")
val imapConnection = startServerAndCreateImapConnection(
server,
clientId = ImapClientId(appName = "AppName", appVersion = "AppVersion"),
)
imapConnection.open()
@ -1113,7 +1128,7 @@ class RealImapConnectionTest {
connectionSecurity: ConnectionSecurity = ConnectionSecurity.NONE,
authType: AuthType = AuthType.PLAIN,
useCompression: Boolean = false,
clientIdAppName: String? = null,
clientId: ImapClientId? = null,
): ImapConnection {
server.start()
@ -1125,7 +1140,7 @@ class RealImapConnectionTest {
username = USERNAME,
password = PASSWORD,
useCompression = useCompression,
clientIdAppName = clientIdAppName,
clientId = clientId,
)
return createImapConnection(settings, socketFactory, oAuth2TokenProvider)

View file

@ -413,7 +413,7 @@ class RealImapStoreTest {
return object : ImapStoreConfig {
override val logLabel: String = "irrelevant"
override fun isSubscribedFoldersOnly(): Boolean = isSubscribedFoldersOnly
override fun clientIdAppName(): String = "irrelevant"
override fun clientId(): ImapClientId = ImapClientId(appName = "irrelevant", appVersion = "irrelevant")
}
}

View file

@ -12,7 +12,7 @@ internal class SimpleImapSettings(
override val username: String,
override val password: String? = null,
override val useCompression: Boolean = false,
override val clientIdAppName: String? = null,
override val clientId: ImapClientId? = null,
) : ImapSettings {
override val clientCertificateAlias: String? = null