[geary/gnumdk/stable: 9/15] client: Add more options for displaying images from messages




commit 8b8c6529fb270e8a5c54208626341cae540db298
Author: Cédric Bellegarde <cedric bellegarde adishatz org>
Date:   Thu Jun 30 14:44:18 2022 +0200

    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/application-configuration.vala     |  39 ++++++
 .../components/components-preferences-window.vala  |  31 +++++
 .../conversation-contact-popover.vala              |  19 ++-
 .../conversation-viewer/conversation-message.vala  | 131 ++++++++++++++-------
 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, 245 insertions(+), 46 deletions(-)
---
diff --git a/desktop/org.gnome.Geary.gschema.xml b/desktop/org.gnome.Geary.gschema.xml
index 89354dc25..136b431e4 100644
--- a/desktop/org.gnome.Geary.gschema.xml
+++ b/desktop/org.gnome.Geary.gschema.xml
@@ -97,6 +97,12 @@
         <description>The last recorded size of the detached composer window.</description>
     </key>
 
+    <key name="images-trusted-domains" type="as">
+        <default>[]</default>
+        <summary>Allow images for these domains</summary>
+        <description>Images from these domains will be trusted</description>
+    </key>
+
     <key name="undo-send-delay" type="i">
         <default>5</default>
         <summary>Undo sending email delay</summary>
diff --git a/src/client/application/application-configuration.vala 
b/src/client/application/application-configuration.vala
index eaaed36f7..3bbf0a61e 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 ea978a3a8..b43d3b829 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 4bc492f1d..0c69d654b 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 c15f3f8fc..597a79fa1 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<ContactFlowBoxChild> searchable_addresses =
         new Gee.LinkedList<ContactFlowBoxChild>();
@@ -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,48 +1413,45 @@ 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);
-            }
-            this.info_bars.add(this.remote_images_info_bar);
-        }
-    }
+            /* 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";
 
-    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
+                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);
+                });
             }
-            break;
-        default:
-            this.info_bars.remove(this.remote_images_info_bar);
-            this.remote_images_info_bar = null;
-            break;
+            this.info_bars.add(this.remote_images_info_bar);
         }
     }
 
@@ -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 ff3c8a6d3..bb485ac84 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 000000000..284189fc8
--- /dev/null
+++ b/src/client/util/util-contact.vala
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 Cédric Bellegarde <cedric bellegarde adishatz org>
+ *
+ * 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 32d0be1c7..1faadf903 100644
--- a/ui/conversation-message-menus.ui
+++ b/ui/conversation-message-menus.ui
@@ -45,4 +45,21 @@
       </item>
     </section>
   </menu>
+  <menu id="show_images_menu">
+    <section>
+      <attribute name="label" translatable="yes">Show images</attribute>
+      <item>
+        <attribute name="label" translatable="yes">For this message</attribute>
+        <attribute name="action">msg.show-images-message</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">For this sender</attribute>
+        <attribute name="action">msg.show-images-sender</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">For this domain</attribute>
+        <attribute name="action">msg.show-images-domain</attribute>
+      </item>
+   </section>
+  </menu>
 </interface>
diff --git a/ui/geary.css b/ui/geary.css
index 6bae621f6..84533f5ff 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;


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]