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); }