Merge pull request #474 from k9mail/tls-client-cert-auth

Client Certificate Authentication
This commit is contained in:
cketti 2014-08-30 01:06:28 +02:00
commit 759fa77c9a
51 changed files with 2347 additions and 621 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

View file

@ -1,51 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
>
android:layout_width="fill_parent" >
<ScrollView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="6dip"
android:fadingEdge="none"
android:scrollbarStyle="outsideInset">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center_horizontal|center_vertical"
android:orientation="vertical">
<EditText
android:id="@+id/account_email"
android:hint="@string/account_setup_basics_email_hint"
android:singleLine="true"
android:inputType="textEmailAddress"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
/>
<EditText
android:id="@+id/account_password"
android:inputType="textPassword"
android:hint="@string/account_setup_basics_password_hint"
android:singleLine="true"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:nextFocusDown="@+id/next"
/>
<CheckBox
android:id="@+id/show_password"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:text="@string/show_password"
/>
<View
android:layout_width="fill_parent"
android:layout_height="0dip"
<ScrollView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
/>
</LinearLayout>
android:padding="6dip"
android:fadingEdge="none"
android:scrollbarStyle="outsideInset" >
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center_horizontal|center_vertical"
android:orientation="vertical" >
<EditText
android:id="@+id/account_email"
android:hint="@string/account_setup_basics_email_hint"
android:singleLine="true"
android:inputType="textEmailAddress"
android:layout_height="wrap_content"
android:layout_width="fill_parent" />
<EditText
android:id="@+id/account_password"
android:inputType="textPassword"
android:hint="@string/account_setup_basics_password_hint"
android:singleLine="true"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:nextFocusDown="@+id/next" />
<CheckBox
android:id="@+id/show_password"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:text="@string/account_setup_basics_show_password" />
<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" />
<com.fsck.k9.view.FoldableLinearLayout
android:id="@+id/foldable_advanced_options"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
custom:foldedLabel="@string/client_certificate_advanced_options"
custom:unFoldedLabel="@string/client_certificate_advanced_options" >
<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" />
</com.fsck.k9.view.FoldableLinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1" />
</LinearLayout>
</ScrollView>
<include layout="@layout/wizard_setup"/>
</LinearLayout>
<include layout="@layout/wizard_setup" />
</LinearLayout>

View file

@ -16,31 +16,6 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<TextView
android:text="@string/account_setup_incoming_username_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<EditText
android:id="@+id/account_username"
android:singleLine="true"
android:inputType="textEmailAddress"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_incoming_username_label" />
<TextView
android:text="@string/account_setup_incoming_password_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<EditText
android:id="@+id/account_password"
android:inputType="textPassword"
android:singleLine="true"
android:layout_height="wrap_content"
android:layout_width="fill_parent" />
<!-- This text may be changed in code if the server is IMAP, etc. -->
<TextView
android:id="@+id/account_server_label"
@ -67,18 +42,6 @@
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_incoming_security_label" />
<TextView
android:id="@+id/account_auth_type_label"
android:text="@string/account_setup_incoming_auth_type_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<Spinner
android:id="@+id/account_auth_type"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_incoming_auth_type_label" />
<TextView
android:text="@string/account_setup_incoming_port_label"
android:layout_height="wrap_content"
@ -92,6 +55,57 @@
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_incoming_port_label" />
<TextView
android:text="@string/account_setup_incoming_username_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<EditText
android:id="@+id/account_username"
android:singleLine="true"
android:inputType="textEmailAddress"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_incoming_username_label" />
<TextView
android:id="@+id/account_auth_type_label"
android:text="@string/account_setup_incoming_auth_type_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<Spinner
android:id="@+id/account_auth_type"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_incoming_auth_type_label" />
<TextView
android:id="@+id/account_password_label"
android:text="@string/account_setup_incoming_password_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<EditText
android:id="@+id/account_password"
android:inputType="textPassword"
android:singleLine="true"
android:layout_height="wrap_content"
android:layout_width="fill_parent" />
<TextView
android:id="@+id/account_client_certificate_label"
android:text="@string/account_setup_incoming_client_certificate_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"
android:visibility="gone" />
<com.fsck.k9.view.ClientCertificateSpinner
android:id="@+id/account_client_certificate_spinner"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:visibility="gone" />
<LinearLayout
android:id="@+id/imap_path_prefix_section"
android:layout_width="fill_parent"

View file

@ -5,108 +5,142 @@
android:layout_height="fill_parent"
android:layout_width="fill_parent">
<ScrollView
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="6dip"
android:fadingEdge="none"
android:scrollbarStyle="outsideInset">
<LinearLayout
<ScrollView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<TextView
android:text="@string/account_setup_outgoing_smtp_server_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<EditText
android:id="@+id/account_server"
android:singleLine="true"
android:inputType="textUri"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_smtp_server_label" />
<TextView
android:text="@string/account_setup_outgoing_security_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<Spinner
android:id="@+id/account_security_type"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_security_label" />
<TextView
android:text="@string/account_setup_outgoing_port_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<EditText
android:id="@+id/account_port"
android:singleLine="true"
android:inputType="number"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_port_label" />
<CheckBox
android:id="@+id/account_require_login"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/account_setup_outgoing_require_login_label" />
android:layout_height="0dp"
android:layout_weight="1"
android:padding="6dip"
android:fadingEdge="none"
android:scrollbarStyle="outsideInset">
<LinearLayout
android:id="@+id/account_require_login_settings"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:text="@string/account_setup_outgoing_authentication_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<Spinner
android:id="@+id/account_auth_type"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_authentication_label" />
android:orientation="vertical">
<TextView
android:text="@string/account_setup_outgoing_username_label"
android:text="@string/account_setup_outgoing_smtp_server_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<EditText
android:id="@+id/account_username"
android:id="@+id/account_server"
android:singleLine="true"
android:inputType="textEmailAddress"
android:inputType="textUri"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_username_label" />
android:contentDescription="@string/account_setup_outgoing_smtp_server_label" />
<TextView
android:text="@string/account_setup_outgoing_password_label"
android:text="@string/account_setup_outgoing_security_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<EditText
android:id="@+id/account_password"
android:singleLine="true"
android:inputType="textPassword"
<Spinner
android:id="@+id/account_security_type"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_password_label" />
android:contentDescription="@string/account_setup_outgoing_security_label" />
<TextView
android:text="@string/account_setup_outgoing_port_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<EditText
android:id="@+id/account_port"
android:singleLine="true"
android:inputType="number"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_port_label" />
<CheckBox
android:id="@+id/account_require_login"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/account_setup_outgoing_require_login_label" />
<LinearLayout
android:id="@+id/account_require_login_settings"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:text="@string/account_setup_outgoing_username_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<EditText
android:id="@+id/account_username"
android:singleLine="true"
android:inputType="textEmailAddress"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_username_label" />
<TextView
android:text="@string/account_setup_outgoing_authentication_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<Spinner
android:id="@+id/account_auth_type"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_authentication_label" />
<TextView
android:id="@+id/account_password_label"
android:text="@string/account_setup_outgoing_password_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<EditText
android:id="@+id/account_password"
android:singleLine="true"
android:inputType="textPassword"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_password_label" />
<TextView
android:id="@+id/account_client_certificate_label"
android:text="@string/account_setup_incoming_client_certificate_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"
android:visibility="gone" />
<com.fsck.k9.view.ClientCertificateSpinner
android:id="@+id/account_client_certificate_spinner"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:visibility="gone" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1" />
</LinearLayout>
</ScrollView>
<include layout="@layout/wizard_next" />
</LinearLayout>

View file

@ -14,20 +14,28 @@
android:layout_height="wrap_content"
android:layout_marginBottom="15dip"/>
<!-- Password prompt for the incoming server -->
<TextView
android:id="@+id/password_prompt_incoming_server"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/incoming_server_password"
android:inputType="textPassword"
android:layout_height="wrap_content"
<!-- Password prompt for the incoming server. Won't be shown for accounts without
user names or accounts with AuthType EXTERNAL! -->
<LinearLayout
android:id="@+id/incoming_server_prompt"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_marginBottom="10dip"/>
android:layout_height="wrap_content">
<TextView
android:id="@+id/password_prompt_incoming_server"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/incoming_server_password"
android:inputType="textPassword"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_marginBottom="10dip"/>
</LinearLayout>
<!-- Password prompt for the outgoing server. Won't be shown for WebDAV accounts! -->
<!-- Password prompt for the outgoing server. Won't be shown for WebDAV accounts,
accounts without user names, or accounts with AuthType EXTERNAL! -->
<LinearLayout
android:id="@+id/outgoing_server_prompt"
android:orientation="vertical"

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
<Button
android:id="@+id/client_certificate_spinner_button"
style="?android:attr/spinnerStyle"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/client_certificate_spinner_empty"
android:freezesText="true" />
<ImageButton
android:id="@+id/client_certificate_spinner_delete"
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="?attr/iconActionCancel" />
</merge>

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<LinearLayout
android:id="@+id/foldableControl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:orientation="horizontal" >
<ImageView
android:id="@+id/foldableIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="10dp"
android:src="?attr/iconActionExpand" />
<TextView
android:id="@+id/foldableText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text=""
android:textColor="?android:attr/textColorTertiary" />
</LinearLayout>
<LinearLayout
android:id="@+id/foldableContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="invisible" />
</LinearLayout>

View file

@ -35,6 +35,8 @@
<attr name="iconActionSave" format="reference" />
<attr name="iconActionCancel" format="reference" />
<attr name="iconActionRequestReadReceipt" format="reference" />
<attr name="iconActionExpand" format="reference" />
<attr name="iconActionCollapse" format="reference" />
<attr name="textColorPrimaryRecipientDropdown" format="reference" />
<attr name="textColorSecondaryRecipientDropdown" format="reference" />
<attr name="backgroundColorChooseAccountHeader" format="color" />
@ -58,5 +60,10 @@
<declare-styleable name="SliderPreference">
<attr name="android:summary" />
</declare-styleable>
<declare-styleable name="FoldableLinearLayout">
<attr name="foldedLabel" format="string" />
<attr name="unFoldedLabel" format="string" />
</declare-styleable>
</resources>

View file

@ -4,5 +4,6 @@
<item type="id" name="dialog_confirm_delete"/>
<item type="id" name="dialog_confirm_spam"/>
<item type="id" name="dialog_attachment_progress"/>
<item type="id" name="dialog_account_setup_error"/>
</resources>

View file

@ -352,6 +352,7 @@ Please submit bug reports, contribute new features and ask questions at
<string name="account_setup_basics_title">Set up a new account</string>
<string name="account_setup_basics_email_hint">Email address</string>
<string name="account_setup_basics_password_hint">Password</string>
<string name="account_setup_basics_show_password">Show password</string>
<string name="account_setup_basics_manual_setup_action">Manual setup</string>
<string name="account_setup_check_settings_title"/>
@ -375,10 +376,12 @@ Please submit bug reports, contribute new features and ask questions at
<string name="account_setup_auth_type_normal_password">Normal password</string>
<string name="account_setup_auth_type_insecure_password">Password, transmitted insecurely</string>
<string name="account_setup_auth_type_encrypted_password">Encrypted password</string>
<string name="account_setup_auth_type_tls_client_certificate">Client certificate</string>
<string name="account_setup_incoming_title">Incoming server settings</string>
<string name="account_setup_incoming_username_label">Username</string>
<string name="account_setup_incoming_password_label">Password</string>
<string name="account_setup_incoming_client_certificate_label">Client certificate</string>
<string name="account_setup_incoming_pop_server_label">POP3 server</string>
<string name="account_setup_incoming_imap_server_label">IMAP server</string>
<string name="account_setup_incoming_webdav_server_label">Exchange server</string>
@ -388,6 +391,7 @@ Please submit bug reports, contribute new features and ask questions at
<string name="account_setup_incoming_security_none_label">None</string>
<string name="account_setup_incoming_security_ssl_label">SSL/TLS</string>
<string name="account_setup_incoming_security_tls_label">STARTTLS</string>
<string name="account_setup_incoming_invalid_setting_combo_notice">\"<xliff:g id="setting_1_label">%1$s</xliff:g> = <xliff:g id="setting_1_value">%2$s</xliff:g>\" is not valid with \"<xliff:g id="setting_2_label">%3$s</xliff:g> = <xliff:g id="setting_2_value">%4$s</xliff:g>\"</string>
<string name="account_setup_incoming_delete_policy_label">When I delete a message</string>
<string name="account_setup_incoming_delete_policy_never_label">Do not delete on server</string>
@ -437,6 +441,7 @@ Please submit bug reports, contribute new features and ask questions at
<string name="account_setup_outgoing_username_label">Username</string>
<string name="account_setup_outgoing_password_label">Password</string>
<string name="account_setup_outgoing_authentication_label">Authentication</string>
<string name="account_setup_outgoing_invalid_setting_combo_notice">\"<xliff:g id="setting_1_label">%1$s</xliff:g> = <xliff:g id="setting_1_value">%2$s</xliff:g>\" is not valid with \"<xliff:g id="setting_2_label">%3$s</xliff:g> = <xliff:g id="setting_2_value">%4$s</xliff:g>\"</string>
<string name="account_setup_bad_uri">Invalid setup: <xliff:g id="err_mess">%s</xliff:g></string>
@ -1096,8 +1101,8 @@ Please submit bug reports, contribute new features and ask questions at
<string name="fetching_attachment_dialog_title_send">Sending message</string>
<string name="fetching_attachment_dialog_title_save">Saving draft</string>
<string name="fetching_attachment_dialog_message">Fetching attachment…</string>
<string name="show_password">Show password</string>
<string name="auth_external_error">Unable to authenticate. The server does not advertise the SASL EXTERNAL capability. This could be due to a problem with the client certificate (expired, unknown certificate authority) or some other configuration problem.</string>
<!-- === OpenPGP specific ================================================================== -->
<string name="openpgp_decrypting_verifying">Decrypting/Verifying…</string>
@ -1114,4 +1119,11 @@ Please submit bug reports, contribute new features and ask questions at
<string name="openpgp_error">OpenPGP Error:</string>
<string name="openpgp_user_id">User Id</string>
<!-- === Client certificates specific ================================================================== -->
<string name="account_setup_basics_client_certificate">Use client certificate</string>
<string name="client_certificate_spinner_empty">No client certificate</string>
<string name="client_certificate_spinner_delete">Remove client certificate selection</string>
<string name="client_certificate_retrieval_failure">"Failed to retrieve client certificate for alias \"<xliff:g id="alias">%s</xliff:g>\""</string>
<string name="client_certificate_advanced_options">Advanced options</string>
<string name="client_certificate_expired">"Client certificate \"<xliff:g id="certificate_alias">%1$s</xliff:g>\" has expired or is not yet valid (<xliff:g id="exception_message">%2$s</xliff:g>)"</string>
</resources>

View file

@ -37,6 +37,8 @@
<item name="iconActionSave">@drawable/ic_action_save_light</item>
<item name="iconActionCancel">@drawable/ic_action_cancel_light</item>
<item name="iconActionRequestReadReceipt">@drawable/ic_action_request_read_receipt_light</item>
<item name="iconActionExpand">@drawable/ic_action_expand_light</item>
<item name="iconActionCollapse">@drawable/ic_action_collapse_light</item>
<item name="textColorPrimaryRecipientDropdown">@android:color/primary_text_light</item>
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_light</item>
<item name="messageListSelectedBackgroundColor">#8038B8E2</item>
@ -89,6 +91,8 @@
<item name="iconActionSave">@drawable/ic_action_save_dark</item>
<item name="iconActionCancel">@drawable/ic_action_cancel_dark</item>
<item name="iconActionRequestReadReceipt">@drawable/ic_action_request_read_receipt_dark</item>
<item name="iconActionExpand">@drawable/ic_action_expand_dark</item>
<item name="iconActionCollapse">@drawable/ic_action_collapse_dark</item>
<item name="textColorPrimaryRecipientDropdown">@android:color/primary_text_dark</item>
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_dark</item>
<item name="messageListSelectedBackgroundColor">#8038B8E2</item>

View file

@ -1879,7 +1879,7 @@ public class Account implements BaseAccount {
public void addCertificate(CheckDirection direction,
X509Certificate certificate) throws CertificateException {
Uri uri;
if (direction.equals(CheckDirection.INCOMING)) {
if (direction == CheckDirection.INCOMING) {
uri = Uri.parse(getStoreUri());
} else {
uri = Uri.parse(getTransportUri());
@ -1896,7 +1896,7 @@ public class Account implements BaseAccount {
public void deleteCertificate(String newHost, int newPort,
CheckDirection direction) {
Uri uri;
if (direction.equals(CheckDirection.INCOMING)) {
if (direction == CheckDirection.INCOMING) {
uri = Uri.parse(getStoreUri());
} else {
uri = Uri.parse(getTransportUri());

View file

@ -75,6 +75,7 @@ import com.fsck.k9.activity.setup.Prefs;
import com.fsck.k9.activity.setup.WelcomeMessage;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.helper.SizeFormatter;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
@ -743,7 +744,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
public boolean retain() {
if (mDialog != null) {
// Retain entered passwords and checkbox state
mIncomingPassword = mIncomingPasswordView.getText().toString();
if (mIncomingPasswordView != null) {
mIncomingPassword = mIncomingPasswordView.getText().toString();
}
if (mOutgoingPasswordView != null) {
mOutgoingPassword = mOutgoingPasswordView.getText().toString();
mUseIncoming = mUseIncomingView.isChecked();
@ -770,9 +773,22 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
ServerSettings incoming = Store.decodeStoreUri(mAccount.getStoreUri());
ServerSettings outgoing = Transport.decodeTransportUri(mAccount.getTransportUri());
// Don't ask for the password to the outgoing server for WebDAV accounts, because
// incoming and outgoing servers are identical for this account type.
boolean configureOutgoingServer = !WebDavStore.STORE_TYPE.equals(outgoing.type);
/*
* Don't ask for the password to the outgoing server for WebDAV
* accounts, because incoming and outgoing servers are identical for
* this account type. Also don't ask when the username is missing.
* Also don't ask when the AuthType is EXTERNAL.
*/
boolean configureOutgoingServer = AuthType.EXTERNAL != outgoing.authenticationType
&& !WebDavStore.STORE_TYPE.equals(outgoing.type)
&& outgoing.username != null
&& !outgoing.username.isEmpty()
&& (outgoing.password == null || outgoing.password
.isEmpty());
boolean configureIncomingServer = AuthType.EXTERNAL != incoming.authenticationType
&& (incoming.password == null || incoming.password
.isEmpty());
// Create a ScrollView that will be used as container for the whole layout
final ScrollView scrollView = new ScrollView(activity);
@ -785,7 +801,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String incomingPassword = mIncomingPasswordView.getText().toString();
String incomingPassword = null;
if (mIncomingPasswordView != null) {
incomingPassword = mIncomingPasswordView.getText().toString();
}
String outgoingPassword = null;
if (mOutgoingPasswordView != null) {
outgoingPassword = (mUseIncomingView.isChecked()) ?
@ -818,19 +837,23 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
// Set the intro text that tells the user what to do
TextView intro = (TextView) layout.findViewById(R.id.password_prompt_intro);
String serverPasswords = activity.getResources().getQuantityString(
R.plurals.settings_import_server_passwords,
(configureOutgoingServer) ? 2 : 1);
R.plurals.settings_import_server_passwords,
(configureIncomingServer && configureOutgoingServer) ? 2 : 1);
intro.setText(activity.getString(R.string.settings_import_activate_account_intro,
mAccount.getDescription(), serverPasswords));
// Display the hostname of the incoming server
TextView incomingText = (TextView) layout.findViewById(
R.id.password_prompt_incoming_server);
incomingText.setText(activity.getString(R.string.settings_import_incoming_server,
incoming.host));
if (configureIncomingServer) {
// Display the hostname of the incoming server
TextView incomingText = (TextView) layout.findViewById(
R.id.password_prompt_incoming_server);
incomingText.setText(activity.getString(R.string.settings_import_incoming_server,
incoming.host));
mIncomingPasswordView = (EditText) layout.findViewById(R.id.incoming_server_password);
mIncomingPasswordView.addTextChangedListener(this);
mIncomingPasswordView = (EditText) layout.findViewById(R.id.incoming_server_password);
mIncomingPasswordView.addTextChangedListener(this);
} else {
layout.findViewById(R.id.incoming_server_prompt).setVisibility(View.GONE);
}
if (configureOutgoingServer) {
// Display the hostname of the outgoing server
@ -844,20 +867,27 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
mOutgoingPasswordView.addTextChangedListener(this);
mUseIncomingView = (CheckBox) layout.findViewById(
R.id.use_incoming_server_password);
mUseIncomingView.setChecked(true);
mUseIncomingView.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
mOutgoingPasswordView.setText(null);
mOutgoingPasswordView.setEnabled(false);
} else {
mOutgoingPasswordView.setText(mIncomingPasswordView.getText());
mOutgoingPasswordView.setEnabled(true);
R.id.use_incoming_server_password);
if (configureIncomingServer) {
mUseIncomingView.setChecked(true);
mUseIncomingView.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
mOutgoingPasswordView.setText(null);
mOutgoingPasswordView.setEnabled(false);
} else {
mOutgoingPasswordView.setText(mIncomingPasswordView.getText());
mOutgoingPasswordView.setEnabled(true);
}
}
}
});
});
} else {
mUseIncomingView.setChecked(false);
mUseIncomingView.setVisibility(View.GONE);
mOutgoingPasswordView.setEnabled(true);
}
} else {
layout.findViewById(R.id.outgoing_server_prompt).setVisibility(View.GONE);
}
@ -871,15 +901,21 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
// Restore the contents of the password boxes and the checkbox (if the dialog was
// retained during a configuration change).
if (restore) {
mIncomingPasswordView.setText(mIncomingPassword);
if (configureIncomingServer) {
mIncomingPasswordView.setText(mIncomingPassword);
}
if (configureOutgoingServer) {
mOutgoingPasswordView.setText(mOutgoingPassword);
mUseIncomingView.setChecked(mUseIncoming);
}
} else {
// Trigger afterTextChanged() being called
// Work around this bug: https://code.google.com/p/android/issues/detail?id=6360
mIncomingPasswordView.setText(mIncomingPasswordView.getText());
if (configureIncomingServer) {
// Trigger afterTextChanged() being called
// Work around this bug: https://code.google.com/p/android/issues/detail?id=6360
mIncomingPasswordView.setText(mIncomingPasswordView.getText());
} else {
mOutgoingPasswordView.setText(mOutgoingPasswordView.getText());
}
}
}
@ -887,17 +923,21 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
public void afterTextChanged(Editable arg0) {
boolean enable = false;
// Is the password box for the incoming server password empty?
if (mIncomingPasswordView.getText().length() > 0) {
// Do we need to check the outgoing server password box?
if (mOutgoingPasswordView == null) {
enable = true;
}
// If the checkbox to use the incoming server password is checked we need to make
// sure that the password box for the outgoing server isn't empty.
else if (mUseIncomingView.isChecked() ||
mOutgoingPasswordView.getText().length() > 0) {
enable = true;
if (mIncomingPasswordView != null) {
if (mIncomingPasswordView.getText().length() > 0) {
// Do we need to check the outgoing server password box?
if (mOutgoingPasswordView == null) {
enable = true;
}
// If the checkbox to use the incoming server password is checked we need to make
// sure that the password box for the outgoing server isn't empty.
else if (mUseIncomingView.isChecked() ||
mOutgoingPasswordView.getText().length() > 0) {
enable = true;
}
}
} else {
enable = mOutgoingPasswordView.getText().length() > 0;
}
// Disable "OK" button if the user hasn't specified all necessary passwords.
@ -948,12 +988,14 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
@Override
protected Void doInBackground(Void... params) {
try {
// Set incoming server password
String storeUri = mAccount.getStoreUri();
ServerSettings incoming = Store.decodeStoreUri(storeUri);
ServerSettings newIncoming = incoming.newPassword(mIncomingPassword);
String newStoreUri = Store.createStoreUri(newIncoming);
mAccount.setStoreUri(newStoreUri);
if (mIncomingPassword != null) {
// Set incoming server password
String storeUri = mAccount.getStoreUri();
ServerSettings incoming = Store.decodeStoreUri(storeUri);
ServerSettings newIncoming = incoming.newPassword(mIncomingPassword);
String newStoreUri = Store.createStoreUri(newIncoming);
mAccount.setStoreUri(newStoreUri);
}
if (mOutgoingPassword != null) {
// Set outgoing server password

View file

@ -89,7 +89,23 @@ public class AccountSetupAccountType extends K9Activity implements OnClickListen
private void onWebDav() {
try {
URI uri = new URI(mAccount.getStoreUri());
uri = new URI("webdav+ssl+", uri.getUserInfo(), uri.getHost(), uri.getPort(), null, null, null);
/*
* The user info we have been given from
* AccountSetupBasics.onManualSetup() is encoded as an IMAP store
* URI: AuthType:UserName:Password (no fields should be empty).
* However, AuthType is not applicable to WebDAV nor to its store
* URI. Re-encode without it, using just the UserName and Password.
*/
String userPass = "";
String[] userInfo = uri.getUserInfo().split(":");
if (userInfo.length > 1) {
userPass = userInfo[1];
}
if (userInfo.length > 2) {
userPass = userPass + ":" + userInfo[2];
}
uri = new URI("webdav+ssl+", userPass, uri.getHost(), uri.getPort(), null, null, null);
mAccount.setStoreUri(uri.toString());
AccountSetupIncoming.actionIncomingSettings(this, mAccount, mMakeDefault);
finish();
@ -112,6 +128,7 @@ public class AccountSetupAccountType extends K9Activity implements OnClickListen
break;
}
}
private void failure(Exception use) {
Log.e(K9.LOG_TAG, "Failure", use);
String toastText = getString(R.string.account_setup_bad_uri, use.getMessage());

View file

@ -8,6 +8,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.Locale;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
@ -26,6 +27,7 @@ import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import com.fsck.k9.Account;
import com.fsck.k9.EmailAddressValidator;
import com.fsck.k9.K9;
@ -34,6 +36,15 @@ import com.fsck.k9.R;
import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.store.ImapStore;
import com.fsck.k9.mail.transport.SmtpTransport;
import com.fsck.k9.view.ClientCertificateSpinner;
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
/**
* Prompts the user for the email address and password.
@ -43,7 +54,7 @@ import com.fsck.k9.helper.Utility;
* AccountSetupAccountType activity.
*/
public class AccountSetupBasics extends K9Activity
implements OnClickListener, TextWatcher {
implements OnClickListener, TextWatcher, OnCheckedChangeListener, OnClientCertificateChangedListener {
private final static String EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account";
private final static int DIALOG_NOTE = 1;
private final static String STATE_KEY_PROVIDER =
@ -53,6 +64,8 @@ public class AccountSetupBasics extends K9Activity
private EditText mEmailView;
private EditText mPasswordView;
private CheckBox mClientCertificateCheckBox;
private ClientCertificateSpinner mClientCertificateSpinner;
private Button mNextButton;
private Button mManualSetupButton;
private Account mAccount;
@ -60,6 +73,7 @@ public class AccountSetupBasics extends K9Activity
private EmailAddressValidator mEmailValidator = new EmailAddressValidator();
private boolean mCheckedIncoming = false;
private CheckBox mShowPasswordCheckBox;
public static void actionNewAccount(Context context) {
Intent i = new Intent(context, AccountSetupBasics.class);
@ -72,31 +86,27 @@ public class AccountSetupBasics extends K9Activity
setContentView(R.layout.account_setup_basics);
mEmailView = (EditText)findViewById(R.id.account_email);
mPasswordView = (EditText)findViewById(R.id.account_password);
mClientCertificateCheckBox = (CheckBox)findViewById(R.id.account_client_certificate);
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
mNextButton = (Button)findViewById(R.id.next);
mManualSetupButton = (Button)findViewById(R.id.manual_setup);
CheckBox showPassword = (CheckBox) findViewById(R.id.show_password);
showPassword.setOnCheckedChangeListener (new OnCheckedChangeListener() {
mShowPasswordCheckBox = (CheckBox) findViewById(R.id.show_password);
mNextButton.setOnClickListener(this);
mManualSetupButton.setOnClickListener(this);
}
private void initializeViewListeners() {
mEmailView.addTextChangedListener(this);
mPasswordView.addTextChangedListener(this);
mClientCertificateCheckBox.setOnCheckedChangeListener(this);
mClientCertificateSpinner.setOnClientCertificateChangedListener(this);
mShowPasswordCheckBox.setOnCheckedChangeListener (new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
mPasswordView.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
} else {
mPasswordView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
showPassword(isChecked);
}
});
mNextButton.setOnClickListener(this);
mManualSetupButton.setOnClickListener(this);
mEmailView.addTextChangedListener(this);
mPasswordView.addTextChangedListener(this);
}
@Override
public void onResume() {
super.onResume();
validateFields();
}
@Override
@ -125,6 +135,25 @@ public class AccountSetupBasics extends K9Activity
}
mCheckedIncoming = savedInstanceState.getBoolean(STATE_KEY_CHECKED_INCOMING);
updateViewVisibility(mClientCertificateCheckBox.isChecked());
showPassword(mShowPasswordCheckBox.isChecked());
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
/*
* We wait until now to initialize the listeners because we didn't want
* the OnCheckedChangeListener active while the
* mClientCertificateCheckBox state was being restored because it could
* trigger the pop-up of a ClientCertificateSpinner.chooseCertificate()
* dialog.
*/
initializeViewListeners();
validateFields();
}
public void afterTextChanged(Editable s) {
@ -137,11 +166,56 @@ public class AccountSetupBasics extends K9Activity
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void onClientCertificateChanged(String alias) {
validateFields();
}
/**
* Called when checking the client certificate CheckBox
*/
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
updateViewVisibility(isChecked);
validateFields();
// Have the user select (or confirm) the client certificate
if (isChecked) {
mClientCertificateSpinner.chooseCertificate();
}
}
private void updateViewVisibility(boolean usingCertificates) {
if (usingCertificates) {
// hide password fields, show client certificate spinner
mPasswordView.setVisibility(View.GONE);
mShowPasswordCheckBox.setVisibility(View.GONE);
mClientCertificateSpinner.setVisibility(View.VISIBLE);
} else {
// show password fields, hide client certificate spinner
mPasswordView.setVisibility(View.VISIBLE);
mShowPasswordCheckBox.setVisibility(View.VISIBLE);
mClientCertificateSpinner.setVisibility(View.GONE);
}
}
private void showPassword(boolean show) {
if (show) {
mPasswordView.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
} else {
mPasswordView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
}
private void validateFields() {
boolean clientCertificateChecked = mClientCertificateCheckBox.isChecked();
String clientCertificateAlias = mClientCertificateSpinner.getAlias();
String email = mEmailView.getText().toString();
boolean valid = Utility.requiredFieldValid(mEmailView)
&& Utility.requiredFieldValid(mPasswordView)
&& mEmailValidator.isValidAddressOnly(email);
&& ((!clientCertificateChecked && Utility.requiredFieldValid(mPasswordView))
|| (clientCertificateChecked && clientCertificateAlias != null))
&& mEmailValidator.isValidAddressOnly(email);
mNextButton.setEnabled(valid);
mManualSetupButton.setEnabled(valid);
@ -277,6 +351,13 @@ public class AccountSetupBasics extends K9Activity
}
protected void onNext() {
if (mClientCertificateCheckBox.isChecked()) {
// Auto-setup doesn't support client certificates.
onManualSetup();
return;
}
String email = mEmailView.getText().toString();
String[] emailParts = splitEmail(email);
String domain = emailParts[1];
@ -317,33 +398,38 @@ public class AccountSetupBasics extends K9Activity
private void onManualSetup() {
String email = mEmailView.getText().toString();
String password = mPasswordView.getText().toString();
String[] emailParts = splitEmail(email);
String user = emailParts[0];
String domain = emailParts[1];
String password = null;
String clientCertificateAlias = null;
AuthType authenticationType = null;
if (mClientCertificateCheckBox.isChecked()) {
authenticationType = AuthType.EXTERNAL;
clientCertificateAlias = mClientCertificateSpinner.getAlias();
} else {
authenticationType = AuthType.PLAIN;
password = mPasswordView.getText().toString();
}
if (mAccount == null) {
mAccount = Preferences.getPreferences(this).newAccount();
}
mAccount.setName(getOwnerName());
mAccount.setEmail(email);
try {
String userEnc = URLEncoder.encode(user, "UTF-8");
String passwordEnc = URLEncoder.encode(password, "UTF-8");
URI uri = new URI("placeholder", userEnc + ":" + passwordEnc, "mail." + domain, -1, null,
null, null);
mAccount.setStoreUri(uri.toString());
mAccount.setTransportUri(uri.toString());
} catch (UnsupportedEncodingException enc) {
// This really shouldn't happen since the encoding is hardcoded to UTF-8
Log.e(K9.LOG_TAG, "Couldn't urlencode username or password.", enc);
} catch (URISyntaxException use) {
/*
* If we can't set up the URL we just continue. It's only for
* convenience.
*/
}
// set default uris
// NOTE: they will be changed again in AccountSetupAccountType!
ServerSettings storeServer = new ServerSettings(ImapStore.STORE_TYPE, "mail." + domain, -1,
ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType, user, password, clientCertificateAlias);
ServerSettings transportServer = new ServerSettings(SmtpTransport.TRANSPORT_TYPE, "mail." + domain, -1,
ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType, user, password, clientCertificateAlias);
String storeUri = Store.createStoreUri(storeServer);
String transportUri = Transport.createTransportUri(transportServer);
mAccount.setStoreUri(storeUri);
mAccount.setTransportUri(transportUri);
mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts));
mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash));
mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent));
@ -450,4 +536,5 @@ public class AccountSetupBasics extends K9Activity
public String note;
}
}

View file

@ -3,6 +3,8 @@ package com.fsck.k9.activity.setup;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DialogFragment;
import android.app.FragmentTransaction;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
@ -15,16 +17,18 @@ import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.fsck.k9.*;
import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.fragment.ConfirmationDialogFragment;
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.store.WebDavStore;
import com.fsck.k9.mail.filter.Hex;
import java.security.cert.CertificateException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
@ -32,15 +36,17 @@ import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
/**
* Checks the given settings to make sure that they can be used to send and
* receive mail.
*
*
* XXX NOTE: The manifest for this app has it ignore config changes, because
* it doesn't correctly deal with restarting while its thread is running.
*/
public class AccountSetupCheckSettings extends K9Activity implements OnClickListener {
public class AccountSetupCheckSettings extends K9Activity implements OnClickListener,
ConfirmationDialogFragmentListener{
public static final int ACTIVITY_REQUEST_CODE = 1;
@ -67,7 +73,8 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
private boolean mDestroyed;
public static void actionCheckSettings(Activity context, Account account, CheckDirection direction) {
public static void actionCheckSettings(Activity context, Account account,
CheckDirection direction) {
Intent i = new Intent(context, AccountSetupCheckSettings.class);
i.putExtra(EXTRA_ACCOUNT, account.getUuid());
i.putExtra(EXTRA_CHECK_DIRECTION, direction);
@ -107,7 +114,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
ctrl.clearCertificateErrorNotifications(AccountSetupCheckSettings.this,
mAccount, mDirection);
if (mDirection.equals(CheckDirection.INCOMING)) {
if (mDirection == CheckDirection.INCOMING) {
store = mAccount.getRemoteStore();
if (store instanceof WebDavStore) {
@ -130,7 +137,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
finish();
return;
}
if (mDirection.equals(CheckDirection.OUTGOING)) {
if (mDirection == CheckDirection.OUTGOING) {
if (!(mAccount.getRemoteStore() instanceof WebDavStore)) {
setMessage(R.string.account_setup_check_settings_check_outgoing_msg);
}
@ -154,25 +161,12 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
R.string.account_setup_failed_dlg_auth_message_fmt,
afe.getMessage() == null ? "" : afe.getMessage());
} catch (final CertificateValidationException cve) {
Log.e(K9.LOG_TAG, "Error while testing settings", cve);
X509Certificate[] chain = cve.getCertChain();
// Avoid NullPointerException in acceptKeyDialog()
if (chain != null) {
acceptKeyDialog(
R.string.account_setup_failed_dlg_certificate_message_fmt,
cve);
} else {
showErrorDialog(
R.string.account_setup_failed_dlg_server_message_fmt,
(cve.getMessage() == null ? "" : cve.getMessage()));
}
handleCertificateValidationException(cve);
} 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()));
}
}
@ -180,6 +174,22 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
.start();
}
private void handleCertificateValidationException(CertificateValidationException cve) {
Log.e(K9.LOG_TAG, "Error while testing settings", cve);
X509Certificate[] chain = cve.getCertChain();
// Avoid NullPointerException in acceptKeyDialog()
if (chain != null) {
acceptKeyDialog(
R.string.account_setup_failed_dlg_certificate_message_fmt,
cve);
} else {
showErrorDialog(
R.string.account_setup_failed_dlg_server_message_fmt,
(cve.getMessage() == null ? "" : cve.getMessage()));
}
}
@Override
public void onDestroy() {
super.onDestroy();
@ -198,41 +208,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
});
}
private void showErrorDialog(final int msgResId, final Object... args) {
mHandler.post(new Runnable() {
public void run() {
if (mDestroyed) {
return;
}
mProgressBar.setIndeterminate(false);
new AlertDialog.Builder(AccountSetupCheckSettings.this)
.setTitle(getString(R.string.account_setup_failed_dlg_title))
.setMessage(getString(msgResId, args))
.setCancelable(true)
.setNegativeButton(
getString(R.string.account_setup_failed_dlg_continue_action),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
mCanceled = false;
setResult(RESULT_OK);
finish();
}
})
.setPositiveButton(
getString(R.string.account_setup_failed_dlg_edit_details_action),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
}
});
}
private void acceptKeyDialog(final int msgResId,
final CertificateValidationException ex) {
private void acceptKeyDialog(final int msgResId, final CertificateValidationException ex) {
mHandler.post(new Runnable() {
public void run() {
if (mDestroyed) {
@ -351,6 +327,8 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
}
}
// TODO: refactor with DialogFragment.
// This is difficult because we need to pass through chain[0] for onClick()
new AlertDialog.Builder(AccountSetupCheckSettings.this)
.setTitle(getString(R.string.account_setup_failed_dlg_invalid_certificate_title))
//.setMessage(getString(R.string.account_setup_failed_dlg_invalid_certificate)
@ -362,15 +340,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
getString(R.string.account_setup_failed_dlg_invalid_certificate_accept),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
try {
mAccount.addCertificate(mDirection, chain[0]);
} catch (CertificateException e) {
showErrorDialog(
R.string.account_setup_failed_dlg_certificate_message_fmt,
e.getMessage() == null ? "" : e.getMessage());
}
AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount,
mDirection);
acceptCertificate(chain[0]);
}
})
.setNegativeButton(
@ -385,13 +355,30 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
});
}
/**
* Permanently accepts a certificate for the INCOMING or OUTGOING direction
* by adding it to the local key store.
*
* @param certificate
*/
private void acceptCertificate(X509Certificate certificate) {
try {
mAccount.addCertificate(mDirection, certificate);
} catch (CertificateException e) {
showErrorDialog(
R.string.account_setup_failed_dlg_certificate_message_fmt,
e.getMessage() == null ? "" : e.getMessage());
}
AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount,
mDirection);
}
@Override
public void onActivityResult(int reqCode, int resCode, Intent data) {
setResult(resCode);
finish();
}
private void onCancel() {
mCanceled = true;
setMessage(R.string.account_setup_check_settings_canceling_msg);
@ -404,4 +391,74 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
break;
}
}
private void showErrorDialog(final int msgResId, final Object... args) {
mHandler.post(new Runnable() {
public void run() {
showDialogFragment(R.id.dialog_account_setup_error, getString(msgResId, args));
}
});
}
private void showDialogFragment(int dialogId, 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;
}
default: {
throw new RuntimeException("Called showDialog(int) with unknown dialog id.");
}
}
FragmentTransaction ta = getFragmentManager().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: {
mCanceled = false;
setResult(RESULT_OK);
finish();
break;
}
}
}
@Override
public void dialogCancelled(int dialogId) {
// nothing to do here...
}
}

View file

@ -12,6 +12,7 @@ import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.*;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.CompoundButton.OnCheckedChangeListener;
import com.fsck.k9.*;
@ -23,23 +24,27 @@ import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.store.ImapStore;
import com.fsck.k9.mail.store.Pop3Store;
import com.fsck.k9.mail.store.WebDavStore;
import com.fsck.k9.mail.store.ImapStore.ImapStoreSettings;
import com.fsck.k9.mail.store.WebDavStore.WebDavStoreSettings;
import com.fsck.k9.mail.transport.SmtpTransport;
import com.fsck.k9.service.MailService;
import com.fsck.k9.view.ClientCertificateSpinner;
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
public class AccountSetupIncoming extends K9Activity implements OnClickListener {
private static final String EXTRA_ACCOUNT = "account";
private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
private static final String STATE_SECURITY_TYPE_POSITION = "stateSecurityTypePosition";
private static final String STATE_AUTH_TYPE_POSITION = "authTypePosition";
private static final String POP3_PORT = "110";
private static final String POP3_SSL_PORT = "995";
@ -51,10 +56,16 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
private String mStoreType;
private EditText mUsernameView;
private EditText mPasswordView;
private ClientCertificateSpinner mClientCertificateSpinner;
private TextView mClientCertificateLabelView;
private TextView mPasswordLabelView;
private EditText mServerView;
private EditText mPortView;
private String mCurrentPortViewSetting;
private Spinner mSecurityTypeView;
private int mCurrentSecurityTypeViewPosition;
private Spinner mAuthTypeView;
private int mCurrentAuthTypeViewPosition;
private CheckBox mImapAutoDetectNamespaceView;
private EditText mImapPathPrefixView;
private EditText mWebdavPathPrefixView;
@ -97,6 +108,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mUsernameView = (EditText)findViewById(R.id.account_username);
mPasswordView = (EditText)findViewById(R.id.account_password);
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
mClientCertificateLabelView = (TextView)findViewById(R.id.account_client_certificate_label);
mPasswordLabelView = (TextView)findViewById(R.id.account_password_label);
TextView serverLabelView = (TextView) findViewById(R.id.account_server_label);
mServerView = (EditText)findViewById(R.id.account_server);
mPortView = (EditText)findViewById(R.id.account_port);
@ -130,28 +144,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mAuthTypeAdapter = AuthType.getArrayAdapter(this);
mAuthTypeView.setAdapter(mAuthTypeAdapter);
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
/* unused */
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
/* unused */
}
};
mUsernameView.addTextChangedListener(validationTextWatcher);
mPasswordView.addTextChangedListener(validationTextWatcher);
mServerView.addTextChangedListener(validationTextWatcher);
mPortView.addTextChangedListener(validationTextWatcher);
/*
* Only allow digits in the port field.
*/
@ -173,6 +165,15 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
try {
ServerSettings settings = Store.decodeStoreUri(mAccount.getStoreUri());
if (savedInstanceState == null) {
// The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter
mCurrentAuthTypeViewPosition = mAuthTypeAdapter.getPosition(settings.authenticationType);
} else {
mCurrentAuthTypeViewPosition = savedInstanceState.getInt(STATE_AUTH_TYPE_POSITION);
}
mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false);
updateViewFromAuthType();
if (settings.username != null) {
mUsernameView.setText(settings.username);
}
@ -181,11 +182,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mPasswordView.setText(settings.password);
}
updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
// The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter
int position = mAuthTypeAdapter.getPosition(settings.authenticationType);
mAuthTypeView.setSelection(position, false);
if (settings.clientCertificateAlias != null) {
mClientCertificateSpinner.setAlias(settings.clientCertificateAlias);
}
mStoreType = settings.type;
if (Pop3Store.STORE_TYPE.equals(settings.type)) {
@ -256,34 +255,29 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
throw new Exception("Unknown account type: " + mAccount.getStoreUri());
}
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(this,
android.R.layout.simple_spinner_item, mConnectionSecurityChoices);
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// Note that mConnectionSecurityChoices is configured above based on server type
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);
if (savedInstanceState == null) {
mCurrentSecurityTypeViewPosition = securityTypesAdapter.getPosition(settings.connectionSecurity);
} else {
/*
* Updates the port when the user changes the security type. This allows
* us to show a reasonable default which the user can change.
*
* Note: It's important that we set the listener *after* an initial option has been
* selected by the code above. Otherwise the listener might be called after
* onCreate() has been processed and the current port value set later in this
* method is overridden with the default port for the selected security type.
*/
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
updatePortFromSecurityType();
}
/*
* Restore the spinner state now, before calling
* setOnItemSelectedListener(), thus avoiding a call to
* onItemSelected(). Then, when the system restores the state
* (again) in onRestoreInstanceState(), The system will see that
* the new state is the same as the current state (set here), so
* once again onItemSelected() will not be called.
*/
mCurrentSecurityTypeViewPosition = savedInstanceState.getInt(STATE_SECURITY_TYPE_POSITION);
}
mSecurityTypeView.setSelection(mCurrentSecurityTypeViewPosition, false);
@Override
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
});
updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
mCompressionMobile.setChecked(mAccount.useCompression(Account.TYPE_MOBILE));
mCompressionWifi.setChecked(mAccount.useCompression(Account.TYPE_WIFI));
@ -298,34 +292,205 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
} else {
updatePortFromSecurityType();
}
mCurrentPortViewSetting = mPortView.getText().toString();
mSubscribedFoldersOnly.setChecked(mAccount.subscribedFoldersOnly());
validateFields();
} catch (Exception e) {
failure(e);
}
}
/**
* Called at the end of either {@code onCreate()} or
* {@code onRestoreInstanceState()}, after the views have been initialized,
* so that the listeners are not triggered during the view initialization.
* This avoids needless calls to {@code validateFields()} which is called
* immediately after this is called.
*/
private void initializeViewListeners() {
/*
* Updates the port when the user changes the security type. This allows
* us to show a reasonable default which the user can change.
*/
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
/*
* We keep our own record of the spinner state so we
* know for sure that onItemSelected() was called
* because of user input, not because of spinner
* state initialization. This assures that the port
* will not be replaced with a default value except
* on user input.
*/
if (mCurrentSecurityTypeViewPosition != position) {
updatePortFromSecurityType();
validateFields();
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
});
mAuthTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
if (mCurrentAuthTypeViewPosition == position) {
return;
}
updateViewFromAuthType();
validateFields();
AuthType selection = (AuthType) mAuthTypeView.getSelectedItem();
// Have the user select (or confirm) the client certificate
if (AuthType.EXTERNAL == selection) {
// This may again invoke validateFields()
mClientCertificateSpinner.chooseCertificate();
} else {
mPasswordView.requestFocus();
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
});
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
mUsernameView.addTextChangedListener(validationTextWatcher);
mPasswordView.addTextChangedListener(validationTextWatcher);
mServerView.addTextChangedListener(validationTextWatcher);
mPortView.addTextChangedListener(validationTextWatcher);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(EXTRA_ACCOUNT, mAccount.getUuid());
outState.putInt(STATE_SECURITY_TYPE_POSITION, mCurrentSecurityTypeViewPosition);
outState.putInt(STATE_AUTH_TYPE_POSITION, mCurrentAuthTypeViewPosition);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
/*
* We didn't want the listeners active while the state was being restored
* because they could overwrite the restored port with a default port when
* the security type was restored.
*/
initializeViewListeners();
validateFields();
}
/**
* Shows/hides password field and client certificate spinner
*/
private void updateViewFromAuthType() {
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType);
if (isAuthTypeExternal) {
// hide password fields, show client certificate fields
mPasswordView.setVisibility(View.GONE);
mPasswordLabelView.setVisibility(View.GONE);
mClientCertificateLabelView.setVisibility(View.VISIBLE);
mClientCertificateSpinner.setVisibility(View.VISIBLE);
} else {
// show password fields, hide client certificate fields
mPasswordView.setVisibility(View.VISIBLE);
mPasswordLabelView.setVisibility(View.VISIBLE);
mClientCertificateLabelView.setVisibility(View.GONE);
mClientCertificateSpinner.setVisibility(View.GONE);
}
}
/**
* This is invoked only when the user makes changes to a widget, not when
* widgets are changed programmatically. (The logic is simpler when you know
* that this is the last thing called after an input change.)
*/
private void validateFields() {
mNextButton
.setEnabled(Utility.requiredFieldValid(mUsernameView)
&& Utility.requiredFieldValid(mPasswordView)
&& Utility.domainFieldValid(mServerView)
&& Utility.requiredFieldValid(mPortView));
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType);
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
boolean hasConnectionSecurity = (connectionSecurity != ConnectionSecurity.NONE);
if (isAuthTypeExternal && !hasConnectionSecurity) {
// Notify user of an invalid combination of AuthType.EXTERNAL & ConnectionSecurity.NONE
String toastText = getString(R.string.account_setup_incoming_invalid_setting_combo_notice,
getString(R.string.account_setup_incoming_auth_type_label),
AuthType.EXTERNAL.toString(),
getString(R.string.account_setup_incoming_security_label),
ConnectionSecurity.NONE.toString());
Toast.makeText(this, toastText, Toast.LENGTH_LONG).show();
// Reset the views back to their previous settings without recursing through here again
OnItemSelectedListener onItemSelectedListener = mAuthTypeView.getOnItemSelectedListener();
mAuthTypeView.setOnItemSelectedListener(null);
mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false);
mAuthTypeView.setOnItemSelectedListener(onItemSelectedListener);
updateViewFromAuthType();
onItemSelectedListener = mSecurityTypeView.getOnItemSelectedListener();
mSecurityTypeView.setOnItemSelectedListener(null);
mSecurityTypeView.setSelection(mCurrentSecurityTypeViewPosition, false);
mSecurityTypeView.setOnItemSelectedListener(onItemSelectedListener);
updateAuthPlainTextFromSecurityType((ConnectionSecurity) mSecurityTypeView.getSelectedItem());
mPortView.removeTextChangedListener(validationTextWatcher);
mPortView.setText(mCurrentPortViewSetting);
mPortView.addTextChangedListener(validationTextWatcher);
authType = (AuthType) mAuthTypeView.getSelectedItem();
isAuthTypeExternal = (AuthType.EXTERNAL == authType);
connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
hasConnectionSecurity = (connectionSecurity != ConnectionSecurity.NONE);
} else {
mCurrentAuthTypeViewPosition = mAuthTypeView.getSelectedItemPosition();
mCurrentSecurityTypeViewPosition = mSecurityTypeView.getSelectedItemPosition();
mCurrentPortViewSetting = mPortView.getText().toString();
}
boolean hasValidCertificateAlias = mClientCertificateSpinner.getAlias() != null;
boolean hasValidUserName = Utility.requiredFieldValid(mUsernameView);
boolean hasValidPasswordSettings = hasValidUserName
&& !isAuthTypeExternal
&& Utility.requiredFieldValid(mPasswordView);
boolean hasValidExternalAuthSettings = hasValidUserName
&& isAuthTypeExternal
&& hasConnectionSecurity
&& hasValidCertificateAlias;
mNextButton.setEnabled(Utility.domainFieldValid(mServerView)
&& Utility.requiredFieldValid(mPortView)
&& (hasValidPasswordSettings || hasValidExternalAuthSettings));
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
}
private void updatePortFromSecurityType() {
ConnectionSecurity securityType = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
mPortView.setText(getDefaultPort(securityType));
updateAuthPlainTextFromSecurityType(securityType);
// Remove listener so as not to trigger validateFields() which is called
// elsewhere as a result of user interaction.
mPortView.removeTextChangedListener(validationTextWatcher);
mPortView.setText(getDefaultPort(securityType));
mPortView.addTextChangedListener(validationTextWatcher);
}
private String getDefaultPort(ConnectionSecurity securityType) {
@ -377,21 +542,22 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
* password the user just set for incoming.
*/
try {
String usernameEnc = URLEncoder.encode(mUsernameView.getText().toString(), "UTF-8");
String passwordEnc = URLEncoder.encode(mPasswordView.getText().toString(), "UTF-8");
String username = mUsernameView.getText().toString();
String password = null;
String clientCertificateAlias = null;
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
if (AuthType.EXTERNAL == authType) {
clientCertificateAlias = mClientCertificateSpinner.getAlias();
} else {
password = mPasswordView.getText().toString();
}
URI oldUri = new URI(mAccount.getTransportUri());
URI uri = new URI(
oldUri.getScheme(),
usernameEnc + ":" + passwordEnc,
oldUri.getHost(),
oldUri.getPort(),
null,
null,
null);
mAccount.setTransportUri(uri.toString());
} catch (UnsupportedEncodingException enc) {
// This really shouldn't happen since the encoding is hardcoded to UTF-8
Log.e(K9.LOG_TAG, "Couldn't urlencode username or password.", enc);
ServerSettings transportServer = new ServerSettings(SmtpTransport.TRANSPORT_TYPE, oldUri.getHost(), oldUri.getPort(),
ConnectionSecurity.SSL_TLS_REQUIRED, authType, username, password, clientCertificateAlias);
String transportUri = Transport.createTransportUri(transportServer);
mAccount.setTransportUri(transportUri);
} catch (URISyntaxException use) {
/*
* If we can't set up the URL we just continue. It's only for
@ -411,8 +577,15 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
String username = mUsernameView.getText().toString();
String password = mPasswordView.getText().toString();
String password = null;
String clientCertificateAlias = null;
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
if (authType == AuthType.EXTERNAL) {
clientCertificateAlias = mClientCertificateSpinner.getAlias();
} else {
password = mPasswordView.getText().toString();
}
String host = mServerView.getText().toString();
int port = Integer.parseInt(mPortView.getText().toString());
@ -435,7 +608,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mAccount.deleteCertificate(host, port, CheckDirection.INCOMING);
ServerSettings settings = new ServerSettings(mStoreType, host, port,
connectionSecurity, authType, username, password, extra);
connectionSecurity, authType, username, password, clientCertificateAlias, extra);
mAccount.setStoreUri(Store.createStoreUri(settings));
@ -470,4 +643,29 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
toast.show();
}
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
/* unused */
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
/* unused */
}
};
OnClientCertificateChangedListener clientCertificateChangedListener = new OnClientCertificateChangedListener() {
@Override
public void onClientCertificateChanged(String alias) {
validateFields();
}
};
}

View file

@ -12,7 +12,9 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.*;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.CompoundButton.OnCheckedChangeListener;
import com.fsck.k9.*;
import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
@ -22,6 +24,8 @@ import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.transport.SmtpTransport;
import com.fsck.k9.view.ClientCertificateSpinner;
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
import java.net.URI;
import java.net.URISyntaxException;
@ -31,18 +35,26 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
private static final String EXTRA_ACCOUNT = "account";
private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
private static final String STATE_SECURITY_TYPE_POSITION = "stateSecurityTypePosition";
private static final String STATE_AUTH_TYPE_POSITION = "authTypePosition";
private static final String SMTP_PORT = "587";
private static final String SMTP_SSL_PORT = "465";
private EditText mUsernameView;
private EditText mPasswordView;
private ClientCertificateSpinner mClientCertificateSpinner;
private TextView mClientCertificateLabelView;
private TextView mPasswordLabelView;
private EditText mServerView;
private EditText mPortView;
private String mCurrentPortViewSetting;
private CheckBox mRequireLoginView;
private ViewGroup mRequireLoginSettingsView;
private Spinner mSecurityTypeView;
private int mCurrentSecurityTypeViewPosition;
private Spinner mAuthTypeView;
private int mCurrentAuthTypeViewPosition;
private ArrayAdapter<AuthType> mAuthTypeAdapter;
private Button mNextButton;
private Account mAccount;
@ -87,6 +99,9 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
mUsernameView = (EditText)findViewById(R.id.account_username);
mPasswordView = (EditText)findViewById(R.id.account_password);
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
mClientCertificateLabelView = (TextView)findViewById(R.id.account_client_certificate_label);
mPasswordLabelView = (TextView)findViewById(R.id.account_password_label);
mServerView = (EditText)findViewById(R.id.account_server);
mPortView = (EditText)findViewById(R.id.account_port);
mRequireLoginView = (CheckBox)findViewById(R.id.account_require_login);
@ -96,36 +111,12 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
mNextButton = (Button)findViewById(R.id.next);
mNextButton.setOnClickListener(this);
mRequireLoginView.setOnCheckedChangeListener(this);
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(this,
android.R.layout.simple_spinner_item, ConnectionSecurity.values());
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mSecurityTypeView.setAdapter(securityTypesAdapter);
mSecurityTypeView.setAdapter(ConnectionSecurity.getArrayAdapter(this));
mAuthTypeAdapter = AuthType.getArrayAdapter(this);
mAuthTypeView.setAdapter(mAuthTypeAdapter);
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
};
mUsernameView.addTextChangedListener(validationTextWatcher);
mPasswordView.addTextChangedListener(validationTextWatcher);
mServerView.addTextChangedListener(validationTextWatcher);
mPortView.addTextChangedListener(validationTextWatcher);
/*
* Only allow digits in the port field.
*/
@ -147,46 +138,48 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
try {
ServerSettings settings = Transport.decodeTransportUri(mAccount.getTransportUri());
String username = settings.username;
String password = settings.password;
if (username != null) {
mUsernameView.setText(username);
mRequireLoginView.setChecked(true);
}
if (password != null) {
mPasswordView.setText(password);
}
updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
// 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 (savedInstanceState == null) {
// The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter
mCurrentAuthTypeViewPosition = mAuthTypeAdapter.getPosition(settings.authenticationType);
} else {
mCurrentAuthTypeViewPosition = savedInstanceState.getInt(STATE_AUTH_TYPE_POSITION);
}
mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false);
updateViewFromAuthType();
// Select currently configured security type
mSecurityTypeView.setSelection(settings.connectionSecurity.ordinal(), false);
if (savedInstanceState == null) {
mCurrentSecurityTypeViewPosition = settings.connectionSecurity.ordinal();
} else {
/*
* Updates the port when the user changes the security type. This allows
* us to show a reasonable default which the user can change.
*
* Note: It's important that we set the listener *after* an initial option has been
* selected by the code above. Otherwise the listener might be called after
* onCreate() has been processed and the current port value set later in this
* method is overridden with the default port for the selected security type.
*/
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
updatePortFromSecurityType();
}
/*
* Restore the spinner state now, before calling
* setOnItemSelectedListener(), thus avoiding a call to
* onItemSelected(). Then, when the system restores the state
* (again) in onRestoreInstanceState(), The system will see that
* the new state is the same as the current state (set here), so
* once again onItemSelected() will not be called.
*/
mCurrentSecurityTypeViewPosition = savedInstanceState.getInt(STATE_SECURITY_TYPE_POSITION);
}
mSecurityTypeView.setSelection(mCurrentSecurityTypeViewPosition, false);
@Override
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
});
if (settings.username != null && !settings.username.isEmpty()) {
mUsernameView.setText(settings.username);
mRequireLoginView.setChecked(true);
mRequireLoginSettingsView.setVisibility(View.VISIBLE);
}
if (settings.password != null) {
mPasswordView.setText(settings.password);
}
if (settings.clientCertificateAlias != null) {
mClientCertificateSpinner.setAlias(settings.clientCertificateAlias);
}
if (settings.host != null) {
mServerView.setText(settings.host);
@ -197,8 +190,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
} else {
updatePortFromSecurityType();
}
validateFields();
mCurrentPortViewSetting = mPortView.getText().toString();
} catch (Exception e) {
/*
* We should always be able to parse our own settings.
@ -208,27 +200,235 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
}
/**
* Called at the end of either {@code onCreate()} or
* {@code onRestoreInstanceState()}, after the views have been initialized,
* so that the listeners are not triggered during the view initialization.
* This avoids needless calls to {@code validateFields()} which is called
* immediately after this is called.
*/
private void initializeViewListeners() {
/*
* Updates the port when the user changes the security type. This allows
* us to show a reasonable default which the user can change.
*/
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
/*
* We keep our own record of the spinner state so we
* know for sure that onItemSelected() was called
* because of user input, not because of spinner
* state initialization. This assures that the port
* will not be replaced with a default value except
* on user input.
*/
if (mCurrentSecurityTypeViewPosition != position) {
updatePortFromSecurityType();
boolean isInsecure = (ConnectionSecurity.NONE == mSecurityTypeView.getSelectedItem());
boolean isAuthExternal = (AuthType.EXTERNAL == mAuthTypeView.getSelectedItem());
boolean loginNotRequired = !mRequireLoginView.isChecked();
/*
* If the user selects ConnectionSecurity.NONE, a
* warning would normally pop up if the authentication
* is AuthType.EXTERNAL (i.e., using client
* certificates). But such a warning is irrelevant if
* login is not required. So to avoid such a warning
* (generated in validateFields()) under those
* conditions, we change the (irrelevant) authentication
* method to PLAIN.
*/
if (isInsecure && isAuthExternal && loginNotRequired) {
OnItemSelectedListener onItemSelectedListener = mAuthTypeView.getOnItemSelectedListener();
mAuthTypeView.setOnItemSelectedListener(null);
mCurrentAuthTypeViewPosition = mAuthTypeAdapter.getPosition(AuthType.PLAIN);
mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false);
mAuthTypeView.setOnItemSelectedListener(onItemSelectedListener);
updateViewFromAuthType();
}
validateFields();
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
});
mAuthTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
if (mCurrentAuthTypeViewPosition == position) {
return;
}
updateViewFromAuthType();
validateFields();
AuthType selection = (AuthType) mAuthTypeView.getSelectedItem();
// Have the user select (or confirm) the client certificate
if (AuthType.EXTERNAL == selection) {
// This may again invoke validateFields()
mClientCertificateSpinner.chooseCertificate();
} else {
mPasswordView.requestFocus();
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
});
mRequireLoginView.setOnCheckedChangeListener(this);
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
mUsernameView.addTextChangedListener(validationTextWatcher);
mPasswordView.addTextChangedListener(validationTextWatcher);
mServerView.addTextChangedListener(validationTextWatcher);
mPortView.addTextChangedListener(validationTextWatcher);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(EXTRA_ACCOUNT, mAccount.getUuid());
outState.putInt(STATE_SECURITY_TYPE_POSITION, mCurrentSecurityTypeViewPosition);
outState.putInt(STATE_AUTH_TYPE_POSITION, mCurrentAuthTypeViewPosition);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (mRequireLoginView.isChecked()) {
mRequireLoginSettingsView.setVisibility(View.VISIBLE);
} else {
mRequireLoginSettingsView.setVisibility(View.GONE);
}
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
/*
* We didn't want the listeners active while the state was being restored
* because they could overwrite the restored port with a default port when
* the security type was restored.
*/
initializeViewListeners();
validateFields();
}
/**
* Shows/hides password field and client certificate spinner
*/
private void updateViewFromAuthType() {
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType);
if (isAuthTypeExternal) {
// hide password fields, show client certificate fields
mPasswordView.setVisibility(View.GONE);
mPasswordLabelView.setVisibility(View.GONE);
mClientCertificateLabelView.setVisibility(View.VISIBLE);
mClientCertificateSpinner.setVisibility(View.VISIBLE);
} else {
// show password fields, hide client certificate fields
mPasswordView.setVisibility(View.VISIBLE);
mPasswordLabelView.setVisibility(View.VISIBLE);
mClientCertificateLabelView.setVisibility(View.GONE);
mClientCertificateSpinner.setVisibility(View.GONE);
}
}
/**
* This is invoked only when the user makes changes to a widget, not when
* widgets are changed programmatically. (The logic is simpler when you know
* that this is the last thing called after an input change.)
*/
private void validateFields() {
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType);
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
boolean hasConnectionSecurity = (connectionSecurity != ConnectionSecurity.NONE);
if (isAuthTypeExternal && !hasConnectionSecurity) {
// Notify user of an invalid combination of AuthType.EXTERNAL & ConnectionSecurity.NONE
String toastText = getString(R.string.account_setup_outgoing_invalid_setting_combo_notice,
getString(R.string.account_setup_incoming_auth_type_label),
AuthType.EXTERNAL.toString(),
getString(R.string.account_setup_incoming_security_label),
ConnectionSecurity.NONE.toString());
Toast.makeText(this, toastText, Toast.LENGTH_LONG).show();
// Reset the views back to their previous settings without recursing through here again
OnItemSelectedListener onItemSelectedListener = mAuthTypeView.getOnItemSelectedListener();
mAuthTypeView.setOnItemSelectedListener(null);
mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false);
mAuthTypeView.setOnItemSelectedListener(onItemSelectedListener);
updateViewFromAuthType();
onItemSelectedListener = mSecurityTypeView.getOnItemSelectedListener();
mSecurityTypeView.setOnItemSelectedListener(null);
mSecurityTypeView.setSelection(mCurrentSecurityTypeViewPosition, false);
mSecurityTypeView.setOnItemSelectedListener(onItemSelectedListener);
updateAuthPlainTextFromSecurityType((ConnectionSecurity) mSecurityTypeView.getSelectedItem());
mPortView.removeTextChangedListener(validationTextWatcher);
mPortView.setText(mCurrentPortViewSetting);
mPortView.addTextChangedListener(validationTextWatcher);
authType = (AuthType) mAuthTypeView.getSelectedItem();
isAuthTypeExternal = (AuthType.EXTERNAL == authType);
connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
hasConnectionSecurity = (connectionSecurity != ConnectionSecurity.NONE);
} else {
mCurrentAuthTypeViewPosition = mAuthTypeView.getSelectedItemPosition();
mCurrentSecurityTypeViewPosition = mSecurityTypeView.getSelectedItemPosition();
mCurrentPortViewSetting = mPortView.getText().toString();
}
boolean hasValidCertificateAlias = mClientCertificateSpinner.getAlias() != null;
boolean hasValidUserName = Utility.requiredFieldValid(mUsernameView);
boolean hasValidPasswordSettings = hasValidUserName
&& !isAuthTypeExternal
&& Utility.requiredFieldValid(mPasswordView);
boolean hasValidExternalAuthSettings = hasValidUserName
&& isAuthTypeExternal
&& hasConnectionSecurity
&& hasValidCertificateAlias;
mNextButton
.setEnabled(
Utility.domainFieldValid(mServerView) &&
Utility.requiredFieldValid(mPortView) &&
(!mRequireLoginView.isChecked() ||
(Utility.requiredFieldValid(mUsernameView) &&
Utility.requiredFieldValid(mPasswordView))));
.setEnabled(Utility.domainFieldValid(mServerView)
&& Utility.requiredFieldValid(mPortView)
&& (!mRequireLoginView.isChecked()
|| hasValidPasswordSettings || hasValidExternalAuthSettings));
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
}
private void updatePortFromSecurityType() {
ConnectionSecurity securityType = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
mPortView.setText(getDefaultSmtpPort(securityType));
updateAuthPlainTextFromSecurityType(securityType);
// Remove listener so as not to trigger validateFields() which is called
// elsewhere as a result of user interaction.
mPortView.removeTextChangedListener(validationTextWatcher);
mPortView.setText(getDefaultSmtpPort(securityType));
mPortView.addTextChangedListener(validationTextWatcher);
}
private String getDefaultSmtpPort(ConnectionSecurity securityType) {
@ -276,17 +476,23 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
String uri;
String username = null;
String password = null;
String clientCertificateAlias = null;
AuthType authType = null;
if (mRequireLoginView.isChecked()) {
username = mUsernameView.getText().toString();
password = mPasswordView.getText().toString();
authType = (AuthType) mAuthTypeView.getSelectedItem();
if (AuthType.EXTERNAL == authType) {
clientCertificateAlias = mClientCertificateSpinner.getAlias();
} else {
password = mPasswordView.getText().toString();
}
}
String newHost = mServerView.getText().toString();
int newPort = Integer.parseInt(mPortView.getText().toString());
String type = SmtpTransport.TRANSPORT_TYPE;
ServerSettings server = new ServerSettings(type, newHost, newPort, securityType, authType, username, password);
ServerSettings server = new ServerSettings(type, newHost, newPort, securityType, authType, username, password, clientCertificateAlias);
uri = Transport.createTransportUri(server);
mAccount.deleteCertificate(newHost, newPort, CheckDirection.OUTGOING);
mAccount.setTransportUri(uri);
@ -312,4 +518,27 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
toast.show();
}
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
};
OnClientCertificateChangedListener clientCertificateChangedListener = new OnClientCertificateChangedListener() {
@Override
public void onClientCertificateChanged(String alias) {
validateFields();
}
};
}

View file

@ -2672,7 +2672,7 @@ public class MessagingController implements Runnable {
final NotificationManager nm = (NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
if (direction.equals(CheckDirection.INCOMING)) {
if (direction == CheckDirection.INCOMING) {
nm.cancel(null, K9.CERTIFICATE_EXCEPTION_NOTIFICATION_INCOMING + account.getAccountNumber());
} else {
nm.cancel(null, K9.CERTIFICATE_EXCEPTION_NOTIFICATION_OUTGOING + account.getAccountNumber());

View file

@ -1,5 +1,6 @@
package com.fsck.k9.fragment;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
@ -7,10 +8,13 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.util.Log;
import com.fsck.k9.K9;
public class ConfirmationDialogFragment extends DialogFragment implements OnClickListener,
OnCancelListener {
private ConfirmationDialogFragmentListener mListener;
private static final String ARG_DIALOG_ID = "dialog_id";
private static final String ARG_TITLE = "title";
@ -34,6 +38,11 @@ public class ConfirmationDialogFragment extends DialogFragment implements OnClic
return fragment;
}
public static ConfirmationDialogFragment newInstance(int dialogId, String title, String message,
String cancelText) {
return newInstance(dialogId, title, message, null, cancelText);
}
public interface ConfirmationDialogFragmentListener {
void doPositiveClick(int dialogId);
@ -53,8 +62,14 @@ public class ConfirmationDialogFragment extends DialogFragment implements OnClic
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(title);
builder.setMessage(message);
builder.setPositiveButton(confirmText, this);
builder.setNegativeButton(cancelText, this);
if (confirmText != null && cancelText != null) {
builder.setPositiveButton(confirmText, this);
builder.setNegativeButton(cancelText, this);
} else if (cancelText != null) {
builder.setNeutralButton(cancelText, this);
} else {
throw new RuntimeException("Set at least cancelText!");
}
return builder.create();
}
@ -70,6 +85,10 @@ public class ConfirmationDialogFragment extends DialogFragment implements OnClic
getListener().doNegativeClick(getDialogId());
break;
}
case DialogInterface.BUTTON_NEUTRAL: {
getListener().doNegativeClick(getDialogId());
break;
}
}
}
@ -83,7 +102,23 @@ public class ConfirmationDialogFragment extends DialogFragment implements OnClic
return getArguments().getInt(ARG_DIALOG_ID);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (ConfirmationDialogFragmentListener) activity;
} catch (ClassCastException e) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, activity.toString() + " did not implement ConfirmationDialogFragmentListener");
}
}
private ConfirmationDialogFragmentListener getListener() {
if (mListener != null) {
return mListener;
}
// fallback to getTargetFragment...
try {
return (ConfirmationDialogFragmentListener) getTargetFragment();
} catch (ClassCastException e) {

View file

@ -33,6 +33,8 @@ public enum AuthType {
CRAM_MD5(R.string.account_setup_auth_type_encrypted_password),
EXTERNAL(R.string.account_setup_auth_type_tls_client_certificate),
/*
* The following are obsolete authentication settings that were used with
* SMTP. They are no longer presented to the user as options, but they may
@ -44,7 +46,7 @@ public enum AuthType {
LOGIN(0);
static public ArrayAdapter<AuthType> getArrayAdapter(Context context) {
AuthType[] authTypes = {PLAIN, CRAM_MD5};
AuthType[] authTypes = new AuthType[]{PLAIN, CRAM_MD5, EXTERNAL};
ArrayAdapter<AuthType> authTypesAdapter = new ArrayAdapter<AuthType>(context,
android.R.layout.simple_spinner_item, authTypes);
authTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

View file

@ -5,6 +5,10 @@ import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLHandshakeException;
import android.security.KeyChainException;
public class CertificateValidationException extends MessagingException {
public static final long serialVersionUID = -1;
private X509Certificate[] mCertChain;
@ -12,7 +16,11 @@ public class CertificateValidationException extends MessagingException {
public CertificateValidationException(String message) {
super(message);
scanForCause();
/*
* Instances created without a Throwable parameter as a cause are
* presumed to need user attention.
*/
mNeedsUserAttention = true;
}
public CertificateValidationException(final String message, Throwable throwable) {
@ -23,16 +31,56 @@ public class CertificateValidationException extends MessagingException {
private void scanForCause() {
Throwable throwable = getCause();
/* user attention is required if the certificate was deemed invalid */
/*
* User attention is required if the server certificate was deemed
* invalid or if there was a problem with a client certificate.
*
* A CertificateException is known to be thrown by the default
* X509TrustManager.checkServerTrusted() if the server certificate
* doesn't validate. The cause of the CertificateException will be a
* CertPathValidatorException. However, it's unlikely those exceptions
* will be encountered here, because they are caught in
* SecureX509TrustManager.checkServerTrusted(), which throws a
* CertificateChainException instead (an extension of
* CertificateException).
*
* A CertificateChainException will likely result in (or, be the cause
* of) an SSLHandshakeException (an extension of SSLException).
*
* The various mail protocol handlers (IMAP, POP3, ...) will catch an
* SSLException and throw a CertificateValidationException (this class)
* with the SSLException as the cause. (They may also throw a
* CertificateValidationException when STARTTLS is not available, just
* for the purpose of triggering a user notification.)
*
* SSLHandshakeException is also known to occur if the *client*
* certificate was not accepted by the server (unknown CA, certificate
* expired, etc.). In this case, the SSLHandshakeException will not have
* a CertificateChainException as a cause.
*
* KeyChainException is known to occur if the device has no client
* certificate that's associated with the alias stored in the server
* settings.
*/
while (throwable != null
&& !(throwable instanceof CertPathValidatorException)
&& !(throwable instanceof CertificateException)) {
&& !(throwable instanceof CertificateException)
&& !(throwable instanceof KeyChainException)
&& !(throwable instanceof SSLHandshakeException)) {
throwable = throwable.getCause();
}
if (throwable != null) {
mNeedsUserAttention = true;
if (throwable instanceof CertificateChainException) {
// See if there is a server certificate chain attached to the SSLHandshakeException
if (throwable instanceof SSLHandshakeException) {
while (throwable != null && !(throwable instanceof CertificateChainException)) {
throwable = throwable.getCause();
}
}
if (throwable != null && throwable instanceof CertificateChainException) {
mCertChain = ((CertificateChainException) throwable).getCertChain();
}
}

View file

@ -1,5 +1,8 @@
package com.fsck.k9.mail;
import android.content.Context;
import android.widget.ArrayAdapter;
import com.fsck.k9.K9;
import com.fsck.k9.R;
@ -8,6 +11,17 @@ public enum ConnectionSecurity {
STARTTLS_REQUIRED(R.string.account_setup_incoming_security_tls_label),
SSL_TLS_REQUIRED(R.string.account_setup_incoming_security_ssl_label);
static public ArrayAdapter<ConnectionSecurity> getArrayAdapter(Context context) {
return getArrayAdapter(context, ConnectionSecurity.values());
}
static public ArrayAdapter<ConnectionSecurity> getArrayAdapter(Context context, ConnectionSecurity[] securityTypes) {
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(context,
android.R.layout.simple_spinner_item, securityTypes);
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
return securityTypesAdapter;
}
private final int mResourceId;
private ConnectionSecurity(int id) {

View file

@ -64,6 +64,14 @@ public class ServerSettings {
*/
public final String password;
/**
* The alias to retrieve a client certificate using Android 4.0 KeyChain API
* for TLS client certificate authentication with the server.
*
* {@code null} if not applicable for the store or transport.
*/
public final String clientCertificateAlias;
/**
* Store- or transport-specific settings as key/value pair.
*
@ -89,10 +97,12 @@ public class ServerSettings {
* see {@link ServerSettings#username}
* @param password
* see {@link ServerSettings#password}
* @param clientCertificateAlias
* see {@link ServerSettings#clientCertificateAlias}
*/
public ServerSettings(String type, String host, int port,
ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
String password) {
String password, String clientCertificateAlias) {
this.type = type;
this.host = host;
this.port = port;
@ -100,6 +110,7 @@ public class ServerSettings {
this.authenticationType = authenticationType;
this.username = username;
this.password = password;
this.clientCertificateAlias = clientCertificateAlias;
this.extra = null;
}
@ -120,12 +131,14 @@ public class ServerSettings {
* see {@link ServerSettings#username}
* @param password
* see {@link ServerSettings#password}
* @param clientCertificateAlias
* see {@link ServerSettings#clientCertificateAlias}
* @param extra
* see {@link ServerSettings#extra}
*/
public ServerSettings(String type, String host, int port,
ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
String password, Map<String, String> extra) {
String password, String clientCertificateAlias, Map<String, String> extra) {
this.type = type;
this.host = host;
this.port = port;
@ -133,6 +146,7 @@ public class ServerSettings {
this.authenticationType = authenticationType;
this.username = username;
this.password = password;
this.clientCertificateAlias = clientCertificateAlias;
this.extra = (extra != null) ?
Collections.unmodifiableMap(new HashMap<String, String>(extra)) : null;
}
@ -153,6 +167,7 @@ public class ServerSettings {
authenticationType = null;
username = null;
password = null;
clientCertificateAlias = null;
extra = null;
}
@ -173,6 +188,11 @@ public class ServerSettings {
public ServerSettings newPassword(String newPassword) {
return new ServerSettings(type, host, port, connectionSecurity, authenticationType,
username, newPassword);
username, newPassword, clientCertificateAlias);
}
public ServerSettings newClientCertificateAlias(String newAlias) {
return new ServerSettings(type, host, port, connectionSecurity, AuthType.EXTERNAL,
username, password, newAlias);
}
}

View file

@ -25,9 +25,7 @@ import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CertificateException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@ -50,9 +48,7 @@ import java.util.regex.Pattern;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import org.apache.commons.io.IOUtils;
@ -100,8 +96,7 @@ import com.fsck.k9.mail.store.ImapResponseParser.ImapList;
import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse;
import com.fsck.k9.mail.store.imap.ImapUtility;
import com.fsck.k9.mail.transport.imap.ImapSettings;
import com.fsck.k9.net.ssl.TrustManagerFactory;
import com.fsck.k9.net.ssl.TrustedSocketFactory;
import com.fsck.k9.net.ssl.SslHelper;
import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZOutputStream;
@ -126,6 +121,7 @@ public class ImapStore extends Store {
private static final String CAPABILITY_IDLE = "IDLE";
private static final String CAPABILITY_AUTH_CRAM_MD5 = "AUTH=CRAM-MD5";
private static final String CAPABILITY_AUTH_PLAIN = "AUTH=PLAIN";
private static final String CAPABILITY_AUTH_EXTERNAL = "AUTH=EXTERNAL";
private static final String CAPABILITY_LOGINDISABLED = "LOGINDISABLED";
private static final String COMMAND_IDLE = "IDLE";
private static final String CAPABILITY_NAMESPACE = "NAMESPACE";
@ -158,6 +154,7 @@ public class ImapStore extends Store {
AuthType authenticationType = null;
String username = null;
String password = null;
String clientCertificateAlias = null;
String pathPrefix = null;
boolean autoDetectNamespace = true;
@ -213,10 +210,15 @@ public class ImapStore extends Store {
authenticationType = AuthType.PLAIN;
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
} else {
} else if (userInfoParts.length == 3) {
authenticationType = AuthType.valueOf(userInfoParts[0]);
username = URLDecoder.decode(userInfoParts[1], "UTF-8");
password = URLDecoder.decode(userInfoParts[2], "UTF-8");
if (AuthType.EXTERNAL == authenticationType) {
clientCertificateAlias = URLDecoder.decode(userInfoParts[2], "UTF-8");
} else {
password = URLDecoder.decode(userInfoParts[2], "UTF-8");
}
}
} catch (UnsupportedEncodingException enc) {
// This shouldn't happen since the encoding is hardcoded to UTF-8
@ -243,7 +245,7 @@ public class ImapStore extends Store {
}
return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, username,
password, autoDetectNamespace, pathPrefix);
password, clientCertificateAlias, autoDetectNamespace, pathPrefix);
}
/**
@ -260,10 +262,13 @@ public class ImapStore extends Store {
public static String createUri(ServerSettings server) {
String userEnc;
String passwordEnc;
String clientCertificateAliasEnc;
try {
userEnc = URLEncoder.encode(server.username, "UTF-8");
passwordEnc = (server.password != null) ?
URLEncoder.encode(server.password, "UTF-8") : "";
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
}
catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Could not encode username or password", e);
@ -284,8 +289,12 @@ public class ImapStore extends Store {
}
AuthType authType = server.authenticationType;
String userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
String userInfo;
if (authType == AuthType.EXTERNAL) {
userInfo = authType.name() + ":" + userEnc + ":" + clientCertificateAliasEnc;
} else {
userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
}
try {
Map<String, String> extra = server.getExtra();
String path = null;
@ -320,10 +329,10 @@ public class ImapStore extends Store {
public final String pathPrefix;
protected ImapStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
AuthType authenticationType, String username, String password,
AuthType authenticationType, String username, String password, String clientCertificateAlias,
boolean autodetectNamespace, String pathPrefix) {
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
password);
password, clientCertificateAlias);
this.autoDetectNamespace = autodetectNamespace;
this.pathPrefix = pathPrefix;
}
@ -339,7 +348,7 @@ public class ImapStore extends Store {
@Override
public ServerSettings newPassword(String newPassword) {
return new ImapStoreSettings(host, port, connectionSecurity, authenticationType,
username, newPassword, autoDetectNamespace, pathPrefix);
username, newPassword, clientCertificateAlias, autoDetectNamespace, pathPrefix);
}
}
@ -348,6 +357,7 @@ public class ImapStore extends Store {
private int mPort;
private String mUsername;
private String mPassword;
private String mClientCertificateAlias;
private ConnectionSecurity mConnectionSecurity;
private AuthType mAuthType;
private volatile String mPathPrefix;
@ -386,6 +396,11 @@ public class ImapStore extends Store {
return mPassword;
}
@Override
public String getClientCertificateAlias() {
return mClientCertificateAlias;
}
@Override
public boolean useCompression(final int type) {
return mAccount.useCompression(type);
@ -458,6 +473,7 @@ public class ImapStore extends Store {
mAuthType = settings.authenticationType;
mUsername = settings.username;
mPassword = settings.password;
mClientCertificateAlias = settings.clientCertificateAlias;
// Make extra sure mPathPrefix is null if "auto-detect namespace" is configured
mPathPrefix = (settings.autoDetectNamespace) ? null : settings.pathPrefix;
@ -2419,14 +2435,8 @@ public class ImapStore extends Store {
mSettings.getPort());
if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext
.init(null,
new TrustManager[] { TrustManagerFactory.get(
mSettings.getHost(),
mSettings.getPort()) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext);
mSocket = SslHelper.createSslSocket(mSettings.getHost(),
mSettings.getPort(), mSettings.getClientCertificateAlias());
} else {
mSocket = new Socket();
}
@ -2475,14 +2485,9 @@ public class ImapStore extends Store {
// STARTTLS
executeSimpleCommand("STARTTLS");
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(
mSettings.getHost(),
mSettings.getPort()) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket,
mSettings.getHost(), mSettings.getPort(), true);
mSocket = SslHelper.createStartTlsSocket(mSocket,
mSettings.getHost(), mSettings.getPort(), true,
mSettings.getClientCertificateAlias());
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket
.getInputStream(), 1024));
@ -2505,8 +2510,7 @@ public class ImapStore extends Store {
* "STARTTLS (if available)" setting.
*/
throw new CertificateValidationException(
"STARTTLS connection security not available",
new CertificateException());
"STARTTLS connection security not available");
}
}
@ -2531,6 +2535,15 @@ public class ImapStore extends Store {
}
break;
case EXTERNAL:
if (hasCapability(CAPABILITY_AUTH_EXTERNAL)) {
saslAuthExternal();
} else {
// Provide notification to user of a problem authenticating using client certificates
throw new CertificateValidationException(K9.app.getString(R.string.auth_external_error));
}
break;
default:
throw new MessagingException(
"Unhandled authentication method found in the server settings (bug).");
@ -2630,7 +2643,6 @@ public class ImapStore extends Store {
}
}
} catch (SSLException e) {
throw new CertificateValidationException(e.getMessage(), e);
} catch (GeneralSecurityException gse) {
@ -2713,6 +2725,23 @@ public class ImapStore extends Store {
}
}
private void saslAuthExternal() throws IOException, MessagingException {
try {
receiveCapabilities(executeSimpleCommand(
String.format("AUTHENTICATE EXTERNAL %s",
Utility.base64Encode(mSettings.getUsername())), false));
} catch (ImapException e) {
/*
* Provide notification to the user of a problem authenticating
* using client certificates. We don't use an
* AuthenticationFailedException because that would trigger a
* "Username or password incorrect" notification in
* AccountSetupCheckSettings.
*/
throw new CertificateValidationException(e.getMessage());
}
}
protected ImapResponse readContinuationResponse(String tag)
throws IOException, MessagingException {
ImapResponse response;

View file

@ -5,27 +5,22 @@ import android.util.Log;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.controller.MessageRetrievalListener;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.*;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.filter.Hex;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.net.ssl.TrustManagerFactory;
import com.fsck.k9.net.ssl.TrustedSocketFactory;
import com.fsck.k9.net.ssl.SslHelper;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import java.io.*;
import java.net.*;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
@ -59,15 +54,16 @@ public class Pop3Store extends Store {
private static final String SASL_CAPABILITY = "SASL";
private static final String AUTH_PLAIN_CAPABILITY = "PLAIN";
private static final String AUTH_CRAM_MD5_CAPABILITY = "CRAM-MD5";
private static final String AUTH_EXTERNAL_CAPABILITY = "EXTERNAL";
/**
* Decodes a Pop3Store URI.
*
* <p>Possible forms:</p>
* <pre>
* pop3://user:password@server:port ConnectionSecurity.NONE
* pop3+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
* pop3+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
* pop3://auth:user:password@server:port ConnectionSecurity.NONE
* pop3+tls+://auth:user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
* pop3+ssl+://auth:user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
* </pre>
*/
public static ServerSettings decodeUri(String uri) {
@ -76,6 +72,7 @@ public class Pop3Store extends Store {
ConnectionSecurity connectionSecurity;
String username = null;
String password = null;
String clientCertificateAlias = null;
URI pop3Uri;
try {
@ -131,7 +128,11 @@ public class Pop3Store extends Store {
}
username = URLDecoder.decode(userInfoParts[userIndex], "UTF-8");
if (userInfoParts.length > passwordIndex) {
password = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
if (authType == AuthType.EXTERNAL) {
clientCertificateAlias = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
} else {
password = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
}
}
} catch (UnsupportedEncodingException enc) {
// This shouldn't happen since the encoding is hardcoded to UTF-8
@ -140,7 +141,7 @@ public class Pop3Store extends Store {
}
return new ServerSettings(STORE_TYPE, host, port, connectionSecurity, authType, username,
password);
password, clientCertificateAlias);
}
/**
@ -157,10 +158,13 @@ public class Pop3Store extends Store {
public static String createUri(ServerSettings server) {
String userEnc;
String passwordEnc;
String clientCertificateAliasEnc;
try {
userEnc = URLEncoder.encode(server.username, "UTF-8");
passwordEnc = (server.password != null) ?
URLEncoder.encode(server.password, "UTF-8") : "";
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
}
catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Could not encode username or password", e);
@ -180,7 +184,14 @@ public class Pop3Store extends Store {
break;
}
String userInfo = server.authenticationType.name() + ":" + userEnc + ":" + passwordEnc;
AuthType authType = server.authenticationType;
String userInfo;
if (AuthType.EXTERNAL == authType) {
userInfo = authType.name() + ":" + userEnc + ":" + clientCertificateAliasEnc;
} else {
userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
}
try {
return new URI(scheme, userInfo, server.host, server.port, null, null,
null).toString();
@ -194,6 +205,7 @@ public class Pop3Store extends Store {
private int mPort;
private String mUsername;
private String mPassword;
private String mClientCertificateAlias;
private AuthType mAuthType;
private ConnectionSecurity mConnectionSecurity;
private HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
@ -224,6 +236,7 @@ public class Pop3Store extends Store {
mUsername = settings.username;
mPassword = settings.password;
mClientCertificateAlias = settings.clientCertificateAlias;
mAuthType = settings.authenticationType;
}
@ -301,11 +314,7 @@ public class Pop3Store extends Store {
try {
SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(mHost,
mPort) }, new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext);
mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
} else {
mSocket = new Socket();
}
@ -327,13 +336,8 @@ public class Pop3Store extends Store {
if (mCapabilities.stls) {
executeSimpleCommand(STLS_COMMAND);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(
mHost, mPort) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
mPort, true);
mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
mClientCertificateAlias);
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
@ -350,8 +354,7 @@ public class Pop3Store extends Store {
* "STARTTLS (if available)" setting.
*/
throw new CertificateValidationException(
"STARTTLS connection security not available",
new CertificateException());
"STARTTLS connection security not available");
}
}
@ -372,6 +375,15 @@ public class Pop3Store extends Store {
}
break;
case EXTERNAL:
if (mCapabilities.external) {
authExternal();
} else {
// Provide notification to user of a problem authenticating using client certificates
throw new CertificateValidationException(K9.app.getString(R.string.auth_external_error));
}
break;
default:
throw new MessagingException(
"Unhandled authentication method found in the server settings (bug).");
@ -455,6 +467,24 @@ public class Pop3Store extends Store {
}
}
private void authExternal() throws MessagingException {
try {
executeSimpleCommand(
String.format("AUTH EXTERNAL %s",
Utility.base64Encode(mUsername)), false);
} catch (Pop3ErrorResponse e) {
/*
* Provide notification to the user of a problem authenticating
* using client certificates. We don't use an
* AuthenticationFailedException because that would trigger a
* "Username or password incorrect" notification in
* AccountSetupCheckSettings.
*/
throw new CertificateValidationException(
"POP3 client certificate authentication failed: " + e.getMessage(), e);
}
}
@Override
public boolean isOpen() {
return (mIn != null && mOut != null && mSocket != null
@ -1046,6 +1076,8 @@ public class Pop3Store extends Store {
capabilities.authPlain = true;
} else if (response.equals(AUTH_CRAM_MD5_CAPABILITY)) {
capabilities.cramMD5 = true;
} else if (response.equals(AUTH_EXTERNAL_CAPABILITY)) {
capabilities.external = true;
}
}
} catch (MessagingException e) {
@ -1193,15 +1225,17 @@ public class Pop3Store extends Store {
public boolean stls;
public boolean top;
public boolean uidl;
public boolean external;
@Override
public String toString() {
return String.format("CRAM-MD5 %b, PLAIN %b, STLS %b, TOP %b, UIDL %b",
return String.format("CRAM-MD5 %b, PLAIN %b, STLS %b, TOP %b, UIDL %b, EXTERNAL %b",
cramMD5,
authPlain,
stls,
top,
uidl);
uidl,
external);
}
}

View file

@ -7,9 +7,9 @@ import com.fsck.k9.K9;
import com.fsck.k9.controller.MessageRetrievalListener;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.*;
import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
import com.fsck.k9.mail.internet.MimeMessage;
import org.apache.commons.io.IOUtils;
import org.apache.http.*;
import org.apache.http.client.CookieStore;
@ -36,6 +36,7 @@ import javax.net.ssl.SSLException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
@ -178,7 +179,7 @@ public class WebDavStore extends Store {
}
return new WebDavStoreSettings(host, port, connectionSecurity, null, username, password,
alias, path, authPath, mailboxPath);
null, alias, path, authPath, mailboxPath);
}
/**
@ -257,10 +258,10 @@ public class WebDavStore extends Store {
public final String mailboxPath;
protected WebDavStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
AuthType authenticationType, String username, String password, String alias,
AuthType authenticationType, String username, String password, String clientCertificateAlias, String alias,
String path, String authPath, String mailboxPath) {
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
password);
password, clientCertificateAlias);
this.alias = alias;
this.path = path;
this.authPath = authPath;
@ -280,7 +281,7 @@ public class WebDavStore extends Store {
@Override
public ServerSettings newPassword(String newPassword) {
return new WebDavStoreSettings(host, port, connectionSecurity, authenticationType,
username, newPassword, alias, path, authPath, mailboxPath);
username, newPassword, clientCertificateAlias, alias, path, authPath, mailboxPath);
}
}

View file

@ -2,23 +2,22 @@
package com.fsck.k9.mail.transport;
import android.util.Log;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.*;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
import com.fsck.k9.mail.filter.LineWrapOutputStream;
import com.fsck.k9.mail.filter.PeekableInputStream;
import com.fsck.k9.mail.filter.SmtpDataStuffing;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.LocalStore.LocalMessage;
import com.fsck.k9.net.ssl.TrustManagerFactory;
import com.fsck.k9.net.ssl.TrustedSocketFactory;
import com.fsck.k9.net.ssl.SslHelper;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@ -27,9 +26,6 @@ import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.*;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.util.*;
public class SmtpTransport extends Transport {
@ -37,21 +33,24 @@ public class SmtpTransport extends Transport {
/**
* Decodes a SmtpTransport URI.
*
* NOTE: In contrast to ImapStore and Pop3Store, the authType is appended at the end!
*
* <p>Possible forms:</p>
* <pre>
* smtp://user:password@server:port ConnectionSecurity.NONE
* smtp+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
* smtp+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
* smtp://user:password:auth@server:port ConnectionSecurity.NONE
* smtp+tls+://user:password:auth@server:port ConnectionSecurity.STARTTLS_REQUIRED
* smtp+ssl+://user:password:auth@server:port ConnectionSecurity.SSL_TLS_REQUIRED
* </pre>
*/
public static ServerSettings decodeUri(String uri) {
String host;
int port;
ConnectionSecurity connectionSecurity;
AuthType authType = AuthType.PLAIN;
AuthType authType = null;
String username = null;
String password = null;
String clientCertificateAlias = null;
URI smtpUri;
try {
@ -95,14 +94,22 @@ public class SmtpTransport extends Transport {
if (smtpUri.getUserInfo() != null) {
try {
String[] userInfoParts = smtpUri.getUserInfo().split(":");
if (userInfoParts.length > 0) {
if (userInfoParts.length == 1) {
authType = AuthType.PLAIN;
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
} else if (userInfoParts.length == 2) {
authType = AuthType.PLAIN;
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
}
if (userInfoParts.length > 1) {
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
}
if (userInfoParts.length > 2) {
} 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 == AuthType.EXTERNAL) {
clientCertificateAlias = URLDecoder.decode(userInfoParts[1], "UTF-8");
} else {
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
}
}
} catch (UnsupportedEncodingException enc) {
// This shouldn't happen since the encoding is hardcoded to UTF-8
@ -111,7 +118,7 @@ public class SmtpTransport extends Transport {
}
return new ServerSettings(TRANSPORT_TYPE, host, port, connectionSecurity,
authType, username, password);
authType, username, password, clientCertificateAlias);
}
/**
@ -128,11 +135,14 @@ public class SmtpTransport extends Transport {
public static String createUri(ServerSettings server) {
String userEnc;
String passwordEnc;
String clientCertificateAliasEnc;
try {
userEnc = (server.username != null) ?
URLEncoder.encode(server.username, "UTF-8") : "";
passwordEnc = (server.password != null) ?
URLEncoder.encode(server.password, "UTF-8") : "";
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
}
catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Could not encode username or password", e);
@ -152,10 +162,17 @@ public class SmtpTransport extends Transport {
break;
}
String userInfo = userEnc + ":" + passwordEnc;
String userInfo = null;
AuthType authType = server.authenticationType;
// NOTE: authType is append at last item, in contrast to ImapStore and Pop3Store!
if (authType != null) {
userInfo += ":" + authType.name();
if (AuthType.EXTERNAL == authType) {
userInfo = userEnc + ":" + clientCertificateAliasEnc + ":" + authType.name();
} else {
userInfo = userEnc + ":" + passwordEnc + ":" + authType.name();
}
} else {
userInfo = userEnc + ":" + passwordEnc;
}
try {
return new URI(scheme, userInfo, server.host, server.port, null, null,
@ -170,6 +187,7 @@ public class SmtpTransport extends Transport {
int mPort;
String mUsername;
String mPassword;
String mClientCertificateAlias;
AuthType mAuthType;
ConnectionSecurity mConnectionSecurity;
Socket mSocket;
@ -194,6 +212,7 @@ public class SmtpTransport extends Transport {
mAuthType = settings.authenticationType;
mUsername = settings.username;
mPassword = settings.password;
mClientCertificateAlias = settings.clientCertificateAlias;
}
@Override
@ -205,12 +224,7 @@ public class SmtpTransport extends Transport {
try {
SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort);
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(
mHost, mPort) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext);
mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
secureConnection = true;
} else {
@ -264,12 +278,9 @@ public class SmtpTransport extends Transport {
if (extensions.containsKey("STARTTLS")) {
executeSimpleCommand("STARTTLS");
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(mHost,
mPort) }, new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
mPort, true);
mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
mClientCertificateAlias);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
1024));
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
@ -288,19 +299,20 @@ public class SmtpTransport extends Transport {
* "STARTTLS (if available)" setting.
*/
throw new CertificateValidationException(
"STARTTLS connection security not available",
new CertificateException());
"STARTTLS connection security not available");
}
}
boolean authLoginSupported = false;
boolean authPlainSupported = false;
boolean authCramMD5Supported = false;
boolean authExternalSupported = false;
if (extensions.containsKey("AUTH")) {
List<String> saslMech = Arrays.asList(extensions.get("AUTH").split(" "));
authLoginSupported = saslMech.contains("LOGIN");
authPlainSupported = saslMech.contains("PLAIN");
authCramMD5Supported = saslMech.contains("CRAM-MD5");
authExternalSupported = saslMech.contains("EXTERNAL");
}
if (extensions.containsKey("SIZE")) {
try {
@ -312,8 +324,9 @@ public class SmtpTransport extends Transport {
}
}
if (mUsername != null && mUsername.length() > 0 &&
mPassword != null && mPassword.length() > 0) {
if (mUsername != null
&& mUsername.length() > 0
&& (mPassword != null && mPassword.length() > 0 || AuthType.EXTERNAL == mAuthType)) {
switch (mAuthType) {
@ -342,6 +355,24 @@ public class SmtpTransport extends Transport {
}
break;
case EXTERNAL:
if (authExternalSupported) {
saslAuthExternal(mUsername);
} else {
/*
* Some SMTP servers are known to provide no error
* indication when a client certificate fails to
* validate, other than to not offer the AUTH EXTERNAL
* capability.
*
* So, we treat it is an error to not offer AUTH
* EXTERNAL when using client certificates. That way, the
* user can be notified of a problem during account setup.
*/
throw new MessagingException(K9.app.getString(R.string.auth_external_error));
}
break;
/*
* AUTOMATIC is an obsolete option which is unavailable to users,
* but it still may exist in a user's settings from a previous
@ -634,6 +665,12 @@ public class SmtpTransport extends Transport {
* Read lines as long as the length is 4 or larger, e.g. "220-banner text here".
* Shorter lines are either errors of contain only a reply code. Those cases will
* be handled by checkLine() below.
*
* TODO: All responses should be checked to confirm that they start with a valid
* reply code, and that the reply code is appropriate for the command being executed.
* That means it should either be a 2xx code (generally) or a 3xx code in special cases
* (e.g., DATA & AUTH LOGIN commands). Reply codes should be made available as part of
* the returned object.
*/
String line = readLine();
while (line.length() >= 4) {
@ -677,29 +714,32 @@ public class SmtpTransport extends Transport {
AuthenticationFailedException, IOException {
try {
executeSimpleCommand("AUTH LOGIN");
executeSimpleCommand(new String(Base64.encodeBase64(username.getBytes())), true);
executeSimpleCommand(new String(Base64.encodeBase64(password.getBytes())), true);
} catch (MessagingException me) {
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
throw new AuthenticationFailedException("AUTH LOGIN failed (" + me.getMessage()
+ ")");
executeSimpleCommand(Utility.base64Encode(username), true);
executeSimpleCommand(Utility.base64Encode(password), true);
} catch (NegativeSmtpReplyException exception) {
if (exception.getReplyCode() == 535) {
// Authentication credentials invalid
throw new AuthenticationFailedException("AUTH LOGIN failed ("
+ exception.getMessage() + ")");
} else {
throw exception;
}
throw me;
}
}
private void saslAuthPlain(String username, String password) throws MessagingException,
AuthenticationFailedException, IOException {
byte[] data = ("\000" + username + "\000" + password).getBytes();
data = new Base64().encode(data);
String data = Utility.base64Encode("\000" + username + "\000" + password);
try {
executeSimpleCommand("AUTH PLAIN " + new String(data), true);
} catch (MessagingException me) {
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
throw new AuthenticationFailedException("AUTH PLAIN failed (" + me.getMessage()
+ ")");
executeSimpleCommand("AUTH PLAIN " + data, true);
} catch (NegativeSmtpReplyException exception) {
if (exception.getReplyCode() == 535) {
// Authentication credentials invalid
throw new AuthenticationFailedException("AUTH PLAIN failed ("
+ exception.getMessage() + ")");
} else {
throw exception;
}
throw me;
}
}
@ -717,10 +757,21 @@ public class SmtpTransport extends Transport {
try {
executeSimpleCommand(b64CRAMString, true);
} catch (NegativeSmtpReplyException exception) {
throw new AuthenticationFailedException(exception.getMessage(), exception);
if (exception.getReplyCode() == 535) {
// Authentication credentials invalid
throw new AuthenticationFailedException(exception.getMessage(), exception);
} else {
throw exception;
}
}
}
private void saslAuthExternal(String username) throws MessagingException, IOException {
executeSimpleCommand(
String.format("AUTH EXTERNAL %s",
Utility.base64Encode(username)), false);
}
/**
* Exception that is thrown when the server sends a negative reply (reply codes 4xx or 5xx).
*/

View file

@ -21,6 +21,8 @@ public interface ImapSettings {
String getPassword();
String getClientCertificateAlias();
boolean useCompression(int type);
String getPathPrefix();

View file

@ -0,0 +1,216 @@
package com.fsck.k9.net.ssl;
import java.net.Socket;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.security.auth.x500.X500Principal;
import android.content.Context;
import android.os.Build;
import android.security.KeyChain;
import android.security.KeyChainException;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.MessagingException;
/**
* For client certificate authentication! Provide private keys and certificates
* during the TLS handshake using the Android 4.0 KeyChain API.
*/
public class KeyChainKeyManager extends X509ExtendedKeyManager {
private static PrivateKey sClientCertificateReferenceWorkaround;
private static synchronized void savePrivateKeyReference(PrivateKey privateKey) {
if (sClientCertificateReferenceWorkaround == null) {
sClientCertificateReferenceWorkaround = privateKey;
}
}
private final String mAlias;
private final X509Certificate[] mChain;
private final PrivateKey mPrivateKey;
/**
* @param alias Must not be null nor empty
* @throws MessagingException
* Indicates an error in retrieving the certificate for the alias
* (likely because the alias is invalid or the certificate was deleted)
*/
public KeyChainKeyManager(Context context, String alias) throws MessagingException {
mAlias = alias;
try {
mChain = fetchCertificateChain(context, alias);
mPrivateKey = fetchPrivateKey(context, alias);
} catch (KeyChainException e) {
// The certificate was possibly deleted. Notify user of error.
final String message = context.getString(
R.string.client_certificate_retrieval_failure, alias);
throw new CertificateValidationException(message, e);
} catch (InterruptedException e) {
final String message = context.getString(
R.string.client_certificate_retrieval_failure, alias);
throw new MessagingException(message, e);
}
}
private X509Certificate[] fetchCertificateChain(Context context, String alias)
throws KeyChainException, InterruptedException, MessagingException {
X509Certificate[] chain = KeyChain.getCertificateChain(context, alias);
if (chain == null || chain.length == 0) {
throw new MessagingException("No certificate chain found for: " + alias);
}
try {
for (X509Certificate certificate : chain) {
certificate.checkValidity();
}
} catch (CertificateException e) {
// Client certificate has expired or is not yet valid
throw new CertificateValidationException(context.getString(R.string.client_certificate_expired, alias, e.toString()));
}
return chain;
}
private PrivateKey fetchPrivateKey(Context context, String alias) throws KeyChainException,
InterruptedException, MessagingException {
PrivateKey privateKey = KeyChain.getPrivateKey(context, alias);
if (privateKey == null) {
throw new MessagingException("No private key found for: " + alias);
}
/*
* We need to keep reference to the first private key retrieved 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
*/
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
savePrivateKeyReference(privateKey);
}
return privateKey;
}
@Override
public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
return chooseAlias(keyTypes, issuers);
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
return (mAlias.equals(alias) ? mChain : null);
}
@Override
public PrivateKey getPrivateKey(String alias) {
return (mAlias.equals(alias) ? mPrivateKey : null);
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return chooseAlias(new String[] { keyType }, issuers);
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
final String al = chooseAlias(new String[] { keyType }, issuers);
return (al == null ? null : new String[] { al });
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
final String al = chooseAlias(new String[] { keyType }, issuers);
return (al == null ? null : new String[] { al });
}
@Override
public String chooseEngineClientAlias(String[] keyTypes, Principal[] issuers, SSLEngine engine) {
return chooseAlias(keyTypes, issuers);
}
@Override
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
return chooseAlias(new String[] { keyType }, issuers);
}
private String chooseAlias(String[] keyTypes, Principal[] issuers) {
if (keyTypes == null || keyTypes.length == 0) {
return null;
}
final X509Certificate cert = mChain[0];
final String certKeyAlg = cert.getPublicKey().getAlgorithm();
final String certSigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
for (String keyAlgorithm : keyTypes) {
if (keyAlgorithm == null) {
continue;
}
final String sigAlgorithm;
// handle cases like EC_EC and EC_RSA
int index = keyAlgorithm.indexOf('_');
if (index == -1) {
sigAlgorithm = null;
} else {
sigAlgorithm = keyAlgorithm.substring(index + 1);
keyAlgorithm = keyAlgorithm.substring(0, index);
}
// key algorithm does not match
if (!certKeyAlg.equals(keyAlgorithm)) {
continue;
}
/*
* TODO find a more reliable test for signature
* algorithm. Unfortunately value varies with
* provider. For example for "EC" it could be
* "SHA1WithECDSA" or simply "ECDSA".
*/
// sig algorithm does not match
if (sigAlgorithm != null && certSigAlg != null
&& !certSigAlg.contains(sigAlgorithm)) {
continue;
}
// no issuers to match
if (issuers == null || issuers.length == 0) {
return mAlias;
}
List<Principal> issuersList = Arrays.asList(issuers);
// check that a certificate in the chain was issued by one of the specified issuers
for (X509Certificate certFromChain : mChain) {
/*
* Note use of X500Principal from
* getIssuerX500Principal as opposed to Principal
* from getIssuerDN. Principal.equals test does
* not work in the case where
* xcertFromChain.getIssuerDN is a bouncycastle
* org.bouncycastle.jce.X509Principal.
*/
X500Principal issuerFromChain = certFromChain.getIssuerX500Principal();
if (issuersList.contains(issuerFromChain)) {
return mAlias;
}
}
Log.w(K9.LOG_TAG, "Client certificate " + mAlias + " not issued by any of the requested issuers");
return null;
}
Log.w(K9.LOG_TAG, "Client certificate " + mAlias + " does not match any of the requested key types");
return null;
}
}

View file

@ -0,0 +1,81 @@
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.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.mail.MessagingException;
/**
* Helper class to create SSL sockets with support for client certificate
* authentication
*/
public class SslHelper {
private static SSLContext createSslContext(String host, int port, String clientCertificateAlias)
throws NoSuchAlgorithmException, KeyManagementException, MessagingException {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "createSslContext: Client certificate alias: "
+ clientCertificateAlias);
KeyManager[] keyManagers;
if (clientCertificateAlias == null || clientCertificateAlias.isEmpty()) {
keyManagers = null;
} else {
keyManagers = new KeyManager[] { new KeyChainKeyManager(K9.app, clientCertificateAlias) };
}
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);
}
}

View file

@ -61,6 +61,7 @@ public class SettingsExporter {
public static final String CONNECTION_SECURITY_ELEMENT = "connection-security";
public static final String AUTHENTICATION_TYPE_ELEMENT = "authentication-type";
public static final String USERNAME_ELEMENT = "username";
public static final String CLIENT_CERTIFICATE_ALIAS_ELEMENT = "client-cert-alias";
public static final String PASSWORD_ELEMENT = "password";
public static final String EXTRA_ELEMENT = "extra";
public static final String IDENTITIES_ELEMENT = "identities";
@ -229,9 +230,14 @@ public class SettingsExporter {
if (incoming.port != -1) {
writeElement(serializer, PORT_ELEMENT, Integer.toString(incoming.port));
}
writeElement(serializer, CONNECTION_SECURITY_ELEMENT, incoming.connectionSecurity.name());
writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType.name());
if (incoming.connectionSecurity != null) {
writeElement(serializer, CONNECTION_SECURITY_ELEMENT, incoming.connectionSecurity.name());
}
if (incoming.authenticationType != null) {
writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType.name());
}
writeElement(serializer, USERNAME_ELEMENT, incoming.username);
writeElement(serializer, CLIENT_CERTIFICATE_ALIAS_ELEMENT, incoming.clientCertificateAlias);
// XXX For now we don't export the password
//writeElement(serializer, PASSWORD_ELEMENT, incoming.password);
@ -256,9 +262,14 @@ public class SettingsExporter {
if (outgoing.port != -1) {
writeElement(serializer, PORT_ELEMENT, Integer.toString(outgoing.port));
}
writeElement(serializer, CONNECTION_SECURITY_ELEMENT, outgoing.connectionSecurity.name());
writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType.name());
if (outgoing.connectionSecurity != null) {
writeElement(serializer, CONNECTION_SECURITY_ELEMENT, outgoing.connectionSecurity.name());
}
if (outgoing.authenticationType != null) {
writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType.name());
}
writeElement(serializer, USERNAME_ELEMENT, outgoing.username);
writeElement(serializer, CLIENT_CERTIFICATE_ALIAS_ELEMENT, outgoing.clientCertificateAlias);
// XXX For now we don't export the password
//writeElement(serializer, PASSWORD_ELEMENT, outgoing.password);

View file

@ -379,9 +379,10 @@ public class SettingsImporter {
String storeUri = Store.createStoreUri(incoming);
putString(editor, accountKeyPrefix + Account.STORE_URI_KEY, Utility.base64Encode(storeUri));
// Mark account as disabled if the settings file didn't contain a password
boolean createAccountDisabled = (incoming.password == null ||
incoming.password.isEmpty());
// Mark account as disabled if the AuthType isn't EXTERNAL and the
// settings file didn't contain a password
boolean createAccountDisabled = AuthType.EXTERNAL != incoming.authenticationType &&
(incoming.password == null || incoming.password.isEmpty());
if (account.outgoing == null && !WebDavStore.STORE_TYPE.equals(account.incoming.type)) {
// All account types except WebDAV need to provide outgoing server settings
@ -394,10 +395,19 @@ public class SettingsImporter {
String transportUri = Transport.createTransportUri(outgoing);
putString(editor, accountKeyPrefix + Account.TRANSPORT_URI_KEY, Utility.base64Encode(transportUri));
// Mark account as disabled if the settings file didn't contain a password
if (outgoing.password == null || outgoing.password.isEmpty()) {
createAccountDisabled = true;
}
/*
* Mark account as disabled if the settings file contained a
* username but no password. However, no password is required for
* the outgoing server for WebDAV accounts, because incoming and
* outgoing servers are identical for this account type. Nor is a
* password required if the AuthType is EXTERNAL.
*/
boolean outgoingPasswordNeeded = AuthType.EXTERNAL != outgoing.authenticationType &&
!WebDavStore.STORE_TYPE.equals(outgoing.type) &&
outgoing.username != null &&
!outgoing.username.isEmpty() &&
(outgoing.password == null || outgoing.password.isEmpty());
createAccountDisabled = outgoingPasswordNeeded || createAccountDisabled;
}
// Write key to mark account as disabled if necessary
@ -976,6 +986,8 @@ public class SettingsImporter {
server.authenticationType = AuthType.valueOf(text);
} else if (SettingsExporter.USERNAME_ELEMENT.equals(element)) {
server.username = getText(xpp);
} else if (SettingsExporter.CLIENT_CERTIFICATE_ALIAS_ELEMENT.equals(element)) {
server.clientCertificateAlias = getText(xpp);
} else if (SettingsExporter.PASSWORD_ELEMENT.equals(element)) {
server.password = getText(xpp);
} else if (SettingsExporter.EXTRA_ELEMENT.equals(element)) {
@ -1090,7 +1102,8 @@ public class SettingsImporter {
public ImportedServerSettings(ImportedServer server) {
super(server.type, server.host, convertPort(server.port),
convertConnectionSecurity(server.connectionSecurity),
server.authenticationType, server.username, server.password);
server.authenticationType, server.username, server.password,
server.clientCertificateAlias);
mImportedServer = server;
}
@ -1155,6 +1168,7 @@ public class SettingsImporter {
public AuthType authenticationType;
public String username;
public String password;
public String clientCertificateAlias;
public ImportedSettings extras;
}

View 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) findViewById(R.id.client_certificate_spinner_button);
mSelection.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
chooseCertificate();
}
});
mDeleteButton = (ImageButton) findViewById(R.id.client_certificate_spinner_delete);
mDeleteButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onDelete();
}
});
}
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() {
updateView();
if (mListener != null) {
mListener.onClientCertificateChanged(mAlias);
}
}
});
}
public String getAlias() {
String alias = mSelection.getText().toString();
if (alias.equals(mActivity.getString(R.string.client_certificate_spinner_empty))) {
return null;
} else {
return alias;
}
}
private void onDelete() {
setAlias(null);
}
public void chooseCertificate() {
// 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);
}
}
}

View file

@ -0,0 +1,253 @@
package com.fsck.k9.view;
import com.fsck.k9.R;
import android.content.Context;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* Class representing a LinearLayout that can fold and hide it's content when
* pressed To use just add the following to your xml layout
* <com.fsck.k9.view.FoldableLinearLayout
* android:layout_width="wrap_content" android:layout_height="wrap_content"
* custom:foldedLabel="@string/TEXT_TO_DISPLAY_WHEN_FOLDED"
* custom:unFoldedLabel="@string/TEXT_TO_DISPLAY_WHEN_UNFOLDED">
* <include layout="@layout/ELEMENTS_TO_BE_FOLDED"/>
* </com.fsck.k9.view.FoldableLinearLayout>
*/
public class FoldableLinearLayout extends LinearLayout {
private ImageView mFoldableIcon;
// Start with the view folded
private boolean mIsFolded = true;
private boolean mHasMigrated = false;
private Integer mShortAnimationDuration = null;
private TextView mFoldableTextView = null;
private LinearLayout mFoldableContainer = null;
private View mFoldableLayout = null;
private String mFoldedLabel;
private String mUnFoldedLabel;
private int mIconActionCollapseId;
private int mIconActionExpandId;
public FoldableLinearLayout(Context context) {
super(context);
processAttributes(context, null);
}
public FoldableLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
processAttributes(context, attrs);
}
public FoldableLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs);
processAttributes(context, attrs);
}
/**
* Load given attributes to inner variables,
*
* @param context
* @param attrs
*/
private void processAttributes(Context context, AttributeSet attrs) {
Theme theme = context.getTheme();
TypedValue outValue = new TypedValue();
boolean found = theme.resolveAttribute(R.attr.iconActionCollapse, outValue, true);
if (found) {
mIconActionCollapseId = outValue.resourceId;
}
found = theme.resolveAttribute(R.attr.iconActionExpand, outValue, true);
if (found) {
mIconActionExpandId = outValue.resourceId;
}
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.FoldableLinearLayout, 0, 0);
mFoldedLabel = a.getString(R.styleable.FoldableLinearLayout_foldedLabel);
mUnFoldedLabel = a.getString(R.styleable.FoldableLinearLayout_unFoldedLabel);
a.recycle();
}
// If any attribute isn't found then set a default one
mFoldedLabel = (mFoldedLabel == null) ? "No text!" : mFoldedLabel;
mUnFoldedLabel = (mUnFoldedLabel == null) ? "No text!" : mUnFoldedLabel;
}
@Override
protected void onFinishInflate() {
// if the migration has already happened
// there is no need to move any children
if (!mHasMigrated) {
migrateChildrenToContainer();
mHasMigrated = true;
}
initialiseInnerViews();
super.onFinishInflate();
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState savedState = new SavedState(superState);
savedState.mFolded = mIsFolded;
return savedState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof SavedState) {
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
mIsFolded = savedState.mFolded;
updateFoldedState(mIsFolded, false);
} else {
super.onRestoreInstanceState(state);
}
}
static class SavedState extends BaseSavedState {
static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<FoldableLinearLayout.SavedState>() {
@Override
public SavedState createFromParcel(Parcel source) {
return new SavedState(source);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
private boolean mFolded;
private SavedState(Parcel parcel) {
super(parcel);
mFolded = (parcel.readInt() == 1);
}
private SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(mFolded ? 1 : 0);
}
}
/**
* Migrates Child views as declared in xml to the inner foldableContainer
*/
private void migrateChildrenToContainer() {
// Collect children of FoldableLinearLayout as declared in XML
int childNum = getChildCount();
View[] children = new View[childNum];
for (int i = 0; i < childNum; i++) {
children[i] = getChildAt(i);
}
if (children[0].getId() == R.id.foldableControl) {
}
// remove all of them from FoldableLinearLayout
detachAllViewsFromParent();
// Inflate the inner foldable_linearlayout.xml
LayoutInflater inflator = (LayoutInflater) getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
mFoldableLayout = inflator.inflate(R.layout.foldable_linearlayout, this, true);
mFoldableContainer = (LinearLayout) mFoldableLayout.findViewById(R.id.foldableContainer);
// Push previously collected children into foldableContainer.
for (int i = 0; i < childNum; i++) {
addView(children[i]);
}
}
private void initialiseInnerViews() {
mFoldableIcon = (ImageView) mFoldableLayout.findViewById(R.id.foldableIcon);
mFoldableTextView = (TextView) mFoldableLayout.findViewById(R.id.foldableText);
mFoldableTextView.setText(mFoldedLabel);
// retrieve and cache the system's short animation time
mShortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);
LinearLayout foldableControl = (LinearLayout) mFoldableLayout
.findViewById(R.id.foldableControl);
foldableControl.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mIsFolded = !mIsFolded;
updateFoldedState(mIsFolded, true);
}
});
}
protected void updateFoldedState(boolean newStateIsFolded, boolean animate) {
if (newStateIsFolded) {
mFoldableIcon.setImageResource(mIconActionExpandId);
if (animate) {
AlphaAnimation animation = new AlphaAnimation(1f, 0f);
animation.setDuration(mShortAnimationDuration);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
/*
* Make sure that at the end the container is
* completely invisible. GONE is not used in
* order to prevent parent views from jumping
* around as they re-center themselves
* vertically.
*/
mFoldableContainer.setVisibility(View.INVISIBLE);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
mFoldableContainer.startAnimation(animation);
} else {
mFoldableContainer.setVisibility(View.INVISIBLE);
}
mFoldableTextView.setText(mFoldedLabel);
} else {
mFoldableIcon.setImageResource(mIconActionCollapseId);
mFoldableContainer.setVisibility(View.VISIBLE);
if (animate) {
AlphaAnimation animation = new AlphaAnimation(0f, 1f);
animation.setDuration(mShortAnimationDuration);
mFoldableContainer.startAnimation(animation);
}
mFoldableTextView.setText(mUnFoldedLabel);
}
}
/**
* Adds provided child view to foldableContainer View
*
* @param child
*/
@Override
public void addView(View child) {
if (mFoldableContainer != null) {
mFoldableContainer.addView(child);
}
}
}

View file

@ -82,7 +82,7 @@ public class ImapStoreUriTest extends TestCase {
extra.put("pathPrefix", "customPathPrefix");
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", extra);
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null, extra);
String uri = Store.createStoreUri(settings);
@ -95,7 +95,7 @@ public class ImapStoreUriTest extends TestCase {
extra.put("pathPrefix", "");
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", extra);
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null, extra);
String uri = Store.createStoreUri(settings);
@ -104,7 +104,7 @@ public class ImapStoreUriTest extends TestCase {
public void testCreateStoreUriImapNoExtra() {
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass");
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null);
String uri = Store.createStoreUri(settings);
@ -116,7 +116,7 @@ public class ImapStoreUriTest extends TestCase {
extra.put("autoDetectNamespace", "true");
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", extra);
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null, extra);
String uri = Store.createStoreUri(settings);