diff --git a/app/src/main/java/eu/faircode/email/ComposeHelper.java b/app/src/main/java/eu/faircode/email/ComposeHelper.java
new file mode 100644
index 0000000000..c1f214b585
--- /dev/null
+++ b/app/src/main/java/eu/faircode/email/ComposeHelper.java
@@ -0,0 +1,569 @@
+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-2023 by Marcel Bokhorst (M66B)
+*/
+
+import static android.system.OsConstants.ENOSPC;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.net.Uri;
+import android.system.ErrnoException;
+import android.text.TextUtils;
+import android.view.View;
+import android.webkit.MimeTypeMap;
+
+import androidx.annotation.NonNull;
+import androidx.documentfile.provider.DocumentFile;
+import androidx.exifinterface.media.ExifInterface;
+import androidx.preference.PreferenceManager;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.mail.Part;
+import javax.mail.internet.ContentType;
+import javax.mail.internet.ParseException;
+
+public class ComposeHelper {
+ static final int REDUCED_IMAGE_SIZE = 1440; // pixels
+
+ private static final int REDUCED_IMAGE_QUALITY = 90; // percent
+ // http://regex.info/blog/lightroom-goodies/jpeg-quality
+ private static final int COPY_ATTACHMENT_TIMEOUT = 60; // seconds
+
+ static EntityAttachment addAttachment(
+ Context context, long id, Uri uri, String type, boolean image, int resize, boolean privacy) throws IOException {
+ Log.w("Add attachment uri=" + uri + " image=" + image + " resize=" + resize + " privacy=" + privacy);
+
+ NoStreamException.check(uri, context);
+
+ EntityAttachment attachment = new EntityAttachment();
+ ComposeHelper.UriInfo info = ComposeHelper.getUriInfo(uri, context);
+
+ EntityLog.log(context, "Add attachment" +
+ " uri=" + uri + " type=" + type + " image=" + image + " resize=" + resize + " privacy=" + privacy +
+ " name=" + info.name + " type=" + info.type + " size=" + info.size);
+
+ if (type == null)
+ type = info.type;
+
+ String ext = Helper.getExtension(info.name);
+ if (info.name != null && ext == null && type != null) {
+ String guessed = MimeTypeMap.getSingleton()
+ .getExtensionFromMimeType(type.toLowerCase(Locale.ROOT));
+ if (!TextUtils.isEmpty(guessed)) {
+ ext = guessed;
+ info.name += '.' + ext;
+ }
+ }
+
+ DB db = DB.getInstance(context);
+ try {
+ db.beginTransaction();
+
+ EntityMessage draft = db.message().getMessage(id);
+ if (draft == null)
+ return null;
+
+ Log.i("Attaching to id=" + id);
+
+ attachment.message = draft.id;
+ attachment.sequence = db.attachment().getAttachmentSequence(draft.id) + 1;
+ if (privacy)
+ attachment.name = "img" + attachment.sequence + (ext == null ? "" : "." + ext);
+ else
+ attachment.name = info.name;
+ attachment.type = type;
+ attachment.disposition = (image ? Part.INLINE : Part.ATTACHMENT);
+ attachment.size = info.size;
+ attachment.progress = 0;
+
+ attachment.id = db.attachment().insertAttachment(attachment);
+ Log.i("Created attachment=" + attachment.name + ":" + attachment.sequence + " type=" + attachment.type);
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ long size = 0;
+ int lastProgress = 0;
+ try {
+ File file = attachment.getFile(context);
+
+ InputStream is = null;
+ OutputStream os = null;
+ try {
+ is = context.getContentResolver().openInputStream(uri);
+ os = new FileOutputStream(file);
+
+ if (is == null)
+ throw new FileNotFoundException(uri.toString());
+
+ final InputStream reader = is;
+ byte[] buffer = new byte[Helper.BUFFER_SIZE];
+ Callable readTask = new Callable() {
+ @Override
+ public Integer call() throws Exception {
+ return reader.read(buffer);
+ }
+ };
+
+ while (true) {
+ Future future = Helper.getDownloadTaskExecutor().submit(readTask);
+ int len = future.get(COPY_ATTACHMENT_TIMEOUT, TimeUnit.SECONDS);
+ if (len == -1)
+ break;
+ if (len == 0) {
+ Thread.sleep(500L);
+ continue;
+ }
+
+ size += len;
+ os.write(buffer, 0, len);
+
+ // Update progress
+ if (attachment.size != null && attachment.size > 0) {
+ int progress = (int) (size * 100 / attachment.size / 20 * 20);
+ if (progress != lastProgress) {
+ lastProgress = progress;
+ db.attachment().setProgress(attachment.id, progress);
+ }
+ }
+ }
+
+ if (image) {
+ attachment.cid = "<" + BuildConfig.APPLICATION_ID + "." + attachment.id + ">";
+ attachment.related = true;
+ db.attachment().setCid(attachment.id, attachment.cid, attachment.related);
+ }
+ } finally {
+ try {
+ if (is != null)
+ is.close();
+ } finally {
+ if (os != null)
+ os.close();
+ }
+ }
+
+ db.attachment().setDownloaded(attachment.id, size);
+
+ if (BuildConfig.APPLICATION_ID.equals(uri.getAuthority()) &&
+ uri.getPathSegments().size() > 0 &&
+ "photo".equals(uri.getPathSegments().get(0))) {
+ // content://eu.faircode.email/photo/nnn.jpg
+ File tmp = new File(context.getFilesDir(), uri.getPath());
+ Log.i("Deleting " + tmp);
+ Helper.secureDelete(tmp);
+ } else
+ Log.i("Authority=" + uri.getAuthority());
+
+ if (resize > 0)
+ resizeAttachment(context, attachment, resize);
+
+ if (privacy && resize == 0)
+ try {
+ ExifInterface exif = new ExifInterface(file);
+
+ exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_SPEED_REF, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_SPEED, null);
+
+ exif.setAttribute(ExifInterface.TAG_GPS_DEST_LATITUDE_REF, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_DEST_LATITUDE, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_DEST_LONGITUDE, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_DEST_BEARING_REF, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_DEST_BEARING, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_DEST_DISTANCE_REF, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_DEST_DISTANCE, null);
+
+ exif.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION_REF, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION, null);
+
+ exif.setAttribute(ExifInterface.TAG_GPS_TRACK_REF, null);
+ exif.setAttribute(ExifInterface.TAG_GPS_TRACK, null);
+
+ exif.setAttribute(ExifInterface.TAG_GPS_AREA_INFORMATION, null);
+
+ exif.setAttribute(ExifInterface.TAG_DATETIME, null);
+ exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, null);
+ exif.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, null);
+
+ exif.setAttribute(ExifInterface.TAG_XMP, null);
+ exif.setAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION, null);
+ //exif.setAttribute(ExifInterface.TAG_MAKE, null);
+ //exif.setAttribute(ExifInterface.TAG_MODEL, null);
+ //exif.setAttribute(ExifInterface.TAG_SOFTWARE, null);
+ exif.setAttribute(ExifInterface.TAG_ARTIST, null);
+ exif.setAttribute(ExifInterface.TAG_COPYRIGHT, null);
+ exif.setAttribute(ExifInterface.TAG_USER_COMMENT, null);
+ exif.setAttribute(ExifInterface.TAG_IMAGE_UNIQUE_ID, null);
+ exif.setAttribute(ExifInterface.TAG_CAMERA_OWNER_NAME, null);
+ exif.setAttribute(ExifInterface.TAG_BODY_SERIAL_NUMBER, null);
+ exif.setAttribute(ExifInterface.TAG_LENS_SERIAL_NUMBER, null);
+
+ exif.saveAttributes();
+ } catch (IOException ex) {
+ Log.i(ex);
+ }
+
+ // https://www.rfc-editor.org/rfc/rfc2231
+ if (attachment.name != null && attachment.name.length() > 60)
+ db.attachment().setWarning(attachment.id, context.getString(R.string.title_attachment_filename));
+
+ } catch (Throwable ex) {
+ // Reset progress on failure
+ Log.e(ex);
+ db.attachment().setError(attachment.id, Log.formatThrowable(ex, false));
+ return null;
+ }
+
+ return attachment;
+ }
+
+ static void resizeAttachment(Context context, EntityAttachment attachment, int resize) throws IOException {
+ File file = attachment.getFile(context);
+ if (file.exists() /* upload cancelled */ &&
+ ("image/jpeg".equals(attachment.type) ||
+ "image/png".equals(attachment.type) ||
+ "image/webp".equals(attachment.type))) {
+ ExifInterface exifSaved;
+ try {
+ exifSaved = new ExifInterface(file);
+ } catch (Throwable ex) {
+ Log.w(ex);
+ exifSaved = null;
+ }
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+
+ int factor = 1;
+ while (options.outWidth / factor > resize ||
+ options.outHeight / factor > resize)
+ factor *= 2;
+
+ Matrix rotation = ("image/jpeg".equals(attachment.type) ? ImageHelper.getImageRotation(file) : null);
+ Log.i("Image type=" + attachment.type + " rotation=" + rotation);
+ if (factor > 1 || rotation != null) {
+ options.inJustDecodeBounds = false;
+ options.inSampleSize = factor;
+
+ Log.i("Image target size=" + resize + " factor=" + factor + " source=" + options.outWidth + "x" + options.outHeight);
+ Bitmap resized = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ if (resized != null) {
+ Log.i("Image result size=" + resized.getWidth() + "x" + resized.getHeight() + " rotation=" + rotation);
+
+ if (rotation != null) {
+ Bitmap rotated = Bitmap.createBitmap(resized, 0, 0, resized.getWidth(), resized.getHeight(), rotation, true);
+ resized.recycle();
+ resized = rotated;
+ }
+
+ Bitmap.CompressFormat format;
+ if ("image/jpeg".equals(attachment.type))
+ format = Bitmap.CompressFormat.JPEG;
+ else if ("image/png".equals(attachment.type))
+ format = Bitmap.CompressFormat.PNG;
+ else if ("image/webp".equals(attachment.type))
+ format = Bitmap.CompressFormat.WEBP;
+ else
+ throw new IllegalArgumentException("Invalid format type=" + attachment.type);
+
+ File tmp = new File(file.getAbsolutePath() + ".tmp");
+ try (OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp))) {
+ if (!resized.compress(format, REDUCED_IMAGE_QUALITY, out))
+ throw new IOException("compress");
+ } catch (Throwable ex) {
+ Log.w(ex);
+ Helper.secureDelete(tmp);
+ } finally {
+ resized.recycle();
+ }
+
+ if (tmp.exists() && tmp.length() > 0) {
+ Helper.secureDelete(file);
+ tmp.renameTo(file);
+ }
+
+ DB db = DB.getInstance(context);
+ db.attachment().setDownloaded(attachment.id, file.length());
+
+ if (exifSaved != null)
+ try {
+ ExifInterface exif = new ExifInterface(file);
+
+ // Preserve time
+ if (exifSaved.hasAttribute(ExifInterface.TAG_DATETIME_ORIGINAL))
+ exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL,
+ exifSaved.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
+ if (exifSaved.hasAttribute(ExifInterface.TAG_GPS_DATESTAMP))
+ exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP,
+ exifSaved.getAttribute(ExifInterface.TAG_GPS_DATESTAMP));
+
+ // Preserve location
+ double[] latlong = exifSaved.getLatLong();
+ if (latlong != null)
+ exif.setLatLong(latlong[0], latlong[1]);
+
+ // Preserve altitude
+ if (exifSaved.hasAttribute(ExifInterface.TAG_GPS_ALTITUDE) &&
+ exifSaved.hasAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF))
+ exif.setAltitude(exifSaved.getAltitude(0));
+
+ exif.saveAttributes();
+ } catch (Throwable ex) {
+ Log.w(ex);
+ }
+ }
+ }
+ }
+ }
+
+ static void addSignature(Context context, Document document, EntityMessage draft, EntityIdentity identity) {
+ if (!draft.signature ||
+ identity == null || TextUtils.isEmpty(identity.signature))
+ return;
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ int signature_location = prefs.getInt("signature_location", 1);
+ boolean usenet = prefs.getBoolean("usenet_signature", false);
+ boolean write_below = prefs.getBoolean("write_below", false);
+ String compose_font = prefs.getString("compose_font", "");
+
+ boolean wb = (draft == null || draft.write_below == null ? write_below : draft.write_below);
+
+ Element div = document.createElement("div");
+ div.attr("fairemail", "signature");
+ if (!TextUtils.isEmpty(compose_font))
+ div.attr("style", "font-family: " + StyleHelper.getFamily(compose_font));
+
+ if (usenet) {
+ // https://datatracker.ietf.org/doc/html/rfc3676#section-4.3
+ Element span = document.createElement("span");
+ span.text("-- ");
+ span.prependElement("br");
+ span.appendElement("br");
+ div.appendChild(span);
+ }
+
+ div.append(identity.signature);
+
+ Elements ref = document.select("div[fairemail=reference]");
+ if (signature_location == 0) // top
+ document.body().prependChild(div);
+ else if (ref.size() == 0 || signature_location == 2) // bottom
+ document.body().appendChild(div);
+ else if (signature_location == 1) // below text
+ if (wb && draft.wasforwardedfrom == null)
+ document.body().appendChild(div);
+ else
+ ref.first().before(div);
+ }
+
+ static void handleException(FragmentBase fragment, View view, Throwable ex) {
+ // External app sending absolute file
+ if (ex instanceof NoStreamException)
+ ((NoStreamException) ex).report(fragment.getActivity());
+ else if (ex instanceof FileNotFoundException ||
+ ex instanceof IllegalArgumentException ||
+ ex instanceof IllegalStateException) {
+ /*
+ java.lang.IllegalStateException: Failed to mount
+ at android.os.Parcel.createException(Parcel.java:2079)
+ at android.os.Parcel.readException(Parcel.java:2039)
+ at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:188)
+ at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:151)
+ at android.content.ContentProviderProxy.openTypedAssetFile(ContentProviderNative.java:705)
+ at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1687)
+ at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1503)
+ at android.content.ContentResolver.openInputStream(ContentResolver.java:1187)
+ at eu.faircode.email.FragmentCompose.addAttachment(SourceFile:27)
+ */
+ Snackbar.make(view, ex.toString(), Snackbar.LENGTH_LONG)
+ .setGestureInsetBottomIgnored(true).show();
+ } else {
+ if (ex instanceof IOException &&
+ ex.getCause() instanceof ErrnoException &&
+ ((ErrnoException) ex.getCause()).errno == ENOSPC)
+ ex = new IOException(fragment.getContext().getString(R.string.app_cake), ex);
+
+ // External app didn't grant URI permissions
+ if (ex instanceof SecurityException)
+ ex = new Throwable(fragment.getString(R.string.title_no_permissions), ex);
+
+ Log.unexpectedError(fragment, ex,
+ !(ex instanceof IOException || ex.getCause() instanceof IOException));
+ /*
+ java.lang.IllegalStateException: java.io.IOException: Failed to redact /storage/emulated/0/Download/97203830-piston-vecteur-icĂ´ne-simple-symbole-plat-sur-fond-blanc.jpg
+ at android.os.Parcel.createExceptionOrNull(Parcel.java:2381)
+ at android.os.Parcel.createException(Parcel.java:2357)
+ at android.os.Parcel.readException(Parcel.java:2340)
+ at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:190)
+ at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:153)
+ at android.content.ContentProviderProxy.openTypedAssetFile(ContentProviderNative.java:804)
+ at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:2002)
+ at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1817)
+ at android.content.ContentResolver.openInputStream(ContentResolver.java:1494)
+ at eu.faircode.email.FragmentCompose.addAttachment(SourceFile:27)
+ */
+ }
+ }
+
+ static void noStorageAccessFramework(View view) {
+ Snackbar snackbar = Snackbar.make(view, R.string.title_no_saf, Snackbar.LENGTH_LONG)
+ .setGestureInsetBottomIgnored(true);
+ snackbar.setAction(R.string.title_fix, new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Helper.viewFAQ(v.getContext(), 25);
+ }
+ });
+ snackbar.show();
+ }
+
+ static List getUris(Intent data) {
+ List result = new ArrayList<>();
+
+ ClipData clipData = data.getClipData();
+ if (clipData == null) {
+ Uri uri = data.getData();
+ if (uri != null)
+ result.add(uri);
+ } else {
+ for (int i = 0; i < clipData.getItemCount(); i++) {
+ ClipData.Item item = clipData.getItemAt(i);
+ Uri uri = item.getUri();
+ if (uri != null)
+ result.add(uri);
+ }
+ }
+
+ // media-uri-list=[content://media/external_primary/images/media/nnn] (ArrayList)
+ // media-file-list=[/storage/emulated/0/Pictures/...]
+ // (ArrayList) media-id-list=[nnn] (ArrayList)
+ if (result.size() == 0 && data.hasExtra("media-uri-list"))
+ try {
+ List uris = data.getParcelableArrayListExtra("media-uri-list");
+ result.addAll(uris);
+ } catch (Throwable ex) {
+ Log.e(ex);
+ }
+
+ return result;
+ }
+
+ @NonNull
+ static UriInfo getUriInfo(Uri uri, Context context) {
+ UriInfo result = new UriInfo();
+
+ // https://stackoverflow.com/questions/76094229/android-13-photo-video-picker-file-name-from-the-uri-is-garbage
+ DocumentFile dfile = null;
+ try {
+ dfile = DocumentFile.fromSingleUri(context, uri);
+ if (dfile != null) {
+ result.name = dfile.getName();
+ result.type = dfile.getType();
+ result.size = dfile.length();
+ EntityLog.log(context, "UriInfo dfile " + result + " uri=" + uri);
+ }
+ } catch (Throwable ex) {
+ Log.e(ex);
+ }
+
+ // Check name
+ if (TextUtils.isEmpty(result.name))
+ result.name = uri.getLastPathSegment();
+
+ // Check type
+ if (!TextUtils.isEmpty(result.type))
+ try {
+ new ContentType(result.type);
+ } catch (ParseException ex) {
+ Log.w(new Throwable(result.type, ex));
+ result.type = null;
+ }
+
+ if (TextUtils.isEmpty(result.type) ||
+ "*/*".equals(result.type) ||
+ "application/*".equals(result.type) ||
+ "application/octet-stream".equals(result.type))
+ result.type = Helper.guessMimeType(result.name);
+
+ if (result.size != null && result.size <= 0)
+ result.size = null;
+
+ EntityLog.log(context, "UriInfo result " + result + " uri=" + uri);
+
+ return result;
+ }
+
+ static class UriInfo {
+ String name;
+ String type;
+ Long size;
+
+ boolean isImage() {
+ return ImageHelper.isImage(type);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "name=" + name + " type=" + type + " size=" + size;
+ }
+ }
+
+ static class DraftData {
+ EntityMessage draft;
+ List identities;
+ }
+}
diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java
index 70709263a2..e1a4c0d716 100644
--- a/app/src/main/java/eu/faircode/email/FragmentCompose.java
+++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java
@@ -39,11 +39,8 @@ import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.database.Cursor;
import android.database.MatrixCursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.ImageDecoder;
-import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
@@ -51,7 +48,6 @@ import android.graphics.drawable.GradientDrawable;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
-import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.net.Uri;
import android.os.Build;
@@ -90,7 +86,6 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
-import android.webkit.MimeTypeMap;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.CompoundButton;
@@ -122,8 +117,6 @@ import androidx.core.graphics.ColorUtils;
import androidx.core.view.MenuCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.cursoradapter.widget.SimpleCursorAdapter;
-import androidx.documentfile.provider.DocumentFile;
-import androidx.exifinterface.media.ExifInterface;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
@@ -166,7 +159,6 @@ import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.util.OpenPgpApi;
-import org.w3c.dom.css.CSSStyleSheet;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@@ -179,7 +171,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
-import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
@@ -187,7 +178,6 @@ import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.Collator;
-import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -201,15 +191,10 @@ import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.UUID;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Pattern;
import javax.activation.DataHandler;
import javax.mail.Address;
import javax.mail.BodyPart;
-import javax.mail.Message;
import javax.mail.MessageRemovedException;
import javax.mail.MessagingException;
import javax.mail.Multipart;
@@ -222,13 +207,8 @@ import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;
-import javax.mail.internet.ParseException;
import javax.mail.util.ByteArrayDataSource;
-import biweekly.ICalendar;
-import biweekly.component.VEvent;
-import biweekly.property.Organizer;
-
public class FragmentCompose extends FragmentBase {
private enum State {NONE, LOADING, LOADED}
@@ -319,14 +299,6 @@ public class FragmentCompose extends FragmentBase {
private int searchIndex = 0;
- static final int REDUCED_IMAGE_SIZE = 1440; // pixels
- private static final int REDUCED_IMAGE_QUALITY = 90; // percent
- // http://regex.info/blog/lightroom-goodies/jpeg-quality
- private static final int COPY_ATTACHMENT_TIMEOUT = 60; // seconds
-
- private static final int MAX_QUOTE_LEVEL = 5;
- private static final int MAX_REASONABLE_SIZE = 5 * 1024 * 1024;
-
private static final int REQUEST_CONTACT_TO = 1;
private static final int REQUEST_CONTACT_CC = 2;
private static final int REQUEST_CONTACT_BCC = 3;
@@ -637,7 +609,7 @@ public class FragmentCompose extends FragmentBase {
public void onInputContent(Uri uri, String type) {
Log.i("Received input uri=" + uri);
boolean resize_paste = prefs.getBoolean("resize_paste", true);
- int resize = prefs.getInt("resize", FragmentCompose.REDUCED_IMAGE_SIZE);
+ int resize = prefs.getInt("resize", ComposeHelper.REDUCED_IMAGE_SIZE);
onAddAttachment(
Arrays.asList(uri),
type == null ? null : new String[]{type},
@@ -3073,23 +3045,11 @@ public class FragmentCompose extends FragmentBase {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
PackageManager pm = getContext().getPackageManager();
if (intent.resolveActivity(pm) == null) // system whitelisted
- noStorageAccessFramework();
+ ComposeHelper.noStorageAccessFramework(view);
else
startActivityForResult(Helper.getChooser(getContext(), intent), REQUEST_ATTACHMENT);
}
- private void noStorageAccessFramework() {
- Snackbar snackbar = Snackbar.make(view, R.string.title_no_saf, Snackbar.LENGTH_LONG)
- .setGestureInsetBottomIgnored(true);
- snackbar.setAction(R.string.title_fix, new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Helper.viewFAQ(v.getContext(), 25);
- }
- });
- snackbar.show();
- }
-
private void onActionLink() {
FragmentDialogInsertLink fragment = new FragmentDialogInsertLink();
fragment.setArguments(FragmentDialogInsertLink.getArguments(etBody));
@@ -3268,7 +3228,7 @@ public class FragmentCompose extends FragmentBase {
break;
case REQUEST_IMAGE_FILE:
if (resultCode == RESULT_OK && data != null)
- onAddImageFile(getUris(data), false);
+ onAddImageFile(ComposeHelper.getUris(data), false);
break;
case REQUEST_TAKE_PHOTO:
if (resultCode == RESULT_OK) {
@@ -3279,7 +3239,7 @@ public class FragmentCompose extends FragmentBase {
case REQUEST_ATTACHMENT:
case REQUEST_RECORD_AUDIO:
if (resultCode == RESULT_OK && data != null)
- onAddAttachment(getUris(data), null, false, 0, false, false);
+ onAddAttachment(ComposeHelper.getUris(data), null, false, 0, false, false);
break;
case REQUEST_OPENPGP:
if (resultCode == RESULT_OK && data != null)
@@ -3605,7 +3565,7 @@ public class FragmentCompose extends FragmentBase {
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
if (intent.resolveActivity(pm) == null) // GET_CONTENT whitelisted
- noStorageAccessFramework();
+ ComposeHelper.noStorageAccessFramework(view);
else
startActivityForResult(Helper.getChooser(context, intent), REQUEST_IMAGE_FILE);
}
@@ -3616,7 +3576,7 @@ public class FragmentCompose extends FragmentBase {
boolean add_inline = prefs.getBoolean("add_inline", true);
boolean resize_images = prefs.getBoolean("resize_images", true);
boolean privacy_images = prefs.getBoolean("privacy_images", false);
- int resize = prefs.getInt("resize", FragmentCompose.REDUCED_IMAGE_SIZE);
+ int resize = prefs.getInt("resize", ComposeHelper.REDUCED_IMAGE_SIZE);
onAddAttachment(uri, null, add_inline, resize_images ? resize : 0, privacy_images, focus);
}
@@ -3658,7 +3618,7 @@ public class FragmentCompose extends FragmentBase {
Uri uri = uris.get(i);
String type = (types != null && i < types.length ? types[i] : null);
- EntityAttachment attachment = addAttachment(context, id, uri, type, image, resize, privacy);
+ EntityAttachment attachment = ComposeHelper.addAttachment(context, id, uri, type, image, resize, privacy);
if (attachment == null)
continue;
if (!image || !attachment.isImage())
@@ -3721,7 +3681,7 @@ public class FragmentCompose extends FragmentBase {
doc.body().appendChild(e);
EntityIdentity identity = db.identity().getIdentity(draft.identity);
- addSignature(context, doc, draft, identity);
+ ComposeHelper.addSignature(context, doc, draft, identity);
Helper.writeText(file, doc.html());
}
@@ -3755,7 +3715,7 @@ public class FragmentCompose extends FragmentBase {
@Override
protected void onException(Bundle args, Throwable ex) {
- handleException(ex);
+ ComposeHelper.handleException(FragmentCompose.this, view, ex);
}
}.serial().execute(this, args, "compose:attachment:add");
}
@@ -3774,11 +3734,11 @@ public class FragmentCompose extends FragmentBase {
ArrayList images = new ArrayList<>();
for (Uri uri : uris)
try {
- UriInfo info = getInfo(uri, context);
+ ComposeHelper.UriInfo info = ComposeHelper.getUriInfo(uri, context);
if (info.isImage())
images.add(uri);
else
- addAttachment(context, id, uri, null, false, 0, false);
+ ComposeHelper.addAttachment(context, id, uri, null, false, 0, false);
} catch (IOException ex) {
Log.e(ex);
}
@@ -3811,42 +3771,11 @@ public class FragmentCompose extends FragmentBase {
@Override
protected void onException(Bundle args, Throwable ex) {
- handleException(ex);
+ ComposeHelper.handleException(FragmentCompose.this, view, ex);
}
}.serial().execute(this, args, "compose:shared");
}
- private List getUris(Intent data) {
- List result = new ArrayList<>();
-
- ClipData clipData = data.getClipData();
- if (clipData == null) {
- Uri uri = data.getData();
- if (uri != null)
- result.add(uri);
- } else {
- for (int i = 0; i < clipData.getItemCount(); i++) {
- ClipData.Item item = clipData.getItemAt(i);
- Uri uri = item.getUri();
- if (uri != null)
- result.add(uri);
- }
- }
-
- // media-uri-list=[content://media/external_primary/images/media/nnn] (ArrayList)
- // media-file-list=[/storage/emulated/0/Pictures/...]
- // (ArrayList) media-id-list=[nnn] (ArrayList)
- if (result.size() == 0 && data.hasExtra("media-uri-list"))
- try {
- List uris = data.getParcelableArrayListExtra("media-uri-list");
- result.addAll(uris);
- } catch (Throwable ex) {
- Log.e(ex);
- }
-
- return result;
- }
-
private void onPgp(Intent data) {
final Bundle args = new Bundle();
args.putParcelable("data", data);
@@ -5005,1398 +4934,187 @@ public class FragmentCompose extends FragmentBase {
args.putBundle("extras", extras);
Log.i("Run execute id=" + working + " reason=" + reason);
- actionLoader.execute(this, args, "compose:action:" + getActionName(action));
+ actionLoader.execute(this, args, "compose:action:" + LoaderComposeAction.getActionName(action));
}
- private static EntityAttachment addAttachment(
- Context context, long id, Uri uri, String type, boolean image, int resize, boolean privacy) throws IOException {
- Log.w("Add attachment uri=" + uri + " image=" + image + " resize=" + resize + " privacy=" + privacy);
-
- NoStreamException.check(uri, context);
-
- EntityAttachment attachment = new EntityAttachment();
- UriInfo info = getInfo(uri, context);
-
- EntityLog.log(context, "Add attachment" +
- " uri=" + uri + " type=" + type + " image=" + image + " resize=" + resize + " privacy=" + privacy +
- " name=" + info.name + " type=" + info.type + " size=" + info.size);
-
- if (type == null)
- type = info.type;
+ private final SimpleTask draftLoader = new LoaderComposeDraft() {
+ @Override
+ protected void onExecuted(Bundle args, final ComposeHelper.DraftData data) {
+ final String action = getArguments().getString("action");
+ Log.i("Loaded draft id=" + data.draft.id + " action=" + action);
- String ext = Helper.getExtension(info.name);
- if (info.name != null && ext == null && type != null) {
- String guessed = MimeTypeMap.getSingleton()
- .getExtensionFromMimeType(type.toLowerCase(Locale.ROOT));
- if (!TextUtils.isEmpty(guessed)) {
- ext = guessed;
- info.name += '.' + ext;
+ FragmentActivity activity = getActivity();
+ if (activity != null) {
+ Intent intent = activity.getIntent();
+ if (intent != null) {
+ intent.putExtra("id", data.draft.id);
+ intent.putExtra("action", "edit");
+ }
}
- }
-
- DB db = DB.getInstance(context);
- try {
- db.beginTransaction();
-
- EntityMessage draft = db.message().getMessage(id);
- if (draft == null)
- return null;
-
- Log.i("Attaching to id=" + id);
-
- attachment.message = draft.id;
- attachment.sequence = db.attachment().getAttachmentSequence(draft.id) + 1;
- if (privacy)
- attachment.name = "img" + attachment.sequence + (ext == null ? "" : "." + ext);
- else
- attachment.name = info.name;
- attachment.type = type;
- attachment.disposition = (image ? Part.INLINE : Part.ATTACHMENT);
- attachment.size = info.size;
- attachment.progress = 0;
-
- attachment.id = db.attachment().insertAttachment(attachment);
- Log.i("Created attachment=" + attachment.name + ":" + attachment.sequence + " type=" + attachment.type);
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- long size = 0;
- int lastProgress = 0;
- try {
- File file = attachment.getFile(context);
- InputStream is = null;
- OutputStream os = null;
- try {
- is = context.getContentResolver().openInputStream(uri);
- os = new FileOutputStream(file);
+ working = data.draft.id;
+ dsn = (data.draft.dsn != null && !EntityMessage.DSN_NONE.equals(data.draft.dsn));
+ encrypt = data.draft.ui_encrypt;
+ invalidateOptionsMenu();
- if (is == null)
- throw new FileNotFoundException(uri.toString());
+ subject = data.draft.subject;
+ saved = args.getBoolean("saved");
- final InputStream reader = is;
- byte[] buffer = new byte[Helper.BUFFER_SIZE];
- Callable readTask = new Callable() {
- @Override
- public Integer call() throws Exception {
- return reader.read(buffer);
- }
- };
+ // Show identities
+ AdapterIdentitySelect iadapter = new AdapterIdentitySelect(getContext(), data.identities);
+ spIdentity.setAdapter(iadapter);
- while (true) {
- Future future = Helper.getDownloadTaskExecutor().submit(readTask);
- int len = future.get(COPY_ATTACHMENT_TIMEOUT, TimeUnit.SECONDS);
- if (len == -1)
+ // Select identity
+ if (data.draft.identity != null)
+ for (int pos = 0; pos < data.identities.size(); pos++) {
+ if (data.identities.get(pos).id.equals(data.draft.identity)) {
+ spIdentity.setTag(pos);
+ spIdentity.setSelection(pos);
break;
- if (len == 0) {
- Thread.sleep(500L);
- continue;
- }
-
- size += len;
- os.write(buffer, 0, len);
-
- // Update progress
- if (attachment.size != null && attachment.size > 0) {
- int progress = (int) (size * 100 / attachment.size / 20 * 20);
- if (progress != lastProgress) {
- lastProgress = progress;
- db.attachment().setProgress(attachment.id, progress);
- }
}
}
- if (image) {
- attachment.cid = "<" + BuildConfig.APPLICATION_ID + "." + attachment.id + ">";
- attachment.related = true;
- db.attachment().setCid(attachment.id, attachment.cid, attachment.related);
- }
- } finally {
- try {
- if (is != null)
- is.close();
- } finally {
- if (os != null)
- os.close();
- }
- }
-
- db.attachment().setDownloaded(attachment.id, size);
-
- if (BuildConfig.APPLICATION_ID.equals(uri.getAuthority()) &&
- uri.getPathSegments().size() > 0 &&
- "photo".equals(uri.getPathSegments().get(0))) {
- // content://eu.faircode.email/photo/nnn.jpg
- File tmp = new File(context.getFilesDir(), uri.getPath());
- Log.i("Deleting " + tmp);
- Helper.secureDelete(tmp);
- } else
- Log.i("Authority=" + uri.getAuthority());
-
- if (resize > 0)
- resizeAttachment(context, attachment, resize);
-
- if (privacy && resize == 0)
- try {
- ExifInterface exif = new ExifInterface(file);
-
- exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, null);
- exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE, null);
- exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, null);
- exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, null);
- exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, null);
- exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, null);
- exif.setAttribute(ExifInterface.TAG_GPS_SPEED_REF, null);
- exif.setAttribute(ExifInterface.TAG_GPS_SPEED, null);
-
- exif.setAttribute(ExifInterface.TAG_GPS_DEST_LATITUDE_REF, null);
- exif.setAttribute(ExifInterface.TAG_GPS_DEST_LATITUDE, null);
- exif.setAttribute(ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, null);
- exif.setAttribute(ExifInterface.TAG_GPS_DEST_LONGITUDE, null);
- exif.setAttribute(ExifInterface.TAG_GPS_DEST_BEARING_REF, null);
- exif.setAttribute(ExifInterface.TAG_GPS_DEST_BEARING, null);
- exif.setAttribute(ExifInterface.TAG_GPS_DEST_DISTANCE_REF, null);
- exif.setAttribute(ExifInterface.TAG_GPS_DEST_DISTANCE, null);
-
- exif.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION_REF, null);
- exif.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION, null);
-
- exif.setAttribute(ExifInterface.TAG_GPS_TRACK_REF, null);
- exif.setAttribute(ExifInterface.TAG_GPS_TRACK, null);
-
- exif.setAttribute(ExifInterface.TAG_GPS_AREA_INFORMATION, null);
-
- exif.setAttribute(ExifInterface.TAG_DATETIME, null);
- exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, null);
- exif.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, null);
-
- exif.setAttribute(ExifInterface.TAG_XMP, null);
- exif.setAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION, null);
- //exif.setAttribute(ExifInterface.TAG_MAKE, null);
- //exif.setAttribute(ExifInterface.TAG_MODEL, null);
- //exif.setAttribute(ExifInterface.TAG_SOFTWARE, null);
- exif.setAttribute(ExifInterface.TAG_ARTIST, null);
- exif.setAttribute(ExifInterface.TAG_COPYRIGHT, null);
- exif.setAttribute(ExifInterface.TAG_USER_COMMENT, null);
- exif.setAttribute(ExifInterface.TAG_IMAGE_UNIQUE_ID, null);
- exif.setAttribute(ExifInterface.TAG_CAMERA_OWNER_NAME, null);
- exif.setAttribute(ExifInterface.TAG_BODY_SERIAL_NUMBER, null);
- exif.setAttribute(ExifInterface.TAG_LENS_SERIAL_NUMBER, null);
-
- exif.saveAttributes();
- } catch (IOException ex) {
- Log.i(ex);
- }
-
- // https://www.rfc-editor.org/rfc/rfc2231
- if (attachment.name != null && attachment.name.length() > 60)
- db.attachment().setWarning(attachment.id, context.getString(R.string.title_attachment_filename));
-
- } catch (Throwable ex) {
- // Reset progress on failure
- Log.e(ex);
- db.attachment().setError(attachment.id, Log.formatThrowable(ex, false));
- return null;
- }
-
- return attachment;
- }
+ etExtra.setText(data.draft.extra);
+ etTo.setText(MessageHelper.formatAddressesCompose(data.draft.to));
+ etCc.setText(MessageHelper.formatAddressesCompose(data.draft.cc));
+ etBcc.setText(MessageHelper.formatAddressesCompose(data.draft.bcc));
+ etSubject.setText(data.draft.subject);
- private static void resizeAttachment(Context context, EntityAttachment attachment, int resize) throws IOException {
- File file = attachment.getFile(context);
- if (file.exists() /* upload cancelled */ &&
- ("image/jpeg".equals(attachment.type) ||
- "image/png".equals(attachment.type) ||
- "image/webp".equals(attachment.type))) {
- ExifInterface exifSaved;
- try {
- exifSaved = new ExifInterface(file);
- } catch (Throwable ex) {
- Log.w(ex);
- exifSaved = null;
- }
-
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(file.getAbsolutePath(), options);
-
- int factor = 1;
- while (options.outWidth / factor > resize ||
- options.outHeight / factor > resize)
- factor *= 2;
-
- Matrix rotation = ("image/jpeg".equals(attachment.type) ? ImageHelper.getImageRotation(file) : null);
- Log.i("Image type=" + attachment.type + " rotation=" + rotation);
- if (factor > 1 || rotation != null) {
- options.inJustDecodeBounds = false;
- options.inSampleSize = factor;
-
- Log.i("Image target size=" + resize + " factor=" + factor + " source=" + options.outWidth + "x" + options.outHeight);
- Bitmap resized = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
- if (resized != null) {
- Log.i("Image result size=" + resized.getWidth() + "x" + resized.getHeight() + " rotation=" + rotation);
-
- if (rotation != null) {
- Bitmap rotated = Bitmap.createBitmap(resized, 0, 0, resized.getWidth(), resized.getHeight(), rotation, true);
- resized.recycle();
- resized = rotated;
- }
+ long reference = args.getLong("reference", -1);
+ etTo.setTag(reference < 0 ? "" : etTo.getText().toString());
+ etSubject.setTag(reference < 0 ? "" : etSubject.getText().toString());
+ cbSignature.setTag(data.draft.signature);
- Bitmap.CompressFormat format;
- if ("image/jpeg".equals(attachment.type))
- format = Bitmap.CompressFormat.JPEG;
- else if ("image/png".equals(attachment.type))
- format = Bitmap.CompressFormat.PNG;
- else if ("image/webp".equals(attachment.type))
- format = Bitmap.CompressFormat.WEBP;
- else
- throw new IllegalArgumentException("Invalid format type=" + attachment.type);
+ grpHeader.setVisibility(View.VISIBLE);
+ if ("reply_all".equals(action) ||
+ (data.draft.cc != null && data.draft.cc.length > 0) ||
+ (data.draft.bcc != null && data.draft.bcc.length > 0)) {
+ ibCcBcc.setImageLevel(0);
+ grpAddresses.setVisibility(View.VISIBLE);
+ }
+ ibCcBcc.setVisibility(View.VISIBLE);
- File tmp = new File(file.getAbsolutePath() + ".tmp");
- try (OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp))) {
- if (!resized.compress(format, REDUCED_IMAGE_QUALITY, out))
- throw new IOException("compress");
- } catch (Throwable ex) {
- Log.w(ex);
- Helper.secureDelete(tmp);
- } finally {
- resized.recycle();
- }
+ bottom_navigation.getMenu().findItem(R.id.action_undo).setVisible(data.draft.revision > 1);
+ bottom_navigation.getMenu().findItem(R.id.action_redo).setVisible(data.draft.revision < data.draft.revisions);
- if (tmp.exists() && tmp.length() > 0) {
- Helper.secureDelete(file);
- tmp.renameTo(file);
+ if (args.getBoolean("incomplete")) {
+ final Snackbar snackbar = Snackbar.make(
+ view, R.string.title_attachments_incomplete, Snackbar.LENGTH_INDEFINITE)
+ .setGestureInsetBottomIgnored(true);
+ snackbar.setAction(android.R.string.ok, new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ snackbar.dismiss();
}
-
- DB db = DB.getInstance(context);
- db.attachment().setDownloaded(attachment.id, file.length());
-
- if (exifSaved != null)
- try {
- ExifInterface exif = new ExifInterface(file);
-
- // Preserve time
- if (exifSaved.hasAttribute(ExifInterface.TAG_DATETIME_ORIGINAL))
- exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL,
- exifSaved.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
- if (exifSaved.hasAttribute(ExifInterface.TAG_GPS_DATESTAMP))
- exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP,
- exifSaved.getAttribute(ExifInterface.TAG_GPS_DATESTAMP));
-
- // Preserve location
- double[] latlong = exifSaved.getLatLong();
- if (latlong != null)
- exif.setLatLong(latlong[0], latlong[1]);
-
- // Preserve altitude
- if (exifSaved.hasAttribute(ExifInterface.TAG_GPS_ALTITUDE) &&
- exifSaved.hasAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF))
- exif.setAltitude(exifSaved.getAltitude(0));
-
- exif.saveAttributes();
- } catch (Throwable ex) {
- Log.w(ex);
- }
- }
+ });
+ snackbar.show();
}
- }
- }
-
- private SimpleTask draftLoader = new SimpleTask() {
- @Override
- protected DraftData onExecute(Context context, Bundle args) throws Throwable {
- String action = args.getString("action");
- long id = args.getLong("id", -1);
- long aid = args.getLong("account", -1);
- long iid = args.getLong("identity", -1);
- long reference = args.getLong("reference", -1);
- int dsn = args.getInt("dsn", EntityMessage.DSN_RECEIPT);
- File ics = (File) args.getSerializable("ics");
- String status = args.getString("status");
- // raw
- long answer = args.getLong("answer", -1);
- String to = args.getString("to");
- String cc = args.getString("cc");
- String bcc = args.getString("bcc");
- // inreplyto
- String external_subject = args.getString("subject", "");
- String external_body = args.getString("body", "");
- String external_text = args.getString("text");
- CharSequence selected_text = args.getCharSequence("selected");
- ArrayList uris = args.getParcelableArrayList("attachments");
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean plain_only = prefs.getBoolean("plain_only", false);
- boolean plain_only_reply = prefs.getBoolean("plain_only_reply", false);
- boolean resize_reply = prefs.getBoolean("resize_reply", true);
- boolean sign_default = prefs.getBoolean("sign_default", false);
- boolean encrypt_default = prefs.getBoolean("encrypt_default", false);
- boolean receipt_default = prefs.getBoolean("receipt_default", false);
- boolean write_below = prefs.getBoolean("write_below", false);
- boolean save_drafts = prefs.getBoolean("save_drafts", true);
- boolean auto_identity = prefs.getBoolean("auto_identity", false);
- boolean suggest_sent = prefs.getBoolean("suggest_sent", true);
- boolean suggest_received = prefs.getBoolean("suggest_received", false);
- boolean forward_new = prefs.getBoolean("forward_new", true);
-
- Log.i("Load draft action=" + action + " id=" + id + " reference=" + reference);
-
- Map crumb = new HashMap<>();
- crumb.put("draft", Long.toString(id));
- crumb.put("reference", Long.toString(reference));
- crumb.put("action", action);
- Log.breadcrumb("compose", crumb);
-
- DraftData data = new DraftData();
-
- DB db = DB.getInstance(context);
- try {
- db.beginTransaction();
- data.identities = db.identity().getComposableIdentities(null);
- if (data.identities == null || data.identities.size() == 0)
- throw new OperationCanceledException(context.getString(R.string.title_no_composable));
+ DB db = DB.getInstance(getContext());
- data.draft = db.message().getMessage(id);
- boolean wb = (data.draft == null || data.draft.write_below == null ? write_below : data.draft.write_below);
- if (data.draft == null || data.draft.ui_hide) {
- // New draft
- if ("edit".equals(action))
- throw new MessageRemovedException("Draft for edit was deleted hide=" + (data.draft != null));
+ db.attachment().liveAttachments(data.draft.id).observe(getViewLifecycleOwner(),
+ new Observer>() {
+ private Integer count = null;
- EntityMessage ref = db.message().getMessage(reference);
+ @Override
+ public void onChanged(@Nullable List attachments) {
+ if (attachments == null)
+ attachments = new ArrayList<>();
- data.draft = new EntityMessage();
- data.draft.msgid = EntityMessage.generateMessageId();
+ List a = new ArrayList<>(attachments);
+ rvAttachment.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (adapter != null)
+ adapter.set(a);
+ } catch (Throwable ex) {
+ Log.e(ex);
+ /*
+ java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling androidx.recyclerview.widget.RecyclerView{f9baa84 VFED..... ........ 0,245-720,1445 #7f0a03fd app:id/rvAttachment}, adapter:eu.faircode.email.AdapterAttachment@954026d, layout:androidx.recyclerview.widget.LinearLayoutManager@ed06ea2, context:eu.faircode.email.ActivityCompose@d14c627
+ at androidx.recyclerview.widget.RecyclerView.assertNotInLayoutOrScroll(SourceFile:3)
+ at androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver.onItemRangeChanged(SourceFile:1)
+ at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeChanged(SourceFile:2)
+ at androidx.recyclerview.widget.RecyclerView$Adapter.notifyItemRangeChanged(SourceFile:3)
+ at androidx.recyclerview.widget.AdapterListUpdateCallback.onChanged(SourceFile:1)
+ at androidx.recyclerview.widget.BatchingListUpdateCallback.dispatchLastEvent(SourceFile:2)
+ at androidx.recyclerview.widget.DiffUtil$DiffResult.dispatchUpdatesTo(SourceFile:36)
+ at eu.faircode.email.AdapterAttachment.set(SourceFile:6)
+ at eu.faircode.email.FragmentCompose$38$3.onChanged(SourceFile:3)
+ at eu.faircode.email.FragmentCompose$38$3.onChanged(SourceFile:1)
+ at androidx.lifecycle.LiveData.considerNotify(SourceFile:6)
+ at androidx.lifecycle.LiveData.dispatchingValue(SourceFile:8)
+ at androidx.lifecycle.LiveData.setValue(SourceFile:4)
+ at androidx.lifecycle.LiveData$1.run(SourceFile:5)
+ at android.os.Handler.handleCallback(Handler.java:751)
+ */
+ }
+ }
+ });
- // Select identity matching from address
- EntityIdentity selected = null;
+ ibRemoveAttachments.setVisibility(attachments.size() > 2 ? View.VISIBLE : View.GONE);
+ grpAttachments.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
- if (aid < 0)
- if (ref == null) {
- EntityAccount primary = db.account().getPrimaryAccount();
- if (primary != null)
- aid = primary.id;
- } else
- aid = ref.account;
- if (iid < 0 && ref != null && ref.identity != null)
- iid = ref.identity;
-
- if (iid >= 0)
- for (EntityIdentity identity : data.identities)
- if (identity.id.equals(iid)) {
- selected = identity;
- EntityLog.log(context, "Selected requested identity=" + iid);
- break;
+ boolean downloading = false;
+ for (EntityAttachment attachment : attachments) {
+ if (attachment.isEncryption())
+ continue;
+ if (attachment.progress != null)
+ downloading = true;
}
- if (ref != null) {
- Address[] refto;
- boolean self = ref.replySelf(data.identities, ref.account);
- if (ref.to == null || ref.to.length == 0 || self)
- refto = ref.from;
- else
- refto = ref.to;
- Log.i("Ref self=" + self +
- " to=" + MessageHelper.formatAddresses(refto));
- if (refto != null && refto.length > 0) {
- if (selected == null)
- for (Address sender : refto)
- for (EntityIdentity identity : data.identities)
- if (identity.account.equals(aid) &&
- identity.sameAddress(sender)) {
- selected = identity;
- EntityLog.log(context, "Selected same account/identity");
- break;
- }
+ Log.i("Attachments=" + attachments.size() + " downloading=" + downloading);
- if (selected == null)
- for (Address sender : refto)
- for (EntityIdentity identity : data.identities)
- if (identity.account.equals(aid) &&
- identity.similarAddress(sender)) {
- selected = identity;
- EntityLog.log(context, "Selected similar account/identity");
- break;
- }
+ rvAttachment.setTag(downloading);
+ checkInternet();
- if (selected == null)
- for (Address sender : refto)
- for (EntityIdentity identity : data.identities)
- if (identity.sameAddress(sender)) {
- selected = identity;
- EntityLog.log(context, "Selected same */identity");
- break;
- }
+ if (count != null && count > attachments.size()) {
+ boolean updated = false;
+ Editable edit = etBody.getEditableText();
- if (selected == null)
- for (Address sender : refto)
- for (EntityIdentity identity : data.identities)
- if (identity.similarAddress(sender)) {
- selected = identity;
- EntityLog.log(context, "Selected similer */identity");
- break;
- }
- }
- }
+ ImageSpan[] spans = edit.getSpans(0, edit.length(), ImageSpan.class);
+ for (int i = 0; i < spans.length && !updated; i++) {
+ ImageSpan span = spans[i];
+ String source = span.getSource();
+ if (source != null && source.startsWith("cid:")) {
+ String cid = "<" + source.substring(4) + ">";
+ boolean found = false;
+ for (EntityAttachment attachment : attachments)
+ if (cid.equals(attachment.cid)) {
+ found = true;
+ break;
+ }
- if (selected == null && auto_identity)
- try {
- Address[] tos = MessageHelper.parseAddresses(context, to);
- if (tos != null && tos.length > 0) {
- String email = ((InternetAddress) tos[0]).getAddress();
- List identities = null;
- if (suggest_sent)
- identities = db.contact().getIdentities(email, EntityContact.TYPE_TO);
- if (suggest_received && (identities == null || identities.size() == 0))
- identities = db.contact().getIdentities(email, EntityContact.TYPE_FROM);
- if (identities != null && identities.size() == 1) {
- EntityIdentity identity = db.identity().getIdentity(identities.get(0));
- if (identity != null)
- selected = identity;
+ if (!found) {
+ updated = true;
+ int start = edit.getSpanStart(span);
+ int end = edit.getSpanEnd(span);
+ edit.removeSpan(span);
+ edit.delete(start, end);
+ }
+ }
}
- }
- } catch (AddressException ex) {
- Log.i(ex);
- }
- if (selected == null)
- for (EntityIdentity identity : data.identities)
- if (identity.account.equals(aid) && identity.primary) {
- selected = identity;
- EntityLog.log(context, "Selected primary account/identity");
- break;
- }
-
- if (selected == null)
- for (EntityIdentity identity : data.identities)
- if (identity.account.equals(aid)) {
- selected = identity;
- EntityLog.log(context, "Selected account/identity");
- break;
- }
-
- if (selected == null)
- for (EntityIdentity identity : data.identities)
- if (identity.primary) {
- selected = identity;
- EntityLog.log(context, "Selected primary */identity");
- break;
+ if (updated)
+ etBody.setText(edit);
}
- if (selected == null)
- for (EntityIdentity identity : data.identities) {
- selected = identity;
- EntityLog.log(context, "Selected */identity");
- break;
+ count = attachments.size();
}
+ });
- if (selected == null)
- throw new OperationCanceledException(context.getString(R.string.title_no_composable));
-
- EntityLog.log(context, "Selected=" + selected.email);
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
+ final boolean plain_only = prefs.getBoolean("plain_only", false);
- if (!"dsn".equals(action)) {
- if (plain_only &&
- !"resend".equals(action) &&
- !"editasnew".equals(action))
- data.draft.plain_only = 1;
-
- if (encrypt_default || selected.encrypt_default)
- if (selected.encrypt == 0)
- data.draft.ui_encrypt = EntityMessage.PGP_SIGNENCRYPT;
- else
- data.draft.ui_encrypt = EntityMessage.SMIME_SIGNENCRYPT;
- else if (sign_default || selected.sign_default)
- if (selected.encrypt == 0)
- data.draft.ui_encrypt = EntityMessage.PGP_SIGNONLY;
- else
- data.draft.ui_encrypt = EntityMessage.SMIME_SIGNONLY;
- }
-
- if (receipt_default)
- data.draft.receipt_request = true;
-
- data.draft.sensitivity = (selected.sensitivity < 1 ? null : selected.sensitivity);
-
- Document document = Document.createShell("");
-
- if (ref == null) {
- data.draft.thread = data.draft.msgid;
-
- try {
- data.draft.to = MessageHelper.parseAddresses(context, to);
- } catch (AddressException ex) {
- Log.w(ex);
- }
-
- try {
- data.draft.cc = MessageHelper.parseAddresses(context, cc);
- } catch (AddressException ex) {
- Log.w(ex);
- }
-
- try {
- data.draft.bcc = MessageHelper.parseAddresses(context, bcc);
- } catch (AddressException ex) {
- Log.w(ex);
- }
-
- data.draft.inreplyto = args.getString("inreplyto", null);
-
- data.draft.subject = external_subject;
-
- if (!TextUtils.isEmpty(external_body)) {
- Document d = JsoupEx.parse(external_body); // Passed html
- Element e = document
- .createElement("div")
- .html(d.body().html());
- document.body().appendChild(e);
- }
-
- EntityAnswer a = (answer < 0
- ? db.answer().getStandardAnswer()
- : db.answer().getAnswer(answer));
- if (a != null) {
- db.answer().applyAnswer(a.id, new Date().getTime());
- if (answer > 0)
- data.draft.subject = a.name;
- if (TextUtils.isEmpty(external_body)) {
- Document d = JsoupEx.parse(a.getHtml(context, null));
- document.body().append(d.body().html());
- }
- }
-
- data.draft.signature = prefs.getBoolean("signature_new", true);
- addSignature(context, document, data.draft, selected);
- } else {
- // Actions:
- // - reply
- // - reply_all
- // - forward
- // - resend
- // - editasnew
- // - list
- // - dsn
- // - receipt
- // - participation
-
- // References
- if ("reply".equals(action) || "reply_all".equals(action) ||
- "list".equals(action) ||
- "dsn".equals(action) ||
- "participation".equals(action)) {
- // https://tools.ietf.org/html/rfc5322#section-3.6.4
- // The "References:" field will contain the contents of the parent's "References:" field (if any)
- // followed by the contents of the parent's "Message-ID:" field (if any).
- String refs = (ref.references == null ? "" : ref.references);
- if (!TextUtils.isEmpty(ref.msgid))
- refs = (TextUtils.isEmpty(refs) ? ref.msgid : refs + " " + ref.msgid);
- data.draft.references = refs;
- data.draft.inreplyto = ref.msgid;
- data.draft.thread = ref.thread;
-
- if ("list".equals(action) && ref.list_post != null)
- data.draft.to = ref.list_post;
- else if ("dsn".equals(action)) {
- if (EntityMessage.DSN_RECEIPT.equals(dsn)) {
- if (ref.receipt_to != null)
- data.draft.to = ref.receipt_to;
- } else if (EntityMessage.DSN_HARD_BOUNCE.equals(dsn)) {
- if (ref.return_path != null)
- data.draft.to = ref.return_path;
- }
- } else {
- // Prevent replying to self
- if (ref.replySelf(data.identities, ref.account)) {
- EntityLog.log(context, "Reply self ref" +
- " from=" + MessageHelper.formatAddresses(ref.from) +
- " to=" + MessageHelper.formatAddresses(ref.to));
- data.draft.from = ref.from;
- data.draft.to = ref.to;
- } else {
- data.draft.from = ref.to;
- data.draft.to = (ref.reply == null || ref.reply.length == 0 ? ref.from : ref.reply);
- }
-
- if (ref.identity != null) {
- EntityIdentity recognized = db.identity().getIdentity(ref.identity);
- EntityLog.log(context, "Recognized=" + (recognized == null ? null : recognized.email));
-
- Address preferred = null;
- if (recognized != null) {
- Address same = null;
- Address similar = null;
-
- List addresses = new ArrayList<>();
- if (data.draft.from != null)
- addresses.addAll(Arrays.asList(data.draft.from));
- if (data.draft.to != null)
- addresses.addAll(Arrays.asList(data.draft.to));
- if (ref.cc != null)
- addresses.addAll(Arrays.asList(ref.cc));
- if (ref.bcc != null)
- addresses.addAll(Arrays.asList(ref.bcc));
-
- for (Address from : addresses) {
- if (same == null && recognized.sameAddress(from))
- same = from;
- if (similar == null && recognized.similarAddress(from))
- similar = from;
- }
-
- //if (ref.deliveredto != null)
- // try {
- // Address deliveredto = new InternetAddress(ref.deliveredto);
- // if (same == null && recognized.sameAddress(deliveredto))
- // same = deliveredto;
- // if (similar == null && recognized.similarAddress(deliveredto))
- // similar = deliveredto;
- // } catch (AddressException ex) {
- // Log.w(ex);
- // }
-
- EntityLog.log(context, "From=" + MessageHelper.formatAddresses(data.draft.from) +
- " delivered-to=" + ref.deliveredto +
- " same=" + (same == null ? null : ((InternetAddress) same).getAddress()) +
- " similar=" + (similar == null ? null : ((InternetAddress) similar).getAddress()));
-
- preferred = (same == null ? similar : same);
- }
-
- if (preferred != null) {
- String from = ((InternetAddress) preferred).getAddress();
- String name = ((InternetAddress) preferred).getPersonal();
- EntityLog.log(context, "Preferred=" + name + " <" + from + ">");
- if (TextUtils.isEmpty(from) || from.equalsIgnoreCase(recognized.email))
- from = null;
- if (!recognized.reply_extra_name ||
- TextUtils.isEmpty(name) || name.equals(recognized.name))
- name = null;
- String username = UriHelper.getEmailUser(from);
- String extra = (name == null ? "" : name + ", ") +
- (username == null ? "" : username);
- data.draft.extra = (TextUtils.isEmpty(extra) ? null : extra);
- } else
- EntityLog.log(context, "Preferred=null");
- } else
- EntityLog.log(context, "Recognized=null");
- }
-
- if ("reply_all".equals(action)) {
- List all = new ArrayList<>();
- for (Address recipient : ref.getAllRecipients(data.identities, ref.account)) {
- boolean found = false;
- if (data.draft.to != null)
- for (Address t : data.draft.to)
- if (MessageHelper.equalEmail(recipient, t)) {
- found = true;
- break;
- }
- if (!found)
- all.add(recipient);
- }
- data.draft.cc = all.toArray(new Address[0]);
- } else if ("dsn".equals(action)) {
- data.draft.dsn = dsn;
- data.draft.receipt_request = false;
- }
-
- } else if ("forward".equals(action)) {
- if (forward_new)
- data.draft.thread = data.draft.msgid; // new thread
- else {
- data.draft.thread = ref.thread;
- data.draft.inreplyto = ref.msgid;
- data.draft.references = (ref.references == null ? "" : ref.references + " ") + ref.msgid;
- }
- data.draft.wasforwardedfrom = ref.msgid;
- } else if ("resend".equals(action)) {
- data.draft.resend = true;
- data.draft.thread = data.draft.msgid;
- data.draft.headers = ref.headers;
- } else if ("editasnew".equals(action))
- data.draft.thread = data.draft.msgid;
-
- // Subject
- String subject = (ref.subject == null ? "" : ref.subject);
- if ("reply".equals(action) || "reply_all".equals(action)) {
- data.draft.subject =
- EntityMessage.getSubject(context, ref.language, subject, false);
-
- if (external_text != null) {
- Element div = document.createElement("div");
- for (String line : external_text.split("\\r?\\n")) {
- Element span = document.createElement("span");
- span.text(line);
- div.appendChild(span);
- div.appendElement("br");
- }
- document.body().appendChild(div);
- }
- } else if ("forward".equals(action)) {
- data.draft.subject =
- EntityMessage.getSubject(context, ref.language, subject, true);
- } else if ("resend".equals(action)) {
- data.draft.subject = ref.subject;
- } else if ("editasnew".equals(action)) {
- if (ref.from != null && ref.from.length == 1) {
- String from = ((InternetAddress) ref.from[0]).getAddress();
- for (EntityIdentity identity : data.identities)
- if (identity.email.equals(from)) {
- selected = identity;
- break;
- }
- }
-
- data.draft.to = ref.to;
- data.draft.cc = ref.cc;
- data.draft.bcc = ref.bcc;
- data.draft.subject = ref.subject;
-
- if (ref.content)
- document = JsoupEx.parse(ref.getFile(context));
- } else if ("list".equals(action)) {
- data.draft.subject = ref.subject;
- } else if ("dsn".equals(action)) {
- if (EntityMessage.DSN_HARD_BOUNCE.equals(dsn))
- data.draft.subject = context.getString(R.string.title_hard_bounce_subject);
- else
- data.draft.subject = context.getString(R.string.title_receipt_subject, subject);
-
- String[] texts;
- if (EntityMessage.DSN_HARD_BOUNCE.equals(dsn))
- texts = new String[]{context.getString(R.string.title_hard_bounce_text)};
- else {
- EntityAnswer receipt = db.answer().getReceiptAnswer();
- if (receipt == null)
- texts = Helper.getStrings(context, ref.language, R.string.title_receipt_text);
- else {
- db.answer().applyAnswer(receipt.id, new Date().getTime());
- texts = new String[0];
- Document d = JsoupEx.parse(receipt.getHtml(context, null));
- document.body().append(d.body().html());
- }
- }
-
- for (int i = 0; i < texts.length; i++) {
- if (i > 0)
- document.body()
- .appendElement("br");
-
- Element div = document.createElement("div");
- div.text(texts[i]);
- document.body()
- .appendChild(div)
- .appendElement("br");
- }
- } else if ("participation".equals(action))
- data.draft.subject = status + ": " + ref.subject;
-
- if (!"dsn".equals(action)) {
- // Sensitivity
- data.draft.sensitivity = ref.sensitivity;
-
- // Plain-only
- if (plain_only_reply && ref.isPlainOnly())
- data.draft.plain_only = 1;
-
- // Encryption
- List recipients = new ArrayList<>();
- if (data.draft.to != null)
- recipients.addAll(Arrays.asList(data.draft.to));
- if (data.draft.cc != null)
- recipients.addAll(Arrays.asList(data.draft.cc));
- if (data.draft.bcc != null)
- recipients.addAll(Arrays.asList(data.draft.bcc));
-
- if (!BuildConfig.DEBUG)
- if (EntityMessage.PGP_SIGNONLY.equals(ref.ui_encrypt) ||
- EntityMessage.PGP_SIGNENCRYPT.equals(ref.ui_encrypt)) {
- if (PgpHelper.isOpenKeychainInstalled(context) &&
- selected.sign_key != null &&
- PgpHelper.hasPgpKey(context, recipients, true))
- data.draft.ui_encrypt = ref.ui_encrypt;
- } else if (EntityMessage.SMIME_SIGNONLY.equals(ref.ui_encrypt) ||
- EntityMessage.SMIME_SIGNENCRYPT.equals(ref.ui_encrypt)) {
- if (ActivityBilling.isPro(context) &&
- selected.sign_key_alias != null &&
- SmimeHelper.hasSmimeKey(context, recipients, true))
- data.draft.ui_encrypt = ref.ui_encrypt;
- }
- }
-
- // Reply template
- EntityAnswer a = null;
- if (answer < 0) {
- if ("reply".equals(action) || "reply_all".equals(action) ||
- "forward".equals(action) || "list".equals(action))
- a = db.answer().getStandardAnswer();
- } else
- a = db.answer().getAnswer(answer);
-
- if (a != null) {
- db.answer().applyAnswer(a.id, new Date().getTime());
- if (a.label != null && ref != null)
- EntityOperation.queue(context, ref, EntityOperation.LABEL, a.label, true);
- Document d = JsoupEx.parse(a.getHtml(context, data.draft.to));
- document.body().append(d.body().html());
- }
-
- // Signature
- if ("reply".equals(action) || "reply_all".equals(action))
- data.draft.signature = prefs.getBoolean("signature_reply", true);
- else if ("forward".equals(action))
- data.draft.signature = prefs.getBoolean("signature_forward", true);
- else
- data.draft.signature = false;
-
- if (ref.content && "resend".equals(action)) {
- document = JsoupEx.parse(ref.getFile(context));
- HtmlHelper.clearAnnotations(document);
- // Save original body
- Element div = document.body()
- .tagName("div")
- .attr("fairemail", "reference");
- Element body = document.createElement("body")
- .appendChild(div);
- document.body().replaceWith(body);
- }
-
- // Reply header
- if (ref.content &&
- !"resend".equals(action) &&
- !"editasnew".equals(action) &&
- !("list".equals(action) && TextUtils.isEmpty(selected_text)) &&
- !"dsn".equals(action)) {
- // Reply/forward
- Element reply = document.createElement("div");
- reply.attr("fairemail", "reference");
-
- // Build reply header
- boolean separate_reply = prefs.getBoolean("separate_reply", false);
- boolean extended_reply = prefs.getBoolean("extended_reply", false);
- Element p = ref.getReplyHeader(context, document, separate_reply, extended_reply);
- reply.appendChild(p);
-
- Document d;
- if (TextUtils.isEmpty(selected_text)) {
- // Get referenced message body
- d = JsoupEx.parse(ref.getFile(context));
- HtmlHelper.normalizeNamespaces(d, false);
- HtmlHelper.clearAnnotations(d); // Legacy left-overs
-
- if (BuildConfig.DEBUG)
- d.select(".faircode_remove").remove();
-
- if ("reply".equals(action) || "reply_all".equals(action)) {
- // Remove signature separators
- boolean remove_signatures = prefs.getBoolean("remove_signatures", false);
- if (remove_signatures)
- HtmlHelper.removeSignatures(d);
-
- // Limit number of nested block quotes
- boolean quote_limit = prefs.getBoolean("quote_limit", true);
- if (quote_limit)
- HtmlHelper.quoteLimit(d, MAX_QUOTE_LEVEL);
- }
- } else {
- // Selected text
- d = Document.createShell("");
-
- Element div = d.createElement("div");
- if (selected_text instanceof Spanned)
- div.html(HtmlHelper.toHtml((Spanned) selected_text, context));
- else
- for (String line : selected_text.toString().split("\\r?\\n")) {
- Element span = document.createElement("span");
- span.text(line);
- div.appendChild(span);
- div.appendElement("br");
- }
-
- d.body().appendChild(div);
- }
-
- Element e = d.body();
-
- // Apply styles
- List sheets = HtmlHelper.parseStyles(d.head().select("style"));
- for (Element element : e.select("*")) {
- String tag = element.tagName();
- String clazz = element.attr("class");
- String style = HtmlHelper.processStyles(context, tag, clazz, null, sheets);
- style = HtmlHelper.mergeStyles(style, element.attr("style"));
- if (!TextUtils.isEmpty(style))
- element.attr("style", style);
- }
-
- // Quote referenced message body
- boolean quote_reply = prefs.getBoolean("quote_reply", true);
- boolean quote = (quote_reply &&
- ("reply".equals(action) || "reply_all".equals(action) || "list".equals(action)));
-
- if (quote) {
- String style = e.attr("style");
- style = HtmlHelper.mergeStyles(style, HtmlHelper.getQuoteStyle(e));
- e.tagName("blockquote").attr("style", style);
- } else
- e.tagName("p");
- reply.appendChild(e);
-
- if (wb && data.draft.wasforwardedfrom == null)
- document.body().prependChild(reply);
- else
- document.body().appendChild(reply);
-
- addSignature(context, document, data.draft, selected);
- }
- }
-
- EntityFolder drafts = db.folder().getFolderByType(selected.account, EntityFolder.DRAFTS);
- if (drafts == null)
- throw new IllegalArgumentException(context.getString(R.string.title_no_drafts));
-
- boolean signature_once = prefs.getBoolean("signature_reply_once", false);
- if (signature_once && data.draft.signature &&
- ref != null && ref.thread != null &&
- ("reply".equals(action) || "reply_all".equals(action))) {
- List outbound = new ArrayList<>();
-
- EntityFolder sent = db.folder().getFolderByType(drafts.account, EntityFolder.SENT);
- if (sent != null)
- outbound.addAll(db.message().getMessagesByThread(drafts.account, ref.thread, null, sent.id));
-
- EntityFolder outbox = db.folder().getOutbox();
- if (outbox != null)
- outbound.addAll(db.message().getMessagesByThread(drafts.account, ref.thread, null, outbox.id));
-
- if (outbound.size() > 0) {
- Log.i("Signature suppressed");
- data.draft.signature = false;
- }
- }
-
- data.draft.account = drafts.account;
- data.draft.folder = drafts.id;
- data.draft.identity = selected.id;
- data.draft.from = new InternetAddress[]{new InternetAddress(selected.email, selected.name, StandardCharsets.UTF_8.name())};
-
- data.draft.sender = MessageHelper.getSortKey(data.draft.from);
- Uri lookupUri = ContactInfo.getLookupUri(data.draft.from);
- data.draft.avatar = (lookupUri == null ? null : lookupUri.toString());
-
- data.draft.received = new Date().getTime();
- data.draft.seen = true;
- data.draft.ui_seen = true;
-
- data.draft.revision = 1;
- data.draft.revisions = 1;
-
- data.draft.id = db.message().insertMessage(data.draft);
-
- String html = document.html();
- Helper.writeText(data.draft.getFile(context), html);
- Helper.writeText(data.draft.getFile(context, data.draft.revision), html);
-
- String text = HtmlHelper.getFullText(html);
- data.draft.preview = HtmlHelper.getPreview(text);
- data.draft.language = HtmlHelper.getLanguage(context, data.draft.subject, text);
- db.message().setMessageContent(data.draft.id,
- true,
- data.draft.language,
- data.draft.plain_only,
- data.draft.preview,
- null);
-
- if ("participation".equals(action)) {
- EntityAttachment attachment = new EntityAttachment();
- attachment.message = data.draft.id;
- attachment.sequence = 1;
- attachment.name = "meeting.ics";
- attachment.type = "text/calendar";
- attachment.disposition = Part.ATTACHMENT;
- attachment.size = ics.length();
- attachment.progress = null;
- attachment.available = true;
- attachment.id = db.attachment().insertAttachment(attachment);
-
- File file = attachment.getFile(context);
- Helper.copy(ics, file);
- Helper.secureDelete(ics);
-
- ICalendar icalendar = CalendarHelper.parse(context, file);
- VEvent event = icalendar.getEvents().get(0);
- Organizer organizer = event.getOrganizer();
- if (organizer != null) {
- String email = organizer.getEmail();
- String name = organizer.getCommonName();
- if (!TextUtils.isEmpty(email)) {
- InternetAddress o = new InternetAddress(email, name, StandardCharsets.UTF_8.name());
- Log.i("Setting organizer=" + o);
- data.draft.to = new Address[]{o};
- }
- }
- }
-
- if ("new".equals(action) && uris != null) {
- ArrayList images = new ArrayList<>();
- for (Uri uri : uris)
- try {
- UriInfo info = getInfo(uri, context);
- if (info.isImage())
- images.add(uri);
- else
- addAttachment(context, data.draft.id, uri, null, false, 0, false);
- } catch (IOException ex) {
- Log.e(ex);
- }
-
- if (images.size() > 0)
- args.putParcelableArrayList("images", images);
- }
-
- if (ref != null &&
- ("reply".equals(action) || "reply_all".equals(action) ||
- "forward".equals(action) ||
- "resend".equals(action) ||
- "editasnew".equals(action))) {
- List cid = new ArrayList<>();
- for (Element img : document.select("img")) {
- String src = img.attr("src");
- if (src.startsWith("cid:"))
- cid.add("<" + src.substring(4) + ">");
- }
-
- int sequence = 0;
- List attachments = db.attachment().getAttachments(ref.id);
- for (EntityAttachment attachment : attachments)
- if (attachment.subsequence == null &&
- !attachment.isEncryption() &&
- (cid.contains(attachment.cid) ||
- !("reply".equals(action) || "reply_all".equals(action)))) {
- if (attachment.available) {
- File source = attachment.getFile(context);
-
- if (cid.contains(attachment.cid))
- attachment.disposition = Part.INLINE;
- else {
- attachment.cid = null;
- attachment.related = false;
- attachment.disposition = Part.ATTACHMENT;
- }
-
- attachment.id = null;
- attachment.message = data.draft.id;
- attachment.sequence = ++sequence;
- attachment.id = db.attachment().insertAttachment(attachment);
-
- File target = attachment.getFile(context);
- Helper.copy(source, target);
-
- if (resize_reply &&
- ("reply".equals(action) || "reply_all".equals(action)))
- resizeAttachment(context, attachment, REDUCED_IMAGE_SIZE);
- } else
- args.putBoolean("incomplete", true);
- }
- }
-
- if (save_drafts &&
- (data.draft.ui_encrypt == null ||
- EntityMessage.ENCRYPT_NONE.equals(data.draft.ui_encrypt)) &&
- (!"new".equals(action) ||
- answer > 0 ||
- !TextUtils.isEmpty(to) ||
- !TextUtils.isEmpty(cc) ||
- !TextUtils.isEmpty(bcc) ||
- !TextUtils.isEmpty(external_subject) ||
- !TextUtils.isEmpty(external_body) ||
- !TextUtils.isEmpty(external_text) ||
- !TextUtils.isEmpty(selected_text) ||
- (uris != null && uris.size() > 0))) {
- Map c = new HashMap<>();
- c.put("id", data.draft.id == null ? null : Long.toString(data.draft.id));
- c.put("encrypt", data.draft.encrypt + "/" + data.draft.ui_encrypt);
- c.put("action", action);
- Log.breadcrumb("Load draft", c);
-
- EntityOperation.queue(context, data.draft, EntityOperation.ADD);
- }
- } else {
- args.putBoolean("saved", true);
-
- if (!data.draft.ui_seen)
- EntityOperation.queue(context, data.draft, EntityOperation.SEEN, true);
-
- // External draft
- if (data.draft.identity == null) {
- for (EntityIdentity identity : data.identities)
- if (identity.account.equals(data.draft.account))
- if (identity.primary) {
- data.draft.identity = identity.id;
- break;
- } else if (data.draft.identity == null)
- data.draft.identity = identity.id;
-
- if (data.draft.identity != null)
- db.message().setMessageIdentity(data.draft.id, data.draft.identity);
- Log.i("Selected external identity=" + data.draft.identity);
- }
-
- if (data.draft.revision == null || data.draft.revisions == null) {
- data.draft.revision = 1;
- data.draft.revisions = 1;
- db.message().setMessageRevision(data.draft.id, data.draft.revision);
- db.message().setMessageRevisions(data.draft.id, data.draft.revisions);
- }
-
- if (data.draft.content || data.draft.uid == null) {
- if (data.draft.uid == null && !data.draft.content)
- Log.e("Draft without uid");
-
- File file = data.draft.getFile(context);
-
- Document doc = (data.draft.content ? JsoupEx.parse(file) : Document.createShell(""));
- doc.select("div[fairemail=signature]").remove();
- Elements ref = doc.select("div[fairemail=reference]");
- ref.remove();
-
- File refFile = data.draft.getRefFile(context);
- if (refFile.exists()) {
- ref.html(Helper.readText(refFile));
- Helper.secureDelete(refFile);
- }
-
- // Possibly external draft
-
- for (Element e : ref)
- if (wb && data.draft.wasforwardedfrom == null)
- doc.body().prependChild(e);
- else
- doc.body().appendChild(e);
-
- EntityIdentity identity = null;
- if (data.draft.identity != null)
- identity = db.identity().getIdentity(data.draft.identity);
-
- addSignature(context, doc, data.draft, identity);
-
- String html = doc.html();
- Helper.writeText(file, html);
- Helper.writeText(data.draft.getFile(context, data.draft.revision), html);
-
- String text = HtmlHelper.getFullText(html);
- data.draft.preview = HtmlHelper.getPreview(text);
- data.draft.language = HtmlHelper.getLanguage(context, data.draft.subject, text);
- db.message().setMessageContent(data.draft.id,
- true,
- data.draft.language,
- data.draft.plain_only,
- data.draft.preview,
- null);
- } else
- EntityOperation.queue(context, data.draft, EntityOperation.BODY);
- }
-
- last_plain_only = data.draft.plain_only;
- last_attachments = db.attachment().getAttachments(data.draft.id);
-
- if (last_attachments != null)
- for (EntityAttachment attachment : last_attachments)
- if (!attachment.available && attachment.progress == null && attachment.error == null)
- EntityOperation.queue(context, data.draft, EntityOperation.ATTACHMENT, attachment.id);
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- ServiceSynchronize.eval(context, "compose/draft");
-
- return data;
- }
-
- @Override
- protected void onExecuted(Bundle args, final DraftData data) {
- final String action = getArguments().getString("action");
- Log.i("Loaded draft id=" + data.draft.id + " action=" + action);
-
- FragmentActivity activity = getActivity();
- if (activity != null) {
- Intent intent = activity.getIntent();
- if (intent != null) {
- intent.putExtra("id", data.draft.id);
- intent.putExtra("action", "edit");
- }
- }
-
- working = data.draft.id;
- dsn = (data.draft.dsn != null && !EntityMessage.DSN_NONE.equals(data.draft.dsn));
- encrypt = data.draft.ui_encrypt;
- invalidateOptionsMenu();
-
- subject = data.draft.subject;
- saved = args.getBoolean("saved");
-
- // Show identities
- AdapterIdentitySelect iadapter = new AdapterIdentitySelect(getContext(), data.identities);
- spIdentity.setAdapter(iadapter);
-
- // Select identity
- if (data.draft.identity != null)
- for (int pos = 0; pos < data.identities.size(); pos++) {
- if (data.identities.get(pos).id.equals(data.draft.identity)) {
- spIdentity.setTag(pos);
- spIdentity.setSelection(pos);
- break;
- }
- }
-
- etExtra.setText(data.draft.extra);
- etTo.setText(MessageHelper.formatAddressesCompose(data.draft.to));
- etCc.setText(MessageHelper.formatAddressesCompose(data.draft.cc));
- etBcc.setText(MessageHelper.formatAddressesCompose(data.draft.bcc));
- etSubject.setText(data.draft.subject);
-
- long reference = args.getLong("reference", -1);
- etTo.setTag(reference < 0 ? "" : etTo.getText().toString());
- etSubject.setTag(reference < 0 ? "" : etSubject.getText().toString());
- cbSignature.setTag(data.draft.signature);
-
- grpHeader.setVisibility(View.VISIBLE);
- if ("reply_all".equals(action) ||
- (data.draft.cc != null && data.draft.cc.length > 0) ||
- (data.draft.bcc != null && data.draft.bcc.length > 0)) {
- ibCcBcc.setImageLevel(0);
- grpAddresses.setVisibility(View.VISIBLE);
- }
- ibCcBcc.setVisibility(View.VISIBLE);
-
- bottom_navigation.getMenu().findItem(R.id.action_undo).setVisible(data.draft.revision > 1);
- bottom_navigation.getMenu().findItem(R.id.action_redo).setVisible(data.draft.revision < data.draft.revisions);
-
- if (args.getBoolean("incomplete")) {
- final Snackbar snackbar = Snackbar.make(
- view, R.string.title_attachments_incomplete, Snackbar.LENGTH_INDEFINITE)
- .setGestureInsetBottomIgnored(true);
- snackbar.setAction(android.R.string.ok, new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- snackbar.dismiss();
- }
- });
- snackbar.show();
- }
-
- DB db = DB.getInstance(getContext());
-
- db.attachment().liveAttachments(data.draft.id).observe(getViewLifecycleOwner(),
- new Observer>() {
- private Integer count = null;
-
- @Override
- public void onChanged(@Nullable List attachments) {
- if (attachments == null)
- attachments = new ArrayList<>();
-
- List a = new ArrayList<>(attachments);
- rvAttachment.post(new Runnable() {
- @Override
- public void run() {
- try {
- if (adapter != null)
- adapter.set(a);
- } catch (Throwable ex) {
- Log.e(ex);
- /*
- java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling androidx.recyclerview.widget.RecyclerView{f9baa84 VFED..... ........ 0,245-720,1445 #7f0a03fd app:id/rvAttachment}, adapter:eu.faircode.email.AdapterAttachment@954026d, layout:androidx.recyclerview.widget.LinearLayoutManager@ed06ea2, context:eu.faircode.email.ActivityCompose@d14c627
- at androidx.recyclerview.widget.RecyclerView.assertNotInLayoutOrScroll(SourceFile:3)
- at androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver.onItemRangeChanged(SourceFile:1)
- at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeChanged(SourceFile:2)
- at androidx.recyclerview.widget.RecyclerView$Adapter.notifyItemRangeChanged(SourceFile:3)
- at androidx.recyclerview.widget.AdapterListUpdateCallback.onChanged(SourceFile:1)
- at androidx.recyclerview.widget.BatchingListUpdateCallback.dispatchLastEvent(SourceFile:2)
- at androidx.recyclerview.widget.DiffUtil$DiffResult.dispatchUpdatesTo(SourceFile:36)
- at eu.faircode.email.AdapterAttachment.set(SourceFile:6)
- at eu.faircode.email.FragmentCompose$38$3.onChanged(SourceFile:3)
- at eu.faircode.email.FragmentCompose$38$3.onChanged(SourceFile:1)
- at androidx.lifecycle.LiveData.considerNotify(SourceFile:6)
- at androidx.lifecycle.LiveData.dispatchingValue(SourceFile:8)
- at androidx.lifecycle.LiveData.setValue(SourceFile:4)
- at androidx.lifecycle.LiveData$1.run(SourceFile:5)
- at android.os.Handler.handleCallback(Handler.java:751)
- */
- }
- }
- });
-
- ibRemoveAttachments.setVisibility(attachments.size() > 2 ? View.VISIBLE : View.GONE);
- grpAttachments.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
-
- boolean downloading = false;
- for (EntityAttachment attachment : attachments) {
- if (attachment.isEncryption())
- continue;
- if (attachment.progress != null)
- downloading = true;
- }
-
- Log.i("Attachments=" + attachments.size() + " downloading=" + downloading);
-
- rvAttachment.setTag(downloading);
- checkInternet();
-
- if (count != null && count > attachments.size()) {
- boolean updated = false;
- Editable edit = etBody.getEditableText();
-
- ImageSpan[] spans = edit.getSpans(0, edit.length(), ImageSpan.class);
- for (int i = 0; i < spans.length && !updated; i++) {
- ImageSpan span = spans[i];
- String source = span.getSource();
- if (source != null && source.startsWith("cid:")) {
- String cid = "<" + source.substring(4) + ">";
- boolean found = false;
- for (EntityAttachment attachment : attachments)
- if (cid.equals(attachment.cid)) {
- found = true;
- break;
- }
-
- if (!found) {
- updated = true;
- int start = edit.getSpanStart(span);
- int end = edit.getSpanEnd(span);
- edit.removeSpan(span);
- edit.delete(start, end);
- }
- }
- }
-
- if (updated)
- etBody.setText(edit);
- }
-
- count = attachments.size();
- }
- });
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
- final boolean plain_only = prefs.getBoolean("plain_only", false);
-
- db.message().liveMessage(data.draft.id).observe(getViewLifecycleOwner(), new Observer() {
- @Override
- public void onChanged(EntityMessage draft) {
- // Draft was deleted
- if (draft == null || draft.ui_hide)
- finish();
- else {
- encrypt = draft.ui_encrypt;
- invalidateOptionsMenu();
+ db.message().liveMessage(data.draft.id).observe(getViewLifecycleOwner(), new Observer() {
+ @Override
+ public void onChanged(EntityMessage draft) {
+ // Draft was deleted
+ if (draft == null || draft.ui_hide)
+ finish();
+ else {
+ encrypt = draft.ui_encrypt;
+ invalidateOptionsMenu();
Log.i("Draft content=" + draft.content);
if (draft.content && state == State.NONE) {
@@ -6561,60 +5279,17 @@ public class FragmentCompose extends FragmentBase {
});
snackbar.show();
} else
- handleException(ex);
+ ComposeHelper.handleException(FragmentCompose.this, view, ex);
}
- }.serial();
- private void handleException(Throwable ex) {
- // External app sending absolute file
- if (ex instanceof NoStreamException)
- ((NoStreamException) ex).report(getActivity());
- else if (ex instanceof FileNotFoundException ||
- ex instanceof IllegalArgumentException ||
- ex instanceof IllegalStateException) {
- /*
- java.lang.IllegalStateException: Failed to mount
- at android.os.Parcel.createException(Parcel.java:2079)
- at android.os.Parcel.readException(Parcel.java:2039)
- at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:188)
- at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:151)
- at android.content.ContentProviderProxy.openTypedAssetFile(ContentProviderNative.java:705)
- at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1687)
- at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1503)
- at android.content.ContentResolver.openInputStream(ContentResolver.java:1187)
- at eu.faircode.email.FragmentCompose.addAttachment(SourceFile:27)
- */
- Snackbar.make(view, ex.toString(), Snackbar.LENGTH_LONG)
- .setGestureInsetBottomIgnored(true).show();
- } else {
- if (ex instanceof IOException &&
- ex.getCause() instanceof ErrnoException &&
- ((ErrnoException) ex.getCause()).errno == ENOSPC)
- ex = new IOException(getContext().getString(R.string.app_cake), ex);
-
- // External app didn't grant URI permissions
- if (ex instanceof SecurityException)
- ex = new Throwable(getString(R.string.title_no_permissions), ex);
-
- Log.unexpectedError(FragmentCompose.this, ex,
- !(ex instanceof IOException || ex.getCause() instanceof IOException));
- /*
- java.lang.IllegalStateException: java.io.IOException: Failed to redact /storage/emulated/0/Download/97203830-piston-vecteur-icĂ´ne-simple-symbole-plat-sur-fond-blanc.jpg
- at android.os.Parcel.createExceptionOrNull(Parcel.java:2381)
- at android.os.Parcel.createException(Parcel.java:2357)
- at android.os.Parcel.readException(Parcel.java:2340)
- at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:190)
- at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:153)
- at android.content.ContentProviderProxy.openTypedAssetFile(ContentProviderNative.java:804)
- at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:2002)
- at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1817)
- at android.content.ContentResolver.openInputStream(ContentResolver.java:1494)
- at eu.faircode.email.FragmentCompose.addAttachment(SourceFile:27)
- */
+ @Override
+ protected void set(Integer plain_only, List attachments) {
+ last_plain_only = plain_only;
+ last_attachments = attachments;
}
- }
+ }.serial();
- private SimpleTask actionLoader = new SimpleTask() {
+ private final SimpleTask actionLoader = new LoaderComposeAction() {
@Override
protected void onPreExecute(Bundle args) {
if (args.getBundle("extras").getBoolean("silent"))
@@ -6634,700 +5309,6 @@ public class FragmentCompose extends FragmentBase {
setBusy(false);
}
- @Override
- protected EntityMessage onExecute(final Context context, Bundle args) throws Throwable {
- // Get data
- long id = args.getLong("id");
- int action = args.getInt("action");
- long aid = args.getLong("account");
- long iid = args.getLong("identity");
- String extra = args.getString("extra");
- String to = args.getString("to");
- String cc = args.getString("cc");
- String bcc = args.getString("bcc");
- String subject = args.getString("subject");
- Spanned loaded = (Spanned) args.getCharSequence("loaded");
- Spanned spanned = (Spanned) args.getCharSequence("spanned");
- boolean signature = args.getBoolean("signature");
- boolean empty = args.getBoolean("empty");
- boolean notext = args.getBoolean("notext");
- Bundle extras = args.getBundle("extras");
-
- boolean silent = extras.getBoolean("silent");
-
- boolean dirty = false;
- String body = HtmlHelper.toHtml(spanned, context);
- EntityMessage draft;
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean discard_delete = prefs.getBoolean("discard_delete", true);
- boolean write_below = prefs.getBoolean("write_below", false);
- boolean save_drafts = prefs.getBoolean("save_drafts", true);
- int send_delayed = prefs.getInt("send_delayed", 0);
-
- DB db = DB.getInstance(context);
- try {
- db.beginTransaction();
-
- // Get draft & selected identity
- draft = db.message().getMessage(id);
- EntityIdentity identity = db.identity().getIdentity(iid);
-
- // Draft deleted by server
- if (draft == null || draft.ui_hide)
- throw new MessageRemovedException("Draft for action was deleted hide=" + (draft != null));
-
- Log.i("Load action id=" + draft.id + " action=" + getActionName(action));
-
- if (action == R.id.action_delete) {
- dirty = true;
- EntityFolder trash = db.folder().getFolderByType(draft.account, EntityFolder.TRASH);
- EntityFolder drafts = db.folder().getFolderByType(draft.account, EntityFolder.DRAFTS);
- if (empty || trash == null || discard_delete || !save_drafts || (drafts != null && drafts.local))
- EntityOperation.queue(context, draft, EntityOperation.DELETE);
- else {
- Map c = new HashMap<>();
- c.put("id", draft.id == null ? null : Long.toString(draft.id));
- c.put("encrypt", draft.encrypt + "/" + draft.ui_encrypt);
- Log.breadcrumb("Discard draft", c);
-
- EntityOperation.queue(context, draft, EntityOperation.ADD);
- EntityOperation.queue(context, draft, EntityOperation.MOVE, trash.id);
- }
-
- getMainHandler().post(new Runnable() {
- public void run() {
- ToastEx.makeText(context, R.string.title_draft_deleted, Toast.LENGTH_LONG).show();
- }
- });
- } else {
- // Move draft to new account
- if (draft.account != aid && aid >= 0) {
- Log.i("Account changed");
-
- Long uid = draft.uid;
- String msgid = draft.msgid;
- boolean content = draft.content;
- Boolean ui_hide = draft.ui_hide;
-
- // To prevent violating constraints
- draft.uid = null;
- draft.msgid = null;
- db.message().updateMessage(draft);
-
- // Create copy to delete
- draft.id = null;
- draft.uid = uid;
- draft.msgid = msgid;
- draft.content = false;
- draft.ui_hide = true;
- draft.id = db.message().insertMessage(draft);
- EntityOperation.queue(context, draft, EntityOperation.DELETE);
-
- // Restore original with new account, no uid and new msgid
- draft.id = id;
- draft.account = aid;
- draft.folder = db.folder().getFolderByType(aid, EntityFolder.DRAFTS).id;
- draft.uid = null;
- draft.msgid = EntityMessage.generateMessageId();
- draft.content = content;
- draft.ui_hide = ui_hide;
- db.message().updateMessage(draft);
-
- if (draft.content)
- dirty = true;
- }
-
- Map crumb = new HashMap<>();
- crumb.put("draft", draft.folder + ":" + draft.id);
- crumb.put("content", Boolean.toString(draft.content));
- crumb.put("revision", Integer.toString(draft.revision == null ? -1 : draft.revision));
- crumb.put("revisions", Integer.toString(draft.revisions == null ? -1 : draft.revisions));
- crumb.put("file", Boolean.toString(draft.getFile(context).exists()));
- crumb.put("action", getActionName(action));
- Log.breadcrumb("compose", crumb);
-
- List attachments = db.attachment().getAttachments(draft.id);
-
- // Get data
- InternetAddress[] afrom = (identity == null ? null : new InternetAddress[]{new InternetAddress(identity.email, identity.name, StandardCharsets.UTF_8.name())});
- InternetAddress[] ato = MessageHelper.dedup(MessageHelper.parseAddresses(context, to));
- InternetAddress[] acc = MessageHelper.dedup(MessageHelper.parseAddresses(context, cc));
- InternetAddress[] abcc = MessageHelper.dedup(MessageHelper.parseAddresses(context, bcc));
-
- // Safe guard
- if (action == R.id.action_send) {
- checkAddress(ato, context);
- checkAddress(acc, context);
- checkAddress(abcc, context);
- }
-
- if (TextUtils.isEmpty(extra))
- extra = null;
-
- List eparts = new ArrayList<>();
- for (EntityAttachment attachment : attachments)
- if (attachment.available)
- if (attachment.isEncryption())
- eparts.add(attachment.encryption);
-
- if (EntityMessage.PGP_SIGNONLY.equals(draft.ui_encrypt)) {
- if (!eparts.contains(EntityAttachment.PGP_KEY) ||
- !eparts.contains(EntityAttachment.PGP_SIGNATURE) ||
- !eparts.contains(EntityAttachment.PGP_CONTENT))
- dirty = true;
- } else if (EntityMessage.PGP_ENCRYPTONLY.equals(draft.ui_encrypt)) {
- if (!eparts.contains(EntityAttachment.PGP_MESSAGE))
- dirty = true;
- } else if (EntityMessage.PGP_SIGNENCRYPT.equals(draft.ui_encrypt)) {
- if (!eparts.contains(EntityAttachment.PGP_KEY) ||
- !eparts.contains(EntityAttachment.PGP_MESSAGE))
- dirty = true;
- } else if (EntityMessage.SMIME_SIGNONLY.equals(draft.ui_encrypt)) {
- if (!eparts.contains(EntityAttachment.SMIME_SIGNATURE) ||
- !eparts.contains(EntityAttachment.SMIME_CONTENT))
- dirty = true;
- } else if (EntityMessage.SMIME_SIGNENCRYPT.equals(draft.ui_encrypt)) {
- if (!eparts.contains(EntityAttachment.SMIME_MESSAGE))
- dirty = true;
- }
-
- Long ident = (identity == null ? null : identity.id);
- if (!Objects.equals(draft.identity, ident) ||
- !Objects.equals(draft.extra, extra) ||
- !MessageHelper.equal(draft.from, afrom) ||
- !MessageHelper.equal(draft.to, ato) ||
- !MessageHelper.equal(draft.cc, acc) ||
- !MessageHelper.equal(draft.bcc, abcc) ||
- !Objects.equals(draft.subject, subject) ||
- !draft.signature.equals(signature) ||
- !Objects.equals(last_plain_only, draft.plain_only) ||
- !EntityAttachment.equals(last_attachments, attachments))
- dirty = true;
-
- last_plain_only = draft.plain_only;
- last_attachments = attachments;
-
- if (dirty) {
- // Update draft
- draft.identity = ident;
- draft.extra = extra;
- draft.from = afrom;
- draft.to = ato;
- draft.cc = acc;
- draft.bcc = abcc;
- draft.subject = subject;
- draft.signature = signature;
- draft.sender = MessageHelper.getSortKey(draft.from);
- Uri lookupUri = ContactInfo.getLookupUri(draft.from);
- draft.avatar = (lookupUri == null ? null : lookupUri.toString());
- db.message().updateMessage(draft);
- }
-
- Document doc = JsoupEx.parse(draft.getFile(context));
- Element first = (doc.body().childrenSize() == 0 ? null : doc.body().child(0));
- boolean below = (first != null && first.attr("fairemail").equals("reference"));
- doc.select("div[fairemail=signature]").remove();
- Elements ref = doc.select("div[fairemail=reference]");
- ref.remove();
-
- if (extras.containsKey("html"))
- dirty = true;
-
- boolean wb = (draft == null || draft.write_below == null ? write_below : draft.write_below);
- if (below != wb &&
- doc.body().childrenSize() > 0 &&
- draft.wasforwardedfrom == null)
- dirty = true;
-
- if (!dirty)
- if (loaded == null) {
- Document b = JsoupEx.parse(body); // Is-dirty
- if (!Objects.equals(b.body().html(), doc.body().html()))
- dirty = true;
- } else {
- // Was not dirty before
- String hloaded = HtmlHelper.toHtml(loaded, context);
- String hspanned = HtmlHelper.toHtml(spanned, context);
- if (!Objects.equals(hloaded, hspanned))
- dirty = true;
- }
-
- if (draft.revision == null) {
- draft.revision = 1;
- draft.revisions = 1;
- }
-
- int revision = draft.revision; // Save for undo/redo
- if (dirty) {
- dirty = true;
-
- // Get saved body
- Document d;
- if (extras.containsKey("html")) {
- // Save current revision
- Document c = JsoupEx.parse(body);
-
- for (Element e : ref)
- if (wb && draft.wasforwardedfrom == null)
- c.body().prependChild(e);
- else
- c.body().appendChild(e);
-
- addSignature(context, c, draft, identity);
-
- Helper.writeText(draft.getFile(context, draft.revision), c.html());
-
- d = JsoupEx.parse(extras.getString("html"));
- } else {
- d = JsoupEx.parse(body); // Save
-
- for (Element e : ref)
- if (wb && draft.wasforwardedfrom == null)
- d.body().prependChild(e);
- else
- d.body().appendChild(e);
-
- addSignature(context, d, draft, identity);
- }
-
- body = d.html();
-
- // Create new revision
- draft.revisions++;
- draft.revision = draft.revisions;
-
- Helper.writeText(draft.getFile(context, draft.revision), body);
- } else
- body = Helper.readText(draft.getFile(context));
-
- if (action == R.id.action_undo || action == R.id.action_redo) {
- if (action == R.id.action_undo) {
- if (revision > 1)
- draft.revision = revision - 1;
- else
- draft.revision = revision;
- } else {
- if (revision < draft.revisions)
- draft.revision = revision + 1;
- else
- draft.revision = revision;
- }
-
- // Restore revision
- Log.i("Restoring revision=" + draft.revision);
- File file = draft.getFile(context, draft.revision);
- if (file.exists())
- body = Helper.readText(file);
- else
- Log.e("Missing" +
- " revision=" + draft.revision + "/" + draft.revisions +
- " action=" + getActionName(action));
-
- dirty = true;
- } else if (action == R.id.action_send) {
- if (!draft.isPlainOnly()) {
- // Remove unused inline images
- List cids = new ArrayList<>();
- Document d = JsoupEx.parse(body);
- for (Element element : d.select("img")) {
- String src = element.attr("src");
- if (src.startsWith("cid:"))
- cids.add("<" + src.substring(4) + ">");
- }
-
- for (EntityAttachment attachment : new ArrayList<>(attachments))
- if (attachment.isInline() && attachment.isImage() &&
- attachment.cid != null && !cids.contains(attachment.cid)) {
- Log.i("Removing unused inline attachment cid=" + attachment.cid);
- attachments.remove(attachment);
- db.attachment().deleteAttachment(attachment.id);
- dirty = true;
- }
- } else {
- // Convert inline images to attachments
- for (EntityAttachment attachment : new ArrayList<>(attachments))
- if (attachment.isInline() && attachment.isImage()) {
- Log.i("Converting to attachment cid=" + attachment.cid);
- attachment.disposition = Part.ATTACHMENT;
- attachment.cid = null;
- db.attachment().setDisposition(attachment.id, attachment.disposition, attachment.cid);
- dirty = true;
- }
- }
- }
-
- File f = draft.getFile(context);
- Helper.writeText(f, body);
- if (f.length() > MAX_REASONABLE_SIZE)
- args.putBoolean("large", true);
-
- String full = HtmlHelper.getFullText(body);
- draft.preview = HtmlHelper.getPreview(full);
- draft.language = HtmlHelper.getLanguage(context, draft.subject, full);
- db.message().setMessageContent(draft.id,
- true,
- draft.language,
- draft.plain_only, // unchanged
- draft.preview,
- null);
-
- db.message().setMessageRevision(draft.id, draft.revision);
- db.message().setMessageRevisions(draft.id, draft.revisions);
-
- if (dirty) {
- draft.received = new Date().getTime();
- draft.sent = draft.received;
- db.message().setMessageReceived(draft.id, draft.received);
- db.message().setMessageSent(draft.id, draft.sent);
- }
-
- if (silent) {
- // Skip storing on the server, etc
- db.setTransactionSuccessful();
- return draft;
- }
-
- // Execute action
- boolean encrypted = extras.getBoolean("encrypted");
- boolean shouldEncrypt = EntityMessage.PGP_ENCRYPTONLY.equals(draft.ui_encrypt) ||
- EntityMessage.PGP_SIGNENCRYPT.equals(draft.ui_encrypt) ||
- (EntityMessage.PGP_SIGNONLY.equals(draft.ui_encrypt) && action == R.id.action_send) ||
- EntityMessage.SMIME_SIGNENCRYPT.equals(draft.ui_encrypt) ||
- (EntityMessage.SMIME_SIGNONLY.equals(draft.ui_encrypt) && action == R.id.action_send);
- boolean needsEncryption = (dirty && !encrypted && shouldEncrypt);
- boolean autosave = extras.getBoolean("autosave");
- if (needsEncryption && !autosave) {
- args.putBoolean("needsEncryption", true);
- db.setTransactionSuccessful();
- return draft;
- }
-
- if (!shouldEncrypt && !autosave)
- for (EntityAttachment attachment : attachments)
- if (attachment.isEncryption())
- db.attachment().deleteAttachment(attachment.id);
-
- if (action == R.id.action_save ||
- action == R.id.action_undo ||
- action == R.id.action_redo ||
- action == R.id.action_check) {
- boolean unencrypted =
- (!EntityMessage.PGP_ENCRYPTONLY.equals(draft.ui_encrypt) &&
- !EntityMessage.PGP_SIGNENCRYPT.equals(draft.ui_encrypt) &&
- !EntityMessage.SMIME_SIGNENCRYPT.equals(draft.ui_encrypt));
- if ((dirty && unencrypted) || encrypted) {
- if (save_drafts) {
- Map c = new HashMap<>();
- c.put("id", draft.id == null ? null : Long.toString(draft.id));
- c.put("dirty", Boolean.toString(dirty));
- c.put("encrypt", draft.encrypt + "/" + draft.ui_encrypt);
- c.put("encrypted", Boolean.toString(encrypted));
- c.put("needsEncryption", Boolean.toString(needsEncryption));
- c.put("autosave", Boolean.toString(autosave));
- Log.breadcrumb("Save draft", c);
-
- EntityOperation.queue(context, draft, EntityOperation.ADD);
- }
- }
-
- if (action == R.id.action_check) {
- // Check data
- if (draft.identity == null)
- throw new IllegalArgumentException(context.getString(R.string.title_from_missing));
-
- if (false) {
- EntityAccount account = db.account().getAccount(draft.account);
- EntityFolder sent = db.folder().getFolderByType(draft.account, EntityFolder.SENT);
- if (account != null && account.protocol == EntityAccount.TYPE_IMAP && sent == null)
- args.putBoolean("sent_missing", true);
- }
-
- try {
- checkAddress(ato, context);
- checkAddress(acc, context);
- checkAddress(abcc, context);
-
- List check = new ArrayList<>();
- List checked = new ArrayList<>();
- List dup = new ArrayList<>();
- if (ato != null)
- check.addAll(Arrays.asList(ato));
- if (acc != null)
- check.addAll(Arrays.asList(acc));
- if (abcc != null)
- check.addAll(Arrays.asList(abcc));
-
- for (InternetAddress a : check) {
- String email = a.getAddress();
- if (TextUtils.isEmpty(email))
- continue;
- if (checked.contains(a.getAddress()))
- dup.add(email);
- else
- checked.add(email);
- }
-
- if (dup.size() > 0)
- throw new AddressException(context.getString(
- R.string.title_address_duplicate,
- TextUtils.join(", ", dup)));
- } catch (AddressException ex) {
- args.putString("address_error", ex.getMessage());
- }
-
- if (draft.to == null && draft.cc == null && draft.bcc == null &&
- (identity == null || (identity.cc == null && identity.bcc == null)))
- args.putBoolean("remind_to", true);
-
- //if (TextUtils.isEmpty(draft.extra) &&
- // identity != null && identity.sender_extra)
- // args.putBoolean("remind_extra", true);
-
- List recipients = new ArrayList<>();
- if (draft.to != null)
- recipients.addAll(Arrays.asList(draft.to));
- if (draft.cc != null)
- recipients.addAll(Arrays.asList(draft.cc));
- if (draft.bcc != null)
- recipients.addAll(Arrays.asList(draft.bcc));
-
- boolean noreply = false;
- for (Address recipient : recipients)
- if (MessageHelper.isNoReply(recipient)) {
- noreply = true;
- break;
- }
- args.putBoolean("remind_noreply", noreply);
-
- if (identity != null && !TextUtils.isEmpty(identity.internal)) {
- boolean external = false;
- String[] internals = identity.internal.split(",");
- for (Address recipient : recipients) {
- String email = ((InternetAddress) recipient).getAddress();
- String domain = UriHelper.getEmailDomain(email);
- if (domain == null)
- continue;
-
- boolean found = false;
- for (String internal : internals)
- if (internal.equalsIgnoreCase(domain)) {
- found = true;
- break;
- }
- if (!found) {
- external = true;
- break;
- }
- }
- args.putBoolean("remind_external", external);
- }
-
- if ((draft.dsn == null ||
- EntityMessage.DSN_NONE.equals(draft.dsn)) &&
- (draft.ui_encrypt == null ||
- EntityMessage.ENCRYPT_NONE.equals(draft.ui_encrypt))) {
- args.putBoolean("remind_pgp", PgpHelper.hasPgpKey(context, recipients, false));
- args.putBoolean("remind_smime", SmimeHelper.hasSmimeKey(context, recipients, false));
- }
-
- if (TextUtils.isEmpty(draft.subject))
- args.putBoolean("remind_subject", true);
-
- Document d = JsoupEx.parse(body);
-
- if (notext &&
- d.select("div[fairemail=reference]").isEmpty())
- args.putBoolean("remind_text", true);
-
- boolean styled = HtmlHelper.isStyled(d);
- args.putBoolean("styled", styled);
-
- int attached = 0;
- List dangerous = new ArrayList<>();
- for (EntityAttachment attachment : attachments) {
- if (!attachment.available)
- throw new IllegalArgumentException(context.getString(R.string.title_attachments_missing));
- else if (attachment.isAttachment())
- attached++;
- String ext = Helper.getExtension(attachment.name);
- if (Helper.DANGEROUS_EXTENSIONS.contains(ext))
- dangerous.add(attachment.name);
- }
- if (dangerous.size() > 0)
- args.putString("remind_extension", String.join(", ", dangerous));
-
- // Check for missing attachments
- if (attached == 0) {
- List keywords = new ArrayList<>();
- for (String text : Helper.getStrings(context, R.string.title_attachment_keywords))
- keywords.addAll(Arrays.asList(text.split(",")));
-
- d.select("div[fairemail=signature]").remove();
- d.select("div[fairemail=reference]").remove();
-
- String text = d.text();
- for (String keyword : keywords)
- if (text.matches("(?si).*\\b" + Pattern.quote(keyword.trim()) + "\\b.*")) {
- args.putBoolean("remind_attachment", true);
- break;
- }
- }
-
- if (EntityMessage.DSN_HARD_BOUNCE.equals(draft.dsn))
- args.putBoolean("remind_dsn", true);
-
- // Check size
- if (identity != null && identity.max_size != null)
- try {
- Properties props = MessageHelper.getSessionProperties(true);
- if (identity.unicode)
- props.put("mail.mime.allowutf8", "true");
- Session isession = Session.getInstance(props, null);
- Message imessage = MessageHelper.from(context, draft, identity, isession, false);
-
- File file = draft.getRawFile(context);
- try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
- imessage.writeTo(os);
- }
-
- long size = file.length();
- if (size > identity.max_size) {
- args.putBoolean("remind_size", true);
- args.putLong("size", size);
- args.putLong("max_size", identity.max_size);
- }
- } catch (Throwable ex) {
- Log.e(ex);
- }
-
- args.putBoolean("remind_internet", !ConnectionHelper.getNetworkState(context).isConnected());
- } else {
- int mid;
- if (action == R.id.action_undo)
- mid = R.string.title_undo;
- else if (action == R.id.action_redo)
- mid = R.string.title_redo;
- else
- mid = R.string.title_draft_saved;
- final String msg = context.getString(mid) +
- (BuildConfig.DEBUG
- ? " " + draft.revision + (dirty ? "*" : "")
- : "");
-
- getMainHandler().post(new Runnable() {
- public void run() {
- ToastEx.makeText(context, msg, Toast.LENGTH_LONG).show();
- }
- });
- }
-
- } else if (action == R.id.action_send) {
- EntityFolder outbox = EntityFolder.getOutbox(context);
-
- // Delay sending message
- if (draft.ui_snoozed == null && send_delayed != 0) {
- if (extras.getBoolean("now"))
- draft.ui_snoozed = null;
- else
- draft.ui_snoozed = new Date().getTime() + send_delayed * 1000L;
- }
-
- if (draft.ui_snoozed != null)
- draft.received = draft.ui_snoozed;
-
- // Copy message to outbox
- long did = draft.id;
-
- draft.id = null;
- draft.folder = outbox.id;
- draft.uid = null;
- draft.fts = false;
- draft.ui_hide = false;
- draft.id = db.message().insertMessage(draft);
- Helper.writeText(draft.getFile(context), body);
-
- // Move attachments
- for (EntityAttachment attachment : attachments)
- db.attachment().setMessage(attachment.id, draft.id);
-
- // Send message
- if (draft.ui_snoozed == null)
- EntityOperation.queue(context, draft, EntityOperation.SEND);
-
- // Delete draft (cannot move to outbox)
- EntityMessage tbd = db.message().getMessage(did);
- if (tbd != null)
- EntityOperation.queue(context, tbd, EntityOperation.DELETE);
-
- final String feedback;
- if (draft.ui_snoozed == null) {
- boolean suitable = ConnectionHelper.getNetworkState(context).isSuitable();
- if (suitable)
- feedback = context.getString(R.string.title_queued);
- else
- feedback = context.getString(R.string.title_notification_waiting);
- } else {
- DateFormat DTF = Helper.getDateTimeInstance(context);
- feedback = context.getString(R.string.title_queued_at, DTF.format(draft.ui_snoozed));
- }
-
- getMainHandler().post(new RunnableEx("compose:toast") {
- public void delegate() {
- if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
- Helper.performHapticFeedback(view, HapticFeedbackConstants.CONFIRM);
- ToastEx.makeText(context, feedback, Toast.LENGTH_LONG).show();
- }
- });
-
- if (extras.getBoolean("archive")) {
- EntityFolder archive = db.folder().getFolderByType(draft.account, EntityFolder.ARCHIVE);
- if (archive != null) {
- List messages = db.message().getMessagesByMsgId(draft.account, draft.inreplyto);
- if (messages != null)
- for (EntityMessage message : messages)
- EntityOperation.queue(context, message, EntityOperation.MOVE, archive.id);
- }
- }
- }
- }
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- if (action == R.id.action_check)
- try {
- InternetAddress[] ato = MessageHelper.dedup(MessageHelper.parseAddresses(context, to));
- InternetAddress[] acc = MessageHelper.dedup(MessageHelper.parseAddresses(context, cc));
- InternetAddress[] abcc = MessageHelper.dedup(MessageHelper.parseAddresses(context, bcc));
-
- try {
- checkMx(ato, context);
- checkMx(acc, context);
- checkMx(abcc, context);
- } catch (UnknownHostException ex) {
- args.putString("mx_error", ex.getMessage());
- }
- } catch (Throwable ignored) {
- }
-
- args.putBoolean("dirty", dirty);
- if (dirty)
- ServiceSynchronize.eval(context, "compose/action");
-
- if (action == R.id.action_send)
- if (draft.ui_snoozed == null)
- ServiceSend.start(context);
- else {
- Log.i("Delayed send id=" + draft.id + " at " + new Date(draft.ui_snoozed));
- EntityMessage.snooze(context, draft.id, draft.ui_snoozed);
- }
-
- return draft;
- }
-
@Override
protected void onExecuted(Bundle args, EntityMessage draft) {
if (draft == null)
@@ -7347,7 +5328,7 @@ public class FragmentCompose extends FragmentBase {
boolean needsEncryption = args.getBoolean("needsEncryption");
int action = args.getInt("action");
Log.i("Loaded action id=" + draft.id +
- " action=" + getActionName(action) + " encryption=" + needsEncryption);
+ " action=" + LoaderComposeAction.getActionName(action) + " encryption=" + needsEncryption);
int toPos = etTo.getSelectionStart();
int ccPos = etCc.getSelectionStart();
@@ -7469,51 +5450,28 @@ public class FragmentCompose extends FragmentBase {
}
}
- private void checkAddress(InternetAddress[] addresses, Context context) throws AddressException {
- if (addresses == null)
- return;
-
- for (InternetAddress address : addresses)
- try {
- address.validate();
- } catch (AddressException ex) {
- throw new AddressException(context.getString(R.string.title_address_parse_error,
- MessageHelper.formatAddressesCompose(new Address[]{address}), ex.getMessage()));
- }
+ @Override
+ protected void set(Integer plain_only, List attachments) {
+ last_plain_only = plain_only;
+ last_attachments = attachments;
}
- private void checkMx(InternetAddress[] addresses, Context context) throws UnknownHostException {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean lookup_mx = prefs.getBoolean("lookup_mx", false);
- if (!lookup_mx)
- return;
-
- if (addresses == null)
- return;
-
- ConnectivityManager cm = Helper.getSystemService(context, ConnectivityManager.class);
- NetworkInfo ani = (cm == null ? null : cm.getActiveNetworkInfo());
- if (ani != null && ani.isConnected())
- DnsHelper.checkMx(context, addresses);
+ @Override
+ protected Pair> get() {
+ return new Pair<>(last_plain_only, last_attachments);
}
- }.serial();
- private String getActionName(int id) {
- if (id == R.id.action_delete) {
- return "delete";
- } else if (id == R.id.action_undo) {
- return "undo";
- } else if (id == R.id.action_redo) {
- return "redo";
- } else if (id == R.id.action_save) {
- return "save";
- } else if (id == R.id.action_check) {
- return "check";
- } else if (id == R.id.action_send) {
- return "send";
+ @Override
+ protected void toast(String feedback) {
+ getMainHandler().post(new RunnableEx("compose:toast") {
+ public void delegate() {
+ if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
+ Helper.performHapticFeedback(view, HapticFeedbackConstants.CONFIRM);
+ ToastEx.makeText(getContext(), feedback, Toast.LENGTH_LONG).show();
+ }
+ });
}
- return Integer.toString(id);
- }
+ }.serial();
private void setBusy(boolean busy) {
state = (busy ? State.LOADING : State.LOADED);
@@ -7521,47 +5479,6 @@ public class FragmentCompose extends FragmentBase {
invalidateOptionsMenu();
}
- private static void addSignature(Context context, Document document, EntityMessage draft, EntityIdentity identity) {
- if (!draft.signature ||
- identity == null || TextUtils.isEmpty(identity.signature))
- return;
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- int signature_location = prefs.getInt("signature_location", 1);
- boolean usenet = prefs.getBoolean("usenet_signature", false);
- boolean write_below = prefs.getBoolean("write_below", false);
- String compose_font = prefs.getString("compose_font", "");
-
- boolean wb = (draft == null || draft.write_below == null ? write_below : draft.write_below);
-
- Element div = document.createElement("div");
- div.attr("fairemail", "signature");
- if (!TextUtils.isEmpty(compose_font))
- div.attr("style", "font-family: " + StyleHelper.getFamily(compose_font));
-
- if (usenet) {
- // https://datatracker.ietf.org/doc/html/rfc3676#section-4.3
- Element span = document.createElement("span");
- span.text("-- ");
- span.prependElement("br");
- span.appendElement("br");
- div.appendChild(span);
- }
-
- div.append(identity.signature);
-
- Elements ref = document.select("div[fairemail=reference]");
- if (signature_location == 0) // top
- document.body().prependChild(div);
- else if (ref.size() == 0 || signature_location == 2) // bottom
- document.body().appendChild(div);
- else if (signature_location == 1) // below text
- if (wb && draft.wasforwardedfrom == null)
- document.body().appendChild(div);
- else
- ref.first().before(div);
- }
-
private void showDraft(final EntityMessage draft, boolean refedit, Runnable postShow, int selection) {
Bundle args = new Bundle();
args.putLong("id", draft.id);
@@ -8049,70 +5966,4 @@ public class FragmentCompose extends FragmentBase {
onExit();
}
};
-
- @NonNull
- private static UriInfo getInfo(Uri uri, Context context) {
- UriInfo result = new UriInfo();
-
- // https://stackoverflow.com/questions/76094229/android-13-photo-video-picker-file-name-from-the-uri-is-garbage
- DocumentFile dfile = null;
- try {
- dfile = DocumentFile.fromSingleUri(context, uri);
- if (dfile != null) {
- result.name = dfile.getName();
- result.type = dfile.getType();
- result.size = dfile.length();
- EntityLog.log(context, "UriInfo dfile " + result + " uri=" + uri);
- }
- } catch (Throwable ex) {
- Log.e(ex);
- }
-
- // Check name
- if (TextUtils.isEmpty(result.name))
- result.name = uri.getLastPathSegment();
-
- // Check type
- if (!TextUtils.isEmpty(result.type))
- try {
- new ContentType(result.type);
- } catch (ParseException ex) {
- Log.w(new Throwable(result.type, ex));
- result.type = null;
- }
-
- if (TextUtils.isEmpty(result.type) ||
- "*/*".equals(result.type) ||
- "application/*".equals(result.type) ||
- "application/octet-stream".equals(result.type))
- result.type = Helper.guessMimeType(result.name);
-
- if (result.size != null && result.size <= 0)
- result.size = null;
-
- EntityLog.log(context, "UriInfo result " + result + " uri=" + uri);
-
- return result;
- }
-
- private static class UriInfo {
- String name;
- String type;
- Long size;
-
- boolean isImage() {
- return ImageHelper.isImage(type);
- }
-
- @NonNull
- @Override
- public String toString() {
- return "name=" + name + " type=" + type + " size=" + size;
- }
- }
-
- private static class DraftData {
- private EntityMessage draft;
- private List identities;
- }
}
diff --git a/app/src/main/java/eu/faircode/email/FragmentDialogAddImage.java b/app/src/main/java/eu/faircode/email/FragmentDialogAddImage.java
index 0f0c7b5b3f..1d6197b17b 100644
--- a/app/src/main/java/eu/faircode/email/FragmentDialogAddImage.java
+++ b/app/src/main/java/eu/faircode/email/FragmentDialogAddImage.java
@@ -54,7 +54,7 @@ public class FragmentDialogAddImage extends FragmentDialogBase {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean add_inline = prefs.getBoolean("add_inline", true);
boolean resize_images = prefs.getBoolean("resize_images", true);
- int resize = prefs.getInt("resize", FragmentCompose.REDUCED_IMAGE_SIZE);
+ int resize = prefs.getInt("resize", ComposeHelper.REDUCED_IMAGE_SIZE);
boolean privacy_images = prefs.getBoolean("privacy_images", false);
boolean image_dialog = prefs.getBoolean("image_dialog", true);
diff --git a/app/src/main/java/eu/faircode/email/LoaderComposeAction.java b/app/src/main/java/eu/faircode/email/LoaderComposeAction.java
new file mode 100644
index 0000000000..8c0e785286
--- /dev/null
+++ b/app/src/main/java/eu/faircode/email/LoaderComposeAction.java
@@ -0,0 +1,812 @@
+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-2023 by Marcel Bokhorst (M66B)
+*/
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.widget.Toast;
+
+import androidx.preference.PreferenceManager;
+
+import org.apache.poi.ss.formula.eval.NotImplementedException;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.regex.Pattern;
+
+import javax.mail.Address;
+import javax.mail.Message;
+import javax.mail.MessageRemovedException;
+import javax.mail.Part;
+import javax.mail.Session;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+
+public abstract class LoaderComposeAction extends SimpleTask {
+ private static final int MAX_REASONABLE_SIZE = 5 * 1024 * 1024;
+
+ @Override
+ protected EntityMessage onExecute(final Context context, Bundle args) throws Throwable {
+ // Get data
+ long id = args.getLong("id");
+ int action = args.getInt("action");
+ long aid = args.getLong("account");
+ long iid = args.getLong("identity");
+ String extra = args.getString("extra");
+ String to = args.getString("to");
+ String cc = args.getString("cc");
+ String bcc = args.getString("bcc");
+ String subject = args.getString("subject");
+ Spanned loaded = (Spanned) args.getCharSequence("loaded");
+ Spanned spanned = (Spanned) args.getCharSequence("spanned");
+ boolean signature = args.getBoolean("signature");
+ boolean empty = args.getBoolean("empty");
+ boolean notext = args.getBoolean("notext");
+ Bundle extras = args.getBundle("extras");
+
+ boolean silent = extras.getBoolean("silent");
+
+ boolean dirty = false;
+ String body = HtmlHelper.toHtml(spanned, context);
+ EntityMessage draft;
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean discard_delete = prefs.getBoolean("discard_delete", true);
+ boolean write_below = prefs.getBoolean("write_below", false);
+ boolean save_drafts = prefs.getBoolean("save_drafts", true);
+ int send_delayed = prefs.getInt("send_delayed", 0);
+
+ DB db = DB.getInstance(context);
+ try {
+ db.beginTransaction();
+
+ // Get draft & selected identity
+ draft = db.message().getMessage(id);
+ EntityIdentity identity = db.identity().getIdentity(iid);
+
+ // Draft deleted by server
+ if (draft == null || draft.ui_hide)
+ throw new MessageRemovedException("Draft for action was deleted hide=" + (draft != null));
+
+ Log.i("Load action id=" + draft.id + " action=" + getActionName(action));
+
+ if (action == R.id.action_delete) {
+ dirty = true;
+ EntityFolder trash = db.folder().getFolderByType(draft.account, EntityFolder.TRASH);
+ EntityFolder drafts = db.folder().getFolderByType(draft.account, EntityFolder.DRAFTS);
+ if (empty || trash == null || discard_delete || !save_drafts || (drafts != null && drafts.local))
+ EntityOperation.queue(context, draft, EntityOperation.DELETE);
+ else {
+ Map c = new HashMap<>();
+ c.put("id", draft.id == null ? null : Long.toString(draft.id));
+ c.put("encrypt", draft.encrypt + "/" + draft.ui_encrypt);
+ Log.breadcrumb("Discard draft", c);
+
+ EntityOperation.queue(context, draft, EntityOperation.ADD);
+ EntityOperation.queue(context, draft, EntityOperation.MOVE, trash.id);
+ }
+
+ ApplicationEx.getMainHandler().post(new Runnable() {
+ public void run() {
+ ToastEx.makeText(context, R.string.title_draft_deleted, Toast.LENGTH_LONG).show();
+ }
+ });
+ } else {
+ // Move draft to new account
+ if (draft.account != aid && aid >= 0) {
+ Log.i("Account changed");
+
+ Long uid = draft.uid;
+ String msgid = draft.msgid;
+ boolean content = draft.content;
+ Boolean ui_hide = draft.ui_hide;
+
+ // To prevent violating constraints
+ draft.uid = null;
+ draft.msgid = null;
+ db.message().updateMessage(draft);
+
+ // Create copy to delete
+ draft.id = null;
+ draft.uid = uid;
+ draft.msgid = msgid;
+ draft.content = false;
+ draft.ui_hide = true;
+ draft.id = db.message().insertMessage(draft);
+ EntityOperation.queue(context, draft, EntityOperation.DELETE);
+
+ // Restore original with new account, no uid and new msgid
+ draft.id = id;
+ draft.account = aid;
+ draft.folder = db.folder().getFolderByType(aid, EntityFolder.DRAFTS).id;
+ draft.uid = null;
+ draft.msgid = EntityMessage.generateMessageId();
+ draft.content = content;
+ draft.ui_hide = ui_hide;
+ db.message().updateMessage(draft);
+
+ if (draft.content)
+ dirty = true;
+ }
+
+ Map crumb = new HashMap<>();
+ crumb.put("draft", draft.folder + ":" + draft.id);
+ crumb.put("content", Boolean.toString(draft.content));
+ crumb.put("revision", Integer.toString(draft.revision == null ? -1 : draft.revision));
+ crumb.put("revisions", Integer.toString(draft.revisions == null ? -1 : draft.revisions));
+ crumb.put("file", Boolean.toString(draft.getFile(context).exists()));
+ crumb.put("action", getActionName(action));
+ Log.breadcrumb("compose", crumb);
+
+ List attachments = db.attachment().getAttachments(draft.id);
+
+ // Get data
+ InternetAddress[] afrom = (identity == null ? null : new InternetAddress[]{new InternetAddress(identity.email, identity.name, StandardCharsets.UTF_8.name())});
+ InternetAddress[] ato = MessageHelper.dedup(MessageHelper.parseAddresses(context, to));
+ InternetAddress[] acc = MessageHelper.dedup(MessageHelper.parseAddresses(context, cc));
+ InternetAddress[] abcc = MessageHelper.dedup(MessageHelper.parseAddresses(context, bcc));
+
+ // Safe guard
+ if (action == R.id.action_send) {
+ checkAddress(ato, context);
+ checkAddress(acc, context);
+ checkAddress(abcc, context);
+ }
+
+ if (TextUtils.isEmpty(extra))
+ extra = null;
+
+ List eparts = new ArrayList<>();
+ for (EntityAttachment attachment : attachments)
+ if (attachment.available)
+ if (attachment.isEncryption())
+ eparts.add(attachment.encryption);
+
+ if (EntityMessage.PGP_SIGNONLY.equals(draft.ui_encrypt)) {
+ if (!eparts.contains(EntityAttachment.PGP_KEY) ||
+ !eparts.contains(EntityAttachment.PGP_SIGNATURE) ||
+ !eparts.contains(EntityAttachment.PGP_CONTENT))
+ dirty = true;
+ } else if (EntityMessage.PGP_ENCRYPTONLY.equals(draft.ui_encrypt)) {
+ if (!eparts.contains(EntityAttachment.PGP_MESSAGE))
+ dirty = true;
+ } else if (EntityMessage.PGP_SIGNENCRYPT.equals(draft.ui_encrypt)) {
+ if (!eparts.contains(EntityAttachment.PGP_KEY) ||
+ !eparts.contains(EntityAttachment.PGP_MESSAGE))
+ dirty = true;
+ } else if (EntityMessage.SMIME_SIGNONLY.equals(draft.ui_encrypt)) {
+ if (!eparts.contains(EntityAttachment.SMIME_SIGNATURE) ||
+ !eparts.contains(EntityAttachment.SMIME_CONTENT))
+ dirty = true;
+ } else if (EntityMessage.SMIME_SIGNENCRYPT.equals(draft.ui_encrypt)) {
+ if (!eparts.contains(EntityAttachment.SMIME_MESSAGE))
+ dirty = true;
+ }
+
+ Long ident = (identity == null ? null : identity.id);
+ Pair> last = get();
+ if (!Objects.equals(draft.identity, ident) ||
+ !Objects.equals(draft.extra, extra) ||
+ !MessageHelper.equal(draft.from, afrom) ||
+ !MessageHelper.equal(draft.to, ato) ||
+ !MessageHelper.equal(draft.cc, acc) ||
+ !MessageHelper.equal(draft.bcc, abcc) ||
+ !Objects.equals(draft.subject, subject) ||
+ !draft.signature.equals(signature) ||
+ !Objects.equals(last.first, draft.plain_only) ||
+ !EntityAttachment.equals(last.second, attachments))
+ dirty = true;
+
+ set(draft.plain_only, attachments);
+
+ if (dirty) {
+ // Update draft
+ draft.identity = ident;
+ draft.extra = extra;
+ draft.from = afrom;
+ draft.to = ato;
+ draft.cc = acc;
+ draft.bcc = abcc;
+ draft.subject = subject;
+ draft.signature = signature;
+ draft.sender = MessageHelper.getSortKey(draft.from);
+ Uri lookupUri = ContactInfo.getLookupUri(draft.from);
+ draft.avatar = (lookupUri == null ? null : lookupUri.toString());
+ db.message().updateMessage(draft);
+ }
+
+ Document doc = JsoupEx.parse(draft.getFile(context));
+ Element first = (doc.body().childrenSize() == 0 ? null : doc.body().child(0));
+ boolean below = (first != null && first.attr("fairemail").equals("reference"));
+ doc.select("div[fairemail=signature]").remove();
+ Elements ref = doc.select("div[fairemail=reference]");
+ ref.remove();
+
+ if (extras.containsKey("html"))
+ dirty = true;
+
+ boolean wb = (draft == null || draft.write_below == null ? write_below : draft.write_below);
+ if (below != wb &&
+ doc.body().childrenSize() > 0 &&
+ draft.wasforwardedfrom == null)
+ dirty = true;
+
+ if (!dirty)
+ if (loaded == null) {
+ Document b = JsoupEx.parse(body); // Is-dirty
+ if (!Objects.equals(b.body().html(), doc.body().html()))
+ dirty = true;
+ } else {
+ // Was not dirty before
+ String hloaded = HtmlHelper.toHtml(loaded, context);
+ String hspanned = HtmlHelper.toHtml(spanned, context);
+ if (!Objects.equals(hloaded, hspanned))
+ dirty = true;
+ }
+
+ if (draft.revision == null) {
+ draft.revision = 1;
+ draft.revisions = 1;
+ }
+
+ int revision = draft.revision; // Save for undo/redo
+ if (dirty) {
+ dirty = true;
+
+ // Get saved body
+ Document d;
+ if (extras.containsKey("html")) {
+ // Save current revision
+ Document c = JsoupEx.parse(body);
+
+ for (Element e : ref)
+ if (wb && draft.wasforwardedfrom == null)
+ c.body().prependChild(e);
+ else
+ c.body().appendChild(e);
+
+ ComposeHelper.addSignature(context, c, draft, identity);
+
+ Helper.writeText(draft.getFile(context, draft.revision), c.html());
+
+ d = JsoupEx.parse(extras.getString("html"));
+ } else {
+ d = JsoupEx.parse(body); // Save
+
+ for (Element e : ref)
+ if (wb && draft.wasforwardedfrom == null)
+ d.body().prependChild(e);
+ else
+ d.body().appendChild(e);
+
+ ComposeHelper.addSignature(context, d, draft, identity);
+ }
+
+ body = d.html();
+
+ // Create new revision
+ draft.revisions++;
+ draft.revision = draft.revisions;
+
+ Helper.writeText(draft.getFile(context, draft.revision), body);
+ } else
+ body = Helper.readText(draft.getFile(context));
+
+ if (action == R.id.action_undo || action == R.id.action_redo) {
+ if (action == R.id.action_undo) {
+ if (revision > 1)
+ draft.revision = revision - 1;
+ else
+ draft.revision = revision;
+ } else {
+ if (revision < draft.revisions)
+ draft.revision = revision + 1;
+ else
+ draft.revision = revision;
+ }
+
+ // Restore revision
+ Log.i("Restoring revision=" + draft.revision);
+ File file = draft.getFile(context, draft.revision);
+ if (file.exists())
+ body = Helper.readText(file);
+ else
+ Log.e("Missing" +
+ " revision=" + draft.revision + "/" + draft.revisions +
+ " action=" + getActionName(action));
+
+ dirty = true;
+ } else if (action == R.id.action_send) {
+ if (!draft.isPlainOnly()) {
+ // Remove unused inline images
+ List cids = new ArrayList<>();
+ Document d = JsoupEx.parse(body);
+ for (Element element : d.select("img")) {
+ String src = element.attr("src");
+ if (src.startsWith("cid:"))
+ cids.add("<" + src.substring(4) + ">");
+ }
+
+ for (EntityAttachment attachment : new ArrayList<>(attachments))
+ if (attachment.isInline() && attachment.isImage() &&
+ attachment.cid != null && !cids.contains(attachment.cid)) {
+ Log.i("Removing unused inline attachment cid=" + attachment.cid);
+ attachments.remove(attachment);
+ db.attachment().deleteAttachment(attachment.id);
+ dirty = true;
+ }
+ } else {
+ // Convert inline images to attachments
+ for (EntityAttachment attachment : new ArrayList<>(attachments))
+ if (attachment.isInline() && attachment.isImage()) {
+ Log.i("Converting to attachment cid=" + attachment.cid);
+ attachment.disposition = Part.ATTACHMENT;
+ attachment.cid = null;
+ db.attachment().setDisposition(attachment.id, attachment.disposition, attachment.cid);
+ dirty = true;
+ }
+ }
+ }
+
+ File f = draft.getFile(context);
+ Helper.writeText(f, body);
+ if (f.length() > MAX_REASONABLE_SIZE)
+ args.putBoolean("large", true);
+
+ String full = HtmlHelper.getFullText(body);
+ draft.preview = HtmlHelper.getPreview(full);
+ draft.language = HtmlHelper.getLanguage(context, draft.subject, full);
+ db.message().setMessageContent(draft.id,
+ true,
+ draft.language,
+ draft.plain_only, // unchanged
+ draft.preview,
+ null);
+
+ db.message().setMessageRevision(draft.id, draft.revision);
+ db.message().setMessageRevisions(draft.id, draft.revisions);
+
+ if (dirty) {
+ draft.received = new Date().getTime();
+ draft.sent = draft.received;
+ db.message().setMessageReceived(draft.id, draft.received);
+ db.message().setMessageSent(draft.id, draft.sent);
+ }
+
+ if (silent) {
+ // Skip storing on the server, etc
+ db.setTransactionSuccessful();
+ return draft;
+ }
+
+ // Execute action
+ boolean encrypted = extras.getBoolean("encrypted");
+ boolean shouldEncrypt = EntityMessage.PGP_ENCRYPTONLY.equals(draft.ui_encrypt) ||
+ EntityMessage.PGP_SIGNENCRYPT.equals(draft.ui_encrypt) ||
+ (EntityMessage.PGP_SIGNONLY.equals(draft.ui_encrypt) && action == R.id.action_send) ||
+ EntityMessage.SMIME_SIGNENCRYPT.equals(draft.ui_encrypt) ||
+ (EntityMessage.SMIME_SIGNONLY.equals(draft.ui_encrypt) && action == R.id.action_send);
+ boolean needsEncryption = (dirty && !encrypted && shouldEncrypt);
+ boolean autosave = extras.getBoolean("autosave");
+ if (needsEncryption && !autosave) {
+ args.putBoolean("needsEncryption", true);
+ db.setTransactionSuccessful();
+ return draft;
+ }
+
+ if (!shouldEncrypt && !autosave)
+ for (EntityAttachment attachment : attachments)
+ if (attachment.isEncryption())
+ db.attachment().deleteAttachment(attachment.id);
+
+ if (action == R.id.action_save ||
+ action == R.id.action_undo ||
+ action == R.id.action_redo ||
+ action == R.id.action_check) {
+ boolean unencrypted =
+ (!EntityMessage.PGP_ENCRYPTONLY.equals(draft.ui_encrypt) &&
+ !EntityMessage.PGP_SIGNENCRYPT.equals(draft.ui_encrypt) &&
+ !EntityMessage.SMIME_SIGNENCRYPT.equals(draft.ui_encrypt));
+ if ((dirty && unencrypted) || encrypted) {
+ if (save_drafts) {
+ Map c = new HashMap<>();
+ c.put("id", draft.id == null ? null : Long.toString(draft.id));
+ c.put("dirty", Boolean.toString(dirty));
+ c.put("encrypt", draft.encrypt + "/" + draft.ui_encrypt);
+ c.put("encrypted", Boolean.toString(encrypted));
+ c.put("needsEncryption", Boolean.toString(needsEncryption));
+ c.put("autosave", Boolean.toString(autosave));
+ Log.breadcrumb("Save draft", c);
+
+ EntityOperation.queue(context, draft, EntityOperation.ADD);
+ }
+ }
+
+ if (action == R.id.action_check) {
+ // Check data
+ if (draft.identity == null)
+ throw new IllegalArgumentException(context.getString(R.string.title_from_missing));
+
+ if (false) {
+ EntityAccount account = db.account().getAccount(draft.account);
+ EntityFolder sent = db.folder().getFolderByType(draft.account, EntityFolder.SENT);
+ if (account != null && account.protocol == EntityAccount.TYPE_IMAP && sent == null)
+ args.putBoolean("sent_missing", true);
+ }
+
+ try {
+ checkAddress(ato, context);
+ checkAddress(acc, context);
+ checkAddress(abcc, context);
+
+ List check = new ArrayList<>();
+ List checked = new ArrayList<>();
+ List dup = new ArrayList<>();
+ if (ato != null)
+ check.addAll(Arrays.asList(ato));
+ if (acc != null)
+ check.addAll(Arrays.asList(acc));
+ if (abcc != null)
+ check.addAll(Arrays.asList(abcc));
+
+ for (InternetAddress a : check) {
+ String email = a.getAddress();
+ if (TextUtils.isEmpty(email))
+ continue;
+ if (checked.contains(a.getAddress()))
+ dup.add(email);
+ else
+ checked.add(email);
+ }
+
+ if (dup.size() > 0)
+ throw new AddressException(context.getString(
+ R.string.title_address_duplicate,
+ TextUtils.join(", ", dup)));
+ } catch (AddressException ex) {
+ args.putString("address_error", ex.getMessage());
+ }
+
+ if (draft.to == null && draft.cc == null && draft.bcc == null &&
+ (identity == null || (identity.cc == null && identity.bcc == null)))
+ args.putBoolean("remind_to", true);
+
+ //if (TextUtils.isEmpty(draft.extra) &&
+ // identity != null && identity.sender_extra)
+ // args.putBoolean("remind_extra", true);
+
+ List recipients = new ArrayList<>();
+ if (draft.to != null)
+ recipients.addAll(Arrays.asList(draft.to));
+ if (draft.cc != null)
+ recipients.addAll(Arrays.asList(draft.cc));
+ if (draft.bcc != null)
+ recipients.addAll(Arrays.asList(draft.bcc));
+
+ boolean noreply = false;
+ for (Address recipient : recipients)
+ if (MessageHelper.isNoReply(recipient)) {
+ noreply = true;
+ break;
+ }
+ args.putBoolean("remind_noreply", noreply);
+
+ if (identity != null && !TextUtils.isEmpty(identity.internal)) {
+ boolean external = false;
+ String[] internals = identity.internal.split(",");
+ for (Address recipient : recipients) {
+ String email = ((InternetAddress) recipient).getAddress();
+ String domain = UriHelper.getEmailDomain(email);
+ if (domain == null)
+ continue;
+
+ boolean found = false;
+ for (String internal : internals)
+ if (internal.equalsIgnoreCase(domain)) {
+ found = true;
+ break;
+ }
+ if (!found) {
+ external = true;
+ break;
+ }
+ }
+ args.putBoolean("remind_external", external);
+ }
+
+ if ((draft.dsn == null ||
+ EntityMessage.DSN_NONE.equals(draft.dsn)) &&
+ (draft.ui_encrypt == null ||
+ EntityMessage.ENCRYPT_NONE.equals(draft.ui_encrypt))) {
+ args.putBoolean("remind_pgp", PgpHelper.hasPgpKey(context, recipients, false));
+ args.putBoolean("remind_smime", SmimeHelper.hasSmimeKey(context, recipients, false));
+ }
+
+ if (TextUtils.isEmpty(draft.subject))
+ args.putBoolean("remind_subject", true);
+
+ Document d = JsoupEx.parse(body);
+
+ if (notext &&
+ d.select("div[fairemail=reference]").isEmpty())
+ args.putBoolean("remind_text", true);
+
+ boolean styled = HtmlHelper.isStyled(d);
+ args.putBoolean("styled", styled);
+
+ int attached = 0;
+ List dangerous = new ArrayList<>();
+ for (EntityAttachment attachment : attachments) {
+ if (!attachment.available)
+ throw new IllegalArgumentException(context.getString(R.string.title_attachments_missing));
+ else if (attachment.isAttachment())
+ attached++;
+ String ext = Helper.getExtension(attachment.name);
+ if (Helper.DANGEROUS_EXTENSIONS.contains(ext))
+ dangerous.add(attachment.name);
+ }
+ if (dangerous.size() > 0)
+ args.putString("remind_extension", String.join(", ", dangerous));
+
+ // Check for missing attachments
+ if (attached == 0) {
+ List keywords = new ArrayList<>();
+ for (String text : Helper.getStrings(context, R.string.title_attachment_keywords))
+ keywords.addAll(Arrays.asList(text.split(",")));
+
+ d.select("div[fairemail=signature]").remove();
+ d.select("div[fairemail=reference]").remove();
+
+ String text = d.text();
+ for (String keyword : keywords)
+ if (text.matches("(?si).*\\b" + Pattern.quote(keyword.trim()) + "\\b.*")) {
+ args.putBoolean("remind_attachment", true);
+ break;
+ }
+ }
+
+ if (EntityMessage.DSN_HARD_BOUNCE.equals(draft.dsn))
+ args.putBoolean("remind_dsn", true);
+
+ // Check size
+ if (identity != null && identity.max_size != null)
+ try {
+ Properties props = MessageHelper.getSessionProperties(true);
+ if (identity.unicode)
+ props.put("mail.mime.allowutf8", "true");
+ Session isession = Session.getInstance(props, null);
+ Message imessage = MessageHelper.from(context, draft, identity, isession, false);
+
+ File file = draft.getRawFile(context);
+ try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
+ imessage.writeTo(os);
+ }
+
+ long size = file.length();
+ if (size > identity.max_size) {
+ args.putBoolean("remind_size", true);
+ args.putLong("size", size);
+ args.putLong("max_size", identity.max_size);
+ }
+ } catch (Throwable ex) {
+ Log.e(ex);
+ }
+
+ args.putBoolean("remind_internet", !ConnectionHelper.getNetworkState(context).isConnected());
+ } else {
+ int mid;
+ if (action == R.id.action_undo)
+ mid = R.string.title_undo;
+ else if (action == R.id.action_redo)
+ mid = R.string.title_redo;
+ else
+ mid = R.string.title_draft_saved;
+ final String msg = context.getString(mid) +
+ (BuildConfig.DEBUG
+ ? " " + draft.revision + (dirty ? "*" : "")
+ : "");
+
+ ApplicationEx.getMainHandler().post(new Runnable() {
+ public void run() {
+ ToastEx.makeText(context, msg, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ } else if (action == R.id.action_send) {
+ EntityFolder outbox = EntityFolder.getOutbox(context);
+
+ // Delay sending message
+ if (draft.ui_snoozed == null && send_delayed != 0) {
+ if (extras.getBoolean("now"))
+ draft.ui_snoozed = null;
+ else
+ draft.ui_snoozed = new Date().getTime() + send_delayed * 1000L;
+ }
+
+ if (draft.ui_snoozed != null)
+ draft.received = draft.ui_snoozed;
+
+ // Copy message to outbox
+ long did = draft.id;
+
+ draft.id = null;
+ draft.folder = outbox.id;
+ draft.uid = null;
+ draft.fts = false;
+ draft.ui_hide = false;
+ draft.id = db.message().insertMessage(draft);
+ Helper.writeText(draft.getFile(context), body);
+
+ // Move attachments
+ for (EntityAttachment attachment : attachments)
+ db.attachment().setMessage(attachment.id, draft.id);
+
+ // Send message
+ if (draft.ui_snoozed == null)
+ EntityOperation.queue(context, draft, EntityOperation.SEND);
+
+ // Delete draft (cannot move to outbox)
+ EntityMessage tbd = db.message().getMessage(did);
+ if (tbd != null)
+ EntityOperation.queue(context, tbd, EntityOperation.DELETE);
+
+ final String feedback;
+ if (draft.ui_snoozed == null) {
+ boolean suitable = ConnectionHelper.getNetworkState(context).isSuitable();
+ if (suitable)
+ feedback = context.getString(R.string.title_queued);
+ else
+ feedback = context.getString(R.string.title_notification_waiting);
+ } else {
+ DateFormat DTF = Helper.getDateTimeInstance(context);
+ feedback = context.getString(R.string.title_queued_at, DTF.format(draft.ui_snoozed));
+ }
+
+ toast(feedback);
+
+ if (extras.getBoolean("archive")) {
+ EntityFolder archive = db.folder().getFolderByType(draft.account, EntityFolder.ARCHIVE);
+ if (archive != null) {
+ List messages = db.message().getMessagesByMsgId(draft.account, draft.inreplyto);
+ if (messages != null)
+ for (EntityMessage message : messages)
+ EntityOperation.queue(context, message, EntityOperation.MOVE, archive.id);
+ }
+ }
+ }
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ if (action == R.id.action_check)
+ try {
+ InternetAddress[] ato = MessageHelper.dedup(MessageHelper.parseAddresses(context, to));
+ InternetAddress[] acc = MessageHelper.dedup(MessageHelper.parseAddresses(context, cc));
+ InternetAddress[] abcc = MessageHelper.dedup(MessageHelper.parseAddresses(context, bcc));
+
+ try {
+ checkMx(ato, context);
+ checkMx(acc, context);
+ checkMx(abcc, context);
+ } catch (UnknownHostException ex) {
+ args.putString("mx_error", ex.getMessage());
+ }
+ } catch (Throwable ignored) {
+ }
+
+ args.putBoolean("dirty", dirty);
+ if (dirty)
+ ServiceSynchronize.eval(context, "compose/action");
+
+ if (action == R.id.action_send)
+ if (draft.ui_snoozed == null)
+ ServiceSend.start(context);
+ else {
+ Log.i("Delayed send id=" + draft.id + " at " + new Date(draft.ui_snoozed));
+ EntityMessage.snooze(context, draft.id, draft.ui_snoozed);
+ }
+
+ return draft;
+ }
+
+ protected Pair> get() {
+ throw new NotImplementedException("LoaderAction");
+ }
+
+ protected void set(Integer plain_only, List attachments) {
+ throw new NotImplementedException("LoaderAction");
+ }
+
+ protected void toast(String feedback) {
+ throw new NotImplementedException("LoaderAction");
+ }
+
+ private void checkAddress(InternetAddress[] addresses, Context context) throws AddressException {
+ if (addresses == null)
+ return;
+
+ for (InternetAddress address : addresses)
+ try {
+ address.validate();
+ } catch (AddressException ex) {
+ throw new AddressException(context.getString(R.string.title_address_parse_error,
+ MessageHelper.formatAddressesCompose(new Address[]{address}), ex.getMessage()));
+ }
+ }
+
+ private void checkMx(InternetAddress[] addresses, Context context) throws UnknownHostException {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean lookup_mx = prefs.getBoolean("lookup_mx", false);
+ if (!lookup_mx)
+ return;
+
+ if (addresses == null)
+ return;
+
+ ConnectivityManager cm = Helper.getSystemService(context, ConnectivityManager.class);
+ NetworkInfo ani = (cm == null ? null : cm.getActiveNetworkInfo());
+ if (ani != null && ani.isConnected())
+ DnsHelper.checkMx(context, addresses);
+ }
+
+ static String getActionName(int id) {
+ if (id == R.id.action_delete) {
+ return "delete";
+ } else if (id == R.id.action_undo) {
+ return "undo";
+ } else if (id == R.id.action_redo) {
+ return "redo";
+ } else if (id == R.id.action_save) {
+ return "save";
+ } else if (id == R.id.action_check) {
+ return "check";
+ } else if (id == R.id.action_send) {
+ return "send";
+ }
+ return Integer.toString(id);
+ }
+}
diff --git a/app/src/main/java/eu/faircode/email/LoaderComposeDraft.java b/app/src/main/java/eu/faircode/email/LoaderComposeDraft.java
new file mode 100644
index 0000000000..d784cbc329
--- /dev/null
+++ b/app/src/main/java/eu/faircode/email/LoaderComposeDraft.java
@@ -0,0 +1,983 @@
+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-2023 by Marcel Bokhorst (M66B)
+*/
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.OperationCanceledException;
+import android.text.Spanned;
+import android.text.TextUtils;
+
+import androidx.preference.PreferenceManager;
+
+import org.apache.poi.ss.formula.eval.NotImplementedException;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.w3c.dom.css.CSSStyleSheet;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.mail.Address;
+import javax.mail.MessageRemovedException;
+import javax.mail.Part;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+
+import biweekly.ICalendar;
+import biweekly.component.VEvent;
+import biweekly.property.Organizer;
+
+public abstract class LoaderComposeDraft extends SimpleTask {
+ private static final int MAX_QUOTE_LEVEL = 5;
+
+ @Override
+ protected ComposeHelper.DraftData onExecute(Context context, Bundle args) throws Throwable {
+ String action = args.getString("action");
+ long id = args.getLong("id", -1);
+ long aid = args.getLong("account", -1);
+ long iid = args.getLong("identity", -1);
+ long reference = args.getLong("reference", -1);
+ int dsn = args.getInt("dsn", EntityMessage.DSN_RECEIPT);
+ File ics = (File) args.getSerializable("ics");
+ String status = args.getString("status");
+ // raw
+ long answer = args.getLong("answer", -1);
+ String to = args.getString("to");
+ String cc = args.getString("cc");
+ String bcc = args.getString("bcc");
+ // inreplyto
+ String external_subject = args.getString("subject", "");
+ String external_body = args.getString("body", "");
+ String external_text = args.getString("text");
+ CharSequence selected_text = args.getCharSequence("selected");
+ ArrayList uris = args.getParcelableArrayList("attachments");
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean plain_only = prefs.getBoolean("plain_only", false);
+ boolean plain_only_reply = prefs.getBoolean("plain_only_reply", false);
+ boolean resize_reply = prefs.getBoolean("resize_reply", true);
+ boolean sign_default = prefs.getBoolean("sign_default", false);
+ boolean encrypt_default = prefs.getBoolean("encrypt_default", false);
+ boolean receipt_default = prefs.getBoolean("receipt_default", false);
+ boolean write_below = prefs.getBoolean("write_below", false);
+ boolean save_drafts = prefs.getBoolean("save_drafts", true);
+ boolean auto_identity = prefs.getBoolean("auto_identity", false);
+ boolean suggest_sent = prefs.getBoolean("suggest_sent", true);
+ boolean suggest_received = prefs.getBoolean("suggest_received", false);
+ boolean forward_new = prefs.getBoolean("forward_new", true);
+
+ Log.i("Load draft action=" + action + " id=" + id + " reference=" + reference);
+
+ Map crumb = new HashMap<>();
+ crumb.put("draft", Long.toString(id));
+ crumb.put("reference", Long.toString(reference));
+ crumb.put("action", action);
+ Log.breadcrumb("compose", crumb);
+
+ ComposeHelper.DraftData data = new ComposeHelper.DraftData();
+
+ DB db = DB.getInstance(context);
+ try {
+ db.beginTransaction();
+
+ data.identities = db.identity().getComposableIdentities(null);
+ if (data.identities == null || data.identities.size() == 0)
+ throw new OperationCanceledException(context.getString(R.string.title_no_composable));
+
+ data.draft = db.message().getMessage(id);
+ boolean wb = (data.draft == null || data.draft.write_below == null ? write_below : data.draft.write_below);
+ if (data.draft == null || data.draft.ui_hide) {
+ // New draft
+ if ("edit".equals(action))
+ throw new MessageRemovedException("Draft for edit was deleted hide=" + (data.draft != null));
+
+ EntityMessage ref = db.message().getMessage(reference);
+
+ data.draft = new EntityMessage();
+ data.draft.msgid = EntityMessage.generateMessageId();
+
+ // Select identity matching from address
+ EntityIdentity selected = null;
+
+ if (aid < 0)
+ if (ref == null) {
+ EntityAccount primary = db.account().getPrimaryAccount();
+ if (primary != null)
+ aid = primary.id;
+ } else
+ aid = ref.account;
+ if (iid < 0 && ref != null && ref.identity != null)
+ iid = ref.identity;
+
+ if (iid >= 0)
+ for (EntityIdentity identity : data.identities)
+ if (identity.id.equals(iid)) {
+ selected = identity;
+ EntityLog.log(context, "Selected requested identity=" + iid);
+ break;
+ }
+
+ if (ref != null) {
+ Address[] refto;
+ boolean self = ref.replySelf(data.identities, ref.account);
+ if (ref.to == null || ref.to.length == 0 || self)
+ refto = ref.from;
+ else
+ refto = ref.to;
+ Log.i("Ref self=" + self +
+ " to=" + MessageHelper.formatAddresses(refto));
+ if (refto != null && refto.length > 0) {
+ if (selected == null)
+ for (Address sender : refto)
+ for (EntityIdentity identity : data.identities)
+ if (identity.account.equals(aid) &&
+ identity.sameAddress(sender)) {
+ selected = identity;
+ EntityLog.log(context, "Selected same account/identity");
+ break;
+ }
+
+ if (selected == null)
+ for (Address sender : refto)
+ for (EntityIdentity identity : data.identities)
+ if (identity.account.equals(aid) &&
+ identity.similarAddress(sender)) {
+ selected = identity;
+ EntityLog.log(context, "Selected similar account/identity");
+ break;
+ }
+
+ if (selected == null)
+ for (Address sender : refto)
+ for (EntityIdentity identity : data.identities)
+ if (identity.sameAddress(sender)) {
+ selected = identity;
+ EntityLog.log(context, "Selected same */identity");
+ break;
+ }
+
+ if (selected == null)
+ for (Address sender : refto)
+ for (EntityIdentity identity : data.identities)
+ if (identity.similarAddress(sender)) {
+ selected = identity;
+ EntityLog.log(context, "Selected similer */identity");
+ break;
+ }
+ }
+ }
+
+ if (selected == null && auto_identity)
+ try {
+ Address[] tos = MessageHelper.parseAddresses(context, to);
+ if (tos != null && tos.length > 0) {
+ String email = ((InternetAddress) tos[0]).getAddress();
+ List identities = null;
+ if (suggest_sent)
+ identities = db.contact().getIdentities(email, EntityContact.TYPE_TO);
+ if (suggest_received && (identities == null || identities.size() == 0))
+ identities = db.contact().getIdentities(email, EntityContact.TYPE_FROM);
+ if (identities != null && identities.size() == 1) {
+ EntityIdentity identity = db.identity().getIdentity(identities.get(0));
+ if (identity != null)
+ selected = identity;
+ }
+ }
+ } catch (AddressException ex) {
+ Log.i(ex);
+ }
+
+ if (selected == null)
+ for (EntityIdentity identity : data.identities)
+ if (identity.account.equals(aid) && identity.primary) {
+ selected = identity;
+ EntityLog.log(context, "Selected primary account/identity");
+ break;
+ }
+
+ if (selected == null)
+ for (EntityIdentity identity : data.identities)
+ if (identity.account.equals(aid)) {
+ selected = identity;
+ EntityLog.log(context, "Selected account/identity");
+ break;
+ }
+
+ if (selected == null)
+ for (EntityIdentity identity : data.identities)
+ if (identity.primary) {
+ selected = identity;
+ EntityLog.log(context, "Selected primary */identity");
+ break;
+ }
+
+ if (selected == null)
+ for (EntityIdentity identity : data.identities) {
+ selected = identity;
+ EntityLog.log(context, "Selected */identity");
+ break;
+ }
+
+ if (selected == null)
+ throw new OperationCanceledException(context.getString(R.string.title_no_composable));
+
+ EntityLog.log(context, "Selected=" + selected.email);
+
+ if (!"dsn".equals(action)) {
+ if (plain_only &&
+ !"resend".equals(action) &&
+ !"editasnew".equals(action))
+ data.draft.plain_only = 1;
+
+ if (encrypt_default || selected.encrypt_default)
+ if (selected.encrypt == 0)
+ data.draft.ui_encrypt = EntityMessage.PGP_SIGNENCRYPT;
+ else
+ data.draft.ui_encrypt = EntityMessage.SMIME_SIGNENCRYPT;
+ else if (sign_default || selected.sign_default)
+ if (selected.encrypt == 0)
+ data.draft.ui_encrypt = EntityMessage.PGP_SIGNONLY;
+ else
+ data.draft.ui_encrypt = EntityMessage.SMIME_SIGNONLY;
+ }
+
+ if (receipt_default)
+ data.draft.receipt_request = true;
+
+ data.draft.sensitivity = (selected.sensitivity < 1 ? null : selected.sensitivity);
+
+ Document document = Document.createShell("");
+
+ if (ref == null) {
+ data.draft.thread = data.draft.msgid;
+
+ try {
+ data.draft.to = MessageHelper.parseAddresses(context, to);
+ } catch (AddressException ex) {
+ Log.w(ex);
+ }
+
+ try {
+ data.draft.cc = MessageHelper.parseAddresses(context, cc);
+ } catch (AddressException ex) {
+ Log.w(ex);
+ }
+
+ try {
+ data.draft.bcc = MessageHelper.parseAddresses(context, bcc);
+ } catch (AddressException ex) {
+ Log.w(ex);
+ }
+
+ data.draft.inreplyto = args.getString("inreplyto", null);
+
+ data.draft.subject = external_subject;
+
+ if (!TextUtils.isEmpty(external_body)) {
+ Document d = JsoupEx.parse(external_body); // Passed html
+ Element e = document
+ .createElement("div")
+ .html(d.body().html());
+ document.body().appendChild(e);
+ }
+
+ EntityAnswer a = (answer < 0
+ ? db.answer().getStandardAnswer()
+ : db.answer().getAnswer(answer));
+ if (a != null) {
+ db.answer().applyAnswer(a.id, new Date().getTime());
+ if (answer > 0)
+ data.draft.subject = a.name;
+ if (TextUtils.isEmpty(external_body)) {
+ Document d = JsoupEx.parse(a.getHtml(context, null));
+ document.body().append(d.body().html());
+ }
+ }
+
+ data.draft.signature = prefs.getBoolean("signature_new", true);
+ ComposeHelper.addSignature(context, document, data.draft, selected);
+ } else {
+ // Actions:
+ // - reply
+ // - reply_all
+ // - forward
+ // - resend
+ // - editasnew
+ // - list
+ // - dsn
+ // - receipt
+ // - participation
+
+ // References
+ if ("reply".equals(action) || "reply_all".equals(action) ||
+ "list".equals(action) ||
+ "dsn".equals(action) ||
+ "participation".equals(action)) {
+ // https://tools.ietf.org/html/rfc5322#section-3.6.4
+ // The "References:" field will contain the contents of the parent's "References:" field (if any)
+ // followed by the contents of the parent's "Message-ID:" field (if any).
+ String refs = (ref.references == null ? "" : ref.references);
+ if (!TextUtils.isEmpty(ref.msgid))
+ refs = (TextUtils.isEmpty(refs) ? ref.msgid : refs + " " + ref.msgid);
+ data.draft.references = refs;
+ data.draft.inreplyto = ref.msgid;
+ data.draft.thread = ref.thread;
+
+ if ("list".equals(action) && ref.list_post != null)
+ data.draft.to = ref.list_post;
+ else if ("dsn".equals(action)) {
+ if (EntityMessage.DSN_RECEIPT.equals(dsn)) {
+ if (ref.receipt_to != null)
+ data.draft.to = ref.receipt_to;
+ } else if (EntityMessage.DSN_HARD_BOUNCE.equals(dsn)) {
+ if (ref.return_path != null)
+ data.draft.to = ref.return_path;
+ }
+ } else {
+ // Prevent replying to self
+ if (ref.replySelf(data.identities, ref.account)) {
+ EntityLog.log(context, "Reply self ref" +
+ " from=" + MessageHelper.formatAddresses(ref.from) +
+ " to=" + MessageHelper.formatAddresses(ref.to));
+ data.draft.from = ref.from;
+ data.draft.to = ref.to;
+ } else {
+ data.draft.from = ref.to;
+ data.draft.to = (ref.reply == null || ref.reply.length == 0 ? ref.from : ref.reply);
+ }
+
+ if (ref.identity != null) {
+ EntityIdentity recognized = db.identity().getIdentity(ref.identity);
+ EntityLog.log(context, "Recognized=" + (recognized == null ? null : recognized.email));
+
+ Address preferred = null;
+ if (recognized != null) {
+ Address same = null;
+ Address similar = null;
+
+ List addresses = new ArrayList<>();
+ if (data.draft.from != null)
+ addresses.addAll(Arrays.asList(data.draft.from));
+ if (data.draft.to != null)
+ addresses.addAll(Arrays.asList(data.draft.to));
+ if (ref.cc != null)
+ addresses.addAll(Arrays.asList(ref.cc));
+ if (ref.bcc != null)
+ addresses.addAll(Arrays.asList(ref.bcc));
+
+ for (Address from : addresses) {
+ if (same == null && recognized.sameAddress(from))
+ same = from;
+ if (similar == null && recognized.similarAddress(from))
+ similar = from;
+ }
+
+ //if (ref.deliveredto != null)
+ // try {
+ // Address deliveredto = new InternetAddress(ref.deliveredto);
+ // if (same == null && recognized.sameAddress(deliveredto))
+ // same = deliveredto;
+ // if (similar == null && recognized.similarAddress(deliveredto))
+ // similar = deliveredto;
+ // } catch (AddressException ex) {
+ // Log.w(ex);
+ // }
+
+ EntityLog.log(context, "From=" + MessageHelper.formatAddresses(data.draft.from) +
+ " delivered-to=" + ref.deliveredto +
+ " same=" + (same == null ? null : ((InternetAddress) same).getAddress()) +
+ " similar=" + (similar == null ? null : ((InternetAddress) similar).getAddress()));
+
+ preferred = (same == null ? similar : same);
+ }
+
+ if (preferred != null) {
+ String from = ((InternetAddress) preferred).getAddress();
+ String name = ((InternetAddress) preferred).getPersonal();
+ EntityLog.log(context, "Preferred=" + name + " <" + from + ">");
+ if (TextUtils.isEmpty(from) || from.equalsIgnoreCase(recognized.email))
+ from = null;
+ if (!recognized.reply_extra_name ||
+ TextUtils.isEmpty(name) || name.equals(recognized.name))
+ name = null;
+ String username = UriHelper.getEmailUser(from);
+ String extra = (name == null ? "" : name + ", ") +
+ (username == null ? "" : username);
+ data.draft.extra = (TextUtils.isEmpty(extra) ? null : extra);
+ } else
+ EntityLog.log(context, "Preferred=null");
+ } else
+ EntityLog.log(context, "Recognized=null");
+ }
+
+ if ("reply_all".equals(action)) {
+ List all = new ArrayList<>();
+ for (Address recipient : ref.getAllRecipients(data.identities, ref.account)) {
+ boolean found = false;
+ if (data.draft.to != null)
+ for (Address t : data.draft.to)
+ if (MessageHelper.equalEmail(recipient, t)) {
+ found = true;
+ break;
+ }
+ if (!found)
+ all.add(recipient);
+ }
+ data.draft.cc = all.toArray(new Address[0]);
+ } else if ("dsn".equals(action)) {
+ data.draft.dsn = dsn;
+ data.draft.receipt_request = false;
+ }
+
+ } else if ("forward".equals(action)) {
+ if (forward_new)
+ data.draft.thread = data.draft.msgid; // new thread
+ else {
+ data.draft.thread = ref.thread;
+ data.draft.inreplyto = ref.msgid;
+ data.draft.references = (ref.references == null ? "" : ref.references + " ") + ref.msgid;
+ }
+ data.draft.wasforwardedfrom = ref.msgid;
+ } else if ("resend".equals(action)) {
+ data.draft.resend = true;
+ data.draft.thread = data.draft.msgid;
+ data.draft.headers = ref.headers;
+ } else if ("editasnew".equals(action))
+ data.draft.thread = data.draft.msgid;
+
+ // Subject
+ String subject = (ref.subject == null ? "" : ref.subject);
+ if ("reply".equals(action) || "reply_all".equals(action)) {
+ data.draft.subject =
+ EntityMessage.getSubject(context, ref.language, subject, false);
+
+ if (external_text != null) {
+ Element div = document.createElement("div");
+ for (String line : external_text.split("\\r?\\n")) {
+ Element span = document.createElement("span");
+ span.text(line);
+ div.appendChild(span);
+ div.appendElement("br");
+ }
+ document.body().appendChild(div);
+ }
+ } else if ("forward".equals(action)) {
+ data.draft.subject =
+ EntityMessage.getSubject(context, ref.language, subject, true);
+ } else if ("resend".equals(action)) {
+ data.draft.subject = ref.subject;
+ } else if ("editasnew".equals(action)) {
+ if (ref.from != null && ref.from.length == 1) {
+ String from = ((InternetAddress) ref.from[0]).getAddress();
+ for (EntityIdentity identity : data.identities)
+ if (identity.email.equals(from)) {
+ selected = identity;
+ break;
+ }
+ }
+
+ data.draft.to = ref.to;
+ data.draft.cc = ref.cc;
+ data.draft.bcc = ref.bcc;
+ data.draft.subject = ref.subject;
+
+ if (ref.content)
+ document = JsoupEx.parse(ref.getFile(context));
+ } else if ("list".equals(action)) {
+ data.draft.subject = ref.subject;
+ } else if ("dsn".equals(action)) {
+ if (EntityMessage.DSN_HARD_BOUNCE.equals(dsn))
+ data.draft.subject = context.getString(R.string.title_hard_bounce_subject);
+ else
+ data.draft.subject = context.getString(R.string.title_receipt_subject, subject);
+
+ String[] texts;
+ if (EntityMessage.DSN_HARD_BOUNCE.equals(dsn))
+ texts = new String[]{context.getString(R.string.title_hard_bounce_text)};
+ else {
+ EntityAnswer receipt = db.answer().getReceiptAnswer();
+ if (receipt == null)
+ texts = Helper.getStrings(context, ref.language, R.string.title_receipt_text);
+ else {
+ db.answer().applyAnswer(receipt.id, new Date().getTime());
+ texts = new String[0];
+ Document d = JsoupEx.parse(receipt.getHtml(context, null));
+ document.body().append(d.body().html());
+ }
+ }
+
+ for (int i = 0; i < texts.length; i++) {
+ if (i > 0)
+ document.body()
+ .appendElement("br");
+
+ Element div = document.createElement("div");
+ div.text(texts[i]);
+ document.body()
+ .appendChild(div)
+ .appendElement("br");
+ }
+ } else if ("participation".equals(action))
+ data.draft.subject = status + ": " + ref.subject;
+
+ if (!"dsn".equals(action)) {
+ // Sensitivity
+ data.draft.sensitivity = ref.sensitivity;
+
+ // Plain-only
+ if (plain_only_reply && ref.isPlainOnly())
+ data.draft.plain_only = 1;
+
+ // Encryption
+ List recipients = new ArrayList<>();
+ if (data.draft.to != null)
+ recipients.addAll(Arrays.asList(data.draft.to));
+ if (data.draft.cc != null)
+ recipients.addAll(Arrays.asList(data.draft.cc));
+ if (data.draft.bcc != null)
+ recipients.addAll(Arrays.asList(data.draft.bcc));
+
+ if (!BuildConfig.DEBUG)
+ if (EntityMessage.PGP_SIGNONLY.equals(ref.ui_encrypt) ||
+ EntityMessage.PGP_SIGNENCRYPT.equals(ref.ui_encrypt)) {
+ if (PgpHelper.isOpenKeychainInstalled(context) &&
+ selected.sign_key != null &&
+ PgpHelper.hasPgpKey(context, recipients, true))
+ data.draft.ui_encrypt = ref.ui_encrypt;
+ } else if (EntityMessage.SMIME_SIGNONLY.equals(ref.ui_encrypt) ||
+ EntityMessage.SMIME_SIGNENCRYPT.equals(ref.ui_encrypt)) {
+ if (ActivityBilling.isPro(context) &&
+ selected.sign_key_alias != null &&
+ SmimeHelper.hasSmimeKey(context, recipients, true))
+ data.draft.ui_encrypt = ref.ui_encrypt;
+ }
+ }
+
+ // Reply template
+ EntityAnswer a = null;
+ if (answer < 0) {
+ if ("reply".equals(action) || "reply_all".equals(action) ||
+ "forward".equals(action) || "list".equals(action))
+ a = db.answer().getStandardAnswer();
+ } else
+ a = db.answer().getAnswer(answer);
+
+ if (a != null) {
+ db.answer().applyAnswer(a.id, new Date().getTime());
+ if (a.label != null && ref != null)
+ EntityOperation.queue(context, ref, EntityOperation.LABEL, a.label, true);
+ Document d = JsoupEx.parse(a.getHtml(context, data.draft.to));
+ document.body().append(d.body().html());
+ }
+
+ // Signature
+ if ("reply".equals(action) || "reply_all".equals(action))
+ data.draft.signature = prefs.getBoolean("signature_reply", true);
+ else if ("forward".equals(action))
+ data.draft.signature = prefs.getBoolean("signature_forward", true);
+ else
+ data.draft.signature = false;
+
+ if (ref.content && "resend".equals(action)) {
+ document = JsoupEx.parse(ref.getFile(context));
+ HtmlHelper.clearAnnotations(document);
+ // Save original body
+ Element div = document.body()
+ .tagName("div")
+ .attr("fairemail", "reference");
+ Element body = document.createElement("body")
+ .appendChild(div);
+ document.body().replaceWith(body);
+ }
+
+ // Reply header
+ if (ref.content &&
+ !"resend".equals(action) &&
+ !"editasnew".equals(action) &&
+ !("list".equals(action) && TextUtils.isEmpty(selected_text)) &&
+ !"dsn".equals(action)) {
+ // Reply/forward
+ Element reply = document.createElement("div");
+ reply.attr("fairemail", "reference");
+
+ // Build reply header
+ boolean separate_reply = prefs.getBoolean("separate_reply", false);
+ boolean extended_reply = prefs.getBoolean("extended_reply", false);
+ Element p = ref.getReplyHeader(context, document, separate_reply, extended_reply);
+ reply.appendChild(p);
+
+ Document d;
+ if (TextUtils.isEmpty(selected_text)) {
+ // Get referenced message body
+ d = JsoupEx.parse(ref.getFile(context));
+ HtmlHelper.normalizeNamespaces(d, false);
+ HtmlHelper.clearAnnotations(d); // Legacy left-overs
+
+ if (BuildConfig.DEBUG)
+ d.select(".faircode_remove").remove();
+
+ if ("reply".equals(action) || "reply_all".equals(action)) {
+ // Remove signature separators
+ boolean remove_signatures = prefs.getBoolean("remove_signatures", false);
+ if (remove_signatures)
+ HtmlHelper.removeSignatures(d);
+
+ // Limit number of nested block quotes
+ boolean quote_limit = prefs.getBoolean("quote_limit", true);
+ if (quote_limit)
+ HtmlHelper.quoteLimit(d, MAX_QUOTE_LEVEL);
+ }
+ } else {
+ // Selected text
+ d = Document.createShell("");
+
+ Element div = d.createElement("div");
+ if (selected_text instanceof Spanned)
+ div.html(HtmlHelper.toHtml((Spanned) selected_text, context));
+ else
+ for (String line : selected_text.toString().split("\\r?\\n")) {
+ Element span = document.createElement("span");
+ span.text(line);
+ div.appendChild(span);
+ div.appendElement("br");
+ }
+
+ d.body().appendChild(div);
+ }
+
+ Element e = d.body();
+
+ // Apply styles
+ List sheets = HtmlHelper.parseStyles(d.head().select("style"));
+ for (Element element : e.select("*")) {
+ String tag = element.tagName();
+ String clazz = element.attr("class");
+ String style = HtmlHelper.processStyles(context, tag, clazz, null, sheets);
+ style = HtmlHelper.mergeStyles(style, element.attr("style"));
+ if (!TextUtils.isEmpty(style))
+ element.attr("style", style);
+ }
+
+ // Quote referenced message body
+ boolean quote_reply = prefs.getBoolean("quote_reply", true);
+ boolean quote = (quote_reply &&
+ ("reply".equals(action) || "reply_all".equals(action) || "list".equals(action)));
+
+ if (quote) {
+ String style = e.attr("style");
+ style = HtmlHelper.mergeStyles(style, HtmlHelper.getQuoteStyle(e));
+ e.tagName("blockquote").attr("style", style);
+ } else
+ e.tagName("p");
+ reply.appendChild(e);
+
+ if (wb && data.draft.wasforwardedfrom == null)
+ document.body().prependChild(reply);
+ else
+ document.body().appendChild(reply);
+
+ ComposeHelper.addSignature(context, document, data.draft, selected);
+ }
+ }
+
+ EntityFolder drafts = db.folder().getFolderByType(selected.account, EntityFolder.DRAFTS);
+ if (drafts == null)
+ throw new IllegalArgumentException(context.getString(R.string.title_no_drafts));
+
+ boolean signature_once = prefs.getBoolean("signature_reply_once", false);
+ if (signature_once && data.draft.signature &&
+ ref != null && ref.thread != null &&
+ ("reply".equals(action) || "reply_all".equals(action))) {
+ List outbound = new ArrayList<>();
+
+ EntityFolder sent = db.folder().getFolderByType(drafts.account, EntityFolder.SENT);
+ if (sent != null)
+ outbound.addAll(db.message().getMessagesByThread(drafts.account, ref.thread, null, sent.id));
+
+ EntityFolder outbox = db.folder().getOutbox();
+ if (outbox != null)
+ outbound.addAll(db.message().getMessagesByThread(drafts.account, ref.thread, null, outbox.id));
+
+ if (outbound.size() > 0) {
+ Log.i("Signature suppressed");
+ data.draft.signature = false;
+ }
+ }
+
+ data.draft.account = drafts.account;
+ data.draft.folder = drafts.id;
+ data.draft.identity = selected.id;
+ data.draft.from = new InternetAddress[]{new InternetAddress(selected.email, selected.name, StandardCharsets.UTF_8.name())};
+
+ data.draft.sender = MessageHelper.getSortKey(data.draft.from);
+ Uri lookupUri = ContactInfo.getLookupUri(data.draft.from);
+ data.draft.avatar = (lookupUri == null ? null : lookupUri.toString());
+
+ data.draft.received = new Date().getTime();
+ data.draft.seen = true;
+ data.draft.ui_seen = true;
+
+ data.draft.revision = 1;
+ data.draft.revisions = 1;
+
+ data.draft.id = db.message().insertMessage(data.draft);
+
+ String html = document.html();
+ Helper.writeText(data.draft.getFile(context), html);
+ Helper.writeText(data.draft.getFile(context, data.draft.revision), html);
+
+ String text = HtmlHelper.getFullText(html);
+ data.draft.preview = HtmlHelper.getPreview(text);
+ data.draft.language = HtmlHelper.getLanguage(context, data.draft.subject, text);
+ db.message().setMessageContent(data.draft.id,
+ true,
+ data.draft.language,
+ data.draft.plain_only,
+ data.draft.preview,
+ null);
+
+ if ("participation".equals(action)) {
+ EntityAttachment attachment = new EntityAttachment();
+ attachment.message = data.draft.id;
+ attachment.sequence = 1;
+ attachment.name = "meeting.ics";
+ attachment.type = "text/calendar";
+ attachment.disposition = Part.ATTACHMENT;
+ attachment.size = ics.length();
+ attachment.progress = null;
+ attachment.available = true;
+ attachment.id = db.attachment().insertAttachment(attachment);
+
+ File file = attachment.getFile(context);
+ Helper.copy(ics, file);
+ Helper.secureDelete(ics);
+
+ ICalendar icalendar = CalendarHelper.parse(context, file);
+ VEvent event = icalendar.getEvents().get(0);
+ Organizer organizer = event.getOrganizer();
+ if (organizer != null) {
+ String email = organizer.getEmail();
+ String name = organizer.getCommonName();
+ if (!TextUtils.isEmpty(email)) {
+ InternetAddress o = new InternetAddress(email, name, StandardCharsets.UTF_8.name());
+ Log.i("Setting organizer=" + o);
+ data.draft.to = new Address[]{o};
+ }
+ }
+ }
+
+ if ("new".equals(action) && uris != null) {
+ ArrayList images = new ArrayList<>();
+ for (Uri uri : uris)
+ try {
+ ComposeHelper.UriInfo info = ComposeHelper.getUriInfo(uri, context);
+ if (info.isImage())
+ images.add(uri);
+ else
+ ComposeHelper.addAttachment(context, data.draft.id, uri, null, false, 0, false);
+ } catch (IOException ex) {
+ Log.e(ex);
+ }
+
+ if (images.size() > 0)
+ args.putParcelableArrayList("images", images);
+ }
+
+ if (ref != null &&
+ ("reply".equals(action) || "reply_all".equals(action) ||
+ "forward".equals(action) ||
+ "resend".equals(action) ||
+ "editasnew".equals(action))) {
+ List cid = new ArrayList<>();
+ for (Element img : document.select("img")) {
+ String src = img.attr("src");
+ if (src.startsWith("cid:"))
+ cid.add("<" + src.substring(4) + ">");
+ }
+
+ int sequence = 0;
+ List attachments = db.attachment().getAttachments(ref.id);
+ for (EntityAttachment attachment : attachments)
+ if (attachment.subsequence == null &&
+ !attachment.isEncryption() &&
+ (cid.contains(attachment.cid) ||
+ !("reply".equals(action) || "reply_all".equals(action)))) {
+ if (attachment.available) {
+ File source = attachment.getFile(context);
+
+ if (cid.contains(attachment.cid))
+ attachment.disposition = Part.INLINE;
+ else {
+ attachment.cid = null;
+ attachment.related = false;
+ attachment.disposition = Part.ATTACHMENT;
+ }
+
+ attachment.id = null;
+ attachment.message = data.draft.id;
+ attachment.sequence = ++sequence;
+ attachment.id = db.attachment().insertAttachment(attachment);
+
+ File target = attachment.getFile(context);
+ Helper.copy(source, target);
+
+ if (resize_reply &&
+ ("reply".equals(action) || "reply_all".equals(action)))
+ ComposeHelper.resizeAttachment(context, attachment, ComposeHelper.REDUCED_IMAGE_SIZE);
+ } else
+ args.putBoolean("incomplete", true);
+ }
+ }
+
+ if (save_drafts &&
+ (data.draft.ui_encrypt == null ||
+ EntityMessage.ENCRYPT_NONE.equals(data.draft.ui_encrypt)) &&
+ (!"new".equals(action) ||
+ answer > 0 ||
+ !TextUtils.isEmpty(to) ||
+ !TextUtils.isEmpty(cc) ||
+ !TextUtils.isEmpty(bcc) ||
+ !TextUtils.isEmpty(external_subject) ||
+ !TextUtils.isEmpty(external_body) ||
+ !TextUtils.isEmpty(external_text) ||
+ !TextUtils.isEmpty(selected_text) ||
+ (uris != null && uris.size() > 0))) {
+ Map c = new HashMap<>();
+ c.put("id", data.draft.id == null ? null : Long.toString(data.draft.id));
+ c.put("encrypt", data.draft.encrypt + "/" + data.draft.ui_encrypt);
+ c.put("action", action);
+ Log.breadcrumb("Load draft", c);
+
+ EntityOperation.queue(context, data.draft, EntityOperation.ADD);
+ }
+ } else {
+ args.putBoolean("saved", true);
+
+ if (!data.draft.ui_seen)
+ EntityOperation.queue(context, data.draft, EntityOperation.SEEN, true);
+
+ // External draft
+ if (data.draft.identity == null) {
+ for (EntityIdentity identity : data.identities)
+ if (identity.account.equals(data.draft.account))
+ if (identity.primary) {
+ data.draft.identity = identity.id;
+ break;
+ } else if (data.draft.identity == null)
+ data.draft.identity = identity.id;
+
+ if (data.draft.identity != null)
+ db.message().setMessageIdentity(data.draft.id, data.draft.identity);
+ Log.i("Selected external identity=" + data.draft.identity);
+ }
+
+ if (data.draft.revision == null || data.draft.revisions == null) {
+ data.draft.revision = 1;
+ data.draft.revisions = 1;
+ db.message().setMessageRevision(data.draft.id, data.draft.revision);
+ db.message().setMessageRevisions(data.draft.id, data.draft.revisions);
+ }
+
+ if (data.draft.content || data.draft.uid == null) {
+ if (data.draft.uid == null && !data.draft.content)
+ Log.e("Draft without uid");
+
+ File file = data.draft.getFile(context);
+
+ Document doc = (data.draft.content ? JsoupEx.parse(file) : Document.createShell(""));
+ doc.select("div[fairemail=signature]").remove();
+ Elements ref = doc.select("div[fairemail=reference]");
+ ref.remove();
+
+ File refFile = data.draft.getRefFile(context);
+ if (refFile.exists()) {
+ ref.html(Helper.readText(refFile));
+ Helper.secureDelete(refFile);
+ }
+
+ // Possibly external draft
+
+ for (Element e : ref)
+ if (wb && data.draft.wasforwardedfrom == null)
+ doc.body().prependChild(e);
+ else
+ doc.body().appendChild(e);
+
+ EntityIdentity identity = null;
+ if (data.draft.identity != null)
+ identity = db.identity().getIdentity(data.draft.identity);
+
+ ComposeHelper.addSignature(context, doc, data.draft, identity);
+
+ String html = doc.html();
+ Helper.writeText(file, html);
+ Helper.writeText(data.draft.getFile(context, data.draft.revision), html);
+
+ String text = HtmlHelper.getFullText(html);
+ data.draft.preview = HtmlHelper.getPreview(text);
+ data.draft.language = HtmlHelper.getLanguage(context, data.draft.subject, text);
+ db.message().setMessageContent(data.draft.id,
+ true,
+ data.draft.language,
+ data.draft.plain_only,
+ data.draft.preview,
+ null);
+ } else
+ EntityOperation.queue(context, data.draft, EntityOperation.BODY);
+ }
+
+ List attachments = db.attachment().getAttachments(data.draft.id);
+ if (attachments != null)
+ for (EntityAttachment attachment : attachments)
+ if (!attachment.available && attachment.progress == null && attachment.error == null)
+ EntityOperation.queue(context, data.draft, EntityOperation.ATTACHMENT, attachment.id);
+
+ set(data.draft.plain_only, attachments);
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ ServiceSynchronize.eval(context, "compose/draft");
+
+ return data;
+ }
+
+ @Override
+ protected void onException(Bundle args, Throwable ex) {
+ throw new NotImplementedException("LoaderDraft");
+ }
+
+ protected void set(Integer plain_only, List attachments) {
+ throw new NotImplementedException("LoaderDraft");
+ }
+}