diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index 79d315dc24..483e7b4300 100644 Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ diff --git a/app/build.gradle b/app/build.gradle index b3c71090be..cc8aecbe99 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,6 +67,8 @@ dependencies { // https://developer.android.com/topic/libraries/architecture/adding-components.html // https://developer.android.com/jetpack/docs/release-notes + // https://github.com/open-keychain/openpgp-api + def androidx_version = "1.0.0-rc01" def constraintlayout_version = "1.1.2" def lifecycle_version = "2.0.0-rc01" @@ -76,6 +78,7 @@ dependencies { def javamail_version = "1.6.0" def jsoup_version = "1.11.3" def jcharset_version = "2.0" + def openpgp_version = "12.0" implementation "androidx.appcompat:appcompat:$androidx_version" implementation "androidx.recyclerview:recyclerview:$androidx_version" @@ -99,4 +102,6 @@ dependencies { implementation "org.jsoup:jsoup:$jsoup_version" implementation "net.freeutils:jcharset:$jcharset_version" + + implementation "org.sufficientlysecure:openpgp-api:$openpgp_version" } diff --git a/app/src/main/java/eu/faircode/email/ActivityCompose.java b/app/src/main/java/eu/faircode/email/ActivityCompose.java index e2d38e80dc..587b5f9a9c 100644 --- a/app/src/main/java/eu/faircode/email/ActivityCompose.java +++ b/app/src/main/java/eu/faircode/email/ActivityCompose.java @@ -36,6 +36,7 @@ public class ActivityCompose extends ActivityBase implements FragmentManager.OnB static final int REQUEST_CONTACT_CC = 2; static final int REQUEST_CONTACT_BCC = 3; static final int REQUEST_ATTACHMENT = 4; + static final int REQUEST_OPENPGP = 5; @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/eu/faircode/email/ActivityView.java b/app/src/main/java/eu/faircode/email/ActivityView.java index 52b9a40b4d..97857f4389 100644 --- a/app/src/main/java/eu/faircode/email/ActivityView.java +++ b/app/src/main/java/eu/faircode/email/ActivityView.java @@ -91,6 +91,8 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack static final int REQUEST_VIEW = 1; static final int REQUEST_UNSEEN = 2; + static final int REQUEST_OPENPGP = 3; + static final int REQUEST_ATTACHMENT_OFFSET = 10; static final String ACTION_VIEW_MESSAGES = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGES"; static final String ACTION_VIEW_MESSAGE = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGE"; @@ -692,7 +694,7 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack create.addCategory(Intent.CATEGORY_OPENABLE); create.setType(intent.getStringExtra("type")); create.putExtra(Intent.EXTRA_TITLE, intent.getStringExtra("name")); - startActivityForResult(create, (int) intent.getLongExtra("id", -1)); + startActivityForResult(create, (int) intent.getLongExtra("id", -1) + REQUEST_ATTACHMENT_OFFSET); } else if (ACTION_ACTIVATE_PRO.equals(intent.getAction())) { try { @@ -724,9 +726,9 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode == Activity.RESULT_OK) { + if (resultCode == Activity.RESULT_OK && requestCode > REQUEST_ATTACHMENT_OFFSET) { Bundle args = new Bundle(); - args.putLong("id", requestCode); + args.putLong("id", requestCode - REQUEST_ATTACHMENT_OFFSET); args.putParcelable("uri", data.getData()); new SimpleTask() { @Override diff --git a/app/src/main/java/eu/faircode/email/FragmentAccount.java b/app/src/main/java/eu/faircode/email/FragmentAccount.java index bec030f832..e5e4a58f09 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAccount.java +++ b/app/src/main/java/eu/faircode/email/FragmentAccount.java @@ -453,6 +453,7 @@ public class FragmentAccount extends FragmentEx { account.password = password; account.synchronize = synchronize; account.primary = (account.synchronize && primary); + account.store_sent = false; account.poll_interval = Integer.parseInt(poll_interval); if (!synchronize) diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 04ccfe57f0..be451b6194 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -20,6 +20,7 @@ package eu.faircode.email; */ import android.Manifest; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -54,7 +55,13 @@ import android.widget.Toast; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.snackbar.Snackbar; +import org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpServiceConnection; + import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -110,8 +117,27 @@ public class FragmentCompose extends FragmentEx { private boolean addresses; private boolean autosave = true; + private String encrypted = null; + private OpenPgpServiceConnection openPgpConnection = null; + private static final int ATTACHMENT_BUFFER_SIZE = 8192; // bytes + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + openPgpConnection = new OpenPgpServiceConnection(getContext(), "org.sufficientlysecure.keychain"); + openPgpConnection.bindToService(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (openPgpConnection != null) { + openPgpConnection.unbindFromService(); + openPgpConnection = null; + } + } + @Override @Nullable public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -317,6 +343,7 @@ public class FragmentCompose extends FragmentEx { public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putLong("working", working); + outState.putString("encrypted", encrypted); } @Override @@ -337,6 +364,8 @@ public class FragmentCompose extends FragmentEx { args.putParcelableArrayList("attachments", getArguments().getParcelableArrayList("attachments")); draftLoader.load(this, args); } else { + encrypted = savedInstanceState.getString("encrypted"); + Bundle args = new Bundle(); args.putString("action", "edit"); args.putLong("id", savedInstanceState.getLong("working")); @@ -365,6 +394,7 @@ public class FragmentCompose extends FragmentEx { menu.findItem(R.id.menu_attachment).setVisible(!free && working >= 0); menu.findItem(R.id.menu_attachment).setEnabled(etBody.isEnabled()); menu.findItem(R.id.menu_addresses).setVisible(!free && working >= 0); + menu.findItem(R.id.menu_encrypt).setVisible(encrypted == null); } @Override @@ -376,6 +406,9 @@ public class FragmentCompose extends FragmentEx { case R.id.menu_addresses: onMenuAddresses(); return true; + case R.id.menu_encrypt: + onMenuEncrypt(); + return true; default: return super.onOptionsItemSelected(item); } @@ -392,12 +425,77 @@ public class FragmentCompose extends FragmentEx { grpAddresses.setVisibility(grpAddresses.getVisibility() == View.GONE ? View.VISIBLE : View.GONE); } + private void onMenuEncrypt() { + Log.i(Helper.TAG, "On encrypt"); + try { + if (openPgpConnection == null) + throw new IllegalArgumentException(); + if (!openPgpConnection.isBound()) + throw new IllegalArgumentException("OpenPgp not available"); + + EntityIdentity identity = (EntityIdentity) spFrom.getSelectedItem(); + if (identity == null) + throw new IllegalArgumentException("No identity selected"); + + Intent data = new Intent(); + data.setAction(OpenPgpApi.ACTION_ENCRYPT); + data.putExtra(OpenPgpApi.EXTRA_USER_IDS, new String[]{identity.email}); + data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + + String plain = etBody.getText().toString(); + final InputStream is = new ByteArrayInputStream(plain.getBytes("UTF-8")); + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + + OpenPgpApi api = new OpenPgpApi(getContext(), openPgpConnection.getService()); + api.executeApiAsync(data, is, os, new OpenPgpApi.IOpenPgpCallback() { + @Override + public void onReturn(Intent result) { + try { + int code = result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + switch (code) { + case OpenPgpApi.RESULT_CODE_SUCCESS: { + Log.i(Helper.TAG, "Encrypted"); + FragmentCompose.this.encrypted = os.toString("UTF-8"); + getActivity().invalidateOptionsMenu(); + etBody.setText(FragmentCompose.this.encrypted); + break; + } + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: { + Log.i(Helper.TAG, "User interaction"); + PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); + getActivity().startIntentSenderForResult( + pi.getIntentSender(), + ActivityCompose.REQUEST_OPENPGP, + null, 0, 0, 0); + break; + } + case OpenPgpApi.RESULT_CODE_ERROR: { + OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); + Log.e(Helper.TAG, error.getMessage()); + Toast.makeText(getContext(), error.getMessage(), Toast.LENGTH_LONG).show(); + } + } + } catch (Throwable ex) { + Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); + Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show(); + } + } + }); + } catch (Throwable ex) { + Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); + Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show(); + } + } + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { if (requestCode == ActivityCompose.REQUEST_ATTACHMENT) { if (data != null) handleAddAttachment(data); + } else if (requestCode == ActivityCompose.REQUEST_OPENPGP) { + Log.i(Helper.TAG, "User interacted"); + onMenuEncrypt(); } else handlePickContact(requestCode, data); } @@ -531,7 +629,6 @@ public class FragmentCompose extends FragmentEx { if (attachment.type == null) attachment.type = "application/octet-stream"; - attachment.size = (s == null ? null : Integer.parseInt(s)); attachment.progress = 0; @@ -903,7 +1000,11 @@ public class FragmentCompose extends FragmentEx { draft.subject = subject; draft.received = new Date().getTime(); - String pbody = "
" + body.replaceAll("\\r?\\n", "
") + "
"; + String pbody; + if (encrypted == null) + pbody = "
" + body.replaceAll("\\r?\\n", "
") + "
"; + else + pbody = encrypted; // Execute action if (action == R.id.action_trash) { diff --git a/app/src/main/java/eu/faircode/email/FragmentMessage.java b/app/src/main/java/eu/faircode/email/FragmentMessage.java index e2a53d6714..3af846b0f5 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessage.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessage.java @@ -19,6 +19,7 @@ package eu.faircode.email; Copyright 2018 by Marcel Bokhorst (M66B) */ +import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -55,8 +56,13 @@ import android.widget.Toast; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.floatingactionbutton.FloatingActionButton; +import org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpServiceConnection; import org.xml.sax.XMLReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -73,6 +79,8 @@ import java.util.Date; import java.util.List; import java.util.Locale; +import javax.mail.internet.InternetAddress; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -86,6 +94,8 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import static android.app.Activity.RESULT_OK; + public class FragmentMessage extends FragmentEx { private ViewGroup view; private TextView tvFrom; @@ -116,11 +126,30 @@ public class FragmentMessage extends FragmentEx { private boolean free = false; private AdapterAttachment adapter; + private String decrypted = null; + private OpenPgpServiceConnection openPgpConnection = null; + private boolean debug; private DateFormat df = SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); private static final long CACHE_IMAGE_DURATION = 24 * 3600 * 1000L; + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + openPgpConnection = new OpenPgpServiceConnection(getContext(), "org.sufficientlysecure.keychain"); + openPgpConnection.bindToService(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (openPgpConnection != null) { + openPgpConnection.unbindFromService(); + openPgpConnection = null; + } + } + @Override @Nullable public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -611,6 +640,7 @@ public class FragmentMessage extends FragmentEx { menu.findItem(R.id.menu_seen).setVisible(!free && message != null && !inOutbox); menu.findItem(R.id.menu_forward).setVisible(!free && message != null && !inOutbox); menu.findItem(R.id.menu_reply_all).setVisible(!free && message != null && message.cc != null && !inOutbox); + menu.findItem(R.id.menu_decrypt).setVisible(decrypted == null); if (message != null) { MenuItem menuSeen = menu.findItem(R.id.menu_seen); @@ -639,6 +669,9 @@ public class FragmentMessage extends FragmentEx { case R.id.menu_reply_all: onMenuReplyAll(message.id); return true; + case R.id.menu_decrypt: + onMenuDecrypt(message); + return true; default: return super.onOptionsItemSelected(item); } @@ -719,6 +752,75 @@ public class FragmentMessage extends FragmentEx { .putExtra("reference", id)); } + private void onMenuDecrypt(EntityMessage message) { + Log.i(Helper.TAG, "On decrypt"); + try { + if (openPgpConnection == null) + throw new IllegalArgumentException(); + if (!openPgpConnection.isBound()) + throw new IllegalArgumentException("OpenPgp not available"); + + InternetAddress to = (InternetAddress) message.to[0]; + + Intent data = new Intent(); + data.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + data.putExtra(OpenPgpApi.EXTRA_USER_IDS, new String[]{to.getAddress()}); + + String encrypted = message.read(getContext()); + final InputStream is = new ByteArrayInputStream(encrypted.getBytes("UTF-8")); + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + + OpenPgpApi api = new OpenPgpApi(getContext(), openPgpConnection.getService()); + api.executeApiAsync(data, is, os, new OpenPgpApi.IOpenPgpCallback() { + @Override + public void onReturn(Intent result) { + try { + int code = result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + switch (code) { + case OpenPgpApi.RESULT_CODE_SUCCESS: { + Log.i(Helper.TAG, "Decrypted"); + FragmentMessage.this.decrypted = os.toString("UTF-8"); + getActivity().invalidateOptionsMenu(); + tvBody.setText(decrypted); + break; + } + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: { + Log.i(Helper.TAG, "User interaction"); + PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); + getActivity().startIntentSenderForResult( + pi.getIntentSender(), + ActivityView.REQUEST_OPENPGP, + null, 0, 0, 0); + break; + } + case OpenPgpApi.RESULT_CODE_ERROR: { + OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); + Log.e(Helper.TAG, error.getMessage()); + Toast.makeText(getContext(), error.getMessage(), Toast.LENGTH_LONG).show(); + } + } + } catch (Throwable ex) { + Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); + Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show(); + } + } + }); + } catch (Throwable ex) { + Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); + Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + if (requestCode == ActivityView.REQUEST_OPENPGP && message != null) { + Log.i(Helper.TAG, "User interacted"); + onMenuDecrypt(message); + } + } + } + private void onActionSpam(final long id) { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder diff --git a/app/src/main/res/menu/menu_compose.xml b/app/src/main/res/menu/menu_compose.xml index 935262437c..c276b08811 100644 --- a/app/src/main/res/menu/menu_compose.xml +++ b/app/src/main/res/menu/menu_compose.xml @@ -13,4 +13,9 @@ android:icon="@drawable/baseline_people_24" android:title="@string/title_show_addresses" app:showAsAction="always" /> + + diff --git a/app/src/main/res/menu/menu_view.xml b/app/src/main/res/menu/menu_view.xml index 4705b82dd8..4f72d7b327 100644 --- a/app/src/main/res/menu/menu_view.xml +++ b/app/src/main/res/menu/menu_view.xml @@ -31,4 +31,9 @@ android:icon="@drawable/baseline_reply_all_24" android:title="@string/title_reply_all" app:showAsAction="never" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 780adf4b89..9a6f190d1a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -160,6 +160,9 @@ Show CC/BCC Add attachment + Encrypt + Decrypt + Sender missing Recipient missing Attachments still loading