Improved reply/forward handling

This commit is contained in:
M66B 2019-01-21 16:45:05 +00:00
parent 2b3d6b94da
commit 332fcb5557
14 changed files with 132 additions and 136 deletions

View File

@ -1215,7 +1215,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
if (encrypted == null) {
EntityMessage message = db.message().getMessage(id);
String body = message.read(context);
String body = Helper.readText(EntityMessage.getFile(context, message.id));
// https://tools.ietf.org/html/rfc4880#section-6.2
int begin = body.indexOf(PGP_BEGIN_MESSAGE);
@ -1252,7 +1252,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
// Write decrypted body
EntityMessage m = db.message().getMessage(id);
m.write(context, decrypted.toString());
Helper.writeText(EntityMessage.getFile(context, m.id), decrypted.toString());
db.message().setMessageStored(id, new Date().getTime());
@ -1275,7 +1275,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
// Write decrypted body
EntityMessage m = db.message().getMessage(id);
m.write(context, parts.getHtml(context));
Helper.writeText(EntityMessage.getFile(context, m.id), parts.getHtml(context));
// Remove previously decrypted attachments
for (EntityAttachment a : attachments)

View File

@ -1115,7 +1115,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
TupleMessageEx message = (TupleMessageEx) args.getSerializable("message");
if (body == null)
try {
body = message.read(context);
body = Helper.readText(EntityMessage.getFile(context, message.id));
} catch (IOException ex) {
Log.e(ex);
body = "";
@ -1496,7 +1496,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
return new String[]{
from,
message.subject,
HtmlHelper.getText(message.read(context))
HtmlHelper.getText(Helper.readText(EntityMessage.getFile(context, message.id)))
};
}

View File

@ -347,12 +347,6 @@ public interface DaoMessage {
" WHERE id = :id")
int setMessageSnoozed(long id, Long wakeup);
@Query("UPDATE message SET replying = :newid WHERE replying = :oldid")
int updateMessageReplying(long oldid, long newid);
@Query("UPDATE message SET forwarding = :newid WHERE forwarding = :oldid")
int updateMessageForwarding(long oldid, long newid);
@Query("DELETE FROM message WHERE id = :id")
int deleteMessage(long id);

View File

@ -24,12 +24,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Serializable;
import java.util.Date;
import java.util.Random;
@ -88,8 +83,8 @@ public class EntityMessage implements Serializable {
public Long folder;
public Long identity;
public String extra; // plus
public Long replying;
public Long forwarding;
public Long replying; // obsolete
public Long forwarding; // obsolete
public Long uid; // compose/moved = null
public String msgid;
public String references;
@ -161,38 +156,11 @@ public class EntityMessage implements Serializable {
return new File(dir, id.toString());
}
void write(Context context, String body) throws IOException {
File file = getFile(context, id);
BufferedWriter out = null;
try {
out = new BufferedWriter(new FileWriter(file));
out.write(body == null ? "" : body);
} finally {
if (out != null)
out.close();
}
}
String read(Context context) throws IOException {
return read(context, this.id);
}
static String read(Context context, Long id) throws IOException {
File file = getFile(context, id);
BufferedReader in = null;
try {
in = new BufferedReader(new FileReader(file));
StringBuilder body = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
body.append(line);
body.append('\n');
}
return body.toString();
} finally {
if (in != null)
in.close();
}
static File getRefFile(Context context, Long id) {
File dir = new File(context.getFilesDir(), "references");
if (!dir.exists())
dir.mkdir();
return new File(dir, id.toString());
}
static File getRawFile(Context context, Long id) {
@ -272,8 +240,6 @@ public class EntityMessage implements Serializable {
return ((this.account == null ? other.account == null : this.account.equals(other.account)) &&
this.folder.equals(other.folder) &&
(this.identity == null ? other.identity == null : this.identity.equals(other.identity)) &&
(this.replying == null ? other.replying == null : this.replying.equals(other.replying)) &&
(this.forwarding == null ? other.forwarding == null : this.forwarding.equals(other.forwarding)) &&
(this.uid == null ? other.uid == null : this.uid.equals(other.uid)) &&
(this.msgid == null ? other.msgid == null : this.msgid.equals(other.msgid)) &&
(this.references == null ? other.references == null : this.references.equals(other.references)) &&

View File

@ -174,10 +174,6 @@ public class EntityOperation {
message.uid = uid;
message.folder = source.id;
// Track reference
db.message().updateMessageReplying(id, newid);
db.message().updateMessageForwarding(id, newid);
if (message.content)
try {
Helper.copy(

View File

@ -207,15 +207,16 @@ public class EntityRule {
reply.folder = db.folder().getOutbox().id;
reply.identity = identity.id;
reply.msgid = EntityMessage.generateMessageId();
reply.references = (message.references == null ? "" : message.references + " ") + message.msgid;
reply.inreplyto = message.msgid;
reply.thread = message.thread;
reply.replying = message.id;
reply.to = (message.reply == null || message.reply.length == 0 ? message.from : message.reply);
reply.from = new InternetAddress[]{new InternetAddress(identity.email, identity.name)};
reply.subject = context.getString(R.string.title_subject_reply, message.subject == null ? "" : message.subject);
reply.sender = MessageHelper.getSortKey(reply.from);
reply.received = new Date().getTime();
reply.id = db.message().insertMessage(reply);
reply.write(context, body);
Helper.writeText(EntityMessage.getFile(context, reply.id), body);
db.message().setMessageContent(reply.id, true, HtmlHelper.getPreview(body));
EntityOperation.queue(context, db, reply, EntityOperation.SEND);

View File

@ -1474,10 +1474,10 @@ public class FragmentCompose extends FragmentBase {
if (answer > 0)
body = EntityAnswer.getAnswerText(db, answer, null) + body;
} else {
result.draft.thread = ref.thread;
if ("reply".equals(action) || "reply_all".equals(action)) {
result.draft.replying = ref.id;
result.draft.references = (ref.references == null ? "" : ref.references + " ") + ref.msgid;
result.draft.inreplyto = ref.msgid;
result.draft.thread = ref.thread;
result.draft.to = (ref.reply == null || ref.reply.length == 0 ? ref.from : ref.reply);
result.draft.from = ref.to;
@ -1499,7 +1499,7 @@ public class FragmentCompose extends FragmentBase {
}
} else if ("forward".equals(action)) {
result.draft.forwarding = ref.id;
result.draft.thread = result.draft.msgid; // new thread
result.draft.from = ref.to;
}
@ -1553,10 +1553,19 @@ public class FragmentCompose extends FragmentBase {
result.draft.received = new Date().getTime();
result.draft.id = db.message().insertMessage(result.draft);
result.draft.write(context, body == null ? "" : body);
Helper.writeText(EntityMessage.getFile(context, result.draft.id), body);
db.message().setMessageContent(result.draft.id, true, HtmlHelper.getPreview(body));
// Write reference text
if (ref != null && ref.content) {
String refBody = String.format("<p>%s %s:</p>\n<blockquote>%s</blockquote>",
Html.escapeHtml(new Date(ref.received).toString()),
Html.escapeHtml(MessageHelper.formatAddresses(ref.from)),
Helper.readText(EntityMessage.getFile(context, ref.id)));
Helper.writeText(EntityMessage.getRefFile(context, result.draft.id), refBody);
}
if ("new".equals(action)) {
ArrayList<Uri> uris = args.getParcelableArrayList("attachments");
if (uris != null)
@ -1841,7 +1850,7 @@ public class FragmentCompose extends FragmentBase {
!MessageHelper.equal(draft.bcc, abcc) ||
(draft.subject == null ? subject != null : !draft.subject.equals(subject)) ||
last_available != available ||
!body.equals(draft.read(context)));
!body.equals(Helper.readText(EntityMessage.getFile(context, draft.id))));
last_available = available;
@ -1857,7 +1866,7 @@ public class FragmentCompose extends FragmentBase {
draft.subject = subject;
draft.received = new Date().getTime();
db.message().updateMessage(draft);
draft.write(context, body);
Helper.writeText(EntityMessage.getFile(context, draft.id), body);
db.message().setMessageContent(draft.id, true, HtmlHelper.getPreview(body));
}
@ -1908,13 +1917,19 @@ public class FragmentCompose extends FragmentBase {
// Delete draft (cannot move to outbox)
EntityOperation.queue(context, db, draft, EntityOperation.DELETE);
File refDraftFile = EntityMessage.getRefFile(context, draft.id);
// Copy message to outbox
draft.id = null;
draft.folder = db.folder().getOutbox().id;
draft.uid = null;
draft.ui_hide = false;
draft.id = db.message().insertMessage(draft);
draft.write(getContext(), body);
Helper.writeText(EntityMessage.getFile(context, draft.id), body);
if (refDraftFile.exists()) {
File refFile = EntityMessage.getRefFile(context, draft.id);
refDraftFile.renameTo(refFile);
}
// Move attachments
for (EntityAttachment attachment : attachments)
@ -2044,10 +2059,6 @@ public class FragmentCompose extends FragmentBase {
private void showDraft(EntityMessage draft) {
Bundle args = new Bundle();
args.putLong("id", draft.id);
if (draft.replying != null)
args.putLong("reference", draft.replying);
else if (draft.forwarding != null)
args.putLong("reference", draft.forwarding);
args.putBoolean("show_images", show_images);
new SimpleTask<Spanned[]>() {
@ -2066,21 +2077,21 @@ public class FragmentCompose extends FragmentBase {
@Override
protected Spanned[] onExecute(final Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
final long reference = args.getLong("reference", -1);
final long id = args.getLong("id");
final boolean show_images = args.getBoolean("show_images", false);
String body = EntityMessage.read(context, id);
String body = Helper.readText(EntityMessage.getFile(context, id));
Spanned spannedBody = Html.fromHtml(body, cidGetter, null);
String quote = (reference < 0 ? null : HtmlHelper.getQuote(context, reference, true));
Spanned spannedReference = null;
if (quote != null) {
File refFile = EntityMessage.getRefFile(context, id);
if (refFile.exists()) {
String quote = Helper.readText(refFile);
Spanned spannedQuote = Html.fromHtml(quote,
new Html.ImageGetter() {
@Override
public Drawable getDrawable(String source) {
Drawable image = HtmlHelper.decodeImage(source, context, reference, show_images);
Drawable image = HtmlHelper.decodeImage(source, context, id, show_images);
float width = context.getResources().getDisplayMetrics().widthPixels -
Helper.dp2pixels(context, 12); // margins;

View File

@ -141,7 +141,7 @@ public class FragmentWebView extends FragmentBase {
protected String onExecute(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
String html = EntityMessage.read(context, id);
String html = Helper.readText(EntityMessage.getFile(context, id));
Document doc = Jsoup.parse(html);
for (Element img : doc.select("img"))

View File

@ -57,9 +57,12 @@ import com.sun.mail.imap.IMAPStore;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -328,7 +331,7 @@ public class Helper {
draft.subject = context.getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME + " debug info";
draft.received = new Date().getTime();
draft.id = db.message().insertMessage(draft);
draft.write(context, body);
writeText(EntityMessage.getFile(context, draft.id), body);
db.message().setMessageContent(draft.id, true, HtmlHelper.getPreview(body));
attachSettings(context, draft.id, 1);
@ -600,6 +603,34 @@ public class Helper {
return TextUtils.join("@", a);
}
static void writeText(File file, String content) throws IOException {
BufferedWriter out = null;
try {
out = new BufferedWriter(new FileWriter(file));
out.write(content == null ? "" : content);
} finally {
if (out != null)
out.close();
}
}
static String readText(File file) throws IOException {
BufferedReader in = null;
try {
in = new BufferedReader(new FileReader(file));
StringBuilder body = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
body.append(line);
body.append('\n');
}
return body.toString();
} finally {
if (in != null)
in.close();
}
}
static void copy(File src, File dst) throws IOException {
InputStream in = new BufferedInputStream(new FileInputStream(src));
try {

View File

@ -24,7 +24,6 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.text.Html;
import android.text.TextUtils;
import android.util.Base64;
@ -47,7 +46,6 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -246,17 +244,6 @@ public class HtmlHelper {
}
}
static String getQuote(Context context, long id, boolean sanitize) throws IOException {
EntityMessage message = DB.getInstance(context).message().getMessage(id);
if (message == null)
return null;
String html = EntityMessage.read(context, id);
return String.format("<p>%s %s:</p>\n<blockquote>%s</blockquote>",
Html.escapeHtml(new Date(message.received).toString()),
Html.escapeHtml(MessageHelper.formatAddresses(message.from)),
sanitize ? sanitize(html, true) : getBody(html));
}
static String getPreview(String body) {
String text = (body == null ? null : Jsoup.parse(body).text());
return (text == null ? null : text.substring(0, Math.min(text.length(), PREVIEW_SIZE)));

View File

@ -36,7 +36,6 @@ public class JobDaily extends JobService {
private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
private static final long CLEANUP_INTERVAL = 4 * 3600 * 1000L; // milliseconds
private static final long FILE_DELETE_THRESHOLD = 30 * 60 * 1000L; // milliseconds
private static final long CACHE_IMAGE_DURATION = 3 * 24 * 3600 * 1000L; // milliseconds
private static final long KEEP_LOG_DURATION = 24 * 3600 * 1000L; // milliseconds
@ -102,29 +101,40 @@ public class JobDaily extends JobService {
Log.i("Cleanup message files");
File[] messages = new File(context.getFilesDir(), "messages").listFiles();
if (messages != null)
for (File file : messages)
if (file.isFile() && (now - file.lastModified()) > FILE_DELETE_THRESHOLD) {
long id = Long.parseLong(file.getName());
if (db.message().countMessage(id) == 0) {
Log.i("Cleanup message id=" + id);
if (!file.delete())
Log.w("Error deleting " + file);
}
for (File file : messages) {
long id = Long.parseLong(file.getName());
if (db.message().countMessage(id) == 0) {
Log.i("Cleanup message id=" + id);
if (!file.delete())
Log.w("Error deleting " + file);
}
}
// Cleanup message files
Log.i("Cleanup reference files");
File[] references = new File(context.getFilesDir(), "references").listFiles();
if (references != null)
for (File file : references) {
long id = Long.parseLong(file.getName());
if (db.message().countMessage(id) == 0) {
Log.i("Cleanup message id=" + id);
if (!file.delete())
Log.w("Error deleting " + file);
}
}
// Cleanup attachment files
Log.i("Cleanup attachment files");
File[] attachments = new File(context.getFilesDir(), "attachments").listFiles();
if (attachments != null)
for (File file : attachments)
if (file.isFile() && (now - file.lastModified()) > FILE_DELETE_THRESHOLD) {
long id = Long.parseLong(file.getName());
if (db.attachment().countAttachment(id) == 0) {
Log.i("Cleanup attachment id=" + id);
if (!file.delete())
Log.w("Error deleting " + file);
}
for (File file : attachments) {
long id = Long.parseLong(file.getName());
if (db.attachment().countAttachment(id) == 0) {
Log.i("Cleanup attachment id=" + id);
if (!file.delete())
Log.w("Error deleting " + file);
}
}
// Cleanup cached images
Log.i("Cleanup cached image files");

View File

@ -196,14 +196,10 @@ public class MessageHelper {
DB db = DB.getInstance(context);
MimeMessageEx imessage = new MimeMessageEx(isession, message.msgid);
EntityMessage replying = null;
if (message.replying != null)
replying = db.message().getMessage(message.replying);
if (replying != null) {
imessage.addHeader("In-Reply-To", replying.msgid);
imessage.addHeader("References", (replying.references == null ? "" : replying.references + " ") + replying.msgid);
}
if (message.references != null)
imessage.addHeader("References", message.references);
if (message.inreplyto != null)
imessage.addHeader("In-Reply-To", message.inreplyto);
imessage.addHeader("X-FairEmail-ID", message.msgid);
imessage.addHeader("X-FairEmail-Thread", message.thread);
@ -300,13 +296,8 @@ public class MessageHelper {
static void build(Context context, EntityMessage message, MimeMessage imessage) throws IOException, MessagingException {
DB db = DB.getInstance(context);
String html = message.read(context);
if (message.replying != null || message.forwarding != null)
html += HtmlHelper.getQuote(context,
message.replying == null ? message.forwarding : message.replying, false);
StringBuilder body = new StringBuilder();
body.append(html);
body.append(Helper.readText(EntityMessage.getFile(context, message.id)));
if (Helper.isPro(context) && message.identity != null) {
EntityIdentity identity = db.identity().getIdentity(message.identity);
@ -314,6 +305,10 @@ public class MessageHelper {
body.append(identity.signature);
}
File refFile = EntityMessage.getRefFile(context, message.id);
if (refFile.exists())
body.append(Helper.readText(refFile));
String plainContent = HtmlHelper.getText(body.toString());
StringBuilder htmlContent = new StringBuilder();
@ -338,7 +333,7 @@ public class MessageHelper {
alternativePart.addBodyPart(htmlPart);
List<String> cids = new ArrayList<>();
for (Element element : Jsoup.parse(html).select("img")) {
for (Element element : Jsoup.parse(body.toString()).select("img")) {
String src = element.attr("src");
if (src.startsWith("cid:"))
cids.add("<" + src.substring(4) + ">");

View File

@ -668,7 +668,7 @@ public class ServiceSynchronize extends LifecycleService {
if (message.content)
try {
String body = message.read(this);
String body = Helper.readText(EntityMessage.getFile(this, message.id));
StringBuilder sb = new StringBuilder();
if (!TextUtils.isEmpty(message.subject))
sb.append(message.subject).append("<br>");
@ -1793,10 +1793,10 @@ public class ServiceSynchronize extends LifecycleService {
Long sid = null;
try {
// Append replied/forwarded text
String body = message.read(this);
if (message.replying != null || message.forwarding != null)
body += HtmlHelper.getQuote(this,
message.replying == null ? message.forwarding : message.replying, false);
String body = Helper.readText(EntityMessage.getFile(this, message.id));
File refFile = EntityMessage.getRefFile(this, message.id);
if (refFile.exists())
body += Helper.readText(refFile);
EntityFolder sent = db.folder().getFolderByType(ident.account, EntityFolder.SENT);
if (sent != null) {
@ -1811,7 +1811,7 @@ public class ServiceSynchronize extends LifecycleService {
message.ui_browsed = true; // prevent deleting on sync
message.error = null;
message.id = db.message().insertMessage(message);
message.write(this, body);
Helper.writeText(EntityMessage.getFile(this, message.id), body);
sid = message.id;
message.id = id;
@ -1835,7 +1835,7 @@ public class ServiceSynchronize extends LifecycleService {
db.message().setMessageSeen(message.id, true);
db.message().setMessageUiSeen(message.id, true);
db.message().setMessageError(message.id, null);
message.write(this, body);
Helper.writeText(EntityMessage.getFile(this, message.id), body);
} else {
db.message().setMessageSent(sid, imessage.getSentDate().getTime());
db.message().setMessageUiHide(sid, false);
@ -1853,9 +1853,14 @@ public class ServiceSynchronize extends LifecycleService {
db.endTransaction();
}
if (message.replying != null) {
EntityMessage replying = db.message().getMessage(message.replying);
EntityOperation.queue(this, db, replying, EntityOperation.ANSWERED, true);
if (refFile.exists())
refFile.delete();
if (message.inreplyto != null) {
List<EntityMessage> replieds = db.message().getMessageByMsgId(message.account, message.inreplyto);
for (EntityMessage replied : replieds)
if (replied.uid != null)
EntityOperation.queue(this, db, replied, EntityOperation.ANSWERED, true);
}
db.identity().setIdentityError(ident.id, null);
@ -1967,7 +1972,7 @@ public class ServiceSynchronize extends LifecycleService {
MessageHelper.MessageParts parts = helper.getMessageParts();
String body = parts.getHtml(this);
String preview = HtmlHelper.getPreview(body);
message.write(this, body);
Helper.writeText(EntityMessage.getFile(this, message.id), body);
db.message().setMessageContent(message.id, true, preview);
db.message().setMessageWarning(message.id, parts.getWarnings(message.warning));
}
@ -2677,7 +2682,7 @@ public class ServiceSynchronize extends LifecycleService {
if (!message.content) {
if (!metered || (message.size != null && message.size < maxSize)) {
String body = parts.getHtml(context);
message.write(context, body);
Helper.writeText(EntityMessage.getFile(context, message.id), body);
db.message().setMessageContent(message.id, true, HtmlHelper.getPreview(body));
db.message().setMessageWarning(message.id, parts.getWarnings(message.warning));
Log.i(folder.name + " downloaded message id=" + message.id + " size=" + message.size);

View File

@ -117,7 +117,7 @@ public class ViewModelBrowse extends ViewModel {
String body = null;
if (message.content)
try {
body = message.read(state.context);
body = Helper.readText(EntityMessage.getFile(state.context, message.id));
} catch (IOException ex) {
Log.e(ex);
}