[evolution-data-server] evo-#1433 - Message List columns for custom headers



commit 23d04dfba832e0c71554edf1d79b971458051a54
Author: Milan Crha <mcrha redhat com>
Date:   Thu Aug 5 10:44:44 2021 +0200

    evo-#1433 - Message List columns for custom headers
    
    Apart of adding custom headers into the folder summary, it also
    adds a 'preview' property, to be used for message body preview
    in the future. That's to have it under a single commit and
    a single soname version bump.
    
    Related to https://gitlab.gnome.org/GNOME/evolution/-/issues/1433

 CMakeLists.txt                                     |   2 +-
 .../org.gnome.evolution-data-server.gschema.xml.in |   5 +
 src/camel/camel-db.c                               |  58 ++-
 src/camel/camel-db.h                               |   8 +
 src/camel/camel-folder-summary.c                   |  24 +-
 src/camel/camel-message-info-base.c                | 153 ++++++++
 src/camel/camel-message-info.c                     | 427 ++++++++++++++++++++-
 src/camel/camel-message-info.h                     |  42 +-
 src/camel/camel-utils.c                            | 244 ++++++++++++
 src/camel/camel-utils.h                            |  15 +
 src/camel/camel-vee-message-info.c                 |  47 +++
 src/camel/camel.c                                  |   8 +
 src/camel/providers/local/camel-local-summary.c    |  10 +-
 13 files changed, 1013 insertions(+), 30 deletions(-)
---
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4350bdbcd..56a27a5e2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -48,7 +48,7 @@ set(USER_PROMPTER_DBUS_SERVICE_NAME   "org.gnome.evolution.dataserver.UserPrompter
 # ******************************
 # Library versioning
 # ******************************
-set(LIBCAMEL_CURRENT 62)
+set(LIBCAMEL_CURRENT 63)
 set(LIBCAMEL_REVISION 0)
 set(LIBCAMEL_AGE 0)
 
diff --git a/data/org.gnome.evolution-data-server.gschema.xml.in 
b/data/org.gnome.evolution-data-server.gschema.xml.in
index 98defd45a..dd0e5f4af 100644
--- a/data/org.gnome.evolution-data-server.gschema.xml.in
+++ b/data/org.gnome.evolution-data-server.gschema.xml.in
@@ -19,6 +19,11 @@
       <_summary>Override SMTP HELO/EHLO argument</_summary>
       <_description>When not empty, it's used as the SMTP HELO/EHLO argument, instead of the local host 
name/IP.</_description>
     </key>
+    <key name="camel-message-info-user-headers" type="as">
+      <default>[]</default>
+      <_summary>Array of user header names</_summary>
+      <_description>These headers can be stored in the folder summary, eventually being visible in the GUI. 
The value can contain a pipe character ('|'), which delimits the display name from the header name. Example: 
'Span Score|X-Spam-Score'</_description>
+    </key>
     <key name="network-monitor-gio-name" type="s">
       <default>''</default>
       <_summary>GIO name of the GNetworkMonitor to use for an ENetworkMonitor instance</_summary>
diff --git a/src/camel/camel-db.c b/src/camel/camel-db.c
index db47c7868..8656359c7 100644
--- a/src/camel/camel-db.c
+++ b/src/camel/camel-db.c
@@ -38,6 +38,8 @@
 
 #include "camel-db.h"
 
+#define MESSAGE_INFO_TABLE_VERSION 3
+
 /* how long to wait before invoking sync on the file */
 #define SYNC_TIMEOUT_SECONDS 5
 
@@ -1682,12 +1684,17 @@ camel_db_create_message_info_table (CamelDB *cdb,
                        "usertags TEXT , "
                        "cinfo TEXT , "
                        "bdata TEXT, "
+                       "userheaders TEXT, "
+                       "preview TEXT, "
                        "created TEXT, "
                        "modified TEXT)",
                        folder_name);
        ret = camel_db_add_to_transaction (cdb, table_creation_query, error);
        sqlite3_free (table_creation_query);
 
+       if (ret != 0)
+               return ret;
+
        /* FIXME: sqlize folder_name before you create the index */
        safe_index = g_strdup_printf ("SINDEX-%s", folder_name);
        table_creation_query = sqlite3_mprintf ("DROP INDEX IF EXISTS %Q", safe_index);
@@ -1695,6 +1702,9 @@ camel_db_create_message_info_table (CamelDB *cdb,
        g_free (safe_index);
        sqlite3_free (table_creation_query);
 
+       if (ret != 0)
+               return ret;
+
        /* Index on deleted*/
        safe_index = g_strdup_printf ("DELINDEX-%s", folder_name);
        table_creation_query = sqlite3_mprintf ("CREATE INDEX IF NOT EXISTS %Q ON %Q (deleted)", safe_index, 
folder_name);
@@ -1702,6 +1712,9 @@ camel_db_create_message_info_table (CamelDB *cdb,
        g_free (safe_index);
        sqlite3_free (table_creation_query);
 
+       if (ret != 0)
+               return ret;
+
        /* Index on Junk*/
        safe_index = g_strdup_printf ("JUNKINDEX-%s", folder_name);
        table_creation_query = sqlite3_mprintf ("CREATE INDEX IF NOT EXISTS %Q ON %Q (junk)", safe_index, 
folder_name);
@@ -1709,6 +1722,9 @@ camel_db_create_message_info_table (CamelDB *cdb,
        g_free (safe_index);
        sqlite3_free (table_creation_query);
 
+       if (ret != 0)
+               return ret;
+
        /* Index on unread*/
        safe_index = g_strdup_printf ("READINDEX-%s", folder_name);
        table_creation_query = sqlite3_mprintf ("CREATE INDEX IF NOT EXISTS %Q ON %Q (read)", safe_index, 
folder_name);
@@ -1733,13 +1749,17 @@ camel_db_migrate_folder_prepare (CamelDB *cdb,
        if (version < 0) {
                ret = camel_db_create_message_info_table (cdb, folder_name, error);
                g_clear_error (error);
-       } else if (version < 1) {
+       } else if (version < 3) {
 
                /* Between version 0-1 the following things are changed
                 * ADDED: created: time
                 * ADDED: modified: time
                 * RENAMED: msg_security to dirty
-                * */
+                *
+                * Between version 2-3 the following things are changed
+                * ADDED: userheaders: text
+                * ADDED: preview: text
+                */
 
                table_creation_query = sqlite3_mprintf ("DROP TABLE IF EXISTS 'mem.%q'", folder_name);
                ret = camel_db_add_to_transaction (cdb, table_creation_query, error);
@@ -1773,6 +1793,8 @@ camel_db_migrate_folder_prepare (CamelDB *cdb,
                                "usertags TEXT , "
                                "cinfo TEXT , "
                                "bdata TEXT, "
+                               "userheaders TEXT, "
+                               "preview TEXT, "
                                "created TEXT, "
                                "modified TEXT )",
                                folder_name);
@@ -1787,7 +1809,7 @@ camel_db_migrate_folder_prepare (CamelDB *cdb,
                        "size , dsent , dreceived , subject , mail_from , "
                        "mail_to , mail_cc , mlist , followup_flag , "
                        "followup_completed_on , followup_due_by , "
-                       "part , labels , usertags , cinfo , bdata , "
+                       "part , labels , usertags , cinfo , bdata , '', '', "
                        "strftime(\"%%s\", 'now'), "
                        "strftime(\"%%s\", 'now') FROM %Q",
                        folder_name, folder_name);
@@ -1820,7 +1842,7 @@ camel_db_migrate_folder_recreate (CamelDB *cdb,
 
        /* Migration stage two: writing back the old data */
 
-       if (version < 2) {
+       if (version < 3) {
                GError *local_error = NULL;
 
                table_creation_query = sqlite3_mprintf (
@@ -1830,7 +1852,7 @@ camel_db_migrate_folder_recreate (CamelDB *cdb,
                        "subject , mail_from , mail_to , mail_cc , mlist , "
                        "followup_flag , followup_completed_on , "
                        "followup_due_by , part , labels , usertags , "
-                       "cinfo , bdata, created, modified FROM 'mem.%q'",
+                       "cinfo , bdata, userheaders, preview, created, modified FROM 'mem.%q'",
                        folder_name, folder_name);
                ret = camel_db_add_to_transaction (cdb, table_creation_query, &local_error);
                sqlite3_free (table_creation_query);
@@ -1910,9 +1932,9 @@ camel_db_write_folder_version (CamelDB *cdb,
        version_creation_query = sqlite3_mprintf ("CREATE TABLE IF NOT EXISTS '%q_version' ( version TEXT )", 
folder_name);
 
        if (old_version == -1)
-               version_insert_query = sqlite3_mprintf ("INSERT INTO '%q_version' VALUES ('2')", folder_name);
+               version_insert_query = sqlite3_mprintf ("INSERT INTO '%q_version' VALUES ('" G_STRINGIFY 
(MESSAGE_INFO_TABLE_VERSION) "')", folder_name);
        else
-               version_insert_query = sqlite3_mprintf ("UPDATE '%q_version' SET version='2'", folder_name);
+               version_insert_query = sqlite3_mprintf ("UPDATE '%q_version' SET version='" G_STRINGIFY 
(MESSAGE_INFO_TABLE_VERSION) "'", folder_name);
 
        ret = camel_db_add_to_transaction (cdb, version_creation_query, error);
        ret = camel_db_add_to_transaction (cdb, version_insert_query, error);
@@ -1989,6 +2011,10 @@ camel_db_prepare_message_info_table (CamelDB *cdb,
                current_version = -1;
        }
 
+       /* Avoid the migration when not needed */
+       if (current_version == MESSAGE_INFO_TABLE_VERSION)
+               goto exit;
+
        camel_db_begin_transaction (cdb, &err);
        in_transaction = TRUE;
 
@@ -2053,7 +2079,7 @@ camel_db_write_message_info_record (CamelDB *cdb,
                "INSERT OR REPLACE INTO %Q VALUES ("
                "%Q, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, "
                "%lld, %lld, %Q, %Q, %Q, %Q, %Q, %Q, %Q, %Q, "
-               "%Q, %Q, %Q, %Q, %Q, "
+               "%Q, %Q, %Q, %Q, %Q, %Q, %Q, "
                "strftime(\"%%s\", 'now'), "
                "strftime(\"%%s\", 'now') )",
                folder_name,
@@ -2082,7 +2108,9 @@ camel_db_write_message_info_record (CamelDB *cdb,
                record->labels,
                record->usertags,
                record->cinfo,
-               record->bdata);
+               record->bdata,
+               record->userheaders,
+               record->preview);
 
        ret = camel_db_add_to_transaction (cdb, ins_query, error);
 
@@ -2274,7 +2302,7 @@ camel_db_read_message_info_record_with_uid (CamelDB *cdb,
        query = sqlite3_mprintf (
                "SELECT uid, flags, size, dsent, dreceived, subject, "
                "mail_from, mail_to, mail_cc, mlist, part, labels, "
-               "usertags, cinfo, bdata FROM %Q WHERE uid = %Q",
+               "usertags, cinfo, bdata, userheaders, preview FROM %Q WHERE uid = %Q",
                folder_name, uid);
        ret = camel_db_select (cdb, query, callback, user_data, error);
        sqlite3_free (query);
@@ -2309,7 +2337,7 @@ camel_db_read_message_info_records (CamelDB *cdb,
        query = sqlite3_mprintf (
                "SELECT uid, flags, size, dsent, dreceived, subject, "
                "mail_from, mail_to, mail_cc, mlist, part, labels, "
-               "usertags, cinfo, bdata FROM %Q ", folder_name);
+               "usertags, cinfo, bdata, userheaders, preview FROM %Q ", folder_name);
        ret = camel_db_select (cdb, query, callback, user_data, error);
        sqlite3_free (query);
 
@@ -2577,6 +2605,8 @@ camel_db_camel_mir_free (CamelMIRecord *record)
                g_free (record->usertags);
                g_free (record->cinfo);
                g_free (record->bdata);
+               g_free (record->userheaders);
+               g_free (record->preview);
 
                g_free (record);
        }
@@ -2720,7 +2750,9 @@ camel_db_start_in_memory_transactions (CamelDB *cdb,
                        "labels TEXT , "
                        "usertags TEXT , "
                        "cinfo TEXT , "
-                       "bdata TEXT )",
+                       "bdata TEXT, "
+                       "userheaders TEXT, "
+                       "preview TEXT)",
                CAMEL_DB_IN_MEMORY_TABLE);
        ret = camel_db_command (cdb, cmd, error);
        if (ret != 0 )
@@ -2792,6 +2824,7 @@ static struct _known_column_names {
        { "mlist",                      CAMEL_DB_COLUMN_MLIST },
        { "nextuid",                    CAMEL_DB_COLUMN_NEXTUID },
        { "part",                       CAMEL_DB_COLUMN_PART },
+       { "preview",                    CAMEL_DB_COLUMN_PREVIEW },
        { "read",                       CAMEL_DB_COLUMN_READ },
        { "replied",                    CAMEL_DB_COLUMN_REPLIED },
        { "saved_count",                CAMEL_DB_COLUMN_SAVED_COUNT },
@@ -2800,6 +2833,7 @@ static struct _known_column_names {
        { "time",                       CAMEL_DB_COLUMN_TIME },
        { "uid",                        CAMEL_DB_COLUMN_UID },
        { "unread_count",               CAMEL_DB_COLUMN_UNREAD_COUNT },
+       { "userheaders",                CAMEL_DB_COLUMN_USERHEADERS },
        { "usertags",                   CAMEL_DB_COLUMN_USERTAGS },
        { "version",                    CAMEL_DB_COLUMN_VERSION },
        { "visible_count",              CAMEL_DB_COLUMN_VISIBLE_COUNT },
diff --git a/src/camel/camel-db.h b/src/camel/camel-db.h
index c40b3ecbe..ec42bf516 100644
--- a/src/camel/camel-db.h
+++ b/src/camel/camel-db.h
@@ -160,6 +160,8 @@ typedef gint (* CamelDBCollate)(gpointer enc, gint length1, gconstpointer data1,
  * @usertags: composite string of user tags
  * @cinfo: content info string - composite string
  * @bdata: provider specific data
+ * @userheaders: value for user-defined message headers; Since: 3.42
+ * @preview: message body preview; Since: 3.42
  *
  * The extensive DB format, supporting basic searching and sorting.
  *
@@ -192,6 +194,8 @@ typedef struct _CamelMIRecord {
        gchar *usertags;
        gchar *cinfo;
        gchar *bdata;
+       gchar *userheaders;
+       gchar *preview;
 } CamelMIRecord;
 
 /**
@@ -254,6 +258,7 @@ typedef struct _CamelFIRecord {
  * @CAMEL_DB_COLUMN_MLIST: mlist
  * @CAMEL_DB_COLUMN_NEXTUID: nextuid
  * @CAMEL_DB_COLUMN_PART: part
+ * @CAMEL_DB_COLUMN_PREVIEW: preview
  * @CAMEL_DB_COLUMN_READ: read
  * @CAMEL_DB_COLUMN_REPLIED: replied
  * @CAMEL_DB_COLUMN_SAVED_COUNT: saved_count
@@ -262,6 +267,7 @@ typedef struct _CamelFIRecord {
  * @CAMEL_DB_COLUMN_TIME: time
  * @CAMEL_DB_COLUMN_UID: uid
  * @CAMEL_DB_COLUMN_UNREAD_COUNT: unread_count
+ * @CAMEL_DB_COLUMN_USERHEADERS: userheaders
  * @CAMEL_DB_COLUMN_USERTAGS: usertags
  * @CAMEL_DB_COLUMN_VERSION: version
  * @CAMEL_DB_COLUMN_VISIBLE_COUNT: visible_count
@@ -296,6 +302,7 @@ typedef enum {
        CAMEL_DB_COLUMN_MLIST,
        CAMEL_DB_COLUMN_NEXTUID,
        CAMEL_DB_COLUMN_PART,
+       CAMEL_DB_COLUMN_PREVIEW,
        CAMEL_DB_COLUMN_READ,
        CAMEL_DB_COLUMN_REPLIED,
        CAMEL_DB_COLUMN_SAVED_COUNT,
@@ -304,6 +311,7 @@ typedef enum {
        CAMEL_DB_COLUMN_TIME,
        CAMEL_DB_COLUMN_UID,
        CAMEL_DB_COLUMN_UNREAD_COUNT,
+       CAMEL_DB_COLUMN_USERHEADERS,
        CAMEL_DB_COLUMN_USERTAGS,
        CAMEL_DB_COLUMN_VERSION,
        CAMEL_DB_COLUMN_VISIBLE_COUNT,
diff --git a/src/camel/camel-folder-summary.c b/src/camel/camel-folder-summary.c
index af80a658e..8cb6dba9f 100644
--- a/src/camel/camel-folder-summary.c
+++ b/src/camel/camel-folder-summary.c
@@ -52,6 +52,7 @@
 #include "camel-stream-null.h"
 #include "camel-string-utils.h"
 #include "camel-store.h"
+#include "camel-utils.h"
 #include "camel-vee-folder.h"
 #include "camel-vtrash-folder.h"
 #include "camel-mime-part-utils.h"
@@ -1814,7 +1815,6 @@ camel_folder_summary_load (CamelFolderSummary *summary,
        CamelStore *parent_store;
        const gchar *full_name;
        gint ret = 0;
-       GError *local_error = NULL;
 
        g_return_val_if_fail (CAMEL_IS_FOLDER_SUMMARY (summary), FALSE);
 
@@ -1844,18 +1844,10 @@ camel_folder_summary_load (CamelFolderSummary *summary,
 
        cdb = camel_store_get_db (parent_store);
 
-       ret = camel_db_get_folder_uids (
-               cdb, full_name, klass->sort_by, klass->collate,
-               summary->priv->uids, &local_error);
+       ret = camel_db_prepare_message_info_table (cdb, full_name, error);
 
-       if (local_error != NULL && local_error->message != NULL &&
-           strstr (local_error->message, "no such table") != NULL) {
-               g_clear_error (&local_error);
-
-               /* create table the first time it is accessed and missing */
-               ret = camel_db_prepare_message_info_table (cdb, full_name, error);
-       } else if (local_error != NULL)
-               g_propagate_error (error, local_error);
+       if (ret == 0)
+               ret = camel_db_get_folder_uids (cdb, full_name, klass->sort_by, klass->collate, 
summary->priv->uids, error);
 
        camel_folder_summary_unlock (summary);
 
@@ -1950,6 +1942,12 @@ mir_from_cols (CamelMIRecord *mir,
                        case CAMEL_DB_COLUMN_BDATA:
                                mir->bdata = cols[i];
                                break;
+                       case CAMEL_DB_COLUMN_USERHEADERS:
+                               mir->userheaders = cols[i];
+                               break;
+                       case CAMEL_DB_COLUMN_PREVIEW:
+                               mir->preview = cols[i];
+                               break;
                        default:
                                g_warn_if_reached ();
                                break;
@@ -2981,6 +2979,8 @@ message_info_new_from_headers (CamelFolderSummary *summary,
        g_free (cc);
        g_free (mlist);
 
+       camel_util_fill_message_info_user_headers (mi, headers);
+
        if ((date = camel_name_value_array_get_named (headers, CAMEL_COMPARE_CASE_INSENSITIVE, "Date")))
                camel_message_info_set_date_sent (mi, camel_header_decode_date (date, NULL));
        else
diff --git a/src/camel/camel-message-info-base.c b/src/camel/camel-message-info-base.c
index dac7834ee..432655b84 100644
--- a/src/camel/camel-message-info-base.c
+++ b/src/camel/camel-message-info-base.c
@@ -40,6 +40,8 @@ struct _CamelMessageInfoBasePrivate {
        guint64 message_id;
        GArray *references;     /* guint64, aka CamelSummaryMessageID */
        CamelNameValueArray *headers;
+       CamelNameValueArray *user_headers;
+       gchar *preview;
 };
 
 G_DEFINE_TYPE_WITH_PRIVATE (CamelMessageInfoBase, camel_message_info_base, CAMEL_TYPE_MESSAGE_INFO)
@@ -779,6 +781,102 @@ message_info_base_take_headers (CamelMessageInfo *mi,
        if (changed) {
                camel_name_value_array_free (bmi->priv->headers);
                bmi->priv->headers = headers;
+
+               /* Automatically fill user headers from the known message headers */
+               if (headers)
+                       camel_util_fill_message_info_user_headers (mi, headers);
+       } else {
+               camel_name_value_array_free (headers);
+       }
+
+       camel_message_info_property_unlock (mi);
+
+       return changed;
+}
+
+static const gchar *
+message_info_base_get_user_header (const CamelMessageInfo *mi,
+                                  const gchar *name)
+{
+       CamelMessageInfoBase *bmi;
+       const gchar *result;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO_BASE (mi), NULL);
+       g_return_val_if_fail (name != NULL, NULL);
+
+       bmi = CAMEL_MESSAGE_INFO_BASE (mi);
+
+       camel_message_info_property_lock (mi);
+       if (bmi->priv->user_headers)
+               result = camel_name_value_array_get_named (bmi->priv->user_headers, 
CAMEL_COMPARE_CASE_INSENSITIVE, name);
+       else
+               result = NULL;
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+static gboolean
+message_info_base_set_user_header (CamelMessageInfo *mi,
+                                  const gchar *name,
+                                  const gchar *value)
+{
+       CamelMessageInfoBase *bmi;
+       gboolean changed;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO_BASE (mi), FALSE);
+       g_return_val_if_fail (name != NULL, FALSE);
+
+       bmi = CAMEL_MESSAGE_INFO_BASE (mi);
+
+       camel_message_info_property_lock (mi);
+       if (!bmi->priv->user_headers)
+               bmi->priv->user_headers = camel_name_value_array_new ();
+
+       if (value)
+               changed = camel_name_value_array_set_named (bmi->priv->user_headers, 
CAMEL_COMPARE_CASE_INSENSITIVE, name, value);
+       else
+               changed = camel_name_value_array_remove_named (bmi->priv->user_headers, 
CAMEL_COMPARE_CASE_INSENSITIVE, name, TRUE);
+       camel_message_info_property_unlock (mi);
+
+       return changed;
+}
+
+static const CamelNameValueArray *
+message_info_base_get_user_headers (const CamelMessageInfo *mi)
+{
+       CamelMessageInfoBase *bmi;
+       const CamelNameValueArray *result;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO_BASE (mi), NULL);
+
+       bmi = CAMEL_MESSAGE_INFO_BASE (mi);
+
+       camel_message_info_property_lock (mi);
+       result = bmi->priv->user_headers;
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+static gboolean
+message_info_base_take_user_headers (CamelMessageInfo *mi,
+                                    CamelNameValueArray *headers)
+{
+       CamelMessageInfoBase *bmi;
+       gboolean changed;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO_BASE (mi), FALSE);
+
+       bmi = CAMEL_MESSAGE_INFO_BASE (mi);
+
+       camel_message_info_property_lock (mi);
+
+       changed = !camel_name_value_array_equal (bmi->priv->user_headers, headers, 
CAMEL_COMPARE_CASE_INSENSITIVE);
+
+       if (changed) {
+               camel_name_value_array_free (bmi->priv->user_headers);
+               bmi->priv->user_headers = headers;
        } else {
                camel_name_value_array_free (headers);
        }
@@ -788,6 +886,51 @@ message_info_base_take_headers (CamelMessageInfo *mi,
        return changed;
 }
 
+static const gchar *
+message_info_base_get_preview (const CamelMessageInfo *mi)
+{
+       CamelMessageInfoBase *bmi;
+       const gchar *result;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO_BASE (mi), NULL);
+
+       bmi = CAMEL_MESSAGE_INFO_BASE (mi);
+
+       camel_message_info_property_lock (mi);
+       result = bmi->priv->preview;
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+static gboolean
+message_info_base_set_preview (CamelMessageInfo *mi,
+                              const gchar *preview)
+{
+       CamelMessageInfoBase *bmi;
+       gboolean changed;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO_BASE (mi), FALSE);
+
+       bmi = CAMEL_MESSAGE_INFO_BASE (mi);
+
+       camel_message_info_property_lock (mi);
+
+       if (preview && !*preview)
+               preview = NULL;
+
+       changed = g_strcmp0 (bmi->priv->preview, preview) != 0;
+
+       if (changed) {
+               g_free (bmi->priv->preview);
+               bmi->priv->preview = g_strdup (preview);
+       }
+
+       camel_message_info_property_unlock (mi);
+
+       return changed;
+}
+
 static void
 message_info_base_dispose (GObject *object)
 {
@@ -804,12 +947,16 @@ message_info_base_dispose (GObject *object)
        g_clear_pointer (&bmi->priv->to, (GDestroyNotify) camel_pstring_free);
        g_clear_pointer (&bmi->priv->cc, (GDestroyNotify) camel_pstring_free);
        g_clear_pointer (&bmi->priv->mlist, (GDestroyNotify) camel_pstring_free);
+       g_clear_pointer (&bmi->priv->preview, g_free);
 
        g_clear_pointer (&bmi->priv->references, g_array_unref);
 
        camel_name_value_array_free (bmi->priv->headers);
        bmi->priv->headers = NULL;
 
+       camel_name_value_array_free (bmi->priv->user_headers);
+       bmi->priv->user_headers = NULL;
+
        /* Chain up to parent's method. */
        G_OBJECT_CLASS (camel_message_info_base_parent_class)->dispose (object);
 }
@@ -855,6 +1002,12 @@ camel_message_info_base_class_init (CamelMessageInfoBaseClass *class)
        mi_class->take_references = message_info_base_take_references;
        mi_class->get_headers = message_info_base_get_headers;
        mi_class->take_headers = message_info_base_take_headers;
+       mi_class->get_user_header = message_info_base_get_user_header;
+       mi_class->set_user_header = message_info_base_set_user_header;
+       mi_class->get_user_headers = message_info_base_get_user_headers;
+       mi_class->take_user_headers = message_info_base_take_user_headers;
+       mi_class->get_preview = message_info_base_get_preview;
+       mi_class->set_preview = message_info_base_set_preview;
 
        object_class = G_OBJECT_CLASS (class);
        object_class->dispose = message_info_base_dispose;
diff --git a/src/camel/camel-message-info.c b/src/camel/camel-message-info.c
index 70e4bedc9..44586a82e 100644
--- a/src/camel/camel-message-info.c
+++ b/src/camel/camel-message-info.c
@@ -63,7 +63,9 @@ enum {
        PROP_DATE_RECEIVED,
        PROP_MESSAGE_ID,
        PROP_REFERENCES,
-       PROP_HEADERS
+       PROP_HEADERS,
+       PROP_USER_HEADERS,
+       PROP_PREVIEW
 };
 
 G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (CamelMessageInfo, camel_message_info, G_TYPE_OBJECT)
@@ -108,6 +110,7 @@ message_info_clone (const CamelMessageInfo *mi,
        camel_message_info_set_to (result, camel_message_info_get_to (mi));
        camel_message_info_set_cc (result, camel_message_info_get_cc (mi));
        camel_message_info_set_mlist (result, camel_message_info_get_mlist (mi));
+       camel_message_info_set_preview (result, camel_message_info_get_preview (mi));
        camel_message_info_set_size (result, camel_message_info_get_size (mi));
        camel_message_info_set_date_sent (result, camel_message_info_get_date_sent (mi));
        camel_message_info_set_date_received (result, camel_message_info_get_date_received (mi));
@@ -135,6 +138,12 @@ message_info_clone (const CamelMessageInfo *mi,
                        camel_name_value_array_copy (headers));
        }
 
+       headers = camel_message_info_get_user_headers (mi);
+       if (headers) {
+               camel_message_info_take_user_headers (result,
+                       camel_name_value_array_copy (headers));
+       }
+
        /* Set flags as the last, to not overwrite 'folder-flagged' flag by
           the "changes" when copying fields. */
        camel_message_info_set_flags (result, ~0, camel_message_info_get_flags (mi));
@@ -173,6 +182,7 @@ message_info_load (CamelMessageInfo *mi,
        camel_message_info_set_to (mi, record->to);
        camel_message_info_set_cc (mi, record->cc);
        camel_message_info_set_mlist (mi, record->mlist);
+       camel_message_info_set_preview (mi, record->preview);
 
        /* Extract Message id & References */
        part = record->part;
@@ -220,6 +230,11 @@ message_info_load (CamelMessageInfo *mi,
                if (label && *label)
                        camel_named_flags_insert (user_flags, label);
 
+               if (camel_named_flags_get_length (user_flags) == 0) {
+                       camel_named_flags_free (user_flags);
+                       user_flags = NULL;
+               }
+
                camel_message_info_take_user_flags (mi, user_flags);
        }
 
@@ -245,9 +260,44 @@ message_info_load (CamelMessageInfo *mi,
                        g_free (value);
                }
 
+               if (camel_name_value_array_get_length (user_tags) == 0) {
+                       camel_name_value_array_free (user_tags);
+                       user_tags = NULL;
+               }
+
                camel_message_info_take_user_tags (mi, user_tags);
        }
 
+       /* Extract User headers */
+       part = record->userheaders;
+       if (part) {
+               CamelNameValueArray *user_headers;
+
+               count = camel_util_bdata_get_number (&part, 0);
+
+               user_headers = camel_name_value_array_new_sized (count);
+
+               for (ii = 0; ii < count; ii++) {
+                       gchar *name, *value;
+
+                       name = camel_util_bdata_get_string (&part, NULL);
+                       value = camel_util_bdata_get_string (&part, NULL);
+
+                       if (name)
+                               camel_name_value_array_set_named (user_headers, CAMEL_COMPARE_CASE_SENSITIVE, 
name, value ? value : "");
+
+                       g_free (name);
+                       g_free (value);
+               }
+
+               if (camel_name_value_array_get_length (user_headers) == 0) {
+                       camel_name_value_array_free (user_headers);
+                       user_headers = NULL;
+               }
+
+               camel_message_info_take_user_headers (mi, user_headers);
+       }
+
        return TRUE;
 }
 
@@ -307,6 +357,7 @@ message_info_save (const CamelMessageInfo *mi,
        record->to = camel_pstring_strdup (camel_message_info_get_to (mi));
        record->cc = camel_pstring_strdup (camel_message_info_get_cc (mi));
        record->mlist = camel_pstring_strdup (camel_message_info_get_mlist (mi));
+       record->preview = g_strdup (camel_message_info_get_preview (mi));
 
        record->followup_flag = g_strdup (camel_message_info_get_user_tag (mi, "follow-up"));
        record->followup_completed_on = g_strdup (camel_message_info_get_user_tag (mi, "completed-on"));
@@ -373,6 +424,28 @@ message_info_save (const CamelMessageInfo *mi,
        }
        record->usertags = g_string_free (tmp, FALSE);
 
+       tmp = g_string_new (NULL);
+       user_tags = camel_message_info_get_user_headers (mi);
+       if (user_tags) {
+               guint32 ii, count;
+
+               count = camel_name_value_array_get_length (user_tags);
+               g_string_append_printf (tmp, "%" G_GUINT32_FORMAT, count);
+
+               for (ii = 0; ii < count; ii++) {
+                       const gchar *name = NULL, *value = NULL;
+
+                       if (camel_name_value_array_get (user_tags, ii, &name, &value) && name && value) {
+                               g_string_append_printf (tmp, " %" G_GUINT32_FORMAT "-%s %" G_GUINT32_FORMAT 
"-%s",
+                                       (guint32) strlen (name), name,
+                                       (guint32) strlen (value), value);
+                       }
+               }
+       } else {
+               g_string_append_c (tmp, '0');
+       }
+       record->userheaders = g_string_free (tmp, FALSE);
+
        return TRUE;
 }
 
@@ -460,6 +533,14 @@ message_info_set_property (GObject *object,
        case PROP_HEADERS:
                camel_message_info_take_headers (mi, g_value_dup_boxed (value));
                return;
+
+       case PROP_USER_HEADERS:
+               camel_message_info_take_user_headers (mi, g_value_dup_boxed (value));
+               return;
+
+       case PROP_PREVIEW:
+               camel_message_info_set_preview (mi, g_value_get_string (value));
+               return;
        }
 
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -553,6 +634,14 @@ message_info_get_property (GObject *object,
        case PROP_HEADERS:
                g_value_take_boxed (value, camel_message_info_dup_headers (mi));
                return;
+
+       case PROP_USER_HEADERS:
+               g_value_take_boxed (value, camel_message_info_dup_user_headers (mi));
+               return;
+
+       case PROP_PREVIEW:
+               g_value_take_string (value, camel_message_info_dup_preview (mi));
+               return;
        }
 
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -993,6 +1082,44 @@ camel_message_info_class_init (CamelMessageInfoClass *class)
                        G_PARAM_READWRITE |
                        G_PARAM_EXPLICIT_NOTIFY |
                        G_PARAM_STATIC_STRINGS));
+
+       /**
+        * CamelMessageInfo:user-headers
+        *
+        * User-defined headers of the associated message. Can be %NULL.
+        *
+        * Since: 3.42
+        **/
+       g_object_class_install_property (
+               object_class,
+               PROP_USER_HEADERS,
+               g_param_spec_boxed (
+                       "user-headers",
+                       "User Headers",
+                       NULL,
+                       CAMEL_TYPE_NAME_VALUE_ARRAY,
+                       G_PARAM_READWRITE |
+                       G_PARAM_EXPLICIT_NOTIFY |
+                       G_PARAM_STATIC_STRINGS));
+
+       /**
+        * CamelMessageInfo:preview
+        *
+        * Body preview of the associated message. Can be %NULL.
+        *
+        * Since: 3.42
+        **/
+       g_object_class_install_property (
+               object_class,
+               PROP_PREVIEW,
+               g_param_spec_string (
+                       "preview",
+                       "Preview",
+                       NULL,
+                       NULL,
+                       G_PARAM_READWRITE |
+                       G_PARAM_EXPLICIT_NOTIFY |
+                       G_PARAM_STATIC_STRINGS));
 }
 
 static void
@@ -3024,6 +3151,304 @@ camel_message_info_take_headers (CamelMessageInfo *mi,
        return changed;
 }
 
+/**
+ * camel_message_info_get_user_header:
+ * @mi: a #CamelMessageInfo
+ * @name: header name
+ *
+ * Returns: (transfer none) (nullable): Value of the header named @name from
+ *    the user-defined message headers of the associated message, or %NULL,
+ *    when not available.
+ *
+ * Since: 3.42
+ **/
+const gchar *
+camel_message_info_get_user_header (const CamelMessageInfo *mi,
+                                   const gchar *name)
+{
+       CamelMessageInfoClass *klass;
+       const gchar *result;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO (mi), NULL);
+       g_return_val_if_fail (name != NULL, NULL);
+
+       klass = CAMEL_MESSAGE_INFO_GET_CLASS (mi);
+       g_return_val_if_fail (klass != NULL, NULL);
+       g_return_val_if_fail (klass->get_user_header != NULL, NULL);
+
+       camel_message_info_property_lock (mi);
+       result = klass->get_user_header (mi, name);
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+/**
+ * camel_message_info_dup_user_header:
+ * @mi: a #CamelMessageInfo
+ * @name: header name
+ *
+ * Returns: (transfer full) (nullable): Value of the header named @name from
+ *    the user-defined message headers of the associated message, or %NULL,
+ *    when not available. Free the returned string with g_free(), when no longer
+ *    needed.
+ *
+ * Since: 3.42
+ **/
+gchar *
+camel_message_info_dup_user_header (const CamelMessageInfo *mi,
+                                   const gchar *name)
+{
+       gchar *result;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO (mi), NULL);
+
+       camel_message_info_property_lock (mi);
+       result = g_strdup (camel_message_info_get_user_header (mi, name));
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+/**
+ * camel_message_info_set_user_header:
+ * @mi: a #CamelMessageInfo
+ * @name: header name
+ * @value: (nullable): header value, or %NULL
+ *
+ * Set @value for a single user-defined message header of the associated message.
+ * When the @value is %NULL, the header @name is removed from the user-defined
+ * headers.
+ *
+ * If the @mi changed, the 'dirty' flag is set automatically, unless the @mi is
+ * aborting notifications. There is not emitted folder's "changed" signal for this @mi.
+ *
+ * Returns: Whether the value changed.
+ *
+ * Since: 3.42
+ **/
+gboolean
+camel_message_info_set_user_header (CamelMessageInfo *mi,
+                                   const gchar *name,
+                                   const gchar *value)
+{
+       CamelMessageInfoClass *klass;
+       gboolean changed, abort_notifications;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO (mi), FALSE);
+
+       klass = CAMEL_MESSAGE_INFO_GET_CLASS (mi);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->set_user_header != NULL, FALSE);
+
+       camel_message_info_property_lock (mi);
+       changed = klass->set_user_header (mi, name, value);
+       abort_notifications = mi->priv->abort_notifications;
+       camel_message_info_property_unlock (mi);
+
+       if (changed && !abort_notifications) {
+               g_object_notify (G_OBJECT (mi), "user-headers");
+               camel_message_info_set_dirty (mi, TRUE);
+       }
+
+       return changed;
+}
+
+/**
+ * camel_message_info_get_user_headers:
+ * @mi: a #CamelMessageInfo
+ *
+ * Returns: (transfer none) (nullable): All the user-defined message headers
+ *    of the associated message, or %NULL, when none are available.
+ *
+ * Since: 3.42
+ **/
+const CamelNameValueArray *
+camel_message_info_get_user_headers (const CamelMessageInfo *mi)
+{
+       CamelMessageInfoClass *klass;
+       const CamelNameValueArray *result;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO (mi), NULL);
+
+       klass = CAMEL_MESSAGE_INFO_GET_CLASS (mi);
+       g_return_val_if_fail (klass != NULL, NULL);
+       g_return_val_if_fail (klass->get_user_headers != NULL, NULL);
+
+       camel_message_info_property_lock (mi);
+       result = klass->get_user_headers (mi);
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+/**
+ * camel_message_info_dup_user_headers:
+ * @mi: a #CamelMessageInfo
+ *
+ * Returns: (transfer full) (nullable): All the user-defined message headers
+ *    of the associated message, or %NULL, when none are available. Free returned
+ *    array with camel_name_value_array_free() when no longer needed.
+ *
+ * Since: 3.42
+ **/
+CamelNameValueArray *
+camel_message_info_dup_user_headers (const CamelMessageInfo *mi)
+{
+       const CamelNameValueArray *arr;
+       CamelNameValueArray *result;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO (mi), NULL);
+
+       camel_message_info_property_lock (mi);
+       arr = camel_message_info_get_user_headers (mi);
+       if (arr) {
+               result = camel_name_value_array_copy (arr);
+       } else {
+               result = NULL;
+       }
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+/**
+ * camel_message_info_take_user_headers:
+ * @mi: a #CamelMessageInfo
+ * @headers: (transfer full) (nullable): headers to set, as #CamelNameValueArray, or %NULL
+ *
+ * Takes user-defined message headers of the associated message.
+ *
+ * If the @mi changed, the 'dirty' flag is set automatically, unless the @mi is
+ * aborting notifications. There is not emitted folder's "changed" signal for this @mi.
+ *
+ * Note that it's not safe to use the @headers after the call to this function,
+ * because it can be freed due to no change.
+ *
+ * Returns: Whether the value changed.
+ *
+ * Since: 3.42
+ **/
+gboolean
+camel_message_info_take_user_headers (CamelMessageInfo *mi,
+                                     CamelNameValueArray *headers)
+{
+       CamelMessageInfoClass *klass;
+       gboolean changed, abort_notifications;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO (mi), FALSE);
+
+       klass = CAMEL_MESSAGE_INFO_GET_CLASS (mi);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->take_user_headers != NULL, FALSE);
+
+       camel_message_info_property_lock (mi);
+       changed = klass->take_user_headers (mi, headers);
+       abort_notifications = mi->priv->abort_notifications;
+       camel_message_info_property_unlock (mi);
+
+       if (changed && !abort_notifications) {
+               g_object_notify (G_OBJECT (mi), "user-headers");
+               camel_message_info_set_dirty (mi, TRUE);
+       }
+
+       return changed;
+}
+
+/**
+ * camel_message_info_get_preview:
+ * @mi: a #CamelMessageInfo
+ *
+ * Returns: (transfer none) (nullable): Body preview of the associated
+ *    message, or %NULL, when not available.
+ *
+ * Since: 3.42
+ **/
+const gchar *
+camel_message_info_get_preview (const CamelMessageInfo *mi)
+{
+       CamelMessageInfoClass *klass;
+       const gchar *result;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO (mi), NULL);
+
+       klass = CAMEL_MESSAGE_INFO_GET_CLASS (mi);
+       g_return_val_if_fail (klass != NULL, NULL);
+       g_return_val_if_fail (klass->get_preview != NULL, NULL);
+
+       camel_message_info_property_lock (mi);
+       result = klass->get_preview (mi);
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+/**
+ * camel_message_info_dup_preview:
+ * @mi: a #CamelMessageInfo
+ * @name: header name
+ *
+ * Returns: (transfer none) (nullable): Body preview of the associated
+ *    message, or %NULL, when not available. Free the returned string
+ *    with g_free(), when no longer needed.
+ *
+ * Since: 3.42
+ **/
+gchar *
+camel_message_info_dup_preview (const CamelMessageInfo *mi)
+{
+       gchar *result;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO (mi), NULL);
+
+       camel_message_info_property_lock (mi);
+       result = g_strdup (camel_message_info_get_preview (mi));
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+/**
+ * camel_message_info_set_preview:
+ * @mi: a #CamelMessageInfo
+ * @preview: (nullable): message body preview, or %NULL
+ *
+ * Set @preview as the body preview of the associated message. Use %NULL or an empty
+ * string to unset the value.
+ *
+ * If the @mi changed, the 'dirty' flag is set automatically, unless the @mi is
+ * aborting notifications. There is not emitted folder's "changed" signal for this @mi.
+ *
+ * Returns: Whether the value changed.
+ *
+ * Since: 3.42
+ **/
+gboolean
+camel_message_info_set_preview (CamelMessageInfo *mi,
+                               const gchar *preview)
+{
+       CamelMessageInfoClass *klass;
+       gboolean changed, abort_notifications;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO (mi), FALSE);
+
+       klass = CAMEL_MESSAGE_INFO_GET_CLASS (mi);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->set_preview != NULL, FALSE);
+
+       camel_message_info_property_lock (mi);
+       changed = klass->set_preview (mi, preview);
+       abort_notifications = mi->priv->abort_notifications;
+       camel_message_info_property_unlock (mi);
+
+       if (changed && !abort_notifications) {
+               g_object_notify (G_OBJECT (mi), "preview");
+               camel_message_info_set_dirty (mi, TRUE);
+       }
+
+       return changed;
+}
+
 /**
  * camel_message_info_dump:
  * @mi: a #CamelMessageInfo
diff --git a/src/camel/camel-message-info.h b/src/camel/camel-message-info.h
index 44cc2e451..5cee1e9ea 100644
--- a/src/camel/camel-message-info.h
+++ b/src/camel/camel-message-info.h
@@ -26,7 +26,6 @@
 
 #include <camel/camel-named-flags.h>
 #include <camel/camel-name-value-array.h>
-#include <camel/camel-utils.h>
 
 /* Standard GObject macros */
 #define CAMEL_TYPE_MESSAGE_INFO \
@@ -188,9 +187,25 @@ struct _CamelMessageInfoClass {
                                (* get_headers) (const CamelMessageInfo *mi);
        gboolean                (* take_headers)(CamelMessageInfo *mi,
                                                 CamelNameValueArray *headers);
+       const gchar *           (* get_user_header)
+                                               (const CamelMessageInfo *mi,
+                                                const gchar *name);
+       gboolean                (* set_user_header)
+                                               (CamelMessageInfo *mi,
+                                                const gchar *name,
+                                                const gchar *value);
+       const CamelNameValueArray *
+                               (* get_user_headers)
+                                               (const CamelMessageInfo *mi);
+       gboolean                (* take_user_headers)
+                                               (CamelMessageInfo *mi,
+                                                CamelNameValueArray *headers);
+       const gchar *           (* get_preview) (const CamelMessageInfo *mi);
+       gboolean                (* set_preview) (CamelMessageInfo *mi,
+                                                const gchar *preview);
 
        /* Padding for future expansion */
-       gpointer reserved[20];
+       gpointer reserved[14];
 };
 
 GType          camel_message_info_get_type     (void);
@@ -319,6 +334,29 @@ CamelNameValueArray *
                camel_message_info_dup_headers  (const CamelMessageInfo *mi);
 gboolean       camel_message_info_take_headers (CamelMessageInfo *mi,
                                                 CamelNameValueArray *headers);
+const gchar *  camel_message_info_get_user_header
+                                               (const CamelMessageInfo *mi,
+                                                const gchar *name);
+gchar *                camel_message_info_dup_user_header
+                                               (const CamelMessageInfo *mi,
+                                                const gchar *name);
+gboolean       camel_message_info_set_user_header
+                                               (CamelMessageInfo *mi,
+                                                const gchar *name,
+                                                const gchar *value);
+const CamelNameValueArray *
+               camel_message_info_get_user_headers
+                                               (const CamelMessageInfo *mi);
+CamelNameValueArray *
+               camel_message_info_dup_user_headers
+                                               (const CamelMessageInfo *mi);
+gboolean       camel_message_info_take_user_headers
+                                               (CamelMessageInfo *mi,
+                                                CamelNameValueArray *headers);
+const gchar *  camel_message_info_get_preview  (const CamelMessageInfo *mi);
+gchar *                camel_message_info_dup_preview  (const CamelMessageInfo *mi);
+gboolean       camel_message_info_set_preview  (CamelMessageInfo *mi,
+                                                const gchar *preview);
 
 /* Debugging functions */
 void           camel_message_info_dump         (CamelMessageInfo *mi);
diff --git a/src/camel/camel-utils.c b/src/camel/camel-utils.c
index 98693197f..e61160ca4 100644
--- a/src/camel/camel-utils.c
+++ b/src/camel/camel-utils.c
@@ -19,7 +19,9 @@
 
 #include <stdio.h>
 #include <string.h>
+#include <gio/gio.h>
 
+#include "camel-mime-utils.h"
 #include "camel-utils.h"
 
 /**
@@ -284,3 +286,245 @@ camel_utils_weak_ref_free (GWeakRef *weak_ref)
        g_weak_ref_clear (weak_ref);
        g_slice_free (GWeakRef, weak_ref);
 }
+
+G_LOCK_DEFINE_STATIC (mi_user_headers);
+static GSettings *mi_user_headers_settings = NULL;
+static gchar **mi_user_headers = NULL;
+
+static void
+mi_user_headers_settings_changed_cb (GSettings *settings,
+                                    const gchar *key,
+                                    gpointer user_data)
+{
+       G_LOCK (mi_user_headers);
+
+       if (mi_user_headers_settings) {
+               gboolean changed;
+               gchar **strv;
+               guint ii, jj = 0;
+
+               strv = g_settings_get_strv (mi_user_headers_settings, "camel-message-info-user-headers");
+               changed = (!mi_user_headers && strv && strv[0]) || (mi_user_headers && (!strv || !strv[0]));
+
+               if (mi_user_headers && strv && !changed) {
+                       for (ii = 0, jj = 0; strv[ii] && mi_user_headers[jj] && jj < 
CAMEL_UTILS_MAX_USER_HEADERS; ii++) {
+                               const gchar *name = NULL;
+
+                               camel_util_decode_user_header_setting (strv[ii], NULL, &name);
+
+                               if (name && *name) {
+                                       if (g_ascii_strcasecmp (mi_user_headers[jj], name) != 0) {
+                                               changed = TRUE;
+                                               break;
+                                       }
+                                       jj++;
+                               }
+                       }
+
+                       changed = changed || (strv[ii] && jj < CAMEL_UTILS_MAX_USER_HEADERS) || (!strv[ii] && 
jj < CAMEL_UTILS_MAX_USER_HEADERS && mi_user_headers[jj]);
+               }
+
+               if (changed) {
+                       GPtrArray *array;
+
+                       array = g_ptr_array_sized_new (jj + 2);
+
+                       for (ii = 0, jj = 0; strv && strv[ii] && jj < CAMEL_UTILS_MAX_USER_HEADERS; ii++) {
+                               const gchar *name = NULL;
+
+                               camel_util_decode_user_header_setting (strv[ii], NULL, &name);
+
+                               if (name && *name) {
+                                       g_ptr_array_add (array, g_strdup (name));
+                                       jj++;
+                               }
+                       }
+
+                       /* NULL-terminated */
+                       g_ptr_array_add (array, NULL);
+
+                       g_strfreev (mi_user_headers);
+                       mi_user_headers = (gchar **) g_ptr_array_free (array, FALSE);
+               }
+
+               g_strfreev (strv);
+       }
+
+       G_UNLOCK (mi_user_headers);
+}
+
+/* private functions */
+void _camel_utils_initialize (void);
+void _camel_utils_shutdown (void);
+
+/* <private> */
+void
+_camel_utils_initialize (void)
+{
+       G_LOCK (mi_user_headers);
+       mi_user_headers_settings = g_settings_new ("org.gnome.evolution-data-server");
+       g_signal_connect (mi_user_headers_settings, "changed::camel-message-info-user-headers",
+               G_CALLBACK (mi_user_headers_settings_changed_cb), NULL);
+       G_UNLOCK (mi_user_headers);
+       mi_user_headers_settings_changed_cb (NULL, NULL, NULL);
+}
+
+/* <private> */
+void
+_camel_utils_shutdown (void)
+{
+       G_LOCK (mi_user_headers);
+       if (mi_user_headers_settings) {
+               g_clear_object (&mi_user_headers_settings);
+               g_strfreev (mi_user_headers);
+               mi_user_headers = NULL;
+       }
+       G_UNLOCK (mi_user_headers);
+}
+
+/**
+ * camel_util_fill_message_info_user_headers:
+ * @info: a #CamelMessageInfo
+ * @headers: a #CamelNameValueArray with the headers to read from
+ *
+ * Fill @info 's user-headers with the user-defined headers from
+ * the @headers array.
+ *
+ * Returns: Whether the @info's user headers changed
+ *
+ * Since: 3.42
+ **/
+gboolean
+camel_util_fill_message_info_user_headers (CamelMessageInfo *info,
+                                          const CamelNameValueArray *headers)
+{
+       gboolean changed = FALSE;
+
+       g_return_val_if_fail (CAMEL_IS_MESSAGE_INFO (info), FALSE);
+       g_return_val_if_fail (headers != NULL, FALSE);
+
+       camel_message_info_freeze_notifications (info);
+
+       G_LOCK (mi_user_headers);
+
+       if (mi_user_headers) {
+               CamelNameValueArray *array;
+               guint ii;
+
+               array = camel_name_value_array_new ();
+
+               for (ii = 0; mi_user_headers[ii]; ii++) {
+                       const gchar *value;
+                       gchar *str;
+
+                       value = camel_name_value_array_get_named (headers, CAMEL_COMPARE_CASE_INSENSITIVE, 
mi_user_headers[ii]);
+                       if (!value)
+                               continue;
+
+                       while (*value && g_ascii_isspace (*value))
+                               value++;
+
+                       str = camel_header_unfold (value);
+
+                       if (str && *str)
+                               camel_name_value_array_set_named (array, CAMEL_COMPARE_CASE_INSENSITIVE, 
mi_user_headers[ii], str);
+                       else
+                               camel_name_value_array_remove_named (array, CAMEL_COMPARE_CASE_INSENSITIVE, 
mi_user_headers[ii], TRUE);
+
+                       g_free (str);
+               }
+
+               if (camel_name_value_array_get_length (array) == 0) {
+                       camel_name_value_array_free (array);
+                       array = NULL;
+               }
+
+               changed = camel_message_info_take_user_headers (info, array);
+       }
+
+       G_UNLOCK (mi_user_headers);
+
+       camel_message_info_thaw_notifications (info);
+
+       return changed;
+}
+
+/**
+ * camel_util_encode_user_header_setting:
+ * @display_name: (nullable): display name for the header name, or %NULL
+ * @header_name: the header name
+ *
+ * Encode the optional @display_name and the @header_name to a value suitable
+ * for GSettings schema org.gnome.evolution-data-server and key camel-message-info-user-headers.
+ *
+ * Free the returned string with g_free(), when no longer needed.
+ *
+ * Returns: (transfer full): a newly allocated string with encoded @display_name
+ *    and @header_name
+ *
+ * Since: 3.42
+ **/
+gchar *
+camel_util_encode_user_header_setting (const gchar *display_name,
+                                      const gchar *header_name)
+{
+       g_return_val_if_fail (header_name && *header_name, NULL);
+
+       if (display_name && *display_name)
+               return g_strconcat (display_name, "|", header_name, NULL);
+
+       return g_strdup (header_name);
+}
+
+/**
+ * camel_util_decode_user_header_setting:
+ * @setting_value: the value to decode
+ * @out_display_name: (out) (transfer full) (nullable): location for the decoded display name, or %NULL when 
not needed
+ * @out_header_name: (out): the location for the decoded header name
+ *
+ * Decode the values previously encoded by camel_util_encode_user_header_setting().
+ * The @out_header_name points to the @setting_value, thus it's valid as long
+ * as the @setting_value is valid and unchanged.
+ *
+ * The @out_header_name can result in %NULL when the @setting_value
+ * contains invalid data.
+ *
+ * The @out_display_name can result in %NULL when the @setting_value
+ * does not contain the display name. In such case the header name can
+ * be used as the display name.
+ *
+ * Since: 3.42
+ **/
+void
+camel_util_decode_user_header_setting (const gchar *setting_value,
+                                      gchar **out_display_name,
+                                      const gchar **out_header_name)
+{
+       const gchar *ptr;
+
+       g_return_if_fail (setting_value != NULL);
+       g_return_if_fail (out_header_name != NULL);
+
+       *out_header_name = NULL;
+
+       if (out_display_name)
+               *out_display_name = NULL;
+
+       if (!*setting_value)
+               return;
+
+       ptr = strchr (setting_value, '|');
+
+       /* Nothing after the pipe means no header name */
+       if (ptr && !ptr[1])
+               return;
+
+       if (ptr) {
+               if (out_display_name && ptr != setting_value)
+                       *out_display_name = g_strndup (setting_value, ptr - setting_value);
+
+               *out_header_name = ptr + 1;
+       } else {
+               *out_header_name = setting_value;
+       }
+}
diff --git a/src/camel/camel-utils.h b/src/camel/camel-utils.h
index 8fde25095..f26f597b2 100644
--- a/src/camel/camel-utils.h
+++ b/src/camel/camel-utils.h
@@ -25,6 +25,10 @@
 #include <glib-object.h>
 #include <time.h>
 #include <camel/camel-enums.h>
+#include <camel/camel-message-info.h>
+#include <camel/camel-name-value-array.h>
+
+#define CAMEL_UTILS_MAX_USER_HEADERS 3
 
 G_BEGIN_DECLS
 
@@ -44,6 +48,17 @@ time_t               camel_time_value_apply          (time_t src_time,
 GWeakRef *     camel_utils_weak_ref_new        (gpointer object);
 void           camel_utils_weak_ref_free       (GWeakRef *weak_ref);
 
+gboolean       camel_util_fill_message_info_user_headers
+                                               (CamelMessageInfo *info,
+                                                const CamelNameValueArray *headers);
+gchar *                camel_util_encode_user_header_setting
+                                               (const gchar *display_name,
+                                                const gchar *header_name);
+void           camel_util_decode_user_header_setting
+                                               (const gchar *setting_value,
+                                                gchar **out_display_name,
+                                                const gchar **out_header_name);
+
 G_END_DECLS
 
 #endif /* CAMEL_UTILS_H */
diff --git a/src/camel/camel-vee-message-info.c b/src/camel/camel-vee-message-info.c
index 926a37849..10355de22 100644
--- a/src/camel/camel-vee-message-info.c
+++ b/src/camel/camel-vee-message-info.c
@@ -430,6 +430,47 @@ vee_message_info_take_headers (CamelMessageInfo *mi,
        vee_call_from_parent_mi (FALSE, gboolean, camel_message_info_take_headers, (orig_mi, headers), TRUE);
 }
 
+static const gchar *
+vee_message_info_get_user_header (const CamelMessageInfo *mi,
+                                 const gchar *name)
+{
+       vee_call_from_parent_mi (NULL, const gchar *, camel_message_info_get_user_header, (orig_mi, name), 
FALSE);
+}
+
+static gboolean
+vee_message_info_set_user_header (CamelMessageInfo *mi,
+                                 const gchar *name,
+                                 const gchar *value)
+{
+       vee_call_from_parent_mi (FALSE, gboolean, camel_message_info_set_user_header, (orig_mi, name, value), 
TRUE);
+}
+
+static const CamelNameValueArray *
+vee_message_info_get_user_headers (const CamelMessageInfo *mi)
+{
+       vee_call_from_parent_mi (NULL, const CamelNameValueArray *, camel_message_info_get_user_headers, 
(orig_mi), FALSE);
+}
+
+static gboolean
+vee_message_info_take_user_headers (CamelMessageInfo *mi,
+                                   CamelNameValueArray *headers)
+{
+       vee_call_from_parent_mi (FALSE, gboolean, camel_message_info_take_user_headers, (orig_mi, headers), 
TRUE);
+}
+
+static const gchar *
+vee_message_info_get_preview (const CamelMessageInfo *mi)
+{
+       vee_call_from_parent_mi (NULL, const gchar *, camel_message_info_get_preview, (orig_mi), FALSE);
+}
+
+static gboolean
+vee_message_info_set_preview (CamelMessageInfo *mi,
+                             const gchar *preview)
+{
+       vee_call_from_parent_mi (FALSE, gboolean, camel_message_info_set_preview, (orig_mi, preview), TRUE);
+}
+
 #undef vee_call_from_parent_mi
 
 static void
@@ -485,6 +526,12 @@ camel_vee_message_info_class_init (CamelVeeMessageInfoClass *class)
        mi_class->take_references = vee_message_info_take_references;
        mi_class->get_headers = vee_message_info_get_headers;
        mi_class->take_headers = vee_message_info_take_headers;
+       mi_class->get_user_header = vee_message_info_get_user_header;
+       mi_class->set_user_header = vee_message_info_set_user_header;
+       mi_class->get_user_headers = vee_message_info_get_user_headers;
+       mi_class->take_user_headers = vee_message_info_take_user_headers;
+       mi_class->get_preview = vee_message_info_get_preview;
+       mi_class->set_preview = vee_message_info_set_preview;
 
        object_class = G_OBJECT_CLASS (class);
        object_class->dispose = vee_message_info_dispose;
diff --git a/src/camel/camel.c b/src/camel/camel.c
index 1d0421ab3..3fbc17c9a 100644
--- a/src/camel/camel.c
+++ b/src/camel/camel.c
@@ -37,6 +37,10 @@
 #include "camel-provider.h"
 #include "camel-win32.h"
 
+/* private functions from camel-utils.c */
+void _camel_utils_initialize (void);
+void _camel_utils_shutdown (void);
+
 /* To protect NSS initialization and shutdown. This prevents
  * concurrent calls to shutdown () and init () by different threads */
 PRLock *nss_initlock = NULL;
@@ -232,6 +236,8 @@ skip_nss_init:
 
        g_object_unref (certdb);
 
+       _camel_utils_initialize ();
+
        initialised = TRUE;
 
        return 0;
@@ -265,6 +271,8 @@ camel_shutdown (void)
                PR_Unlock (nss_initlock);
        }
 
+       _camel_utils_shutdown ();
+
        initialised = FALSE;
 }
 
diff --git a/src/camel/providers/local/camel-local-summary.c b/src/camel/providers/local/camel-local-summary.c
index 09a897f38..60d0ba1a2 100644
--- a/src/camel/providers/local/camel-local-summary.c
+++ b/src/camel/providers/local/camel-local-summary.c
@@ -477,11 +477,17 @@ local_summary_sync (CamelLocalSummary *cls,
                     GError **error)
 {
        CamelFolderSummary *folder_summary;
+       GError *local_error = NULL;
 
        folder_summary = CAMEL_FOLDER_SUMMARY (cls);
 
-       if (!camel_folder_summary_save (folder_summary, error)) {
-               g_warning ("Could not save summary for local providers");
+       if (!camel_folder_summary_save (folder_summary, &local_error)) {
+               CamelFolder *folder = camel_folder_summary_get_folder (folder_summary);
+               g_warning ("Could not save summary for local providers folder '%s': %s",
+                       folder ? camel_folder_get_full_name (folder) : "???",
+                       local_error ? local_error->message : "Unknown error");
+               if (local_error)
+                       g_propagate_error (error, local_error);
                return -1;
        }
 


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