[balsa/imap-improvements] implement sender-dependent preferences for HTML messages (#13)
- From: Albrecht Dreß <albrecht src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [balsa/imap-improvements] implement sender-dependent preferences for HTML messages (#13)
- Date: Wed, 5 May 2021 19:19:44 +0000 (UTC)
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]