mirror of https://github.com/M66B/FairEmail.git
Basic encryption support
This commit is contained in:
parent
0139a2eb7d
commit
cf858524d2
|
@ -84,6 +84,7 @@ dependencies {
|
|||
def jsoup_version = "1.11.3"
|
||||
def jcharset_version = "2.0"
|
||||
def dnsjava_version = "2.1.8"
|
||||
def openpgp_version = "12.0"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$androidx_version"
|
||||
implementation "androidx.recyclerview:recyclerview:$androidx_version"
|
||||
|
@ -113,6 +114,9 @@ dependencies {
|
|||
// http://www.xbill.org/dnsjava/
|
||||
implementation "dnsjava:dnsjava:$dnsjava_version"
|
||||
|
||||
// https://github.com/open-keychain/openpgp-api
|
||||
implementation "org.sufficientlysecure:openpgp-api:$openpgp_version"
|
||||
|
||||
// git clone https://android.googlesource.com/platform/frameworks/opt/colorpicker
|
||||
implementation project(path: ':colorpicker')
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ public class ActivityCompose extends ActivityBilling implements FragmentManager.
|
|||
static final int REQUEST_CONTACT_BCC = 3;
|
||||
static final int REQUEST_IMAGE = 4;
|
||||
static final int REQUEST_ATTACHMENT = 5;
|
||||
static final int REQUEST_ENCRYPT = 6;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
|
|
@ -20,6 +20,7 @@ package eu.faircode.email;
|
|||
*/
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
@ -45,14 +46,22 @@ import android.widget.ListView;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.openintents.openpgp.OpenPgpError;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
import org.openintents.openpgp.util.OpenPgpServiceConnection;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.text.Collator;
|
||||
|
@ -76,7 +85,6 @@ import androidx.lifecycle.Lifecycle;
|
|||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.paging.PagedList;
|
||||
|
||||
public class ActivityView extends ActivityBilling implements FragmentManager.OnBackStackChangedListener {
|
||||
private View view;
|
||||
|
@ -85,7 +93,9 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
private ActionBarDrawerToggle drawerToggle;
|
||||
|
||||
private long attachment = -1;
|
||||
private PagedList<TupleMessageEx> messages = null;
|
||||
private long decryptId = -1;
|
||||
private File decryptFile = null;
|
||||
private OpenPgpServiceConnection pgpService;
|
||||
|
||||
private static final int ATTACHMENT_BUFFER_SIZE = 8192; // bytes
|
||||
|
||||
|
@ -95,6 +105,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
|
||||
static final int REQUEST_ATTACHMENT = 1;
|
||||
static final int REQUEST_INVITE = 2;
|
||||
static final int REQUEST_DECRYPT = 3;
|
||||
|
||||
static final String ACTION_VIEW_MESSAGES = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGES";
|
||||
static final String ACTION_VIEW_THREAD = BuildConfig.APPLICATION_ID + ".VIEW_THREAD";
|
||||
|
@ -102,6 +113,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
static final String ACTION_EDIT_FOLDER = BuildConfig.APPLICATION_ID + ".EDIT_FOLDER";
|
||||
static final String ACTION_EDIT_ANSWER = BuildConfig.APPLICATION_ID + ".EDIT_ANSWER";
|
||||
static final String ACTION_STORE_ATTACHMENT = BuildConfig.APPLICATION_ID + ".STORE_ATTACHMENT";
|
||||
static final String ACTION_DECRYPT = BuildConfig.APPLICATION_ID + ".DECRYPT";
|
||||
static final String ACTION_SHOW_PRO = BuildConfig.APPLICATION_ID + ".SHOW_PRO";
|
||||
|
||||
static final String UPDATE_LATEST_API = "https://api.github.com/repos/M66B/open-source-email/releases/latest";
|
||||
|
@ -262,6 +274,9 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
checkCrash();
|
||||
if (!Helper.isPlayStoreInstall(this))
|
||||
checkUpdate();
|
||||
|
||||
pgpService = new OpenPgpServiceConnection(this, "org.sufficientlysecure.keychain");
|
||||
pgpService.bindToService();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -294,6 +309,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
iff.addAction(ACTION_EDIT_FOLDER);
|
||||
iff.addAction(ACTION_EDIT_ANSWER);
|
||||
iff.addAction(ACTION_STORE_ATTACHMENT);
|
||||
iff.addAction(ACTION_DECRYPT);
|
||||
iff.addAction(ACTION_SHOW_PRO);
|
||||
lbm.registerReceiver(receiver, iff);
|
||||
|
||||
|
@ -369,6 +385,14 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
lbm.unregisterReceiver(receiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (pgpService != null)
|
||||
pgpService.unbindFromService();
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
@ -801,6 +825,8 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
onEditAnswer(intent);
|
||||
else if (ACTION_STORE_ATTACHMENT.equals(intent.getAction()))
|
||||
onStoreAttachment(intent);
|
||||
else if (ACTION_DECRYPT.equals(intent.getAction()))
|
||||
onDecrypt(intent);
|
||||
else if (ACTION_SHOW_PRO.equals(intent.getAction()))
|
||||
onShowPro(intent);
|
||||
}
|
||||
|
@ -868,76 +894,207 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
startActivityForResult(create, REQUEST_ATTACHMENT);
|
||||
}
|
||||
|
||||
private void onDecrypt(Intent intent) {
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", intent.getLongExtra("id", -1));
|
||||
|
||||
new SimpleTask<File>() {
|
||||
@Override
|
||||
protected File onLoad(Context context, Bundle args) throws Throwable {
|
||||
long id = args.getLong("id");
|
||||
|
||||
DB db = DB.getInstance(context);
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
for (EntityAttachment attachment : db.attachment().getAttachments(id))
|
||||
if (attachment.available && "encrypted.asc".equals(attachment.name))
|
||||
return EntityAttachment.getFile(context, attachment.id);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLoaded(Bundle args, File file) {
|
||||
if (file != null)
|
||||
try {
|
||||
if (!pgpService.isBound())
|
||||
throw new IllegalArgumentException(getString(R.string.title_no_openpgp));
|
||||
|
||||
Intent data = new Intent();
|
||||
data.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
|
||||
data.putExtra(OpenPgpApi.EXTRA_USER_IDS, new String[]{args.getString("to")});
|
||||
data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
|
||||
decrypt(data, args.getLong("id"), file);
|
||||
} catch (Throwable ex) {
|
||||
if (ex instanceof IllegalArgumentException)
|
||||
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||
else
|
||||
Helper.unexpectedError(ActivityView.this, ex);
|
||||
}
|
||||
}
|
||||
}.load(this, args);
|
||||
}
|
||||
|
||||
private void onShowPro(Intent intent) {
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
|
||||
private void decrypt(Intent data, final long id, final File file) throws FileNotFoundException {
|
||||
final OpenPgpApi api = new OpenPgpApi(this, pgpService.getService());
|
||||
final FileInputStream msg = new FileInputStream(file);
|
||||
final ByteArrayOutputStream decrypted = new ByteArrayOutputStream();
|
||||
|
||||
api.executeApiAsync(data, msg, decrypted, new OpenPgpApi.IOpenPgpCallback() {
|
||||
@Override
|
||||
public void onReturn(Intent result) {
|
||||
Log.i(Helper.TAG, "Pgp result=" + result);
|
||||
Bundle extras = result.getExtras();
|
||||
for (String key : extras.keySet())
|
||||
Log.i(Helper.TAG, key + "=" + extras.get(key));
|
||||
|
||||
try {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", id);
|
||||
|
||||
new SimpleTask<Void>() {
|
||||
@Override
|
||||
protected Void onLoad(Context context, Bundle args) throws Throwable {
|
||||
long id = args.getLong("id");
|
||||
|
||||
DB db = DB.getInstance(context);
|
||||
EntityMessage message = db.message().getMessage(id);
|
||||
message.write(context, decrypted.toString("UTF-8"));
|
||||
db.message().setMessageStored(id, new Date().getTime());
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
Helper.unexpectedError(ActivityView.this, ex);
|
||||
}
|
||||
}.load(ActivityView.this, args);
|
||||
|
||||
break;
|
||||
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
decryptId = id;
|
||||
decryptFile = file;
|
||||
PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
||||
startIntentSenderForResult(
|
||||
pi.getIntentSender(),
|
||||
ActivityView.REQUEST_DECRYPT,
|
||||
null, 0, 0, 0, null);
|
||||
break;
|
||||
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
|
||||
throw new IllegalArgumentException(error.getMessage());
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
if (ex instanceof IllegalArgumentException)
|
||||
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||
else
|
||||
Helper.unexpectedError(ActivityView.this, ex);
|
||||
} finally {
|
||||
try {
|
||||
msg.close();
|
||||
} catch (IOException ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
Log.i(Helper.TAG, "View onActivityResult request=" + requestCode + " result=" + resultCode + " data=" + data);
|
||||
if (resultCode == Activity.RESULT_OK)
|
||||
if (requestCode == REQUEST_ATTACHMENT) {
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", attachment);
|
||||
args.putParcelable("uri", data.getData());
|
||||
new SimpleTask<Void>() {
|
||||
@Override
|
||||
protected Void onLoad(Context context, Bundle args) throws Throwable {
|
||||
long id = args.getLong("id");
|
||||
Uri uri = args.getParcelable("uri");
|
||||
if (data != null) {
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", attachment);
|
||||
args.putParcelable("uri", data.getData());
|
||||
|
||||
File file = EntityAttachment.getFile(context, id);
|
||||
new SimpleTask<Void>() {
|
||||
@Override
|
||||
protected Void onLoad(Context context, Bundle args) throws Throwable {
|
||||
long id = args.getLong("id");
|
||||
Uri uri = args.getParcelable("uri");
|
||||
|
||||
ParcelFileDescriptor pfd = null;
|
||||
FileOutputStream fos = null;
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
pfd = context.getContentResolver().openFileDescriptor(uri, "w");
|
||||
fos = new FileOutputStream(pfd.getFileDescriptor());
|
||||
fis = new FileInputStream(file);
|
||||
File file = EntityAttachment.getFile(context, id);
|
||||
|
||||
byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE];
|
||||
int read;
|
||||
while ((read = fis.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, read);
|
||||
}
|
||||
} finally {
|
||||
ParcelFileDescriptor pfd = null;
|
||||
FileOutputStream fos = null;
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
if (pfd != null)
|
||||
pfd.close();
|
||||
} catch (Throwable ex) {
|
||||
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
try {
|
||||
if (fos != null)
|
||||
fos.close();
|
||||
} catch (Throwable ex) {
|
||||
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
try {
|
||||
if (fis != null)
|
||||
fis.close();
|
||||
} catch (Throwable ex) {
|
||||
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
pfd = context.getContentResolver().openFileDescriptor(uri, "w");
|
||||
fos = new FileOutputStream(pfd.getFileDescriptor());
|
||||
fis = new FileInputStream(file);
|
||||
|
||||
byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE];
|
||||
int read;
|
||||
while ((read = fis.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, read);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (pfd != null)
|
||||
pfd.close();
|
||||
} catch (Throwable ex) {
|
||||
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
try {
|
||||
if (fos != null)
|
||||
fos.close();
|
||||
} catch (Throwable ex) {
|
||||
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
try {
|
||||
if (fis != null)
|
||||
fis.close();
|
||||
} catch (Throwable ex) {
|
||||
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
protected void onLoaded(Bundle args, Void data) {
|
||||
Toast.makeText(ActivityView.this, R.string.title_attachment_saved, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLoaded(Bundle args, Void data) {
|
||||
Toast.makeText(ActivityView.this, R.string.title_attachment_saved, Toast.LENGTH_LONG).show();
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
Helper.unexpectedError(ActivityView.this, ex);
|
||||
}
|
||||
}.load(this, args);
|
||||
}
|
||||
} else if (requestCode == REQUEST_DECRYPT) {
|
||||
if (data != null)
|
||||
try {
|
||||
decrypt(data, decryptId, decryptFile);
|
||||
} catch (Throwable ex) {
|
||||
if (ex instanceof IllegalArgumentException)
|
||||
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||
else
|
||||
Helper.unexpectedError(ActivityView.this, ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
Helper.unexpectedError(ActivityView.this, ex);
|
||||
}
|
||||
}.load(this, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1071,6 +1071,14 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
|
|||
.putExtra("from", MessageHelper.getFormattedAddresses(data.message.from, true)));
|
||||
}
|
||||
|
||||
private void onDecrypt(ActionData data) {
|
||||
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
|
||||
lbm.sendBroadcast(
|
||||
new Intent(ActivityView.ACTION_DECRYPT)
|
||||
.putExtra("id", data.message.id)
|
||||
.putExtra("to", ((InternetAddress) data.message.to[0]).getAddress()));
|
||||
}
|
||||
|
||||
private void onMore(final ActionData data) {
|
||||
boolean inOutbox = EntityFolder.OUTBOX.equals(data.message.folderType);
|
||||
boolean show_headers = properties.showHeaders(data.message.id);
|
||||
|
@ -1086,18 +1094,20 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
|
|||
popupMenu.getMenu().findItem(R.id.menu_reply_all).setEnabled(data.message.content);
|
||||
popupMenu.getMenu().findItem(R.id.menu_reply_all).setVisible(!inOutbox);
|
||||
|
||||
popupMenu.getMenu().findItem(R.id.menu_answer).setEnabled(data.message.content);
|
||||
popupMenu.getMenu().findItem(R.id.menu_answer).setVisible(!inOutbox);
|
||||
|
||||
popupMenu.getMenu().findItem(R.id.menu_unseen).setVisible(data.message.uid != null && !inOutbox);
|
||||
|
||||
popupMenu.getMenu().findItem(R.id.menu_flag).setChecked(data.message.unflagged != 1);
|
||||
popupMenu.getMenu().findItem(R.id.menu_flag).setVisible(data.message.uid != null && !inOutbox);
|
||||
|
||||
popupMenu.getMenu().findItem(R.id.menu_show_headers).setChecked(show_headers);
|
||||
popupMenu.getMenu().findItem(R.id.menu_show_headers).setVisible(data.message.uid != null);
|
||||
|
||||
popupMenu.getMenu().findItem(R.id.menu_show_html).setEnabled(data.message.content && Helper.classExists("android.webkit.WebView"));
|
||||
|
||||
popupMenu.getMenu().findItem(R.id.menu_flag).setChecked(data.message.unflagged != 1);
|
||||
popupMenu.getMenu().findItem(R.id.menu_flag).setVisible(data.message.uid != null && !inOutbox);
|
||||
|
||||
popupMenu.getMenu().findItem(R.id.menu_unseen).setVisible(data.message.uid != null && !inOutbox);
|
||||
|
||||
popupMenu.getMenu().findItem(R.id.menu_answer).setEnabled(data.message.content);
|
||||
popupMenu.getMenu().findItem(R.id.menu_answer).setVisible(!inOutbox);
|
||||
popupMenu.getMenu().findItem(R.id.menu_decrypt).setEnabled(data.message.to != null && data.message.to.length > 0);
|
||||
|
||||
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
|
@ -1112,20 +1122,23 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
|
|||
case R.id.menu_reply_all:
|
||||
onReplyAll(data);
|
||||
return true;
|
||||
case R.id.menu_answer:
|
||||
onAnswer(data);
|
||||
return true;
|
||||
case R.id.menu_unseen:
|
||||
onUnseen(data);
|
||||
return true;
|
||||
case R.id.menu_flag:
|
||||
onFlag(data);
|
||||
return true;
|
||||
case R.id.menu_show_headers:
|
||||
onShowHeaders(data);
|
||||
return true;
|
||||
case R.id.menu_show_html:
|
||||
onShowHtml(data);
|
||||
return true;
|
||||
case R.id.menu_flag:
|
||||
onFlag(data);
|
||||
return true;
|
||||
case R.id.menu_unseen:
|
||||
onUnseen(data);
|
||||
return true;
|
||||
case R.id.menu_answer:
|
||||
onAnswer(data);
|
||||
case R.id.menu_decrypt:
|
||||
onDecrypt(data);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
@ -1382,7 +1395,6 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
|
|||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
|
||||
this.contacts = (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
|
||||
== PackageManager.PERMISSION_GRANTED);
|
||||
this.avatars = (prefs.getBoolean("avatars", true) && this.contacts);
|
||||
|
|
|
@ -225,6 +225,9 @@ public interface DaoMessage {
|
|||
@Query("UPDATE message SET headers = :headers WHERE id = :id")
|
||||
int setMessageHeaders(long id, String headers);
|
||||
|
||||
@Query("UPDATE message SET stored = :stored WHERE id = :id")
|
||||
int setMessageStored(long id, long stored);
|
||||
|
||||
@Query("UPDATE message SET ui_ignored = 1" +
|
||||
" WHERE NOT ui_ignored" +
|
||||
" AND folder IN (SELECT id FROM folder WHERE type = '" + EntityFolder.INBOX + "')")
|
||||
|
|
|
@ -37,7 +37,6 @@ import javax.mail.Address;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
|
@ -116,9 +115,6 @@ public class EntityMessage implements Serializable {
|
|||
public Boolean ui_ignored;
|
||||
public String error;
|
||||
|
||||
@Ignore
|
||||
String body = null;
|
||||
|
||||
static String generateMessageId() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append('<')
|
||||
|
@ -139,9 +135,8 @@ public class EntityMessage implements Serializable {
|
|||
File file = getFile(context, id);
|
||||
BufferedWriter out = null;
|
||||
try {
|
||||
this.body = (body == null ? "" : body);
|
||||
out = new BufferedWriter(new FileWriter(file));
|
||||
out.write(this.body);
|
||||
out.write(body == null ? "" : body);
|
||||
} finally {
|
||||
if (out != null)
|
||||
try {
|
||||
|
@ -153,9 +148,7 @@ public class EntityMessage implements Serializable {
|
|||
}
|
||||
|
||||
String read(Context context) throws IOException {
|
||||
if (body == null)
|
||||
body = read(context, this.id);
|
||||
return body;
|
||||
return read(context, this.id);
|
||||
}
|
||||
|
||||
static String read(Context context, Long id) throws IOException {
|
||||
|
|
|
@ -20,6 +20,7 @@ package eu.faircode.email;
|
|||
*/
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
@ -64,8 +65,14 @@ 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.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -119,6 +126,8 @@ public class FragmentCompose extends FragmentEx {
|
|||
private long working = -1;
|
||||
private boolean autosave = false;
|
||||
|
||||
private OpenPgpServiceConnection pgpService;
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
|
@ -189,20 +198,14 @@ public class FragmentCompose extends FragmentEx {
|
|||
@Override
|
||||
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
|
||||
int action = item.getItemId();
|
||||
if (action == R.id.action_delete) {
|
||||
new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner())
|
||||
.setMessage(R.string.title_ask_discard)
|
||||
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
onAction(R.id.action_delete);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
switch (action) {
|
||||
case R.id.action_delete:
|
||||
onDelete();
|
||||
break;
|
||||
|
||||
} else
|
||||
onAction(action);
|
||||
default:
|
||||
onAction(action);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
@ -298,12 +301,19 @@ public class FragmentCompose extends FragmentEx {
|
|||
adapter = new AdapterAttachment(getContext(), getViewLifecycleOwner(), false);
|
||||
rvAttachment.setAdapter(adapter);
|
||||
|
||||
pgpService = new OpenPgpServiceConnection(getContext(), "org.sufficientlysecure.keychain");
|
||||
pgpService.bindToService();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
adapter = null;
|
||||
|
||||
if (pgpService != null)
|
||||
pgpService.unbindFromService();
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
|
@ -457,6 +467,145 @@ public class FragmentCompose extends FragmentEx {
|
|||
grpAddresses.setVisibility(grpAddresses.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void onDelete() {
|
||||
new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner())
|
||||
.setMessage(R.string.title_ask_discard)
|
||||
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
onAction(R.id.action_delete);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void onEncrypt() {
|
||||
try {
|
||||
if (!pgpService.isBound())
|
||||
throw new IllegalArgumentException(getString(R.string.title_no_openpgp));
|
||||
|
||||
String to = etTo.getText().toString();
|
||||
InternetAddress ato[] = (TextUtils.isEmpty(to) ? new InternetAddress[0] : InternetAddress.parse(to));
|
||||
if (ato.length == 0)
|
||||
throw new IllegalArgumentException(getString(R.string.title_to_missing));
|
||||
|
||||
String[] tos = new String[ato.length];
|
||||
for (int i = 0; i < ato.length; i++)
|
||||
tos[i] = ato[i].getAddress();
|
||||
|
||||
Intent data = new Intent();
|
||||
data.setAction(OpenPgpApi.ACTION_ENCRYPT);
|
||||
data.putExtra(OpenPgpApi.EXTRA_USER_IDS, tos);
|
||||
data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
|
||||
encrypt(data);
|
||||
} catch (Throwable ex) {
|
||||
if (ex instanceof IllegalArgumentException)
|
||||
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||
else
|
||||
Helper.unexpectedError(getContext(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void encrypt(Intent data) throws IOException {
|
||||
final OpenPgpApi api = new OpenPgpApi(getContext(), pgpService.getService());
|
||||
final FileInputStream msg = new FileInputStream(EntityMessage.getFile(getContext(), working));
|
||||
final ByteArrayOutputStream encrypted = new ByteArrayOutputStream();
|
||||
|
||||
final Bundle args = new Bundle();
|
||||
args.putLong("id", working);
|
||||
|
||||
api.executeApiAsync(data, msg, encrypted, new OpenPgpApi.IOpenPgpCallback() {
|
||||
@Override
|
||||
public void onReturn(Intent result) {
|
||||
Log.i(Helper.TAG, "Pgp result=" + result);
|
||||
Bundle extras = result.getExtras();
|
||||
for (String key : extras.keySet())
|
||||
Log.i(Helper.TAG, key + "=" + extras.get(key));
|
||||
|
||||
try {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
new SimpleTask<Void>() {
|
||||
@Override
|
||||
protected Void onLoad(Context context, Bundle args) throws Throwable {
|
||||
long id = args.getLong("id");
|
||||
EntityAttachment attachment = new EntityAttachment();
|
||||
|
||||
DB db = DB.getInstance(context);
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
int seq = db.attachment().getAttachmentCount(id);
|
||||
|
||||
attachment.message = id;
|
||||
attachment.sequence = seq + 1;
|
||||
attachment.name = "encrypted.asc";
|
||||
attachment.type = "application/octet-stream";
|
||||
attachment.id = db.attachment().insertAttachment(attachment);
|
||||
|
||||
File file = EntityAttachment.getFile(context, attachment.id);
|
||||
|
||||
OutputStream os = null;
|
||||
try {
|
||||
os = new BufferedOutputStream(new FileOutputStream(file));
|
||||
byte[] data = encrypted.toByteArray();
|
||||
os.write(data);
|
||||
|
||||
attachment.size = data.length;
|
||||
attachment.progress = null;
|
||||
attachment.available = true;
|
||||
db.attachment().updateAttachment(attachment);
|
||||
} finally {
|
||||
if (os != null)
|
||||
os.close();
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
Helper.unexpectedError(getContext(), ex);
|
||||
}
|
||||
}.load(FragmentCompose.this, args);
|
||||
|
||||
break;
|
||||
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
||||
startIntentSenderForResult(
|
||||
pi.getIntentSender(),
|
||||
ActivityCompose.REQUEST_ENCRYPT,
|
||||
null, 0, 0, 0, null);
|
||||
break;
|
||||
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
|
||||
throw new IllegalArgumentException(error.getMessage());
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
if (ex instanceof IllegalArgumentException)
|
||||
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||
else
|
||||
Helper.unexpectedError(getContext(), ex);
|
||||
} finally {
|
||||
try {
|
||||
msg.close();
|
||||
} catch (IOException ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
Log.i(Helper.TAG, "Compose onActivityResult request=" + requestCode + " result=" + resultCode + " data=" + data);
|
||||
|
@ -467,6 +616,16 @@ public class FragmentCompose extends FragmentEx {
|
|||
} else if (requestCode == ActivityCompose.REQUEST_ATTACHMENT) {
|
||||
if (data != null)
|
||||
handleAddAttachment(data, false);
|
||||
} else if (requestCode == ActivityCompose.REQUEST_ENCRYPT) {
|
||||
if (data != null)
|
||||
try {
|
||||
encrypt(data);
|
||||
} catch (Throwable ex) {
|
||||
if (ex instanceof IllegalArgumentException)
|
||||
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||
else
|
||||
Helper.unexpectedError(getContext(), ex);
|
||||
}
|
||||
} else {
|
||||
if (data != null)
|
||||
handlePickContact(requestCode, data);
|
||||
|
@ -626,7 +785,8 @@ public class FragmentCompose extends FragmentEx {
|
|||
actionLoader.load(this, args);
|
||||
}
|
||||
|
||||
private static EntityAttachment addAttachment(Context context, long id, Uri uri, boolean image) throws IOException {
|
||||
private static EntityAttachment addAttachment(Context context, long id, Uri uri,
|
||||
boolean image) throws IOException {
|
||||
EntityAttachment attachment = new EntityAttachment();
|
||||
|
||||
String name = null;
|
||||
|
@ -1159,7 +1319,7 @@ public class FragmentCompose extends FragmentEx {
|
|||
Toast.makeText(context, R.string.title_draft_deleted, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
} else if (action == R.id.action_save) {
|
||||
} else if (action == R.id.action_save || action == R.id.action_encrypt) {
|
||||
db.message().updateMessage(draft);
|
||||
draft.write(context, body);
|
||||
|
||||
|
@ -1250,6 +1410,9 @@ public class FragmentCompose extends FragmentEx {
|
|||
} else if (action == R.id.action_save) {
|
||||
// Do nothing
|
||||
|
||||
} else if (action == R.id.action_encrypt) {
|
||||
onEncrypt();
|
||||
|
||||
} else if (action == R.id.action_send) {
|
||||
autosave = false;
|
||||
getFragmentManager().popBackStack();
|
||||
|
|
|
@ -195,7 +195,41 @@ public class MessageHelper {
|
|||
if (message.subject != null)
|
||||
imessage.setSubject(message.subject);
|
||||
|
||||
// TODO: plain message?
|
||||
imessage.setSentDate(new Date());
|
||||
|
||||
for (final EntityAttachment attachment : attachments)
|
||||
if (attachment.available && "encrypted.asc".equals(attachment.name)) {
|
||||
Multipart multipart = new MimeMultipart("encrypted; protocol=\"application/pgp-encrypted\"");
|
||||
|
||||
BodyPart pgp = new MimeBodyPart();
|
||||
pgp.setContent("", "application/pgp-encrypted");
|
||||
multipart.addBodyPart(pgp);
|
||||
|
||||
BodyPart bpAttachment = new MimeBodyPart();
|
||||
bpAttachment.setFileName(attachment.name);
|
||||
|
||||
File file = EntityAttachment.getFile(context, attachment.id);
|
||||
FileDataSource dataSource = new FileDataSource(file);
|
||||
dataSource.setFileTypeMap(new FileTypeMap() {
|
||||
@Override
|
||||
public String getContentType(File file) {
|
||||
return attachment.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType(String filename) {
|
||||
return attachment.type;
|
||||
}
|
||||
});
|
||||
bpAttachment.setDataHandler(new DataHandler(dataSource));
|
||||
bpAttachment.setDisposition(Part.INLINE);
|
||||
|
||||
multipart.addBodyPart(bpAttachment);
|
||||
|
||||
imessage.setContent(multipart);
|
||||
|
||||
return imessage;
|
||||
}
|
||||
|
||||
String body = message.read(context);
|
||||
|
||||
|
@ -246,8 +280,6 @@ public class MessageHelper {
|
|||
imessage.setContent(multipart);
|
||||
}
|
||||
|
||||
imessage.setSentDate(new Date());
|
||||
|
||||
return imessage;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
|
||||
</vector>
|
|
@ -11,6 +11,11 @@
|
|||
android:icon="@drawable/baseline_save_alt_24"
|
||||
android:title="@string/title_save" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_encrypt"
|
||||
android:icon="@drawable/baseline_lock_24"
|
||||
android:title="@string/title_encrypt" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_send"
|
||||
android:icon="@drawable/baseline_send_24"
|
||||
|
|
|
@ -33,4 +33,8 @@
|
|||
<item
|
||||
android:id="@+id/menu_show_html"
|
||||
android:title="@string/title_show_html" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_decrypt"
|
||||
android:title="@string/title_decrypt" />
|
||||
</menu>
|
||||
|
|
|
@ -182,6 +182,7 @@
|
|||
<string name="title_reply_all">Reply to all</string>
|
||||
<string name="title_show_headers">Show headers</string>
|
||||
<string name="title_show_html">Show original</string>
|
||||
<string name="title_decrypt">Decrypt</string>
|
||||
|
||||
<string name="title_trash">Trash</string>
|
||||
<string name="title_delete">Delete</string>
|
||||
|
@ -211,6 +212,7 @@
|
|||
<string name="title_body_hint">Your message</string>
|
||||
<string name="title_discard">Discard</string>
|
||||
<string name="title_save">Save</string>
|
||||
<string name="title_encrypt">Encrypt</string>
|
||||
<string name="title_send">Send</string>
|
||||
|
||||
<string name="title_clipboard_empty">Clipboard empty</string>
|
||||
|
@ -228,6 +230,8 @@
|
|||
<string name="title_draft_saved">Draft saved</string>
|
||||
<string name="title_queued">Sending message</string>
|
||||
|
||||
<string name="title_no_openpgp">OpenPgp not found</string>
|
||||
|
||||
<string name="title_search">Search</string>
|
||||
<string name="title_search_hint">Search on server</string>
|
||||
<string name="title_searching">Searching \'%1$s\'</string>
|
||||
|
|
Loading…
Reference in New Issue