1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-27 11:42:16 -05:00

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,30 +1,32 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:orientation="vertical"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:layout_width="fill_parent" android:layout_width="fill_parent" >
>
<ScrollView <ScrollView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:padding="6dip" android:padding="6dip"
android:fadingEdge="none" android:fadingEdge="none"
android:scrollbarStyle="outsideInset"> android:scrollbarStyle="outsideInset" >
<LinearLayout <LinearLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:layout_gravity="center_horizontal|center_vertical" android:layout_gravity="center_horizontal|center_vertical"
android:orientation="vertical"> android:orientation="vertical" >
<EditText <EditText
android:id="@+id/account_email" android:id="@+id/account_email"
android:hint="@string/account_setup_basics_email_hint" android:hint="@string/account_setup_basics_email_hint"
android:singleLine="true" android:singleLine="true"
android:inputType="textEmailAddress" android:inputType="textEmailAddress"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent" />
/>
<EditText <EditText
android:id="@+id/account_password" android:id="@+id/account_password"
android:inputType="textPassword" android:inputType="textPassword"
@ -32,20 +34,42 @@
android:singleLine="true" android:singleLine="true"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:nextFocusDown="@+id/next" android:nextFocusDown="@+id/next" />
/>
<CheckBox <CheckBox
android:id="@+id/show_password" android:id="@+id/show_password"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:text="@string/show_password" 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 <View
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="0dip" android:layout_height="0dip"
android:layout_weight="1" android:layout_weight="1" />
/>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
<include layout="@layout/wizard_setup"/>
<include layout="@layout/wizard_setup" />
</LinearLayout> </LinearLayout>

View File

@ -16,31 +16,6 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:orientation="vertical"> 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. --> <!-- This text may be changed in code if the server is IMAP, etc. -->
<TextView <TextView
android:id="@+id/account_server_label" android:id="@+id/account_server_label"
@ -67,18 +42,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_incoming_security_label" /> 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 <TextView
android:text="@string/account_setup_incoming_port_label" android:text="@string/account_setup_incoming_port_label"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -92,6 +55,57 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_incoming_port_label" /> 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 <LinearLayout
android:id="@+id/imap_path_prefix_section" android:id="@+id/imap_path_prefix_section"
android:layout_width="fill_parent" android:layout_width="fill_parent"

View File

@ -5,23 +5,26 @@
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:layout_width="fill_parent"> android:layout_width="fill_parent">
<ScrollView <ScrollView
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:padding="6dip" android:padding="6dip"
android:fadingEdge="none" android:fadingEdge="none"
android:scrollbarStyle="outsideInset"> android:scrollbarStyle="outsideInset">
<LinearLayout <LinearLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:text="@string/account_setup_outgoing_smtp_server_label" android:text="@string/account_setup_outgoing_smtp_server_label"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" /> android:textColor="?android:attr/textColorPrimary" />
<EditText <EditText
android:id="@+id/account_server" android:id="@+id/account_server"
android:singleLine="true" android:singleLine="true"
@ -29,23 +32,27 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_smtp_server_label" /> android:contentDescription="@string/account_setup_outgoing_smtp_server_label" />
<TextView <TextView
android:text="@string/account_setup_outgoing_security_label" android:text="@string/account_setup_outgoing_security_label"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" /> android:textColor="?android:attr/textColorPrimary" />
<Spinner <Spinner
android:id="@+id/account_security_type" android:id="@+id/account_security_type"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_security_label" /> android:contentDescription="@string/account_setup_outgoing_security_label" />
<TextView <TextView
android:text="@string/account_setup_outgoing_port_label" android:text="@string/account_setup_outgoing_port_label"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" /> android:textColor="?android:attr/textColorPrimary" />
<EditText <EditText
android:id="@+id/account_port" android:id="@+id/account_port"
android:singleLine="true" android:singleLine="true"
@ -53,34 +60,27 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_port_label" /> android:contentDescription="@string/account_setup_outgoing_port_label" />
<CheckBox <CheckBox
android:id="@+id/account_require_login" android:id="@+id/account_require_login"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/account_setup_outgoing_require_login_label" /> android:text="@string/account_setup_outgoing_require_login_label" />
<LinearLayout <LinearLayout
android:id="@+id/account_require_login_settings" android:id="@+id/account_require_login_settings"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:orientation="vertical" android:orientation="vertical"
android:visibility="gone"> 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" />
<TextView <TextView
android:text="@string/account_setup_outgoing_username_label" android:text="@string/account_setup_outgoing_username_label"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" /> android:textColor="?android:attr/textColorPrimary" />
<EditText <EditText
android:id="@+id/account_username" android:id="@+id/account_username"
android:singleLine="true" android:singleLine="true"
@ -88,12 +88,28 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_username_label" /> android:contentDescription="@string/account_setup_outgoing_username_label" />
<TextView <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:text="@string/account_setup_outgoing_password_label"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" /> android:textColor="?android:attr/textColorPrimary" />
<EditText <EditText
android:id="@+id/account_password" android:id="@+id/account_password"
android:singleLine="true" android:singleLine="true"
@ -101,12 +117,30 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_password_label" /> android:contentDescription="@string/account_setup_outgoing_password_label" />
<TextView
android:id="@+id/account_client_certificate_label"
android:text="@string/account_setup_incoming_client_certificate_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"
android:visibility="gone" />
<com.fsck.k9.view.ClientCertificateSpinner
android:id="@+id/account_client_certificate_spinner"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:visibility="gone" />
</LinearLayout> </LinearLayout>
<View <View
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="0dip" android:layout_height="0dip"
android:layout_weight="1" /> android:layout_weight="1" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
<include layout="@layout/wizard_next" /> <include layout="@layout/wizard_next" />
</LinearLayout> </LinearLayout>

View File

@ -14,7 +14,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="15dip"/> android:layout_marginBottom="15dip"/>
<!-- Password prompt for the incoming server --> <!-- 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_height="wrap_content">
<TextView <TextView
android:id="@+id/password_prompt_incoming_server" android:id="@+id/password_prompt_incoming_server"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
@ -26,8 +32,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_marginBottom="10dip"/> 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 <LinearLayout
android:id="@+id/outgoing_server_prompt" android:id="@+id/outgoing_server_prompt"
android:orientation="vertical" 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="iconActionSave" format="reference" />
<attr name="iconActionCancel" format="reference" /> <attr name="iconActionCancel" format="reference" />
<attr name="iconActionRequestReadReceipt" format="reference" /> <attr name="iconActionRequestReadReceipt" format="reference" />
<attr name="iconActionExpand" format="reference" />
<attr name="iconActionCollapse" format="reference" />
<attr name="textColorPrimaryRecipientDropdown" format="reference" /> <attr name="textColorPrimaryRecipientDropdown" format="reference" />
<attr name="textColorSecondaryRecipientDropdown" format="reference" /> <attr name="textColorSecondaryRecipientDropdown" format="reference" />
<attr name="backgroundColorChooseAccountHeader" format="color" /> <attr name="backgroundColorChooseAccountHeader" format="color" />
@ -59,4 +61,9 @@
<attr name="android:summary" /> <attr name="android:summary" />
</declare-styleable> </declare-styleable>
<declare-styleable name="FoldableLinearLayout">
<attr name="foldedLabel" format="string" />
<attr name="unFoldedLabel" format="string" />
</declare-styleable>
</resources> </resources>

View File

@ -4,5 +4,6 @@
<item type="id" name="dialog_confirm_delete"/> <item type="id" name="dialog_confirm_delete"/>
<item type="id" name="dialog_confirm_spam"/> <item type="id" name="dialog_confirm_spam"/>
<item type="id" name="dialog_attachment_progress"/> <item type="id" name="dialog_attachment_progress"/>
<item type="id" name="dialog_account_setup_error"/>
</resources> </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_title">Set up a new account</string>
<string name="account_setup_basics_email_hint">Email address</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_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_basics_manual_setup_action">Manual setup</string>
<string name="account_setup_check_settings_title"/> <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_normal_password">Normal password</string>
<string name="account_setup_auth_type_insecure_password">Password, transmitted insecurely</string> <string name="account_setup_auth_type_insecure_password">Password, transmitted insecurely</string>
<string name="account_setup_auth_type_encrypted_password">Encrypted password</string> <string name="account_setup_auth_type_encrypted_password">Encrypted password</string>
<string name="account_setup_auth_type_tls_client_certificate">Client certificate</string>
<string name="account_setup_incoming_title">Incoming server settings</string> <string name="account_setup_incoming_title">Incoming server settings</string>
<string name="account_setup_incoming_username_label">Username</string> <string name="account_setup_incoming_username_label">Username</string>
<string name="account_setup_incoming_password_label">Password</string> <string name="account_setup_incoming_password_label">Password</string>
<string name="account_setup_incoming_client_certificate_label">Client certificate</string>
<string name="account_setup_incoming_pop_server_label">POP3 server</string> <string name="account_setup_incoming_pop_server_label">POP3 server</string>
<string name="account_setup_incoming_imap_server_label">IMAP server</string> <string name="account_setup_incoming_imap_server_label">IMAP server</string>
<string name="account_setup_incoming_webdav_server_label">Exchange server</string> <string name="account_setup_incoming_webdav_server_label">Exchange server</string>
@ -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_none_label">None</string>
<string name="account_setup_incoming_security_ssl_label">SSL/TLS</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_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_label">When I delete a message</string>
<string name="account_setup_incoming_delete_policy_never_label">Do not delete on server</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_username_label">Username</string>
<string name="account_setup_outgoing_password_label">Password</string> <string name="account_setup_outgoing_password_label">Password</string>
<string name="account_setup_outgoing_authentication_label">Authentication</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> <string name="account_setup_bad_uri">Invalid setup: <xliff:g id="err_mess">%s</xliff:g></string>
@ -1097,7 +1102,7 @@ Please submit bug reports, contribute new features and ask questions at
<string name="fetching_attachment_dialog_title_save">Saving draft</string> <string name="fetching_attachment_dialog_title_save">Saving draft</string>
<string name="fetching_attachment_dialog_message">Fetching attachment…</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 ================================================================== --> <!-- === OpenPGP specific ================================================================== -->
<string name="openpgp_decrypting_verifying">Decrypting/Verifying…</string> <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_error">OpenPGP Error:</string>
<string name="openpgp_user_id">User Id</string> <string name="openpgp_user_id">User Id</string>
<!-- === Client certificates specific ================================================================== -->
<string name="account_setup_basics_client_certificate">Use client certificate</string>
<string name="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> </resources>

View File

@ -37,6 +37,8 @@
<item name="iconActionSave">@drawable/ic_action_save_light</item> <item name="iconActionSave">@drawable/ic_action_save_light</item>
<item name="iconActionCancel">@drawable/ic_action_cancel_light</item> <item name="iconActionCancel">@drawable/ic_action_cancel_light</item>
<item name="iconActionRequestReadReceipt">@drawable/ic_action_request_read_receipt_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="textColorPrimaryRecipientDropdown">@android:color/primary_text_light</item>
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_light</item> <item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_light</item>
<item name="messageListSelectedBackgroundColor">#8038B8E2</item> <item name="messageListSelectedBackgroundColor">#8038B8E2</item>
@ -89,6 +91,8 @@
<item name="iconActionSave">@drawable/ic_action_save_dark</item> <item name="iconActionSave">@drawable/ic_action_save_dark</item>
<item name="iconActionCancel">@drawable/ic_action_cancel_dark</item> <item name="iconActionCancel">@drawable/ic_action_cancel_dark</item>
<item name="iconActionRequestReadReceipt">@drawable/ic_action_request_read_receipt_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="textColorPrimaryRecipientDropdown">@android:color/primary_text_dark</item>
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_dark</item> <item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_dark</item>
<item name="messageListSelectedBackgroundColor">#8038B8E2</item> <item name="messageListSelectedBackgroundColor">#8038B8E2</item>

View File

@ -1879,7 +1879,7 @@ public class Account implements BaseAccount {
public void addCertificate(CheckDirection direction, public void addCertificate(CheckDirection direction,
X509Certificate certificate) throws CertificateException { X509Certificate certificate) throws CertificateException {
Uri uri; Uri uri;
if (direction.equals(CheckDirection.INCOMING)) { if (direction == CheckDirection.INCOMING) {
uri = Uri.parse(getStoreUri()); uri = Uri.parse(getStoreUri());
} else { } else {
uri = Uri.parse(getTransportUri()); uri = Uri.parse(getTransportUri());
@ -1896,7 +1896,7 @@ public class Account implements BaseAccount {
public void deleteCertificate(String newHost, int newPort, public void deleteCertificate(String newHost, int newPort,
CheckDirection direction) { CheckDirection direction) {
Uri uri; Uri uri;
if (direction.equals(CheckDirection.INCOMING)) { if (direction == CheckDirection.INCOMING) {
uri = Uri.parse(getStoreUri()); uri = Uri.parse(getStoreUri());
} else { } else {
uri = Uri.parse(getTransportUri()); 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.activity.setup.WelcomeMessage;
import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.helper.SizeFormatter; import com.fsck.k9.helper.SizeFormatter;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.Transport;
@ -743,7 +744,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
public boolean retain() { public boolean retain() {
if (mDialog != null) { if (mDialog != null) {
// Retain entered passwords and checkbox state // Retain entered passwords and checkbox state
if (mIncomingPasswordView != null) {
mIncomingPassword = mIncomingPasswordView.getText().toString(); mIncomingPassword = mIncomingPasswordView.getText().toString();
}
if (mOutgoingPasswordView != null) { if (mOutgoingPasswordView != null) {
mOutgoingPassword = mOutgoingPasswordView.getText().toString(); mOutgoingPassword = mOutgoingPasswordView.getText().toString();
mUseIncoming = mUseIncomingView.isChecked(); mUseIncoming = mUseIncomingView.isChecked();
@ -770,9 +773,22 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
ServerSettings incoming = Store.decodeStoreUri(mAccount.getStoreUri()); ServerSettings incoming = Store.decodeStoreUri(mAccount.getStoreUri());
ServerSettings outgoing = Transport.decodeTransportUri(mAccount.getTransportUri()); 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. * Don't ask for the password to the outgoing server for WebDAV
boolean configureOutgoingServer = !WebDavStore.STORE_TYPE.equals(outgoing.type); * 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 // Create a ScrollView that will be used as container for the whole layout
final ScrollView scrollView = new ScrollView(activity); final ScrollView scrollView = new ScrollView(activity);
@ -785,7 +801,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
new DialogInterface.OnClickListener() { new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { 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; String outgoingPassword = null;
if (mOutgoingPasswordView != null) { if (mOutgoingPasswordView != null) {
outgoingPassword = (mUseIncomingView.isChecked()) ? outgoingPassword = (mUseIncomingView.isChecked()) ?
@ -819,10 +838,11 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
TextView intro = (TextView) layout.findViewById(R.id.password_prompt_intro); TextView intro = (TextView) layout.findViewById(R.id.password_prompt_intro);
String serverPasswords = activity.getResources().getQuantityString( String serverPasswords = activity.getResources().getQuantityString(
R.plurals.settings_import_server_passwords, R.plurals.settings_import_server_passwords,
(configureOutgoingServer) ? 2 : 1); (configureIncomingServer && configureOutgoingServer) ? 2 : 1);
intro.setText(activity.getString(R.string.settings_import_activate_account_intro, intro.setText(activity.getString(R.string.settings_import_activate_account_intro,
mAccount.getDescription(), serverPasswords)); mAccount.getDescription(), serverPasswords));
if (configureIncomingServer) {
// Display the hostname of the incoming server // Display the hostname of the incoming server
TextView incomingText = (TextView) layout.findViewById( TextView incomingText = (TextView) layout.findViewById(
R.id.password_prompt_incoming_server); R.id.password_prompt_incoming_server);
@ -831,6 +851,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
mIncomingPasswordView = (EditText) layout.findViewById(R.id.incoming_server_password); mIncomingPasswordView = (EditText) layout.findViewById(R.id.incoming_server_password);
mIncomingPasswordView.addTextChangedListener(this); mIncomingPasswordView.addTextChangedListener(this);
} else {
layout.findViewById(R.id.incoming_server_prompt).setVisibility(View.GONE);
}
if (configureOutgoingServer) { if (configureOutgoingServer) {
// Display the hostname of the outgoing server // Display the hostname of the outgoing server
@ -845,6 +868,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
mUseIncomingView = (CheckBox) layout.findViewById( mUseIncomingView = (CheckBox) layout.findViewById(
R.id.use_incoming_server_password); R.id.use_incoming_server_password);
if (configureIncomingServer) {
mUseIncomingView.setChecked(true); mUseIncomingView.setChecked(true);
mUseIncomingView.setOnCheckedChangeListener(new OnCheckedChangeListener() { mUseIncomingView.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override @Override
@ -858,6 +883,11 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
} }
} }
}); });
} else {
mUseIncomingView.setChecked(false);
mUseIncomingView.setVisibility(View.GONE);
mOutgoingPasswordView.setEnabled(true);
}
} else { } else {
layout.findViewById(R.id.outgoing_server_prompt).setVisibility(View.GONE); 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 // Restore the contents of the password boxes and the checkbox (if the dialog was
// retained during a configuration change). // retained during a configuration change).
if (restore) { if (restore) {
if (configureIncomingServer) {
mIncomingPasswordView.setText(mIncomingPassword); mIncomingPasswordView.setText(mIncomingPassword);
}
if (configureOutgoingServer) { if (configureOutgoingServer) {
mOutgoingPasswordView.setText(mOutgoingPassword); mOutgoingPasswordView.setText(mOutgoingPassword);
mUseIncomingView.setChecked(mUseIncoming); mUseIncomingView.setChecked(mUseIncoming);
} }
} else { } else {
if (configureIncomingServer) {
// Trigger afterTextChanged() being called // Trigger afterTextChanged() being called
// Work around this bug: https://code.google.com/p/android/issues/detail?id=6360 // Work around this bug: https://code.google.com/p/android/issues/detail?id=6360
mIncomingPasswordView.setText(mIncomingPasswordView.getText()); mIncomingPasswordView.setText(mIncomingPasswordView.getText());
} else {
mOutgoingPasswordView.setText(mOutgoingPasswordView.getText());
}
} }
} }
@ -887,6 +923,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
public void afterTextChanged(Editable arg0) { public void afterTextChanged(Editable arg0) {
boolean enable = false; boolean enable = false;
// Is the password box for the incoming server password empty? // Is the password box for the incoming server password empty?
if (mIncomingPasswordView != null) {
if (mIncomingPasswordView.getText().length() > 0) { if (mIncomingPasswordView.getText().length() > 0) {
// Do we need to check the outgoing server password box? // Do we need to check the outgoing server password box?
if (mOutgoingPasswordView == null) { if (mOutgoingPasswordView == null) {
@ -899,6 +936,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
enable = true; enable = true;
} }
} }
} else {
enable = mOutgoingPasswordView.getText().length() > 0;
}
// Disable "OK" button if the user hasn't specified all necessary passwords. // Disable "OK" button if the user hasn't specified all necessary passwords.
mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(enable); mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(enable);
@ -948,12 +988,14 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
@Override @Override
protected Void doInBackground(Void... params) { protected Void doInBackground(Void... params) {
try { try {
if (mIncomingPassword != null) {
// Set incoming server password // Set incoming server password
String storeUri = mAccount.getStoreUri(); String storeUri = mAccount.getStoreUri();
ServerSettings incoming = Store.decodeStoreUri(storeUri); ServerSettings incoming = Store.decodeStoreUri(storeUri);
ServerSettings newIncoming = incoming.newPassword(mIncomingPassword); ServerSettings newIncoming = incoming.newPassword(mIncomingPassword);
String newStoreUri = Store.createStoreUri(newIncoming); String newStoreUri = Store.createStoreUri(newIncoming);
mAccount.setStoreUri(newStoreUri); mAccount.setStoreUri(newStoreUri);
}
if (mOutgoingPassword != null) { if (mOutgoingPassword != null) {
// Set outgoing server password // Set outgoing server password

View File

@ -89,7 +89,23 @@ public class AccountSetupAccountType extends K9Activity implements OnClickListen
private void onWebDav() { private void onWebDav() {
try { try {
URI uri = new URI(mAccount.getStoreUri()); 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()); mAccount.setStoreUri(uri.toString());
AccountSetupIncoming.actionIncomingSettings(this, mAccount, mMakeDefault); AccountSetupIncoming.actionIncomingSettings(this, mAccount, mMakeDefault);
finish(); finish();
@ -112,6 +128,7 @@ public class AccountSetupAccountType extends K9Activity implements OnClickListen
break; break;
} }
} }
private void failure(Exception use) { private void failure(Exception use) {
Log.e(K9.LOG_TAG, "Failure", use); Log.e(K9.LOG_TAG, "Failure", use);
String toastText = getString(R.string.account_setup_bad_uri, use.getMessage()); String toastText = getString(R.string.account_setup_bad_uri, use.getMessage());

View File

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

View File

@ -3,6 +3,8 @@ package com.fsck.k9.activity.setup;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.DialogFragment;
import android.app.FragmentTransaction;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
@ -15,16 +17,18 @@ import android.view.View.OnClickListener;
import android.widget.Button; import android.widget.Button;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import com.fsck.k9.*; import com.fsck.k9.*;
import com.fsck.k9.activity.K9Activity; import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.fragment.ConfirmationDialogFragment;
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.CertificateValidationException; import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.store.WebDavStore; import com.fsck.k9.mail.store.WebDavStore;
import com.fsck.k9.mail.filter.Hex; import com.fsck.k9.mail.filter.Hex;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
@ -32,6 +36,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Locale;
/** /**
* Checks the given settings to make sure that they can be used to send and * Checks the given settings to make sure that they can be used to send and
@ -40,7 +45,8 @@ import java.util.List;
* XXX NOTE: The manifest for this app has it ignore config changes, because * XXX NOTE: The manifest for this app has it ignore config changes, because
* it doesn't correctly deal with restarting while its thread is running. * it doesn't correctly deal with restarting while its thread is running.
*/ */
public class AccountSetupCheckSettings extends K9Activity implements OnClickListener { public class AccountSetupCheckSettings extends K9Activity implements OnClickListener,
ConfirmationDialogFragmentListener{
public static final int ACTIVITY_REQUEST_CODE = 1; public static final int ACTIVITY_REQUEST_CODE = 1;
@ -67,7 +73,8 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
private boolean mDestroyed; private boolean mDestroyed;
public static void actionCheckSettings(Activity context, Account account, CheckDirection direction) { public static void actionCheckSettings(Activity context, Account account,
CheckDirection direction) {
Intent i = new Intent(context, AccountSetupCheckSettings.class); Intent i = new Intent(context, AccountSetupCheckSettings.class);
i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_ACCOUNT, account.getUuid());
i.putExtra(EXTRA_CHECK_DIRECTION, direction); i.putExtra(EXTRA_CHECK_DIRECTION, direction);
@ -107,7 +114,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
ctrl.clearCertificateErrorNotifications(AccountSetupCheckSettings.this, ctrl.clearCertificateErrorNotifications(AccountSetupCheckSettings.this,
mAccount, mDirection); mAccount, mDirection);
if (mDirection.equals(CheckDirection.INCOMING)) { if (mDirection == CheckDirection.INCOMING) {
store = mAccount.getRemoteStore(); store = mAccount.getRemoteStore();
if (store instanceof WebDavStore) { if (store instanceof WebDavStore) {
@ -130,7 +137,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
finish(); finish();
return; return;
} }
if (mDirection.equals(CheckDirection.OUTGOING)) { if (mDirection == CheckDirection.OUTGOING) {
if (!(mAccount.getRemoteStore() instanceof WebDavStore)) { if (!(mAccount.getRemoteStore() instanceof WebDavStore)) {
setMessage(R.string.account_setup_check_settings_check_outgoing_msg); setMessage(R.string.account_setup_check_settings_check_outgoing_msg);
} }
@ -154,6 +161,20 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
R.string.account_setup_failed_dlg_auth_message_fmt, R.string.account_setup_failed_dlg_auth_message_fmt,
afe.getMessage() == null ? "" : afe.getMessage()); afe.getMessage() == null ? "" : afe.getMessage());
} catch (final CertificateValidationException cve) { } catch (final CertificateValidationException cve) {
handleCertificateValidationException(cve);
} catch (final Throwable t) {
Log.e(K9.LOG_TAG, "Error while testing settings", t);
showErrorDialog(
R.string.account_setup_failed_dlg_server_message_fmt,
(t.getMessage() == null ? "" : t.getMessage()));
}
}
}
.start();
}
private void handleCertificateValidationException(CertificateValidationException cve) {
Log.e(K9.LOG_TAG, "Error while testing settings", cve); Log.e(K9.LOG_TAG, "Error while testing settings", cve);
X509Certificate[] chain = cve.getCertChain(); X509Certificate[] chain = cve.getCertChain();
@ -167,17 +188,6 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
R.string.account_setup_failed_dlg_server_message_fmt, R.string.account_setup_failed_dlg_server_message_fmt,
(cve.getMessage() == null ? "" : cve.getMessage())); (cve.getMessage() == null ? "" : cve.getMessage()));
} }
} catch (final Throwable t) {
Log.e(K9.LOG_TAG, "Error while testing settings", t);
showErrorDialog(
R.string.account_setup_failed_dlg_server_message_fmt,
(t.getMessage() == null ? "" : t.getMessage()));
}
}
}
.start();
} }
@Override @Override
@ -198,41 +208,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
}); });
} }
private void showErrorDialog(final int msgResId, final Object... args) { private void acceptKeyDialog(final int msgResId, final CertificateValidationException ex) {
mHandler.post(new Runnable() {
public void run() {
if (mDestroyed) {
return;
}
mProgressBar.setIndeterminate(false);
new AlertDialog.Builder(AccountSetupCheckSettings.this)
.setTitle(getString(R.string.account_setup_failed_dlg_title))
.setMessage(getString(msgResId, args))
.setCancelable(true)
.setNegativeButton(
getString(R.string.account_setup_failed_dlg_continue_action),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
mCanceled = false;
setResult(RESULT_OK);
finish();
}
})
.setPositiveButton(
getString(R.string.account_setup_failed_dlg_edit_details_action),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
}
});
}
private void acceptKeyDialog(final int msgResId,
final CertificateValidationException ex) {
mHandler.post(new Runnable() { mHandler.post(new Runnable() {
public void run() { public void run() {
if (mDestroyed) { if (mDestroyed) {
@ -351,6 +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) new AlertDialog.Builder(AccountSetupCheckSettings.this)
.setTitle(getString(R.string.account_setup_failed_dlg_invalid_certificate_title)) .setTitle(getString(R.string.account_setup_failed_dlg_invalid_certificate_title))
//.setMessage(getString(R.string.account_setup_failed_dlg_invalid_certificate) //.setMessage(getString(R.string.account_setup_failed_dlg_invalid_certificate)
@ -362,15 +340,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
getString(R.string.account_setup_failed_dlg_invalid_certificate_accept), getString(R.string.account_setup_failed_dlg_invalid_certificate_accept),
new DialogInterface.OnClickListener() { new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
try { acceptCertificate(chain[0]);
mAccount.addCertificate(mDirection, chain[0]);
} catch (CertificateException e) {
showErrorDialog(
R.string.account_setup_failed_dlg_certificate_message_fmt,
e.getMessage() == null ? "" : e.getMessage());
}
AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount,
mDirection);
} }
}) })
.setNegativeButton( .setNegativeButton(
@ -385,13 +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 @Override
public void onActivityResult(int reqCode, int resCode, Intent data) { public void onActivityResult(int reqCode, int resCode, Intent data) {
setResult(resCode); setResult(resCode);
finish(); finish();
} }
private void onCancel() { private void onCancel() {
mCanceled = true; mCanceled = true;
setMessage(R.string.account_setup_check_settings_canceling_msg); setMessage(R.string.account_setup_check_settings_canceling_msg);
@ -404,4 +391,74 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
break; break;
} }
} }
private void showErrorDialog(final int msgResId, final Object... args) {
mHandler.post(new Runnable() {
public void run() {
showDialogFragment(R.id.dialog_account_setup_error, getString(msgResId, args));
}
});
}
private void showDialogFragment(int dialogId, 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;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.*; import android.widget.*;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.CompoundButton.OnCheckedChangeListener;
import com.fsck.k9.*; 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.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.store.ImapStore; import com.fsck.k9.mail.store.ImapStore;
import com.fsck.k9.mail.store.Pop3Store; import com.fsck.k9.mail.store.Pop3Store;
import com.fsck.k9.mail.store.WebDavStore; import com.fsck.k9.mail.store.WebDavStore;
import com.fsck.k9.mail.store.ImapStore.ImapStoreSettings; import com.fsck.k9.mail.store.ImapStore.ImapStoreSettings;
import com.fsck.k9.mail.store.WebDavStore.WebDavStoreSettings; import com.fsck.k9.mail.store.WebDavStore.WebDavStoreSettings;
import com.fsck.k9.mail.transport.SmtpTransport;
import com.fsck.k9.service.MailService; import com.fsck.k9.service.MailService;
import com.fsck.k9.view.ClientCertificateSpinner;
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
public class AccountSetupIncoming extends K9Activity implements OnClickListener { public class AccountSetupIncoming extends K9Activity implements OnClickListener {
private static final String EXTRA_ACCOUNT = "account"; private static final String EXTRA_ACCOUNT = "account";
private static final String EXTRA_MAKE_DEFAULT = "makeDefault"; 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_PORT = "110";
private static final String POP3_SSL_PORT = "995"; private static final String POP3_SSL_PORT = "995";
@ -51,10 +56,16 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
private String mStoreType; private String mStoreType;
private EditText mUsernameView; private EditText mUsernameView;
private EditText mPasswordView; private EditText mPasswordView;
private ClientCertificateSpinner mClientCertificateSpinner;
private TextView mClientCertificateLabelView;
private TextView mPasswordLabelView;
private EditText mServerView; private EditText mServerView;
private EditText mPortView; private EditText mPortView;
private String mCurrentPortViewSetting;
private Spinner mSecurityTypeView; private Spinner mSecurityTypeView;
private int mCurrentSecurityTypeViewPosition;
private Spinner mAuthTypeView; private Spinner mAuthTypeView;
private int mCurrentAuthTypeViewPosition;
private CheckBox mImapAutoDetectNamespaceView; private CheckBox mImapAutoDetectNamespaceView;
private EditText mImapPathPrefixView; private EditText mImapPathPrefixView;
private EditText mWebdavPathPrefixView; private EditText mWebdavPathPrefixView;
@ -97,6 +108,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mUsernameView = (EditText)findViewById(R.id.account_username); mUsernameView = (EditText)findViewById(R.id.account_username);
mPasswordView = (EditText)findViewById(R.id.account_password); mPasswordView = (EditText)findViewById(R.id.account_password);
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
mClientCertificateLabelView = (TextView)findViewById(R.id.account_client_certificate_label);
mPasswordLabelView = (TextView)findViewById(R.id.account_password_label);
TextView serverLabelView = (TextView) findViewById(R.id.account_server_label); TextView serverLabelView = (TextView) findViewById(R.id.account_server_label);
mServerView = (EditText)findViewById(R.id.account_server); mServerView = (EditText)findViewById(R.id.account_server);
mPortView = (EditText)findViewById(R.id.account_port); mPortView = (EditText)findViewById(R.id.account_port);
@ -130,28 +144,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mAuthTypeAdapter = AuthType.getArrayAdapter(this); mAuthTypeAdapter = AuthType.getArrayAdapter(this);
mAuthTypeView.setAdapter(mAuthTypeAdapter); mAuthTypeView.setAdapter(mAuthTypeAdapter);
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
/* unused */
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
/* unused */
}
};
mUsernameView.addTextChangedListener(validationTextWatcher);
mPasswordView.addTextChangedListener(validationTextWatcher);
mServerView.addTextChangedListener(validationTextWatcher);
mPortView.addTextChangedListener(validationTextWatcher);
/* /*
* Only allow digits in the port field. * Only allow digits in the port field.
*/ */
@ -173,6 +165,15 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
try { try {
ServerSettings settings = Store.decodeStoreUri(mAccount.getStoreUri()); 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) { if (settings.username != null) {
mUsernameView.setText(settings.username); mUsernameView.setText(settings.username);
} }
@ -181,11 +182,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mPasswordView.setText(settings.password); mPasswordView.setText(settings.password);
} }
updateAuthPlainTextFromSecurityType(settings.connectionSecurity); if (settings.clientCertificateAlias != null) {
mClientCertificateSpinner.setAlias(settings.clientCertificateAlias);
// The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter }
int position = mAuthTypeAdapter.getPosition(settings.authenticationType);
mAuthTypeView.setSelection(position, false);
mStoreType = settings.type; mStoreType = settings.type;
if (Pop3Store.STORE_TYPE.equals(settings.type)) { if (Pop3Store.STORE_TYPE.equals(settings.type)) {
@ -256,34 +255,29 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
throw new Exception("Unknown account type: " + mAccount.getStoreUri()); throw new Exception("Unknown account type: " + mAccount.getStoreUri());
} }
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(this, // Note that mConnectionSecurityChoices is configured above based on server type
android.R.layout.simple_spinner_item, mConnectionSecurityChoices); ArrayAdapter<ConnectionSecurity> securityTypesAdapter =
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); ConnectionSecurity.getArrayAdapter(this, mConnectionSecurityChoices);
mSecurityTypeView.setAdapter(securityTypesAdapter); mSecurityTypeView.setAdapter(securityTypesAdapter);
// Select currently configured security type // Select currently configured security type
int index = securityTypesAdapter.getPosition(settings.connectionSecurity); if (savedInstanceState == null) {
mSecurityTypeView.setSelection(index, false); mCurrentSecurityTypeViewPosition = securityTypesAdapter.getPosition(settings.connectionSecurity);
} else {
/* /*
* Updates the port when the user changes the security type. This allows * Restore the spinner state now, before calling
* us to show a reasonable default which the user can change. * setOnItemSelectedListener(), thus avoiding a call to
* * onItemSelected(). Then, when the system restores the state
* Note: It's important that we set the listener *after* an initial option has been * (again) in onRestoreInstanceState(), The system will see that
* selected by the code above. Otherwise the listener might be called after * the new state is the same as the current state (set here), so
* onCreate() has been processed and the current port value set later in this * once again onItemSelected() will not be called.
* method is overridden with the default port for the selected security type.
*/ */
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { mCurrentSecurityTypeViewPosition = savedInstanceState.getInt(STATE_SECURITY_TYPE_POSITION);
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
updatePortFromSecurityType();
} }
mSecurityTypeView.setSelection(mCurrentSecurityTypeViewPosition, false);
@Override updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
});
mCompressionMobile.setChecked(mAccount.useCompression(Account.TYPE_MOBILE)); mCompressionMobile.setChecked(mAccount.useCompression(Account.TYPE_MOBILE));
mCompressionWifi.setChecked(mAccount.useCompression(Account.TYPE_WIFI)); mCompressionWifi.setChecked(mAccount.useCompression(Account.TYPE_WIFI));
@ -298,34 +292,205 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
} else { } else {
updatePortFromSecurityType(); updatePortFromSecurityType();
} }
mCurrentPortViewSetting = mPortView.getText().toString();
mSubscribedFoldersOnly.setChecked(mAccount.subscribedFoldersOnly()); mSubscribedFoldersOnly.setChecked(mAccount.subscribedFoldersOnly());
validateFields();
} catch (Exception e) { } catch (Exception e) {
failure(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 @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putString(EXTRA_ACCOUNT, mAccount.getUuid()); 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() { private void validateFields() {
mNextButton AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
.setEnabled(Utility.requiredFieldValid(mUsernameView) boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType);
&& Utility.requiredFieldValid(mPasswordView)
&& Utility.domainFieldValid(mServerView) ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
&& Utility.requiredFieldValid(mPortView)); 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); Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
} }
private void updatePortFromSecurityType() { private void updatePortFromSecurityType() {
ConnectionSecurity securityType = (ConnectionSecurity) mSecurityTypeView.getSelectedItem(); ConnectionSecurity securityType = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
mPortView.setText(getDefaultPort(securityType));
updateAuthPlainTextFromSecurityType(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) { private String getDefaultPort(ConnectionSecurity securityType) {
@ -377,21 +542,22 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
* password the user just set for incoming. * password the user just set for incoming.
*/ */
try { try {
String usernameEnc = URLEncoder.encode(mUsernameView.getText().toString(), "UTF-8"); String username = mUsernameView.getText().toString();
String passwordEnc = URLEncoder.encode(mPasswordView.getText().toString(), "UTF-8");
String password = null;
String clientCertificateAlias = null;
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
if (AuthType.EXTERNAL == authType) {
clientCertificateAlias = mClientCertificateSpinner.getAlias();
} else {
password = mPasswordView.getText().toString();
}
URI oldUri = new URI(mAccount.getTransportUri()); URI oldUri = new URI(mAccount.getTransportUri());
URI uri = new URI( ServerSettings transportServer = new ServerSettings(SmtpTransport.TRANSPORT_TYPE, oldUri.getHost(), oldUri.getPort(),
oldUri.getScheme(), ConnectionSecurity.SSL_TLS_REQUIRED, authType, username, password, clientCertificateAlias);
usernameEnc + ":" + passwordEnc, String transportUri = Transport.createTransportUri(transportServer);
oldUri.getHost(), mAccount.setTransportUri(transportUri);
oldUri.getPort(),
null,
null,
null);
mAccount.setTransportUri(uri.toString());
} catch (UnsupportedEncodingException enc) {
// This really shouldn't happen since the encoding is hardcoded to UTF-8
Log.e(K9.LOG_TAG, "Couldn't urlencode username or password.", enc);
} catch (URISyntaxException use) { } catch (URISyntaxException use) {
/* /*
* If we can't set up the URL we just continue. It's only for * If we can't set up the URL we just continue. It's only for
@ -411,8 +577,15 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem(); ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
String username = mUsernameView.getText().toString(); String username = mUsernameView.getText().toString();
String password = mPasswordView.getText().toString(); String password = null;
String clientCertificateAlias = null;
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem(); AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
if (authType == AuthType.EXTERNAL) {
clientCertificateAlias = mClientCertificateSpinner.getAlias();
} else {
password = mPasswordView.getText().toString();
}
String host = mServerView.getText().toString(); String host = mServerView.getText().toString();
int port = Integer.parseInt(mPortView.getText().toString()); int port = Integer.parseInt(mPortView.getText().toString());
@ -435,7 +608,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mAccount.deleteCertificate(host, port, CheckDirection.INCOMING); mAccount.deleteCertificate(host, port, CheckDirection.INCOMING);
ServerSettings settings = new ServerSettings(mStoreType, host, port, ServerSettings settings = new ServerSettings(mStoreType, host, port,
connectionSecurity, authType, username, password, extra); connectionSecurity, authType, username, password, clientCertificateAlias, extra);
mAccount.setStoreUri(Store.createStoreUri(settings)); mAccount.setStoreUri(Store.createStoreUri(settings));
@ -470,4 +643,29 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG); Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
toast.show(); toast.show();
} }
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
/* unused */
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
/* unused */
}
};
OnClientCertificateChangedListener clientCertificateChangedListener = new OnClientCertificateChangedListener() {
@Override
public void onClientCertificateChanged(String alias) {
validateFields();
}
};
} }

View File

@ -12,7 +12,9 @@ import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.*; import android.widget.*;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.CompoundButton.OnCheckedChangeListener;
import com.fsck.k9.*; import com.fsck.k9.*;
import com.fsck.k9.activity.K9Activity; import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
@ -22,6 +24,8 @@ import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.transport.SmtpTransport; import com.fsck.k9.mail.transport.SmtpTransport;
import com.fsck.k9.view.ClientCertificateSpinner;
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -31,18 +35,26 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
private static final String EXTRA_ACCOUNT = "account"; private static final String EXTRA_ACCOUNT = "account";
private static final String EXTRA_MAKE_DEFAULT = "makeDefault"; 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_PORT = "587";
private static final String SMTP_SSL_PORT = "465"; private static final String SMTP_SSL_PORT = "465";
private EditText mUsernameView; private EditText mUsernameView;
private EditText mPasswordView; private EditText mPasswordView;
private ClientCertificateSpinner mClientCertificateSpinner;
private TextView mClientCertificateLabelView;
private TextView mPasswordLabelView;
private EditText mServerView; private EditText mServerView;
private EditText mPortView; private EditText mPortView;
private String mCurrentPortViewSetting;
private CheckBox mRequireLoginView; private CheckBox mRequireLoginView;
private ViewGroup mRequireLoginSettingsView; private ViewGroup mRequireLoginSettingsView;
private Spinner mSecurityTypeView; private Spinner mSecurityTypeView;
private int mCurrentSecurityTypeViewPosition;
private Spinner mAuthTypeView; private Spinner mAuthTypeView;
private int mCurrentAuthTypeViewPosition;
private ArrayAdapter<AuthType> mAuthTypeAdapter; private ArrayAdapter<AuthType> mAuthTypeAdapter;
private Button mNextButton; private Button mNextButton;
private Account mAccount; private Account mAccount;
@ -87,6 +99,9 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
mUsernameView = (EditText)findViewById(R.id.account_username); mUsernameView = (EditText)findViewById(R.id.account_username);
mPasswordView = (EditText)findViewById(R.id.account_password); mPasswordView = (EditText)findViewById(R.id.account_password);
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
mClientCertificateLabelView = (TextView)findViewById(R.id.account_client_certificate_label);
mPasswordLabelView = (TextView)findViewById(R.id.account_password_label);
mServerView = (EditText)findViewById(R.id.account_server); mServerView = (EditText)findViewById(R.id.account_server);
mPortView = (EditText)findViewById(R.id.account_port); mPortView = (EditText)findViewById(R.id.account_port);
mRequireLoginView = (CheckBox)findViewById(R.id.account_require_login); mRequireLoginView = (CheckBox)findViewById(R.id.account_require_login);
@ -96,36 +111,12 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
mNextButton = (Button)findViewById(R.id.next); mNextButton = (Button)findViewById(R.id.next);
mNextButton.setOnClickListener(this); mNextButton.setOnClickListener(this);
mRequireLoginView.setOnCheckedChangeListener(this);
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(this, mSecurityTypeView.setAdapter(ConnectionSecurity.getArrayAdapter(this));
android.R.layout.simple_spinner_item, ConnectionSecurity.values());
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mSecurityTypeView.setAdapter(securityTypesAdapter);
mAuthTypeAdapter = AuthType.getArrayAdapter(this); mAuthTypeAdapter = AuthType.getArrayAdapter(this);
mAuthTypeView.setAdapter(mAuthTypeAdapter); mAuthTypeView.setAdapter(mAuthTypeAdapter);
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
};
mUsernameView.addTextChangedListener(validationTextWatcher);
mPasswordView.addTextChangedListener(validationTextWatcher);
mServerView.addTextChangedListener(validationTextWatcher);
mPortView.addTextChangedListener(validationTextWatcher);
/* /*
* Only allow digits in the port field. * Only allow digits in the port field.
*/ */
@ -147,46 +138,48 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
try { try {
ServerSettings settings = Transport.decodeTransportUri(mAccount.getTransportUri()); ServerSettings settings = Transport.decodeTransportUri(mAccount.getTransportUri());
String username = settings.username;
String password = settings.password;
if (username != null) {
mUsernameView.setText(username);
mRequireLoginView.setChecked(true);
}
if (password != null) {
mPasswordView.setText(password);
}
updateAuthPlainTextFromSecurityType(settings.connectionSecurity); updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
if (savedInstanceState == null) {
// The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter // The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter
int position = mAuthTypeAdapter.getPosition(settings.authenticationType); mCurrentAuthTypeViewPosition = mAuthTypeAdapter.getPosition(settings.authenticationType);
mAuthTypeView.setSelection(position, false); } else {
mCurrentAuthTypeViewPosition = savedInstanceState.getInt(STATE_AUTH_TYPE_POSITION);
}
mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false);
updateViewFromAuthType();
// Select currently configured security type // 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 * Restore the spinner state now, before calling
* us to show a reasonable default which the user can change. * setOnItemSelectedListener(), thus avoiding a call to
* * onItemSelected(). Then, when the system restores the state
* Note: It's important that we set the listener *after* an initial option has been * (again) in onRestoreInstanceState(), The system will see that
* selected by the code above. Otherwise the listener might be called after * the new state is the same as the current state (set here), so
* onCreate() has been processed and the current port value set later in this * once again onItemSelected() will not be called.
* method is overridden with the default port for the selected security type.
*/ */
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { mCurrentSecurityTypeViewPosition = savedInstanceState.getInt(STATE_SECURITY_TYPE_POSITION);
@Override }
public void onItemSelected(AdapterView<?> parent, View view, int position, mSecurityTypeView.setSelection(mCurrentSecurityTypeViewPosition, false);
long id) {
updatePortFromSecurityType(); if (settings.username != null && !settings.username.isEmpty()) {
mUsernameView.setText(settings.username);
mRequireLoginView.setChecked(true);
mRequireLoginSettingsView.setVisibility(View.VISIBLE);
} }
@Override if (settings.password != null) {
public void onNothingSelected(AdapterView<?> parent) { /* unused */ } mPasswordView.setText(settings.password);
}); }
if (settings.clientCertificateAlias != null) {
mClientCertificateSpinner.setAlias(settings.clientCertificateAlias);
}
if (settings.host != null) { if (settings.host != null) {
mServerView.setText(settings.host); mServerView.setText(settings.host);
@ -197,8 +190,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
} else { } else {
updatePortFromSecurityType(); updatePortFromSecurityType();
} }
mCurrentPortViewSetting = mPortView.getText().toString();
validateFields();
} catch (Exception e) { } catch (Exception e) {
/* /*
* We should always be able to parse our own settings. * 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 @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putString(EXTRA_ACCOUNT, mAccount.getUuid()); 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() { 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 mNextButton
.setEnabled( .setEnabled(Utility.domainFieldValid(mServerView)
Utility.domainFieldValid(mServerView) && && Utility.requiredFieldValid(mPortView)
Utility.requiredFieldValid(mPortView) && && (!mRequireLoginView.isChecked()
(!mRequireLoginView.isChecked() || || hasValidPasswordSettings || hasValidExternalAuthSettings));
(Utility.requiredFieldValid(mUsernameView) &&
Utility.requiredFieldValid(mPasswordView))));
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128); Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
} }
private void updatePortFromSecurityType() { private void updatePortFromSecurityType() {
ConnectionSecurity securityType = (ConnectionSecurity) mSecurityTypeView.getSelectedItem(); ConnectionSecurity securityType = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
mPortView.setText(getDefaultSmtpPort(securityType));
updateAuthPlainTextFromSecurityType(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) { private String getDefaultSmtpPort(ConnectionSecurity securityType) {
@ -276,17 +476,23 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
String uri; String uri;
String username = null; String username = null;
String password = null; String password = null;
String clientCertificateAlias = null;
AuthType authType = null; AuthType authType = null;
if (mRequireLoginView.isChecked()) { if (mRequireLoginView.isChecked()) {
username = mUsernameView.getText().toString(); username = mUsernameView.getText().toString();
password = mPasswordView.getText().toString();
authType = (AuthType) mAuthTypeView.getSelectedItem(); authType = (AuthType) mAuthTypeView.getSelectedItem();
if (AuthType.EXTERNAL == authType) {
clientCertificateAlias = mClientCertificateSpinner.getAlias();
} else {
password = mPasswordView.getText().toString();
}
} }
String newHost = mServerView.getText().toString(); String newHost = mServerView.getText().toString();
int newPort = Integer.parseInt(mPortView.getText().toString()); int newPort = Integer.parseInt(mPortView.getText().toString());
String type = SmtpTransport.TRANSPORT_TYPE; String type = SmtpTransport.TRANSPORT_TYPE;
ServerSettings server = new ServerSettings(type, newHost, newPort, securityType, authType, username, password); ServerSettings server = new ServerSettings(type, newHost, newPort, securityType, authType, username, password, clientCertificateAlias);
uri = Transport.createTransportUri(server); uri = Transport.createTransportUri(server);
mAccount.deleteCertificate(newHost, newPort, CheckDirection.OUTGOING); mAccount.deleteCertificate(newHost, newPort, CheckDirection.OUTGOING);
mAccount.setTransportUri(uri); mAccount.setTransportUri(uri);
@ -312,4 +518,27 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG); Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
toast.show(); toast.show();
} }
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
};
OnClientCertificateChangedListener clientCertificateChangedListener = new OnClientCertificateChangedListener() {
@Override
public void onClientCertificateChanged(String alias) {
validateFields();
}
};
} }

View File

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

View File

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

View File

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

View File

@ -5,6 +5,10 @@ import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import javax.net.ssl.SSLHandshakeException;
import android.security.KeyChainException;
public class CertificateValidationException extends MessagingException { public class CertificateValidationException extends MessagingException {
public static final long serialVersionUID = -1; public static final long serialVersionUID = -1;
private X509Certificate[] mCertChain; private X509Certificate[] mCertChain;
@ -12,7 +16,11 @@ public class CertificateValidationException extends MessagingException {
public CertificateValidationException(String message) { public CertificateValidationException(String message) {
super(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) { public CertificateValidationException(final String message, Throwable throwable) {
@ -23,16 +31,56 @@ public class CertificateValidationException extends MessagingException {
private void scanForCause() { private void scanForCause() {
Throwable throwable = getCause(); 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 while (throwable != null
&& !(throwable instanceof CertPathValidatorException) && !(throwable instanceof CertPathValidatorException)
&& !(throwable instanceof CertificateException)) { && !(throwable instanceof CertificateException)
&& !(throwable instanceof KeyChainException)
&& !(throwable instanceof SSLHandshakeException)) {
throwable = throwable.getCause(); throwable = throwable.getCause();
} }
if (throwable != null) { if (throwable != null) {
mNeedsUserAttention = true; 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(); mCertChain = ((CertificateChainException) throwable).getCertChain();
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,23 +2,22 @@
package com.fsck.k9.mail.transport; package com.fsck.k9.mail.transport;
import android.util.Log; import android.util.Log;
import com.fsck.k9.Account; import com.fsck.k9.Account;
import com.fsck.k9.K9; import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.*; import com.fsck.k9.mail.*;
import com.fsck.k9.mail.Message.RecipientType; 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.EOLConvertingOutputStream;
import com.fsck.k9.mail.filter.LineWrapOutputStream; import com.fsck.k9.mail.filter.LineWrapOutputStream;
import com.fsck.k9.mail.filter.PeekableInputStream; import com.fsck.k9.mail.filter.PeekableInputStream;
import com.fsck.k9.mail.filter.SmtpDataStuffing; import com.fsck.k9.mail.filter.SmtpDataStuffing;
import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.LocalStore.LocalMessage;
import com.fsck.k9.net.ssl.TrustManagerFactory; import com.fsck.k9.net.ssl.SslHelper;
import com.fsck.k9.net.ssl.TrustedSocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
@ -27,9 +26,6 @@ import java.io.OutputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.*; import java.net.*;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.util.*; import java.util.*;
public class SmtpTransport extends Transport { public class SmtpTransport extends Transport {
@ -38,20 +34,23 @@ public class SmtpTransport extends Transport {
/** /**
* Decodes a SmtpTransport URI. * Decodes a SmtpTransport URI.
* *
* NOTE: In contrast to ImapStore and Pop3Store, the authType is appended at the end!
*
* <p>Possible forms:</p> * <p>Possible forms:</p>
* <pre> * <pre>
* smtp://user:password@server:port ConnectionSecurity.NONE * smtp://user:password:auth@server:port ConnectionSecurity.NONE
* smtp+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED * smtp+tls+://user:password:auth@server:port ConnectionSecurity.STARTTLS_REQUIRED
* smtp+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED * smtp+ssl+://user:password:auth@server:port ConnectionSecurity.SSL_TLS_REQUIRED
* </pre> * </pre>
*/ */
public static ServerSettings decodeUri(String uri) { public static ServerSettings decodeUri(String uri) {
String host; String host;
int port; int port;
ConnectionSecurity connectionSecurity; ConnectionSecurity connectionSecurity;
AuthType authType = AuthType.PLAIN; AuthType authType = null;
String username = null; String username = null;
String password = null; String password = null;
String clientCertificateAlias = null;
URI smtpUri; URI smtpUri;
try { try {
@ -95,14 +94,22 @@ public class SmtpTransport extends Transport {
if (smtpUri.getUserInfo() != null) { if (smtpUri.getUserInfo() != null) {
try { try {
String[] userInfoParts = smtpUri.getUserInfo().split(":"); String[] userInfoParts = smtpUri.getUserInfo().split(":");
if (userInfoParts.length > 0) { if (userInfoParts.length == 1) {
authType = AuthType.PLAIN;
username = URLDecoder.decode(userInfoParts[0], "UTF-8"); username = URLDecoder.decode(userInfoParts[0], "UTF-8");
} } else if (userInfoParts.length == 2) {
if (userInfoParts.length > 1) { authType = AuthType.PLAIN;
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
} else if (userInfoParts.length == 3) {
// NOTE: In SmptTransport URIs, the authType comes last!
authType = AuthType.valueOf(userInfoParts[2]);
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
if (authType == AuthType.EXTERNAL) {
clientCertificateAlias = URLDecoder.decode(userInfoParts[1], "UTF-8");
} else {
password = URLDecoder.decode(userInfoParts[1], "UTF-8"); password = URLDecoder.decode(userInfoParts[1], "UTF-8");
} }
if (userInfoParts.length > 2) {
authType = AuthType.valueOf(userInfoParts[2]);
} }
} catch (UnsupportedEncodingException enc) { } catch (UnsupportedEncodingException enc) {
// This shouldn't happen since the encoding is hardcoded to UTF-8 // This shouldn't happen since the encoding is hardcoded to UTF-8
@ -111,7 +118,7 @@ public class SmtpTransport extends Transport {
} }
return new ServerSettings(TRANSPORT_TYPE, host, port, connectionSecurity, return new ServerSettings(TRANSPORT_TYPE, host, port, connectionSecurity,
authType, username, password); authType, username, password, clientCertificateAlias);
} }
/** /**
@ -128,11 +135,14 @@ public class SmtpTransport extends Transport {
public static String createUri(ServerSettings server) { public static String createUri(ServerSettings server) {
String userEnc; String userEnc;
String passwordEnc; String passwordEnc;
String clientCertificateAliasEnc;
try { try {
userEnc = (server.username != null) ? userEnc = (server.username != null) ?
URLEncoder.encode(server.username, "UTF-8") : ""; URLEncoder.encode(server.username, "UTF-8") : "";
passwordEnc = (server.password != null) ? passwordEnc = (server.password != null) ?
URLEncoder.encode(server.password, "UTF-8") : ""; URLEncoder.encode(server.password, "UTF-8") : "";
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
} }
catch (UnsupportedEncodingException e) { catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Could not encode username or password", e); throw new IllegalArgumentException("Could not encode username or password", e);
@ -152,10 +162,17 @@ public class SmtpTransport extends Transport {
break; break;
} }
String userInfo = userEnc + ":" + passwordEnc; String userInfo = null;
AuthType authType = server.authenticationType; AuthType authType = server.authenticationType;
// NOTE: authType is append at last item, in contrast to ImapStore and Pop3Store!
if (authType != null) { if (authType != null) {
userInfo += ":" + authType.name(); if (AuthType.EXTERNAL == authType) {
userInfo = userEnc + ":" + clientCertificateAliasEnc + ":" + authType.name();
} else {
userInfo = userEnc + ":" + passwordEnc + ":" + authType.name();
}
} else {
userInfo = userEnc + ":" + passwordEnc;
} }
try { try {
return new URI(scheme, userInfo, server.host, server.port, null, null, return new URI(scheme, userInfo, server.host, server.port, null, null,
@ -170,6 +187,7 @@ public class SmtpTransport extends Transport {
int mPort; int mPort;
String mUsername; String mUsername;
String mPassword; String mPassword;
String mClientCertificateAlias;
AuthType mAuthType; AuthType mAuthType;
ConnectionSecurity mConnectionSecurity; ConnectionSecurity mConnectionSecurity;
Socket mSocket; Socket mSocket;
@ -194,6 +212,7 @@ public class SmtpTransport extends Transport {
mAuthType = settings.authenticationType; mAuthType = settings.authenticationType;
mUsername = settings.username; mUsername = settings.username;
mPassword = settings.password; mPassword = settings.password;
mClientCertificateAlias = settings.clientCertificateAlias;
} }
@Override @Override
@ -205,12 +224,7 @@ public class SmtpTransport extends Transport {
try { try {
SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort); SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort);
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) { if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
SSLContext sslContext = SSLContext.getInstance("TLS"); mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(
mHost, mPort) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext);
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
secureConnection = true; secureConnection = true;
} else { } else {
@ -264,12 +278,9 @@ public class SmtpTransport extends Transport {
if (extensions.containsKey("STARTTLS")) { if (extensions.containsKey("STARTTLS")) {
executeSimpleCommand("STARTTLS"); executeSimpleCommand("STARTTLS");
SSLContext sslContext = SSLContext.getInstance("TLS"); mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
sslContext.init(null, mClientCertificateAlias);
new TrustManager[] { TrustManagerFactory.get(mHost,
mPort) }, new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
mPort, true);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
1024)); 1024));
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024); mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
@ -288,19 +299,20 @@ public class SmtpTransport extends Transport {
* "STARTTLS (if available)" setting. * "STARTTLS (if available)" setting.
*/ */
throw new CertificateValidationException( throw new CertificateValidationException(
"STARTTLS connection security not available", "STARTTLS connection security not available");
new CertificateException());
} }
} }
boolean authLoginSupported = false; boolean authLoginSupported = false;
boolean authPlainSupported = false; boolean authPlainSupported = false;
boolean authCramMD5Supported = false; boolean authCramMD5Supported = false;
boolean authExternalSupported = false;
if (extensions.containsKey("AUTH")) { if (extensions.containsKey("AUTH")) {
List<String> saslMech = Arrays.asList(extensions.get("AUTH").split(" ")); List<String> saslMech = Arrays.asList(extensions.get("AUTH").split(" "));
authLoginSupported = saslMech.contains("LOGIN"); authLoginSupported = saslMech.contains("LOGIN");
authPlainSupported = saslMech.contains("PLAIN"); authPlainSupported = saslMech.contains("PLAIN");
authCramMD5Supported = saslMech.contains("CRAM-MD5"); authCramMD5Supported = saslMech.contains("CRAM-MD5");
authExternalSupported = saslMech.contains("EXTERNAL");
} }
if (extensions.containsKey("SIZE")) { if (extensions.containsKey("SIZE")) {
try { try {
@ -312,8 +324,9 @@ public class SmtpTransport extends Transport {
} }
} }
if (mUsername != null && mUsername.length() > 0 && if (mUsername != null
mPassword != null && mPassword.length() > 0) { && mUsername.length() > 0
&& (mPassword != null && mPassword.length() > 0 || AuthType.EXTERNAL == mAuthType)) {
switch (mAuthType) { switch (mAuthType) {
@ -342,6 +355,24 @@ public class SmtpTransport extends Transport {
} }
break; 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, * AUTOMATIC is an obsolete option which is unavailable to users,
* but it still may exist in a user's settings from a previous * 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". * 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 * Shorter lines are either errors of contain only a reply code. Those cases will
* be handled by checkLine() below. * 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(); String line = readLine();
while (line.length() >= 4) { while (line.length() >= 4) {
@ -677,29 +714,32 @@ public class SmtpTransport extends Transport {
AuthenticationFailedException, IOException { AuthenticationFailedException, IOException {
try { try {
executeSimpleCommand("AUTH LOGIN"); executeSimpleCommand("AUTH LOGIN");
executeSimpleCommand(new String(Base64.encodeBase64(username.getBytes())), true); executeSimpleCommand(Utility.base64Encode(username), true);
executeSimpleCommand(new String(Base64.encodeBase64(password.getBytes())), true); executeSimpleCommand(Utility.base64Encode(password), true);
} catch (MessagingException me) { } catch (NegativeSmtpReplyException exception) {
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') { if (exception.getReplyCode() == 535) {
throw new AuthenticationFailedException("AUTH LOGIN failed (" + me.getMessage() // 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, private void saslAuthPlain(String username, String password) throws MessagingException,
AuthenticationFailedException, IOException { AuthenticationFailedException, IOException {
byte[] data = ("\000" + username + "\000" + password).getBytes(); String data = Utility.base64Encode("\000" + username + "\000" + password);
data = new Base64().encode(data);
try { try {
executeSimpleCommand("AUTH PLAIN " + new String(data), true); executeSimpleCommand("AUTH PLAIN " + data, true);
} catch (MessagingException me) { } catch (NegativeSmtpReplyException exception) {
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') { if (exception.getReplyCode() == 535) {
throw new AuthenticationFailedException("AUTH PLAIN failed (" + me.getMessage() // Authentication credentials invalid
+ ")"); throw new AuthenticationFailedException("AUTH PLAIN failed ("
+ exception.getMessage() + ")");
} else {
throw exception;
} }
throw me;
} }
} }
@ -717,9 +757,20 @@ public class SmtpTransport extends Transport {
try { try {
executeSimpleCommand(b64CRAMString, true); executeSimpleCommand(b64CRAMString, true);
} catch (NegativeSmtpReplyException exception) { } catch (NegativeSmtpReplyException exception) {
if (exception.getReplyCode() == 535) {
// Authentication credentials invalid
throw new AuthenticationFailedException(exception.getMessage(), exception); 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). * 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 getPassword();
String getClientCertificateAlias();
boolean useCompression(int type); boolean useCompression(int type);
String getPathPrefix(); 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 CONNECTION_SECURITY_ELEMENT = "connection-security";
public static final String AUTHENTICATION_TYPE_ELEMENT = "authentication-type"; public static final String AUTHENTICATION_TYPE_ELEMENT = "authentication-type";
public static final String USERNAME_ELEMENT = "username"; 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 PASSWORD_ELEMENT = "password";
public static final String EXTRA_ELEMENT = "extra"; public static final String EXTRA_ELEMENT = "extra";
public static final String IDENTITIES_ELEMENT = "identities"; public static final String IDENTITIES_ELEMENT = "identities";
@ -229,9 +230,14 @@ public class SettingsExporter {
if (incoming.port != -1) { if (incoming.port != -1) {
writeElement(serializer, PORT_ELEMENT, Integer.toString(incoming.port)); writeElement(serializer, PORT_ELEMENT, Integer.toString(incoming.port));
} }
if (incoming.connectionSecurity != null) {
writeElement(serializer, CONNECTION_SECURITY_ELEMENT, incoming.connectionSecurity.name()); writeElement(serializer, CONNECTION_SECURITY_ELEMENT, incoming.connectionSecurity.name());
}
if (incoming.authenticationType != null) {
writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType.name()); writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType.name());
}
writeElement(serializer, USERNAME_ELEMENT, incoming.username); writeElement(serializer, USERNAME_ELEMENT, incoming.username);
writeElement(serializer, CLIENT_CERTIFICATE_ALIAS_ELEMENT, incoming.clientCertificateAlias);
// XXX For now we don't export the password // XXX For now we don't export the password
//writeElement(serializer, PASSWORD_ELEMENT, incoming.password); //writeElement(serializer, PASSWORD_ELEMENT, incoming.password);
@ -256,9 +262,14 @@ public class SettingsExporter {
if (outgoing.port != -1) { if (outgoing.port != -1) {
writeElement(serializer, PORT_ELEMENT, Integer.toString(outgoing.port)); writeElement(serializer, PORT_ELEMENT, Integer.toString(outgoing.port));
} }
if (outgoing.connectionSecurity != null) {
writeElement(serializer, CONNECTION_SECURITY_ELEMENT, outgoing.connectionSecurity.name()); writeElement(serializer, CONNECTION_SECURITY_ELEMENT, outgoing.connectionSecurity.name());
}
if (outgoing.authenticationType != null) {
writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType.name()); writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType.name());
}
writeElement(serializer, USERNAME_ELEMENT, outgoing.username); writeElement(serializer, USERNAME_ELEMENT, outgoing.username);
writeElement(serializer, CLIENT_CERTIFICATE_ALIAS_ELEMENT, outgoing.clientCertificateAlias);
// XXX For now we don't export the password // XXX For now we don't export the password
//writeElement(serializer, PASSWORD_ELEMENT, outgoing.password); //writeElement(serializer, PASSWORD_ELEMENT, outgoing.password);

View File

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

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"); extra.put("pathPrefix", "customPathPrefix");
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143, 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); String uri = Store.createStoreUri(settings);
@ -95,7 +95,7 @@ public class ImapStoreUriTest extends TestCase {
extra.put("pathPrefix", ""); extra.put("pathPrefix", "");
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143, 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); String uri = Store.createStoreUri(settings);
@ -104,7 +104,7 @@ public class ImapStoreUriTest extends TestCase {
public void testCreateStoreUriImapNoExtra() { public void testCreateStoreUriImapNoExtra() {
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143, 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); String uri = Store.createStoreUri(settings);
@ -116,7 +116,7 @@ public class ImapStoreUriTest extends TestCase {
extra.put("autoDetectNamespace", "true"); extra.put("autoDetectNamespace", "true");
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143, 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); String uri = Store.createStoreUri(settings);