From 974e9459a3e7ac1d0b5d889b5ff1881dfd206f1e Mon Sep 17 00:00:00 2001 From: Robert Schroll Date: Tue, 14 May 2013 18:07:53 -0700 Subject: [PATCH] Allow external images from whitelisted senders to be displayed: Closes #5642 This also cleans up some of the update logic for ContactTable, which could drop contacts' human-readable names when updating their importance. --- sql/CMakeLists.txt | 1 + sql/version-009.sql | 6 + src/CMakeLists.txt | 5 +- src/client/views/conversation-viewer.vala | 74 +++++++++-- src/engine/api/geary-contact-flags.vala | 50 ++++++++ src/engine/api/geary-contact-store.vala | 9 +- src/engine/api/geary-contact.vala | 9 +- src/engine/api/geary-conversation.vala | 6 +- src/engine/api/geary-email-flags.vala | 121 +++--------------- ...-email-flag.vala => geary-named-flag.vala} | 16 ++- src/engine/api/geary-named-flags.vala | 107 ++++++++++++++++ src/engine/imap-db/imap-db-account.vala | 23 +++- src/engine/imap-db/imap-db-contact.vala | 69 ++++++++-- src/engine/imap-db/imap-db-database.vala | 2 +- src/engine/imap-db/imap-db-folder.vala | 8 +- .../imap-engine-contact-store.vala | 26 ++++ src/engine/imap/api/imap-email-flags.vala | 8 +- 17 files changed, 392 insertions(+), 148 deletions(-) create mode 100644 sql/version-009.sql create mode 100644 src/engine/api/geary-contact-flags.vala rename src/engine/api/{geary-email-flag.vala => geary-named-flag.vala} (54%) create mode 100644 src/engine/api/geary-named-flags.vala create mode 100644 src/engine/imap-engine/imap-engine-contact-store.vala diff --git a/sql/CMakeLists.txt b/sql/CMakeLists.txt index 86490129..02cc18f9 100644 --- a/sql/CMakeLists.txt +++ b/sql/CMakeLists.txt @@ -8,3 +8,4 @@ install(FILES version-005.sql DESTINATION ${SQL_DEST}) install(FILES version-006.sql DESTINATION ${SQL_DEST}) install(FILES version-007.sql DESTINATION ${SQL_DEST}) install(FILES version-008.sql DESTINATION ${SQL_DEST}) +install(FILES version-009.sql DESTINATION ${SQL_DEST}) diff --git a/sql/version-009.sql b/sql/version-009.sql new file mode 100644 index 00000000..ab236902 --- /dev/null +++ b/sql/version-009.sql @@ -0,0 +1,6 @@ +-- +-- Add flags column to the ContactTable +-- + +ALTER TABLE ContactTable ADD COLUMN flags TEXT; + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5c8dfd4b..810fbfd4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,13 +18,13 @@ engine/api/geary-attachment.vala engine/api/geary-base-object.vala engine/api/geary-composed-email.vala engine/api/geary-contact.vala +engine/api/geary-contact-flags.vala engine/api/geary-contact-importance.vala engine/api/geary-contact-store.vala engine/api/geary-conversation.vala engine/api/geary-conversation-monitor.vala engine/api/geary-credentials.vala engine/api/geary-credentials-mediator.vala -engine/api/geary-email-flag.vala engine/api/geary-email-flags.vala engine/api/geary-email-identifier.vala engine/api/geary-email-properties.vala @@ -42,6 +42,8 @@ engine/api/geary-folder-supports-mark.vala engine/api/geary-folder-supports-move.vala engine/api/geary-folder-supports-remove.vala engine/api/geary-logging.vala +engine/api/geary-named-flag.vala +engine/api/geary-named-flags.vala engine/api/geary-service-provider.vala engine/api/geary-special-folder-type.vala @@ -123,6 +125,7 @@ engine/imap-db/outbox/smtp-outbox-folder-root.vala engine/imap-engine/imap-engine.vala engine/imap-engine/imap-engine-account-synchronizer.vala engine/imap-engine/imap-engine-batch-operations.vala +engine/imap-engine/imap-engine-contact-store.vala engine/imap-engine/imap-engine-email-flag-watcher.vala engine/imap-engine/imap-engine-email-prefetcher.vala engine/imap-engine/imap-engine-generic-account.vala diff --git a/src/client/views/conversation-viewer.vala b/src/client/views/conversation-viewer.vala index 7fc58dff..9e0f2487 100644 --- a/src/client/views/conversation-viewer.vala +++ b/src/client/views/conversation-viewer.vala @@ -190,17 +190,23 @@ public class ConversationViewer : Gtk.Box { } if (remote_images) { - if (email.load_remote_images().is_certain()) { - show_images_email(div_message); + Geary.Contact contact = current_folder.account.get_contact_store().get_by_rfc822( + email.get_primary_originator()); + bool always_load = contact != null && contact.always_load_remote_images(); + + if (always_load || email.load_remote_images().is_certain()) { + show_images_email(div_message, false); } else { WebKit.DOM.HTMLElement remote_images_bar = Util.DOM.select(div_message, ".remote_images"); try { ((WebKit.DOM.Element) remote_images_bar).get_class_list().add("show"); remote_images_bar.set_inner_html("""%s %s - """.printf( + + """.printf( remote_images_bar.get_inner_html(), - _("This message contains remote images."), _("Show Images"))); + _("This message contains remote images."), _("Show images"), + _("Always show from sender"))); } catch (Error error) { warning("Error showing remote images bar: %s", error.message); } @@ -239,6 +245,7 @@ public class ConversationViewer : Gtk.Box { bind_event(web_view, ".attachment_container .attachment", "click", (Callback) on_attachment_clicked, this); bind_event(web_view, ".attachment_container .attachment", "contextmenu", (Callback) on_attachment_menu, this); bind_event(web_view, ".remote_images .show_images", "click", (Callback) on_show_images, this); + bind_event(web_view, ".remote_images .show_from", "click", (Callback) on_show_images_from, this); bind_event(web_view, ".remote_images .close_show_images", "click", (Callback) on_close_show_images, this); // Update the search results @@ -689,11 +696,50 @@ public class ConversationViewer : Gtk.Box { ConversationViewer conversation_viewer) { WebKit.DOM.HTMLElement? email_element = closest_ancestor(element, ".email"); if (email_element != null) - conversation_viewer.show_images_email(email_element); + conversation_viewer.show_images_email(email_element, true); } - private void show_images_email(WebKit.DOM.Element email_element) { - // TODO: Remember that these images have been shown. + private static void on_show_images_from(WebKit.DOM.Element element, WebKit.DOM.Event event, + ConversationViewer conversation_viewer) { + Geary.Email? email = conversation_viewer.get_email_from_element(element); + if (email == null) + return; + + Geary.ContactStore contact_store = + conversation_viewer.current_folder.account.get_contact_store(); + Geary.Contact? contact = contact_store.get_by_rfc822(email.get_primary_originator()); + if (contact == null) { + debug("Couldn't find contact for %s", email.from.to_string()); + return; + } + + Geary.ContactFlags flags = new Geary.ContactFlags(); + flags.add(Geary.ContactFlags.ALWAYS_LOAD_REMOTE_IMAGES); + Gee.ArrayList contact_list = new Gee.ArrayList(); + contact_list.add(contact); + contact_store.mark_contacts_async.begin(contact_list, flags, null); + + WebKit.DOM.Document document = conversation_viewer.web_view.get_dom_document(); + try { + WebKit.DOM.NodeList nodes = document.query_selector_all(".email"); + for (ulong i = 0; i < nodes.length; i ++) { + WebKit.DOM.Element? email_element = nodes.item(i) as WebKit.DOM.Element; + if (email_element != null) { + WebKit.DOM.Element? address = email_element.query_selector(".address_name"); + if (address != null) { + WebKit.DOM.Element? mailto_link = address.parent_node as WebKit.DOM.Element; + if (mailto_link != null && contact.normalized_email == + mailto_link.get_attribute("href").substring(7).normalize().casefold()) + conversation_viewer.show_images_email(email_element, false); + } + } + } + } catch (Error error) { + debug("Error showing images: %s", error.message); + } + } + + private void show_images_email(WebKit.DOM.Element email_element, bool remember) { try { WebKit.DOM.NodeList body_nodes = email_element.query_selector_all(".body"); for (ulong j = 0; j < body_nodes.length; j++) { @@ -720,12 +766,14 @@ public class ConversationViewer : Gtk.Box { warning("Error showing images: %s", error.message); } - // only add flag to load remote images if not already present - Geary.Email? message = get_email_from_element(email_element); - if (message != null && !message.load_remote_images().is_certain()) { - Geary.EmailFlags flags = new Geary.EmailFlags(); - flags.add(Geary.EmailFlags.LOAD_REMOTE_IMAGES); - mark_message(message, flags, null); + if (remember) { + // only add flag to load remote images if not already present + Geary.Email? message = get_email_from_element(email_element); + if (message != null && !message.load_remote_images().is_certain()) { + Geary.EmailFlags flags = new Geary.EmailFlags(); + flags.add(Geary.EmailFlags.LOAD_REMOTE_IMAGES); + mark_message(message, flags, null); + } } } diff --git a/src/engine/api/geary-contact-flags.vala b/src/engine/api/geary-contact-flags.vala new file mode 100644 index 00000000..237e04b8 --- /dev/null +++ b/src/engine/api/geary-contact-flags.vala @@ -0,0 +1,50 @@ +/* Copyright 2013 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. + */ + +/** + * A collection of NamedFlags that can be used to enable/disable various user-defined + * options for a contact. System- or Geary-defined flags are available as static + * members. + */ + +public class Geary.ContactFlags : Geary.NamedFlags { + private static NamedFlag? _always_load_remote_images = null; + public static NamedFlag ALWAYS_LOAD_REMOTE_IMAGES { get { + if (_always_load_remote_images == null) + _always_load_remote_images = new NamedFlag("ALWAYSLOADREMOTEIMAGES"); + + return _always_load_remote_images; + } } + + public ContactFlags() { + } + + public static ContactFlags deserialize(string? flags) { + if (String.is_empty(flags)) + return new ContactFlags(); + + ContactFlags result = new ContactFlags(); + + string[] tokens = flags.split(" "); + foreach (string flag in tokens) + result.add(new NamedFlag(flag)); + + return result; + } + + public inline bool always_load_remote_images() { + return contains(ALWAYS_LOAD_REMOTE_IMAGES); + } + + public string serialize() { + string ret = ""; + foreach (NamedFlag flag in list) + ret += flag.serialize() + " "; + + return ret.strip(); + } +} + diff --git a/src/engine/api/geary-contact-store.vala b/src/engine/api/geary-contact-store.vala index b1c73c82..2a308762 100644 --- a/src/engine/api/geary-contact-store.vala +++ b/src/engine/api/geary-contact-store.vala @@ -4,7 +4,7 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.ContactStore : BaseObject { +public abstract class Geary.ContactStore : BaseObject { public Gee.Collection contacts { owned get { return contact_map.values; } } @@ -24,6 +24,13 @@ public class Geary.ContactStore : BaseObject { update_contact(contact); } + public abstract async void mark_contacts_async(Gee.Collection contacts, ContactFlags? to_add, + ContactFlags? to_remove) throws Error; + + public Contact? get_by_rfc822(Geary.RFC822.MailboxAddress address) { + return contact_map[address.address.normalize().casefold()]; + } + private void update_contact(Contact contact) { Contact? old_contact = contact_map[contact.normalized_email]; if (old_contact == null) { diff --git a/src/engine/api/geary-contact.vala b/src/engine/api/geary-contact.vala index 180a4460..9ac575b9 100644 --- a/src/engine/api/geary-contact.vala +++ b/src/engine/api/geary-contact.vala @@ -9,12 +9,15 @@ public class Geary.Contact : BaseObject { public string email { get; private set; } public string? real_name { get; private set; } public int highest_importance { get; set; } + public ContactFlags? contact_flags { get; set; default = null; } - public Contact(string email, string? real_name, int highest_importance, string? normalized_email = null) { + public Contact(string email, string? real_name, int highest_importance, + string? normalized_email = null, ContactFlags? contact_flags = null) { this.normalized_email = normalized_email ?? email.normalize().casefold(); this.email = email; this.real_name = real_name; this.highest_importance = highest_importance; + this.contact_flags = contact_flags; } public Contact.from_rfc822_address(RFC822.MailboxAddress address, int highest_importance) { @@ -24,4 +27,8 @@ public class Geary.Contact : BaseObject { public RFC822.MailboxAddress get_rfc822_address() { return new RFC822.MailboxAddress(real_name, email); } + + public inline bool always_load_remote_images() { + return contact_flags != null && contact_flags.always_load_remote_images(); + } } diff --git a/src/engine/api/geary-conversation.vala b/src/engine/api/geary-conversation.vala index d78091f6..c31a30e2 100644 --- a/src/engine/api/geary-conversation.vala +++ b/src/engine/api/geary-conversation.vala @@ -126,7 +126,7 @@ public abstract class Geary.Conversation : BaseObject { */ public abstract Geary.EmailIdentifier? get_lowest_email_id(); - private bool check_flag(Geary.EmailFlag flag, bool contains) { + private bool check_flag(Geary.NamedFlag flag, bool contains) { foreach (Geary.Email email in get_emails(Ordering.NONE)) { if (email.email_flags != null && email.email_flags.contains(flag) == contains) return true; @@ -135,11 +135,11 @@ public abstract class Geary.Conversation : BaseObject { return false; } - private bool has_flag(Geary.EmailFlag flag) { + private bool has_flag(Geary.NamedFlag flag) { return check_flag(flag, true); } - private bool is_missing_flag(Geary.EmailFlag flag) { + private bool is_missing_flag(Geary.NamedFlag flag) { return check_flag(flag, false); } } diff --git a/src/engine/api/geary-email-flags.vala b/src/engine/api/geary-email-flags.vala index dd4c0dc3..1a505036 100644 --- a/src/engine/api/geary-email-flags.vala +++ b/src/engine/api/geary-email-flags.vala @@ -4,97 +4,44 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.EmailFlags : BaseObject, Gee.Hashable { - private static EmailFlag? _unread = null; - public static EmailFlag UNREAD { get { +/** + * A collection of NamedFlags that can be used to enable/disable various user-defined + * options for an email message. System- or Geary-defined flags are available as static + * members. + * + * Note that how flags are represented by a particular email storage system may differ from + * how they're presented here. In particular, the manner of serializing and deserializing + * the flags may be handled by an internal subclass. + */ + +public class Geary.EmailFlags : Geary.NamedFlags { + private static NamedFlag? _unread = null; + public static NamedFlag UNREAD { get { if (_unread == null) - _unread = new EmailFlag("UNREAD"); + _unread = new NamedFlag("UNREAD"); return _unread; } } - private static EmailFlag? _flagged = null; - public static EmailFlag FLAGGED { get { + private static NamedFlag? _flagged = null; + public static NamedFlag FLAGGED { get { if (_flagged == null) - _flagged = new EmailFlag("FLAGGED"); + _flagged = new NamedFlag("FLAGGED"); return _flagged; } } - private static EmailFlag? _load_remote_images = null; - public static EmailFlag LOAD_REMOTE_IMAGES { get { + private static NamedFlag? _load_remote_images = null; + public static NamedFlag LOAD_REMOTE_IMAGES { get { if (_load_remote_images == null) - _load_remote_images = new EmailFlag("LOADREMOTEIMAGES"); + _load_remote_images = new NamedFlag("LOADREMOTEIMAGES"); return _load_remote_images; } } - private Gee.Set list = new Gee.HashSet(); - - public virtual signal void added(Gee.Collection flags) { - } - - public virtual signal void removed(Gee.Collection flags) { - } - public EmailFlags() { } - protected virtual void notify_added(Gee.Collection flags) { - added(flags); - } - - protected virtual void notify_removed(Gee.Collection flags) { - removed(flags); - } - - public bool contains(EmailFlag flag) { - return list.contains(flag); - } - - public Gee.Set get_all() { - return list.read_only_view; - } - - public virtual void add(EmailFlag flag) { - if (!list.contains(flag)) { - list.add(flag); - notify_added(new Collection.SingleItem(flag)); - } - } - - public virtual void add_all(EmailFlags flags) { - Gee.ArrayList added = new Gee.ArrayList(); - foreach (EmailFlag flag in flags.get_all()) { - if (!list.contains(flag)) - added.add(flag); - } - - list.add_all(added); - notify_added(added); - } - - public virtual bool remove(EmailFlag flag) { - bool removed = list.remove(flag); - if (removed) - notify_removed(new Collection.SingleItem(flag)); - - return removed; - } - - public virtual bool remove_all(EmailFlags flags) { - Gee.ArrayList removed = new Gee.ArrayList(); - foreach (EmailFlag flag in flags.get_all()) { - if (list.contains(flag)) - removed.add(flag); - } - - list.remove_all(removed); - notify_removed(removed); - - return removed.size > 0; - } - // Convenience method to check if the unread flag is set. public inline bool is_unread() { return contains(UNREAD); @@ -107,33 +54,5 @@ public class Geary.EmailFlags : BaseObject, Gee.Hashable { public inline bool load_remote_images() { return contains(LOAD_REMOTE_IMAGES); } - - public bool equal_to(Geary.EmailFlags other) { - if (this == other) - return true; - - if (list.size != other.list.size) - return false; - - foreach (EmailFlag flag in list) { - if (!other.contains(flag)) - return false; - } - - return true; - } - - public uint hash() { - return Geary.String.stri_hash(to_string()); - } - - public string to_string() { - string ret = "["; - foreach (EmailFlag flag in list) { - ret += flag.to_string() + " "; - } - - return ret + "]"; - } } diff --git a/src/engine/api/geary-email-flag.vala b/src/engine/api/geary-named-flag.vala similarity index 54% rename from src/engine/api/geary-email-flag.vala rename to src/engine/api/geary-named-flag.vala index f25277b3..13534c81 100644 --- a/src/engine/api/geary-email-flag.vala +++ b/src/engine/api/geary-named-flag.vala @@ -4,14 +4,20 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.EmailFlag : BaseObject, Gee.Hashable { +/** + * Geary offers a couple of places where the user may mark an object (email, contact) + * with a named flag. The presence of the flag indicates if the state is enabled/on + * or disabled/off. + */ + +public class Geary.NamedFlag : BaseObject, Gee.Hashable { private string name; - public EmailFlag(string name) { + public NamedFlag(string name) { this.name = name; } - public bool equal_to(Geary.EmailFlag other) { + public bool equal_to(Geary.NamedFlag other) { if (this == other) return true; @@ -22,6 +28,10 @@ public class Geary.EmailFlag : BaseObject, Gee.Hashable { return name.down().hash(); } + public string serialize() { + return name; + } + public string to_string() { return name; } diff --git a/src/engine/api/geary-named-flags.vala b/src/engine/api/geary-named-flags.vala new file mode 100644 index 00000000..0592ecec --- /dev/null +++ b/src/engine/api/geary-named-flags.vala @@ -0,0 +1,107 @@ +/* Copyright 2011-2013 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. + */ + +/** + * A signalled collection of NamedFlags. Currently Geary uses these flags for enabling/disabling + * options with email or contacts. + */ + +public class Geary.NamedFlags : BaseObject, Gee.Hashable { + protected Gee.Set list = new Gee.HashSet(); + + public virtual signal void added(Gee.Collection flags) { + } + + public virtual signal void removed(Gee.Collection flags) { + } + + public NamedFlags() { + } + + protected virtual void notify_added(Gee.Collection flags) { + added(flags); + } + + protected virtual void notify_removed(Gee.Collection flags) { + removed(flags); + } + + public bool contains(NamedFlag flag) { + return list.contains(flag); + } + + public Gee.Set get_all() { + return list.read_only_view; + } + + public virtual void add(NamedFlag flag) { + if (!list.contains(flag)) { + list.add(flag); + notify_added(new Collection.SingleItem(flag)); + } + } + + public virtual void add_all(NamedFlags flags) { + Gee.ArrayList added = new Gee.ArrayList(); + foreach (NamedFlag flag in flags.get_all()) { + if (!list.contains(flag)) + added.add(flag); + } + + list.add_all(added); + notify_added(added); + } + + public virtual bool remove(NamedFlag flag) { + bool removed = list.remove(flag); + if (removed) + notify_removed(new Collection.SingleItem(flag)); + + return removed; + } + + public virtual bool remove_all(NamedFlags flags) { + Gee.ArrayList removed = new Gee.ArrayList(); + foreach (NamedFlag flag in flags.get_all()) { + if (list.contains(flag)) + removed.add(flag); + } + + list.remove_all(removed); + notify_removed(removed); + + return removed.size > 0; + } + + public bool equal_to(Geary.NamedFlags other) { + if (this == other) + return true; + + if (list.size != other.list.size) + return false; + + foreach (NamedFlag flag in list) { + if (!other.contains(flag)) + return false; + } + + return true; + } + + public uint hash() { + return Geary.String.stri_hash(to_string()); + } + + public string to_string() { + string ret = "["; + foreach (NamedFlag flag in list) { + ret += flag.to_string() + " "; + } + + return ret + "]"; + } +} + diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala index f8653e58..09a4de0a 100644 --- a/src/engine/imap-db/imap-db-account.vala +++ b/src/engine/imap-db/imap-db-account.vala @@ -23,11 +23,11 @@ private class Geary.ImapDB.Account : BaseObject { private ImapDB.Database? db = null; private Gee.HashMap folder_refs = new Gee.HashMap(); - public ContactStore contact_store { get; private set; } + public ImapEngine.ContactStore contact_store { get; private set; } public Account(Geary.AccountInformation account_information) { this.account_information = account_information; - contact_store = new ContactStore(); + contact_store = new ImapEngine.ContactStore(this); name = "IMAP database account for %s".printf(account_information.imap_credentials.user); } @@ -262,14 +262,14 @@ private class Geary.ImapDB.Account : BaseObject { Db.TransactionOutcome outcome = db.exec_transaction(Db.TransactionType.RO, (context) => { Db.Statement statement = context.prepare( - "SELECT email, real_name, highest_importance, normalized_email " + + "SELECT email, real_name, highest_importance, normalized_email, flags " + "FROM ContactTable"); Db.Result result = statement.exec(cancellable); while (!result.finished) { try { Contact contact = new Contact(result.string_at(0), result.string_at(1), - result.int_at(2), result.string_at(3)); + result.int_at(2), result.string_at(3), ContactFlags.deserialize(result.string_at(4))); contacts.add(contact); } catch (Geary.DatabaseError err) { // We don't want to abandon loading all contacts just because there was a @@ -552,6 +552,21 @@ private class Geary.ImapDB.Account : BaseObject { return email; } + public async void update_contact_flags_async(Geary.Contact contact, Cancellable? cancellable) + throws Error{ + check_open(); + + yield db.exec_transaction_async(Db.TransactionType.RW, (cx, cancellable) => { + Db.Statement update_stmt = + cx.prepare("UPDATE ContactTable SET flags=? WHERE email=?"); + update_stmt.bind_string(0, contact.contact_flags.serialize()); + update_stmt.bind_string(1, contact.email); + update_stmt.exec(cancellable); + + return Db.TransactionOutcome.COMMIT; + }, cancellable); + } + private void clear_duplicate_folders() { int count = 0; diff --git a/src/engine/imap-db/imap-db-contact.vala b/src/engine/imap-db/imap-db-contact.vala index 9568575c..f109328f 100644 --- a/src/engine/imap-db/imap-db-contact.vala +++ b/src/engine/imap-db/imap-db-contact.vala @@ -6,19 +6,64 @@ namespace Geary.ImapDB { -private static void do_update_contact_importance(Db.Connection connection, Contact contact, - Cancellable? cancellable = null) throws Error { - // TODO: Don't overwrite a non-null real_name with a null real_name. - Db.Statement statement = connection.prepare( - "INSERT OR REPLACE INTO ContactTable(normalized_email, email, real_name, highest_importance) " - + "VALUES(?, ?, ?, MAX(COALESCE((SELECT highest_importance FROM ContactTable " - + "WHERE email=?1), -1), ?))"); - statement.bind_string(0, contact.normalized_email); - statement.bind_string(1, contact.email); - statement.bind_string(2, contact.real_name); - statement.bind_int(3, contact.highest_importance); +private Contact? do_fetch_contact(Db.Connection cx, string email, Cancellable? cancellable) + throws Error { + Db.Statement stmt = cx.prepare( + "SELECT real_name, highest_importance, normalized_email, flags FROM ContactTable " + + "WHERE email=?"); + stmt.bind_string(0, email); - statement.exec(cancellable); + Db.Result result = stmt.exec(cancellable); + if (result.finished) + return null; + + return new Contact(email, result.string_at(0), result.int_at(1), result.string_at(2), + ContactFlags.deserialize(result.string_at(3))); +} + +// Insert or update a contact in the ContactTable. If contact already exists, flags are merged +// and the importance is updated to the highest importance seen. +private void do_update_contact(Db.Connection connection, Contact contact, + Cancellable? cancellable) throws Error { + Contact? existing_contact = do_fetch_contact(connection, contact.email, cancellable); + + // If not found, insert and done + if (existing_contact == null) { + Db.Statement stmt = connection.prepare( + "INSERT INTO ContactTable(normalized_email, email, real_name, flags, highest_importance) " + + "VALUES(?, ?, ?, ?, ?)"); + stmt.bind_string(0, contact.normalized_email); + stmt.bind_string(1, contact.email); + stmt.bind_string(2, contact.real_name); + stmt.bind_string(3, (contact.contact_flags != null) ? contact.contact_flags.serialize() : null); + stmt.bind_int(4, contact.highest_importance); + + stmt.exec(cancellable); + + return; + } + + // merge two flags sets together + ContactFlags? merged_flags = contact.contact_flags; + if (existing_contact.contact_flags != null) { + if (merged_flags != null) + merged_flags.add_all(existing_contact.contact_flags); + else + merged_flags = existing_contact.contact_flags; + } + + // update remaining fields, careful not to overwrite non-null real_name with null (but + // using latest real_name if supplied) ... email is not updated (it's how existing_contact was + // keyed), normalized_email is inserted at the same time as email, leaving only real_name, + // flags, and highest_importance + Db.Statement stmt = connection.prepare( + "UPDATE ContactTable SET real_name=?, flags=?, highest_importance=? WHERE email=?"); + stmt.bind_string(0, !String.is_empty(contact.real_name) ? contact.real_name : existing_contact.real_name); + stmt.bind_string(1, (merged_flags != null) ? merged_flags.serialize() : null); + stmt.bind_int(2, int.max(contact.highest_importance, existing_contact.highest_importance)); + stmt.bind_string(3, contact.email); + + stmt.exec(cancellable); } } diff --git a/src/engine/imap-db/imap-db-database.vala b/src/engine/imap-db/imap-db-database.vala index 72878584..8f2828c1 100644 --- a/src/engine/imap-db/imap-db-database.vala +++ b/src/engine/imap-db/imap-db-database.vala @@ -43,7 +43,7 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase { MessageAddresses message_addresses = new MessageAddresses.from_result(account_owner_email, result); foreach (Contact contact in message_addresses.contacts) - do_update_contact_importance(get_master_connection(), contact); + do_update_contact(get_master_connection(), contact, null); result.next(); } } catch (Error err) { diff --git a/src/engine/imap-db/imap-db-folder.vala b/src/engine/imap-db/imap-db-folder.vala index 733dde14..e993404b 100644 --- a/src/engine/imap-db/imap-db-folder.vala +++ b/src/engine/imap-db/imap-db-folder.vala @@ -559,12 +559,12 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { Geary.Imap.EmailFlags flags = ((Geary.Imap.EmailFlags) map.get(id)); if (flags_to_add != null) { - foreach (Geary.EmailFlag flag in flags_to_add.get_all()) + foreach (Geary.NamedFlag flag in flags_to_add.get_all()) flags.add(flag); } if (flags_to_remove != null) { - foreach (Geary.EmailFlag flag in flags_to_remove.get_all()) + foreach (Geary.NamedFlag flag in flags_to_remove.get_all()) flags.remove(flag); } } @@ -912,7 +912,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { MessageAddresses message_addresses = new MessageAddresses.from_email(account_owner_email, email); foreach (Contact contact in message_addresses.contacts) - do_update_contact_importance(cx, contact, cancellable); + do_update_contact(cx, contact, cancellable); updated_contacts = message_addresses.contacts; return true; @@ -1238,7 +1238,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics { MessageAddresses message_addresses = new MessageAddresses.from_row(account_owner_email, row); foreach (Geary.Contact contact in message_addresses.contacts) - do_update_contact_importance(cx, contact, cancellable); + do_update_contact(cx, contact, cancellable); updated_contacts = message_addresses.contacts; } diff --git a/src/engine/imap-engine/imap-engine-contact-store.vala b/src/engine/imap-engine/imap-engine-contact-store.vala new file mode 100644 index 00000000..0972d3c5 --- /dev/null +++ b/src/engine/imap-engine/imap-engine-contact-store.vala @@ -0,0 +1,26 @@ +/* Copyright 2013 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. + */ + +internal class Geary.ImapEngine.ContactStore : Geary.ContactStore { + private weak ImapDB.Account account; + + internal ContactStore(ImapDB.Account account) { + this.account = account; + } + + public override async void mark_contacts_async(Gee.Collection contacts, ContactFlags? to_add, + ContactFlags? to_remove) throws Error{ + foreach (Contact contact in contacts) { + if (to_add != null) + contact.contact_flags.add_all(to_add); + + if (to_remove != null) + contact.contact_flags.remove_all(to_remove); + + yield account.update_contact_flags_async(contact, null); + } + } +} diff --git a/src/engine/imap/api/imap-email-flags.vala b/src/engine/imap/api/imap-email-flags.vala index 90808b45..4e996bde 100644 --- a/src/engine/imap/api/imap-email-flags.vala +++ b/src/engine/imap/api/imap-email-flags.vala @@ -20,8 +20,8 @@ public class Geary.Imap.EmailFlags : Geary.EmailFlags { add(LOAD_REMOTE_IMAGES); } - protected override void notify_added(Gee.Collection added) { - foreach (EmailFlag flag in added) { + protected override void notify_added(Gee.Collection added) { + foreach (NamedFlag flag in added) { if (flag.equal_to(UNREAD)) message_flags.remove(MessageFlag.SEEN); @@ -35,8 +35,8 @@ public class Geary.Imap.EmailFlags : Geary.EmailFlags { base.notify_added(added); } - protected override void notify_removed(Gee.Collection removed) { - foreach (EmailFlag flag in removed) { + protected override void notify_removed(Gee.Collection removed) { + foreach (NamedFlag flag in removed) { if (flag.equal_to(UNREAD)) message_flags.add(MessageFlag.SEEN);