mirror of
https://github.com/moparisthebest/k-9
synced 2024-11-30 13:12:25 -05:00
Client Certificate Authentication
This commit is contained in:
parent
2fdcb77c5c
commit
aad171ff7e
@ -34,6 +34,17 @@
|
|||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:nextFocusDown="@+id/next"
|
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
|
<View
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="0dip"
|
android:layout_height="0dip"
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:contentDescription="@string/account_setup_incoming_username_label" />
|
android:contentDescription="@string/account_setup_incoming_username_label" />
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/account_password_label"
|
||||||
android:text="@string/account_setup_incoming_password_label"
|
android:text="@string/account_setup_incoming_password_label"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
@ -41,6 +42,19 @@
|
|||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="fill_parent" />
|
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. -->
|
<!-- This text may be changed in code if the server is IMAP, etc. -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/account_server_label"
|
android:id="@+id/account_server_label"
|
||||||
|
@ -89,6 +89,7 @@
|
|||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:contentDescription="@string/account_setup_outgoing_username_label" />
|
android:contentDescription="@string/account_setup_outgoing_username_label" />
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/account_password_label"
|
||||||
android:text="@string/account_setup_outgoing_password_label"
|
android:text="@string/account_setup_outgoing_password_label"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
@ -101,6 +102,19 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:contentDescription="@string/account_setup_outgoing_password_label" />
|
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>
|
</LinearLayout>
|
||||||
<View
|
<View
|
||||||
android:layout_width="fill_parent"
|
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_delete"/>
|
||||||
<item type="id" name="dialog_confirm_spam"/>
|
<item type="id" name="dialog_confirm_spam"/>
|
||||||
<item type="id" name="dialog_attachment_progress"/>
|
<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>
|
</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_normal_password">Normal password</string>
|
||||||
<string name="account_setup_auth_type_insecure_password">Password, transmitted insecurely</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_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_title">Incoming server settings</string>
|
||||||
<string name="account_setup_incoming_username_label">Username</string>
|
<string name="account_setup_incoming_username_label">Username</string>
|
||||||
<string name="account_setup_incoming_password_label">Password</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_pop_server_label">POP3 server</string>
|
||||||
<string name="account_setup_incoming_imap_server_label">IMAP server</string>
|
<string name="account_setup_incoming_imap_server_label">IMAP server</string>
|
||||||
<string name="account_setup_incoming_webdav_server_label">Exchange 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_error">OpenPGP Error:</string>
|
||||||
<string name="openpgp_user_id">User Id</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>
|
</resources>
|
||||||
|
@ -1305,6 +1305,10 @@ public class Account implements BaseAccount {
|
|||||||
return Store.getRemoteInstance(this);
|
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
|
// It'd be great if this actually went into the store implementation
|
||||||
// to get this, but that's expensive and not easily accessible
|
// to get this, but that's expensive and not easily accessible
|
||||||
// during initialization
|
// during initialization
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
package com.fsck.k9;
|
package com.fsck.k9;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.security.PrivateKey;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -1414,4 +1415,14 @@ public class K9 extends Application {
|
|||||||
editor.commit();
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void failure(Exception use) {
|
private void failure(Exception use) {
|
||||||
Log.e(K9.LOG_TAG, "Failure", use);
|
Log.e(K9.LOG_TAG, "Failure", use);
|
||||||
String toastText = getString(R.string.account_setup_bad_uri, use.getMessage());
|
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;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
|
||||||
import com.fsck.k9.*;
|
import com.fsck.k9.*;
|
||||||
import com.fsck.k9.activity.K9Activity;
|
import com.fsck.k9.activity.K9Activity;
|
||||||
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
|
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
|
||||||
import com.fsck.k9.helper.Utility;
|
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.Serializable;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@ -34,7 +48,7 @@ import java.util.Locale;
|
|||||||
* AccountSetupAccountType activity.
|
* AccountSetupAccountType activity.
|
||||||
*/
|
*/
|
||||||
public class AccountSetupBasics extends K9Activity
|
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 String EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account";
|
||||||
private final static int DIALOG_NOTE = 1;
|
private final static int DIALOG_NOTE = 1;
|
||||||
private final static String STATE_KEY_PROVIDER =
|
private final static String STATE_KEY_PROVIDER =
|
||||||
@ -44,6 +58,8 @@ public class AccountSetupBasics extends K9Activity
|
|||||||
|
|
||||||
private EditText mEmailView;
|
private EditText mEmailView;
|
||||||
private EditText mPasswordView;
|
private EditText mPasswordView;
|
||||||
|
private CheckBox mClientCertificateCheckBox;
|
||||||
|
private ClientCertificateSpinner mClientCertificateSpinner;
|
||||||
private Button mNextButton;
|
private Button mNextButton;
|
||||||
private Button mManualSetupButton;
|
private Button mManualSetupButton;
|
||||||
private Account mAccount;
|
private Account mAccount;
|
||||||
@ -63,6 +79,8 @@ public class AccountSetupBasics extends K9Activity
|
|||||||
setContentView(R.layout.account_setup_basics);
|
setContentView(R.layout.account_setup_basics);
|
||||||
mEmailView = (EditText)findViewById(R.id.account_email);
|
mEmailView = (EditText)findViewById(R.id.account_email);
|
||||||
mPasswordView = (EditText)findViewById(R.id.account_password);
|
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);
|
mNextButton = (Button)findViewById(R.id.next);
|
||||||
mManualSetupButton = (Button)findViewById(R.id.manual_setup);
|
mManualSetupButton = (Button)findViewById(R.id.manual_setup);
|
||||||
|
|
||||||
@ -71,6 +89,8 @@ public class AccountSetupBasics extends K9Activity
|
|||||||
|
|
||||||
mEmailView.addTextChangedListener(this);
|
mEmailView.addTextChangedListener(this);
|
||||||
mPasswordView.addTextChangedListener(this);
|
mPasswordView.addTextChangedListener(this);
|
||||||
|
mClientCertificateCheckBox.setOnCheckedChangeListener(this);
|
||||||
|
mClientCertificateSpinner.setOnClientCertificateChangedListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -117,10 +137,46 @@ public class AccountSetupBasics extends K9Activity
|
|||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
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() {
|
private void validateFields() {
|
||||||
|
boolean clientCertificateChecked = mClientCertificateCheckBox.isChecked();
|
||||||
|
String clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||||
String email = mEmailView.getText().toString();
|
String email = mEmailView.getText().toString();
|
||||||
|
|
||||||
boolean valid = Utility.requiredFieldValid(mEmailView)
|
boolean valid = Utility.requiredFieldValid(mEmailView)
|
||||||
&& Utility.requiredFieldValid(mPasswordView)
|
&& (Utility.requiredFieldValid(mPasswordView)
|
||||||
|
|| (clientCertificateChecked && clientCertificateAlias != null))
|
||||||
&& mEmailValidator.isValidAddressOnly(email);
|
&& mEmailValidator.isValidAddressOnly(email);
|
||||||
|
|
||||||
mNextButton.setEnabled(valid);
|
mNextButton.setEnabled(valid);
|
||||||
@ -297,33 +353,38 @@ public class AccountSetupBasics extends K9Activity
|
|||||||
|
|
||||||
private void onManualSetup() {
|
private void onManualSetup() {
|
||||||
String email = mEmailView.getText().toString();
|
String email = mEmailView.getText().toString();
|
||||||
String password = mPasswordView.getText().toString();
|
|
||||||
String[] emailParts = splitEmail(email);
|
String[] emailParts = splitEmail(email);
|
||||||
String user = emailParts[0];
|
String user = emailParts[0];
|
||||||
String domain = emailParts[1];
|
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) {
|
if (mAccount == null) {
|
||||||
mAccount = Preferences.getPreferences(this).newAccount();
|
mAccount = Preferences.getPreferences(this).newAccount();
|
||||||
}
|
}
|
||||||
mAccount.setName(getOwnerName());
|
mAccount.setName(getOwnerName());
|
||||||
mAccount.setEmail(email);
|
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,
|
// set default uris
|
||||||
null, null);
|
// NOTE: they will be changed again in AccountSetupAccountType!
|
||||||
mAccount.setStoreUri(uri.toString());
|
ServerSettings storeServer = new ServerSettings(ImapStore.STORE_TYPE, "mail." + domain, -1,
|
||||||
mAccount.setTransportUri(uri.toString());
|
ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType, user, password, clientCertificateAlias);
|
||||||
} catch (UnsupportedEncodingException enc) {
|
ServerSettings transportServer = new ServerSettings(SmtpTransport.TRANSPORT_TYPE, "mail." + domain, -1,
|
||||||
// This really shouldn't happen since the encoding is hardcoded to UTF-8
|
ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType, user, password, clientCertificateAlias);
|
||||||
Log.e(K9.LOG_TAG, "Couldn't urlencode username or password.", enc);
|
String storeUri = Store.createStoreUri(storeServer);
|
||||||
} catch (URISyntaxException use) {
|
String transportUri = Transport.createTransportUri(transportServer);
|
||||||
/*
|
mAccount.setStoreUri(storeUri);
|
||||||
* If we can't set up the URL we just continue. It's only for
|
mAccount.setTransportUri(transportUri);
|
||||||
* convenience.
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts));
|
mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts));
|
||||||
mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash));
|
mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash));
|
||||||
mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent));
|
mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent));
|
||||||
@ -430,4 +491,5 @@ public class AccountSetupBasics extends K9Activity
|
|||||||
|
|
||||||
public String note;
|
public String note;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,21 +9,30 @@ import android.net.Uri;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
|
import android.support.v4.app.DialogFragment;
|
||||||
|
import android.support.v4.app.FragmentTransaction;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.fsck.k9.*;
|
import com.fsck.k9.*;
|
||||||
import com.fsck.k9.activity.K9Activity;
|
import com.fsck.k9.activity.K9Activity;
|
||||||
import com.fsck.k9.controller.MessagingController;
|
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.AuthenticationFailedException;
|
||||||
import com.fsck.k9.mail.CertificateValidationException;
|
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.Store;
|
||||||
import com.fsck.k9.mail.Transport;
|
import com.fsck.k9.mail.Transport;
|
||||||
import com.fsck.k9.mail.store.WebDavStore;
|
import com.fsck.k9.mail.store.WebDavStore;
|
||||||
import com.fsck.k9.mail.filter.Hex;
|
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.CertificateException;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
@ -32,6 +41,7 @@ import java.security.NoSuchAlgorithmException;
|
|||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the given settings to make sure that they can be used to send and
|
* 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
|
* 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.
|
* 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;
|
public static final int ACTIVITY_REQUEST_CODE = 1;
|
||||||
|
|
||||||
@ -61,13 +72,16 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
|||||||
|
|
||||||
private Account mAccount;
|
private Account mAccount;
|
||||||
|
|
||||||
|
private boolean mIsClientCertSet;
|
||||||
|
|
||||||
private CheckDirection mDirection;
|
private CheckDirection mDirection;
|
||||||
|
|
||||||
private boolean mCanceled;
|
private boolean mCanceled;
|
||||||
|
|
||||||
private boolean mDestroyed;
|
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);
|
Intent i = new Intent(context, AccountSetupCheckSettings.class);
|
||||||
i.putExtra(EXTRA_ACCOUNT, account.getUuid());
|
i.putExtra(EXTRA_ACCOUNT, account.getUuid());
|
||||||
i.putExtra(EXTRA_CHECK_DIRECTION, direction);
|
i.putExtra(EXTRA_CHECK_DIRECTION, direction);
|
||||||
@ -108,7 +122,9 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
|||||||
mAccount, mDirection);
|
mAccount, mDirection);
|
||||||
|
|
||||||
if (mDirection.equals(CheckDirection.INCOMING)) {
|
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) {
|
if (store instanceof WebDavStore) {
|
||||||
setMessage(R.string.account_setup_check_settings_authenticate);
|
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,
|
R.string.account_setup_failed_dlg_auth_message_fmt,
|
||||||
afe.getMessage() == null ? "" : afe.getMessage());
|
afe.getMessage() == null ? "" : afe.getMessage());
|
||||||
} catch (final CertificateValidationException cve) {
|
} 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);
|
Log.e(K9.LOG_TAG, "Error while testing settings", cve);
|
||||||
|
|
||||||
X509Certificate[] chain = cve.getCertChain();
|
X509Certificate[] chain = cve.getCertChain();
|
||||||
@ -167,17 +199,85 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
|||||||
R.string.account_setup_failed_dlg_server_message_fmt,
|
R.string.account_setup_failed_dlg_server_message_fmt,
|
||||||
(cve.getMessage() == null ? "" : cve.getMessage()));
|
(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
|
@Override
|
||||||
@ -198,41 +298,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showErrorDialog(final int msgResId, final Object... args) {
|
private void acceptKeyDialog(final int msgResId, final CertificateValidationException ex) {
|
||||||
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) {
|
|
||||||
mHandler.post(new Runnable() {
|
mHandler.post(new Runnable() {
|
||||||
public void run() {
|
public void run() {
|
||||||
if (mDestroyed) {
|
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)
|
new AlertDialog.Builder(AccountSetupCheckSettings.this)
|
||||||
.setTitle(getString(R.string.account_setup_failed_dlg_invalid_certificate_title))
|
.setTitle(getString(R.string.account_setup_failed_dlg_invalid_certificate_title))
|
||||||
//.setMessage(getString(R.string.account_setup_failed_dlg_invalid_certificate)
|
//.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),
|
getString(R.string.account_setup_failed_dlg_invalid_certificate_accept),
|
||||||
new DialogInterface.OnClickListener() {
|
new DialogInterface.OnClickListener() {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
try {
|
acceptCertificate(chain[0]);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setNegativeButton(
|
.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
|
@Override
|
||||||
public void onActivityResult(int reqCode, int resCode, Intent data) {
|
public void onActivityResult(int reqCode, int resCode, Intent data) {
|
||||||
setResult(resCode);
|
setResult(resCode);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void onCancel() {
|
private void onCancel() {
|
||||||
mCanceled = true;
|
mCanceled = true;
|
||||||
setMessage(R.string.account_setup_check_settings_canceling_msg);
|
setMessage(R.string.account_setup_check_settings_canceling_msg);
|
||||||
@ -404,4 +481,87 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
|||||||
break;
|
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.ConnectionSecurity;
|
||||||
import com.fsck.k9.mail.ServerSettings;
|
import com.fsck.k9.mail.ServerSettings;
|
||||||
import com.fsck.k9.mail.Store;
|
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.ImapStore;
|
||||||
import com.fsck.k9.mail.store.Pop3Store;
|
import com.fsck.k9.mail.store.Pop3Store;
|
||||||
import com.fsck.k9.mail.store.WebDavStore;
|
import com.fsck.k9.mail.store.WebDavStore;
|
||||||
import com.fsck.k9.mail.store.ImapStore.ImapStoreSettings;
|
import com.fsck.k9.mail.store.ImapStore.ImapStoreSettings;
|
||||||
import com.fsck.k9.mail.store.WebDavStore.WebDavStoreSettings;
|
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.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.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -51,6 +54,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
|||||||
private String mStoreType;
|
private String mStoreType;
|
||||||
private EditText mUsernameView;
|
private EditText mUsernameView;
|
||||||
private EditText mPasswordView;
|
private EditText mPasswordView;
|
||||||
|
private ClientCertificateSpinner mClientCertificateSpinner;
|
||||||
|
private TextView mClientCertificateLabelView;
|
||||||
|
private TextView mPasswordLabelView;
|
||||||
private EditText mServerView;
|
private EditText mServerView;
|
||||||
private EditText mPortView;
|
private EditText mPortView;
|
||||||
private Spinner mSecurityTypeView;
|
private Spinner mSecurityTypeView;
|
||||||
@ -97,6 +103,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
|||||||
|
|
||||||
mUsernameView = (EditText)findViewById(R.id.account_username);
|
mUsernameView = (EditText)findViewById(R.id.account_username);
|
||||||
mPasswordView = (EditText)findViewById(R.id.account_password);
|
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);
|
TextView serverLabelView = (TextView) findViewById(R.id.account_server_label);
|
||||||
mServerView = (EditText)findViewById(R.id.account_server);
|
mServerView = (EditText)findViewById(R.id.account_server);
|
||||||
mPortView = (EditText)findViewById(R.id.account_port);
|
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);
|
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);
|
mUsernameView.addTextChangedListener(validationTextWatcher);
|
||||||
mPasswordView.addTextChangedListener(validationTextWatcher);
|
mPasswordView.addTextChangedListener(validationTextWatcher);
|
||||||
mServerView.addTextChangedListener(validationTextWatcher);
|
mServerView.addTextChangedListener(validationTextWatcher);
|
||||||
mPortView.addTextChangedListener(validationTextWatcher);
|
mPortView.addTextChangedListener(validationTextWatcher);
|
||||||
|
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Only allow digits in the port field.
|
* Only allow digits in the port field.
|
||||||
@ -173,6 +177,20 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
|||||||
try {
|
try {
|
||||||
ServerSettings settings = Store.decodeStoreUri(mAccount.getStoreUri());
|
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) {
|
if (settings.username != null) {
|
||||||
mUsernameView.setText(settings.username);
|
mUsernameView.setText(settings.username);
|
||||||
}
|
}
|
||||||
@ -181,11 +199,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
|||||||
mPasswordView.setText(settings.password);
|
mPasswordView.setText(settings.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
|
if (settings.clientCertificateAlias != null) {
|
||||||
|
mClientCertificateSpinner.setAlias(settings.clientCertificateAlias);
|
||||||
// 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);
|
|
||||||
|
|
||||||
mStoreType = settings.type;
|
mStoreType = settings.type;
|
||||||
if (Pop3Store.STORE_TYPE.equals(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());
|
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
|
* Updates the port when the user changes the security type. This allows
|
||||||
* us to show a reasonable default which the user can change.
|
* us to show a reasonable default which the user can change.
|
||||||
@ -278,6 +285,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
|||||||
@Override
|
@Override
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||||
long id) {
|
long id) {
|
||||||
|
// this indirectly triggers validateFields because the port text is watched
|
||||||
updatePortFromSecurityType();
|
updatePortFromSecurityType();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,12 +321,55 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
|||||||
outState.putString(EXTRA_ACCOUNT, mAccount.getUuid());
|
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() {
|
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
|
mNextButton
|
||||||
.setEnabled(Utility.requiredFieldValid(mUsernameView)
|
.setEnabled(Utility.requiredFieldValid(mUsernameView)
|
||||||
&& Utility.requiredFieldValid(mPasswordView)
|
&& (Utility.requiredFieldValid(mPasswordView)
|
||||||
|
|| (isAuthTypeExternal && mClientCertificateSpinner.getAlias() != null))
|
||||||
&& Utility.domainFieldValid(mServerView)
|
&& Utility.domainFieldValid(mServerView)
|
||||||
&& Utility.requiredFieldValid(mPortView));
|
&& Utility.requiredFieldValid(mPortView)
|
||||||
|
&& (!isAuthTypeExternal || hasConnectionSecurity));
|
||||||
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
|
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.
|
* password the user just set for incoming.
|
||||||
*/
|
*/
|
||||||
try {
|
try {
|
||||||
String usernameEnc = URLEncoder.encode(mUsernameView.getText().toString(), "UTF-8");
|
String username = mUsernameView.getText().toString();
|
||||||
String passwordEnc = URLEncoder.encode(mPasswordView.getText().toString(), "UTF-8");
|
|
||||||
|
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 oldUri = new URI(mAccount.getTransportUri());
|
||||||
URI uri = new URI(
|
ServerSettings transportServer = new ServerSettings(SmtpTransport.TRANSPORT_TYPE, oldUri.getHost(), oldUri.getPort(),
|
||||||
oldUri.getScheme(),
|
ConnectionSecurity.SSL_TLS_REQUIRED, authType, username, password, clientCertificateAlias);
|
||||||
usernameEnc + ":" + passwordEnc,
|
String transportUri = Transport.createTransportUri(transportServer);
|
||||||
oldUri.getHost(),
|
mAccount.setTransportUri(transportUri);
|
||||||
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);
|
|
||||||
} catch (URISyntaxException use) {
|
} catch (URISyntaxException use) {
|
||||||
/*
|
/*
|
||||||
* If we can't set up the URL we just continue. It's only for
|
* 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();
|
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
|
||||||
|
|
||||||
String username = mUsernameView.getText().toString();
|
String username = mUsernameView.getText().toString();
|
||||||
String password = mPasswordView.getText().toString();
|
String password = null;
|
||||||
|
String clientCertificateAlias = null;
|
||||||
|
|
||||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||||
|
if (authType.equals(AuthType.EXTERNAL)) {
|
||||||
|
clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||||
|
} else {
|
||||||
|
password = mPasswordView.getText().toString();
|
||||||
|
}
|
||||||
String host = mServerView.getText().toString();
|
String host = mServerView.getText().toString();
|
||||||
int port = Integer.parseInt(mPortView.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);
|
mAccount.deleteCertificate(host, port, CheckDirection.INCOMING);
|
||||||
ServerSettings settings = new ServerSettings(mStoreType, host, port,
|
ServerSettings settings = new ServerSettings(mStoreType, host, port,
|
||||||
connectionSecurity, authType, username, password, extra);
|
connectionSecurity, authType, username, password, clientCertificateAlias, extra);
|
||||||
|
|
||||||
mAccount.setStoreUri(Store.createStoreUri(settings));
|
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 toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
|
||||||
toast.show();
|
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.view.ViewGroup;
|
||||||
import android.widget.*;
|
import android.widget.*;
|
||||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||||
|
|
||||||
import com.fsck.k9.*;
|
import com.fsck.k9.*;
|
||||||
import com.fsck.k9.activity.K9Activity;
|
import com.fsck.k9.activity.K9Activity;
|
||||||
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
|
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.ServerSettings;
|
||||||
import com.fsck.k9.mail.Transport;
|
import com.fsck.k9.mail.Transport;
|
||||||
import com.fsck.k9.mail.transport.SmtpTransport;
|
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.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
@ -37,6 +41,9 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
|||||||
|
|
||||||
private EditText mUsernameView;
|
private EditText mUsernameView;
|
||||||
private EditText mPasswordView;
|
private EditText mPasswordView;
|
||||||
|
private ClientCertificateSpinner mClientCertificateSpinner;
|
||||||
|
private TextView mClientCertificateLabelView;
|
||||||
|
private TextView mPasswordLabelView;
|
||||||
private EditText mServerView;
|
private EditText mServerView;
|
||||||
private EditText mPortView;
|
private EditText mPortView;
|
||||||
private CheckBox mRequireLoginView;
|
private CheckBox mRequireLoginView;
|
||||||
@ -87,6 +94,9 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
|||||||
|
|
||||||
mUsernameView = (EditText)findViewById(R.id.account_username);
|
mUsernameView = (EditText)findViewById(R.id.account_username);
|
||||||
mPasswordView = (EditText)findViewById(R.id.account_password);
|
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);
|
mServerView = (EditText)findViewById(R.id.account_server);
|
||||||
mPortView = (EditText)findViewById(R.id.account_port);
|
mPortView = (EditText)findViewById(R.id.account_port);
|
||||||
mRequireLoginView = (CheckBox)findViewById(R.id.account_require_login);
|
mRequireLoginView = (CheckBox)findViewById(R.id.account_require_login);
|
||||||
@ -98,33 +108,27 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
|||||||
mNextButton.setOnClickListener(this);
|
mNextButton.setOnClickListener(this);
|
||||||
mRequireLoginView.setOnCheckedChangeListener(this);
|
mRequireLoginView.setOnCheckedChangeListener(this);
|
||||||
|
|
||||||
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(this,
|
mSecurityTypeView.setAdapter(ConnectionSecurity.getArrayAdapter(this));
|
||||||
android.R.layout.simple_spinner_item, ConnectionSecurity.values());
|
|
||||||
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
||||||
mSecurityTypeView.setAdapter(securityTypesAdapter);
|
|
||||||
|
|
||||||
mAuthTypeAdapter = AuthType.getArrayAdapter(this);
|
mAuthTypeAdapter = AuthType.getArrayAdapter(this, SslHelper.isClientCertificateSupportAvailable());
|
||||||
mAuthTypeView.setAdapter(mAuthTypeAdapter);
|
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);
|
mUsernameView.addTextChangedListener(validationTextWatcher);
|
||||||
mPasswordView.addTextChangedListener(validationTextWatcher);
|
mPasswordView.addTextChangedListener(validationTextWatcher);
|
||||||
mServerView.addTextChangedListener(validationTextWatcher);
|
mServerView.addTextChangedListener(validationTextWatcher);
|
||||||
mPortView.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.
|
* Only allow digits in the port field.
|
||||||
@ -147,17 +151,6 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
ServerSettings settings = Transport.decodeTransportUri(mAccount.getTransportUri());
|
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);
|
updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
|
||||||
|
|
||||||
@ -168,6 +161,19 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
|||||||
// Select currently configured security type
|
// Select currently configured security type
|
||||||
mSecurityTypeView.setSelection(settings.connectionSecurity.ordinal(), false);
|
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
|
* Updates the port when the user changes the security type. This allows
|
||||||
* us to show a reasonable default which the user can change.
|
* us to show a reasonable default which the user can change.
|
||||||
@ -181,6 +187,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
|||||||
@Override
|
@Override
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||||
long id) {
|
long id) {
|
||||||
|
// this indirectly triggers validateFields because the port text is watched
|
||||||
updatePortFromSecurityType();
|
updatePortFromSecurityType();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,14 +221,57 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
|||||||
outState.putString(EXTRA_ACCOUNT, mAccount.getUuid());
|
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() {
|
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
|
mNextButton
|
||||||
.setEnabled(
|
.setEnabled(Utility.domainFieldValid(mServerView)
|
||||||
Utility.domainFieldValid(mServerView) &&
|
&& Utility.requiredFieldValid(mPortView)
|
||||||
Utility.requiredFieldValid(mPortView) &&
|
&& (!mRequireLoginView.isChecked()
|
||||||
(!mRequireLoginView.isChecked() ||
|
|| (Utility.requiredFieldValid(mUsernameView) && Utility.requiredFieldValid(mPasswordView))
|
||||||
(Utility.requiredFieldValid(mUsernameView) &&
|
|| (Utility.requiredFieldValid(mUsernameView) && isAuthTypeExternal)
|
||||||
Utility.requiredFieldValid(mPasswordView))));
|
)
|
||||||
|
&& (!isAuthTypeExternal || hasConnectionSecurity)
|
||||||
|
);
|
||||||
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
|
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,17 +326,23 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
|||||||
String uri;
|
String uri;
|
||||||
String username = null;
|
String username = null;
|
||||||
String password = null;
|
String password = null;
|
||||||
|
String clientCertificateAlias = null;
|
||||||
AuthType authType = null;
|
AuthType authType = null;
|
||||||
if (mRequireLoginView.isChecked()) {
|
if (mRequireLoginView.isChecked()) {
|
||||||
username = mUsernameView.getText().toString();
|
username = mUsernameView.getText().toString();
|
||||||
password = mPasswordView.getText().toString();
|
|
||||||
authType = (AuthType) mAuthTypeView.getSelectedItem();
|
authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||||
|
if (AuthType.EXTERNAL.equals(authType)) {
|
||||||
|
clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||||
|
} else {
|
||||||
|
password = mPasswordView.getText().toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String newHost = mServerView.getText().toString();
|
String newHost = mServerView.getText().toString();
|
||||||
int newPort = Integer.parseInt(mPortView.getText().toString());
|
int newPort = Integer.parseInt(mPortView.getText().toString());
|
||||||
String type = SmtpTransport.TRANSPORT_TYPE;
|
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);
|
uri = Transport.createTransportUri(server);
|
||||||
mAccount.deleteCertificate(newHost, newPort, CheckDirection.OUTGOING);
|
mAccount.deleteCertificate(newHost, newPort, CheckDirection.OUTGOING);
|
||||||
mAccount.setTransportUri(uri);
|
mAccount.setTransportUri(uri);
|
||||||
@ -312,4 +368,27 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
|||||||
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
|
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
|
||||||
toast.show();
|
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;
|
package com.fsck.k9.fragment;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.DialogInterface.OnCancelListener;
|
import android.content.DialogInterface.OnCancelListener;
|
||||||
import android.content.DialogInterface.OnClickListener;
|
import android.content.DialogInterface.OnClickListener;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import com.actionbarsherlock.app.SherlockDialogFragment;
|
import com.actionbarsherlock.app.SherlockDialogFragment;
|
||||||
|
import com.fsck.k9.K9;
|
||||||
|
|
||||||
|
|
||||||
public class ConfirmationDialogFragment extends SherlockDialogFragment implements OnClickListener,
|
public class ConfirmationDialogFragment extends SherlockDialogFragment implements OnClickListener,
|
||||||
OnCancelListener {
|
OnCancelListener {
|
||||||
|
private ConfirmationDialogFragmentListener mListener;
|
||||||
|
|
||||||
private static final String ARG_DIALOG_ID = "dialog_id";
|
private static final String ARG_DIALOG_ID = "dialog_id";
|
||||||
private static final String ARG_TITLE = "title";
|
private static final String ARG_TITLE = "title";
|
||||||
@ -35,6 +39,11 @@ public class ConfirmationDialogFragment extends SherlockDialogFragment implement
|
|||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ConfirmationDialogFragment newInstance(int dialogId, String title, String message,
|
||||||
|
String cancelText) {
|
||||||
|
return newInstance(dialogId, title, message, null, cancelText);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public interface ConfirmationDialogFragmentListener {
|
public interface ConfirmationDialogFragmentListener {
|
||||||
void doPositiveClick(int dialogId);
|
void doPositiveClick(int dialogId);
|
||||||
@ -54,8 +63,14 @@ public class ConfirmationDialogFragment extends SherlockDialogFragment implement
|
|||||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||||
builder.setTitle(title);
|
builder.setTitle(title);
|
||||||
builder.setMessage(message);
|
builder.setMessage(message);
|
||||||
|
if (confirmText != null && cancelText != null) {
|
||||||
builder.setPositiveButton(confirmText, this);
|
builder.setPositiveButton(confirmText, this);
|
||||||
builder.setNegativeButton(cancelText, this);
|
builder.setNegativeButton(cancelText, this);
|
||||||
|
} else if (cancelText != null) {
|
||||||
|
builder.setNeutralButton(cancelText, this);
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Set at least cancelText!");
|
||||||
|
}
|
||||||
|
|
||||||
return builder.create();
|
return builder.create();
|
||||||
}
|
}
|
||||||
@ -71,6 +86,10 @@ public class ConfirmationDialogFragment extends SherlockDialogFragment implement
|
|||||||
getListener().doNegativeClick(getDialogId());
|
getListener().doNegativeClick(getDialogId());
|
||||||
break;
|
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);
|
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() {
|
private ConfirmationDialogFragmentListener getListener() {
|
||||||
|
if (mListener != null) {
|
||||||
|
return mListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to getTargetFragment...
|
||||||
try {
|
try {
|
||||||
return (ConfirmationDialogFragmentListener) getTargetFragment();
|
return (ConfirmationDialogFragmentListener) getTargetFragment();
|
||||||
} catch (ClassCastException e) {
|
} catch (ClassCastException e) {
|
||||||
|
@ -33,6 +33,8 @@ public enum AuthType {
|
|||||||
|
|
||||||
CRAM_MD5(R.string.account_setup_auth_type_encrypted_password),
|
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
|
* The following are obsolete authentication settings that were used with
|
||||||
* SMTP. They are no longer presented to the user as options, but they may
|
* SMTP. They are no longer presented to the user as options, but they may
|
||||||
@ -43,8 +45,13 @@ public enum AuthType {
|
|||||||
|
|
||||||
LOGIN(0);
|
LOGIN(0);
|
||||||
|
|
||||||
static public ArrayAdapter<AuthType> getArrayAdapter(Context context) {
|
static public ArrayAdapter<AuthType> getArrayAdapter(Context context, boolean includeExternal) {
|
||||||
AuthType[] authTypes = {PLAIN, CRAM_MD5};
|
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,
|
ArrayAdapter<AuthType> authTypesAdapter = new ArrayAdapter<AuthType>(context,
|
||||||
android.R.layout.simple_spinner_item, authTypes);
|
android.R.layout.simple_spinner_item, authTypes);
|
||||||
authTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
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;
|
package com.fsck.k9.mail;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
|
||||||
import com.fsck.k9.K9;
|
import com.fsck.k9.K9;
|
||||||
import com.fsck.k9.R;
|
import com.fsck.k9.R;
|
||||||
|
|
||||||
@ -8,6 +11,17 @@ public enum ConnectionSecurity {
|
|||||||
STARTTLS_REQUIRED(R.string.account_setup_incoming_security_tls_label),
|
STARTTLS_REQUIRED(R.string.account_setup_incoming_security_tls_label),
|
||||||
SSL_TLS_REQUIRED(R.string.account_setup_incoming_security_ssl_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 final int mResourceId;
|
||||||
|
|
||||||
private ConnectionSecurity(int id) {
|
private ConnectionSecurity(int id) {
|
||||||
|
@ -64,6 +64,14 @@ public class ServerSettings {
|
|||||||
*/
|
*/
|
||||||
public final String password;
|
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.
|
* Store- or transport-specific settings as key/value pair.
|
||||||
*
|
*
|
||||||
@ -89,10 +97,12 @@ public class ServerSettings {
|
|||||||
* see {@link ServerSettings#username}
|
* see {@link ServerSettings#username}
|
||||||
* @param password
|
* @param password
|
||||||
* see {@link ServerSettings#password}
|
* see {@link ServerSettings#password}
|
||||||
|
* @param clientCertificateAlias
|
||||||
|
* see {@link ServerSettings#clientCertificateAlias}
|
||||||
*/
|
*/
|
||||||
public ServerSettings(String type, String host, int port,
|
public ServerSettings(String type, String host, int port,
|
||||||
ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
|
ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
|
||||||
String password) {
|
String password, String clientCertificateAlias) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
@ -100,6 +110,7 @@ public class ServerSettings {
|
|||||||
this.authenticationType = authenticationType;
|
this.authenticationType = authenticationType;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
|
this.clientCertificateAlias = clientCertificateAlias;
|
||||||
this.extra = null;
|
this.extra = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,12 +131,14 @@ public class ServerSettings {
|
|||||||
* see {@link ServerSettings#username}
|
* see {@link ServerSettings#username}
|
||||||
* @param password
|
* @param password
|
||||||
* see {@link ServerSettings#password}
|
* see {@link ServerSettings#password}
|
||||||
|
* @param clientCertificateAlias
|
||||||
|
* see {@link ServerSettings#clientCertificateAlias}
|
||||||
* @param extra
|
* @param extra
|
||||||
* see {@link ServerSettings#extra}
|
* see {@link ServerSettings#extra}
|
||||||
*/
|
*/
|
||||||
public ServerSettings(String type, String host, int port,
|
public ServerSettings(String type, String host, int port,
|
||||||
ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
|
ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
|
||||||
String password, Map<String, String> extra) {
|
String password, String clientCertificateAlias, Map<String, String> extra) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
@ -133,6 +146,7 @@ public class ServerSettings {
|
|||||||
this.authenticationType = authenticationType;
|
this.authenticationType = authenticationType;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
|
this.clientCertificateAlias = clientCertificateAlias;
|
||||||
this.extra = (extra != null) ?
|
this.extra = (extra != null) ?
|
||||||
Collections.unmodifiableMap(new HashMap<String, String>(extra)) : null;
|
Collections.unmodifiableMap(new HashMap<String, String>(extra)) : null;
|
||||||
}
|
}
|
||||||
@ -153,6 +167,7 @@ public class ServerSettings {
|
|||||||
authenticationType = null;
|
authenticationType = null;
|
||||||
username = null;
|
username = null;
|
||||||
password = null;
|
password = null;
|
||||||
|
clientCertificateAlias = null;
|
||||||
extra = null;
|
extra = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +188,11 @@ public class ServerSettings {
|
|||||||
|
|
||||||
public ServerSettings newPassword(String newPassword) {
|
public ServerSettings newPassword(String newPassword) {
|
||||||
return new ServerSettings(type, host, port, connectionSecurity, authenticationType,
|
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.
|
* Get an instance of a remote mail store.
|
||||||
*/
|
*/
|
||||||
public synchronized static Store getRemoteInstance(Account account) throws MessagingException {
|
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();
|
String uri = account.getStoreUri();
|
||||||
|
|
||||||
if (uri.startsWith("local")) {
|
if (uri.startsWith("local")) {
|
||||||
@ -58,7 +68,7 @@ public abstract class Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Store store = sStores.get(uri);
|
Store store = sStores.get(uri);
|
||||||
if (store == null) {
|
if (store == null || reload) {
|
||||||
if (uri.startsWith("imap")) {
|
if (uri.startsWith("imap")) {
|
||||||
store = new ImapStore(account);
|
store = new ImapStore(account);
|
||||||
} else if (uri.startsWith("pop3")) {
|
} else if (uri.startsWith("pop3")) {
|
||||||
|
@ -25,7 +25,6 @@ import java.nio.charset.Charset;
|
|||||||
import java.nio.charset.CharsetDecoder;
|
import java.nio.charset.CharsetDecoder;
|
||||||
import java.nio.charset.CodingErrorAction;
|
import java.nio.charset.CodingErrorAction;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
@ -50,9 +49,7 @@ import java.util.regex.Pattern;
|
|||||||
import java.util.zip.Inflater;
|
import java.util.zip.Inflater;
|
||||||
import java.util.zip.InflaterInputStream;
|
import java.util.zip.InflaterInputStream;
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import javax.net.ssl.SSLException;
|
import javax.net.ssl.SSLException;
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
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.ImapResponseParser.ImapResponse;
|
||||||
import com.fsck.k9.mail.store.imap.ImapUtility;
|
import com.fsck.k9.mail.store.imap.ImapUtility;
|
||||||
import com.fsck.k9.mail.transport.imap.ImapSettings;
|
import com.fsck.k9.mail.transport.imap.ImapSettings;
|
||||||
import com.fsck.k9.net.ssl.TrustManagerFactory;
|
import com.fsck.k9.net.ssl.SslHelper;
|
||||||
import com.fsck.k9.net.ssl.TrustedSocketFactory;
|
|
||||||
import com.jcraft.jzlib.JZlib;
|
import com.jcraft.jzlib.JZlib;
|
||||||
import com.jcraft.jzlib.ZOutputStream;
|
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_IDLE = "IDLE";
|
||||||
private static final String CAPABILITY_AUTH_CRAM_MD5 = "AUTH=CRAM-MD5";
|
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_PLAIN = "AUTH=PLAIN";
|
||||||
|
private static final String CAPABILITY_AUTH_EXTERNAL = "AUTH=EXTERNAL";
|
||||||
private static final String CAPABILITY_LOGINDISABLED = "LOGINDISABLED";
|
private static final String CAPABILITY_LOGINDISABLED = "LOGINDISABLED";
|
||||||
private static final String COMMAND_IDLE = "IDLE";
|
private static final String COMMAND_IDLE = "IDLE";
|
||||||
private static final String CAPABILITY_NAMESPACE = "NAMESPACE";
|
private static final String CAPABILITY_NAMESPACE = "NAMESPACE";
|
||||||
@ -158,6 +155,7 @@ public class ImapStore extends Store {
|
|||||||
AuthType authenticationType = null;
|
AuthType authenticationType = null;
|
||||||
String username = null;
|
String username = null;
|
||||||
String password = null;
|
String password = null;
|
||||||
|
String clientCertificateAlias = null;
|
||||||
String pathPrefix = null;
|
String pathPrefix = null;
|
||||||
boolean autoDetectNamespace = true;
|
boolean autoDetectNamespace = true;
|
||||||
|
|
||||||
@ -213,11 +211,16 @@ public class ImapStore extends Store {
|
|||||||
authenticationType = AuthType.PLAIN;
|
authenticationType = AuthType.PLAIN;
|
||||||
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
|
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
|
||||||
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
||||||
} else {
|
} else if (userInfoParts.length == 3) {
|
||||||
authenticationType = AuthType.valueOf(userInfoParts[0]);
|
authenticationType = AuthType.valueOf(userInfoParts[0]);
|
||||||
username = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
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");
|
password = URLDecoder.decode(userInfoParts[2], "UTF-8");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (UnsupportedEncodingException enc) {
|
} catch (UnsupportedEncodingException enc) {
|
||||||
// This shouldn't happen since the encoding is hardcoded to UTF-8
|
// This shouldn't happen since the encoding is hardcoded to UTF-8
|
||||||
throw new IllegalArgumentException("Couldn't urldecode username or password.", enc);
|
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,
|
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) {
|
public static String createUri(ServerSettings server) {
|
||||||
String userEnc;
|
String userEnc;
|
||||||
String passwordEnc;
|
String passwordEnc;
|
||||||
|
String clientCertificateAliasEnc;
|
||||||
try {
|
try {
|
||||||
userEnc = URLEncoder.encode(server.username, "UTF-8");
|
userEnc = URLEncoder.encode(server.username, "UTF-8");
|
||||||
passwordEnc = (server.password != null) ?
|
passwordEnc = (server.password != null) ?
|
||||||
URLEncoder.encode(server.password, "UTF-8") : "";
|
URLEncoder.encode(server.password, "UTF-8") : "";
|
||||||
|
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
|
||||||
|
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
|
||||||
}
|
}
|
||||||
catch (UnsupportedEncodingException e) {
|
catch (UnsupportedEncodingException e) {
|
||||||
throw new IllegalArgumentException("Could not encode username or password", e);
|
throw new IllegalArgumentException("Could not encode username or password", e);
|
||||||
@ -284,8 +290,12 @@ public class ImapStore extends Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AuthType authType = server.authenticationType;
|
AuthType authType = server.authenticationType;
|
||||||
|
String userInfo;
|
||||||
String userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
|
if (authType.equals(AuthType.EXTERNAL)) {
|
||||||
|
userInfo = authType.name() + ":" + userEnc + ":" + clientCertificateAliasEnc;
|
||||||
|
} else {
|
||||||
|
userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
Map<String, String> extra = server.getExtra();
|
Map<String, String> extra = server.getExtra();
|
||||||
String path = null;
|
String path = null;
|
||||||
@ -320,10 +330,10 @@ public class ImapStore extends Store {
|
|||||||
public final String pathPrefix;
|
public final String pathPrefix;
|
||||||
|
|
||||||
protected ImapStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
|
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) {
|
boolean autodetectNamespace, String pathPrefix) {
|
||||||
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
|
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
|
||||||
password);
|
password, clientCertificateAlias);
|
||||||
this.autoDetectNamespace = autodetectNamespace;
|
this.autoDetectNamespace = autodetectNamespace;
|
||||||
this.pathPrefix = pathPrefix;
|
this.pathPrefix = pathPrefix;
|
||||||
}
|
}
|
||||||
@ -339,7 +349,7 @@ public class ImapStore extends Store {
|
|||||||
@Override
|
@Override
|
||||||
public ServerSettings newPassword(String newPassword) {
|
public ServerSettings newPassword(String newPassword) {
|
||||||
return new ImapStoreSettings(host, port, connectionSecurity, authenticationType,
|
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 int mPort;
|
||||||
private String mUsername;
|
private String mUsername;
|
||||||
private String mPassword;
|
private String mPassword;
|
||||||
|
private String mClientCertificateAlias;
|
||||||
private ConnectionSecurity mConnectionSecurity;
|
private ConnectionSecurity mConnectionSecurity;
|
||||||
private AuthType mAuthType;
|
private AuthType mAuthType;
|
||||||
private volatile String mPathPrefix;
|
private volatile String mPathPrefix;
|
||||||
@ -386,6 +397,11 @@ public class ImapStore extends Store {
|
|||||||
return mPassword;
|
return mPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientCertificateAlias() {
|
||||||
|
return mClientCertificateAlias;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean useCompression(final int type) {
|
public boolean useCompression(final int type) {
|
||||||
return mAccount.useCompression(type);
|
return mAccount.useCompression(type);
|
||||||
@ -458,6 +474,7 @@ public class ImapStore extends Store {
|
|||||||
mAuthType = settings.authenticationType;
|
mAuthType = settings.authenticationType;
|
||||||
mUsername = settings.username;
|
mUsername = settings.username;
|
||||||
mPassword = settings.password;
|
mPassword = settings.password;
|
||||||
|
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||||
|
|
||||||
// Make extra sure mPathPrefix is null if "auto-detect namespace" is configured
|
// Make extra sure mPathPrefix is null if "auto-detect namespace" is configured
|
||||||
mPathPrefix = (settings.autoDetectNamespace) ? null : settings.pathPrefix;
|
mPathPrefix = (settings.autoDetectNamespace) ? null : settings.pathPrefix;
|
||||||
@ -2419,14 +2436,8 @@ public class ImapStore extends Store {
|
|||||||
mSettings.getPort());
|
mSettings.getPort());
|
||||||
|
|
||||||
if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
|
if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
mSocket = SslHelper.createSslSocket(mSettings.getHost(),
|
||||||
sslContext
|
mSettings.getPort(), mSettings.getClientCertificateAlias());
|
||||||
.init(null,
|
|
||||||
new TrustManager[] { TrustManagerFactory.get(
|
|
||||||
mSettings.getHost(),
|
|
||||||
mSettings.getPort()) },
|
|
||||||
new SecureRandom());
|
|
||||||
mSocket = TrustedSocketFactory.createSocket(sslContext);
|
|
||||||
} else {
|
} else {
|
||||||
mSocket = new Socket();
|
mSocket = new Socket();
|
||||||
}
|
}
|
||||||
@ -2475,14 +2486,9 @@ public class ImapStore extends Store {
|
|||||||
// STARTTLS
|
// STARTTLS
|
||||||
executeSimpleCommand("STARTTLS");
|
executeSimpleCommand("STARTTLS");
|
||||||
|
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
mSocket = SslHelper.createStartTlsSocket(mSocket,
|
||||||
sslContext.init(null,
|
mSettings.getHost(), mSettings.getPort(), true,
|
||||||
new TrustManager[] { TrustManagerFactory.get(
|
mSettings.getClientCertificateAlias());
|
||||||
mSettings.getHost(),
|
|
||||||
mSettings.getPort()) },
|
|
||||||
new SecureRandom());
|
|
||||||
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket,
|
|
||||||
mSettings.getHost(), mSettings.getPort(), true);
|
|
||||||
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
|
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
|
||||||
mIn = new PeekableInputStream(new BufferedInputStream(mSocket
|
mIn = new PeekableInputStream(new BufferedInputStream(mSocket
|
||||||
.getInputStream(), 1024));
|
.getInputStream(), 1024));
|
||||||
@ -2531,6 +2537,17 @@ public class ImapStore extends Store {
|
|||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
throw new MessagingException(
|
throw new MessagingException(
|
||||||
"Unhandled authentication method found in the server settings (bug).");
|
"Unhandled authentication method found in the server settings (bug).");
|
||||||
@ -2630,7 +2647,6 @@ public class ImapStore extends Store {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (SSLException e) {
|
} catch (SSLException e) {
|
||||||
throw new CertificateValidationException(e.getMessage(), e);
|
throw new CertificateValidationException(e.getMessage(), e);
|
||||||
} catch (GeneralSecurityException gse) {
|
} catch (GeneralSecurityException gse) {
|
||||||
|
@ -8,23 +8,18 @@ import com.fsck.k9.K9;
|
|||||||
import com.fsck.k9.controller.MessageRetrievalListener;
|
import com.fsck.k9.controller.MessageRetrievalListener;
|
||||||
import com.fsck.k9.helper.Utility;
|
import com.fsck.k9.helper.Utility;
|
||||||
import com.fsck.k9.mail.*;
|
import com.fsck.k9.mail.*;
|
||||||
|
|
||||||
import com.fsck.k9.mail.filter.Base64;
|
import com.fsck.k9.mail.filter.Base64;
|
||||||
import com.fsck.k9.mail.filter.Hex;
|
import com.fsck.k9.mail.filter.Hex;
|
||||||
import com.fsck.k9.mail.internet.MimeMessage;
|
import com.fsck.k9.mail.internet.MimeMessage;
|
||||||
import com.fsck.k9.net.ssl.TrustManagerFactory;
|
import com.fsck.k9.net.ssl.SslHelper;
|
||||||
import com.fsck.k9.net.ssl.TrustedSocketFactory;
|
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import javax.net.ssl.SSLException;
|
import javax.net.ssl.SSLException;
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.*;
|
import java.net.*;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -59,15 +54,16 @@ public class Pop3Store extends Store {
|
|||||||
private static final String SASL_CAPABILITY = "SASL";
|
private static final String SASL_CAPABILITY = "SASL";
|
||||||
private static final String AUTH_PLAIN_CAPABILITY = "PLAIN";
|
private static final String AUTH_PLAIN_CAPABILITY = "PLAIN";
|
||||||
private static final String AUTH_CRAM_MD5_CAPABILITY = "CRAM-MD5";
|
private static final String AUTH_CRAM_MD5_CAPABILITY = "CRAM-MD5";
|
||||||
|
private static final String AUTH_EXTERNAL_CAPABILITY = "EXTERNAL";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes a Pop3Store URI.
|
* Decodes a Pop3Store URI.
|
||||||
*
|
*
|
||||||
* <p>Possible forms:</p>
|
* <p>Possible forms:</p>
|
||||||
* <pre>
|
* <pre>
|
||||||
* pop3://user:password@server:port ConnectionSecurity.NONE
|
* pop3://auth:user:password@server:port ConnectionSecurity.NONE
|
||||||
* pop3+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
|
* pop3+tls+://auth:user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
|
||||||
* pop3+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
* pop3+ssl+://auth:user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public static ServerSettings decodeUri(String uri) {
|
public static ServerSettings decodeUri(String uri) {
|
||||||
@ -76,6 +72,7 @@ public class Pop3Store extends Store {
|
|||||||
ConnectionSecurity connectionSecurity;
|
ConnectionSecurity connectionSecurity;
|
||||||
String username = null;
|
String username = null;
|
||||||
String password = null;
|
String password = null;
|
||||||
|
String clientCertificateAlias = null;
|
||||||
|
|
||||||
URI pop3Uri;
|
URI pop3Uri;
|
||||||
try {
|
try {
|
||||||
@ -131,8 +128,12 @@ public class Pop3Store extends Store {
|
|||||||
}
|
}
|
||||||
username = URLDecoder.decode(userInfoParts[userIndex], "UTF-8");
|
username = URLDecoder.decode(userInfoParts[userIndex], "UTF-8");
|
||||||
if (userInfoParts.length > passwordIndex) {
|
if (userInfoParts.length > passwordIndex) {
|
||||||
|
if (authType.equals(AuthType.EXTERNAL)) {
|
||||||
|
clientCertificateAlias = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
|
||||||
|
} else {
|
||||||
password = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
|
password = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (UnsupportedEncodingException enc) {
|
} catch (UnsupportedEncodingException enc) {
|
||||||
// This shouldn't happen since the encoding is hardcoded to UTF-8
|
// This shouldn't happen since the encoding is hardcoded to UTF-8
|
||||||
throw new IllegalArgumentException("Couldn't urldecode username or password.", enc);
|
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,
|
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) {
|
public static String createUri(ServerSettings server) {
|
||||||
String userEnc;
|
String userEnc;
|
||||||
String passwordEnc;
|
String passwordEnc;
|
||||||
|
String clientCertificateAliasEnc;
|
||||||
try {
|
try {
|
||||||
userEnc = URLEncoder.encode(server.username, "UTF-8");
|
userEnc = URLEncoder.encode(server.username, "UTF-8");
|
||||||
passwordEnc = (server.password != null) ?
|
passwordEnc = (server.password != null) ?
|
||||||
URLEncoder.encode(server.password, "UTF-8") : "";
|
URLEncoder.encode(server.password, "UTF-8") : "";
|
||||||
|
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
|
||||||
|
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
|
||||||
}
|
}
|
||||||
catch (UnsupportedEncodingException e) {
|
catch (UnsupportedEncodingException e) {
|
||||||
throw new IllegalArgumentException("Could not encode username or password", e);
|
throw new IllegalArgumentException("Could not encode username or password", e);
|
||||||
@ -180,7 +184,14 @@ public class Pop3Store extends Store {
|
|||||||
break;
|
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 {
|
try {
|
||||||
return new URI(scheme, userInfo, server.host, server.port, null, null,
|
return new URI(scheme, userInfo, server.host, server.port, null, null,
|
||||||
null).toString();
|
null).toString();
|
||||||
@ -194,6 +205,7 @@ public class Pop3Store extends Store {
|
|||||||
private int mPort;
|
private int mPort;
|
||||||
private String mUsername;
|
private String mUsername;
|
||||||
private String mPassword;
|
private String mPassword;
|
||||||
|
private String mClientCertificateAlias;
|
||||||
private AuthType mAuthType;
|
private AuthType mAuthType;
|
||||||
private ConnectionSecurity mConnectionSecurity;
|
private ConnectionSecurity mConnectionSecurity;
|
||||||
private HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
|
private HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
|
||||||
@ -224,6 +236,7 @@ public class Pop3Store extends Store {
|
|||||||
|
|
||||||
mUsername = settings.username;
|
mUsername = settings.username;
|
||||||
mPassword = settings.password;
|
mPassword = settings.password;
|
||||||
|
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||||
mAuthType = settings.authenticationType;
|
mAuthType = settings.authenticationType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,11 +314,7 @@ public class Pop3Store extends Store {
|
|||||||
try {
|
try {
|
||||||
SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
|
SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
|
||||||
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
|
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
|
||||||
sslContext.init(null,
|
|
||||||
new TrustManager[] { TrustManagerFactory.get(mHost,
|
|
||||||
mPort) }, new SecureRandom());
|
|
||||||
mSocket = TrustedSocketFactory.createSocket(sslContext);
|
|
||||||
} else {
|
} else {
|
||||||
mSocket = new Socket();
|
mSocket = new Socket();
|
||||||
}
|
}
|
||||||
@ -327,13 +336,8 @@ public class Pop3Store extends Store {
|
|||||||
if (mCapabilities.stls) {
|
if (mCapabilities.stls) {
|
||||||
executeSimpleCommand(STLS_COMMAND);
|
executeSimpleCommand(STLS_COMMAND);
|
||||||
|
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
|
||||||
sslContext.init(null,
|
mClientCertificateAlias);
|
||||||
new TrustManager[] { TrustManagerFactory.get(
|
|
||||||
mHost, mPort) },
|
|
||||||
new SecureRandom());
|
|
||||||
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
|
|
||||||
mPort, true);
|
|
||||||
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
|
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
|
||||||
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
|
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
|
||||||
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
|
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
|
||||||
@ -372,6 +376,17 @@ public class Pop3Store extends Store {
|
|||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
throw new MessagingException(
|
throw new MessagingException(
|
||||||
"Unhandled authentication method found in the server settings (bug).");
|
"Unhandled authentication method found in the server settings (bug).");
|
||||||
@ -1046,6 +1061,8 @@ public class Pop3Store extends Store {
|
|||||||
capabilities.authPlain = true;
|
capabilities.authPlain = true;
|
||||||
} else if (response.equals(AUTH_CRAM_MD5_CAPABILITY)) {
|
} else if (response.equals(AUTH_CRAM_MD5_CAPABILITY)) {
|
||||||
capabilities.cramMD5 = true;
|
capabilities.cramMD5 = true;
|
||||||
|
} else if (response.equals(AUTH_EXTERNAL_CAPABILITY)) {
|
||||||
|
capabilities.external = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (MessagingException e) {
|
} catch (MessagingException e) {
|
||||||
@ -1193,15 +1210,17 @@ public class Pop3Store extends Store {
|
|||||||
public boolean stls;
|
public boolean stls;
|
||||||
public boolean top;
|
public boolean top;
|
||||||
public boolean uidl;
|
public boolean uidl;
|
||||||
|
public boolean external;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
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,
|
cramMD5,
|
||||||
authPlain,
|
authPlain,
|
||||||
stls,
|
stls,
|
||||||
top,
|
top,
|
||||||
uidl);
|
uidl,
|
||||||
|
external);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,9 +7,9 @@ import com.fsck.k9.K9;
|
|||||||
import com.fsck.k9.controller.MessageRetrievalListener;
|
import com.fsck.k9.controller.MessageRetrievalListener;
|
||||||
import com.fsck.k9.helper.Utility;
|
import com.fsck.k9.helper.Utility;
|
||||||
import com.fsck.k9.mail.*;
|
import com.fsck.k9.mail.*;
|
||||||
|
|
||||||
import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
|
import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
|
||||||
import com.fsck.k9.mail.internet.MimeMessage;
|
import com.fsck.k9.mail.internet.MimeMessage;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.http.*;
|
import org.apache.http.*;
|
||||||
import org.apache.http.client.CookieStore;
|
import org.apache.http.client.CookieStore;
|
||||||
@ -36,6 +36,7 @@ import javax.net.ssl.SSLException;
|
|||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
import javax.xml.parsers.SAXParser;
|
import javax.xml.parsers.SAXParser;
|
||||||
import javax.xml.parsers.SAXParserFactory;
|
import javax.xml.parsers.SAXParserFactory;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
@ -178,7 +179,7 @@ public class WebDavStore extends Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new WebDavStoreSettings(host, port, connectionSecurity, null, username, password,
|
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;
|
public final String mailboxPath;
|
||||||
|
|
||||||
protected WebDavStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
|
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) {
|
String path, String authPath, String mailboxPath) {
|
||||||
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
|
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
|
||||||
password);
|
password, clientCertificateAlias);
|
||||||
this.alias = alias;
|
this.alias = alias;
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.authPath = authPath;
|
this.authPath = authPath;
|
||||||
@ -280,7 +281,7 @@ public class WebDavStore extends Store {
|
|||||||
@Override
|
@Override
|
||||||
public ServerSettings newPassword(String newPassword) {
|
public ServerSettings newPassword(String newPassword) {
|
||||||
return new WebDavStoreSettings(host, port, connectionSecurity, authenticationType,
|
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 mUsername; /* Stores the username for authentications */
|
||||||
private String mAlias; /* Stores the alias for the user's mailbox */
|
private String mAlias; /* Stores the alias for the user's mailbox */
|
||||||
private String mPassword; /* Stores the password for authentications */
|
private String mPassword; /* Stores the password for authentications */
|
||||||
|
private String mClientCertificateAlias;
|
||||||
private String mUrl; /* Stores the base URL for the server */
|
private String mUrl; /* Stores the base URL for the server */
|
||||||
private String mHost; /* Stores the host name for the server */
|
private String mHost; /* Stores the host name for the server */
|
||||||
private int mPort;
|
private int mPort;
|
||||||
@ -324,6 +326,7 @@ public class WebDavStore extends Store {
|
|||||||
|
|
||||||
mUsername = settings.username;
|
mUsername = settings.username;
|
||||||
mPassword = settings.password;
|
mPassword = settings.password;
|
||||||
|
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||||
mAlias = settings.alias;
|
mAlias = settings.alias;
|
||||||
|
|
||||||
mPath = settings.path;
|
mPath = settings.path;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
package com.fsck.k9.mail.transport;
|
package com.fsck.k9.mail.transport;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
import com.fsck.k9.Account;
|
||||||
import com.fsck.k9.K9;
|
import com.fsck.k9.K9;
|
||||||
import com.fsck.k9.mail.*;
|
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.filter.SmtpDataStuffing;
|
||||||
import com.fsck.k9.mail.internet.MimeUtility;
|
import com.fsck.k9.mail.internet.MimeUtility;
|
||||||
import com.fsck.k9.mail.store.LocalStore.LocalMessage;
|
import com.fsck.k9.mail.store.LocalStore.LocalMessage;
|
||||||
import com.fsck.k9.net.ssl.TrustManagerFactory;
|
import com.fsck.k9.net.ssl.SslHelper;
|
||||||
import com.fsck.k9.net.ssl.TrustedSocketFactory;
|
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import javax.net.ssl.SSLException;
|
import javax.net.ssl.SSLException;
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
@ -27,9 +25,7 @@ import java.io.OutputStream;
|
|||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.*;
|
import java.net.*;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public class SmtpTransport extends Transport {
|
public class SmtpTransport extends Transport {
|
||||||
@ -38,20 +34,23 @@ public class SmtpTransport extends Transport {
|
|||||||
/**
|
/**
|
||||||
* Decodes a SmtpTransport URI.
|
* Decodes a SmtpTransport URI.
|
||||||
*
|
*
|
||||||
|
* NOTE: In contrast to ImapStore and Pop3Store, the authType is appended at the end!
|
||||||
|
*
|
||||||
* <p>Possible forms:</p>
|
* <p>Possible forms:</p>
|
||||||
* <pre>
|
* <pre>
|
||||||
* smtp://user:password@server:port ConnectionSecurity.NONE
|
* smtp://user:password:auth@server:port ConnectionSecurity.NONE
|
||||||
* smtp+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
|
* smtp+tls+://user:password:auth@server:port ConnectionSecurity.STARTTLS_REQUIRED
|
||||||
* smtp+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
* smtp+ssl+://user:password:auth@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public static ServerSettings decodeUri(String uri) {
|
public static ServerSettings decodeUri(String uri) {
|
||||||
String host;
|
String host;
|
||||||
int port;
|
int port;
|
||||||
ConnectionSecurity connectionSecurity;
|
ConnectionSecurity connectionSecurity;
|
||||||
AuthType authType = AuthType.PLAIN;
|
AuthType authType = null;
|
||||||
String username = null;
|
String username = null;
|
||||||
String password = null;
|
String password = null;
|
||||||
|
String clientCertificateAlias = null;
|
||||||
|
|
||||||
URI smtpUri;
|
URI smtpUri;
|
||||||
try {
|
try {
|
||||||
@ -95,14 +94,22 @@ public class SmtpTransport extends Transport {
|
|||||||
if (smtpUri.getUserInfo() != null) {
|
if (smtpUri.getUserInfo() != null) {
|
||||||
try {
|
try {
|
||||||
String[] userInfoParts = smtpUri.getUserInfo().split(":");
|
String[] userInfoParts = smtpUri.getUserInfo().split(":");
|
||||||
if (userInfoParts.length > 0) {
|
if (userInfoParts.length == 1) {
|
||||||
|
authType = AuthType.PLAIN;
|
||||||
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
|
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
|
||||||
}
|
} else if (userInfoParts.length == 2) {
|
||||||
if (userInfoParts.length > 1) {
|
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");
|
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
||||||
}
|
}
|
||||||
if (userInfoParts.length > 2) {
|
|
||||||
authType = AuthType.valueOf(userInfoParts[2]);
|
|
||||||
}
|
}
|
||||||
} catch (UnsupportedEncodingException enc) {
|
} catch (UnsupportedEncodingException enc) {
|
||||||
// This shouldn't happen since the encoding is hardcoded to UTF-8
|
// 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,
|
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) {
|
public static String createUri(ServerSettings server) {
|
||||||
String userEnc;
|
String userEnc;
|
||||||
String passwordEnc;
|
String passwordEnc;
|
||||||
|
String clientCertificateAliasEnc;
|
||||||
try {
|
try {
|
||||||
userEnc = (server.username != null) ?
|
userEnc = (server.username != null) ?
|
||||||
URLEncoder.encode(server.username, "UTF-8") : "";
|
URLEncoder.encode(server.username, "UTF-8") : "";
|
||||||
passwordEnc = (server.password != null) ?
|
passwordEnc = (server.password != null) ?
|
||||||
URLEncoder.encode(server.password, "UTF-8") : "";
|
URLEncoder.encode(server.password, "UTF-8") : "";
|
||||||
|
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
|
||||||
|
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
|
||||||
}
|
}
|
||||||
catch (UnsupportedEncodingException e) {
|
catch (UnsupportedEncodingException e) {
|
||||||
throw new IllegalArgumentException("Could not encode username or password", e);
|
throw new IllegalArgumentException("Could not encode username or password", e);
|
||||||
@ -152,10 +162,17 @@ public class SmtpTransport extends Transport {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
String userInfo = userEnc + ":" + passwordEnc;
|
String userInfo = null;
|
||||||
AuthType authType = server.authenticationType;
|
AuthType authType = server.authenticationType;
|
||||||
|
// NOTE: authType is append at last item, in contrast to ImapStore and Pop3Store!
|
||||||
if (authType != null) {
|
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 {
|
try {
|
||||||
return new URI(scheme, userInfo, server.host, server.port, null, null,
|
return new URI(scheme, userInfo, server.host, server.port, null, null,
|
||||||
@ -170,6 +187,7 @@ public class SmtpTransport extends Transport {
|
|||||||
int mPort;
|
int mPort;
|
||||||
String mUsername;
|
String mUsername;
|
||||||
String mPassword;
|
String mPassword;
|
||||||
|
String mClientCertificateAlias;
|
||||||
AuthType mAuthType;
|
AuthType mAuthType;
|
||||||
ConnectionSecurity mConnectionSecurity;
|
ConnectionSecurity mConnectionSecurity;
|
||||||
Socket mSocket;
|
Socket mSocket;
|
||||||
@ -194,6 +212,7 @@ public class SmtpTransport extends Transport {
|
|||||||
mAuthType = settings.authenticationType;
|
mAuthType = settings.authenticationType;
|
||||||
mUsername = settings.username;
|
mUsername = settings.username;
|
||||||
mPassword = settings.password;
|
mPassword = settings.password;
|
||||||
|
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -205,12 +224,7 @@ public class SmtpTransport extends Transport {
|
|||||||
try {
|
try {
|
||||||
SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort);
|
SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort);
|
||||||
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
|
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
|
||||||
sslContext.init(null,
|
|
||||||
new TrustManager[] { TrustManagerFactory.get(
|
|
||||||
mHost, mPort) },
|
|
||||||
new SecureRandom());
|
|
||||||
mSocket = TrustedSocketFactory.createSocket(sslContext);
|
|
||||||
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
|
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
|
||||||
secureConnection = true;
|
secureConnection = true;
|
||||||
} else {
|
} else {
|
||||||
@ -264,12 +278,9 @@ public class SmtpTransport extends Transport {
|
|||||||
if (extensions.containsKey("STARTTLS")) {
|
if (extensions.containsKey("STARTTLS")) {
|
||||||
executeSimpleCommand("STARTTLS");
|
executeSimpleCommand("STARTTLS");
|
||||||
|
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
|
||||||
sslContext.init(null,
|
mClientCertificateAlias);
|
||||||
new TrustManager[] { TrustManagerFactory.get(mHost,
|
|
||||||
mPort) }, new SecureRandom());
|
|
||||||
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
|
|
||||||
mPort, true);
|
|
||||||
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
|
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
|
||||||
1024));
|
1024));
|
||||||
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
|
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
|
||||||
|
@ -21,6 +21,8 @@ public interface ImapSettings {
|
|||||||
|
|
||||||
String getPassword();
|
String getPassword();
|
||||||
|
|
||||||
|
String getClientCertificateAlias();
|
||||||
|
|
||||||
boolean useCompression(int type);
|
boolean useCompression(int type);
|
||||||
|
|
||||||
String getPathPrefix();
|
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) {
|
public ImportedServerSettings(ImportedServer server) {
|
||||||
super(server.type, server.host, convertPort(server.port),
|
super(server.type, server.host, convertPort(server.port),
|
||||||
convertConnectionSecurity(server.connectionSecurity),
|
convertConnectionSecurity(server.connectionSecurity),
|
||||||
server.authenticationType, server.username, server.password);
|
server.authenticationType, server.username, server.password,
|
||||||
|
server.clientCertificateAlias);
|
||||||
mImportedServer = server;
|
mImportedServer = server;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1155,6 +1156,7 @@ public class SettingsImporter {
|
|||||||
public AuthType authenticationType;
|
public AuthType authenticationType;
|
||||||
public String username;
|
public String username;
|
||||||
public String password;
|
public String password;
|
||||||
|
public String clientCertificateAlias;
|
||||||
public ImportedSettings extras;
|
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