diff --git a/res/layout/account_setup_incoming.xml b/res/layout/account_setup_incoming.xml
index 561153f25..300348072 100644
--- a/res/layout/account_setup_incoming.xml
+++ b/res/layout/account_setup_incoming.xml
@@ -62,6 +62,16 @@
android:id="@+id/account_security_type"
android:layout_height="wrap_content"
android:layout_width="fill_parent" />
+
+
WebDAV (Exchange) server
Port
Security type
+ Authentication type
None
SSL (if available)
SSL (always)
diff --git a/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java
index 9e7ad12d0..02b5b4c5f 100644
--- a/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java
+++ b/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java
@@ -59,6 +59,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
private EditText mServerView;
private EditText mPortView;
private Spinner mSecurityTypeView;
+ private Spinner mAuthTypeView;
private EditText mImapPathPrefixView;
private Button mImapFolderDrafts;
private Button mImapFolderSent;
@@ -99,6 +100,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mServerView = (EditText)findViewById(R.id.account_server);
mPortView = (EditText)findViewById(R.id.account_port);
mSecurityTypeView = (Spinner)findViewById(R.id.account_security_type);
+ mAuthTypeView = (Spinner)findViewById(R.id.account_auth_type);
mImapPathPrefixView = (EditText)findViewById(R.id.imap_path_prefix);
mImapFolderDrafts = (Button)findViewById(R.id.account_imap_folder_drafts);
mImapFolderSent = (Button)findViewById(R.id.account_imap_folder_sent);
@@ -126,11 +128,22 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
new SpinnerOption(4, getString(R.string.account_setup_incoming_security_tls_label)),
};
+ SpinnerOption authTypes[] =
+ {
+ new SpinnerOption(0, "PLAIN" ),
+ new SpinnerOption(1, "CRAM_MD5" )
+ };
+
ArrayAdapter securityTypesAdapter = new ArrayAdapter(this,
android.R.layout.simple_spinner_item, securityTypes);
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mSecurityTypeView.setAdapter(securityTypesAdapter);
+ ArrayAdapter authTypesAdapter = new ArrayAdapter(this,
+ android.R.layout.simple_spinner_item, authTypes);
+ authTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mAuthTypeView.setAdapter(authTypesAdapter);
+
/*
* Updates the port when the user changes the security type. This allows
* us to show a reasonable default which the user can change.
@@ -228,6 +241,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
findViewById(R.id.imap_folder_setup_section).setVisibility(View.GONE);
findViewById(R.id.webdav_path_prefix_section).setVisibility(View.GONE);
findViewById(R.id.webdav_path_debug_section).setVisibility(View.GONE);
+ findViewById(R.id.account_auth_type).setVisibility(View.GONE);
mAccount.setDeletePolicy(Account.DELETE_POLICY_NEVER);
@@ -261,6 +275,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
/** Hide the unnecessary fields */
findViewById(R.id.imap_path_prefix_section).setVisibility(View.GONE);
findViewById(R.id.imap_folder_setup_section).setVisibility(View.GONE);
+ findViewById(R.id.account_auth_type).setVisibility(View.GONE);
if (uri.getPath() != null && uri.getPath().length() > 0)
{
String[] pathParts = uri.getPath().split("\\|");
@@ -433,9 +448,19 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
path = path + "|" + mWebdavMailboxPathView.getText();
}
+ final String userInfo;
+ if (mAccountSchemes[securityType].startsWith("imap"))
+ {
+ String authType = ((SpinnerOption)mAuthTypeView.getSelectedItem()).label;
+ userInfo = authType + ":" + mUsernameView.getText() + ":" + mPasswordView.getText();
+ }
+ else
+ {
+ userInfo = mUsernameView.getText() + ":" + mPasswordView.getText();
+ }
URI uri = new URI(
mAccountSchemes[securityType],
- mUsernameView.getText() + ":" + mPasswordView.getText(),
+ userInfo,
mServerView.getText().toString(),
Integer.parseInt(mPortView.getText().toString()),
path, // path
diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java
index f897a1e36..999f4b24b 100644
--- a/src/com/fsck/k9/mail/store/ImapStore.java
+++ b/src/com/fsck/k9/mail/store/ImapStore.java
@@ -16,12 +16,19 @@ import com.beetstra.jutf7.CharsetProvider;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
+
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.binary.Hex;
+
import java.io.*;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.*;
@@ -51,6 +58,8 @@ public class ImapStore extends Store
public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
+ private enum AuthType { PLAIN, CRAM_MD5 };
+
private static final int IDLE_READ_TIMEOUT = 29 * 60 * 1000; // 29 minutes
private static final int IDLE_REFRESH_INTERVAL = 20 * 60 * 1000; // 20 minutes
@@ -66,6 +75,7 @@ public class ImapStore extends Store
private String mUsername;
private String mPassword;
private int mConnectionSecurity;
+ private AuthType mAuthType;
private volatile String mPathPrefix;
private volatile String mCombinedPrefix = null;
private volatile String mPathDelimeter;
@@ -87,11 +97,11 @@ public class ImapStore extends Store
private HashMap mFolderCache = new HashMap();
/**
- * imap://user:password@server:port CONNECTION_SECURITY_NONE
- * imap+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
- * imap+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
- * imap+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
- * imap+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+ * imap://auth:user:password@server:port CONNECTION_SECURITY_NONE
+ * imap+tls://auth:user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
+ * imap+tls+://auth:user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
+ * imap+ssl+://auth:user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
+ * imap+ssl://auth:user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
*
* @param _uri
*/
@@ -147,12 +157,10 @@ public class ImapStore extends Store
if (uri.getUserInfo() != null)
{
- String[] userInfoParts = uri.getUserInfo().split(":", 2);
- mUsername = userInfoParts[0];
- if (userInfoParts.length > 1)
- {
- mPassword = userInfoParts[1];
- }
+ String[] userInfoParts = uri.getUserInfo().split(":");
+ mAuthType = AuthType.valueOf(userInfoParts[0]);
+ mUsername = userInfoParts[1];
+ mPassword = userInfoParts[2];
}
if ((uri.getPath() != null) && (uri.getPath().length() > 0))
@@ -1899,9 +1907,14 @@ public class ImapStore extends Store
try
{
- // TODO eventually we need to add additional authentication
- // options such as SASL
- executeSimpleCommand("LOGIN \"" + escapeString(mUsername) + "\" \"" + escapeString(mPassword) + "\"", true);
+ if ( mAuthType == AuthType.CRAM_MD5 )
+ {
+ authCramMD5();
+ }
+ else if ( mAuthType == AuthType.PLAIN )
+ {
+ executeSimpleCommand("LOGIN \"" + escapeString(mUsername) + "\" \"" + escapeString(mPassword) + "\"", true);
+ }
authSuccess = true;
}
catch (ImapException ie)
@@ -2002,6 +2015,88 @@ public class ImapStore extends Store
}
}
+ protected void authCramMD5() throws AuthenticationFailedException, MessagingException
+ {
+ try
+ {
+ String tag = sendCommand("AUTHENTICATE CRAM-MD5", false);
+ byte[] buf = new byte[ 1024 ];
+ int b64NonceLen = 0;
+ for ( int i = 0; i < buf.length; i++ )
+ {
+ buf[ i ] = (byte)mIn.read();
+ if ( buf[i] == 0x0a )
+ {
+ b64NonceLen = i;
+ break;
+ }
+ }
+ if ( b64NonceLen == 0 )
+ {
+ throw new AuthenticationFailedException( "Error negotiating CRAM-MD5: nonce too long." );
+ }
+ byte[] b64NonceTrim = new byte[ b64NonceLen - 2 ];
+ System.arraycopy(buf, 1, b64NonceTrim, 0, b64NonceLen - 2);
+ byte[] nonce = Base64.decodeBase64(b64NonceTrim);
+ if ( K9.DEBUG )
+ {
+ Log.d(K9.LOG_TAG, "Got nonce: " + new String( b64NonceTrim, "US-ASCII" ) );
+ Log.d(K9.LOG_TAG, "Plaintext nonce: " + new String( nonce, "US-ASCII" ) );
+ }
+ byte[] ipad = new byte[64];
+ byte[] opad = new byte[64];
+ byte[] secretBytes = mPassword.getBytes("US-ASCII");
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ if ( secretBytes.length > 64 )
+ {
+ secretBytes = md.digest(secretBytes);
+ }
+ System.arraycopy(secretBytes, 0, ipad, 0, secretBytes.length);
+ System.arraycopy(secretBytes, 0, opad, 0, secretBytes.length);
+ for ( int i = 0; i < ipad.length; i++ ) ipad[i] ^= 0x36;
+ for ( int i = 0; i < opad.length; i++ ) opad[i] ^= 0x5c;
+ md.update(ipad);
+ byte[] firstPass = md.digest(nonce);
+ md.update(opad);
+ byte[] result = md.digest(firstPass);
+ String plainCRAM = mUsername + " " + new String(Hex.encodeHex(result));
+ byte[] b64CRAM = Base64.encodeBase64(plainCRAM.getBytes("US-ASCII"));
+ if ( K9.DEBUG )
+ {
+ Log.d(K9.LOG_TAG, "Username == " + mUsername);
+ Log.d( K9.LOG_TAG, "plainCRAM: " + plainCRAM );
+ Log.d( K9.LOG_TAG, "b64CRAM: " + new String(b64CRAM, "US-ASCII"));
+ }
+ mOut.write( b64CRAM );
+ mOut.write( new byte[] { 0x0d, 0x0a } );
+ mOut.flush();
+ int respLen = 0;
+ for ( int i = 0; i < buf.length; i++ )
+ {
+ buf[ i ] = (byte)mIn.read();
+ if ( buf[i] == 0x0a )
+ {
+ respLen = i;
+ break;
+ }
+ }
+ String toMatch = tag + " OK";
+ String respStr = new String( buf, 0, respLen );
+ if ( !respStr.startsWith( toMatch ) )
+ {
+ throw new AuthenticationFailedException( "CRAM-MD5 error: " + respStr );
+ }
+ }
+ catch ( IOException ioe )
+ {
+ throw new AuthenticationFailedException( "CRAM-MD5 Auth Failed." );
+ }
+ catch ( NoSuchAlgorithmException nsae )
+ {
+ throw new AuthenticationFailedException( "MD5 Not Available." );
+ }
+ }
+
protected void setReadTimeout(int millis) throws SocketException
{
mSocket.setSoTimeout(millis);
diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java
index 4c0d45bee..ec1fbc8bb 100644
--- a/src/com/fsck/k9/mail/transport/SmtpTransport.java
+++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java
@@ -18,7 +18,10 @@ import java.io.IOException;
import java.io.OutputStream;
import java.net.*;
import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
+import org.apache.commons.codec.binary.Hex;
import java.util.ArrayList;
import java.util.List;
@@ -222,6 +225,7 @@ public class SmtpTransport extends Transport
*/
boolean authLoginSupported = false;
boolean authPlainSupported = false;
+ boolean authCramMD5Supported = false;
for (String result : results)
{
if (result.matches(".*AUTH.*LOGIN.*$") == true)
@@ -232,12 +236,20 @@ public class SmtpTransport extends Transport
{
authPlainSupported = true;
}
+ if (result.matches(".*AUTH.*CRAM-MD5.*$") == true)
+ {
+ authCramMD5Supported = true;
+ }
}
if (mUsername != null && mUsername.length() > 0 && mPassword != null
&& mPassword.length() > 0)
{
- if (authPlainSupported)
+ if (authCramMD5Supported)
+ {
+ saslAuthCramMD5(mUsername, mPassword);
+ }
+ else if (authPlainSupported)
{
saslAuthPlain(mUsername, mPassword);
}
@@ -484,4 +496,48 @@ public class SmtpTransport extends Transport
throw me;
}
}
+
+ private void saslAuthCramMD5(String username, String password) throws MessagingException,
+ AuthenticationFailedException, IOException
+ {
+ List respList = executeSimpleCommand( "AUTH CRAM-MD5" );
+ if ( respList.size() != 1 ) throw new AuthenticationFailedException( "Unable to negotiate CRAM-MD5" );
+ String b64Nonce = respList.get(0);
+ byte[] nonce = Base64.decodeBase64( b64Nonce.getBytes("US-ASCII") );
+ byte[] ipad = new byte[64];
+ byte[] opad = new byte[64];
+ byte[] secretBytes = password.getBytes("US-ASCII");
+ MessageDigest md;
+ try
+ {
+ md = MessageDigest.getInstance("MD5");
+ }
+ catch ( NoSuchAlgorithmException nsae )
+ {
+ throw new AuthenticationFailedException( "MD5 Not Available." );
+ }
+ if ( secretBytes.length > 64 )
+ {
+ secretBytes = md.digest(secretBytes);
+ }
+ System.arraycopy(secretBytes, 0, ipad, 0, secretBytes.length);
+ System.arraycopy(secretBytes, 0, opad, 0, secretBytes.length);
+ for ( int i = 0; i < ipad.length; i++ ) ipad[i] ^= 0x36;
+ for ( int i = 0; i < opad.length; i++ ) opad[i] ^= 0x5c;
+ md.update(ipad);
+ byte[] firstPass = md.digest(nonce);
+ md.update(opad);
+ byte[] result = md.digest(firstPass);
+ String plainCRAM = username + " " + new String(Hex.encodeHex(result));
+ byte[] b64CRAM = Base64.encodeBase64(plainCRAM.getBytes("US-ASCII"));
+ String b64CRAMString = new String( b64CRAM, "US-ASCII" );
+ try
+ {
+ executeSimpleCommand( b64CRAMString );
+ }
+ catch ( MessagingException me )
+ {
+ throw new AuthenticationFailedException( "Unable to negotiate MD5 CRAM" );
+ }
+ }
}