diff --git a/Makefile b/Makefile index 45792bef..aec577ad 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,8 @@ ENGINE_SRC := \ src/engine/api/FolderProperties.vala \ src/engine/api/Credentials.vala \ src/engine/api/EngineError.vala \ + src/engine/api/RemoteInterfaces.vala \ + src/engine/api/LocalInterfaces.vala \ src/engine/sqlite/Database.vala \ src/engine/sqlite/Table.vala \ src/engine/sqlite/Row.vala \ diff --git a/sql/Create.sql b/sql/Create.sql index 94aadcd5..80191bd5 100644 --- a/sql/Create.sql +++ b/sql/Create.sql @@ -18,6 +18,7 @@ CREATE INDEX FolderTableParentIndex ON FolderTable(parent_id); CREATE TABLE MessageTable ( id INTEGER PRIMARY KEY, + fields INTEGER, date_field TEXT, date_time_t INTEGER, diff --git a/src/client/ui/MessageListStore.vala b/src/client/ui/MessageListStore.vala index ef7d13e7..970d67be 100644 --- a/src/client/ui/MessageListStore.vala +++ b/src/client/ui/MessageListStore.vala @@ -55,6 +55,8 @@ public class MessageListStore : Gtk.TreeStore { } // The Email should've been fetched with Geary.Email.Field.ENVELOPE, at least. + // + // TODO: Need to insert email's in their proper position, not merely append. public void append_envelope(Geary.Email envelope) { Gtk.TreeIter iter; append(out iter, null); diff --git a/src/engine/EngineFolder.vala b/src/engine/EngineFolder.vala index 00702224..1c4a5994 100644 --- a/src/engine/EngineFolder.vala +++ b/src/engine/EngineFolder.vala @@ -5,13 +5,13 @@ */ private class Geary.EngineFolder : Object, Geary.Folder { - private NetworkAccount net; + private RemoteAccount remote; private LocalAccount local; - private Geary.Folder local_folder; - private Geary.Folder net_folder; + private RemoteFolder remote_folder; + private LocalFolder local_folder; - public EngineFolder(NetworkAccount net, LocalAccount local, Geary.Folder local_folder) { - this.net = net; + public EngineFolder(RemoteAccount remote, LocalAccount local, LocalFolder local_folder) { + this.remote = remote; this.local = local; this.local_folder = local_folder; @@ -30,152 +30,222 @@ private class Geary.EngineFolder : Object, Geary.Folder { return null; } - public async void create_email_async(Geary.Email email, Geary.Email.Field fields, - Cancellable? cancellable) throws Error { + public async void create_email_async(Geary.Email email, Cancellable? cancellable) throws Error { throw new EngineError.READONLY("Engine currently read-only"); } public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { - if (net_folder == null) { - net_folder = yield net.fetch_folder_async(null, local_folder.get_name(), cancellable); - net_folder.updated.connect(on_net_updated); + yield local_folder.open_async(readonly, cancellable); + + if (remote_folder == null) { + remote_folder = (RemoteFolder) yield remote.fetch_folder_async(null, local_folder.get_name(), + cancellable); + remote_folder.updated.connect(on_remote_updated); } - yield net_folder.open_async(readonly, cancellable); + yield remote_folder.open_async(readonly, cancellable); + + notify_opened(); } public async void close_async(Cancellable? cancellable = null) throws Error { - if (net_folder != null) { - net_folder.updated.disconnect(on_net_updated); - yield net_folder.close_async(cancellable); - } + yield local_folder.close_async(cancellable); - net_folder = null; + if (remote_folder != null) { + remote_folder.updated.disconnect(on_remote_updated); + yield remote_folder.close_async(cancellable); + remote_folder = null; + + notify_closed(CloseReason.FOLDER_CLOSED); + } } - public int get_message_count() throws Error { + public async int get_email_count(Cancellable? cancellable = null) throws Error { + // TODO return 0; } - public async Gee.List? list_email_async(int low, int count, Geary.Email.Field fields, - Cancellable? cancellable = null) throws Error { + public async Gee.List? list_email_async(int low, int count, + Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error { assert(low >= 1); assert(count >= 0); if (count == 0) return null; - Gee.List? local_list = yield local_folder.list_email_async(low, count, fields, - cancellable); + Gee.List? local_list = yield local_folder.list_email_async(low, count, + required_fields, cancellable); int local_list_size = (local_list != null) ? local_list.size : 0; debug("local list found %d", local_list_size); - if (net_folder != null && local_list_size != count) { - // go through the positions from (low) to (low + count) and see if they're not already - // present in local_list; whatever isn't present needs to be fetched - int[] needed_by_position = new int[0]; - int position = low; - for (int index = 0; (index < count) && (position <= (low + count - 1)); position++) { - while ((index < local_list_size) && (local_list[index].location.position < position)) - index++; - - if (index >= local_list_size || local_list[index].location.position != position) - needed_by_position += position; - } + if (remote_folder == null || local_list_size == count) + return local_list; + + // go through the positions from (low) to (low + count) and see if they're not already + // present in local_list; whatever isn't present needs to be fetched + int[] needed_by_position = new int[0]; + int position = low; + for (int index = 0; (index < count) && (position <= (low + count - 1)); position++) { + while ((index < local_list_size) && (local_list[index].location.position < position)) + index++; - if (needed_by_position.length != 0) - background_update_email_list.begin(needed_by_position, fields, cancellable); + if (index >= local_list_size || local_list[index].location.position != position) + needed_by_position += position; } - return local_list; + if (needed_by_position.length == 0) + return local_list; + + Gee.List? remote_list = yield remote_list_email(needed_by_position, + required_fields, cancellable); + + return combine_lists(local_list, remote_list); } public async Gee.List? list_email_sparse_async(int[] by_position, - Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { + Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error { if (by_position.length == 0) return null; Gee.List? local_list = yield local_folder.list_email_sparse_async(by_position, - fields, cancellable); + required_fields, cancellable); int local_list_size = (local_list != null) ? local_list.size : 0; - if (net_folder != null && local_list_size != by_position.length) { - // go through the list looking for anything not already in the sparse by_position list - // to fetch from the server; since by_position is not guaranteed to be sorted, the local - // list needs to be searched each iteration. - // - // TODO: Optimize this, especially if large lists/sparse sets are supplied - int[] needed_by_position = new int[0]; - foreach (int position in by_position) { - bool found = false; - if (local_list != null) { - foreach (Geary.Email email in local_list) { - if (email.location.position == position) { - found = true; - - break; - } + if (remote_folder == null || local_list_size == by_position.length) + return local_list; + + // go through the list looking for anything not already in the sparse by_position list + // to fetch from the server; since by_position is not guaranteed to be sorted, the local + // list needs to be searched each iteration. + // + // TODO: Optimize this, especially if large lists/sparse sets are supplied + int[] needed_by_position = new int[0]; + foreach (int position in by_position) { + bool found = false; + if (local_list != null) { + foreach (Geary.Email email in local_list) { + if (email.location.position == position) { + found = true; + + break; } } - - if (!found) - needed_by_position += position; } - if (needed_by_position.length != 0) - background_update_email_list.begin(needed_by_position, fields, cancellable); + if (!found) + needed_by_position += position; } - return local_list; + if (needed_by_position.length == 0) + return local_list; + + Gee.List? remote_list = yield remote_list_email(needed_by_position, + required_fields, cancellable); + + return combine_lists(local_list, remote_list); } - private async void background_update_email_list(int[] needed_by_position, Geary.Email.Field fields, - Cancellable? cancellable) { + private async Gee.List? remote_list_email(int[] needed_by_position, + Geary.Email.Field required_fields, Cancellable? cancellable) throws Error { debug("Background fetching %d emails for %s", needed_by_position.length, get_name()); - Gee.List? net_list = null; - try { - net_list = yield net_folder.list_email_sparse_async(needed_by_position, fields, - cancellable); - } catch (Error net_err) { - message("Unable to fetch emails from server: %s", net_err.message); - - if (net_err is IOError.CANCELLED) - return; - } + Gee.List? remote_list = yield remote_folder.list_email_sparse_async( + needed_by_position, required_fields, cancellable); - if (net_list != null && net_list.size == 0) - net_list = null; + if (remote_list != null && remote_list.size == 0) + remote_list = null; - if (net_list != null) - notify_email_added_removed(net_list, null); - - if (net_list != null) { - foreach (Geary.Email email in net_list) { - try { - yield local_folder.create_email_async(email, fields, cancellable); - } catch (Error local_err) { - message("Unable to create email in local store: %s", local_err.message); - - if (local_err is IOError.CANCELLED) - return; + // if any were fetched, store locally + // TODO: Bulk writing + if (remote_list != null) { + foreach (Geary.Email email in remote_list) { + bool exists_in_system = false; + if (email.message_id != null) { + int count; + exists_in_system = yield local.has_message_id_async(email.message_id, out count, + cancellable); + } + + bool exists_in_folder = yield local_folder.is_email_associated_async(email, + cancellable); + + // NOTE: Although this looks redundant, this is a complex decision case and laying + // it out like this helps explain the logic. Also, this code relies on the fact + // that update_email_async() is a powerful call which might be broken down in the + // future (requiring a duplicate email be manually associated with the folder, + // for example), and so would like to keep this around to facilitate that. + if (!exists_in_system && !exists_in_folder) { + // This case indicates the email is new to the local store OR has no + // Message-ID and so a new copy must be stored. + yield local_folder.create_email_async(email, cancellable); + } else if (exists_in_system && !exists_in_folder) { + // This case indicates the email has been (partially) stored previously but + // was not associated with this folder; update it (which implies association) + yield local_folder.update_email_async(email, false, cancellable); + } else if (!exists_in_system && exists_in_folder) { + // This case indicates the message doesn't have a Message-ID and can only be + // identified by a folder-specific ID, so it can be updated in the folder + // (This may result in multiple copies of the message stored locally.) + yield local_folder.update_email_async(email, true, cancellable); + } else if (exists_in_system && exists_in_folder) { + // This indicates the message is in the local store and was previously + // associated with this folder, so merely update the local store + yield local_folder.update_email_async(email, false, cancellable); } } } + + return remote_list; } public async Geary.Email fetch_email_async(int num, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { - if (net_folder == null) + if (remote_folder == null) throw new EngineError.OPEN_REQUIRED("Folder %s not opened", get_name()); - return yield net_folder.fetch_email_async(num, fields, cancellable); + try { + return yield local_folder.fetch_email_async(num, fields, cancellable); + } catch (Error err) { + // TODO: Better parsing of error; currently merely falling through and trying network + // for copy + debug("Unable to fetch email from local store: %s", err.message); + } + + // To reach here indicates either the local version does not have all the requested fields + // or it's simply not present. If it's not present, want to ensure that the Message-ID + // is requested, as that's a good way to manage duplicate messages in the system + Geary.Email.Field available_fields; + bool is_present = yield local_folder.is_email_present_at(num, out available_fields, cancellable); + if (!is_present) + fields = fields.set(Geary.Email.Field.REFERENCES); + + // fetch from network + Geary.Email email = yield remote_folder.fetch_email_async(num, fields, cancellable); + + // save to local store + yield local_folder.update_email_async(email, false, cancellable); + + return email; } private void on_local_updated() { } - private void on_net_updated() { + private void on_remote_updated() { + } + + private Gee.List? combine_lists(Gee.List? a, Gee.List? b) { + if (a == null) + return b; + + if (b == null) + return a; + + Gee.List combined = new Gee.ArrayList(); + combined.add_all(a); + combined.add_all(b); + + return combined; } } diff --git a/src/engine/ImapEngine.vala b/src/engine/ImapEngine.vala index a1b1de98..e94c233a 100644 --- a/src/engine/ImapEngine.vala +++ b/src/engine/ImapEngine.vala @@ -5,14 +5,20 @@ */ private class Geary.ImapEngine : Object, Geary.Account { - private NetworkAccount net; + private RemoteAccount remote; private LocalAccount local; - public ImapEngine(NetworkAccount net, LocalAccount local) { - this.net = net; + public ImapEngine(RemoteAccount remote, LocalAccount local) { + this.remote = remote; this.local = local; } + public Geary.Email.Field get_required_fields_for_writing() { + // Return the more restrictive of the two, which is the NetworkAccount's. + // TODO: This could be determined at runtime rather than fixed in stone here. + return Geary.Email.Field.HEADER | Geary.Email.Field.BODY; + } + public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder, Cancellable? cancellable = null) throws Error { } @@ -27,7 +33,7 @@ private class Geary.ImapEngine : Object, Geary.Account { Gee.Collection engine_list = new Gee.ArrayList(); foreach (Geary.Folder local_folder in local_list) - engine_list.add(new EngineFolder(net, local, local_folder)); + engine_list.add(new EngineFolder(remote, local, (LocalFolder) local_folder)); background_update_folders.begin(parent, engine_list); @@ -38,8 +44,9 @@ private class Geary.ImapEngine : Object, Geary.Account { public async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name, Cancellable? cancellable = null) throws Error { - Geary.Folder local_folder = yield local.fetch_folder_async(parent, folder_name, cancellable); - Geary.Folder engine_folder = new EngineFolder(net, local, local_folder); + LocalFolder local_folder = (LocalFolder) yield local.fetch_folder_async(parent, folder_name, + cancellable); + Geary.Folder engine_folder = new EngineFolder(remote, local, local_folder); return engine_folder; } @@ -65,20 +72,20 @@ private class Geary.ImapEngine : Object, Geary.Account { private async void background_update_folders(Geary.Folder? parent, Gee.Collection engine_folders) { - Gee.Collection net_folders; + Gee.Collection remote_folders; try { - net_folders = yield net.list_folders_async(parent); - } catch (Error neterror) { - error("Unable to retrieve folder list from server: %s", neterror.message); + remote_folders = yield remote.list_folders_async(parent); + } catch (Error remote_error) { + error("Unable to retrieve folder list from server: %s", remote_error.message); } Gee.Set local_names = get_folder_names(engine_folders); - Gee.Set net_names = get_folder_names(net_folders); + Gee.Set remote_names = get_folder_names(remote_folders); - debug("%d local names, %d net names", local_names.size, net_names.size); + debug("%d local names, %d remote names", local_names.size, remote_names.size); - Gee.List? to_add = get_excluded_folders(net_folders, local_names); - Gee.List? to_remove = get_excluded_folders(engine_folders, net_names); + Gee.List? to_add = get_excluded_folders(remote_folders, local_names); + Gee.List? to_remove = get_excluded_folders(engine_folders, remote_names); debug("Adding %d, removing %d to/from local store", to_add.size, to_remove.size); @@ -98,10 +105,10 @@ private class Geary.ImapEngine : Object, Geary.Account { Gee.Collection engine_added = null; if (to_add != null) { engine_added = new Gee.ArrayList(); - foreach (Geary.Folder net_folder in to_add) { + foreach (Geary.Folder remote_folder in to_add) { try { - engine_added.add(new EngineFolder(net, local, - yield local.fetch_folder_async(parent, net_folder.get_name()))); + engine_added.add(new EngineFolder(remote, local, + (LocalFolder) yield local.fetch_folder_async(parent, remote_folder.get_name()))); } catch (Error convert_err) { error("Unable to fetch local folder: %s", convert_err.message); } diff --git a/src/engine/api/Account.vala b/src/engine/api/Account.vala index b8557e2a..12b7c997 100644 --- a/src/engine/api/Account.vala +++ b/src/engine/api/Account.vala @@ -13,6 +13,18 @@ public interface Geary.Account : Object { folders_added_removed(added, removed); } + /** + * This method returns which Geary.Email.Field fields must be available in a Geary.Email to + * write (or save or store) the message to the backing medium. Different implementations will + * have different requirements, which must be reconciled. + * + * In this case, Geary.Email.Field.NONE means "any". + * + * If a write operation is attempted on an email that does not have all these fields fulfilled, + * an EngineError.INCOMPLETE_MESSAGE will be thrown. + */ + public abstract Geary.Email.Field get_required_fields_for_writing(); + public abstract async void create_folder_async(Geary.Folder? parent, Geary.Folder folder, Cancellable? cancellable = null) throws Error; @@ -32,9 +44,3 @@ public interface Geary.Account : Object { Cancellable? cancellable = null) throws Error; } -public interface Geary.NetworkAccount : Object, Geary.Account { -} - -public interface Geary.LocalAccount : Object, Geary.Account { -} - diff --git a/src/engine/api/Email.vala b/src/engine/api/Email.vala index 320f2324..6a3f5780 100644 --- a/src/engine/api/Email.vala +++ b/src/engine/api/Email.vala @@ -5,19 +5,19 @@ */ public class Geary.Email : Object { - [Flags] + // THESE VALUES ARE PERSISTED. Change them only if you know what you're doing. public enum Field { - NONE = 0, - DATE, - ORIGINATORS, - RECEIVERS, - REFERENCES, - SUBJECT, - HEADER, - BODY, - PROPERTIES, - ENVELOPE = DATE | ORIGINATORS | RECEIVERS | REFERENCES | SUBJECT, - ALL = 0xFFFFFFFF; + NONE = 0, + DATE = 1 << 0, + ORIGINATORS = 1 << 1, + RECEIVERS = 1 << 2, + REFERENCES = 1 << 3, + SUBJECT = 1 << 4, + HEADER = 1 << 5, + BODY = 1 << 6, + PROPERTIES = 1 << 7, + ENVELOPE = DATE | ORIGINATORS | RECEIVERS | REFERENCES | SUBJECT, + ALL = 0xFFFFFFFF; public static Field[] all() { return { @@ -31,43 +31,112 @@ public class Geary.Email : Object { PROPERTIES }; } + + public inline bool is_set(Field required_fields) { + return (this & required_fields) == required_fields; + } + + public inline Field set(Field field) { + return (this | field); + } + + public inline Field clear(Field field) { + return (this & ~(field)); + } } public Geary.EmailLocation location { get; private set; } // DATE - public Geary.RFC822.Date? date = null; + public Geary.RFC822.Date? date { get; private set; default = null; } // ORIGINATORS - public Geary.RFC822.MailboxAddresses? from = null; - public Geary.RFC822.MailboxAddresses? sender = null; - public Geary.RFC822.MailboxAddresses? reply_to = null; + public Geary.RFC822.MailboxAddresses? from { get; private set; default = null; } + public Geary.RFC822.MailboxAddresses? sender { get; private set; default = null; } + public Geary.RFC822.MailboxAddresses? reply_to { get; private set; default = null; } // RECEIVERS - public Geary.RFC822.MailboxAddresses? to = null; - public Geary.RFC822.MailboxAddresses? cc = null; - public Geary.RFC822.MailboxAddresses? bcc = null; + public Geary.RFC822.MailboxAddresses? to { get; private set; default = null; } + public Geary.RFC822.MailboxAddresses? cc { get; private set; default = null; } + public Geary.RFC822.MailboxAddresses? bcc { get; private set; default = null; } // REFERENCES - public Geary.RFC822.MessageID? message_id = null; - public Geary.RFC822.MessageID? in_reply_to = null; + public Geary.RFC822.MessageID? message_id { get; private set; default = null; } + public Geary.RFC822.MessageID? in_reply_to { get; private set; default = null; } // SUBJECT - public Geary.RFC822.Subject? subject = null; + public Geary.RFC822.Subject? subject { get; private set; default = null; } // HEADER - public RFC822.Header? header = null; + public RFC822.Header? header { get; private set; default = null; } // BODY - public RFC822.Text? body = null; + public RFC822.Text? body { get; private set; default = null; } // PROPERTIES - public Geary.EmailProperties? properties = null; + public Geary.EmailProperties? properties { get; private set; default = null; } + + public Geary.Email.Field fields { get; private set; default = Field.NONE; } public Email(Geary.EmailLocation location) { this.location = location; } + public void set_send_date(Geary.RFC822.Date date) { + this.date = date; + + fields |= Field.DATE; + } + + public void set_originators(Geary.RFC822.MailboxAddresses? from, + Geary.RFC822.MailboxAddresses? sender, Geary.RFC822.MailboxAddresses? reply_to) { + this.from = from; + this.sender = sender; + this.reply_to = reply_to; + + fields |= Field.ORIGINATORS; + } + + public void set_receivers(Geary.RFC822.MailboxAddresses? to, + Geary.RFC822.MailboxAddresses? cc, Geary.RFC822.MailboxAddresses? bcc) { + this.to = to; + this.cc = cc; + this.bcc = bcc; + + fields |= Field.RECEIVERS; + } + + public void set_references(Geary.RFC822.MessageID? message_id, Geary.RFC822.MessageID? in_reply_to) { + this.message_id = message_id; + this.in_reply_to = in_reply_to; + + fields |= Field.REFERENCES; + } + + public void set_message_subject(Geary.RFC822.Subject subject) { + this.subject = subject; + + fields |= Field.SUBJECT; + } + + public void set_message_header(Geary.RFC822.Header header) { + this.header = header; + + fields |= Field.HEADER; + } + + public void set_message_body(Geary.RFC822.Text body) { + this.body = body; + + fields |= Field.BODY; + } + + public void set_email_properties(Geary.EmailProperties properties) { + this.properties = properties; + + fields |= Field.PROPERTIES; + } + public string to_string() { StringBuilder builder = new StringBuilder(); diff --git a/src/engine/api/EngineError.vala b/src/engine/api/EngineError.vala index 90c62be0..2abf47a9 100644 --- a/src/engine/api/EngineError.vala +++ b/src/engine/api/EngineError.vala @@ -7,8 +7,10 @@ public errordomain Geary.EngineError { OPEN_REQUIRED, ALREADY_OPEN, + ALREADY_EXISTS, NOT_FOUND, READONLY, - BAD_PARAMETERS + BAD_PARAMETERS, + INCOMPLETE_MESSAGE } diff --git a/src/engine/api/Folder.vala b/src/engine/api/Folder.vala index 7e199bdb..50065c25 100644 --- a/src/engine/api/Folder.vala +++ b/src/engine/api/Folder.vala @@ -11,28 +11,71 @@ public interface Geary.Folder : Object { FOLDER_CLOSED } + /** + * This is fired when the Folder is successfully opened by a caller. It will only fire once + * until the Folder is closed. + */ public signal void opened(); + /** + * This is fired when the Folder is successfully closed by a caller. It will only fire once + * until the Folder is re-opened. + * + * The CloseReason enum can be used to inspect why the folder was closed: the connection was + * broken locally or remotely, or the Folder was simply closed (and the underlying connection + * is still available). + */ 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. + */ public signal void email_added_removed(Gee.List? added, Gee.List? removed); + /** + * TBD. + */ public signal void updated(); - public virtual void notify_opened() { + /** + * 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() { opened(); } - public virtual 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_closed(CloseReason reason) { closed(reason); } - public virtual void notify_email_added_removed(Gee.List? added, + /** + * 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(); } @@ -41,31 +84,120 @@ public interface Geary.Folder : Object { public abstract Geary.FolderProperties? get_properties(); + /** + * The Folder must be opened before most operations may be performed on it. Depending on the + * implementation this might entail opening a network connection or setting the connection to + * a particular state, opening a file or database, and so on. + * + * If the Folder has been opened previously, EngineError.ALREADY_OPEN is thrown. There are no + * other side-effects. + */ public abstract async void open_async(bool readonly, Cancellable? cancellable = null) throws Error; + /** + * The Folder should be closed when operations on it are concluded. Depending on the + * implementation this might entail closing a network connection or reverting it to another + * state, or closing file handles or database connections. + * + * If the Folder is already closed, the method silently returns. + */ public abstract async void close_async(Cancellable? cancellable = null) throws Error; - public abstract int get_message_count() throws Error; - - public abstract async void create_email_async(Geary.Email email, - Geary.Email.Field fields, Cancellable? cancellable = null) throws Error; + /* + * Returns the number of messages in the Folder. They can be addressed by their position, + * from 1 to n. + * + * Note that this only returns the number of messages available to the backing medium. In the + * case of the local store, this might be less than the number on the network server. Folders + * created by Engine are aggregating objects and will return the true count. + * + * Also note that local folders may be sparsely populated. get_count() returns the last position + * available, but not all emails from 1 to n may be available. + * + * The Folder must be opened prior to attempting this operation. + */ + public abstract async int get_email_count(Cancellable? cancellable = null) throws Error; /** + * If the Folder object detects that the supplied Email does not have sufficient fields for + * writing it, it should throw an EngineError.INCOMPLETE_MESSAGE. Use + * get_required_fields_for_writing() to determine which fields must be present to create the + * email. + * + * This method will throw EngineError.ALREADY_EXISTS if the email already exists in the folder + * *and* the backing medium allows for checking prior to creation (which is not necessarily + * the case with network folders). Use LocalFolder.update_email_async() to update fields on + * an existing message in the local store. Saving an email on the server will be available + * later. + * + * The Folder must be opened prior to attempting this operation. + */ + public abstract async void create_email_async(Geary.Email email, Cancellable? cancellable = null) + throws Error; + + /** + * Returns a list of messages that fulfill the required_fields flags starting at the low + * position and moving up to (low + count). The list is not guaranteed to be in any + * particular order. + * + * If any position in low to (low + count) are out of range, only the email within range are + * reported. No error is thrown. This allows callers to blindly request the first n emails + * in a folder without determining the count first. + * + * Note that this only returns the emails with the required fields that are available to the + * Folder's backing medium. The local store may have fewer or incomplete messages, meaning that + * this will return an incomplete list. It is up to the caller to determine what's missing + * and take the appropriate steps. + * + * In the case of a Folder returned by Engine, it will use what's available in the local store + * and fetch from the network only what it needs, so that the caller gets a full list. + * Note that this means the call may take some time to complete. + * + * TODO: Delayed listing methods (where what's available are reported via a callback after the + * async method has completed) will be implemented in the future for more responsive behavior. + * These may be operations only available from Folders returned by Engine. + * + * The Folder must be opened prior to attempting this operation. + * * low is one-based. */ public abstract async Gee.List? list_email_async(int low, int count, - Geary.Email.Field fields, Cancellable? cancellable = null) throws Error; + Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; /** + * Like list_email_async(), but the caller passes a sparse list of email by it's ordered + * position in the folder. If any of the positions in the sparse list are out of range, + * only the emails within range are reported. The list is not guaranteed to be in any + * particular order. + * + * See the notes in list_email_async() regarding issues about local versus remote stores and + * possible future additions to the API. + * + * The Folder must be opened prior to attempting this operation. + * * All positions are one-based. */ public abstract async Gee.List? list_email_sparse_async(int[] by_position, - Geary.Email.Field fields, Cancellable? cancellable = null) throws Error; + Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; /** + * 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 + * 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(int position, Geary.Email.Field fields, + public abstract async Geary.Email fetch_email_async(int position, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; + + /** + * Used for debugging. Should not be used for user-visible labels. + */ + public virtual string to_string() { + return get_name(); + } } diff --git a/src/engine/api/LocalInterfaces.vala b/src/engine/api/LocalInterfaces.vala new file mode 100644 index 00000000..57ae5309 --- /dev/null +++ b/src/engine/api/LocalInterfaces.vala @@ -0,0 +1,71 @@ +/* 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. + */ + +public interface Geary.LocalAccount : Object, Geary.Account { + /** + * Returns true if the email (identified by its Message-ID) already exists in the account's + * local store, no matter the folder. + * + * Note that there are no guarantees of the uniqueness of a Message-ID, or even that a message + * will have one. Because of this situation the method can return the number of messages + * found with that ID. + */ + public async abstract bool has_message_id_async(Geary.RFC822.MessageID message_id, + out int count, Cancellable? cancellable = null) throws Error; +} + +public interface Geary.LocalFolder : Object, Geary.Folder { + /** + * Unlike the remote store, the local store can be sparsely populated, both by fields within + * an email and position (ordering) within the list. This checks if the email at position + * is available. If it returns true, the available_fields indicate what is stored locally. + */ + public async abstract bool is_email_present_at(int position, out Geary.Email.Field available_fields, + Cancellable? cancellable = null) 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 + * case if it's not the method will throw an EngineError.INCOMPLETE_MESSAGE. + * + * If the email is not in the local store, this method returns false. + */ + public async abstract bool is_email_associated_async(Geary.Email email, Cancellable? cancellable = null) + throws Error; + + /** + * Geary allows for a single message to exist in multiple folders. It also allows for partial + * email information to be stored and updated, building the local store as more information is + * downloaded from the server. + * + * update_email_async() updates the email's information in the local store, adding any new + * fields not already present. If the email has fields already stored, the local version *will* + * be overwritten with this new information. However, if the email has fewer fields than the + * local version, the old information will not be lost. In this sense this is a merge + * operation. + * + * update_email_async() will also attempt to associate an email existing in the system with this + * folder. If the message has folder-specific properties that identify it, those will be used; + * if not, update_email_async() will attempt to use the Message-ID. If the Message-ID is not + * available in the email, it will throw EngineError.INCOMPLETE_MESSAGE unless + * duplicate_okay is true, which confirms that it's okay to not attempt the linkage (which + * should be done if the message simply lacks a Message-ID). + * TODO: Examine other fields in the email and attempt to match it with existing messages. + * + * The EmailLocation field is used to position the email in the folder's ordering. + * If another email exists at the same EmailLocation.position, EngineError.ALREADY_EXISTS + * will be thrown. + * + * If the email does not exist in the local store OR the email has no Message-ID and + * no_incomplete_error is true OR multiple messages are found in the system with the same + * Message-ID, update_email-async() will see if there's any indication of the email being + * associated with the folder. If so, it will merge in the new information. If not, this + * method will fall-through to create_email_async(). + */ + public async abstract void update_email_async(Geary.Email email, bool duplicate_okay, + Cancellable? cancellable = null) throws Error; +} + diff --git a/src/engine/api/RemoteInterfaces.vala b/src/engine/api/RemoteInterfaces.vala new file mode 100644 index 00000000..5ea478ba --- /dev/null +++ b/src/engine/api/RemoteInterfaces.vala @@ -0,0 +1,12 @@ +/* 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. + */ + +public interface Geary.RemoteAccount : Object, Geary.Account { +} + +public interface Geary.RemoteFolder : Object, Geary.Folder { +} + diff --git a/src/engine/common/MessageData.vala b/src/engine/common/MessageData.vala index c9733e56..92a06546 100644 --- a/src/engine/common/MessageData.vala +++ b/src/engine/common/MessageData.vala @@ -54,6 +54,18 @@ public abstract class Geary.Common.LongMessageData : Geary.Common.MessageData { } } +public abstract class Geary.Common.Int64MessageData : Geary.Common.MessageData { + public int64 value { get; private set; } + + public Int64MessageData(int64 value) { + this.value = value; + } + + public override string to_string() { + return value.to_string(); + } +} + public abstract class Geary.Common.BlockMessageData : Geary.Common.MessageData { public string data_name { get; private set; } public Geary.Memory.AbstractBuffer buffer { get; private set; } diff --git a/src/engine/imap/Mailbox.vala b/src/engine/imap/Mailbox.vala index 450cc20f..55ce2e0f 100644 --- a/src/engine/imap/Mailbox.vala +++ b/src/engine/imap/Mailbox.vala @@ -49,8 +49,10 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { FetchResults[] results = FetchResults.decode(resp); foreach (FetchResults res in results) { - // TODO: Add UID - Geary.Email email = new Geary.Email(new Geary.Imap.EmailLocation(res.msg_num, 0)); + UID? uid = res.get_data(FetchDataType.UID) as UID; + assert(uid != null); + + Geary.Email email = new Geary.Email(new Geary.Imap.EmailLocation(res.msg_num, uid)); fetch_results_to_email(res, fields, email); msgs.add(email); } @@ -79,8 +81,10 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { results[0].msg_num, msg_num); } - // TODO: Add UID - Geary.Email email = new Geary.Email(new Geary.Imap.EmailLocation(results[0].msg_num, 0)); + UID? uid = results[0].get_data(FetchDataType.UID) as UID; + assert(uid != null); + + Geary.Email email = new Geary.Email(new Geary.Imap.EmailLocation(results[0].msg_num, uid)); fetch_results_to_email(results[0], fields, email); return email; @@ -132,11 +136,14 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { } assert(data_type_set.size > 0); - FetchDataType[] data_types = new FetchDataType[data_type_set.size]; + FetchDataType[] data_types = new FetchDataType[data_type_set.size + 1]; int ctr = 0; foreach (FetchDataType data_type in data_type_set) data_types[ctr++] = data_type; + // UID is always fetched, no matter what the caller requests + data_types[ctr] = FetchDataType.UID; + return data_types; } @@ -152,39 +159,31 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { Envelope envelope = (Envelope) data; if ((fields & Geary.Email.Field.DATE) != 0) - email.date = envelope.sent; + email.set_send_date(envelope.sent); if ((fields & Geary.Email.Field.SUBJECT) != 0) - email.subject = envelope.subject; + email.set_message_subject(envelope.subject); - if ((fields & Geary.Email.Field.ORIGINATORS) != 0) { - email.from = envelope.from; - email.sender = envelope.sender; - email.reply_to = envelope.reply_to; - } + if ((fields & Geary.Email.Field.ORIGINATORS) != 0) + email.set_originators(envelope.from, envelope.sender, envelope.reply_to); - if ((fields & Geary.Email.Field.RECEIVERS) != 0) { - email.to = envelope.to; - email.cc = envelope.cc; - email.bcc = envelope.bcc; - } + if ((fields & Geary.Email.Field.RECEIVERS) != 0) + email.set_receivers(envelope.to, envelope.cc, envelope.bcc); - if ((fields & Geary.Email.Field.REFERENCES) != 0) { - email.in_reply_to = envelope.in_reply_to; - email.message_id = envelope.message_id; - } + if ((fields & Geary.Email.Field.REFERENCES) != 0) + email.set_references(envelope.message_id, envelope.in_reply_to); break; case FetchDataType.RFC822_HEADER: - email.header = (RFC822.Header) data; + email.set_message_header((RFC822.Header) data); break; case FetchDataType.RFC822_TEXT: - email.body = (RFC822.Text) data; + email.set_message_body((RFC822.Text) data); break; case FetchDataType.FLAGS: - email.properties = new Imap.EmailProperties((MessageFlags) data); + email.set_email_properties(new Imap.EmailProperties((MessageFlags) data)); break; default: diff --git a/src/engine/imap/MessageData.vala b/src/engine/imap/MessageData.vala index 42cf68e6..5bdddb06 100644 --- a/src/engine/imap/MessageData.vala +++ b/src/engine/imap/MessageData.vala @@ -19,8 +19,8 @@ public interface Geary.Imap.MessageData : Geary.Common.MessageData { } -public class Geary.Imap.UID : Geary.Common.IntMessageData, Geary.Imap.MessageData { - public UID(int value) { +public class Geary.Imap.UID : Geary.Common.Int64MessageData, Geary.Imap.MessageData { + public UID(int64 value) { base (value); } } @@ -109,13 +109,13 @@ public class Geary.Imap.Envelope : Geary.Common.MessageData, Geary.Imap.MessageD public Geary.RFC822.MailboxAddresses? cc { get; private set; } public Geary.RFC822.MailboxAddresses? bcc { get; private set; } public Geary.RFC822.MessageID? in_reply_to { get; private set; } - public Geary.RFC822.MessageID message_id { get; private set; } + public Geary.RFC822.MessageID? message_id { get; private set; } public Envelope(Geary.RFC822.Date sent, Geary.RFC822.Subject subject, Geary.RFC822.MailboxAddresses from, Geary.RFC822.MailboxAddresses sender, Geary.RFC822.MailboxAddresses? reply_to, Geary.RFC822.MailboxAddresses? to, Geary.RFC822.MailboxAddresses? cc, Geary.RFC822.MailboxAddresses? bcc, - Geary.RFC822.MessageID? in_reply_to, Geary.RFC822.MessageID message_id) { + Geary.RFC822.MessageID? in_reply_to, Geary.RFC822.MessageID? message_id) { this.sent = sent; this.subject = subject; this.from = from; diff --git a/src/engine/imap/api/Account.vala b/src/engine/imap/api/Account.vala index 702fd9d2..44aba97c 100644 --- a/src/engine/imap/api/Account.vala +++ b/src/engine/imap/api/Account.vala @@ -4,13 +4,17 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Imap.Account : Object, Geary.Account, Geary.NetworkAccount { +public class Geary.Imap.Account : Object, Geary.Account, Geary.RemoteAccount { private ClientSessionManager session_mgr; public Account(Credentials cred, uint default_port) { session_mgr = new ClientSessionManager(cred, default_port); } + public Geary.Email.Field get_required_fields_for_writing() { + return Geary.Email.Field.HEADER | Geary.Email.Field.BODY; + } + public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder, Cancellable? cancellable = null) throws Error { throw new EngineError.READONLY("IMAP readonly"); diff --git a/src/engine/imap/api/EmailLocation.vala b/src/engine/imap/api/EmailLocation.vala index 35026078..18436a3d 100644 --- a/src/engine/imap/api/EmailLocation.vala +++ b/src/engine/imap/api/EmailLocation.vala @@ -4,10 +4,15 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ +// +// The EmailLocation for any message originating from an ImapAccount is guaranteed to have its +// UID attached to it, whether or not it was requested in the FETCH operation. +// + private class Geary.Imap.EmailLocation : Geary.EmailLocation { - public int64 uid { get; private set; } + public Geary.Imap.UID uid { get; private set; } - public EmailLocation(int position, int64 uid) { + public EmailLocation(int position, Geary.Imap.UID uid) { base (position); this.uid = uid; diff --git a/src/engine/imap/api/Folder.vala b/src/engine/imap/api/Folder.vala index b5ec4d98..1c5382e1 100644 --- a/src/engine/imap/api/Folder.vala +++ b/src/engine/imap/api/Folder.vala @@ -4,7 +4,7 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Imap.Folder : Object, Geary.Folder { +public class Geary.Imap.Folder : Object, Geary.Folder, Geary.RemoteFolder { private ClientSessionManager session_mgr; private MailboxInformation info; private string name; @@ -42,23 +42,32 @@ public class Geary.Imap.Folder : Object, Geary.Folder { this.readonly = Trillian.from_boolean(readonly); properties.uid_validity = mailbox.uid_validity; + + notify_opened(); } public async void close_async(Cancellable? cancellable = null) throws Error { + if (mailbox == null) + return; + mailbox = null; readonly = Trillian.UNKNOWN; properties.uid_validity = null; + + notify_closed(CloseReason.FOLDER_CLOSED); } - public int get_message_count() throws Error { + public async int get_email_count(Cancellable? cancellable = null) throws Error { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); return mailbox.count; } - public async void create_email_async(Geary.Email email, Geary.Email.Field fields, - Cancellable? cancellable = null) throws Error { + public async void create_email_async(Geary.Email email, Cancellable? cancellable = null) throws Error { + if (mailbox == null) + throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); + throw new EngineError.READONLY("IMAP currently read-only"); } @@ -83,11 +92,9 @@ public class Geary.Imap.Folder : Object, Geary.Folder { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); + // TODO: If position out of range, throw EngineError.NOT_FOUND + return yield mailbox.fetch_async(position, fields, cancellable); } - - public string to_string() { - return name; - } } diff --git a/src/engine/imap/decoders/FetchDataDecoder.vala b/src/engine/imap/decoders/FetchDataDecoder.vala index 43d93a78..fd931be3 100644 --- a/src/engine/imap/decoders/FetchDataDecoder.vala +++ b/src/engine/imap/decoders/FetchDataDecoder.vala @@ -115,7 +115,12 @@ public class Geary.Imap.EnvelopeDecoder : Geary.Imap.FetchDataDecoder { ListParameter? cc = listp.get_as_nullable_list(6); ListParameter? bcc = listp.get_as_nullable_list(7); StringParameter? in_reply_to = listp.get_as_nullable_string(8); - StringParameter message_id = listp.get_as_string(9); + StringParameter? message_id = listp.get_as_string(9); + + // Although Message-ID is required to be returned by IMAP, it may be blank if the email + // does not supply it (optional according to RFC822); deal with this cognitive dissonance + if (String.is_empty(message_id.value)) + message_id = null; return new Envelope(new Geary.RFC822.Date(sent.value), new Geary.RFC822.Subject(subject.value), @@ -124,7 +129,7 @@ public class Geary.Imap.EnvelopeDecoder : Geary.Imap.FetchDataDecoder { (cc != null) ? parse_addresses(cc) : null, (bcc != null) ? parse_addresses(bcc) : null, (in_reply_to != null) ? new Geary.RFC822.MessageID(in_reply_to.value) : null, - new Geary.RFC822.MessageID(message_id.value)); + (message_id != null) ? new Geary.RFC822.MessageID(message_id.value) : null); } // TODO: This doesn't handle group lists (see Johnson, p.268) -- this will throw an diff --git a/src/engine/sqlite/ImapMessageLocationPropertiesRow.vala b/src/engine/sqlite/ImapMessageLocationPropertiesRow.vala index 39c22bf2..74cfac6c 100644 --- a/src/engine/sqlite/ImapMessageLocationPropertiesRow.vala +++ b/src/engine/sqlite/ImapMessageLocationPropertiesRow.vala @@ -7,10 +7,10 @@ public class Geary.Sqlite.ImapMessageLocationPropertiesRow : Geary.Sqlite.Row { public int64 id { get; private set; } public int64 location_id { get; private set; } - public int64 uid { get; private set; } + public Geary.Imap.UID uid { get; private set; } public ImapMessageLocationPropertiesRow(ImapMessageLocationPropertiesTable table, int64 id, - int64 location_id, int64 uid) { + int64 location_id, Geary.Imap.UID uid) { base (table); this.id = id; diff --git a/src/engine/sqlite/ImapMessageLocationPropertiesTable.vala b/src/engine/sqlite/ImapMessageLocationPropertiesTable.vala index 1cf2477f..79d667a2 100644 --- a/src/engine/sqlite/ImapMessageLocationPropertiesTable.vala +++ b/src/engine/sqlite/ImapMessageLocationPropertiesTable.vala @@ -21,7 +21,7 @@ public class Geary.Sqlite.ImapMessageLocationPropertiesTable : Geary.Sqlite.Tabl SQLHeavy.Query query = db.prepare( "INSERT INTO ImapMessageLocationPropertiesTable (location_id, uid) VALUES (?, ?)"); query.bind_int64(0, row.location_id); - query.bind_int64(1, row.uid); + query.bind_int64(1, row.uid.value); return yield query.execute_insert_async(cancellable); } @@ -37,7 +37,29 @@ public class Geary.Sqlite.ImapMessageLocationPropertiesTable : Geary.Sqlite.Tabl return null; return new ImapMessageLocationPropertiesRow(this, result.fetch_int64(0), location_id, - result.fetch_int64(1)); + new Geary.Imap.UID(result.fetch_int64(1))); + } + + public async bool search_uid_in_folder(Geary.Imap.UID uid, int64 folder_id, + out int64 message_id, Cancellable? cancellable = null) throws Error { + message_id = Row.INVALID_ID; + + SQLHeavy.Query query = db.prepare( + "SELECT MessageLocationTable.message_id " + + "FROM ImapMessageLocationPropertiesTable " + + "INNER JOIN MessageLocationTable " + + "WHERE MessageLocationTable.folder_id=? " + + "AND ImapMessageLocationPropertiesTable.location_id=MessageLocationTable.id " + + "AND ImapMessageLocationPropertiesTable.uid=?"); + query.bind_int64(0, folder_id); + query.bind_int64(1, uid.value); + + SQLHeavy.QueryResult result = yield query.execute_async(cancellable); + + if (!result.finished) + message_id = result.fetch_int64(0); + + return !result.finished; } } diff --git a/src/engine/sqlite/MessageRow.vala b/src/engine/sqlite/MessageRow.vala index 660db35c..e7c687f5 100644 --- a/src/engine/sqlite/MessageRow.vala +++ b/src/engine/sqlite/MessageRow.vala @@ -6,6 +6,7 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row { public int64 id { get; set; default = INVALID_ID; } + public Geary.Email.Field fields { get; set; default = Geary.Email.Field.NONE; } public string? date { get; set; } public time_t date_time_t { get; set; default = -1; } @@ -34,33 +35,19 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row { public MessageRow.from_email(MessageTable table, Geary.Email email) { base (table); - date = (email.date != null) ? email.date.original : null; - date_time_t = (email.date != null) ? email.date.as_time_t : -1; - - from = flatten_addresses(email.from); - sender = flatten_addresses(email.sender); - reply_to = flatten_addresses(email.reply_to); - - to = flatten_addresses(email.to); - cc = flatten_addresses(email.cc); - bcc = flatten_addresses(email.bcc); - - message_id = (email.message_id != null) ? email.message_id.value : null; - in_reply_to = (email.in_reply_to != null) ? email.in_reply_to.value : null; - - subject = (email.subject != null) ? email.subject.value : null; - - header = (email.header != null) ? email.header.buffer.to_ascii_string() : null; - - body = (email.body != null) ? email.body.buffer.to_ascii_string() : null; + set_from_email(email.fields, email); } - public MessageRow.from_query_result(Table table, Geary.Email.Field fields, SQLHeavy.QueryResult result) - throws Error { + public MessageRow.from_query_result(Table table, Geary.Email.Field requested_fields, + SQLHeavy.QueryResult result) throws Error { base (table); id = fetch_int64_for(result, MessageTable.Column.ID); + // the available fields are an intersection of what's available in the database and + // what was requested + fields = requested_fields & fetch_int_for(result, MessageTable.Column.FIELDS); + if ((fields & Geary.Email.Field.DATE) != 0) { date = fetch_string_for(result, MessageTable.Column.DATE_FIELD); date_time_t = (time_t) fetch_int64_for(result, MessageTable.Column.DATE_INT64); @@ -96,31 +83,46 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row { public Geary.Email to_email(Geary.EmailLocation location) throws Error { Geary.Email email = new Geary.Email(location); - email.date = (date != null) ? new RFC822.Date(date) : null; + if (((fields & Geary.Email.Field.DATE) != 0) && (date != null)) + email.set_send_date(new RFC822.Date(date)); - email.from = unflatten_addresses(from); - email.sender = unflatten_addresses(sender); - email.reply_to = unflatten_addresses(reply_to); + if ((fields & Geary.Email.Field.ORIGINATORS) != 0) { + email.set_originators(unflatten_addresses(from), unflatten_addresses(sender), + unflatten_addresses(reply_to)); + } - email.to = unflatten_addresses(to); - email.cc = unflatten_addresses(cc); - email.bcc = unflatten_addresses(bcc); + if ((fields & Geary.Email.Field.RECEIVERS) != 0) { + email.set_receivers(unflatten_addresses(to), unflatten_addresses(cc), + unflatten_addresses(bcc)); + } - email.message_id = (message_id != null) ? new RFC822.MessageID(message_id) : null; - email.in_reply_to = (in_reply_to != null) ? new RFC822.MessageID(in_reply_to) : null; + if ((fields & Geary.Email.Field.REFERENCES) != 0) { + email.set_references((message_id != null) ? new RFC822.MessageID(message_id) : null, + (in_reply_to != null) ? new RFC822.MessageID(in_reply_to) : null); + } - email.subject = (subject != null) ? new RFC822.Subject(subject) : null; + if (((fields & Geary.Email.Field.SUBJECT) != 0) && (subject != null)) + email.set_message_subject(new RFC822.Subject(subject)); - email.header = (header != null) ? new RFC822.Header(new Geary.Memory.StringBuffer(header)) - : null; + if (((fields & Geary.Email.Field.HEADER) != 0) && (header != null)) + email.set_message_header(new RFC822.Header(new Geary.Memory.StringBuffer(header))); - email.body = (body != null) ? new RFC822.Text(new Geary.Memory.StringBuffer(body)) - : null; + if (((fields & Geary.Email.Field.BODY) != 0) && (body != null)) + email.set_message_body(new RFC822.Text(new Geary.Memory.StringBuffer(body))); return email; } - public string? flatten_addresses(RFC822.MailboxAddresses? addrs) { + public void merge_from_network(Geary.Email email) { + foreach (Geary.Email.Field field in Geary.Email.Field.all()) { + if ((email.fields & field) != 0) + set_from_email(field, email); + else + unset_fields(field); + } + } + + private string? flatten_addresses(RFC822.MailboxAddresses? addrs) { if (addrs == null) return null; @@ -144,8 +146,111 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row { } } - public RFC822.MailboxAddresses? unflatten_addresses(string? str) { + private RFC822.MailboxAddresses? unflatten_addresses(string? str) { return String.is_empty(str) ? null : new RFC822.MailboxAddresses.from_rfc822_string(str); } + + private void set_from_email(Geary.Email.Field fields, Geary.Email email) { + // Although the fields bitmask might indicate various fields are set, they may still be + // null if empty + + if ((fields & Geary.Email.Field.DATE) != 0) { + date = (email.date != null) ? email.date.original : null; + date_time_t = (email.date != null) ? email.date.as_time_t : -1; + + this.fields = this.fields.set(Geary.Email.Field.DATE); + } + + if ((fields & Geary.Email.Field.ORIGINATORS) != 0) { + from = flatten_addresses(email.from); + sender = flatten_addresses(email.sender); + reply_to = flatten_addresses(email.reply_to); + + this.fields = this.fields.set(Geary.Email.Field.ORIGINATORS); + } + + if ((fields & Geary.Email.Field.RECEIVERS) != 0) { + to = flatten_addresses(email.to); + cc = flatten_addresses(email.cc); + bcc = flatten_addresses(email.bcc); + + this.fields = this.fields.set(Geary.Email.Field.RECEIVERS); + } + + if ((fields & Geary.Email.Field.REFERENCES) != 0) { + message_id = (email.message_id != null) ? email.message_id.value : null; + in_reply_to = (email.in_reply_to != null) ? email.in_reply_to.value : null; + + this.fields = this.fields.set(Geary.Email.Field.REFERENCES); + } + + if ((fields & Geary.Email.Field.SUBJECT) != 0) { + subject = (email.subject != null) ? email.subject.value : null; + + this.fields = this.fields.set(Geary.Email.Field.SUBJECT); + } + + if ((fields & Geary.Email.Field.HEADER) != 0) { + header = (email.header != null) ? email.header.buffer.to_ascii_string() : null; + + this.fields = this.fields.set(Geary.Email.Field.HEADER); + } + + if ((fields & Geary.Email.Field.BODY) != 0) { + body = (email.body != null) ? email.body.buffer.to_ascii_string() : null; + + this.fields = this.fields.set(Geary.Email.Field.BODY); + } + } + + private void unset_fields(Geary.Email.Field fields) { + if ((fields & Geary.Email.Field.DATE) != 0) { + date = null; + date_time_t = -1; + + this.fields = this.fields.clear(Geary.Email.Field.DATE); + } + + if ((fields & Geary.Email.Field.ORIGINATORS) != 0) { + from = null; + sender = null; + reply_to = null; + + this.fields = this.fields.clear(Geary.Email.Field.ORIGINATORS); + } + + if ((fields & Geary.Email.Field.RECEIVERS) != 0) { + to = null; + cc = null; + bcc = null; + + this.fields = this.fields.clear(Geary.Email.Field.RECEIVERS); + } + + if ((fields & Geary.Email.Field.REFERENCES) != 0) { + message_id = null; + in_reply_to = null; + + this.fields = this.fields.clear(Geary.Email.Field.REFERENCES); + } + + if ((fields & Geary.Email.Field.SUBJECT) != 0) { + subject = null; + + this.fields = this.fields.clear(Geary.Email.Field.SUBJECT); + } + + if ((fields & Geary.Email.Field.HEADER) != 0) { + header = null; + + this.fields = this.fields.clear(Geary.Email.Field.HEADER); + } + + if ((fields & Geary.Email.Field.BODY) != 0) { + body = null; + + this.fields = this.fields.clear(Geary.Email.Field.BODY); + } + } } diff --git a/src/engine/sqlite/MessageTable.vala b/src/engine/sqlite/MessageTable.vala index f561e954..241bc38b 100644 --- a/src/engine/sqlite/MessageTable.vala +++ b/src/engine/sqlite/MessageTable.vala @@ -8,6 +8,7 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table { // This *must* match the column order in the database public enum Column { ID, + FIELDS, DATE_FIELD, DATE_INT64, @@ -37,31 +38,116 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table { public async int64 create_async(MessageRow row, Cancellable? cancellable) throws Error { SQLHeavy.Query query = db.prepare( "INSERT INTO MessageTable " - + "(date_field, date_time_t, from_field, sender, reply_to, to_field, cc, bcc, message_id, in_reply_to, subject, header, body) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - query.bind_string(0, row.date); - query.bind_int64(1, row.date_time_t); - query.bind_string(2, row.from); - query.bind_string(3, row.sender); - query.bind_string(4, row.reply_to); - query.bind_string(5, row.to); - query.bind_string(6, row.cc); - query.bind_string(7, row.bcc); - query.bind_string(8, row.message_id); - query.bind_string(9, row.in_reply_to); - query.bind_string(10, row.subject); - query.bind_string(11, row.header); - query.bind_string(12, row.body); + + "(fields, date_field, date_time_t, from_field, sender, reply_to, to_field, cc, bcc, " + + "message_id, in_reply_to, subject, header, body) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + query.bind_int(0, row.fields); + query.bind_string(1, row.date); + query.bind_int64(2, row.date_time_t); + query.bind_string(3, row.from); + query.bind_string(4, row.sender); + query.bind_string(5, row.reply_to); + query.bind_string(6, row.to); + query.bind_string(7, row.cc); + query.bind_string(8, row.bcc); + query.bind_string(9, row.message_id); + query.bind_string(10, row.in_reply_to); + query.bind_string(11, row.subject); + query.bind_string(12, row.header); + query.bind_string(13, row.body); return yield query.execute_insert_async(cancellable); } + public async void merge_async(MessageRow row, Cancellable? cancellable = null) throws Error { + SQLHeavy.Transaction transaction = db.begin_transaction(); + + // merge the valid fields in the row + SQLHeavy.Query query = transaction.prepare( + "UPDATE MessageTable SET fields = fields | ? WHERE id=?"); + query.bind_int(0, row.fields); + query.bind_int64(1, row.id); + + query.execute(); + + if (row.fields.is_set(Geary.Email.Field.DATE)) { + query = transaction.prepare( + "UPDATE MessageTable SET date_field=?, date_time_t=? WHERE id=?"); + query.bind_string(0, row.date); + query.bind_int64(1, row.date_time_t); + query.bind_int64(2, row.id); + + query.execute(); + } + + if (row.fields.is_set(Geary.Email.Field.ORIGINATORS)) { + query = transaction.prepare( + "UPDATE MessageTable SET from_field=?, sender=?, reply_to=? WHERE id=?"); + query.bind_string(0, row.from); + query.bind_string(1, row.sender); + query.bind_string(2, row.reply_to); + query.bind_int64(3, row.id); + + query.execute(); + } + + if (row.fields.is_set(Geary.Email.Field.RECEIVERS)) { + query = transaction.prepare( + "UPDATE MessageTable SET to_field=?, cc=?, bcc=? WHERE id=?"); + query.bind_string(0, row.to); + query.bind_string(1, row.cc); + query.bind_string(2, row.bcc); + query.bind_int64(3, row.id); + + query.execute(); + } + + if (row.fields.is_set(Geary.Email.Field.REFERENCES)) { + query = transaction.prepare( + "UPDATE MessageTable SET message_id=?, in_reply_to=? WHERE id=?"); + query.bind_string(0, row.message_id); + query.bind_string(1, row.in_reply_to); + query.bind_int64(2, row.id); + + query.execute(); + } + + if (row.fields.is_set(Geary.Email.Field.SUBJECT)) { + query = transaction.prepare( + "UPDATE MessageTable SET subject=? WHERE id=?"); + query.bind_string(0, row.subject); + query.bind_int64(1, row.id); + + query.execute(); + } + + if (row.fields.is_set(Geary.Email.Field.HEADER)) { + query = transaction.prepare( + "UPDATE MessageTable SET header=? WHERE id=?"); + query.bind_string(0, row.header); + query.bind_int64(1, row.id); + + query.execute(); + } + + if (row.fields.is_set(Geary.Email.Field.BODY)) { + query = transaction.prepare( + "UPDATE MessageTable SET body=? WHERE id=?"); + query.bind_string(0, row.body); + query.bind_int64(1, row.id); + + query.execute(); + } + + transaction.commit(); + } + public async Gee.List? list_by_message_id_async(Geary.RFC822.MessageID message_id, Geary.Email.Field fields, Cancellable? cancellable) throws Error { assert(fields != Geary.Email.Field.NONE); SQLHeavy.Query query = db.prepare( - "SELECT %s FROM MessageTable WHERE message_id = ?".printf(fields_to_columns(fields))); + "SELECT %s FROM MessageTable WHERE message_id=?".printf(fields_to_columns(fields))); query.bind_string(0, message_id.value); SQLHeavy.QueryResult results = yield query.execute_async(cancellable); @@ -77,25 +163,42 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table { return (list.size > 0) ? list : null; } - public async MessageRow? fetch_async(int64 id, Geary.Email.Field fields, + public async MessageRow? fetch_async(int64 id, Geary.Email.Field requested_fields, Cancellable? cancellable = null) throws Error { - assert(fields != Geary.Email.Field.NONE); + assert(requested_fields != Geary.Email.Field.NONE); SQLHeavy.Query query = db.prepare( - "SELECT id, %s FROM MessageTable WHERE id = ?".printf(fields_to_columns(fields))); + "SELECT %s FROM MessageTable WHERE id=?".printf(fields_to_columns(requested_fields))); query.bind_int64(0, id); SQLHeavy.QueryResult results = yield query.execute_async(cancellable); if (results.finished) return null; - MessageRow row = new MessageRow.from_query_result(this, fields, results); + MessageRow row = new MessageRow.from_query_result(this, requested_fields, results); return row; } + public async bool fetch_fields_async(int64 id, out Geary.Email.Field available_fields, + Cancellable? cancellable = null) throws Error { + available_fields = Geary.Email.Field.NONE; + + SQLHeavy.Query query = db.prepare( + "SELECT fields FROM MessageTable WHERE id=?"); + query.bind_int64(0, id); + + SQLHeavy.QueryResult result = yield query.execute_async(cancellable); + if (result.finished) + return false; + + available_fields = (Geary.Email.Field) result.fetch_int(0); + + return true; + } + private static string fields_to_columns(Geary.Email.Field fields) { - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new StringBuilder("id, fields"); foreach (Geary.Email.Field field in Geary.Email.Field.all()) { string? append = null; if ((fields & field) != 0) { @@ -140,5 +243,35 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table { return builder.str; } + + public async int search_message_id_count_async(Geary.RFC822.MessageID message_id, + Cancellable? cancellable = null) throws Error { + SQLHeavy.Query query = db.prepare( + "SELECT COUNT(*) FROM MessageTable WHERE message_id=?"); + query.bind_string(0, message_id.value); + + SQLHeavy.QueryResult result = yield query.execute_async(cancellable); + + return (result.finished) ? 0 : result.fetch_int(0); + } + + public async Gee.List? search_message_id_async(Geary.RFC822.MessageID message_id, + Cancellable? cancellable = null) throws Error { + SQLHeavy.Query query = db.prepare( + "SELECT id FROM MessageTable WHERE message_id=?"); + query.bind_string(0, message_id.value); + + SQLHeavy.QueryResult result = yield query.execute_async(cancellable); + if (result.finished) + return null; + + Gee.List list = new Gee.ArrayList(); + do { + list.add(result.fetch_int64(0)); + yield result.next_async(cancellable); + } while (!result.finished); + + return list; + } } diff --git a/src/engine/sqlite/Row.vala b/src/engine/sqlite/Row.vala index 76895786..0deac4f8 100644 --- a/src/engine/sqlite/Row.vala +++ b/src/engine/sqlite/Row.vala @@ -14,15 +14,25 @@ public abstract class Geary.Sqlite.Row { } public int fetch_int_for(SQLHeavy.QueryResult result, int col) throws SQLHeavy.Error { - return result.fetch_int(result.field_index(table.get_field_name(col))); + return result.fetch_int(field_index(result, col)); } public int64 fetch_int64_for(SQLHeavy.QueryResult result, int col) throws SQLHeavy.Error { - return result.fetch_int64(result.field_index(table.get_field_name(col))); + return result.fetch_int64(field_index(result, col)); } public string fetch_string_for(SQLHeavy.QueryResult result, int col) throws SQLHeavy.Error { - return result.fetch_string(result.field_index(table.get_field_name(col))); + return result.fetch_string(field_index(result, col)); + } + + private int field_index(SQLHeavy.QueryResult result, int col) throws SQLHeavy.Error { + try { + return result.field_index(table.get_field_name(col)); + } catch (SQLHeavy.Error err) { + debug("Bad column #%d in %s: %s", col, table.to_string(), err.message); + + throw err; + } } } diff --git a/src/engine/sqlite/Table.vala b/src/engine/sqlite/Table.vala index 3d37fd8f..2f5d6d9d 100644 --- a/src/engine/sqlite/Table.vala +++ b/src/engine/sqlite/Table.vala @@ -22,5 +22,9 @@ public abstract class Geary.Sqlite.Table { public string get_field_name(int col) throws SQLHeavy.Error { return table.field_name(col); } + + public string to_string() { + return table.name; + } } diff --git a/src/engine/sqlite/api/Account.vala b/src/engine/sqlite/api/Account.vala index eb27133a..7ff1298c 100644 --- a/src/engine/sqlite/api/Account.vala +++ b/src/engine/sqlite/api/Account.vala @@ -7,6 +7,7 @@ public class Geary.Sqlite.Account : Object, Geary.Account, Geary.LocalAccount { private MailDatabase db; private FolderTable folder_table; + private MessageTable message_table; public Account(Geary.Credentials cred) { try { @@ -16,6 +17,11 @@ public class Geary.Sqlite.Account : Object, Geary.Account, Geary.LocalAccount { } folder_table = db.get_folder_table(); + message_table = db.get_message_table(); + } + + public Geary.Email.Field get_required_fields_for_writing() { + return Geary.Email.Field.NONE; } public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder, @@ -62,5 +68,12 @@ public class Geary.Sqlite.Account : Object, Geary.Account, Geary.LocalAccount { Cancellable? cancellable = null) throws Error { // TODO } + + public async bool has_message_id_async(Geary.RFC822.MessageID message_id, out int count, + Cancellable? cancellable = null) throws Error { + count = yield message_table.search_message_id_count_async(message_id); + + return (count > 0); + } } diff --git a/src/engine/sqlite/api/Folder.vala b/src/engine/sqlite/api/Folder.vala index 38d9eda8..f5e4f444 100644 --- a/src/engine/sqlite/api/Folder.vala +++ b/src/engine/sqlite/api/Folder.vala @@ -4,13 +4,17 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Sqlite.Folder : Object, Geary.Folder { +// 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. + +public class Geary.Sqlite.Folder : Object, Geary.Folder, Geary.LocalFolder { private MailDatabase db; private FolderRow folder_row; private MessageTable message_table; private MessageLocationTable location_table; private ImapMessageLocationPropertiesTable imap_location_table; private string name; + private bool opened = false; internal Folder(MailDatabase db, FolderRow folder_row) throws Error { this.db = db; @@ -23,6 +27,11 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder { imap_location_table = db.get_imap_message_location_table(); } + private void check_open() throws Error { + if (!opened) + throw new EngineError.OPEN_REQUIRED("%s not open", to_string()); + } + public string get_name() { return name; } @@ -32,23 +41,46 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder { } public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { + if (opened) + throw new EngineError.ALREADY_OPEN("%s already open", to_string()); + + opened = true; + notify_opened(); } public async void close_async(Cancellable? cancellable = null) throws Error { + if (!opened) + return; + + opened = false; + notify_closed(CloseReason.FOLDER_CLOSED); } - public int get_message_count() throws Error { + public async int get_email_count(Cancellable? cancellable = null) throws Error { + check_open(); + + // TODO return 0; } - public async void create_email_async(Geary.Email email, Geary.Email.Field fields, - Cancellable? cancellable = null) throws Error { - int64 message_id = yield message_table.create_async( - new MessageRow.from_email(message_table, email), - cancellable); + public async void create_email_async(Geary.Email email, Cancellable? cancellable = null) throws Error { + check_open(); Geary.Imap.EmailLocation location = (Geary.Imap.EmailLocation) email.location; + // See if it already exists; first by UID (which is only guaranteed to be unique in a folder, + // not account-wide) + int64 message_id; + if (yield imap_location_table.search_uid_in_folder(location.uid, folder_row.id, out message_id, + cancellable)) { + throw new EngineError.ALREADY_EXISTS("Email with UID %s already exists in %s", + location.uid.to_string(), get_name()); + } + + message_id = yield message_table.create_async( + new MessageRow.from_email(message_table, email), + cancellable); + MessageLocationRow location_row = new MessageLocationRow(location_table, Row.INVALID_ID, message_id, folder_row.id, location.position); int64 location_id = yield location_table.create_async(location_row, cancellable); @@ -58,40 +90,54 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder { yield imap_location_table.create_async(imap_location_row, cancellable); } - public async Gee.List? list_email_async(int low, int count, Geary.Email.Field fields, - Cancellable? cancellable) throws Error { + public async Gee.List? list_email_async(int low, int count, + Geary.Email.Field required_fields, Cancellable? cancellable) throws Error { assert(low >= 1); assert(count >= 1); - // low is zero-based in the database. + check_open(); + Gee.List? list = yield location_table.list_async(folder_row.id, low, count, cancellable); - return yield list_email(list, fields, cancellable); + return yield list_email(list, required_fields, cancellable); } public async Gee.List? list_email_sparse_async(int[] by_position, - Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { + Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error { + check_open(); + Gee.List? list = yield location_table.list_sparse_async(folder_row.id, by_position, cancellable); - return yield list_email(list, fields, cancellable); + return yield list_email(list, required_fields, cancellable); } private async Gee.List? list_email(Gee.List? list, - Geary.Email.Field fields, Cancellable? cancellable) throws Error { + Geary.Email.Field required_fields, Cancellable? cancellable) throws Error { + check_open(); + if (list == null || list.size == 0) return null; + // TODO: As this loop involves multiple database operations to form an email, might make + // sense in the future to launch each async method separately, putting the final results + // together when all the information is fetched Gee.List emails = new Gee.ArrayList(); foreach (MessageLocationRow location_row in list) { + // fetch the IMAP message location properties that are associated with the generic + // message location ImapMessageLocationPropertiesRow? imap_location_row = yield imap_location_table.fetch_async( location_row.id, cancellable); assert(imap_location_row != null); + // fetch the message itself MessageRow? message_row = yield message_table.fetch_async(location_row.message_id, - fields, cancellable); + required_fields, cancellable); assert(message_row != null); + // only add to the list if the email contains all the required fields + if (!message_row.fields.is_set(required_fields)) + continue; emails.add(message_row.to_email(new Geary.Imap.EmailLocation(location_row.position, imap_location_row.uid))); @@ -100,11 +146,12 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder { return (emails.size > 0) ? emails : null; } - public async Geary.Email fetch_email_async(int position, Geary.Email.Field fields, + public async Geary.Email fetch_email_async(int position, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error { assert(position >= 1); - // num is zero-based in the database. + check_open(); + MessageLocationRow? location_row = yield location_table.fetch_async(folder_row.id, position, cancellable); if (location_row == null) @@ -121,13 +168,143 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder { assert(imap_location_row.location_id == location_row.id); - MessageRow? message_row = yield message_table.fetch_async(location_row.message_id, fields, - cancellable); + MessageRow? message_row = yield message_table.fetch_async(location_row.message_id, + required_fields, cancellable); if (message_row == null) throw new EngineError.NOT_FOUND("No message at position %d in folder %s", position, name); + if (!message_row.fields.is_set(required_fields)) { + throw new EngineError.INCOMPLETE_MESSAGE( + "Message at position %d in folder %s only fulfills %Xh fields", position, to_string(), + message_row.fields); + } + return message_row.to_email(new Geary.Imap.EmailLocation(location_row.position, imap_location_row.uid)); } + + public async bool is_email_present_at(int position, out Geary.Email.Field available_fields, + Cancellable? cancellable = null) throws Error { + check_open(); + + available_fields = Geary.Email.Field.NONE; + + MessageLocationRow? location_row = yield location_table.fetch_async(folder_row.id, position, + cancellable); + if (location_row == null) + return false; + + return yield message_table.fetch_fields_async(location_row.message_id, out available_fields, + cancellable); + } + + public async bool is_email_associated_async(Geary.Email email, Cancellable? cancellable = null) + throws Error { + check_open(); + + int64 message_id; + return yield imap_location_table.search_uid_in_folder( + ((Geary.Imap.EmailLocation) email.location).uid, folder_row.id, out message_id, + cancellable); + } + + public async void update_email_async(Geary.Email email, bool duplicate_okay, + Cancellable? cancellable = null) throws Error { + check_open(); + + Geary.Imap.EmailLocation location = (Geary.Imap.EmailLocation) email.location; + + // See if the message can be identified in the folder (which both reveals association and + // a message_id that can be used for a merge; note that this works without a Message-ID) + int64 message_id; + bool associated = yield imap_location_table.search_uid_in_folder(location.uid, folder_row.id, + 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 + // a merge + if (email.message_id == null) { + if (!associated) { + if (!duplicate_okay) + throw new EngineError.INCOMPLETE_MESSAGE("No Message-ID"); + + yield create_email_async(email, cancellable); + } else { + yield merge_email_async(message_id, email, cancellable); + } + + return; + } + + // If not associated, find message with matching Message-ID + if (!associated) { + Gee.List? list = yield message_table.search_message_id_async(email.message_id, + cancellable); + + // If none found, this operation is a create + if (list == null || list.size == 0) { + yield create_email_async(email, cancellable); + + return; + } + + // Too many found turns this operation into a create + if (list.size != 1) { + yield create_email_async(email, cancellable); + + return; + } + + message_id = list[0]; + } + + // Found a message. If not associated with this folder, associate now. + // TODO: Need to lock the database during this operation, as these steps should be atomic. + if (!associated) { + // see if an email exists at this position + MessageLocationRow? location_row = yield location_table.fetch_async(folder_row.id, + location.position); + if (location_row != null) { + throw new EngineError.ALREADY_EXISTS("Email already exists at position %d in %s", + email.location.position, to_string()); + } + + // insert email at supplied position + location_row = new MessageLocationRow(location_table, Row.INVALID_ID, message_id, + folder_row.id, location.position); + int64 location_id = yield location_table.create_async(location_row, cancellable); + + // update position propeties + ImapMessageLocationPropertiesRow imap_location_row = new ImapMessageLocationPropertiesRow( + imap_location_table, Row.INVALID_ID, location_id, location.uid); + yield imap_location_table.create_async(imap_location_row, cancellable); + } + + // Merge any new information with the existing message in the local store + yield merge_email_async(message_id, email, cancellable); + + // Done. + } + + // TODO: The database should be locked around this method, as it should be atomic. + // TODO: Merge email properties + private async void merge_email_async(int64 message_id, Geary.Email email, + Cancellable? cancellable = null) throws Error { + assert(message_id != Row.INVALID_ID); + + // if nothing to merge, nothing to do + if (email.fields == Geary.Email.Field.NONE) + return; + + MessageRow? message_row = yield message_table.fetch_async(message_id, email.fields, + cancellable); + assert(message_row != null); + + message_row.merge_from_network(email); + + // possible nothing has changed or been added + if (message_row.fields != Geary.Email.Field.NONE) + yield message_table.merge_async(message_row, cancellable); + } }