diff --git a/sql/Create.sql b/sql/Create.sql index 80191bd5..7b17d9af 100644 --- a/sql/Create.sql +++ b/sql/Create.sql @@ -69,8 +69,7 @@ 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 + attributes TEXT ); CREATE INDEX ImapFolderPropertiesTableFolderIDIndex ON ImapFolderPropertiesTable(folder_id); @@ -82,13 +81,7 @@ CREATE INDEX ImapFolderPropertiesTableFolderIDIndex ON ImapFolderPropertiesTable 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 + flags TEXT ); CREATE INDEX ImapMessagePropertiesTableMessageIDIndex ON ImapMessagePropertiesTable(message_id); diff --git a/src/client/ui/main-window.vala b/src/client/ui/main-window.vala index 48a39172..6c7d7bba 100644 --- a/src/client/ui/main-window.vala +++ b/src/client/ui/main-window.vala @@ -93,7 +93,7 @@ public class MainWindow : Gtk.Window { else debug("no folders"); } catch (Error err) { - error("%s", err.message); + warning("%s", err.message); } } diff --git a/src/console/main.vala b/src/console/main.vala index 27061aa7..7979beed 100644 --- a/src/console/main.vala +++ b/src/console/main.vala @@ -91,7 +91,8 @@ class ImapConsole : Gtk.Window { "exit", "quit", "gmail", - "keepalive" + "keepalive", + "status" }; private void exec(string input) { @@ -180,6 +181,10 @@ class ImapConsole : Gtk.Window { keepalive(cmd, args); break; + case "status": + folder_status(cmd, args); + break; + default: status("Unknown command \"%s\"".printf(cmd)); break; @@ -393,6 +398,28 @@ class ImapConsole : Gtk.Window { } } + private void folder_status(string cmd, string[] args) throws Error { + check_min_connected(cmd, args, 2, " "); + + status("Status %s".printf(args[0])); + + Geary.Imap.StatusDataType[] data_items = new Geary.Imap.StatusDataType[0]; + for (int ctr = 1; ctr < args.length; ctr++) + data_items += Geary.Imap.StatusDataType.decode(args[ctr]); + + cx.send_async.begin(new Geary.Imap.StatusCommand(cx.generate_tag(), args[0], data_items), + null, on_get_status); + } + + private void on_get_status(Object? source, AsyncResult result) { + try { + cx.send_async.end(result); + status("Get status"); + } catch (Error err) { + exception(err); + } + } + private void quit(string cmd, string[] args) throws Error { Gtk.main_quit(); } diff --git a/src/engine/api/geary-abstract-folder.vala b/src/engine/api/geary-abstract-folder.vala index 77cbd11c..c6878e1a 100644 --- a/src/engine/api/geary-abstract-folder.vala +++ b/src/engine/api/geary-abstract-folder.vala @@ -5,8 +5,8 @@ */ public abstract class Geary.AbstractFolder : Object, Geary.Folder { - protected virtual void notify_opened() { - opened(); + protected virtual void notify_opened(Geary.Folder.OpenState state) { + opened(state); } protected virtual void notify_closed(Geary.Folder.CloseReason reason) { diff --git a/src/engine/api/geary-engine-folder.vala b/src/engine/api/geary-engine-folder.vala index 827469ba..89e10630 100644 --- a/src/engine/api/geary-engine-folder.vala +++ b/src/engine/api/geary-engine-folder.vala @@ -7,10 +7,11 @@ private class Geary.EngineFolder : Geary.AbstractFolder { private const int REMOTE_FETCH_CHUNK_COUNT = 10; - private RemoteAccount remote; - private LocalAccount local; - private RemoteFolder? remote_folder = null; - private LocalFolder local_folder; + protected RemoteAccount remote; + protected LocalAccount local; + protected RemoteFolder? remote_folder = null; + protected LocalFolder local_folder; + private bool opened = false; private Geary.Common.NonblockingSemaphore remote_semaphore = new Geary.Common.NonblockingSemaphore(true); @@ -59,8 +60,6 @@ private class Geary.EngineFolder : Geary.AbstractFolder { open_remote_async.begin(readonly, cancellable, on_open_remote_completed); opened = true; - - notify_opened(); } private async void open_remote_async(bool readonly, Cancellable? cancellable) throws Error { @@ -77,6 +76,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder { private void on_open_remote_completed(Object? source, AsyncResult result) { try { open_remote_async.end(result); + + notify_opened(Geary.Folder.OpenState.BOTH); } catch (Error err) { debug("Unable to open remote folder %s: %s", to_string(), err.message); @@ -86,6 +87,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder { } catch (Error err) { debug("Unable to notify remote folder ready: %s", err.message); } + + notify_opened(Geary.Folder.OpenState.LOCAL); } } @@ -142,7 +145,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { Gee.List? accumulator, EmailCallback? cb, Cancellable? cancellable = null) throws Error { assert(low >= 1); - assert(count >= 0); + assert(count >= 0 || count == -1); if (!opened) throw new EngineError.OPEN_REQUIRED("%s is not open", to_string()); @@ -361,8 +364,10 @@ private class Geary.EngineFolder : Geary.AbstractFolder { list = needed_by_position; } + // Always get the flags, and the generic end-user won't know to ask for them until they + // need them Gee.List? remote_list = yield remote_folder.list_email_sparse_async( - list, required_fields, cancellable); + list, required_fields | Geary.Email.Field.PROPERTIES, cancellable); if (remote_list == null || remote_list.size == 0) break; diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala index e21cd472..93f7ab2d 100644 --- a/src/engine/api/geary-folder.vala +++ b/src/engine/api/geary-folder.vala @@ -7,6 +7,12 @@ public delegate void Geary.EmailCallback(Gee.List? emails, Error? err); public interface Geary.Folder : Object { + public enum OpenState { + REMOTE, + LOCAL, + BOTH + } + public enum CloseReason { LOCAL_CLOSE, REMOTE_CLOSE, @@ -15,9 +21,9 @@ public interface Geary.Folder : Object { /** * This is fired when the Folder is successfully opened by a caller. It will only fire once - * until the Folder is closed. + * until the Folder is closed, with the OpenState indicating what has been opened. */ - public signal void opened(); + public signal void opened(OpenState state); /** * This is fired when the Folder is successfully closed by a caller. It will only fire once @@ -50,8 +56,8 @@ public interface Geary.Folder : Object { * directly. This allows subclasses and superclasses the opportunity to inspect the email * and update state before and/or after the signal has been fired. */ - protected virtual void notify_opened() { - opened(); + protected virtual void notify_opened(OpenState state) { + opened(state); } /** @@ -140,7 +146,8 @@ public interface Geary.Folder : Object { /** * 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 + * position and moving up to (low + count). If count is -1, the returned list starts at low + * and proceeds to all available emails. The returned 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 diff --git a/src/engine/api/geary-generic-imap-account.vala b/src/engine/api/geary-generic-imap-account.vala index c44a55d3..6694d88e 100644 --- a/src/engine/api/geary-generic-imap-account.vala +++ b/src/engine/api/geary-generic-imap-account.vala @@ -50,7 +50,7 @@ private class Geary.GenericImapAccount : Geary.EngineAccount { Gee.Collection engine_list = new Gee.ArrayList(); if (local_list != null && local_list.size > 0) { foreach (Geary.Folder local_folder in local_list) - engine_list.add(new EngineFolder(remote, local, (LocalFolder) local_folder)); + engine_list.add(new GenericImapFolder(remote, local, (LocalFolder) local_folder)); } background_update_folders.begin(parent, engine_list); @@ -72,7 +72,7 @@ private class Geary.GenericImapAccount : Geary.EngineAccount { try { local_folder = (LocalFolder) yield local.fetch_folder_async(path, cancellable); - return new EngineFolder(remote, local, local_folder); + return new GenericImapFolder(remote, local, local_folder); } catch (EngineError err) { // don't thrown NOT_FOUND's, that means we need to fall through and clone from the // server @@ -94,10 +94,10 @@ private class Geary.GenericImapAccount : Geary.EngineAccount { yield local.clone_folder_async(remote_folder, cancellable); } - // Fetch the local account's version of the folder for the EngineFolder + // Fetch the local account's version of the folder for the GenericImapFolder local_folder = (LocalFolder) yield local.fetch_folder_async(path, cancellable); - return new EngineFolder(remote, local, local_folder); + return new GenericImapFolder(remote, local, local_folder); } private Gee.Set get_folder_names(Gee.Collection folders) { @@ -140,11 +140,15 @@ private class Geary.GenericImapAccount : Geary.EngineAccount { if (to_remove.size == 0) to_remove = null; - try { - if (to_add != null) - yield local.clone_many_folders_async(to_add); - } catch (Error err) { - error("Unable to add/remove folders: %s", err.message); + if (to_add != null) { + foreach (Geary.Folder folder in to_add) { + try { + yield local.clone_folder_async(folder); + } catch (Error err) { + debug("Unable to add/remove folder %s: %s", folder.get_path().to_string(), + err.message); + } + } } Gee.Collection engine_added = null; @@ -154,7 +158,7 @@ private class Geary.GenericImapAccount : Geary.EngineAccount { try { LocalFolder local_folder = (LocalFolder) yield local.fetch_folder_async( remote_folder.get_path()); - engine_added.add(new EngineFolder(remote, local, local_folder)); + engine_added.add(new GenericImapFolder(remote, local, local_folder)); } catch (Error convert_err) { error("Unable to fetch local folder: %s", convert_err.message); } diff --git a/src/engine/api/geary-generic-imap-folder.vala b/src/engine/api/geary-generic-imap-folder.vala new file mode 100644 index 00000000..071d4afa --- /dev/null +++ b/src/engine/api/geary-generic-imap-folder.vala @@ -0,0 +1,11 @@ +/* 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.GenericImapFolder : Geary.EngineFolder { + public GenericImapFolder(RemoteAccount remote, LocalAccount local, LocalFolder local_folder) { + base (remote, local, local_folder); + } +} diff --git a/src/engine/api/geary-local-interfaces.vala b/src/engine/api/geary-local-interfaces.vala index 5a55d95c..f05860ce 100644 --- a/src/engine/api/geary-local-interfaces.vala +++ b/src/engine/api/geary-local-interfaces.vala @@ -8,9 +8,6 @@ public interface Geary.LocalAccount : Object, Geary.Account { public abstract async void clone_folder_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error; - public abstract async void clone_many_folders_async(Gee.Collection folders, - Cancellable? cancellable = null) throws Error; - /** * Returns true if the email (identified by its Message-ID) already exists in the account's * local store, no matter the folder. diff --git a/src/engine/imap/api/imap-account.vala b/src/engine/imap/api/imap-account.vala index fbcf9ad0..055710a0 100644 --- a/src/engine/imap/api/imap-account.vala +++ b/src/engine/imap/api/imap-account.vala @@ -66,7 +66,18 @@ public class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount { if (processed == null) delims.set(path.get_root().basename, mbox.delim); - folders.add(new Geary.Imap.Folder(session_mgr, path, mbox)); + UIDValidity? uid_validity = null; + if (!mbox.attrs.contains(MailboxAttribute.NO_SELECT)) { + try { + StatusResults results = yield session_mgr.status_async(path.get_fullpath(), + { StatusDataType.UIDVALIDITY }, cancellable); + uid_validity = results.uidvalidity; + } catch (Error status_err) { + message("Unable to fetch UID Validity for %s: %s", path.to_string(), status_err.message); + } + } + + folders.add(new Geary.Imap.Folder(session_mgr, path, uid_validity, mbox)); } return folders; @@ -93,7 +104,14 @@ public class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount { if (mbox == null) throw_not_found(path); - return new Geary.Imap.Folder(session_mgr, processed, mbox); + UIDValidity? uid_validity = null; + if (!mbox.attrs.contains(MailboxAttribute.NO_SELECT)) { + StatusResults results = yield session_mgr.status_async(processed.get_fullpath(), + { StatusDataType.UIDVALIDITY }, cancellable); + uid_validity = results.uidvalidity; + } + + return new Geary.Imap.Folder(session_mgr, processed, uid_validity, mbox); } catch (ImapError err) { if (err is ImapError.SERVER_ERROR) throw_not_found(path); diff --git a/src/engine/imap/api/imap-email-properties.vala b/src/engine/imap/api/imap-email-properties.vala index 38a7cb76..7ef3ad3e 100644 --- a/src/engine/imap/api/imap-email-properties.vala +++ b/src/engine/imap/api/imap-email-properties.vala @@ -5,10 +5,27 @@ */ public class Geary.Imap.EmailProperties : Geary.EmailProperties { + public bool answered { get; private set; } + public bool deleted { get; private set; } + public bool draft { get; private set; } + public bool flagged { get; private set; } + public bool recent { get; private set; } + public bool seen { get; private set; } public MessageFlags flags { get; private set; } public EmailProperties(MessageFlags flags) { this.flags = flags; + + answered = flags.contains(MessageFlag.ANSWERED); + deleted = flags.contains(MessageFlag.DELETED); + draft = flags.contains(MessageFlag.DRAFT); + flagged = flags.contains(MessageFlag.FLAGGED); + recent = flags.contains(MessageFlag.RECENT); + seen = flags.contains(MessageFlag.SEEN); + } + + public bool is_empty() { + return (flags.size == 0); } public override bool is_unread() { diff --git a/src/engine/imap/api/imap-folder-properties.vala b/src/engine/imap/api/imap-folder-properties.vala index 9407c498..d1898b53 100644 --- a/src/engine/imap/api/imap-folder-properties.vala +++ b/src/engine/imap/api/imap-folder-properties.vala @@ -5,13 +5,13 @@ */ public class Geary.Imap.FolderProperties : Geary.FolderProperties { - public UID? uid_validity { get; set; } + public UIDValidity? 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) { + public FolderProperties(UIDValidity? uid_validity, MailboxAttributes attrs) { this.uid_validity = uid_validity; this.attrs = attrs; diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala index 4c733570..42196497 100644 --- a/src/engine/imap/api/imap-folder.vala +++ b/src/engine/imap/api/imap-folder.vala @@ -14,13 +14,14 @@ public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder { private Imap.FolderProperties properties; private Mailbox? mailbox = null; - internal Folder(ClientSessionManager session_mgr, Geary.FolderPath path, MailboxInformation info) { + internal Folder(ClientSessionManager session_mgr, Geary.FolderPath path, UIDValidity? uid_validity, + MailboxInformation info) { this.session_mgr = session_mgr; this.info = info; this.path = path; readonly = Trillian.UNKNOWN; - properties = new Imap.FolderProperties(null, info.attrs); + properties = new Imap.FolderProperties(uid_validity, info.attrs); } public override Geary.FolderPath get_path() { @@ -46,7 +47,7 @@ public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder { this.readonly = Trillian.from_boolean(readonly); properties.uid_validity = mailbox.uid_validity; - notify_opened(); + notify_opened(Geary.Folder.OpenState.REMOTE); } public override async void close_async(Cancellable? cancellable = null) throws Error { @@ -79,7 +80,11 @@ public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); - return yield mailbox.list_set_async(new MessageSet.range(low, count), fields, cancellable); + MessageSet msg_set = (count != -1) + ? new MessageSet.range(low, count) + : new MessageSet.range_to_highest(low); + + return yield mailbox.list_set_async(msg_set, fields, cancellable); } public override async Gee.List? list_email_sparse_async(int[] by_position, diff --git a/src/engine/imap/decoders/imap-select-examine-results.vala b/src/engine/imap/decoders/imap-select-examine-results.vala index 9afaaeb6..7dc24993 100644 --- a/src/engine/imap/decoders/imap-select-examine-results.vala +++ b/src/engine/imap/decoders/imap-select-examine-results.vala @@ -17,13 +17,13 @@ public class Geary.Imap.SelectExamineResults : Geary.Imap.CommandResults { * -1 if not specified. */ public int unseen { get; private set; } - public UID? uid_validity { get; private set; } + public UIDValidity? uid_validity { get; private set; } public Flags? flags { get; private set; } public Flags? permanentflags { get; private set; } public bool readonly { get; private set; } private SelectExamineResults(StatusResponse status_response, int exists, int recent, int unseen, - UID? uidvalidity, Flags? flags, Flags? permanentflags, bool readonly) { + UIDValidity? uidvalidity, Flags? flags, Flags? permanentflags, bool readonly) { base (status_response); this.exists = exists; @@ -41,7 +41,7 @@ public class Geary.Imap.SelectExamineResults : Geary.Imap.CommandResults { int exists = -1; int recent = -1; int unseen = -1; - UID? uidvalidity = null; + UIDValidity? uidvalidity = null; UID? uidnext = null; MessageFlags? flags = null; MessageFlags? permanentflags = null; @@ -75,7 +75,7 @@ public class Geary.Imap.SelectExamineResults : Geary.Imap.CommandResults { break; case ResponseCodeType.UIDVALIDITY: - uidvalidity = new UID( + uidvalidity = new UIDValidity( ok_response.response_code.get_as_string(1).as_int()); break; diff --git a/src/engine/imap/decoders/imap-status-results.vala b/src/engine/imap/decoders/imap-status-results.vala index 79febfb3..77f7bc30 100644 --- a/src/engine/imap/decoders/imap-status-results.vala +++ b/src/engine/imap/decoders/imap-status-results.vala @@ -15,14 +15,14 @@ public class Geary.Imap.StatusResults : Geary.Imap.CommandResults { */ public int recent { get; private set; } public UID? uidnext { get; private set; } - public UID? uidvalidity { get; private set; } + public UIDValidity? uidvalidity { get; private set; } /** * -1 if not set. */ public int unseen { get; private set; } public StatusResults(StatusResponse status_response, string mailbox, int messages, int recent, - UID? uidnext, UID? uidvalidity, int unseen) { + UID? uidnext, UIDValidity? uidvalidity, int unseen) { base (status_response); this.mailbox = mailbox; @@ -54,7 +54,7 @@ public class Geary.Imap.StatusResults : Geary.Imap.CommandResults { int messages = -1; int recent = -1; UID? uidnext = null; - UID? uidvalidity = null; + UIDValidity? uidvalidity = null; int unseen = -1; for (int ctr = 0; ctr < values.get_count(); ctr += 2) { @@ -76,7 +76,7 @@ public class Geary.Imap.StatusResults : Geary.Imap.CommandResults { break; case StatusDataType.UIDVALIDITY: - uidvalidity = new UID(valuep.as_int()); + uidvalidity = new UIDValidity(valuep.as_int()); break; case StatusDataType.UNSEEN: diff --git a/src/engine/imap/message/imap-message-data.vala b/src/engine/imap/message/imap-message-data.vala index ba75e8a4..bf173b47 100644 --- a/src/engine/imap/message/imap-message-data.vala +++ b/src/engine/imap/message/imap-message-data.vala @@ -25,6 +25,12 @@ public class Geary.Imap.UID : Geary.Common.Int64MessageData, Geary.Imap.MessageD } } +public class Geary.Imap.UIDValidity : Geary.Common.Int64MessageData, Geary.Imap.MessageData { + public UIDValidity(int64 value) { + base (value); + } +} + public class Geary.Imap.MessageNumber : Geary.Common.IntMessageData, Geary.Imap.MessageData { public MessageNumber(int value) { base (value); @@ -32,6 +38,8 @@ public class Geary.Imap.MessageNumber : Geary.Common.IntMessageData, Geary.Imap. } public abstract class Geary.Imap.Flags : Geary.Common.MessageData, Geary.Imap.MessageData { + public int size { get { return list.size; } } + private Gee.Set list; public Flags(Gee.Collection flags) { @@ -47,6 +55,14 @@ public abstract class Geary.Imap.Flags : Geary.Common.MessageData, Geary.Imap.Me return list.read_only_view; } + /** + * Returns the flags in serialized form, which is each flag separated by a space (legal in + * IMAP, as flags must be atoms and atoms prohibit spaces). + */ + public virtual string serialize() { + return to_string(); + } + public override string to_string() { StringBuilder builder = new StringBuilder(); foreach (Flag flag in list) { @@ -79,12 +95,32 @@ public class Geary.Imap.MessageFlags : Geary.Imap.Flags { return new MessageFlags(list); } + + public static MessageFlags deserialize(string str) { + string[] tokens = str.split(" "); + + Gee.Collection flags = new Gee.ArrayList(); + foreach (string token in tokens) + flags.add(new MessageFlag(token)); + + return new MessageFlags(flags); + } } public class Geary.Imap.MailboxAttributes : Geary.Imap.Flags { public MailboxAttributes(Gee.Collection attrs) { base (attrs); } + + public static MailboxAttributes deserialize(string str) { + string[] tokens = str.split(" "); + + Gee.Collection attrs = new Gee.ArrayList(); + foreach (string token in tokens) + attrs.add(new MailboxAttribute(token)); + + return new MailboxAttributes(attrs); + } } public class Geary.Imap.InternalDate : Geary.RFC822.Date, Geary.Imap.MessageData { diff --git a/src/engine/imap/transport/imap-client-session-manager.vala b/src/engine/imap/transport/imap-client-session-manager.vala index dff03ab4..65942079 100644 --- a/src/engine/imap/transport/imap-client-session-manager.vala +++ b/src/engine/imap/transport/imap-client-session-manager.vala @@ -98,6 +98,19 @@ public class Geary.Imap.ClientSessionManager { return (results.get_count() > 0) ? results.get_all()[0] : null; } + public async Geary.Imap.StatusResults status_async(string path, StatusDataType[] types, + Cancellable? cancellable = null) throws Error { + ClientSession session = yield get_authorized_session(cancellable); + + StatusResults results = StatusResults.decode(yield session.send_command_async( + new StatusCommand(session.generate_tag(), path, types), cancellable)); + + if (results.status_response.status != Status.OK) + throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string()); + + return results; + } + public async Mailbox select_mailbox(string path, Cancellable? cancellable = null) throws Error { return yield select_examine_mailbox(path, true, cancellable); } diff --git a/src/engine/imap/transport/imap-mailbox.vala b/src/engine/imap/transport/imap-mailbox.vala index 55ce2e0f..3610b4f3 100644 --- a/src/engine/imap/transport/imap-mailbox.vala +++ b/src/engine/imap/transport/imap-mailbox.vala @@ -8,7 +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; } + public UIDValidity uid_validity { get; private set; } private SelectedContext context; @@ -203,7 +203,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 UIDValidity uid_validity { get; protected set; } public signal void exists_changed(int exists); diff --git a/src/engine/sqlite/abstract/sqlite-table.vala b/src/engine/sqlite/abstract/sqlite-table.vala index 2f5d6d9d..1caa7188 100644 --- a/src/engine/sqlite/abstract/sqlite-table.vala +++ b/src/engine/sqlite/abstract/sqlite-table.vala @@ -23,6 +23,14 @@ public abstract class Geary.Sqlite.Table { return table.field_name(col); } + protected inline static int bool_to_int(bool b) { + return b ? 1 : 0; + } + + protected inline static bool int_to_bool(int i) { + return !(i == 0); + } + public string to_string() { return table.name; } diff --git a/src/engine/sqlite/api/sqlite-account.vala b/src/engine/sqlite/api/sqlite-account.vala index 0406b92a..acb8fa31 100644 --- a/src/engine/sqlite/api/sqlite-account.vala +++ b/src/engine/sqlite/api/sqlite-account.vala @@ -7,6 +7,7 @@ public class Geary.Sqlite.Account : Geary.AbstractAccount, Geary.LocalAccount { private MailDatabase db; private FolderTable folder_table; + private ImapFolderPropertiesTable folder_properties_table; private MessageTable message_table; public Account(Geary.Credentials cred) { @@ -19,6 +20,7 @@ public class Geary.Sqlite.Account : Geary.AbstractAccount, Geary.LocalAccount { } folder_table = db.get_folder_table(); + folder_properties_table = db.get_imap_folder_properties_table(); message_table = db.get_message_table(); } @@ -42,20 +44,21 @@ public class Geary.Sqlite.Account : Geary.AbstractAccount, Geary.LocalAccount { public async void clone_folder_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error { - int64 parent_id = yield fetch_parent_id_async(folder.get_path(), cancellable); - yield folder_table.create_async(new FolderRow(folder_table, folder.get_path().basename, - parent_id), cancellable); - } - - public async void clone_many_folders_async(Gee.Collection folders, - Cancellable? cancellable = null) throws Error { - Gee.List rows = new Gee.ArrayList(); - foreach (Geary.Folder folder in folders) { - int64 parent_id = yield fetch_parent_id_async(folder.get_path(), cancellable); - rows.add(new FolderRow(db.get_folder_table(), folder.get_path().basename, parent_id)); - } + Geary.Imap.Folder imap_folder = (Geary.Imap.Folder) folder; + Geary.Imap.FolderProperties? imap_folder_properties = (Geary.Imap.FolderProperties?) + imap_folder.get_properties(); - yield folder_table.create_many_async(rows, cancellable); + // properties *must* be available to perform a clone + assert(imap_folder_properties != null); + + int64 parent_id = yield fetch_parent_id_async(folder.get_path(), cancellable); + + int64 folder_id = yield folder_table.create_async(new FolderRow(folder_table, + imap_folder.get_path().basename, parent_id), cancellable); + + yield folder_properties_table.create_async( + new ImapFolderPropertiesRow.from_imap_properties(folder_properties_table, folder_id, + imap_folder_properties)); } public override async Gee.Collection list_folders_async(Geary.FolderPath? parent, @@ -75,11 +78,14 @@ public class Geary.Sqlite.Account : Geary.AbstractAccount, Geary.LocalAccount { Gee.Collection folders = new Gee.ArrayList(); foreach (FolderRow row in rows) { + ImapFolderPropertiesRow? properties = yield folder_properties_table.fetch_async(row.id, + cancellable); + Geary.FolderPath path = (parent != null) ? parent.get_child(row.name) : new Geary.FolderRoot(row.name, "/", Geary.Imap.Folder.CASE_SENSITIVE); - folders.add(new Geary.Sqlite.Folder(db, row, path)); + folders.add(new Geary.Sqlite.Folder(db, row, properties, path)); } return folders; @@ -105,7 +111,10 @@ public class Geary.Sqlite.Account : Geary.AbstractAccount, Geary.LocalAccount { if (row == null) throw new EngineError.NOT_FOUND("%s not found in local database", path.to_string()); - return new Geary.Sqlite.Folder(db, row, path); + ImapFolderPropertiesRow? properties = yield folder_properties_table.fetch_async(row.id, + cancellable); + + return new Geary.Sqlite.Folder(db, row, properties, path); } public async bool has_message_id_async(Geary.RFC822.MessageID message_id, out int count, diff --git a/src/engine/sqlite/api/sqlite-folder.vala b/src/engine/sqlite/api/sqlite-folder.vala index c7067931..b7ebbfb8 100644 --- a/src/engine/sqlite/api/sqlite-folder.vala +++ b/src/engine/sqlite/api/sqlite-folder.vala @@ -10,20 +10,25 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { private MailDatabase db; private FolderRow folder_row; + private Geary.FolderProperties? properties; private MessageTable message_table; private MessageLocationTable location_table; private ImapMessageLocationPropertiesTable imap_location_table; + private ImapMessagePropertiesTable imap_message_properties_table; private Geary.FolderPath path; private bool opened = false; - internal Folder(MailDatabase db, FolderRow folder_row, Geary.FolderPath path) throws Error { + internal Folder(MailDatabase db, FolderRow folder_row, ImapFolderPropertiesRow? properties, + Geary.FolderPath path) throws Error { this.db = db; this.folder_row = folder_row; + this.properties = (properties != null) ? properties.get_imap_folder_properties() : null; this.path = path; message_table = db.get_message_table(); location_table = db.get_message_location_table(); imap_location_table = db.get_imap_message_location_table(); + imap_message_properties_table = db.get_imap_message_properties_table(); } private void check_open() throws Error { @@ -36,7 +41,7 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { } public override Geary.FolderProperties? get_properties() { - return null; + return properties; } public override async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { @@ -44,7 +49,8 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { throw new EngineError.ALREADY_OPEN("%s already open", to_string()); opened = true; - notify_opened(); + + notify_opened(Geary.Folder.OpenState.LOCAL); } public override async void close_async(Cancellable? cancellable = null) throws Error { @@ -52,6 +58,7 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { return; opened = false; + notify_closed(CloseReason.FOLDER_CLOSED); } @@ -89,15 +96,27 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { 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); + + // only write out the IMAP email properties if they're supplied and there's something to + // write out -- no need to create an empty row + Geary.Imap.EmailProperties? properties = (Geary.Imap.EmailProperties?) email.properties; + if (email.fields.fulfills(Geary.Email.Field.PROPERTIES) && properties != null && !properties.is_empty()) { + ImapMessagePropertiesRow properties_row = new ImapMessagePropertiesRow.from_imap_properties( + imap_message_properties_table, message_id, properties); + yield imap_message_properties_table.create_async(properties_row, cancellable); + } } public override 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); + assert(count >= 0 || count == -1); check_open(); + if (count == 0) + return null; + Gee.List? list = yield location_table.list_async(folder_row.id, low, count, cancellable); @@ -136,12 +155,23 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { MessageRow? message_row = yield message_table.fetch_async(location_row.message_id, 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))); + ImapMessagePropertiesRow? properties = null; + if (required_fields.fulfills(Geary.Email.Field.PROPERTIES)) { + properties = yield imap_message_properties_table.fetch_async(location_row.message_id, + cancellable); + } + + Geary.Email email = message_row.to_email(new Geary.Imap.EmailLocation(location_row.position, + imap_location_row.uid)); + if (properties != null) + email.set_email_properties(properties.get_imap_email_properties()); + + emails.add(email); } return (emails.size > 0) ? emails : null; @@ -184,8 +214,18 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { message_row.fields); } - return message_row.to_email(new Geary.Imap.EmailLocation(location_row.position, + ImapMessagePropertiesRow? properties = null; + if (required_fields.fulfills(Geary.Email.Field.PROPERTIES)) { + properties = yield imap_message_properties_table.fetch_async(location_row.message_id, + cancellable); + } + + Geary.Email email = message_row.to_email(new Geary.Imap.EmailLocation(location_row.position, imap_location_row.uid)); + if (properties != null) + email.set_email_properties(properties.get_imap_email_properties()); + + return email; } public async bool is_email_present_at(int position, out Geary.Email.Field available_fields, @@ -292,7 +332,6 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { } // 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); @@ -310,6 +349,12 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { // possible nothing has changed or been added if (message_row.fields != Geary.Email.Field.NONE) yield message_table.merge_async(message_row, cancellable); + + // update IMAP properties + if (email.fields.fulfills(Geary.Email.Field.PROPERTIES)) { + yield imap_message_properties_table.update_async(message_id, + ((Geary.Imap.EmailProperties) email.properties).flags.serialize(), cancellable); + } } } diff --git a/src/engine/sqlite/email/sqlite-folder-table.vala b/src/engine/sqlite/email/sqlite-folder-table.vala index f53c8244..09e92012 100644 --- a/src/engine/sqlite/email/sqlite-folder-table.vala +++ b/src/engine/sqlite/email/sqlite-folder-table.vala @@ -33,20 +33,11 @@ public class Geary.Sqlite.FolderTable : Geary.Sqlite.Table { query.bind_null(1); } - public async void create_async(FolderRow row, Cancellable? cancellable = null) throws Error { + public async int64 create_async(FolderRow row, Cancellable? cancellable = null) throws Error { SQLHeavy.Query query = create_query(); create_binding(query, row); - yield query.execute_insert_async(cancellable); - } - - public async void create_many_async(Gee.Collection rows, Cancellable? cancellable = null) - throws Error { - SQLHeavy.Query query = create_query(); - foreach (FolderRow row in rows) { - create_binding(query, row); - query.execute_insert(); - } + return yield query.execute_insert_async(cancellable); } public async Gee.List list_async(int64 parent_id, Cancellable? cancellable = null) diff --git a/src/engine/sqlite/email/sqlite-mail-database.vala b/src/engine/sqlite/email/sqlite-mail-database.vala index cae638eb..68fe7bca 100644 --- a/src/engine/sqlite/email/sqlite-mail-database.vala +++ b/src/engine/sqlite/email/sqlite-mail-database.vala @@ -40,6 +40,7 @@ public class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database { : (MessageLocationTable) add_table(new MessageLocationTable(this, heavy_table)); } + // TODO: This belongs in a subclass. public Geary.Sqlite.ImapMessageLocationPropertiesTable get_imap_message_location_table() { SQLHeavy.Table heavy_table; ImapMessageLocationPropertiesTable? imap_location_table = get_table( @@ -49,5 +50,25 @@ public class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database { ? imap_location_table : (ImapMessageLocationPropertiesTable) add_table(new ImapMessageLocationPropertiesTable(this, heavy_table)); } + + // TODO: This belongs in a subclass. + public Geary.Sqlite.ImapFolderPropertiesTable get_imap_folder_properties_table() { + SQLHeavy.Table heavy_table; + ImapFolderPropertiesTable? imap_folder_properties_table = get_table( + "ImapFolderPropertiesTable", out heavy_table) as ImapFolderPropertiesTable; + + return imap_folder_properties_table + ?? (ImapFolderPropertiesTable) add_table(new ImapFolderPropertiesTable(this, heavy_table)); + } + + // TODO: This belongs in a subclass. + public Geary.Sqlite.ImapMessagePropertiesTable get_imap_message_properties_table() { + SQLHeavy.Table heavy_table; + ImapMessagePropertiesTable? imap_message_properties_table = get_table( + "ImapMessagePropertiesTable", out heavy_table) as ImapMessagePropertiesTable; + + return imap_message_properties_table + ?? (ImapMessagePropertiesTable) add_table(new ImapMessagePropertiesTable(this, heavy_table)); + } } diff --git a/src/engine/sqlite/email/sqlite-message-location-table.vala b/src/engine/sqlite/email/sqlite-message-location-table.vala index dbb7259c..f2aeb213 100644 --- a/src/engine/sqlite/email/sqlite-message-location-table.vala +++ b/src/engine/sqlite/email/sqlite-message-location-table.vala @@ -29,18 +29,29 @@ public class Geary.Sqlite.MessageLocationTable : Geary.Sqlite.Table { } /** - * low is one-based. + * low is one-based. If count is -1, all messages starting at low are returned. */ public async Gee.List? list_async(int64 folder_id, int low, int count, Cancellable? cancellable = null) throws Error { assert(low >= 1); + assert(count >= 0 || count == -1); - SQLHeavy.Query query = db.prepare( - "SELECT id, message_id, position FROM MessageLocationTable WHERE folder_id = ? " - + "ORDER BY position LIMIT ? OFFSET ?"); - query.bind_int64(0, folder_id); - query.bind_int(1, count); - query.bind_int(2, low - 1); + SQLHeavy.Query query; + if (count >= 0) { + query = db.prepare( + "SELECT id, message_id, position FROM MessageLocationTable WHERE folder_id = ? " + + "ORDER BY position LIMIT ? OFFSET ?"); + query.bind_int64(0, folder_id); + query.bind_int(1, count); + query.bind_int(2, low - 1); + } else { + // count == -1 + query = db.prepare( + "SELECT id, message_id, position FROM MessageLocationTable WHERE folder_id = ? " + + "ORDER BY position OFFSET ?"); + query.bind_int64(0, folder_id); + query.bind_int(1, low - 1); + } SQLHeavy.QueryResult results = yield query.execute_async(cancellable); if (results.finished) @@ -103,7 +114,7 @@ public class Geary.Sqlite.MessageLocationTable : Geary.Sqlite.Table { "SELECT id, message_id, position FROM MessageLocationTable WHERE folder_id = ? " + "AND position = ?"); query.bind_int64(0, folder_id); - query.bind_int64(1, position); + query.bind_int(1, position); SQLHeavy.QueryResult results = yield query.execute_async(cancellable); if (results.finished) diff --git a/src/engine/sqlite/imap/sqlite-imap-folder-properties-row.vala b/src/engine/sqlite/imap/sqlite-imap-folder-properties-row.vala new file mode 100644 index 00000000..3ea5f908 --- /dev/null +++ b/src/engine/sqlite/imap/sqlite-imap-folder-properties-row.vala @@ -0,0 +1,38 @@ +/* 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.ImapFolderPropertiesRow : Geary.Sqlite.Row { + public int64 id { get; private set; } + public int64 folder_id { get; private set; } + public Geary.Imap.UIDValidity? uid_validity { get; private set; } + public string attributes { get; private set; } + + public ImapFolderPropertiesRow(ImapFolderPropertiesTable table, int64 id, int64 folder_id, + Geary.Imap.UIDValidity? uid_validity, string attributes) { + base (table); + + this.id = id; + this.folder_id = folder_id; + this.uid_validity = uid_validity; + this.attributes = attributes; + } + + public ImapFolderPropertiesRow.from_imap_properties(ImapFolderPropertiesTable table, + int64 folder_id, Geary.Imap.FolderProperties properties) { + base (table); + + id = Row.INVALID_ID; + this.folder_id = folder_id; + uid_validity = properties.uid_validity; + attributes = properties.attrs.serialize(); + } + + public Geary.Imap.FolderProperties get_imap_folder_properties() { + return new Geary.Imap.FolderProperties(uid_validity, + Geary.Imap.MailboxAttributes.deserialize(attributes)); + } +} + diff --git a/src/engine/sqlite/imap/sqlite-imap-folder-properties-table.vala b/src/engine/sqlite/imap/sqlite-imap-folder-properties-table.vala new file mode 100644 index 00000000..3b576533 --- /dev/null +++ b/src/engine/sqlite/imap/sqlite-imap-folder-properties-table.vala @@ -0,0 +1,49 @@ +/* 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.ImapFolderPropertiesTable : Geary.Sqlite.Table { + // This *must* be in the same order as the schema. + public enum Column { + ID, + FOLDER_ID, + UID_VALIDITY, + FLAGS + } + + public ImapFolderPropertiesTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { + base (gdb, table); + } + + public async int64 create_async(ImapFolderPropertiesRow row, Cancellable? cancellable = null) + throws Error { + SQLHeavy.Query query = db.prepare( + "INSERT INTO ImapFolderPropertiesTable (folder_id, uid_validity, attributes) VALUES (?, ?, ?)"); + query.bind_int64(0, row.folder_id); + query.bind_int64(1, (row.uid_validity != null) ? row.uid_validity.value : -1); + query.bind_string(2, row.attributes); + + return yield query.execute_insert_async(cancellable); + } + + public async ImapFolderPropertiesRow? fetch_async(int64 folder_id, Cancellable? cancellable = null) + throws Error { + SQLHeavy.Query query = db.prepare( + "SELECT id, uid_validity, attributes FROM ImapFolderPropertiesTable WHERE folder_id = ?"); + query.bind_int64(0, folder_id); + + SQLHeavy.QueryResult result = yield query.execute_async(cancellable); + if (result.finished) + return null; + + Geary.Imap.UIDValidity? uid_validity = null; + if (result.fetch_int64(1) >= 0) + uid_validity = new Geary.Imap.UIDValidity(result.fetch_int64(1)); + + return new ImapFolderPropertiesRow(this, result.fetch_int64(0), folder_id, uid_validity, + result.fetch_string(2)); + } +} + diff --git a/src/engine/sqlite/imap/sqlite-imap-message-properties-row.vala b/src/engine/sqlite/imap/sqlite-imap-message-properties-row.vala new file mode 100644 index 00000000..d284117e --- /dev/null +++ b/src/engine/sqlite/imap/sqlite-imap-message-properties-row.vala @@ -0,0 +1,34 @@ +/* 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.ImapMessagePropertiesRow : Geary.Sqlite.Row { + public int64 id { get; private set; } + public int64 message_id { get; private set; } + public string flags { get; private set; } + + public ImapMessagePropertiesRow(ImapMessagePropertiesTable table, int64 id, int64 message_id, + string flags) { + base (table); + + this.id = id; + this.message_id = message_id; + this.flags = flags; + } + + public ImapMessagePropertiesRow.from_imap_properties(ImapMessagePropertiesTable table, + int64 message_id, Geary.Imap.EmailProperties properties) { + base (table); + + id = Row.INVALID_ID; + this.message_id = message_id; + flags = properties.flags.serialize(); + } + + public Geary.Imap.EmailProperties get_imap_email_properties() { + return new Geary.Imap.EmailProperties(Geary.Imap.MessageFlags.deserialize(flags)); + } +} + diff --git a/src/engine/sqlite/imap/sqlite-imap-message-properties-table.vala b/src/engine/sqlite/imap/sqlite-imap-message-properties-table.vala new file mode 100644 index 00000000..cbeb5c78 --- /dev/null +++ b/src/engine/sqlite/imap/sqlite-imap-message-properties-table.vala @@ -0,0 +1,52 @@ +/* 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.ImapMessagePropertiesTable : Geary.Sqlite.Table { + // This *must* be in the same order as the schema. + public enum Column { + ID, + FLAGS + } + + public ImapMessagePropertiesTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { + base (gdb, table); + } + + public async int64 create_async(ImapMessagePropertiesRow row, Cancellable? cancellable = null) + throws Error { + SQLHeavy.Query query = db.prepare( + "INSERT INTO ImapMessagePropertiesTable (message_id, flags) VALUES (?, ?)"); + query.bind_int64(0, row.message_id); + query.bind_string(1, row.flags); + + return yield query.execute_insert_async(cancellable); + } + + public async ImapMessagePropertiesRow? fetch_async(int64 message_id, Cancellable? cancellable = null) + throws Error { + SQLHeavy.Query query = db.prepare( + "SELECT id, flags FROM ImapMessagePropertiesTable WHERE message_id = ?"); + query.bind_int64(0, message_id); + + SQLHeavy.QueryResult result = yield query.execute_async(cancellable); + if (result.finished) + return null; + + return new ImapMessagePropertiesRow(this, result.fetch_int64(0), message_id, + result.fetch_string(1)); + } + + public async void update_async(int64 message_id, string flags, Cancellable? cancellable = null) + throws Error { + SQLHeavy.Query query = db.prepare( + "UPDATE ImapMessagePropertiesTable SET flags = ? WHERE message_id = ?"); + query.bind_string(0, flags); + query.bind_int64(1, message_id); + + yield query.execute_async(cancellable); + } +} + diff --git a/src/engine/util/util-trillian.vala b/src/engine/util/util-trillian.vala index f96719b0..600737f7 100644 --- a/src/engine/util/util-trillian.vala +++ b/src/engine/util/util-trillian.vala @@ -9,6 +9,7 @@ */ public enum Geary.Trillian { + // DO NOT MODIFY unless you know what you're doing. These values are persisted. UNKNOWN = -1, FALSE = 0, TRUE = 1; diff --git a/src/wscript b/src/wscript index ac8d62be..b06fce24 100644 --- a/src/wscript +++ b/src/wscript @@ -29,6 +29,7 @@ def build(bld): '../engine/api/geary-folder-properties.vala', '../engine/api/geary-folder.vala', '../engine/api/geary-generic-imap-account.vala', + '../engine/api/geary-generic-imap-folder.vala', '../engine/api/geary-gmail-account.vala', '../engine/api/geary-local-interfaces.vala', '../engine/api/geary-personality.vala', @@ -99,8 +100,12 @@ def build(bld): '../engine/sqlite/email/sqlite-message-location-table.vala', '../engine/sqlite/email/sqlite-message-row.vala', '../engine/sqlite/email/sqlite-message-table.vala', + '../engine/sqlite/imap/sqlite-imap-folder-properties-row.vala', + '../engine/sqlite/imap/sqlite-imap-folder-properties-table.vala', '../engine/sqlite/imap/sqlite-imap-message-location-properties-row.vala', '../engine/sqlite/imap/sqlite-imap-message-location-properties-table.vala', + '../engine/sqlite/imap/sqlite-imap-message-properties-row.vala', + '../engine/sqlite/imap/sqlite-imap-message-properties-table.vala', '../engine/state/state-machine-descriptor.vala', '../engine/state/state-machine.vala',