From 9221937e95d078af53937faf2957cd4e56816dd4 Mon Sep 17 00:00:00 2001 From: Jim Nelson Date: Thu, 16 Jun 2011 16:27:08 -0700 Subject: [PATCH] Addition of MessageTable and MessageLocationTable toward fixing #3742. Much of the API between the local and net stores had to be reworked for consistency as well as planning ahead for how messages will be retrieved and stored efficiently. This work also attempts to keep in mind that other mail sources (POP, etc.) may be required in the future, and hopefully can be added without major rework. --- Makefile | 11 +- sql/Create.sql | 92 ++++++++++- src/client/ui/MainWindow.vala | 25 +-- src/client/ui/MessageListStore.vala | 20 +-- src/client/ui/MessageListView.vala | 8 +- src/engine/Engine.vala | 120 +------------- src/engine/EngineFolder.vala | 30 ++-- src/engine/ImapEngine.vala | 123 ++++++++++++++ src/engine/api/Account.vala | 21 +-- src/engine/api/Email.vala | 111 +++++++++---- src/engine/api/EmailOrdering.vala | 14 ++ src/engine/api/EmailProperties.vala | 9 + src/engine/api/EngineError.vala | 4 +- src/engine/api/Folder.vala | 31 ++-- src/engine/api/FolderProperties.vala | 9 + src/engine/imap/Email.vala | 18 -- src/engine/imap/Mailbox.vala | 137 ++++++++++++++-- src/engine/imap/MessageData.vala | 3 +- src/engine/imap/api/Account.vala | 43 +++-- src/engine/imap/api/EmailProperties.vala | 14 ++ src/engine/imap/api/Folder.vala | 43 ++--- src/engine/imap/api/FolderProperties.vala | 25 +++ src/engine/imap/decoders/FetchResults.vala | 4 + .../imap/decoders/SelectExamineResults.vala | 4 +- src/engine/rfc822/MessageData.vala | 17 +- src/engine/sqlite/Database.vala | 5 +- src/engine/sqlite/FolderRow.vala | 25 ++- src/engine/sqlite/FolderTable.vala | 107 +++++------- src/engine/sqlite/MailDatabase.vala | 26 ++- src/engine/sqlite/MessageLocationRow.vala | 33 ++++ src/engine/sqlite/MessageLocationTable.vala | 80 +++++++++ src/engine/sqlite/MessageRow.vala | 154 ++++++++++++++++++ src/engine/sqlite/MessageTable.vala | 143 ++++++++++++++++ src/engine/sqlite/Row.vala | 21 ++- src/engine/sqlite/Table.vala | 4 + src/engine/sqlite/api/Account.vala | 49 +++--- src/engine/sqlite/api/Folder.vala | 92 +++++++---- src/engine/util/Memory.vala | 24 +++ 38 files changed, 1239 insertions(+), 460 deletions(-) create mode 100644 src/engine/ImapEngine.vala create mode 100644 src/engine/api/EmailOrdering.vala create mode 100644 src/engine/api/EmailProperties.vala create mode 100644 src/engine/api/FolderProperties.vala delete mode 100644 src/engine/imap/Email.vala create mode 100644 src/engine/imap/api/EmailProperties.vala create mode 100644 src/engine/imap/api/FolderProperties.vala create mode 100644 src/engine/sqlite/MessageLocationRow.vala create mode 100644 src/engine/sqlite/MessageLocationTable.vala create mode 100644 src/engine/sqlite/MessageRow.vala create mode 100644 src/engine/sqlite/MessageTable.vala diff --git a/Makefile b/Makefile index c6cdb1f8..cac493b7 100644 --- a/Makefile +++ b/Makefile @@ -10,10 +10,14 @@ APPS := geary console watchmbox ENGINE_SRC := \ src/engine/Engine.vala \ + src/engine/ImapEngine.vala \ src/engine/EngineFolder.vala \ src/engine/api/Account.vala \ src/engine/api/Email.vala \ + src/engine/api/EmailProperties.vala \ + src/engine/api/EmailOrdering.vala \ src/engine/api/Folder.vala \ + src/engine/api/FolderProperties.vala \ src/engine/api/Credentials.vala \ src/engine/api/EngineError.vala \ src/engine/sqlite/Database.vala \ @@ -22,6 +26,10 @@ ENGINE_SRC := \ src/engine/sqlite/MailDatabase.vala \ src/engine/sqlite/FolderTable.vala \ src/engine/sqlite/FolderRow.vala \ + src/engine/sqlite/MessageRow.vala \ + src/engine/sqlite/MessageTable.vala \ + src/engine/sqlite/MessageLocationRow.vala \ + src/engine/sqlite/MessageLocationTable.vala \ src/engine/sqlite/api/Account.vala \ src/engine/sqlite/api/Folder.vala \ src/engine/state/Machine.vala \ @@ -33,7 +41,6 @@ ENGINE_SRC := \ src/engine/imap/ClientSessionManager.vala \ src/engine/imap/DataFormat.vala \ src/engine/imap/Mailbox.vala \ - src/engine/imap/Email.vala \ src/engine/imap/Parameter.vala \ src/engine/imap/Tag.vala \ src/engine/imap/Command.vala \ @@ -63,7 +70,9 @@ ENGINE_SRC := \ src/engine/imap/decoders/SelectExamineResults.vala \ src/engine/imap/decoders/StatusResults.vala \ src/engine/imap/api/Account.vala \ + src/engine/imap/api/EmailProperties.vala \ src/engine/imap/api/Folder.vala \ + src/engine/imap/api/FolderProperties.vala \ src/engine/rfc822/MailboxAddress.vala \ src/engine/rfc822/MessageData.vala \ src/engine/util/Memory.vala \ diff --git a/sql/Create.sql b/sql/Create.sql index 3b587228..69910eca 100644 --- a/sql/Create.sql +++ b/sql/Create.sql @@ -1,12 +1,94 @@ +-- +-- FolderTable +-- + CREATE TABLE FolderTable ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, - supports_children INTEGER, - is_openable INTEGER, - parent_id INTEGER + parent_id INTEGER REFERENCES FolderTable ON DELETE RESTRICT ); -CREATE INDEX FolderTableNameIndex ON FolderTable (name); -CREATE INDEX FolderTableParentIndex ON FolderTable (parent_id); +CREATE INDEX FolderTableNameIndex ON FolderTable(name); +CREATE INDEX FolderTableParentIndex ON FolderTable(parent_id); + +-- +-- MessageTable +-- + +CREATE TABLE MessageTable ( + id INTEGER PRIMARY KEY, + + date_field TEXT, + date_time_t INTEGER, + + from_field TEXT, + sender TEXT, + reply_to TEXT, + + to_field TEXT, + cc TEXT, + bcc TEXT, + + message_id TEXT, + in_reply_to TEXT, + + subject TEXT, + + header TEXT, + + body TEXT +); + +CREATE INDEX MessageTableMessageIDIndex ON MessageTable(message_id); + +-- +-- MessageLocationTable +-- + +CREATE TABLE MessageLocationTable ( + id INTEGER PRIMARY KEY, + message_id INTEGER REFERENCES MessageTable ON DELETE CASCADE, + folder_id INTEGER REFERENCES FolderTable ON DELETE CASCADE, + ordering INTEGER +); + +CREATE INDEX MessageLocationTableMessageIDIndex ON MessageLocationTable(message_id); +CREATE INDEX MessageLocationTableFolderIDIndex ON MessageLocationTable(folder_id); + +-- +-- IMAP-specific tables +-- + +-- +-- ImapFolderPropertiesTable +-- + +CREATE TABLE ImapFolderPropertiesTable ( + id INTEGER PRIMARY KEY, + folder_id INTEGER UNIQUE REFERENCES FolderTable ON DELETE CASCADE, + uid_validity INTEGER, + supports_children INTEGER, + is_openable INTEGER +); + +CREATE INDEX ImapFolderPropertiesTableFolderIDIndex ON ImapFolderPropertiesTable(folder_id); + +-- +-- ImapMessagePropertiesTable +-- + +CREATE TABLE ImapMessagePropertiesTable ( + id INTEGER PRIMARY KEY, + message_id INTEGER UNIQUE REFERENCES MessageTable ON DELETE CASCADE, + answered INTEGER, + deleted INTEGER, + draft INTEGER, + flagged INTEGER, + recent INTEGER, + seen INTEGER, + all_flags TEXT +); + +CREATE INDEX ImapMessagePropertiesTableMessageIDIndex ON ImapMessagePropertiesTable(message_id); diff --git a/src/client/ui/MainWindow.vala b/src/client/ui/MainWindow.vala index 43b51ee8..d8ea7c57 100644 --- a/src/client/ui/MainWindow.vala +++ b/src/client/ui/MainWindow.vala @@ -79,7 +79,7 @@ public class MainWindow : Gtk.Window { private async void do_start() { try { // pull down the root-level folders - Gee.Collection folders = yield account.list_async(null); + Gee.Collection folders = yield account.list_folders_async(null); if (folders != null) on_folders_added_removed(folders, null); else @@ -203,10 +203,11 @@ public class MainWindow : Gtk.Window { yield current_folder.open_async(true); - Gee.List? headers = yield current_folder.read_async(1, 100); - if (headers != null && headers.size > 0) { - foreach (Geary.EmailHeader header in headers) - message_list_store.append_header(header); + Gee.List? email = yield current_folder.list_email_async(1, 100, + Geary.Email.Field.ENVELOPE); + if (email != null && email.size > 0) { + foreach (Geary.Email envelope in email) + message_list_store.append_header(envelope); } } @@ -218,25 +219,25 @@ public class MainWindow : Gtk.Window { } } - private void on_message_selected(Geary.EmailHeader? header) { - if (header == null) { + private void on_message_selected(Geary.Email? email) { + if (email == null) { message_buffer.set_text(""); return; } - do_select_message.begin(header, on_select_message_completed); + do_select_message.begin(email, on_select_message_completed); } - private async void do_select_message(Geary.EmailHeader header) throws Error { + private async void do_select_message(Geary.Email email) throws Error { if (current_folder == null) { - debug("Message %s selected with no folder selected", header.to_string()); + debug("Message %s selected with no folder selected", email.to_string()); return; } - Geary.Email email = yield current_folder.fetch_async(header); - message_buffer.set_text(email.full); + Geary.Email text = yield current_folder.fetch_email_async(email.msg_num, Geary.Email.Field.BODY); + message_buffer.set_text(text.body.buffer.to_ascii_string()); } private void on_select_message_completed(Object? source, AsyncResult result) { diff --git a/src/client/ui/MessageListStore.vala b/src/client/ui/MessageListStore.vala index 91ffe44f..d498f388 100644 --- a/src/client/ui/MessageListStore.vala +++ b/src/client/ui/MessageListStore.vala @@ -26,7 +26,7 @@ public class MessageListStore : Gtk.TreeStore { typeof (string), // DATE typeof (string), // FROM typeof (string), // SUBJECT - typeof (Geary.EmailHeader) // MESSAGE_OBJECT + typeof (Geary.Email) // MESSAGE_OBJECT }; } @@ -54,27 +54,27 @@ public class MessageListStore : Gtk.TreeStore { set_column_types(Column.get_types()); } - public void append_header(Geary.EmailHeader header) { + public void append_header(Geary.Email envelope) { Gtk.TreeIter iter; append(out iter, null); set(iter, - Column.DATE, Date.pretty_print(header.sent.value), - Column.FROM, header.from.get_at(0).get_short_address(), - Column.SUBJECT, header.subject.value, - Column.MESSAGE_OBJECT, header + Column.DATE, Date.pretty_print(envelope.date.value), + Column.FROM, envelope.from[0].get_short_address(), + Column.SUBJECT, envelope.subject.value, + Column.MESSAGE_OBJECT, envelope ); } - public Geary.EmailHeader? get_message_at(Gtk.TreePath path) { + public Geary.Email? get_message_at(Gtk.TreePath path) { Gtk.TreeIter iter; if (!get_iter(out iter, path)) return null; - Geary.EmailHeader header; - get(iter, Column.MESSAGE_OBJECT, out header); + Geary.Email email; + get(iter, Column.MESSAGE_OBJECT, out email); - return header; + return email; } } diff --git a/src/client/ui/MessageListView.vala b/src/client/ui/MessageListView.vala index 3f5089a4..de4aa7fb 100644 --- a/src/client/ui/MessageListView.vala +++ b/src/client/ui/MessageListView.vala @@ -5,7 +5,7 @@ */ public class MessageListView : Gtk.TreeView { - public signal void message_selected(Geary.EmailHeader? email); + public signal void message_selected(Geary.Email? email); public MessageListView(MessageListStore store) { set_model(store); @@ -48,9 +48,9 @@ public class MessageListView : Gtk.TreeView { return; } - Geary.EmailHeader? header = get_store().get_message_at(path); - if (header != null) - message_selected(header); + Geary.Email? email = get_store().get_message_at(path); + if (email != null) + message_selected(email); } } diff --git a/src/engine/Engine.vala b/src/engine/Engine.vala index 372def40..963917c2 100644 --- a/src/engine/Engine.vala +++ b/src/engine/Engine.vala @@ -4,124 +4,12 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Engine : Object, Geary.Account { - private NetworkAccount net; - private LocalAccount local; - - private Engine(NetworkAccount net, LocalAccount local) { - this.net = net; - this.local = local; - } - - public static Account open(Geary.Credentials cred) throws Error { - return new Engine( +public class Geary.Engine { + public static Geary.Account open(Geary.Credentials cred) throws Error { + // Only ImapEngine today + return new ImapEngine( new Geary.Imap.Account(cred, Imap.ClientConnection.DEFAULT_PORT_TLS), new Geary.Sqlite.Account(cred)); } - - public async Gee.Collection list_async(string? parent_folder, - Cancellable? cancellable = null) throws Error { - Gee.Collection local_list = yield local.list_async(parent_folder, cancellable); - - Gee.Collection engine_list = new Gee.ArrayList(); - foreach (Geary.Folder local_folder in local_list) - engine_list.add(new EngineFolder(net, local, local_folder)); - - background_update_folders.begin(parent_folder, engine_list); - - debug("Reporting %d folders", engine_list.size); - - return engine_list; - } - - public async Geary.Folder fetch_async(string? parent_folder, string folder_name, - Cancellable? cancellable = null) throws Error { - Geary.Folder local_folder = yield local.fetch_async(parent_folder, folder_name, cancellable); - Geary.Folder engine_folder = new EngineFolder(net, local, local_folder); - - return engine_folder; - } - - private Gee.Set get_folder_names(Gee.Collection folders) { - Gee.Set names = new Gee.HashSet(); - foreach (Geary.Folder folder in folders) - names.add(folder.get_name()); - - return names; - } - - private Gee.List get_excluded_folders(Gee.Collection folders, - Gee.Set names) { - Gee.List excluded = new Gee.ArrayList(); - foreach (Geary.Folder folder in folders) { - if (!names.contains(folder.get_name())) - excluded.add(folder); - } - - return excluded; - } - - private async void background_update_folders(string? parent_folder, - Gee.Collection engine_folders) { - Gee.Collection net_folders; - try { - net_folders = yield net.list_async(parent_folder); - } catch (Error neterror) { - error("Unable to retrieve folder list from server: %s", neterror.message); - } - - Gee.Set local_names = get_folder_names(engine_folders); - Gee.Set net_names = get_folder_names(net_folders); - - debug("%d local names, %d net names", local_names.size, net_names.size); - - Gee.List? to_add = get_excluded_folders(net_folders, local_names); - Gee.List? to_remove = get_excluded_folders(engine_folders, net_names); - - debug("Adding %d, removing %d to/from local store", to_add.size, to_remove.size); - - if (to_add.size == 0) - to_add = null; - - if (to_remove.size == 0) - to_remove = null; - - try { - if (to_add != null) - yield local.create_many_async(to_add); - } catch (Error err) { - error("Unable to add/remove folders: %s", err.message); - } - - Gee.Collection engine_added = null; - if (to_add != null) { - engine_added = new Gee.ArrayList(); - foreach (Geary.Folder net_folder in to_add) { - try { - engine_added.add(new EngineFolder(net, local, - yield local.fetch_async(parent_folder, net_folder.get_name()))); - } catch (Error convert_err) { - error("Unable to fetch local folder: %s", convert_err.message); - } - } - } - - if (engine_added != null) - notify_folders_added_removed(engine_added, null); - } - - public async void create_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error { - } - - public async void create_many_async(Gee.Collection folders, - Cancellable? cancellable = null) throws Error { - } - - public async void remove_async(string folder, Cancellable? cancellable = null) throws Error { - } - - public async void remove_many_async(Gee.Set folders, Cancellable? cancellable = null) - throws Error { - } } diff --git a/src/engine/EngineFolder.vala b/src/engine/EngineFolder.vala index f8da8b3f..6306721d 100644 --- a/src/engine/EngineFolder.vala +++ b/src/engine/EngineFolder.vala @@ -26,25 +26,18 @@ private class Geary.EngineFolder : Object, Geary.Folder { return local_folder.get_name(); } - public Trillian is_readonly() { - return local_folder.is_readonly(); + public Geary.FolderProperties? get_properties() { + return null; } - public Trillian does_support_children() { - return local_folder.does_support_children(); - } - - public Trillian has_children() { - return local_folder.has_children(); - } - - public Trillian is_openable() { - return local_folder.is_openable(); + public async void create_email_async(Geary.Email email, Geary.EmailOrdering ordering, + 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_async(null, local_folder.get_name(), cancellable); + net_folder = yield net.fetch_folder_async(null, local_folder.get_name(), cancellable); net_folder.updated.connect(on_net_updated); } @@ -64,20 +57,17 @@ private class Geary.EngineFolder : Object, Geary.Folder { return 0; } - public async Gee.List? read_async(int low, int count, + public async Gee.List list_email_async(int low, int count, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { - if (net_folder == null) - throw new EngineError.OPEN_REQUIRED("Folder %s not opened", get_name()); - - return yield net_folder.read_async(low, count, cancellable); + return yield net_folder.list_email_async(low, count, fields, cancellable); } - public async Geary.Email fetch_async(Geary.EmailHeader header, + public async Geary.Email fetch_email_async(int num, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { if (net_folder == null) throw new EngineError.OPEN_REQUIRED("Folder %s not opened", get_name()); - return yield net_folder.fetch_async(header, cancellable); + return yield net_folder.fetch_email_async(num, fields, cancellable); } private void on_local_updated() { diff --git a/src/engine/ImapEngine.vala b/src/engine/ImapEngine.vala new file mode 100644 index 00000000..a1b1de98 --- /dev/null +++ b/src/engine/ImapEngine.vala @@ -0,0 +1,123 @@ +/* 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 class Geary.ImapEngine : Object, Geary.Account { + private NetworkAccount net; + private LocalAccount local; + + public ImapEngine(NetworkAccount net, LocalAccount local) { + this.net = net; + this.local = local; + } + + public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder, + Cancellable? cancellable = null) throws Error { + } + + public async void create_many_folders_async(Geary.Folder? parent, + Gee.Collection folders, Cancellable? cancellable = null) throws Error { + } + + public async Gee.Collection list_folders_async(Geary.Folder? parent, + Cancellable? cancellable = null) throws Error { + Gee.Collection local_list = yield local.list_folders_async(parent, cancellable); + + Gee.Collection engine_list = new Gee.ArrayList(); + foreach (Geary.Folder local_folder in local_list) + engine_list.add(new EngineFolder(net, local, local_folder)); + + background_update_folders.begin(parent, engine_list); + + debug("Reporting %d folders", engine_list.size); + + return engine_list; + } + + 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); + + return engine_folder; + } + + private Gee.Set get_folder_names(Gee.Collection folders) { + Gee.Set names = new Gee.HashSet(); + foreach (Geary.Folder folder in folders) + names.add(folder.get_name()); + + return names; + } + + private Gee.List get_excluded_folders(Gee.Collection folders, + Gee.Set names) { + Gee.List excluded = new Gee.ArrayList(); + foreach (Geary.Folder folder in folders) { + if (!names.contains(folder.get_name())) + excluded.add(folder); + } + + return excluded; + } + + private async void background_update_folders(Geary.Folder? parent, + Gee.Collection engine_folders) { + Gee.Collection net_folders; + try { + net_folders = yield net.list_folders_async(parent); + } catch (Error neterror) { + error("Unable to retrieve folder list from server: %s", neterror.message); + } + + Gee.Set local_names = get_folder_names(engine_folders); + Gee.Set net_names = get_folder_names(net_folders); + + debug("%d local names, %d net names", local_names.size, net_names.size); + + Gee.List? to_add = get_excluded_folders(net_folders, local_names); + Gee.List? to_remove = get_excluded_folders(engine_folders, net_names); + + debug("Adding %d, removing %d to/from local store", to_add.size, to_remove.size); + + if (to_add.size == 0) + to_add = null; + + if (to_remove.size == 0) + to_remove = null; + + try { + if (to_add != null) + yield local.create_many_folders_async(parent, to_add); + } catch (Error err) { + error("Unable to add/remove folders: %s", err.message); + } + + Gee.Collection engine_added = null; + if (to_add != null) { + engine_added = new Gee.ArrayList(); + foreach (Geary.Folder net_folder in to_add) { + try { + engine_added.add(new EngineFolder(net, local, + yield local.fetch_folder_async(parent, net_folder.get_name()))); + } catch (Error convert_err) { + error("Unable to fetch local folder: %s", convert_err.message); + } + } + } + + if (engine_added != null) + notify_folders_added_removed(engine_added, null); + } + + public async void remove_folder_async(Geary.Folder folder, Cancellable? cancellable = null) + throws Error { + } + + public async void remove_many_folders_async(Gee.Set folders, + Cancellable? cancellable = null) throws Error { + } +} + diff --git a/src/engine/api/Account.vala b/src/engine/api/Account.vala index f720817b..b8557e2a 100644 --- a/src/engine/api/Account.vala +++ b/src/engine/api/Account.vala @@ -13,29 +13,26 @@ public interface Geary.Account : Object { folders_added_removed(added, removed); } - public abstract async Gee.Collection list_async(string? parent_folder, + public abstract async void create_folder_async(Geary.Folder? parent, Geary.Folder folder, Cancellable? cancellable = null) throws Error; - public abstract async Geary.Folder fetch_async(string? parent_folder, string folder_name, + public abstract async void create_many_folders_async(Geary.Folder? parent, + Gee.Collection folders, Cancellable? cancellable = null) throws Error; + + public abstract async Gee.Collection list_folders_async(Geary.Folder? parent, Cancellable? cancellable = null) throws Error; - public abstract async void create_async(Geary.Folder folder, Cancellable? cancellable = null) - throws Error; - - public abstract async void create_many_async(Gee.Collection folders, + public abstract async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name, Cancellable? cancellable = null) throws Error; - public abstract async void remove_async(string folder, Cancellable? cancellable = null) + public abstract async void remove_folder_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error; - public abstract async void remove_many_async(Gee.Set folders, Cancellable? cancellable = null) - throws Error; + public abstract async void remove_many_folders_async(Gee.Set folders, + Cancellable? cancellable = null) throws Error; } public interface Geary.NetworkAccount : Object, Geary.Account { - public signal void connectivity_changed(bool online); - - public abstract bool is_online(); } public interface Geary.LocalAccount : Object, Geary.Account { diff --git a/src/engine/api/Email.vala b/src/engine/api/Email.vala index d6e22e9f..c0250449 100644 --- a/src/engine/api/Email.vala +++ b/src/engine/api/Email.vala @@ -4,39 +4,92 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.EmailHeader : Object { - public int msg_num { get; private set; } - public Geary.RFC822.MailboxAddresses from { get; private set; } - public Geary.RFC822.Subject subject { get; private set; } - public Geary.RFC822.Date sent { get; private set; } - - public EmailHeader(int msg_num, Geary.RFC822.MailboxAddresses from, Geary.RFC822.Subject subject, - Geary.RFC822.Date sent) { - this.msg_num = msg_num; - this.from = from; - this.subject = subject; - this.sent = sent; - } - - public string to_string() { - return "[%d] %s: %s (%s)".printf(msg_num, from.to_string(), subject.to_string(), sent.to_string()); - } -} - public class Geary.Email : Object { - public EmailHeader header { get; private set; } - public string full { get; private set; } - - public Email(EmailHeader header, string full) { - this.header = header; - this.full = full; + [Flags] + public enum Field { + NONE = 0, + DATE, + ORIGINATORS, + RECEIVERS, + REFERENCES, + SUBJECT, + HEADER, + BODY, + PROPERTIES, + ENVELOPE = DATE | ORIGINATORS | RECEIVERS | REFERENCES | SUBJECT, + ALL = 0xFFFFFFFF; + + public static Field[] all() { + return { + DATE, + ORIGINATORS, + RECEIVERS, + REFERENCES, + SUBJECT, + HEADER, + BODY, + PROPERTIES + }; + } + } + + public int msg_num { get; private set; } + + // DATE + public Geary.RFC822.Date? date = null; + + // ORIGINATORS + public Geary.RFC822.MailboxAddresses? from = null; + public Geary.RFC822.MailboxAddresses? sender = null; + public Geary.RFC822.MailboxAddresses? reply_to = null; + + // RECEIVERS + public Geary.RFC822.MailboxAddresses? to = null; + public Geary.RFC822.MailboxAddresses? cc = null; + public Geary.RFC822.MailboxAddresses? bcc = null; + + // REFERENCES + public Geary.RFC822.MessageID? message_id = null; + public Geary.RFC822.MessageID? in_reply_to = null; + + // SUBJECT + public Geary.RFC822.Subject? subject = null; + + // HEADER + public RFC822.Header? header = null; + + // BODY + public RFC822.Text? body = null; + + // PROPERTIES + public Geary.EmailProperties? properties = null; + + public Email(int msg_num) { + this.msg_num = msg_num; } - /** - * This does not return the full body or any portion of it. It's intended only for debugging. - */ public string to_string() { - return "%s (%d bytes)".printf(header.to_string(), full.data.length); + StringBuilder builder = new StringBuilder(); + + if (date != null) + builder.append_printf("[%s]", date.to_string()); + + if (from != null) + builder.append_printf("[From: %s]", from.to_string()); + + if (to != null) + builder.append_printf("[To: %s]", to.to_string()); + + if (subject != null) + builder.append_printf("[Subj: %s]", subject.to_string()); + + if (header != null) + builder.append_printf("[Header: %lub]", header.buffer.get_size()); + + if (body != null) + builder.append_printf("[Body: %lub]", body.buffer.get_size()); + + return builder.str; } } diff --git a/src/engine/api/EmailOrdering.vala b/src/engine/api/EmailOrdering.vala new file mode 100644 index 00000000..c9e7fd54 --- /dev/null +++ b/src/engine/api/EmailOrdering.vala @@ -0,0 +1,14 @@ +/* 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 class Geary.EmailOrdering { + public int64 ordinal { get; private set; } + + public EmailOrdering(int64 ordinal) { + this.ordinal = ordinal; + } +} + diff --git a/src/engine/api/EmailProperties.vala b/src/engine/api/EmailProperties.vala new file mode 100644 index 00000000..18aefa44 --- /dev/null +++ b/src/engine/api/EmailProperties.vala @@ -0,0 +1,9 @@ +/* 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 abstract class Geary.EmailProperties : Object { +} + diff --git a/src/engine/api/EngineError.vala b/src/engine/api/EngineError.vala index ec6c83db..90c62be0 100644 --- a/src/engine/api/EngineError.vala +++ b/src/engine/api/EngineError.vala @@ -7,6 +7,8 @@ public errordomain Geary.EngineError { OPEN_REQUIRED, ALREADY_OPEN, - NOT_FOUND + NOT_FOUND, + READONLY, + BAD_PARAMETERS } diff --git a/src/engine/api/Folder.vala b/src/engine/api/Folder.vala index 6ba16887..7451676f 100644 --- a/src/engine/api/Folder.vala +++ b/src/engine/api/Folder.vala @@ -17,16 +17,21 @@ public interface Geary.Folder : Object { public signal void updated(); + public virtual void notify_opened() { + opened(); + } + + public virtual void notify_closed(CloseReason reason) { + closed(reason); + } + + public virtual void notify_updated() { + updated(); + } + public abstract string get_name(); - // This is only for when a context has been selected - public abstract Trillian is_readonly(); - - public abstract Trillian does_support_children(); - - public abstract Trillian has_children(); - - public abstract Trillian is_openable(); + public abstract Geary.FolderProperties? get_properties(); public abstract async void open_async(bool readonly, Cancellable? cancellable = null) throws Error; @@ -34,10 +39,16 @@ public interface Geary.Folder : Object { public abstract int get_message_count() throws Error; - public abstract async Gee.List? read_async(int low, int count, + public abstract async void create_email_async(Geary.Email email, Geary.EmailOrdering ordering, Cancellable? cancellable = null) throws Error; - public abstract async Geary.Email fetch_async(Geary.EmailHeader header, + /** + * 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; + + public abstract async Geary.Email fetch_email_async(int msg_num, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error; } diff --git a/src/engine/api/FolderProperties.vala b/src/engine/api/FolderProperties.vala new file mode 100644 index 00000000..d5ad166c --- /dev/null +++ b/src/engine/api/FolderProperties.vala @@ -0,0 +1,9 @@ +/* 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 abstract class Geary.FolderProperties : Object { +} + diff --git a/src/engine/imap/Email.vala b/src/engine/imap/Email.vala deleted file mode 100644 index 2449366c..00000000 --- a/src/engine/imap/Email.vala +++ /dev/null @@ -1,18 +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. - */ - -public class Geary.Imap.EmailHeader : Geary.EmailHeader { - public EmailHeader(int msg_num, Envelope envelope) { - base (msg_num, envelope.from, envelope.subject, envelope.sent); - } -} - -public class Geary.Imap.Email : Geary.Email { - public Email(EmailHeader header, string full) { - base (header, full); - } -} - diff --git a/src/engine/imap/Mailbox.vala b/src/engine/imap/Mailbox.vala index a3f04c7a..2676a0c6 100644 --- a/src/engine/imap/Mailbox.vala +++ b/src/engine/imap/Mailbox.vala @@ -8,6 +8,7 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { public string name { get; private set; } public int count { get; private set; } public bool is_readonly { get; private set; } + public UID uid_validity { get; private set; } private SelectedContext context; @@ -26,42 +27,44 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { name = context.name; count = context.exists; is_readonly = context.is_readonly; + uid_validity = context.uid_validity; } - public async Gee.List? read(int low, int count, Cancellable? cancellable = null) - throws Error { + public async Gee.List? list_async(int low, int count, Geary.Email.Field fields, + Cancellable? cancellable = null) throws Error { if (context.is_closed()) throw new ImapError.NOT_SELECTED("Mailbox %s closed", name); + if (fields == Geary.Email.Field.NONE) + throw new EngineError.BAD_PARAMETERS("No email fields specify for list"); + CommandResponse resp = yield context.session.send_command_async( new FetchCommand(context.session.generate_tag(), new MessageSet.range(low, count), - { FetchDataType.ENVELOPE }), cancellable); + fields_to_fetch_data_types(fields)), cancellable); if (resp.status_response.status != Status.OK) throw new ImapError.SERVER_ERROR("Server error: %s", resp.to_string()); - Gee.List msgs = new Gee.ArrayList(); + Gee.List msgs = new Gee.ArrayList(); FetchResults[] results = FetchResults.decode(resp); foreach (FetchResults res in results) { - Envelope envelope = (Envelope) res.get_data(FetchDataType.ENVELOPE); - msgs.add(new EmailHeader(res.msg_num, envelope)); + Geary.Email email = new Geary.Email(res.msg_num); + fetch_results_to_email(res, fields, email); + msgs.add(email); } return msgs; } - public async Geary.Email fetch(Geary.EmailHeader hdr, Cancellable? cancellable = null) - throws Error { - Geary.Imap.EmailHeader? header = hdr as Geary.Imap.EmailHeader; - assert(header != null); - + public async Geary.Email fetch_async(int msg_num, Geary.Email.Field fields, + Cancellable? cancellable = null) throws Error { if (context.is_closed()) throw new ImapError.NOT_SELECTED("Mailbox %s closed", name); CommandResponse resp = yield context.session.send_command_async( - new FetchCommand(context.session.generate_tag(), new MessageSet(hdr.msg_num), - { FetchDataType.RFC822_TEXT }), cancellable); + new FetchCommand(context.session.generate_tag(), new MessageSet(msg_num), + fields_to_fetch_data_types(fields)), cancellable); if (resp.status_response.status != Status.OK) throw new ImapError.SERVER_ERROR("Server error: %s", resp.to_string()); @@ -70,9 +73,15 @@ 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.RFC822.Text text = (Geary.RFC822.Text) results[0].get_data(FetchDataType.RFC822_TEXT); + if (results[0].msg_num != msg_num) { + throw new ImapError.SERVER_ERROR("Server returns message #%d, requested %d", + results[0].msg_num, msg_num); + } - return new Email(header, text.buffer.to_ascii_string()); + Geary.Email email = new Geary.Email(msg_num); + fetch_results_to_email(results[0], fields, email); + + return email; } private void on_exists_changed(int exists) { @@ -86,6 +95,102 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { private void on_disconnected(bool local) { disconnected(local); } + + private static FetchDataType[] fields_to_fetch_data_types(Geary.Email.Field fields) { + Gee.HashSet data_type_set = new Gee.HashSet(); + foreach (Geary.Email.Field field in Geary.Email.Field.all()) { + switch (fields & field) { + case Geary.Email.Field.DATE: + case Geary.Email.Field.ORIGINATORS: + case Geary.Email.Field.RECEIVERS: + case Geary.Email.Field.REFERENCES: + case Geary.Email.Field.SUBJECT: + data_type_set.add(FetchDataType.ENVELOPE); + break; + + case Geary.Email.Field.HEADER: + data_type_set.add(FetchDataType.RFC822_HEADER); + break; + + case Geary.Email.Field.BODY: + data_type_set.add(FetchDataType.RFC822_TEXT); + break; + + case Geary.Email.Field.PROPERTIES: + data_type_set.add(FetchDataType.FLAGS); + break; + + case Geary.Email.Field.NONE: + // not set + break; + + default: + assert_not_reached(); + } + } + + assert(data_type_set.size > 0); + FetchDataType[] data_types = new FetchDataType[data_type_set.size]; + int ctr = 0; + foreach (FetchDataType data_type in data_type_set) + data_types[ctr++] = data_type; + + return data_types; + } + + private static void fetch_results_to_email(FetchResults res, Geary.Email.Field fields, + Geary.Email email) { + foreach (FetchDataType data_type in res.get_all_types()) { + MessageData? data = res.get_data(data_type); + if (data == null) + continue; + + switch (data_type) { + case FetchDataType.ENVELOPE: + Envelope envelope = (Envelope) data; + + if ((fields & Geary.Email.Field.DATE) != 0) + email.date = envelope.sent; + + if ((fields & Geary.Email.Field.SUBJECT) != 0) + email.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.RECEIVERS) != 0) { + email.to = envelope.to; + email.cc = envelope.cc; + email.bcc = envelope.bcc; + } + + if ((fields & Geary.Email.Field.REFERENCES) != 0) { + email.in_reply_to = envelope.in_reply_to; + email.message_id = envelope.message_id; + } + break; + + case FetchDataType.RFC822_HEADER: + email.header = (RFC822.Header) data; + break; + + case FetchDataType.RFC822_TEXT: + email.body = (RFC822.Text) data; + break; + + case FetchDataType.FLAGS: + email.properties = new Imap.EmailProperties((MessageFlags) data); + break; + + default: + // everything else dropped on the floor (not applicable to Geary.Email) + break; + } + } + } } internal class Geary.Imap.SelectedContext : Object, Geary.ReferenceSemantics { @@ -97,6 +202,7 @@ internal class Geary.Imap.SelectedContext : Object, Geary.ReferenceSemantics { public int exists { get; protected set; } public int recent { get; protected set; } public bool is_readonly { get; protected set; } + public UID uid_validity { get; protected set; } public signal void exists_changed(int exists); @@ -113,6 +219,7 @@ internal class Geary.Imap.SelectedContext : Object, Geary.ReferenceSemantics { is_readonly = results.readonly; exists = results.exists; recent = results.recent; + uid_validity = results.uid_validity; session.current_mailbox_changed.connect(on_session_mailbox_changed); session.unsolicited_exists.connect(on_unsolicited_exists); diff --git a/src/engine/imap/MessageData.vala b/src/engine/imap/MessageData.vala index 1fe54011..42cf68e6 100644 --- a/src/engine/imap/MessageData.vala +++ b/src/engine/imap/MessageData.vala @@ -11,8 +11,7 @@ * * Note that IMAP specifies that Flags and Attributes are *always* returned as a list, even if only * one is present, which is why these elements are MessageData but not the elements within the - * lists (Flag, Attribute). Obviously these classes are closely related, hence their presence - * here. + * lists (Flag, Attribute). * * Also note that Imap.MessageData inherits from Common.MessageData. */ diff --git a/src/engine/imap/api/Account.vala b/src/engine/imap/api/Account.vala index 8e0ea796..702fd9d2 100644 --- a/src/engine/imap/api/Account.vala +++ b/src/engine/imap/api/Account.vala @@ -11,13 +11,20 @@ public class Geary.Imap.Account : Object, Geary.Account, Geary.NetworkAccount { session_mgr = new ClientSessionManager(cred, default_port); } - public bool is_online() { - return true; + public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder, + Cancellable? cancellable = null) throws Error { + throw new EngineError.READONLY("IMAP readonly"); } - public async Gee.Collection list_async(string? parent_folder, + public async void create_many_folders_async(Geary.Folder? parent, Gee.Collection folders, Cancellable? cancellable = null) throws Error { - Gee.Collection mboxes = yield session_mgr.list(parent_folder, cancellable); + throw new EngineError.READONLY("IMAP readonly"); + } + + public async Gee.Collection list_folders_async(Geary.Folder? parent, + Cancellable? cancellable = null) throws Error { + Gee.Collection mboxes = yield session_mgr.list( + (parent != null) ? parent.get_name() : null, cancellable); Gee.Collection folders = new Gee.ArrayList(); foreach (MailboxInformation mbox in mboxes) @@ -26,32 +33,24 @@ public class Geary.Imap.Account : Object, Geary.Account, Geary.NetworkAccount { return folders; } - public async Geary.Folder fetch_async(string? parent_folder, string folder_name, + public async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name, Cancellable? cancellable = null) throws Error { - MailboxInformation? mbox = yield session_mgr.fetch_async(parent_folder, folder_name, - cancellable); + MailboxInformation? mbox = yield session_mgr.fetch_async( + (parent != null) ? parent.get_name() : null, folder_name, cancellable); if (mbox == null) throw new EngineError.NOT_FOUND("Folder %s not found on server", folder_name); return new Geary.Imap.Folder(session_mgr, mbox); } - public async void create_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error { - // TODO - } - - public async void create_many_async(Gee.Collection folders, - Cancellable? cancellable = null) throws Error { - // TODO - } - - public async void remove_async(string folder, Cancellable? cancellable = null) throws Error { - // TODO - } - - public async void remove_many_async(Gee.Set folders, Cancellable? cancellable = null) + public async void remove_folder_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error { - // TODO + throw new EngineError.READONLY("IMAP readonly"); + } + + public async void remove_many_folders_async(Gee.Set folders, + Cancellable? cancellable = null) throws Error { + throw new EngineError.READONLY("IMAP readonly"); } } diff --git a/src/engine/imap/api/EmailProperties.vala b/src/engine/imap/api/EmailProperties.vala new file mode 100644 index 00000000..48559b21 --- /dev/null +++ b/src/engine/imap/api/EmailProperties.vala @@ -0,0 +1,14 @@ +/* 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 class Geary.Imap.EmailProperties : Geary.EmailProperties { + public MessageFlags flags { get; private set; } + + public EmailProperties(MessageFlags flags) { + this.flags = flags; + } +} + diff --git a/src/engine/imap/api/Folder.vala b/src/engine/imap/api/Folder.vala index 27290a6b..2518897e 100644 --- a/src/engine/imap/api/Folder.vala +++ b/src/engine/imap/api/Folder.vala @@ -9,9 +9,7 @@ public class Geary.Imap.Folder : Object, Geary.Folder { private MailboxInformation info; private string name; private Trillian readonly; - private Trillian supports_children; - private Trillian children; - private Trillian openable; + private Imap.FolderProperties properties; private Mailbox? mailbox = null; internal Folder(ClientSessionManager session_mgr, MailboxInformation info) { @@ -20,11 +18,7 @@ public class Geary.Imap.Folder : Object, Geary.Folder { name = info.name; readonly = Trillian.UNKNOWN; - supports_children = Trillian.from_boolean(!info.attrs.contains(MailboxAttribute.NO_INFERIORS)); - // \HasNoChildren is an optional attribute and lack of presence doesn't indiciate anything - children = info.attrs.contains(MailboxAttribute.HAS_NO_CHILDREN) ? Trillian.TRUE - : Trillian.UNKNOWN; - openable = Trillian.from_boolean(!info.attrs.contains(MailboxAttribute.NO_SELECT)); + properties = new Imap.FolderProperties(null, info.attrs); } public string get_name() { @@ -35,16 +29,8 @@ public class Geary.Imap.Folder : Object, Geary.Folder { return readonly; } - public Trillian does_support_children() { - return supports_children; - } - - public Trillian has_children() { - return children; - } - - public Trillian is_openable() { - return openable; + public Geary.FolderProperties? get_properties() { + return properties; } public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { @@ -55,11 +41,13 @@ public class Geary.Imap.Folder : Object, Geary.Folder { // hook up signals this.readonly = Trillian.from_boolean(readonly); + properties.uid_validity = mailbox.uid_validity; } public async void close_async(Cancellable? cancellable = null) throws Error { mailbox = null; readonly = Trillian.UNKNOWN; + properties.uid_validity = null; } public int get_message_count() throws Error { @@ -69,20 +57,25 @@ public class Geary.Imap.Folder : Object, Geary.Folder { return mailbox.count; } - public async Gee.List? read_async(int low, int count, + public async void create_email_async(Geary.Email email, Geary.EmailOrdering ordring, Cancellable? cancellable = null) throws Error { - if (mailbox == null) - throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); - - return yield mailbox.read(low, count, cancellable); + throw new EngineError.READONLY("IMAP currently read-only"); } - public async Geary.Email fetch_async(Geary.EmailHeader header, + public async Gee.List list_email_async(int low, int count, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); - return yield mailbox.fetch(header, cancellable); + return yield mailbox.list_async(low, count, fields, cancellable); + } + + public async Geary.Email fetch_email_async(int msg_num, Geary.Email.Field fields, + Cancellable? cancellable = null) throws Error { + if (mailbox == null) + throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); + + return yield mailbox.fetch_async(msg_num, fields, cancellable); } public string to_string() { diff --git a/src/engine/imap/api/FolderProperties.vala b/src/engine/imap/api/FolderProperties.vala new file mode 100644 index 00000000..9407c498 --- /dev/null +++ b/src/engine/imap/api/FolderProperties.vala @@ -0,0 +1,25 @@ +/* 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 class Geary.Imap.FolderProperties : Geary.FolderProperties { + public UID? uid_validity { get; set; } + public MailboxAttributes attrs { get; private set; } + public Trillian supports_children { get; private set; } + public Trillian has_children { get; private set; } + public Trillian is_openable { get; private set; } + + public FolderProperties(UID? uid_validity, MailboxAttributes attrs) { + this.uid_validity = uid_validity; + this.attrs = attrs; + + supports_children = Trillian.from_boolean(!attrs.contains(MailboxAttribute.NO_INFERIORS)); + // \HasNoChildren is an optional attribute and lack of presence doesn't indiciate anything + supports_children = attrs.contains(MailboxAttribute.HAS_NO_CHILDREN) ? Trillian.TRUE + : Trillian.UNKNOWN; + is_openable = Trillian.from_boolean(!attrs.contains(MailboxAttribute.NO_SELECT)); + } +} + diff --git a/src/engine/imap/decoders/FetchResults.vala b/src/engine/imap/decoders/FetchResults.vala index 06083bee..5c4ac966 100644 --- a/src/engine/imap/decoders/FetchResults.vala +++ b/src/engine/imap/decoders/FetchResults.vala @@ -73,6 +73,10 @@ public class Geary.Imap.FetchResults : Geary.Imap.CommandResults { return array; } + public Gee.Set get_all_types() { + return map.keys; + } + public void set_data(FetchDataType data_item, MessageData primitive) { map.set(data_item, primitive); } diff --git a/src/engine/imap/decoders/SelectExamineResults.vala b/src/engine/imap/decoders/SelectExamineResults.vala index 2fdaa07e..9afaaeb6 100644 --- a/src/engine/imap/decoders/SelectExamineResults.vala +++ b/src/engine/imap/decoders/SelectExamineResults.vala @@ -17,7 +17,7 @@ public class Geary.Imap.SelectExamineResults : Geary.Imap.CommandResults { * -1 if not specified. */ public int unseen { get; private set; } - public UID? uidvalidity { get; private set; } + public UID? uid_validity { get; private set; } public Flags? flags { get; private set; } public Flags? permanentflags { get; private set; } public bool readonly { get; private set; } @@ -29,7 +29,7 @@ public class Geary.Imap.SelectExamineResults : Geary.Imap.CommandResults { this.exists = exists; this.recent = recent; this.unseen = unseen; - this.uidvalidity = uidvalidity; + this.uid_validity = uid_validity; this.flags = flags; this.permanentflags = permanentflags; this.readonly = readonly; diff --git a/src/engine/rfc822/MessageData.vala b/src/engine/rfc822/MessageData.vala index ecbd4195..d4fae919 100644 --- a/src/engine/rfc822/MessageData.vala +++ b/src/engine/rfc822/MessageData.vala @@ -22,13 +22,14 @@ public class Geary.RFC822.MessageID : Geary.Common.StringMessageData, Geary.RFC8 public class Geary.RFC822.Date : Geary.RFC822.MessageData, Geary.Common.MessageData { public string original { get; private set; } public DateTime value { get; private set; } + public time_t as_time_t { get; private set; } public Date(string iso8601) throws ImapError { - time_t tm = GMime.utils_header_decode_date(iso8601, null); - if (tm == 0) + as_time_t = GMime.utils_header_decode_date(iso8601, null); + if (as_time_t == 0) throw new ImapError.PARSE_ERROR("Unable to parse \"%s\": not ISO-8601 date", iso8601); - value = new DateTime.from_unix_local(tm); + value = new DateTime.from_unix_local(as_time_t); original = iso8601; } @@ -50,18 +51,20 @@ public class Geary.RFC822.Subject : Geary.Common.StringMessageData, Geary.RFC822 } public class Geary.RFC822.MailboxAddresses : Geary.Common.MessageData, Geary.RFC822.MessageData { + public int size { get { return addrs.size; } } + private Gee.List addrs = new Gee.ArrayList(); public MailboxAddresses(Gee.Collection addrs) { this.addrs.add_all(addrs); } - public int get_count() { - return addrs.size; + public MailboxAddress? get(int index) { + return addrs.get(index); } - public MailboxAddress? get_at(int index) { - return addrs.get(index); + public Gee.Iterator iterator() { + return addrs.iterator(); } public Gee.List get_all() { diff --git a/src/engine/sqlite/Database.vala b/src/engine/sqlite/Database.vala index 859088df..f05dba28 100644 --- a/src/engine/sqlite/Database.vala +++ b/src/engine/sqlite/Database.vala @@ -15,6 +15,7 @@ public abstract class Geary.Sqlite.Database { db_file.get_parent().make_directory_with_parents(); db = new SQLHeavy.VersionedDatabase(db_file.get_path(), schema_dir.get_path()); + db.foreign_keys = true; } protected Geary.Sqlite.Table? get_table(string name, out SQLHeavy.Table heavy_table) { @@ -27,8 +28,10 @@ public abstract class Geary.Sqlite.Database { return table_map.get(heavy_table); } - protected void add_table(Geary.Sqlite.Table table) { + protected Geary.Sqlite.Table add_table(Geary.Sqlite.Table table) { table_map.set(table.table, table); + + return table; } } diff --git a/src/engine/sqlite/FolderRow.vala b/src/engine/sqlite/FolderRow.vala index f5b8ee99..6ac91381 100644 --- a/src/engine/sqlite/FolderRow.vala +++ b/src/engine/sqlite/FolderRow.vala @@ -7,27 +7,22 @@ public class Geary.Sqlite.FolderRow : Geary.Sqlite.Row { public int64 id { get; private set; } public string name { get; private set; } - public Trillian supports_children { get; private set; } - public Trillian is_openable { get; private set; } public int64 parent_id { get; private set; } - public FolderRow(string name, Trillian supports_children, Trillian is_openable, - int64 parent_id = INVALID_ID) { - this.id = -1; + public FolderRow(FolderTable table, string name, int64 parent_id) { + base (table); + + this.id = INVALID_ID; this.name = name; - this.supports_children = supports_children; - this.is_openable = is_openable; this.parent_id = parent_id; } - public FolderRow.from_query_result(SQLHeavy.QueryResult result) throws Error { - id = fetch_int64_for(result, FolderTable.Column.ID.colname()); - name = fetch_string_for(result, FolderTable.Column.NAME.colname()); - supports_children = Trillian.from_int(fetch_int_for(result, - FolderTable.Column.SUPPORTS_CHILDREN.colname())); - is_openable = Trillian.from_int(fetch_int_for(result, - FolderTable.Column.IS_OPENABLE.colname())); - parent_id = fetch_int64_for(result, FolderTable.Column.PARENT_ID.colname()); + public FolderRow.from_query_result(FolderTable table, SQLHeavy.QueryResult result) throws Error { + base (table); + + id = fetch_int64_for(result, FolderTable.Column.ID); + name = fetch_string_for(result, FolderTable.Column.NAME); + parent_id = fetch_int64_for(result, FolderTable.Column.PARENT_ID); } } diff --git a/src/engine/sqlite/FolderTable.vala b/src/engine/sqlite/FolderTable.vala index 03722a3e..60b65f21 100644 --- a/src/engine/sqlite/FolderTable.vala +++ b/src/engine/sqlite/FolderTable.vala @@ -9,69 +9,17 @@ public class Geary.Sqlite.FolderTable : Geary.Sqlite.Table { public enum Column { ID, NAME, - SUPPORTS_CHILDREN, - IS_OPENABLE, - PARENT_ID; - - public string colname() { - switch (this) { - case ID: - return "id"; - - case NAME: - return "name"; - - case SUPPORTS_CHILDREN: - return "supports_children"; - - case IS_OPENABLE: - return "is_openable"; - - case PARENT_ID: - return "parent_id"; - - default: - assert_not_reached(); - } - } + PARENT_ID } internal FolderTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { base (gdb, table); } - public async Gee.List list_async(int64 parent_id, Cancellable? cancellable = null) - throws Error { - SQLHeavy.Query query = db.prepare("SELECT * FROM FolderTable WHERE parent_id=?"); - query.bind_int64(0, parent_id); - - SQLHeavy.QueryResult result = yield query.execute_async(cancellable); - - Gee.List rows = new Gee.ArrayList(); - while (!result.finished) { - rows.add(new FolderRow.from_query_result(result)); - - yield result.next_async(cancellable); - } - - return rows; - } - - public async FolderRow? fetch_async(int64 parent_id, string name, Cancellable? cancellable = null) - throws Error { - SQLHeavy.Query query = db.prepare("SELECT * FROM FolderTable WHERE parent_id=? AND name=?"); - query.bind_int64(0, parent_id); - query.bind_string(1, name); - - SQLHeavy.QueryResult result = yield query.execute_async(cancellable); - - return (!result.finished) ? new FolderRow.from_query_result(result) : null; - } - private SQLHeavy.Query create_query(SQLHeavy.Queryable? queryable = null) throws SQLHeavy.Error { SQLHeavy.Queryable q = queryable ?? db; SQLHeavy.Query query = q.prepare( - "INSERT INTO FolderTable (name, supports_children, is_openable, parent_id) VALUES (?, ?, ?, ?)"); + "INSERT INTO FolderTable (name, parent_id) VALUES (?, ?)"); return query; } @@ -79,9 +27,10 @@ public class Geary.Sqlite.FolderTable : Geary.Sqlite.Table { private void create_binding(SQLHeavy.Query query, FolderRow row) throws SQLHeavy.Error { query.clear(); query.bind_string(0, row.name); - query.bind_int(1, row.supports_children.to_int()); - query.bind_int(2, row.is_openable.to_int()); - query.bind_int64(3, row.parent_id); + if (row.parent_id != Row.INVALID_ID) + query.bind_int64(1, row.parent_id); + else + query.bind_null(1); } public async void create_async(FolderRow row, Cancellable? cancellable = null) throws Error { @@ -93,16 +42,50 @@ public class Geary.Sqlite.FolderTable : Geary.Sqlite.Table { public async void create_many_async(Gee.Collection rows, Cancellable? cancellable = null) throws Error { - SQLHeavy.Transaction transaction = db.begin_transaction(); - - SQLHeavy.Query query = create_query(transaction); + SQLHeavy.Query query = create_query(); foreach (FolderRow row in rows) { create_binding(query, row); query.execute_insert(); } + } + + public async Gee.List list_async(int64 parent_id, Cancellable? cancellable = null) + throws Error { + SQLHeavy.Query query; + if (parent_id != Row.INVALID_ID) { + query = db.prepare("SELECT * FROM FolderTable WHERE parent_id=?"); + query.bind_int64(0, parent_id); + } else { + query = db.prepare("SELECT * FROM FolderTable WHERE parent_id IS NULL"); + } - // TODO: Need an async transaction commit - transaction.commit(); + SQLHeavy.QueryResult result = yield query.execute_async(cancellable); + + Gee.List rows = new Gee.ArrayList(); + while (!result.finished) { + rows.add(new FolderRow.from_query_result(this, result)); + + yield result.next_async(cancellable); + } + + return rows; + } + + public async FolderRow? fetch_async(int64 parent_id, string name, Cancellable? cancellable = null) + throws Error { + SQLHeavy.Query query; + if (parent_id != Row.INVALID_ID) { + query = db.prepare("SELECT * FROM FolderTable WHERE parent_id=? AND name=?"); + query.bind_int64(0, parent_id); + query.bind_string(1, name); + } else { + query = db.prepare("SELECT * FROM FolderTable WHERE name=? AND parent_id IS NULL"); + query.bind_string(0, name); + } + + SQLHeavy.QueryResult result = yield query.execute_async(cancellable); + + return (!result.finished) ? new FolderRow.from_query_result(this, result) : null; } } diff --git a/src/engine/sqlite/MailDatabase.vala b/src/engine/sqlite/MailDatabase.vala index 9b06375c..028c3f64 100644 --- a/src/engine/sqlite/MailDatabase.vala +++ b/src/engine/sqlite/MailDatabase.vala @@ -15,13 +15,29 @@ public class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database { public Geary.Sqlite.FolderTable get_folder_table() { SQLHeavy.Table heavy_table; FolderTable? folder_table = get_table("FolderTable", out heavy_table) as FolderTable; - if (folder_table != null) - return folder_table; - folder_table = new FolderTable(this, heavy_table); - add_table(folder_table); + return (folder_table != null) + ? folder_table + : (FolderTable) add_table(new FolderTable(this, heavy_table)); + } + + public Geary.Sqlite.MessageTable get_message_table() { + SQLHeavy.Table heavy_table; + MessageTable? message_table = get_table("MessageTable", out heavy_table) as MessageTable; - return folder_table; + return (message_table != null) + ? message_table + : (MessageTable) add_table(new MessageTable(this, heavy_table)); + } + + public Geary.Sqlite.MessageLocationTable get_message_location_table() { + SQLHeavy.Table heavy_table; + MessageLocationTable? location_table = get_table("MessageLocationTable", out heavy_table) + as MessageLocationTable; + + return (location_table != null) + ? location_table + : (MessageLocationTable) add_table(new MessageLocationTable(this, heavy_table)); } } diff --git a/src/engine/sqlite/MessageLocationRow.vala b/src/engine/sqlite/MessageLocationRow.vala new file mode 100644 index 00000000..a6bda62f --- /dev/null +++ b/src/engine/sqlite/MessageLocationRow.vala @@ -0,0 +1,33 @@ +/* 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 class Geary.Sqlite.MessageLocationRow : Geary.Sqlite.Row { + public int64 id { get; private set; } + public int64 message_id { get; private set; } + public int64 folder_id { get; private set; } + public int64 ordering { get; private set; } + + public MessageLocationRow(MessageLocationTable table, int64 id, int64 message_id, int64 folder_id, + int64 ordering) { + base (table); + + this.id = id; + this.message_id = message_id; + this.folder_id = folder_id; + this.ordering = ordering; + } + + public MessageLocationRow.from_query_result(MessageLocationTable table, + SQLHeavy.QueryResult result) throws Error { + base (table); + + id = fetch_int64_for(result, MessageLocationTable.Column.ID); + message_id = fetch_int64_for(result, MessageLocationTable.Column.MESSAGE_ID); + folder_id = fetch_int64_for(result, MessageLocationTable.Column.FOLDER_ID); + ordering = fetch_int64_for(result, MessageLocationTable.Column.ORDERING); + } +} + diff --git a/src/engine/sqlite/MessageLocationTable.vala b/src/engine/sqlite/MessageLocationTable.vala new file mode 100644 index 00000000..bdc09d99 --- /dev/null +++ b/src/engine/sqlite/MessageLocationTable.vala @@ -0,0 +1,80 @@ +/* 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 class Geary.Sqlite.MessageLocationTable : Geary.Sqlite.Table { + // This row *must* match the order in the schema + public enum Column { + ID, + MESSAGE_ID, + FOLDER_ID, + ORDERING + } + + public MessageLocationTable(Geary.Sqlite.Database db, SQLHeavy.Table table) { + base (db, table); + } + + public async int64 create_async(MessageLocationRow row, Cancellable? cancellable = null) + throws Error { + SQLHeavy.Query query = db.prepare( + "INSERT INTO MessageLocationTable (message_id, folder_id, ordering) VALUES (?, ?, ?)"); + query.bind_int64(0, row.message_id); + query.bind_int64(1, row.folder_id); + query.bind_int64(2, row.ordering); + + return yield query.execute_insert_async(cancellable); + } + + /** + * low is zero-based. + */ + public async Gee.List? list_async(int64 folder_id, int low, int count, + Cancellable? cancellable = null) throws Error { + assert(low >= 0); + + SQLHeavy.Query query = db.prepare( + "SELECT id, message_id, ordering FROM MessageLocationTable WHERE folder_id = ? " + + "LIMIT ? OFFSET ? ORDER BY ordering"); + query.bind_int64(0, folder_id); + query.bind_int(1, count); + query.bind_int(2, low); + + SQLHeavy.QueryResult results = yield query.execute_async(cancellable); + if (results.finished) + return null; + + Gee.List list = new Gee.ArrayList(); + do { + list.add(new MessageLocationRow(this, results.fetch_int64(0), results.fetch_int64(1), + folder_id, results.fetch_int64(2))); + yield results.next_async(cancellable); + } while (!results.finished); + + return list; + } + + /** + * num is zero-based. + */ + public async MessageLocationRow? fetch_async(int64 folder_id, int num, + Cancellable? cancellable = null) throws Error { + assert(num >= 0); + + SQLHeavy.Query query = db.prepare( + "SELECT id, message_id, ordering FROM MessageLocationTable WHERE folder_id = ? " + + "LIMIT 1 OFFSET ? ORDER BY ordering"); + query.bind_int64(0, folder_id); + query.bind_int64(1, num); + + SQLHeavy.QueryResult results = yield query.execute_async(cancellable); + if (results.finished) + return null; + + return new MessageLocationRow(this, results.fetch_int64(0), results.fetch_int64(1), folder_id, + results.fetch_int64(2)); + } +} + diff --git a/src/engine/sqlite/MessageRow.vala b/src/engine/sqlite/MessageRow.vala new file mode 100644 index 00000000..17d9c967 --- /dev/null +++ b/src/engine/sqlite/MessageRow.vala @@ -0,0 +1,154 @@ +/* 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 class Geary.Sqlite.MessageRow : Geary.Sqlite.Row { + public int64 id { get; set; default = INVALID_ID; } + + public string? date { get; set; } + public time_t date_time_t { get; set; default = -1; } + + public string? from { get; set; } + public string? sender { get; set; } + public string? reply_to { get; set; } + + public string? to { get; set; } + public string? cc { get; set; } + public string? bcc { get; set; } + + public string? message_id { get; set; } + public string? in_reply_to { get; set; } + + public string? subject { get; set; } + + public string? header { get; set; } + + public string? body { get; set; } + + public MessageRow(Table table) { + base (table); + } + + 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; + } + + public MessageRow.from_query_result(Table table, Geary.Email.Field fields, SQLHeavy.QueryResult result) + throws Error { + base (table); + + id = fetch_int64_for(result, MessageTable.Column.ID); + + 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); + } + + if ((fields & Geary.Email.Field.ORIGINATORS) != 0) { + from = fetch_string_for(result, MessageTable.Column.FROM_FIELD); + sender = fetch_string_for(result, MessageTable.Column.SENDER); + reply_to = fetch_string_for(result, MessageTable.Column.REPLY_TO); + } + + if ((fields & Geary.Email.Field.RECEIVERS) != 0) { + to = fetch_string_for(result, MessageTable.Column.TO_FIELD); + cc = fetch_string_for(result, MessageTable.Column.CC); + bcc = fetch_string_for(result, MessageTable.Column.BCC); + } + + if ((fields & Geary.Email.Field.REFERENCES) != 0) { + message_id = fetch_string_for(result, MessageTable.Column.MESSAGE_ID); + in_reply_to = fetch_string_for(result, MessageTable.Column.IN_REPLY_TO); + } + + if ((fields & Geary.Email.Field.SUBJECT) != 0) + subject = fetch_string_for(result, MessageTable.Column.SUBJECT); + + if ((fields & Geary.Email.Field.HEADER) != 0) + header = fetch_string_for(result, MessageTable.Column.HEADER); + + if ((fields & Geary.Email.Field.BODY) != 0) + body = fetch_string_for(result, MessageTable.Column.BODY); + } + + public Geary.Email to_email(int msg_num) throws Error { + Geary.Email email = new Geary.Email(msg_num); + + email.date = (date != null) ? new RFC822.Date(date) : null; + + email.from = unflatten_addresses(from); + email.sender = unflatten_addresses(sender); + email.reply_to = unflatten_addresses(reply_to); + + email.to = unflatten_addresses(to); + email.cc = unflatten_addresses(cc); + email.bcc = 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; + + email.subject = (subject != null) ? new RFC822.Subject(subject) : null; + + email.header = (header != null) ? new RFC822.Header(new Geary.Memory.StringBuffer(header)) + : null; + + email.body = (body != null) ? new RFC822.Text(new Geary.Memory.StringBuffer(body)) + : null; + + return email; + } + + public string? flatten_addresses(RFC822.MailboxAddresses? addrs) { + if (addrs == null) + return null; + + switch (addrs.size) { + case 0: + return null; + + case 1: + return addrs[0].get_full_address(); + + default: + StringBuilder builder = new StringBuilder(); + foreach (RFC822.MailboxAddress addr in addrs) { + if (!String.is_empty(builder.str)) + builder.append(", "); + + builder.append(addr.get_full_address()); + } + + return builder.str; + } + } + + public RFC822.MailboxAddresses? unflatten_addresses(string? str) { + if (str == null) + return null; + + return null; + } +} + diff --git a/src/engine/sqlite/MessageTable.vala b/src/engine/sqlite/MessageTable.vala new file mode 100644 index 00000000..49a771b9 --- /dev/null +++ b/src/engine/sqlite/MessageTable.vala @@ -0,0 +1,143 @@ +/* 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 class Geary.Sqlite.MessageTable : Geary.Sqlite.Table { + // This *must* match the column order in the database + public enum Column { + ID, + + DATE_FIELD, + DATE_INT64, + + FROM_FIELD, + SENDER, + REPLY_TO, + + TO_FIELD, + CC, + BCC, + + MESSAGE_ID, + IN_REPLY_TO, + + SUBJECT, + + HEADER, + + BODY; + } + + internal MessageTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { + base (gdb, 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); + + return yield query.execute_insert_async(cancellable); + } + + 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))); + query.bind_string(0, message_id.value); + + SQLHeavy.QueryResult results = yield query.execute_async(cancellable); + if (results.finished) + return null; + + Gee.List list = new Gee.ArrayList(); + do { + list.add(new MessageRow.from_query_result(this, fields, results)); + yield results.next_async(cancellable); + } while (!results.finished); + + return (list.size > 0) ? list : null; + } + + public async MessageRow? fetch_async(int64 id, Geary.Email.Field fields, + Cancellable? cancellable = null) throws Error { + assert(fields != Geary.Email.Field.NONE); + + SQLHeavy.Query query = db.prepare( + "SELECT %s FROM MessageTable WHERE id = ?".printf(fields_to_columns(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); + row.id = id; + + return row; + } + + private static string fields_to_columns(Geary.Email.Field fields) { + StringBuilder builder = new StringBuilder(); + foreach (Geary.Email.Field field in Geary.Email.Field.all()) { + string? append = null; + switch (field) { + case Geary.Email.Field.DATE: + append = "date_field, date_time_t"; + break; + + case Geary.Email.Field.ORIGINATORS: + append = "from_field, sender, reply_to"; + break; + + case Geary.Email.Field.RECEIVERS: + append = "to_field, cc, bcc"; + break; + + case Geary.Email.Field.REFERENCES: + append = "message_id, in_reply_to"; + break; + + case Geary.Email.Field.SUBJECT: + append = "subject"; + break; + + case Geary.Email.Field.HEADER: + append = "header"; + break; + + case Geary.Email.Field.BODY: + append = "body"; + break; + } + + if (append != null) { + if (!String.is_empty(builder.str)) + builder.append(", "); + + builder.append(append); + } + } + + return builder.str; + } +} + diff --git a/src/engine/sqlite/Row.vala b/src/engine/sqlite/Row.vala index e2c92ec0..76895786 100644 --- a/src/engine/sqlite/Row.vala +++ b/src/engine/sqlite/Row.vala @@ -7,19 +7,22 @@ public abstract class Geary.Sqlite.Row { public const int64 INVALID_ID = -1; - public static int fetch_int_for(SQLHeavy.QueryResult result, string name) - throws SQLHeavy.Error { - return result.fetch_int(result.field_index(name)); + private Table table; + + public Row(Table table) { + this.table = table; } - public static int64 fetch_int64_for(SQLHeavy.QueryResult result, string name) - throws SQLHeavy.Error { - return result.fetch_int64(result.field_index(name)); + 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))); } - public static string fetch_string_for(SQLHeavy.QueryResult result, string name) - throws SQLHeavy.Error { - return result.fetch_string(result.field_index(name)); + 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))); + } + + 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))); } } diff --git a/src/engine/sqlite/Table.vala b/src/engine/sqlite/Table.vala index 462f42e4..3d37fd8f 100644 --- a/src/engine/sqlite/Table.vala +++ b/src/engine/sqlite/Table.vala @@ -18,5 +18,9 @@ public abstract class Geary.Sqlite.Table { this.gdb = gdb; this.table = table; } + + public string get_field_name(int col) throws SQLHeavy.Error { + return table.field_name(col); + } } diff --git a/src/engine/sqlite/api/Account.vala b/src/engine/sqlite/api/Account.vala index dc707813..eb27133a 100644 --- a/src/engine/sqlite/api/Account.vala +++ b/src/engine/sqlite/api/Account.vala @@ -18,49 +18,48 @@ public class Geary.Sqlite.Account : Object, Geary.Account, Geary.LocalAccount { folder_table = db.get_folder_table(); } - public async Gee.Collection list_async(string? parent_folder, + public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder, + Cancellable? cancellable = null) throws Error { + yield folder_table.create_async(new FolderRow(folder_table, folder.get_name(), Row.INVALID_ID), + cancellable); + } + + public async void create_many_folders_async(Geary.Folder? parent, Gee.Collection folders, + Cancellable? cancellable = null) throws Error { + Gee.List rows = new Gee.ArrayList(); + foreach (Geary.Folder folder in folders) + rows.add(new FolderRow(db.get_folder_table(), folder.get_name(), Row.INVALID_ID)); + + yield folder_table.create_many_async(rows, cancellable); + } + + public async Gee.Collection list_folders_async(Geary.Folder? parent, Cancellable? cancellable = null) throws Error { Gee.List rows = yield folder_table.list_async(Row.INVALID_ID, cancellable); Gee.Collection folders = new Gee.ArrayList(); foreach (FolderRow row in rows) - folders.add(new Geary.Sqlite.Folder(row)); + folders.add(new Geary.Sqlite.Folder(db, row)); return folders; } - public async Geary.Folder fetch_async(string? parent_folder, string folder_name, + public async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name, Cancellable? cancellable = null) throws Error { FolderRow? row = yield folder_table.fetch_async(Row.INVALID_ID, folder_name, cancellable); if (row == null) - throw new EngineError.NOT_FOUND("%s not found in local database", folder_name); + throw new EngineError.NOT_FOUND("\"%s\" not found in local database", folder_name); - return new Geary.Sqlite.Folder(row); + return new Geary.Sqlite.Folder(db, row); } - public async void create_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error { - yield folder_table.create_async( - new FolderRow(folder.get_name(), folder.does_support_children(), folder.is_openable()), - cancellable); - } - - public async void create_many_async(Gee.Collection folders, - Cancellable? cancellable = null) throws Error { - Gee.List rows = new Gee.ArrayList(); - foreach (Geary.Folder folder in folders) { - rows.add(new FolderRow(folder.get_name(), folder.does_support_children(), - folder.is_openable())); - } - - yield folder_table.create_many_async(rows, cancellable); - } - - public async void remove_async(string folder, Cancellable? cancellable = null) throws Error { + public async void remove_folder_async(Geary.Folder folder, Cancellable? cancellable = null) + throws Error { // TODO } - public async void remove_many_async(Gee.Set folders, Cancellable? cancellable = null) - throws Error { + public async void remove_many_folders_async(Gee.Set folders, + Cancellable? cancellable = null) throws Error { // TODO } } diff --git a/src/engine/sqlite/api/Folder.vala b/src/engine/sqlite/api/Folder.vala index acce3b11..5f607251 100644 --- a/src/engine/sqlite/api/Folder.vala +++ b/src/engine/sqlite/api/Folder.vala @@ -5,63 +5,91 @@ */ public class Geary.Sqlite.Folder : Object, Geary.Folder { - private FolderRow row; + private MailDatabase db; + private FolderRow folder_row; + private MessageTable message_table; + private MessageLocationTable location_table; private string name; - private Trillian readonly; - private Trillian supports_children; - private Trillian children; - private Trillian openable; - internal Folder(FolderRow row) throws Error { - this.row = row; + internal Folder(MailDatabase db, FolderRow folder_row) throws Error { + this.db = db; + this.folder_row = folder_row; - name = row.name; - readonly = Trillian.UNKNOWN; - supports_children = row.supports_children; - children = Trillian.UNKNOWN; - openable = row.is_openable; + name = folder_row.name; + + message_table = db.get_message_table(); + location_table = db.get_message_location_table(); } public string get_name() { return name; } - public Trillian is_readonly() { - return readonly; - } - - public Trillian does_support_children() { - return supports_children; - } - - public Trillian has_children() { - return children; - } - - public Trillian is_openable() { - return openable; + public Geary.FolderProperties? get_properties() { + return null; } public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { - this.readonly = Trillian.TRUE; } public async void close_async(Cancellable? cancellable = null) throws Error { - this.readonly = Trillian.UNKNOWN; } public int get_message_count() throws Error { return 0; } - public async Gee.List? read_async(int low, int count, + public async void create_email_async(Geary.Email email, Geary.EmailOrdering ordering, Cancellable? cancellable = null) throws Error { - return null; + int64 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, ordering.ordinal); + yield location_table.create_async(location_row, cancellable); } - public async Geary.Email fetch_async(Geary.EmailHeader header, + public async Gee.List list_email_async(int low, int count, Geary.Email.Field fields, + Cancellable? cancellable) throws Error { + assert(low >= 1); + assert(count >= 1); + + // low is zero-based in the database. + Gee.List? list = yield location_table.list_async(folder_row.id, low - 1, + count, cancellable); + if (list == null || list.size == 0) + throw new EngineError.NOT_FOUND("No messages found at position %d in %s", low, name); + + Gee.List emails = new Gee.ArrayList(); + int msg_num = 1; + foreach (MessageLocationRow location_row in list) { + MessageRow? message_row = yield message_table.fetch_async(location_row.message_id, + fields, cancellable); + assert(message_row != null); + + emails.add(message_row.to_email(msg_num++)); + } + + return (emails.size > 0) ? emails : null; + } + + public async Geary.Email fetch_email_async(int num, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { - throw new EngineError.OPEN_REQUIRED("Not implemented"); + assert(num >= 0); + + // num is zero-based in the database. + MessageLocationRow? location_row = yield location_table.fetch_async(folder_row.id, num - 1, + cancellable); + if (location_row == null) + throw new EngineError.NOT_FOUND("No message number %s in folder %s", num, name); + + MessageRow? message_row = yield message_table.fetch_async(location_row.message_id, fields, + cancellable); + if (message_row == null) + throw new EngineError.NOT_FOUND("No message number %s in folde %s", num, name); + + return message_row.to_email(num); } } diff --git a/src/engine/util/Memory.vala b/src/engine/util/Memory.vala index 06da380f..871f9656 100644 --- a/src/engine/util/Memory.vala +++ b/src/engine/util/Memory.vala @@ -34,6 +34,30 @@ public abstract class Geary.Memory.AbstractBuffer { } } +public class Geary.Memory.StringBuffer : Geary.Memory.AbstractBuffer { + private string str; + + public StringBuffer(string str) { + this.str = str; + } + + public override size_t get_size() { + return str.data.length; + } + + public override size_t get_actual_size() { + return str.data.length; + } + + public override uint8[] get_buffer() { + return str.data; + } + + public override InputStream get_input_stream() { + return new MemoryInputStream.from_data(str.data, null); + } +} + public class Geary.Memory.Buffer : Geary.Memory.AbstractBuffer { private uint8[] buffer; private size_t filled;