Skip to content

Commit

Permalink
Add MissingCapabilityException
Browse files Browse the repository at this point in the history
  • Loading branch information
cketti committed Nov 27, 2023
1 parent 5815506 commit f79db8e
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.fsck.k9.mail

class MissingCapabilityException(
val capabilityName: String,
) : MessagingException("Missing capability: $capabilityName", true)
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.fsck.k9.mail.CertificateValidationException
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.K9MailLib
import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mail.MissingCapabilityException
import com.fsck.k9.mail.NetworkTimeouts.SOCKET_CONNECT_TIMEOUT
import com.fsck.k9.mail.NetworkTimeouts.SOCKET_READ_TIMEOUT
import com.fsck.k9.mail.filter.Base64
Expand Down Expand Up @@ -260,14 +261,7 @@ internal class RealImapConnection(

private fun upgradeToTls() {
if (!hasCapability(Capabilities.STARTTLS)) {
/*
* This exception triggers a "Certificate error"
* notification that takes the user to the incoming
* server settings for review. This might be needed if
* the account was configured with an obsolete
* "STARTTLS (if available)" setting.
*/
throw CertificateValidationException("STARTTLS connection security not available")
throw MissingCapabilityException(Capabilities.STARTTLS)
}

startTls()
Expand Down Expand Up @@ -298,20 +292,20 @@ internal class RealImapConnection(
if (oauthTokenProvider == null) {
throw MessagingException("No OAuthToken Provider available.")
} else if (!hasCapability(Capabilities.SASL_IR)) {
throw MessagingException("SASL-IR capability is missing.")
throw MissingCapabilityException(Capabilities.SASL_IR)
} else if (hasCapability(Capabilities.AUTH_OAUTHBEARER)) {
authWithOAuthToken(OAuthMethod.OAUTHBEARER)
} else if (hasCapability(Capabilities.AUTH_XOAUTH2)) {
authWithOAuthToken(OAuthMethod.XOAUTH2)
} else {
throw MessagingException("Server doesn't support SASL OAUTHBEARER or XOAUTH2.")
throw MissingCapabilityException(Capabilities.AUTH_OAUTHBEARER)
}
}
AuthType.CRAM_MD5 -> {
if (hasCapability(Capabilities.AUTH_CRAM_MD5)) {
authCramMD5()
} else {
throw MessagingException("Server doesn't support encrypted passwords using CRAM-MD5.")
throw MissingCapabilityException(Capabilities.AUTH_CRAM_MD5)
}
}
AuthType.PLAIN -> {
Expand All @@ -320,17 +314,14 @@ internal class RealImapConnection(
} else if (!hasCapability(Capabilities.LOGINDISABLED)) {
login()
} else {
throw MessagingException(
"Server doesn't support unencrypted passwords using AUTH=PLAIN and LOGIN is disabled.",
)
throw MissingCapabilityException(Capabilities.AUTH_PLAIN)
}
}
AuthType.EXTERNAL -> {
if (hasCapability(Capabilities.AUTH_EXTERNAL)) {
saslAuthExternal()
} else {
// Provide notification to user of a problem authenticating using client certificates
throw CertificateValidationException(CertificateValidationException.Reason.MissingCapability)
throw MissingCapabilityException(Capabilities.AUTH_EXTERNAL)
}
}
else -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import com.fsck.k9.mail.AuthenticationFailedException
import com.fsck.k9.mail.CertificateValidationException
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.K9MailLib
import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mail.MissingCapabilityException
import com.fsck.k9.mail.SystemOutLogger
import com.fsck.k9.mail.XOAuth2ChallengeParserTest
import com.fsck.k9.mail.helpers.TestTrustedSocketFactory
Expand Down Expand Up @@ -143,8 +143,8 @@ class RealImapConnectionTest {

assertFailure {
imapConnection.open()
}.isInstanceOf<MessagingException>()
.hasMessage("Server doesn't support unencrypted passwords using AUTH=PLAIN and LOGIN is disabled.")
}.isInstanceOf<MissingCapabilityException>()
.prop(MissingCapabilityException::capabilityName).isEqualTo("AUTH=PLAIN")

server.verifyConnectionClosed()
server.verifyInteractionCompleted()
Expand Down Expand Up @@ -302,8 +302,8 @@ class RealImapConnectionTest {

assertFailure {
imapConnection.open()
}.isInstanceOf<MessagingException>()
.hasMessage("Server doesn't support encrypted passwords using CRAM-MD5.")
}.isInstanceOf<MissingCapabilityException>()
.prop(MissingCapabilityException::capabilityName).isEqualTo("AUTH=CRAM-MD5")

server.verifyConnectionClosed()
server.verifyInteractionCompleted()
Expand Down Expand Up @@ -375,6 +375,38 @@ class RealImapConnectionTest {
server.verifyInteractionCompleted()
}

@Test
fun `open() AUTH OAUTHBEARER with SASL-IR capability missing`() {
val server = MockImapServer().apply {
preAuthenticationDialog(capabilities = "AUTH=OAUTHBEARER")
}
val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.XOAUTH2)

assertFailure {
imapConnection.open()
}.isInstanceOf<MissingCapabilityException>()
.prop(MissingCapabilityException::capabilityName).isEqualTo("SASL-IR")

server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}

@Test
fun `open() AUTH OAUTHBEARER with AUTH=OAUTHBEARER capability missing`() {
val server = MockImapServer().apply {
preAuthenticationDialog(capabilities = "SASL-IR")
}
val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.XOAUTH2)

assertFailure {
imapConnection.open()
}.isInstanceOf<MissingCapabilityException>()
.prop(MissingCapabilityException::capabilityName).isEqualTo("AUTH=OAUTHBEARER")

server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}

@Test
fun `open() AUTH XOAUTH2 throws exception on 401 response`() {
val server = MockImapServer().apply {
Expand Down Expand Up @@ -539,9 +571,8 @@ class RealImapConnectionTest {

assertFailure {
imapConnection.open()
}.isInstanceOf<CertificateValidationException>()
.prop(CertificateValidationException::getReason)
.isEqualTo(CertificateValidationException.Reason.MissingCapability)
}.isInstanceOf<MissingCapabilityException>()
.prop(MissingCapabilityException::capabilityName).isEqualTo("AUTH=EXTERNAL")

server.verifyConnectionClosed()
server.verifyInteractionCompleted()
Expand Down Expand Up @@ -686,11 +717,10 @@ class RealImapConnectionTest {
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED,
)

// FIXME: CertificateValidationException seems wrong
assertFailure {
imapConnection.open()
}.isInstanceOf<CertificateValidationException>()
.hasMessage("STARTTLS connection security not available")
}.isInstanceOf<MissingCapabilityException>()
.prop(MissingCapabilityException::capabilityName).isEqualTo("STARTTLS")

server.verifyConnectionClosed()
server.verifyInteractionCompleted()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,26 @@
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.K9MailLib;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.MissingCapabilityException;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.filter.Hex;
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
import javax.net.ssl.SSLException;

import static com.fsck.k9.mail.CertificateValidationException.Reason.MissingCapability;
import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_POP3;
import static com.fsck.k9.mail.NetworkTimeouts.SOCKET_CONNECT_TIMEOUT;
import static com.fsck.k9.mail.NetworkTimeouts.SOCKET_READ_TIMEOUT;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.*;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.AUTH_CRAM_MD5_CAPABILITY;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.AUTH_EXTERNAL_CAPABILITY;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.AUTH_PLAIN_CAPABILITY;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.CAPA_COMMAND;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.PASS_COMMAND;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.SASL_CAPABILITY;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.STLS_CAPABILITY;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.STLS_COMMAND;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.TOP_CAPABILITY;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.UIDL_CAPABILITY;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.USER_COMMAND;


class Pop3Connection {
Expand Down Expand Up @@ -159,8 +169,7 @@ private void performStartTlsUpgrade(TrustedSocketFactory trustedSocketFactory,
}
capabilities = getCapabilities();
} else {
throw new CertificateValidationException(
"STARTTLS connection security not available");
throw new MissingCapabilityException(STLS_CAPABILITY);
}

}
Expand Down Expand Up @@ -188,8 +197,7 @@ private void performAuthentication(AuthType authType, String serverGreeting)
if (capabilities.external) {
authExternal();
} else {
// Provide notification to user of a problem authenticating using client certificates
throw new CertificateValidationException(MissingCapability);
throw new MissingCapabilityException(SASL_CAPABILITY + " " + AUTH_EXTERNAL_CAPABILITY);
}
break;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.fsck.k9.mail.ConnectionSecurity.NONE
import com.fsck.k9.mail.ConnectionSecurity.SSL_TLS_REQUIRED
import com.fsck.k9.mail.ConnectionSecurity.STARTTLS_REQUIRED
import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mail.MissingCapabilityException
import com.fsck.k9.mail.helpers.TestTrustedSocketFactory
import com.fsck.k9.mail.ssl.TrustedSocketFactory
import java.io.IOException
Expand Down Expand Up @@ -78,14 +79,17 @@ class Pop3ConnectionTest {
createAndOpenPop3Connection(settings, mockSocketFactory)
}

@Test(expected = CertificateValidationException::class)
fun `open() with STLS capability unavailable should throw CertificateValidationException`() {
@Test
fun `open() with STLS capability unavailable should throw`() {
val server = startServer {
setupServerWithAuthenticationMethods("PLAIN")
}
val settings = server.createSettings(connectionSecurity = STARTTLS_REQUIRED)

createAndOpenPop3Connection(settings)
assertFailure {
createAndOpenPop3Connection(settings)
}.isInstanceOf<MissingCapabilityException>()
.prop(MissingCapabilityException::capabilityName).isEqualTo("STLS")
}

@Test(expected = Pop3ErrorResponse::class)
Expand Down Expand Up @@ -300,9 +304,8 @@ class Pop3ConnectionTest {

assertFailure {
createAndOpenPop3Connection(settings)
}.isInstanceOf<CertificateValidationException>()
.prop(CertificateValidationException::getReason)
.isEqualTo(CertificateValidationException.Reason.MissingCapability)
}.isInstanceOf<MissingCapabilityException>()
.prop(MissingCapabilityException::capabilityName).isEqualTo("SASL EXTERNAL")

server.verifyConnectionStillOpen()
server.verifyInteractionCompleted()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.fsck.k9.mail.K9MailLib
import com.fsck.k9.mail.Message
import com.fsck.k9.mail.Message.RecipientType
import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mail.MissingCapabilityException
import com.fsck.k9.mail.NetworkTimeouts.SOCKET_CONNECT_TIMEOUT
import com.fsck.k9.mail.NetworkTimeouts.SOCKET_READ_TIMEOUT
import com.fsck.k9.mail.ServerSettings
Expand Down Expand Up @@ -125,10 +126,7 @@ class SmtpTransport(
extensions = sendHello(helloName)
secureConnection = true
} else {
// This exception triggers a "Certificate error" notification that takes the user to the incoming
// server settings for review. This might be needed if the account was configured with an obsolete
// "STARTTLS (if available)" setting.
throw CertificateValidationException("STARTTLS connection security not available")
throw MissingCapabilityException("STARTTLS")
}
}

Expand Down Expand Up @@ -161,14 +159,14 @@ class SmtpTransport(
} else if (authLoginSupported) {
saslAuthLogin()
} else {
throw MessagingException("Authentication methods SASL PLAIN and LOGIN are unavailable.")
throw MissingCapabilityException("AUTH PLAIN")
}
}
AuthType.CRAM_MD5 -> {
if (authCramMD5Supported) {
saslAuthCramMD5()
} else {
throw MessagingException("Authentication method CRAM-MD5 is unavailable.")
throw MissingCapabilityException("AUTH CRAM-MD5")
}
}
AuthType.XOAUTH2 -> {
Expand All @@ -179,20 +177,14 @@ class SmtpTransport(
} else if (authXoauth2Supported) {
saslOAuth(OAuthMethod.XOAUTH2)
} else {
throw MessagingException("Server doesn't support SASL OAUTHBEARER or XOAUTH2.")
throw MissingCapabilityException("AUTH OAUTHBEARER")
}
}
AuthType.EXTERNAL -> {
if (authExternalSupported) {
saslAuthExternal()
} else {
// Some SMTP servers are known to provide no error indication when a client certificate
// fails to validate, other than to not offer the AUTH EXTERNAL capability.
// So, we treat it is an error to not offer AUTH EXTERNAL when using client certificates.
// That way, the user can be notified of a problem during account setup.
throw CertificateValidationException(
CertificateValidationException.Reason.MissingCapability,
)
throw MissingCapabilityException("AUTH EXTERNAL")
}
}
AuthType.AUTOMATIC -> {
Expand All @@ -205,7 +197,7 @@ class SmtpTransport(
} else if (authCramMD5Supported) {
saslAuthCramMD5()
} else {
throw MessagingException("No supported authentication methods available.")
throw MissingCapabilityException("AUTH PLAIN")
}
} else {
if (authCramMD5Supported) {
Expand Down
Loading

0 comments on commit f79db8e

Please sign in to comment.