[balsa/imap-improvements] implement sender-dependent preferences for HTML messages (#13)



commit 68d4a8ac5c6a665a3eb4a06e4ff90f4f9ecabd88
Author: Albrecht Dreß <albrecht dress netcologne de>
Date:   Wed May 5 21:20:08 2021 +0200

    implement sender-dependent preferences for HTML messages (#13)
    
    This patch implements a small SQLite database to store From: address
    dependent preferences for the display of html or plain text messages,
    and for automatically loading external images (which should be
    considered dangerous, but may be helpful under some circumstances):
    - additional check boxes are added to the HTML context menu, and
    - a dialogue for managing the database is added to the prefs manager.
    
    Modifications:
    - libbalsa/html-pref-db.[ch]: (new) implement the prefs database
    - libbalsa/html.[ch]: use the prefs database
    - libbalsa/Makefile.am, libbalsa.meson.build: add new source files
    - src/balsa-message.c, src/print-gtk.c: use the prefs database
    - src/balsa-mime-widget-text.c: use the prefs database, extend HTML
      popup menu
    - src/pref-manager.c: add button to run the prefs database dialogue
    - src/save-restore.c: register the prefs database dialogue for geometry
      management
    - configure.ac, meson.build: require SQLite if HTML is enabled
    - README: add remark about new requirement
    
    Signed-off-by: Albrecht Dreß <albrecht dress netcologne de>

 README                       |   3 +-
 configure.ac                 |   4 +-
 libbalsa/Makefile.am         |   2 +
 libbalsa/html-pref-db.c      | 523 +++++++++++++++++++++++++++++++++++++++++++
 libbalsa/html-pref-db.h      |  76 +++++++
 libbalsa/html.c              |   6 +-
 libbalsa/html.h              |   5 +-
 libbalsa/meson.build         |   2 +
 meson.build                  |   3 +-
 src/balsa-message.c          |  17 +-
 src/balsa-mime-widget-text.c |  41 +++-
 src/pref-manager.c           |   5 +
 src/print-gtk.c              |  13 +-
 src/save-restore.c           |   3 +
 14 files changed, 687 insertions(+), 16 deletions(-)
---
diff --git a/README b/README
index a18ab3b35..5188aba2c 100644
--- a/README
+++ b/README
@@ -71,7 +71,8 @@ Specify the kerberos directory as the argument.
        When using webkit2, in order to quote html-only messages
 it is recommended to install a html-to-text conversion tool.  Supported
 tools are python-html2text, html2markdown, html2markdown.py2,
-html2markdown.py3 and html2text.
+html2markdown.py3 and html2text.  Additionally, sqlite3 is required for
+managing sender-dependent HTML preferences.
 
 --with-spell-checker=(internal|gtkspell|gspell)
        Select the spell checker for the message composer. The internal spell
diff --git a/configure.ac b/configure.ac
index f9759f0de..34c394ae5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -260,7 +260,9 @@ AC_MSG_CHECKING(whether to use an HTML widget)
 case "$use_html_widget" in
     webkit2)
         AC_MSG_RESULT([$use_html_widget])
-        PKG_CHECK_MODULES(HTML, [ webkit2gtk-4.0 >= 2.28.0 ])
+        # note: sqlite3 is needed to manage html vs. plain and image download preferences
+        PKG_CHECK_MODULES(HTML, [ webkit2gtk-4.0 >= 2.28.0
+                                  sqlite3 >= 3.24.0])
         AC_PATH_PROGS(HTML2TEXT,
                       [python-html2text  \
                        html2markdown     \
diff --git a/libbalsa/Makefile.am b/libbalsa/Makefile.am
index 2792472d0..d81ff9084 100644
--- a/libbalsa/Makefile.am
+++ b/libbalsa/Makefile.am
@@ -60,6 +60,8 @@ libbalsa_a_SOURCES =          \
        gmime-part-rfc2440.c    \
        html.c                  \
        html.h                  \
+       html-pref-db.c          \
+       html-pref-db.h          \
        identity.c              \
        identity.h              \
        imap-server.c           \
diff --git a/libbalsa/html-pref-db.c b/libbalsa/html-pref-db.c
new file mode 100644
index 000000000..d16d285af
--- /dev/null
+++ b/libbalsa/html-pref-db.c
@@ -0,0 +1,523 @@
+/* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
+/* Balsa E-Mail Client
+ *
+ * Copyright (C) 1997-2021 Stuart Parmenter and others,
+ *                         See the file AUTHORS for a list.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H
+# include "config.h"
+#endif                          /* HAVE_CONFIG_H */
+
+#ifdef HAVE_HTML_WIDGET
+
+#ifdef G_LOG_DOMAIN
+#  undef G_LOG_DOMAIN
+#endif
+#define G_LOG_DOMAIN "html"
+
+#include <glib/gstdio.h>
+#include <glib/gi18n.h>
+#include <sqlite3.h>
+#include "geometry-manager.h"
+#include "html-pref-db.h"
+
+
+enum {
+       PREFS_ADDRESS_COLUMN = 0,
+       PREFS_PREFER_HTML_COLUMN,
+       PREFS_LOAD_IMAGES_COLUMN,
+       PREFS_DB_VIEW_COLUMNS
+};
+
+
+#define DB_SCHEMA                                                              \
+       "PRAGMA auto_vacuum = 1;"                                       \
+       "CREATE TABLE html_prefs("                                      \
+               "addr TEXT PRIMARY KEY NOT NULL, "              \
+               "prefer_html BOOLEAN DEFAULT 0, "               \
+               "prefer_load_img BOOLEAN DEFAULT 0);"
+#define NUM_QUERIES                    5
+
+
+static sqlite3 *pref_db = NULL;
+static sqlite3_stmt *query[NUM_QUERIES] = { NULL, NULL, NULL, NULL, NULL };
+G_LOCK_DEFINE_STATIC(db_mutex);
+
+
+static gboolean pref_db_check(void);
+
+static gboolean pref_db_get(InternetAddressList *from,
+                            int                  col);
+static void pref_db_set_ial(InternetAddressList *from,
+                            int                  pref_idx,
+                            gboolean             value);
+static gboolean pref_db_set_name(const gchar *sender,
+                                 int          pref_idx,
+                                 gboolean     value);
+
+static gboolean popup_menu_cb(GtkWidget *widget,
+                              gpointer   user_data);
+static void button_press_cb(GtkGestureMultiPress *multi_press_gesture,
+                            gint                  n_press,
+                            gdouble               x,
+                            gdouble               y,
+                            gpointer              user_data);
+static void popup_menu_real(GtkWidget      *widget,
+                            const GdkEvent *event);
+static void remove_item_cb(GtkMenuItem G_GNUC_UNUSED *menuitem,
+                           gpointer                   user_data);
+static void on_prefs_button_toggled(GtkCellRendererToggle *cell_renderer,
+                                    gchar                 *path,
+                                    gpointer               user_data);
+
+static void html_pref_db_close(void);
+
+
+gboolean
+libbalsa_html_get_prefer_html(InternetAddressList *from)
+{
+       return pref_db_get(from, 1);
+}
+
+
+gboolean
+libbalsa_html_get_load_images(InternetAddressList *from)
+{
+       return pref_db_get(from, 2);
+}
+
+
+void
+libbalsa_html_prefer_set_prefer_html(InternetAddressList *from, gboolean state)
+{
+       pref_db_set_ial(from, 1, state);
+}
+
+
+void
+libbalsa_html_prefer_set_load_images(InternetAddressList *from, gboolean state)
+{
+       pref_db_set_ial(from, 2, state);
+}
+
+
+void
+libbalsa_html_pref_dialog_run(GtkWindow *parent)
+{
+       GtkWidget *dialog;
+       GtkWidget *vbox;
+       GtkWidget *scrolled_window;
+       GtkListStore *model;
+       GtkWidget *tree_view;
+       GtkGesture *gesture;
+       GtkTreeSelection *selection;
+       GtkCellRenderer *renderer;
+       GtkTreeViewColumn *column;
+       int sqlite_res;
+
+       if (!pref_db_check()) {
+               return;
+       }
+
+       dialog = gtk_dialog_new_with_buttons(_("HTML preferences"), parent, GTK_DIALOG_DESTROY_WITH_PARENT | 
libbalsa_dialog_flags(),
+               _("_Close"), GTK_RESPONSE_CLOSE, NULL);
+       geometry_manager_attach(GTK_WINDOW(dialog), "HTMLPrefsDB");
+
+       vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
+       gtk_container_add(GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), vbox);
+       gtk_widget_set_vexpand(vbox, TRUE);
+
+       scrolled_window = gtk_scrolled_window_new(NULL, NULL);
+       gtk_container_set_border_width(GTK_CONTAINER(scrolled_window), 12U);
+       gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled_window), GTK_SHADOW_ETCHED_IN);
+       gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window), GTK_POLICY_AUTOMATIC, 
GTK_POLICY_AUTOMATIC);
+       gtk_box_pack_start(GTK_BOX(vbox), scrolled_window, TRUE, TRUE, 0);
+
+       model = gtk_list_store_new(PREFS_DB_VIEW_COLUMNS,
+               G_TYPE_STRING,                  /* address */
+               G_TYPE_BOOLEAN,                 /* prefer html over plain text */
+               G_TYPE_BOOLEAN);                /* auto-load images */
+
+       tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(model));
+
+       gesture = gtk_gesture_multi_press_new(tree_view);
+       gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(gesture), 0);
+       g_signal_connect(gesture, "pressed", G_CALLBACK(button_press_cb), NULL);
+       gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(gesture), GTK_PHASE_CAPTURE);
+       g_signal_connect(tree_view, "popup-menu", G_CALLBACK(popup_menu_cb), NULL);
+
+       gtk_container_add(GTK_CONTAINER(scrolled_window), tree_view);
+       selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view));
+       gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
+
+       /* add all database items */
+       G_LOCK(db_mutex);
+       sqlite_res = sqlite3_step(query[4]);
+       while (sqlite_res == SQLITE_ROW) {
+               GtkTreeIter iter;
+
+               gtk_list_store_append(model, &iter);
+               gtk_list_store_set(model, &iter,
+                       PREFS_ADDRESS_COLUMN, sqlite3_column_text(query[4], 0),
+                       PREFS_PREFER_HTML_COLUMN, sqlite3_column_int(query[4], 1),
+                       PREFS_LOAD_IMAGES_COLUMN, sqlite3_column_int(query[4], 2),
+                       -1);
+               sqlite_res = sqlite3_step(query[4]);
+       }
+       sqlite3_reset(query[4]);
+       G_UNLOCK(db_mutex);
+
+       /* set up the tree view */
+       renderer = gtk_cell_renderer_text_new();
+       column = gtk_tree_view_column_new_with_attributes(_("Sender"), renderer, "text", 
PREFS_ADDRESS_COLUMN, NULL);
+       gtk_tree_view_column_set_sort_column_id(column, PREFS_ADDRESS_COLUMN);
+       gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
+       gtk_tree_view_column_set_resizable(column, TRUE);
+
+       renderer = gtk_cell_renderer_toggle_new();
+       g_object_set_data(G_OBJECT(renderer), "dbcol", GINT_TO_POINTER(PREFS_PREFER_HTML_COLUMN));
+       g_signal_connect(renderer, "toggled", G_CALLBACK(on_prefs_button_toggled), model);
+       column = gtk_tree_view_column_new_with_attributes(_("Prefer HTML"), renderer, "active", 
PREFS_PREFER_HTML_COLUMN, NULL);
+       gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
+       gtk_tree_view_column_set_resizable(column, TRUE);
+       gtk_widget_show_all(vbox);
+
+       renderer = gtk_cell_renderer_toggle_new();
+       g_object_set_data(G_OBJECT(renderer), "dbcol", GINT_TO_POINTER(PREFS_LOAD_IMAGES_COLUMN));
+       g_signal_connect(renderer, "toggled", G_CALLBACK(on_prefs_button_toggled), model);
+       column = gtk_tree_view_column_new_with_attributes(_("Auto-load images"), renderer, "active", 
PREFS_LOAD_IMAGES_COLUMN, NULL);
+       gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
+       gtk_tree_view_column_set_resizable(column, TRUE);
+       gtk_widget_show_all(vbox);
+
+       gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model), PREFS_ADDRESS_COLUMN, 
GTK_SORT_ASCENDING);
+       g_object_unref(model);
+
+       (void) gtk_dialog_run(GTK_DIALOG(dialog));
+       gtk_widget_destroy(dialog);
+}
+
+
+/** \brief Open and if necessary initialise the HTML preferences database
+ *
+ * \return TRUE on success
+ */
+static gboolean
+pref_db_check(void)
+{
+       static const gchar * const prepare_statements[NUM_QUERIES] = {
+               "SELECT * FROM html_prefs WHERE addr = LOWER(?)",
+               "INSERT INTO html_prefs (addr, prefer_html) VALUES (?1, ?2) "
+                               "ON CONFLICT (addr) DO UPDATE SET prefer_html = ?2",
+               "INSERT INTO html_prefs (addr, prefer_load_img) VALUES (?1, ?2) "
+                               "ON CONFLICT (addr) DO UPDATE SET prefer_load_img = ?2",
+               "DELETE FROM html_prefs WHERE addr = LOWER(?1)",
+               "SELECT addr, prefer_html, prefer_load_img FROM html_prefs ORDER BY addr ASC"
+       };
+       gboolean result = TRUE;
+
+       G_LOCK(db_mutex);
+       if (pref_db == NULL) {
+               gchar *db_path;
+               gboolean require_init;
+               int sqlite_res;
+
+               g_debug("open HTML preferences database");
+               /* ensure that the config folder exists, otherwise Balsa will throw an error on first use */
+               libbalsa_assure_balsa_dir();
+               db_path = g_build_filename(g_get_home_dir(), ".balsa", "html-prefs.db", NULL);
+               require_init = (g_access(db_path, R_OK + W_OK) != 0);
+               sqlite_res = sqlite3_open(db_path, &pref_db);
+               if (sqlite_res == SQLITE_OK) {
+                       guint n;
+
+                       /* write the schema if the database is new */
+                       if (require_init) {
+                               sqlite_res = sqlite3_exec(pref_db, DB_SCHEMA, NULL, NULL, NULL);
+                       }
+
+                       /* always vacuum the database */
+                       if (sqlite_res == SQLITE_OK) {
+                               sqlite_res = sqlite3_exec(pref_db, "VACUUM", NULL, NULL, NULL);
+                       }
+
+                       /* prepare statements */
+                       for (n = 0U; (sqlite_res == SQLITE_OK) && (n < NUM_QUERIES); n++) {
+                               sqlite_res = sqlite3_prepare_v2(pref_db, prepare_statements[n], -1, 
&query[n], NULL);
+                       }
+               }
+               G_UNLOCK(db_mutex);
+
+               /* error checks... */
+               if (sqlite_res != SQLITE_OK) {
+                       libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("Cannot initialise HTML 
preferences database “%s”: %s"), db_path,
+                               sqlite3_errmsg(pref_db));
+                       html_pref_db_close();
+                       result = FALSE;
+               } else {
+                       atexit(html_pref_db_close);
+               }
+               g_free(db_path);
+       } else {
+               G_UNLOCK(db_mutex);
+       }
+
+       return result;
+}
+
+
+/** \brief Get the HTML preferences setting for a sender
+ *
+ * \param from From: address list, may be NULL or empty
+ * \param col 1 prefer HTML, 2 auto-load images
+ * \return the requested setting, FALSE on error, empty address list or missing entry
+ */
+static gboolean
+pref_db_get(InternetAddressList *from, int col)
+{
+       gboolean result = FALSE;
+
+       if (from != NULL) {
+               InternetAddress *sender_address;
+
+               sender_address = internet_address_list_get_address(from, 0);
+               if (INTERNET_ADDRESS_IS_MAILBOX(sender_address)) {
+                       const gchar *sender;
+
+                       sender = internet_address_mailbox_get_addr(INTERNET_ADDRESS_MAILBOX(sender_address));
+                       if ((sender != NULL) && pref_db_check()) {
+                               G_LOCK(db_mutex);
+                               if (sqlite3_bind_text(query[0], 1, sender, -1, SQLITE_STATIC) == SQLITE_OK) {
+                                       int sqlite_res;
+
+                                       sqlite_res = sqlite3_step(query[0]);
+                                       if (sqlite_res == SQLITE_ROW) {
+                                               result = (sqlite3_column_int(query[0], col) != 0);
+                                               sqlite_res = sqlite3_step(query[0]);
+                                       }
+                                       if (sqlite_res != SQLITE_DONE) {
+                                               libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("Cannot 
read HTML preferences for “%s”: %s"), sender,
+                                                       sqlite3_errmsg(pref_db));
+                                               result = FALSE;
+                                       }
+                               }
+                               sqlite3_reset(query[0]);
+                               G_UNLOCK(db_mutex);
+                       }
+               }
+       }
+
+       return result;
+}
+
+
+/** \brief Set the HTML preferences setting for a sender
+ *
+ * \param from From: address list, must not be NULL
+ * \param pref_idx 1 prefer HTML, 2 auto-load images
+ */
+static void
+pref_db_set_ial(InternetAddressList *from, int pref_idx, gboolean value)
+{
+       InternetAddress *sender_address;
+
+       sender_address = internet_address_list_get_address(from, 0);
+       if (INTERNET_ADDRESS_IS_MAILBOX(sender_address)) {
+               const gchar *sender;
+
+               sender = internet_address_mailbox_get_addr(INTERNET_ADDRESS_MAILBOX(sender_address));
+               if (sender != NULL) {
+                       (void) pref_db_set_name(sender, pref_idx, value);
+               }
+       }
+}
+
+
+/** \brief Set the HTML preferences setting for a sender
+ *
+ * \param sender From: mailbox, must not be NULL
+ * \param pref_idx 1 prefer HTML, 2 auto-load images
+ * \return TRUE if the operation was successful
+ */
+static gboolean
+pref_db_set_name(const gchar *sender, int pref_idx, gboolean value)
+{
+       gboolean result = FALSE;
+
+       if (pref_db_check()) {
+               G_LOCK(db_mutex);
+               if ((sqlite3_bind_text(query[pref_idx], 1, sender, -1, SQLITE_STATIC) != SQLITE_OK) ||
+                       (sqlite3_bind_int(query[pref_idx], 2, value) != SQLITE_OK) ||
+                       (sqlite3_step(query[pref_idx]) != SQLITE_DONE)) {
+                       libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("Cannot save HTML preferences for 
“%s”: %s"), sender,
+                               sqlite3_errmsg(pref_db));
+               } else {
+                       result = TRUE;
+               }
+               sqlite3_reset(query[pref_idx]);
+               G_UNLOCK(db_mutex);
+       }
+
+       return result;
+}
+
+
+/* callback: popup menu key in html prefs database dialogue activated */
+static gboolean
+popup_menu_cb(GtkWidget *widget, gpointer G_GNUC_UNUSED user_data)
+{
+       GtkTreeView *tree_view = GTK_TREE_VIEW(widget);
+       GtkTreeModel *model;
+       GtkTreeSelection *selection;
+       GtkTreeIter iter;
+
+       selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
+       if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
+               GtkTreePath *path;
+
+               path = gtk_tree_model_get_path(model, &iter);
+               gtk_tree_view_scroll_to_cell(tree_view, path, NULL, FALSE, 0.0, 0.0);
+               gtk_tree_path_free(path);
+               popup_menu_real(widget, NULL);
+       }
+
+       return TRUE;
+}
+
+
+/* callback: mouse click in html prefs database dialogue activated */
+static void
+button_press_cb(GtkGestureMultiPress *multi_press_gesture, gint G_GNUC_UNUSED n_press, gdouble x,
+       gdouble y, gpointer G_GNUC_UNUSED user_data)
+{
+       GtkWidget *widget = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(multi_press_gesture));
+       GtkTreeView *tree_view = GTK_TREE_VIEW(widget);
+       GtkGesture *gesture;
+       GdkEventSequence *sequence;
+       const GdkEvent *event;
+
+       gesture = GTK_GESTURE(multi_press_gesture);
+       sequence = gtk_gesture_single_get_current_sequence(GTK_GESTURE_SINGLE(multi_press_gesture));
+       event = gtk_gesture_get_last_event(gesture, sequence);
+       if (gdk_event_triggers_context_menu(event) && (gdk_event_get_window(event) == 
gtk_tree_view_get_bin_window(tree_view))) {
+               gint bx;
+               gint by;
+               GtkTreePath *path;
+
+               gtk_tree_view_convert_widget_to_bin_window_coords(tree_view, (gint) x, (gint) y, &bx, &by);
+               if (gtk_tree_view_get_path_at_pos(tree_view, bx, by, &path, NULL, NULL, NULL)) {
+                       GtkTreeSelection *selection = gtk_tree_view_get_selection(tree_view);
+                       GtkTreeModel *model = gtk_tree_view_get_model(tree_view);
+                       GtkTreeIter iter;
+
+                       gtk_tree_selection_unselect_all(selection);
+                       gtk_tree_selection_select_path(selection, path);
+                       gtk_tree_view_set_cursor(GTK_TREE_VIEW(tree_view), path, NULL, FALSE);
+                       if (gtk_tree_model_get_iter(model, &iter, path)) {
+                               popup_menu_real(GTK_WIDGET(tree_view), event);
+                       }
+                       gtk_tree_path_free(path);
+               }
+       }
+}
+
+
+/* html prefs database dialogue context menu */
+static void
+popup_menu_real(GtkWidget *widget, const GdkEvent *event)
+{
+       GtkWidget *popup_menu;
+       GtkWidget* menu_item;
+
+       popup_menu = gtk_menu_new();
+       menu_item = gtk_menu_item_new_with_mnemonic(_("_Delete"));
+       g_signal_connect(menu_item, "activate", G_CALLBACK(remove_item_cb), widget);
+       gtk_menu_shell_append(GTK_MENU_SHELL(popup_menu), menu_item);
+       gtk_widget_show_all(popup_menu);
+       if (event != NULL) {
+               gtk_menu_popup_at_pointer(GTK_MENU(popup_menu), event);
+       } else {
+               gtk_menu_popup_at_widget(GTK_MENU(popup_menu), widget, GDK_GRAVITY_CENTER, 
GDK_GRAVITY_CENTER, NULL);
+       }
+}
+
+
+/* context menu callback: remove entry from database */
+static void
+remove_item_cb(GtkMenuItem G_GNUC_UNUSED *menuitem, gpointer user_data)
+{
+       GtkTreeModel *model;
+       GtkTreeSelection *selection;
+       GtkTreeIter iter;
+
+       selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(user_data));
+       if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
+               gchar *addr;
+
+               gtk_tree_model_get(model, &iter, PREFS_ADDRESS_COLUMN, &addr, -1);
+               if ((sqlite3_bind_text(query[3], 1, addr, -1, SQLITE_STATIC) != SQLITE_OK) ||
+                       (sqlite3_step(query[3]) != SQLITE_DONE)) {
+                       libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("Cannot delete database entry: 
%s"), sqlite3_errmsg(pref_db));
+               }
+               gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
+               sqlite3_reset(query[3]);
+               g_free(addr);
+       }
+}
+
+
+/* callback: toggles setting in database dialogue */
+static void
+on_prefs_button_toggled(GtkCellRendererToggle *cell_renderer, gchar *path, gpointer user_data)
+{
+       GtkTreeIter iter;
+       GtkListStore *model = GTK_LIST_STORE(user_data);
+
+       if (gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(model), &iter, path)) {
+               gchar *addr;
+               gint column;
+               gboolean new_state;
+
+               new_state = !gtk_cell_renderer_toggle_get_active(cell_renderer);
+               column = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(cell_renderer), "dbcol"));
+               gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, PREFS_ADDRESS_COLUMN, &addr, -1);
+               if (pref_db_set_name(addr, column, new_state)) {
+                       gtk_list_store_set(model, &iter, column, new_state, -1);
+               }
+               g_free(addr);
+       }
+}
+
+
+/* close the database and free prepared statements */
+static void
+html_pref_db_close(void)
+{
+       guint n;
+
+       g_debug("close HTML preferences database");
+       G_LOCK(db_mutex);
+       for (n = 0U; n < NUM_QUERIES; n++) {
+               sqlite3_finalize(query[n]);
+               query[n] = NULL;
+       }
+       sqlite3_close(pref_db);
+       pref_db = NULL;
+       G_UNLOCK(db_mutex);
+}
+
+#endif         /* HAVE_HTML_WIDGET */
diff --git a/libbalsa/html-pref-db.h b/libbalsa/html-pref-db.h
new file mode 100644
index 000000000..4f3f90e2a
--- /dev/null
+++ b/libbalsa/html-pref-db.h
@@ -0,0 +1,76 @@
+/* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
+/* Balsa E-Mail Client
+ *
+ * Copyright (C) 1997-2021 Stuart Parmenter and others,
+ *                         See the file AUTHORS for a list.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef HTML_PREF_DB_H_
+#define HTML_PREF_DB_H_
+
+#ifndef BALSA_VERSION
+# error "Include config.h before this file."
+#endif
+
+#include "libbalsa.h"
+
+#ifdef HAVE_HTML_WIDGET
+
+/* SQLite3 database to manage HTML preferences */
+
+
+/** \brief Check if HTML is preferred for messages from a sender
+ *
+ * \param from From: address list, may be NULL or empty
+ * \return TRUE if HTML is preferred
+ */
+gboolean libbalsa_html_get_prefer_html(InternetAddressList *from);
+
+/** \brief Check if images in HTML messages from a sender shall be loaded automatically
+ *
+ * \param from From: address list, may be NULL or empty
+ * \return TRUE if images in HTML messages shall be loaded without confirmation
+ */
+gboolean libbalsa_html_get_load_images(InternetAddressList *from);
+
+/** \brief Remember if HTML is preferred for messages from a sender
+ *
+ * \param from From: address list, must not be NULL
+ * \param state TRUE if HTML is preferred
+ * \note The function is a no-op if the InternetAddressList does not contain an InternetAddressMailbox as 
1st element.
+ */
+void libbalsa_html_prefer_set_prefer_html(InternetAddressList *from,
+                                          gboolean             state);
+
+/** \brief Remember if images in HTML messages from a sender shall be loaded automatically
+ *
+ * \param from From: address list, must not be NULL
+ * \param state TRUE if images in HTML messages shall be loaded without confirmation
+ * \note The function is a no-op if the InternetAddressList does not contain an InternetAddressMailbox as 
1st element.
+ */
+void libbalsa_html_prefer_set_load_images(InternetAddressList *from,
+                                          gboolean             state);
+
+/** \brief Show the dialogue for managing the HTML preferences database
+ *
+ * \param parent transient parent window
+ */
+void libbalsa_html_pref_dialog_run(GtkWindow *parent);
+
+
+#endif         /* HAVE_HTML_WIDGET */
+
+#endif         /* HTML_PREF_DB_H_ */
diff --git a/libbalsa/html.c b/libbalsa/html.c
index 5bd3615cb..59c3e770c 100644
--- a/libbalsa/html.c
+++ b/libbalsa/html.c
@@ -750,7 +750,8 @@ libbalsa_html_print_bitmap(LibBalsaMessageBody *body,
 GtkWidget *
 libbalsa_html_new(LibBalsaMessageBody * body,
                   LibBalsaHtmlCallback  hover_cb,
-                  LibBalsaHtmlCallback  clicked_cb)
+                  LibBalsaHtmlCallback  clicked_cb,
+                  gboolean              auto_load_images)
 {
     gchar *text;
     gssize len;
@@ -771,7 +772,8 @@ libbalsa_html_new(LibBalsaMessageBody * body,
     have_src_cid = g_regex_match_simple(CID_REGEX, text, G_REGEX_CASELESS, 0);
     have_src_oth = g_regex_match_simple(SRC_REGEX, text, G_REGEX_CASELESS, 0);
 
-    info->web_view = lbh_web_view_new(info, LBH_NATURAL_SIZE, have_src_cid && !have_src_oth);
+    info->web_view = lbh_web_view_new(info, LBH_NATURAL_SIZE,
+       auto_load_images || (have_src_cid && !have_src_oth));
 
     g_signal_connect(info->web_view, "mouse-target-changed",
                      G_CALLBACK(lbh_mouse_target_changed_cb), info);
diff --git a/libbalsa/html.h b/libbalsa/html.h
index d526d2bd4..e68867444 100644
--- a/libbalsa/html.h
+++ b/libbalsa/html.h
@@ -38,11 +38,14 @@ typedef enum {
 
 # ifdef HAVE_HTML_WIDGET
 
+#include "html-pref-db.h"
+
 typedef void (*LibBalsaHtmlCallback) (const gchar * uri);
 
 GtkWidget *libbalsa_html_new(LibBalsaMessageBody * body,
                              LibBalsaHtmlCallback hover_cb,
-                             LibBalsaHtmlCallback clicked_cb);
+                             LibBalsaHtmlCallback clicked_cb,
+                             gboolean             auto_load_images);
 void libbalsa_html_to_string(gchar ** text, size_t len);
 gboolean libbalsa_html_can_zoom(GtkWidget * widget);
 void libbalsa_html_zoom(GtkWidget * widget, gint in_out);
diff --git a/libbalsa/meson.build b/libbalsa/meson.build
index 24c37f76c..8abc1824b 100644
--- a/libbalsa/meson.build
+++ b/libbalsa/meson.build
@@ -57,6 +57,8 @@ libbalsa_a_sources = [
   'gmime-part-rfc2440.c',
   'html.c',
   'html.h',
+  'html-pref-db.c',
+  'html-pref-db.h',
   'identity.c',
   'identity.h',
   'imap-server.c',
diff --git a/meson.build b/meson.build
index 8539d041b..fd8ecf7d6 100644
--- a/meson.build
+++ b/meson.build
@@ -167,6 +167,7 @@ libnetclient_deps = [glib_dep,
 #
 if html_widget == 'webkit2'
   html_dep = dependency('webkit2gtk-4.0', version : '>= 2.28.0')
+  htmlpref_dep = dependency('sqlite3', version : '>= 3.24.0')
 
   html2text = find_program('python-html2text',
                            'html2markdown',
@@ -194,7 +195,7 @@ if html_widget == 'webkit2'
 
   conf.set('HAVE_HTML_WIDGET', 1,
     description : 'Defined when an HTML widget can be used.')
-  balsa_deps += html_dep
+  balsa_deps += [html_dep, htmlpref_dep]
 endif
 
 # Autocrypt
diff --git a/src/balsa-message.c b/src/balsa-message.c
index f2584d66b..a024f8639 100644
--- a/src/balsa-message.c
+++ b/src/balsa-message.c
@@ -1997,16 +1997,16 @@ balsa_message_has_previous_part(BalsaMessage * balsa_message)
 
 #ifdef HAVE_HTML_WIDGET
 static gboolean
-libbalsa_can_display(LibBalsaMessageBody *part)
+libbalsa_can_display(LibBalsaMessageBody *part, InternetAddressList *from)
 {
     gchar *content_type = libbalsa_message_body_get_mime_type(part);
     gboolean res = FALSE;
-    if (!balsa_app.display_alt_plain &&
+    if ((!balsa_app.display_alt_plain || libbalsa_html_get_prefer_html(from)) &&
        libbalsa_html_type(content_type))
        res = TRUE;
     else if(strcmp(content_type, "multipart/related") == 0 &&
            part->parts)
-       res = libbalsa_can_display(part->parts);
+       res = libbalsa_can_display(part->parts, from);
     g_free(content_type);
     return res;
 }
@@ -2026,7 +2026,7 @@ libbalsa_can_display(LibBalsaMessageBody *part)
    In the case as above, B & C should be displayed.
 */
 static LibBalsaMessageBody*
-preferred_part(LibBalsaMessageBody *parts)
+preferred_part(LibBalsaMessageBody *parts, InternetAddressList *from)
 {
     LibBalsaMessageBody *body, *preferred = parts;
 
@@ -2039,7 +2039,7 @@ preferred_part(LibBalsaMessageBody *parts)
             strcmp(content_type, "text/calendar") == 0)
             preferred = body;
 #ifdef HAVE_HTML_WIDGET
-        else if (libbalsa_can_display(body))
+        else if (libbalsa_can_display(body, from))
             preferred = body;
 #endif                          /* HAVE_HTML_WIDGET */
 
@@ -2163,8 +2163,11 @@ add_multipart(BalsaMessage *balsa_message, LibBalsaMessageBody *body,
         /* add the compound object root part */
         body = add_body(balsa_message, libbalsa_message_body_mp_related_root(body), container);
     } else if (g_mime_content_type_is_type(type, "multipart", "alternative")) {
-            /* Add the most suitable part. */
-        body = add_body(balsa_message, preferred_part(body->parts), container);
+        InternetAddressList *from = NULL;
+
+        from = libbalsa_message_get_headers(balsa_message->message)->from;
+        /* Add the most suitable part. */
+        body = add_body(balsa_message, preferred_part(body->parts, from), container);
     } else if (g_mime_content_type_is_type(type, "multipart", "digest")) {
        body = add_multipart_digest(balsa_message, body->parts, container);
     } else { /* default to multipart/mixed */
diff --git a/src/balsa-mime-widget-text.c b/src/balsa-mime-widget-text.c
index 1fdd35273..fff9aa45e 100644
--- a/src/balsa-mime-widget-text.c
+++ b/src/balsa-mime-widget-text.c
@@ -1103,12 +1103,29 @@ bmwt_html_select_all_cb(GtkWidget * html)
     libbalsa_html_select_all(html);
 }
 
+static void
+bmwt_html_prefer_html_changed(GtkCheckMenuItem *checkmenuitem,
+                              gpointer          user_data)
+{
+       libbalsa_html_prefer_set_prefer_html(INTERNET_ADDRESS_LIST(user_data),
+               gtk_check_menu_item_get_active(checkmenuitem));
+}
+
+static void
+bmwt_html_load_images_changed(GtkCheckMenuItem *checkmenuitem,
+                              gpointer          user_data)
+{
+       libbalsa_html_prefer_set_load_images(INTERNET_ADDRESS_LIST(user_data),
+               gtk_check_menu_item_get_active(checkmenuitem));
+}
+
 static void
 bmwt_html_populate_popup_menu(BalsaMessage * bm,
                               GtkWidget    * html,
                               GtkMenu      * menu)
 {
     GtkWidget *menuitem;
+    InternetAddressList *from;
     gpointer mime_body = g_object_get_data(G_OBJECT(html), "mime-body");
 
     menuitem = gtk_menu_item_new_with_label(_("Zoom In"));
@@ -1158,6 +1175,25 @@ bmwt_html_populate_popup_menu(BalsaMessage * bm,
                              G_CALLBACK(libbalsa_html_print), html);
     gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
     gtk_widget_set_sensitive(menuitem, libbalsa_html_can_print(html));
+
+    menuitem = gtk_separator_menu_item_new();
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+
+    from = libbalsa_message_get_headers(balsa_message_get_message(bm))->from;
+    menuitem = gtk_check_menu_item_new_with_label(_("Prefer HTML for this sender"));
+    gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem),
+        libbalsa_html_get_prefer_html(from));
+    gtk_widget_set_sensitive(menuitem, from != NULL);
+    g_signal_connect(menuitem, "toggled", G_CALLBACK(bmwt_html_prefer_html_changed), from);
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+
+    menuitem = gtk_check_menu_item_new_with_label(_("Load images for this sender"));
+    gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem),
+        libbalsa_html_get_load_images(from));
+    gtk_widget_set_sensitive(menuitem, from != NULL);
+    g_signal_connect(menuitem, "toggled", G_CALLBACK(bmwt_html_load_images_changed), from);
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+
     gtk_widget_show_all(GTK_WIDGET(menu));
 }
 
@@ -1230,14 +1266,17 @@ static BalsaMimeWidget *
 bm_widget_new_html(BalsaMessage * bm, LibBalsaMessageBody * mime_body)
 {
     BalsaMimeWidgetText *mwt = g_object_new(BALSA_TYPE_MIME_WIDGET_TEXT, NULL);
+    InternetAddressList *from;
     GtkWidget *widget;
     GtkWidget *popup_menu;
     GtkEventController *key_controller;
 
+    from = libbalsa_message_get_headers(balsa_message_get_message(bm))->from;
     mwt->text_widget = widget =
         libbalsa_html_new(mime_body,
                          (LibBalsaHtmlCallback) bm_widget_on_url,
-                         (LibBalsaHtmlCallback) handle_url);
+                         (LibBalsaHtmlCallback) handle_url,
+                         libbalsa_html_get_load_images(from));
     gtk_container_add(GTK_CONTAINER(mwt), widget);
 
     g_object_set_data(G_OBJECT(widget), "mime-body", mime_body);
diff --git a/src/pref-manager.c b/src/pref-manager.c
index 41f024f23..1ca43cc4c 100644
--- a/src/pref-manager.c
+++ b/src/pref-manager.c
@@ -2553,6 +2553,11 @@ pm_grid_add_alternative_group(GtkWidget * grid_widget)
        pm_grid_attach_check(grid, 1, ++row, 1, 1, _("Prefer text/plain over HTML"));
 
 #ifdef HAVE_HTML_WIDGET
+    button = gtk_button_new_with_label(_("Manage exceptions…"));
+    g_signal_connect_swapped(button, "clicked",
+        G_CALLBACK(libbalsa_html_pref_dialog_run), property_box);
+    pm_grid_attach(grid, button, 2, row, 1, 1);
+
     label = gtk_label_new(NULL);
     set_html_cache_label_str(GTK_LABEL(label));
     pm_grid_attach(grid, label, 1, ++row, 1, 1);
diff --git a/src/print-gtk.c b/src/print-gtk.c
index 421191dfc..fb3bcbe2e 100644
--- a/src/print-gtk.c
+++ b/src/print-gtk.c
@@ -26,6 +26,7 @@
 #include "balsa-app.h"
 #include "print.h"
 #include "misc.h"
+#include "html.h"
 #include "balsa-message.h"
 #include "quote-color.h"
 #include <glib/gi18n.h>
@@ -51,6 +52,8 @@ typedef struct {
 #ifdef HAVE_HTML_WIDGET
     GtkWidget *html_print;
     GtkWidget *html_load_imgs;
+    gboolean prefer_text;
+    gboolean load_images;
     BalsaPrintSetup *setup;
 #endif
 } BalsaPrintPrefs;
@@ -607,11 +610,11 @@ message_prefs_widget(GtkPrintOperation * operation,
     grid = create_options_group(_("Highlighting"), page, 1, 1, 1);
 
     print_prefs->html_print = gtk_check_button_new_with_mnemonic(_("Prefer text/plain over HTML"));
-    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(print_prefs->html_print), balsa_app.display_alt_plain);
+    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(print_prefs->html_print), print_prefs->prefer_text);
     gtk_grid_attach(GTK_GRID(grid), print_prefs->html_print, 1, 0, 1, 1);
 
     print_prefs->html_load_imgs = gtk_check_button_new_with_mnemonic(_("Download images from remote servers 
(may be dangerous)"));
-    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(print_prefs->html_load_imgs), FALSE);
+    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(print_prefs->html_load_imgs), print_prefs->load_images);
     gtk_grid_attach(GTK_GRID(grid), print_prefs->html_load_imgs, 1, 1, 1, 1);
 
     /* phantom alignment */
@@ -737,6 +740,9 @@ message_print(LibBalsaMessage * msg, GtkWindow * parent)
     GtkPrintOperationResult res;
     BalsaPrintData *print_data;
     BalsaPrintPrefs print_prefs;
+#ifdef HAVE_HTML_WIDGET
+    InternetAddressList *from;
+#endif
     GError *err = NULL;
 
     print = gtk_print_operation_new();
@@ -760,6 +766,9 @@ message_print(LibBalsaMessage * msg, GtkWindow * parent)
     print_data->message = msg;
 #ifdef HAVE_HTML_WIDGET
     print_prefs.setup = &print_data->setup;
+    from = libbalsa_message_get_headers(msg)->from;
+    print_prefs.prefer_text = balsa_app.display_alt_plain && !libbalsa_html_get_prefer_html(from);
+    print_prefs.load_images = libbalsa_html_get_load_images(from);
 #endif
 
     g_signal_connect(print, "begin_print", G_CALLBACK(begin_print), print_data);
diff --git a/src/save-restore.c b/src/save-restore.c
index fbbf90886..223fe6390 100644
--- a/src/save-restore.c
+++ b/src/save-restore.c
@@ -727,6 +727,9 @@ config_global_load(void)
     geometry_manager_init("IMAPSelectParent", 200, 160, FALSE);
     geometry_manager_init("KeyDialog", 400, 200, FALSE);
     geometry_manager_init("KeyList", 300, 200, FALSE);
+#ifdef HAVE_HTML_WIDGET
+    geometry_manager_init("HTMLPrefsDB", 300, 200, FALSE);
+#endif
 #ifdef ENABLE_AUTOCRYPT
     geometry_manager_init("AutocryptDB", 300, 200, FALSE);
 #endif  /* ENABLE_AUTOCRYPT */


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