diff --git a/app/src/main/java/eu/faircode/email/AdapterCertificate.java b/app/src/main/java/eu/faircode/email/AdapterCertificate.java
new file mode 100644
index 0000000000..62d6e932e4
--- /dev/null
+++ b/app/src/main/java/eu/faircode/email/AdapterCertificate.java
@@ -0,0 +1,186 @@
+package eu.faircode.email;
+
+/*
+ This file is part of FairEmail.
+
+ FairEmail is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ FairEmail is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with FairEmail. If not, see .
+
+ Copyright 2018-2019 by Marcel Bokhorst (M66B)
+*/
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListUpdateCallback;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AdapterCertificate extends RecyclerView.Adapter {
+ private ICertificate intf;
+ private Context context;
+ private LifecycleOwner owner;
+ private LayoutInflater inflater;
+
+ private List items = new ArrayList<>();
+
+ public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+ private View view;
+ private TextView tvEmail;
+ private TextView tvSubject;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+
+ view = itemView.findViewById(R.id.clItem);
+ tvEmail = itemView.findViewById(R.id.tvEmail);
+ tvSubject = itemView.findViewById(R.id.tvSubject);
+ }
+
+ @Override
+ public void onClick(View v) {
+ int pos = getAdapterPosition();
+ if (pos == RecyclerView.NO_POSITION)
+ return;
+
+ EntityCertificate certificate = items.get(pos);
+ intf.onSelected(certificate);
+ }
+
+ private void wire() {
+ view.setOnClickListener(this);
+ }
+
+ private void unwire() {
+ view.setOnClickListener(null);
+ }
+
+ private void bindTo(EntityCertificate certificate) {
+ tvEmail.setText(certificate.email);
+ tvSubject.setText(certificate.subject);
+ }
+ }
+
+ AdapterCertificate(Fragment parentFragment, ICertificate intf) {
+ this.intf = intf;
+ this.context = parentFragment.getContext();
+ this.owner = parentFragment.getViewLifecycleOwner();
+ this.inflater = LayoutInflater.from(parentFragment.getContext());
+
+ setHasStableIds(true);
+ }
+
+ public void set(@NonNull List certificates) {
+ Log.i("Set certificates=" + certificates.size());
+
+ DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new DiffCallback(items, certificates), false);
+
+ items = certificates;
+
+ diff.dispatchUpdatesTo(new ListUpdateCallback() {
+ @Override
+ public void onInserted(int position, int count) {
+ Log.i("Inserted @" + position + " #" + count);
+ }
+
+ @Override
+ public void onRemoved(int position, int count) {
+ Log.i("Removed @" + position + " #" + count);
+ }
+
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ Log.i("Moved " + fromPosition + ">" + toPosition);
+ }
+
+ @Override
+ public void onChanged(int position, int count, Object payload) {
+ Log.i("Changed @" + position + " #" + count);
+ }
+ });
+ diff.dispatchUpdatesTo(this);
+ }
+
+ private class DiffCallback extends DiffUtil.Callback {
+ private List prev = new ArrayList<>();
+ private List next = new ArrayList<>();
+
+ DiffCallback(List prev, List next) {
+ this.prev.addAll(prev);
+ this.next.addAll(next);
+ }
+
+ @Override
+ public int getOldListSize() {
+ return prev.size();
+ }
+
+ @Override
+ public int getNewListSize() {
+ return next.size();
+ }
+
+ @Override
+ public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+ EntityCertificate c1 = prev.get(oldItemPosition);
+ EntityCertificate c2 = next.get(newItemPosition);
+ return c1.id.equals(c2.id);
+ }
+
+ @Override
+ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+ EntityCertificate c1 = prev.get(oldItemPosition);
+ EntityCertificate c2 = next.get(newItemPosition);
+ return c1.equals(c2);
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return items.get(position).id;
+ }
+
+ @Override
+ public int getItemCount() {
+ return items.size();
+ }
+
+ @Override
+ @NonNull
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new ViewHolder(inflater.inflate(R.layout.item_certificate, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ holder.unwire();
+
+ EntityCertificate certificate = items.get(position);
+ holder.bindTo(certificate);
+
+ holder.wire();
+ }
+
+ interface ICertificate {
+ void onSelected(EntityCertificate certificate);
+ }
+}
diff --git a/app/src/main/java/eu/faircode/email/DaoCertificate.java b/app/src/main/java/eu/faircode/email/DaoCertificate.java
index 75947b5f29..d9a54cd226 100644
--- a/app/src/main/java/eu/faircode/email/DaoCertificate.java
+++ b/app/src/main/java/eu/faircode/email/DaoCertificate.java
@@ -29,9 +29,13 @@ import java.util.List;
@Dao
public interface DaoCertificate {
@Query("SELECT * FROM certificate" +
- " ORDER BY email DESC")
+ " ORDER BY email")
LiveData> liveCertificates();
+ @Query("SELECT * FROM certificate" +
+ " WHERE id = :id")
+ EntityCertificate getCertificate(long id);
+
@Query("SELECT * FROM certificate" +
" WHERE fingerprint = :fingerprint" +
" AND email = :email")
diff --git a/app/src/main/java/eu/faircode/email/EntityCertificate.java b/app/src/main/java/eu/faircode/email/EntityCertificate.java
index 0d2862f7e9..bfe7abde84 100644
--- a/app/src/main/java/eu/faircode/email/EntityCertificate.java
+++ b/app/src/main/java/eu/faircode/email/EntityCertificate.java
@@ -19,6 +19,8 @@ package eu.faircode.email;
Copyright 2018-2019 by Marcel Bokhorst (M66B)
*/
+import android.util.Base64;
+
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.Index;
@@ -48,6 +50,14 @@ public class EntityCertificate {
@NonNull
public String data;
+ void setEncoded(byte[] encoded) {
+ this.data = Base64.encodeToString(encoded, Base64.NO_WRAP);
+ }
+
+ byte[] getEncoded() {
+ return Base64.decode(this.data, Base64.NO_WRAP);
+ }
+
@Override
public boolean equals(Object obj) {
if (obj instanceof EntityCertificate) {
diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java
index ea95cf69f4..84b6d8dfa7 100644
--- a/app/src/main/java/eu/faircode/email/FragmentCompose.java
+++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java
@@ -60,7 +60,6 @@ import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.text.style.QuoteSpan;
-import android.util.Base64;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -78,6 +77,7 @@ import android.widget.EditText;
import android.widget.FilterQueryProvider;
import android.widget.ImageButton;
import android.widget.MultiAutoCompleteTextView;
+import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
@@ -255,6 +255,7 @@ public class FragmentCompose extends FragmentBase {
private static final int REQUEST_LINK = 12;
private static final int REQUEST_DISCARD = 13;
private static final int REQUEST_SEND = 14;
+ private static final int REQUEST_SELECT_CERTIFICATE = 15;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -1247,7 +1248,18 @@ public class FragmentCompose extends FragmentBase {
@Override
public void run() {
try {
- onSmime(draft, alias);
+ Bundle args = new Bundle();
+ args.putLong("id", draft.id);
+ args.putInt("type", draft.encrypt);
+ args.putString("alias", alias);
+
+ if (EntityMessage.SMIME_SIGNENCRYPT.equals(draft.encrypt)) {
+ FragmentDialogCertificate fragment = new FragmentDialogCertificate();
+ fragment.setArguments(args);
+ fragment.setTargetFragment(FragmentCompose.this, REQUEST_SELECT_CERTIFICATE);
+ fragment.show(getParentFragmentManager(), "compose:certificate");
+ } else
+ onSmime(args);
} catch (Throwable ex) {
Log.e(ex);
}
@@ -1367,6 +1379,10 @@ public class FragmentCompose extends FragmentBase {
if (resultCode == RESULT_OK)
onActionSend();
break;
+ case REQUEST_SELECT_CERTIFICATE:
+ if (resultCode == RESULT_OK && data != null)
+ onSmime(data.getBundleExtra("args"));
+ break;
}
} catch (Throwable ex) {
Log.e(ex);
@@ -1844,18 +1860,14 @@ public class FragmentCompose extends FragmentBase {
}.execute(this, args, "compose:pgp");
}
- private void onSmime(EntityMessage draft, String alias) {
- Bundle args = new Bundle();
- args.putLong("id", draft.id);
- args.putInt("type", draft.encrypt);
- args.putString("alias", alias);
-
+ private void onSmime(Bundle args) {
new SimpleTask() {
@Override
protected Void onExecute(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
int type = args.getInt("type");
String alias = args.getString("alias");
+ long cid = args.getLong("certificate", -1);
DB db = DB.getInstance(context);
@@ -1866,6 +1878,9 @@ public class FragmentCompose extends FragmentBase {
EntityIdentity identity = db.identity().getIdentity(draft.identity);
if (identity == null)
throw new IllegalArgumentException(getString(R.string.title_from_missing));
+ EntityCertificate certificate = db.certificate().getCertificate(cid);
+ if (certificate == null && EntityMessage.SMIME_SIGNENCRYPT.equals(type))
+ throw new IllegalArgumentException("Certificate missing");
// Get/clean attachments
List attachments = db.attachment().getAttachments(id);
@@ -1974,19 +1989,8 @@ public class FragmentCompose extends FragmentBase {
return null;
}
- // Get recipient
- if (draft.to == null || draft.to.length != 1)
- throw new IllegalArgumentException(getString(R.string.title_to_missing));
- String to = ((InternetAddress) draft.to[0]).getAddress();
-
- // Get public key
- List c = db.certificate().getCertificateByEmail(to);
- if (c == null || c.size() == 0)
- throw new IllegalArgumentException("Certificate not found");
-
- byte[] encoded = Base64.decode(c.get(0).data, Base64.NO_WRAP);
X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509")
- .generateCertificate(new ByteArrayInputStream(encoded));
+ .generateCertificate(new ByteArrayInputStream(certificate.getEncoded()));
// Build signature
BodyPart bpSignature = new MimeBodyPart();
@@ -3933,6 +3937,50 @@ public class FragmentCompose extends FragmentBase {
}
}
+ public static class FragmentDialogCertificate extends FragmentDialogBase {
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_certificate, null);
+ final RecyclerView rvCertificate = dview.findViewById(R.id.rvCertificate);
+ final ProgressBar pbWait = dview.findViewById(R.id.pbWait);
+
+ final Dialog dialog = new AlertDialog.Builder(getContext())
+ .setTitle(R.string.title_select_certificate)
+ .setView(dview)
+ .setNegativeButton(android.R.string.cancel, null).create();
+
+ rvCertificate.setHasFixedSize(false);
+ LinearLayoutManager llm = new LinearLayoutManager(getContext());
+ rvCertificate.setLayoutManager(llm);
+
+ final AdapterCertificate adapter = new AdapterCertificate(this, new AdapterCertificate.ICertificate() {
+ @Override
+ public void onSelected(EntityCertificate certificate) {
+ dialog.dismiss();
+ getArguments().putLong("certificate", certificate.id);
+ sendResult(RESULT_OK);
+ }
+ });
+ rvCertificate.setAdapter(adapter);
+
+ rvCertificate.setVisibility(View.GONE);
+ pbWait.setVisibility(View.VISIBLE);
+
+ DB db = DB.getInstance(getContext());
+ db.certificate().liveCertificates().observe(getViewLifecycleOwner(), new Observer>() {
+ @Override
+ public void onChanged(List certificates) {
+ pbWait.setVisibility(View.GONE);
+ rvCertificate.setVisibility(View.VISIBLE);
+ adapter.set(certificates);
+ }
+ });
+
+ return dialog;
+ }
+ }
+
public static class FragmentDialogSend extends FragmentDialogBase {
@NonNull
@Override
diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java
index a2973f1c8a..c47d175432 100644
--- a/app/src/main/java/eu/faircode/email/FragmentMessages.java
+++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java
@@ -52,7 +52,6 @@ import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.text.TextUtils;
import android.text.format.DateUtils;
-import android.util.Base64;
import android.util.LongSparseArray;
import android.util.Pair;
import android.util.TypedValue;
@@ -4398,7 +4397,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
record.fingerprint = fingerprint;
record.email = email;
record.subject = cert.getSubjectX500Principal().getName(X500Principal.RFC2253);
- record.data = Base64.encodeToString(cert.getEncoded(), Base64.NO_WRAP);
+ record.setEncoded(cert.getEncoded());
record.id = db.certificate().insertCertificate(record);
}
}
diff --git a/app/src/main/res/layout/dialog_certificate.xml b/app/src/main/res/layout/dialog_certificate.xml
new file mode 100644
index 0000000000..ff77d6a8e4
--- /dev/null
+++ b/app/src/main/res/layout/dialog_certificate.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_certificate.xml b/app/src/main/res/layout/item_certificate.xml
new file mode 100644
index 0000000000..5890491ae5
--- /dev/null
+++ b/app/src/main/res/layout/item_certificate.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1cdf10e74b..1a0df4b7b7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -673,6 +673,7 @@
Insert template
Edit as plain text
Edit as reformatted text
+ Select certificate
Plain text only
Request delivery/read receipt
Most providers and email clients ignore receipt requests