Commit 974e9459 authored by Robert Schroll's avatar Robert Schroll Committed by Jim Nelson

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.
parent 6ce275f6
......@@ -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})
--
-- 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-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
......
......@@ -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
<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(),
_("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 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<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) {
// TODO: Remember that these images have been shown.
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);
}
}
}
......
/* 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.
*/
public class Geary.ContactStore : BaseObject {
public abstract class Geary.ContactStore : BaseObject {
public Gee.Collection<Contact> 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<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) {
Contact? old_contact = contact_map[contact.normalized_email];
if (old_contact == null) {
......
......@@ -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();
}
}
......@@ -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);
}
}
......
......@@ -4,97 +4,44 @@
* (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;
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<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() {
}
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.
public inline bool is_unread() {
return contains(UNREAD);
......@@ -107,33 +54,5 @@ public class Geary.EmailFlags : BaseObject, Gee.Hashable<Geary.EmailFlags> {
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 + "]";
}
}
......@@ -4,14 +4,20 @@
* (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;
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<Geary.EmailFlag> {
return name.down().hash();
}
public string serialize() {
return name;
}
public string to_string() {
return name;
}
......
/* 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 Gee.HashMap<Geary.FolderPath, FolderReference> folder_refs =
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) {
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;
......
......@@ -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);
}
}
......
......@@ -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)