This adds --status-fd and --command-fd options to the show and reply
commands which are used to specify a file descriptor for communicating
with notmuch as it is running. Notmuch will write status messages to
the status-fd and expect responses to them on the command-fd. These
options are inspired by similar options for controlling gpg.

Currently the only use for this is to have a way for the CLI program
to query passwords from the calling application. When the client needs
a password it will write a string in the following format to the
status-fd:

[NOTMUCH:] GET_HIDDEN <user_id> <prompt> <reprompt>

The user_id and prompt are string arguments that are encoded like C
strings. Ie, they will be surrounded by "quotes" and will contain
backslash escape sequences. These can also be parsed directly by the
read function in Emacs. The last argument is either a 1 or 0 to
specify whether the password is being prompted for a second time.

When the application sees this status it is expected to write the
password followed by a newline to the command-fd.
---
 crypto.c         | 184 +++++++++++++++++++++++++++++++++++++++++++++++++++----
 notmuch-client.h |   2 +
 notmuch-reply.c  |   6 +-
 notmuch-show.c   |   6 +-
 4 files changed, 185 insertions(+), 13 deletions(-)

diff --git a/crypto.c b/crypto.c
index 9736517..eded53f 100644
--- a/crypto.c
+++ b/crypto.c
@@ -22,28 +22,188 @@

 #ifdef GMIME_ATLEAST_26

+#define NOTMUCH_PASSWORD_DATA_KEY "notmuch-password-data"
+
+typedef struct
+{
+    int status_fd;
+    GIOChannel *command_channel;
+} notmuch_password_data_t;
+
+static void
+free_password_data_cb (void *data_ptr)
+{
+    notmuch_password_data_t *data = data_ptr;
+
+    g_io_channel_unref (data->command_channel);
+    free (data);
+}
+
+static void
+escape_string (GString *buf,
+              const char *str)
+{
+    const char *p;
+
+    g_string_append_c (buf, '"');
+
+    for (p = str; *p; p++)
+       switch (*p) {
+       case '\n':
+           g_string_append (buf, "\\n");
+           break;
+       case '\t':
+           g_string_append (buf, "\\t");
+           break;
+       case '\r':
+           g_string_append (buf, "\\t");
+           break;
+       case '"':
+           g_string_append (buf, "\\\"");
+           break;
+       default:
+           if (*p < 32)
+               g_string_append_printf (buf, "\\0%o", *p);
+           else
+               g_string_append_c (buf, *p);
+           break;
+       }
+
+    g_string_append_c (buf, '"');
+}
+
+static gboolean
+write_all (const char *buf,
+          size_t length,
+          int fd)
+{
+    ssize_t wrote;
+
+    while (length > 0) {
+       wrote = write (fd, buf, length);
+       if (wrote == -1)
+           return FALSE;
+       buf += wrote;
+       length -= wrote;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+password_request_cb (GMimeCryptoContext *ctx,
+                    const char *user_id,
+                    const char *prompt_ctx,
+                    gboolean reprompt,
+                    GMimeStream *response,
+                    GError **err)
+{
+    notmuch_password_data_t *data;
+    GString *buf;
+    gboolean write_result;
+    char *line = NULL;
+    gsize line_length;
+    ssize_t wrote;
+
+    data = g_object_get_data (G_OBJECT (ctx), NOTMUCH_PASSWORD_DATA_KEY);
+
+    buf = g_string_new ("[NOTMUCH:] GET_HIDDEN ");
+    escape_string (buf, user_id);
+    g_string_append_c (buf, ' ');
+    escape_string (buf, prompt_ctx);
+    g_string_append_c (buf, ' ');
+    g_string_append_c (buf, (!!reprompt) + '0');
+    g_string_append_c (buf, '\n');
+
+    write_result = write_all (buf->str, buf->len, data->status_fd);
+
+    g_string_free (buf, TRUE);
+
+    if (!write_result) {
+       g_set_error_literal (err,
+                            G_FILE_ERROR,
+                            g_file_error_from_errno (errno),
+                            strerror (errno));
+       return FALSE;
+    }
+
+    if (!g_io_channel_read_line (data->command_channel,
+                                &line,
+                                &line_length,
+                                NULL, /* terminator_pos */
+                                err))
+       return FALSE;
+
+    wrote = g_mime_stream_write (response, line, line_length);
+
+    /* TODO: is there a more reliable way to clear the memory
+     * containing a password? */
+    memset (line, 0, line_length);
+
+    g_free (line);
+
+    if (wrote < (ssize_t) line_length) {
+       g_set_error_literal (err,
+                            G_FILE_ERROR,
+                            G_FILE_ERROR_FAILED,
+                            "Failed to write password response");
+       return FALSE;
+    }
+
+    return TRUE;
+}
+
 /* Create a GPG context (GMime 2.6) */
-static notmuch_crypto_context_t *
-create_gpg_context (void)
+static notmuch_bool_t
+create_gpg_context (notmuch_crypto_t *crypto)
 {
     notmuch_crypto_context_t *gpgctx;
+    GMimePasswordRequestFunc password_func;
+    notmuch_password_data_t *password_data;
+
+    /* If the --status-fd and --command-fd options were specified then
+     * we can handle password requests by forwarding them on to the
+     * application that invoked notmuch */
+    if (crypto->status_fd != -1 && crypto->command_fd != -1) {
+       password_func = password_request_cb;
+       password_data = malloc (sizeof (*password_data));
+       if (password_data == NULL)
+           return FALSE;
+       password_data->status_fd = crypto->status_fd;
+       password_data->command_channel =
+           g_io_channel_unix_new (crypto->command_fd);
+    } else {
+       password_func = NULL;
+       password_data = NULL;
+    }

-    /* TODO: GMimePasswordRequestFunc */
-    gpgctx = g_mime_gpg_context_new (NULL, "gpg");
+    gpgctx = g_mime_gpg_context_new (password_func, "gpg");
     if (! gpgctx)
-       return NULL;
+       return FALSE;
+
+    /* The password callback for GMime doesn't seem to provide any
+     * user data pointer so we'll work around it by attaching the data
+     * to the gpg context, which it does pass */
+    if (password_data) {
+       g_object_set_data_full (G_OBJECT (gpgctx),
+                               NOTMUCH_PASSWORD_DATA_KEY,
+                               password_data,
+                               free_password_data_cb);
+    }

     g_mime_gpg_context_set_use_agent ((GMimeGpgContext *) gpgctx, TRUE);
     g_mime_gpg_context_set_always_trust ((GMimeGpgContext *) gpgctx, FALSE);

-    return gpgctx;
+    crypto->gpgctx = gpgctx;
+
+    return TRUE;
 }

 #else /* GMIME_ATLEAST_26 */

 /* Create a GPG context (GMime 2.4) */
-static notmuch_crypto_context_t *
-create_gpg_context (void)
+static notmuch_bool_t
+create_gpg_context (notmuch_crypto_t *crypto)
 {
     GMimeSession *session;
     notmuch_crypto_context_t *gpgctx;
@@ -53,11 +213,13 @@ create_gpg_context (void)
     g_object_unref (session);

     if (! gpgctx)
-       return NULL;
+       return FALSE;

     g_mime_gpg_context_set_always_trust ((GMimeGpgContext *) gpgctx, FALSE);

-    return gpgctx;
+    crypto->gpgctx = gpgctx;
+
+    return TRUE;
 }

 #endif /* GMIME_ATLEAST_26 */
@@ -78,7 +240,7 @@ notmuch_crypto_get_context (notmuch_crypto_t *crypto, const 
char *protocol)
     if (strcasecmp (protocol, "application/pgp-signature") == 0 ||
        strcasecmp (protocol, "application/pgp-encrypted") == 0) {
        if (! crypto->gpgctx) {
-           crypto->gpgctx = create_gpg_context ();
+           create_gpg_context (crypto);
            if (! crypto->gpgctx)
                fprintf (stderr, "Failed to construct gpg context.\n");
        }
diff --git a/notmuch-client.h b/notmuch-client.h
index 5f2a6d0..edf47ce 100644
--- a/notmuch-client.h
+++ b/notmuch-client.h
@@ -80,6 +80,8 @@ typedef struct notmuch_crypto {
     notmuch_crypto_context_t* gpgctx;
     notmuch_bool_t verify;
     notmuch_bool_t decrypt;
+    int status_fd;
+    int command_fd;
 } notmuch_crypto_t;

 typedef struct notmuch_show_params {
diff --git a/notmuch-reply.c b/notmuch-reply.c
index e151f78..d46bec4 100644
--- a/notmuch-reply.c
+++ b/notmuch-reply.c
@@ -718,7 +718,9 @@ notmuch_reply_command (notmuch_config_t *config, int argc, 
char *argv[])
        .part = -1,
        .crypto = {
            .verify = FALSE,
-           .decrypt = FALSE
+           .decrypt = FALSE,
+           .status_fd = -1,
+           .command_fd = -1
        }
     };
     int format = FORMAT_DEFAULT;
@@ -738,6 +740,8 @@ notmuch_reply_command (notmuch_config_t *config, int argc, 
char *argv[])
                                  { "sender", FALSE },
                                  { 0, 0 } } },
        { NOTMUCH_OPT_BOOLEAN, &params.crypto.decrypt, "decrypt", 'd', 0 },
+       { NOTMUCH_OPT_INT, &params.crypto.status_fd, "status-fd", 0, 0 },
+       { NOTMUCH_OPT_INT, &params.crypto.command_fd, "command-fd", 0, 0 },
        { 0, 0, 0, 0, 0 }
     };

diff --git a/notmuch-show.c b/notmuch-show.c
index 62178f7..0a67807 100644
--- a/notmuch-show.c
+++ b/notmuch-show.c
@@ -1076,7 +1076,9 @@ notmuch_show_command (notmuch_config_t *config, int argc, 
char *argv[])
        .output_body = TRUE,
        .crypto = {
            .verify = FALSE,
-           .decrypt = FALSE
+           .decrypt = FALSE,
+           .status_fd = -1,
+           .command_fd = -1
        }
     };
     int format_sel = NOTMUCH_FORMAT_NOT_SPECIFIED;
@@ -1104,6 +1106,8 @@ notmuch_show_command (notmuch_config_t *config, int argc, 
char *argv[])
        { NOTMUCH_OPT_INT, &params.part, "part", 'p', 0 },
        { NOTMUCH_OPT_BOOLEAN, &params.crypto.decrypt, "decrypt", 'd', 0 },
        { NOTMUCH_OPT_BOOLEAN, &params.crypto.verify, "verify", 'v', 0 },
+       { NOTMUCH_OPT_INT, &params.crypto.status_fd, "status-fd", 0, 0 },
+       { NOTMUCH_OPT_INT, &params.crypto.command_fd, "command-fd", 0, 0 },
        { NOTMUCH_OPT_BOOLEAN, &params.output_body, "body", 'b', 0 },
        { 0, 0, 0, 0, 0 }
     };
-- 
1.7.11.3.g3c3efa5

Reply via email to