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.
This commit is contained in:
parent
6ce275f61d
commit
974e9459a3
17 changed files with 392 additions and 148 deletions
|
|
@ -8,3 +8,4 @@ install(FILES version-005.sql DESTINATION ${SQL_DEST})
|
||||||
install(FILES version-006.sql DESTINATION ${SQL_DEST})
|
install(FILES version-006.sql DESTINATION ${SQL_DEST})
|
||||||
install(FILES version-007.sql DESTINATION ${SQL_DEST})
|
install(FILES version-007.sql DESTINATION ${SQL_DEST})
|
||||||
install(FILES version-008.sql DESTINATION ${SQL_DEST})
|
install(FILES version-008.sql DESTINATION ${SQL_DEST})
|
||||||
|
install(FILES version-009.sql DESTINATION ${SQL_DEST})
|
||||||
|
|
|
||||||
6
sql/version-009.sql
Normal file
6
sql/version-009.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
--
|
||||||
|
-- Add flags column to the ContactTable
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ContactTable ADD COLUMN flags TEXT;
|
||||||
|
|
||||||
|
|
@ -18,13 +18,13 @@ engine/api/geary-attachment.vala
|
||||||
engine/api/geary-base-object.vala
|
engine/api/geary-base-object.vala
|
||||||
engine/api/geary-composed-email.vala
|
engine/api/geary-composed-email.vala
|
||||||
engine/api/geary-contact.vala
|
engine/api/geary-contact.vala
|
||||||
|
engine/api/geary-contact-flags.vala
|
||||||
engine/api/geary-contact-importance.vala
|
engine/api/geary-contact-importance.vala
|
||||||
engine/api/geary-contact-store.vala
|
engine/api/geary-contact-store.vala
|
||||||
engine/api/geary-conversation.vala
|
engine/api/geary-conversation.vala
|
||||||
engine/api/geary-conversation-monitor.vala
|
engine/api/geary-conversation-monitor.vala
|
||||||
engine/api/geary-credentials.vala
|
engine/api/geary-credentials.vala
|
||||||
engine/api/geary-credentials-mediator.vala
|
engine/api/geary-credentials-mediator.vala
|
||||||
engine/api/geary-email-flag.vala
|
|
||||||
engine/api/geary-email-flags.vala
|
engine/api/geary-email-flags.vala
|
||||||
engine/api/geary-email-identifier.vala
|
engine/api/geary-email-identifier.vala
|
||||||
engine/api/geary-email-properties.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-move.vala
|
||||||
engine/api/geary-folder-supports-remove.vala
|
engine/api/geary-folder-supports-remove.vala
|
||||||
engine/api/geary-logging.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-service-provider.vala
|
||||||
engine/api/geary-special-folder-type.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.vala
|
||||||
engine/imap-engine/imap-engine-account-synchronizer.vala
|
engine/imap-engine/imap-engine-account-synchronizer.vala
|
||||||
engine/imap-engine/imap-engine-batch-operations.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-flag-watcher.vala
|
||||||
engine/imap-engine/imap-engine-email-prefetcher.vala
|
engine/imap-engine/imap-engine-email-prefetcher.vala
|
||||||
engine/imap-engine/imap-engine-generic-account.vala
|
engine/imap-engine/imap-engine-generic-account.vala
|
||||||
|
|
|
||||||
|
|
@ -190,17 +190,23 @@ public class ConversationViewer : Gtk.Box {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remote_images) {
|
if (remote_images) {
|
||||||
if (email.load_remote_images().is_certain()) {
|
Geary.Contact contact = current_folder.account.get_contact_store().get_by_rfc822(
|
||||||
show_images_email(div_message);
|
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 {
|
} else {
|
||||||
WebKit.DOM.HTMLElement remote_images_bar =
|
WebKit.DOM.HTMLElement remote_images_bar =
|
||||||
Util.DOM.select(div_message, ".remote_images");
|
Util.DOM.select(div_message, ".remote_images");
|
||||||
try {
|
try {
|
||||||
((WebKit.DOM.Element) remote_images_bar).get_class_list().add("show");
|
((WebKit.DOM.Element) remote_images_bar).get_class_list().add("show");
|
||||||
remote_images_bar.set_inner_html("""%s %s
|
remote_images_bar.set_inner_html("""%s %s
|
||||||
<input type="button" value="%s" class="show_images" />""".printf(
|
<input type="button" value="%s" class="show_images" />
|
||||||
|
<input type="button" value="%s" class="show_from" />""".printf(
|
||||||
remote_images_bar.get_inner_html(),
|
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) {
|
} catch (Error error) {
|
||||||
warning("Error showing remote images bar: %s", error.message);
|
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", "click", (Callback) on_attachment_clicked, this);
|
||||||
bind_event(web_view, ".attachment_container .attachment", "contextmenu", (Callback) on_attachment_menu, 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_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);
|
bind_event(web_view, ".remote_images .close_show_images", "click", (Callback) on_close_show_images, this);
|
||||||
|
|
||||||
// Update the search results
|
// Update the search results
|
||||||
|
|
@ -689,11 +696,50 @@ public class ConversationViewer : Gtk.Box {
|
||||||
ConversationViewer conversation_viewer) {
|
ConversationViewer conversation_viewer) {
|
||||||
WebKit.DOM.HTMLElement? email_element = closest_ancestor(element, ".email");
|
WebKit.DOM.HTMLElement? email_element = closest_ancestor(element, ".email");
|
||||||
if (email_element != null)
|
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) {
|
private static void on_show_images_from(WebKit.DOM.Element element, WebKit.DOM.Event event,
|
||||||
// TODO: Remember that these images have been shown.
|
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<Geary.Contact> contact_list = new Gee.ArrayList<Geary.Contact>();
|
||||||
|
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 {
|
try {
|
||||||
WebKit.DOM.NodeList body_nodes = email_element.query_selector_all(".body");
|
WebKit.DOM.NodeList body_nodes = email_element.query_selector_all(".body");
|
||||||
for (ulong j = 0; j < body_nodes.length; j++) {
|
for (ulong j = 0; j < body_nodes.length; j++) {
|
||||||
|
|
@ -720,6 +766,7 @@ public class ConversationViewer : Gtk.Box {
|
||||||
warning("Error showing images: %s", error.message);
|
warning("Error showing images: %s", error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (remember) {
|
||||||
// only add flag to load remote images if not already present
|
// only add flag to load remote images if not already present
|
||||||
Geary.Email? message = get_email_from_element(email_element);
|
Geary.Email? message = get_email_from_element(email_element);
|
||||||
if (message != null && !message.load_remote_images().is_certain()) {
|
if (message != null && !message.load_remote_images().is_certain()) {
|
||||||
|
|
@ -728,6 +775,7 @@ public class ConversationViewer : Gtk.Box {
|
||||||
mark_message(message, flags, null);
|
mark_message(message, flags, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void on_close_show_images(WebKit.DOM.Element element, WebKit.DOM.Event event,
|
private static void on_close_show_images(WebKit.DOM.Element element, WebKit.DOM.Event event,
|
||||||
ConversationViewer conversation_viewer) {
|
ConversationViewer conversation_viewer) {
|
||||||
|
|
|
||||||
50
src/engine/api/geary-contact-flags.vala
Normal file
50
src/engine/api/geary-contact-flags.vala
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
* (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<Contact> contacts {
|
public Gee.Collection<Contact> contacts {
|
||||||
owned get { return contact_map.values; }
|
owned get { return contact_map.values; }
|
||||||
}
|
}
|
||||||
|
|
@ -24,6 +24,13 @@ public class Geary.ContactStore : BaseObject {
|
||||||
update_contact(contact);
|
update_contact(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract async void mark_contacts_async(Gee.Collection<Contact> 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) {
|
private void update_contact(Contact contact) {
|
||||||
Contact? old_contact = contact_map[contact.normalized_email];
|
Contact? old_contact = contact_map[contact.normalized_email];
|
||||||
if (old_contact == null) {
|
if (old_contact == null) {
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,15 @@ public class Geary.Contact : BaseObject {
|
||||||
public string email { get; private set; }
|
public string email { get; private set; }
|
||||||
public string? real_name { get; private set; }
|
public string? real_name { get; private set; }
|
||||||
public int highest_importance { get; 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.normalized_email = normalized_email ?? email.normalize().casefold();
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.real_name = real_name;
|
this.real_name = real_name;
|
||||||
this.highest_importance = highest_importance;
|
this.highest_importance = highest_importance;
|
||||||
|
this.contact_flags = contact_flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Contact.from_rfc822_address(RFC822.MailboxAddress address, int highest_importance) {
|
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() {
|
public RFC822.MailboxAddress get_rfc822_address() {
|
||||||
return new RFC822.MailboxAddress(real_name, email);
|
return new RFC822.MailboxAddress(real_name, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public inline bool always_load_remote_images() {
|
||||||
|
return contact_flags != null && contact_flags.always_load_remote_images();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ public abstract class Geary.Conversation : BaseObject {
|
||||||
*/
|
*/
|
||||||
public abstract Geary.EmailIdentifier? get_lowest_email_id();
|
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)) {
|
foreach (Geary.Email email in get_emails(Ordering.NONE)) {
|
||||||
if (email.email_flags != null && email.email_flags.contains(flag) == contains)
|
if (email.email_flags != null && email.email_flags.contains(flag) == contains)
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -135,11 +135,11 @@ public abstract class Geary.Conversation : BaseObject {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool has_flag(Geary.EmailFlag flag) {
|
private bool has_flag(Geary.NamedFlag flag) {
|
||||||
return check_flag(flag, true);
|
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);
|
return check_flag(flag, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,97 +4,44 @@
|
||||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class Geary.EmailFlags : BaseObject, Gee.Hashable<Geary.EmailFlags> {
|
/**
|
||||||
private static EmailFlag? _unread = null;
|
* A collection of NamedFlags that can be used to enable/disable various user-defined
|
||||||
public static EmailFlag UNREAD { get {
|
* 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)
|
if (_unread == null)
|
||||||
_unread = new EmailFlag("UNREAD");
|
_unread = new NamedFlag("UNREAD");
|
||||||
|
|
||||||
return _unread;
|
return _unread;
|
||||||
} }
|
} }
|
||||||
|
|
||||||
private static EmailFlag? _flagged = null;
|
private static NamedFlag? _flagged = null;
|
||||||
public static EmailFlag FLAGGED { get {
|
public static NamedFlag FLAGGED { get {
|
||||||
if (_flagged == null)
|
if (_flagged == null)
|
||||||
_flagged = new EmailFlag("FLAGGED");
|
_flagged = new NamedFlag("FLAGGED");
|
||||||
|
|
||||||
return _flagged;
|
return _flagged;
|
||||||
} }
|
} }
|
||||||
|
|
||||||
private static EmailFlag? _load_remote_images = null;
|
private static NamedFlag? _load_remote_images = null;
|
||||||
public static EmailFlag LOAD_REMOTE_IMAGES { get {
|
public static NamedFlag LOAD_REMOTE_IMAGES { get {
|
||||||
if (_load_remote_images == null)
|
if (_load_remote_images == null)
|
||||||
_load_remote_images = new EmailFlag("LOADREMOTEIMAGES");
|
_load_remote_images = new NamedFlag("LOADREMOTEIMAGES");
|
||||||
|
|
||||||
return _load_remote_images;
|
return _load_remote_images;
|
||||||
} }
|
} }
|
||||||
|
|
||||||
private Gee.Set<EmailFlag> list = new Gee.HashSet<EmailFlag>();
|
|
||||||
|
|
||||||
public virtual signal void added(Gee.Collection<EmailFlag> flags) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual signal void removed(Gee.Collection<EmailFlag> flags) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public EmailFlags() {
|
public EmailFlags() {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void notify_added(Gee.Collection<EmailFlag> flags) {
|
|
||||||
added(flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void notify_removed(Gee.Collection<EmailFlag> flags) {
|
|
||||||
removed(flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool contains(EmailFlag flag) {
|
|
||||||
return list.contains(flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Gee.Set<EmailFlag> 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<EmailFlag>(flag));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void add_all(EmailFlags flags) {
|
|
||||||
Gee.ArrayList<EmailFlag> added = new Gee.ArrayList<EmailFlag>();
|
|
||||||
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<EmailFlag>(flag));
|
|
||||||
|
|
||||||
return removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual bool remove_all(EmailFlags flags) {
|
|
||||||
Gee.ArrayList<EmailFlag> removed = new Gee.ArrayList<EmailFlag>();
|
|
||||||
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.
|
// Convenience method to check if the unread flag is set.
|
||||||
public inline bool is_unread() {
|
public inline bool is_unread() {
|
||||||
return contains(UNREAD);
|
return contains(UNREAD);
|
||||||
|
|
@ -107,33 +54,5 @@ public class Geary.EmailFlags : BaseObject, Gee.Hashable<Geary.EmailFlags> {
|
||||||
public inline bool load_remote_images() {
|
public inline bool load_remote_images() {
|
||||||
return contains(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 + "]";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,20 @@
|
||||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class Geary.EmailFlag : BaseObject, Gee.Hashable<Geary.EmailFlag> {
|
/**
|
||||||
|
* 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<Geary.NamedFlag> {
|
||||||
private string name;
|
private string name;
|
||||||
|
|
||||||
public EmailFlag(string name) {
|
public NamedFlag(string name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool equal_to(Geary.EmailFlag other) {
|
public bool equal_to(Geary.NamedFlag other) {
|
||||||
if (this == other)
|
if (this == other)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|
@ -22,6 +28,10 @@ public class Geary.EmailFlag : BaseObject, Gee.Hashable<Geary.EmailFlag> {
|
||||||
return name.down().hash();
|
return name.down().hash();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string serialize() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
public string to_string() {
|
public string to_string() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
107
src/engine/api/geary-named-flags.vala
Normal file
107
src/engine/api/geary-named-flags.vala
Normal file
|
|
@ -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<Geary.NamedFlags> {
|
||||||
|
protected Gee.Set<NamedFlag> list = new Gee.HashSet<NamedFlag>();
|
||||||
|
|
||||||
|
public virtual signal void added(Gee.Collection<NamedFlag> flags) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual signal void removed(Gee.Collection<NamedFlag> flags) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public NamedFlags() {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void notify_added(Gee.Collection<NamedFlag> flags) {
|
||||||
|
added(flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void notify_removed(Gee.Collection<NamedFlag> flags) {
|
||||||
|
removed(flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool contains(NamedFlag flag) {
|
||||||
|
return list.contains(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gee.Set<NamedFlag> 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<NamedFlag>(flag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void add_all(NamedFlags flags) {
|
||||||
|
Gee.ArrayList<NamedFlag> added = new Gee.ArrayList<NamedFlag>();
|
||||||
|
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<NamedFlag>(flag));
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool remove_all(NamedFlags flags) {
|
||||||
|
Gee.ArrayList<NamedFlag> removed = new Gee.ArrayList<NamedFlag>();
|
||||||
|
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 + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -23,11 +23,11 @@ private class Geary.ImapDB.Account : BaseObject {
|
||||||
private ImapDB.Database? db = null;
|
private ImapDB.Database? db = null;
|
||||||
private Gee.HashMap<Geary.FolderPath, FolderReference> folder_refs =
|
private Gee.HashMap<Geary.FolderPath, FolderReference> folder_refs =
|
||||||
new Gee.HashMap<Geary.FolderPath, FolderReference>();
|
new Gee.HashMap<Geary.FolderPath, FolderReference>();
|
||||||
public ContactStore contact_store { get; private set; }
|
public ImapEngine.ContactStore contact_store { get; private set; }
|
||||||
|
|
||||||
public Account(Geary.AccountInformation account_information) {
|
public Account(Geary.AccountInformation account_information) {
|
||||||
this.account_information = 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);
|
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,
|
Db.TransactionOutcome outcome = db.exec_transaction(Db.TransactionType.RO,
|
||||||
(context) => {
|
(context) => {
|
||||||
Db.Statement statement = context.prepare(
|
Db.Statement statement = context.prepare(
|
||||||
"SELECT email, real_name, highest_importance, normalized_email " +
|
"SELECT email, real_name, highest_importance, normalized_email, flags " +
|
||||||
"FROM ContactTable");
|
"FROM ContactTable");
|
||||||
|
|
||||||
Db.Result result = statement.exec(cancellable);
|
Db.Result result = statement.exec(cancellable);
|
||||||
while (!result.finished) {
|
while (!result.finished) {
|
||||||
try {
|
try {
|
||||||
Contact contact = new Contact(result.string_at(0), result.string_at(1),
|
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);
|
contacts.add(contact);
|
||||||
} catch (Geary.DatabaseError err) {
|
} catch (Geary.DatabaseError err) {
|
||||||
// We don't want to abandon loading all contacts just because there was a
|
// 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;
|
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() {
|
private void clear_duplicate_folders() {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,64 @@
|
||||||
|
|
||||||
namespace Geary.ImapDB {
|
namespace Geary.ImapDB {
|
||||||
|
|
||||||
private static void do_update_contact_importance(Db.Connection connection, Contact contact,
|
private Contact? do_fetch_contact(Db.Connection cx, string email, Cancellable? cancellable)
|
||||||
Cancellable? cancellable = null) throws Error {
|
throws Error {
|
||||||
// TODO: Don't overwrite a non-null real_name with a null real_name.
|
Db.Statement stmt = cx.prepare(
|
||||||
Db.Statement statement = connection.prepare(
|
"SELECT real_name, highest_importance, normalized_email, flags FROM ContactTable "
|
||||||
"INSERT OR REPLACE INTO ContactTable(normalized_email, email, real_name, highest_importance) "
|
+ "WHERE email=?");
|
||||||
+ "VALUES(?, ?, ?, MAX(COALESCE((SELECT highest_importance FROM ContactTable "
|
stmt.bind_string(0, email);
|
||||||
+ "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);
|
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
|
||||||
MessageAddresses message_addresses =
|
MessageAddresses message_addresses =
|
||||||
new MessageAddresses.from_result(account_owner_email, result);
|
new MessageAddresses.from_result(account_owner_email, result);
|
||||||
foreach (Contact contact in message_addresses.contacts)
|
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();
|
result.next();
|
||||||
}
|
}
|
||||||
} catch (Error err) {
|
} catch (Error err) {
|
||||||
|
|
|
||||||
|
|
@ -559,12 +559,12 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
||||||
Geary.Imap.EmailFlags flags = ((Geary.Imap.EmailFlags) map.get(id));
|
Geary.Imap.EmailFlags flags = ((Geary.Imap.EmailFlags) map.get(id));
|
||||||
|
|
||||||
if (flags_to_add != null) {
|
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);
|
flags.add(flag);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags_to_remove != null) {
|
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);
|
flags.remove(flag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -912,7 +912,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
||||||
MessageAddresses message_addresses =
|
MessageAddresses message_addresses =
|
||||||
new MessageAddresses.from_email(account_owner_email, email);
|
new MessageAddresses.from_email(account_owner_email, email);
|
||||||
foreach (Contact contact in message_addresses.contacts)
|
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;
|
updated_contacts = message_addresses.contacts;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1238,7 +1238,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
||||||
MessageAddresses message_addresses =
|
MessageAddresses message_addresses =
|
||||||
new MessageAddresses.from_row(account_owner_email, row);
|
new MessageAddresses.from_row(account_owner_email, row);
|
||||||
foreach (Geary.Contact contact in message_addresses.contacts)
|
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;
|
updated_contacts = message_addresses.contacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
26
src/engine/imap-engine/imap-engine-contact-store.vala
Normal file
26
src/engine/imap-engine/imap-engine-contact-store.vala
Normal file
|
|
@ -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<Contact> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,8 +20,8 @@ public class Geary.Imap.EmailFlags : Geary.EmailFlags {
|
||||||
add(LOAD_REMOTE_IMAGES);
|
add(LOAD_REMOTE_IMAGES);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void notify_added(Gee.Collection<EmailFlag> added) {
|
protected override void notify_added(Gee.Collection<NamedFlag> added) {
|
||||||
foreach (EmailFlag flag in added) {
|
foreach (NamedFlag flag in added) {
|
||||||
if (flag.equal_to(UNREAD))
|
if (flag.equal_to(UNREAD))
|
||||||
message_flags.remove(MessageFlag.SEEN);
|
message_flags.remove(MessageFlag.SEEN);
|
||||||
|
|
||||||
|
|
@ -35,8 +35,8 @@ public class Geary.Imap.EmailFlags : Geary.EmailFlags {
|
||||||
base.notify_added(added);
|
base.notify_added(added);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void notify_removed(Gee.Collection<EmailFlag> removed) {
|
protected override void notify_removed(Gee.Collection<NamedFlag> removed) {
|
||||||
foreach (EmailFlag flag in removed) {
|
foreach (NamedFlag flag in removed) {
|
||||||
if (flag.equal_to(UNREAD))
|
if (flag.equal_to(UNREAD))
|
||||||
message_flags.add(MessageFlag.SEEN);
|
message_flags.add(MessageFlag.SEEN);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue