[evolution] Bug 704246 - Cannot send encrypted mail to contact with certificate



commit 3627d75e1cb968b3111b1c952c3a572f97613a09
Author: Milan Crha <mcrha redhat com>
Date:   Wed Jul 4 11:56:33 2018 +0200

    Bug 704246 - Cannot send encrypted mail to contact with certificate

 data/org.gnome.evolution.mail.gschema.xml.in       |  15 +-
 .../evolution-mail-composer-docs.sgml.in           |   4 +
 .../evolution-mail-engine-docs.sgml.in             |   4 +
 src/composer/e-msg-composer.c                      | 128 ++++++++++++++-
 src/libemail-engine/e-mail-engine-enums.h          |  19 +++
 src/libemail-engine/e-mail-session.c               | 175 ++++++++++++++++++++-
 src/libemail-engine/e-mail-session.h               |   6 +
 7 files changed, 346 insertions(+), 5 deletions(-)
---
diff --git a/data/org.gnome.evolution.mail.gschema.xml.in b/data/org.gnome.evolution.mail.gschema.xml.in
index 9564c946bd..a3e381a1c3 100644
--- a/data/org.gnome.evolution.mail.gschema.xml.in
+++ b/data/org.gnome.evolution.mail.gschema.xml.in
@@ -23,7 +23,7 @@
     <value nick='outlook'      value='3'/>
   </enum>
 
-  <!-- Keep this synchronized with EMailImageLoadingPolicy. -->
+  <!-- Keep this synchronized with EImageLoadingPolicy. -->
   <enum id="org.gnome.evolution.mail.ImageLoadingPolicy">
     <value nick='never'     value='0'/>
     <value nick='sometimes' value='1'/>
@@ -37,6 +37,13 @@
     <value nick='inconsistent' value='2'/>
   </enum>
 
+  <!-- Keep this synchronized with EMailRecipientCertificateLookup. -->
+  <enum id="org.gnome.evolution.mail.MailRecipientCertificateLookup">
+    <value nick='off'           value='0'/>
+    <value nick='autocompleted' value='1'/>
+    <value nick='books'         value='2'/>
+  </enum>
+
   <schema gettext-domain="evolution" id="org.gnome.evolution.mail" path="/org/gnome/evolution/mail/">
     <key name="prompt-check-if-default-mailer" type="b">
       <default>true</default>
@@ -787,6 +794,12 @@
       <default>false</default>
       <_summary>Collapse archive folders in Move/Copy message to Folder and Go to Folder 
selectors.</_summary>
     </key>
+    <key name="lookup-recipient-certificates" enum="org.gnome.evolution.mail.MailRecipientCertificateLookup">
+      <default>'books'</default>
+      <_summary>Where to lookup recipient S/MIME certificates or PGP keys when encrypting 
messages.</_summary>
+      <_description>The “off” value completely disables certificate lookup; the “autocompleted” value 
provides certificates only for auto-completed contacts; the “books” value uses certificates from 
auto-completed contacts and searches in books marked for auto-completion.</_description>
+    </key>
+
     <!-- The following keys are deprecated. -->
 
     <key name="forward-style" type="i">
diff --git a/docs/reference/evolution-mail-composer/evolution-mail-composer-docs.sgml.in 
b/docs/reference/evolution-mail-composer/evolution-mail-composer-docs.sgml.in
index 2ed1fee181..b610a05024 100644
--- a/docs/reference/evolution-mail-composer/evolution-mail-composer-docs.sgml.in
+++ b/docs/reference/evolution-mail-composer/evolution-mail-composer-docs.sgml.in
@@ -29,6 +29,10 @@
     <title>API Index</title>
     <xi:include href="xml/api-index-full.xml"><xi:fallback /></xi:include>
   </index>
+  <index id="api-index-3-30" role="3.30">
+    <title>Index of new symbols in 3.30</title>
+    <xi:include href="xml/api-index-3.30.xml"><xi:fallback /></xi:include>
+  </index>
   <index id="api-index-3-22" role="3.22">
     <title>Index of new symbols in 3.22</title>
     <xi:include href="xml/api-index-3.22.xml"><xi:fallback /></xi:include>
diff --git a/docs/reference/evolution-mail-engine/evolution-mail-engine-docs.sgml.in 
b/docs/reference/evolution-mail-engine/evolution-mail-engine-docs.sgml.in
index d440c0674e..6f2e36fb14 100644
--- a/docs/reference/evolution-mail-engine/evolution-mail-engine-docs.sgml.in
+++ b/docs/reference/evolution-mail-engine/evolution-mail-engine-docs.sgml.in
@@ -46,6 +46,10 @@
     <title>API Index</title>
     <xi:include href="xml/api-index-full.xml"><xi:fallback /></xi:include>
   </index>
+  <index id="api-index-3-30" role="3.30">
+    <title>Index of new symbols in 3.30</title>
+    <xi:include href="xml/api-index-3.30.xml"><xi:fallback /></xi:include>
+  </index>
   <index id="api-index-3-24" role="3.24">
     <title>Index of new symbols in 3.24</title>
     <xi:include href="xml/api-index-3.24.xml"><xi:fallback /></xi:include>
diff --git a/src/composer/e-msg-composer.c b/src/composer/e-msg-composer.c
index 73bb890069..529a80d8de 100644
--- a/src/composer/e-msg-composer.c
+++ b/src/composer/e-msg-composer.c
@@ -64,6 +64,7 @@ struct _AsyncContext {
        GtkPrintOperationAction print_action;
 
        GPtrArray *recipients;
+       GSList *recipients_with_certificate; /* EContact * */
 
        guint skip_content : 1;
        guint need_thread : 1;
@@ -167,6 +168,9 @@ async_context_free (AsyncContext *context)
        if (context->recipients != NULL)
                g_ptr_array_free (context->recipients, TRUE);
 
+       if (context->recipients_with_certificate)
+               g_slist_free_full (context->recipients_with_certificate, g_object_unref);
+
        g_slice_free (AsyncContext, context);
 }
 
@@ -716,6 +720,107 @@ composer_add_quoted_printable_filter (CamelStream *stream)
        g_object_unref (filter);
 }
 
+/* Extracts auto-completed contacts which have X.509 or PGP certificate set.
+   This should be called in the GUI thread, because it accesses GtkWidget-s. */
+static GSList * /* EContact * */
+composer_get_completed_recipients_with_certificate (EMsgComposer *composer)
+{
+       EComposerHeaderTable *table;
+       GSList *contacts = NULL;
+       EDestination **to, **cc, **bcc;
+       gint ii;
+
+       g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL);
+
+       table = e_msg_composer_get_header_table (composer);
+       to = e_composer_header_table_get_destinations_to (table);
+       cc = e_composer_header_table_get_destinations_cc (table);
+       bcc = e_composer_header_table_get_destinations_bcc (table);
+
+       #define traverse_destv(x) \
+               for (ii = 0; x && x[ii]; ii++) { \
+                       EDestination *dest = x[ii]; \
+                       EContactCert *x509cert, *pgpcert; \
+                       EContact *contact; \
+                        \
+                       contact = e_destination_get_contact (dest); \
+                        \
+                       /* Get certificates only for individuals, not for lists */ \
+                       if (!contact || e_destination_is_evolution_list (dest)) \
+                               continue; \
+                        \
+                       x509cert = e_contact_get (contact, E_CONTACT_X509_CERT); \
+                       pgpcert = e_contact_get (contact, E_CONTACT_PGP_CERT); \
+                        \
+                       if (x509cert || pgpcert) \
+                               contacts = g_slist_prepend (contacts, e_contact_duplicate (contact)); \
+                        \
+                       e_contact_cert_free (x509cert); \
+                       e_contact_cert_free (pgpcert); \
+               }
+
+       traverse_destv (to);
+       traverse_destv (cc);
+       traverse_destv (bcc);
+
+       #undef traverse_destv
+
+       e_destination_freev (to);
+       e_destination_freev (cc);
+       e_destination_freev (bcc);
+
+       return contacts;
+}
+
+static gchar *
+composer_get_recipient_certificate_cb (EMailSession *session,
+                                      guint32 flags, /* bit-or of CamelRecipientCertificateFlags */
+                                      const gchar *email_address,
+                                      gpointer user_data)
+{
+       AsyncContext *context = user_data;
+       EContactField field_id;
+       GSList *link;
+       gchar *base64_cert = NULL;
+
+       g_return_val_if_fail (context != NULL, NULL);
+
+       if (!email_address || !*email_address)
+               return NULL;
+
+       if ((flags & CAMEL_RECIPIENT_CERTIFICATE_SMIME) != 0)
+               field_id = E_CONTACT_X509_CERT;
+       else
+               field_id = E_CONTACT_PGP_CERT;
+
+       for (link = context->recipients_with_certificate; link && !base64_cert; link = g_slist_next (link)) {
+               EContact *contact = link->data;
+               GList *emails, *elink;
+               EContactCert *cert;
+
+               cert = e_contact_get (contact, field_id);
+               if (!cert || !cert->data || !cert->length) {
+                       e_contact_cert_free (cert);
+                       continue;
+               }
+
+               emails = e_contact_get (contact, E_CONTACT_EMAIL);
+
+               for (elink = emails; elink && !base64_cert; elink = g_list_next (elink)) {
+                       const gchar *contact_email = elink->data;
+
+                       if (contact_email && g_ascii_strcasecmp (contact_email, email_address) == 0) {
+                               base64_cert = g_base64_encode ((const guchar *) cert->data, cert->length);
+                       }
+               }
+
+               g_list_free_full (emails, g_free);
+               e_contact_cert_free (cert);
+       }
+
+       return base64_cert;
+}
+
 /* Helper for composer_build_message_thread() */
 static gboolean
 composer_build_message_pgp (AsyncContext *context,
@@ -792,6 +897,7 @@ composer_build_message_pgp (AsyncContext *context,
 
        if (context->pgp_encrypt) {
                CamelMimePart *npart;
+               gulong handler_id;
                gboolean success;
 
                npart = camel_mime_part_new ();
@@ -807,10 +913,16 @@ composer_build_message_pgp (AsyncContext *context,
                camel_gpg_context_set_always_trust (CAMEL_GPG_CONTEXT (cipher), always_trust);
                camel_gpg_context_set_prefer_inline (CAMEL_GPG_CONTEXT (cipher), prefer_inline);
 
+               handler_id = g_signal_connect (context->session, "get-recipient-certificate",
+                       G_CALLBACK (composer_get_recipient_certificate_cb), context);
+
                success = camel_cipher_context_encrypt_sync (
                        cipher, pgp_key_id, context->recipients,
                        mime_part, npart, cancellable, error);
 
+               if (handler_id)
+                       g_signal_handler_disconnect (context->session, handler_id);
+
                g_object_unref (cipher);
 
                if (encrypt_to_self && pgp_key_id != NULL)
@@ -951,6 +1063,7 @@ composer_build_message_smime (AsyncContext *context,
        }
 
        if (context->smime_encrypt) {
+               gulong handler_id;
                gboolean success;
 
                /* Check to see if we should encrypt to self.
@@ -965,12 +1078,18 @@ composer_build_message_smime (AsyncContext *context,
                        (CamelSMIMEContext *) cipher, TRUE,
                        encryption_certificate);
 
+               handler_id = g_signal_connect (context->session, "get-recipient-certificate",
+                       G_CALLBACK (composer_get_recipient_certificate_cb), context);
+
                success = camel_cipher_context_encrypt_sync (
                        cipher, NULL,
                        context->recipients, mime_part,
                        CAMEL_MIME_PART (context->message),
                        cancellable, error);
 
+               if (handler_id)
+                       g_signal_handler_disconnect (context->session, handler_id);
+
                g_object_unref (cipher);
 
                if (!success)
@@ -1534,13 +1653,16 @@ composer_build_message (EMsgComposer *composer,
        }
 
        /* Run any blocking operations in a separate thread. */
-       if (context->need_thread)
+       if (context->need_thread) {
+               context->recipients_with_certificate = composer_get_completed_recipients_with_certificate 
(composer);
+
                g_simple_async_result_run_in_thread (
                        simple, (GSimpleAsyncThreadFunc)
                        composer_build_message_thread,
                        io_priority, cancellable);
-       else
+       } else {
                g_simple_async_result_complete (simple);
+       }
 
        e_msg_composer_dec_soft_busy (composer);
 
@@ -2675,7 +2797,7 @@ e_msg_composer_is_busy (EMsgComposer *composer)
  * Returns: %TRUE when e_msg_composer_is_busy() returns %TRUE or
  *    when the asynchronous operations are disabled.
  *
- * Since: 3.29.3
+ * Since: 3.30
  **/
 gboolean
 e_msg_composer_is_soft_busy (EMsgComposer *composer)
diff --git a/src/libemail-engine/e-mail-engine-enums.h b/src/libemail-engine/e-mail-engine-enums.h
index b8e98ed4a8..19d7f9daaf 100644
--- a/src/libemail-engine/e-mail-engine-enums.h
+++ b/src/libemail-engine/e-mail-engine-enums.h
@@ -39,6 +39,25 @@ typedef enum {
        E_MAIL_NUM_LOCAL_FOLDERS
 } EMailLocalFolder;
 
+/**
+ * EMailRecipientCertificateLookup:
+ * @E_MAIL_RECIPIENT_CERTIFICATE_LOOKUP_OFF: Do not do any recipient certificate lookup
+ * @E_MAIL_RECIPIENT_CERTIFICATE_LOOKUP_AUTOCOMPLETED: Lookup recipient certificates
+ *    between auto-completed recipients only
+ * @E_MAIL_RECIPIENT_CERTIFICATE_LOOKUP_BOOKS: Lookup recipient certificates between
+ *    auto-completed recipients and all books marked for auto-completion
+ *
+ * Used to set whether and where S/MIME certificates or PGP keys for message encryption
+ * should be looked up for.
+ *
+ * Since: 3.30
+ **/
+typedef enum {
+       E_MAIL_RECIPIENT_CERTIFICATE_LOOKUP_OFF,
+       E_MAIL_RECIPIENT_CERTIFICATE_LOOKUP_AUTOCOMPLETED,
+       E_MAIL_RECIPIENT_CERTIFICATE_LOOKUP_BOOKS
+} EMailRecipientCertificateLookup;
+
 G_END_DECLS
 
 #endif /* E_MAIL_ENGINE_ENUMS_H */
diff --git a/src/libemail-engine/e-mail-session.c b/src/libemail-engine/e-mail-session.c
index c86947c9e8..2270d7c0ab 100644
--- a/src/libemail-engine/e-mail-session.c
+++ b/src/libemail-engine/e-mail-session.c
@@ -128,6 +128,7 @@ enum {
        STORE_ADDED,
        STORE_REMOVED,
        ALLOW_AUTH_PROMPT,
+       GET_RECIPIENT_CERTIFICATE,
        LAST_SIGNAL
 };
 
@@ -1546,6 +1547,146 @@ mail_session_get_oauth2_access_token_sync (CamelSession *session,
        return success;
 }
 
+static gboolean
+mail_session_is_email_address (const gchar *str)
+{
+       gboolean has_at = FALSE, has_dot_after_at = FALSE;
+       gint ii;
+
+       if (!str)
+               return FALSE;
+
+       for (ii = 0; str[ii]; ii++) {
+               if (str[ii] == '@') {
+                       if (has_at)
+                               return FALSE;
+
+                       has_at = TRUE;
+               } else if (has_at && str[ii] == '.') {
+                       has_dot_after_at = TRUE;
+               } else if (g_ascii_isspace (str[ii])) {
+                       return FALSE;
+               } else if (strchr ("<>;,\\\"'|", str[ii])) {
+                       return FALSE;
+               }
+       }
+
+       return has_at && has_dot_after_at;
+}
+
+static gboolean
+mail_session_get_recipient_certificates_sync (CamelSession *session,
+                                             guint32 flags, /* bit-or of CamelRecipientCertificateFlags */
+                                             const GPtrArray *recipients, /* gchar * */
+                                             GSList **out_certificates, /* gchar * */
+                                             GCancellable *cancellable,
+                                             GError **error)
+{
+       GHashTable *certificates; /* guint index-to-recipients ~> gchar *certificate */
+       EMailRecipientCertificateLookup lookup_settings;
+       GSettings *settings;
+       gboolean success = TRUE;
+       guint ii;
+
+       g_return_val_if_fail (E_IS_MAIL_SESSION (session), FALSE);
+       g_return_val_if_fail (recipients != NULL, FALSE);
+       g_return_val_if_fail (out_certificates != NULL, FALSE);
+
+       *out_certificates = NULL;
+
+       settings = e_util_ref_settings ("org.gnome.evolution.mail");
+       lookup_settings = g_settings_get_enum (settings, "lookup-recipient-certificates");
+       g_object_unref (settings);
+
+       if (lookup_settings == E_MAIL_RECIPIENT_CERTIFICATE_LOOKUP_OFF)
+               return TRUE;
+
+       certificates = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+       for (ii = 0; ii < recipients->len; ii++) {
+               gchar *certstr = NULL;
+
+               g_signal_emit (session, signals[GET_RECIPIENT_CERTIFICATE], 0, flags, recipients->pdata[ii], 
&certstr);
+
+               if (certstr && *certstr)
+                       g_hash_table_insert (certificates, GUINT_TO_POINTER (ii + 1), certstr);
+               else
+                       g_free (certstr);
+       }
+
+       if (lookup_settings == E_MAIL_RECIPIENT_CERTIFICATE_LOOKUP_BOOKS &&
+           g_hash_table_size (certificates) != recipients->len) {
+               ESourceRegistry *registry;
+               GPtrArray *todo_recipients;
+               GSList *found_certificates = NULL;
+
+               todo_recipients = g_ptr_array_new ();
+               for (ii = 0; ii < recipients->len; ii++) {
+                       /* Lookup address books only with email addresses. */
+                       if (!g_hash_table_contains (certificates, GUINT_TO_POINTER (ii + 1)) &&
+                           mail_session_is_email_address (recipients->pdata[ii])) {
+                               g_ptr_array_add (todo_recipients, recipients->pdata[ii]);
+                       }
+               }
+
+               if (todo_recipients->len) {
+                       registry = e_mail_session_get_registry (E_MAIL_SESSION (session));
+
+                       if ((flags & CAMEL_RECIPIENT_CERTIFICATE_SMIME) != 0)
+                               camel_operation_push_message (cancellable, "%s", _("Looking up recipient 
S/MIME certificates in address books…"));
+                       else
+                               camel_operation_push_message (cancellable, "%s", _("Looking up recipient PGP 
keys in address books…"));
+
+                       success = e_book_utils_get_recipient_certificates_sync (registry, NULL, flags, 
todo_recipients, &found_certificates, cancellable, error);
+
+                       camel_operation_pop_message (cancellable);
+               }
+
+               if (success && found_certificates && g_slist_length (found_certificates) == 
todo_recipients->len) {
+                       GSList *link;
+
+                       for (link = found_certificates, ii = 0; link && ii < recipients->len; ii++) {
+                               if (!g_hash_table_contains (certificates, GUINT_TO_POINTER (ii + 1))) {
+                                       if (link->data) {
+                                               g_hash_table_insert (certificates, GUINT_TO_POINTER (ii + 1), 
link->data);
+                                               link->data = NULL;
+                                       }
+
+                                       link = g_slist_next (link);
+                               }
+                       }
+               }
+
+               g_slist_free_full (found_certificates, g_free);
+               g_ptr_array_free (todo_recipients, TRUE);
+       }
+
+       if (success) {
+               for (ii = 0; ii < recipients->len; ii++) {
+                       *out_certificates = g_slist_prepend (*out_certificates,
+                               g_hash_table_lookup (certificates, GUINT_TO_POINTER (ii + 1)));
+               }
+
+               *out_certificates = g_slist_reverse (*out_certificates);
+       } else {
+               GHashTableIter iter;
+               gpointer value;
+
+               /* There is no destructor for the 'value', to be able to easily pass it to
+                  the out_certificates. This code is here to free the values, though it might
+                  not be usually used, because e_book_utils_get_recipient_certificates_sync()
+                  returns TRUE usually. */
+               g_hash_table_iter_init (&iter, certificates);
+               while (g_hash_table_iter_next (&iter, NULL, &value)) {
+                       g_free (value);
+               }
+       }
+
+       g_hash_table_destroy (certificates);
+
+       return success;
+}
+
 static EMVFolderContext *
 mail_session_create_vfolder_context (EMailSession *session)
 {
@@ -1573,6 +1714,7 @@ e_mail_session_class_init (EMailSessionClass *class)
        session_class->forget_password = mail_session_forget_password;
        session_class->forward_to_sync = mail_session_forward_to_sync;
        session_class->get_oauth2_access_token_sync = mail_session_get_oauth2_access_token_sync;
+       session_class->get_recipient_certificates_sync = mail_session_get_recipient_certificates_sync;
 
        class->create_vfolder_context = mail_session_create_vfolder_context;
 
@@ -1688,7 +1830,7 @@ e_mail_session_class_init (EMailSessionClass *class)
                CAMEL_TYPE_STORE);
 
        /**
-        * EMailSession::store-removed
+        * EMailSession::allow-auth-prompt
         * @session: the #EMailSession that emitted the signal
         * @source: an #ESource
         *
@@ -1707,6 +1849,37 @@ e_mail_session_class_init (EMailSessionClass *class)
                G_TYPE_NONE, 1,
                E_TYPE_SOURCE);
 
+       /**
+        * EMailSession::get-recipient-certificate
+        * @session: the #EMailSession that emitted the signal
+        * @flags: a bit-or of #CamelRecipientCertificateFlags
+        * @email_address: recipient's email address
+        *
+        * This signal is used to get recipient's S/MIME certificate or
+        * PGP key for encryption, as part of camel_session_get_recipient_certificates_sync().
+        * The listener is not supposed to do any expensive look ups, it should only check
+        * whether it has the certificate available for the given @email_address and
+        * eventually return it as base64 encoded string.
+        *
+        * The caller of the action signal will free returned pointer with g_free(),
+        * when no longer needed.
+        *
+        * Returns: (transfer full) (nullable): %NULL when the certificate not known,
+        *    or a newly allocated base64-encoded string with the certificate.
+        *
+        * Since: 3.30
+        **/
+       signals[GET_RECIPIENT_CERTIFICATE] = g_signal_new (
+               "get-recipient-certificate",
+               G_OBJECT_CLASS_TYPE (object_class),
+               G_SIGNAL_ACTION,
+               G_STRUCT_OFFSET (EMailSessionClass, get_recipient_certificate),
+               NULL, NULL,
+               NULL,
+               G_TYPE_STRING, 2,
+               G_TYPE_UINT,
+               G_TYPE_STRING);
+
        camel_null_store_register_provider ();
 
        /* Make sure ESourceCamel picks up the "none" provider. */
diff --git a/src/libemail-engine/e-mail-session.h b/src/libemail-engine/e-mail-session.h
index 44374cd353..7d72b31045 100644
--- a/src/libemail-engine/e-mail-session.h
+++ b/src/libemail-engine/e-mail-session.h
@@ -84,6 +84,12 @@ struct _EMailSessionClass {
                                                 CamelStore *store);
        void            (*allow_auth_prompt)    (EMailSession *session,
                                                 ESource *source);
+       gchar *         (*get_recipient_certificate)
+                                               (EMailSession *session,
+                                                guint flags, /* bit-or of CamelRecipientCertificateFlags */
+                                                const gchar *email_address);
+       /* Padding for future expansion */
+       gpointer reserved[10];
 };
 
 GType          e_mail_session_get_type         (void);


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