diff --git a/FAQ.md b/FAQ.md index 47394532ca..3e1b2b43f1 100644 --- a/FAQ.md +++ b/FAQ.md @@ -86,7 +86,7 @@ Related questions: * ~~Unified starred messages view~~ (there is already a special search for this) * ~~Notification move action~~ * Search for settings: low priority -* Signed-only messages: waiting for sponsoring +* Signed-only messages * S/MIME: waiting for sponsoring Anything on this list is in random order and *might* be added in the near future. diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index 756e09d4ce..ff1a218c75 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -266,6 +266,7 @@ public class AdapterMessage extends RecyclerView.Adapter 0 ? View.VISIBLE : View.GONE); ivEncrypted.setVisibility(message.encrypted > 0 ? View.VISIBLE : View.GONE); tvFrom.setText(MessageHelper.formatAddresses(addresses, name_email, false)); tvFrom.setPaintFlags(tvFrom.getPaintFlags() & ~Paint.UNDERLINE_TEXT_FLAG); @@ -995,6 +1004,7 @@ public class AdapterMessage extends RecyclerView.Adapter 0)" + " ORDER BY received DESC" + " LIMIT :limit OFFSET :offset") List matchMessages( @@ -284,6 +287,7 @@ public interface DaoMessage { ", CASE WHEN message.ui_seen THEN 0 ELSE 1 END AS unseen" + ", CASE WHEN message.ui_flagged THEN 0 ELSE 1 END AS unflagged" + ", (folder.type = '" + EntityFolder.DRAFTS + "') AS drafts" + + ", (message.encrypt = 2) AS signed" + ", (message.encrypt = 1) AS encrypted" + ", 1 AS visible" + ", message.total AS totalSize" + @@ -318,6 +322,7 @@ public interface DaoMessage { ", 1 AS unseen" + ", 0 AS unflagged" + ", 0 AS drafts" + + ", (message.encrypt = 2) AS signed" + ", (message.encrypt = 1) AS encrypted" + ", 1 AS visible" + ", message.total AS totalSize" + @@ -485,7 +490,7 @@ public interface DaoMessage { int setMessagePlainOnly(long id, boolean plain_only); @Query("UPDATE message SET encrypt = :encrypt WHERE id = :id") - int setMessageEncrypt(long id, boolean encrypt); + int setMessageEncrypt(long id, Integer encrypt); @Query("UPDATE message SET last_attempt = :last_attempt WHERE id = :id") int setMessageLastAttempt(long id, long last_attempt); diff --git a/app/src/main/java/eu/faircode/email/EntityAttachment.java b/app/src/main/java/eu/faircode/email/EntityAttachment.java index 83723b03fe..b5d2e822a1 100644 --- a/app/src/main/java/eu/faircode/email/EntityAttachment.java +++ b/app/src/main/java/eu/faircode/email/EntityAttachment.java @@ -59,6 +59,7 @@ public class EntityAttachment { static final Integer PGP_MESSAGE = 1; static final Integer PGP_SIGNATURE = 2; static final Integer PGP_KEY = 3; + static final Integer PGP_CONTENT = 4; // https://developer.android.com/guide/topics/media/media-formats#image-formats private static final List IMAGE_TYPES = Collections.unmodifiableList(Arrays.asList( diff --git a/app/src/main/java/eu/faircode/email/EntityMessage.java b/app/src/main/java/eu/faircode/email/EntityMessage.java index 409e84b0e4..ba4b3ec27d 100644 --- a/app/src/main/java/eu/faircode/email/EntityMessage.java +++ b/app/src/main/java/eu/faircode/email/EntityMessage.java @@ -80,6 +80,10 @@ import static androidx.room.ForeignKey.SET_NULL; public class EntityMessage implements Serializable { static final String TABLE_NAME = "message"; + static final Integer ENCRYPTION_NONE = 0; + static final Integer ENCRYPTION_SIGNENCRYPT = 1; + static final Integer ENCRYPTION_SIGNONLY = 2; + static final Integer PRIORITIY_LOW = 0; static final Integer PRIORITIY_NORMAL = 1; static final Integer PRIORITIY_HIGH = 2; @@ -127,7 +131,7 @@ public class EntityMessage implements Serializable { @NonNull public Boolean content = false; public Boolean plain_only = null; - public Boolean encrypt = null; + public Integer encrypt = null; public String preview; @NonNull public Boolean signature = true; diff --git a/app/src/main/java/eu/faircode/email/EntityOperation.java b/app/src/main/java/eu/faircode/email/EntityOperation.java index a38cb35467..8e29ad766b 100644 --- a/app/src/main/java/eu/faircode/email/EntityOperation.java +++ b/app/src/main/java/eu/faircode/email/EntityOperation.java @@ -121,7 +121,7 @@ public class EntityOperation { for (Object value : values) jargs.put(value); - if (MOVE.equals(name) && message.encrypt != null && message.encrypt) { + if (MOVE.equals(name) && EntityMessage.ENCRYPTION_SIGNENCRYPT.equals(message.encrypt)) { EntityFolder folder = db.folder().getFolder(message.folder); if (folder != null && EntityFolder.DRAFTS.equals(folder.type)) name = DELETE; diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 627d849aaa..9b1347aa86 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -131,13 +131,17 @@ import java.util.Properties; import java.util.regex.Pattern; import javax.mail.Address; +import javax.mail.BodyPart; import javax.mail.MessageRemovedException; +import javax.mail.Multipart; import javax.mail.Part; import javax.mail.Session; import javax.mail.internet.AddressException; import javax.mail.internet.ContentType; import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; import javax.mail.internet.ParseException; import static android.app.Activity.RESULT_CANCELED; @@ -188,7 +192,7 @@ public class FragmentCompose extends FragmentBase { private boolean prefix_once = false; private boolean monospaced = false; - private boolean encrypt = false; + private Integer encrypt = null; private boolean media = true; private boolean compact = false; private int zoom = 0; @@ -205,6 +209,8 @@ public class FragmentCompose extends FragmentBase { private String[] pgpUserIds; private long[] pgpKeyIds; private long pgpSignKeyId; + private String pgpContent; + private String pgpContentType; static final int REDUCED_IMAGE_SIZE = 1440; // pixels static final int REDUCED_IMAGE_QUALITY = 90; // percent @@ -930,14 +936,20 @@ public class FragmentCompose extends FragmentBase { int colorEncrypt = Helper.resolveColor(getContext(), R.attr.colorEncrypt); ImageButton ib = (ImageButton) menu.findItem(R.id.menu_encrypt).getActionView(); ib.setEnabled(!busy); - ib.setImageResource(encrypt ? R.drawable.baseline_lock_24 : R.drawable.baseline_lock_open_24); - ib.setImageTintList(encrypt ? ColorStateList.valueOf(colorEncrypt) : null); + ib.setImageResource(EntityMessage.ENCRYPTION_SIGNENCRYPT.equals(encrypt) + ? R.drawable.baseline_lock_24 : R.drawable.baseline_lock_open_24); + ib.setImageTintList(EntityMessage.ENCRYPTION_SIGNENCRYPT.equals(encrypt) + ? ColorStateList.valueOf(colorEncrypt) : null); menu.findItem(R.id.menu_media).setChecked(media); menu.findItem(R.id.menu_compact).setChecked(compact); - bottom_navigation.getMenu().findItem(R.id.action_send) - .setTitle(encrypt ? R.string.title_encrypt : R.string.title_send); + if (EntityMessage.ENCRYPTION_SIGNONLY.equals(encrypt)) + bottom_navigation.getMenu().findItem(R.id.action_send).setTitle(R.string.title_sign); + else if (EntityMessage.ENCRYPTION_SIGNENCRYPT.equals(encrypt)) + bottom_navigation.getMenu().findItem(R.id.action_send).setTitle(R.string.title_encrypt); + else + bottom_navigation.getMenu().findItem(R.id.action_send).setTitle(R.string.title_send); } @Override @@ -987,21 +999,25 @@ public class FragmentCompose extends FragmentBase { } private void onMenuEncrypt() { - encrypt = !encrypt; + encrypt = (EntityMessage.ENCRYPTION_SIGNENCRYPT.equals(encrypt) + ? EntityMessage.ENCRYPTION_NONE : EntityMessage.ENCRYPTION_SIGNENCRYPT); getActivity().invalidateOptionsMenu(); Bundle args = new Bundle(); args.putLong("id", working); - args.putBoolean("encrypt", encrypt); + args.putInt("encrypt", encrypt); new SimpleTask() { @Override protected Void onExecute(Context context, Bundle args) { long id = args.getLong("id"); - boolean encrypt = args.getBoolean("encrypt"); + int encrypt = args.getInt("encrypt"); DB db = DB.getInstance(context); - db.message().setMessageEncrypt(id, encrypt); + if (EntityMessage.ENCRYPTION_NONE.equals(encrypt)) + db.message().setMessageEncrypt(id, null); + else + db.message().setMessageEncrypt(id, encrypt); return null; } @@ -1200,9 +1216,17 @@ public class FragmentCompose extends FragmentBase { pgpUserIds[i] = recipient.getAddress().toLowerCase(Locale.ROOT); } - Intent intent = new Intent(OpenPgpApi.ACTION_GET_KEY_IDS); - intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, pgpUserIds); - intent.putExtra(BuildConfig.APPLICATION_ID, working); + Intent intent; + if (EntityMessage.ENCRYPTION_SIGNONLY.equals(draft.encrypt)) { + intent = new Intent(OpenPgpApi.ACTION_GET_SIGN_KEY_ID); + intent.putExtra(BuildConfig.APPLICATION_ID, working); + } else if (EntityMessage.ENCRYPTION_SIGNENCRYPT.equals(draft.encrypt)) { + intent = new Intent(OpenPgpApi.ACTION_GET_KEY_IDS); + intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, pgpUserIds); + intent.putExtra(BuildConfig.APPLICATION_ID, working); + } else + throw new IllegalArgumentException("Invalid encrypt=" + draft.encrypt); + onPgp(intent); } catch (Throwable ex) { if (ex instanceof IllegalArgumentException) @@ -1479,36 +1503,70 @@ public class FragmentCompose extends FragmentBase { DB db = DB.getInstance(context); // Get data - EntityMessage message = db.message().getMessage(id); - if (message == null) - throw new MessageRemovedException(); - EntityIdentity identity = db.identity().getIdentity(message.identity); + EntityMessage draft = db.message().getMessage(id); + if (draft == null) + throw new MessageRemovedException("PGP"); + EntityIdentity identity = db.identity().getIdentity(draft.identity); if (identity == null) throw new IllegalArgumentException(getString(R.string.title_from_missing)); - List attachments = db.attachment().getAttachments(id); - for (EntityAttachment attachment : new ArrayList<>(attachments)) - if (attachment.encryption != null) { - if (OpenPgpApi.ACTION_GET_KEY_IDS.equals(data.getAction())) - db.attachment().deleteAttachment(attachment.id); - attachments.remove(attachment); - } - // Create files File input = new File(context.getCacheDir(), "input." + id); File output = new File(context.getCacheDir(), "output." + id); // Serializing messages is NOT reproducible - if (OpenPgpApi.ACTION_GET_KEY_IDS.equals(data.getAction())) { + if ((EntityMessage.ENCRYPTION_SIGNONLY.equals(draft.encrypt) && + OpenPgpApi.ACTION_GET_SIGN_KEY_ID.equals(data.getAction())) || + (EntityMessage.ENCRYPTION_SIGNENCRYPT.equals(draft.encrypt) && + OpenPgpApi.ACTION_GET_KEY_IDS.equals(data.getAction()))) { + // Get attachments + List attachments = db.attachment().getAttachments(id); + for (EntityAttachment attachment : new ArrayList<>(attachments)) + if (attachment.encryption != null) { + db.attachment().deleteAttachment(attachment.id); + attachments.remove(attachment); + } + // Build message Properties props = MessageHelper.getSessionProperties(); Session isession = Session.getInstance(props, null); MimeMessage imessage = new MimeMessage(isession); - MessageHelper.build(context, message, attachments, identity, imessage); + MessageHelper.build(context, draft, attachments, identity, imessage); + + if (OpenPgpApi.ACTION_GET_SIGN_KEY_ID.equals(data.getAction())) { + // Serialize content + imessage.saveChanges(); + Object content = imessage.getContent(); + if (content instanceof String) { + pgpContent = (String) content; + pgpContentType = imessage.getContentType(); + + // Build plain text part with headers + BodyPart plainPart = new MimeBodyPart(); + plainPart.setContent(pgpContent, pgpContentType); + Multipart plainMultiPart = new MimeMultipart(); + plainMultiPart.addBodyPart(plainPart); + MimeMessage m = new MimeMessage(isession); + m.setContent(plainMultiPart); + m.saveChanges(); + + try (OutputStream out = new FileOutputStream(input)) { + plainPart.writeTo(out); + } + } else if (content instanceof Multipart) { + pgpContent = null; + pgpContentType = ((MimeMultipart) content).getContentType(); - // Serialize message - try (OutputStream out = new FileOutputStream(input)) { - imessage.writeTo(out); + try (OutputStream out = new FileOutputStream(input)) { + ((MimeMultipart) content).writeTo(out); + } + } else + throw new ParseException(content.getClass().getName()); + } else { + // Serialize message + try (OutputStream out = new FileOutputStream(input)) { + imessage.writeTo(out); + } } } @@ -1533,6 +1591,7 @@ public class FragmentCompose extends FragmentBase { db.beginTransaction(); String name; + String type = "application/octet-stream"; int encryption; if (OpenPgpApi.ACTION_GET_KEY.equals(data.getAction())) { name = "keydata.asc"; @@ -1543,6 +1602,8 @@ public class FragmentCompose extends FragmentBase { } else if (OpenPgpApi.ACTION_DETACHED_SIGN.equals(data.getAction())) { name = "signature.asc"; encryption = EntityAttachment.PGP_SIGNATURE; + type = "application/pgp-signature; micalg=\"" + + result.getStringExtra(OpenPgpApi.RESULT_SIGNATURE_MICALG) + "\""; } else throw new IllegalStateException(data.getAction()); @@ -1550,7 +1611,7 @@ public class FragmentCompose extends FragmentBase { attachment.message = id; attachment.sequence = db.attachment().getAttachmentSequence(id) + 1; attachment.name = name; - attachment.type = "application/octet-stream"; + attachment.type = type; attachment.disposition = Part.INLINE; attachment.encryption = encryption; attachment.id = db.attachment().insertAttachment(attachment); @@ -1593,43 +1654,79 @@ public class FragmentCompose extends FragmentBase { if (OpenPgpApi.ACTION_GET_KEY.equals(data.getAction()) || (OpenPgpApi.ACTION_GET_KEY_IDS.equals(data.getAction()) && pgpKeyIds.length > 1)) { - if (identity.sign_key != null) { - // Encrypt message - Intent intent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT); - intent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, pgpKeyIds); - intent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, identity.sign_key); + if (EntityMessage.ENCRYPTION_SIGNONLY.equals(draft.encrypt)) { + // Sign message + Intent intent = new Intent(OpenPgpApi.ACTION_DETACHED_SIGN); + intent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, pgpSignKeyId); intent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); intent.putExtra(BuildConfig.APPLICATION_ID, id); return intent; } else { - // Get sign key - Intent intent = new Intent(OpenPgpApi.ACTION_GET_SIGN_KEY_ID); - intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, pgpUserIds); - intent.putExtra(BuildConfig.APPLICATION_ID, id); - return intent; + if (identity.sign_key != null) { + // Encrypt message + Intent intent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT); + intent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, pgpKeyIds); + intent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, identity.sign_key); + intent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + intent.putExtra(BuildConfig.APPLICATION_ID, id); + return intent; + } else { + // Get sign key + Intent intent = new Intent(OpenPgpApi.ACTION_GET_SIGN_KEY_ID); + intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, pgpUserIds); + intent.putExtra(BuildConfig.APPLICATION_ID, id); + return intent; + } } } else if (OpenPgpApi.ACTION_GET_SIGN_KEY_ID.equals(data.getAction())) { pgpSignKeyId = result.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, -1); db.identity().setIdentitySignKey(identity.id, pgpSignKeyId); - // Encrypt message - Intent intent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT); - intent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, pgpKeyIds); - intent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, pgpSignKeyId); - intent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - intent.putExtra(BuildConfig.APPLICATION_ID, id); - return intent; + if (EntityMessage.ENCRYPTION_SIGNONLY.equals(draft.encrypt)) { + // Get sign key + Intent intent = new Intent(OpenPgpApi.ACTION_GET_KEY); + intent.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpSignKeyId); + intent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + intent.putExtra(BuildConfig.APPLICATION_ID, id); + return intent; + } else if (EntityMessage.ENCRYPTION_SIGNENCRYPT.equals(draft.encrypt)) { + // Encrypt message + Intent intent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT); + intent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, pgpKeyIds); + intent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, pgpSignKeyId); + intent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + intent.putExtra(BuildConfig.APPLICATION_ID, id); + return intent; + } else + throw new IllegalArgumentException("Invalid encrypt=" + draft.encrypt); } else if (OpenPgpApi.ACTION_SIGN_AND_ENCRYPT.equals(data.getAction())) { input.delete(); - // Get signature - //Intent intent = new Intent(OpenPgpApi.ACTION_DETACHED_SIGN); - //intent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, pgpSignKeyId); - //intent.putExtra(BuildConfig.APPLICATION_ID, id); - // send message return null; } else if (OpenPgpApi.ACTION_DETACHED_SIGN.equals(data.getAction())) { + + EntityAttachment attachment = new EntityAttachment(); + attachment.message = id; + attachment.sequence = db.attachment().getAttachmentSequence(id) + 1; + attachment.name = "content.txt"; + attachment.type = pgpContentType; + attachment.disposition = Part.INLINE; + attachment.encryption = EntityAttachment.PGP_CONTENT; + attachment.id = db.attachment().insertAttachment(attachment); + + // Restore plain text without headers + ContentType ct = new ContentType(pgpContentType); + if (!"multipart".equals(ct.getPrimaryType())) + try (OutputStream out = new FileOutputStream(input)) { + out.write(pgpContent.getBytes()); + } + + File file = attachment.getFile(context); + input.renameTo(file); + + db.attachment().setDownloaded(attachment.id, file.length()); + // send message return null; } else @@ -1845,7 +1942,7 @@ public class FragmentCompose extends FragmentBase { } private void onActionSend(EntityMessage draft) { - if (draft.encrypt != null && draft.encrypt) + if (draft.encrypt != null && draft.encrypt != 0) onEncrypt(draft); else onAction(R.id.action_send); @@ -2135,7 +2232,7 @@ public class FragmentCompose extends FragmentBase { if (plain_only) data.draft.plain_only = true; if (encrypt_default) - data.draft.encrypt = true; + data.draft.encrypt = EntityMessage.ENCRYPTION_SIGNENCRYPT; if (receipt_default) data.draft.receipt_request = true; @@ -2371,8 +2468,8 @@ public class FragmentCompose extends FragmentBase { if (ref.plain_only != null && ref.plain_only) data.draft.plain_only = true; - if (ref.encrypt != null && ref.encrypt) - data.draft.encrypt = true; + if (ref.encrypt != null && ref.encrypt != 0) + data.draft.encrypt = ref.encrypt; if (answer > 0) { EntityAnswer a = db.answer().getAnswer(answer); @@ -2558,7 +2655,7 @@ public class FragmentCompose extends FragmentBase { } } - if (data.draft.encrypt == null || !data.draft.encrypt) + if (data.draft.encrypt == null || data.draft.encrypt == 0) EntityOperation.queue(context, data.draft, EntityOperation.ADD); } else { if (data.draft.revision == null) { @@ -2627,7 +2724,7 @@ public class FragmentCompose extends FragmentBase { Log.i("Loaded draft id=" + data.draft.id + " action=" + action); working = data.draft.id; - encrypt = (data.draft.encrypt != null && data.draft.encrypt); + encrypt = data.draft.encrypt; getActivity().invalidateOptionsMenu(); // Show identities @@ -2716,7 +2813,7 @@ public class FragmentCompose extends FragmentBase { if (draft == null || draft.ui_hide) finish(); else { - encrypt = (draft.encrypt != null && draft.encrypt); + encrypt = draft.encrypt; getActivity().invalidateOptionsMenu(); Log.i("Draft content=" + draft.content); @@ -2864,7 +2961,7 @@ public class FragmentCompose extends FragmentBase { draft.ui_hide = ui_hide; db.message().updateMessage(draft); - if (draft.content && (draft.encrypt == null || !draft.encrypt)) + if (draft.content && (draft.encrypt == null || draft.encrypt == 0)) EntityOperation.queue(context, draft, EntityOperation.ADD); } @@ -3040,7 +3137,7 @@ public class FragmentCompose extends FragmentBase { action == R.id.action_redo || action == R.id.action_check) { if (BuildConfig.DEBUG || dirty) - if (draft.encrypt == null || !draft.encrypt) + if (draft.encrypt == null || draft.encrypt == 0) EntityOperation.queue(context, draft, EntityOperation.ADD); if (action == R.id.action_check) { @@ -3602,6 +3699,7 @@ public class FragmentCompose extends FragmentBase { int send_delayed = prefs.getInt("send_delayed", 0); boolean send_dialog = prefs.getBoolean("send_dialog", true); + final int[] encryptValues = getResources().getIntArray(R.array.encryptValues); final int[] sendDelayedValues = getResources().getIntArray(R.array.sendDelayedValues); final String[] sendDelayedNames = getResources().getStringArray(R.array.sendDelayedNames); @@ -3612,9 +3710,9 @@ public class FragmentCompose extends FragmentBase { final TextView tvTo = dview.findViewById(R.id.tvTo); final TextView tvVia = dview.findViewById(R.id.tvVia); final CheckBox cbPlainOnly = dview.findViewById(R.id.cbPlainOnly); - final CheckBox cbEncrypt = dview.findViewById(R.id.cbEncrypt); final CheckBox cbReceipt = dview.findViewById(R.id.cbReceipt); final TextView tvReceipt = dview.findViewById(R.id.tvReceipt); + final Spinner spEncrypt = dview.findViewById(R.id.spEncrypt); final Spinner spPriority = dview.findViewById(R.id.spPriority); final TextView tvSendAt = dview.findViewById(R.id.tvSendAt); final ImageButton ibSendAt = dview.findViewById(R.id.ibSendAt); @@ -3627,6 +3725,8 @@ public class FragmentCompose extends FragmentBase { tvTo.setText(null); tvVia.setText(null); tvReceipt.setVisibility(View.GONE); + spEncrypt.setTag(0); + spEncrypt.setSelection(0); spPriority.setTag(1); spPriority.setSelection(1); tvSendAt.setText(null); @@ -3671,21 +3771,23 @@ public class FragmentCompose extends FragmentBase { } }); - cbEncrypt.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + cbReceipt.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + tvReceipt.setVisibility(checked ? View.VISIBLE : View.GONE); + Bundle args = new Bundle(); args.putLong("id", id); - args.putBoolean("encrypt", checked); + args.putBoolean("receipt", checked); new SimpleTask() { @Override protected Void onExecute(Context context, Bundle args) { long id = args.getLong("id"); - boolean encrypt = args.getBoolean("encrypt"); + boolean receipt = args.getBoolean("receipt"); DB db = DB.getInstance(context); - db.message().setMessageEncrypt(id, encrypt); + db.message().setMessageReceiptRequest(id, receipt); return null; } @@ -3694,27 +3796,39 @@ public class FragmentCompose extends FragmentBase { protected void onException(Bundle args, Throwable ex) { Helper.unexpectedError(getParentFragmentManager(), ex); } - }.execute(FragmentDialogSend.this, args, "compose:encrypt"); + }.execute(FragmentDialogSend.this, args, "compose:receipt"); } }); - cbReceipt.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + spEncrypt.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { - tvReceipt.setVisibility(checked ? View.VISIBLE : View.GONE); + public void onItemSelected(AdapterView parent, View view, int position, long id) { + int last = (int) spEncrypt.getTag(); + if (last != position) { + spEncrypt.setTag(position); + setEncrypt(encryptValues[position]); + } + } + @Override + public void onNothingSelected(AdapterView parent) { + spEncrypt.setTag(0); + setEncrypt(encryptValues[0]); + } + + private void setEncrypt(int encrypt) { Bundle args = new Bundle(); args.putLong("id", id); - args.putBoolean("receipt", checked); + args.putInt("encrypt", encrypt); new SimpleTask() { @Override protected Void onExecute(Context context, Bundle args) { long id = args.getLong("id"); - boolean receipt = args.getBoolean("receipt"); + int encrypt = args.getInt("encrypt"); DB db = DB.getInstance(context); - db.message().setMessageReceiptRequest(id, receipt); + db.message().setMessageEncrypt(id, encrypt); return null; } @@ -3723,7 +3837,7 @@ public class FragmentCompose extends FragmentBase { protected void onException(Bundle args, Throwable ex) { Helper.unexpectedError(getParentFragmentManager(), ex); } - }.execute(FragmentDialogSend.this, args, "compose:receipt"); + }.execute(FragmentDialogSend.this, args, "compose:encrypt"); } }); @@ -3797,13 +3911,20 @@ public class FragmentCompose extends FragmentBase { tvVia.setText(draft.identityEmail); cbPlainOnly.setChecked(draft.plain_only != null && draft.plain_only); - cbEncrypt.setChecked(draft.encrypt != null && draft.encrypt); cbReceipt.setChecked(draft.receipt_request != null && draft.receipt_request); cbPlainOnly.setVisibility(draft.receipt != null && draft.receipt ? View.GONE : View.VISIBLE); - cbEncrypt.setVisibility(draft.receipt != null && draft.receipt ? View.GONE : View.VISIBLE); cbReceipt.setVisibility(draft.receipt != null && draft.receipt ? View.GONE : View.VISIBLE); + int encrypt = (draft.encrypt == null ? EntityMessage.ENCRYPTION_NONE : draft.encrypt); + for (int i = 0; i < encryptValues.length; i++) + if (encryptValues[i] == encrypt) { + spEncrypt.setTag(i); + spEncrypt.setSelection(i); + break; + } + spEncrypt.setVisibility(draft.receipt != null && draft.receipt ? View.GONE : View.VISIBLE); + int priority = (draft.priority == null ? 1 : draft.priority); spPriority.setTag(priority); spPriority.setSelection(priority); diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index 576000777b..7412b796a9 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -4116,7 +4116,8 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. // Find encrypted data for (EntityAttachment attachment : attachments) - if (EntityAttachment.PGP_MESSAGE.equals(attachment.encryption)) { + if (EntityAttachment.PGP_MESSAGE.equals(attachment.encryption) || + EntityAttachment.PGP_CONTENT.equals(attachment.encryption)) { if (!attachment.available) if (auto) return null; @@ -4126,6 +4127,13 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. File file = attachment.getFile(context); in = new FileInputStream(file); break; + } else if (EntityAttachment.PGP_SIGNATURE.equals(attachment.encryption)) { + File file = attachment.getFile(context); + byte[] signature = new byte[(int) file.length()]; + try (FileInputStream fis = new FileInputStream(file)) { + fis.read(signature); + } + data.putExtra(OpenPgpApi.ACTION_DETACHED_SIGN, signature); } if (in == null) { diff --git a/app/src/main/java/eu/faircode/email/MessageHelper.java b/app/src/main/java/eu/faircode/email/MessageHelper.java index 5b432fd033..c434e6a992 100644 --- a/app/src/main/java/eu/faircode/email/MessageHelper.java +++ b/app/src/main/java/eu/faircode/email/MessageHelper.java @@ -223,7 +223,7 @@ public class MessageHelper { if (message.from != null && message.from.length > 0) for (EntityAttachment attachment : attachments) - if (attachment.available && EntityAttachment.PGP_KEY.equals(attachment.encryption)) { + if (EntityAttachment.PGP_KEY.equals(attachment.encryption)) { InternetAddress from = (InternetAddress) message.from[0]; File file = attachment.getFile(context); StringBuilder sb = new StringBuilder(); @@ -240,21 +240,73 @@ public class MessageHelper { " keydata=" + sb.toString()); } + // https://tools.ietf.org/html/rfc3156 for (final EntityAttachment attachment : attachments) - if (attachment.available && EntityAttachment.PGP_MESSAGE.equals(attachment.encryption)) { - // https://tools.ietf.org/html/rfc3156 - Multipart multipart = new MimeMultipart("encrypted; protocol=\"application/pgp-encrypted\""); - - BodyPart pgp = new MimeBodyPart(); - pgp.setContent("", "application/pgp-encrypted"); - multipart.addBodyPart(pgp); + if (EntityAttachment.PGP_SIGNATURE.equals(attachment.encryption)) { + Log.i("Sending signed message"); + + for (final EntityAttachment content : attachments) + if (EntityAttachment.PGP_CONTENT.equals(content.encryption)) { + final ContentType ct = new ContentType(content.type); + final ContentType cts = new ContentType(attachment.type); + + // Build content + FileDataSource dsContent = new FileDataSource(content.getFile(context)); + dsContent.setFileTypeMap(new FileTypeMap() { + @Override + public String getContentType(File file) { + return ct.toString(); + } - BodyPart bpAttachment = new MimeBodyPart(); - bpAttachment.setFileName(attachment.name); + @Override + public String getContentType(String filename) { + return ct.toString(); + } + }); + BodyPart bpContent = new MimeBodyPart(); + bpContent.setDataHandler(new DataHandler(dsContent)); + + // Build signature + BodyPart bpSignature = new MimeBodyPart(); + bpSignature.setFileName(attachment.name); + FileDataSource dsSignature = new FileDataSource(attachment.getFile(context)); + dsSignature.setFileTypeMap(new FileTypeMap() { + @Override + public String getContentType(File file) { + return cts.getBaseType(); + } - File file = attachment.getFile(context); - FileDataSource dataSource = new FileDataSource(file); - dataSource.setFileTypeMap(new FileTypeMap() { + @Override + public String getContentType(String filename) { + return cts.getBaseType(); + } + }); + bpSignature.setDataHandler(new DataHandler(dsSignature)); + bpSignature.setDisposition(Part.INLINE); + + // Build message + Multipart multipart = new MimeMultipart("signed;" + + " micalg=\"" + cts.getParameter("micalg") + "\";" + + " protocol=\"application/pgp-signature\""); + multipart.addBodyPart(bpContent); + multipart.addBodyPart(bpSignature); + imessage.setContent(multipart); + + return imessage; + } + throw new IllegalStateException("Content not found"); + } else if (EntityAttachment.PGP_MESSAGE.equals(attachment.encryption)) { + Log.i("Sending encrypted message"); + + // Build header + BodyPart bpHeader = new MimeBodyPart(); + bpHeader.setContent("Version: 1\r\n", "application/pgp-encrypted"); + + // Build content + BodyPart bpContent = new MimeBodyPart(); + bpContent.setFileName(attachment.name); + FileDataSource dsContent = new FileDataSource(attachment.getFile(context)); + dsContent.setFileTypeMap(new FileTypeMap() { @Override public String getContentType(File file) { return attachment.type; @@ -265,11 +317,13 @@ public class MessageHelper { return attachment.type; } }); - bpAttachment.setDataHandler(new DataHandler(dataSource)); - bpAttachment.setDisposition(Part.INLINE); - - multipart.addBodyPart(bpAttachment); + bpContent.setDataHandler(new DataHandler(dsContent)); + bpContent.setDisposition(Part.INLINE); + // Build message + Multipart multipart = new MimeMultipart("encrypted; protocol=\"application/pgp-encrypted\""); + multipart.addBodyPart(bpHeader); + multipart.addBodyPart(bpContent); imessage.setContent(multipart); return imessage; @@ -1112,44 +1166,51 @@ public class MessageHelper { // Download attachment File file = EntityAttachment.getFile(context, local.id, local.name); db.attachment().setProgress(local.id, null); - try (InputStream is = apart.part.getInputStream()) { - long size = 0; - long total = apart.part.getSize(); - int lastprogress = 0; + if (EntityAttachment.PGP_CONTENT.equals(apart.encrypt)) { try (OutputStream os = new FileOutputStream(file)) { - byte[] buffer = new byte[Helper.BUFFER_SIZE]; - for (int len = is.read(buffer); len != -1; len = is.read(buffer)) { - size += len; - os.write(buffer, 0, len); - - // Update progress - if (total > 0) { - int progress = (int) (size * 100 / total / 20 * 20); - if (progress != lastprogress) { - lastprogress = progress; - db.attachment().setProgress(local.id, progress); + apart.part.writeTo(os); + } + db.attachment().setDownloaded(local.id, file.length()); + } else + try (InputStream is = apart.part.getInputStream()) { + long size = 0; + long total = apart.part.getSize(); + int lastprogress = 0; + + try (OutputStream os = new FileOutputStream(file)) { + byte[] buffer = new byte[Helper.BUFFER_SIZE]; + for (int len = is.read(buffer); len != -1; len = is.read(buffer)) { + size += len; + os.write(buffer, 0, len); + + // Update progress + if (total > 0) { + int progress = (int) (size * 100 / total / 20 * 20); + if (progress != lastprogress) { + lastprogress = progress; + db.attachment().setProgress(local.id, progress); + } } } } - } - - // Store attachment data - db.attachment().setDownloaded(local.id, size); - Log.i("Downloaded attachment size=" + size); - } catch (FolderClosedIOException ex) { - db.attachment().setError(local.id, Helper.formatThrowable(ex)); - throw new FolderClosedException(ex.getFolder(), "downloadAttachment", ex); - } catch (MessageRemovedIOException ex) { - db.attachment().setError(local.id, Helper.formatThrowable(ex)); - throw new MessagingException("downloadAttachment", ex); - } catch (Throwable ex) { - // Reset progress on failure - Log.e(ex); - db.attachment().setError(local.id, Helper.formatThrowable(ex)); - throw ex; - } + // Store attachment data + db.attachment().setDownloaded(local.id, size); + + Log.i("Downloaded attachment size=" + size); + } catch (FolderClosedIOException ex) { + db.attachment().setError(local.id, Helper.formatThrowable(ex)); + throw new FolderClosedException(ex.getFolder(), "downloadAttachment", ex); + } catch (MessageRemovedIOException ex) { + db.attachment().setError(local.id, Helper.formatThrowable(ex)); + throw new MessagingException("downloadAttachment", ex); + } catch (Throwable ex) { + // Reset progress on failure + Log.e(ex); + db.attachment().setError(local.id, Helper.formatThrowable(ex)); + throw ex; + } } String getWarnings(String existing) { @@ -1165,18 +1226,56 @@ public class MessageHelper { class AttachmentPart { String disposition; String filename; - boolean pgp; + Integer encrypt; Part part; EntityAttachment attachment; } MessageParts getMessageParts() throws IOException, MessagingException { MessageParts parts = new MessageParts(); - getMessageParts(imessage, parts, false); + + try { + if (imessage.isMimeType("multipart/signed")) { + Multipart multipart = (Multipart) imessage.getContent(); + if (multipart.getCount() == 2) { + getMessageParts(multipart.getBodyPart(0), parts, null); + getMessageParts(multipart.getBodyPart(1), parts, EntityAttachment.PGP_SIGNATURE); + + AttachmentPart apart = new AttachmentPart(); + apart.disposition = Part.INLINE; + apart.filename = "content.txt"; + apart.encrypt = EntityAttachment.PGP_CONTENT; + apart.part = multipart.getBodyPart(0); + + ContentType ct = new ContentType(apart.part.getContentType()); + + apart.attachment = new EntityAttachment(); + apart.attachment.disposition = apart.disposition; + apart.attachment.name = apart.filename; + apart.attachment.type = ct.getBaseType().toLowerCase(Locale.ROOT); + apart.attachment.encryption = apart.encrypt; + + parts.attachments.add(apart); + + return parts; + } + } else if (imessage.isMimeType("multipart/encrypted")) { + Multipart multipart = (Multipart) imessage.getContent(); + if (multipart.getCount() == 2) { + // Ignore header + getMessageParts(multipart.getBodyPart(1), parts, EntityAttachment.PGP_MESSAGE); + return parts; + } + } + } catch (ParseException ex) { + Log.w(ex); + } + + getMessageParts(imessage, parts, null); return parts; } - private void getMessageParts(Part part, MessageParts parts, boolean pgp) throws IOException, MessagingException { + private void getMessageParts(Part part, MessageParts parts, Integer encrypt) throws IOException, MessagingException { try { if (BuildConfig.DEBUG) Log.i("Part class=" + part.getClass() + " type=" + part.getContentType()); @@ -1194,19 +1293,7 @@ public class MessageHelper { for (int i = 0; i < multipart.getCount(); i++) try { - Part cpart = multipart.getBodyPart(i); - - try { - ContentType ct = new ContentType(cpart.getContentType()); - if ("application/pgp-encrypted".equals(ct.getBaseType().toLowerCase(Locale.ROOT))) { - pgp = true; - continue; - } - } catch (ParseException ex) { - Log.w(ex); - } - - getMessageParts(cpart, parts, pgp); + getMessageParts(multipart.getBodyPart(i), parts, encrypt); } catch (ParseException ex) { // Nested body: try to continue // ParseException: In parameter list boundary="...">, expected parameter name, got ";" @@ -1263,7 +1350,7 @@ public class MessageHelper { AttachmentPart apart = new AttachmentPart(); apart.disposition = disposition; apart.filename = filename; - apart.pgp = pgp; + apart.encrypt = encrypt; apart.part = part; String[] cid = null; @@ -1276,12 +1363,12 @@ public class MessageHelper { } apart.attachment = new EntityAttachment(); + apart.attachment.disposition = apart.disposition; apart.attachment.name = apart.filename; apart.attachment.type = contentType.getBaseType().toLowerCase(Locale.ROOT); - apart.attachment.disposition = apart.disposition; apart.attachment.size = (long) apart.part.getSize(); apart.attachment.cid = (cid == null || cid.length == 0 ? null : MimeUtility.unfold(cid[0])); - apart.attachment.encryption = (apart.pgp ? EntityAttachment.PGP_MESSAGE : null); + apart.attachment.encryption = apart.encrypt; if ("text/calendar".equalsIgnoreCase(apart.attachment.type) && TextUtils.isEmpty(apart.attachment.name)) diff --git a/app/src/main/java/eu/faircode/email/TupleMessageEx.java b/app/src/main/java/eu/faircode/email/TupleMessageEx.java index 64498472d4..63c5f1e185 100644 --- a/app/src/main/java/eu/faircode/email/TupleMessageEx.java +++ b/app/src/main/java/eu/faircode/email/TupleMessageEx.java @@ -45,6 +45,7 @@ public class TupleMessageEx extends EntityMessage { public int unseen; public int unflagged; public int drafts; + public int signed; public int encrypted; public int visible; public Long totalSize; @@ -79,6 +80,7 @@ public class TupleMessageEx extends EntityMessage { this.unseen == other.unseen && this.unflagged == other.unflagged && this.drafts == other.drafts && + this.signed == other.signed && this.encrypted == other.encrypted && this.visible == other.visible && Objects.equals(this.totalSize, other.totalSize) && diff --git a/app/src/main/res/drawable/baseline_security_24.xml b/app/src/main/res/drawable/baseline_security_24.xml new file mode 100644 index 0000000000..a368caf019 --- /dev/null +++ b/app/src/main/res/drawable/baseline_security_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/dialog_send.xml b/app/src/main/res/layout/dialog_send.xml index f825197caf..1620290ab5 100644 --- a/app/src/main/res/layout/dialog_send.xml +++ b/app/src/main/res/layout/dialog_send.xml @@ -111,16 +111,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvVia" /> - - + app:layout_constraintTop_toBottomOf="@id/cbPlainOnly" /> + + + + + app:layout_constraintTop_toBottomOf="@id/spEncrypt" /> + + + + + + + + + + Send via Send … Send at … + Encryption Priority No server found at \'%1$s\' @@ -688,6 +689,7 @@ Sending message Message will be sent around %1$s + Sign Encrypt Decrypt Resync @@ -828,6 +830,7 @@ Has draft Has high priority Has low priority + Is signed Is encrypted Authentication failed Is snoozed @@ -1148,6 +1151,18 @@ Large + + None + PGP sign-only + PGP sign+encrypt + + + + 0 + 2 + 1 + + 17BA15C1AF55D925F98B99CEA4375D4CDF4C174B MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtFbxEbzL8u5accPGgBw/XdyiSS5BBE6ZQ9ELpKyJ/OQN+kdYniCAOw3lsQ/GuJScy4Y2HobqbBgLL8GLHG+Yu2EHC9dLjA3v2Mc25vvnfn86BsrpQvz1poN2n+roTBdq09FWbtebJ8m0hDBVmtfRi7RhTKIL4No3kodLhksdnucKjcFheubebWKgpmvbmw7NwuELhaZmyhw8WTtnQ4rZPMhjY1JJZgzwNExXgD7zzg4pJPkuQlfkuRkkvBpHpi3C7VDnYjrBlLHngI4wv3wxQBVwJqlvAT9PmX8dOVnTsWWdJdLQBZVWphuqVY54kjBIovN+o8w03WjsV9QiOQq+XwIDAQAB