Merge pull request #474 from k9mail/tls-client-cert-auth
Client Certificate Authentication
BIN
res/drawable-hdpi/ic_action_collapse_dark.png
Normal file
After Width: | Height: | Size: 416 B |
BIN
res/drawable-hdpi/ic_action_collapse_light.png
Normal file
After Width: | Height: | Size: 467 B |
BIN
res/drawable-hdpi/ic_action_expand_dark.png
Normal file
After Width: | Height: | Size: 414 B |
BIN
res/drawable-hdpi/ic_action_expand_light.png
Normal file
After Width: | Height: | Size: 415 B |
BIN
res/drawable-mdpi/ic_action_collapse_dark.png
Normal file
After Width: | Height: | Size: 317 B |
BIN
res/drawable-mdpi/ic_action_collapse_light.png
Normal file
After Width: | Height: | Size: 404 B |
BIN
res/drawable-mdpi/ic_action_expand_dark.png
Normal file
After Width: | Height: | Size: 327 B |
BIN
res/drawable-mdpi/ic_action_expand_light.png
Normal file
After Width: | Height: | Size: 345 B |
BIN
res/drawable-xhdpi/ic_action_collapse_dark.png
Normal file
After Width: | Height: | Size: 602 B |
BIN
res/drawable-xhdpi/ic_action_collapse_light.png
Normal file
After Width: | Height: | Size: 631 B |
BIN
res/drawable-xhdpi/ic_action_expand_dark.png
Normal file
After Width: | Height: | Size: 529 B |
BIN
res/drawable-xhdpi/ic_action_expand_light.png
Normal file
After Width: | Height: | Size: 582 B |
BIN
res/drawable-xxhdpi/ic_action_collapse_dark.png
Normal file
After Width: | Height: | Size: 681 B |
BIN
res/drawable-xxhdpi/ic_action_collapse_light.png
Normal file
After Width: | Height: | Size: 901 B |
BIN
res/drawable-xxhdpi/ic_action_expand_dark.png
Normal file
After Width: | Height: | Size: 726 B |
BIN
res/drawable-xxhdpi/ic_action_expand_light.png
Normal file
After Width: | Height: | Size: 974 B |
@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:custom="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_width="fill_parent"
|
||||
>
|
||||
android:layout_width="fill_parent" >
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="wrap_content"
|
||||
@ -12,19 +12,21 @@
|
||||
android:padding="6dip"
|
||||
android:fadingEdge="none"
|
||||
android:scrollbarStyle="outsideInset" >
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_gravity="center_horizontal|center_vertical"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<EditText
|
||||
android:id="@+id/account_email"
|
||||
android:hint="@string/account_setup_basics_email_hint"
|
||||
android:singleLine="true"
|
||||
android:inputType="textEmailAddress"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
/>
|
||||
android:layout_width="fill_parent" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/account_password"
|
||||
android:inputType="textPassword"
|
||||
@ -32,20 +34,42 @@
|
||||
android:singleLine="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:nextFocusDown="@+id/next"
|
||||
/>
|
||||
android:nextFocusDown="@+id/next" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/show_password"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:text="@string/show_password"
|
||||
/>
|
||||
android:text="@string/account_setup_basics_show_password" />
|
||||
|
||||
<com.fsck.k9.view.ClientCertificateSpinner
|
||||
android:id="@+id/account_client_certificate_spinner"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.fsck.k9.view.FoldableLinearLayout
|
||||
android:id="@+id/foldable_advanced_options"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
custom:foldedLabel="@string/client_certificate_advanced_options"
|
||||
custom:unFoldedLabel="@string/client_certificate_advanced_options" >
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/account_client_certificate"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/account_setup_basics_client_certificate" />
|
||||
</com.fsck.k9.view.FoldableLinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="0dip"
|
||||
android:layout_weight="1"
|
||||
/>
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<include layout="@layout/wizard_setup" />
|
||||
|
||||
</LinearLayout>
|
@ -16,31 +16,6 @@
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:text="@string/account_setup_incoming_username_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
<EditText
|
||||
android:id="@+id/account_username"
|
||||
android:singleLine="true"
|
||||
android:inputType="textEmailAddress"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_incoming_username_label" />
|
||||
<TextView
|
||||
android:text="@string/account_setup_incoming_password_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
<EditText
|
||||
android:id="@+id/account_password"
|
||||
android:inputType="textPassword"
|
||||
android:singleLine="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent" />
|
||||
<!-- This text may be changed in code if the server is IMAP, etc. -->
|
||||
<TextView
|
||||
android:id="@+id/account_server_label"
|
||||
@ -67,18 +42,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_incoming_security_label" />
|
||||
<TextView
|
||||
android:id="@+id/account_auth_type_label"
|
||||
android:text="@string/account_setup_incoming_auth_type_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
<Spinner
|
||||
android:id="@+id/account_auth_type"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_incoming_auth_type_label" />
|
||||
<TextView
|
||||
android:text="@string/account_setup_incoming_port_label"
|
||||
android:layout_height="wrap_content"
|
||||
@ -92,6 +55,57 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_incoming_port_label" />
|
||||
<TextView
|
||||
android:text="@string/account_setup_incoming_username_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
<EditText
|
||||
android:id="@+id/account_username"
|
||||
android:singleLine="true"
|
||||
android:inputType="textEmailAddress"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_incoming_username_label" />
|
||||
<TextView
|
||||
android:id="@+id/account_auth_type_label"
|
||||
android:text="@string/account_setup_incoming_auth_type_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
<Spinner
|
||||
android:id="@+id/account_auth_type"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_incoming_auth_type_label" />
|
||||
<TextView
|
||||
android:id="@+id/account_password_label"
|
||||
android:text="@string/account_setup_incoming_password_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
<EditText
|
||||
android:id="@+id/account_password"
|
||||
android:inputType="textPassword"
|
||||
android:singleLine="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent" />
|
||||
<TextView
|
||||
android:id="@+id/account_client_certificate_label"
|
||||
android:text="@string/account_setup_incoming_client_certificate_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:visibility="gone" />
|
||||
<com.fsck.k9.view.ClientCertificateSpinner
|
||||
android:id="@+id/account_client_certificate_spinner"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:visibility="gone" />
|
||||
<LinearLayout
|
||||
android:id="@+id/imap_path_prefix_section"
|
||||
android:layout_width="fill_parent"
|
||||
|
@ -12,16 +12,19 @@
|
||||
android:padding="6dip"
|
||||
android:fadingEdge="none"
|
||||
android:scrollbarStyle="outsideInset">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:text="@string/account_setup_outgoing_smtp_server_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/account_server"
|
||||
android:singleLine="true"
|
||||
@ -29,23 +32,27 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_outgoing_smtp_server_label" />
|
||||
|
||||
<TextView
|
||||
android:text="@string/account_setup_outgoing_security_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/account_security_type"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_outgoing_security_label" />
|
||||
|
||||
<TextView
|
||||
android:text="@string/account_setup_outgoing_port_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/account_port"
|
||||
android:singleLine="true"
|
||||
@ -53,34 +60,27 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_outgoing_port_label" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/account_require_login"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/account_setup_outgoing_require_login_label" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/account_require_login_settings"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
<TextView
|
||||
android:text="@string/account_setup_outgoing_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:text="@string/account_setup_outgoing_username_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/account_username"
|
||||
android:singleLine="true"
|
||||
@ -88,12 +88,28 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_outgoing_username_label" />
|
||||
|
||||
<TextView
|
||||
android:text="@string/account_setup_outgoing_authentication_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/account_auth_type"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_outgoing_authentication_label" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_password_label"
|
||||
android:text="@string/account_setup_outgoing_password_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/account_password"
|
||||
android:singleLine="true"
|
||||
@ -101,12 +117,30 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:contentDescription="@string/account_setup_outgoing_password_label" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_client_certificate_label"
|
||||
android:text="@string/account_setup_incoming_client_certificate_label"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.fsck.k9.view.ClientCertificateSpinner
|
||||
android:id="@+id/account_client_certificate_spinner"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="0dip"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<include layout="@layout/wizard_next" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -14,7 +14,13 @@
|
||||
android:layout_height="wrap_content"
|
||||
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
|
||||
android:id="@+id/password_prompt_incoming_server"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
@ -26,8 +32,10 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginBottom="10dip"/>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Password prompt for the outgoing server. Won't be shown for WebDAV accounts! -->
|
||||
<!-- Password prompt for the outgoing server. Won't be shown for WebDAV accounts,
|
||||
accounts without user names, or accounts with AuthType EXTERNAL! -->
|
||||
<LinearLayout
|
||||
android:id="@+id/outgoing_server_prompt"
|
||||
android:orientation="vertical"
|
||||
|
22
res/layout/client_certificate_spinner.xml
Normal 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>
|
38
res/layout/foldable_linearlayout.xml
Normal 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>
|
@ -35,6 +35,8 @@
|
||||
<attr name="iconActionSave" format="reference" />
|
||||
<attr name="iconActionCancel" format="reference" />
|
||||
<attr name="iconActionRequestReadReceipt" format="reference" />
|
||||
<attr name="iconActionExpand" format="reference" />
|
||||
<attr name="iconActionCollapse" format="reference" />
|
||||
<attr name="textColorPrimaryRecipientDropdown" format="reference" />
|
||||
<attr name="textColorSecondaryRecipientDropdown" format="reference" />
|
||||
<attr name="backgroundColorChooseAccountHeader" format="color" />
|
||||
@ -59,4 +61,9 @@
|
||||
<attr name="android:summary" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="FoldableLinearLayout">
|
||||
<attr name="foldedLabel" format="string" />
|
||||
<attr name="unFoldedLabel" format="string" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
@ -4,5 +4,6 @@
|
||||
<item type="id" name="dialog_confirm_delete"/>
|
||||
<item type="id" name="dialog_confirm_spam"/>
|
||||
<item type="id" name="dialog_attachment_progress"/>
|
||||
<item type="id" name="dialog_account_setup_error"/>
|
||||
|
||||
</resources>
|
||||
|
@ -352,6 +352,7 @@ Please submit bug reports, contribute new features and ask questions at
|
||||
<string name="account_setup_basics_title">Set up a new account</string>
|
||||
<string name="account_setup_basics_email_hint">Email address</string>
|
||||
<string name="account_setup_basics_password_hint">Password</string>
|
||||
<string name="account_setup_basics_show_password">Show password</string>
|
||||
<string name="account_setup_basics_manual_setup_action">Manual setup</string>
|
||||
|
||||
<string name="account_setup_check_settings_title"/>
|
||||
@ -375,10 +376,12 @@ Please submit bug reports, contribute new features and ask questions at
|
||||
<string name="account_setup_auth_type_normal_password">Normal password</string>
|
||||
<string name="account_setup_auth_type_insecure_password">Password, transmitted insecurely</string>
|
||||
<string name="account_setup_auth_type_encrypted_password">Encrypted password</string>
|
||||
<string name="account_setup_auth_type_tls_client_certificate">Client certificate</string>
|
||||
|
||||
<string name="account_setup_incoming_title">Incoming server settings</string>
|
||||
<string name="account_setup_incoming_username_label">Username</string>
|
||||
<string name="account_setup_incoming_password_label">Password</string>
|
||||
<string name="account_setup_incoming_client_certificate_label">Client certificate</string>
|
||||
<string name="account_setup_incoming_pop_server_label">POP3 server</string>
|
||||
<string name="account_setup_incoming_imap_server_label">IMAP server</string>
|
||||
<string name="account_setup_incoming_webdav_server_label">Exchange server</string>
|
||||
@ -388,6 +391,7 @@ Please submit bug reports, contribute new features and ask questions at
|
||||
<string name="account_setup_incoming_security_none_label">None</string>
|
||||
<string name="account_setup_incoming_security_ssl_label">SSL/TLS</string>
|
||||
<string name="account_setup_incoming_security_tls_label">STARTTLS</string>
|
||||
<string name="account_setup_incoming_invalid_setting_combo_notice">\"<xliff:g id="setting_1_label">%1$s</xliff:g> = <xliff:g id="setting_1_value">%2$s</xliff:g>\" is not valid with \"<xliff:g id="setting_2_label">%3$s</xliff:g> = <xliff:g id="setting_2_value">%4$s</xliff:g>\"</string>
|
||||
|
||||
<string name="account_setup_incoming_delete_policy_label">When I delete a message</string>
|
||||
<string name="account_setup_incoming_delete_policy_never_label">Do not delete on server</string>
|
||||
@ -437,6 +441,7 @@ Please submit bug reports, contribute new features and ask questions at
|
||||
<string name="account_setup_outgoing_username_label">Username</string>
|
||||
<string name="account_setup_outgoing_password_label">Password</string>
|
||||
<string name="account_setup_outgoing_authentication_label">Authentication</string>
|
||||
<string name="account_setup_outgoing_invalid_setting_combo_notice">\"<xliff:g id="setting_1_label">%1$s</xliff:g> = <xliff:g id="setting_1_value">%2$s</xliff:g>\" is not valid with \"<xliff:g id="setting_2_label">%3$s</xliff:g> = <xliff:g id="setting_2_value">%4$s</xliff:g>\"</string>
|
||||
|
||||
<string name="account_setup_bad_uri">Invalid setup: <xliff:g id="err_mess">%s</xliff:g></string>
|
||||
|
||||
@ -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_message">Fetching attachment…</string>
|
||||
|
||||
<string name="show_password">Show password</string>
|
||||
<string name="auth_external_error">Unable to authenticate. The server does not advertise the SASL EXTERNAL capability. This could be due to a problem with the client certificate (expired, unknown certificate authority) or some other configuration problem.</string>
|
||||
|
||||
<!-- === OpenPGP specific ================================================================== -->
|
||||
<string name="openpgp_decrypting_verifying">Decrypting/Verifying…</string>
|
||||
@ -1114,4 +1119,11 @@ Please submit bug reports, contribute new features and ask questions at
|
||||
<string name="openpgp_error">OpenPGP Error:</string>
|
||||
<string name="openpgp_user_id">User Id</string>
|
||||
|
||||
<!-- === Client certificates specific ================================================================== -->
|
||||
<string name="account_setup_basics_client_certificate">Use client certificate</string>
|
||||
<string name="client_certificate_spinner_empty">No client certificate</string>
|
||||
<string name="client_certificate_spinner_delete">Remove client certificate selection</string>
|
||||
<string name="client_certificate_retrieval_failure">"Failed to retrieve client certificate for alias \"<xliff:g id="alias">%s</xliff:g>\""</string>
|
||||
<string name="client_certificate_advanced_options">Advanced options</string>
|
||||
<string name="client_certificate_expired">"Client certificate \"<xliff:g id="certificate_alias">%1$s</xliff:g>\" has expired or is not yet valid (<xliff:g id="exception_message">%2$s</xliff:g>)"</string>
|
||||
</resources>
|
||||
|
@ -37,6 +37,8 @@
|
||||
<item name="iconActionSave">@drawable/ic_action_save_light</item>
|
||||
<item name="iconActionCancel">@drawable/ic_action_cancel_light</item>
|
||||
<item name="iconActionRequestReadReceipt">@drawable/ic_action_request_read_receipt_light</item>
|
||||
<item name="iconActionExpand">@drawable/ic_action_expand_light</item>
|
||||
<item name="iconActionCollapse">@drawable/ic_action_collapse_light</item>
|
||||
<item name="textColorPrimaryRecipientDropdown">@android:color/primary_text_light</item>
|
||||
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_light</item>
|
||||
<item name="messageListSelectedBackgroundColor">#8038B8E2</item>
|
||||
@ -89,6 +91,8 @@
|
||||
<item name="iconActionSave">@drawable/ic_action_save_dark</item>
|
||||
<item name="iconActionCancel">@drawable/ic_action_cancel_dark</item>
|
||||
<item name="iconActionRequestReadReceipt">@drawable/ic_action_request_read_receipt_dark</item>
|
||||
<item name="iconActionExpand">@drawable/ic_action_expand_dark</item>
|
||||
<item name="iconActionCollapse">@drawable/ic_action_collapse_dark</item>
|
||||
<item name="textColorPrimaryRecipientDropdown">@android:color/primary_text_dark</item>
|
||||
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_dark</item>
|
||||
<item name="messageListSelectedBackgroundColor">#8038B8E2</item>
|
||||
|
@ -1879,7 +1879,7 @@ public class Account implements BaseAccount {
|
||||
public void addCertificate(CheckDirection direction,
|
||||
X509Certificate certificate) throws CertificateException {
|
||||
Uri uri;
|
||||
if (direction.equals(CheckDirection.INCOMING)) {
|
||||
if (direction == CheckDirection.INCOMING) {
|
||||
uri = Uri.parse(getStoreUri());
|
||||
} else {
|
||||
uri = Uri.parse(getTransportUri());
|
||||
@ -1896,7 +1896,7 @@ public class Account implements BaseAccount {
|
||||
public void deleteCertificate(String newHost, int newPort,
|
||||
CheckDirection direction) {
|
||||
Uri uri;
|
||||
if (direction.equals(CheckDirection.INCOMING)) {
|
||||
if (direction == CheckDirection.INCOMING) {
|
||||
uri = Uri.parse(getStoreUri());
|
||||
} else {
|
||||
uri = Uri.parse(getTransportUri());
|
||||
|
@ -75,6 +75,7 @@ import com.fsck.k9.activity.setup.Prefs;
|
||||
import com.fsck.k9.activity.setup.WelcomeMessage;
|
||||
import com.fsck.k9.controller.MessagingController;
|
||||
import com.fsck.k9.helper.SizeFormatter;
|
||||
import com.fsck.k9.mail.AuthType;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.Store;
|
||||
import com.fsck.k9.mail.Transport;
|
||||
@ -743,7 +744,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
|
||||
public boolean retain() {
|
||||
if (mDialog != null) {
|
||||
// Retain entered passwords and checkbox state
|
||||
if (mIncomingPasswordView != null) {
|
||||
mIncomingPassword = mIncomingPasswordView.getText().toString();
|
||||
}
|
||||
if (mOutgoingPasswordView != null) {
|
||||
mOutgoingPassword = mOutgoingPasswordView.getText().toString();
|
||||
mUseIncoming = mUseIncomingView.isChecked();
|
||||
@ -770,9 +773,22 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
|
||||
ServerSettings incoming = Store.decodeStoreUri(mAccount.getStoreUri());
|
||||
ServerSettings outgoing = Transport.decodeTransportUri(mAccount.getTransportUri());
|
||||
|
||||
// Don't ask for the password to the outgoing server for WebDAV accounts, because
|
||||
// incoming and outgoing servers are identical for this account type.
|
||||
boolean configureOutgoingServer = !WebDavStore.STORE_TYPE.equals(outgoing.type);
|
||||
/*
|
||||
* Don't ask for the password to the outgoing server for WebDAV
|
||||
* accounts, because incoming and outgoing servers are identical for
|
||||
* this account type. Also don't ask when the username is missing.
|
||||
* Also don't ask when the AuthType is EXTERNAL.
|
||||
*/
|
||||
boolean configureOutgoingServer = AuthType.EXTERNAL != outgoing.authenticationType
|
||||
&& !WebDavStore.STORE_TYPE.equals(outgoing.type)
|
||||
&& outgoing.username != null
|
||||
&& !outgoing.username.isEmpty()
|
||||
&& (outgoing.password == null || outgoing.password
|
||||
.isEmpty());
|
||||
|
||||
boolean configureIncomingServer = AuthType.EXTERNAL != incoming.authenticationType
|
||||
&& (incoming.password == null || incoming.password
|
||||
.isEmpty());
|
||||
|
||||
// Create a ScrollView that will be used as container for the whole layout
|
||||
final ScrollView scrollView = new ScrollView(activity);
|
||||
@ -785,7 +801,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
String incomingPassword = mIncomingPasswordView.getText().toString();
|
||||
String incomingPassword = null;
|
||||
if (mIncomingPasswordView != null) {
|
||||
incomingPassword = mIncomingPasswordView.getText().toString();
|
||||
}
|
||||
String outgoingPassword = null;
|
||||
if (mOutgoingPasswordView != null) {
|
||||
outgoingPassword = (mUseIncomingView.isChecked()) ?
|
||||
@ -819,10 +838,11 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
|
||||
TextView intro = (TextView) layout.findViewById(R.id.password_prompt_intro);
|
||||
String serverPasswords = activity.getResources().getQuantityString(
|
||||
R.plurals.settings_import_server_passwords,
|
||||
(configureOutgoingServer) ? 2 : 1);
|
||||
(configureIncomingServer && configureOutgoingServer) ? 2 : 1);
|
||||
intro.setText(activity.getString(R.string.settings_import_activate_account_intro,
|
||||
mAccount.getDescription(), serverPasswords));
|
||||
|
||||
if (configureIncomingServer) {
|
||||
// Display the hostname of the incoming server
|
||||
TextView incomingText = (TextView) layout.findViewById(
|
||||
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.addTextChangedListener(this);
|
||||
} else {
|
||||
layout.findViewById(R.id.incoming_server_prompt).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (configureOutgoingServer) {
|
||||
// Display the hostname of the outgoing server
|
||||
@ -845,6 +868,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
|
||||
|
||||
mUseIncomingView = (CheckBox) layout.findViewById(
|
||||
R.id.use_incoming_server_password);
|
||||
|
||||
if (configureIncomingServer) {
|
||||
mUseIncomingView.setChecked(true);
|
||||
mUseIncomingView.setOnCheckedChangeListener(new OnCheckedChangeListener() {
|
||||
@Override
|
||||
@ -858,6 +883,11 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mUseIncomingView.setChecked(false);
|
||||
mUseIncomingView.setVisibility(View.GONE);
|
||||
mOutgoingPasswordView.setEnabled(true);
|
||||
}
|
||||
} else {
|
||||
layout.findViewById(R.id.outgoing_server_prompt).setVisibility(View.GONE);
|
||||
}
|
||||
@ -871,15 +901,21 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
|
||||
// Restore the contents of the password boxes and the checkbox (if the dialog was
|
||||
// retained during a configuration change).
|
||||
if (restore) {
|
||||
if (configureIncomingServer) {
|
||||
mIncomingPasswordView.setText(mIncomingPassword);
|
||||
}
|
||||
if (configureOutgoingServer) {
|
||||
mOutgoingPasswordView.setText(mOutgoingPassword);
|
||||
mUseIncomingView.setChecked(mUseIncoming);
|
||||
}
|
||||
} else {
|
||||
if (configureIncomingServer) {
|
||||
// Trigger afterTextChanged() being called
|
||||
// Work around this bug: https://code.google.com/p/android/issues/detail?id=6360
|
||||
mIncomingPasswordView.setText(mIncomingPasswordView.getText());
|
||||
} else {
|
||||
mOutgoingPasswordView.setText(mOutgoingPasswordView.getText());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -887,6 +923,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
|
||||
public void afterTextChanged(Editable arg0) {
|
||||
boolean enable = false;
|
||||
// Is the password box for the incoming server password empty?
|
||||
if (mIncomingPasswordView != null) {
|
||||
if (mIncomingPasswordView.getText().length() > 0) {
|
||||
// Do we need to check the outgoing server password box?
|
||||
if (mOutgoingPasswordView == null) {
|
||||
@ -899,6 +936,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
|
||||
enable = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
enable = mOutgoingPasswordView.getText().length() > 0;
|
||||
}
|
||||
|
||||
// Disable "OK" button if the user hasn't specified all necessary passwords.
|
||||
mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(enable);
|
||||
@ -948,12 +988,14 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try {
|
||||
if (mIncomingPassword != null) {
|
||||
// Set incoming server password
|
||||
String storeUri = mAccount.getStoreUri();
|
||||
ServerSettings incoming = Store.decodeStoreUri(storeUri);
|
||||
ServerSettings newIncoming = incoming.newPassword(mIncomingPassword);
|
||||
String newStoreUri = Store.createStoreUri(newIncoming);
|
||||
mAccount.setStoreUri(newStoreUri);
|
||||
}
|
||||
|
||||
if (mOutgoingPassword != null) {
|
||||
// Set outgoing server password
|
||||
|
@ -89,7 +89,23 @@ public class AccountSetupAccountType extends K9Activity implements OnClickListen
|
||||
private void onWebDav() {
|
||||
try {
|
||||
URI uri = new URI(mAccount.getStoreUri());
|
||||
uri = new URI("webdav+ssl+", uri.getUserInfo(), uri.getHost(), uri.getPort(), null, null, null);
|
||||
|
||||
/*
|
||||
* The user info we have been given from
|
||||
* AccountSetupBasics.onManualSetup() is encoded as an IMAP store
|
||||
* URI: AuthType:UserName:Password (no fields should be empty).
|
||||
* However, AuthType is not applicable to WebDAV nor to its store
|
||||
* URI. Re-encode without it, using just the UserName and Password.
|
||||
*/
|
||||
String userPass = "";
|
||||
String[] userInfo = uri.getUserInfo().split(":");
|
||||
if (userInfo.length > 1) {
|
||||
userPass = userInfo[1];
|
||||
}
|
||||
if (userInfo.length > 2) {
|
||||
userPass = userPass + ":" + userInfo[2];
|
||||
}
|
||||
uri = new URI("webdav+ssl+", userPass, uri.getHost(), uri.getPort(), null, null, null);
|
||||
mAccount.setStoreUri(uri.toString());
|
||||
AccountSetupIncoming.actionIncomingSettings(this, mAccount, mMakeDefault);
|
||||
finish();
|
||||
@ -112,6 +128,7 @@ public class AccountSetupAccountType extends K9Activity implements OnClickListen
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void failure(Exception use) {
|
||||
Log.e(K9.LOG_TAG, "Failure", use);
|
||||
String toastText = getString(R.string.account_setup_bad_uri, use.getMessage());
|
||||
|
@ -8,6 +8,7 @@ import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Locale;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
@ -26,6 +27,7 @@ import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
import android.widget.EditText;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.EmailAddressValidator;
|
||||
import com.fsck.k9.K9;
|
||||
@ -34,6 +36,15 @@ import com.fsck.k9.R;
|
||||
import com.fsck.k9.activity.K9Activity;
|
||||
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.mail.AuthType;
|
||||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.Store;
|
||||
import com.fsck.k9.mail.Transport;
|
||||
import com.fsck.k9.mail.store.ImapStore;
|
||||
import com.fsck.k9.mail.transport.SmtpTransport;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
|
||||
|
||||
/**
|
||||
* Prompts the user for the email address and password.
|
||||
@ -43,7 +54,7 @@ import com.fsck.k9.helper.Utility;
|
||||
* AccountSetupAccountType activity.
|
||||
*/
|
||||
public class AccountSetupBasics extends K9Activity
|
||||
implements OnClickListener, TextWatcher {
|
||||
implements OnClickListener, TextWatcher, OnCheckedChangeListener, OnClientCertificateChangedListener {
|
||||
private final static String EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account";
|
||||
private final static int DIALOG_NOTE = 1;
|
||||
private final static String STATE_KEY_PROVIDER =
|
||||
@ -53,6 +64,8 @@ public class AccountSetupBasics extends K9Activity
|
||||
|
||||
private EditText mEmailView;
|
||||
private EditText mPasswordView;
|
||||
private CheckBox mClientCertificateCheckBox;
|
||||
private ClientCertificateSpinner mClientCertificateSpinner;
|
||||
private Button mNextButton;
|
||||
private Button mManualSetupButton;
|
||||
private Account mAccount;
|
||||
@ -60,6 +73,7 @@ public class AccountSetupBasics extends K9Activity
|
||||
|
||||
private EmailAddressValidator mEmailValidator = new EmailAddressValidator();
|
||||
private boolean mCheckedIncoming = false;
|
||||
private CheckBox mShowPasswordCheckBox;
|
||||
|
||||
public static void actionNewAccount(Context context) {
|
||||
Intent i = new Intent(context, AccountSetupBasics.class);
|
||||
@ -72,31 +86,27 @@ public class AccountSetupBasics extends K9Activity
|
||||
setContentView(R.layout.account_setup_basics);
|
||||
mEmailView = (EditText)findViewById(R.id.account_email);
|
||||
mPasswordView = (EditText)findViewById(R.id.account_password);
|
||||
mClientCertificateCheckBox = (CheckBox)findViewById(R.id.account_client_certificate);
|
||||
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
|
||||
mNextButton = (Button)findViewById(R.id.next);
|
||||
mManualSetupButton = (Button)findViewById(R.id.manual_setup);
|
||||
CheckBox showPassword = (CheckBox) findViewById(R.id.show_password);
|
||||
showPassword.setOnCheckedChangeListener (new OnCheckedChangeListener() {
|
||||
mShowPasswordCheckBox = (CheckBox) findViewById(R.id.show_password);
|
||||
mNextButton.setOnClickListener(this);
|
||||
mManualSetupButton.setOnClickListener(this);
|
||||
}
|
||||
|
||||
private void initializeViewListeners() {
|
||||
mEmailView.addTextChangedListener(this);
|
||||
mPasswordView.addTextChangedListener(this);
|
||||
mClientCertificateCheckBox.setOnCheckedChangeListener(this);
|
||||
mClientCertificateSpinner.setOnClientCertificateChangedListener(this);
|
||||
mShowPasswordCheckBox.setOnCheckedChangeListener (new OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
if (isChecked) {
|
||||
mPasswordView.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
} else {
|
||||
mPasswordView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
}
|
||||
showPassword(isChecked);
|
||||
}
|
||||
});
|
||||
|
||||
mNextButton.setOnClickListener(this);
|
||||
mManualSetupButton.setOnClickListener(this);
|
||||
|
||||
mEmailView.addTextChangedListener(this);
|
||||
mPasswordView.addTextChangedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
validateFields();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -125,6 +135,25 @@ public class AccountSetupBasics extends K9Activity
|
||||
}
|
||||
|
||||
mCheckedIncoming = savedInstanceState.getBoolean(STATE_KEY_CHECKED_INCOMING);
|
||||
|
||||
updateViewVisibility(mClientCertificateCheckBox.isChecked());
|
||||
|
||||
showPassword(mShowPasswordCheckBox.isChecked());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
/*
|
||||
* We wait until now to initialize the listeners because we didn't want
|
||||
* the OnCheckedChangeListener active while the
|
||||
* mClientCertificateCheckBox state was being restored because it could
|
||||
* trigger the pop-up of a ClientCertificateSpinner.chooseCertificate()
|
||||
* dialog.
|
||||
*/
|
||||
initializeViewListeners();
|
||||
validateFields();
|
||||
}
|
||||
|
||||
public void afterTextChanged(Editable s) {
|
||||
@ -137,10 +166,55 @@ public class AccountSetupBasics extends K9Activity
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClientCertificateChanged(String alias) {
|
||||
validateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when checking the client certificate CheckBox
|
||||
*/
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
updateViewVisibility(isChecked);
|
||||
validateFields();
|
||||
|
||||
// Have the user select (or confirm) the client certificate
|
||||
if (isChecked) {
|
||||
mClientCertificateSpinner.chooseCertificate();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateViewVisibility(boolean usingCertificates) {
|
||||
if (usingCertificates) {
|
||||
// hide password fields, show client certificate spinner
|
||||
mPasswordView.setVisibility(View.GONE);
|
||||
mShowPasswordCheckBox.setVisibility(View.GONE);
|
||||
mClientCertificateSpinner.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// show password fields, hide client certificate spinner
|
||||
mPasswordView.setVisibility(View.VISIBLE);
|
||||
mShowPasswordCheckBox.setVisibility(View.VISIBLE);
|
||||
mClientCertificateSpinner.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void showPassword(boolean show) {
|
||||
if (show) {
|
||||
mPasswordView.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
} else {
|
||||
mPasswordView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFields() {
|
||||
boolean clientCertificateChecked = mClientCertificateCheckBox.isChecked();
|
||||
String clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||
String email = mEmailView.getText().toString();
|
||||
|
||||
boolean valid = Utility.requiredFieldValid(mEmailView)
|
||||
&& Utility.requiredFieldValid(mPasswordView)
|
||||
&& ((!clientCertificateChecked && Utility.requiredFieldValid(mPasswordView))
|
||||
|| (clientCertificateChecked && clientCertificateAlias != null))
|
||||
&& mEmailValidator.isValidAddressOnly(email);
|
||||
|
||||
mNextButton.setEnabled(valid);
|
||||
@ -277,6 +351,13 @@ public class AccountSetupBasics extends K9Activity
|
||||
}
|
||||
|
||||
protected void onNext() {
|
||||
if (mClientCertificateCheckBox.isChecked()) {
|
||||
|
||||
// Auto-setup doesn't support client certificates.
|
||||
onManualSetup();
|
||||
return;
|
||||
}
|
||||
|
||||
String email = mEmailView.getText().toString();
|
||||
String[] emailParts = splitEmail(email);
|
||||
String domain = emailParts[1];
|
||||
@ -317,33 +398,38 @@ public class AccountSetupBasics extends K9Activity
|
||||
|
||||
private void onManualSetup() {
|
||||
String email = mEmailView.getText().toString();
|
||||
String password = mPasswordView.getText().toString();
|
||||
String[] emailParts = splitEmail(email);
|
||||
String user = emailParts[0];
|
||||
String domain = emailParts[1];
|
||||
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
AuthType authenticationType = null;
|
||||
if (mClientCertificateCheckBox.isChecked()) {
|
||||
authenticationType = AuthType.EXTERNAL;
|
||||
clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||
} else {
|
||||
authenticationType = AuthType.PLAIN;
|
||||
password = mPasswordView.getText().toString();
|
||||
}
|
||||
|
||||
if (mAccount == null) {
|
||||
mAccount = Preferences.getPreferences(this).newAccount();
|
||||
}
|
||||
mAccount.setName(getOwnerName());
|
||||
mAccount.setEmail(email);
|
||||
try {
|
||||
String userEnc = URLEncoder.encode(user, "UTF-8");
|
||||
String passwordEnc = URLEncoder.encode(password, "UTF-8");
|
||||
|
||||
URI uri = new URI("placeholder", userEnc + ":" + passwordEnc, "mail." + domain, -1, null,
|
||||
null, null);
|
||||
mAccount.setStoreUri(uri.toString());
|
||||
mAccount.setTransportUri(uri.toString());
|
||||
} catch (UnsupportedEncodingException enc) {
|
||||
// This really shouldn't happen since the encoding is hardcoded to UTF-8
|
||||
Log.e(K9.LOG_TAG, "Couldn't urlencode username or password.", enc);
|
||||
} catch (URISyntaxException use) {
|
||||
/*
|
||||
* If we can't set up the URL we just continue. It's only for
|
||||
* convenience.
|
||||
*/
|
||||
}
|
||||
// set default uris
|
||||
// NOTE: they will be changed again in AccountSetupAccountType!
|
||||
ServerSettings storeServer = new ServerSettings(ImapStore.STORE_TYPE, "mail." + domain, -1,
|
||||
ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType, user, password, clientCertificateAlias);
|
||||
ServerSettings transportServer = new ServerSettings(SmtpTransport.TRANSPORT_TYPE, "mail." + domain, -1,
|
||||
ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType, user, password, clientCertificateAlias);
|
||||
String storeUri = Store.createStoreUri(storeServer);
|
||||
String transportUri = Transport.createTransportUri(transportServer);
|
||||
mAccount.setStoreUri(storeUri);
|
||||
mAccount.setTransportUri(transportUri);
|
||||
|
||||
mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts));
|
||||
mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash));
|
||||
mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent));
|
||||
@ -450,4 +536,5 @@ public class AccountSetupBasics extends K9Activity
|
||||
|
||||
public String note;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package com.fsck.k9.activity.setup;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.DialogFragment;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
@ -15,16 +17,18 @@ import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.fsck.k9.*;
|
||||
import com.fsck.k9.activity.K9Activity;
|
||||
import com.fsck.k9.controller.MessagingController;
|
||||
import com.fsck.k9.fragment.ConfirmationDialogFragment;
|
||||
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
|
||||
import com.fsck.k9.mail.AuthenticationFailedException;
|
||||
import com.fsck.k9.mail.CertificateValidationException;
|
||||
import com.fsck.k9.mail.Store;
|
||||
import com.fsck.k9.mail.Transport;
|
||||
import com.fsck.k9.mail.store.WebDavStore;
|
||||
import com.fsck.k9.mail.filter.Hex;
|
||||
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
@ -32,6 +36,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Checks the given settings to make sure that they can be used to send and
|
||||
@ -40,7 +45,8 @@ import java.util.List;
|
||||
* XXX NOTE: The manifest for this app has it ignore config changes, because
|
||||
* it doesn't correctly deal with restarting while its thread is running.
|
||||
*/
|
||||
public class AccountSetupCheckSettings extends K9Activity implements OnClickListener {
|
||||
public class AccountSetupCheckSettings extends K9Activity implements OnClickListener,
|
||||
ConfirmationDialogFragmentListener{
|
||||
|
||||
public static final int ACTIVITY_REQUEST_CODE = 1;
|
||||
|
||||
@ -67,7 +73,8 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
|
||||
private boolean mDestroyed;
|
||||
|
||||
public static void actionCheckSettings(Activity context, Account account, CheckDirection direction) {
|
||||
public static void actionCheckSettings(Activity context, Account account,
|
||||
CheckDirection direction) {
|
||||
Intent i = new Intent(context, AccountSetupCheckSettings.class);
|
||||
i.putExtra(EXTRA_ACCOUNT, account.getUuid());
|
||||
i.putExtra(EXTRA_CHECK_DIRECTION, direction);
|
||||
@ -107,7 +114,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
ctrl.clearCertificateErrorNotifications(AccountSetupCheckSettings.this,
|
||||
mAccount, mDirection);
|
||||
|
||||
if (mDirection.equals(CheckDirection.INCOMING)) {
|
||||
if (mDirection == CheckDirection.INCOMING) {
|
||||
store = mAccount.getRemoteStore();
|
||||
|
||||
if (store instanceof WebDavStore) {
|
||||
@ -130,7 +137,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (mDirection.equals(CheckDirection.OUTGOING)) {
|
||||
if (mDirection == CheckDirection.OUTGOING) {
|
||||
if (!(mAccount.getRemoteStore() instanceof WebDavStore)) {
|
||||
setMessage(R.string.account_setup_check_settings_check_outgoing_msg);
|
||||
}
|
||||
@ -154,6 +161,20 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
R.string.account_setup_failed_dlg_auth_message_fmt,
|
||||
afe.getMessage() == null ? "" : afe.getMessage());
|
||||
} catch (final CertificateValidationException cve) {
|
||||
handleCertificateValidationException(cve);
|
||||
} catch (final Throwable t) {
|
||||
Log.e(K9.LOG_TAG, "Error while testing settings", t);
|
||||
showErrorDialog(
|
||||
R.string.account_setup_failed_dlg_server_message_fmt,
|
||||
(t.getMessage() == null ? "" : t.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.start();
|
||||
}
|
||||
|
||||
private void handleCertificateValidationException(CertificateValidationException cve) {
|
||||
Log.e(K9.LOG_TAG, "Error while testing settings", cve);
|
||||
|
||||
X509Certificate[] chain = cve.getCertChain();
|
||||
@ -167,17 +188,6 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
R.string.account_setup_failed_dlg_server_message_fmt,
|
||||
(cve.getMessage() == null ? "" : cve.getMessage()));
|
||||
}
|
||||
} catch (final Throwable t) {
|
||||
Log.e(K9.LOG_TAG, "Error while testing settings", t);
|
||||
showErrorDialog(
|
||||
R.string.account_setup_failed_dlg_server_message_fmt,
|
||||
(t.getMessage() == null ? "" : t.getMessage()));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -198,41 +208,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
});
|
||||
}
|
||||
|
||||
private void showErrorDialog(final int msgResId, final Object... args) {
|
||||
mHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
if (mDestroyed) {
|
||||
return;
|
||||
}
|
||||
mProgressBar.setIndeterminate(false);
|
||||
new AlertDialog.Builder(AccountSetupCheckSettings.this)
|
||||
.setTitle(getString(R.string.account_setup_failed_dlg_title))
|
||||
.setMessage(getString(msgResId, args))
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(
|
||||
getString(R.string.account_setup_failed_dlg_continue_action),
|
||||
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mCanceled = false;
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.setPositiveButton(
|
||||
getString(R.string.account_setup_failed_dlg_edit_details_action),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void acceptKeyDialog(final int msgResId,
|
||||
final CertificateValidationException ex) {
|
||||
private void acceptKeyDialog(final int msgResId, final CertificateValidationException ex) {
|
||||
mHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
if (mDestroyed) {
|
||||
@ -351,6 +327,8 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: refactor with DialogFragment.
|
||||
// This is difficult because we need to pass through chain[0] for onClick()
|
||||
new AlertDialog.Builder(AccountSetupCheckSettings.this)
|
||||
.setTitle(getString(R.string.account_setup_failed_dlg_invalid_certificate_title))
|
||||
//.setMessage(getString(R.string.account_setup_failed_dlg_invalid_certificate)
|
||||
@ -362,15 +340,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
getString(R.string.account_setup_failed_dlg_invalid_certificate_accept),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
try {
|
||||
mAccount.addCertificate(mDirection, chain[0]);
|
||||
} catch (CertificateException e) {
|
||||
showErrorDialog(
|
||||
R.string.account_setup_failed_dlg_certificate_message_fmt,
|
||||
e.getMessage() == null ? "" : e.getMessage());
|
||||
}
|
||||
AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount,
|
||||
mDirection);
|
||||
acceptCertificate(chain[0]);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(
|
||||
@ -385,13 +355,30 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently accepts a certificate for the INCOMING or OUTGOING direction
|
||||
* by adding it to the local key store.
|
||||
*
|
||||
* @param certificate
|
||||
*/
|
||||
private void acceptCertificate(X509Certificate certificate) {
|
||||
try {
|
||||
mAccount.addCertificate(mDirection, certificate);
|
||||
} catch (CertificateException e) {
|
||||
showErrorDialog(
|
||||
R.string.account_setup_failed_dlg_certificate_message_fmt,
|
||||
e.getMessage() == null ? "" : e.getMessage());
|
||||
}
|
||||
AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount,
|
||||
mDirection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int reqCode, int resCode, Intent data) {
|
||||
setResult(resCode);
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
private void onCancel() {
|
||||
mCanceled = true;
|
||||
setMessage(R.string.account_setup_check_settings_canceling_msg);
|
||||
@ -404,4 +391,74 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void showErrorDialog(final int msgResId, final Object... args) {
|
||||
mHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
showDialogFragment(R.id.dialog_account_setup_error, getString(msgResId, args));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showDialogFragment(int dialogId, String customMessage) {
|
||||
if (mDestroyed) {
|
||||
return;
|
||||
}
|
||||
mProgressBar.setIndeterminate(false);
|
||||
|
||||
DialogFragment fragment;
|
||||
switch (dialogId) {
|
||||
case R.id.dialog_account_setup_error: {
|
||||
fragment = ConfirmationDialogFragment.newInstance(dialogId,
|
||||
getString(R.string.account_setup_failed_dlg_title),
|
||||
customMessage,
|
||||
getString(R.string.account_setup_failed_dlg_edit_details_action),
|
||||
getString(R.string.account_setup_failed_dlg_continue_action)
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new RuntimeException("Called showDialog(int) with unknown dialog id.");
|
||||
}
|
||||
}
|
||||
|
||||
FragmentTransaction ta = getFragmentManager().beginTransaction();
|
||||
ta.add(fragment, getDialogTag(dialogId));
|
||||
ta.commitAllowingStateLoss();
|
||||
|
||||
// TODO: commitAllowingStateLoss() is used to prevent https://code.google.com/p/android/issues/detail?id=23761
|
||||
// but is a bad...
|
||||
//fragment.show(ta, getDialogTag(dialogId));
|
||||
}
|
||||
|
||||
private String getDialogTag(int dialogId) {
|
||||
return String.format(Locale.US, "dialog-%d", dialogId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doPositiveClick(int dialogId) {
|
||||
switch (dialogId) {
|
||||
case R.id.dialog_account_setup_error: {
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doNegativeClick(int dialogId) {
|
||||
switch (dialogId) {
|
||||
case R.id.dialog_account_setup_error: {
|
||||
mCanceled = false;
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dialogCancelled(int dialogId) {
|
||||
// nothing to do here...
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.*;
|
||||
import android.widget.AdapterView.OnItemSelectedListener;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
|
||||
import com.fsck.k9.*;
|
||||
@ -23,23 +24,27 @@ import com.fsck.k9.mail.AuthType;
|
||||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.Store;
|
||||
import com.fsck.k9.mail.Transport;
|
||||
import com.fsck.k9.mail.store.ImapStore;
|
||||
import com.fsck.k9.mail.store.Pop3Store;
|
||||
import com.fsck.k9.mail.store.WebDavStore;
|
||||
import com.fsck.k9.mail.store.ImapStore.ImapStoreSettings;
|
||||
import com.fsck.k9.mail.store.WebDavStore.WebDavStoreSettings;
|
||||
import com.fsck.k9.mail.transport.SmtpTransport;
|
||||
import com.fsck.k9.service.MailService;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class AccountSetupIncoming extends K9Activity implements OnClickListener {
|
||||
private static final String EXTRA_ACCOUNT = "account";
|
||||
private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
|
||||
private static final String STATE_SECURITY_TYPE_POSITION = "stateSecurityTypePosition";
|
||||
private static final String STATE_AUTH_TYPE_POSITION = "authTypePosition";
|
||||
|
||||
private static final String POP3_PORT = "110";
|
||||
private static final String POP3_SSL_PORT = "995";
|
||||
@ -51,10 +56,16 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
private String mStoreType;
|
||||
private EditText mUsernameView;
|
||||
private EditText mPasswordView;
|
||||
private ClientCertificateSpinner mClientCertificateSpinner;
|
||||
private TextView mClientCertificateLabelView;
|
||||
private TextView mPasswordLabelView;
|
||||
private EditText mServerView;
|
||||
private EditText mPortView;
|
||||
private String mCurrentPortViewSetting;
|
||||
private Spinner mSecurityTypeView;
|
||||
private int mCurrentSecurityTypeViewPosition;
|
||||
private Spinner mAuthTypeView;
|
||||
private int mCurrentAuthTypeViewPosition;
|
||||
private CheckBox mImapAutoDetectNamespaceView;
|
||||
private EditText mImapPathPrefixView;
|
||||
private EditText mWebdavPathPrefixView;
|
||||
@ -97,6 +108,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
|
||||
mUsernameView = (EditText)findViewById(R.id.account_username);
|
||||
mPasswordView = (EditText)findViewById(R.id.account_password);
|
||||
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
|
||||
mClientCertificateLabelView = (TextView)findViewById(R.id.account_client_certificate_label);
|
||||
mPasswordLabelView = (TextView)findViewById(R.id.account_password_label);
|
||||
TextView serverLabelView = (TextView) findViewById(R.id.account_server_label);
|
||||
mServerView = (EditText)findViewById(R.id.account_server);
|
||||
mPortView = (EditText)findViewById(R.id.account_port);
|
||||
@ -130,28 +144,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
mAuthTypeAdapter = AuthType.getArrayAdapter(this);
|
||||
mAuthTypeView.setAdapter(mAuthTypeAdapter);
|
||||
|
||||
/*
|
||||
* Calls validateFields() which enables or disables the Next button
|
||||
* based on the fields' validity.
|
||||
*/
|
||||
TextWatcher validationTextWatcher = new TextWatcher() {
|
||||
public void afterTextChanged(Editable s) {
|
||||
validateFields();
|
||||
}
|
||||
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
/* unused */
|
||||
}
|
||||
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
/* unused */
|
||||
}
|
||||
};
|
||||
mUsernameView.addTextChangedListener(validationTextWatcher);
|
||||
mPasswordView.addTextChangedListener(validationTextWatcher);
|
||||
mServerView.addTextChangedListener(validationTextWatcher);
|
||||
mPortView.addTextChangedListener(validationTextWatcher);
|
||||
|
||||
/*
|
||||
* Only allow digits in the port field.
|
||||
*/
|
||||
@ -173,6 +165,15 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
try {
|
||||
ServerSettings settings = Store.decodeStoreUri(mAccount.getStoreUri());
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter
|
||||
mCurrentAuthTypeViewPosition = mAuthTypeAdapter.getPosition(settings.authenticationType);
|
||||
} else {
|
||||
mCurrentAuthTypeViewPosition = savedInstanceState.getInt(STATE_AUTH_TYPE_POSITION);
|
||||
}
|
||||
mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false);
|
||||
updateViewFromAuthType();
|
||||
|
||||
if (settings.username != null) {
|
||||
mUsernameView.setText(settings.username);
|
||||
}
|
||||
@ -181,11 +182,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
mPasswordView.setText(settings.password);
|
||||
}
|
||||
|
||||
updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
|
||||
|
||||
// The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter
|
||||
int position = mAuthTypeAdapter.getPosition(settings.authenticationType);
|
||||
mAuthTypeView.setSelection(position, false);
|
||||
if (settings.clientCertificateAlias != null) {
|
||||
mClientCertificateSpinner.setAlias(settings.clientCertificateAlias);
|
||||
}
|
||||
|
||||
mStoreType = settings.type;
|
||||
if (Pop3Store.STORE_TYPE.equals(settings.type)) {
|
||||
@ -256,34 +255,29 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
throw new Exception("Unknown account type: " + mAccount.getStoreUri());
|
||||
}
|
||||
|
||||
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(this,
|
||||
android.R.layout.simple_spinner_item, mConnectionSecurityChoices);
|
||||
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
// Note that mConnectionSecurityChoices is configured above based on server type
|
||||
ArrayAdapter<ConnectionSecurity> securityTypesAdapter =
|
||||
ConnectionSecurity.getArrayAdapter(this, mConnectionSecurityChoices);
|
||||
mSecurityTypeView.setAdapter(securityTypesAdapter);
|
||||
|
||||
// Select currently configured security type
|
||||
int index = securityTypesAdapter.getPosition(settings.connectionSecurity);
|
||||
mSecurityTypeView.setSelection(index, false);
|
||||
if (savedInstanceState == null) {
|
||||
mCurrentSecurityTypeViewPosition = securityTypesAdapter.getPosition(settings.connectionSecurity);
|
||||
} else {
|
||||
|
||||
/*
|
||||
* Updates the port when the user changes the security type. This allows
|
||||
* us to show a reasonable default which the user can change.
|
||||
*
|
||||
* Note: It's important that we set the listener *after* an initial option has been
|
||||
* selected by the code above. Otherwise the listener might be called after
|
||||
* onCreate() has been processed and the current port value set later in this
|
||||
* method is overridden with the default port for the selected security type.
|
||||
* Restore the spinner state now, before calling
|
||||
* setOnItemSelectedListener(), thus avoiding a call to
|
||||
* onItemSelected(). Then, when the system restores the state
|
||||
* (again) in onRestoreInstanceState(), The system will see that
|
||||
* the new state is the same as the current state (set here), so
|
||||
* once again onItemSelected() will not be called.
|
||||
*/
|
||||
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
updatePortFromSecurityType();
|
||||
mCurrentSecurityTypeViewPosition = savedInstanceState.getInt(STATE_SECURITY_TYPE_POSITION);
|
||||
}
|
||||
mSecurityTypeView.setSelection(mCurrentSecurityTypeViewPosition, false);
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
|
||||
});
|
||||
updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
|
||||
|
||||
mCompressionMobile.setChecked(mAccount.useCompression(Account.TYPE_MOBILE));
|
||||
mCompressionWifi.setChecked(mAccount.useCompression(Account.TYPE_WIFI));
|
||||
@ -298,34 +292,205 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
} else {
|
||||
updatePortFromSecurityType();
|
||||
}
|
||||
mCurrentPortViewSetting = mPortView.getText().toString();
|
||||
|
||||
mSubscribedFoldersOnly.setChecked(mAccount.subscribedFoldersOnly());
|
||||
|
||||
validateFields();
|
||||
} catch (Exception e) {
|
||||
failure(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at the end of either {@code onCreate()} or
|
||||
* {@code onRestoreInstanceState()}, after the views have been initialized,
|
||||
* so that the listeners are not triggered during the view initialization.
|
||||
* This avoids needless calls to {@code validateFields()} which is called
|
||||
* immediately after this is called.
|
||||
*/
|
||||
private void initializeViewListeners() {
|
||||
|
||||
/*
|
||||
* Updates the port when the user changes the security type. This allows
|
||||
* us to show a reasonable default which the user can change.
|
||||
*/
|
||||
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
|
||||
/*
|
||||
* We keep our own record of the spinner state so we
|
||||
* know for sure that onItemSelected() was called
|
||||
* because of user input, not because of spinner
|
||||
* state initialization. This assures that the port
|
||||
* will not be replaced with a default value except
|
||||
* on user input.
|
||||
*/
|
||||
if (mCurrentSecurityTypeViewPosition != position) {
|
||||
updatePortFromSecurityType();
|
||||
validateFields();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
|
||||
});
|
||||
|
||||
mAuthTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
if (mCurrentAuthTypeViewPosition == position) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateViewFromAuthType();
|
||||
validateFields();
|
||||
AuthType selection = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
|
||||
// Have the user select (or confirm) the client certificate
|
||||
if (AuthType.EXTERNAL == selection) {
|
||||
|
||||
// This may again invoke validateFields()
|
||||
mClientCertificateSpinner.chooseCertificate();
|
||||
} else {
|
||||
mPasswordView.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
|
||||
});
|
||||
|
||||
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
|
||||
mUsernameView.addTextChangedListener(validationTextWatcher);
|
||||
mPasswordView.addTextChangedListener(validationTextWatcher);
|
||||
mServerView.addTextChangedListener(validationTextWatcher);
|
||||
mPortView.addTextChangedListener(validationTextWatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(EXTRA_ACCOUNT, mAccount.getUuid());
|
||||
outState.putInt(STATE_SECURITY_TYPE_POSITION, mCurrentSecurityTypeViewPosition);
|
||||
outState.putInt(STATE_AUTH_TYPE_POSITION, mCurrentAuthTypeViewPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
/*
|
||||
* We didn't want the listeners active while the state was being restored
|
||||
* because they could overwrite the restored port with a default port when
|
||||
* the security type was restored.
|
||||
*/
|
||||
initializeViewListeners();
|
||||
validateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows/hides password field and client certificate spinner
|
||||
*/
|
||||
private void updateViewFromAuthType() {
|
||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType);
|
||||
|
||||
if (isAuthTypeExternal) {
|
||||
|
||||
// hide password fields, show client certificate fields
|
||||
mPasswordView.setVisibility(View.GONE);
|
||||
mPasswordLabelView.setVisibility(View.GONE);
|
||||
mClientCertificateLabelView.setVisibility(View.VISIBLE);
|
||||
mClientCertificateSpinner.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
|
||||
// show password fields, hide client certificate fields
|
||||
mPasswordView.setVisibility(View.VISIBLE);
|
||||
mPasswordLabelView.setVisibility(View.VISIBLE);
|
||||
mClientCertificateLabelView.setVisibility(View.GONE);
|
||||
mClientCertificateSpinner.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is invoked only when the user makes changes to a widget, not when
|
||||
* widgets are changed programmatically. (The logic is simpler when you know
|
||||
* that this is the last thing called after an input change.)
|
||||
*/
|
||||
private void validateFields() {
|
||||
mNextButton
|
||||
.setEnabled(Utility.requiredFieldValid(mUsernameView)
|
||||
&& Utility.requiredFieldValid(mPasswordView)
|
||||
&& Utility.domainFieldValid(mServerView)
|
||||
&& Utility.requiredFieldValid(mPortView));
|
||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType);
|
||||
|
||||
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
|
||||
boolean hasConnectionSecurity = (connectionSecurity != ConnectionSecurity.NONE);
|
||||
|
||||
if (isAuthTypeExternal && !hasConnectionSecurity) {
|
||||
|
||||
// Notify user of an invalid combination of AuthType.EXTERNAL & ConnectionSecurity.NONE
|
||||
String toastText = getString(R.string.account_setup_incoming_invalid_setting_combo_notice,
|
||||
getString(R.string.account_setup_incoming_auth_type_label),
|
||||
AuthType.EXTERNAL.toString(),
|
||||
getString(R.string.account_setup_incoming_security_label),
|
||||
ConnectionSecurity.NONE.toString());
|
||||
Toast.makeText(this, toastText, Toast.LENGTH_LONG).show();
|
||||
|
||||
// Reset the views back to their previous settings without recursing through here again
|
||||
OnItemSelectedListener onItemSelectedListener = mAuthTypeView.getOnItemSelectedListener();
|
||||
mAuthTypeView.setOnItemSelectedListener(null);
|
||||
mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false);
|
||||
mAuthTypeView.setOnItemSelectedListener(onItemSelectedListener);
|
||||
updateViewFromAuthType();
|
||||
|
||||
onItemSelectedListener = mSecurityTypeView.getOnItemSelectedListener();
|
||||
mSecurityTypeView.setOnItemSelectedListener(null);
|
||||
mSecurityTypeView.setSelection(mCurrentSecurityTypeViewPosition, false);
|
||||
mSecurityTypeView.setOnItemSelectedListener(onItemSelectedListener);
|
||||
updateAuthPlainTextFromSecurityType((ConnectionSecurity) mSecurityTypeView.getSelectedItem());
|
||||
|
||||
mPortView.removeTextChangedListener(validationTextWatcher);
|
||||
mPortView.setText(mCurrentPortViewSetting);
|
||||
mPortView.addTextChangedListener(validationTextWatcher);
|
||||
|
||||
authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
isAuthTypeExternal = (AuthType.EXTERNAL == authType);
|
||||
|
||||
connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
|
||||
hasConnectionSecurity = (connectionSecurity != ConnectionSecurity.NONE);
|
||||
} else {
|
||||
mCurrentAuthTypeViewPosition = mAuthTypeView.getSelectedItemPosition();
|
||||
mCurrentSecurityTypeViewPosition = mSecurityTypeView.getSelectedItemPosition();
|
||||
mCurrentPortViewSetting = mPortView.getText().toString();
|
||||
}
|
||||
|
||||
boolean hasValidCertificateAlias = mClientCertificateSpinner.getAlias() != null;
|
||||
boolean hasValidUserName = Utility.requiredFieldValid(mUsernameView);
|
||||
|
||||
boolean hasValidPasswordSettings = hasValidUserName
|
||||
&& !isAuthTypeExternal
|
||||
&& Utility.requiredFieldValid(mPasswordView);
|
||||
|
||||
boolean hasValidExternalAuthSettings = hasValidUserName
|
||||
&& isAuthTypeExternal
|
||||
&& hasConnectionSecurity
|
||||
&& hasValidCertificateAlias;
|
||||
|
||||
mNextButton.setEnabled(Utility.domainFieldValid(mServerView)
|
||||
&& Utility.requiredFieldValid(mPortView)
|
||||
&& (hasValidPasswordSettings || hasValidExternalAuthSettings));
|
||||
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
|
||||
}
|
||||
|
||||
private void updatePortFromSecurityType() {
|
||||
ConnectionSecurity securityType = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
|
||||
mPortView.setText(getDefaultPort(securityType));
|
||||
updateAuthPlainTextFromSecurityType(securityType);
|
||||
|
||||
// Remove listener so as not to trigger validateFields() which is called
|
||||
// elsewhere as a result of user interaction.
|
||||
mPortView.removeTextChangedListener(validationTextWatcher);
|
||||
mPortView.setText(getDefaultPort(securityType));
|
||||
mPortView.addTextChangedListener(validationTextWatcher);
|
||||
}
|
||||
|
||||
private String getDefaultPort(ConnectionSecurity securityType) {
|
||||
@ -377,21 +542,22 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
* password the user just set for incoming.
|
||||
*/
|
||||
try {
|
||||
String usernameEnc = URLEncoder.encode(mUsernameView.getText().toString(), "UTF-8");
|
||||
String passwordEnc = URLEncoder.encode(mPasswordView.getText().toString(), "UTF-8");
|
||||
String username = mUsernameView.getText().toString();
|
||||
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
if (AuthType.EXTERNAL == authType) {
|
||||
clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||
} else {
|
||||
password = mPasswordView.getText().toString();
|
||||
}
|
||||
|
||||
URI oldUri = new URI(mAccount.getTransportUri());
|
||||
URI uri = new URI(
|
||||
oldUri.getScheme(),
|
||||
usernameEnc + ":" + passwordEnc,
|
||||
oldUri.getHost(),
|
||||
oldUri.getPort(),
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
mAccount.setTransportUri(uri.toString());
|
||||
} catch (UnsupportedEncodingException enc) {
|
||||
// This really shouldn't happen since the encoding is hardcoded to UTF-8
|
||||
Log.e(K9.LOG_TAG, "Couldn't urlencode username or password.", enc);
|
||||
ServerSettings transportServer = new ServerSettings(SmtpTransport.TRANSPORT_TYPE, oldUri.getHost(), oldUri.getPort(),
|
||||
ConnectionSecurity.SSL_TLS_REQUIRED, authType, username, password, clientCertificateAlias);
|
||||
String transportUri = Transport.createTransportUri(transportServer);
|
||||
mAccount.setTransportUri(transportUri);
|
||||
} catch (URISyntaxException use) {
|
||||
/*
|
||||
* If we can't set up the URL we just continue. It's only for
|
||||
@ -411,8 +577,15 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
|
||||
|
||||
String username = mUsernameView.getText().toString();
|
||||
String password = mPasswordView.getText().toString();
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
|
||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
if (authType == AuthType.EXTERNAL) {
|
||||
clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||
} else {
|
||||
password = mPasswordView.getText().toString();
|
||||
}
|
||||
String host = mServerView.getText().toString();
|
||||
int port = Integer.parseInt(mPortView.getText().toString());
|
||||
|
||||
@ -435,7 +608,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
|
||||
mAccount.deleteCertificate(host, port, CheckDirection.INCOMING);
|
||||
ServerSettings settings = new ServerSettings(mStoreType, host, port,
|
||||
connectionSecurity, authType, username, password, extra);
|
||||
connectionSecurity, authType, username, password, clientCertificateAlias, extra);
|
||||
|
||||
mAccount.setStoreUri(Store.createStoreUri(settings));
|
||||
|
||||
@ -470,4 +643,29 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
||||
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
/*
|
||||
* Calls validateFields() which enables or disables the Next button
|
||||
* based on the fields' validity.
|
||||
*/
|
||||
TextWatcher validationTextWatcher = new TextWatcher() {
|
||||
public void afterTextChanged(Editable s) {
|
||||
validateFields();
|
||||
}
|
||||
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
/* unused */
|
||||
}
|
||||
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
/* unused */
|
||||
}
|
||||
};
|
||||
|
||||
OnClientCertificateChangedListener clientCertificateChangedListener = new OnClientCertificateChangedListener() {
|
||||
@Override
|
||||
public void onClientCertificateChanged(String alias) {
|
||||
validateFields();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -12,7 +12,9 @@ import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.*;
|
||||
import android.widget.AdapterView.OnItemSelectedListener;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
|
||||
import com.fsck.k9.*;
|
||||
import com.fsck.k9.activity.K9Activity;
|
||||
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
|
||||
@ -22,6 +24,8 @@ import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.Transport;
|
||||
import com.fsck.k9.mail.transport.SmtpTransport;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
@ -31,18 +35,26 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
private static final String EXTRA_ACCOUNT = "account";
|
||||
|
||||
private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
|
||||
private static final String STATE_SECURITY_TYPE_POSITION = "stateSecurityTypePosition";
|
||||
private static final String STATE_AUTH_TYPE_POSITION = "authTypePosition";
|
||||
|
||||
private static final String SMTP_PORT = "587";
|
||||
private static final String SMTP_SSL_PORT = "465";
|
||||
|
||||
private EditText mUsernameView;
|
||||
private EditText mPasswordView;
|
||||
private ClientCertificateSpinner mClientCertificateSpinner;
|
||||
private TextView mClientCertificateLabelView;
|
||||
private TextView mPasswordLabelView;
|
||||
private EditText mServerView;
|
||||
private EditText mPortView;
|
||||
private String mCurrentPortViewSetting;
|
||||
private CheckBox mRequireLoginView;
|
||||
private ViewGroup mRequireLoginSettingsView;
|
||||
private Spinner mSecurityTypeView;
|
||||
private int mCurrentSecurityTypeViewPosition;
|
||||
private Spinner mAuthTypeView;
|
||||
private int mCurrentAuthTypeViewPosition;
|
||||
private ArrayAdapter<AuthType> mAuthTypeAdapter;
|
||||
private Button mNextButton;
|
||||
private Account mAccount;
|
||||
@ -87,6 +99,9 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
|
||||
mUsernameView = (EditText)findViewById(R.id.account_username);
|
||||
mPasswordView = (EditText)findViewById(R.id.account_password);
|
||||
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
|
||||
mClientCertificateLabelView = (TextView)findViewById(R.id.account_client_certificate_label);
|
||||
mPasswordLabelView = (TextView)findViewById(R.id.account_password_label);
|
||||
mServerView = (EditText)findViewById(R.id.account_server);
|
||||
mPortView = (EditText)findViewById(R.id.account_port);
|
||||
mRequireLoginView = (CheckBox)findViewById(R.id.account_require_login);
|
||||
@ -96,36 +111,12 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
mNextButton = (Button)findViewById(R.id.next);
|
||||
|
||||
mNextButton.setOnClickListener(this);
|
||||
mRequireLoginView.setOnCheckedChangeListener(this);
|
||||
|
||||
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(this,
|
||||
android.R.layout.simple_spinner_item, ConnectionSecurity.values());
|
||||
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
mSecurityTypeView.setAdapter(securityTypesAdapter);
|
||||
mSecurityTypeView.setAdapter(ConnectionSecurity.getArrayAdapter(this));
|
||||
|
||||
mAuthTypeAdapter = AuthType.getArrayAdapter(this);
|
||||
mAuthTypeView.setAdapter(mAuthTypeAdapter);
|
||||
|
||||
/*
|
||||
* Calls validateFields() which enables or disables the Next button
|
||||
* based on the fields' validity.
|
||||
*/
|
||||
TextWatcher validationTextWatcher = new TextWatcher() {
|
||||
public void afterTextChanged(Editable s) {
|
||||
validateFields();
|
||||
}
|
||||
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
};
|
||||
mUsernameView.addTextChangedListener(validationTextWatcher);
|
||||
mPasswordView.addTextChangedListener(validationTextWatcher);
|
||||
mServerView.addTextChangedListener(validationTextWatcher);
|
||||
mPortView.addTextChangedListener(validationTextWatcher);
|
||||
|
||||
/*
|
||||
* Only allow digits in the port field.
|
||||
*/
|
||||
@ -147,46 +138,48 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
|
||||
try {
|
||||
ServerSettings settings = Transport.decodeTransportUri(mAccount.getTransportUri());
|
||||
String username = settings.username;
|
||||
String password = settings.password;
|
||||
|
||||
if (username != null) {
|
||||
mUsernameView.setText(username);
|
||||
mRequireLoginView.setChecked(true);
|
||||
}
|
||||
|
||||
if (password != null) {
|
||||
mPasswordView.setText(password);
|
||||
}
|
||||
|
||||
updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// 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);
|
||||
mCurrentAuthTypeViewPosition = mAuthTypeAdapter.getPosition(settings.authenticationType);
|
||||
} else {
|
||||
mCurrentAuthTypeViewPosition = savedInstanceState.getInt(STATE_AUTH_TYPE_POSITION);
|
||||
}
|
||||
mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false);
|
||||
updateViewFromAuthType();
|
||||
|
||||
// Select currently configured security type
|
||||
mSecurityTypeView.setSelection(settings.connectionSecurity.ordinal(), false);
|
||||
if (savedInstanceState == null) {
|
||||
mCurrentSecurityTypeViewPosition = settings.connectionSecurity.ordinal();
|
||||
} else {
|
||||
|
||||
/*
|
||||
* Updates the port when the user changes the security type. This allows
|
||||
* us to show a reasonable default which the user can change.
|
||||
*
|
||||
* Note: It's important that we set the listener *after* an initial option has been
|
||||
* selected by the code above. Otherwise the listener might be called after
|
||||
* onCreate() has been processed and the current port value set later in this
|
||||
* method is overridden with the default port for the selected security type.
|
||||
* Restore the spinner state now, before calling
|
||||
* setOnItemSelectedListener(), thus avoiding a call to
|
||||
* onItemSelected(). Then, when the system restores the state
|
||||
* (again) in onRestoreInstanceState(), The system will see that
|
||||
* the new state is the same as the current state (set here), so
|
||||
* once again onItemSelected() will not be called.
|
||||
*/
|
||||
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
updatePortFromSecurityType();
|
||||
mCurrentSecurityTypeViewPosition = savedInstanceState.getInt(STATE_SECURITY_TYPE_POSITION);
|
||||
}
|
||||
mSecurityTypeView.setSelection(mCurrentSecurityTypeViewPosition, false);
|
||||
|
||||
if (settings.username != null && !settings.username.isEmpty()) {
|
||||
mUsernameView.setText(settings.username);
|
||||
mRequireLoginView.setChecked(true);
|
||||
mRequireLoginSettingsView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
|
||||
});
|
||||
if (settings.password != null) {
|
||||
mPasswordView.setText(settings.password);
|
||||
}
|
||||
|
||||
if (settings.clientCertificateAlias != null) {
|
||||
mClientCertificateSpinner.setAlias(settings.clientCertificateAlias);
|
||||
}
|
||||
|
||||
if (settings.host != null) {
|
||||
mServerView.setText(settings.host);
|
||||
@ -197,8 +190,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
} else {
|
||||
updatePortFromSecurityType();
|
||||
}
|
||||
|
||||
validateFields();
|
||||
mCurrentPortViewSetting = mPortView.getText().toString();
|
||||
} catch (Exception e) {
|
||||
/*
|
||||
* We should always be able to parse our own settings.
|
||||
@ -208,27 +200,235 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at the end of either {@code onCreate()} or
|
||||
* {@code onRestoreInstanceState()}, after the views have been initialized,
|
||||
* so that the listeners are not triggered during the view initialization.
|
||||
* This avoids needless calls to {@code validateFields()} which is called
|
||||
* immediately after this is called.
|
||||
*/
|
||||
private void initializeViewListeners() {
|
||||
|
||||
/*
|
||||
* Updates the port when the user changes the security type. This allows
|
||||
* us to show a reasonable default which the user can change.
|
||||
*/
|
||||
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
|
||||
/*
|
||||
* We keep our own record of the spinner state so we
|
||||
* know for sure that onItemSelected() was called
|
||||
* because of user input, not because of spinner
|
||||
* state initialization. This assures that the port
|
||||
* will not be replaced with a default value except
|
||||
* on user input.
|
||||
*/
|
||||
if (mCurrentSecurityTypeViewPosition != position) {
|
||||
updatePortFromSecurityType();
|
||||
|
||||
boolean isInsecure = (ConnectionSecurity.NONE == mSecurityTypeView.getSelectedItem());
|
||||
boolean isAuthExternal = (AuthType.EXTERNAL == mAuthTypeView.getSelectedItem());
|
||||
boolean loginNotRequired = !mRequireLoginView.isChecked();
|
||||
|
||||
/*
|
||||
* If the user selects ConnectionSecurity.NONE, a
|
||||
* warning would normally pop up if the authentication
|
||||
* is AuthType.EXTERNAL (i.e., using client
|
||||
* certificates). But such a warning is irrelevant if
|
||||
* login is not required. So to avoid such a warning
|
||||
* (generated in validateFields()) under those
|
||||
* conditions, we change the (irrelevant) authentication
|
||||
* method to PLAIN.
|
||||
*/
|
||||
if (isInsecure && isAuthExternal && loginNotRequired) {
|
||||
OnItemSelectedListener onItemSelectedListener = mAuthTypeView.getOnItemSelectedListener();
|
||||
mAuthTypeView.setOnItemSelectedListener(null);
|
||||
mCurrentAuthTypeViewPosition = mAuthTypeAdapter.getPosition(AuthType.PLAIN);
|
||||
mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false);
|
||||
mAuthTypeView.setOnItemSelectedListener(onItemSelectedListener);
|
||||
updateViewFromAuthType();
|
||||
}
|
||||
|
||||
validateFields();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
|
||||
});
|
||||
|
||||
mAuthTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
if (mCurrentAuthTypeViewPosition == position) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateViewFromAuthType();
|
||||
validateFields();
|
||||
AuthType selection = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
|
||||
// Have the user select (or confirm) the client certificate
|
||||
if (AuthType.EXTERNAL == selection) {
|
||||
|
||||
// This may again invoke validateFields()
|
||||
mClientCertificateSpinner.chooseCertificate();
|
||||
} else {
|
||||
mPasswordView.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
|
||||
});
|
||||
|
||||
mRequireLoginView.setOnCheckedChangeListener(this);
|
||||
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
|
||||
mUsernameView.addTextChangedListener(validationTextWatcher);
|
||||
mPasswordView.addTextChangedListener(validationTextWatcher);
|
||||
mServerView.addTextChangedListener(validationTextWatcher);
|
||||
mPortView.addTextChangedListener(validationTextWatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(EXTRA_ACCOUNT, mAccount.getUuid());
|
||||
outState.putInt(STATE_SECURITY_TYPE_POSITION, mCurrentSecurityTypeViewPosition);
|
||||
outState.putInt(STATE_AUTH_TYPE_POSITION, mCurrentAuthTypeViewPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
|
||||
if (mRequireLoginView.isChecked()) {
|
||||
mRequireLoginSettingsView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mRequireLoginSettingsView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
/*
|
||||
* We didn't want the listeners active while the state was being restored
|
||||
* because they could overwrite the restored port with a default port when
|
||||
* the security type was restored.
|
||||
*/
|
||||
initializeViewListeners();
|
||||
validateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows/hides password field and client certificate spinner
|
||||
*/
|
||||
private void updateViewFromAuthType() {
|
||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType);
|
||||
|
||||
if (isAuthTypeExternal) {
|
||||
|
||||
// hide password fields, show client certificate fields
|
||||
mPasswordView.setVisibility(View.GONE);
|
||||
mPasswordLabelView.setVisibility(View.GONE);
|
||||
mClientCertificateLabelView.setVisibility(View.VISIBLE);
|
||||
mClientCertificateSpinner.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
|
||||
// show password fields, hide client certificate fields
|
||||
mPasswordView.setVisibility(View.VISIBLE);
|
||||
mPasswordLabelView.setVisibility(View.VISIBLE);
|
||||
mClientCertificateLabelView.setVisibility(View.GONE);
|
||||
mClientCertificateSpinner.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is invoked only when the user makes changes to a widget, not when
|
||||
* widgets are changed programmatically. (The logic is simpler when you know
|
||||
* that this is the last thing called after an input change.)
|
||||
*/
|
||||
private void validateFields() {
|
||||
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType);
|
||||
|
||||
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
|
||||
boolean hasConnectionSecurity = (connectionSecurity != ConnectionSecurity.NONE);
|
||||
|
||||
if (isAuthTypeExternal && !hasConnectionSecurity) {
|
||||
|
||||
// Notify user of an invalid combination of AuthType.EXTERNAL & ConnectionSecurity.NONE
|
||||
String toastText = getString(R.string.account_setup_outgoing_invalid_setting_combo_notice,
|
||||
getString(R.string.account_setup_incoming_auth_type_label),
|
||||
AuthType.EXTERNAL.toString(),
|
||||
getString(R.string.account_setup_incoming_security_label),
|
||||
ConnectionSecurity.NONE.toString());
|
||||
Toast.makeText(this, toastText, Toast.LENGTH_LONG).show();
|
||||
|
||||
// Reset the views back to their previous settings without recursing through here again
|
||||
OnItemSelectedListener onItemSelectedListener = mAuthTypeView.getOnItemSelectedListener();
|
||||
mAuthTypeView.setOnItemSelectedListener(null);
|
||||
mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false);
|
||||
mAuthTypeView.setOnItemSelectedListener(onItemSelectedListener);
|
||||
updateViewFromAuthType();
|
||||
|
||||
onItemSelectedListener = mSecurityTypeView.getOnItemSelectedListener();
|
||||
mSecurityTypeView.setOnItemSelectedListener(null);
|
||||
mSecurityTypeView.setSelection(mCurrentSecurityTypeViewPosition, false);
|
||||
mSecurityTypeView.setOnItemSelectedListener(onItemSelectedListener);
|
||||
updateAuthPlainTextFromSecurityType((ConnectionSecurity) mSecurityTypeView.getSelectedItem());
|
||||
|
||||
mPortView.removeTextChangedListener(validationTextWatcher);
|
||||
mPortView.setText(mCurrentPortViewSetting);
|
||||
mPortView.addTextChangedListener(validationTextWatcher);
|
||||
|
||||
authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
isAuthTypeExternal = (AuthType.EXTERNAL == authType);
|
||||
|
||||
connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
|
||||
hasConnectionSecurity = (connectionSecurity != ConnectionSecurity.NONE);
|
||||
} else {
|
||||
mCurrentAuthTypeViewPosition = mAuthTypeView.getSelectedItemPosition();
|
||||
mCurrentSecurityTypeViewPosition = mSecurityTypeView.getSelectedItemPosition();
|
||||
mCurrentPortViewSetting = mPortView.getText().toString();
|
||||
}
|
||||
|
||||
boolean hasValidCertificateAlias = mClientCertificateSpinner.getAlias() != null;
|
||||
boolean hasValidUserName = Utility.requiredFieldValid(mUsernameView);
|
||||
|
||||
boolean hasValidPasswordSettings = hasValidUserName
|
||||
&& !isAuthTypeExternal
|
||||
&& Utility.requiredFieldValid(mPasswordView);
|
||||
|
||||
boolean hasValidExternalAuthSettings = hasValidUserName
|
||||
&& isAuthTypeExternal
|
||||
&& hasConnectionSecurity
|
||||
&& hasValidCertificateAlias;
|
||||
|
||||
mNextButton
|
||||
.setEnabled(
|
||||
Utility.domainFieldValid(mServerView) &&
|
||||
Utility.requiredFieldValid(mPortView) &&
|
||||
(!mRequireLoginView.isChecked() ||
|
||||
(Utility.requiredFieldValid(mUsernameView) &&
|
||||
Utility.requiredFieldValid(mPasswordView))));
|
||||
.setEnabled(Utility.domainFieldValid(mServerView)
|
||||
&& Utility.requiredFieldValid(mPortView)
|
||||
&& (!mRequireLoginView.isChecked()
|
||||
|| hasValidPasswordSettings || hasValidExternalAuthSettings));
|
||||
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
|
||||
}
|
||||
|
||||
private void updatePortFromSecurityType() {
|
||||
ConnectionSecurity securityType = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
|
||||
mPortView.setText(getDefaultSmtpPort(securityType));
|
||||
updateAuthPlainTextFromSecurityType(securityType);
|
||||
|
||||
// Remove listener so as not to trigger validateFields() which is called
|
||||
// elsewhere as a result of user interaction.
|
||||
mPortView.removeTextChangedListener(validationTextWatcher);
|
||||
mPortView.setText(getDefaultSmtpPort(securityType));
|
||||
mPortView.addTextChangedListener(validationTextWatcher);
|
||||
}
|
||||
|
||||
private String getDefaultSmtpPort(ConnectionSecurity securityType) {
|
||||
@ -276,17 +476,23 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
String uri;
|
||||
String username = null;
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
AuthType authType = null;
|
||||
if (mRequireLoginView.isChecked()) {
|
||||
username = mUsernameView.getText().toString();
|
||||
password = mPasswordView.getText().toString();
|
||||
|
||||
authType = (AuthType) mAuthTypeView.getSelectedItem();
|
||||
if (AuthType.EXTERNAL == authType) {
|
||||
clientCertificateAlias = mClientCertificateSpinner.getAlias();
|
||||
} else {
|
||||
password = mPasswordView.getText().toString();
|
||||
}
|
||||
}
|
||||
|
||||
String newHost = mServerView.getText().toString();
|
||||
int newPort = Integer.parseInt(mPortView.getText().toString());
|
||||
String type = SmtpTransport.TRANSPORT_TYPE;
|
||||
ServerSettings server = new ServerSettings(type, newHost, newPort, securityType, authType, username, password);
|
||||
ServerSettings server = new ServerSettings(type, newHost, newPort, securityType, authType, username, password, clientCertificateAlias);
|
||||
uri = Transport.createTransportUri(server);
|
||||
mAccount.deleteCertificate(newHost, newPort, CheckDirection.OUTGOING);
|
||||
mAccount.setTransportUri(uri);
|
||||
@ -312,4 +518,27 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
||||
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
/*
|
||||
* Calls validateFields() which enables or disables the Next button
|
||||
* based on the fields' validity.
|
||||
*/
|
||||
TextWatcher validationTextWatcher = new TextWatcher() {
|
||||
public void afterTextChanged(Editable s) {
|
||||
validateFields();
|
||||
}
|
||||
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
};
|
||||
|
||||
OnClientCertificateChangedListener clientCertificateChangedListener = new OnClientCertificateChangedListener() {
|
||||
@Override
|
||||
public void onClientCertificateChanged(String alias) {
|
||||
validateFields();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -2672,7 +2672,7 @@ public class MessagingController implements Runnable {
|
||||
final NotificationManager nm = (NotificationManager)
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
if (direction.equals(CheckDirection.INCOMING)) {
|
||||
if (direction == CheckDirection.INCOMING) {
|
||||
nm.cancel(null, K9.CERTIFICATE_EXCEPTION_NOTIFICATION_INCOMING + account.getAccountNumber());
|
||||
} else {
|
||||
nm.cancel(null, K9.CERTIFICATE_EXCEPTION_NOTIFICATION_OUTGOING + account.getAccountNumber());
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.fsck.k9.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.DialogFragment;
|
||||
@ -7,10 +8,13 @@ import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnCancelListener;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.K9;
|
||||
|
||||
public class ConfirmationDialogFragment extends DialogFragment implements OnClickListener,
|
||||
OnCancelListener {
|
||||
private ConfirmationDialogFragmentListener mListener;
|
||||
|
||||
private static final String ARG_DIALOG_ID = "dialog_id";
|
||||
private static final String ARG_TITLE = "title";
|
||||
@ -34,6 +38,11 @@ public class ConfirmationDialogFragment extends DialogFragment implements OnClic
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public static ConfirmationDialogFragment newInstance(int dialogId, String title, String message,
|
||||
String cancelText) {
|
||||
return newInstance(dialogId, title, message, null, cancelText);
|
||||
}
|
||||
|
||||
|
||||
public interface ConfirmationDialogFragmentListener {
|
||||
void doPositiveClick(int dialogId);
|
||||
@ -53,8 +62,14 @@ public class ConfirmationDialogFragment extends DialogFragment implements OnClic
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(title);
|
||||
builder.setMessage(message);
|
||||
if (confirmText != null && cancelText != null) {
|
||||
builder.setPositiveButton(confirmText, this);
|
||||
builder.setNegativeButton(cancelText, this);
|
||||
} else if (cancelText != null) {
|
||||
builder.setNeutralButton(cancelText, this);
|
||||
} else {
|
||||
throw new RuntimeException("Set at least cancelText!");
|
||||
}
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
@ -70,6 +85,10 @@ public class ConfirmationDialogFragment extends DialogFragment implements OnClic
|
||||
getListener().doNegativeClick(getDialogId());
|
||||
break;
|
||||
}
|
||||
case DialogInterface.BUTTON_NEUTRAL: {
|
||||
getListener().doNegativeClick(getDialogId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,7 +102,23 @@ public class ConfirmationDialogFragment extends DialogFragment implements OnClic
|
||||
return getArguments().getInt(ARG_DIALOG_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
mListener = (ConfirmationDialogFragmentListener) activity;
|
||||
} catch (ClassCastException e) {
|
||||
if (K9.DEBUG)
|
||||
Log.d(K9.LOG_TAG, activity.toString() + " did not implement ConfirmationDialogFragmentListener");
|
||||
}
|
||||
}
|
||||
|
||||
private ConfirmationDialogFragmentListener getListener() {
|
||||
if (mListener != null) {
|
||||
return mListener;
|
||||
}
|
||||
|
||||
// fallback to getTargetFragment...
|
||||
try {
|
||||
return (ConfirmationDialogFragmentListener) getTargetFragment();
|
||||
} catch (ClassCastException e) {
|
||||
|
@ -33,6 +33,8 @@ public enum AuthType {
|
||||
|
||||
CRAM_MD5(R.string.account_setup_auth_type_encrypted_password),
|
||||
|
||||
EXTERNAL(R.string.account_setup_auth_type_tls_client_certificate),
|
||||
|
||||
/*
|
||||
* The following are obsolete authentication settings that were used with
|
||||
* SMTP. They are no longer presented to the user as options, but they may
|
||||
@ -44,7 +46,7 @@ public enum AuthType {
|
||||
LOGIN(0);
|
||||
|
||||
static public ArrayAdapter<AuthType> getArrayAdapter(Context context) {
|
||||
AuthType[] authTypes = {PLAIN, CRAM_MD5};
|
||||
AuthType[] authTypes = new AuthType[]{PLAIN, CRAM_MD5, EXTERNAL};
|
||||
ArrayAdapter<AuthType> authTypesAdapter = new ArrayAdapter<AuthType>(context,
|
||||
android.R.layout.simple_spinner_item, authTypes);
|
||||
authTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
|
@ -5,6 +5,10 @@ import java.security.cert.CertPathValidatorException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
import android.security.KeyChainException;
|
||||
|
||||
public class CertificateValidationException extends MessagingException {
|
||||
public static final long serialVersionUID = -1;
|
||||
private X509Certificate[] mCertChain;
|
||||
@ -12,7 +16,11 @@ public class CertificateValidationException extends MessagingException {
|
||||
|
||||
public CertificateValidationException(String message) {
|
||||
super(message);
|
||||
scanForCause();
|
||||
/*
|
||||
* Instances created without a Throwable parameter as a cause are
|
||||
* presumed to need user attention.
|
||||
*/
|
||||
mNeedsUserAttention = true;
|
||||
}
|
||||
|
||||
public CertificateValidationException(final String message, Throwable throwable) {
|
||||
@ -23,16 +31,56 @@ public class CertificateValidationException extends MessagingException {
|
||||
private void scanForCause() {
|
||||
Throwable throwable = getCause();
|
||||
|
||||
/* user attention is required if the certificate was deemed invalid */
|
||||
/*
|
||||
* User attention is required if the server certificate was deemed
|
||||
* invalid or if there was a problem with a client certificate.
|
||||
*
|
||||
* A CertificateException is known to be thrown by the default
|
||||
* X509TrustManager.checkServerTrusted() if the server certificate
|
||||
* doesn't validate. The cause of the CertificateException will be a
|
||||
* CertPathValidatorException. However, it's unlikely those exceptions
|
||||
* will be encountered here, because they are caught in
|
||||
* SecureX509TrustManager.checkServerTrusted(), which throws a
|
||||
* CertificateChainException instead (an extension of
|
||||
* CertificateException).
|
||||
*
|
||||
* A CertificateChainException will likely result in (or, be the cause
|
||||
* of) an SSLHandshakeException (an extension of SSLException).
|
||||
*
|
||||
* The various mail protocol handlers (IMAP, POP3, ...) will catch an
|
||||
* SSLException and throw a CertificateValidationException (this class)
|
||||
* with the SSLException as the cause. (They may also throw a
|
||||
* CertificateValidationException when STARTTLS is not available, just
|
||||
* for the purpose of triggering a user notification.)
|
||||
*
|
||||
* SSLHandshakeException is also known to occur if the *client*
|
||||
* certificate was not accepted by the server (unknown CA, certificate
|
||||
* expired, etc.). In this case, the SSLHandshakeException will not have
|
||||
* a CertificateChainException as a cause.
|
||||
*
|
||||
* KeyChainException is known to occur if the device has no client
|
||||
* certificate that's associated with the alias stored in the server
|
||||
* settings.
|
||||
*/
|
||||
while (throwable != null
|
||||
&& !(throwable instanceof CertPathValidatorException)
|
||||
&& !(throwable instanceof CertificateException)) {
|
||||
&& !(throwable instanceof CertificateException)
|
||||
&& !(throwable instanceof KeyChainException)
|
||||
&& !(throwable instanceof SSLHandshakeException)) {
|
||||
throwable = throwable.getCause();
|
||||
}
|
||||
|
||||
if (throwable != null) {
|
||||
mNeedsUserAttention = true;
|
||||
if (throwable instanceof CertificateChainException) {
|
||||
|
||||
// See if there is a server certificate chain attached to the SSLHandshakeException
|
||||
if (throwable instanceof SSLHandshakeException) {
|
||||
while (throwable != null && !(throwable instanceof CertificateChainException)) {
|
||||
throwable = throwable.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
if (throwable != null && throwable instanceof CertificateChainException) {
|
||||
mCertChain = ((CertificateChainException) throwable).getCertChain();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
package com.fsck.k9.mail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.R;
|
||||
|
||||
@ -8,6 +11,17 @@ public enum ConnectionSecurity {
|
||||
STARTTLS_REQUIRED(R.string.account_setup_incoming_security_tls_label),
|
||||
SSL_TLS_REQUIRED(R.string.account_setup_incoming_security_ssl_label);
|
||||
|
||||
static public ArrayAdapter<ConnectionSecurity> getArrayAdapter(Context context) {
|
||||
return getArrayAdapter(context, ConnectionSecurity.values());
|
||||
}
|
||||
|
||||
static public ArrayAdapter<ConnectionSecurity> getArrayAdapter(Context context, ConnectionSecurity[] securityTypes) {
|
||||
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(context,
|
||||
android.R.layout.simple_spinner_item, securityTypes);
|
||||
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
return securityTypesAdapter;
|
||||
}
|
||||
|
||||
private final int mResourceId;
|
||||
|
||||
private ConnectionSecurity(int id) {
|
||||
|
@ -64,6 +64,14 @@ public class ServerSettings {
|
||||
*/
|
||||
public final String password;
|
||||
|
||||
/**
|
||||
* The alias to retrieve a client certificate using Android 4.0 KeyChain API
|
||||
* for TLS client certificate authentication with the server.
|
||||
*
|
||||
* {@code null} if not applicable for the store or transport.
|
||||
*/
|
||||
public final String clientCertificateAlias;
|
||||
|
||||
/**
|
||||
* Store- or transport-specific settings as key/value pair.
|
||||
*
|
||||
@ -89,10 +97,12 @@ public class ServerSettings {
|
||||
* see {@link ServerSettings#username}
|
||||
* @param password
|
||||
* see {@link ServerSettings#password}
|
||||
* @param clientCertificateAlias
|
||||
* see {@link ServerSettings#clientCertificateAlias}
|
||||
*/
|
||||
public ServerSettings(String type, String host, int port,
|
||||
ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
|
||||
String password) {
|
||||
String password, String clientCertificateAlias) {
|
||||
this.type = type;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
@ -100,6 +110,7 @@ public class ServerSettings {
|
||||
this.authenticationType = authenticationType;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.clientCertificateAlias = clientCertificateAlias;
|
||||
this.extra = null;
|
||||
}
|
||||
|
||||
@ -120,12 +131,14 @@ public class ServerSettings {
|
||||
* see {@link ServerSettings#username}
|
||||
* @param password
|
||||
* see {@link ServerSettings#password}
|
||||
* @param clientCertificateAlias
|
||||
* see {@link ServerSettings#clientCertificateAlias}
|
||||
* @param extra
|
||||
* see {@link ServerSettings#extra}
|
||||
*/
|
||||
public ServerSettings(String type, String host, int port,
|
||||
ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
|
||||
String password, Map<String, String> extra) {
|
||||
String password, String clientCertificateAlias, Map<String, String> extra) {
|
||||
this.type = type;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
@ -133,6 +146,7 @@ public class ServerSettings {
|
||||
this.authenticationType = authenticationType;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.clientCertificateAlias = clientCertificateAlias;
|
||||
this.extra = (extra != null) ?
|
||||
Collections.unmodifiableMap(new HashMap<String, String>(extra)) : null;
|
||||
}
|
||||
@ -153,6 +167,7 @@ public class ServerSettings {
|
||||
authenticationType = null;
|
||||
username = null;
|
||||
password = null;
|
||||
clientCertificateAlias = null;
|
||||
extra = null;
|
||||
}
|
||||
|
||||
@ -173,6 +188,11 @@ public class ServerSettings {
|
||||
|
||||
public ServerSettings newPassword(String newPassword) {
|
||||
return new ServerSettings(type, host, port, connectionSecurity, authenticationType,
|
||||
username, newPassword);
|
||||
username, newPassword, clientCertificateAlias);
|
||||
}
|
||||
|
||||
public ServerSettings newClientCertificateAlias(String newAlias) {
|
||||
return new ServerSettings(type, host, port, connectionSecurity, AuthType.EXTERNAL,
|
||||
username, password, newAlias);
|
||||
}
|
||||
}
|
@ -25,9 +25,7 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -50,9 +48,7 @@ import java.util.regex.Pattern;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
@ -100,8 +96,7 @@ import com.fsck.k9.mail.store.ImapResponseParser.ImapList;
|
||||
import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse;
|
||||
import com.fsck.k9.mail.store.imap.ImapUtility;
|
||||
import com.fsck.k9.mail.transport.imap.ImapSettings;
|
||||
import com.fsck.k9.net.ssl.TrustManagerFactory;
|
||||
import com.fsck.k9.net.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.net.ssl.SslHelper;
|
||||
import com.jcraft.jzlib.JZlib;
|
||||
import com.jcraft.jzlib.ZOutputStream;
|
||||
|
||||
@ -126,6 +121,7 @@ public class ImapStore extends Store {
|
||||
private static final String CAPABILITY_IDLE = "IDLE";
|
||||
private static final String CAPABILITY_AUTH_CRAM_MD5 = "AUTH=CRAM-MD5";
|
||||
private static final String CAPABILITY_AUTH_PLAIN = "AUTH=PLAIN";
|
||||
private static final String CAPABILITY_AUTH_EXTERNAL = "AUTH=EXTERNAL";
|
||||
private static final String CAPABILITY_LOGINDISABLED = "LOGINDISABLED";
|
||||
private static final String COMMAND_IDLE = "IDLE";
|
||||
private static final String CAPABILITY_NAMESPACE = "NAMESPACE";
|
||||
@ -158,6 +154,7 @@ public class ImapStore extends Store {
|
||||
AuthType authenticationType = null;
|
||||
String username = null;
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
String pathPrefix = null;
|
||||
boolean autoDetectNamespace = true;
|
||||
|
||||
@ -213,11 +210,16 @@ public class ImapStore extends Store {
|
||||
authenticationType = AuthType.PLAIN;
|
||||
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
|
||||
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
||||
} else {
|
||||
} else if (userInfoParts.length == 3) {
|
||||
authenticationType = AuthType.valueOf(userInfoParts[0]);
|
||||
username = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
||||
|
||||
if (AuthType.EXTERNAL == authenticationType) {
|
||||
clientCertificateAlias = URLDecoder.decode(userInfoParts[2], "UTF-8");
|
||||
} else {
|
||||
password = URLDecoder.decode(userInfoParts[2], "UTF-8");
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException enc) {
|
||||
// This shouldn't happen since the encoding is hardcoded to UTF-8
|
||||
throw new IllegalArgumentException("Couldn't urldecode username or password.", enc);
|
||||
@ -243,7 +245,7 @@ public class ImapStore extends Store {
|
||||
}
|
||||
|
||||
return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, username,
|
||||
password, autoDetectNamespace, pathPrefix);
|
||||
password, clientCertificateAlias, autoDetectNamespace, pathPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -260,10 +262,13 @@ public class ImapStore extends Store {
|
||||
public static String createUri(ServerSettings server) {
|
||||
String userEnc;
|
||||
String passwordEnc;
|
||||
String clientCertificateAliasEnc;
|
||||
try {
|
||||
userEnc = URLEncoder.encode(server.username, "UTF-8");
|
||||
passwordEnc = (server.password != null) ?
|
||||
URLEncoder.encode(server.password, "UTF-8") : "";
|
||||
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
|
||||
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalArgumentException("Could not encode username or password", e);
|
||||
@ -284,8 +289,12 @@ public class ImapStore extends Store {
|
||||
}
|
||||
|
||||
AuthType authType = server.authenticationType;
|
||||
|
||||
String userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
|
||||
String userInfo;
|
||||
if (authType == AuthType.EXTERNAL) {
|
||||
userInfo = authType.name() + ":" + userEnc + ":" + clientCertificateAliasEnc;
|
||||
} else {
|
||||
userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
|
||||
}
|
||||
try {
|
||||
Map<String, String> extra = server.getExtra();
|
||||
String path = null;
|
||||
@ -320,10 +329,10 @@ public class ImapStore extends Store {
|
||||
public final String pathPrefix;
|
||||
|
||||
protected ImapStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
|
||||
AuthType authenticationType, String username, String password,
|
||||
AuthType authenticationType, String username, String password, String clientCertificateAlias,
|
||||
boolean autodetectNamespace, String pathPrefix) {
|
||||
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
|
||||
password);
|
||||
password, clientCertificateAlias);
|
||||
this.autoDetectNamespace = autodetectNamespace;
|
||||
this.pathPrefix = pathPrefix;
|
||||
}
|
||||
@ -339,7 +348,7 @@ public class ImapStore extends Store {
|
||||
@Override
|
||||
public ServerSettings newPassword(String newPassword) {
|
||||
return new ImapStoreSettings(host, port, connectionSecurity, authenticationType,
|
||||
username, newPassword, autoDetectNamespace, pathPrefix);
|
||||
username, newPassword, clientCertificateAlias, autoDetectNamespace, pathPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
@ -348,6 +357,7 @@ public class ImapStore extends Store {
|
||||
private int mPort;
|
||||
private String mUsername;
|
||||
private String mPassword;
|
||||
private String mClientCertificateAlias;
|
||||
private ConnectionSecurity mConnectionSecurity;
|
||||
private AuthType mAuthType;
|
||||
private volatile String mPathPrefix;
|
||||
@ -386,6 +396,11 @@ public class ImapStore extends Store {
|
||||
return mPassword;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientCertificateAlias() {
|
||||
return mClientCertificateAlias;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useCompression(final int type) {
|
||||
return mAccount.useCompression(type);
|
||||
@ -458,6 +473,7 @@ public class ImapStore extends Store {
|
||||
mAuthType = settings.authenticationType;
|
||||
mUsername = settings.username;
|
||||
mPassword = settings.password;
|
||||
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||
|
||||
// Make extra sure mPathPrefix is null if "auto-detect namespace" is configured
|
||||
mPathPrefix = (settings.autoDetectNamespace) ? null : settings.pathPrefix;
|
||||
@ -2419,14 +2435,8 @@ public class ImapStore extends Store {
|
||||
mSettings.getPort());
|
||||
|
||||
if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext
|
||||
.init(null,
|
||||
new TrustManager[] { TrustManagerFactory.get(
|
||||
mSettings.getHost(),
|
||||
mSettings.getPort()) },
|
||||
new SecureRandom());
|
||||
mSocket = TrustedSocketFactory.createSocket(sslContext);
|
||||
mSocket = SslHelper.createSslSocket(mSettings.getHost(),
|
||||
mSettings.getPort(), mSettings.getClientCertificateAlias());
|
||||
} else {
|
||||
mSocket = new Socket();
|
||||
}
|
||||
@ -2475,14 +2485,9 @@ public class ImapStore extends Store {
|
||||
// STARTTLS
|
||||
executeSimpleCommand("STARTTLS");
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null,
|
||||
new TrustManager[] { TrustManagerFactory.get(
|
||||
mSettings.getHost(),
|
||||
mSettings.getPort()) },
|
||||
new SecureRandom());
|
||||
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket,
|
||||
mSettings.getHost(), mSettings.getPort(), true);
|
||||
mSocket = SslHelper.createStartTlsSocket(mSocket,
|
||||
mSettings.getHost(), mSettings.getPort(), true,
|
||||
mSettings.getClientCertificateAlias());
|
||||
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
|
||||
mIn = new PeekableInputStream(new BufferedInputStream(mSocket
|
||||
.getInputStream(), 1024));
|
||||
@ -2505,8 +2510,7 @@ public class ImapStore extends Store {
|
||||
* "STARTTLS (if available)" setting.
|
||||
*/
|
||||
throw new CertificateValidationException(
|
||||
"STARTTLS connection security not available",
|
||||
new CertificateException());
|
||||
"STARTTLS connection security not available");
|
||||
}
|
||||
}
|
||||
|
||||
@ -2531,6 +2535,15 @@ public class ImapStore extends Store {
|
||||
}
|
||||
break;
|
||||
|
||||
case EXTERNAL:
|
||||
if (hasCapability(CAPABILITY_AUTH_EXTERNAL)) {
|
||||
saslAuthExternal();
|
||||
} else {
|
||||
// Provide notification to user of a problem authenticating using client certificates
|
||||
throw new CertificateValidationException(K9.app.getString(R.string.auth_external_error));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new MessagingException(
|
||||
"Unhandled authentication method found in the server settings (bug).");
|
||||
@ -2630,7 +2643,6 @@ public class ImapStore extends Store {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (SSLException e) {
|
||||
throw new CertificateValidationException(e.getMessage(), e);
|
||||
} catch (GeneralSecurityException gse) {
|
||||
@ -2713,6 +2725,23 @@ public class ImapStore extends Store {
|
||||
}
|
||||
}
|
||||
|
||||
private void saslAuthExternal() throws IOException, MessagingException {
|
||||
try {
|
||||
receiveCapabilities(executeSimpleCommand(
|
||||
String.format("AUTHENTICATE EXTERNAL %s",
|
||||
Utility.base64Encode(mSettings.getUsername())), false));
|
||||
} catch (ImapException e) {
|
||||
/*
|
||||
* Provide notification to the user of a problem authenticating
|
||||
* using client certificates. We don't use an
|
||||
* AuthenticationFailedException because that would trigger a
|
||||
* "Username or password incorrect" notification in
|
||||
* AccountSetupCheckSettings.
|
||||
*/
|
||||
throw new CertificateValidationException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected ImapResponse readContinuationResponse(String tag)
|
||||
throws IOException, MessagingException {
|
||||
ImapResponse response;
|
||||
|
@ -5,27 +5,22 @@ import android.util.Log;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.R;
|
||||
import com.fsck.k9.controller.MessageRetrievalListener;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.mail.*;
|
||||
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
import com.fsck.k9.mail.filter.Hex;
|
||||
import com.fsck.k9.mail.internet.MimeMessage;
|
||||
import com.fsck.k9.net.ssl.TrustManagerFactory;
|
||||
import com.fsck.k9.net.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.net.ssl.SslHelper;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
@ -59,15 +54,16 @@ public class Pop3Store extends Store {
|
||||
private static final String SASL_CAPABILITY = "SASL";
|
||||
private static final String AUTH_PLAIN_CAPABILITY = "PLAIN";
|
||||
private static final String AUTH_CRAM_MD5_CAPABILITY = "CRAM-MD5";
|
||||
private static final String AUTH_EXTERNAL_CAPABILITY = "EXTERNAL";
|
||||
|
||||
/**
|
||||
* Decodes a Pop3Store URI.
|
||||
*
|
||||
* <p>Possible forms:</p>
|
||||
* <pre>
|
||||
* pop3://user:password@server:port ConnectionSecurity.NONE
|
||||
* pop3+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
|
||||
* pop3+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
* pop3://auth:user:password@server:port ConnectionSecurity.NONE
|
||||
* pop3+tls+://auth:user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
|
||||
* pop3+ssl+://auth:user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
* </pre>
|
||||
*/
|
||||
public static ServerSettings decodeUri(String uri) {
|
||||
@ -76,6 +72,7 @@ public class Pop3Store extends Store {
|
||||
ConnectionSecurity connectionSecurity;
|
||||
String username = null;
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
|
||||
URI pop3Uri;
|
||||
try {
|
||||
@ -131,8 +128,12 @@ public class Pop3Store extends Store {
|
||||
}
|
||||
username = URLDecoder.decode(userInfoParts[userIndex], "UTF-8");
|
||||
if (userInfoParts.length > passwordIndex) {
|
||||
if (authType == AuthType.EXTERNAL) {
|
||||
clientCertificateAlias = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
|
||||
} else {
|
||||
password = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException enc) {
|
||||
// This shouldn't happen since the encoding is hardcoded to UTF-8
|
||||
throw new IllegalArgumentException("Couldn't urldecode username or password.", enc);
|
||||
@ -140,7 +141,7 @@ public class Pop3Store extends Store {
|
||||
}
|
||||
|
||||
return new ServerSettings(STORE_TYPE, host, port, connectionSecurity, authType, username,
|
||||
password);
|
||||
password, clientCertificateAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -157,10 +158,13 @@ public class Pop3Store extends Store {
|
||||
public static String createUri(ServerSettings server) {
|
||||
String userEnc;
|
||||
String passwordEnc;
|
||||
String clientCertificateAliasEnc;
|
||||
try {
|
||||
userEnc = URLEncoder.encode(server.username, "UTF-8");
|
||||
passwordEnc = (server.password != null) ?
|
||||
URLEncoder.encode(server.password, "UTF-8") : "";
|
||||
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
|
||||
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalArgumentException("Could not encode username or password", e);
|
||||
@ -180,7 +184,14 @@ public class Pop3Store extends Store {
|
||||
break;
|
||||
}
|
||||
|
||||
String userInfo = server.authenticationType.name() + ":" + userEnc + ":" + passwordEnc;
|
||||
AuthType authType = server.authenticationType;
|
||||
String userInfo;
|
||||
if (AuthType.EXTERNAL == authType) {
|
||||
userInfo = authType.name() + ":" + userEnc + ":" + clientCertificateAliasEnc;
|
||||
} else {
|
||||
userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
|
||||
}
|
||||
|
||||
try {
|
||||
return new URI(scheme, userInfo, server.host, server.port, null, null,
|
||||
null).toString();
|
||||
@ -194,6 +205,7 @@ public class Pop3Store extends Store {
|
||||
private int mPort;
|
||||
private String mUsername;
|
||||
private String mPassword;
|
||||
private String mClientCertificateAlias;
|
||||
private AuthType mAuthType;
|
||||
private ConnectionSecurity mConnectionSecurity;
|
||||
private HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
|
||||
@ -224,6 +236,7 @@ public class Pop3Store extends Store {
|
||||
|
||||
mUsername = settings.username;
|
||||
mPassword = settings.password;
|
||||
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||
mAuthType = settings.authenticationType;
|
||||
}
|
||||
|
||||
@ -301,11 +314,7 @@ public class Pop3Store extends Store {
|
||||
try {
|
||||
SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
|
||||
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null,
|
||||
new TrustManager[] { TrustManagerFactory.get(mHost,
|
||||
mPort) }, new SecureRandom());
|
||||
mSocket = TrustedSocketFactory.createSocket(sslContext);
|
||||
mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
|
||||
} else {
|
||||
mSocket = new Socket();
|
||||
}
|
||||
@ -327,13 +336,8 @@ public class Pop3Store extends Store {
|
||||
if (mCapabilities.stls) {
|
||||
executeSimpleCommand(STLS_COMMAND);
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null,
|
||||
new TrustManager[] { TrustManagerFactory.get(
|
||||
mHost, mPort) },
|
||||
new SecureRandom());
|
||||
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
|
||||
mPort, true);
|
||||
mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
|
||||
mClientCertificateAlias);
|
||||
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
|
||||
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
|
||||
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
|
||||
@ -350,8 +354,7 @@ public class Pop3Store extends Store {
|
||||
* "STARTTLS (if available)" setting.
|
||||
*/
|
||||
throw new CertificateValidationException(
|
||||
"STARTTLS connection security not available",
|
||||
new CertificateException());
|
||||
"STARTTLS connection security not available");
|
||||
}
|
||||
}
|
||||
|
||||
@ -372,6 +375,15 @@ public class Pop3Store extends Store {
|
||||
}
|
||||
break;
|
||||
|
||||
case EXTERNAL:
|
||||
if (mCapabilities.external) {
|
||||
authExternal();
|
||||
} else {
|
||||
// Provide notification to user of a problem authenticating using client certificates
|
||||
throw new CertificateValidationException(K9.app.getString(R.string.auth_external_error));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new MessagingException(
|
||||
"Unhandled authentication method found in the server settings (bug).");
|
||||
@ -455,6 +467,24 @@ public class Pop3Store extends Store {
|
||||
}
|
||||
}
|
||||
|
||||
private void authExternal() throws MessagingException {
|
||||
try {
|
||||
executeSimpleCommand(
|
||||
String.format("AUTH EXTERNAL %s",
|
||||
Utility.base64Encode(mUsername)), false);
|
||||
} catch (Pop3ErrorResponse e) {
|
||||
/*
|
||||
* Provide notification to the user of a problem authenticating
|
||||
* using client certificates. We don't use an
|
||||
* AuthenticationFailedException because that would trigger a
|
||||
* "Username or password incorrect" notification in
|
||||
* AccountSetupCheckSettings.
|
||||
*/
|
||||
throw new CertificateValidationException(
|
||||
"POP3 client certificate authentication failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return (mIn != null && mOut != null && mSocket != null
|
||||
@ -1046,6 +1076,8 @@ public class Pop3Store extends Store {
|
||||
capabilities.authPlain = true;
|
||||
} else if (response.equals(AUTH_CRAM_MD5_CAPABILITY)) {
|
||||
capabilities.cramMD5 = true;
|
||||
} else if (response.equals(AUTH_EXTERNAL_CAPABILITY)) {
|
||||
capabilities.external = true;
|
||||
}
|
||||
}
|
||||
} catch (MessagingException e) {
|
||||
@ -1193,15 +1225,17 @@ public class Pop3Store extends Store {
|
||||
public boolean stls;
|
||||
public boolean top;
|
||||
public boolean uidl;
|
||||
public boolean external;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("CRAM-MD5 %b, PLAIN %b, STLS %b, TOP %b, UIDL %b",
|
||||
return String.format("CRAM-MD5 %b, PLAIN %b, STLS %b, TOP %b, UIDL %b, EXTERNAL %b",
|
||||
cramMD5,
|
||||
authPlain,
|
||||
stls,
|
||||
top,
|
||||
uidl);
|
||||
uidl,
|
||||
external);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,9 +7,9 @@ import com.fsck.k9.K9;
|
||||
import com.fsck.k9.controller.MessageRetrievalListener;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.mail.*;
|
||||
|
||||
import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
|
||||
import com.fsck.k9.mail.internet.MimeMessage;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.*;
|
||||
import org.apache.http.client.CookieStore;
|
||||
@ -36,6 +36,7 @@ import javax.net.ssl.SSLException;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.parsers.SAXParser;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
@ -178,7 +179,7 @@ public class WebDavStore extends Store {
|
||||
}
|
||||
|
||||
return new WebDavStoreSettings(host, port, connectionSecurity, null, username, password,
|
||||
alias, path, authPath, mailboxPath);
|
||||
null, alias, path, authPath, mailboxPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -257,10 +258,10 @@ public class WebDavStore extends Store {
|
||||
public final String mailboxPath;
|
||||
|
||||
protected WebDavStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
|
||||
AuthType authenticationType, String username, String password, String alias,
|
||||
AuthType authenticationType, String username, String password, String clientCertificateAlias, String alias,
|
||||
String path, String authPath, String mailboxPath) {
|
||||
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
|
||||
password);
|
||||
password, clientCertificateAlias);
|
||||
this.alias = alias;
|
||||
this.path = path;
|
||||
this.authPath = authPath;
|
||||
@ -280,7 +281,7 @@ public class WebDavStore extends Store {
|
||||
@Override
|
||||
public ServerSettings newPassword(String newPassword) {
|
||||
return new WebDavStoreSettings(host, port, connectionSecurity, authenticationType,
|
||||
username, newPassword, alias, path, authPath, mailboxPath);
|
||||
username, newPassword, clientCertificateAlias, alias, path, authPath, mailboxPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,23 +2,22 @@
|
||||
package com.fsck.k9.mail.transport;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.R;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.mail.*;
|
||||
import com.fsck.k9.mail.Message.RecipientType;
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
|
||||
import com.fsck.k9.mail.filter.LineWrapOutputStream;
|
||||
import com.fsck.k9.mail.filter.PeekableInputStream;
|
||||
import com.fsck.k9.mail.filter.SmtpDataStuffing;
|
||||
import com.fsck.k9.mail.internet.MimeUtility;
|
||||
import com.fsck.k9.mail.store.LocalStore.LocalMessage;
|
||||
import com.fsck.k9.net.ssl.TrustManagerFactory;
|
||||
import com.fsck.k9.net.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.net.ssl.SslHelper;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
@ -27,9 +26,6 @@ import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.*;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class SmtpTransport extends Transport {
|
||||
@ -38,20 +34,23 @@ public class SmtpTransport extends Transport {
|
||||
/**
|
||||
* Decodes a SmtpTransport URI.
|
||||
*
|
||||
* NOTE: In contrast to ImapStore and Pop3Store, the authType is appended at the end!
|
||||
*
|
||||
* <p>Possible forms:</p>
|
||||
* <pre>
|
||||
* smtp://user:password@server:port ConnectionSecurity.NONE
|
||||
* smtp+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
|
||||
* smtp+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
* smtp://user:password:auth@server:port ConnectionSecurity.NONE
|
||||
* smtp+tls+://user:password:auth@server:port ConnectionSecurity.STARTTLS_REQUIRED
|
||||
* smtp+ssl+://user:password:auth@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
* </pre>
|
||||
*/
|
||||
public static ServerSettings decodeUri(String uri) {
|
||||
String host;
|
||||
int port;
|
||||
ConnectionSecurity connectionSecurity;
|
||||
AuthType authType = AuthType.PLAIN;
|
||||
AuthType authType = null;
|
||||
String username = null;
|
||||
String password = null;
|
||||
String clientCertificateAlias = null;
|
||||
|
||||
URI smtpUri;
|
||||
try {
|
||||
@ -95,14 +94,22 @@ public class SmtpTransport extends Transport {
|
||||
if (smtpUri.getUserInfo() != null) {
|
||||
try {
|
||||
String[] userInfoParts = smtpUri.getUserInfo().split(":");
|
||||
if (userInfoParts.length > 0) {
|
||||
if (userInfoParts.length == 1) {
|
||||
authType = AuthType.PLAIN;
|
||||
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
|
||||
}
|
||||
if (userInfoParts.length > 1) {
|
||||
} else if (userInfoParts.length == 2) {
|
||||
authType = AuthType.PLAIN;
|
||||
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
|
||||
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
||||
} else if (userInfoParts.length == 3) {
|
||||
// NOTE: In SmptTransport URIs, the authType comes last!
|
||||
authType = AuthType.valueOf(userInfoParts[2]);
|
||||
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
|
||||
if (authType == AuthType.EXTERNAL) {
|
||||
clientCertificateAlias = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
||||
} else {
|
||||
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
|
||||
}
|
||||
if (userInfoParts.length > 2) {
|
||||
authType = AuthType.valueOf(userInfoParts[2]);
|
||||
}
|
||||
} catch (UnsupportedEncodingException enc) {
|
||||
// This shouldn't happen since the encoding is hardcoded to UTF-8
|
||||
@ -111,7 +118,7 @@ public class SmtpTransport extends Transport {
|
||||
}
|
||||
|
||||
return new ServerSettings(TRANSPORT_TYPE, host, port, connectionSecurity,
|
||||
authType, username, password);
|
||||
authType, username, password, clientCertificateAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -128,11 +135,14 @@ public class SmtpTransport extends Transport {
|
||||
public static String createUri(ServerSettings server) {
|
||||
String userEnc;
|
||||
String passwordEnc;
|
||||
String clientCertificateAliasEnc;
|
||||
try {
|
||||
userEnc = (server.username != null) ?
|
||||
URLEncoder.encode(server.username, "UTF-8") : "";
|
||||
passwordEnc = (server.password != null) ?
|
||||
URLEncoder.encode(server.password, "UTF-8") : "";
|
||||
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
|
||||
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalArgumentException("Could not encode username or password", e);
|
||||
@ -152,10 +162,17 @@ public class SmtpTransport extends Transport {
|
||||
break;
|
||||
}
|
||||
|
||||
String userInfo = userEnc + ":" + passwordEnc;
|
||||
String userInfo = null;
|
||||
AuthType authType = server.authenticationType;
|
||||
// NOTE: authType is append at last item, in contrast to ImapStore and Pop3Store!
|
||||
if (authType != null) {
|
||||
userInfo += ":" + authType.name();
|
||||
if (AuthType.EXTERNAL == authType) {
|
||||
userInfo = userEnc + ":" + clientCertificateAliasEnc + ":" + authType.name();
|
||||
} else {
|
||||
userInfo = userEnc + ":" + passwordEnc + ":" + authType.name();
|
||||
}
|
||||
} else {
|
||||
userInfo = userEnc + ":" + passwordEnc;
|
||||
}
|
||||
try {
|
||||
return new URI(scheme, userInfo, server.host, server.port, null, null,
|
||||
@ -170,6 +187,7 @@ public class SmtpTransport extends Transport {
|
||||
int mPort;
|
||||
String mUsername;
|
||||
String mPassword;
|
||||
String mClientCertificateAlias;
|
||||
AuthType mAuthType;
|
||||
ConnectionSecurity mConnectionSecurity;
|
||||
Socket mSocket;
|
||||
@ -194,6 +212,7 @@ public class SmtpTransport extends Transport {
|
||||
mAuthType = settings.authenticationType;
|
||||
mUsername = settings.username;
|
||||
mPassword = settings.password;
|
||||
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -205,12 +224,7 @@ public class SmtpTransport extends Transport {
|
||||
try {
|
||||
SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort);
|
||||
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null,
|
||||
new TrustManager[] { TrustManagerFactory.get(
|
||||
mHost, mPort) },
|
||||
new SecureRandom());
|
||||
mSocket = TrustedSocketFactory.createSocket(sslContext);
|
||||
mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
|
||||
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
|
||||
secureConnection = true;
|
||||
} else {
|
||||
@ -264,12 +278,9 @@ public class SmtpTransport extends Transport {
|
||||
if (extensions.containsKey("STARTTLS")) {
|
||||
executeSimpleCommand("STARTTLS");
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null,
|
||||
new TrustManager[] { TrustManagerFactory.get(mHost,
|
||||
mPort) }, new SecureRandom());
|
||||
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
|
||||
mPort, true);
|
||||
mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
|
||||
mClientCertificateAlias);
|
||||
|
||||
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
|
||||
1024));
|
||||
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
|
||||
@ -288,19 +299,20 @@ public class SmtpTransport extends Transport {
|
||||
* "STARTTLS (if available)" setting.
|
||||
*/
|
||||
throw new CertificateValidationException(
|
||||
"STARTTLS connection security not available",
|
||||
new CertificateException());
|
||||
"STARTTLS connection security not available");
|
||||
}
|
||||
}
|
||||
|
||||
boolean authLoginSupported = false;
|
||||
boolean authPlainSupported = false;
|
||||
boolean authCramMD5Supported = false;
|
||||
boolean authExternalSupported = false;
|
||||
if (extensions.containsKey("AUTH")) {
|
||||
List<String> saslMech = Arrays.asList(extensions.get("AUTH").split(" "));
|
||||
authLoginSupported = saslMech.contains("LOGIN");
|
||||
authPlainSupported = saslMech.contains("PLAIN");
|
||||
authCramMD5Supported = saslMech.contains("CRAM-MD5");
|
||||
authExternalSupported = saslMech.contains("EXTERNAL");
|
||||
}
|
||||
if (extensions.containsKey("SIZE")) {
|
||||
try {
|
||||
@ -312,8 +324,9 @@ public class SmtpTransport extends Transport {
|
||||
}
|
||||
}
|
||||
|
||||
if (mUsername != null && mUsername.length() > 0 &&
|
||||
mPassword != null && mPassword.length() > 0) {
|
||||
if (mUsername != null
|
||||
&& mUsername.length() > 0
|
||||
&& (mPassword != null && mPassword.length() > 0 || AuthType.EXTERNAL == mAuthType)) {
|
||||
|
||||
switch (mAuthType) {
|
||||
|
||||
@ -342,6 +355,24 @@ public class SmtpTransport extends Transport {
|
||||
}
|
||||
break;
|
||||
|
||||
case EXTERNAL:
|
||||
if (authExternalSupported) {
|
||||
saslAuthExternal(mUsername);
|
||||
} else {
|
||||
/*
|
||||
* Some SMTP servers are known to provide no error
|
||||
* indication when a client certificate fails to
|
||||
* validate, other than to not offer the AUTH EXTERNAL
|
||||
* capability.
|
||||
*
|
||||
* So, we treat it is an error to not offer AUTH
|
||||
* EXTERNAL when using client certificates. That way, the
|
||||
* user can be notified of a problem during account setup.
|
||||
*/
|
||||
throw new MessagingException(K9.app.getString(R.string.auth_external_error));
|
||||
}
|
||||
break;
|
||||
|
||||
/*
|
||||
* AUTOMATIC is an obsolete option which is unavailable to users,
|
||||
* but it still may exist in a user's settings from a previous
|
||||
@ -634,6 +665,12 @@ public class SmtpTransport extends Transport {
|
||||
* Read lines as long as the length is 4 or larger, e.g. "220-banner text here".
|
||||
* Shorter lines are either errors of contain only a reply code. Those cases will
|
||||
* be handled by checkLine() below.
|
||||
*
|
||||
* TODO: All responses should be checked to confirm that they start with a valid
|
||||
* reply code, and that the reply code is appropriate for the command being executed.
|
||||
* That means it should either be a 2xx code (generally) or a 3xx code in special cases
|
||||
* (e.g., DATA & AUTH LOGIN commands). Reply codes should be made available as part of
|
||||
* the returned object.
|
||||
*/
|
||||
String line = readLine();
|
||||
while (line.length() >= 4) {
|
||||
@ -677,29 +714,32 @@ public class SmtpTransport extends Transport {
|
||||
AuthenticationFailedException, IOException {
|
||||
try {
|
||||
executeSimpleCommand("AUTH LOGIN");
|
||||
executeSimpleCommand(new String(Base64.encodeBase64(username.getBytes())), true);
|
||||
executeSimpleCommand(new String(Base64.encodeBase64(password.getBytes())), true);
|
||||
} catch (MessagingException me) {
|
||||
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
|
||||
throw new AuthenticationFailedException("AUTH LOGIN failed (" + me.getMessage()
|
||||
+ ")");
|
||||
executeSimpleCommand(Utility.base64Encode(username), true);
|
||||
executeSimpleCommand(Utility.base64Encode(password), true);
|
||||
} catch (NegativeSmtpReplyException exception) {
|
||||
if (exception.getReplyCode() == 535) {
|
||||
// Authentication credentials invalid
|
||||
throw new AuthenticationFailedException("AUTH LOGIN failed ("
|
||||
+ exception.getMessage() + ")");
|
||||
} else {
|
||||
throw exception;
|
||||
}
|
||||
throw me;
|
||||
}
|
||||
}
|
||||
|
||||
private void saslAuthPlain(String username, String password) throws MessagingException,
|
||||
AuthenticationFailedException, IOException {
|
||||
byte[] data = ("\000" + username + "\000" + password).getBytes();
|
||||
data = new Base64().encode(data);
|
||||
String data = Utility.base64Encode("\000" + username + "\000" + password);
|
||||
try {
|
||||
executeSimpleCommand("AUTH PLAIN " + new String(data), true);
|
||||
} catch (MessagingException me) {
|
||||
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
|
||||
throw new AuthenticationFailedException("AUTH PLAIN failed (" + me.getMessage()
|
||||
+ ")");
|
||||
executeSimpleCommand("AUTH PLAIN " + data, true);
|
||||
} catch (NegativeSmtpReplyException exception) {
|
||||
if (exception.getReplyCode() == 535) {
|
||||
// Authentication credentials invalid
|
||||
throw new AuthenticationFailedException("AUTH PLAIN failed ("
|
||||
+ exception.getMessage() + ")");
|
||||
} else {
|
||||
throw exception;
|
||||
}
|
||||
throw me;
|
||||
}
|
||||
}
|
||||
|
||||
@ -717,9 +757,20 @@ public class SmtpTransport extends Transport {
|
||||
try {
|
||||
executeSimpleCommand(b64CRAMString, true);
|
||||
} catch (NegativeSmtpReplyException exception) {
|
||||
if (exception.getReplyCode() == 535) {
|
||||
// Authentication credentials invalid
|
||||
throw new AuthenticationFailedException(exception.getMessage(), exception);
|
||||
} else {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saslAuthExternal(String username) throws MessagingException, IOException {
|
||||
executeSimpleCommand(
|
||||
String.format("AUTH EXTERNAL %s",
|
||||
Utility.base64Encode(username)), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception that is thrown when the server sends a negative reply (reply codes 4xx or 5xx).
|
||||
|
@ -21,6 +21,8 @@ public interface ImapSettings {
|
||||
|
||||
String getPassword();
|
||||
|
||||
String getClientCertificateAlias();
|
||||
|
||||
boolean useCompression(int type);
|
||||
|
||||
String getPathPrefix();
|
||||
|
216
src/com/fsck/k9/net/ssl/KeyChainKeyManager.java
Normal 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;
|
||||
}
|
||||
}
|
81
src/com/fsck/k9/net/ssl/SslHelper.java
Normal 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);
|
||||
}
|
||||
}
|
@ -61,6 +61,7 @@ public class SettingsExporter {
|
||||
public static final String CONNECTION_SECURITY_ELEMENT = "connection-security";
|
||||
public static final String AUTHENTICATION_TYPE_ELEMENT = "authentication-type";
|
||||
public static final String USERNAME_ELEMENT = "username";
|
||||
public static final String CLIENT_CERTIFICATE_ALIAS_ELEMENT = "client-cert-alias";
|
||||
public static final String PASSWORD_ELEMENT = "password";
|
||||
public static final String EXTRA_ELEMENT = "extra";
|
||||
public static final String IDENTITIES_ELEMENT = "identities";
|
||||
@ -229,9 +230,14 @@ public class SettingsExporter {
|
||||
if (incoming.port != -1) {
|
||||
writeElement(serializer, PORT_ELEMENT, Integer.toString(incoming.port));
|
||||
}
|
||||
if (incoming.connectionSecurity != null) {
|
||||
writeElement(serializer, CONNECTION_SECURITY_ELEMENT, incoming.connectionSecurity.name());
|
||||
}
|
||||
if (incoming.authenticationType != null) {
|
||||
writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType.name());
|
||||
}
|
||||
writeElement(serializer, USERNAME_ELEMENT, incoming.username);
|
||||
writeElement(serializer, CLIENT_CERTIFICATE_ALIAS_ELEMENT, incoming.clientCertificateAlias);
|
||||
// XXX For now we don't export the password
|
||||
//writeElement(serializer, PASSWORD_ELEMENT, incoming.password);
|
||||
|
||||
@ -256,9 +262,14 @@ public class SettingsExporter {
|
||||
if (outgoing.port != -1) {
|
||||
writeElement(serializer, PORT_ELEMENT, Integer.toString(outgoing.port));
|
||||
}
|
||||
if (outgoing.connectionSecurity != null) {
|
||||
writeElement(serializer, CONNECTION_SECURITY_ELEMENT, outgoing.connectionSecurity.name());
|
||||
}
|
||||
if (outgoing.authenticationType != null) {
|
||||
writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType.name());
|
||||
}
|
||||
writeElement(serializer, USERNAME_ELEMENT, outgoing.username);
|
||||
writeElement(serializer, CLIENT_CERTIFICATE_ALIAS_ELEMENT, outgoing.clientCertificateAlias);
|
||||
// XXX For now we don't export the password
|
||||
//writeElement(serializer, PASSWORD_ELEMENT, outgoing.password);
|
||||
|
||||
|
@ -379,9 +379,10 @@ public class SettingsImporter {
|
||||
String storeUri = Store.createStoreUri(incoming);
|
||||
putString(editor, accountKeyPrefix + Account.STORE_URI_KEY, Utility.base64Encode(storeUri));
|
||||
|
||||
// Mark account as disabled if the settings file didn't contain a password
|
||||
boolean createAccountDisabled = (incoming.password == null ||
|
||||
incoming.password.isEmpty());
|
||||
// Mark account as disabled if the AuthType isn't EXTERNAL and the
|
||||
// settings file didn't contain a password
|
||||
boolean createAccountDisabled = AuthType.EXTERNAL != incoming.authenticationType &&
|
||||
(incoming.password == null || incoming.password.isEmpty());
|
||||
|
||||
if (account.outgoing == null && !WebDavStore.STORE_TYPE.equals(account.incoming.type)) {
|
||||
// All account types except WebDAV need to provide outgoing server settings
|
||||
@ -394,10 +395,19 @@ public class SettingsImporter {
|
||||
String transportUri = Transport.createTransportUri(outgoing);
|
||||
putString(editor, accountKeyPrefix + Account.TRANSPORT_URI_KEY, Utility.base64Encode(transportUri));
|
||||
|
||||
// Mark account as disabled if the settings file didn't contain a password
|
||||
if (outgoing.password == null || outgoing.password.isEmpty()) {
|
||||
createAccountDisabled = true;
|
||||
}
|
||||
/*
|
||||
* Mark account as disabled if the settings file contained a
|
||||
* username but no password. However, no password is required for
|
||||
* the outgoing server for WebDAV accounts, because incoming and
|
||||
* outgoing servers are identical for this account type. Nor is a
|
||||
* password required if the AuthType is EXTERNAL.
|
||||
*/
|
||||
boolean outgoingPasswordNeeded = AuthType.EXTERNAL != outgoing.authenticationType &&
|
||||
!WebDavStore.STORE_TYPE.equals(outgoing.type) &&
|
||||
outgoing.username != null &&
|
||||
!outgoing.username.isEmpty() &&
|
||||
(outgoing.password == null || outgoing.password.isEmpty());
|
||||
createAccountDisabled = outgoingPasswordNeeded || createAccountDisabled;
|
||||
}
|
||||
|
||||
// Write key to mark account as disabled if necessary
|
||||
@ -976,6 +986,8 @@ public class SettingsImporter {
|
||||
server.authenticationType = AuthType.valueOf(text);
|
||||
} else if (SettingsExporter.USERNAME_ELEMENT.equals(element)) {
|
||||
server.username = getText(xpp);
|
||||
} else if (SettingsExporter.CLIENT_CERTIFICATE_ALIAS_ELEMENT.equals(element)) {
|
||||
server.clientCertificateAlias = getText(xpp);
|
||||
} else if (SettingsExporter.PASSWORD_ELEMENT.equals(element)) {
|
||||
server.password = getText(xpp);
|
||||
} else if (SettingsExporter.EXTRA_ELEMENT.equals(element)) {
|
||||
@ -1090,7 +1102,8 @@ public class SettingsImporter {
|
||||
public ImportedServerSettings(ImportedServer server) {
|
||||
super(server.type, server.host, convertPort(server.port),
|
||||
convertConnectionSecurity(server.connectionSecurity),
|
||||
server.authenticationType, server.username, server.password);
|
||||
server.authenticationType, server.username, server.password,
|
||||
server.clientCertificateAlias);
|
||||
mImportedServer = server;
|
||||
}
|
||||
|
||||
@ -1155,6 +1168,7 @@ public class SettingsImporter {
|
||||
public AuthType authenticationType;
|
||||
public String username;
|
||||
public String password;
|
||||
public String clientCertificateAlias;
|
||||
public ImportedSettings extras;
|
||||
}
|
||||
|
||||
|
121
src/com/fsck/k9/view/ClientCertificateSpinner.java
Normal file
@ -0,0 +1,121 @@
|
||||
|
||||
package com.fsck.k9.view;
|
||||
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.security.KeyChain;
|
||||
import android.security.KeyChainAliasCallback;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
public class ClientCertificateSpinner extends LinearLayout {
|
||||
Activity mActivity;
|
||||
OnClientCertificateChangedListener mListener;
|
||||
|
||||
Button mSelection;
|
||||
ImageButton mDeleteButton;
|
||||
|
||||
String mAlias;
|
||||
|
||||
public interface OnClientCertificateChangedListener {
|
||||
void onClientCertificateChanged(String alias);
|
||||
}
|
||||
|
||||
public void setOnClientCertificateChangedListener(OnClientCertificateChangedListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public ClientCertificateSpinner(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
if (context instanceof Activity) {
|
||||
mActivity = (Activity) context;
|
||||
} else {
|
||||
Log.e(K9.LOG_TAG, "ClientCertificateSpinner init failed! Please inflate with Activity!");
|
||||
}
|
||||
|
||||
setOrientation(LinearLayout.HORIZONTAL);
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater.inflate(R.layout.client_certificate_spinner, this, true);
|
||||
|
||||
mSelection = (Button) 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
253
src/com/fsck/k9/view/FoldableLinearLayout.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -82,7 +82,7 @@ public class ImapStoreUriTest extends TestCase {
|
||||
extra.put("pathPrefix", "customPathPrefix");
|
||||
|
||||
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
|
||||
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", extra);
|
||||
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null, extra);
|
||||
|
||||
String uri = Store.createStoreUri(settings);
|
||||
|
||||
@ -95,7 +95,7 @@ public class ImapStoreUriTest extends TestCase {
|
||||
extra.put("pathPrefix", "");
|
||||
|
||||
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
|
||||
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", extra);
|
||||
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null, extra);
|
||||
|
||||
String uri = Store.createStoreUri(settings);
|
||||
|
||||
@ -104,7 +104,7 @@ public class ImapStoreUriTest extends TestCase {
|
||||
|
||||
public void testCreateStoreUriImapNoExtra() {
|
||||
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
|
||||
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass");
|
||||
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null);
|
||||
|
||||
String uri = Store.createStoreUri(settings);
|
||||
|
||||
@ -116,7 +116,7 @@ public class ImapStoreUriTest extends TestCase {
|
||||
extra.put("autoDetectNamespace", "true");
|
||||
|
||||
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
|
||||
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", extra);
|
||||
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null, extra);
|
||||
|
||||
String uri = Store.createStoreUri(settings);
|
||||
|
||||
|