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