Add an "--format=(json|text)" command-line option to both notmuch-search and notmuch...
authorScott Robinson <scott@quadhome.com>
Thu, 31 Dec 2009 15:17:40 +0000 (11:17 -0400)
committerCarl Worth <cworth@cworth.org>
Tue, 23 Feb 2010 20:01:12 +0000 (12:01 -0800)
In the case of notmuch-show, "--format=json" also implies
"--entire-thread" as the thread structure is implicit in the emitted
document tree.

As a coincidence to the implementation, multipart message ID numbers are
now incremented with each part printed. This changes the previous
semantics, which were unclear and not necessary related to the actual
ordering of the message parts.

Makefile.local
json.c [new file with mode: 0644]
notmuch-client.h
notmuch-search.c
notmuch-show.c
notmuch.1
notmuch.c
show-message.c

index 04bac838a7b23efe14a5950c9c38129106bb19fe..034b20361307a12514107d3c6a6ada4dbdd9d9be 100644 (file)
@@ -18,7 +18,8 @@ notmuch_client_srcs =         \
        notmuch-tag.c           \
        notmuch-time.c          \
        query-string.c          \
-       show-message.c
+       show-message.c          \
+       json.c
 
 notmuch_client_modules = $(notmuch_client_srcs:.c=.o)
 notmuch: $(notmuch_client_modules) lib/notmuch.a
diff --git a/json.c b/json.c
new file mode 100644 (file)
index 0000000..3960750
--- /dev/null
+++ b/json.c
@@ -0,0 +1,72 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Scott Robinson
+ *
+ * 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 3 of the License, 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/ .
+ *
+ * Authors: Scott Robinson <scott@quadhome.com>
+ *
+ */
+
+#include "notmuch-client.h"
+
+/*
+ * json_quote_str derived from cJSON's print_string_ptr,
+ * Copyright (c) 2009 Dave Gamble
+ */
+
+char *
+json_quote_str(const void *ctx, const char *str)
+{
+    const char *ptr;
+    char *ptr2;
+    char *out;
+    int len = 0;
+
+    if (!str)
+       return NULL;
+
+    for (ptr = str; *ptr; len++, ptr++) {
+       if (*ptr < 32 || *ptr == '\"' || *ptr == '\\')
+           len++;
+    }
+
+    out = talloc_array (ctx, char, len + 3);
+
+    ptr = str;
+    ptr2 = out;
+
+    *ptr2++ = '\"';
+    while (*ptr) {
+           if (*ptr > 31 && *ptr != '\"' && *ptr != '\\') {
+               *ptr2++ = *ptr++;
+           } else {
+               *ptr2++ = '\\';
+               switch (*ptr++) {
+                   case '\"':  *ptr2++ = '\"'; break;
+                   case '\\':  *ptr2++ = '\\'; break;
+                   case '\b':  *ptr2++ = 'b';  break;
+                   case '\f':  *ptr2++ = 'f';  break;
+                   case '\n':  *ptr2++ = 'n';  break;
+                   case '\r':  *ptr2++ = 'r';  break;
+                   case '\t':  *ptr2++ = 't';  break;
+                   default:     ptr2--;        break;
+               }
+           }
+    }
+    *ptr2++ = '\"';
+    *ptr2++ = '\0';
+
+    return out;
+}
index 77766de2cb56cda4bb8ac019426e16380c380839..c80b39cb4ce4faf5b5669b43c80540bce68f31b7 100644 (file)
@@ -123,6 +123,9 @@ notmuch_status_t
 show_message_body (const char *filename,
                   void (*show_part) (GMimeObject *part, int *part_count));
 
+char *
+json_quote_str (const void *ctx, const char *str);
+
 /* notmuch-config.c */
 
 typedef struct _notmuch_config notmuch_config_t;
index dc44eb66bf4a249949dd25f004c7b98b85ff8912..482c6e8ea8c061d7865728c857a30f1dfe31497a 100644 (file)
 
 #include "notmuch-client.h"
 
+typedef struct search_format {
+    const char *results_start;
+    const char *thread_start;
+    void (*thread) (const void *ctx,
+                   const char *id,
+                   const time_t date,
+                   const int matched,
+                   const int total,
+                   const char *authors,
+                   const char *subject);
+    const char *tag_start;
+    const char *tag;
+    const char *tag_sep;
+    const char *tag_end;
+    const char *thread_sep;
+    const char *thread_end;
+    const char *results_end;
+} search_format_t;
+
+static void
+format_thread_text (const void *ctx,
+                   const char *id,
+                   const time_t date,
+                   const int matched,
+                   const int total,
+                   const char *authors,
+                   const char *subject);
+static const search_format_t format_text = {
+    "",
+       "",
+           format_thread_text,
+           " (",
+               "%s", " ",
+           ")", "",
+       "\n",
+    "",
+};
+
+static void
+format_thread_json (const void *ctx,
+                   const char *id,
+                   const time_t date,
+                   const int matched,
+                   const int total,
+                   const char *authors,
+                   const char *subject);
+static const search_format_t format_json = {
+    "[",
+       "{",
+           format_thread_json,
+           "\"tags\": [",
+               "\"%s\"", ", ",
+           "]", ",\n",
+       "}",
+    "]\n",
+};
+
+static void
+format_thread_text (const void *ctx,
+                   const char *id,
+                   const time_t date,
+                   const int matched,
+                   const int total,
+                   const char *authors,
+                   const char *subject)
+{
+    printf ("thread:%s %12s [%d/%d] %s; %s",
+           id,
+           notmuch_time_relative_date (ctx, date),
+           matched,
+           total,
+           authors,
+           subject);
+}
+
+static void
+format_thread_json (const void *ctx,
+                   const char *id,
+                   const time_t date,
+                   const int matched,
+                   const int total,
+                   const char *authors,
+                   const char *subject)
+{
+    struct tm *tm;
+    char timestamp[40];
+    void *ctx_quote = talloc_new (ctx);
+
+    tm = gmtime (&date);
+    if (tm == NULL)
+       INTERNAL_ERROR ("gmtime failed on thread %s.", id);
+
+    if (strftime (timestamp, sizeof (timestamp), "%s", tm) == 0)
+       INTERNAL_ERROR ("strftime failed on thread %s.", id);
+
+    printf ("\"id\": %s,\n"
+           "\"timestamp\": %s,\n"
+           "\"matched\": %d,\n"
+           "\"total\": %d,\n"
+           "\"authors\": %s,\n"
+           "\"subject\": %s,\n",
+           json_quote_str (ctx_quote, id),
+           timestamp,
+           matched,
+           total,
+           json_quote_str (ctx_quote, authors),
+           json_quote_str (ctx_quote, subject));
+
+    talloc_free (ctx_quote);
+}
+
 static void
 do_search_threads (const void *ctx,
+                  const search_format_t *format,
                   notmuch_query_t *query,
                   notmuch_sort_t sort)
 {
@@ -29,7 +141,9 @@ do_search_threads (const void *ctx,
     notmuch_threads_t *threads;
     notmuch_tags_t *tags;
     time_t date;
-    const char *relative_date;
+    int first_thread = 1;
+
+    fputs (format->results_start, stdout);
 
     for (threads = notmuch_query_search_threads (query);
         notmuch_threads_has_more (threads);
@@ -37,6 +151,9 @@ do_search_threads (const void *ctx,
     {
        int first_tag = 1;
 
+       if (! first_thread)
+           fputs (format->thread_sep, stdout);
+
        thread = notmuch_threads_get (threads);
 
        if (sort == NOTMUCH_SORT_OLDEST_FIRST)
@@ -44,30 +161,37 @@ do_search_threads (const void *ctx,
        else
            date = notmuch_thread_get_newest_date (thread);
 
-       relative_date = notmuch_time_relative_date (ctx, date);
+       fputs (format->thread_start, stdout);
+
+       format->thread (ctx,
+                       notmuch_thread_get_thread_id (thread),
+                       date,
+                       notmuch_thread_get_matched_messages (thread),
+                       notmuch_thread_get_total_messages (thread),
+                       notmuch_thread_get_authors (thread),
+                       notmuch_thread_get_subject (thread));
 
-       printf ("thread:%s %12s [%d/%d] %s; %s",
-               notmuch_thread_get_thread_id (thread),
-               relative_date,
-               notmuch_thread_get_matched_messages (thread),
-               notmuch_thread_get_total_messages (thread),
-               notmuch_thread_get_authors (thread),
-               notmuch_thread_get_subject (thread));
+       fputs (format->tag_start, stdout);
 
-       printf (" (");
        for (tags = notmuch_thread_get_tags (thread);
             notmuch_tags_has_more (tags);
             notmuch_tags_advance (tags))
        {
            if (! first_tag)
-               printf (" ");
-           printf ("%s", notmuch_tags_get (tags));
+               fputs (format->tag_sep, stdout);
+           printf (format->tag, notmuch_tags_get (tags));
            first_tag = 0;
        }
-       printf (")\n");
+
+       fputs (format->tag_end, stdout);
+       fputs (format->thread_end, stdout);
+
+       first_thread = 0;
 
        notmuch_thread_destroy (thread);
     }
+
+    fputs (format->results_end, stdout);
 }
 
 int
@@ -79,6 +203,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     char *query_str;
     char *opt;
     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
+    const search_format_t *format = &format_text;
     int i;
 
     for (i = 0; i < argc && argv[i][0] == '-'; i++) {
@@ -96,6 +221,16 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
                fprintf (stderr, "Invalid value for --sort: %s\n", opt);
                return 1;
            }
+       } else if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
+           opt = argv[i] + sizeof ("--format=") - 1;
+           if (strcmp (opt, "text") == 0) {
+               format = &format_text;
+           } else if (strcmp (opt, "json") == 0) {
+               format = &format_json;
+           } else {
+               fprintf (stderr, "Invalid value for --format: %s\n", opt);
+               return 1;
+           }
        } else {
            fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
            return 1;
@@ -132,7 +267,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     notmuch_query_set_sort (query, sort);
 
-    do_search_threads (ctx, query, sort);
+    do_search_threads (ctx, format, query, sort);
 
     notmuch_query_destroy (query);
     notmuch_database_close (notmuch);
index 376aacd7bcbe03be72d3562529f74d05d8efa1a3..1a1d6019eac95713f92976f2cefafd474256866a 100644 (file)
 
 #include "notmuch-client.h"
 
+typedef struct show_format {
+    const char *message_set_start;
+    const char *message_start;
+    void (*message) (const void *ctx,
+                    notmuch_message_t *message,
+                    int indent);
+    const char *header_start;
+    void (*header) (const void *ctx,
+                   notmuch_message_t *message);
+    const char *header_end;
+    const char *body_start;
+    void (*part) (GMimeObject *part,
+                 int *part_count);
+    const char *body_end;
+    const char *message_end;
+    const char *message_set_sep;
+    const char *message_set_end;
+} show_format_t;
+
+static void
+format_message_text (unused (const void *ctx),
+                    notmuch_message_t *message,
+                    int indent);
+static void
+format_headers_text (const void *ctx,
+                    notmuch_message_t *message);
+static void
+format_part_text (GMimeObject *part,
+                 int *part_count);
+static const show_format_t format_text = {
+    "",
+       "\fmessage{ ", format_message_text,
+           "\fheader{\n", format_headers_text, "\fheader}\n",
+           "\fbody{\n", format_part_text, "\fbody}\n",
+       "\fmessage}\n", "",
+    ""
+};
+
+static void
+format_message_json (const void *ctx,
+                    notmuch_message_t *message,
+                    unused (int indent));
+static void
+format_headers_json (const void *ctx,
+                    notmuch_message_t *message);
+static void
+format_part_json (GMimeObject *part,
+                 int *part_count);
+static const show_format_t format_json = {
+    "[",
+       "{", format_message_json,
+           ", \"headers\": {", format_headers_json, "}",
+           ", \"body\": [", format_part_json, "]",
+       "}", ", ",
+    "]"
+};
+
 static const char *
-_get_tags_as_string (void *ctx, notmuch_message_t *message)
+_get_tags_as_string (const void *ctx, notmuch_message_t *message)
 {
     notmuch_tags_t *tags;
     int first = 1;
@@ -48,7 +105,7 @@ _get_tags_as_string (void *ctx, notmuch_message_t *message)
 
 /* Get a nice, single-line summary of message. */
 static const char *
-_get_one_line_summary (void *ctx, notmuch_message_t *message)
+_get_one_line_summary (const void *ctx, notmuch_message_t *message)
 {
     const char *from;
     time_t date;
@@ -67,18 +124,87 @@ _get_one_line_summary (void *ctx, notmuch_message_t *message)
 }
 
 static void
-show_part_content (GMimeObject *part)
+format_message_text (unused (const void *ctx), notmuch_message_t *message, int indent)
+{
+    printf ("id:%s depth:%d match:%d filename:%s\n",
+           notmuch_message_get_message_id (message),
+           indent,
+           notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),
+           notmuch_message_get_filename (message));
+}
+
+static void
+format_message_json (const void *ctx, notmuch_message_t *message, unused (int indent))
+{
+    void *ctx_quote = talloc_new (ctx);
+
+    printf ("\"id\": %s, \"match\": %s, \"filename\": %s",
+           json_quote_str (ctx_quote, notmuch_message_get_message_id (message)),
+           notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? "true" : "false",
+           json_quote_str (ctx_quote, notmuch_message_get_filename (message)));
+
+    talloc_free (ctx_quote);
+}
+
+static void
+format_headers_text (const void *ctx, notmuch_message_t *message)
+{
+    const char *headers[] = {
+       "Subject", "From", "To", "Cc", "Bcc", "Date"
+    };
+    const char *name, *value;
+    unsigned int i;
+
+    printf ("%s\n", _get_one_line_summary (ctx, message));
+
+    for (i = 0; i < ARRAY_SIZE (headers); i++) {
+       name = headers[i];
+       value = notmuch_message_get_header (message, name);
+       if (value)
+           printf ("%s: %s\n", name, value);
+    }
+}
+
+static void
+format_headers_json (const void *ctx, notmuch_message_t *message)
+{
+    const char *headers[] = {
+       "Subject", "From", "To", "Cc", "Bcc", "Date"
+    };
+    const char *name, *value;
+    unsigned int i;
+    int first_header = 1;
+    void *ctx_quote = talloc_new (ctx);
+
+    for (i = 0; i < ARRAY_SIZE (headers); i++) {
+       name = headers[i];
+       value = notmuch_message_get_header (message, name);
+       if (value)
+       {
+           if (!first_header)
+               fputs (", ", stdout);
+           first_header = 0;
+
+           printf ("%s: %s",
+                   json_quote_str (ctx_quote, name),
+                   json_quote_str (ctx_quote, value));
+       }
+    }
+
+    talloc_free (ctx_quote);
+}
+
+static void
+show_part_content (GMimeObject *part, GMimeStream *stream_out)
 {
-    GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
     GMimeStream *stream_filter = NULL;
     GMimeDataWrapper *wrapper;
     const char *charset;
 
     charset = g_mime_object_get_content_type_parameter (part, "charset");
 
-    if (stream_stdout) {
-       g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
-       stream_filter = g_mime_stream_filter_new(stream_stdout);
+    if (stream_out) {
+       stream_filter = g_mime_stream_filter_new(stream_out);
        g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),
                                 g_mime_filter_crlf_new(FALSE, FALSE));
         if (charset) {
@@ -92,15 +218,16 @@ show_part_content (GMimeObject *part)
        g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
     if (stream_filter)
        g_object_unref(stream_filter);
-    if (stream_stdout)
-       g_object_unref(stream_stdout);
 }
 
 static void
-show_part (GMimeObject *part, int *part_count)
+format_part_text (GMimeObject *part, int *part_count)
 {
     GMimeContentDisposition *disposition;
     GMimeContentType *content_type;
+    GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
+
+    g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
 
     disposition = g_mime_object_get_content_disposition (part);
     if (disposition &&
@@ -118,11 +245,14 @@ show_part (GMimeObject *part, int *part_count)
        if (g_mime_content_type_is_type (content_type, "text", "*") &&
            !g_mime_content_type_is_type (content_type, "text", "html"))
        {
-           show_part_content (part);
+           show_part_content (part, stream_stdout);
        }
 
        printf ("\fattachment}\n");
 
+       if (stream_stdout)
+           g_object_unref(stream_stdout);
+
        return;
     }
 
@@ -135,7 +265,7 @@ show_part (GMimeObject *part, int *part_count)
     if (g_mime_content_type_is_type (content_type, "text", "*") &&
        !g_mime_content_type_is_type (content_type, "text", "html"))
     {
-       show_part_content (part);
+       show_part_content (part, stream_stdout);
     }
     else
     {
@@ -144,57 +274,96 @@ show_part (GMimeObject *part, int *part_count)
     }
 
     printf ("\fpart}\n");
+
+    if (stream_stdout)
+       g_object_unref(stream_stdout);
 }
 
 static void
-show_message (void *ctx, notmuch_message_t *message, int indent)
+format_part_json (GMimeObject *part, int *part_count)
 {
-    const char *headers[] = {
-       "Subject", "From", "To", "Cc", "Bcc", "Date"
-    };
-    const char *name, *value;
-    unsigned int i;
+    GMimeContentType *content_type;
+    GMimeContentDisposition *disposition;
+    void *ctx = talloc_new (NULL);
+    GMimeStream *stream_memory = g_mime_stream_mem_new ();
+    GByteArray *part_content;
 
-    printf ("\fmessage{ id:%s depth:%d match:%d filename:%s\n",
-           notmuch_message_get_message_id (message),
-           indent,
-           notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),
-           notmuch_message_get_filename (message));
+    content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
 
-    printf ("\fheader{\n");
+    if (*part_count > 1)
+       fputs (", ", stdout);
 
-    printf ("%s\n", _get_one_line_summary (ctx, message));
+    printf ("{\"id\": %d, \"content-type\": %s",
+           *part_count,
+           json_quote_str (ctx, g_mime_content_type_to_string (content_type)));
 
-    for (i = 0; i < ARRAY_SIZE (headers); i++) {
-       name = headers[i];
-       value = notmuch_message_get_header (message, name);
-       if (value)
-           printf ("%s: %s\n", name, value);
+    disposition = g_mime_object_get_content_disposition (part);
+    if (disposition &&
+       strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
+    {
+       const char *filename = g_mime_part_get_filename (GMIME_PART (part));
+
+       printf (", \"filename\": %s", json_quote_str (ctx, filename));
     }
 
-    printf ("\fheader}\n");
-    printf ("\fbody{\n");
+    if (g_mime_content_type_is_type (content_type, "text", "*") &&
+       !g_mime_content_type_is_type (content_type, "text", "html"))
+    {
+       show_part_content (part, stream_memory);
+       part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
+
+       printf (", \"content\": %s", json_quote_str (ctx, (char *) part_content->data));
+    }
+
+    fputs ("}", stdout);
+
+    talloc_free (ctx);
+    if (stream_memory)
+       g_object_unref (stream_memory);
+}
+
+static void
+show_message (void *ctx, const show_format_t *format, notmuch_message_t *message, int indent)
+{
+    fputs (format->message_start, stdout);
+    if (format->message)
+       format->message(ctx, message, indent);
 
-    show_message_body (notmuch_message_get_filename (message), show_part);
+    fputs (format->header_start, stdout);
+    if (format->header)
+       format->header(ctx, message);
+    fputs (format->header_end, stdout);
 
-    printf ("\fbody}\n");
+    fputs (format->body_start, stdout);
+    if (format->part)
+       show_message_body (notmuch_message_get_filename (message), format->part);
+    fputs (format->body_end, stdout);
 
-    printf ("\fmessage}\n");
+    fputs (format->message_end, stdout);
 }
 
 
 static void
-show_messages (void *ctx, notmuch_messages_t *messages, int indent,
+show_messages (void *ctx, const show_format_t *format, notmuch_messages_t *messages, int indent,
               notmuch_bool_t entire_thread)
 {
     notmuch_message_t *message;
     notmuch_bool_t match;
+    int first_set = 1;
     int next_indent;
 
+    fputs (format->message_set_start, stdout);
+
     for (;
         notmuch_messages_has_more (messages);
         notmuch_messages_advance (messages))
     {
+       if (!first_set)
+           fputs (format->message_set_sep, stdout);
+       first_set = 0;
+
+       fputs (format->message_set_start, stdout);
+
        message = notmuch_messages_get (messages);
 
        match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
@@ -202,15 +371,21 @@ show_messages (void *ctx, notmuch_messages_t *messages, int indent,
        next_indent = indent;
 
        if (match || entire_thread) {
-           show_message (ctx, message, indent);
+           show_message (ctx, format, message, indent);
            next_indent = indent + 1;
+
+           fputs (format->message_set_sep, stdout);
        }
 
-       show_messages (ctx, notmuch_message_get_replies (message),
+       show_messages (ctx, format, notmuch_message_get_replies (message),
                       next_indent, entire_thread);
 
        notmuch_message_destroy (message);
+
+       fputs (format->message_set_end, stdout);
     }
+
+    fputs (format->message_set_end, stdout);
 }
 
 int
@@ -223,15 +398,29 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
     notmuch_thread_t *thread;
     notmuch_messages_t *messages;
     char *query_string;
+    char *opt;
+    const show_format_t *format = &format_text;
     int entire_thread = 0;
     int i;
+    int first_toplevel = 1;
 
     for (i = 0; i < argc && argv[i][0] == '-'; i++) {
        if (strcmp (argv[i], "--") == 0) {
            i++;
            break;
        }
-        if (strcmp(argv[i], "--entire-thread") == 0) {
+       if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
+           opt = argv[i] + sizeof ("--format=") - 1;
+           if (strcmp (opt, "text") == 0) {
+               format = &format_text;
+           } else if (strcmp (opt, "json") == 0) {
+               format = &format_json;
+               entire_thread = 1;
+           } else {
+               fprintf (stderr, "Invalid value for --format: %s\n", opt);
+               return 1;
+           }
+       } else if (STRNCMP_LITERAL (argv[i], "--entire-thread") == 0) {
            entire_thread = 1;
        } else {
            fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
@@ -268,6 +457,8 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
        return 1;
     }
 
+    fputs (format->message_set_start, stdout);
+
     for (threads = notmuch_query_search_threads (query);
         notmuch_threads_has_more (threads);
         notmuch_threads_advance (threads))
@@ -280,11 +471,18 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
            INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
                            notmuch_thread_get_thread_id (thread));
 
-       show_messages (ctx, messages, 0, entire_thread);
+       if (!first_toplevel)
+           fputs (format->message_set_sep, stdout);
+       first_toplevel = 0;
+
+       show_messages (ctx, format, messages, 0, entire_thread);
 
        notmuch_thread_destroy (thread);
+
     }
 
+    fputs (format->message_set_end, stdout);
+
     notmuch_query_destroy (query);
     notmuch_database_close (notmuch);
 
index 8b773e568d7c969c4e4f2da3e1f4b0039d2b0126..2df320d8657c2cedb22c0fb6502c5a95451a696c 100644 (file)
--- a/notmuch.1
+++ b/notmuch.1
@@ -146,6 +146,12 @@ Supported options for
 include
 .RS 4
 .TP 4
+.BR \-\-format= ( json | text )
+
+Presents the results in either JSON or plain-text (default).
+.RE
+.RS 4
+.TP 4
 .BR \-\-sort= ( newest\-first | oldest\-first )
 
 This option can be used to present results in either chronological order
@@ -194,7 +200,14 @@ matched message will be displayed.
 .RE
 
 .RS 4
-The  output format  is plain-text,  with all  text-content  MIME parts
+.TP 4
+.B \-\-format=(json|text)
+
+.RS 4
+.TP 4
+.B text
+
+The default plain-text format  has  text-content  MIME parts
 decoded. Various components in the output,
 .RB ( message ", " header ", " body ", " attachment ", and MIME " part ),
 will be delimited by easily-parsed markers. Each marker consists of a
@@ -202,6 +215,18 @@ Control-L character (ASCII decimal 12), the name of the marker, and
 then either an opening or closing brace, ('{' or '}'), to either open
 or close the component.
 
+.RE
+.RS 4
+.TP 4
+.B json
+
+Format output as Javascript Object Notation (JSON). JSON output always
+includes all messages in a matching thread; in effect
+.B \-\-format=json
+implies
+.B \-\-entire\-thread
+
+.RE
 A common use of
 .B notmuch show
 is to display a single thread of email messages. For this, use a
index 87479f81056b53cef82859c61adb7794f19ecf79..dca8d3829f6afe8f9057dbba41d366c942b808ce 100644 (file)
--- a/notmuch.c
+++ b/notmuch.c
@@ -157,6 +157,11 @@ command_t commands[] = {
       "\n"
       "\t\tSupported options for search include:\n"
       "\n"
+      "\t\t--format=(json|text)\n"
+      "\n"
+      "\t\t\tPresents the results in either JSON or\n"
+      "\t\t\tplain-text (default)\n"
+      "\n"
       "\t\t--sort=(newest-first|oldest-first)\n"
       "\n"
       "\t\t\tPresent results in either chronological order\n"
@@ -181,13 +186,23 @@ command_t commands[] = {
       "\t\t\tall messages in the same thread as any matched\n"
       "\t\t\tmessage will be displayed.\n"
       "\n"
-      "\t\tThe output format is plain-text, with all text-content\n"
-      "\t\tMIME parts decoded. Various components in the output,\n"
-      "\t\t('message', 'header', 'body', 'attachment', and MIME 'part')\n"
-      "\t\tare delimited by easily-parsed markers. Each marker consists\n"
-      "\t\tof a Control-L character (ASCII decimal 12), the name of\n"
-      "\t\tthe marker, and then either an opening or closing brace,\n"
-      "\t\t'{' or '}' to either open or close the component.\n"
+      "\t\t--format=(json|text)\n"
+      "\n"
+      "\t\t\ttext\t(default)\n"
+      "\n"
+      "\t\t\tThe plain-text has all text-content MIME parts decoded.\n"
+      "\t\t\tVarious components in the output, ('message', 'header',\n"
+      "\t\t\t'body', 'attachment', and MIME 'part') are delimited by\n"
+      "\t\t\teasily-parsed markers. Each marker consists of a Control-L\n"
+      "\t\t\tcharacter (ASCII decimal 12), the name of the marker, and\n"
+      "\t\t\tthen either an opening or closing brace, '{' or '}' to\n"
+      "\t\t\teither open or close the component.\n"
+      "\n"
+      "\t\t\tjson\n"
+      "\n"
+      "\t\t\tFormat output as Javascript Object Notation (JSON).\n"
+      "\t\t\tJSON output always includes all messages in a matching,\n"
+      "\t\t\tthread i.e. '--output=json' implies '--entire-thread'\n"
       "\n"
       "\t\tA common use of \"notmuch show\" is to display a single\n"
       "\t\tthread of email messages. For this, use a search term of\n"
index 784981b6b190166564cfd82c2114bb1b1c3ec9d1..05ced9c421c5ec780563312765ea43fa2a5f87df 100644 (file)
@@ -26,8 +26,6 @@ static void
 show_message_part (GMimeObject *part, int *part_count,
                   void (*show_part) (GMimeObject *part, int *part_count))
 {
-    *part_count = *part_count + 1;
-
     if (GMIME_IS_MULTIPART (part)) {
        GMimeMultipart *multipart = GMIME_MULTIPART (part);
        int i;
@@ -56,6 +54,8 @@ show_message_part (GMimeObject *part, int *part_count,
        return;
     }
 
+    *part_count = *part_count + 1;
+
     (*show_part) (part, part_count);
 }