[balsa/autocrypt] Autocrypt support for Balsa



commit 9dfcd79cc67949fcb85fa1d9813f94efc4388073
Author: Albrecht Dreß <albrecht dress arcor de>
Date:   Thu Dec 27 22:17:06 2018 -0500

    Autocrypt support for Balsa
    
    - configure.ac, meson.build, meson_options.txt,
    libbalsa/meson.build: add new option (Note: meson is not thoroughly
    tested…)
    - libbalsa/Makefile.am: add Autocrypt implementation sources
    - libbalsa/autocrypt.[ch]: implementation of the Autocrypt feature
    - libbalsa/identity.[ch]: add identity Autocrypt configuration
    - libbalsa/libbalsa-gpgme-keys.[ch]: add function for exporting a
    minimal public key for Autocrypt, add function for importing a binary
    gpg key
    - libbalsa/libbalsa-gpgme.[ch]: check and provide capabilities of the
    underlying gpg engine, required for exporting the minimal public key
    - libbalsa/send.c: include Autocrypt header if requested
    - src/balsa-message.c: check for Autocrypt header in a new message
    - src/balsa-mime-widget-crypto.c: offer importing a missing public key
    from the Autocrypt database, fix memory leak in
    create_import_keys_widget()
    - src/main-window.c, ui/main-window.ui: add Autocrypt database viewer to
    menus
    - src/main.c: initialise Autocrypt database on startup
    - src/sendmsg-window.c: implement Autocrypt recommendation on startup,
    refactoring
    
    Signed-off-by: Peter Bloomfield <PeterBloomfield bellsouth net>

 configure.ac                   |  20 +
 libbalsa/Makefile.am           |   4 +
 libbalsa/autocrypt.c           | 912 +++++++++++++++++++++++++++++++++++++++++
 libbalsa/autocrypt.h           | 148 +++++++
 libbalsa/identity.c            |  78 ++++
 libbalsa/identity.h            |   4 +
 libbalsa/libbalsa-gpgme-keys.c | 126 +++++-
 libbalsa/libbalsa-gpgme-keys.h |  30 ++
 libbalsa/libbalsa-gpgme.c      |  56 +++
 libbalsa/libbalsa-gpgme.h      |  10 +
 libbalsa/meson.build           |   2 +
 libbalsa/send.c                |  14 +
 meson.build                    |  15 +
 meson_options.txt              |   5 +
 src/balsa-message.c            |  34 +-
 src/balsa-mime-widget-crypto.c |  41 +-
 src/main-window.c              |  16 +
 src/main.c                     |  11 +
 src/sendmsg-window.c           | 288 ++++++++++---
 ui/main-window.ui              |  10 +
 20 files changed, 1742 insertions(+), 82 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 2db4559f2..00b3dc4a8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -65,6 +65,10 @@ AC_ARG_WITH([gpgme],
                   [build with gpgme/GnuPG support (default=no, path to gpgme-config optional)]),
                  [ gpgmecfg=$withval ], [ gpgmecfg=no ])
 
+AC_ARG_ENABLE([autocrypt],
+   AC_HELP_STRING([--enable-autocrypt],
+                  [build with Autocrypt support (see https://autocrypt.org/), default=no, requires gpgme and 
sqlite3)]),
+                  [autocrypt=$enableval], [autocrypt=no])
 
 AC_ARG_WITH(canberra,
    AC_HELP_STRING([--with-canberra],
@@ -360,6 +364,21 @@ if test x"$gpgmecfg" != xno ; then
 fi
 AM_CONDITIONAL([BUILD_WITH_GPGME], [test $gpgmecfg = "yes"])
 
+# Autocrypt support
+AC_MSG_CHECKING(whether to build with Autocrypt support)
+if test x"$gpgmecfg" != xno ; then
+       if test x"$autocrypt" != xno ; then
+               AC_MSG_RESULT([yes])
+               PKG_CHECK_MODULES(AUTOCRYPT, [sqlite3])
+               AC_DEFINE(ENABLE_AUTOCRYPT,1,[If defined, enable Autocrypt support])
+               BALSA_CFLAGS="$BALSA_CFLAGS $AUTOCRYPT_CFLAGS"
+               BALSA_LIBS="$BALSA_LIBS $AUTOCRYPT_LIBS"
+       else
+               AC_MSG_RESULT([no])
+       fi
+else
+       AC_MSG_RESULT([skipped, gpgme is disabled])
+fi
 
 # OpenLDAP configuration.
 #
@@ -716,6 +735,7 @@ echo "               HTML widget: $use_html_widget"
 echo "                 Use GNOME: $with_gnome"
 echo "              Use Canberra: $with_canberra"
 echo "                 Use GPGME: $gpgmecfg"
+echo "             Use Autocrypt: $autocrypt"
 echo "                  Use LDAP: $with_ldap"
 echo "                   Use GSS: $with_gss"
 echo "                Use SQLite: $with_sqlite"
diff --git a/libbalsa/Makefile.am b/libbalsa/Makefile.am
index d3420fedf..edf589a22 100644
--- a/libbalsa/Makefile.am
+++ b/libbalsa/Makefile.am
@@ -5,6 +5,8 @@ noinst_LIBRARIES = libbalsa.a
 
 if BUILD_WITH_GPGME
 libbalsa_gpgme_extra =                 \
+       autocrypt.h                             \
+       autocrypt.c                             \
        libbalsa-gpgme.h                \
        libbalsa-gpgme.c                \
        libbalsa-gpgme-cb.h             \
@@ -25,6 +27,8 @@ libbalsa_gpgme_extra_dist =
 else
 libbalsa_gpgme_extra =
 libbalsa_gpgme_extra_dist =    \
+       autocrypt.h                             \
+       autocrypt.c                             \
        libbalsa-gpgme.h                \
        libbalsa-gpgme.c                \
        libbalsa-gpgme-cb.h             \
diff --git a/libbalsa/autocrypt.c b/libbalsa/autocrypt.c
new file mode 100644
index 000000000..7c4be82c3
--- /dev/null
+++ b/libbalsa/autocrypt.c
@@ -0,0 +1,912 @@
+/* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
+/* Balsa E-Mail Client
+ *
+ * Copyright (C) 1997-2018 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 <http://www.gnu.org/licenses/>.
+ */
+
+#if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H
+#      include "config.h"
+#endif                          /* HAVE_CONFIG_H */
+
+#if defined ENABLE_AUTOCRYPT
+
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include <sqlite3.h>
+#include "libbalsa-gpgme.h"
+#include "libbalsa-gpgme-keys.h"
+#include "libbalsa-gpgme-widgets.h"
+#include "identity.h"
+#include "autocrypt.h"
+
+
+#ifdef G_LOG_DOMAIN
+#  undef G_LOG_DOMAIN
+#endif
+#define G_LOG_DOMAIN "autocrypt"
+
+
+/* the autocrypt SQL table contains the following data:
+ * addr: email address
+ * last_seen: time_t value when the last message from addr has been seen
+ * ac_timestamp: time_t value when the last message with a valid Autocrypt header from addr has been seen
+ * pubkey: raw (binary) public key data
+ * fingerprint: the fingerprint of pubkey, stored to avoid frequently importing pubkey into a temporary 
context
+ * expires: the expiry time of pubkey (0 for never), stored to avoid frequently importing pubkey into a 
temporary context
+ * prefer_encrypt: TRUE (1) if the prefer-encrypt=mutual attribute was given in the latest Autocrypt header
+ *
+ * notes: SQLite stores BOOLEAN as INTEGER
+ *        We do not support key gossip, so storing everything in a flat table is sufficient */
+#define DB_SCHEMA                                                              \
+       "PRAGMA auto_vacuum = 1;"                                       \
+       "CREATE TABLE autocrypt("                                       \
+               "addr TEXT PRIMARY KEY NOT NULL, "              \
+               "last_seen BIGINT, "                                    \
+               "ac_timestamp BIGINT, "                                 \
+               "pubkey BLOB NOT NULL, "                                \
+               "fingerprint TEXT NOT NULL, "                   \
+               "expires BIGINT NOT NULL, "                             \
+               "prefer_encrypt BOOLEAN DEFAULT 0);"
+
+
+#define NUM_QUERIES                                                            6U
+
+
+struct _AutocryptData {
+       gchar *addr;
+       time_t last_seen;
+       time_t ac_timestamp;
+       GBytes *keydata;
+       gchar *fingerprint;
+       time_t expires;
+       gboolean prefer_encrypt;
+};
+
+typedef struct _AutocryptData AutocryptData;
+
+
+enum {
+       AC_ADDRESS_COLUMN = 0,
+       AC_LAST_SEEN_COLUMN,
+       AC_TIMESTAMP_COLUMN,
+       AC_PREFER_ENCRYPT_COLUMN,
+       AC_KEY_PTR_COLUMN,
+       AC_DB_VIEW_COLUMNS
+};
+
+
+static void autocrypt_close(void);
+static AutocryptData *parse_autocrypt_header(const gchar *value);
+static gboolean eval_autocrypt_attr(const gchar   *attr,
+                                                                       const gchar   *value,
+                                                                       gboolean      *seen,
+                                                                       AutocryptData *target);
+static void add_or_update_user_info(const AutocryptData  *user_info,
+                                                                       time_t                date_header,
+                                                                       gboolean              update,
+                                                                       GError              **error);
+static void update_last_seen(const gchar  *addr,
+                                                        time_t        date_header,
+                                                        GError      **error);
+static AutocryptData *autocrypt_user_info(const gchar  *mailbox,
+                                                                                 GError      **error)
+       G_GNUC_WARN_UNUSED_RESULT;
+static void autocrypt_free(AutocryptData *data);
+static AutocryptRecommend autocrypt_check_ia_list(gpgme_ctx_t           gpgme_ctx,
+                                                                                                 
InternetAddressList  *recipients,
+                                                                                                 time_t      
          ref_time,
+                                                                                                 GList       
        **missing_keys,
+                                                                                                 GError      
        **error);
+static gboolean key_button_event_press_cb(GtkWidget      *widget,
+                                                                                 GdkEventButton *event,
+                                                                                 gpointer        data);
+
+
+static sqlite3 *autocrypt_db = NULL;
+static sqlite3_stmt *query[NUM_QUERIES] = { NULL, NULL, NULL, NULL, NULL, NULL };
+G_LOCK_DEFINE_STATIC(db_mutex);
+
+
+/* documentation: see header file */
+gboolean
+autocrypt_init(GError **error)
+{
+       static const gchar * const prepare_statements[NUM_QUERIES] = {
+               "SELECT * FROM autocrypt WHERE LOWER(addr) = ?",
+               "INSERT INTO autocrypt VALUES (?1, ?2, ?2, ?3, ?4, ?5, ?6)",
+               "UPDATE autocrypt SET last_seen = MAX(?2, last_seen), ac_timestamp = ?2, pubkey = ?3, 
fingerprint = ?4,"
+               " expires = ?5, prefer_encrypt = ?6 WHERE addr = ?1",
+               "UPDATE autocrypt SET last_seen = ?2 WHERE addr = ?1 AND last_seen < ?2 AND ac_timestamp < 
?2",
+               "SELECT pubkey FROM autocrypt WHERE fingerprint LIKE ?",
+               "SELECT addr, last_seen, ac_timestamp, prefer_encrypt, pubkey FROM autocrypt ORDER BY 
LOWER(addr) ASC"
+       };
+       gboolean result;
+
+       G_LOCK(db_mutex);
+       if (autocrypt_db == NULL) {
+               gchar *db_path;
+               gboolean require_init;
+               int sqlite_res;
+
+               db_path = g_build_filename(g_get_home_dir(), ".balsa", "autocrypt.db", NULL);
+               require_init = (g_access(db_path, R_OK + W_OK) != 0);
+               sqlite_res = sqlite3_open(db_path, &autocrypt_db);
+               if (sqlite_res == SQLITE_OK) {
+                       guint n;
+
+                       /* write the schema if the database is new */
+                       if (require_init) {
+                               sqlite_res = sqlite3_exec(autocrypt_db, DB_SCHEMA, NULL, NULL, NULL);
+                       }
+
+                       /* always vacuum the database */
+                       if (sqlite_res == SQLITE_OK) {
+                               sqlite_res = sqlite3_exec(autocrypt_db, "VACUUM", NULL, NULL, NULL);
+                       }
+
+                       /* prepare statements */
+                       for (n = 0U; (sqlite_res == SQLITE_OK) && (n < NUM_QUERIES); n++) {
+                               sqlite_res = sqlite3_prepare_v2(autocrypt_db, prepare_statements[n], -1, 
&query[n], NULL);
+                       }
+               }
+               G_UNLOCK(db_mutex);
+
+               /* error checks... */
+               if (sqlite_res != SQLITE_OK) {
+                       g_set_error(error, AUTOCRYPT_ERROR_QUARK, sqlite_res, _("cannot initialise Autocrypt 
database “%s”: %s"), db_path,
+                               sqlite3_errmsg(autocrypt_db));
+                       autocrypt_close();
+                       result = FALSE;
+               } else {
+                       atexit(autocrypt_close);
+                       result = TRUE;
+               }
+               g_free(db_path);
+       } else {
+               G_UNLOCK(db_mutex);
+               result = TRUE;
+       }
+
+       return result;
+}
+
+
+/* documentation: see header file */
+void
+autocrypt_from_message(LibBalsaMessage  *message,
+                                          GError          **error)
+{
+       const gchar *from_addr;
+       GMimeHeaderList *headers;
+       GMimeHeaderIter iter;
+       AutocryptData *autocrypt = NULL;
+
+       g_return_if_fail(LIBBALSA_IS_MESSAGE(message) && (message->headers != NULL) && 
(message->headers->from != NULL) &&
+               (message->headers->content_type != NULL) && GMIME_IS_OBJECT(message->mime_msg) && 
(autocrypt_db != NULL));
+
+       // FIXME - we should ignore spam - how can we detect it?
+
+       /* check for content types which shall be ignored */
+       if (autocrypt_ignore(message->headers->content_type)) {
+               g_debug("ignore %s/%s", g_mime_content_type_get_media_type(message->headers->content_type),
+                       g_mime_content_type_get_media_subtype(message->headers->content_type));
+               return;
+       }
+
+       /* check for exactly one From: mailbox address - others shall be ignored */
+       if ((internet_address_list_length(message->headers->from) != 1) ||
+               !INTERNET_ADDRESS_IS_MAILBOX(internet_address_list_get_address(message->headers->from, 0))) {
+               g_debug("require exactly one From: address, ignored");
+               return;
+       }
+
+       /* ignore messages without a Date: header or with a date in the future */
+       if ((message->headers->date == 0) || (message->headers->date > time(NULL))) {
+               g_debug("no Date: header or value in the future, ignored");
+               return;
+       }
+
+       /* get the From: address (is a mailbox, checked above) */
+       from_addr =
+               
internet_address_mailbox_get_addr(INTERNET_ADDRESS_MAILBOX(internet_address_list_get_address(message->headers->from,
 0)));
+       g_debug("message from '%s', date %ld", from_addr, message->headers->date);
+
+       /* scan for Autocrypt headers */
+       headers = g_mime_object_get_header_list(GMIME_OBJECT(message->mime_msg));
+    if (g_mime_header_list_get_iter(headers, &iter)) {
+       do {
+               if ((g_ascii_strcasecmp(g_mime_header_iter_get_name(&iter), "Autocrypt") == 0) &&
+                       (g_mime_header_iter_get_value(&iter) != NULL)) {
+                       AutocryptData *new_data;
+
+                       new_data = parse_autocrypt_header(g_mime_header_iter_get_value(&iter));
+                       if (new_data != NULL) {
+                               if (autocrypt == NULL) {
+                                       autocrypt = new_data;
+                               } else {
+                                       g_info("more than one valid Autocrypt header, ignore message");
+                                       autocrypt_free(autocrypt);
+                                       autocrypt_free(new_data);
+                                       return;
+                               }
+                       } else {
+                               /* ignore message with broken Autocrypt header */
+                               autocrypt_free(autocrypt);
+                               return;
+                       }
+               }
+       } while (g_mime_header_iter_next(&iter));
+    }
+
+    /* check if addr matches From: - ignore otherwise */
+    if (autocrypt != NULL) {
+       if (g_ascii_strcasecmp(autocrypt->addr, from_addr) != 0) {
+               g_info("Autocrypt header for '%s' in message from '%s', ignore message", autocrypt->addr, 
from_addr);
+               autocrypt_free(autocrypt);
+               return;
+       }
+    }
+
+    /* update the database */
+    G_LOCK(db_mutex);
+    if (autocrypt != NULL) {
+       AutocryptData *db_info;
+
+       db_info = autocrypt_user_info(autocrypt->addr, error);
+       if (db_info != NULL) {
+               if (message->headers->date > db_info->ac_timestamp) {
+                       add_or_update_user_info(autocrypt, message->headers->date, TRUE, error);
+               } else {
+                       g_info("message timestamp %ld not newer than autocrypt db timestamp %ld, ignore 
message",
+                               (long) message->headers->date, (long) db_info->ac_timestamp);
+               }
+               autocrypt_free(db_info);
+       } else {
+               add_or_update_user_info(autocrypt, message->headers->date, FALSE, error);
+       }
+       autocrypt_free(autocrypt);
+    } else {
+       update_last_seen(from_addr, message->headers->date, error);
+    }
+    G_UNLOCK(db_mutex);
+}
+
+
+/* documentation: see header file */
+gchar *
+autocrypt_header(const LibBalsaIdentity *identity, GError **error)
+{
+       const gchar *mailbox;
+       gchar *use_fpr = NULL;
+       gchar *result = NULL;
+       gchar *keydata;
+
+       g_return_val_if_fail((identity != NULL) && (identity->autocrypt_mode != AUTOCRYPT_DISABLE), NULL);
+       mailbox = internet_address_mailbox_get_addr(INTERNET_ADDRESS_MAILBOX(identity->ia));
+
+       /* no key fingerprint has been passed - try to find the fingerprint of a secret key matching the 
passed mailbox */
+       if ((identity->force_gpg_key_id == NULL) || (identity->force_gpg_key_id[0] == '\0')) {
+               gpgme_ctx_t ctx;
+
+               ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_OpenPGP, NULL, NULL, error);
+               if (ctx != NULL) {
+                       GList *keys = NULL;
+
+                       libbalsa_gpgme_list_keys(ctx, &keys, NULL, mailbox, TRUE, FALSE, FALSE, error);
+                       if (keys != NULL) {
+                               gpgme_key_t key = (gpgme_key_t) keys->data;
+
+                               if ((key != NULL) && (key->subkeys != NULL)) {
+                                       use_fpr = g_strdup(key->subkeys->fpr);
+                               }
+                               g_list_free_full(keys, (GDestroyNotify) gpgme_key_release);
+                       }
+                       gpgme_release(ctx);
+               }
+               g_debug("found fingerprint %s for '%s'", use_fpr, mailbox);
+       } else {
+               use_fpr = g_strdup(identity->force_gpg_key_id);
+       }
+
+       keydata = libbalsa_gpgme_export_autocrypt_key(use_fpr, mailbox, error);
+       g_free(use_fpr);
+       if (keydata != NULL) {
+               GString *buffer;
+               gssize ins_fws;
+
+               buffer = g_string_new(NULL);
+               g_string_append_printf(buffer, "addr=%s;", mailbox);
+               if (identity->autocrypt_mode == AUTOCRYPT_PREFER_ENCRYPT) {
+                       g_string_append(buffer, "prefer-encrypt=mutual;");
+               }
+               g_string_append_printf(buffer, "keydata=%s", keydata);
+               for (ins_fws = 66U; ins_fws < (gssize) buffer->len; ins_fws += 78) {
+                       g_string_insert(buffer, ins_fws, "\n\t");
+               }
+               result = g_string_free(buffer, FALSE);
+       }
+
+       return result;
+}
+
+
+/* documentation: see header file */
+gboolean
+autocrypt_ignore(GMimeContentType *content_type)
+{
+       g_return_val_if_fail(GMIME_IS_CONTENT_TYPE(content_type), TRUE);
+
+       return g_mime_content_type_is_type(content_type, "multipart", "report") ||
+               g_mime_content_type_is_type(content_type, "text", "calendar");
+}
+
+
+/* documentation: see header file */
+GBytes *
+autocrypt_get_key(const gchar *fingerprint, GError **error)
+{
+       gchar *param;
+       int sqlite_res;
+       GBytes *result = NULL;
+
+       g_return_val_if_fail(fingerprint != NULL, NULL);
+
+       /* prepend SQL "LIKE" wildcard */
+       param = g_strconcat("%", fingerprint, NULL);
+
+       sqlite_res = sqlite3_bind_text(query[4], 1, param, -1, SQLITE_STATIC);
+       if (sqlite_res == SQLITE_OK) {
+               sqlite_res = sqlite3_step(query[4]);
+               if (sqlite_res == SQLITE_ROW) {
+                       result = g_bytes_new(sqlite3_column_blob(query[4], 0), sqlite3_column_bytes(query[4], 
0));
+                       sqlite_res = sqlite3_step(query[4]);
+               }
+
+               if (sqlite_res != SQLITE_DONE) {
+                       g_set_error(error, AUTOCRYPT_ERROR_QUARK, sqlite_res, _("error reading Autocrypt data 
for “%s”: %s"), fingerprint,
+                               sqlite3_errmsg(autocrypt_db));
+                       if (result != NULL) {
+                               g_bytes_unref(result);
+                               result = NULL;
+                       }
+               }
+       } else {
+               g_set_error(error, AUTOCRYPT_ERROR_QUARK, sqlite_res, _("error reading Autocrypt data for 
“%s”: %s"), fingerprint,
+                       sqlite3_errmsg(autocrypt_db));
+       }
+       sqlite3_reset(query[4]);
+       g_free(param);
+
+       return result;
+}
+
+
+/* documentation: see header file */
+AutocryptRecommend
+autocrypt_recommendation(InternetAddressList *recipients, GList **missing_keys, GError **error)
+{
+       AutocryptRecommend result;
+       gpgme_ctx_t gpgme_ctx;
+
+       g_return_val_if_fail(IS_INTERNET_ADDRESS_LIST(recipients), AUTOCRYPT_ENCR_DISABLE);
+
+    /* create the gpgme context and set the protocol */
+    gpgme_ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_OpenPGP, NULL, NULL, error);
+    if (gpgme_ctx == NULL) {
+       result = AUTOCRYPT_ENCR_ERROR;
+    } else {
+       result = autocrypt_check_ia_list(gpgme_ctx, recipients, time(NULL), missing_keys, error);
+       gpgme_release(gpgme_ctx);
+
+       if ((result == AUTOCRYPT_ENCR_ERROR) && (missing_keys != NULL) && (*missing_keys != NULL)) {
+               g_list_free_full(*missing_keys, (GDestroyNotify) g_bytes_unref);
+               *missing_keys = NULL;
+       }
+    }
+
+       return result;
+}
+
+
+/* documentation: see header file */
+void
+autocrypt_db_dialog_run(const gchar *date_string, GtkWindow *parent)
+{
+       GtkWidget *dialog;
+       GtkWidget *vbox;
+    GtkWidget *label;
+    GtkWidget *scrolled_window;
+    GtkWidget *tree_view;
+    GtkListStore *model;
+    GtkTreeSelection *selection;
+       GtkCellRenderer *renderer;
+       GtkTreeViewColumn *column;
+    GList *keys = NULL;
+       int sqlite_res;
+
+       dialog = gtk_dialog_new_with_buttons(_("Autocrypt database"), parent,
+               GTK_DIALOG_DESTROY_WITH_PARENT | libbalsa_dialog_flags(), _("_Close"), GTK_RESPONSE_CLOSE, 
NULL);
+
+    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);
+    label = gtk_label_new(_("Double-click key to show details"));
+    gtk_widget_set_halign(label, GTK_ALIGN_START);
+    gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, TRUE, 0);
+
+    scrolled_window = gtk_scrolled_window_new(NULL, NULL);
+    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(AC_DB_VIEW_COLUMNS, G_TYPE_STRING,      /* address */
+       G_TYPE_STRING,                                                                                        
  /* formatted last seen timestamp */
+               G_TYPE_STRING,                                                                                
          /* formatted last Autocrypt message timestamp */
+               G_TYPE_BOOLEAN,                                                                               
          /* user prefers encrypted messages */
+               G_TYPE_POINTER);                                                                              
          /* key */
+
+    tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(model));
+    g_signal_connect(tree_view, "button_press_event", G_CALLBACK(key_button_event_press_cb), dialog);
+    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 the keys */
+    sqlite_res = sqlite3_step(query[5]);
+    while (sqlite_res == SQLITE_ROW) {
+       gchar *last_seen_buf;
+       gchar *last_ac_buf;
+       GBytes *key;
+        GtkTreeIter iter;
+
+       last_seen_buf = libbalsa_date_to_utf8(sqlite3_column_int64(query[5], 1), date_string);
+       last_ac_buf = libbalsa_date_to_utf8(sqlite3_column_int64(query[5], 2), date_string);
+       key = g_bytes_new(sqlite3_column_blob(query[5], 4), sqlite3_column_bytes(query[5], 4));
+       keys = g_list_prepend(keys, key);
+
+               gtk_list_store_append(model, &iter);
+               gtk_list_store_set(model, &iter,
+                       AC_ADDRESS_COLUMN, sqlite3_column_text(query[5], 0),
+                       AC_LAST_SEEN_COLUMN, last_seen_buf,
+                       AC_TIMESTAMP_COLUMN, last_ac_buf,
+                       AC_PREFER_ENCRYPT_COLUMN, sqlite3_column_int(query[5], 3),
+                       AC_KEY_PTR_COLUMN, key,
+                       -1);
+               g_free(last_seen_buf);
+               g_free(last_ac_buf);
+
+       sqlite_res = sqlite3_step(query[5]);
+    }
+    sqlite3_reset(query[5]);
+
+    /* set up the tree view */
+    g_object_unref(G_OBJECT(model));
+
+       renderer = gtk_cell_renderer_text_new();
+       column = gtk_tree_view_column_new_with_attributes(_("Mailbox"), renderer, "text", 0, NULL);
+       gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
+       gtk_tree_view_column_set_resizable(column, TRUE);
+
+       renderer = gtk_cell_renderer_text_new();
+       column = gtk_tree_view_column_new_with_attributes(_("Last seen"), renderer, "text", 1, NULL);
+       gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
+       gtk_tree_view_column_set_resizable(column, TRUE);
+
+       renderer = gtk_cell_renderer_text_new();
+       column = gtk_tree_view_column_new_with_attributes(_("Last Autocrypt message"), renderer, "text", 2, 
NULL);
+       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();
+       column = gtk_tree_view_column_new_with_attributes(_("Prefer encryption"), renderer, "active", 3, 
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);
+
+       (void) gtk_dialog_run(GTK_DIALOG(dialog));
+       gtk_widget_destroy(dialog);
+       g_list_free_full(keys, (GDestroyNotify) g_bytes_unref);
+}
+
+
+static AutocryptRecommend
+autocrypt_check_ia_list(gpgme_ctx_t           gpgme_ctx,
+                                               InternetAddressList  *recipients,
+                                               time_t                ref_time,
+                                               GList               **missing_keys,
+                                               GError              **error)
+{
+       AutocryptRecommend result = AUTOCRYPT_ENCR_AVAIL_MUTUAL;
+       gint i;
+
+       for (i = 0; (result > AUTOCRYPT_ENCR_DISABLE) && (i < internet_address_list_length(recipients)); i++) 
{
+       InternetAddress *ia = internet_address_list_get_address(recipients, i);
+
+       /* check all entries in the list, handle groups recursively */
+       if (INTERNET_ADDRESS_IS_GROUP(ia)) {
+               result = autocrypt_check_ia_list(gpgme_ctx, INTERNET_ADDRESS_GROUP(ia)->members, ref_time, 
missing_keys, error);
+       } else {
+               AutocryptData *autocrypt_user;
+               const gchar *mailbox;
+
+               mailbox = INTERNET_ADDRESS_MAILBOX(ia)->addr;
+               autocrypt_user = autocrypt_user_info(mailbox, NULL);
+               if (autocrypt_user == NULL) {
+                       GList *keys = NULL;
+
+                       /* check if we have a public key, keep the state if we found one, disable if not */
+                       if (libbalsa_gpgme_list_keys(gpgme_ctx, &keys, NULL, mailbox, FALSE, FALSE, FALSE, 
error)) {
+                               if (keys != NULL) {
+                                       g_list_free_full(keys, (GDestroyNotify) gpgme_key_unref);
+                                       g_debug("'%s': found in public key ring, overall status %d", mailbox, 
result);
+                               } else {
+                                       result = AUTOCRYPT_ENCR_DISABLE;
+                                       g_debug("'%s': not in Autocrypt db or public key ring, overall status 
%d", mailbox, result);
+                               }
+                       } else {
+                               result = AUTOCRYPT_ENCR_ERROR;
+                       }
+               } else {
+                       /* we found Autocrypt data for this user */
+                       if ((autocrypt_user->expires > 0) && (autocrypt_user->expires <= ref_time)) {
+                               result = AUTOCRYPT_ENCR_DISABLE;                /* key has expired */
+                       } else if (autocrypt_user->ac_timestamp < (autocrypt_user->last_seen - (35 * 24 * 60 
* 60))) {
+                               result = MIN(result, AUTOCRYPT_ENCR_DISCOURAGE);        /* Autocrypt 
timestamp > 35 days older than last seen */
+                       } else if (autocrypt_user->prefer_encrypt) {
+                               result = MIN(result, AUTOCRYPT_ENCR_AVAIL_MUTUAL);      /* user requested 
"prefer-encrypt=mutual" */
+                       } else {
+                               result = MIN(result, AUTOCRYPT_ENCR_AVAIL);                     /* user did 
not request "prefer-encrypt=mutual" */
+                       }
+
+                       /* check if the Autocrypt key is already in the key ring, add it to the list of 
missing ones otherwise */
+                       if (missing_keys != NULL) {
+                       GList *keys = NULL;
+
+                       if (libbalsa_gpgme_list_keys(gpgme_ctx, &keys, NULL, autocrypt_user->fingerprint, 
FALSE, FALSE, FALSE, error)) {
+                               if (keys != NULL) {
+                                       g_list_free_full(keys, (GDestroyNotify) gpgme_key_unref);
+                               } else {
+                                       *missing_keys = g_list_prepend(*missing_keys, 
g_bytes_ref(autocrypt_user->keydata));
+                               }
+                       } else {
+                               result = AUTOCRYPT_ENCR_ERROR;
+                       }
+                       }
+                       autocrypt_free(autocrypt_user);
+                       g_debug("'%s': found in Autocrypt db, overall status %d", mailbox, result);
+               }
+       }
+       }
+
+       return result;
+}
+
+
+static void
+autocrypt_free(AutocryptData *data)
+{
+       if (data != NULL) {
+               g_free(data->addr);
+               g_free(data->fingerprint);
+               if (data->keydata) {
+                       g_bytes_unref(data->keydata);
+               }
+               g_free(data);
+       }
+}
+
+
+static void
+autocrypt_close(void)
+{
+       guint n;
+
+       g_debug("closing Autocrypt database");
+       G_LOCK(db_mutex);
+       for (n = 0U; n < NUM_QUERIES; n++) {
+               sqlite3_finalize(query[n]);
+               query[n] = NULL;
+       }
+       sqlite3_close(autocrypt_db);
+       autocrypt_db = NULL;
+       G_UNLOCK(db_mutex);
+}
+
+
+static AutocryptData *
+parse_autocrypt_header(const gchar *value)
+{
+       gchar **attributes;
+       gboolean attr_seen[3] = { FALSE, FALSE, FALSE };
+       AutocryptData *new_data;
+       gint n;
+       gboolean broken;
+
+       new_data = g_new0(AutocryptData, 1U);
+       attributes = g_strsplit(value, ";", -1);
+       if (attributes == NULL) {
+               g_info("empty Autocrypt header");
+               broken = TRUE;
+       } else {
+               broken = FALSE;
+       }
+
+       for (n = 0; !broken && (attributes[n] != NULL); n++) {
+               gchar **items;
+
+               items = g_strsplit(attributes[n], "=", 2);
+               if ((items == NULL) || (items[0] == NULL) || (items[1] == NULL)) {
+                       g_info("bad Autocrypt header attribute");
+                       broken = TRUE;
+               } else {
+                       broken = !eval_autocrypt_attr(g_strstrip(items[0]), g_strstrip(items[1]), attr_seen, 
new_data);
+               }
+               g_strfreev(items);
+       }
+       g_strfreev(attributes);
+
+       if (!broken) {
+               if (!attr_seen[0] || !attr_seen[2]) {
+                       g_info("missing mandatory Autocrypt header attribute");
+                       broken = TRUE;
+               }
+       }
+
+       /* try to import the key into a temporary context */
+       if (!broken) {
+               gboolean success = FALSE;
+               gpgme_ctx_t ctx;
+
+               ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_OpenPGP, NULL, NULL, NULL);
+               if (ctx != NULL) {
+                       gchar *temp_dir = NULL;
+
+                       if (!libbalsa_mktempdir(&temp_dir)) {
+                               g_warning("Failed to create a temporary folder");
+                       } else {
+                               GList *keys = NULL;
+                               GError *error = NULL;
+
+                               success = libbalsa_gpgme_ctx_set_home(ctx, temp_dir, &error) &&
+                                       libbalsa_gpgme_import_bin_key(ctx, new_data->keydata, NULL, &error) &&
+                                       libbalsa_gpgme_list_keys(ctx, &keys, NULL, NULL, FALSE, FALSE, FALSE, 
&error);
+                               if (success && (keys != NULL) && (keys->next == NULL)) {
+                                       gpgme_key_t key = (gpgme_key_t) keys->data;
+
+                                       if ((key != NULL) && (key->subkeys != NULL)) {
+                                               new_data->fingerprint = g_strdup(key->subkeys->fpr);
+                                               new_data->expires = key->subkeys->expires;
+                                       }
+                               } else {
+                                       g_warning("Failed to import key data: %s", (error != NULL) ? 
error->message : "unknown");
+                               }
+                               g_clear_error(&error);
+
+                               g_list_free_full(keys, (GDestroyNotify) gpgme_key_release);
+                               libbalsa_delete_directory_contents(temp_dir);
+                               g_rmdir(temp_dir);
+                       }
+
+                       gpgme_release(ctx);
+               }
+       }
+
+       /* check if a broken header has been detected, or if importing the key failed */
+       if (broken || (new_data->fingerprint == NULL)) {
+               autocrypt_free(new_data);
+               new_data = NULL;
+       } else {
+               g_debug("valid Autocrypt header for '%s', prefer encrypt %d, key fingerprint %s", 
new_data->addr, new_data->prefer_encrypt,
+                       new_data->fingerprint);
+       }
+
+       return new_data;
+}
+
+
+static gboolean
+eval_autocrypt_attr(const gchar *attr, const gchar *value, gboolean *seen, AutocryptData *target)
+{
+       gboolean result = FALSE;
+
+       if (seen[2]) {
+               g_info("broken Autocrypt header, extra attribute after keydata");
+       } else if (strcmp(attr, "addr") == 0) {
+               if (seen[0]) {
+                       g_info("duplicated Autocrypt header attribute 'addr'");
+               } else {
+                       seen[0] = TRUE;
+                       /* note: not exactly the canonicalisation as required by the Autocrypt standard, but 
should work in all practical use
+                        * cases... */
+                       target->addr = g_ascii_strdown(value, -1);
+                       result = TRUE;
+               }
+       } else if (strcmp(attr, "prefer-encrypt") == 0) {
+               if (seen[1]) {
+                       g_info("duplicated Autocrypt header attribute 'addr'");
+               } else {
+                       seen[1] = TRUE;
+                       if (strcmp(value, "mutual") == 0) {
+                               target->prefer_encrypt = TRUE;
+                               result = TRUE;
+                       } else {
+                               g_info("bad value '%s' for Autocrypt header attribute 'prefer-encrypt'", 
value);
+                       }
+               }
+       } else if (strcmp(attr, "keydata") == 0) {
+               guchar *data;
+               gsize len;
+
+               seen[2] = TRUE;
+               data = g_base64_decode(value, &len);
+               if (data == NULL) {
+                       g_info("invalid keydata in Autocrypt header");
+               } else {
+                       target->keydata = g_bytes_new_take(data, len);
+                       result = TRUE;
+               }
+       } else if (attr[0] == '_') {
+               g_debug("ignoring non-critical Autocrypt header attribute '%s'", attr);
+               result = TRUE;          /* note that this is no error */
+       } else {
+               g_info("unexpected Autocrypt header attribute '%s'", attr);
+       }
+
+       return result;
+}
+
+
+static AutocryptData *
+autocrypt_user_info(const gchar *mailbox, GError **error)
+{
+       int sqlite_res;
+       AutocryptData *user_info = NULL;
+
+       g_return_val_if_fail((mailbox != NULL) && (autocrypt_db != NULL), NULL);
+
+       sqlite_res = sqlite3_bind_text(query[0], 1, mailbox, -1, SQLITE_STATIC);
+       if (sqlite_res == SQLITE_OK) {
+               sqlite_res = sqlite3_step(query[0]);
+               if (sqlite_res == SQLITE_ROW) {
+                       user_info = g_new0(AutocryptData, 1U);
+                       user_info->addr = g_strdup((const gchar *) sqlite3_column_text(query[0], 0));
+                       user_info->last_seen = sqlite3_column_int64(query[0], 1);
+                       user_info->ac_timestamp = sqlite3_column_int64(query[0], 2);
+                       user_info->keydata = g_bytes_new(sqlite3_column_blob(query[0], 3), 
sqlite3_column_bytes(query[0], 3));
+                       user_info->fingerprint = g_strdup((const gchar *) sqlite3_column_text(query[0], 4));
+                       user_info->expires = sqlite3_column_int64(query[0], 5);
+                       user_info->prefer_encrypt = (sqlite3_column_int(query[0], 6) != 0);
+                       sqlite_res = sqlite3_step(query[0]);
+               }
+
+               if (sqlite_res != SQLITE_DONE) {
+                       g_set_error(error, AUTOCRYPT_ERROR_QUARK, sqlite_res, _("error reading Autocrypt data 
for “%s”: %s"), mailbox,
+                               sqlite3_errmsg(autocrypt_db));
+                       autocrypt_free(user_info);
+                       user_info = NULL;
+               }
+       } else {
+               g_set_error(error, AUTOCRYPT_ERROR_QUARK, sqlite_res, _("error reading Autocrypt data for 
“%s”: %s"), mailbox,
+                       sqlite3_errmsg(autocrypt_db));
+       }
+       sqlite3_reset(query[0]);
+
+       return user_info;
+}
+
+
+static void
+add_or_update_user_info(const AutocryptData *user_info, time_t date_header, gboolean update, GError **error)
+{
+       guint query_idx;
+       gconstpointer keyvalue;
+       gsize keysize;
+
+       query_idx = update ? 2 : 1;
+       keyvalue = g_bytes_get_data(user_info->keydata, &keysize);
+       if ((sqlite3_bind_text(query[query_idx], 1, user_info->addr, -1, SQLITE_STATIC) != SQLITE_OK) ||
+               (sqlite3_bind_int64(query[query_idx], 2, date_header) != SQLITE_OK) ||
+               (sqlite3_bind_blob(query[query_idx], 3, keyvalue, keysize, SQLITE_STATIC) != SQLITE_OK) ||
+               (sqlite3_bind_text(query[query_idx], 4, user_info->fingerprint, -1, SQLITE_STATIC) != 
SQLITE_OK) ||
+               (sqlite3_bind_int64(query[query_idx], 5, user_info->expires) != SQLITE_OK) ||
+               (sqlite3_bind_int(query[query_idx], 6, user_info->prefer_encrypt) != SQLITE_OK) ||
+               (sqlite3_step(query[query_idx]) != SQLITE_DONE)) {
+               g_set_error(error, AUTOCRYPT_ERROR_QUARK, -1, _("%s user “%s” failed: %s"), update ? 
_("update") : _("insert"),
+                       user_info->addr, sqlite3_errmsg(autocrypt_db));
+       } else {
+               g_debug("%s user '%s'", update ? "updated" : "inserted", user_info->addr);
+       }
+       sqlite3_reset(query[query_idx]);
+}
+
+
+static void
+update_last_seen(const gchar *addr, time_t date_header, GError **error)
+{
+       if ((sqlite3_bind_text(query[3], 1, addr, -1, SQLITE_STATIC) != SQLITE_OK) ||
+               (sqlite3_bind_int64(query[3], 2, date_header) != SQLITE_OK) ||
+               (sqlite3_step(query[3]) != SQLITE_DONE)) {
+               g_set_error(error, AUTOCRYPT_ERROR_QUARK, -1, _("update user “%s” failed: %s"), addr, 
sqlite3_errmsg(autocrypt_db));
+       } else {
+               g_debug("updated last_seen for '%s'", addr);
+       }
+       sqlite3_reset(query[3]);
+}
+
+
+static gboolean
+key_button_event_press_cb(GtkWidget      *widget,
+                                                 GdkEventButton *event,
+                                                 gpointer        data)
+{
+    GtkTreeView *tree_view = GTK_TREE_VIEW(widget);
+    GtkTreeSelection *selection = gtk_tree_view_get_selection(tree_view);
+    GtkTreePath *path;
+    GtkTreeIter iter;
+    GtkTreeModel *model;
+
+    g_return_val_if_fail(event != NULL, FALSE);
+    if ((event->type != GDK_2BUTTON_PRESS) || event->window != gtk_tree_view_get_bin_window(tree_view)) {
+        return FALSE;
+    }
+
+    if (gtk_tree_view_get_path_at_pos(tree_view, event->x, event->y, &path, NULL, NULL, NULL)) {
+        if (!gtk_tree_selection_path_is_selected(selection, path)) {
+            gtk_tree_view_set_cursor(tree_view, path, NULL, FALSE);
+            gtk_tree_view_scroll_to_cell(tree_view, path, NULL, FALSE, 0, 0);
+        }
+        gtk_tree_path_free(path);
+    }
+
+    /* note: silently ignore all errors below... */
+    if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
+               gpgme_ctx_t ctx;
+
+               ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_OpenPGP, NULL, NULL, NULL);
+               if (ctx != NULL) {
+                       gchar *temp_dir = NULL;
+
+                       if (libbalsa_mktempdir(&temp_dir)) {
+                               GBytes *key;
+                               GList *keys = NULL;
+                               gboolean success;
+
+                               gtk_tree_model_get(model, &iter, AC_KEY_PTR_COLUMN, &key, -1);
+                               success = libbalsa_gpgme_ctx_set_home(ctx, temp_dir, NULL) &&
+                                       libbalsa_gpgme_import_bin_key(ctx, key, NULL, NULL) &&
+                                       libbalsa_gpgme_list_keys(ctx, &keys, NULL, NULL, FALSE, FALSE, TRUE, 
NULL);
+                               if (success && (keys != NULL)) {
+                               GtkWidget *dialog;
+
+                               dialog = libbalsa_key_dialog(GTK_WINDOW(data), GTK_BUTTONS_CLOSE, 
(gpgme_key_t) keys->data, GPG_SUBKEY_CAP_ALL,
+                                       NULL, NULL);
+                               (void) gtk_dialog_run(GTK_DIALOG(dialog));
+                               gtk_widget_destroy(dialog);
+                               g_list_free_full(keys, (GDestroyNotify) gpgme_key_release);
+                               }
+                               libbalsa_delete_directory_contents(temp_dir);
+                               g_rmdir(temp_dir);
+                       }
+
+                       gpgme_release(ctx);
+               }
+    }
+
+    return TRUE;
+}
+
+#endif  /* ENABLE_AUTOCRYPT */
diff --git a/libbalsa/autocrypt.h b/libbalsa/autocrypt.h
new file mode 100644
index 000000000..11f9dd21f
--- /dev/null
+++ b/libbalsa/autocrypt.h
@@ -0,0 +1,148 @@
+/* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
+/* Balsa E-Mail Client
+ *
+ * Copyright (C) 1997-2018 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 <http://www.gnu.org/licenses/>.
+ *
+ * Note: see https://autocrypt.org/level1.html for the Autocrypt specs
+ */
+
+#ifndef LIBBALSA_AUTOCRYPT_H_
+#define LIBBALSA_AUTOCRYPT_H_
+
+#ifndef BALSA_VERSION
+# error "Include config.h before this file."
+#endif
+
+#ifdef ENABLE_AUTOCRYPT
+
+#include "libbalsa.h"
+
+
+#define AUTOCRYPT_ERROR_QUARK                  (g_quark_from_static_string("autocrypt"))
+
+
+enum _AutocryptMode {
+       AUTOCRYPT_DISABLE,                                      /**< Disable Autocrypt support. */
+       AUTOCRYPT_NOPREFERENCE,                         /**< Enable Autocrypt support, but do not request 
"prefer-encrypt=mutual". */
+       AUTOCRYPT_PREFER_ENCRYPT                        /**< Enable Autocrypt support and request 
"prefer-encrypt=mutual". */
+};
+
+typedef enum _AutocryptMode AutocryptMode;
+
+
+enum _AutocryptRecommend {
+       AUTOCRYPT_ENCR_ERROR,                           /**< An error occurred when calculating the 
recommendation for encryption. */
+       AUTOCRYPT_ENCR_DISABLE,                         /**< Encryption is not possible due to a missing 
usable key. */
+       AUTOCRYPT_ENCR_DISCOURAGE,                      /**< Encryption is possible but discouraged by 
Autocrypt. */
+       AUTOCRYPT_ENCR_AVAIL,                           /**< Encryption is possible, but at least one 
recipient does not request
+                                                                                * "prefer-encrypt=mutual". */
+       AUTOCRYPT_ENCR_AVAIL_MUTUAL                     /**< Encryption is possible, and all recipients 
request "prefer-encrypt=mutual". */
+};
+
+typedef enum _AutocryptRecommend AutocryptRecommend;
+
+
+/** \brief Initialise the Autocrypt subsystem
+ *
+ * \param error filled with error information on error, may be NULL
+ * \return TRUE on success, FALSE if any error coourred
+ *
+ * Open and if necessary initialise the Autocrypt SQLite3 database <tt>autocrypt.db</tt> in the user's Balsa 
folder.
+ */
+gboolean autocrypt_init(GError **error);
+
+/** \brief Update the Autocrypt database from a received message
+ *
+ * \param message Balsa message
+ * \param error filled with error information on error, may be NULL
+ *
+ * Scan the headers of the passed message and update the Autocrypt database according to the Autocrypt 
specifications, section 2.3
+ * <em>Updating Autocrypt Peer State</em>.
+ *
+ * \todo Spam messages should be ignored, but how can we detect them?
+ */
+void autocrypt_from_message(LibBalsaMessage  *message,
+                                                       GError          **error);
+
+/** \brief Create an Autocrypt header value
+ *
+ * \param identity the identity for which the Autocrypt header shall be created
+ * \param error filled with error information on error, may be NULL
+ * \return a newly allocated string containing the properly folded Autocrypt header
+ *
+ * Create a an Autocrypt header value according to the Autocrypt specifications.  Note that the included key 
data may or may not be
+ * minimalistic, depending upon the export capabilities of the gpg backend being used.  It is an error to 
call this function if the
+ * Autocrypt mode of the passed identity is AUTOCRYPT_DISABLE.
+ */
+gchar *autocrypt_header(const LibBalsaIdentity  *identity,
+                                               GError                     **error)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+/** \brief Check if a media type shall be ignored for Autocrypt
+ *
+ * \param content_type message content type
+ * \return TRUE if the media type shall be ignored
+ *
+ * The standard requests that multipart/report shall be ignored.  This function also blacklists 
text/calendar which is not required
+ * by the standard (see https://lists.mayfirst.org/pipermail/autocrypt/2018-November/000441.html for a 
discussion).
+ */
+gboolean autocrypt_ignore(GMimeContentType *content_type);
+
+/** \brief Get a key from the Autocrypt database
+ *
+ * \param fingerprint key fingerprint
+ * \param error filled with error information on error, may be NULL
+ * \return a new object containing the raw key data on success, or NULL if the key is not in the Autocrypt 
database
+ *
+ * If available, returns the key whose fingerprint ends in the passed value from the Autocrypt database.
+ */
+GBytes *autocrypt_get_key(const gchar  *fingerprint,
+                                                 GError      **error)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+/** \brief Get the recommendation for encryption
+ *
+ * \param recipients message recipients
+ * \param missing_keys filled with a list of GBytes *, containing all Autocrypt keys missing in the key 
ring, may be NULL
+ * \param error filled with error information on error, may be NULL
+ * \return the result of the recommendation check
+ *
+ * Calculate the Autocrypt recommendation for encryption, according to sect. 2.4 of the standard.  Note that 
all recipients which
+ * are not listed in the Autocrypt database, but for which a valid key exists in the GnuPG key ring, are 
treated as if they
+ * requested "prefer-encrypt=mutual".
+ *
+ * \sa https://autocrypt.org/level1.html#provide-a-recommendation-for-message-encryption
+ */
+AutocryptRecommend autocrypt_recommendation(InternetAddressList  *recipients,
+                                                                                       GList                 
          **missing_keys,
+                                                                                       GError              
**error);
+
+/** \brief Show the Autocrypt database
+ *
+ * \param date_string time stamp formatting template
+ * \param parent parent window
+ *
+ * Display a modal dialog with the contents of the Autocrypt database.
+ */
+void autocrypt_db_dialog_run(const gchar *date_string,
+                                                        GtkWindow   *parent);
+
+
+#endif /* ENABLE_AUTOCRYPT */
+
+
+#endif /* LIBBALSA_AUTOCRYPT_H_ */
diff --git a/libbalsa/identity.c b/libbalsa/identity.c
index 6b2d73ecd..215dbc1a1 100644
--- a/libbalsa/identity.c
+++ b/libbalsa/identity.c
@@ -117,6 +117,9 @@ libbalsa_identity_init(LibBalsaIdentity* ident)
     ident->crypt_protocol = LIBBALSA_PROTECT_OPENPGP;
     ident->force_gpg_key_id = NULL;
     ident->force_smime_key_id = NULL;
+#ifdef ENABLE_AUTOCRYPT
+    ident->autocrypt_mode = AUTOCRYPT_DISABLE;
+#endif
     ident->request_mdn = FALSE;
     ident->request_dsn = FALSE;
     /*
@@ -635,6 +638,16 @@ static void ident_dialog_add_gpg_menu(GtkWidget * grid, gint row,
                                       GtkDialog * dialog,
                                       const gchar * label_name,
                                       const gchar * menu_key);
+#ifdef ENABLE_AUTOCRYPT
+static void ident_dialog_add_autocrypt_menu(GtkWidget   *grid,
+                                                                                       gint         row,
+                                                                                       GtkDialog   *dialog,
+                                                                                       const gchar 
*label_name,
+                                                                                       const gchar 
*menu_key);
+static void display_frame_set_autocrypt_mode(GObject       *dialog,
+                                                                                        const gchar   *key,
+                                                                                        AutocryptMode 
*value);
+#endif
 static void add_show_menu(const char *label, gpointer data,
                           GtkWidget * menu);
 static void ident_dialog_free_values(GPtrArray * values);
@@ -1067,6 +1080,11 @@ setup_ident_frame(GtkDialog * dialog, gboolean createp, gpointer tree,
                                          _("use certificate with this id for signing S/MIME messages\n"
                                                "(leave empty for automatic selection)"),
                                                                  "identity-keyid-sm");
+#ifdef ENABLE_AUTOCRYPT
+    ident_dialog_add_autocrypt_menu(grid, row++, dialog,
+                                                                       _("Autocrypt mode"),
+                                                                       "identity-autocrypt");
+#endif
 #ifndef HAVE_GPGME
     gtk_widget_set_sensitive(grid, FALSE);
 #endif
@@ -1546,6 +1564,9 @@ ident_dialog_update(GObject * dlg)
     id->force_gpg_key_id = g_strstrip(ident_dialog_get_text(dlg, "identity-keyid"));
     g_free(id->force_smime_key_id);
     id->force_smime_key_id = g_strstrip(ident_dialog_get_text(dlg, "identity-keyid-sm"));
+#ifdef ENABLE_AUTOCRYPT
+    id->autocrypt_mode  = GPOINTER_TO_INT(ident_dialog_get_value(dlg, "identity-autocrypt"));
+#endif
 
     return TRUE;
 }
@@ -1931,6 +1952,9 @@ display_frame_update(GObject * dialog, LibBalsaIdentity* ident)
                           &ident->crypt_protocol);
     display_frame_set_field(dialog, "identity-keyid", ident->force_gpg_key_id);
     display_frame_set_field(dialog, "identity-keyid-sm", ident->force_smime_key_id);
+#ifdef ENABLE_AUTOCRYPT
+    display_frame_set_autocrypt_mode(dialog, "identity-autocrypt", &ident->autocrypt_mode);
+#endif
 }
 
 
@@ -2033,6 +2057,9 @@ libbalsa_identity_new_config(const gchar* name)
     ident->crypt_protocol = libbalsa_conf_get_int("CryptProtocol=16");
     ident->force_gpg_key_id = libbalsa_conf_get_string("ForceKeyID");
     ident->force_smime_key_id = libbalsa_conf_get_string("ForceKeyIDSMime");
+#ifdef ENABLE_AUTOCRYPT
+    ident->autocrypt_mode = libbalsa_conf_get_int("Autocrypt=0");
+#endif
 
     return ident;
 }
@@ -2079,6 +2106,9 @@ libbalsa_identity_save(LibBalsaIdentity* ident, const gchar* group)
     libbalsa_conf_set_int("CryptProtocol", ident->crypt_protocol);
     libbalsa_conf_set_string("ForceKeyID", ident->force_gpg_key_id);
     libbalsa_conf_set_string("ForceKeyIDSMime", ident->force_smime_key_id);
+#ifdef ENABLE_AUTOCRYPT
+    libbalsa_conf_set_int("Autocrypt", ident->autocrypt_mode);
+#endif
 
     libbalsa_conf_pop_group();
 }
@@ -2131,6 +2161,54 @@ display_frame_set_gpg_mode(GObject * dialog, const gchar* key, gint * value)
         }
 }
 
+#ifdef ENABLE_AUTOCRYPT
+static void
+display_frame_set_autocrypt_mode(GObject * dialog, const gchar* key, AutocryptMode * value)
+{
+    GtkComboBox *opt_menu = g_object_get_data(G_OBJECT(dialog), key);
+
+    switch (*value)
+        {
+        case AUTOCRYPT_NOPREFERENCE:
+           gtk_combo_box_set_active(opt_menu, 1);
+            break;
+        case AUTOCRYPT_PREFER_ENCRYPT:
+           gtk_combo_box_set_active(opt_menu, 2);
+            break;
+        default:
+           gtk_combo_box_set_active(opt_menu, 0);
+            *value = AUTOCRYPT_DISABLE;
+        }
+}
+
+static void
+ident_dialog_add_autocrypt_menu(GtkWidget * grid, gint row, GtkDialog * dialog,
+                                   const gchar * label_name, const gchar * menu_key)
+{
+    GtkWidget *label;
+    GtkWidget *opt_menu;
+    GPtrArray *values;
+
+    label = gtk_label_new_with_mnemonic(label_name);
+    gtk_widget_set_halign(label, GTK_ALIGN_START);
+    gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
+
+    opt_menu = gtk_combo_box_text_new();
+    values = g_ptr_array_sized_new(3);
+    g_object_set_data_full(G_OBJECT(opt_menu), "identity-value", values,
+                           (GDestroyNotify) ident_dialog_free_values);
+    gtk_grid_attach(GTK_GRID(grid), opt_menu, 1, row, 1, 1);
+    g_object_set_data(G_OBJECT(dialog), menu_key, opt_menu);
+
+    add_show_menu(_("disabled"),
+                  GINT_TO_POINTER(AUTOCRYPT_DISABLE), opt_menu);
+    add_show_menu(_("enabled, no preference"),
+                  GINT_TO_POINTER(AUTOCRYPT_NOPREFERENCE), opt_menu);
+    add_show_menu(_("enabled, prefer encryption"),
+                  GINT_TO_POINTER(AUTOCRYPT_PREFER_ENCRYPT), opt_menu);
+}
+#endif
+
 /*
  * Add an option menu to the given dialog with a label next to it
  * explaining the contents.  A reference to the entry is stored as
diff --git a/libbalsa/identity.h b/libbalsa/identity.h
index d8768d27c..d3f3d93e9 100644
--- a/libbalsa/identity.h
+++ b/libbalsa/identity.h
@@ -28,6 +28,7 @@
 #include <gtk/gtk.h>
 #include <gmime/internet-address.h>
 
+#include "autocrypt.h"
 #include "libbalsa.h"
 
 
@@ -85,6 +86,9 @@ G_BEGIN_DECLS
        gint crypt_protocol;
         gchar *force_gpg_key_id;
         gchar *force_smime_key_id;
+#ifdef ENABLE_AUTOCRYPT
+        AutocryptMode autocrypt_mode;
+#endif
        LibBalsaSmtpServer *smtp_server;
     };
 
diff --git a/libbalsa/libbalsa-gpgme-keys.c b/libbalsa/libbalsa-gpgme-keys.c
index 57f5fbff9..085c71bb7 100644
--- a/libbalsa/libbalsa-gpgme-keys.c
+++ b/libbalsa/libbalsa-gpgme-keys.c
@@ -41,6 +41,11 @@ typedef struct _keyserver_op_t {
 } keyserver_op_t;
 
 
+static gboolean import_key_real(gpgme_ctx_t     ctx,
+                                                               gconstpointer   key_buf,
+                                                               gsize               buf_len,
+                                                               gchar         **import_info,
+                                                               GError        **error);
 static inline gboolean check_key(const gpgme_key_t key,
                                                                 gboolean          secret,
                                                                 gboolean          on_keyserver,
@@ -265,26 +270,134 @@ libbalsa_gpgme_export_key(gpgme_ctx_t   ctx,
 }
 
 
+/* documentation: see header file */
+gchar *
+libbalsa_gpgme_export_autocrypt_key(const gchar *fingerprint, const gchar *mailbox, GError **error)
+{
+       gchar *export_args[10] = { "", "--export", "--export-options", "export-minimal,no-export-attributes",
+               NULL, NULL, NULL, NULL, NULL, NULL };
+       gpgme_ctx_t ctx;
+       gchar *result = NULL;
+
+       g_return_val_if_fail((fingerprint != NULL) && (mailbox != NULL), NULL);
+
+       ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_SPAWN, NULL, NULL, error);
+       if (ctx != NULL) {
+               gpgme_data_t keybuf;
+               gpgme_error_t gpgme_err;
+
+               gpgme_err = gpgme_data_new(&keybuf);
+               if (gpgme_err != GPG_ERR_NO_ERROR) {
+                       libbalsa_gpgme_set_error(error, gpgme_err, _("cannot create data buffer"));
+               } else {
+                       const gpg_capabilities *gpg_capas;
+                       guint param_idx;
+
+                       gpg_capas = libbalsa_gpgme_gpg_capabilities();
+                       g_assert(gpg_capas != NULL);            /* paranoid - we're called for OpenPGP, so 
the info /should/ be there... */
+                       param_idx = 4U;
+                       if (gpg_capas->export_filter_subkey) {
+                               export_args[param_idx++] = g_strdup("--export-filter");
+                               export_args[param_idx++] = g_strdup("drop-subkey=usage!~e && usage!~s");
+
+                       }
+                       if (gpg_capas->export_filter_uid) {
+                               export_args[param_idx++] = g_strdup("--export-filter");
+                               export_args[param_idx++] = g_strdup_printf("keep-uid=mbox=%s", mailbox);
+                       }
+                       export_args[param_idx] = g_strdup(fingerprint);
+
+                       /* run... */
+                       gpgme_err = gpgme_op_spawn(ctx, gpg_capas->gpg_path, (const gchar **) export_args, 
NULL, keybuf, NULL, 0);
+                       for (param_idx = 4U; export_args[param_idx] != NULL; param_idx++) {
+                               g_free(export_args[param_idx]);
+                       }
+                       if (gpgme_err != GPG_ERR_NO_ERROR) {
+                               libbalsa_gpgme_set_error(error, gpgme_err, _("cannot export minimal key for 
“%s”"), mailbox);
+                               gpgme_data_release(keybuf);
+                       } else {
+                               size_t keysize;
+                               void *keydata;
+
+                               keydata = gpgme_data_release_and_get_mem(keybuf, &keysize);
+                               if ((keydata == NULL) || (keysize == 0U)) {
+                                       g_set_error(error, GPGME_ERROR_QUARK, -1, _("cannot export minimal 
key for “%s”"), mailbox);
+                               } else {
+                                       result = g_base64_encode(keydata, keysize);
+                               }
+                               gpgme_free(keydata);
+                       }
+               }
+
+               gpgme_release(ctx);
+       }
+
+       return result;
+}
+
+
 /* documentation: see header file */
 gboolean
 libbalsa_gpgme_import_ascii_key(gpgme_ctx_t   ctx,
                                                                const gchar  *key_buf,
                                                                gchar       **import_info,
                                                                GError      **error)
+{
+       g_return_val_if_fail((ctx != NULL) && (key_buf != NULL), FALSE);
+
+       return import_key_real(ctx, key_buf, strlen(key_buf), import_info, error);
+}
+
+
+/* documentation: see header file */
+gboolean
+libbalsa_gpgme_import_bin_key(gpgme_ctx_t   ctx,
+                                                         GBytes           *key_buf,
+                                                         gchar       **import_info,
+                                                         GError      **error)
+{
+       gconstpointer key_data;
+       gsize key_len;
+
+       g_return_val_if_fail((ctx != NULL) && (key_buf != NULL), FALSE);
+       key_data = g_bytes_get_data(key_buf, &key_len);
+       return import_key_real(ctx, key_data, key_len, import_info, error);
+}
+
+
+/* ---- local functions ------------------------------------------------------ */
+
+/** \brief Import a binary or ASCII-armoured key
+ *
+ * \param ctx GpgME context
+ * \param key_buf ASCII or binary GnuPG key buffer
+ * \param buf_len number of bytes in the GnuPG key buffer
+ * \param import_info filled with human-readable information about the import, may be NULL
+ * \param error filled with error information on error, may be NULL
+ * \return TRUE on success, or FALSE on error
+ *
+ * Import an ASCII-armoured or binary GnuPG key into the key ring.
+ */
+static gboolean
+import_key_real(gpgme_ctx_t     ctx,
+                               gconstpointer   key_buf,
+                               gsize               buf_len,
+                               gchar         **import_info,
+                               GError        **error)
 {
        gpgme_data_t buffer;
        gpgme_error_t gpgme_err;
        gboolean result = FALSE;
 
-       g_return_val_if_fail((ctx != NULL) && (key_buf != NULL), FALSE);
+       g_return_val_if_fail(buf_len > 0, FALSE);
 
-       gpgme_err = gpgme_data_new_from_mem(&buffer, key_buf, strlen(key_buf), 1);
+       gpgme_err = gpgme_data_new_from_mem(&buffer, key_buf, buf_len, 0);
        if (gpgme_err != GPG_ERR_NO_ERROR) {
                libbalsa_gpgme_set_error(error, gpgme_err, _("cannot create data buffer"));
        } else {
                gpgme_err = gpgme_op_import(ctx, buffer);
                if (gpgme_err != GPG_ERR_NO_ERROR) {
-                       libbalsa_gpgme_set_error(error, gpgme_err, _("importing ASCII-armored key data 
failed"));
+                       libbalsa_gpgme_set_error(error, gpgme_err, _("importing key data failed"));
                } else {
                        result = TRUE;
                        if (import_info != NULL) {
@@ -298,8 +411,6 @@ libbalsa_gpgme_import_ascii_key(gpgme_ctx_t   ctx,
 }
 
 
-/* ---- local functions ------------------------------------------------------ */
-
 /** \brief Check if a key is usable
  *
  * \param key GpgME key
@@ -363,11 +474,10 @@ gpgme_keyserver_run(gpointer user_data)
                                GTK_DIALOG_DESTROY_WITH_PARENT | libbalsa_dialog_flags(), GTK_MESSAGE_INFO, 
GTK_BUTTONS_CLOSE,
                                _("Cannot find a key with fingerprint %s on the key server."), 
keyserver_op->fingerprint);
                } else if (keys->next != NULL) {
+                       /* more than one key found */
                        dialog = gtk_message_dialog_new(keyserver_op->parent,
                                GTK_DIALOG_DESTROY_WITH_PARENT | libbalsa_dialog_flags(), 
GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE,
-                               ngettext("Found %u key with fingerprint %s on the key server. Please check 
and import the proper key manually.",
-                                        "Found %u keys with fingerprint %s on the key server. Please check 
and import the proper key manually.",
-                                        g_list_length(keys)),
+                               _("Found %u keys with fingerprint %s on the key server. Please check and 
import the proper key manually."),
                                g_list_length(keys), keyserver_op->fingerprint);
                } else {
                        dialog = gpgme_keyserver_do_import(keyserver_op, (gpgme_key_t) keys->data);
diff --git a/libbalsa/libbalsa-gpgme-keys.h b/libbalsa/libbalsa-gpgme-keys.h
index 0468f833e..9141132a7 100644
--- a/libbalsa/libbalsa-gpgme-keys.h
+++ b/libbalsa/libbalsa-gpgme-keys.h
@@ -115,6 +115,22 @@ gchar *libbalsa_gpgme_export_key(gpgme_ctx_t   ctx,
                                                                 GError      **error)
        G_GNUC_WARN_UNUSED_RESULT;
 
+/** \brief Export a key for Autocrypt
+ *
+ * \param fingerprint key fingerprint, may be NULL
+ * \param mailbox key uid
+ * \param error filled with error information on error
+ * \return a newly allocated string containing the BASE64-encoded key on success, NULL on error
+ *
+ * Export the minimal key for using it in a Autocrypt: header.  If specified, the key is selected by the 
passed fingerprint,
+ * otherwise the first key matching the passed mailbox is used.  Depending on the gpg backend version, all 
other uid's and all
+ * subkeys which are not required are stripped.
+ */
+gchar *libbalsa_gpgme_export_autocrypt_key(const gchar  *fingerprint,
+                                                                                  const gchar  *mailbox,
+                                                                                  GError      **error)
+       G_GNUC_WARN_UNUSED_RESULT;
+
 /** \brief Import an ASCII-armoured key
  *
  * \param ctx GpgME context
@@ -130,6 +146,20 @@ gboolean libbalsa_gpgme_import_ascii_key(gpgme_ctx_t   ctx,
                                                                                 gchar       **import_info,
                                                                                 GError      **error);
 
+/** \brief Import a binary key
+ *
+ * \param ctx GpgME context
+ * \param key_buf binary GnuPG key buffer
+ * \param import_info filled with human-readable information about the import, may be NULL
+ * \param error filled with error information on error, may be NULL
+ * \return TRUE on success, or FALSE on error
+ *
+ * Import a binary GnuPG key into the key ring.
+ */
+gboolean libbalsa_gpgme_import_bin_key(gpgme_ctx_t   ctx,
+                                                                          GBytes       *key_buf,
+                                                                          gchar       **import_info,
+                                                                          GError      **error);
 
 G_END_DECLS
 
diff --git a/libbalsa/libbalsa-gpgme.c b/libbalsa/libbalsa-gpgme.c
index 98d797696..41dfe560f 100644
--- a/libbalsa/libbalsa-gpgme.c
+++ b/libbalsa/libbalsa-gpgme.c
@@ -78,9 +78,13 @@ static gchar *utf8_valid_str(const char *gpgme_str)
 static const gchar *get_utf8_locale(int category);
 #endif
 
+static void gpg_check_capas(const gchar *gpg_path,
+                                                       const gchar *version);
+
 
 static gboolean has_proto_openpgp = FALSE;
 static gboolean has_proto_cms = FALSE;
+static gpg_capabilities gpg_capas;
 
 static gpgme_passphrase_cb_t gpgme_passphrase_cb = NULL;
 static lbgpgme_select_key_cb select_key_cb = NULL;
@@ -126,6 +130,9 @@ libbalsa_gpgme_init(gpgme_passphrase_cb_t get_passphrase,
                g_debug("protocol %s: engine %s (home %s, version %s)",
                      gpgme_get_protocol_name(e->protocol),
                      e->file_name, e->home_dir, e->version);
+               if (e->protocol == GPGME_PROTOCOL_OpenPGP) {
+                       gpg_check_capas(e->file_name, e->version);
+               }
            e = e->next;
        }
     }
@@ -184,6 +191,24 @@ libbalsa_gpgme_check_crypto_engine(gpgme_protocol_t protocol)
 }
 
 
+/** \brief Get capabilities of the gpg engine
+ *
+ * \return a pointer to the capabilities of the GnuPG engine, or NULL if it is not supported
+ *
+ * If an engine for the OpenPGP protocol is available, return a structure containing the path of the 
executable, and information if
+ * some \em export-filter options are available.  This information is needed to export a minimal Autocrypt 
key, but unfortunately
+ * cannot be determined from the engine version.
+ *
+ * \sa libbalsa_gpgme_export_autocrypt_key(), gpg_check_capas()
+ * \todo Actually, gpgme should provide a minimalistic key export.
+ */
+const gpg_capabilities *
+libbalsa_gpgme_gpg_capabilities(void)
+{
+       return has_proto_openpgp ? &gpg_capas : NULL;
+}
+
+
 /** \brief Create a new GpgME context for a protocol
  *
  * \param protocol requested protocol
@@ -1132,3 +1157,34 @@ get_utf8_locale(int category)
     return localebuf;
 }
 #endif
+
+/*
+ * Note: this function is a hack to detect if the gpg engine in use support the '--export-filter' options 
'keep-uid=...' and
+ * 'drop-subkey=...' (since 2.2.9) needed for exporting a minimal Autocrypt key.
+ */
+static void
+gpg_check_capas(const gchar *gpg_path, const gchar *version)
+{
+       gchar *gpg_args[] = { (gchar *) gpg_path, "--export", "--export-filter", "keep-uid=primary=1", 
"0000000000000000", NULL };
+       gint exit_status;
+       guint major;
+       guint minor;
+       guint release;
+
+       gpg_capas.gpg_path = g_strdup(gpg_path);
+
+       /* check for the "--export-filter keep-uid=..." option */
+       if (g_spawn_sync(NULL, gpg_args, NULL, G_SPAWN_STDOUT_TO_DEV_NULL + G_SPAWN_STDERR_TO_DEV_NULL, NULL, 
NULL, NULL, NULL,
+                                        &exit_status, NULL)) {
+               gpg_capas.export_filter_uid = g_spawn_check_exit_status(exit_status, NULL);
+       }
+       g_debug("%s supports '--export-filter keep-uid=...': %d", gpg_path, gpg_capas.export_filter_uid);
+
+       /* check for the "--export-filter drop-subkey=usage!~e && usage!~s" option */
+       if (sscanf(version, "%u.%u.%u", &major, &minor, &release) == 3) {
+               gpg_capas.export_filter_subkey = (major > 2U) ||
+                       ((major == 2U) && (minor > 2U)) ||
+                       ((major == 2U) && (minor == 2U) && (release >= 9U));
+       }
+       g_debug("%s supports '--export-filter drop-subkey=...': %d", gpg_path, 
gpg_capas.export_filter_subkey);
+}
diff --git a/libbalsa/libbalsa-gpgme.h b/libbalsa/libbalsa-gpgme.h
index 8a52164c8..7339e378d 100644
--- a/libbalsa/libbalsa-gpgme.h
+++ b/libbalsa/libbalsa-gpgme.h
@@ -43,6 +43,15 @@ G_BEGIN_DECLS
 #define GPGME_ERROR_QUARK (g_quark_from_static_string("gmime-gpgme"))
 
 
+struct _gpg_capabilities {
+       const gchar *gpg_path;                          /**< OpenPGP engine path */
+       gboolean export_filter_uid;                     /**< OpenPGP engine supports the 'keep-uid=...' 
export-filter option. */
+       gboolean export_filter_subkey;          /**< OpenPGP engine supports the 'drop-subkey=...' 
export-filter option. */
+};
+
+typedef struct _gpg_capabilities gpg_capabilities;
+
+
 /** Callback to select a key from a list
  * Parameters:
  * - user name
@@ -75,6 +84,7 @@ void libbalsa_gpgme_init(gpgme_passphrase_cb_t       get_passphrase,
                                                 lbgpgme_select_key_cb       select_key_cb,
                                                 lbgpgme_accept_low_trust_cb accept_low_trust);
 gboolean libbalsa_gpgme_check_crypto_engine(gpgme_protocol_t protocol);
+const gpg_capabilities *libbalsa_gpgme_gpg_capabilities(void);
 gpgme_ctx_t libbalsa_gpgme_new_with_proto(gpgme_protocol_t        protocol,
                                                                                  gpgme_passphrase_cb_t   
callback,
                                                                                  GtkWindow                   
           *parent,
diff --git a/libbalsa/meson.build b/libbalsa/meson.build
index dc5685269..d0d8911b1 100644
--- a/libbalsa/meson.build
+++ b/libbalsa/meson.build
@@ -2,6 +2,8 @@
 
 if gpgmecfg == 'true'
   libbalsa_gpgme_extra = [
+       'autocrypt.h',
+       'autocrypt.c',
     'libbalsa-gpgme.h',
     'libbalsa-gpgme.c',
     'libbalsa-gpgme-cb.h',
diff --git a/libbalsa/send.c b/libbalsa/send.c
index f4e747ada..33ed67fed 100644
--- a/libbalsa/send.c
+++ b/libbalsa/send.c
@@ -1509,6 +1509,20 @@ libbalsa_message_create_mime_message(LibBalsaMessage *message,
     g_mime_object_append_header(GMIME_OBJECT(mime_message), "X-Mailer", tmp);
     g_free(tmp);
 
+#ifdef ENABLE_AUTOCRYPT
+    /* add Autocrypt header if requested */
+    if ((message->ident != NULL) && (message->ident->autocrypt_mode != AUTOCRYPT_DISABLE) &&
+       !autocrypt_ignore(g_mime_object_get_content_type(mime_root))) {
+       tmp = autocrypt_header(message->ident, NULL);
+       if (tmp == NULL) {
+               g_object_unref(G_OBJECT(mime_message));
+               return LIBBALSA_MESSAGE_CREATE_ERROR;
+       }
+       g_mime_object_append_header(GMIME_OBJECT(mime_message), "Autocrypt", tmp);
+       g_free(tmp);
+    }
+#endif
+
     message->mime_msg = mime_message;
 
     return LIBBALSA_MESSAGE_CREATE_OK;
diff --git a/meson.build b/meson.build
index ad9f7a9c3..a623bbafa 100644
--- a/meson.build
+++ b/meson.build
@@ -47,6 +47,7 @@ endif
 
 gnome_desktop = get_option('gnome-desktop')
 gpgmecfg      = get_option('gpgme')
+autocryptcfg  = get_option('autocrypt')
 canberra      = get_option('canberra')
 compface      = get_option('compface')
 gss           = get_option('gss')
@@ -242,6 +243,19 @@ if gpgmecfg != 'false'
   endif
 endif
 
+# Autocrypt
+if gpgmecfg != 'false'
+  if autocryptcfg == 'true'
+    autocrypt_dep = dependency('sqlite3', required : true)
+    if autocrypt_dep.found()
+      conf.set('ENABLE_AUTOCRYPT', 1, description : 'If defined, enable Autocrypt support.')
+    else
+      error('*** You enabled Autocrypt but sqlite3 library is not found.')
+    endif
+    balsa_deps += autocrypt_dep
+  endif
+endif
+
 # OpenLDAP configuration.
 #
 if ldap != 'false'
@@ -616,6 +630,7 @@ summary = [
   '              Use GNOME: @0@'.format(gnome_desktop),
   '           Use Canberra: @0@'.format(canberra),
   '              Use GPGME: @0@'.format(gpgmecfg),
+  '          Use Autocrypt: @0@'.format(autocryptcfg),
   '               Use LDAP: @0@'.format(ldap),
   '                Use GSS: @0@'.format(gss),
   '             Use SQLite: @0@'.format(sqlite),
diff --git a/meson_options.txt b/meson_options.txt
index 76ea2c3a7..d72c558a6 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -18,6 +18,11 @@ option('gpgme',
   value       : 'false',
   description : 'build with gpgme/GnuPG support (true|false|path to gpgme-config, default=false)')
 
+option('autocrypt',
+  type        : 'string',
+  value       : 'false',
+  description : 'build with Autocrypt support (see https://autocrypt.org/), default=no, requires gpgme and 
sqlite3')
+
 option('canberra',
   type        : 'boolean',
   value       : false,
diff --git a/src/balsa-message.c b/src/balsa-message.c
index 1ad4ec1dc..b95d3cde0 100644
--- a/src/balsa-message.c
+++ b/src/balsa-message.c
@@ -39,6 +39,7 @@
 #include "balsa-mime-widget-message.h"
 #include "balsa-mime-widget-image.h"
 #include "balsa-mime-widget-crypto.h"
+#include "autocrypt.h"
 
 #include <gdk/gdkkeysyms.h>
 #include <gdk-pixbuf/gdk-pixbuf.h>
@@ -162,6 +163,9 @@ static GdkPixbuf * get_crypto_content_icon(LibBalsaMessageBody * body,
 static void message_recheck_crypto_cb(GtkWidget * button, BalsaMessage * bm);
 #endif /* HAVE_GPGME */
 
+#ifdef ENABLE_AUTOCRYPT
+static inline gboolean autocrypt_in_use(void);
+#endif
 
 static void
 balsa_part_info_class_init(BalsaPartInfoClass *klass)
@@ -1216,8 +1220,22 @@ balsa_message_set(BalsaMessage * bm, LibBalsaMailbox * mailbox, guint msgno)
      * present.
      *
      */
-    if (is_new && message->headers->dispnotify_to)
+    if (is_new && message->headers->dispnotify_to) {
         handle_mdn_request (balsa_get_parent_window(GTK_WIDGET(bm)), message);
+    }
+
+#ifdef ENABLE_AUTOCRYPT
+    /* check for Autocrypt information if the message is new only */
+    if (is_new && autocrypt_in_use()) {
+       GError *error = NULL;
+
+       autocrypt_from_message(message, &error);
+       if (error != NULL) {
+               libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("Autocrypt error: %s"), error->message);
+       }
+       g_clear_error(&error);
+    }
+#endif
 
     if (!gtk_tree_model_get_iter_first (gtk_tree_view_get_model(GTK_TREE_VIEW(bm->treeview)),
                                         &iter))
@@ -3320,3 +3338,17 @@ balsa_message_find_in_message(BalsaMessage * bm)
             gtk_widget_grab_focus(bm->find_entry);
     }
 }
+
+#ifdef ENABLE_AUTOCRYPT
+static inline gboolean
+autocrypt_in_use(void)
+{
+       gboolean result = FALSE;
+       GList *ident;
+
+       for (ident = balsa_app.identities; !result && (ident != NULL); ident = ident->next) {
+               result = LIBBALSA_IDENTITY(ident->data)->autocrypt_mode != AUTOCRYPT_DISABLE;
+       }
+       return result;
+}
+#endif
diff --git a/src/balsa-mime-widget-crypto.c b/src/balsa-mime-widget-crypto.c
index 49abdae80..d2c6121f4 100644
--- a/src/balsa-mime-widget-crypto.c
+++ b/src/balsa-mime-widget-crypto.c
@@ -120,17 +120,33 @@ balsa_mime_widget_signature_widget(LibBalsaMessageBody * mime_body,
     gtk_widget_set_halign(label, GTK_ALIGN_START);
     gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
     if (mime_body->sig_info->protocol == GPGME_PROTOCOL_OpenPGP) {
+       GtkWidget *hbox;
         GtkWidget *button;
 
+        hbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
+        gtk_button_box_set_layout(GTK_BUTTON_BOX(hbox), GTK_BUTTONBOX_EXPAND);
+        gtk_box_set_spacing(GTK_BOX(hbox), BMW_HBOX_SPACE);
+        gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
         if (mime_body->sig_info->status == GPG_ERR_NO_PUBKEY) {
+#ifdef ENABLE_AUTOCRYPT
+               GBytes *autocrypt_key;
+
+               autocrypt_key = autocrypt_get_key(mime_body->sig_info->fingerprint, NULL);
+               if (autocrypt_key != NULL) {
+                       button = gtk_button_new_with_mnemonic(_("_Import Autocrypt key"));
+                       g_object_set_data_full(G_OBJECT(button), "autocrypt_key", autocrypt_key, 
(GDestroyNotify) g_bytes_unref);
+                       g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(on_key_import_button), NULL);
+                       gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
+               }
+#endif
             button = gtk_button_new_with_mnemonic(_("_Search key server for this key"));
         } else {
             button = gtk_button_new_with_mnemonic(_("_Search key server for updates of this key"));
         }
         g_signal_connect(G_OBJECT(button), "clicked",
                          G_CALLBACK(on_gpg_key_button),
-                         (gpointer)mime_body->sig_info->fingerprint);
-        gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0);
+                         (gpointer) mime_body->sig_info->fingerprint);
+        gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
     }
 
     /* Hack alert: if we omit the box below and use the expander as signature widget
@@ -241,17 +257,27 @@ on_key_import_button(GtkButton *button,
                                         gpointer   user_data)
 {
        gpgme_ctx_t ctx;
-       gboolean success;
+       gboolean success = FALSE;
        GError *error = NULL;
        gchar *import_info = NULL;
        GtkWidget *dialog;
 
        ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_OpenPGP, NULL, NULL, &error);
        if (ctx != NULL) {
-               success = libbalsa_gpgme_import_ascii_key(ctx, g_object_get_data(G_OBJECT(button), 
"keydata"), &import_info, &error);
+               const gchar *keydata;
+
+               keydata = g_object_get_data(G_OBJECT(button), "keydata");
+               if (keydata != NULL) {
+                       success = libbalsa_gpgme_import_ascii_key(ctx, keydata, &import_info, &error);
+               } else {
+                       GBytes *key_buf;
+
+                       key_buf = (GBytes *) g_object_get_data(G_OBJECT(button), "autocrypt_key");
+                       if (key_buf != NULL) {
+                               success = libbalsa_gpgme_import_bin_key(ctx, key_buf, &import_info, &error);
+                       }
+               }
                gpgme_release(ctx);
-       } else {
-               success = FALSE;
        }
 
        if (success) {
@@ -266,7 +292,7 @@ on_key_import_button(GtkButton *button,
                        GTK_DIALOG_DESTROY_WITH_PARENT | libbalsa_dialog_flags(),
                        GTK_MESSAGE_ERROR,
                        GTK_BUTTONS_CLOSE,
-                       _("Error importing key data: %s"), error->message);
+                       _("Error importing key data: %s"), (error != NULL) ? error->message : _("unknown 
error"));
                g_clear_error(&error);
        }
        g_free(import_info);
@@ -328,6 +354,7 @@ create_import_keys_widget(GtkBox *box, const gchar *key_buf, GError **error)
 
                        libbalsa_delete_directory_contents(temp_dir);
                        g_rmdir(temp_dir);
+                       g_free(temp_dir);
                }
 
                gpgme_release(ctx);
diff --git a/src/main-window.c b/src/main-window.c
index 10b64d0b0..9529e1154 100644
--- a/src/main-window.c
+++ b/src/main-window.c
@@ -896,6 +896,16 @@ address_book_activated(GSimpleAction * action,
     gtk_widget_show(GTK_WIDGET(ab));
 }
 
+#ifdef ENABLE_AUTOCRYPT
+static void
+autocrypt_db_activated(GSimpleAction G_GNUC_UNUSED *action,
+                       GVariant      G_GNUC_UNUSED *parameter,
+                       gpointer                     user_data)
+{
+       autocrypt_db_dialog_run(balsa_app.date_string, GTK_WINDOW(user_data));
+}
+#endif
+
 static void
 prefs_activated(GSimpleAction * action,
                 GVariant      * parameter,
@@ -1890,6 +1900,9 @@ bw_add_app_action_entries(GActionMap * action_map, gpointer user_data)
         {"toolbars",              toolbars_activated},
         {"identities",            identities_activated},
         {"address-book",          address_book_activated},
+#ifdef ENABLE_AUTOCRYPT
+        {"autocrypt-db",          autocrypt_db_activated},
+#endif
         {"prefs",                 prefs_activated},
         {"help",                  help_activated},
         {"about",                 about_activated},
@@ -1912,6 +1925,9 @@ bw_add_win_action_entries(GActionMap * action_map)
         {"page-setup",            page_setup_activated},
         {"print",                 print_activated},
         {"address-book",          address_book_activated},
+#ifdef ENABLE_AUTOCRYPT
+        {"autocrypt-db",          autocrypt_db_activated},
+#endif
         {"quit",                  quit_activated},
         {"copy",                  copy_activated},
         {"select-all",            select_all_activated},
diff --git a/src/main.c b/src/main.c
index 24243f731..343b280a4 100644
--- a/src/main.c
+++ b/src/main.c
@@ -47,6 +47,7 @@
 #include "information.h"
 #include "imap-server.h"
 #include "libbalsa-conf.h"
+#include "autocrypt.h"
 
 #include "libinit_balsa/assistant_init.h"
 
@@ -503,6 +504,9 @@ balsa_startup_cb(GApplication *application,
            gpointer      user_data)
 {
     gchar *default_icon;
+#ifdef ENABLE_AUTOCRYPT
+    GError *error = NULL;
+#endif
 
 #ifdef ENABLE_NLS
     /* Initialize the i18n stuff */
@@ -537,6 +541,13 @@ balsa_startup_cb(GApplication *application,
 
     libbalsa_mailbox_date_format = &balsa_app.date_string;
 
+#ifdef ENABLE_AUTOCRYPT
+    if (!autocrypt_init(&error)) {
+       libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("Autocrypt error: %s"), error->message);
+       g_error_free(error);
+    }
+#endif
+
     /* checking for valid config files */
     config_init(cmd_get_stats);
 
diff --git a/src/sendmsg-window.c b/src/sendmsg-window.c
index 5e39b2715..d47b709c4 100644
--- a/src/sendmsg-window.c
+++ b/src/sendmsg-window.c
@@ -79,6 +79,11 @@
 #if HAVE_GTKSOURCEVIEW
 #include <gtksourceview/gtksource.h>
 #endif                          /* HAVE_GTKSOURCEVIEW */
+#ifdef ENABLE_AUTOCRYPT
+#include "autocrypt.h"
+#include "libbalsa-gpgme.h"
+#include "libbalsa-gpgme-keys.h"
+#endif                                                 /* ENABLE_AUTOCRYPT */
 
 typedef struct {
     pid_t pid_editor;
@@ -5045,6 +5050,58 @@ subject_not_empty(BalsaSendmsg * bsmsg)
 }
 
 #ifdef HAVE_GPGME
+
+static void
+config_dlg_button(GtkDialog *dialog, gint response_id, const gchar *icon_id)
+{
+       GtkWidget *button;
+
+       button = gtk_dialog_get_widget_for_response(dialog, response_id);
+       if (button != NULL) {
+               GtkWidget *image;
+
+               image = gtk_image_new_from_icon_name(icon_id, GTK_ICON_SIZE_BUTTON);
+               gtk_button_set_image(GTK_BUTTON(button), image);
+       }
+}
+
+static gboolean
+run_check_encrypt_dialog(BalsaSendmsg *bsmsg, const gchar *secondary_msg, gint default_button)
+{
+       GtkWidget *dialog;
+       gboolean result = TRUE;
+       gint choice;
+
+       dialog = gtk_message_dialog_new(GTK_WINDOW(bsmsg->window),
+            GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL | libbalsa_dialog_flags(),
+            GTK_MESSAGE_QUESTION,
+            GTK_BUTTONS_NONE,
+                _("Message could be encrypted"));
+       gtk_message_dialog_format_secondary_markup(GTK_MESSAGE_DIALOG(dialog), "%s", secondary_msg);
+       gtk_dialog_add_buttons(GTK_DIALOG(dialog),
+               _("Send _encrypted"), GTK_RESPONSE_YES,
+               _("Send _unencrypted"), GTK_RESPONSE_NO,
+               _("_Cancel"), GTK_RESPONSE_CANCEL,
+               NULL);
+       gtk_dialog_set_default_response(GTK_DIALOG(dialog), default_button);
+
+       /* add button images */
+       config_dlg_button(GTK_DIALOG(dialog), GTK_RESPONSE_YES, balsa_icon_id(BALSA_PIXMAP_GPG_ENCRYPT));
+       config_dlg_button(GTK_DIALOG(dialog), GTK_RESPONSE_NO, balsa_icon_id(BALSA_PIXMAP_SEND));
+
+       choice = gtk_dialog_run(GTK_DIALOG(dialog));
+       gtk_widget_destroy(dialog);
+       if (choice == GTK_RESPONSE_YES) {
+           bsmsg_setup_gpg_ui_by_mode(bsmsg, bsmsg->gpg_mode | LIBBALSA_PROTECT_ENCRYPT);
+       } else if ((choice == GTK_RESPONSE_CANCEL) || (choice == GTK_RESPONSE_DELETE_EVENT)) {
+           result = FALSE;
+    } else {
+       /* nothing to do */
+    }
+
+       return result;
+}
+
 static gboolean
 check_suggest_encryption(BalsaSendmsg * bsmsg)
 {
@@ -5052,9 +5109,10 @@ check_suggest_encryption(BalsaSendmsg * bsmsg)
     gboolean can_encrypt;
     gpgme_protocol_t protocol;
     gint len;
+    gboolean result = TRUE;
 
     /* check if the user wants to see the message */
-    if (!bsmsg->ident->warn_send_plain)
+    if ((bsmsg->ident == NULL) || !bsmsg->ident->warn_send_plain)
        return TRUE;
 
     /* nothing to do if encryption is already enabled */
@@ -5087,77 +5145,168 @@ check_suggest_encryption(BalsaSendmsg * bsmsg)
         g_object_unref(ia_list);
     }
 
-    /* ask the user if we could encrypt this message */
+    /* ask the user if we should encrypt this message */
     if (can_encrypt) {
-       GtkWidget *dialog;
-       gint choice;
-       GtkWidget *button;
-       GtkWidget *hbox;
-       GtkWidget *image;
-       GtkWidget *label;
+       gchar *message;
 
-       dialog = gtk_message_dialog_new
-           (GTK_WINDOW(bsmsg->window),
-            GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
-            GTK_MESSAGE_QUESTION,
-            GTK_BUTTONS_NONE,
-            _("You did not select encryption for this message, although "
-               "%s public keys are available for all recipients. In order "
-               "to protect your privacy, the message could be %s encrypted."),
-             gpgme_get_protocol_name(protocol),
-             gpgme_get_protocol_name(protocol));
-#if HAVE_MACOSX_DESKTOP
-        libbalsa_macosx_menu_for_parent(dialog, GTK_WINDOW(bsmsg->window));
-#endif
+       message = g_markup_printf_escaped(_("You did not select encryption for this message, although "
+               "%s public keys are available for all recipients. In order "
+            "to protect your privacy, the message could be %s encrypted."),
+               gpgme_get_protocol_name(protocol), gpgme_get_protocol_name(protocol));
+       result = run_check_encrypt_dialog(bsmsg, message, GTK_RESPONSE_YES);
+       g_free(message);
+    }
+    return result;
+}
 
+#ifdef ENABLE_AUTOCRYPT
+static gboolean
+import_autocrypt_keys(GList *missing_keys, GError **error)
+{
+       gpgme_ctx_t ctx;
+       gboolean result;
 
-       button = gtk_button_new();
-       gtk_dialog_add_action_widget(GTK_DIALOG(dialog), button, GTK_RESPONSE_YES);
-        gtk_widget_set_can_default(button, TRUE);
-       gtk_widget_grab_focus(button);
-
-       hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
-        gtk_widget_set_halign(hbox, GTK_ALIGN_CENTER);
-        gtk_widget_set_valign(hbox, GTK_ALIGN_CENTER);
-       gtk_container_add(GTK_CONTAINER(button), hbox);
-       image = gtk_image_new_from_icon_name(balsa_icon_id(BALSA_PIXMAP_GPG_ENCRYPT),
-                                             GTK_ICON_SIZE_BUTTON);
-       gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0);
-       label = gtk_label_new_with_mnemonic(_("Send _encrypted"));
-       gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
-       gtk_widget_show_all(button);
-
-       button = gtk_button_new();
-       gtk_dialog_add_action_widget(GTK_DIALOG(dialog), button, GTK_RESPONSE_NO);
-        gtk_widget_set_can_default(button, TRUE);
-
-       hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
-        gtk_widget_set_halign(hbox, GTK_ALIGN_CENTER);
-        gtk_widget_set_valign(hbox, GTK_ALIGN_CENTER);
-       gtk_container_add(GTK_CONTAINER(button), hbox);
-       image = gtk_image_new_from_icon_name(balsa_icon_id(BALSA_PIXMAP_SEND),
-                                             GTK_ICON_SIZE_BUTTON);
-       gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0);
-       label = gtk_label_new_with_mnemonic(_("Send _unencrypted"));
-       gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
-       gtk_widget_show_all(button);
-
-       button = gtk_button_new_with_mnemonic(_("_Cancel"));
-       gtk_widget_show(button);
-       gtk_dialog_add_action_widget(GTK_DIALOG(dialog), button, GTK_RESPONSE_CANCEL);
-        gtk_widget_set_can_default(button, TRUE);
+       ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_OpenPGP, NULL, NULL, error);
+       if (ctx != NULL) {
+               GList *key;
 
-       choice = gtk_dialog_run(GTK_DIALOG(dialog));
-       gtk_widget_destroy(dialog);
-       if (choice == GTK_RESPONSE_YES)
-           bsmsg_setup_gpg_ui_by_mode(bsmsg, bsmsg->gpg_mode | LIBBALSA_PROTECT_ENCRYPT);
-       else if (choice == GTK_RESPONSE_CANCEL || choice == GTK_RESPONSE_DELETE_EVENT)
-           return FALSE;
+               result = TRUE;
+               for (key = missing_keys; result && (key != NULL); key = key->next) {
+                       result = libbalsa_gpgme_import_bin_key(ctx, (GBytes *) key->data, NULL, error);
+               }
+               gpgme_release(ctx);
+       } else {
+               result = FALSE;
+       }
+
+       return result;
+}
+
+static gboolean
+check_autocrypt_recommendation(BalsaSendmsg *bsmsg)
+{
+    InternetAddressList *check_list;
+    InternetAddressList *tmp_list;
+    gint len;
+    AutocryptRecommend autocrypt_mode;
+    GList *missing_keys = NULL;
+    GError *error = NULL;
+    gboolean result;
+
+    /* check if autocrypt is enabled, use the non-Autocrypt approach if not */
+       if ((bsmsg->ident == NULL) || (bsmsg->ident->autocrypt_mode == AUTOCRYPT_DISABLE)) {
+               return check_suggest_encryption(bsmsg);
+       }
+
+    /* nothing to do if encryption is already enabled or if S/MIME mode is selected */
+    if ((bsmsg->gpg_mode & (LIBBALSA_PROTECT_ENCRYPT | LIBBALSA_PROTECT_SMIMEV3)) != 0) {
+       return TRUE;
     }
 
-    return TRUE;
+    /* we can not encrypt if we have bcc recipients */
+    tmp_list = libbalsa_address_view_get_list(bsmsg->recipient_view, "BCC:");
+    len = internet_address_list_length(tmp_list);
+    g_object_unref(tmp_list);
+    if (len > 0) {
+        return TRUE;
+    }
+
+    /* get the Autocrypt recommendation for all To: and Cc: addresses */
+    check_list = libbalsa_address_view_get_list(bsmsg->recipient_view, "To:");
+    tmp_list = libbalsa_address_view_get_list(bsmsg->recipient_view, "CC:");
+    internet_address_list_append(check_list, tmp_list);
+    g_object_unref(tmp_list);
+    internet_address_list_add(check_list, bsmsg->ident->ia);           /* validates that we have a key for 
the current identity */
+    autocrypt_mode = autocrypt_recommendation(check_list, &missing_keys, &error);
+    g_object_unref(check_list);
+
+    /* eject on error or disabled */
+    if (autocrypt_mode <= AUTOCRYPT_ENCR_DISABLE) {
+       if (autocrypt_mode == AUTOCRYPT_ENCR_ERROR) {
+               libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("error checking Autocrypt keys: %s"),
+                       (error != NULL) ? error->message : _("unknown"));
+               g_clear_error(&error);
+               result = FALSE;
+       } else {
+               result = TRUE;
+       }
+    } else {
+       gchar *message;
+       const gchar *protoname;
+       gint default_choice;
+
+       protoname = gpgme_get_protocol_name(GPGME_PROTOCOL_OpenPGP);
+               message = g_markup_printf_escaped(_("You did not select encryption for this message, although 
"
+               "%s public keys are available for all recipients. In order "
+            "to protect your privacy, the message could be %s encrypted."),
+                       protoname, protoname);
+
+       /* default to encryption if all participants have prefer-encrypt=mutual, or if we reply to an 
encrypted message */
+       if (((autocrypt_mode == AUTOCRYPT_ENCR_AVAIL_MUTUAL) && (bsmsg->ident->autocrypt_mode == 
AUTOCRYPT_PREFER_ENCRYPT)) ||
+               ((bsmsg->parent_message != NULL) && (bsmsg->parent_message->prot_state == 
LIBBALSA_MSG_PROTECT_CRYPT))) {
+               default_choice = GTK_RESPONSE_YES;
+       } else if (autocrypt_mode == AUTOCRYPT_ENCR_AVAIL) {
+               default_choice = GTK_RESPONSE_NO;
+       } else {                        /* autocrypt_mode == AUTOCRYPT_ENCR_DISCOURAGE */
+               gchar *tmp_msg;
+
+               default_choice = GTK_RESPONSE_NO;
+               tmp_msg = g_strconcat(message,
+                       _("\nHowever, encryption is discouraged as the Autocrypt status indicates that "
+                         "some recipients <i>might</i> no be able to read the message."), NULL);
+               g_free(message);
+               message = tmp_msg;
+       }
+
+       /* add a note if keys are imported into the key ring */
+       if (missing_keys != NULL) {
+               guint key_count;
+               gchar *key_msg;
+               gchar *tmp_msg;
+
+               key_count = g_list_length(missing_keys);
+               key_msg = g_strdup_printf(ngettext("<i>Note:</i> choosing encryption will import %u key from "
+                                                                                  "the Autocrypt database 
into the GnuPG key ring.",
+                                                                                          "<i>Note:</i> 
choosing encryption will import %u keys from "
+                                                                                  "the Autocrypt database 
into the GnuPG key ring.", key_count), key_count);
+               tmp_msg = g_strconcat(message, "\n", key_msg, NULL);
+               g_free(message);
+               g_free(key_msg);
+               message = tmp_msg;
+       }
+
+       /* run the dialog */
+       result = run_check_encrypt_dialog(bsmsg, message, default_choice);
+
+       if (result && ((bsmsg->gpg_mode & LIBBALSA_PROTECT_ENCRYPT) != 0)) {
+               /* make sure the message is also signed as required by the Autocrypt standard, and that a 
protocol is selected */
+               if ((bsmsg->gpg_mode & LIBBALSA_PROTECT_PROTOCOL) == 0) {
+                       bsmsg_setup_gpg_ui_by_mode(bsmsg, bsmsg->gpg_mode | (LIBBALSA_PROTECT_RFC3156 + 
LIBBALSA_PROTECT_SIGN));
+               } else {
+                       bsmsg_setup_gpg_ui_by_mode(bsmsg, bsmsg->gpg_mode | LIBBALSA_PROTECT_SIGN);
+               }
+
+               /* import any missing keys */
+               if (missing_keys != NULL) {
+                       result = import_autocrypt_keys(missing_keys, &error);
+                       if (!result) {
+                               libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("Cannot import Autocrypt 
keys: %s"), error->message);
+                               g_clear_error(&error);
+                       }
+               }
+       }
+    }
+
+    /* clean up the missing keys list */
+    if (missing_keys != NULL) {
+       g_list_free_full(missing_keys, (GDestroyNotify) g_bytes_unref);
+    }
+
+    return result;
 }
-#endif
+#endif         /* ENABLE_AUTOCRYPT */
+#endif         /* HAVE_GPGME */
+
 
 /* "send message" menu and toolbar callback.
  */
@@ -5179,8 +5328,15 @@ send_message_handler(BalsaSendmsg * bsmsg, gboolean queue_only)
        return FALSE;
 
 #ifdef HAVE_GPGME
-    if (!check_suggest_encryption(bsmsg))
-       return FALSE;
+#ifdef ENABLE_AUTOCRYPT
+    if (!check_autocrypt_recommendation(bsmsg)) {
+       return FALSE;
+    }
+#else
+    if (!check_suggest_encryption(bsmsg)) {
+       return FALSE;
+    }
+#endif /* ENABLE_AUTOCRYPT */
 
     if ((bsmsg->gpg_mode & LIBBALSA_PROTECT_OPENPGP) != 0) {
         gboolean warn_mp;
diff --git a/ui/main-window.ui b/ui/main-window.ui
index 548da3366..4fdd3ef80 100644
--- a/ui/main-window.ui
+++ b/ui/main-window.ui
@@ -59,6 +59,11 @@
           translatable='yes'>_Address Book</attribute>
         <attribute name='action'>app.address-book</attribute>
       </item>
+      <item>
+        <attribute name='label'
+          translatable='yes'>A_utocrypt Database</attribute>
+        <attribute name='action'>app.autocrypt-db</attribute>
+      </item>
     </section>
     <section>
       <item>
@@ -139,6 +144,11 @@
           <attribute name='action'>win.address-book</attribute>
           <attribute name='accel'>b</attribute>
         </item>
+       <item>
+          <attribute name='label'
+               translatable='yes'>A_utocrypt Database</attribute>
+          <attribute name='action'>win.autocrypt-db</attribute>
+               </item>
       </section>
       <section>
         <item>


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