diff --git a/src/client/ui/main-window.vala b/src/client/ui/main-window.vala index 0acabae7..a0ba7134 100644 --- a/src/client/ui/main-window.vala +++ b/src/client/ui/main-window.vala @@ -205,17 +205,16 @@ public class MainWindow : Gtk.Window { message_list_store.clear(); if (current_folder != null) { - current_folder.email_added_removed.disconnect(on_email_added_removed); + current_folder.list_appended.disconnect(on_folder_list_appended); yield current_folder.close_async(); } current_folder = folder; - current_folder.email_added_removed.connect(on_email_added_removed); + current_folder.list_appended.connect(on_folder_list_appended); yield current_folder.open_async(true); - current_folder.lazy_list_email_async(-1, 50, MessageListStore.REQUIRED_FIELDS, - on_list_email_ready); + current_folder.lazy_list_email(-1, 50, MessageListStore.REQUIRED_FIELDS, on_list_email_ready); } private void on_list_email_ready(Gee.List? email, Error? err) { @@ -288,11 +287,17 @@ public class MainWindow : Gtk.Window { } } - private void on_email_added_removed(Gee.List? added, Gee.List? removed) { - if (added != null) { - foreach (Geary.Email email in added) - message_list_store.append_envelope(email); + private void on_folder_list_appended() { + int high = message_list_store.get_highest_folder_position(); + if (high < 0) { + debug("Unable to find highest message position in %s", current_folder.to_string()); + + return; } + + // Want to get the one *after* the highest position in the message list + current_folder.lazy_list_email(high + 1, -1, MessageListStore.REQUIRED_FIELDS, + on_list_email_ready); } private async void search_folders_for_children(Gee.Collection folders) { diff --git a/src/client/ui/message-list-store.vala b/src/client/ui/message-list-store.vala index 7ef33078..bf892056 100644 --- a/src/client/ui/message-list-store.vala +++ b/src/client/ui/message-list-store.vala @@ -66,7 +66,7 @@ public class MessageListStore : Gtk.TreeStore { Gtk.TreeIter iter; append(out iter, null); - string? pre = null;; + string? pre = null; string? post = null; if (envelope.properties != null) { pre = envelope.properties.is_unread() ? "" : null; @@ -96,6 +96,27 @@ public class MessageListStore : Gtk.TreeStore { return email; } + // Returns -1 if the list is empty. + public int get_highest_folder_position() { + Gtk.TreeIter iter; + if (!get_iter_first(out iter)) + return -1; + + int high = int.MIN; + + // TODO: It would be more efficient to maintain highest and lowest values in a table or + // as items are added and removed; this will do for now. + do { + Geary.Email email; + get(iter, Column.MESSAGE_OBJECT, out email); + + if (email.location.position > high) + high = email.location.position; + } while (iter_next(ref iter)); + + return high; + } + private int sort_by_date(Gtk.TreeModel model, Gtk.TreeIter aiter, Gtk.TreeIter biter) { Geary.Email aenvelope; get(aiter, Column.MESSAGE_OBJECT, out aenvelope); diff --git a/src/console/main.vala b/src/console/main.vala index 7fd3f0a0..e055c471 100644 --- a/src/console/main.vala +++ b/src/console/main.vala @@ -87,6 +87,7 @@ class ImapConsole : Gtk.Window { "xlist", "examine", "fetch", + "uid-fetch", "help", "exit", "quit", @@ -159,6 +160,7 @@ class ImapConsole : Gtk.Window { break; case "fetch": + case "uid-fetch": fetch(cmd, args); break; @@ -386,12 +388,16 @@ class ImapConsole : Gtk.Window { status("Fetching %s".printf(args[0])); + Geary.Imap.MessageSet msg_set = (cmd.down() == "fetch") + ? new Geary.Imap.MessageSet.custom(args[0]) + : new Geary.Imap.MessageSet.uid_custom(args[0]); + Geary.Imap.FetchDataType[] data_items = new Geary.Imap.FetchDataType[0]; for (int ctr = 1; ctr < args.length; ctr++) data_items += Geary.Imap.FetchDataType.decode(args[ctr]); - cx.send_async.begin(new Geary.Imap.FetchCommand(cx.generate_tag(), - new Geary.Imap.MessageSet.custom(args[0]), data_items), null, on_fetch); + cx.send_async.begin(new Geary.Imap.FetchCommand(cx.generate_tag(), msg_set, data_items), + null, on_fetch); } private void on_fetch(Object? source, AsyncResult result) { diff --git a/src/engine/api/geary-abstract-folder.vala b/src/engine/api/geary-abstract-folder.vala index 39ee49b2..65b3794d 100644 --- a/src/engine/api/geary-abstract-folder.vala +++ b/src/engine/api/geary-abstract-folder.vala @@ -13,13 +13,8 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder { closed(reason); } - protected virtual void notify_email_added_removed(Gee.List? added, - Gee.List? removed) { - email_added_removed(added, removed); - } - - protected virtual void notify_updated() { - updated(); + protected virtual void notify_list_appended(int total) { + list_appended(total); } public abstract Geary.FolderPath get_path(); @@ -30,7 +25,7 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder { public abstract async void close_async(Cancellable? cancellable = null) throws Error; - public abstract async int get_email_count(Cancellable? cancellable = null) throws Error; + public abstract async int get_email_count_async(Cancellable? cancellable = null) throws Error; public abstract async void create_email_async(Geary.Email email, Cancellable? cancellable = null) throws Error; @@ -38,7 +33,7 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder { public abstract async Gee.List? list_email_async(int low, int count, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; - public virtual void lazy_list_email_async(int low, int count, Geary.Email.Field required_fields, + public virtual void lazy_list_email(int low, int count, Geary.Email.Field required_fields, EmailCallback cb, Cancellable? cancellable = null) { do_lazy_list_email_async.begin(low, count, required_fields, cb, cancellable); } @@ -61,7 +56,7 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder { public abstract async Gee.List? list_email_sparse_async(int[] by_position, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; - public virtual void lazy_list_email_sparse_async(int[] by_position, + public virtual void lazy_list_email_sparse(int[] by_position, Geary.Email.Field required_fields, EmailCallback cb, Cancellable? cancellable = null) { do_lazy_list_email_sparse_async.begin(by_position, required_fields, cb, cancellable); } diff --git a/src/engine/api/geary-engine-folder.vala b/src/engine/api/geary-engine-folder.vala index e301fd02..2882f9b0 100644 --- a/src/engine/api/geary-engine-folder.vala +++ b/src/engine/api/geary-engine-folder.vala @@ -9,8 +9,9 @@ private class Geary.EngineFolder : Geary.AbstractFolder { private RemoteAccount remote; private LocalAccount local; - private RemoteFolder? remote_folder = null; private LocalFolder local_folder; + private RemoteFolder? remote_folder = null; + private int remote_count = -1; private bool opened = false; private Geary.NonblockingSemaphore remote_semaphore = new Geary.NonblockingSemaphore(true); @@ -18,15 +19,11 @@ private class Geary.EngineFolder : Geary.AbstractFolder { this.remote = remote; this.local = local; this.local_folder = local_folder; - - local_folder.updated.connect(on_local_updated); } ~EngineFolder() { if (opened) warning("Folder %s destroyed without closing", to_string()); - - local_folder.updated.disconnect(on_local_updated); } public override Geary.FolderPath get_path() { @@ -89,9 +86,14 @@ private class Geary.EngineFolder : Geary.AbstractFolder { // update flags, properties, etc. yield local.update_folder_async(folder, cancellable); + // signals + folder.list_appended.connect(on_remote_list_appended); + + // state + remote_count = yield folder.get_email_count_async(cancellable); + // all set; bless the remote folder as opened remote_folder = folder; - remote_folder.updated.connect(on_remote_updated); } else { debug("Unable to prepare remote folder %s: prepare_opened_file() failed", to_string()); } @@ -129,10 +131,12 @@ private class Geary.EngineFolder : Geary.AbstractFolder { yield remote_semaphore.wait_async(); remote_semaphore = new Geary.NonblockingSemaphore(true); - remote_folder.updated.disconnect(on_remote_updated); RemoteFolder? folder = remote_folder; remote_folder = null; + // signals + folder.list_appended.disconnect(on_remote_list_appended); + folder.close_async.begin(cancellable); notify_closed(CloseReason.FOLDER_CLOSED); @@ -141,15 +145,63 @@ private class Geary.EngineFolder : Geary.AbstractFolder { opened = false; } - public override async int get_email_count(Cancellable? cancellable = null) throws Error { + private void on_remote_list_appended(int total) { + // need to prefetch PROPERTIES (or, in the future NONE or LOCATION) fields to create a + // normalized placeholder in the local database of the message, so all positions are + // properly relative to the end of the message list; once this is done, notify user of new + // messages + do_normalize_appended_messages.begin(total); + } + + private async void do_normalize_appended_messages(int new_remote_count) { + // this only works when the list is grown + assert(new_remote_count > remote_count); + + try { + // if no mail in local store, nothing needs to be done here; the store is "normalized" + int local_count = yield local_folder.get_email_count_async(); + if (local_count == 0) { + notify_list_appended(new_remote_count); + + return; + } + + if (!yield wait_for_remote_to_open()) { + notify_list_appended(new_remote_count); + + return; + } + + // normalize starting at the message *after* the highest position of the local store, + // which has now changed + Gee.List? list = yield remote_folder.list_email_async(remote_count + 1, -1, + Geary.Email.Field.PROPERTIES, null); + assert(list != null && list.size > 0); + + foreach (Geary.Email email in list) + yield local_folder.create_email_async(email, null); + + // save new remote count + remote_count = new_remote_count; + + notify_list_appended(new_remote_count); + } catch (Error err) { + debug("Unable to normalize local store of newly appended messages to %s: %s", + to_string(), err.message); + } + } + + public override async int get_email_count_async(Cancellable? cancellable = null) throws Error { // TODO: Use monitoring to avoid round-trip to the server if (!opened) throw new EngineError.OPEN_REQUIRED("%s is not open", to_string()); + // if connected, use stashed remote count (which is always kept current once remote folder + // is opened) if (yield wait_for_remote_to_open()) - return yield remote_folder.get_email_count(cancellable); + return remote_count; - return yield local_folder.get_email_count(cancellable); + return yield local_folder.get_email_count_async(cancellable); } public override async Gee.List? list_email_async(int low, int count, @@ -165,7 +217,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { return accumulator; } - public override void lazy_list_email_async(int low, int count, Geary.Email.Field required_fields, + public override void lazy_list_email(int low, int count, Geary.Email.Field required_fields, EmailCallback cb, Cancellable? cancellable = null) { // schedule do_list_email_async(), using the callback to drive availability of email do_list_email_async.begin(low, count, required_fields, null, cb, cancellable); @@ -236,10 +288,11 @@ private class Geary.EngineFolder : Geary.AbstractFolder { } // fixup local email positions to match server's positions - if (local_list_size > 0) { + if (local_list_size > 0 && local_count < remote_count) { + int adjustment = remote_count - local_count; foreach (Geary.Email email in local_list) { - int new_position = email.location.position + (low - local_low); - email.update_location(new Geary.EmailLocation(new_position, email.location.ordering)); + email.update_location(new Geary.EmailLocation(email.location.position + adjustment, + email.location.ordering)); } } @@ -306,7 +359,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { return accumulator; } - public override void lazy_list_email_sparse_async(int[] by_position, Geary.Email.Field required_fields, + public override void lazy_list_email_sparse(int[] by_position, Geary.Email.Field required_fields, EmailCallback cb, Cancellable? cancellable = null) { // schedule listing in the background, using the callback to drive availability of email do_list_email_sparse_async.begin(by_position, required_fields, null, cb, cancellable); @@ -560,12 +613,6 @@ private class Geary.EngineFolder : Geary.AbstractFolder { yield local_folder.remove_email_async(email, cancellable); } - private void on_local_updated() { - } - - private void on_remote_updated() { - } - // In order to maintain positions for all messages without storing all of them locally, // the database stores entries for the lowest requested email to the highest (newest), which // means there can be no gaps between the last in the database and the last on the server. @@ -576,8 +623,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder { if (!yield wait_for_remote_to_open()) throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string()); - local_count = yield local_folder.get_email_count(cancellable); - remote_count = yield remote_folder.get_email_count(cancellable); + local_count = yield local_folder.get_email_count_async(cancellable); + remote_count = yield remote_folder.get_email_count_async(cancellable); // fixup span specifier normalize_span_specifiers(ref low, ref count, remote_count); @@ -598,7 +645,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder { // Use PROPERTIES as they're the most useful information for certain actions (such as // finding duplicates when we start using INTERNALDATE and RFC822.SIZE) and cheap to fetch - // TODO: Consider only fetching their UID; would need Geary.Email.Field.LOCATION (or\ + // + // TODO: Consider only fetching their UID; would need Geary.Email.Field.LOCATION (or // perhaps NONE is considered a call for just the UID). Gee.List? list = yield remote_folder.list_email_async(high, prefetch_count, Geary.Email.Field.PROPERTIES, cancellable); diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala index 9fa9d00b..ac34d0e2 100644 --- a/src/engine/api/geary-folder.vala +++ b/src/engine/api/geary-folder.vala @@ -19,6 +19,11 @@ public interface Geary.Folder : Object { FOLDER_CLOSED } + public enum Direction { + BEFORE, + AFTER + } + /** * This is fired when the Folder is successfully opened by a caller. It will only fire once * until the Folder is closed, with the OpenState indicating what has been opened. @@ -36,57 +41,32 @@ public interface Geary.Folder : Object { public signal void closed(CloseReason reason); /** - * "email-added-removed" is fired when new email has been detected due to background monitoring - * operations or if an unrelated operation causes or reveals the existence or removal of - * messages. - * - * There are no guarantees of what Geary.Email.Field fields will be available when these are - * reported. If more information is required, use the fetch or list operations. + * "list-appended" is fired when new messages have been appended to the list of messages in the + * folder (and therefore old message position numbers remain valid, but the total count of the + * messages in the folder has changed). */ - public signal void email_added_removed(Gee.List? added, - Gee.List? removed); - - /** - * TBD. - */ - public signal void updated(); + public signal void list_appended(int total); /** * This helper method should be called by implementors of Folder rather than firing the signal * directly. This allows subclasses and superclasses the opportunity to inspect the email * and update state before and/or after the signal has been fired. */ - protected virtual void notify_opened(OpenState state) { - opened(state); - } + protected abstract void notify_opened(OpenState state); /** * This helper method should be called by implementors of Folder rather than firing the signal * directly. This allows subclasses and superclasses the opportunity to inspect the email * and update state before and/or after the signal has been fired. */ - protected virtual void notify_closed(CloseReason reason) { - closed(reason); - } + protected abstract void notify_closed(CloseReason reason); /** * This helper method should be called by implementors of Folder rather than firing the signal * directly. This allows subclasses and superclasses the opportunity to inspect the email * and update state before and/or after the signal has been fired. */ - protected virtual void notify_email_added_removed(Gee.List? added, - Gee.List? removed) { - email_added_removed(added, removed); - } - - /** - * This helper method should be called by implementors of Folder rather than firing the signal - * directly. This allows subclasses and superclasses the opportunity to inspect the email - * and update state before and/or after the signal has been fired. - */ - public virtual void notify_updated() { - updated(); - } + protected abstract void notify_list_appended(int total); public abstract Geary.FolderPath get_path(); @@ -125,7 +105,7 @@ public interface Geary.Folder : Object { * * The Folder must be opened prior to attempting this operation. */ - public abstract async int get_email_count(Cancellable? cancellable = null) throws Error; + public abstract async int get_email_count_async(Cancellable? cancellable = null) throws Error; /** * If the Folder object detects that the supplied Email does not have sufficient fields for @@ -188,7 +168,7 @@ public interface Geary.Folder : Object { * * The Folder must be opened prior to attempting this operation. */ - public abstract void lazy_list_email_async(int low, int count, Geary.Email.Field required_fields, + public abstract void lazy_list_email(int low, int count, Geary.Email.Field required_fields, EmailCallback cb, Cancellable? cancellable = null); /** @@ -207,7 +187,7 @@ public interface Geary.Folder : Object { Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; /** - * Similar in contract to list_email_sparse_async(), but like lazy_list_email_async(), the + * Similar in contract to list_email_sparse_async(), but like lazy_list_email(), the * messages are passed back to the caller in chunks as they're retrieved. When null is passed * as the first parameter, all the messages have been fetched. If an Error occurs during * processing, it's passed as the second parameter. There's no guarantee of the returned @@ -215,7 +195,7 @@ public interface Geary.Folder : Object { * * The Folder must be opened prior to attempting this operation. */ - public abstract void lazy_list_email_sparse_async(int[] by_position, + public abstract void lazy_list_email_sparse(int[] by_position, Geary.Email.Field required_fields, EmailCallback cb, Cancellable? cancellable = null); /** diff --git a/src/engine/api/geary-generic-imap-folder.vala b/src/engine/api/geary-generic-imap-folder.vala index 462ddbe6..fc3fd0fb 100644 --- a/src/engine/api/geary-generic-imap-folder.vala +++ b/src/engine/api/geary-generic-imap-folder.vala @@ -60,21 +60,41 @@ private class Geary.GenericImapFolder : Geary.EngineFolder { // if same, no problem-o if (local_properties.uid_next.value != remote_properties.uid_next.value) { - debug("UID next changed: %lld -> %lld", local_properties.uid_next.value, + debug("UID next changed for %s: %lld -> %lld", to_string(), local_properties.uid_next.value, remote_properties.uid_next.value); - // fetch everything from the last seen UID (+1) to the current next UID + // fetch everything from the last seen UID (+1) to the current next UID that's not + // already in the local store (since the uidnext field isn't reported by NOOP or IDLE, + // it's possible these were fetched the last time the folder was selected) + // // TODO: Could break this fetch up in chunks if it helps - Gee.List? newest = yield imap_remote_folder.list_email_uid_async( - local_properties.uid_next, null, Geary.Email.Field.PROPERTIES, cancellable); + int64 uid_start_value = local_properties.uid_next.value; + for (;;) { + Geary.EmailIdentifier start_id = new Imap.EmailIdentifier(new Imap.UID(uid_start_value)); + Geary.Email.Field available_fields; + if (!yield imap_local_folder.is_email_present(start_id, out available_fields, cancellable)) + break; + + debug("already have UID %lld in %s local store", uid_start_value, to_string()); + + if (++uid_start_value >= remote_properties.uid_next.value) + break; + } - if (newest != null && newest.size > 0) { - debug("saving %d newest emails", newest.size); - foreach (Geary.Email email in newest) { - try { - yield local_folder.create_email_async(email, cancellable); - } catch (Error newest_err) { - debug("Unable to save new email in %s: %s", to_string(), newest_err.message); + if (uid_start_value < remote_properties.uid_next.value) { + Imap.UID uid_start = new Imap.UID(uid_start_value); + + Gee.List? newest = yield imap_remote_folder.list_email_uid_async( + uid_start, null, Geary.Email.Field.PROPERTIES, cancellable); + + if (newest != null && newest.size > 0) { + debug("saving %d newest emails in %s", newest.size, to_string()); + foreach (Geary.Email email in newest) { + try { + yield local_folder.create_email_async(email, cancellable); + } catch (Error newest_err) { + debug("Unable to save new email in %s: %s", to_string(), newest_err.message); + } } } } diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala index d65c9af9..2f815e59 100644 --- a/src/engine/imap/api/imap-folder.vala +++ b/src/engine/imap/api/imap-folder.vala @@ -45,12 +45,16 @@ public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Geary mailbox = yield session_mgr.select_examine_mailbox(path.get_fullpath(info.delim), !readonly, cancellable); - // TODO: hook up signals // update with new information this.readonly = Trillian.from_boolean(readonly); - properties = new Imap.FolderProperties(mailbox.count, mailbox.recent, mailbox.unseen, + // connect to signals + mailbox.exists_altered.connect(on_exists_altered); + mailbox.flags_altered.connect(on_flags_altered); + mailbox.expunged.connect(on_expunged); + + properties = new Imap.FolderProperties(mailbox.exists, mailbox.recent, mailbox.unseen, mailbox.uid_validity, mailbox.uid_next, properties.attrs); notify_opened(Geary.Folder.OpenState.REMOTE); @@ -60,18 +64,36 @@ public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Geary if (mailbox == null) return; + mailbox.exists_altered.disconnect(on_exists_altered); + mailbox.flags_altered.disconnect(on_flags_altered); + mailbox.expunged.disconnect(on_expunged); + mailbox = null; readonly = Trillian.UNKNOWN; notify_closed(CloseReason.FOLDER_CLOSED); } - public override async int get_email_count(Cancellable? cancellable = null) throws Error { + private void on_exists_altered(int exists) { + assert(mailbox != null); + notify_list_appended(exists); + } + + private void on_flags_altered(FetchResults flags) { + assert(mailbox != null); + // TODO: Notify of changes + } + + private void on_expunged(MessageNumber expunged) { + assert(mailbox != null); + // TODO: Notify of changes + } + + public override async int get_email_count_async(Cancellable? cancellable = null) throws Error { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); - // TODO: Need to monitor folder for updates to the message count - return mailbox.count; + return mailbox.exists; } public override async void create_email_async(Geary.Email email, Cancellable? cancellable = null) throws Error { @@ -86,8 +108,7 @@ public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Geary if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); - // TODO: Need to use a monitored count - normalize_span_specifiers(ref low, ref count, mailbox.count); + normalize_span_specifiers(ref low, ref count, mailbox.exists); return yield mailbox.list_set_async(new MessageSet.range(low, count), fields, cancellable); } diff --git a/src/engine/imap/decoders/imap-noop-results.vala b/src/engine/imap/decoders/imap-noop-results.vala index ffdbd34f..02c509db 100644 --- a/src/engine/imap/decoders/imap-noop-results.vala +++ b/src/engine/imap/decoders/imap-noop-results.vala @@ -36,7 +36,7 @@ public class Geary.Imap.NoopResults : Geary.Imap.CommandResults { foreach (ServerData data in response.server_data) { try { - int ordinal = data.get_as_string(1).as_int().clamp(-1, int.MAX); + int ordinal = data.get_as_string(1).as_int(-1, int.MAX); ServerDataType type = ServerDataType.from_parameter(data.get_as_string(2)); switch (type) { diff --git a/src/engine/imap/transport/imap-client-session-manager.vala b/src/engine/imap/transport/imap-client-session-manager.vala index 932aa5ce..175d20a9 100644 --- a/src/engine/imap/transport/imap-client-session-manager.vala +++ b/src/engine/imap/transport/imap-client-session-manager.vala @@ -6,6 +6,7 @@ public class Geary.Imap.ClientSessionManager { public const int MIN_POOL_SIZE = 2; + public const int SELECTED_KEEPALIVE_SEC = 5; private Credentials cred; private uint default_port; @@ -14,6 +15,7 @@ public class Geary.Imap.ClientSessionManager { private Gee.HashSet examined_contexts = new Gee.HashSet(); private Gee.HashSet selected_contexts = new Gee.HashSet(); private int keepalive_sec = ClientSession.DEFAULT_KEEPALIVE_SEC; + private int selected_keepalive_sec = SELECTED_KEEPALIVE_SEC; public ClientSessionManager(Credentials cred, uint default_port) { this.cred = cred; @@ -36,14 +38,23 @@ public class Geary.Imap.ClientSessionManager { /** * Set to zero or negative value if keepalives should be disabled. (This is not recommended.) + * + * This only affects newly created sessions or sessions leaving the selected/examined state + * and returning to an authorized state. */ public void set_keepalive(int keepalive_sec) { // set for future connections this.keepalive_sec = keepalive_sec; - - // set for all current connections - foreach (ClientSession session in sessions) - session.enable_keepalives(keepalive_sec); + } + + /** + * Set to zero or negative value if keepalives should be disabled when a mailbox is selected + * or examined. (This is not recommended.) + * + * This only affects newly selected/examined sessions. + */ + public void set_selected_keepalive(int selected_keepalive_sec) { + this.selected_keepalive_sec = selected_keepalive_sec; } public async Gee.Collection list_roots( @@ -166,8 +177,10 @@ public class Geary.Imap.ClientSessionManager { bool removed = contexts.remove(context); assert(removed); - if (context.session != null) + if (context.session != null) { context.session.close_mailbox_async.begin(); + context.session.enable_keepalives(keepalive_sec); + } } // This should only be called when sessions_mutex is locked. @@ -219,6 +232,8 @@ public class Geary.Imap.ClientSessionManager { ClientSession authd = yield get_authorized_session(cancellable); + authd.enable_keepalives(selected_keepalive_sec); + results = yield authd.select_examine_async(folder, is_select, cancellable); return authd; diff --git a/src/engine/imap/transport/imap-mailbox.vala b/src/engine/imap/transport/imap-mailbox.vala index 4359e855..653c1897 100644 --- a/src/engine/imap/transport/imap-mailbox.vala +++ b/src/engine/imap/transport/imap-mailbox.vala @@ -5,16 +5,24 @@ */ public class Geary.Imap.Mailbox : Geary.SmartReference { - public string name { get; private set; } - public int count { get; private set; } - public int recent { get; private set; } - public int unseen { get; private set; } - public bool is_readonly { get; private set; } - public UIDValidity? uid_validity { get; private set; } - public UID? uid_next { get; private set; } + public string name { get { return context.name; } } + public int exists { get { return context.exists; } } + public int recent { get { return context.recent; } } + public int unseen { get { return context.unseen; } } + public bool is_readonly { get { return context.is_readonly; } } + public UIDValidity? uid_validity { get { return context.uid_validity; } } + public UID? uid_next { get { return context.uid_next; } } private SelectedContext context; + public signal void exists_altered(int exists); + + public signal void recent_altered(int recent); + + public signal void flags_altered(FetchResults flags); + + public signal void expunged(MessageNumber msg_num); + public signal void closed(); public signal void disconnected(bool local); @@ -23,17 +31,22 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { base (context); this.context = context; - context.exists_changed.connect(on_exists_changed); + context.closed.connect(on_closed); context.disconnected.connect(on_disconnected); - - name = context.name; - count = context.exists; - recent = context.recent; - unseen = context.unseen; - is_readonly = context.is_readonly; - uid_validity = context.uid_validity; - uid_next = context.uid_next; + context.unsolicited_exists.connect(on_unsolicited_exists); + context.unsolicited_expunged.connect(on_unsolicited_expunged); + context.unsolicited_flags.connect(on_unsolicited_flags); + context.unsolicited_recent.connect(on_unsolicited_recent); + } + + ~Mailbox() { + context.closed.disconnect(on_closed); + context.disconnected.disconnect(on_disconnected); + context.session.unsolicited_exists.disconnect(on_unsolicited_exists); + context.session.unsolicited_expunged.disconnect(on_unsolicited_expunged); + context.session.unsolicited_flags.disconnect(on_unsolicited_flags); + context.session.unsolicited_recent.disconnect(on_unsolicited_recent); } public async Gee.List? list_set_async(MessageSet msg_set, Geary.Email.Field fields, @@ -107,10 +120,6 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { return email; } - private void on_exists_changed(int exists) { - count = exists; - } - private void on_closed() { closed(); } @@ -119,6 +128,22 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { disconnected(local); } + private void on_unsolicited_exists(int exists) { + exists_altered(exists); + } + + private void on_unsolicited_recent(int recent) { + recent_altered(recent); + } + + private void on_unsolicited_expunged(MessageNumber msg_num) { + expunged(msg_num); + } + + private void on_unsolicited_flags(FetchResults flags) { + flags_altered(flags); + } + // store FetchDataTypes in a set because the same data type may be requested multiple times // by different fields (i.e. ENVELOPE) private static void fields_to_fetch_data_types(Geary.Email.Field fields, @@ -225,11 +250,22 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { } } -internal class Geary.Imap.SelectedContext : Object, Geary.ReferenceSemantics { +// A SelectedContext is a ReferenceSemantics object wrapping a ClientSession that is in a SELECTED +// or EXAMINED state (i.e. it has "cd'd" into a folder). Multiple Mailbox objects may be created +// that refer to this SelectedContext. When they're all destroyed, the session is returned to +// the AUTHORIZED state by the ClientSessionManager. +// +// This means there is some duplication between the SelectedContext and the Mailbox. In particular +// signals must be reflected to ensure order-of-operation is preserved (i.e. when the ClientSession +// "unsolicited-exists" signal is fired, a signal subscriber may then query SelectedContext for +// its exists count before it has received the notification). +// +// All this fancy stepping should not be exposed to a user of the IMAP portion of Geary, who should +// only see Geary.Imap.Mailbox, nor should it be exposed to the user of Geary.Engine, where all this +// should only be exposed via Geary.Folder. +private class Geary.Imap.SelectedContext : Object, Geary.ReferenceSemantics { public ClientSession? session { get; private set; } - protected int manual_ref_count { get; protected set; } - public string name { get; protected set; } public int exists { get; protected set; } public int recent { get; protected set; } @@ -238,9 +274,15 @@ internal class Geary.Imap.SelectedContext : Object, Geary.ReferenceSemantics { public UIDValidity? uid_validity { get; protected set; } public UID? uid_next { get; protected set; } - public signal void exists_changed(int exists); + protected int manual_ref_count { get; protected set; } - public signal void recent_changed(int recent); + public signal void unsolicited_exists(int exists); + + public signal void unsolicited_recent(int recent); + + public signal void unsolicited_expunged(MessageNumber expunged); + + public signal void unsolicited_flags(FetchResults flags); public signal void closed(); @@ -260,6 +302,8 @@ internal class Geary.Imap.SelectedContext : Object, Geary.ReferenceSemantics { session.current_mailbox_changed.connect(on_session_mailbox_changed); session.unsolicited_exists.connect(on_unsolicited_exists); session.unsolicited_recent.connect(on_unsolicited_recent); + session.unsolicited_expunged.connect(on_unsolicited_expunged); + session.unsolicited_flags.connect(on_unsolicited_flags); session.logged_out.connect(on_session_logged_out); session.disconnected.connect(on_session_disconnected); } @@ -269,6 +313,8 @@ internal class Geary.Imap.SelectedContext : Object, Geary.ReferenceSemantics { session.current_mailbox_changed.disconnect(on_session_mailbox_changed); session.unsolicited_exists.disconnect(on_unsolicited_exists); session.unsolicited_recent.disconnect(on_unsolicited_recent); + session.unsolicited_recent.disconnect(on_unsolicited_recent); + session.unsolicited_expunged.disconnect(on_unsolicited_expunged); session.logged_out.disconnect(on_session_logged_out); session.disconnected.disconnect(on_session_disconnected); } @@ -280,12 +326,20 @@ internal class Geary.Imap.SelectedContext : Object, Geary.ReferenceSemantics { private void on_unsolicited_exists(int exists) { this.exists = exists; - exists_changed(exists); + unsolicited_exists(exists); } private void on_unsolicited_recent(int recent) { this.recent = recent; - recent_changed(recent); + unsolicited_recent(recent); + } + + private void on_unsolicited_expunged(MessageNumber expunged) { + unsolicited_expunged(expunged); + } + + private void on_unsolicited_flags(FetchResults results) { + unsolicited_flags(results); } private void on_session_mailbox_changed(string? old_mailbox, string? new_mailbox, bool readonly) { diff --git a/src/engine/sqlite/api/sqlite-folder.vala b/src/engine/sqlite/api/sqlite-folder.vala index b2230251..4df3da2e 100644 --- a/src/engine/sqlite/api/sqlite-folder.vala +++ b/src/engine/sqlite/api/sqlite-folder.vala @@ -68,7 +68,7 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gear notify_closed(CloseReason.FOLDER_CLOSED); } - public override async int get_email_count(Cancellable? cancellable = null) throws Error { + public override async int get_email_count_async(Cancellable? cancellable = null) throws Error { check_open(); // TODO: This can be cached and updated when changes occur @@ -109,13 +109,15 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gear imap_message_properties_table, message_id, properties); yield imap_message_properties_table.create_async(properties_row, cancellable); } + + notify_list_appended(yield get_email_count_async(cancellable)); } public override async Gee.List? list_email_async(int low, int count, Geary.Email.Field required_fields, Cancellable? cancellable) throws Error { check_open(); - normalize_span_specifiers(ref low, ref count, yield get_email_count(cancellable)); + normalize_span_specifiers(ref low, ref count, yield get_email_count_async(cancellable)); if (count == 0) return null; @@ -259,6 +261,8 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gear throw new EngineError.NOT_FOUND("UID required to delete local email"); yield location_table.remove_by_ordering_async(folder_row.id, uid.value, cancellable); + + // TODO: Notify of changes } public async bool is_email_present(Geary.EmailIdentifier id, out Geary.Email.Field available_fields,