From 3f6d8eac5ad684d25db4a6dd1a39380d44069cc9 Mon Sep 17 00:00:00 2001 From: Jim Nelson Date: Wed, 9 Nov 2011 16:40:28 -0800 Subject: [PATCH] Moving away from positional addressing. Greater emphasis on EmailIdentifiers for addressing. Positional addressing is a nightmare for a lot of reasons, especially keeping positions up-to-date as the Folder mutates. Now, positions are returned with the email but for advisory reasons only, and do not keep up-to-date. It is expected that a client will use positional addressing to "bootstrap" the application's data store, then use EmailIdentifier addressing to traverse the Folder's contents. --- src/client/ui/main-window.vala | 2 +- src/client/util/util-email.vala | 6 +- src/engine/api/geary-conversations.vala | 106 +++++++--- src/engine/api/geary-email-identifier.vala | 32 ++- src/engine/api/geary-email-location.vala | 135 ------------- src/engine/api/geary-email.vala | 36 +++- src/engine/api/geary-folder.vala | 91 +++++++-- .../imap/api/imap-email-identifier.vala | 11 + src/engine/imap/api/imap-email-location.vala | 16 -- .../imap/api/imap-folder-extensions.vala | 24 --- src/engine/imap/api/imap-folder.vala | 33 ++- src/engine/imap/message/imap-message-set.vala | 31 +++ src/engine/imap/transport/imap-mailbox.vala | 8 +- src/engine/impl/geary-abstract-folder.vala | 36 +++- src/engine/impl/geary-engine-folder.vala | 190 +++++++++++++----- .../impl/geary-generic-imap-folder.vala | 51 +++-- src/engine/impl/geary-local-interfaces.vala | 15 ++ src/engine/impl/geary-remote-interfaces.vala | 8 + src/engine/sqlite/api/sqlite-folder.vala | 93 ++++++--- .../email/sqlite-message-location-table.vala | 43 ++-- .../sqlite/email/sqlite-message-row.vala | 4 +- src/wscript | 3 - 22 files changed, 602 insertions(+), 372 deletions(-) delete mode 100644 src/engine/api/geary-email-location.vala delete mode 100644 src/engine/imap/api/imap-email-location.vala delete mode 100644 src/engine/imap/api/imap-folder-extensions.vala diff --git a/src/client/ui/main-window.vala b/src/client/ui/main-window.vala index 6ea20f89..cec67eea 100644 --- a/src/client/ui/main-window.vala +++ b/src/client/ui/main-window.vala @@ -200,7 +200,7 @@ public class MainWindow : Gtk.Window { current_conversations.lazy_load(-1, -1, Geary.Folder.ListFlags.FAST, cancellable_folder); } - public void on_scan_started(int low, int count) { + public void on_scan_started() { debug("on scan started"); } diff --git a/src/client/util/util-email.vala b/src/client/util/util-email.vala index 3af1e901..60d7bf5f 100644 --- a/src/client/util/util-email.vala +++ b/src/client/util/util-email.vala @@ -6,9 +6,7 @@ public int compare_email(Geary.Email aenvelope, Geary.Email benvelope) { int diff = aenvelope.date.value.compare(benvelope.date.value); - if (diff != 0) - return diff; - // stabilize sort by using the mail's position, which is always unique in a folder - return aenvelope.location.position - benvelope.location.position; + // stabilize sort by using the mail's ordering, which is always unique in a folder + return (diff != 0) ? diff : aenvelope.id.compare(benvelope.id); } diff --git a/src/engine/api/geary-conversations.vala b/src/engine/api/geary-conversations.vala index 85a5192f..b0c1bf91 100644 --- a/src/engine/api/geary-conversations.vala +++ b/src/engine/api/geary-conversations.vala @@ -190,7 +190,13 @@ public class Geary.Conversations : Object { private bool monitor_new = false; private Cancellable? cancellable_monitor = null; - public virtual signal void scan_started(int low, int count) { + /** + * "scan-started" is fired whenever beginning to load messages into the Conversations object. + * If id is not null, then the scan is starting at an identifier and progressing according to + * count (see Geary.Folder.list_email_by_id_async()). Otherwise, the scan is using positional + * addressing and low is a valid one-based position (see Geary.Folder.list_email_async()). + */ + public virtual signal void scan_started(Geary.EmailIdentifier? id, int low, int count) { } public virtual signal void scan_error(Error err) { @@ -224,8 +230,8 @@ public class Geary.Conversations : Object { folder.messages_appended.disconnect(on_folder_messages_appended); } - protected virtual void notify_scan_started(int low, int count) { - scan_started(low, count); + protected virtual void notify_scan_started(Geary.EmailIdentifier? id, int low, int count) { + scan_started(id, low, count); } protected virtual void notify_scan_error(Error err) { @@ -254,11 +260,28 @@ public class Geary.Conversations : Object { return conversations.read_only_view; } + public bool monitor_new_messages(Cancellable? cancellable = null) { + if (monitor_new) + return false; + + monitor_new = true; + cancellable_monitor = cancellable; + folder.messages_appended.connect(on_folder_messages_appended); + + return true; + } + + /** + * See Geary.Folder.list_email_async() for details of how these parameters operate. Instead + * of returning emails, this method will load the Conversations object with them sorted into + * Conversation objects. + */ public async void load_async(int low, int count, Geary.Folder.ListFlags flags, Cancellable? cancellable) throws Error { - notify_scan_started(low, count); + notify_scan_started(null, low, count); try { - Gee.List? list = yield folder.list_email_async(low, count, required_fields, flags); + Gee.List? list = yield folder.list_email_async(low, count, required_fields, flags, + cancellable); on_email_listed(list, null); if (list != null) on_email_listed(null, null); @@ -267,20 +290,45 @@ public class Geary.Conversations : Object { } } - public void lazy_load(int low, int count, Geary.Folder.ListFlags flags, Cancellable? cancellable) - throws Error { - notify_scan_started(low, count); + /** + * See Geary.Folder.lazy_list_email_async() for details of how these parameters operate. Instead + * of returning emails, this method will load the Conversations object with them sorted into + * Conversation objects. + */ + public void lazy_load(int low, int count, Geary.Folder.ListFlags flags, Cancellable? cancellable) { + notify_scan_started(null, low, count); folder.lazy_list_email(low, count, required_fields, flags, on_email_listed, cancellable); } - public bool monitor_new_messages(Cancellable? cancellable = null) { - if (monitor_new) - return false; - - monitor_new = true; - cancellable_monitor = cancellable; - folder.messages_appended.connect(on_folder_messages_appended); - return true; + /** + * See Geary.Folder.list_email_by_id_async() for details of how these parameters operate. Instead + * of returning emails, this method will load the Conversations object with them sorted into + * Conversation objects. + */ + public async void load_by_id_async(Geary.EmailIdentifier initial_id, int count, + Geary.Folder.ListFlags flags, Cancellable? cancellable) throws Error { + notify_scan_started(initial_id, -1, count); + try { + Gee.List? list = yield folder.list_email_by_id_async(initial_id, count, + required_fields, flags, cancellable); + on_email_listed(list, null); + if (list != null) + on_email_listed(null, null); + } catch (Error err) { + on_email_listed(null, err); + } + } + + /** + * See Geary.Folder.lazy_list_email_by_id() for details of how these parameters operate. Instead + * of returning emails, this method will load the Conversations object with them sorted into + * Conversation objects. + */ + public void lazy_load_by_id(Geary.EmailIdentifier initial_id, int count, Geary.Folder.ListFlags flags, + Cancellable? cancellable) { + notify_scan_started(initial_id, -1, count); + folder.lazy_list_email_by_id(initial_id, count, required_fields, flags, on_email_listed, + cancellable); } private void on_email_listed(Gee.List? emails, Error? err) { @@ -518,29 +566,27 @@ public class Geary.Conversations : Object { } private void on_folder_messages_appended() { - // Find highest position. + // Find highest identifier by ordering // TODO: optimize. - int high = -1; - foreach (Conversation c in conversations) - foreach (Email e in c.get_pool()) - if (e.location.position > high) - high = e.location.position; + Geary.EmailIdentifier? highest = null; + foreach (Conversation c in conversations) { + foreach (Email e in c.get_pool()) { + if (highest == null || (e.id.compare(highest) > 0)) + highest = e.id; + } + } - if (high < 0) { + if (highest == null) { debug("Unable to find highest message position in %s", folder.to_string()); return; } - debug("Message(s) appended to %s, fetching email at %d and above", folder.to_string(), - high + 1); + debug("Message(s) appended to %s, fetching email at %s and above", folder.to_string(), + highest.to_string()); // Want to get the one *after* the highest position in the list - try { - lazy_load(high + 1, -1, Folder.ListFlags.NONE, cancellable_monitor); - } catch (Error e) { - warning("Error getting new mail: %s", e.message); - } + lazy_load_by_id(highest.next(), int.MAX, Folder.ListFlags.NONE, cancellable_monitor); } } diff --git a/src/engine/api/geary-email-identifier.vala b/src/engine/api/geary-email-identifier.vala index da7644e6..dffe45a0 100644 --- a/src/engine/api/geary-email-identifier.vala +++ b/src/engine/api/geary-email-identifier.vala @@ -10,11 +10,41 @@ * message is located in; an EmailIdentifier cannot be used in another Folder to determine if the * message is duplicated there. (Either EmailIdentifier will be expanded to allow for this or * another system will be offered.) + * + * EmailIdentifier is Comparable because it can be used to compare against other EmailIdentifiers + * (in the same Folder) for sort order that corresponds to their position in the Folder. It does + * this through an ordering field that provides an integer that can be compared to other ordering + * fields in the same Folder that match the email's position within it. The ordering field may + * or may not be the actual unique identifier; in IMAP, for example, it is, while in other systems + * it may not be. */ -public abstract class Geary.EmailIdentifier : Object, Geary.Equalable { +public abstract class Geary.EmailIdentifier : Object, Geary.Equalable, Geary.Comparable { + public abstract int64 ordering { get; protected set; } + + public abstract EmailIdentifier next(); + + public abstract EmailIdentifier previous(); + public abstract bool equals(Geary.Equalable other); + public virtual int compare(Geary.Comparable o) { + Geary.EmailIdentifier? other = o as Geary.EmailIdentifier; + if (other == null) + return -1; + + if (this == other) + return 0; + + int64 diff = ordering - other.ordering; + if (diff < 0) + return -1; + else if (diff > 0) + return 1; + else + return 0; + } + public abstract string to_string(); } diff --git a/src/engine/api/geary-email-location.vala b/src/engine/api/geary-email-location.vala deleted file mode 100644 index ad8f7ab9..00000000 --- a/src/engine/api/geary-email-location.vala +++ /dev/null @@ -1,135 +0,0 @@ -/* Copyright 2011 Yorba Foundation - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -/** - * An EmailLocation represents an Email's position (cardinal ordering) in a Folder. Unlike other - * elements in an Email, it may change over time, depending on events within the Folder itself. - * It reports these changes via signals. - * - * An EmailLocation's position is a cardinal (1 to n) of an Email's ordering within the Folder. - * Its ordering is an opaque number that was used to determine this ordering -- it may be exactly - * equal to its position, or it may not. This value is particular to the message provider and - * should not be depended on. - * - * When the Folder closes, the EmailLocation will fire its "invalidated" signal and go into an - * invalidated state. Its position will be -1. The EmailLocation will never go back to a valid - * state; the owner should discard the EmailLocation (and, by extension, its Email) after the Folder - * has closed and fetch fresh ones once its re-opened. - * - * Note the differences between an EmailLocation, which gives a cardinal way of addressing mail in - * a folder, an an EmailIdentifier, which is unique and immutable, but is only addressable as a - * singleton, and not by a range. - */ -public class Geary.EmailLocation : Object { - /** - * Returns -1 if invalidated. - */ - public int position { get; private set; } - public int64 ordering { get; private set; } - - private Geary.Folder folder; - private int local_adjustment; - - public signal void position_altered(int old_position, int new_position); - - public signal void position_deleted(int position); - - /** - * "invalidated" is fired when the EmailLocation object is no longer valid due to the Folder - * closing. At this point, its position will be -1. - */ - public signal void invalidated(); - - public EmailLocation(Geary.Folder folder, int position, int64 ordering) { - init(folder, position, ordering, -1); - } - - public EmailLocation.local(Geary.Folder folder, int position, int64 ordering, int local_adjustment) { - init(folder, position, ordering, local_adjustment); - } - - ~EmailLocation() { - invalidate(false); - } - - private void init(Geary.Folder folder, int position, int64 ordering, int local_adjustment) { - assert(position >= 1); - - this.folder = folder; - this.position = position; - this.ordering = ordering; - this.local_adjustment = local_adjustment; - - folder.message_removed.connect(on_message_removed); - folder.opened.connect(on_folder_opened); - folder.closed.connect(on_folder_closed); - } - - private void invalidate(bool signalled) { - if (position < 0) - return; - - position = -1; - - folder.message_removed.disconnect(on_message_removed); - folder.opened.disconnect(on_folder_opened); - folder.closed.disconnect(on_folder_closed); - - if (signalled) - invalidated(); - } - - private void on_message_removed(int position, int total) { - // bail out if invalidated - if (position < 1) - return; - - // if the removed position is greater than this one's, no change in this position - if (this.position < position) - return; - - // if the same, can't adjust (adjust it to what?), but notify that this EmailLocation has - // been removed - if (this.position == position) { - position_deleted(position); - - return; - } - - // adjust this position downward - int old_position = this.position; - this.position--; - assert(this.position >= 1); - - position_altered(old_position, this.position); - } - - private void on_folder_closed() { - invalidate(true); - } - - private void on_folder_opened(Geary.Folder.OpenState state, int count) { - // If no local_adjustment, nothing needs to be done; if the local_adjustment is greater or - // equal to the remote folder's count, that indicates messages have been removed, in which - // case let the adjustments occur via on_message_removed(). - if (local_adjustment < 0 || count <= local_adjustment) { - local_adjustment = -1; - - return; - } - - int old_position = position; - position = count - local_adjustment; - assert(position >= 1); - - // mark as completed, to prevent this from happening again - local_adjustment = -1; - - if (position != old_position) - position_altered(old_position, position); - } -} - diff --git a/src/engine/api/geary-email.vala b/src/engine/api/geary-email.vala index 64f512e3..0a568047 100644 --- a/src/engine/api/geary-email.vala +++ b/src/engine/api/geary-email.vala @@ -57,7 +57,25 @@ public class Geary.Email : Object { } } - public Geary.EmailLocation location { get; private set; } + /** + * position is the one-based addressing of the email in the folder, in the notion that messages + * are "stacked" from 1 to n, earliest to newest. "Earliest" and "newest" do not necessarily + * correspond to the emails' send or receive time, merely how they've been arranged on the stack. + * + * This value is only good at the time the Email is requested from the folder. Subsequent + * operations may change the Email's position in the folder (or simply remove it). This value + * is *not* updated to reflect this. + * + * This field is always returned, no matter what Fields are used to retrieve the Email. + */ + public int position { get; private set; } + + /** + * id is a unique identifier for the Email in the Folder. It is guaranteed to be unique for + * as long as the Folder is open. Once closed, guarantees are no longer made. + * + * This field is always returned, no matter what Fields are used to retrieve the Email. + */ public Geary.EmailIdentifier id { get; private set; } // DATE @@ -94,13 +112,17 @@ public class Geary.Email : Object { private Geary.RFC822.Message? message = null; - public Email(Geary.EmailLocation location, Geary.EmailIdentifier id) { - this.location = location; + public Email(int position, Geary.EmailIdentifier id) { + assert(position >= 1); + + this.position = position; this.id = id; } - public void update_location(Geary.EmailLocation location) { - this.location = location; + internal void update_position(int position) { + assert(position >= 1); + + this.position = position; } public void set_send_date(Geary.RFC822.Date date) { @@ -185,10 +207,10 @@ public class Geary.Email : Object { public string to_string() { StringBuilder builder = new StringBuilder(); - builder.append_printf("[#%d/%s] ", location.position, id.to_string()); + builder.append_printf("[#%d/%s] ", position, id.to_string()); if (date != null) - builder.append_printf("[%s]", date.to_string()); + builder.append_printf("%s/", date.to_string()); return builder.str; } diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala index 34b29b6c..9bee068c 100644 --- a/src/engine/api/geary-folder.vala +++ b/src/engine/api/geary-folder.vala @@ -19,9 +19,9 @@ public interface Geary.Folder : Object { FOLDER_CLOSED } - public enum Direction { - BEFORE, - AFTER + public enum CountChangeReason { + ADDED, + REMOVED } [Flags] @@ -67,20 +67,21 @@ public interface Geary.Folder : Object { * "message-removed" is fired when a message has been removed (deleted or moved) from the * folder (and therefore old message position numbers may no longer be valid, i.e. those after * the removed message). + * + * NOTE: It's possible for the remote server to report a message has been removed that is not + * known locally (and therefore the caller could not have record of). If this happens, this + * signal will *not* fire, although "email-count-changed" will. */ - public signal void message_removed(int position, int total); + public signal void message_removed(Geary.EmailIdentifier id); /** - * "positions-reordered" is fired when message positions on emails in the folder may no longer - * be valid, which may happen even if a message has not been removed. In other words, if a - * message is removed and it causes positions to change, "message-remove" will be fired followed - * by this signal. + * "email-count-changed" is fired when the total count of email in a folder has changed in any way. * - * Although reordering may be rare (positions shifting is a better description), it is possible - * for messages in a folder to change positions completely. This signal covers both - * circumstances. + * Note that this signal will be fired alongside "messages-appended" or "message-removed". + * That is, do not use both signals to process email count changes; one will suffice. + * This signal will fire after those (although see the note at "message-removed"). */ - public signal void positions_reordered(); + public signal void email_count_changed(int new_count, CountChangeReason reason); /** * This helper method should be called by implementors of Folder rather than firing the signal @@ -108,14 +109,14 @@ public interface Geary.Folder : Object { * 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 abstract void notify_positions_reordered(); + protected abstract void notify_message_removed(Geary.EmailIdentifier id); /** * 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 abstract void notify_message_removed(int position, int total); + protected abstract void notify_email_count_changed(int new_count, CountChangeReason reason); public abstract Geary.FolderPath get_path(); @@ -267,7 +268,7 @@ public interface Geary.Folder : Object { * 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 - * message's order. + * messages' order. * * The Folder must be opened prior to attempting this operation. */ @@ -275,27 +276,73 @@ public interface Geary.Folder : Object { Geary.Email.Field required_fields, ListFlags flags, EmailCallback cb, Cancellable? cancellable = null); + /** + * Similar in contract to list_email_async(), but uses Geary.EmailIdentifier rather than + * positional addressing. This allows for a batch of messages to be listed from a starting + * identifier, going up and down the stack depending on the count parameter. + * + * The count parameter is exclusive of the Email at initial_id. That is, if count is one, + * two Emails may be returned: the one for initial_id and the next one. If count is zero, + * only the Email with the specified initial_id will be listed, making this method operate + * like fetch_email_async(). + * + * There is no guarantee that a message with the initial_id will be returned however. + * (It is up to the implementation to deal with spans starting from a non-existant or + * unavailable EmailIdentifier.) To fetch email exclusive of the initial_id, use + * EmailIdentifier.next() or EmailIdentifier.previous(). + * + * If count is positive, initial_id is the *lowest* identifier and the returned list is going + * up the stack (toward the most recently added). If the count is negative, initial_id is + * the *highest* identifier and the returned list is going down the stack (toward the earliest + * added). + * + * To fetch all available messages in one direction or another, use int.MIN or int.MAX. + * + * There's no guarantee of the returned messages' order. + * + * There is (currently) no sparse version of list_email_by_id_async(). + * + * The Folder must be opened prior to attempting this operation. + */ + public abstract async Gee.List? list_email_by_id_async(Geary.EmailIdentifier initial_id, + int count, Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable = null) + throws Error; + + /** + * Similar in contract to lazy_list_email_async(), but uses Geary.EmailIdentifier rather than + * positional addressing, much like list_email_by_id_async(). See that method for more + * information on its contract and how the count parameter works. + * + * Like the other "lazy" methods, this method will call EmailCallback while the operation is + * processing. This method does not block. + * + * There is (currently) no sparse version of lazy_list_email_by_id(). + * + * The Folder must be opened prior to attempting this operation. + */ + public abstract void lazy_list_email_by_id(Geary.EmailIdentifier initial_id, int count, + Geary.Email.Field required_fields, ListFlags flags, EmailCallback cb, + Cancellable? cancellable = null); + /** * Returns a single email that fulfills the required_fields flag at the ordered position in - * the folder. If position is invalid for the folder's contents, an EngineError.NOT_FOUND + * the folder. If the email_id is invalid for the folder's contents, an EngineError.NOT_FOUND * error is thrown. If the requested fields are not available, EngineError.INCOMPLETE_MESSAGE * is thrown. * * The Folder must be opened prior to attempting this operation. - * - * position is one-based. */ public abstract async Geary.Email fetch_email_async(Geary.EmailIdentifier email_id, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; /** - * Removes the email at the supplied position from the folder. If the email position is - * invalid for any reason, EngineError.NOT_FOUND is thrown. + * Removes the email at the supplied position from the folder. If the email_id is invalid for + * any reason, EngineError.NOT_FOUND is thrown. * * The Folder must be opened prior to attempting this operation. */ - public abstract async void remove_email_async(int position, Cancellable? cancellable = null) - throws Error; + public abstract async void remove_email_async(Geary.EmailIdentifier email_id, + Cancellable? cancellable = null) throws Error; /** * check_span_specifiers() verifies that the span specifiers match the requirements set by diff --git a/src/engine/imap/api/imap-email-identifier.vala b/src/engine/imap/api/imap-email-identifier.vala index 949a689a..1733a12c 100644 --- a/src/engine/imap/api/imap-email-identifier.vala +++ b/src/engine/imap/api/imap-email-identifier.vala @@ -5,10 +5,21 @@ */ private class Geary.Imap.EmailIdentifier : Geary.EmailIdentifier { + public override int64 ordering { get; protected set; } + public Imap.UID uid { get; private set; } + public override Geary.EmailIdentifier next() { + return new Geary.Imap.EmailIdentifier(new Imap.UID((uid.value + 1).clamp(1, uint32.MAX))); + } + + public override Geary.EmailIdentifier previous() { + return new Geary.Imap.EmailIdentifier(new Imap.UID((uid.value - 1).clamp(1, uint32.MAX))); + } + public EmailIdentifier(Imap.UID uid) { this.uid = uid; + ordering = uid.value; } public override bool equals(Equalable o) { diff --git a/src/engine/imap/api/imap-email-location.vala b/src/engine/imap/api/imap-email-location.vala deleted file mode 100644 index 097bc719..00000000 --- a/src/engine/imap/api/imap-email-location.vala +++ /dev/null @@ -1,16 +0,0 @@ -/* Copyright 2011 Yorba Foundation - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -/* - * The IMAP implementation of Geary.EmailLocation uses the email's UID to order the messages. - */ - -private class Geary.Imap.EmailLocation : Geary.EmailLocation { - public EmailLocation(Geary.Folder folder, int position, Geary.Imap.UID uid) { - base (folder, position, uid.value); - } -} - diff --git a/src/engine/imap/api/imap-folder-extensions.vala b/src/engine/imap/api/imap-folder-extensions.vala deleted file mode 100644 index 8e09ae0a..00000000 --- a/src/engine/imap/api/imap-folder-extensions.vala +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright 2011 Yorba Foundation - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -private interface Geary.Imap.FolderExtensions : Geary.Folder { - /** - * Much like Geary.Folder.list_email_async(), but this list operation allows for a range of - * emails to be specified by their UID rather than position (message number). If low is null - * that indicates to search from the lowest UID (1) to high. Likewise, if high is null it - * indicates to search from low to the highest UID. Setting both to null will return all - * emails in the folder. - * - * Unlike list_email_async(), this call guarantees that the messages will be returned in UID - * order, from lowest to highest. - * - * The folder must be open before making this call. - */ - public abstract async Gee.List? list_email_uid_async(Geary.Imap.UID? low, - Geary.Imap.UID? high, Geary.Email.Field fields, Cancellable? cancellable = null) - throws Error; -} - diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala index deabeb95..64d69528 100644 --- a/src/engine/imap/api/imap-folder.vala +++ b/src/engine/imap/api/imap-folder.vala @@ -4,7 +4,7 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Geary.Imap.FolderExtensions { +private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder { public const bool CASE_SENSITIVE = true; private ClientSessionManager session_mgr; @@ -27,6 +27,10 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Gear : new Imap.FolderProperties(0, 0, 0, null, null, info.attrs); } + protected void notify_message_at_removed(int position, int total) { + message_at_removed(position, total); + } + public override Geary.FolderPath get_path() { return path; } @@ -92,7 +96,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Gear private void on_expunged(MessageNumber expunged, int total) { assert(mailbox != null); - notify_message_removed(expunged.value, total); + notify_message_at_removed(expunged.value, total); } public override async int get_email_count_async(Cancellable? cancellable = null) throws Error { @@ -128,14 +132,27 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Gear return yield mailbox.list_set_async(this, new MessageSet.sparse(by_position), fields, cancellable); } - public async Gee.List? list_email_uid_async(Geary.Imap.UID? low, - Geary.Imap.UID? high, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { + public override async Gee.List? list_email_by_id_async(Geary.EmailIdentifier email_id, + int count, Geary.Email.Field fields, Geary.Folder.ListFlags flags, Cancellable? cancellable = null) + throws Error { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); - MessageSet msg_set = (high != null) - ? new MessageSet.uid_range((low != null) ? low : new Geary.Imap.UID(1), high) - : new MessageSet.uid_range_to_highest(low); + UID uid = ((Imap.EmailIdentifier) email_id).uid; + + MessageSet msg_set; + if (count > 0) { + msg_set = (count == int.MAX) + ? new MessageSet.uid_range_to_highest(uid) + : new MessageSet.uid_range_by_count(uid, count); + } else if (count < 0) { + msg_set = (count != int.MIN) + ? new MessageSet.uid_range(new UID(1), uid) + : new MessageSet.uid_range_by_count(uid, count); + } else { + // count == 0 + msg_set = new MessageSet.uid(uid); + } return yield mailbox.list_set_async(this, msg_set, fields, cancellable); } @@ -150,7 +167,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Gear return yield mailbox.fetch_async(this, ((Imap.EmailIdentifier) id).uid, fields, cancellable); } - public override async void remove_email_async(int position, Cancellable? cancellable = null) + public override async void remove_email_async(Geary.EmailIdentifier email_id, Cancellable? cancellable = null) throws Error { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); diff --git a/src/engine/imap/message/imap-message-set.vala b/src/engine/imap/message/imap-message-set.vala index b1ad9d25..8134ce9f 100644 --- a/src/engine/imap/message/imap-message-set.vala +++ b/src/engine/imap/message/imap-message-set.vala @@ -45,6 +45,37 @@ public class Geary.Imap.MessageSet { value = "%d:*".printf(low_msg_num); } + /** + * A positive count yields a range going from initial up the stack (toward the most recently + * added message). A negative count yields a range going from initial down the stack (toward + * the earliest added message). A count of zero yields a message range for one UID, initial. + * + * Underflows and overflows are accounted for by clamping the arithmetic result to the possible + * range of UID's. + */ + public MessageSet.uid_range_by_count(UID initial, int count) { + assert(initial.value > 0); + + if (count == 0) { + MessageSet.uid(initial); + + return; + } + + int64 low, high; + if (count < 0) { + high = initial.value; + low = (high + count).clamp(1, uint32.MAX); + } else { + // count > 0 + low = initial.value; + high = (low + count).clamp(1, uint32.MAX); + } + + value = "%lld:%lld".printf(low, high); + is_uid = true; + } + public MessageSet.uid_range_to_highest(UID low) { assert(low.value > 0); diff --git a/src/engine/imap/transport/imap-mailbox.vala b/src/engine/imap/transport/imap-mailbox.vala index a4c72d83..2ddb53c1 100644 --- a/src/engine/imap/transport/imap-mailbox.vala +++ b/src/engine/imap/transport/imap-mailbox.vala @@ -78,9 +78,7 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { // see fields_to_fetch_data_types() for why this is guaranteed assert(uid != null); - Geary.Email email = new Geary.Email( - new Geary.Imap.EmailLocation(folder, res.msg_num, uid), - new Geary.Imap.EmailIdentifier(uid)); + Geary.Email email = new Geary.Email(res.msg_num, new Geary.Imap.EmailIdentifier(uid)); fetch_results_to_email(res, fields, email); msgs.add(email); @@ -111,9 +109,7 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { if (results.length != 1) throw new ImapError.SERVER_ERROR("Too many responses from server: %d", results.length); - Geary.Email email = new Geary.Email( - new Geary.Imap.EmailLocation(folder, results[0].msg_num, uid), - new Geary.Imap.EmailIdentifier(uid)); + Geary.Email email = new Geary.Email(results[0].msg_num, new Geary.Imap.EmailIdentifier(uid)); fetch_results_to_email(results[0], fields, email); return email; diff --git a/src/engine/impl/geary-abstract-folder.vala b/src/engine/impl/geary-abstract-folder.vala index 2fa5bce4..c38a99fd 100644 --- a/src/engine/impl/geary-abstract-folder.vala +++ b/src/engine/impl/geary-abstract-folder.vala @@ -17,12 +17,12 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder { messages_appended(total); } - protected virtual void notify_message_removed(int position, int total) { - message_removed(position, total); + protected virtual void notify_message_removed(Geary.EmailIdentifier id) { + message_removed(id); } - protected virtual void notify_positions_reordered() { - positions_reordered(); + protected virtual void notify_email_count_changed(int new_count, Folder.CountChangeReason reason) { + email_count_changed(new_count, reason); } public abstract Geary.FolderPath get_path(); @@ -90,10 +90,36 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder { } } + public abstract async Gee.List? list_email_by_id_async(Geary.EmailIdentifier initial_id, + int count, Geary.Email.Field required_fields, Folder.ListFlags flags, Cancellable? cancellable = null) + throws Error; + + public virtual void lazy_list_email_by_id(Geary.EmailIdentifier initial_id, int count, + Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, + Cancellable? cancellable = null) { + do_lazy_list_email_by_id_async.begin(initial_id, count, required_fields, flags, cb, cancellable); + } + + private async void do_lazy_list_email_by_id_async(Geary.EmailIdentifier initial_id, int count, + Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, + Cancellable? cancellable) { + try { + Gee.List? list = yield list_email_by_id_async(initial_id, count, + required_fields, flags, cancellable); + + if (list != null && list.size > 0) + cb(list, null); + + cb(null, null); + } catch (Error err) { + cb(null, err); + } + } + public abstract async Geary.Email fetch_email_async(Geary.EmailIdentifier id, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; - public abstract async void remove_email_async(int position, Cancellable? cancellable = null) + public abstract async void remove_email_async(Geary.EmailIdentifier email_id, Cancellable? cancellable = null) throws Error; public virtual string to_string() { diff --git a/src/engine/impl/geary-engine-folder.vala b/src/engine/impl/geary-engine-folder.vala index e9668162..65be75f2 100644 --- a/src/engine/impl/geary-engine-folder.vala +++ b/src/engine/impl/geary-engine-folder.vala @@ -27,6 +27,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { public EngineFolder owner; public int position; public int new_remote_count; + public EmailIdentifier? id; public ReplayRemoval(EngineFolder owner, int position, int new_remote_count) { base ("Removal"); @@ -34,10 +35,20 @@ private class Geary.EngineFolder : Geary.AbstractFolder { this.owner = owner; this.position = position; this.new_remote_count = new_remote_count; + id = null; + } + + public ReplayRemoval.with_id(EngineFolder owner, EmailIdentifier id) { + base ("Removal.with_id"); + + this.owner = owner; + position = -1; + new_remote_count = -1; + this.id = id; } public override async void replay() { - yield owner.do_replay_remove_message(position, new_remote_count); + yield owner.do_replay_remove_message(position, new_remote_count, id); } } @@ -128,6 +139,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { // signals folder.messages_appended.connect(on_remote_messages_appended); folder.message_removed.connect(on_remote_message_removed); + folder.message_at_removed.connect(on_remote_message_at_removed); // state remote_count = yield folder.get_email_count_async(cancellable); @@ -260,38 +272,52 @@ private class Geary.EngineFolder : Geary.AbstractFolder { } } - private void on_remote_message_removed(int position, int total) { - debug("on_remote_message_removed: position=%d total=%d", position, total); + private void on_remote_message_removed(Geary.EmailIdentifier id) { + debug("on_remote_message_removed: %s", id.to_string()); + replay_queue.schedule(new ReplayRemoval.with_id(this, id)); + } + + private void on_remote_message_at_removed(int position, int total) { + debug("on_remote_message_at_removed: position=%d total=%d", position, total); replay_queue.schedule(new ReplayRemoval(this, position, total)); } // This MUST only be called from ReplayRemoval. - private async void do_replay_remove_message(int remote_position, int new_remote_count) { - try { - // calculate the local position of the message in the local store - int local_count = yield local_folder.get_email_count_async(); - int local_low = ((remote_count - local_count) + 1).clamp(1, remote_count); - - if (remote_position < local_low) { - debug("do_replay_remove_message: Not removing message at %d from local store, not present", - remote_position); - } else { - // Adjust remote position to local position - yield local_folder.remove_email_async((remote_position - local_low) + 1); + private async void do_replay_remove_message(int remote_position, int new_remote_count, + Geary.EmailIdentifier? id) { + if (remote_position < 1) + assert(id != null); + else + assert(new_remote_count >= 0); + + if (id == null) { + try { + Gee.List? local = yield local_folder.list_email_async(remote_position, 1, + Geary.Email.Field.NONE, Geary.Folder.ListFlags.NONE, null); + if (local != null && local.size > 0) + id = local[0].id; + } catch (Error err) { + debug("Unable to determine ID of removed message #%d from %s: %s", remote_position, + to_string(), err.message); } - - // save new remote count - remote_count = new_remote_count; - - notify_message_removed(remote_position, new_remote_count); - - // only fire "positions-altered" if indeed positions have been altered - if (remote_position != new_remote_count) - notify_positions_reordered(); - } catch (Error err) { - debug("Unable to remove message #%d from %s: %s", remote_position, to_string(), - err.message); } + + if (id != null) { + try { + // Reflect change in the local store and notify subscribers + yield local_folder.remove_email_async(id, null); + + notify_message_removed(id); + } catch (Error err2) { + debug("Unable to remove message #%d from %s: %s", remote_position, to_string(), + err2.message); + } + } + + // save new remote count and notify of change + remote_count = new_remote_count; + + notify_email_count_changed(remote_count, CountChangeReason.REMOVED); } public override async int get_email_count_async(Cancellable? cancellable = null) throws Error { @@ -322,6 +348,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { return accumulator; } + // TODO: Capture Error and report via EmailCallback. public override void lazy_list_email(int low, int count, Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null) { // schedule do_list_email_async(), using the callback to drive availability of email @@ -392,19 +419,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder { // fixup local email positions to match server's positions if (local_list_size > 0 && remote_count > 0 && local_count < remote_count) { int adjustment = remote_count - local_count; - foreach (Geary.Email email in local_list) { - email.update_location(new Geary.EmailLocation(this, - email.location.position + adjustment, email.location.ordering)); - } - } else if (local_list_size > 0 && local_only) { - // if remote_count is -1, the remote folder hasn't been opened so the true count hasn't - // been determined; create local EmailLocations that update themselves when the - // folder is opened and the count is known (adjusted by the local_offset passed in) - foreach (Geary.Email local_email in local_list) { - local_email.update_location(new Geary.EmailLocation.local(this, - local_email.location.position, local_email.location.ordering, - (count + low - 1) - local_email.location.position)); - } + foreach (Geary.Email email in local_list) + email.update_position(email.position + adjustment); } // report list @@ -434,7 +450,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { for (int position = low; position <= (low + (count - 1)); position++) { bool found = false; for (int ctr = 0; ctr < local_list_size; ctr++) { - if (local_list[ctr].location.position == position) { + if (local_list[ctr].position == position) { found = true; break; @@ -486,6 +502,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { return accumulator; } + // TODO: Capture Error and report via EmailCallback. public override void lazy_list_email_sparse(int[] by_position, Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null) { // schedule listing in the background, using the callback to drive availability of email @@ -546,11 +563,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder { // reverse the process, fixing up all the returned messages to match the server's notions if (local_list_size > 0 && local_offset > 0) { - foreach (Geary.Email email in local_list) { - int new_position = email.location.position + local_offset; - email.update_location(new Geary.EmailLocation(this, new_position, - email.location.ordering)); - } + foreach (Geary.Email email in local_list) + email.update_position(email.position + local_offset); } if (local_list_size == by_position.length || local_only) { @@ -576,7 +590,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { bool found = false; if (local_list != null) { foreach (Geary.Email email2 in local_list) { - if (email2.location.position == position) { + if (email2.position == position) { found = true; break; @@ -624,6 +638,84 @@ private class Geary.EngineFolder : Geary.AbstractFolder { cb(null, null); } + public override async Gee.List? list_email_by_id_async(Geary.EmailIdentifier initial_id, + int count, Geary.Email.Field required_fields, Folder.ListFlags flags, + Cancellable? cancellable = null) throws Error { + Gee.List list = new Gee.ArrayList(); + yield do_list_email_by_id_async(initial_id, count, required_fields, list, null, cancellable, + flags.is_all_set(Folder.ListFlags.FAST)); + + return (list.size > 0) ? list : null; + } + + public override void lazy_list_email_by_id(Geary.EmailIdentifier initial_id, int count, + Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, + Cancellable? cancellable = null) { + do_lazy_list_email_by_id_async.begin(initial_id, count, required_fields, cb, cancellable, + flags.is_all_set(Folder.ListFlags.FAST)); + } + + private async void do_lazy_list_email_by_id_async(Geary.EmailIdentifier initial_id, int count, + Geary.Email.Field required_fields, EmailCallback cb, Cancellable? cancellable, bool local_only) { + try { + yield do_list_email_by_id_async(initial_id, count, required_fields, null, cb, cancellable, + local_only); + } catch (Error err) { + cb(null, err); + } + } + + // STRATEGY: Determine position number of message at initial_id then work via positions from + // that point on. + private async void do_list_email_by_id_async(Geary.EmailIdentifier initial_id, int count, + Geary.Email.Field required_fields, Gee.List? accumulator, EmailCallback? cb, + Cancellable? cancellable, bool local_only) throws Error { + if (!opened) + throw new EngineError.OPEN_REQUIRED("%s is not open", to_string()); + + // listing by ID requires the remote to be open and fully synchronized, as there's no + // reliable way to determine certain counts and positions without it + // + // TODO: Need to deal with this in a sane manner when offline + if (!yield wait_for_remote_to_open()) + throw new EngineError.SERVER_UNAVAILABLE("Must be synchronized with server for listing by ID"); + + assert(remote_count >= 0); + + int local_count = yield local_folder.get_email_count_async(cancellable); + + int initial_position = yield local_folder.get_id_position_async(initial_id, cancellable); + if (initial_position <= 0) { + throw new EngineError.NOT_FOUND("Email ID %s in %s not known to local store", + initial_id.to_string(), to_string); + } + + // since count can also indicate "to earliest" or "to latest", normalize + // (count is exclusive of initial_id, hence adding/substracting one, meaning that a count + // of zero or one are accepted) + int low, high; + if (count < 0) { + low = (count != int.MIN) ? (initial_position + count + 1) : 1; + high = initial_position; + } else if (count > 0) { + low = initial_position; + high = (count != int.MAX) ? (initial_position + count - 1) : remote_count; + } else { + // count == 0 + low = initial_position; + high = initial_position; + } + + int actual_count = (high - low + 1); + + debug("do_list_email_by_id_async: initial_id=%s initial_position=%d count=%d actual_count=%d low=%d high=%d local_count=%d remote_count=%d", + initial_id.to_string(), initial_position, count, actual_count, low, high, local_count, + remote_count); + + yield do_list_email_async(low, actual_count, required_fields, accumulator, cb, cancellable, + local_only); + } + private async Gee.List? remote_list_email(int[] needed_by_position, Geary.Email.Field required_fields, EmailCallback? cb, Cancellable? cancellable) throws Error { // possible to call remote multiple times, wait for it to open once and go @@ -737,8 +829,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder { return email; } - public override async void remove_email_async(int position, Cancellable? cancellable = null) - throws Error { + public override async void remove_email_async(Geary.EmailIdentifier email_id, + Cancellable? cancellable = null) throws Error { if (!opened) throw new EngineError.OPEN_REQUIRED("Folder %s not opened", to_string()); diff --git a/src/engine/impl/geary-generic-imap-folder.vala b/src/engine/impl/geary-generic-imap-folder.vala index 47080d85..7a718b50 100644 --- a/src/engine/impl/geary-generic-imap-folder.vala +++ b/src/engine/impl/geary-generic-imap-folder.vala @@ -83,10 +83,12 @@ private class Geary.GenericImapFolder : Geary.EngineFolder { } if (uid_start_value < remote_properties.uid_next.value) { - Imap.UID uid_start = new Imap.UID(uid_start_value); + Geary.Imap.EmailIdentifier uid_start = new Geary.Imap.EmailIdentifier( + new Geary.Imap.UID(uid_start_value)); - Gee.List? newest = yield imap_remote_folder.list_email_uid_async( - uid_start, null, Geary.Email.Field.PROPERTIES, cancellable); + Gee.List? newest = yield imap_remote_folder.list_email_by_id_async( + uid_start, int.MAX, Geary.Email.Field.PROPERTIES, Geary.Folder.ListFlags.NONE, + cancellable); if (newest != null && newest.size > 0) { debug("saving %d newest emails in %s", newest.size, to_string()); @@ -103,8 +105,8 @@ private class Geary.GenericImapFolder : Geary.EngineFolder { // fetch email from earliest email to last to (a) remove any deletions and (b) update // any flags that may have changed - Geary.Imap.UID last_uid = new Geary.Imap.UID(local_properties.uid_next.value - 1); Geary.Imap.UID? earliest_uid = yield imap_local_folder.get_earliest_uid_async(cancellable); + int64 full_uid_count = local_properties.uid_next.value - 1 - earliest_uid.value; // if no earliest UID, that means no messages in local store, so nothing to update if (earliest_uid == null || !earliest_uid.is_valid()) { @@ -113,8 +115,21 @@ private class Geary.GenericImapFolder : Geary.EngineFolder { return true; } - Gee.List? old_local = yield imap_local_folder.list_email_uid_async(earliest_uid, - last_uid, Geary.Email.Field.PROPERTIES, cancellable); + // If no UID's, nothing to update + if (full_uid_count <= 0 || (full_uid_count > int.MAX)) { + debug("No valid UID range in local folder %s (count=%lld), nothing to update", to_string(), + full_uid_count); + + return true; + } + + Geary.Imap.EmailIdentifier earliest_id = new Geary.Imap.EmailIdentifier(earliest_uid); + int full_id_count = (int) full_uid_count; + + // Get the local emails in the range + Gee.List? old_local = yield imap_local_folder.list_email_by_id_async( + earliest_id, full_id_count, Geary.Email.Field.PROPERTIES, Geary.Folder.ListFlags.NONE, + cancellable); int local_length = (old_local != null) ? old_local.size : 0; // as before, if empty folder, nothing to update @@ -124,8 +139,10 @@ private class Geary.GenericImapFolder : Geary.EngineFolder { return true; } - Gee.List? old_remote = yield imap_remote_folder.list_email_uid_async(earliest_uid, - last_uid, Geary.Email.Field.PROPERTIES, cancellable); + // Get the remote emails in the range + Gee.List? old_remote = yield imap_remote_folder.list_email_by_id_async( + earliest_id, full_id_count, Geary.Email.Field.PROPERTIES, Geary.Folder.ListFlags.NONE, + cancellable); int remote_length = (old_remote != null) ? old_remote.size : 0; int remote_ctr = 0; @@ -164,34 +181,42 @@ private class Geary.GenericImapFolder : Geary.EngineFolder { // local's email on the server has been removed, remove locally try { - yield local_folder.remove_email_async(old_local[local_ctr].location.position, - cancellable); + yield local_folder.remove_email_async(old_local[local_ctr].id, cancellable); } catch (Error remove_err) { debug("Unable to remove discarded email from %s: %s", to_string(), remove_err.message); } + notify_message_removed(old_local[local_ctr].id); + local_ctr++; } } // add newly-discovered emails to local store + int appended = 0; for (; remote_ctr < remote_length; remote_ctr++) { try { yield local_folder.create_email_async(old_remote[remote_ctr], cancellable); + appended++; } catch (Error append_err) { debug("Unable to append new email to %s: %s", to_string(), append_err.message); } } - // remove anything left over + if (appended > 0) + notify_messages_appended(appended); + + // remove anything left over ... use local count rather than remote as we're still in a stage + // where only the local messages are available for (; local_ctr < local_length; local_ctr++) { try { - yield local_folder.remove_email_async(old_local[local_ctr].location.position, - cancellable); + yield local_folder.remove_email_async(old_local[local_ctr].id, cancellable); } catch (Error discard_err) { debug("Unable to discard email from %s: %s", to_string(), discard_err.message); } + + notify_message_removed(old_local[local_ctr].id); } return true; diff --git a/src/engine/impl/geary-local-interfaces.vala b/src/engine/impl/geary-local-interfaces.vala index b9d956a3..6317b31e 100644 --- a/src/engine/impl/geary-local-interfaces.vala +++ b/src/engine/impl/geary-local-interfaces.vala @@ -27,6 +27,21 @@ private interface Geary.LocalFolder : Object, Geary.Folder { public async abstract bool is_email_present_async(Geary.EmailIdentifier id, out Geary.Email.Field available_fields, Cancellable? cancellable = null) throws Error; + /** + * Converts an EmailIdentifier into positional addressing in the Folder. This call relies on + * the fact that when a Folder is fully opened, the local stores' tail list of messages (the + * messages located at the top of the stack, i.e. the latest ones added) are synchronized with + * the server and is gap-free, even if all the fields for those messages is not entirely + * available. + * + * Returns a positive value locating the position of the email. Other values (zero, negative) + * indicate the EmailIdentifier is unknown, which could mean the message is not associated with + * the folder, or is buried so far down the list on the remote server that it's not known + * locally (yet). + */ + public async abstract int get_id_position_async(Geary.EmailIdentifier id, Cancellable? cancellable) + throws Error; + /** * Geary allows for a single message to exist in multiple folders. This method checks if the * email is associated with this folder. It may rely on a Message-ID being present, in which diff --git a/src/engine/impl/geary-remote-interfaces.vala b/src/engine/impl/geary-remote-interfaces.vala index eaa7aacf..e0179cb8 100644 --- a/src/engine/impl/geary-remote-interfaces.vala +++ b/src/engine/impl/geary-remote-interfaces.vala @@ -16,5 +16,13 @@ private interface Geary.RemoteAccount : Object, Geary.Account { } private interface Geary.RemoteFolder : Object, Geary.Folder { + /** + * A remote folder may report *either* a message has been removed by its EmailIdentifier + * (in which case it should use "message-removed") or by its position (in which case it should + * use this signal, "message-at-removed"), but never both for the same removal. + */ + public signal void message_at_removed(int position, int total); + + protected abstract void notify_message_at_removed(int position, int total); } diff --git a/src/engine/sqlite/api/sqlite-folder.vala b/src/engine/sqlite/api/sqlite-folder.vala index f60e8b56..dc2a4a78 100644 --- a/src/engine/sqlite/api/sqlite-folder.vala +++ b/src/engine/sqlite/api/sqlite-folder.vala @@ -7,8 +7,7 @@ // TODO: This class currently deals with generic email storage as well as IMAP-specific issues; in // the future, to support other email services, will need to break this up. -private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Geary.Imap.FolderExtensions, - Geary.ReferenceSemantics { +private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Geary.ReferenceSemantics { protected int manual_ref_count { get; protected set; } private ImapDatabase db; @@ -79,6 +78,23 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea return yield location_table.fetch_count_for_folder_async(null, folder_row.id, cancellable); } + public async int get_id_position_async(Geary.EmailIdentifier id, Cancellable? cancellable) + throws Error { + check_open(); + + Transaction transaction = yield db.begin_transaction_async("Folder.get_id_position_async", + cancellable); + + int64 message_id; + if (!yield location_table.does_ordering_exist_async(transaction, folder_row.id, + id.ordering, out message_id, cancellable)) { + return -1; + } + + return yield location_table.fetch_message_position_async(transaction, message_id, folder_row.id, + cancellable); + } + public override async void create_email_async(Geary.Email email, Cancellable? cancellable = null) throws Error { yield atomic_create_email_async(null, email, cancellable); @@ -88,8 +104,6 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea Cancellable? cancellable) throws Error { check_open(); - Geary.Imap.EmailIdentifier id = (Geary.Imap.EmailIdentifier) email.id; - Transaction transaction = supplied_transaction ?? yield db.begin_transaction_async( "Folder.atomic_create_email_async", cancellable); @@ -97,9 +111,9 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea // not account-wide) int64 message_id; if (yield location_table.does_ordering_exist_async(transaction, folder_row.id, - email.location.ordering, out message_id, cancellable)) { - throw new EngineError.ALREADY_EXISTS("Email with UID %s already exists in %s", - id.uid.to_string(), to_string()); + email.id.ordering, out message_id, cancellable)) { + throw new EngineError.ALREADY_EXISTS("Email with ID %s already exists in %s", + email.id.to_string(), to_string()); } // TODO: Also check by Message-ID (and perhaps other EmailProperties) to link an existing @@ -108,10 +122,9 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea message_id = yield message_table.create_async(transaction, new MessageRow.from_email(message_table, email), cancellable); - // create the message location in the location lookup table using its UID for the ordering - // (which fulfills the requirements for the ordering column) + // create the message location in the location lookup table MessageLocationRow location_row = new MessageLocationRow(location_table, Row.INVALID_ID, - message_id, folder_row.id, email.location.ordering, email.location.position); + message_id, folder_row.id, email.id.ordering, email.position); yield location_table.create_async(transaction, location_row, cancellable); // only write out the IMAP email properties if they're supplied and there's something to @@ -166,16 +179,37 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea return yield list_email(transaction, list, required_fields, cancellable); } - public async Gee.List? list_email_uid_async(Geary.Imap.UID? low, Geary.Imap.UID? high, - Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error { + public override async Gee.List? list_email_by_id_async(Geary.EmailIdentifier initial_id, + int count, Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, + Cancellable? cancellable = null) throws Error { + if (count == 0) { + Geary.Email email = yield fetch_email_async(initial_id, required_fields, cancellable); + + Gee.List singleton = new Gee.ArrayList(); + singleton.add(email); + + return singleton; + } + check_open(); - Transaction transaction = yield db.begin_transaction_async("Folder.list_email_uid_async", + Geary.Imap.UID uid = ((Geary.Imap.EmailIdentifier) initial_id).uid; + + Transaction transaction = yield db.begin_transaction_async("Folder.list_email_by_id_async", cancellable); + int64 low, high; + if (count < 0) { + high = uid.value; + low = (count != int.MIN) ? (high + count).clamp(1, uint32.MAX) : -1; + } else { + // count > 0 + low = uid.value; + high = (count != int.MAX) ? (low + count).clamp(1, uint32.MAX) : -1; + } + Gee.List? list = yield location_table.list_ordering_async(transaction, - folder_row.id,(low != null) ? low.value : 1, (high != null) ? high.value : -1, - cancellable); + folder_row.id, low, high, cancellable); return yield list_email(transaction, list, required_fields, cancellable); } @@ -219,9 +253,7 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea continue; } - Geary.Email email = message_row.to_email( - new Geary.Imap.EmailLocation(this, position, uid), - new Geary.Imap.EmailIdentifier(uid)); + Geary.Email email = message_row.to_email(position, new Geary.Imap.EmailIdentifier(uid)); if (properties != null) email.set_email_properties(properties.get_imap_email_properties()); @@ -279,7 +311,7 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea id.to_string(), to_string()); } - Geary.Email email = message_row.to_email(new Geary.Imap.EmailLocation(this, position, uid), id); + Geary.Email email = message_row.to_email(position, id); if (properties != null) email.set_email_properties(properties.get_imap_email_properties()); @@ -295,7 +327,7 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea return (ordering >= 1) ? new Geary.Imap.UID(ordering) : null; } - public override async void remove_email_async(int position, Cancellable? cancellable = null) + public override async void remove_email_async(Geary.EmailIdentifier id, Cancellable? cancellable = null) throws Error { check_open(); @@ -306,17 +338,15 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea // (since it may be located in multiple folders). This means at some point in the future // a vacuum will be required to remove emails that are completely unassociated with the // account - if (!yield location_table.remove_by_position_async(transaction, folder_row.id, position, cancellable)) { - throw new EngineError.NOT_FOUND("Message #%d in local store of %s not found", position, + if (!yield location_table.remove_by_ordering_async(transaction, folder_row.id, id.ordering, + cancellable)) { + throw new EngineError.NOT_FOUND("Message %s in local store of %s not found", id.to_string(), to_string()); } - int count = yield location_table.fetch_count_for_folder_async(transaction, folder_row.id, - cancellable); - yield transaction.commit_async(cancellable); - notify_message_removed(position, count); + notify_message_removed(id); } public async bool is_email_present_async(Geary.EmailIdentifier id, out Geary.Email.Field available_fields, @@ -352,9 +382,6 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea Cancellable? cancellable = null) throws Error { check_open(); - Geary.Imap.EmailLocation location = (Geary.Imap.EmailLocation) email.location; - Geary.Imap.EmailIdentifier id = (Geary.Imap.EmailIdentifier) email.id; - Transaction transaction = yield db.begin_transaction_async("Folder.update_email_async", cancellable); @@ -362,7 +389,7 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea // a message_id that can be used for a merge; note that this works without a Message-ID) int64 message_id; bool associated = yield location_table.does_ordering_exist_async(transaction, folder_row.id, - id.uid.value, out message_id, cancellable); + email.id.ordering, out message_id, cancellable); // If working around the lack of a Message-ID and not associated with this folder, treat // this operation as a create; otherwise, since a folder-association is determined, do @@ -413,15 +440,15 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea if (!associated) { // see if an email exists at this position MessageLocationRow? location_row = yield location_table.fetch_async(transaction, - folder_row.id, location.position, cancellable); + folder_row.id, email.position, cancellable); if (location_row != null) { throw new EngineError.ALREADY_EXISTS("Email already exists at position %d in %s", - email.location.position, to_string()); + email.position, to_string()); } // insert email at supplied position location_row = new MessageLocationRow(location_table, Row.INVALID_ID, message_id, - folder_row.id, id.uid.value, location.position); + folder_row.id, email.id.ordering, email.position); yield location_table.create_async(transaction, location_row, cancellable); } diff --git a/src/engine/sqlite/email/sqlite-message-location-table.vala b/src/engine/sqlite/email/sqlite-message-location-table.vala index 80adb259..f7a55c29 100644 --- a/src/engine/sqlite/email/sqlite-message-location-table.vala +++ b/src/engine/sqlite/email/sqlite-message-location-table.vala @@ -226,6 +226,30 @@ public class Geary.Sqlite.MessageLocationTable : Geary.Sqlite.Table { return -1; } + public async int fetch_message_position_async(Transaction? transaction, int64 message_id, + int64 folder_id, Cancellable? cancellable) throws Error { + Transaction locked = yield obtain_lock_async(transaction, + "MessageLocationTable.fetch_message_position_async", cancellable); + + SQLHeavy.Query query = locked.prepare( + "SELECT message_id FROM MessageLocationTable WHERE folder_id=? ORDER BY ordering"); + query.bind_int64(0, folder_id); + + SQLHeavy.QueryResult results = yield query.execute_async(cancellable); + + int position = 1; + while (!results.finished) { + if (results.fetch_int64(0) == message_id) + return position; + + yield results.next_async(cancellable); + position++; + } + + // not found + return -1; + } + public async int fetch_count_for_folder_async(Transaction? transaction, int64 folder_id, Cancellable? cancellable) throws Error { Transaction locked = yield obtain_lock_async(transaction, @@ -278,33 +302,26 @@ public class Geary.Sqlite.MessageLocationTable : Geary.Sqlite.Table { return (!result.finished) ? result.fetch_int64(0) : -1; } - public async bool remove_by_position_async(Transaction? transaction, int64 folder_id, - int position, Cancellable? cancellable) throws Error { - assert(position >= 1); - + public async bool remove_by_ordering_async(Transaction? transaction, int64 folder_id, + int64 ordering, Cancellable? cancellable) throws Error { Transaction locked = yield obtain_lock_async(transaction, - "MessageLocationTable.remove_by_position_async", cancellable); + "MessageLocationTable.remove_by_ordering_async", cancellable); SQLHeavy.Query query = locked.prepare( - "SELECT id FROM MessageLocationTable WHERE folder_id = ? ORDER BY ordering LIMIT 1 OFFSET ?"); + "SELECT id FROM MessageLocationTable WHERE folder_id=? AND ordering=?"); query.bind_int64(0, folder_id); - query.bind_int(1, position - 1); + query.bind_int64(1, ordering); SQLHeavy.QueryResult results = yield query.execute_async(cancellable); if (results.finished) return false; - query = locked.prepare( - "DELETE FROM MessageLocationTable WHERE id = ?"); + query = locked.prepare("DELETE FROM MessageLocationTable WHERE id=?"); query.bind_int64(0, results.fetch_int(0)); yield query.execute_async(cancellable); locked.set_commit_required(); - // only commit if performing our own transaction - if (transaction == null) - yield locked.commit_async(cancellable); - yield release_lock_async(transaction, locked, cancellable); return true; diff --git a/src/engine/sqlite/email/sqlite-message-row.vala b/src/engine/sqlite/email/sqlite-message-row.vala index 167bc774..5c3aec78 100644 --- a/src/engine/sqlite/email/sqlite-message-row.vala +++ b/src/engine/sqlite/email/sqlite-message-row.vala @@ -82,8 +82,8 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row { body = fetch_string_for(result, MessageTable.Column.BODY); } - public Geary.Email to_email(Geary.EmailLocation location, Geary.EmailIdentifier id) throws Error { - Geary.Email email = new Geary.Email(location, id); + public Geary.Email to_email(int position, Geary.EmailIdentifier id) throws Error { + Geary.Email email = new Geary.Email(position, id); if (((fields & Geary.Email.Field.DATE) != 0) && (date != null)) email.set_send_date(new RFC822.Date(date)); diff --git a/src/wscript b/src/wscript index 83c49141..ab73d11f 100644 --- a/src/wscript +++ b/src/wscript @@ -22,7 +22,6 @@ def build(bld): '../engine/api/geary-conversations.vala', '../engine/api/geary-credentials.vala', '../engine/api/geary-email-identifier.vala', - '../engine/api/geary-email-location.vala', '../engine/api/geary-email-properties.vala', '../engine/api/geary-email.vala', '../engine/api/geary-endpoint.vala', @@ -38,9 +37,7 @@ def build(bld): '../engine/imap/api/imap-account.vala', '../engine/imap/api/imap-email-identifier.vala', - '../engine/imap/api/imap-email-location.vala', '../engine/imap/api/imap-email-properties.vala', - '../engine/imap/api/imap-folder-extensions.vala', '../engine/imap/api/imap-folder-properties.vala', '../engine/imap/api/imap-folder.vala', '../engine/imap/command/imap-command-response.vala',