From a6a2c07d6f6e2a28b29dd67849f5ab67691c1e60 Mon Sep 17 00:00:00 2001 From: M66B Date: Sun, 26 Dec 2021 18:39:39 +0100 Subject: [PATCH] Experiment: process receipts --- FAQ.md | 25 ++++++++ app/src/main/java/eu/faircode/email/Core.java | 60 +++++++++++++++++++ .../java/eu/faircode/email/DaoOperation.java | 1 + .../eu/faircode/email/EntityOperation.java | 1 + .../java/eu/faircode/email/MessageHelper.java | 24 ++++++++ 5 files changed, 111 insertions(+) diff --git a/FAQ.md b/FAQ.md index 75684d8ef8..ee2eece806 100644 --- a/FAQ.md +++ b/FAQ.md @@ -448,6 +448,7 @@ The low priority status bar notification shows the number of pending operations, * *exists*: check if message exists * *rule*: execute rule on body text * *expunge*: permanently delete messages +* *report*: process delivery or read receipt (experimental) Operations are processed only when there is a connection to the email server or when manually synchronizing. See also [this FAQ](#user-content-faq16). @@ -3429,6 +3430,30 @@ Remarks:
+*Process delivery/read receipt (version 1.1797+)* + +On receiving a delivery or read receipt, the related message will be looked up in the sent messages folder +and the following keywords will be set depending on the contents of the report: + +``` +$Delivered +$NotDelivered +$Displayed +$NotDisplayed +``` + +* Delivered: action = *delivered*, *relayed*, or *expanded*, [see here](https://datatracker.ietf.org/doc/html/rfc3464#section-2.3.3) +* Displayed: disposition = *displayed*, [see here](https://datatracker.ietf.org/doc/html/rfc3798#section-3.2.6) + +It is probably a good idea to enable *Show keywords in message header* in the display settings. + +Note that the email server needs to support IMAP flags (keywords) for this feature. + +Filter rules will be applied to the received receipt, so it is possible to move/archive the receipt. +See [this FAQ](#user-content-faq71) for a header condition to recognize receipts. + +
+ **(126) Can message previews be sent to my wearable?** diff --git a/app/src/main/java/eu/faircode/email/Core.java b/app/src/main/java/eu/faircode/email/Core.java index 36294347e4..33d3138ee7 100644 --- a/app/src/main/java/eu/faircode/email/Core.java +++ b/app/src/main/java/eu/faircode/email/Core.java @@ -239,6 +239,7 @@ class Core { if (message == null && !EntityOperation.FETCH.equals(op.name) && + !EntityOperation.REPORT.equals(op.name) && !EntityOperation.SYNC.equals(op.name) && !EntityOperation.SUBSCRIBE.equals(op.name) && !EntityOperation.PURGE.equals(op.name) && @@ -346,6 +347,7 @@ class Core { case EntityOperation.ANSWERED: case EntityOperation.ADD: + case EntityOperation.REPORT: // Do nothing break; @@ -445,6 +447,10 @@ class Core { onExists(context, jargs, account, folder, message, op, (IMAPFolder) ifolder); break; + case EntityOperation.REPORT: + onReport(context, jargs, folder, (IMAPStore) istore, (IMAPFolder) ifolder, state); + break; + case EntityOperation.SYNC: Helper.gc(); onSynchronizeMessages(context, jargs, account, folder, (IMAPStore) istore, (IMAPFolder) ifolder, state); @@ -1970,6 +1976,43 @@ class Core { } } + private static void onReport(Context context, JSONArray jargs, EntityFolder folder, IMAPStore istore, IMAPFolder ifolder, State state) throws JSONException, MessagingException { + String msgid = jargs.getString(0); + String keyword = jargs.getString(1); + + if (TextUtils.isEmpty(msgid)) + throw new IllegalArgumentException("msgid missing"); + + if (TextUtils.isEmpty(keyword)) + throw new IllegalArgumentException("keyword missing"); + + if (folder.read_only) + throw new IllegalArgumentException(folder.name + " read-only"); + + if (!ifolder.getPermanentFlags().contains(Flags.Flag.USER)) + throw new IllegalArgumentException(folder.name + " has no keywords"); + + Message[] imessages = ifolder.search(new MessageIDTerm(msgid)); + if (imessages == null || imessages.length == 0) + throw new IllegalArgumentException(msgid + " not found"); + + for (Message imessage : imessages) { + long uid = ifolder.getUID(imessage); + Log.i("Report uid=" + uid + " keyword=" + keyword); + + Flags flags = new Flags(keyword); + imessage.setFlags(flags, true); + + try { + JSONArray fargs = new JSONArray(); + fargs.put(uid); + onFetch(context, fargs, folder, istore, ifolder, state); + } catch (Throwable ex) { + Log.w(ex); + } + } + } + static void onSynchronizeFolders( Context context, EntityAccount account, Store istore, State state, boolean keep_alive, boolean force) throws MessagingException { @@ -3564,6 +3607,7 @@ class Core { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean download_headers = prefs.getBoolean("download_headers", false); boolean notify_known = prefs.getBoolean("notify_known", false); + boolean experiments = prefs.getBoolean("experiments", false); boolean pro = ActivityBilling.isPro(context); long uid = ifolder.getUID(imessage); @@ -3863,6 +3907,22 @@ class Core { List
headers = (needsHeaders ? helper.getAllHeaders() : null); String body = (needsBody ? helper.getMessageParts().getHtml(context) : null); + if (experiments && helper.isReport()) + try { + MessageHelper.Report r = parts.getReport(); + EntityFolder s = db.folder().getFolderByType(folder.account, EntityFolder.SENT); + if (r != null && s != null) { + if (r.isDeliveryStatus()) + EntityOperation.queue(context, s, EntityOperation.REPORT, + message.inreplyto, r.isDelivered() ? "$Delivered" : "$NotDelivered"); + else if (r.isDispositionNotification()) + EntityOperation.queue(context, s, EntityOperation.REPORT, + message.inreplyto, r.isDisplayed() ? "$Displayed" : "$NotDisplayed"); + } + } catch (Throwable ex) { + Log.w(ex); + } + try { db.beginTransaction(); diff --git a/app/src/main/java/eu/faircode/email/DaoOperation.java b/app/src/main/java/eu/faircode/email/DaoOperation.java index c88bf90bfc..a57a3db98b 100644 --- a/app/src/main/java/eu/faircode/email/DaoOperation.java +++ b/app/src/main/java/eu/faircode/email/DaoOperation.java @@ -39,6 +39,7 @@ public interface DaoOperation { // Other operations: add, delete, seen, answered, flag, keyword, label, subscribe, send " WHEN operation.name = '" + EntityOperation.FETCH + "' THEN 2" + " WHEN operation.name = '" + EntityOperation.EXISTS + "' THEN 3" + + " WHEN operation.name = '" + EntityOperation.REPORT + "' THEN 3" + " WHEN operation.name = '" + EntityOperation.COPY + "' THEN 4" + " WHEN operation.name = '" + EntityOperation.MOVE + "' THEN 5" + " WHEN operation.name = '" + EntityOperation.PURGE + "' THEN 6" + diff --git a/app/src/main/java/eu/faircode/email/EntityOperation.java b/app/src/main/java/eu/faircode/email/EntityOperation.java index f706721236..bdb3dbffa1 100644 --- a/app/src/main/java/eu/faircode/email/EntityOperation.java +++ b/app/src/main/java/eu/faircode/email/EntityOperation.java @@ -101,6 +101,7 @@ public class EntityOperation { static final String RULE = "rule"; static final String PURGE = "purge"; static final String EXPUNGE = "expunge"; + static final String REPORT = "report"; private static final int MAX_FETCH = 100; // operations private static final long FORCE_WITHIN = 30 * 1000; // milliseconds diff --git a/app/src/main/java/eu/faircode/email/MessageHelper.java b/app/src/main/java/eu/faircode/email/MessageHelper.java index 01e69fb6c6..e24145e911 100644 --- a/app/src/main/java/eu/faircode/email/MessageHelper.java +++ b/app/src/main/java/eu/faircode/email/MessageHelper.java @@ -2598,6 +2598,22 @@ public class MessageHelper { return sb.toString(); } + Report getReport() throws MessagingException, IOException { + for (PartHolder h : extra) + if (h.isReport()) { + String result; + Object content = h.part.getContent(); + if (content instanceof String) + result = (String) content; + else if (content instanceof InputStream) + result = Helper.readStream((InputStream) content); + else + result = content.toString(); + return new Report(h.contentType.getBaseType(), result); + } + return null; + } + List getAttachmentParts() { return attachments; } @@ -3789,6 +3805,14 @@ public class MessageHelper { this.html = report.toString(); } + boolean isDeliveryStatus() { + return isDeliveryStatus(type); + } + + boolean isDispositionNotification() { + return isDispositionNotification(type); + } + boolean isDelivered() { return ("delivered".equals(action) || "relayed".equals(action) || "expanded".equals(action)); }