diff --git a/FAQ.md b/FAQ.md
index 7c818414eb..0c0db3876a 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -2819,7 +2819,9 @@ Reformatting and displaying such messages will take too long. You can try to use
**(125) What are the current experimental features?**
-* ...
+* [IMAP NOTIFY](https://tools.ietf.org/html/rfc5465) support
+
+NOTIFY support means that notifications of *subscribed* folders will be requested and when a notification is received that the folder will be synchronized.
diff --git a/app/build.gradle b/app/build.gradle
index 740d5e689e..d1c71a206e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,7 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'com.bugsnag.android.gradle'
-def getVersionCode = { -> return 1412 }
+def getVersionCode = { -> return 1413 }
def getReleaseName = { -> return "\"Bambiraptor\"" }
// https://en.wikipedia.org/wiki/List_of_dinosaur_genera
diff --git a/app/src/main/java/com/sun/mail/imap/IMAPFolder.java b/app/src/main/java/com/sun/mail/imap/IMAPFolder.java
index 247bded28e..67492c55ae 100644
--- a/app/src/main/java/com/sun/mail/imap/IMAPFolder.java
+++ b/app/src/main/java/com/sun/mail/imap/IMAPFolder.java
@@ -3627,7 +3627,14 @@ public class IMAPFolder extends Folder implements UIDFolder, ResponseHandler {
} else if (ir.keyEquals("RECENT")) {
// update 'recent'
recent = ir.getNumber();
- }
+ } else if (ir.keyEquals("STATUS"))
+ try {
+ String mailbox = ir.readAtomString();
+ Folder f = store.getFolder(mailbox);
+ notifyFolderChangeListeners(f);
+ } catch (Throwable ex) {
+ eu.faircode.email.Log.e(ex);
+ }
}
/**
diff --git a/app/src/main/java/eu/faircode/email/ApplicationEx.java b/app/src/main/java/eu/faircode/email/ApplicationEx.java
index e2345ea9eb..0dfdfe0782 100644
--- a/app/src/main/java/eu/faircode/email/ApplicationEx.java
+++ b/app/src/main/java/eu/faircode/email/ApplicationEx.java
@@ -388,6 +388,8 @@ public class ApplicationEx extends Application implements SharedPreferences.OnSh
editor.remove("tcp_keep_alive");
else if (version < 1407)
editor.remove("print_html_confirmed");
+ else if (version < 1413)
+ editor.remove("experiments");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !BuildConfig.DEBUG)
editor.remove("background_service");
diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
index 2155b15b7b..13b16ab374 100644
--- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
+++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
@@ -49,8 +49,13 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.preference.PreferenceManager;
+import com.sun.mail.iap.Argument;
+import com.sun.mail.iap.ProtocolException;
+import com.sun.mail.iap.Response;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPStore;
+import com.sun.mail.imap.protocol.IMAPProtocol;
+import com.sun.mail.imap.protocol.IMAPResponse;
import java.text.DateFormat;
import java.util.ArrayList;
@@ -139,7 +144,7 @@ public class ServiceSynchronize extends ServiceBase implements SharedPreferences
"sync_shared_folders",
"prefer_ip4", "tcp_keep_alive", "ssl_harden", // force reconnect
"badge", "unseen_ignored", // force update badge/widget
- "protocol", "debug", // force reconnect
+ "experiments", "debug", "protocol", // force reconnect
"auth_plain",
"auth_login",
"auth_sasl"
@@ -944,6 +949,7 @@ public class ServiceSynchronize extends ServiceBase implements SharedPreferences
// Debug
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean experiments = prefs.getBoolean("experiments", false);
boolean debug = (prefs.getBoolean("debug", false) || BuildConfig.DEBUG);
final EmailService iservice = new EmailService(
@@ -1035,6 +1041,8 @@ public class ServiceSynchronize extends ServiceBase implements SharedPreferences
if (!capIdle || account.poll_interval < OPTIMIZE_KEEP_ALIVE_INTERVAL)
optimizeAccount(account, "IDLE");
+ final boolean capNotify = (experiments && iservice.hasCapability("NOTIFY"));
+
db.account().setAccountState(account.id, "connected");
db.account().setAccountError(account.id, null);
db.account().setAccountWarning(account.id, null);
@@ -1096,6 +1104,21 @@ public class ServiceSynchronize extends ServiceBase implements SharedPreferences
wlFolder.release();
}
}
+
+ @Override
+ public void folderChanged(FolderEvent e) {
+ try {
+ wlFolder.acquire();
+
+ String name = e.getFolder().getFullName();
+ EntityLog.log(ServiceSynchronize.this, "Folder changed=" + name);
+ EntityFolder folder = db.folder().getFolderByName(account.id, name);
+ if (folder != null)
+ EntityOperation.sync(ServiceSynchronize.this, folder.id, false);
+ } finally {
+ wlFolder.release();
+ }
+ }
});
// Update folder list
@@ -1290,6 +1313,40 @@ public class ServiceSynchronize extends ServiceBase implements SharedPreferences
if (sync && folder.selectable)
EntityOperation.sync(this, folder.id, false);
+
+ if (capNotify && EntityFolder.INBOX.equals(folder.type))
+ ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
+ @Override
+ public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
+ EntityLog.log(ServiceSynchronize.this, account.name + " NOTIFY enable");
+
+ // https://tools.ietf.org/html/rfc5465
+ Argument arg = new Argument();
+ arg.writeAtom("SET STATUS (subscribed (MessageNew MessageExpunge FlagChange))");
+
+ Response[] responses = protocol.command("NOTIFY", arg);
+
+ if (responses.length == 0)
+ throw new ProtocolException("No response");
+ if (!responses[responses.length - 1].isOK())
+ throw new ProtocolException(responses[responses.length - 1]);
+
+ for (int i = 0; i < responses.length - 1; i++) {
+ EntityLog.log(ServiceSynchronize.this, account.name + " " + responses[i]);
+ if (responses[i] instanceof IMAPResponse) {
+ IMAPResponse ir = (IMAPResponse) responses[i];
+ if (ir.keyEquals("STATUS")) {
+ String mailbox = ir.readAtomString();
+ EntityFolder f = db.folder().getFolderByName(account.id, mailbox);
+ if (f != null)
+ EntityOperation.sync(ServiceSynchronize.this, f.id, false);
+ }
+ }
+ }
+
+ return null;
+ }
+ });
} else {
mapFolders.put(folder, null);
db.folder().setFolderState(folder.id, null);
diff --git a/app/src/main/java/javax/mail/Folder.java b/app/src/main/java/javax/mail/Folder.java
index 0ddce216fc..aeec7e02fb 100644
--- a/app/src/main/java/javax/mail/Folder.java
+++ b/app/src/main/java/javax/mail/Folder.java
@@ -1469,6 +1469,10 @@ public abstract class Folder implements AutoCloseable {
store.notifyFolderRenamedListeners(this, folder);
}
+ protected void notifyFolderChangeListeners(Folder folder) {
+ store.notifyFolderListeners(FolderEvent.CHANGED, folder);
+ }
+
// Vector of MessageCount listeners
private volatile Vector messageCountListeners = null;
diff --git a/app/src/main/java/javax/mail/event/FolderEvent.java b/app/src/main/java/javax/mail/event/FolderEvent.java
index 4c89ae63ab..03428fb933 100644
--- a/app/src/main/java/javax/mail/event/FolderEvent.java
+++ b/app/src/main/java/javax/mail/event/FolderEvent.java
@@ -46,6 +46,8 @@ public class FolderEvent extends MailEvent {
/** The folder was renamed. */
public static final int RENAMED = 3;
+ public static final int CHANGED = 4;
+
/**
* The event type.
*
@@ -140,5 +142,7 @@ public class FolderEvent extends MailEvent {
((FolderListener)listener).folderDeleted(this);
else if (type == RENAMED)
((FolderListener)listener).folderRenamed(this);
+ else if (type == CHANGED)
+ ((FolderListener)listener).folderChanged(this);
}
}
diff --git a/app/src/main/java/javax/mail/event/FolderListener.java b/app/src/main/java/javax/mail/event/FolderListener.java
index 5c652a35c3..e9206062e5 100644
--- a/app/src/main/java/javax/mail/event/FolderListener.java
+++ b/app/src/main/java/javax/mail/event/FolderListener.java
@@ -45,4 +45,6 @@ public interface FolderListener extends java.util.EventListener {
* @param e the FolderEvent
*/
public void folderRenamed(FolderEvent e);
+
+ public void folderChanged(FolderEvent e);
}