diff --git a/images/drawables-pgp/bullet_point_negative.svg b/images/drawables-pgp/bullet_point_negative.svg new file mode 100644 index 000000000..bb185efbf --- /dev/null +++ b/images/drawables-pgp/bullet_point_negative.svg @@ -0,0 +1,66 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/images/drawables-pgp/bullet_point_positive.svg b/images/drawables-pgp/bullet_point_positive.svg new file mode 100644 index 000000000..9c7c22537 --- /dev/null +++ b/images/drawables-pgp/bullet_point_positive.svg @@ -0,0 +1,65 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/images/drawables-pgp/compatibility.svg b/images/drawables-pgp/compatibility.svg new file mode 100644 index 000000000..cf4ec96cf --- /dev/null +++ b/images/drawables-pgp/compatibility.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/drawables-pgp/docs/disabled.svg b/images/drawables-pgp/docs/disabled.svg new file mode 100644 index 000000000..e378be987 --- /dev/null +++ b/images/drawables-pgp/docs/disabled.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + lock-closed + + + + + lock-closed + Created with Sketch. + + + + + + + + diff --git a/images/drawables-pgp/docs/signcrypt_confirmed.svg b/images/drawables-pgp/docs/signcrypt_confirmed.svg new file mode 100644 index 000000000..1af6d2781 --- /dev/null +++ b/images/drawables-pgp/docs/signcrypt_confirmed.svg @@ -0,0 +1,67 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/images/drawables-pgp/docs/signcrypt_error.svg b/images/drawables-pgp/docs/signcrypt_error.svg new file mode 100644 index 000000000..7ab2982de --- /dev/null +++ b/images/drawables-pgp/docs/signcrypt_error.svg @@ -0,0 +1,91 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/images/drawables-pgp/docs/signcrypt_unconfirmed.svg b/images/drawables-pgp/docs/signcrypt_unconfirmed.svg new file mode 100644 index 000000000..dc6be7769 --- /dev/null +++ b/images/drawables-pgp/docs/signcrypt_unconfirmed.svg @@ -0,0 +1,67 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/images/drawables-pgp/docs/signcrypt_unknown.svg b/images/drawables-pgp/docs/signcrypt_unknown.svg new file mode 100644 index 000000000..34592ecae --- /dev/null +++ b/images/drawables-pgp/docs/signcrypt_unknown.svg @@ -0,0 +1,80 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/images/update-drawables-pgp.sh b/images/update-drawables-pgp.sh index 4e3cdbded..4c92ef316 100755 --- a/images/update-drawables-pgp.sh +++ b/images/update-drawables-pgp.sh @@ -9,7 +9,7 @@ XXXDPI_DIR=$APP_DIR/res/drawable-xxxhdpi SRC_DIR=./drawables-pgp/ -for NAME in "status_lock" "status_lock_closed" "status_lock_error" "status_lock_open" "status_lock_disabled" "status_lock_opportunistic" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" +for NAME in "bullet_point_positive" "bullet_point_negative" "compatibility" "status_lock" "status_lock_closed" "status_lock_error" "status_lock_open" "status_lock_disabled" "status_lock_opportunistic" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" do echo $NAME inkscape -w 24 -h 24 -e "$MDPI_DIR/$NAME.png" "$SRC_DIR/$NAME.svg" diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Flag.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Flag.java index 058bcb7a5..0ade76386 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Flag.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Flag.java @@ -57,4 +57,9 @@ public enum Flag { * TODO Messages with this flag should be redownloaded, if possible. */ X_MIGRATED_FROM_V50, + + /** + * This flag is used for drafts where the message should be sent as PGP/INLINE. + */ + X_DRAFT_OPENPGP_INLINE, } diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java index 3313f7378..56f834393 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java @@ -32,10 +32,15 @@ public class MimeMessageHelper { setEncoding(part, MimeUtil.ENC_8BIT); } } else if (body instanceof TextBody) { - String contentType = String.format("%s;\r\n charset=utf-8", part.getMimeType()); - String name = MimeUtility.getHeaderParameter(part.getContentType(), "name"); - if (name != null) { - contentType += String.format(";\r\n name=\"%s\"", name); + String contentType; + if (MimeUtility.mimeTypeMatches(part.getMimeType(), "text/*")) { + contentType = String.format("%s;\r\n charset=utf-8", part.getMimeType()); + String name = MimeUtility.getHeaderParameter(part.getContentType(), "name"); + if (name != null) { + contentType += String.format(";\r\n name=\"%s\"", name); + } + } else { + contentType = part.getMimeType(); } part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java index c9a69921e..dbfd4d736 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java @@ -132,4 +132,8 @@ public class TextBody implements Body, SizeAware { return countingOutputStream.getCount(); } + + public String getEncoding() { + return mEncoding; + } } diff --git a/k9mail/build.gradle b/k9mail/build.gradle index 9eb08093c..92f7dc6c1 100644 --- a/k9mail/build.gradle +++ b/k9mail/build.gradle @@ -30,6 +30,7 @@ dependencies { compile 'com.github.bumptech.glide:glide:3.6.1' compile 'com.splitwise:tokenautocomplete:2.0.7' compile 'de.cketti.safecontentresolver:safe-content-resolver-v14:0.0.1' + compile 'com.github.amlcurran.showcaseview:library:5.4.1' androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' diff --git a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java index 83231f647..cdc563ee8 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java @@ -56,10 +56,12 @@ import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.R; import com.fsck.k9.activity.compose.ComposeCryptoStatus; +import com.fsck.k9.activity.compose.ComposeCryptoStatus.AttachErrorState; import com.fsck.k9.activity.compose.ComposeCryptoStatus.SendErrorState; import com.fsck.k9.activity.compose.CryptoSettingsDialog.OnCryptoModeChangedListener; import com.fsck.k9.activity.compose.IdentityAdapter; import com.fsck.k9.activity.compose.IdentityAdapter.IdentityContainer; +import com.fsck.k9.activity.compose.PgpInlineDialog.OnOpenPgpInlineChangeListener; import com.fsck.k9.activity.compose.RecipientMvpView; import com.fsck.k9.activity.compose.RecipientPresenter; import com.fsck.k9.activity.compose.RecipientPresenter.CryptoMode; @@ -86,6 +88,7 @@ import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.LocalBodyPart; import com.fsck.k9.mailstore.LocalMessage; +import com.fsck.k9.message.ComposePgpInlineDecider; import com.fsck.k9.message.IdentityField; import com.fsck.k9.message.IdentityHeaderParser; import com.fsck.k9.message.MessageBuilder; @@ -101,7 +104,8 @@ import com.fsck.k9.ui.compose.QuotedMessagePresenter; @SuppressWarnings("deprecation") public class MessageCompose extends K9Activity implements OnClickListener, - CancelListener, OnFocusChangeListener, OnCryptoModeChangedListener, MessageBuilder.Callback { + CancelListener, OnFocusChangeListener, OnCryptoModeChangedListener, + OnOpenPgpInlineChangeListener, MessageBuilder.Callback { private static final int DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE = 1; private static final int DIALOG_CONFIRM_DISCARD_ON_BACK = 2; @@ -215,6 +219,11 @@ public class MessageCompose extends K9Activity implements OnClickListener, recipientPresenter.onCryptoModeChanged(cryptoMode); } + @Override + public void onOpenPgpInlineChange(boolean enabled) { + recipientPresenter.onCryptoPgpInlineChanged(enabled); + } + public enum Action { COMPOSE(R.string.compose_title_compose), REPLY(R.string.compose_title_reply), @@ -333,8 +342,6 @@ public class MessageCompose extends K9Activity implements OnClickListener, private FontSizes mFontSizes = K9.getFontSizes(); - - @Override public void onCreate(Bundle savedInstanceState) { @@ -392,7 +399,8 @@ public class MessageCompose extends K9Activity implements OnClickListener, mChooseIdentityButton.setOnClickListener(this); RecipientMvpView recipientMvpView = new RecipientMvpView(this); - recipientPresenter = new RecipientPresenter(this, recipientMvpView, mAccount); + ComposePgpInlineDecider composePgpInlineDecider = new ComposePgpInlineDecider(); + recipientPresenter = new RecipientPresenter(this, recipientMvpView, mAccount, composePgpInlineDecider); mSubjectView = (EditText) findViewById(R.id.subject); mSubjectView.getInputExtras(true).putBoolean("allowEmoji", true); @@ -808,7 +816,8 @@ public class MessageCompose extends K9Activity implements OnClickListener, .setSignatureChanged(mSignatureChanged) .setCursorPosition(mMessageContentView.getSelectionStart()) .setMessageReference(mMessageReference) - .setDraft(isDraft); + .setDraft(isDraft) + .setIsPgpInlineEnabled(cryptoStatus.isPgpInlineModeEnabled()); quotedMessagePresenter.builderSetProperties(builder); @@ -928,6 +937,12 @@ public class MessageCompose extends K9Activity implements OnClickListener, */ @SuppressLint("InlinedApi") private void onAddAttachment() { + AttachErrorState maybeAttachErrorState = recipientPresenter.getCurrentCryptoStatus().getAttachErrorStateOrNull(); + if (maybeAttachErrorState != null) { + recipientPresenter.showPgpAttachError(maybeAttachErrorState); + return; + } + Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); i.addCategory(Intent.CATEGORY_OPENABLE); @@ -1286,6 +1301,12 @@ public class MessageCompose extends K9Activity implements OnClickListener, case R.id.add_from_contacts: recipientPresenter.onMenuAddFromContacts(); break; + case R.id.openpgp_inline_enable: + recipientPresenter.onMenuSetPgpInline(true); + break; + case R.id.openpgp_inline_disable: + recipientPresenter.onMenuSetPgpInline(false); + break; case R.id.add_attachment: onAddAttachment(); break; diff --git a/k9mail/src/main/java/com/fsck/k9/activity/compose/ComposeCryptoStatus.java b/k9mail/src/main/java/com/fsck/k9/activity/compose/ComposeCryptoStatus.java index dbcfe2123..82d6e086c 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/compose/ComposeCryptoStatus.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/compose/ComposeCryptoStatus.java @@ -25,6 +25,7 @@ public class ComposeCryptoStatus { private Long signingKeyId; private Long selfEncryptKeyId; private String[] recipientAddresses; + private boolean enablePgpInline; public long[] getEncryptKeyIds() { @@ -55,7 +56,7 @@ public class ComposeCryptoStatus { // provider status is ok -> return value is based on cryptoMode break; default: - throw new AssertionError("all CryptoProviderStates must be handled, this is a bug!"); + throw new AssertionError("all CryptoProviderStates must be handled!"); } switch (cryptoMode) { @@ -82,7 +83,7 @@ public class ComposeCryptoStatus { case DISABLE: return CryptoStatusDisplayType.DISABLED; default: - throw new AssertionError("all CryptoModes must be handled, this is a bug!"); + throw new AssertionError("all CryptoModes must be handled!"); } } @@ -102,6 +103,17 @@ public class ComposeCryptoStatus { return cryptoMode != CryptoMode.DISABLE && signingKeyId != null; } + public boolean isPgpInlineModeEnabled() { + return enablePgpInline; + } + + public boolean isCryptoDisabled() { + return cryptoMode == CryptoMode.DISABLE; + } + + public boolean isProviderStateOk() { + return cryptoProviderState == CryptoProviderState.OK; + } public static class ComposeCryptoStatusBuilder { @@ -110,6 +122,7 @@ public class ComposeCryptoStatus { private Long signingKeyId; private Long selfEncryptKeyId; private List recipients; + private Boolean enablePgpInline; public ComposeCryptoStatusBuilder setCryptoProviderState(CryptoProviderState cryptoProviderState) { this.cryptoProviderState = cryptoProviderState; @@ -136,15 +149,23 @@ public class ComposeCryptoStatus { return this; } + public ComposeCryptoStatusBuilder setEnablePgpInline(boolean cryptoEnableCompat) { + this.enablePgpInline = cryptoEnableCompat; + return this; + } + public ComposeCryptoStatus build() { if (cryptoProviderState == null) { - throw new AssertionError("cryptoProviderState must be set. this is a bug!"); + throw new AssertionError("cryptoProviderState must be set!"); } if (cryptoMode == null) { - throw new AssertionError("crypto mode must be set. this is a bug!"); + throw new AssertionError("crypto mode must be set!"); } if (recipients == null) { - throw new AssertionError("recipients must be set. this is a bug!"); + throw new AssertionError("recipients must be set!"); + } + if (enablePgpInline == null) { + throw new AssertionError("enablePgpInline must be set!"); } ArrayList recipientAddresses = new ArrayList<>(); @@ -172,6 +193,7 @@ public class ComposeCryptoStatus { result.hasRecipients = hasRecipients; result.signingKeyId = signingKeyId; result.selfEncryptKeyId = selfEncryptKeyId; + result.enablePgpInline = enablePgpInline; return result; } } @@ -197,4 +219,20 @@ public class ComposeCryptoStatus { return null; } + public enum AttachErrorState { + IS_INLINE + } + + public AttachErrorState getAttachErrorStateOrNull() { + if (cryptoProviderState == CryptoProviderState.UNCONFIGURED) { + return null; + } + + if (enablePgpInline) { + return AttachErrorState.IS_INLINE; + } + + return null; + } + } diff --git a/k9mail/src/main/java/com/fsck/k9/activity/compose/PgpInlineDialog.java b/k9mail/src/main/java/com/fsck/k9/activity/compose/PgpInlineDialog.java new file mode 100644 index 000000000..370131cb0 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/activity/compose/PgpInlineDialog.java @@ -0,0 +1,79 @@ +package com.fsck.k9.activity.compose; + + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.Bundle; +import android.support.annotation.IdRes; +import android.view.LayoutInflater; +import android.view.View; + +import com.fsck.k9.R; +import com.fsck.k9.view.HighlightDialogFragment; + + +public class PgpInlineDialog extends HighlightDialogFragment { + public static final String ARG_FIRST_TIME = "first_time"; + + + public static PgpInlineDialog newInstance(boolean firstTime, @IdRes int showcaseView) { + PgpInlineDialog dialog = new PgpInlineDialog(); + + Bundle args = new Bundle(); + args.putInt(ARG_FIRST_TIME, firstTime ? 1 : 0); + args.putInt(ARG_HIGHLIGHT_VIEW, showcaseView); + dialog.setArguments(args); + + return dialog; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Activity activity = getActivity(); + + @SuppressLint("InflateParams") + View view = LayoutInflater.from(activity).inflate(R.layout.openpgp_inline_dialog, null); + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setView(view); + + if (getArguments().getInt(ARG_FIRST_TIME) != 0) { + builder.setPositiveButton(R.string.openpgp_inline_ok, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + } else { + builder.setPositiveButton(R.string.openpgp_inline_disable, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + ((OnOpenPgpInlineChangeListener) activity).onOpenPgpInlineChange(false); + dialog.dismiss(); + } + }); + builder.setNegativeButton(R.string.openpgp_inline_keep_enabled, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + } + + return builder.create(); + } + + public interface OnOpenPgpInlineChangeListener { + void onOpenPgpInlineChange(boolean enabled); + } + +} diff --git a/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientMvpView.java b/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientMvpView.java index b695fb213..151ecfb73 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientMvpView.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientMvpView.java @@ -47,6 +47,7 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener private final RecipientSelectView bccView; private final ViewAnimator cryptoStatusView; private final ViewAnimator recipientExpanderContainer; + private final View pgpInlineIndicator; private RecipientPresenter presenter; @@ -63,6 +64,7 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener recipientExpanderContainer = (ViewAnimator) activity.findViewById(R.id.recipient_expander_container); cryptoStatusView = (ViewAnimator) activity.findViewById(R.id.crypto_status); cryptoStatusView.setOnClickListener(this); + pgpInlineIndicator = activity.findViewById(R.id.pgp_inline_indicator); toView.setOnFocusChangeListener(this); ccView.setOnFocusChangeListener(this); @@ -77,6 +79,8 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener toLabel.setOnClickListener(this); ccLabel.setOnClickListener(this); bccLabel.setOnClickListener(this); + + pgpInlineIndicator.setOnClickListener(this); } public void setPresenter(final RecipientPresenter presenter) { @@ -265,6 +269,11 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener bccView.setError(bccView.getContext().getString(R.string.compose_error_incomplete_recipient)); } + public void showPgpInlineModeIndicator(boolean pgpInlineModeEnabled) { + pgpInlineIndicator.setVisibility(pgpInlineModeEnabled ? View.VISIBLE : View.GONE); + activity.invalidateOptionsMenu(); + } + public void showCryptoStatus(final CryptoStatusDisplayType cryptoStatusDisplayType) { boolean shouldBeHidden = cryptoStatusDisplayType.childToDisplay == VIEW_INDEX_HIDDEN; if (shouldBeHidden) { @@ -301,6 +310,10 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener Toast.makeText(activity, R.string.compose_error_private_missing_keys, Toast.LENGTH_LONG).show(); } + public void showErrorAttachInline() { + Toast.makeText(activity, R.string.error_crypto_inline_attach, Toast.LENGTH_LONG).show(); + } + @Override public void onFocusChange(View view, boolean hasFocus) { if (!hasFocus) { @@ -346,6 +359,9 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener presenter.onClickCryptoStatus(); break; } + case R.id.pgp_inline_indicator: { + presenter.onClickPgpInlineIndicator(); + } } } @@ -354,6 +370,11 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener dialog.show(activity.getFragmentManager(), "crypto_settings"); } + public void showOpenPgpInlineDialog(boolean firstTime) { + PgpInlineDialog dialog = PgpInlineDialog.newInstance(firstTime, R.id.pgp_inline_indicator); + dialog.show(activity.getFragmentManager(), "openpgp_inline"); + } + public void launchUserInteractionPendingIntent(PendingIntent pendingIntent, int requestCode) { activity.launchUserInteractionPendingIntent(pendingIntent, requestCode); } diff --git a/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.java b/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.java index ed7f2023a..52c66e1e3 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.java @@ -21,6 +21,7 @@ import com.fsck.k9.Account; import com.fsck.k9.Identity; import com.fsck.k9.K9; import com.fsck.k9.R; +import com.fsck.k9.activity.compose.ComposeCryptoStatus.AttachErrorState; import com.fsck.k9.activity.compose.ComposeCryptoStatus.ComposeCryptoStatusBuilder; import com.fsck.k9.activity.compose.ComposeCryptoStatus.SendErrorState; import com.fsck.k9.helper.Contacts; @@ -28,11 +29,13 @@ import com.fsck.k9.helper.MailTo; import com.fsck.k9.helper.ReplyToParser; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.message.PgpMessageBuilder; +import com.fsck.k9.message.ComposePgpInlineDecider; import com.fsck.k9.view.RecipientSelectView.Recipient; import org.openintents.openpgp.IOpenPgpService2; import org.openintents.openpgp.util.OpenPgpApi; @@ -45,7 +48,8 @@ public class RecipientPresenter implements PermissionPingCallback { private static final String STATE_KEY_CC_SHOWN = "state:ccShown"; private static final String STATE_KEY_BCC_SHOWN = "state:bccShown"; private static final String STATE_KEY_LAST_FOCUSED_TYPE = "state:lastFocusedType"; - private static final String STATE_KEY_CURRENT_CRYPTO_MODE = "key:initialOrFormerCryptoMode"; + private static final String STATE_KEY_CURRENT_CRYPTO_MODE = "state:currentCryptoMode"; + private static final String STATE_KEY_CRYPTO_ENABLE_PGP_INLINE = "state:cryptoEnablePgpInline"; private static final int CONTACT_PICKER_TO = 1; private static final int CONTACT_PICKER_CC = 2; @@ -56,6 +60,7 @@ public class RecipientPresenter implements PermissionPingCallback { // transient state, which is either obtained during construction and initialization, or cached private final Context context; private final RecipientMvpView recipientMvpView; + private final ComposePgpInlineDecider composePgpInlineDecider; private Account account; private String cryptoProvider; private Boolean hasContactPicker; @@ -69,11 +74,14 @@ public class RecipientPresenter implements PermissionPingCallback { private RecipientType lastFocusedType = RecipientType.TO; // TODO initialize cryptoMode to other values under some circumstances, e.g. if we reply to an encrypted e-mail private CryptoMode currentCryptoMode = CryptoMode.OPPORTUNISTIC; + private boolean cryptoEnablePgpInline = false; - public RecipientPresenter(Context context, RecipientMvpView recipientMvpView, Account account) { + public RecipientPresenter(Context context, RecipientMvpView recipientMvpView, Account account, + ComposePgpInlineDecider composePgpInlineDecider) { this.recipientMvpView = recipientMvpView; this.context = context; + this.composePgpInlineDecider = composePgpInlineDecider; recipientMvpView.setPresenter(this); onSwitchAccount(account); @@ -161,6 +169,12 @@ public class RecipientPresenter implements PermissionPingCallback { } } + + boolean shouldSendAsPgpInline = composePgpInlineDecider.shouldReplyInline(message); + if (shouldSendAsPgpInline) { + cryptoEnablePgpInline = true; + } + } catch (MessagingException e) { // can't happen, we know the recipient types exist throw new AssertionError(e); @@ -196,6 +210,7 @@ public class RecipientPresenter implements PermissionPingCallback { recipientMvpView.setBccVisibility(savedInstanceState.getBoolean(STATE_KEY_BCC_SHOWN)); lastFocusedType = RecipientType.valueOf(savedInstanceState.getString(STATE_KEY_LAST_FOCUSED_TYPE)); currentCryptoMode = CryptoMode.valueOf(savedInstanceState.getString(STATE_KEY_CURRENT_CRYPTO_MODE)); + cryptoEnablePgpInline = savedInstanceState.getBoolean(STATE_KEY_CRYPTO_ENABLE_PGP_INLINE); updateRecipientExpanderVisibility(); } @@ -204,9 +219,15 @@ public class RecipientPresenter implements PermissionPingCallback { outState.putBoolean(STATE_KEY_BCC_SHOWN, recipientMvpView.isBccVisible()); outState.putString(STATE_KEY_LAST_FOCUSED_TYPE, lastFocusedType.toString()); outState.putString(STATE_KEY_CURRENT_CRYPTO_MODE, currentCryptoMode.toString()); + outState.putBoolean(STATE_KEY_CRYPTO_ENABLE_PGP_INLINE, cryptoEnablePgpInline); } public void initFromDraftMessage(LocalMessage message) { + initRecipientsFromDraftMessage(message); + initPgpInlineFromDraftMessage(message); + } + + private void initRecipientsFromDraftMessage(LocalMessage message) { try { addToAddresses(message.getRecipients(RecipientType.TO)); @@ -221,6 +242,10 @@ public class RecipientPresenter implements PermissionPingCallback { } } + private void initPgpInlineFromDraftMessage(LocalMessage message) { + cryptoEnablePgpInline = message.isSet(Flag.X_DRAFT_OPENPGP_INLINE); + } + void addToAddresses(Address... toAddresses) { addRecipientsFromAddresses(RecipientType.TO, toAddresses); } @@ -248,6 +273,10 @@ public class RecipientPresenter implements PermissionPingCallback { } public void onPrepareOptionsMenu(Menu menu) { + boolean isCryptoConfigured = cryptoProviderState != CryptoProviderState.UNCONFIGURED; + menu.findItem(R.id.openpgp_inline_enable).setVisible(isCryptoConfigured && !cryptoEnablePgpInline); + menu.findItem(R.id.openpgp_inline_disable).setVisible(isCryptoConfigured && cryptoEnablePgpInline); + boolean noContactPickerAvailable = !hasContactPicker(); if (noContactPickerAvailable) { menu.findItem(R.id.add_from_contacts).setVisible(false); @@ -345,6 +374,7 @@ public class RecipientPresenter implements PermissionPingCallback { } recipientMvpView.showCryptoStatus(getCurrentCryptoStatus().getCryptoStatusDisplayType()); + recipientMvpView.showPgpInlineModeIndicator(getCurrentCryptoStatus().isPgpInlineModeEnabled()); } public ComposeCryptoStatus getCurrentCryptoStatus() { @@ -352,6 +382,7 @@ public class RecipientPresenter implements PermissionPingCallback { ComposeCryptoStatusBuilder builder = new ComposeCryptoStatusBuilder() .setCryptoProviderState(cryptoProviderState) .setCryptoMode(currentCryptoMode) + .setEnablePgpInline(cryptoEnablePgpInline) .setRecipients(getAllRecipients()); long accountCryptoKey = account.getCryptoKey(); @@ -427,6 +458,11 @@ public class RecipientPresenter implements PermissionPingCallback { updateCryptoStatus(); } + public void onCryptoPgpInlineChanged(boolean enablePgpInline) { + cryptoEnablePgpInline = enablePgpInline; + updateCryptoStatus(); + } + private void addRecipientsFromAddresses(final RecipientType recipientType, final Address... addresses) { new RecipientLoader(context, cryptoProvider, addresses) { @Override @@ -578,6 +614,16 @@ public class RecipientPresenter implements PermissionPingCallback { } } + public void showPgpAttachError(AttachErrorState attachErrorState) { + switch (attachErrorState) { + case IS_INLINE: + recipientMvpView.showErrorAttachInline(); + break; + default: + throw new AssertionError("not all error states handled, this is a bug!"); + } + } + private void setCryptoProvider(String cryptoProvider) { boolean providerIsBound = openPgpServiceConnection != null && openPgpServiceConnection.isBound(); @@ -683,11 +729,24 @@ public class RecipientPresenter implements PermissionPingCallback { return new OpenPgpApi(context, openPgpServiceConnection.getService()); } + public void builderSetProperties(PgpMessageBuilder pgpBuilder) { pgpBuilder.setOpenPgpApi(getOpenPgpApi()); pgpBuilder.setCryptoStatus(getCurrentCryptoStatus()); } + public void onMenuSetPgpInline(boolean enablePgpInline) { + cryptoEnablePgpInline = enablePgpInline; + updateCryptoStatus(); + if (enablePgpInline) { + recipientMvpView.showOpenPgpInlineDialog(true); + } + } + + public void onClickPgpInlineIndicator() { + recipientMvpView.showOpenPgpInlineDialog(false); + } + public enum CryptoProviderState { UNCONFIGURED, UNINITIALIZED, diff --git a/k9mail/src/main/java/com/fsck/k9/message/ComposePgpInlineDecider.java b/k9mail/src/main/java/com/fsck/k9/message/ComposePgpInlineDecider.java new file mode 100644 index 000000000..cef058c41 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/message/ComposePgpInlineDecider.java @@ -0,0 +1,21 @@ +package com.fsck.k9.message; + + +import java.util.List; + +import com.fsck.k9.crypto.MessageDecryptVerifier; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.Part; + + +public class ComposePgpInlineDecider { + public boolean shouldReplyInline(Message localMessage) { + // TODO more criteria for this? maybe check the User-Agent header? + return messageHasPgpInlineParts(localMessage); + } + + private boolean messageHasPgpInlineParts(Message localMessage) { + List inlineParts = MessageDecryptVerifier.findPgpInlineParts(localMessage); + return !inlineParts.isEmpty(); + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/message/MessageBuilder.java b/k9mail/src/main/java/com/fsck/k9/message/MessageBuilder.java index 293aaf8e3..8af7e2fe7 100644 --- a/k9mail/src/main/java/com/fsck/k9/message/MessageBuilder.java +++ b/k9mail/src/main/java/com/fsck/k9/message/MessageBuilder.java @@ -20,6 +20,7 @@ import com.fsck.k9.activity.MessageReference; import com.fsck.k9.activity.misc.Attachment; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.MimeBodyPart; @@ -61,6 +62,7 @@ public abstract class MessageBuilder { private int cursorPosition; private MessageReference messageReference; private boolean isDraft; + private boolean isPgpInlineEnabled; public MessageBuilder(Context context) { this.context = context; @@ -70,7 +72,7 @@ public abstract class MessageBuilder { * Build the message to be sent (or saved). If there is another message quoted in this one, it will be baked * into the message here. */ - public MimeMessage build() throws MessagingException { + protected MimeMessage build() throws MessagingException { //FIXME: check arguments MimeMessage message = new MimeMessage(); @@ -114,6 +116,10 @@ public abstract class MessageBuilder { } message.generateMessageId(); + + if (isDraft && isPgpInlineEnabled) { + message.setFlag(Flag.X_DRAFT_OPENPGP_INLINE, true); + } } private void buildBody(MimeMessage message) throws MessagingException { @@ -435,6 +441,11 @@ public abstract class MessageBuilder { return this; } + public MessageBuilder setIsPgpInlineEnabled(boolean isPgpInlineEnabled) { + this.isPgpInlineEnabled = isPgpInlineEnabled; + return this; + } + public boolean isDraft() { return isDraft; } diff --git a/k9mail/src/main/java/com/fsck/k9/message/PgpMessageBuilder.java b/k9mail/src/main/java/com/fsck/k9/message/PgpMessageBuilder.java index 67e1b8a5d..7e8ba5698 100644 --- a/k9mail/src/main/java/com/fsck/k9/message/PgpMessageBuilder.java +++ b/k9mail/src/main/java/com/fsck/k9/message/PgpMessageBuilder.java @@ -2,11 +2,13 @@ package com.fsck.k9.message; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -21,8 +23,10 @@ import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mailstore.BinaryMemoryBody; +import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.util.MimeUtil; import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.util.OpenPgpApi; @@ -31,13 +35,14 @@ import org.openintents.openpgp.util.OpenPgpApi.OpenPgpDataSource; public class PgpMessageBuilder extends MessageBuilder { - public static final int REQUEST_SIGN_INTERACTION = 1; - public static final int REQUEST_ENCRYPT_INTERACTION = 2; + public static final int REQUEST_USER_INTERACTION = 1; private OpenPgpApi openPgpApi; private MimeMessage currentProcessedMimeMessage; private ComposeCryptoStatus cryptoStatus; + private boolean opportunisticSkipEncryption; + private boolean opportunisticSecondPass; public PgpMessageBuilder(Context context) { super(context); @@ -47,73 +52,70 @@ public class PgpMessageBuilder extends MessageBuilder { this.openPgpApi = openPgpApi; } - /** This class keeps track of its internal state explicitly. */ - private enum State { - IDLE, START, FAILURE, - OPENPGP_SIGN, OPENPGP_SIGN_UI, OPENPGP_SIGN_OK, - OPENPGP_ENCRYPT, OPENPGP_ENCRYPT_UI, OPENPGP_ENCRYPT_OK; - - public boolean isBreakState() { - return this == OPENPGP_SIGN_UI || this == OPENPGP_ENCRYPT_UI || this == FAILURE; - } - - public boolean isReentrantState() { - return this == OPENPGP_SIGN || this == OPENPGP_ENCRYPT; - } - - public boolean isSignOk() { - return this == OPENPGP_SIGN_OK || this == OPENPGP_ENCRYPT - || this == OPENPGP_ENCRYPT_UI || this == OPENPGP_ENCRYPT_OK; - } - } - - State currentState = State.IDLE; - @Override protected void buildMessageInternal() { - if (currentState != State.IDLE) { - throw new IllegalStateException("internal error, a PgpMessageBuilder can only be built once!"); + if (currentProcessedMimeMessage != null) { + throw new IllegalStateException("message can only be built once!"); + } + if (cryptoStatus == null) { + throw new IllegalStateException("PgpMessageBuilder must have cryptoStatus set before building!"); + } + if (cryptoStatus.isCryptoDisabled()) { + throw new AssertionError("PgpMessageBuilder must not be used if crypto is disabled!"); } try { + if (!cryptoStatus.isProviderStateOk()) { + throw new MessagingException("OpenPGP Provider is not ready!"); + } + currentProcessedMimeMessage = build(); } catch (MessagingException me) { queueMessageBuildException(me); return; } - currentState = State.START; startOrContinueBuildMessage(null); } @Override - public void buildMessageOnActivityResult(int requestCode, Intent userInteractionResult) { - if (requestCode == REQUEST_SIGN_INTERACTION && currentState == State.OPENPGP_SIGN_UI) { - currentState = State.OPENPGP_SIGN; - startOrContinueBuildMessage(userInteractionResult); - } else if (requestCode == REQUEST_ENCRYPT_INTERACTION && currentState == State.OPENPGP_ENCRYPT_UI) { - currentState = State.OPENPGP_ENCRYPT; - startOrContinueBuildMessage(userInteractionResult); - } else { - throw new IllegalStateException("illegal state!"); + public void buildMessageOnActivityResult(int requestCode, @NonNull Intent userInteractionResult) { + if (currentProcessedMimeMessage == null) { + throw new AssertionError("build message from activity result must not be called individually"); } + startOrContinueBuildMessage(userInteractionResult); } - private void startOrContinueBuildMessage(@Nullable Intent userInteractionResult) { - if (currentState != State.START && !currentState.isReentrantState()) { - throw new IllegalStateException("bad state!"); - } - + private void startOrContinueBuildMessage(@Nullable Intent pgpApiIntent) { try { - startOrContinueSigningIfRequested(userInteractionResult); + boolean shouldSign = cryptoStatus.isSigningEnabled(); + boolean shouldEncrypt = cryptoStatus.isEncryptionEnabled() && !opportunisticSkipEncryption; + boolean isPgpInlineMode = cryptoStatus.isPgpInlineModeEnabled(); - if (currentState.isBreakState()) { + if (!shouldSign && !shouldEncrypt) { return; } - startOrContinueEncryptionIfRequested(userInteractionResult); + boolean isSimpleTextMessage = + MimeUtility.isSameMimeType("text/plain", currentProcessedMimeMessage.getMimeType()); + if (isPgpInlineMode && !isSimpleTextMessage) { + throw new MessagingException("Attachments are not supported in PGP/INLINE format!"); + } - if (currentState.isBreakState()) { + if (pgpApiIntent == null) { + pgpApiIntent = buildOpenPgpApiIntent(shouldSign, shouldEncrypt, isPgpInlineMode); + } + + PendingIntent returnedPendingIntent = launchOpenPgpApiIntent( + pgpApiIntent, shouldEncrypt || isPgpInlineMode, shouldEncrypt || !isPgpInlineMode, isPgpInlineMode); + if (returnedPendingIntent != null) { + queueMessageBuildPendingIntent(returnedPendingIntent, REQUEST_USER_INTERACTION); + return; + } + + if (opportunisticSkipEncryption && !opportunisticSecondPass) { + opportunisticSecondPass = true; + startOrContinueBuildMessage(null); return; } @@ -123,97 +125,63 @@ public class PgpMessageBuilder extends MessageBuilder { } } - private void startOrContinueEncryptionIfRequested(Intent userInteractionResult) throws MessagingException { - boolean reenterOperation = currentState == State.OPENPGP_ENCRYPT; - if (reenterOperation) { - mimeIntentLaunch(userInteractionResult); - return; - } - - if (!cryptoStatus.isEncryptionEnabled()) { - return; - } - - Intent encryptIntent = new Intent(OpenPgpApi.ACTION_ENCRYPT); - encryptIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - - long[] encryptKeyIds = cryptoStatus.getEncryptKeyIds(); - if (encryptKeyIds != null) { - encryptIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, encryptKeyIds); - } - - if(!isDraft()) { - String[] encryptRecipientAddresses = cryptoStatus.getRecipientAddresses(); - boolean hasRecipientAddresses = encryptRecipientAddresses != null && encryptRecipientAddresses.length > 0; - if (!hasRecipientAddresses) { - // TODO safeguard here once this is better handled by the caller? - // throw new MessagingException("Encryption is enabled, but no encryption key specified!"); - return; + @NonNull + private Intent buildOpenPgpApiIntent(boolean shouldSign, boolean shouldEncrypt, boolean isPgpInlineMode) + throws MessagingException { + Intent pgpApiIntent; + if (shouldEncrypt) { + if (!shouldSign) { + throw new IllegalStateException("encrypt-only is not supported at this point and should never happen!"); } - encryptIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, encryptRecipientAddresses); - encryptIntent.putExtra(OpenPgpApi.EXTRA_ENCRYPT_OPPORTUNISTIC, cryptoStatus.isEncryptionOpportunistic()); + // pgpApiIntent = new Intent(shouldSign ? OpenPgpApi.ACTION_SIGN_AND_ENCRYPT : OpenPgpApi.ACTION_ENCRYPT); + pgpApiIntent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT); + + long[] encryptKeyIds = cryptoStatus.getEncryptKeyIds(); + if (encryptKeyIds != null) { + pgpApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, encryptKeyIds); + } + + if(!isDraft()) { + String[] encryptRecipientAddresses = cryptoStatus.getRecipientAddresses(); + boolean hasRecipientAddresses = encryptRecipientAddresses != null && encryptRecipientAddresses.length > 0; + if (!hasRecipientAddresses) { + throw new MessagingException("encryption is enabled, but no recipient specified!"); + } + pgpApiIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, encryptRecipientAddresses); + pgpApiIntent.putExtra(OpenPgpApi.EXTRA_ENCRYPT_OPPORTUNISTIC, cryptoStatus.isEncryptionOpportunistic()); + } + } else { + pgpApiIntent = new Intent(isPgpInlineMode ? OpenPgpApi.ACTION_SIGN : OpenPgpApi.ACTION_DETACHED_SIGN); } - currentState = State.OPENPGP_ENCRYPT; - mimeIntentLaunch(encryptIntent); + if (shouldSign) { + pgpApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, cryptoStatus.getSigningKeyId()); + } + + pgpApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + return pgpApiIntent; } - private void startOrContinueSigningIfRequested(Intent userInteractionResult) throws MessagingException { - boolean reenterOperation = currentState == State.OPENPGP_SIGN; - if (reenterOperation) { - mimeIntentLaunch(userInteractionResult); - return; - } - - boolean signingDisabled = !cryptoStatus.isSigningEnabled(); - boolean alreadySigned = currentState.isSignOk(); - boolean isDraft = isDraft(); - if (signingDisabled || alreadySigned || isDraft) { - return; - } - - Intent signIntent = new Intent(OpenPgpApi.ACTION_DETACHED_SIGN); - signIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, cryptoStatus.getSigningKeyId()); - - currentState = State.OPENPGP_SIGN; - mimeIntentLaunch(signIntent); - } - - /** This method executes the given Intent with the OpenPGP Api. It will pass the - * entire current message as input. On success, either mimeBuildSignedMessage() or - * mimeBuildEncryptedMessage() will be called with their appropriate inputs. If an - * error or PendingInput is returned, this will be passed as a result to the - * operation. - */ - private void mimeIntentLaunch(Intent openPgpIntent) throws MessagingException { + private PendingIntent launchOpenPgpApiIntent(@NonNull Intent openPgpIntent, + boolean captureOutputPart, boolean capturedOutputPartIs7Bit, boolean writeBodyContentOnly) throws MessagingException { final MimeBodyPart bodyPart = currentProcessedMimeMessage.toBodyPart(); - String[] contentType = currentProcessedMimeMessage.getHeader(MimeHeader.HEADER_CONTENT_TYPE); if (contentType.length > 0) { bodyPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType[0]); } bodyPart.setUsing7bitTransport(); - // This data will be read in a worker thread - OpenPgpDataSource dataSource = new OpenPgpDataSource() { - @Override - public void writeTo(OutputStream os) throws IOException { - try { - bodyPart.writeTo(os); - } catch (MessagingException e) { - throw new IOException(e); - } - } - }; + OpenPgpDataSource dataSource = createOpenPgpDataSourceFromBodyPart(bodyPart, writeBodyContentOnly); - BinaryTempFileBody encryptedTempBody = null; + BinaryTempFileBody pgpResultTempBody = null; OutputStream outputStream = null; - if (currentState == State.OPENPGP_ENCRYPT) { + if (captureOutputPart) { try { - encryptedTempBody = new BinaryTempFileBody(MimeUtil.ENC_7BIT); - outputStream = encryptedTempBody.getOutputStream(); + pgpResultTempBody = new BinaryTempFileBody( + capturedOutputPartIs7Bit ? MimeUtil.ENC_7BIT : MimeUtil.ENC_8BIT); + outputStream = pgpResultTempBody.getOutputStream(); } catch (IOException e) { - throw new MessagingException("Could not allocate temp file for storage!", e); + throw new MessagingException("could not allocate temp file for storage!", e); } } @@ -221,56 +189,91 @@ public class PgpMessageBuilder extends MessageBuilder { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: - if (currentState == State.OPENPGP_SIGN) { - mimeBuildSignedMessage(bodyPart, result); - } else if (currentState == State.OPENPGP_ENCRYPT) { - mimeBuildEncryptedMessage(encryptedTempBody, result); - } else { - throw new IllegalStateException("state error!"); - } - return; + mimeBuildMessage(result, bodyPart, pgpResultTempBody); + return null; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - launchUserInteraction(result); - return; + PendingIntent returnedPendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); + if (returnedPendingIntent == null) { + throw new MessagingException("openpgp api needs user interaction, but returned no pendingintent!"); + } + return returnedPendingIntent; case OpenPgpApi.RESULT_CODE_ERROR: OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); + if (error == null) { + throw new MessagingException("internal openpgp api error"); + } boolean isOpportunisticError = error.getErrorId() == OpenPgpError.OPPORTUNISTIC_MISSING_KEYS; if (isOpportunisticError) { skipEncryptingMessage(); - return; + return null; } throw new MessagingException(error.getMessage()); - - default: - throw new IllegalStateException("unreachable code segment reached - this is a bug"); } + + throw new IllegalStateException("unreachable code segment reached"); } - private void launchUserInteraction(Intent result) { - PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); - - if (currentState == State.OPENPGP_ENCRYPT) { - currentState = State.OPENPGP_ENCRYPT_UI; - queueMessageBuildPendingIntent(pendingIntent, REQUEST_ENCRYPT_INTERACTION); - } else if (currentState == State.OPENPGP_SIGN) { - currentState = State.OPENPGP_SIGN_UI; - queueMessageBuildPendingIntent(pendingIntent, REQUEST_SIGN_INTERACTION); - } else { - throw new IllegalStateException("illegal state!"); - } + @NonNull + private OpenPgpDataSource createOpenPgpDataSourceFromBodyPart(final MimeBodyPart bodyPart, + final boolean writeBodyContentOnly) + throws MessagingException { + return new OpenPgpDataSource() { + @Override + public void writeTo(OutputStream os) throws IOException { + try { + if (writeBodyContentOnly) { + Body body = bodyPart.getBody(); + InputStream inputStream = body.getInputStream(); + IOUtils.copy(inputStream, os); + } else { + bodyPart.writeTo(os); + } + } catch (MessagingException e) { + throw new IOException(e); + } + } + }; } - private void mimeBuildSignedMessage(BodyPart signedBodyPart, Intent result) throws MessagingException { + private void mimeBuildMessage( + @NonNull Intent result, @NonNull MimeBodyPart bodyPart, @Nullable BinaryTempFileBody pgpResultTempBody) + throws MessagingException { + if (pgpResultTempBody == null) { + boolean shouldHaveResultPart = cryptoStatus.isPgpInlineModeEnabled() || + (cryptoStatus.isEncryptionEnabled() && !opportunisticSkipEncryption); + if (shouldHaveResultPart) { + throw new AssertionError("encryption or pgp/inline is enabled, but no output part!"); + } + + mimeBuildSignedMessage(bodyPart, result); + return; + } + + if (cryptoStatus.isPgpInlineModeEnabled()) { + mimeBuildInlineMessage(pgpResultTempBody); + return; + } + + mimeBuildEncryptedMessage(pgpResultTempBody); + } + + private void mimeBuildSignedMessage(@NonNull BodyPart signedBodyPart, Intent result) throws MessagingException { + if (!cryptoStatus.isSigningEnabled()) { + throw new IllegalStateException("call to mimeBuildSignedMessage while signing isn't enabled!"); + } + byte[] signedData = result.getByteArrayExtra(OpenPgpApi.RESULT_DETACHED_SIGNATURE); + if (signedData == null) { + throw new MessagingException("didn't find expected RESULT_DETACHED_SIGNATURE in api call result"); + } MimeMultipart multipartSigned = new MimeMultipart(); multipartSigned.setSubType("signed"); multipartSigned.addBodyPart(signedBodyPart); multipartSigned.addBodyPart( new MimeBodyPart(new BinaryMemoryBody(signedData, MimeUtil.ENC_7BIT), "application/pgp-signature")); - MimeMessageHelper.setBody(currentProcessedMimeMessage, multipartSigned); String contentType = String.format( @@ -283,113 +286,46 @@ public class PgpMessageBuilder extends MessageBuilder { Log.e(K9.LOG_TAG, "missing micalg parameter for pgp multipart/signed!"); } currentProcessedMimeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); - - currentState = State.OPENPGP_SIGN_OK; } - @SuppressWarnings("UnusedParameters") - private void mimeBuildEncryptedMessage(Body encryptedBodyPart, Intent result) throws MessagingException { + private void mimeBuildEncryptedMessage(@NonNull Body encryptedBodyPart) throws MessagingException { + if (!cryptoStatus.isEncryptionEnabled()) { + throw new IllegalStateException("call to mimeBuildEncryptedMessage while encryption isn't enabled!"); + } + MimeMultipart multipartEncrypted = new MimeMultipart(); multipartEncrypted.setSubType("encrypted"); - multipartEncrypted.addBodyPart(new MimeBodyPart(new TextBody("Version: 1"), "application/pgp-encrypted")); multipartEncrypted.addBodyPart(new MimeBodyPart(encryptedBodyPart, "application/octet-stream")); MimeMessageHelper.setBody(currentProcessedMimeMessage, multipartEncrypted); + String contentType = String.format( "multipart/encrypted; boundary=\"%s\";\r\n protocol=\"application/pgp-encrypted\"", multipartEncrypted.getBoundary()); currentProcessedMimeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); + } - currentState = State.OPENPGP_ENCRYPT_OK; + private void mimeBuildInlineMessage(@NonNull Body inlineBodyPart) throws MessagingException { + if (!cryptoStatus.isPgpInlineModeEnabled()) { + throw new IllegalStateException("call to mimeBuildInlineMessage while pgp/inline isn't enabled!"); + } + + boolean isCleartextSignature = !cryptoStatus.isEncryptionEnabled(); + if (isCleartextSignature) { + inlineBodyPart.setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE); + } + MimeMessageHelper.setBody(currentProcessedMimeMessage, inlineBodyPart); } private void skipEncryptingMessage() throws MessagingException { if (!cryptoStatus.isEncryptionOpportunistic()) { throw new AssertionError("Got opportunistic error, but encryption wasn't supposed to be opportunistic!"); } - currentState = State.OPENPGP_ENCRYPT_OK; + opportunisticSkipEncryption = true; } public void setCryptoStatus(ComposeCryptoStatus cryptoStatus) { this.cryptoStatus = cryptoStatus; } - /* TODO re-add PGP/INLINE - if (isCryptoProviderEnabled() && ! mAccount.isUsePgpMime()) { - // OpenPGP Provider API - - // If not already encrypted but user wants to encrypt... - if (mPgpData.getEncryptedData() == null && - (mEncryptCheckbox.isChecked() || mCryptoSignatureCheckbox.isChecked())) { - - String[] emailsArray = null; - if (mEncryptCheckbox.isChecked()) { - // get emails as array - List emails = new ArrayList(); - - for (Address address : recipientPresenter.getAllRecipientAddresses()) { - emails.add(address.getAddress()); - } - emailsArray = emails.toArray(new String[emails.size()]); - } - if (mEncryptCheckbox.isChecked() && mCryptoSignatureCheckbox.isChecked()) { - Intent intent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT); - intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, emailsArray); - intent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, mAccount.getCryptoKey()); - executeOpenPgpMethod(intent); - } else if (mCryptoSignatureCheckbox.isChecked()) { - Intent intent = new Intent(OpenPgpApi.ACTION_SIGN); - intent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, mAccount.getCryptoKey()); - executeOpenPgpMethod(intent); - } else if (mEncryptCheckbox.isChecked()) { - Intent intent = new Intent(OpenPgpApi.ACTION_ENCRYPT); - intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, emailsArray); - executeOpenPgpMethod(intent); - } - - // onSend() is called again in SignEncryptCallback and with - // encryptedData set in pgpData! - return; - } - } - */ - - /* TODO re-add attach public key - private Attachment attachedPublicKey() throws OpenPgpApiException { - try { - Attachment publicKey = new Attachment(); - publicKey.contentType = "application/pgp-keys"; - - String keyName = "0x" + Long.toString(mAccount.getCryptoKey(), 16).substring(8); - publicKey.name = keyName + ".asc"; - Intent intent = new Intent(OpenPgpApi.ACTION_GET_KEY); - intent.putExtra(OpenPgpApi.EXTRA_KEY_ID, mAccount.getCryptoKey()); - intent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - OpenPgpApi api = new OpenPgpApi(this, mOpenPgpServiceConnection.getService()); - File keyTempFile = File.createTempFile("key", ".asc", getCacheDir()); - keyTempFile.deleteOnExit(); - try { - CountingOutputStream keyFileStream = new CountingOutputStream(new BufferedOutputStream( - new FileOutputStream(keyTempFile))); - Intent res = api.executeApi(intent, null, new EOLConvertingOutputStream(keyFileStream)); - if (res.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR) != OpenPgpApi.RESULT_CODE_SUCCESS - || keyFileStream.getByteCount() == 0) { - keyTempFile.delete(); - throw new OpenPgpApiException(String.format(getString(R.string.openpgp_no_public_key_returned), - getString(R.string.btn_attach_key))); - } - publicKey.filename = keyTempFile.getAbsolutePath(); - publicKey.state = Attachment.LoadingState.COMPLETE; - publicKey.size = keyFileStream.getByteCount(); - return publicKey; - } catch(RuntimeException e){ - keyTempFile.delete(); - throw e; - } - } catch(IOException e){ - throw new RuntimeException(getString(R.string.error_cant_create_temporary_file), e); - } - } - */ - } diff --git a/k9mail/src/main/java/com/fsck/k9/message/TextBodyBuilder.java b/k9mail/src/main/java/com/fsck/k9/message/TextBodyBuilder.java index 0aec41258..a90b4d2ad 100644 --- a/k9mail/src/main/java/com/fsck/k9/message/TextBodyBuilder.java +++ b/k9mail/src/main/java/com/fsck/k9/message/TextBodyBuilder.java @@ -1,13 +1,15 @@ package com.fsck.k9.message; + import android.text.TextUtils; import android.util.Log; import com.fsck.k9.K9; -import com.fsck.k9.mail.Body; import com.fsck.k9.helper.HtmlConverter; +import com.fsck.k9.mail.Body; import com.fsck.k9.mail.internet.TextBody; + class TextBodyBuilder { private boolean mIncludeQuotedText = true; private boolean mReplyAfterQuote = false; diff --git a/k9mail/src/main/java/com/fsck/k9/view/HighlightDialogFragment.java b/k9mail/src/main/java/com/fsck/k9/view/HighlightDialogFragment.java new file mode 100644 index 000000000..c09789524 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/view/HighlightDialogFragment.java @@ -0,0 +1,98 @@ +package com.fsck.k9.view; + + +import android.app.Activity; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import com.fsck.k9.R; +import com.github.amlcurran.showcaseview.ShowcaseView; +import com.github.amlcurran.showcaseview.ShowcaseView.Builder; +import com.github.amlcurran.showcaseview.targets.ViewTarget; + + +public class HighlightDialogFragment extends DialogFragment { + public static final String ARG_HIGHLIGHT_VIEW = "highlighted_view"; + public static final float BACKGROUND_DIM_AMOUNT = 0.25f; + + + private ShowcaseView showcaseView; + + + protected void highlightViewInBackground() { + if (!getArguments().containsKey(ARG_HIGHLIGHT_VIEW)) { + return; + } + + Activity activity = getActivity(); + if (activity == null) { + throw new IllegalStateException("fragment must be attached to set highlight!"); + } + + boolean alreadyShowing = showcaseView != null && showcaseView.isShowing(); + if (alreadyShowing) { + return; + } + + int highlightedView = getArguments().getInt(ARG_HIGHLIGHT_VIEW); + showcaseView = new Builder(activity) + .setTarget(new ViewTarget(highlightedView, activity)) + .hideOnTouchOutside() + .blockAllTouches() + .withMaterialShowcase() + .setStyle(R.style.ShowcaseTheme) + .build(); + showcaseView.hideButton(); + } + + @Override + public void onStart() { + super.onStart(); + + hideKeyboard(); + highlightViewInBackground(); + setDialogBackgroundDim(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + + hideShowcaseView(); + } + + private void setDialogBackgroundDim() { + Dialog dialog = getDialog(); + if (dialog == null) { + return; + } + dialog.getWindow().setDimAmount(BACKGROUND_DIM_AMOUNT); + } + + private void hideKeyboard() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + // check if no view has focus + View v = activity.getCurrentFocus(); + if (v == null) { + return; + } + + InputMethodManager inputManager = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + + private void hideShowcaseView() { + if (showcaseView != null && showcaseView.isShowing()) { + showcaseView.hide(); + } + showcaseView = null; + } +} diff --git a/k9mail/src/main/res/drawable-hdpi/bullet_point_negative.png b/k9mail/src/main/res/drawable-hdpi/bullet_point_negative.png new file mode 100644 index 000000000..bd8eb7b66 Binary files /dev/null and b/k9mail/src/main/res/drawable-hdpi/bullet_point_negative.png differ diff --git a/k9mail/src/main/res/drawable-hdpi/bullet_point_positive.png b/k9mail/src/main/res/drawable-hdpi/bullet_point_positive.png new file mode 100644 index 000000000..28ac58099 Binary files /dev/null and b/k9mail/src/main/res/drawable-hdpi/bullet_point_positive.png differ diff --git a/k9mail/src/main/res/drawable-hdpi/compatibility.png b/k9mail/src/main/res/drawable-hdpi/compatibility.png new file mode 100644 index 000000000..6b6552349 Binary files /dev/null and b/k9mail/src/main/res/drawable-hdpi/compatibility.png differ diff --git a/k9mail/src/main/res/drawable-mdpi/bullet_point_negative.png b/k9mail/src/main/res/drawable-mdpi/bullet_point_negative.png new file mode 100644 index 000000000..bc6861657 Binary files /dev/null and b/k9mail/src/main/res/drawable-mdpi/bullet_point_negative.png differ diff --git a/k9mail/src/main/res/drawable-mdpi/bullet_point_positive.png b/k9mail/src/main/res/drawable-mdpi/bullet_point_positive.png new file mode 100644 index 000000000..c65c0b254 Binary files /dev/null and b/k9mail/src/main/res/drawable-mdpi/bullet_point_positive.png differ diff --git a/k9mail/src/main/res/drawable-mdpi/compatibility.png b/k9mail/src/main/res/drawable-mdpi/compatibility.png new file mode 100644 index 000000000..bed9fd7a8 Binary files /dev/null and b/k9mail/src/main/res/drawable-mdpi/compatibility.png differ diff --git a/k9mail/src/main/res/drawable-xhdpi/bullet_point_negative.png b/k9mail/src/main/res/drawable-xhdpi/bullet_point_negative.png new file mode 100644 index 000000000..651beffa6 Binary files /dev/null and b/k9mail/src/main/res/drawable-xhdpi/bullet_point_negative.png differ diff --git a/k9mail/src/main/res/drawable-xhdpi/bullet_point_positive.png b/k9mail/src/main/res/drawable-xhdpi/bullet_point_positive.png new file mode 100644 index 000000000..f164b378e Binary files /dev/null and b/k9mail/src/main/res/drawable-xhdpi/bullet_point_positive.png differ diff --git a/k9mail/src/main/res/drawable-xhdpi/compatibility.png b/k9mail/src/main/res/drawable-xhdpi/compatibility.png new file mode 100644 index 000000000..669dabf6c Binary files /dev/null and b/k9mail/src/main/res/drawable-xhdpi/compatibility.png differ diff --git a/k9mail/src/main/res/drawable-xxhdpi/bullet_point_negative.png b/k9mail/src/main/res/drawable-xxhdpi/bullet_point_negative.png new file mode 100644 index 000000000..089bf972a Binary files /dev/null and b/k9mail/src/main/res/drawable-xxhdpi/bullet_point_negative.png differ diff --git a/k9mail/src/main/res/drawable-xxhdpi/bullet_point_positive.png b/k9mail/src/main/res/drawable-xxhdpi/bullet_point_positive.png new file mode 100644 index 000000000..0e3f48f9a Binary files /dev/null and b/k9mail/src/main/res/drawable-xxhdpi/bullet_point_positive.png differ diff --git a/k9mail/src/main/res/drawable-xxhdpi/compatibility.png b/k9mail/src/main/res/drawable-xxhdpi/compatibility.png new file mode 100644 index 000000000..97bb639fe Binary files /dev/null and b/k9mail/src/main/res/drawable-xxhdpi/compatibility.png differ diff --git a/k9mail/src/main/res/layout/message_compose_recipients.xml b/k9mail/src/main/res/layout/message_compose_recipients.xml index 816a3ef31..7eeb7426a 100644 --- a/k9mail/src/main/res/layout/message_compose_recipients.xml +++ b/k9mail/src/main/res/layout/message_compose_recipients.xml @@ -10,54 +10,66 @@ android:orientation="vertical" tools:showIn="@layout/message_compose"> - + android:minHeight="50dp" + android:animateLayoutChanges="true"> + style="@style/ComposeTextLabel" + /> + + + + custom:previewInitialChild="2"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k9mail/src/main/res/menu/message_compose_option.xml b/k9mail/src/main/res/menu/message_compose_option.xml index 17c53fb55..2bf329781 100644 --- a/k9mail/src/main/res/menu/message_compose_option.xml +++ b/k9mail/src/main/res/menu/message_compose_option.xml @@ -37,4 +37,16 @@ android:title="@string/read_receipt" android:icon="?attr/iconActionRequestReadReceipt" /> + + diff --git a/k9mail/src/main/res/values/attrs.xml b/k9mail/src/main/res/values/attrs.xml index 809b6e3bc..a3c80e71f 100644 --- a/k9mail/src/main/res/values/attrs.xml +++ b/k9mail/src/main/res/values/attrs.xml @@ -54,6 +54,8 @@ + + diff --git a/k9mail/src/main/res/values/colors.xml b/k9mail/src/main/res/values/colors.xml index edf4449d6..1cf9d27fa 100644 --- a/k9mail/src/main/res/values/colors.xml +++ b/k9mail/src/main/res/values/colors.xml @@ -1,7 +1,9 @@ #eeeeee - + + #444 + #CC0000 #FF8800 #669900 diff --git a/k9mail/src/main/res/values/strings.xml b/k9mail/src/main/res/values/strings.xml index 65df32a9e..024cf0734 100644 --- a/k9mail/src/main/res/values/strings.xml +++ b/k9mail/src/main/res/values/strings.xml @@ -1152,5 +1152,16 @@ Please submit bug reports, contribute new features and ask questions at Always Sign, Always Encrypt. Cannot connect to crypto provider, check your settings or click crypto icon to retry! Crypto provider access denied, click crypto icon to retry! + PGP/INLINE mode does not support attachments! + Enable PGP/INLINE + Disable PGP/INLINE + PGP/INLINE Mode + The email is sent in PGP/INLINE format.\nThis should only be used for compatibility: + Some clients only support this format + Signatures may break during transit + Attachments are not supported + Got it! + Disable + Keep Enabled diff --git a/k9mail/src/main/res/values/styles.xml b/k9mail/src/main/res/values/styles.xml index b4235380c..65445e859 100644 --- a/k9mail/src/main/res/values/styles.xml +++ b/k9mail/src/main/res/values/styles.xml @@ -51,5 +51,19 @@ #aaa + + + + + + diff --git a/k9mail/src/main/res/values/themes.xml b/k9mail/src/main/res/values/themes.xml index a7b186b4a..bd2b93379 100644 --- a/k9mail/src/main/res/values/themes.xml +++ b/k9mail/src/main/res/values/themes.xml @@ -61,6 +61,8 @@ #ffababab #ccc @android:color/background_light + #77aa22 + #dd2222