From 9f893adc472079bd4ea55921f0de8c43a086ebbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Bellegarde?= Date: Thu, 30 Jun 2022 14:44:18 +0200 Subject: [PATCH] client: Add more options for displaying images from messages - An application setting allowing to always trust images - An option to trust images from an email domain - Replaces buttons by a menu in infobar --- desktop/org.gnome.Geary.gschema.xml | 6 + .../application-configuration.vala | 39 +++++ .../components-preferences-window.vala | 31 ++++ .../conversation-contact-popover.vala | 19 ++- .../conversation-message.vala | 133 ++++++++++++------ src/client/meson.build | 1 + src/client/util/util-contact.vala | 34 +++++ ui/conversation-message-menus.ui | 17 +++ ui/geary.css | 13 ++ 9 files changed, 246 insertions(+), 47 deletions(-) create mode 100644 src/client/util/util-contact.vala diff --git a/desktop/org.gnome.Geary.gschema.xml b/desktop/org.gnome.Geary.gschema.xml index 89354dc2..136b431e 100644 --- a/desktop/org.gnome.Geary.gschema.xml +++ b/desktop/org.gnome.Geary.gschema.xml @@ -97,6 +97,12 @@ The last recorded size of the detached composer window. + + [] + Allow images for these domains + Images from these domains will be trusted + + 5 Undo sending email delay diff --git a/src/client/application/application-configuration.vala b/src/client/application/application-configuration.vala index eaaed36f..3bbf0a61 100644 --- a/src/client/application/application-configuration.vala +++ b/src/client/application/application-configuration.vala @@ -30,6 +30,7 @@ public class Application.Configuration : Geary.BaseObject { public const string WINDOW_HEIGHT_KEY = "window-height"; public const string WINDOW_MAXIMIZE_KEY = "window-maximize"; public const string WINDOW_WIDTH_KEY = "window-width"; + public const string IMAGES_TRUSTED_DOMAINS = "images-trusted-domains"; public enum DesktopEnvironment { @@ -156,6 +157,16 @@ public class Application.Configuration : Geary.BaseObject { settings.bind(key, object, property, flags); } + public void bind_with_mapping(string key, Object object, string property, + SettingsBindGetMappingShared get_mapping, + SettingsBindSetMappingShared set_mapping, + SettingsBindFlags flags = GLib.SettingsBindFlags.DEFAULT) { + settings.bind_with_mapping( + key, object, property, flags, + get_mapping, set_mapping, null, null + ); + } + private void set_boolean(string name, bool value) { if (!settings.set_boolean(name, value)) message("Unable to set configuration value %s = %s", name, value.to_string()); @@ -178,6 +189,34 @@ public class Application.Configuration : Geary.BaseObject { this.settings.set_value(COMPOSER_WINDOW_SIZE_KEY, value); } + /** Returns list of trusted domains for which images loading is allowed. */ + public string[] get_images_trusted_domains() { + return this.settings.get_strv(IMAGES_TRUSTED_DOMAINS); + } + + /** Sets list of trusted domains for which images loading is allowed. */ + public void set_images_trusted_domains(string[] value) { + this.settings.set_strv(IMAGES_TRUSTED_DOMAINS, value); + } + + /** Adds domain to trusted list for which images loading is allowed. */ + public void add_images_trusted_domain(string domain) { + var domains = get_images_trusted_domains(); + domains += domain; + set_images_trusted_domains(domains); + } + + /** Removes domain from trusted for which images loading is allowed. */ + public void remove_images_trusted_domain(string domain) { + var domains = get_images_trusted_domains(); + string[] new_domains = {}; + foreach (var _domain in domains) { + if (domain != _domain) + new_domains += _domain; + } + set_images_trusted_domains(new_domains); + } + /** * Returns list of optional plugins to load by default */ diff --git a/src/client/components/components-preferences-window.vala b/src/client/components/components-preferences-window.vala index ea978a3a..b43d3b82 100644 --- a/src/client/components/components-preferences-window.vala +++ b/src/client/components/components-preferences-window.vala @@ -163,6 +163,16 @@ public class Components.PreferencesWindow : Hdy.PreferencesWindow { startup_notifications_row.activatable_widget = startup_notifications; startup_notifications_row.add(startup_notifications); + var trust_images = new Gtk.Switch(); + trust_images.valign = CENTER; + + var trust_images_row = new Hdy.ActionRow(); + /// Translators: Preferences label + trust_images_row.title = _("_Always load images"); + trust_images_row.use_underline = true; + trust_images_row.activatable_widget = autoselect; + trust_images_row.add(trust_images); + var group = new Hdy.PreferencesGroup(); /// Translators: Preferences group title //group.title = _("General"); @@ -172,6 +182,7 @@ public class Components.PreferencesWindow : Hdy.PreferencesWindow { group.add(display_preview_row); group.add(single_key_shortucts_row); group.add(startup_notifications_row); + group.add(trust_images_row); var page = new Hdy.PreferencesPage(); /// Translators: Preferences page title @@ -209,6 +220,13 @@ public class Components.PreferencesWindow : Hdy.PreferencesWindow { startup_notifications, "state" ); + config.bind_with_mapping( + Application.Configuration.IMAGES_TRUSTED_DOMAINS, + trust_images, + "state", + (GLib.SettingsBindGetMappingShared) settings_trust_images_getter, + (GLib.SettingsBindSetMappingShared) settings_trust_images_setter + ); } this.delete_event.connect(on_delete); @@ -252,4 +270,17 @@ public class Components.PreferencesWindow : Hdy.PreferencesWindow { return Gdk.EVENT_PROPAGATE; } + private static bool settings_trust_images_getter(GLib.Value value, GLib.Variant variant, void* user_data) { + var domains = variant.get_strv(); + value.set_boolean(domains.length > 0 && domains[0] == "*"); + return true; + } + + private static GLib.Variant settings_trust_images_setter(GLib.Value value, GLib.VariantType expected_type, void* user_data) { + var trusted = value.get_boolean(); + string[] values = {}; + if (trusted) + values += "*"; + return new GLib.Variant.strv(values); + } } diff --git a/src/client/conversation-viewer/conversation-contact-popover.vala b/src/client/conversation-viewer/conversation-contact-popover.vala index 4bc492f1..0c69d654 100644 --- a/src/client/conversation-viewer/conversation-contact-popover.vala +++ b/src/client/conversation-viewer/conversation-contact-popover.vala @@ -41,6 +41,8 @@ public class Conversation.ContactPopover : Gtk.Popover { private GLib.Cancellable load_cancellable = new GLib.Cancellable(); + private Application.Configuration config; + [GtkChild] private unowned Gtk.Grid contact_pane; [GtkChild] private unowned Hdy.Avatar avatar; @@ -74,11 +76,13 @@ public class Conversation.ContactPopover : Gtk.Popover { public ContactPopover(Gtk.Widget relative_to, Application.Contact contact, - Geary.RFC822.MailboxAddress mailbox) { + Geary.RFC822.MailboxAddress mailbox, + Application.Configuration config) { this.relative_to = relative_to; this.contact = contact; this.mailbox = mailbox; + this.config = config; this.load_remote_button.role = CHECK; @@ -143,7 +147,10 @@ public class Conversation.ContactPopover : Gtk.Popover { actions.lookup_action(ACTION_LOAD_REMOTE); load_remote.set_state( new GLib.Variant.boolean( - is_desktop || this.contact.load_remote_resources + is_desktop || + Util.Contact.should_load_images( + this.contact, + this.config) ) ); } else { @@ -177,6 +184,14 @@ public class Conversation.ContactPopover : Gtk.Popover { private async void set_load_remote_resources(bool enabled) { try { + // Remove all contact email domains from trusted list + // Otherwise, user may not understand why images are always shown + if (!enabled) { + var email_addresses = this.contact.email_addresses; + foreach (Geary.RFC822.MailboxAddress email in email_addresses) { + this.config.remove_images_trusted_domain(email.domain); + } + } yield this.contact.set_remote_resource_loading(enabled, null); load_remote_resources_changed(enabled); } catch (GLib.Error err) { diff --git a/src/client/conversation-viewer/conversation-message.vala b/src/client/conversation-viewer/conversation-message.vala index d7492b9f..722ce090 100644 --- a/src/client/conversation-viewer/conversation-message.vala +++ b/src/client/conversation-viewer/conversation-message.vala @@ -40,6 +40,9 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { private const string ACTION_OPEN_LINK = "open-link"; private const string ACTION_SAVE_IMAGE = "save-image"; private const string ACTION_SELECT_ALL = "select-all"; + private const string ACTION_SHOW_IMAGES_MESSAGE = "show-images-message"; + private const string ACTION_SHOW_IMAGES_SENDER = "show-images-sender"; + private const string ACTION_SHOW_IMAGES_DOMAIN = "show-images-domain"; // Widget used to display sender/recipient email addresses in @@ -377,6 +380,9 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { private MenuModel context_menu_main; private MenuModel? context_menu_inspector = null; + // Menu model for creating the show images menu + private MenuModel show_images_menu; + // Address fields that can be search through private Gee.List searchable_addresses = new Gee.LinkedList(); @@ -397,6 +403,8 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { private int remote_resources_loaded = 0; + private bool authenticated_message = false; + // Timeouts for showing the progress bar and hiding it when // complete. The former is so that when loading cached images it // doesn't pop up and then go away immediately afterwards. @@ -504,6 +512,12 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { .activate.connect(on_link_activated); add_action(ACTION_SAVE_IMAGE, true, new VariantType("(sms)")) .activate.connect(on_save_image); + add_action(ACTION_SHOW_IMAGES_MESSAGE, true) + .activate.connect(on_show_images); + add_action(ACTION_SHOW_IMAGES_SENDER, true) + .activate.connect(on_show_images_sender); + add_action(ACTION_SHOW_IMAGES_DOMAIN, true) + .activate.connect(on_show_images_domain); insert_action_group("msg", message_actions); // Context menu @@ -515,6 +529,9 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { context_menu_email = (MenuModel) builder.get_object("context_menu_email"); context_menu_image = (MenuModel) builder.get_object("context_menu_image"); context_menu_main = (MenuModel) builder.get_object("context_menu_main"); + + show_images_menu = (MenuModel) builder.get_object("show_images_menu"); + if (config.enable_inspector) { context_menu_inspector = (MenuModel) builder.get_object("context_menu_inspector"); @@ -872,11 +889,15 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { initialize_web_view(); } - bool contact_load_images = ( - this.primary_contact != null && - this.primary_contact.load_remote_resources + bool contact_load_images = Util.Contact.should_load_images( + this.primary_contact, this.config ); - if (this.load_remote_resources || contact_load_images) { + this.authenticated_message = message.auth_results != null && ( + message.auth_results.is_dkim_valid() || + message.auth_results.is_dmarc_valid() + ); + if (this.load_remote_resources || ( + contact_load_images && this.authenticated_message)) { yield this.web_view.load_remote_resources(load_cancelled); } @@ -1244,7 +1265,8 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { Conversation.ContactPopover popover = new Conversation.ContactPopover( address_child, address_child.contact, - address + address, + this.config ); popover.set_position(Gtk.PositionType.BOTTOM); popover.load_remote_resources_changed.connect((enabled) => { @@ -1391,51 +1413,48 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { private void on_remote_resources_blocked() { if (this.remote_images_info_bar == null) { - this.remote_images_info_bar = new Components.InfoBar( - // Translators: Info bar status message - _("Remote images not shown"), - // Translators: Info bar description - _("Only show remote images from senders you trust.") - ); - var show = this.remote_images_info_bar.add_button( - // Translators: Info bar button label - _("Show"), 1 - ); - this.remote_images_info_bar.add_button( - // Translators: Info bar button label - _("Always show from sender"), 2 - ); - this.remote_images_info_bar.response.connect(on_remote_images_response); - var buttons = this.remote_images_info_bar.get_action_area() as Gtk.ButtonBox; - if (buttons != null) { - buttons.set_child_non_homogeneous(show, true); + /* If message is authenticated, user is allowed to whitelist + * images loading for sender/domain sender. + */ + if (this.authenticated_message) { + this.remote_images_info_bar = new Components.InfoBar( + // Translators: Info bar status message + _("Remote images not shown"), + // Translators: Info bar description + _("Only show remote images from senders you trust.") + ); + + var menu_image = new Gtk.Image(); + menu_image.icon_name = "view-more-symbolic"; + + var menu_button = new Gtk.MenuButton(); + menu_button.use_popover = true; + menu_button.image = menu_image; + menu_button.menu_model = this.show_images_menu; + menu_button.halign = Gtk.Align.END; + menu_button.hexpand =true; + menu_button.show_all(); + + this.remote_images_info_bar.get_action_area().add(menu_button); + } else { + this.remote_images_info_bar = new Components.InfoBar( + // Translators: Info bar status message + _("Remote images not shown"), + // Translators: Info bar description + _("This message can't be trusted.") + ); + this.remote_images_info_bar.add_button( + // Translators: Info bar button label + _("Show"), 1 + ); + this.remote_images_info_bar.response.connect(() => { + show_images(true); + }); } this.info_bars.add(this.remote_images_info_bar); } } - private void on_remote_images_response(Components.InfoBar info_bar, int response_id) { - switch (response_id) { - case 1: - // Show images for the message - show_images(true); - break; - case 2: - // Show images for sender - show_images(false); - if (this.primary_contact != null) { - this.primary_contact.set_remote_resource_loading.begin( - true, null - ); - } - break; - default: - this.info_bars.remove(this.remote_images_info_bar); - this.remote_images_info_bar = null; - break; - } - } - private void on_copy_link(Variant? param) { Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD); clipboard.set_text(param.get_string(), -1); @@ -1484,6 +1503,30 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { } } + private void on_show_images(Variant? param) { + show_images(true); + } + + private void on_show_images_sender(Variant? param) { + show_images(false); + if (this.primary_contact != null) { + this.primary_contact.set_remote_resource_loading.begin( + true, null + ); + } + } + + private void on_show_images_domain(Variant? param) { + show_images(false); + if (this.primary_contact != null) { + var email_addresses = this.primary_contact.email_addresses; + foreach (Geary.RFC822.MailboxAddress email in email_addresses) { + this.config.add_images_trusted_domain(email.domain); + break; + } + } + } + private void on_link_activated(GLib.Variant? param) { string link = param.get_string(); diff --git a/src/client/meson.build b/src/client/meson.build index e6d4a8b4..71535832 100644 --- a/src/client/meson.build +++ b/src/client/meson.build @@ -138,6 +138,7 @@ client_vala_sources = files( 'util/util-avatar.vala', 'util/util-cache.vala', + 'util/util-contact.vala', 'util/util-date.vala', 'util/util-email.vala', 'util/util-files.vala', diff --git a/src/client/util/util-contact.vala b/src/client/util/util-contact.vala new file mode 100644 index 00000000..284189fc --- /dev/null +++ b/src/client/util/util-contact.vala @@ -0,0 +1,34 @@ +/* + * Copyright 2022 Cédric Bellegarde + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace Util.Contact { + + /** + * Returns true if loading images for contact is allowed + */ + public bool should_load_images(Application.Contact contact, Application.Configuration config) { + var email_addresses = contact.email_addresses; + var domains = config.get_images_trusted_domains(); + if (contact == null) { + return false; + // Contact trusted + } else if (contact.load_remote_resources) { + return true; + // All emails are trusted + } else if (domains.length > 0 && domains[0] == "*") { + return true; + // Contact domain trusted + } else { + foreach (Geary.RFC822.MailboxAddress email in email_addresses) { + if (email.domain in domains) { + return true; + } + } + } + return false; + } +} diff --git a/ui/conversation-message-menus.ui b/ui/conversation-message-menus.ui index 32d0be1c..1faadf90 100644 --- a/ui/conversation-message-menus.ui +++ b/ui/conversation-message-menus.ui @@ -45,4 +45,21 @@ + +
+ Show images + + For this message + msg.show-images-message + + + For this sender + msg.show-images-sender + + + For this domain + msg.show-images-domain + +
+
diff --git a/ui/geary.css b/ui/geary.css index 6bae621f..84533f5f 100644 --- a/ui/geary.css +++ b/ui/geary.css @@ -159,6 +159,19 @@ row.geary-folder-popover-list-row > label { border-width: 0; } +.geary-message infobar box button { + background: alpha(black, 0.1); + color: alpha(@theme_text_color, 0.7); + border: none; + box-shadow: none; +} + +.geary-message infobar box button:hover, +.geary-message infobar box button:checked { + background: alpha(black, 0.2); + color: @theme_text_color; +} + grid.geary-message-summary { border-top: 4px solid transparent; padding: 12px;