From fe099d9fb961201632597dd170ee7b8f112dedcd Mon Sep 17 00:00:00 2001 From: Eric Gregory Date: Wed, 7 Dec 2011 18:46:05 -0800 Subject: [PATCH] Mark messages as read when clicked. Closes #4476 --- src/client/geary-controller.vala | 11 ++- src/client/ui/message-list-store.vala | 2 +- src/client/ui/message-viewer.vala | 2 +- src/engine/api/geary-conversation.vala | 2 +- src/engine/api/geary-email-properties.vala | 28 +++++- src/engine/api/geary-folder.vala | 9 ++ .../imap/api/imap-email-properties.vala | 7 +- src/engine/imap/api/imap-folder.vala | 18 ++++ src/engine/imap/command/imap-commands.vala | 23 ++++- src/engine/imap/message/imap-flag.vala | 15 +++ src/engine/imap/message/imap-message-set.vala | 34 +++++-- src/engine/imap/transport/imap-mailbox.vala | 91 ++++++++++++++++++- src/engine/impl/geary-abstract-folder.vala | 4 + src/engine/impl/geary-engine-folder.vala | 10 ++ src/engine/sqlite/api/sqlite-folder.vala | 46 ++++++++++ .../sqlite-imap-message-properties-table.vala | 16 ++++ 16 files changed, 297 insertions(+), 21 deletions(-) diff --git a/src/client/geary-controller.vala b/src/client/geary-controller.vala index 06b67e54..6409e867 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -203,7 +203,7 @@ public class GearyController { current_folder = folder; - yield current_folder.open_async(true, cancellable_folder); + yield current_folder.open_async(false, cancellable_folder); current_conversations = new Geary.Conversations(current_folder, MessageListStore.REQUIRED_FIELDS); @@ -351,6 +351,7 @@ public class GearyController { private async void do_select_message(Geary.Conversation conversation, Cancellable? cancellable = null) throws Error { + Gee.List messages = new Gee.ArrayList(); if (current_folder == null) { debug("Conversation selected with no folder selected"); @@ -366,7 +367,15 @@ public class GearyController { break; main_window.message_viewer.add_message(full_email); + + if (full_email.properties.email_flags.is_unread()) + messages.add(full_email.id); } + + // Mark as read. + if (messages.size > 0) + yield current_folder.mark_email_async(messages, Geary.EmailProperties.EmailFlags.NONE, + Geary.EmailProperties.EmailFlags.UNREAD, cancellable); } private void on_select_message_completed(Object? source, AsyncResult result) { diff --git a/src/client/ui/message-list-store.vala b/src/client/ui/message-list-store.vala index f7389bee..50869131 100644 --- a/src/client/ui/message-list-store.vala +++ b/src/client/ui/message-list-store.vala @@ -128,7 +128,7 @@ public class MessageListStore : Gtk.TreeStore { // If it exists, return oldest unread message. foreach (Geary.Email email in pool) - if (email.properties.is_unread()) + if (email.properties.email_flags.is_unread()) return email; // All e-mail was read, so return the newest one. diff --git a/src/client/ui/message-viewer.vala b/src/client/ui/message-viewer.vala index d31bda5c..12a4ae63 100644 --- a/src/client/ui/message-viewer.vala +++ b/src/client/ui/message-viewer.vala @@ -103,7 +103,7 @@ public class MessageViewer : Gtk.Viewport { header.column_spacing = HEADER_COL_SPACING; header.row_spacing = HEADER_ROW_SPACING; - if (email.properties.is_unread()) + if (email.properties.email_flags.is_unread()) icon_area.add(new Gtk.Image.from_pixbuf(IconFactory.instance.unread)); int header_height = 0; diff --git a/src/engine/api/geary-conversation.vala b/src/engine/api/geary-conversation.vala index 68891eb2..c639e4cb 100644 --- a/src/engine/api/geary-conversation.vala +++ b/src/engine/api/geary-conversation.vala @@ -168,7 +168,7 @@ public abstract class Geary.Conversation : Object { return false; foreach (Geary.Email email in list) { - if (email.properties.is_unread()) + if (email.properties.email_flags.is_unread()) return true; } diff --git a/src/engine/api/geary-email-properties.vala b/src/engine/api/geary-email-properties.vala index 7d38939d..3e80c67d 100644 --- a/src/engine/api/geary-email-properties.vala +++ b/src/engine/api/geary-email-properties.vala @@ -5,9 +5,33 @@ */ public abstract class Geary.EmailProperties : Object { - public EmailProperties() { + // Flags that can be set or cleared on a given e-mail. + public enum EmailFlags { + NONE = 0, + UNREAD = 1 << 0; + + public inline bool is_all_set(EmailFlags required_flags) { + return (this & required_flags) == required_flags; + } + + public inline EmailFlags set(EmailFlags flags) { + return (this | flags); + } + + public inline EmailFlags clear(EmailFlags flags) { + return (this & ~(flags)); + } + + // Convenience method to check if the unread flag is set. + public inline bool is_unread() { + return is_all_set(UNREAD); + } } - public abstract bool is_unread(); + // Flags se on the email object. + public EmailFlags email_flags { get; protected set; default = EmailFlags.NONE; } + + public EmailProperties() { + } } diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala index c4cc3947..6af46a0a 100644 --- a/src/engine/api/geary-folder.vala +++ b/src/engine/api/geary-folder.vala @@ -348,6 +348,15 @@ public interface Geary.Folder : Object { public abstract async void remove_email_async(Geary.EmailIdentifier email_id, Cancellable? cancellable = null) throws Error; + /** + * Adds or removes a flag from a list of messages. + * + * The Folder must be opened prior to attempting this operation. + */ + public abstract async void mark_email_async(Gee.List to_mark, + Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags + flags_to_remove, Cancellable? cancellable = null) throws Error; + /** * check_span_specifiers() verifies that the span specifiers match the requirements set by * list_email_async() and lazy_list_email_async(). If not, this method throws diff --git a/src/engine/imap/api/imap-email-properties.vala b/src/engine/imap/api/imap-email-properties.vala index c4a6425f..a49526fb 100644 --- a/src/engine/imap/api/imap-email-properties.vala +++ b/src/engine/imap/api/imap-email-properties.vala @@ -26,10 +26,9 @@ public class Geary.Imap.EmailProperties : Geary.EmailProperties, Equalable { flagged = flags.contains(MessageFlag.FLAGGED); recent = flags.contains(MessageFlag.RECENT); seen = flags.contains(MessageFlag.SEEN); - } - - public override bool is_unread() { - return !flags.contains(MessageFlag.SEEN); + + if (!seen) + email_flags = email_flags.set(Geary.EmailProperties.EmailFlags.UNREAD); } public bool equals(Equalable e) { diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala index 890f7b47..59994a97 100644 --- a/src/engine/imap/api/imap-folder.vala +++ b/src/engine/imap/api/imap-folder.vala @@ -191,5 +191,23 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder { throw new EngineError.READONLY("IMAP currently read-only"); } + + public override async void mark_email_async(Gee.List to_mark, + Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags + flags_to_remove, Cancellable? cancellable = null) throws Error { + if (mailbox == null) + throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); + + // Build an array of UIDs. + Geary.Imap.UID[] sparse_set = new Geary.Imap.UID[to_mark.size]; + int i = 0; + foreach(Geary.EmailIdentifier id in to_mark) { + sparse_set[i] = ((Geary.Imap.EmailIdentifier) id).uid; + i++; + } + + MessageSet message_set = new MessageSet.uid_sparse(sparse_set); + mailbox.mark_email_async(message_set, flags_to_add, flags_to_remove, cancellable); + } } diff --git a/src/engine/imap/command/imap-commands.vala b/src/engine/imap/command/imap-commands.vala index 81ae5e69..fa8ffeca 100644 --- a/src/engine/imap/command/imap-commands.vala +++ b/src/engine/imap/command/imap-commands.vala @@ -94,7 +94,7 @@ public class Geary.Imap.StatusCommand : Command { public StatusCommand(string mailbox, StatusDataType[] data_items) { base (NAME); - add (new StringParameter(mailbox)); + add(new StringParameter(mailbox)); assert(data_items.length > 0); ListParameter data_item_list = new ListParameter(this); @@ -105,3 +105,24 @@ public class Geary.Imap.StatusCommand : Command { } } +public class Geary.Imap.StoreCommand : Command { + public const string NAME = "store"; + public const string UID_NAME = "uid store"; + + public StoreCommand(MessageSet message_set, Gee.List flag_list, bool add_flag, + bool silent) { + base (message_set.is_uid ? UID_NAME : NAME); + + add(message_set.to_parameter()); + add(new StringParameter("%sflags%s".printf(add_flag ? "+" : "-", silent ? ".silent" : ""))); + + ListParameter list = new ListParameter(this); + foreach(MessageFlag flag in flag_list) + list.add(new StringParameter(flag.value)); + + add(list); + + debug("command: %s", this.to_string()); + } +} + diff --git a/src/engine/imap/message/imap-flag.vala b/src/engine/imap/message/imap-flag.vala index 29c34e2c..33c03c19 100644 --- a/src/engine/imap/message/imap-flag.vala +++ b/src/engine/imap/message/imap-flag.vala @@ -96,6 +96,21 @@ public class Geary.Imap.MessageFlag : Geary.Imap.Flag { public MessageFlag(string value) { base (value); } + + // Converts a list of email flags to add and remove to a list of message + // flags to add and remove. + public static void from_email_flags(Geary.EmailProperties.EmailFlags email_flags_add, + Geary.EmailProperties.EmailFlags email_flags_remove, out Gee.List msg_flags_add, + out Gee.List msg_flags_remove) { + msg_flags_add = new Gee.ArrayList(); + msg_flags_remove = new Gee.ArrayList(); + + if (email_flags_add.is_all_set(Geary.EmailProperties.EmailFlags.UNREAD)) + msg_flags_remove.add(MessageFlag.SEEN); + + if (email_flags_remove.is_all_set(Geary.EmailProperties.EmailFlags.UNREAD)) + msg_flags_add.add(MessageFlag.SEEN); + } } public class Geary.Imap.MailboxAttribute : Geary.Imap.Flag { diff --git a/src/engine/imap/message/imap-message-set.vala b/src/engine/imap/message/imap-message-set.vala index 8134ce9f..04086266 100644 --- a/src/engine/imap/message/imap-message-set.vala +++ b/src/engine/imap/message/imap-message-set.vala @@ -84,11 +84,16 @@ public class Geary.Imap.MessageSet { } public MessageSet.sparse(int[] msg_nums) { - value = build_sparse_range(msg_nums); + value = build_sparse_range(msg_array_to_int64(msg_nums)); + } + + public MessageSet.uid_sparse(UID[] msg_uids) { + value = build_sparse_range(uid_array_to_int64(msg_uids)); + is_uid = true; } public MessageSet.sparse_to_highest(int[] msg_nums) { - value = "%s:*".printf(build_sparse_range(msg_nums)); + value = "%s:*".printf(build_sparse_range(msg_array_to_int64(msg_nums))); } public MessageSet.multirange(MessageSet[] msg_sets) { @@ -128,25 +133,42 @@ public class Geary.Imap.MessageSet { is_uid = true; } + // Builds sparse range of either UID values or message numbers. // TODO: It would be more efficient to look for runs in the numbers and form the set specifier // with them. - private static string build_sparse_range(int[] msg_nums) { + private static string build_sparse_range(int64[] msg_nums) { assert(msg_nums.length > 0); StringBuilder builder = new StringBuilder(); for (int ctr = 0; ctr < msg_nums.length; ctr++) { - int msg_num = msg_nums[ctr]; + int64 msg_num = msg_nums[ctr]; assert(msg_num >= 0); if (ctr < (msg_nums.length - 1)) - builder.append_printf("%d,", msg_num); + builder.append_printf("%lld,", msg_num); else - builder.append_printf("%d", msg_num); + builder.append_printf("%lld", msg_num); } return builder.str; } + private static int64[] msg_array_to_int64(int[] msg_nums) { + int64[] ret = new int64[0]; + foreach (int num in msg_nums) + ret += (int64) num; + + return ret; + } + + private static int64[] uid_array_to_int64(UID[] msg_uids) { + int64[] ret = new int64[0]; + foreach (UID uid in msg_uids) + ret += uid.value; + + return ret; + } + public Parameter to_parameter() { // Message sets are not quoted, even if they use an atom-special character (this *might* // be a Gmailism...) diff --git a/src/engine/imap/transport/imap-mailbox.vala b/src/engine/imap/transport/imap-mailbox.vala index 0501bbe5..6f11f39d 100644 --- a/src/engine/imap/transport/imap-mailbox.vala +++ b/src/engine/imap/transport/imap-mailbox.vala @@ -89,10 +89,21 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { int plain_id = batch.add(new MailboxOperation(context, fetch_cmd)); + int body_id = NonblockingBatch.INVALID_ID; int preview_id = NonblockingBatch.INVALID_ID; int preview_charset_id = NonblockingBatch.INVALID_ID; int properties_id = NonblockingBatch.INVALID_ID; + if (fields.require(Geary.Email.Field.BODY)) { + // Fetch the body. + Gee.List types = new Gee.ArrayList(); + types.add(new FetchBodyDataType.peek( + FetchBodyDataType.SectionPart.TEXT, null, -1, -1, null)); + FetchCommand fetch_body = new FetchCommand(msg_set, null, types); + + body_id = batch.add(new MailboxOperation(context, fetch_body)); + } + if (fields.require(Geary.Email.Field.PREVIEW)) { // Preview text. FetchBodyDataType fetch_preview = new FetchBodyDataType.peek(FetchBodyDataType.SectionPart.NONE, @@ -155,6 +166,26 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { map.set(plain_res.msg_num, email); } + // Process body results. + if (body_id != NonblockingBatch.INVALID_ID) { + MailboxOperation body_op = (MailboxOperation) batch.get_operation(body_id); + CommandResponse body_resp = (CommandResponse) batch.get_result(body_id); + + if (body_resp.status_response.status != Status.OK) { + throw new ImapError.SERVER_ERROR("Server error for %s: %s", + body_op.cmd.to_string(), body_resp.to_string()); + } + + FetchResults[] body_results = FetchResults.decode(body_resp); + foreach (FetchResults body_res in body_results) { + Geary.Email? body_email = map.get(body_res.msg_num); + if (body_email == null) + continue; + + body_email.set_message_body(new Geary.RFC822.Text(body_res.get_body_data().get(0))); + } + } + // Process properties results. if (properties_id != NonblockingBatch.INVALID_ID) { MailboxOperation properties_op = (MailboxOperation) batch.get_operation(properties_id); @@ -291,13 +322,10 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { break; case Geary.Email.Field.BODY: - data_types_list.add(FetchDataType.RFC822_TEXT); - break; - case Geary.Email.Field.PROPERTIES: case Geary.Email.Field.NONE: case Geary.Email.Field.PREVIEW: - // not set (or, for previews and properties, fetched separately) + // not set (or, for body previews and properties, fetched separately) break; default: @@ -467,6 +495,61 @@ public class Geary.Imap.Mailbox : Geary.SmartReference { if (fields.require(Geary.Email.Field.REFERENCES)) email.set_full_references(message_id, in_reply_to, references); } + + public async void mark_email_async(MessageSet to_mark, Geary.EmailProperties.EmailFlags + flags_to_add, Geary.EmailProperties.EmailFlags flags_to_remove, + Cancellable? cancellable = null) throws Error { + + if (context.is_closed()) + throw new ImapError.NOT_SELECTED("Mailbox %s closed", name); + + Gee.List msg_flags_add = new Gee.ArrayList(); + Gee.List msg_flags_remove = new Gee.ArrayList(); + MessageFlag.from_email_flags(flags_to_add, flags_to_remove, out msg_flags_add, + out msg_flags_remove); + + NonblockingBatch batch = new NonblockingBatch(); + int add_flags_id = NonblockingBatch.INVALID_ID; + int remove_flags_id = NonblockingBatch.INVALID_ID; + + if (msg_flags_add.size > 0) + add_flags_id = batch.add(new MailboxOperation(context, new StoreCommand( + to_mark, msg_flags_add, true, true))); + + if (msg_flags_remove.size > 0) + remove_flags_id = batch.add(new MailboxOperation(context, new StoreCommand( + to_mark, msg_flags_remove, false, true))); + + yield batch.execute_all_async(cancellable); + + if (add_flags_id != NonblockingBatch.INVALID_ID) { + MailboxOperation add_op = (MailboxOperation) batch.get_operation(add_flags_id); + CommandResponse add_resp = (CommandResponse) batch.get_result(add_flags_id); + + if (add_resp.status_response == null) + throw new ImapError.SERVER_ERROR("Server error. Command: %s No status response. %s", + add_op.cmd.to_string(), add_resp.to_string()); + + if (add_resp.status_response.status != Status.OK) + throw new ImapError.SERVER_ERROR("Server error. Command: %s Response: %s Error: %s", + add_op.cmd.to_string(), add_resp.to_string(), + add_resp.status_response.status.to_string()); + } + + if (remove_flags_id != NonblockingBatch.INVALID_ID) { + MailboxOperation remove_op = (MailboxOperation) batch.get_operation(remove_flags_id); + CommandResponse remove_resp = (CommandResponse) batch.get_result(remove_flags_id); + + if (remove_resp.status_response == null) + throw new ImapError.SERVER_ERROR("Server error. Command: %s No status response. %s", + remove_op.cmd.to_string(), remove_resp.to_string()); + + if (remove_resp.status_response.status != Status.OK) + throw new ImapError.SERVER_ERROR("Server error. Command: %s Response: %s Error: %s", + remove_op.cmd.to_string(), remove_resp.to_string(), + remove_resp.status_response.status.to_string()); + } + } } // A SelectedContext is a ReferenceSemantics object wrapping a ClientSession that is in a SELECTED diff --git a/src/engine/impl/geary-abstract-folder.vala b/src/engine/impl/geary-abstract-folder.vala index c38a99fd..e1fb7b90 100644 --- a/src/engine/impl/geary-abstract-folder.vala +++ b/src/engine/impl/geary-abstract-folder.vala @@ -122,6 +122,10 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder { public abstract async void remove_email_async(Geary.EmailIdentifier email_id, Cancellable? cancellable = null) throws Error; + public abstract async void mark_email_async(Gee.List to_mark, + Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags + flags_to_remove, Cancellable? cancellable = null) throws Error; + public virtual string to_string() { return get_path().to_string(); } diff --git a/src/engine/impl/geary-engine-folder.vala b/src/engine/impl/geary-engine-folder.vala index b525d088..2105d505 100644 --- a/src/engine/impl/geary-engine-folder.vala +++ b/src/engine/impl/geary-engine-folder.vala @@ -939,5 +939,15 @@ private class Geary.EngineFolder : Geary.AbstractFolder { debug("prefetched %d for %s", prefetch_count, to_string()); } + + public override async void mark_email_async(Gee.List to_mark, + Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags + flags_to_remove, Cancellable? cancellable = null) throws Error { + if (!yield wait_for_remote_to_open()) + throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string()); + + yield remote_folder.mark_email_async(to_mark, flags_to_add, flags_to_remove, cancellable); + yield local_folder.mark_email_async(to_mark, flags_to_add, flags_to_remove, cancellable); + } } diff --git a/src/engine/sqlite/api/sqlite-folder.vala b/src/engine/sqlite/api/sqlite-folder.vala index 02ae8204..bf710aef 100644 --- a/src/engine/sqlite/api/sqlite-folder.vala +++ b/src/engine/sqlite/api/sqlite-folder.vala @@ -471,6 +471,52 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea notify_message_removed(id); } + public override async void mark_email_async(Gee.List to_mark, + Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags + flags_to_remove, Cancellable? cancellable = null) throws Error { + check_open(); + + Transaction transaction = yield db.begin_transaction_async("Folder.mark_email_async", + cancellable); + + Gee.List msg_flags_add = new Gee.ArrayList(); + Gee.List msg_flags_remove = + new Gee.ArrayList(); + Geary.Imap.MessageFlag.from_email_flags(flags_to_add, flags_to_remove, out msg_flags_add, + out msg_flags_remove); + + foreach (Geary.EmailIdentifier id in to_mark) { + MessageLocationRow? location_row = yield location_table.fetch_by_ordering_async( + transaction, folder_row.id, ((Geary.Imap.EmailIdentifier) id).uid.value, cancellable); + if (location_row == null) { + throw new EngineError.NOT_FOUND("No message with ID %s in folder %s", id.to_string(), + to_string()); + } + + ImapMessagePropertiesRow? row = yield imap_message_properties_table.fetch_async( + transaction, location_row.id, cancellable); + + if (row == null) { + warning("Message not found in database: %lld", location_row.id); + continue; + } + + // Create new set of message flags with the appropriate flags added and/or removed. + Gee.HashSet mutable_copy = + new Gee.HashSet(); + mutable_copy.add_all(Geary.Imap.MessageFlags.deserialize(row.flags).get_all()); + mutable_copy.remove_all(msg_flags_remove); + mutable_copy.add_all(msg_flags_add); + + Geary.Imap.MessageFlags new_flags = new Geary.Imap.MessageFlags(mutable_copy); + + yield imap_message_properties_table.update_flags_async(transaction, location_row.id, + new_flags.serialize(), cancellable); + } + + yield transaction.commit_async(cancellable); + } + public async bool is_email_present_async(Geary.EmailIdentifier id, out Geary.Email.Field available_fields, Cancellable? cancellable = null) throws Error { check_open(); diff --git a/src/engine/sqlite/imap/sqlite-imap-message-properties-table.vala b/src/engine/sqlite/imap/sqlite-imap-message-properties-table.vala index 564391be..ab54b695 100644 --- a/src/engine/sqlite/imap/sqlite-imap-message-properties-table.vala +++ b/src/engine/sqlite/imap/sqlite-imap-message-properties-table.vala @@ -76,6 +76,22 @@ public class Geary.Sqlite.ImapMessagePropertiesTable : Geary.Sqlite.Table { yield release_lock_async(transaction, locked, cancellable); } + public async void update_flags_async(Transaction? transaction, int64 message_id, string? flags, + Cancellable? cancellable) throws Error { + Transaction locked = yield obtain_lock_async(transaction, + "ImapMessagePropertiesTable.update_flags_async", cancellable); + + SQLHeavy.Query query = locked.prepare( + "UPDATE ImapMessagePropertiesTable SET flags = ? WHERE message_id = ?"); + query.bind_string(0, flags); + query.bind_int64(1, message_id); + + yield query.execute_async(cancellable); + locked.set_commit_required(); + + yield release_lock_async(transaction, locked, cancellable); + } + public async Gee.List? search_for_duplicates_async(Transaction? transaction, string? internaldate, long rfc822_size, Cancellable? cancellable) throws Error { bool has_internaldate = !String.is_empty(internaldate);