mirror of
https://github.com/moparisthebest/k-9
synced 2024-11-27 11:42:16 -05:00
Client Certificate Authentication
This commit is contained in:
parent
2fdcb77c5c
commit
aad171ff7e
@ -34,6 +34,17 @@
|
||||
android:layout_width="fill_parent"
|
||||
android:nextFocusDown="@+id/next"
|
||||
/>
|
||||
<com.fsck.k9.view.ClientCertificateSpinner
|
||||
android:id="@+id/account_client_certificate_spinner"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
<CheckBox
|
||||
android:id="@+id/account_client_certificate"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/account_setup_basics_client_certificate" />
|
||||
<View
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="0dip"
|
||||
|
@ -30,6 +30,7 @@
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_incoming_username_label" />
|
||||
<TextView
|
||||
android:id="@+id/account_password_label"
|
||||
android:text="@string/account_setup_incoming_password_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
@ -41,6 +42,19 @@
|
||||
android:singleLine="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent" />
|
||||
<TextView
|
||||
android:id="@+id/account_client_certificate_label"
|
||||
android:text="@string/account_setup_incoming_client_certificate_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:visibility="gone" />
|
||||
<com.fsck.k9.view.ClientCertificateSpinner
|
||||
android:id="@+id/account_client_certificate_spinner"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:visibility="gone" />
|
||||
<!-- This text may be changed in code if the server is IMAP, etc. -->
|
||||
<TextView
|
||||
android:id="@+id/account_server_label"
|
||||
|
@ -89,6 +89,7 @@
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_outgoing_username_label" />
|
||||
<TextView
|
||||
android:id="@+id/account_password_label"
|
||||
android:text="@string/account_setup_outgoing_password_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
@ -101,6 +102,19 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_outgoing_password_label" />
|
||||
<TextView
|
||||
android:id="@+id/account_client_certificate_label"
|
||||
android:text="@string/account_setup_incoming_client_certificate_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:visibility="gone" />
|
||||
<com.fsck.k9.view.ClientCertificateSpinner
|
||||
android:id="@+id/account_client_certificate_spinner"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
<View
|
||||
android:layout_width="fill_parent"
|
||||
|
18
res/layout/client_certificate_spinner.xml
Normal file
18
res/layout/client_certificate_spinner.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
|
||||
<Button
|
||||
style="?android:attr/spinnerStyle"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:contentDescription="@string/client_certificate_spinner_delete"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_action_cancel_light" />
|
||||
|
||||
</merge>
|
@ -4,5 +4,7 @@
|
||||
<item type="id" name="dialog_confirm_delete"/>
|
||||
<item type="id" name="dialog_confirm_spam"/>
|
||||
<item type="id" name="dialog_attachment_progress"/>
|
||||
<item type="id" name="dialog_client_certificate_not_supported"/>
|
||||
<item type="id" name="dialog_account_setup_error"/>
|
||||
|
||||
</resources>
|
||||
|
@ -373,10 +373,12 @@ Please submit bug reports, contribute new features and ask questions at
|
||||
<string name="account_setup_auth_type_normal_password">Normal password</string>
|
||||
<string name="account_setup_auth_type_insecure_password">Password, transmitted insecurely</string>
|
||||
<string name="account_setup_auth_type_encrypted_password">Encrypted password</string>
|
||||
<string name="account_setup_auth_type_tls_client_certificate">Client certificate</string>
|
||||
|
||||
<string name="account_setup_incoming_title">Incoming server settings</string>
|
||||
<string name="account_setup_incoming_username_label">Username</string>
|
||||
<string name="account_setup_incoming_password_label">Password</string>
|
||||
<string name="account_setup_incoming_client_certificate_label">Client certificate</string>
|
||||
<string name="account_setup_incoming_pop_server_label">POP3 server</string>
|
||||
<string name="account_setup_incoming_imap_server_label">IMAP server</string>
|
||||
<string name="account_setup_incoming_webdav_server_label">Exchange server</string>
|
||||
@ -1116,4 +1118,13 @@ Please submit bug reports, contribute new features and ask questions at
|
||||
<string name="openpgp_error">OpenPGP Error:</string>
|
||||
<string name="openpgp_user_id">User Id</string>
|
||||
|
||||
<!-- === Client certificates specific ================================================================== -->
|
||||
<string name="account_setup_basics_client_certificate">Use client certificate</string>
|
||||
|
||||
<string name="dialog_client_certificate_title">Client Certificate Authentication</string>
|
||||
<string name="dialog_client_certificate_not_supported">Client certificates are not supported on Android versions below 4.0.</string>
|
||||
<string name="dialog_client_certificate_required">This server requires a valid client certificate to be selected.</string>
|
||||
|
||||
<string name="client_certificate_spinner_empty">No client certificate</string>
|
||||
<string name="client_certificate_spinner_delete">Remove client certificate selection</string>
|
||||
</resources>
|
||||
|
@ -1305,6 +1305,10 @@ public class Account implements BaseAccount {
|
||||
return Store.getRemoteInstance(this);
|
||||
}
|
||||
|
||||
public Store getRemoteStore(boolean reload) throws MessagingException {
|
||||
return Store.getRemoteInstance(this, reload);
|
||||
}
|
||||
|
||||
// It'd be great if this actually went into the store implementation
|
||||
// to get this, but that's expensive and not easily accessible
|
||||
// during initialization
|
||||
|
@ -2,6 +2,7 @@
|
||||
package com.fsck.k9;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -1414,4 +1415,14 @@ public class K9 extends Application {
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holding a reference to PrivateKey selected for client certificate
|
||||
* authentication. We need to keep reference to this key so it won't get
|
||||
* garbage collected. If it will then the whole app will crash
|
||||
* on Android <= 4.2 with "Fatal signal 11 code=1".
|
||||
*
|
||||
* see https://code.google.com/p/android/issues/detail?id=62319
|
||||
*/
|
||||
public static ArrayList<PrivateKey> sClientCertificateReferenceWorkaround = new ArrayList<PrivateKey>();
|
||||
}
|
||||
|
@ -112,6 +112,7 @@ public class AccountSetupAccountType extends K9Activity implements OnClickListen
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void failure(Exception use) {
|
||||
Log.e(K9.LOG_TAG, "Failure", use);
|
||||
String toastText = getString(R.string.account_setup_bad_uri, use.getMessage());
|
||||
|
@ -14,11 +14,25 @@ import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
import android.widget.EditText;
|
||||
|
||||
import com.fsck.k9.*;
|
||||
import com.fsck.k9.activity.K9Activity;
|
||||
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.mail.AuthType;
|
||||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.Store;
|
||||
import com.fsck.k9.mail.Transport;
|
||||
import com.fsck.k9.mail.store.ImapStore;
|
||||
import com.fsck.k9.mail.transport.SmtpTransport;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
@ -34,7 +48,7 @@ import java.util.Locale;
|
||||
* AccountSetupAccountType activity.
|
||||
*/
|
||||
public class AccountSetupBasics extends K9Activity
|
||||
implements OnClickListener, TextWatcher {
|
||||
implements OnClickListener, TextWatcher, OnCheckedChangeListener, OnClientCertificateChangedListener {
|
||||
private final static String EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account";
|
||||
private final static int DIALOG_NOTE = 1;
|
||||
private final static String STATE_KEY_PROVIDER =
|
||||
@ -44,6 +58,8 @@ public class AccountSetupBasics extends K9Activity
|
||||
|
||||
private EditText mEmailView;
|
||||
private EditText mPasswordView;
|
||||
private CheckBox mClientCertificateCheckBox;
|
||||
private ClientCertificateSpinner mClientCertificateSpinner;
|
||||
private Button mNextButton;
|
||||
private Button mManualSetupButton;
|
||||
private Account mAccount;
|
||||
@ -63,6 +79,8 @@ public class AccountSetupBasics extends K9Activity
|
||||
setContentView(R.layout.account_setup_basics);
|
||||
mEmailView = (EditText)findViewById(R.id.account_email);
|
||||
mPasswordView = (EditText)findViewById(R.id.account_password);
|
||||
mClientCertificateCheckBox = (CheckBox)findViewById(R.id.account_client_certificate);
|
||||
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
|
||||
mNextButton = (Button)findViewById(R.id.next);
|
||||
mManualSetupButton = (Button)findViewById(R.id.manual_setup);
|
||||
|
||||
@ -71,6 +89,8 @@ public class AccountSetupBasics extends K9Activity
|
||||
|
||||
mEmailView.addTextChangedListener(this);
|
||||
mPasswordView.addTextChangedListener(this);
|
||||
mClientCertificateCheckBox.setOnCheckedChangeListener(this);
|
||||
mClientCertificateSpinner.setOnClientCertificateChangedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -117,10 +137,46 @@ public class AccountSetupBasics extends K9Activity
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClientCertificateChanged(String alias) {
|
||||
validateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when checking the client certificate CheckBox
|
||||
*/
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
if (isChecked) {
|
||||
// clear password field
|
||||
mPasswordView.removeTextChangedListener(this);
|
||||
mPasswordView.setText("");
|
||||
mPasswordView.addTextChangedListener(this);
|
||||
|
||||
// hide password fields, show client certificate spinner
|
||||
mPasswordView.setVisibility(View.GONE);
|
||||
mClientCertificateSpinner.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// clear client certificate spinner
|
||||
mClientCertificateSpinner.setOnClientCertificateChangedListener(null);
|
||||
mClientCertificateSpinner.setAlias(null);
|
||||
mClientCertificateSpinner.setOnClientCertificateChangedListener(this);
|
||||
|
||||
// show password fields, hide client certificate spinner
|
||||
mPasswordView.setVisibility(View.VISIBLE);
|
||||
mClientCertificateSpinner.setVisibility(View.GONE);
|
||||
}
|
||||
validateFields();
|
||||
}
|
||||
|
||||
private void validateFields() {
|
||||
boolean clientCertificateChecked = mClientCertificateCheckBox.isChecked();
|
||||
String clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||
String email = mEmailView.getText().toString();
|
||||
|
||||
boolean valid = Utility.requiredFieldValid(mEmailView)
|
||||
&& Utility.requiredFieldValid(mPasswordView)
|
||||
&& (Utility.requiredFieldValid(mPasswordView)
|
||||
|| (clientCertificateChecked && clientCertificateAlias != null))
|
||||
&& mEmailValidator.isValidAddressOnly(email);
|
||||
|
||||
mNextButton.setEnabled(valid);
|
||||
@ -297,33 +353,38 @@ public class AccountSetupBasics extends K9Activity
|
||||
|
||||
private void onManualSetup() {
|
||||
String email = mEmailView.getText().toString();
|
||||
String password = mPasswordView.getText().toString();
|
||||
String[] emailParts = splitEmail(email);
|
||||
String user = emailParts[0];
|
||||
String domain = emailParts[1];
|
||||
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
AuthType authenticationType = null;
|
||||
if (mClientCertificateCheckBox.isChecked()) {
|
||||
authenticationType = AuthType.EXTERNAL;
|
||||
clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||
} else {
|
||||
authenticationType = AuthType.PLAIN;
|
||||
password = mPasswordView.getText().toString();
|
||||
}
|
||||
|
||||
if (mAccount == null) {
|
||||
mAccount = Preferences.getPreferences(this).newAccount();
|
||||
}
|
||||
mAccount.setName(getOwnerName());
|
||||
mAccount.setEmail(email);
|
||||
try {
|
||||
String userEnc = URLEncoder.encode(user, "UTF-8");
|
||||
String passwordEnc = URLEncoder.encode(password, "UTF-8");
|
||||
|
||||
URI uri = new URI("placeholder", userEnc + ":" + passwordEnc, "mail." + domain, -1, null,
|
||||
null, null);
|
||||
mAccount.setStoreUri(uri.toString());
|
||||
mAccount.setTransportUri(uri.toString());
|
||||
} catch (UnsupportedEncodingException enc) {
|
||||
// This really shouldn't happen since the encoding is hardcoded to UTF-8
|
||||
Log.e(K9.LOG_TAG, "Couldn't urlencode username or password.", enc);
|
||||
} catch (URISyntaxException use) {
|
||||
/*
|
||||
* If we can't set up the URL we just continue. It's only for
|
||||
* convenience.
|
||||
*/
|
||||
}
|
||||
// set default uris
|
||||
// NOTE: they will be changed again in AccountSetupAccountType!
|
||||
ServerSettings storeServer = new ServerSettings(ImapStore.STORE_TYPE, "mail." + domain, -1,
|
||||
ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType, user, password, clientCertificateAlias);
|
||||
ServerSettings transportServer = new ServerSettings(SmtpTransport.TRANSPORT_TYPE, "mail." + domain, -1,
|
||||
ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType, user, password, clientCertificateAlias);
|
||||
String storeUri = Store.createStoreUri(storeServer);
|
||||
String transportUri = Transport.createTransportUri(transportServer);
|
||||
mAccount.setStoreUri(storeUri);
|
||||
mAccount.setTransportUri(transportUri);
|
||||
|
||||
mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts));
|
||||
mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash));
|
||||
mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent));
|
||||
@ -430,4 +491,5 @@ public class AccountSetupBasics extends K9Activity
|
||||
|
||||
public String note;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,21 +9,30 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Process;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.fsck.k9.*;
|
||||
import com.fsck.k9.activity.K9Activity;
|
||||
import com.fsck.k9.controller.MessagingController;
|
||||
import com.fsck.k9.fragment.ConfirmationDialogFragment;
|
||||
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
|
||||
import com.fsck.k9.mail.AuthenticationFailedException;
|
||||
import com.fsck.k9.mail.CertificateValidationException;
|
||||
import com.fsck.k9.mail.ClientCertificateRequiredException;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.Store;
|
||||
import com.fsck.k9.mail.Transport;
|
||||
import com.fsck.k9.mail.store.WebDavStore;
|
||||
import com.fsck.k9.mail.filter.Hex;
|
||||
import com.fsck.k9.net.ssl.SslHelper;
|
||||
import com.fsck.k9.security.KeyChainKeyManager;
|
||||
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
@ -32,6 +41,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Checks the given settings to make sure that they can be used to send and
|
||||
@ -40,7 +50,8 @@ import java.util.List;
|
||||
* XXX NOTE: The manifest for this app has it ignore config changes, because
|
||||
* it doesn't correctly deal with restarting while its thread is running.
|
||||
*/
|
||||
public class AccountSetupCheckSettings extends K9Activity implements OnClickListener {
|
||||
public class AccountSetupCheckSettings extends K9Activity implements OnClickListener,
|
||||
ConfirmationDialogFragmentListener{
|
||||
|
||||
public static final int ACTIVITY_REQUEST_CODE = 1;
|
||||
|
||||
@ -61,13 +72,16 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
|
||||
private Account mAccount;
|
||||
|
||||
private boolean mIsClientCertSet;
|
||||
|
||||
private CheckDirection mDirection;
|
||||
|
||||
private boolean mCanceled;
|
||||
|
||||
private boolean mDestroyed;
|
||||
|
||||
public static void actionCheckSettings(Activity context, Account account, CheckDirection direction) {
|
||||
public static void actionCheckSettings(Activity context, Account account,
|
||||
CheckDirection direction) {
|
||||
Intent i = new Intent(context, AccountSetupCheckSettings.class);
|
||||
i.putExtra(EXTRA_ACCOUNT, account.getUuid());
|
||||
i.putExtra(EXTRA_CHECK_DIRECTION, direction);
|
||||
@ -108,7 +122,9 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
mAccount, mDirection);
|
||||
|
||||
if (mDirection.equals(CheckDirection.INCOMING)) {
|
||||
store = mAccount.getRemoteStore();
|
||||
// refresh URI that stores settings in order to include
|
||||
// client certificate set by user
|
||||
store = mAccount.getRemoteStore(mIsClientCertSet);
|
||||
|
||||
if (store instanceof WebDavStore) {
|
||||
setMessage(R.string.account_setup_check_settings_authenticate);
|
||||
@ -154,6 +170,22 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
R.string.account_setup_failed_dlg_auth_message_fmt,
|
||||
afe.getMessage() == null ? "" : afe.getMessage());
|
||||
} catch (final CertificateValidationException cve) {
|
||||
handleCertificateValidationException(cve);
|
||||
} catch (final ClientCertificateRequiredException ccr) {
|
||||
handleClientCertificateRequiredException(ccr);
|
||||
} catch (final Throwable t) {
|
||||
Log.e(K9.LOG_TAG, "Error while testing settings", t);
|
||||
showErrorDialog(
|
||||
R.string.account_setup_failed_dlg_server_message_fmt,
|
||||
(t.getMessage() == null ? "" : t.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.start();
|
||||
}
|
||||
|
||||
private void handleCertificateValidationException(CertificateValidationException cve) {
|
||||
Log.e(K9.LOG_TAG, "Error while testing settings", cve);
|
||||
|
||||
X509Certificate[] chain = cve.getCertChain();
|
||||
@ -167,17 +199,85 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
R.string.account_setup_failed_dlg_server_message_fmt,
|
||||
(cve.getMessage() == null ? "" : cve.getMessage()));
|
||||
}
|
||||
} catch (final Throwable t) {
|
||||
Log.e(K9.LOG_TAG, "Error while testing settings", t);
|
||||
showErrorDialog(
|
||||
R.string.account_setup_failed_dlg_server_message_fmt,
|
||||
(t.getMessage() == null ? "" : t.getMessage()));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClientCertificateRequiredException(ClientCertificateRequiredException ccr) {
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, "Client certificate alias required: " + ccr.getMessage());
|
||||
|
||||
/*
|
||||
* If the KeyChain API is not available on this Android
|
||||
* version, inform user and abort
|
||||
*/
|
||||
if (!SslHelper.isClientCertificateSupportAvailable()) {
|
||||
mHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
showDialogFragment(R.id.dialog_client_certificate_not_supported);
|
||||
}
|
||||
});
|
||||
|
||||
// abort
|
||||
return;
|
||||
}
|
||||
|
||||
String alias = null;
|
||||
if (CheckDirection.INCOMING.equals(mDirection)) {
|
||||
ServerSettings storeSettings = Store.decodeStoreUri(mAccount.getStoreUri());
|
||||
alias = storeSettings.clientCertificateAlias;
|
||||
} else if (CheckDirection.OUTGOING.equals(mDirection)) {
|
||||
ServerSettings transportSettings = Transport.decodeTransportUri(mAccount.getTransportUri());
|
||||
alias = transportSettings.clientCertificateAlias;
|
||||
}
|
||||
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, "Client certificate alias is: " + alias);
|
||||
|
||||
alias = KeyChainKeyManager.interactivelyChooseClientCertificateAlias(
|
||||
AccountSetupCheckSettings.this,
|
||||
ccr.getKeyTypes(),
|
||||
ccr.getIssuers(),
|
||||
ccr.getHostName(),
|
||||
ccr.getPort(),
|
||||
alias);
|
||||
|
||||
// Note: KeyChainKeyManager gives back "" on cancel
|
||||
if (alias != null && alias.equals("")) {
|
||||
alias = null;
|
||||
}
|
||||
|
||||
// save client certificate alias
|
||||
if (alias != null) {
|
||||
if (CheckDirection.INCOMING.equals(mDirection)) {
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, "Setting store client certificate alias to: " + alias);
|
||||
|
||||
// Set incoming server client certificate alias
|
||||
String storeUri = mAccount.getStoreUri();
|
||||
ServerSettings incoming = Store.decodeStoreUri(storeUri);
|
||||
ServerSettings newIncoming = incoming.newClientCertificateAlias(alias);
|
||||
String newStoreUri = Store.createStoreUri(newIncoming);
|
||||
mAccount.setStoreUri(newStoreUri);
|
||||
} else if (CheckDirection.OUTGOING.equals(mDirection)) {
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, "Setting transport client certificate alias to: " + alias);
|
||||
|
||||
// Set outgoing server client certificate alias
|
||||
String transportUri = mAccount.getTransportUri();
|
||||
ServerSettings outgoing = Transport.decodeTransportUri(transportUri);
|
||||
ServerSettings newOutgoing = outgoing.newClientCertificateAlias(alias);
|
||||
String newTransportUri = Transport.createTransportUri(newOutgoing);
|
||||
mAccount.setTransportUri(newTransportUri);
|
||||
}
|
||||
|
||||
// Save the account settings
|
||||
mAccount.save(Preferences.getPreferences(AccountSetupCheckSettings.this));
|
||||
|
||||
// try again
|
||||
AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount,
|
||||
mDirection);
|
||||
} else {
|
||||
showErrorDialog(R.string.dialog_client_certificate_required);
|
||||
}
|
||||
.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -198,41 +298,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
});
|
||||
}
|
||||
|
||||
private void showErrorDialog(final int msgResId, final Object... args) {
|
||||
mHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
if (mDestroyed) {
|
||||
return;
|
||||
}
|
||||
mProgressBar.setIndeterminate(false);
|
||||
new AlertDialog.Builder(AccountSetupCheckSettings.this)
|
||||
.setTitle(getString(R.string.account_setup_failed_dlg_title))
|
||||
.setMessage(getString(msgResId, args))
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(
|
||||
getString(R.string.account_setup_failed_dlg_continue_action),
|
||||
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mCanceled = false;
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.setPositiveButton(
|
||||
getString(R.string.account_setup_failed_dlg_edit_details_action),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void acceptKeyDialog(final int msgResId,
|
||||
final CertificateValidationException ex) {
|
||||
private void acceptKeyDialog(final int msgResId, final CertificateValidationException ex) {
|
||||
mHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
if (mDestroyed) {
|
||||
@ -351,6 +417,8 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: refactor with DialogFragment.
|
||||
// This is difficult because we need to pass through chain[0] for onClick()
|
||||
new AlertDialog.Builder(AccountSetupCheckSettings.this)
|
||||
.setTitle(getString(R.string.account_setup_failed_dlg_invalid_certificate_title))
|
||||
//.setMessage(getString(R.string.account_setup_failed_dlg_invalid_certificate)
|
||||
@ -362,15 +430,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
getString(R.string.account_setup_failed_dlg_invalid_certificate_accept),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
try {
|
||||
mAccount.addCertificate(mDirection, chain[0]);
|
||||
} catch (CertificateException e) {
|
||||
showErrorDialog(
|
||||
R.string.account_setup_failed_dlg_certificate_message_fmt,
|
||||
e.getMessage() == null ? "" : e.getMessage());
|
||||
}
|
||||
AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount,
|
||||
mDirection);
|
||||
acceptCertificate(chain[0]);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(
|
||||
@ -385,13 +445,30 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently accepts a certificate for the INCOMING or OUTGOING direction
|
||||
* by adding it to the local key store.
|
||||
*
|
||||
* @param certificate
|
||||
*/
|
||||
private void acceptCertificate(X509Certificate certificate) {
|
||||
try {
|
||||
mAccount.addCertificate(mDirection, certificate);
|
||||
} catch (CertificateException e) {
|
||||
showErrorDialog(
|
||||
R.string.account_setup_failed_dlg_certificate_message_fmt,
|
||||
e.getMessage() == null ? "" : e.getMessage());
|
||||
}
|
||||
AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount,
|
||||
mDirection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int reqCode, int resCode, Intent data) {
|
||||
setResult(resCode);
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
private void onCancel() {
|
||||
mCanceled = true;
|
||||
setMessage(R.string.account_setup_check_settings_canceling_msg);
|
||||
@ -404,4 +481,87 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void showErrorDialog(final int msgResId, final Object... args) {
|
||||
mHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
showDialogFragment(R.id.dialog_account_setup_error, getString(msgResId, args));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showDialogFragment(int dialogId) {
|
||||
showDialogFragment(dialogId, null);
|
||||
}
|
||||
|
||||
private void showDialogFragment(int dialogId, String customMessage) {
|
||||
if (mDestroyed) {
|
||||
return;
|
||||
}
|
||||
mProgressBar.setIndeterminate(false);
|
||||
|
||||
DialogFragment fragment;
|
||||
switch (dialogId) {
|
||||
case R.id.dialog_account_setup_error: {
|
||||
fragment = ConfirmationDialogFragment.newInstance(dialogId,
|
||||
getString(R.string.account_setup_failed_dlg_title),
|
||||
customMessage,
|
||||
getString(R.string.account_setup_failed_dlg_edit_details_action),
|
||||
getString(R.string.account_setup_failed_dlg_continue_action)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case R.id.dialog_client_certificate_not_supported: {
|
||||
fragment = ConfirmationDialogFragment.newInstance(dialogId,
|
||||
getString(R.string.dialog_client_certificate_title),
|
||||
getString(R.string.dialog_client_certificate_not_supported),
|
||||
getString(android.R.string.ok)
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new RuntimeException("Called showDialog(int) with unknown dialog id.");
|
||||
}
|
||||
}
|
||||
|
||||
FragmentTransaction ta = getSupportFragmentManager().beginTransaction();
|
||||
ta.add(fragment, getDialogTag(dialogId));
|
||||
ta.commitAllowingStateLoss();
|
||||
|
||||
// TODO: commitAllowingStateLoss() is used to prevent https://code.google.com/p/android/issues/detail?id=23761
|
||||
// but is a bad...
|
||||
//fragment.show(ta, getDialogTag(dialogId));
|
||||
}
|
||||
|
||||
private String getDialogTag(int dialogId) {
|
||||
return String.format(Locale.US, "dialog-%d", dialogId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doPositiveClick(int dialogId) {
|
||||
switch (dialogId) {
|
||||
case R.id.dialog_account_setup_error: {
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doNegativeClick(int dialogId) {
|
||||
switch (dialogId) {
|
||||
case R.id.dialog_account_setup_error:
|
||||
case R.id.dialog_client_certificate_not_supported: {
|
||||
mCanceled = false;
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dialogCancelled(int dialogId) {
|
||||
// nothing to do here...
|
||||
}
|
||||
}
|
||||
|
@ -23,17 +23,20 @@ import com.fsck.k9.mail.AuthType;
|
||||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.Store;
|
||||
import com.fsck.k9.mail.Transport;
|
||||
import com.fsck.k9.mail.store.ImapStore;
|
||||
import com.fsck.k9.mail.store.Pop3Store;
|
||||
import com.fsck.k9.mail.store.WebDavStore;
|
||||
import com.fsck.k9.mail.store.ImapStore.ImapStoreSettings;
|
||||
import com.fsck.k9.mail.store.WebDavStore.WebDavStoreSettings;
|
||||
import com.fsck.k9.mail.transport.SmtpTransport;
|
||||
import com.fsck.k9.net.ssl.SslHelper;
|
||||
import com.fsck.k9.service.MailService;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@ -51,6 +54,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
private String mStoreType;
|
||||
private EditText mUsernameView;
|
||||
private EditText mPasswordView;
|
||||
private ClientCertificateSpinner mClientCertificateSpinner;
|
||||
private TextView mClientCertificateLabelView;
|
||||
private TextView mPasswordLabelView;
|
||||
private EditText mServerView;
|
||||
private EditText mPortView;
|
||||
private Spinner mSecurityTypeView;
|
||||
@ -97,6 +103,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
|
||||
mUsernameView = (EditText)findViewById(R.id.account_username);
|
||||
mPasswordView = (EditText)findViewById(R.id.account_password);
|
||||
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
|
||||
mClientCertificateLabelView = (TextView)findViewById(R.id.account_client_certificate_label);
|
||||
mPasswordLabelView = (TextView)findViewById(R.id.account_password_label);
|
||||
TextView serverLabelView = (TextView) findViewById(R.id.account_server_label);
|
||||
mServerView = (EditText)findViewById(R.id.account_server);
|
||||
mPortView = (EditText)findViewById(R.id.account_port);
|
||||
@ -127,30 +136,25 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
}
|
||||
});
|
||||
|
||||
mAuthTypeAdapter = AuthType.getArrayAdapter(this);
|
||||
mAuthTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
updateViewFromAuthType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
|
||||
});
|
||||
|
||||
mAuthTypeAdapter = AuthType.getArrayAdapter(this, SslHelper.isClientCertificateSupportAvailable());
|
||||
mAuthTypeView.setAdapter(mAuthTypeAdapter);
|
||||
|
||||
/*
|
||||
* Calls validateFields() which enables or disables the Next button
|
||||
* based on the fields' validity.
|
||||
*/
|
||||
TextWatcher validationTextWatcher = new TextWatcher() {
|
||||
public void afterTextChanged(Editable s) {
|
||||
validateFields();
|
||||
}
|
||||
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
/* unused */
|
||||
}
|
||||
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
/* unused */
|
||||
}
|
||||
};
|
||||
mUsernameView.addTextChangedListener(validationTextWatcher);
|
||||
mPasswordView.addTextChangedListener(validationTextWatcher);
|
||||
mServerView.addTextChangedListener(validationTextWatcher);
|
||||
mPortView.addTextChangedListener(validationTextWatcher);
|
||||
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
|
||||
|
||||
/*
|
||||
* Only allow digits in the port field.
|
||||
@ -173,6 +177,20 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
try {
|
||||
ServerSettings settings = Store.decodeStoreUri(mAccount.getStoreUri());
|
||||
|
||||
ArrayAdapter<ConnectionSecurity> securityTypesAdapter =
|
||||
ConnectionSecurity.getArrayAdapter(this, mConnectionSecurityChoices);
|
||||
mSecurityTypeView.setAdapter(securityTypesAdapter);
|
||||
|
||||
// Select currently configured security type
|
||||
int index = securityTypesAdapter.getPosition(settings.connectionSecurity);
|
||||
mSecurityTypeView.setSelection(index, false);
|
||||
|
||||
updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
|
||||
|
||||
// The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter
|
||||
int position = mAuthTypeAdapter.getPosition(settings.authenticationType);
|
||||
mAuthTypeView.setSelection(position, false);
|
||||
|
||||
if (settings.username != null) {
|
||||
mUsernameView.setText(settings.username);
|
||||
}
|
||||
@ -181,11 +199,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
mPasswordView.setText(settings.password);
|
||||
}
|
||||
|
||||
updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
|
||||
|
||||
// The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter
|
||||
int position = mAuthTypeAdapter.getPosition(settings.authenticationType);
|
||||
mAuthTypeView.setSelection(position, false);
|
||||
if (settings.clientCertificateAlias != null) {
|
||||
mClientCertificateSpinner.setAlias(settings.clientCertificateAlias);
|
||||
}
|
||||
|
||||
mStoreType = settings.type;
|
||||
if (Pop3Store.STORE_TYPE.equals(settings.type)) {
|
||||
@ -256,15 +272,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
throw new Exception("Unknown account type: " + mAccount.getStoreUri());
|
||||
}
|
||||
|
||||
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(this,
|
||||
android.R.layout.simple_spinner_item, mConnectionSecurityChoices);
|
||||
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
mSecurityTypeView.setAdapter(securityTypesAdapter);
|
||||
|
||||
// Select currently configured security type
|
||||
int index = securityTypesAdapter.getPosition(settings.connectionSecurity);
|
||||
mSecurityTypeView.setSelection(index, false);
|
||||
|
||||
/*
|
||||
* Updates the port when the user changes the security type. This allows
|
||||
* us to show a reasonable default which the user can change.
|
||||
@ -278,6 +285,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
// this indirectly triggers validateFields because the port text is watched
|
||||
updatePortFromSecurityType();
|
||||
}
|
||||
|
||||
@ -313,12 +321,55 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
outState.putString(EXTRA_ACCOUNT, mAccount.getUuid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows/hides password field and client certificate spinner
|
||||
*/
|
||||
private void updateViewFromAuthType() {
|
||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
boolean isAuthTypeExternal = AuthType.EXTERNAL.equals(authType);
|
||||
|
||||
// if user wants to user external authentication only, then we need to
|
||||
// hide password field, since it won't be used
|
||||
if (isAuthTypeExternal) {
|
||||
// clear password field
|
||||
mPasswordView.removeTextChangedListener(validationTextWatcher);
|
||||
mPasswordView.setText("");
|
||||
mPasswordView.addTextChangedListener(validationTextWatcher);
|
||||
|
||||
// hide password fields, show client certificate fields
|
||||
mPasswordView.setVisibility(View.GONE);
|
||||
mPasswordLabelView.setVisibility(View.GONE);
|
||||
mClientCertificateLabelView.setVisibility(View.VISIBLE);
|
||||
mClientCertificateSpinner.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// clear client certificate
|
||||
mClientCertificateSpinner.setOnClientCertificateChangedListener(null);
|
||||
mClientCertificateSpinner.setAlias(null);
|
||||
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
|
||||
|
||||
// show password fields, hide client certificate fields
|
||||
mPasswordView.setVisibility(View.VISIBLE);
|
||||
mPasswordLabelView.setVisibility(View.VISIBLE);
|
||||
mClientCertificateLabelView.setVisibility(View.GONE);
|
||||
mClientCertificateSpinner.setVisibility(View.GONE);
|
||||
}
|
||||
validateFields();
|
||||
}
|
||||
|
||||
private void validateFields() {
|
||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
boolean isAuthTypeExternal = AuthType.EXTERNAL.equals(authType);
|
||||
|
||||
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
|
||||
boolean hasConnectionSecurity = (!connectionSecurity.equals(ConnectionSecurity.NONE));
|
||||
|
||||
mNextButton
|
||||
.setEnabled(Utility.requiredFieldValid(mUsernameView)
|
||||
&& Utility.requiredFieldValid(mPasswordView)
|
||||
&& (Utility.requiredFieldValid(mPasswordView)
|
||||
|| (isAuthTypeExternal && mClientCertificateSpinner.getAlias() != null))
|
||||
&& Utility.domainFieldValid(mServerView)
|
||||
&& Utility.requiredFieldValid(mPortView));
|
||||
&& Utility.requiredFieldValid(mPortView)
|
||||
&& (!isAuthTypeExternal || hasConnectionSecurity));
|
||||
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
|
||||
}
|
||||
|
||||
@ -377,21 +428,22 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
* password the user just set for incoming.
|
||||
*/
|
||||
try {
|
||||
String usernameEnc = URLEncoder.encode(mUsernameView.getText().toString(), "UTF-8");
|
||||
String passwordEnc = URLEncoder.encode(mPasswordView.getText().toString(), "UTF-8");
|
||||
String username = mUsernameView.getText().toString();
|
||||
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
if (AuthType.EXTERNAL.equals(authType)) {
|
||||
clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||
} else {
|
||||
password = mPasswordView.getText().toString();
|
||||
}
|
||||
|
||||
URI oldUri = new URI(mAccount.getTransportUri());
|
||||
URI uri = new URI(
|
||||
oldUri.getScheme(),
|
||||
usernameEnc + ":" + passwordEnc,
|
||||
oldUri.getHost(),
|
||||
oldUri.getPort(),
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
mAccount.setTransportUri(uri.toString());
|
||||
} catch (UnsupportedEncodingException enc) {
|
||||
// This really shouldn't happen since the encoding is hardcoded to UTF-8
|
||||
Log.e(K9.LOG_TAG, "Couldn't urlencode username or password.", enc);
|
||||
ServerSettings transportServer = new ServerSettings(SmtpTransport.TRANSPORT_TYPE, oldUri.getHost(), oldUri.getPort(),
|
||||
ConnectionSecurity.SSL_TLS_REQUIRED, authType, username, password, clientCertificateAlias);
|
||||
String transportUri = Transport.createTransportUri(transportServer);
|
||||
mAccount.setTransportUri(transportUri);
|
||||
} catch (URISyntaxException use) {
|
||||
/*
|
||||
* If we can't set up the URL we just continue. It's only for
|
||||
@ -411,8 +463,15 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
|
||||
|
||||
String username = mUsernameView.getText().toString();
|
||||
String password = mPasswordView.getText().toString();
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
|
||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
if (authType.equals(AuthType.EXTERNAL)) {
|
||||
clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||
} else {
|
||||
password = mPasswordView.getText().toString();
|
||||
}
|
||||
String host = mServerView.getText().toString();
|
||||
int port = Integer.parseInt(mPortView.getText().toString());
|
||||
|
||||
@ -435,7 +494,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
|
||||
mAccount.deleteCertificate(host, port, CheckDirection.INCOMING);
|
||||
ServerSettings settings = new ServerSettings(mStoreType, host, port,
|
||||
connectionSecurity, authType, username, password, extra);
|
||||
connectionSecurity, authType, username, password, clientCertificateAlias, extra);
|
||||
|
||||
mAccount.setStoreUri(Store.createStoreUri(settings));
|
||||
|
||||
@ -470,4 +529,29 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
/*
|
||||
* Calls validateFields() which enables or disables the Next button
|
||||
* based on the fields' validity.
|
||||
*/
|
||||
TextWatcher validationTextWatcher = new TextWatcher() {
|
||||
public void afterTextChanged(Editable s) {
|
||||
validateFields();
|
||||
}
|
||||
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
/* unused */
|
||||
}
|
||||
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
/* unused */
|
||||
}
|
||||
};
|
||||
|
||||
OnClientCertificateChangedListener clientCertificateChangedListener = new OnClientCertificateChangedListener() {
|
||||
@Override
|
||||
public void onClientCertificateChanged(String alias) {
|
||||
validateFields();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.*;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
|
||||
import com.fsck.k9.*;
|
||||
import com.fsck.k9.activity.K9Activity;
|
||||
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
|
||||
@ -22,6 +23,9 @@ import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.Transport;
|
||||
import com.fsck.k9.mail.transport.SmtpTransport;
|
||||
import com.fsck.k9.net.ssl.SslHelper;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
@ -37,6 +41,9 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
|
||||
private EditText mUsernameView;
|
||||
private EditText mPasswordView;
|
||||
private ClientCertificateSpinner mClientCertificateSpinner;
|
||||
private TextView mClientCertificateLabelView;
|
||||
private TextView mPasswordLabelView;
|
||||
private EditText mServerView;
|
||||
private EditText mPortView;
|
||||
private CheckBox mRequireLoginView;
|
||||
@ -87,6 +94,9 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
|
||||
mUsernameView = (EditText)findViewById(R.id.account_username);
|
||||
mPasswordView = (EditText)findViewById(R.id.account_password);
|
||||
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
|
||||
mClientCertificateLabelView = (TextView)findViewById(R.id.account_client_certificate_label);
|
||||
mPasswordLabelView = (TextView)findViewById(R.id.account_password_label);
|
||||
mServerView = (EditText)findViewById(R.id.account_server);
|
||||
mPortView = (EditText)findViewById(R.id.account_port);
|
||||
mRequireLoginView = (CheckBox)findViewById(R.id.account_require_login);
|
||||
@ -98,33 +108,27 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
mNextButton.setOnClickListener(this);
|
||||
mRequireLoginView.setOnCheckedChangeListener(this);
|
||||
|
||||
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(this,
|
||||
android.R.layout.simple_spinner_item, ConnectionSecurity.values());
|
||||
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
mSecurityTypeView.setAdapter(securityTypesAdapter);
|
||||
mSecurityTypeView.setAdapter(ConnectionSecurity.getArrayAdapter(this));
|
||||
|
||||
mAuthTypeAdapter = AuthType.getArrayAdapter(this);
|
||||
mAuthTypeAdapter = AuthType.getArrayAdapter(this, SslHelper.isClientCertificateSupportAvailable());
|
||||
mAuthTypeView.setAdapter(mAuthTypeAdapter);
|
||||
|
||||
/*
|
||||
* Calls validateFields() which enables or disables the Next button
|
||||
* based on the fields' validity.
|
||||
*/
|
||||
TextWatcher validationTextWatcher = new TextWatcher() {
|
||||
public void afterTextChanged(Editable s) {
|
||||
validateFields();
|
||||
}
|
||||
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
};
|
||||
mUsernameView.addTextChangedListener(validationTextWatcher);
|
||||
mPasswordView.addTextChangedListener(validationTextWatcher);
|
||||
mServerView.addTextChangedListener(validationTextWatcher);
|
||||
mPortView.addTextChangedListener(validationTextWatcher);
|
||||
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
|
||||
|
||||
mAuthTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
updateViewFromAuthType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
|
||||
});
|
||||
|
||||
/*
|
||||
* Only allow digits in the port field.
|
||||
@ -147,17 +151,6 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
|
||||
try {
|
||||
ServerSettings settings = Transport.decodeTransportUri(mAccount.getTransportUri());
|
||||
String username = settings.username;
|
||||
String password = settings.password;
|
||||
|
||||
if (username != null) {
|
||||
mUsernameView.setText(username);
|
||||
mRequireLoginView.setChecked(true);
|
||||
}
|
||||
|
||||
if (password != null) {
|
||||
mPasswordView.setText(password);
|
||||
}
|
||||
|
||||
updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
|
||||
|
||||
@ -168,6 +161,19 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
// Select currently configured security type
|
||||
mSecurityTypeView.setSelection(settings.connectionSecurity.ordinal(), false);
|
||||
|
||||
if (settings.username != null) {
|
||||
mUsernameView.setText(settings.username);
|
||||
mRequireLoginView.setChecked(true);
|
||||
}
|
||||
|
||||
if (settings.password != null) {
|
||||
mPasswordView.setText(settings.password);
|
||||
}
|
||||
|
||||
if (settings.clientCertificateAlias != null) {
|
||||
mClientCertificateSpinner.setAlias(settings.clientCertificateAlias);
|
||||
}
|
||||
|
||||
/*
|
||||
* Updates the port when the user changes the security type. This allows
|
||||
* us to show a reasonable default which the user can change.
|
||||
@ -181,6 +187,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
// this indirectly triggers validateFields because the port text is watched
|
||||
updatePortFromSecurityType();
|
||||
}
|
||||
|
||||
@ -214,14 +221,57 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
outState.putString(EXTRA_ACCOUNT, mAccount.getUuid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows/hides password field and client certificate spinner
|
||||
*/
|
||||
private void updateViewFromAuthType() {
|
||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
boolean isAuthTypeExternal = AuthType.EXTERNAL.equals(authType);
|
||||
|
||||
// if user wants to user external authentication only, then we need to
|
||||
// hide password field, since it won't be used
|
||||
if (isAuthTypeExternal) {
|
||||
// clear password field
|
||||
mPasswordView.removeTextChangedListener(validationTextWatcher);
|
||||
mPasswordView.setText("");
|
||||
mPasswordView.addTextChangedListener(validationTextWatcher);
|
||||
|
||||
// hide password fields, show client certificate fields
|
||||
mPasswordView.setVisibility(View.GONE);
|
||||
mPasswordLabelView.setVisibility(View.GONE);
|
||||
mClientCertificateLabelView.setVisibility(View.VISIBLE);
|
||||
mClientCertificateSpinner.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// clear client certificate
|
||||
mClientCertificateSpinner.setOnClientCertificateChangedListener(null);
|
||||
mClientCertificateSpinner.setAlias(null);
|
||||
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
|
||||
|
||||
// show password fields, hide client certificate fields
|
||||
mPasswordView.setVisibility(View.VISIBLE);
|
||||
mPasswordLabelView.setVisibility(View.VISIBLE);
|
||||
mClientCertificateLabelView.setVisibility(View.GONE);
|
||||
mClientCertificateSpinner.setVisibility(View.GONE);
|
||||
}
|
||||
validateFields();
|
||||
}
|
||||
|
||||
private void validateFields() {
|
||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
boolean isAuthTypeExternal = AuthType.EXTERNAL.equals(authType);
|
||||
|
||||
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
|
||||
boolean hasConnectionSecurity = (!connectionSecurity.equals(ConnectionSecurity.NONE));
|
||||
|
||||
mNextButton
|
||||
.setEnabled(
|
||||
Utility.domainFieldValid(mServerView) &&
|
||||
Utility.requiredFieldValid(mPortView) &&
|
||||
(!mRequireLoginView.isChecked() ||
|
||||
(Utility.requiredFieldValid(mUsernameView) &&
|
||||
Utility.requiredFieldValid(mPasswordView))));
|
||||
.setEnabled(Utility.domainFieldValid(mServerView)
|
||||
&& Utility.requiredFieldValid(mPortView)
|
||||
&& (!mRequireLoginView.isChecked()
|
||||
|| (Utility.requiredFieldValid(mUsernameView) && Utility.requiredFieldValid(mPasswordView))
|
||||
|| (Utility.requiredFieldValid(mUsernameView) && isAuthTypeExternal)
|
||||
)
|
||||
&& (!isAuthTypeExternal || hasConnectionSecurity)
|
||||
);
|
||||
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
|
||||
}
|
||||
|
||||
@ -276,17 +326,23 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
String uri;
|
||||
String username = null;
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
AuthType authType = null;
|
||||
if (mRequireLoginView.isChecked()) {
|
||||
username = mUsernameView.getText().toString();
|
||||
password = mPasswordView.getText().toString();
|
||||
|
||||
authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
if (AuthType.EXTERNAL.equals(authType)) {
|
||||
clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||
} else {
|
||||
password = mPasswordView.getText().toString();
|
||||
}
|
||||
}
|
||||
|
||||
String newHost = mServerView.getText().toString();
|
||||
int newPort = Integer.parseInt(mPortView.getText().toString());
|
||||
String type = SmtpTransport.TRANSPORT_TYPE;
|
||||
ServerSettings server = new ServerSettings(type, newHost, newPort, securityType, authType, username, password);
|
||||
ServerSettings server = new ServerSettings(type, newHost, newPort, securityType, authType, username, password, clientCertificateAlias);
|
||||
uri = Transport.createTransportUri(server);
|
||||
mAccount.deleteCertificate(newHost, newPort, CheckDirection.OUTGOING);
|
||||
mAccount.setTransportUri(uri);
|
||||
@ -312,4 +368,27 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
/*
|
||||
* Calls validateFields() which enables or disables the Next button
|
||||
* based on the fields' validity.
|
||||
*/
|
||||
TextWatcher validationTextWatcher = new TextWatcher() {
|
||||
public void afterTextChanged(Editable s) {
|
||||
validateFields();
|
||||
}
|
||||
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
};
|
||||
|
||||
OnClientCertificateChangedListener clientCertificateChangedListener = new OnClientCertificateChangedListener() {
|
||||
@Override
|
||||
public void onClientCertificateChanged(String alias) {
|
||||
validateFields();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,17 +1,21 @@
|
||||
package com.fsck.k9.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnCancelListener;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockDialogFragment;
|
||||
import com.fsck.k9.K9;
|
||||
|
||||
|
||||
public class ConfirmationDialogFragment extends SherlockDialogFragment implements OnClickListener,
|
||||
OnCancelListener {
|
||||
private ConfirmationDialogFragmentListener mListener;
|
||||
|
||||
private static final String ARG_DIALOG_ID = "dialog_id";
|
||||
private static final String ARG_TITLE = "title";
|
||||
@ -35,6 +39,11 @@ public class ConfirmationDialogFragment extends SherlockDialogFragment implement
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public static ConfirmationDialogFragment newInstance(int dialogId, String title, String message,
|
||||
String cancelText) {
|
||||
return newInstance(dialogId, title, message, null, cancelText);
|
||||
}
|
||||
|
||||
|
||||
public interface ConfirmationDialogFragmentListener {
|
||||
void doPositiveClick(int dialogId);
|
||||
@ -54,8 +63,14 @@ public class ConfirmationDialogFragment extends SherlockDialogFragment implement
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(title);
|
||||
builder.setMessage(message);
|
||||
if (confirmText != null && cancelText != null) {
|
||||
builder.setPositiveButton(confirmText, this);
|
||||
builder.setNegativeButton(cancelText, this);
|
||||
} else if (cancelText != null) {
|
||||
builder.setNeutralButton(cancelText, this);
|
||||
} else {
|
||||
throw new RuntimeException("Set at least cancelText!");
|
||||
}
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
@ -71,6 +86,10 @@ public class ConfirmationDialogFragment extends SherlockDialogFragment implement
|
||||
getListener().doNegativeClick(getDialogId());
|
||||
break;
|
||||
}
|
||||
case DialogInterface.BUTTON_NEUTRAL: {
|
||||
getListener().doNegativeClick(getDialogId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,7 +103,23 @@ public class ConfirmationDialogFragment extends SherlockDialogFragment implement
|
||||
return getArguments().getInt(ARG_DIALOG_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
mListener = (ConfirmationDialogFragmentListener) activity;
|
||||
} catch (ClassCastException e) {
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, activity.toString() + " did not implement ConfirmationDialogFragmentListener");
|
||||
}
|
||||
}
|
||||
|
||||
private ConfirmationDialogFragmentListener getListener() {
|
||||
if (mListener != null) {
|
||||
return mListener;
|
||||
}
|
||||
|
||||
// fallback to getTargetFragment...
|
||||
try {
|
||||
return (ConfirmationDialogFragmentListener) getTargetFragment();
|
||||
} catch (ClassCastException e) {
|
||||
|
@ -33,6 +33,8 @@ public enum AuthType {
|
||||
|
||||
CRAM_MD5(R.string.account_setup_auth_type_encrypted_password),
|
||||
|
||||
EXTERNAL(R.string.account_setup_auth_type_tls_client_certificate),
|
||||
|
||||
/*
|
||||
* The following are obsolete authentication settings that were used with
|
||||
* SMTP. They are no longer presented to the user as options, but they may
|
||||
@ -43,8 +45,13 @@ public enum AuthType {
|
||||
|
||||
LOGIN(0);
|
||||
|
||||
static public ArrayAdapter<AuthType> getArrayAdapter(Context context) {
|
||||
AuthType[] authTypes = {PLAIN, CRAM_MD5};
|
||||
static public ArrayAdapter<AuthType> getArrayAdapter(Context context, boolean includeExternal) {
|
||||
AuthType[] authTypes;
|
||||
if (includeExternal) {
|
||||
authTypes = new AuthType[]{PLAIN, CRAM_MD5, EXTERNAL};
|
||||
} else {
|
||||
authTypes = new AuthType[]{PLAIN, CRAM_MD5};
|
||||
}
|
||||
ArrayAdapter<AuthType> authTypesAdapter = new ArrayAdapter<AuthType>(context,
|
||||
android.R.layout.simple_spinner_item, authTypes);
|
||||
authTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
|
48
src/com/fsck/k9/mail/ClientCertificateRequiredException.java
Normal file
48
src/com/fsck/k9/mail/ClientCertificateRequiredException.java
Normal file
@ -0,0 +1,48 @@
|
||||
|
||||
package com.fsck.k9.mail;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* This exception is thrown when, during an SSL handshake, a client certificate
|
||||
* alias is requested but we want the user to select one instead of using the
|
||||
* previously selected one silently. This must be a RuntimeException because the
|
||||
* implemented interface of X509ExtendedKeyManager (where it is thrown) does not
|
||||
* allow anything else.
|
||||
*/
|
||||
public class ClientCertificateRequiredException extends RuntimeException {
|
||||
public static final long serialVersionUID = -1;
|
||||
|
||||
String[] mKeyTypes;
|
||||
Principal[] mIssuers;
|
||||
String mHostName;
|
||||
int mPort;
|
||||
|
||||
public ClientCertificateRequiredException(String[] keyTypes,
|
||||
Principal[] issuers,
|
||||
String hostName,
|
||||
int port) {
|
||||
super("interactive client certificate alias choice required");
|
||||
this.mKeyTypes = keyTypes;
|
||||
this.mIssuers = issuers;
|
||||
this.mHostName = hostName;
|
||||
this.mPort = port;
|
||||
}
|
||||
|
||||
public String[] getKeyTypes() {
|
||||
return mKeyTypes;
|
||||
}
|
||||
|
||||
public Principal[] getIssuers() {
|
||||
return mIssuers;
|
||||
}
|
||||
|
||||
public String getHostName() {
|
||||
return mHostName;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return mPort;
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
package com.fsck.k9.mail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.R;
|
||||
|
||||
@ -8,6 +11,17 @@ public enum ConnectionSecurity {
|
||||
STARTTLS_REQUIRED(R.string.account_setup_incoming_security_tls_label),
|
||||
SSL_TLS_REQUIRED(R.string.account_setup_incoming_security_ssl_label);
|
||||
|
||||
static public ArrayAdapter<ConnectionSecurity> getArrayAdapter(Context context) {
|
||||
return getArrayAdapter(context, ConnectionSecurity.values());
|
||||
}
|
||||
|
||||
static public ArrayAdapter<ConnectionSecurity> getArrayAdapter(Context context, ConnectionSecurity[] securityTypes) {
|
||||
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(context,
|
||||
android.R.layout.simple_spinner_item, securityTypes);
|
||||
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
return securityTypesAdapter;
|
||||
}
|
||||
|
||||
private final int mResourceId;
|
||||
|
||||
private ConnectionSecurity(int id) {
|
||||
|
@ -64,6 +64,14 @@ public class ServerSettings {
|
||||
*/
|
||||
public final String password;
|
||||
|
||||
/**
|
||||
* The alias to retrieve a client certificate using Android 4.0 KeyChain API
|
||||
* for TLS client certificate authentication with the server.
|
||||
*
|
||||
* {@code null} if not applicable for the store or transport.
|
||||
*/
|
||||
public final String clientCertificateAlias;
|
||||
|
||||
/**
|
||||
* Store- or transport-specific settings as key/value pair.
|
||||
*
|
||||
@ -89,10 +97,12 @@ public class ServerSettings {
|
||||
* see {@link ServerSettings#username}
|
||||
* @param password
|
||||
* see {@link ServerSettings#password}
|
||||
* @param clientCertificateAlias
|
||||
* see {@link ServerSettings#clientCertificateAlias}
|
||||
*/
|
||||
public ServerSettings(String type, String host, int port,
|
||||
ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
|
||||
String password) {
|
||||
String password, String clientCertificateAlias) {
|
||||
this.type = type;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
@ -100,6 +110,7 @@ public class ServerSettings {
|
||||
this.authenticationType = authenticationType;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.clientCertificateAlias = clientCertificateAlias;
|
||||
this.extra = null;
|
||||
}
|
||||
|
||||
@ -120,12 +131,14 @@ public class ServerSettings {
|
||||
* see {@link ServerSettings#username}
|
||||
* @param password
|
||||
* see {@link ServerSettings#password}
|
||||
* @param clientCertificateAlias
|
||||
* see {@link ServerSettings#clientCertificateAlias}
|
||||
* @param extra
|
||||
* see {@link ServerSettings#extra}
|
||||
*/
|
||||
public ServerSettings(String type, String host, int port,
|
||||
ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
|
||||
String password, Map<String, String> extra) {
|
||||
String password, String clientCertificateAlias, Map<String, String> extra) {
|
||||
this.type = type;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
@ -133,6 +146,7 @@ public class ServerSettings {
|
||||
this.authenticationType = authenticationType;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.clientCertificateAlias = clientCertificateAlias;
|
||||
this.extra = (extra != null) ?
|
||||
Collections.unmodifiableMap(new HashMap<String, String>(extra)) : null;
|
||||
}
|
||||
@ -153,6 +167,7 @@ public class ServerSettings {
|
||||
authenticationType = null;
|
||||
username = null;
|
||||
password = null;
|
||||
clientCertificateAlias = null;
|
||||
extra = null;
|
||||
}
|
||||
|
||||
@ -173,6 +188,11 @@ public class ServerSettings {
|
||||
|
||||
public ServerSettings newPassword(String newPassword) {
|
||||
return new ServerSettings(type, host, port, connectionSecurity, authenticationType,
|
||||
username, newPassword);
|
||||
username, newPassword, clientCertificateAlias);
|
||||
}
|
||||
|
||||
public ServerSettings newClientCertificateAlias(String newAlias) {
|
||||
return new ServerSettings(type, host, port, connectionSecurity, AuthType.EXTERNAL,
|
||||
username, password, newAlias);
|
||||
}
|
||||
}
|
@ -51,6 +51,16 @@ public abstract class Store {
|
||||
* Get an instance of a remote mail store.
|
||||
*/
|
||||
public synchronized static Store getRemoteInstance(Account account) throws MessagingException {
|
||||
return getRemoteInstance(account, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of a remote mail store.
|
||||
*
|
||||
* @param reload
|
||||
* reload store also if already instantiated
|
||||
*/
|
||||
public synchronized static Store getRemoteInstance(Account account, boolean reload) throws MessagingException {
|
||||
String uri = account.getStoreUri();
|
||||
|
||||
if (uri.startsWith("local")) {
|
||||
@ -58,7 +68,7 @@ public abstract class Store {
|
||||
}
|
||||
|
||||
Store store = sStores.get(uri);
|
||||
if (store == null) {
|
||||
if (store == null || reload) {
|
||||
if (uri.startsWith("imap")) {
|
||||
store = new ImapStore(account);
|
||||
} else if (uri.startsWith("pop3")) {
|
||||
|
@ -25,7 +25,6 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.text.SimpleDateFormat;
|
||||
@ -50,9 +49,7 @@ import java.util.regex.Pattern;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
@ -100,8 +97,7 @@ import com.fsck.k9.mail.store.ImapResponseParser.ImapList;
|
||||
import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse;
|
||||
import com.fsck.k9.mail.store.imap.ImapUtility;
|
||||
import com.fsck.k9.mail.transport.imap.ImapSettings;
|
||||
import com.fsck.k9.net.ssl.TrustManagerFactory;
|
||||
import com.fsck.k9.net.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.net.ssl.SslHelper;
|
||||
import com.jcraft.jzlib.JZlib;
|
||||
import com.jcraft.jzlib.ZOutputStream;
|
||||
|
||||
@ -126,6 +122,7 @@ public class ImapStore extends Store {
|
||||
private static final String CAPABILITY_IDLE = "IDLE";
|
||||
private static final String CAPABILITY_AUTH_CRAM_MD5 = "AUTH=CRAM-MD5";
|
||||
private static final String CAPABILITY_AUTH_PLAIN = "AUTH=PLAIN";
|
||||
private static final String CAPABILITY_AUTH_EXTERNAL = "AUTH=EXTERNAL";
|
||||
private static final String CAPABILITY_LOGINDISABLED = "LOGINDISABLED";
|
||||
private static final String COMMAND_IDLE = "IDLE";
|
||||
private static final String CAPABILITY_NAMESPACE = "NAMESPACE";
|
||||
@ -158,6 +155,7 @@ public class ImapStore extends Store {
|
||||
AuthType authenticationType = null;
|
||||
String username = null;
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
String pathPrefix = null;
|
||||
boolean autoDetectNamespace = true;
|
||||
|
||||
@ -213,11 +211,16 @@ public class ImapStore extends Store {
|
||||
authenticationType = AuthType.PLAIN;
|
||||
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
|
||||
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
||||
} else {
|
||||
} else if (userInfoParts.length == 3) {
|
||||
authenticationType = AuthType.valueOf(userInfoParts[0]);
|
||||
username = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
||||
|
||||
if (AuthType.EXTERNAL.equals(authenticationType)) {
|
||||
clientCertificateAlias = URLDecoder.decode(userInfoParts[2], "UTF-8");
|
||||
} else {
|
||||
password = URLDecoder.decode(userInfoParts[2], "UTF-8");
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException enc) {
|
||||
// This shouldn't happen since the encoding is hardcoded to UTF-8
|
||||
throw new IllegalArgumentException("Couldn't urldecode username or password.", enc);
|
||||
@ -243,7 +246,7 @@ public class ImapStore extends Store {
|
||||
}
|
||||
|
||||
return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, username,
|
||||
password, autoDetectNamespace, pathPrefix);
|
||||
password, clientCertificateAlias, autoDetectNamespace, pathPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -260,10 +263,13 @@ public class ImapStore extends Store {
|
||||
public static String createUri(ServerSettings server) {
|
||||
String userEnc;
|
||||
String passwordEnc;
|
||||
String clientCertificateAliasEnc;
|
||||
try {
|
||||
userEnc = URLEncoder.encode(server.username, "UTF-8");
|
||||
passwordEnc = (server.password != null) ?
|
||||
URLEncoder.encode(server.password, "UTF-8") : "";
|
||||
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
|
||||
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalArgumentException("Could not encode username or password", e);
|
||||
@ -284,8 +290,12 @@ public class ImapStore extends Store {
|
||||
}
|
||||
|
||||
AuthType authType = server.authenticationType;
|
||||
|
||||
String userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
|
||||
String userInfo;
|
||||
if (authType.equals(AuthType.EXTERNAL)) {
|
||||
userInfo = authType.name() + ":" + userEnc + ":" + clientCertificateAliasEnc;
|
||||
} else {
|
||||
userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
|
||||
}
|
||||
try {
|
||||
Map<String, String> extra = server.getExtra();
|
||||
String path = null;
|
||||
@ -320,10 +330,10 @@ public class ImapStore extends Store {
|
||||
public final String pathPrefix;
|
||||
|
||||
protected ImapStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
|
||||
AuthType authenticationType, String username, String password,
|
||||
AuthType authenticationType, String username, String password, String clientCertificateAlias,
|
||||
boolean autodetectNamespace, String pathPrefix) {
|
||||
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
|
||||
password);
|
||||
password, clientCertificateAlias);
|
||||
this.autoDetectNamespace = autodetectNamespace;
|
||||
this.pathPrefix = pathPrefix;
|
||||
}
|
||||
@ -339,7 +349,7 @@ public class ImapStore extends Store {
|
||||
@Override
|
||||
public ServerSettings newPassword(String newPassword) {
|
||||
return new ImapStoreSettings(host, port, connectionSecurity, authenticationType,
|
||||
username, newPassword, autoDetectNamespace, pathPrefix);
|
||||
username, newPassword, clientCertificateAlias, autoDetectNamespace, pathPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
@ -348,6 +358,7 @@ public class ImapStore extends Store {
|
||||
private int mPort;
|
||||
private String mUsername;
|
||||
private String mPassword;
|
||||
private String mClientCertificateAlias;
|
||||
private ConnectionSecurity mConnectionSecurity;
|
||||
private AuthType mAuthType;
|
||||
private volatile String mPathPrefix;
|
||||
@ -386,6 +397,11 @@ public class ImapStore extends Store {
|
||||
return mPassword;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientCertificateAlias() {
|
||||
return mClientCertificateAlias;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useCompression(final int type) {
|
||||
return mAccount.useCompression(type);
|
||||
@ -458,6 +474,7 @@ public class ImapStore extends Store {
|
||||
mAuthType = settings.authenticationType;
|
||||
mUsername = settings.username;
|
||||
mPassword = settings.password;
|
||||
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||
|
||||
// Make extra sure mPathPrefix is null if "auto-detect namespace" is configured
|
||||
mPathPrefix = (settings.autoDetectNamespace) ? null : settings.pathPrefix;
|
||||
@ -2419,14 +2436,8 @@ public class ImapStore extends Store {
|
||||
mSettings.getPort());
|
||||
|
||||
if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext
|
||||
.init(null,
|
||||
new TrustManager[] { TrustManagerFactory.get(
|
||||
mSettings.getHost(),
|
||||
mSettings.getPort()) },
|
||||
new SecureRandom());
|
||||
mSocket = TrustedSocketFactory.createSocket(sslContext);
|
||||
mSocket = SslHelper.createSslSocket(mSettings.getHost(),
|
||||
mSettings.getPort(), mSettings.getClientCertificateAlias());
|
||||
} else {
|
||||
mSocket = new Socket();
|
||||
}
|
||||
@ -2475,14 +2486,9 @@ public class ImapStore extends Store {
|
||||
// STARTTLS
|
||||
executeSimpleCommand("STARTTLS");
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null,
|
||||
new TrustManager[] { TrustManagerFactory.get(
|
||||
mSettings.getHost(),
|
||||
mSettings.getPort()) },
|
||||
new SecureRandom());
|
||||
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket,
|
||||
mSettings.getHost(), mSettings.getPort(), true);
|
||||
mSocket = SslHelper.createStartTlsSocket(mSocket,
|
||||
mSettings.getHost(), mSettings.getPort(), true,
|
||||
mSettings.getClientCertificateAlias());
|
||||
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
|
||||
mIn = new PeekableInputStream(new BufferedInputStream(mSocket
|
||||
.getInputStream(), 1024));
|
||||
@ -2531,6 +2537,17 @@ public class ImapStore extends Store {
|
||||
}
|
||||
break;
|
||||
|
||||
case EXTERNAL:
|
||||
if (hasCapability(CAPABILITY_AUTH_EXTERNAL)) {
|
||||
executeSimpleCommand(
|
||||
String.format("AUTHENTICATE EXTERNAL %s",
|
||||
Utility.base64Encode(mSettings.getUsername())), false);
|
||||
} else {
|
||||
throw new MessagingException(
|
||||
"EXTERNAL authentication not advertised by server");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new MessagingException(
|
||||
"Unhandled authentication method found in the server settings (bug).");
|
||||
@ -2630,7 +2647,6 @@ public class ImapStore extends Store {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (SSLException e) {
|
||||
throw new CertificateValidationException(e.getMessage(), e);
|
||||
} catch (GeneralSecurityException gse) {
|
||||
|
@ -8,23 +8,18 @@ import com.fsck.k9.K9;
|
||||
import com.fsck.k9.controller.MessageRetrievalListener;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.mail.*;
|
||||
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
import com.fsck.k9.mail.filter.Hex;
|
||||
import com.fsck.k9.mail.internet.MimeMessage;
|
||||
import com.fsck.k9.net.ssl.TrustManagerFactory;
|
||||
import com.fsck.k9.net.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.net.ssl.SslHelper;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -59,15 +54,16 @@ public class Pop3Store extends Store {
|
||||
private static final String SASL_CAPABILITY = "SASL";
|
||||
private static final String AUTH_PLAIN_CAPABILITY = "PLAIN";
|
||||
private static final String AUTH_CRAM_MD5_CAPABILITY = "CRAM-MD5";
|
||||
private static final String AUTH_EXTERNAL_CAPABILITY = "EXTERNAL";
|
||||
|
||||
/**
|
||||
* Decodes a Pop3Store URI.
|
||||
*
|
||||
* <p>Possible forms:</p>
|
||||
* <pre>
|
||||
* pop3://user:password@server:port ConnectionSecurity.NONE
|
||||
* pop3+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
|
||||
* pop3+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
* pop3://auth:user:password@server:port ConnectionSecurity.NONE
|
||||
* pop3+tls+://auth:user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
|
||||
* pop3+ssl+://auth:user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
* </pre>
|
||||
*/
|
||||
public static ServerSettings decodeUri(String uri) {
|
||||
@ -76,6 +72,7 @@ public class Pop3Store extends Store {
|
||||
ConnectionSecurity connectionSecurity;
|
||||
String username = null;
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
|
||||
URI pop3Uri;
|
||||
try {
|
||||
@ -131,8 +128,12 @@ public class Pop3Store extends Store {
|
||||
}
|
||||
username = URLDecoder.decode(userInfoParts[userIndex], "UTF-8");
|
||||
if (userInfoParts.length > passwordIndex) {
|
||||
if (authType.equals(AuthType.EXTERNAL)) {
|
||||
clientCertificateAlias = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
|
||||
} else {
|
||||
password = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException enc) {
|
||||
// This shouldn't happen since the encoding is hardcoded to UTF-8
|
||||
throw new IllegalArgumentException("Couldn't urldecode username or password.", enc);
|
||||
@ -140,7 +141,7 @@ public class Pop3Store extends Store {
|
||||
}
|
||||
|
||||
return new ServerSettings(STORE_TYPE, host, port, connectionSecurity, authType, username,
|
||||
password);
|
||||
password, clientCertificateAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -157,10 +158,13 @@ public class Pop3Store extends Store {
|
||||
public static String createUri(ServerSettings server) {
|
||||
String userEnc;
|
||||
String passwordEnc;
|
||||
String clientCertificateAliasEnc;
|
||||
try {
|
||||
userEnc = URLEncoder.encode(server.username, "UTF-8");
|
||||
passwordEnc = (server.password != null) ?
|
||||
URLEncoder.encode(server.password, "UTF-8") : "";
|
||||
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
|
||||
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalArgumentException("Could not encode username or password", e);
|
||||
@ -180,7 +184,14 @@ public class Pop3Store extends Store {
|
||||
break;
|
||||
}
|
||||
|
||||
String userInfo = server.authenticationType.name() + ":" + userEnc + ":" + passwordEnc;
|
||||
AuthType authType = server.authenticationType;
|
||||
String userInfo;
|
||||
if (AuthType.EXTERNAL.equals(authType)) {
|
||||
userInfo = authType.name() + ":" + userEnc + ":" + clientCertificateAliasEnc;
|
||||
} else {
|
||||
userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
|
||||
}
|
||||
|
||||
try {
|
||||
return new URI(scheme, userInfo, server.host, server.port, null, null,
|
||||
null).toString();
|
||||
@ -194,6 +205,7 @@ public class Pop3Store extends Store {
|
||||
private int mPort;
|
||||
private String mUsername;
|
||||
private String mPassword;
|
||||
private String mClientCertificateAlias;
|
||||
private AuthType mAuthType;
|
||||
private ConnectionSecurity mConnectionSecurity;
|
||||
private HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
|
||||
@ -224,6 +236,7 @@ public class Pop3Store extends Store {
|
||||
|
||||
mUsername = settings.username;
|
||||
mPassword = settings.password;
|
||||
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||
mAuthType = settings.authenticationType;
|
||||
}
|
||||
|
||||
@ -301,11 +314,7 @@ public class Pop3Store extends Store {
|
||||
try {
|
||||
SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
|
||||
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null,
|
||||
new TrustManager[] { TrustManagerFactory.get(mHost,
|
||||
mPort) }, new SecureRandom());
|
||||
mSocket = TrustedSocketFactory.createSocket(sslContext);
|
||||
mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
|
||||
} else {
|
||||
mSocket = new Socket();
|
||||
}
|
||||
@ -327,13 +336,8 @@ public class Pop3Store extends Store {
|
||||
if (mCapabilities.stls) {
|
||||
executeSimpleCommand(STLS_COMMAND);
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null,
|
||||
new TrustManager[] { TrustManagerFactory.get(
|
||||
mHost, mPort) },
|
||||
new SecureRandom());
|
||||
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
|
||||
mPort, true);
|
||||
mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
|
||||
mClientCertificateAlias);
|
||||
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
|
||||
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
|
||||
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
|
||||
@ -372,6 +376,17 @@ public class Pop3Store extends Store {
|
||||
}
|
||||
break;
|
||||
|
||||
case EXTERNAL:
|
||||
if (mCapabilities.external) {
|
||||
executeSimpleCommand(
|
||||
String.format("AUTHENTICATE EXTERNAL %s",
|
||||
Utility.base64Encode(mUsername)), false);
|
||||
} else {
|
||||
throw new MessagingException(
|
||||
"EXTERNAL authentication not advertised by server");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new MessagingException(
|
||||
"Unhandled authentication method found in the server settings (bug).");
|
||||
@ -1046,6 +1061,8 @@ public class Pop3Store extends Store {
|
||||
capabilities.authPlain = true;
|
||||
} else if (response.equals(AUTH_CRAM_MD5_CAPABILITY)) {
|
||||
capabilities.cramMD5 = true;
|
||||
} else if (response.equals(AUTH_EXTERNAL_CAPABILITY)) {
|
||||
capabilities.external = true;
|
||||
}
|
||||
}
|
||||
} catch (MessagingException e) {
|
||||
@ -1193,15 +1210,17 @@ public class Pop3Store extends Store {
|
||||
public boolean stls;
|
||||
public boolean top;
|
||||
public boolean uidl;
|
||||
public boolean external;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("CRAM-MD5 %b, PLAIN %b, STLS %b, TOP %b, UIDL %b",
|
||||
return String.format("CRAM-MD5 %b, PLAIN %b, STLS %b, TOP %b, UIDL %b, EXTERNAL %b",
|
||||
cramMD5,
|
||||
authPlain,
|
||||
stls,
|
||||
top,
|
||||
uidl);
|
||||
uidl,
|
||||
external);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,9 +7,9 @@ import com.fsck.k9.K9;
|
||||
import com.fsck.k9.controller.MessageRetrievalListener;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.mail.*;
|
||||
|
||||
import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
|
||||
import com.fsck.k9.mail.internet.MimeMessage;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.*;
|
||||
import org.apache.http.client.CookieStore;
|
||||
@ -36,6 +36,7 @@ import javax.net.ssl.SSLException;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.parsers.SAXParser;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
@ -178,7 +179,7 @@ public class WebDavStore extends Store {
|
||||
}
|
||||
|
||||
return new WebDavStoreSettings(host, port, connectionSecurity, null, username, password,
|
||||
alias, path, authPath, mailboxPath);
|
||||
null, alias, path, authPath, mailboxPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -257,10 +258,10 @@ public class WebDavStore extends Store {
|
||||
public final String mailboxPath;
|
||||
|
||||
protected WebDavStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
|
||||
AuthType authenticationType, String username, String password, String alias,
|
||||
AuthType authenticationType, String username, String password, String clientCertificateAlias, String alias,
|
||||
String path, String authPath, String mailboxPath) {
|
||||
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
|
||||
password);
|
||||
password, clientCertificateAlias);
|
||||
this.alias = alias;
|
||||
this.path = path;
|
||||
this.authPath = authPath;
|
||||
@ -280,7 +281,7 @@ public class WebDavStore extends Store {
|
||||
@Override
|
||||
public ServerSettings newPassword(String newPassword) {
|
||||
return new WebDavStoreSettings(host, port, connectionSecurity, authenticationType,
|
||||
username, newPassword, alias, path, authPath, mailboxPath);
|
||||
username, newPassword, clientCertificateAlias, alias, path, authPath, mailboxPath);
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,6 +290,7 @@ public class WebDavStore extends Store {
|
||||
private String mUsername; /* Stores the username for authentications */
|
||||
private String mAlias; /* Stores the alias for the user's mailbox */
|
||||
private String mPassword; /* Stores the password for authentications */
|
||||
private String mClientCertificateAlias;
|
||||
private String mUrl; /* Stores the base URL for the server */
|
||||
private String mHost; /* Stores the host name for the server */
|
||||
private int mPort;
|
||||
@ -324,6 +326,7 @@ public class WebDavStore extends Store {
|
||||
|
||||
mUsername = settings.username;
|
||||
mPassword = settings.password;
|
||||
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||
mAlias = settings.alias;
|
||||
|
||||
mPath = settings.path;
|
||||
|
@ -2,6 +2,7 @@
|
||||
package com.fsck.k9.mail.transport;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.mail.*;
|
||||
@ -13,12 +14,9 @@ import com.fsck.k9.mail.filter.PeekableInputStream;
|
||||
import com.fsck.k9.mail.filter.SmtpDataStuffing;
|
||||
import com.fsck.k9.mail.internet.MimeUtility;
|
||||
import com.fsck.k9.mail.store.LocalStore.LocalMessage;
|
||||
import com.fsck.k9.net.ssl.TrustManagerFactory;
|
||||
import com.fsck.k9.net.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.net.ssl.SslHelper;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
@ -27,9 +25,7 @@ import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.*;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class SmtpTransport extends Transport {
|
||||
@ -38,20 +34,23 @@ public class SmtpTransport extends Transport {
|
||||
/**
|
||||
* Decodes a SmtpTransport URI.
|
||||
*
|
||||
* NOTE: In contrast to ImapStore and Pop3Store, the authType is appended at the end!
|
||||
*
|
||||
* <p>Possible forms:</p>
|
||||
* <pre>
|
||||
* smtp://user:password@server:port ConnectionSecurity.NONE
|
||||
* smtp+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
|
||||
* smtp+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
* smtp://user:password:auth@server:port ConnectionSecurity.NONE
|
||||
* smtp+tls+://user:password:auth@server:port ConnectionSecurity.STARTTLS_REQUIRED
|
||||
* smtp+ssl+://user:password:auth@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
* </pre>
|
||||
*/
|
||||
public static ServerSettings decodeUri(String uri) {
|
||||
String host;
|
||||
int port;
|
||||
ConnectionSecurity connectionSecurity;
|
||||
AuthType authType = AuthType.PLAIN;
|
||||
AuthType authType = null;
|
||||
String username = null;
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
|
||||
URI smtpUri;
|
||||
try {
|
||||
@ -95,14 +94,22 @@ public class SmtpTransport extends Transport {
|
||||
if (smtpUri.getUserInfo() != null) {
|
||||
try {
|
||||
String[] userInfoParts = smtpUri.getUserInfo().split(":");
|
||||
if (userInfoParts.length > 0) {
|
||||
if (userInfoParts.length == 1) {
|
||||
authType = AuthType.PLAIN;
|
||||
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
|
||||
}
|
||||
if (userInfoParts.length > 1) {
|
||||
} else if (userInfoParts.length == 2) {
|
||||
authType = AuthType.PLAIN;
|
||||
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
|
||||
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
||||
} else if (userInfoParts.length == 3) {
|
||||
// NOTE: In SmptTransport URIs, the authType comes last!
|
||||
authType = AuthType.valueOf(userInfoParts[2]);
|
||||
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
|
||||
if (authType.equals(AuthType.EXTERNAL)) {
|
||||
clientCertificateAlias = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
||||
} else {
|
||||
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
||||
}
|
||||
if (userInfoParts.length > 2) {
|
||||
authType = AuthType.valueOf(userInfoParts[2]);
|
||||
}
|
||||
} catch (UnsupportedEncodingException enc) {
|
||||
// This shouldn't happen since the encoding is hardcoded to UTF-8
|
||||
@ -111,7 +118,7 @@ public class SmtpTransport extends Transport {
|
||||
}
|
||||
|
||||
return new ServerSettings(TRANSPORT_TYPE, host, port, connectionSecurity,
|
||||
authType, username, password);
|
||||
authType, username, password, clientCertificateAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -128,11 +135,14 @@ public class SmtpTransport extends Transport {
|
||||
public static String createUri(ServerSettings server) {
|
||||
String userEnc;
|
||||
String passwordEnc;
|
||||
String clientCertificateAliasEnc;
|
||||
try {
|
||||
userEnc = (server.username != null) ?
|
||||
URLEncoder.encode(server.username, "UTF-8") : "";
|
||||
passwordEnc = (server.password != null) ?
|
||||
URLEncoder.encode(server.password, "UTF-8") : "";
|
||||
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
|
||||
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalArgumentException("Could not encode username or password", e);
|
||||
@ -152,10 +162,17 @@ public class SmtpTransport extends Transport {
|
||||
break;
|
||||
}
|
||||
|
||||
String userInfo = userEnc + ":" + passwordEnc;
|
||||
String userInfo = null;
|
||||
AuthType authType = server.authenticationType;
|
||||
// NOTE: authType is append at last item, in contrast to ImapStore and Pop3Store!
|
||||
if (authType != null) {
|
||||
userInfo += ":" + authType.name();
|
||||
if (AuthType.EXTERNAL.equals(authType)) {
|
||||
userInfo = userEnc + ":" + clientCertificateAliasEnc + ":" + authType.name();
|
||||
} else {
|
||||
userInfo = userEnc + ":" + passwordEnc + ":" + authType.name();
|
||||
}
|
||||
} else {
|
||||
userInfo = userEnc + ":" + passwordEnc;
|
||||
}
|
||||
try {
|
||||
return new URI(scheme, userInfo, server.host, server.port, null, null,
|
||||
@ -170,6 +187,7 @@ public class SmtpTransport extends Transport {
|
||||
int mPort;
|
||||
String mUsername;
|
||||
String mPassword;
|
||||
String mClientCertificateAlias;
|
||||
AuthType mAuthType;
|
||||
ConnectionSecurity mConnectionSecurity;
|
||||
Socket mSocket;
|
||||
@ -194,6 +212,7 @@ public class SmtpTransport extends Transport {
|
||||
mAuthType = settings.authenticationType;
|
||||
mUsername = settings.username;
|
||||
mPassword = settings.password;
|
||||
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -205,12 +224,7 @@ public class SmtpTransport extends Transport {
|
||||
try {
|
||||
SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort);
|
||||
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null,
|
||||
new TrustManager[] { TrustManagerFactory.get(
|
||||
mHost, mPort) },
|
||||
new SecureRandom());
|
||||
mSocket = TrustedSocketFactory.createSocket(sslContext);
|
||||
mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
|
||||
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
|
||||
secureConnection = true;
|
||||
} else {
|
||||
@ -264,12 +278,9 @@ public class SmtpTransport extends Transport {
|
||||
if (extensions.containsKey("STARTTLS")) {
|
||||
executeSimpleCommand("STARTTLS");
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null,
|
||||
new TrustManager[] { TrustManagerFactory.get(mHost,
|
||||
mPort) }, new SecureRandom());
|
||||
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
|
||||
mPort, true);
|
||||
mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
|
||||
mClientCertificateAlias);
|
||||
|
||||
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
|
||||
1024));
|
||||
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
|
||||
|
@ -21,6 +21,8 @@ public interface ImapSettings {
|
||||
|
||||
String getPassword();
|
||||
|
||||
String getClientCertificateAlias();
|
||||
|
||||
boolean useCompression(int type);
|
||||
|
||||
String getPathPrefix();
|
||||
|
103
src/com/fsck/k9/net/ssl/SslHelper.java
Normal file
103
src/com/fsck/k9/net/ssl/SslHelper.java
Normal file
@ -0,0 +1,103 @@
|
||||
|
||||
package com.fsck.k9.net.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.security.KeyChainKeyManager;
|
||||
|
||||
/**
|
||||
* Helper class to create SSL sockets with support for client certificate
|
||||
* authentication
|
||||
*/
|
||||
public class SslHelper {
|
||||
|
||||
/**
|
||||
* KeyChain API available on Android >= 4.0
|
||||
*
|
||||
* @return true if API is available
|
||||
*/
|
||||
public static boolean isClientCertificateSupportAvailable() {
|
||||
return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH);
|
||||
}
|
||||
|
||||
@SuppressLint("TrulyRandom")
|
||||
private static SSLContext createSslContext(String host, int port, String clientCertificateAlias)
|
||||
throws NoSuchAlgorithmException, KeyManagementException, MessagingException {
|
||||
if (clientCertificateAlias != null && !isClientCertificateSupportAvailable()) {
|
||||
throw new MessagingException(
|
||||
"Client certificate support is only availble on Android >= 4.0", true);
|
||||
}
|
||||
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, "createSslContext: Client certificate alias: "
|
||||
+ clientCertificateAlias);
|
||||
|
||||
KeyManager[] keyManagers = null;
|
||||
if (clientCertificateAlias != null) {
|
||||
keyManagers = new KeyManager[] {
|
||||
new KeyChainKeyManager(clientCertificateAlias)
|
||||
};
|
||||
} else {
|
||||
keyManagers = new KeyManager[] {
|
||||
new KeyChainKeyManager()
|
||||
};
|
||||
}
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(keyManagers,
|
||||
new TrustManager[] {
|
||||
TrustManagerFactory.get(
|
||||
host, port)
|
||||
},
|
||||
new SecureRandom());
|
||||
|
||||
return sslContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SSL socket
|
||||
*
|
||||
* @param host
|
||||
* @param port
|
||||
* @param clientCertificateAlias if not null, uses client certificate
|
||||
* retrieved by this alias for authentication
|
||||
*/
|
||||
public static Socket createSslSocket(String host, int port, String clientCertificateAlias)
|
||||
throws NoSuchAlgorithmException, KeyManagementException, IOException,
|
||||
MessagingException {
|
||||
SSLContext sslContext = createSslContext(host, port, clientCertificateAlias);
|
||||
return TrustedSocketFactory.createSocket(sslContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create socket for START_TLS. autoClose = true
|
||||
*
|
||||
* @param socket
|
||||
* @param host
|
||||
* @param port
|
||||
* @param secure
|
||||
* @param clientCertificateAlias if not null, uses client certificate
|
||||
* retrieved by this alias for authentication
|
||||
*/
|
||||
public static Socket createStartTlsSocket(Socket socket, String host, int port, boolean secure,
|
||||
String clientCertificateAlias) throws NoSuchAlgorithmException,
|
||||
KeyManagementException, IOException, MessagingException {
|
||||
SSLContext sslContext = createSslContext(host, port, clientCertificateAlias);
|
||||
boolean autoClose = true;
|
||||
return TrustedSocketFactory.createSocket(sslContext, socket, host, port, autoClose);
|
||||
}
|
||||
}
|
@ -1090,7 +1090,8 @@ public class SettingsImporter {
|
||||
public ImportedServerSettings(ImportedServer server) {
|
||||
super(server.type, server.host, convertPort(server.port),
|
||||
convertConnectionSecurity(server.connectionSecurity),
|
||||
server.authenticationType, server.username, server.password);
|
||||
server.authenticationType, server.username, server.password,
|
||||
server.clientCertificateAlias);
|
||||
mImportedServer = server;
|
||||
}
|
||||
|
||||
@ -1155,6 +1156,7 @@ public class SettingsImporter {
|
||||
public AuthType authenticationType;
|
||||
public String username;
|
||||
public String password;
|
||||
public String clientCertificateAlias;
|
||||
public ImportedSettings extras;
|
||||
}
|
||||
|
||||
|
172
src/com/fsck/k9/security/KeyChainKeyManager.java
Normal file
172
src/com/fsck/k9/security/KeyChainKeyManager.java
Normal file
@ -0,0 +1,172 @@
|
||||
|
||||
package com.fsck.k9.security;
|
||||
|
||||
import java.net.Socket;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import javax.net.ssl.X509ExtendedKeyManager;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.security.KeyChain;
|
||||
import android.security.KeyChainAliasCallback;
|
||||
import android.security.KeyChainException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.mail.ClientCertificateRequiredException;
|
||||
|
||||
/**
|
||||
* For client certificate authentication! Provide private keys and certificates
|
||||
* during the TLS handshake using the Android 4.0 KeyChain API. If interactive
|
||||
* selection is requested, we harvest the parameters during the handshake and
|
||||
* abort with a custom (runtime) ClientCertificateRequiredException.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||
public class KeyChainKeyManager extends X509ExtendedKeyManager {
|
||||
|
||||
private String mAlias;
|
||||
|
||||
public KeyChainKeyManager() {
|
||||
mAlias = null;
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, "KeyChainKeyManager set to interactive prompting required");
|
||||
}
|
||||
|
||||
public KeyChainKeyManager(String alias) {
|
||||
if (alias == null || "".equals(alias)) {
|
||||
throw new IllegalArgumentException(
|
||||
"KeyChainKeyManager: The provided alias is null or empty!");
|
||||
}
|
||||
|
||||
mAlias = alias;
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, "KeyChainKeyManager set up with for auto-selected alias " + alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
|
||||
if (mAlias == null) {
|
||||
throw new ClientCertificateRequiredException(keyTypes, issuers,
|
||||
socket.getInetAddress().getHostName(), socket.getPort());
|
||||
}
|
||||
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, "KeyChainKeyManager.chooseClientAlias returning preselected alias "
|
||||
+ mAlias);
|
||||
|
||||
return mAlias;
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getCertificateChain(String alias) {
|
||||
try {
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, "KeyChainKeyManager.getCertificateChain for " + alias);
|
||||
|
||||
X509Certificate[] chain = KeyChain.getCertificateChain(K9.app, alias);
|
||||
|
||||
if (chain == null || chain.length == 0) {
|
||||
throw new IllegalStateException("No certificate chain found for: " + alias);
|
||||
}
|
||||
|
||||
return chain;
|
||||
} catch (KeyChainException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey getPrivateKey(String alias) {
|
||||
try {
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, "KeyChainKeyManager.getPrivateKey for " + alias);
|
||||
|
||||
PrivateKey key = KeyChain.getPrivateKey(K9.app, alias);
|
||||
|
||||
if (key == null) {
|
||||
throw new IllegalStateException("No private key found for: " + alias);
|
||||
}
|
||||
|
||||
/*
|
||||
* We need to keep reference to this key so it won't get garbage
|
||||
* collected. If it will then the whole app will crash on Android <=
|
||||
* 4.2 with "Fatal signal 11 code=1". See
|
||||
* https://code.google.com/p/android/issues/detail?id=62319
|
||||
*/
|
||||
K9.sClientCertificateReferenceWorkaround.add(key);
|
||||
|
||||
return key;
|
||||
} catch (KeyChainException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
|
||||
// not valid for client side
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
||||
// not valid for client side
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getServerAliases(String keyType, Principal[] issuers) {
|
||||
// not valid for client side
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public static String interactivelyChooseClientCertificateAlias(Activity activity,
|
||||
String[] keyTypes, Principal[] issuers, String hostName, int port,
|
||||
String preSelectedAlias) {
|
||||
// defined as array to be able to set it inside the callback
|
||||
final String[] selectedAlias = new String[1];
|
||||
|
||||
KeyChain.choosePrivateKeyAlias(activity, new KeyChainAliasCallback() {
|
||||
@Override
|
||||
public void alias(String alias) {
|
||||
synchronized (selectedAlias) {
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, "User has selected client certificate alias:" + alias);
|
||||
|
||||
// see below. not null is condition for breaking out of loop
|
||||
if (alias == null) {
|
||||
alias = "";
|
||||
}
|
||||
selectedAlias[0] = alias;
|
||||
selectedAlias.notifyAll();
|
||||
}
|
||||
|
||||
}
|
||||
}, keyTypes, issuers, hostName, port, preSelectedAlias);
|
||||
|
||||
synchronized (selectedAlias) {
|
||||
while (selectedAlias[0] == null) {
|
||||
try {
|
||||
selectedAlias.wait();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ("".equals(selectedAlias[0])) {
|
||||
selectedAlias[0] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return selectedAlias[0];
|
||||
}
|
||||
}
|
121
src/com/fsck/k9/view/ClientCertificateSpinner.java
Normal file
121
src/com/fsck/k9/view/ClientCertificateSpinner.java
Normal file
@ -0,0 +1,121 @@
|
||||
|
||||
package com.fsck.k9.view;
|
||||
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.security.KeyChain;
|
||||
import android.security.KeyChainAliasCallback;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
public class ClientCertificateSpinner extends LinearLayout {
|
||||
Activity mActivity;
|
||||
OnClientCertificateChangedListener mListener;
|
||||
|
||||
Button mSelection;
|
||||
ImageButton mDeleteButton;
|
||||
|
||||
String mAlias;
|
||||
|
||||
public interface OnClientCertificateChangedListener {
|
||||
void onClientCertificateChanged(String alias);
|
||||
}
|
||||
|
||||
public void setOnClientCertificateChangedListener(OnClientCertificateChangedListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public ClientCertificateSpinner(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
if (context instanceof Activity) {
|
||||
mActivity = (Activity) context;
|
||||
} else {
|
||||
Log.e(K9.LOG_TAG, "ClientCertificateSpinner init failed! Please inflate with Activity!");
|
||||
}
|
||||
|
||||
setOrientation(LinearLayout.HORIZONTAL);
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater.inflate(R.layout.client_certificate_spinner, this, true);
|
||||
|
||||
mSelection = (Button) getChildAt(0);
|
||||
updateView();
|
||||
mSelection.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onSelect();
|
||||
}
|
||||
});
|
||||
|
||||
mDeleteButton = (ImageButton) getChildAt(1);
|
||||
mDeleteButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onDelete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ClientCertificateSpinner(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public void setAlias(String alias) {
|
||||
// Note: KeyChainAliasCallback gives back "" on cancel
|
||||
if (alias != null && alias.equals("")) {
|
||||
alias = null;
|
||||
}
|
||||
|
||||
mAlias = alias;
|
||||
// Note: KeyChainAliasCallback is a different thread than the UI
|
||||
mActivity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mListener != null) {
|
||||
mListener.onClientCertificateChanged(mAlias);
|
||||
}
|
||||
updateView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getAlias() {
|
||||
return mAlias;
|
||||
}
|
||||
|
||||
private void onDelete() {
|
||||
setAlias(null);
|
||||
}
|
||||
|
||||
private void onSelect() {
|
||||
// NOTE: keyTypes, issuers, hosts, port are not known before we actually
|
||||
// open a connection, thus we cannot set them here!
|
||||
KeyChain.choosePrivateKeyAlias(mActivity, new KeyChainAliasCallback() {
|
||||
@Override
|
||||
public void alias(String alias) {
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, "User has selected client certificate alias:" + alias);
|
||||
|
||||
setAlias(alias);
|
||||
}
|
||||
}, null, null, null, -1, getAlias());
|
||||
}
|
||||
|
||||
private void updateView() {
|
||||
if (mAlias != null) {
|
||||
mSelection.setText(mAlias);
|
||||
} else {
|
||||
mSelection.setText(R.string.client_certificate_spinner_empty);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user