[notmuch] [PATCH] Add an "--output=(json|text|)" command-line option to both notmuch...
authordavid <david@tethera.net>
Fri, 18 Dec 2009 12:59:55 +0000 (08:59 +2000)
committerW. Trevor King <wking@tremily.us>
Fri, 7 Nov 2014 17:35:53 +0000 (09:35 -0800)
51/d849f6798c34e91ef581fd43d3650f0b01d237 [new file with mode: 0644]

diff --git a/51/d849f6798c34e91ef581fd43d3650f0b01d237 b/51/d849f6798c34e91ef581fd43d3650f0b01d237
new file mode 100644 (file)
index 0000000..42b4be5
--- /dev/null
@@ -0,0 +1,912 @@
+Return-Path: <bremner@pivot.cs.unb.ca>\r
+X-Original-To: notmuch@notmuchmail.org\r
+Delivered-To: notmuch@notmuchmail.org\r
+Received: from localhost (localhost [127.0.0.1])\r
+       by olra.theworths.org (Postfix) with ESMTP id 6449F431FBF\r
+       for <notmuch@notmuchmail.org>; Fri, 18 Dec 2009 05:00:18 -0800 (PST)\r
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
+Received: from olra.theworths.org ([127.0.0.1])\r
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
+       with ESMTP id wpoolJXwoZYh for <notmuch@notmuchmail.org>;\r
+       Fri, 18 Dec 2009 05:00:16 -0800 (PST)\r
+Received: from pivot.cs.unb.ca (pivot.cs.unb.ca [131.202.240.57])\r
+       by olra.theworths.org (Postfix) with ESMTP id 5493D431FAE\r
+       for <notmuch@notmuchmail.org>; Fri, 18 Dec 2009 05:00:16 -0800 (PST)\r
+Received: from\r
+       fctnnbsc30w-142167182194.pppoe-dynamic.high-speed.nb.bellaliant.net\r
+       ([142.167.182.194] helo=localhost)\r
+       by pivot.cs.unb.ca with esmtpsa (TLS1.0:RSA_AES_256_CBC_SHA1:32)\r
+       (Exim 4.69) (envelope-from <bremner@pivot.cs.unb.ca>)\r
+       id 1NLcRK-0006j6-OQ; Fri, 18 Dec 2009 09:00:15 -0400\r
+Received: from bremner by localhost with local (Exim 4.69)\r
+       (envelope-from <bremner@pivot.cs.unb.ca>)\r
+       id 1NLcRF-0001Qy-6t; Fri, 18 Dec 2009 09:00:09 -0400\r
+From: david@tethera.net\r
+To: notmuch@notmuchmail.org\r
+Date: Fri, 18 Dec 2009 08:59:55 -0400\r
+Message-Id: <1261141195-5469-1-git-send-email-david@tethera.net>\r
+X-Mailer: git-send-email 1.6.5.3\r
+In-Reply-To: <1261114167-sup-8228@lisa>\r
+References: <1261114167-sup-8228@lisa>\r
+X-Sender-Verified: bremner@pivot.cs.unb.ca\r
+Subject: [notmuch] [PATCH] Add an "--output=(json|text|)" command-line\r
+       option to both notmuch-search and notmuch-show.\r
+X-BeenThere: notmuch@notmuchmail.org\r
+X-Mailman-Version: 2.1.12\r
+Precedence: list\r
+List-Id: "Use and development of the notmuch mail system."\r
+       <notmuch.notmuchmail.org>\r
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
+List-Post: <mailto:notmuch@notmuchmail.org>\r
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
+X-List-Received-Date: Fri, 18 Dec 2009 13:00:18 -0000\r
+\r
+From: Scott Robinson <scott@quadhome.com>\r
+\r
+In the case of notmuch-show, "--output=json" also implies\r
+"--entire-thread" as the thread structure is implicit in the emitted\r
+document tree.\r
+\r
+As a coincidence to the implementation, multipart message ID numbers are\r
+now incremented with each part printed. This changes the previous\r
+semantics, which were unclear and not necessary related to the actual\r
+ordering of the message parts.\r
+\r
+Edited-By: David Bremner <david@tethera.net>\r
+Reviewed-By: David Bremner <david@tethera.net>\r
+---\r
+\r
+It took me a little work to apply Scott's patch, so rather than asking\r
+him to resend it from git-send-email, I am just sending. I hope no-one\r
+is offended (much).\r
+\r
+Other than manually extracting the patch from the output of notmuch\r
+show (for me the message arrived base64 encoded), I deleted trailing\r
+whitespace on line 465. \r
+\r
+It compiles, it doesn't seem to screw up the original output, and at\r
+least in a few tests, it generates parseable json. Yay!.\r
+\r
+I'm thinking that the patch I sent out last night to only dump message\r
+ids could be reworked to use the framework of this patch.  I also\r
+think it would be reasonably simple to add an --output=mbox option,\r
+for archiving and so on.\r
+\r
+ Makefile.local   |    3 +-\r
+ json.c           |   73 ++++++++++++++\r
+ notmuch-client.h |    3 +\r
+ notmuch-search.c |  163 +++++++++++++++++++++++++++++---\r
+ notmuch-show.c   |  275 ++++++++++++++++++++++++++++++++++++++++++++++--------\r
+ notmuch.c        |   24 ++++--\r
+ show-message.c   |    4 +-\r
+ 7 files changed, 481 insertions(+), 64 deletions(-)\r
+ create mode 100644 json.c\r
+\r
+diff --git a/Makefile.local b/Makefile.local\r
+index 933ff4c..53b474b 100644\r
+--- a/Makefile.local\r
++++ b/Makefile.local\r
+@@ -18,7 +18,8 @@ notmuch_client_srcs =                \\r
+       notmuch-tag.c           \\r
+       notmuch-time.c          \\r
+       query-string.c          \\r
+-      show-message.c\r
++      show-message.c          \\r
++      json.c\r
\r
+ notmuch_client_modules = $(notmuch_client_srcs:.c=.o)\r
+ notmuch: $(notmuch_client_modules) lib/notmuch.a\r
+diff --git a/json.c b/json.c\r
+new file mode 100644\r
+index 0000000..ee563d6\r
+--- /dev/null\r
++++ b/json.c\r
+@@ -0,0 +1,73 @@\r
++/* notmuch - Not much of an email program, (just index and search)\r
++ *\r
++ * Copyright © 2009 Carl Worth\r
++ * Copyright © 2009 Keith Packard\r
++ *\r
++ * This program is free software: you can redistribute it and/or modify\r
++ * it under the terms of the GNU General Public License as published by\r
++ * the Free Software Foundation, either version 3 of the License, or\r
++ * (at your option) any later version.\r
++ *\r
++ * This program is distributed in the hope that it will be useful,\r
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
++ * GNU General Public License for more details.\r
++ *\r
++ * You should have received a copy of the GNU General Public License\r
++ * along with this program.  If not, see http://www.gnu.org/licenses/ .\r
++ *\r
++ * Authors: Carl Worth <cworth@cworth.org>\r
++ *        Keith Packard <keithp@keithp.com>\r
++ */\r
++\r
++#include "notmuch-client.h"\r
++\r
++/*\r
++ * json_quote_str derived from cJSON's print_string_ptr,\r
++ * Copyright (c) 2009 Dave Gamble\r
++ */\r
++\r
++char *\r
++json_quote_str(const void *ctx, const char *str)\r
++{\r
++    const char *ptr;\r
++    char *ptr2;\r
++    char *out;\r
++    int len = 0;\r
++\r
++    if (!str)\r
++      return NULL;\r
++\r
++    for (ptr = str; *ptr; len++, ptr++) {\r
++      if (*ptr < 32 || *ptr == '\"' || *ptr == '\\')\r
++          len++;\r
++    }\r
++\r
++    out = talloc_array (ctx, char, len + 3);\r
++\r
++    ptr = str;\r
++    ptr2 = out;\r
++\r
++    *ptr2++ = '\"';\r
++    while (*ptr) {\r
++          if (*ptr > 31 && *ptr != '\"' && *ptr != '\\') {\r
++              *ptr2++ = *ptr++;\r
++          } else {\r
++              *ptr2++ = '\\';\r
++              switch (*ptr++) {\r
++                  case '\"':  *ptr2++ = '\"'; break;\r
++                  case '\\':  *ptr2++ = '\\'; break;\r
++                  case '\b':  *ptr2++ = 'b';  break;\r
++                  case '\f':  *ptr2++ = 'f';  break;\r
++                  case '\n':  *ptr2++ = 'n';  break;\r
++                  case '\r':  *ptr2++ = 'r';  break;\r
++                  case '\t':  *ptr2++ = 't';  break;\r
++                  default:     ptr2--;        break;\r
++              }\r
++          }\r
++    }\r
++    *ptr2++ = '\"';\r
++    *ptr2++ = '\0';\r
++\r
++    return out;\r
++}\r
+diff --git a/notmuch-client.h b/notmuch-client.h\r
+index 50a30fe..7b844b9 100644\r
+--- a/notmuch-client.h\r
++++ b/notmuch-client.h\r
+@@ -143,6 +143,9 @@ notmuch_status_t\r
+ show_message_body (const char *filename,\r
+                  void (*show_part) (GMimeObject *part, int *part_count));\r
\r
++char *\r
++json_quote_str (const void *ctx, const char *str);\r
++\r
+ /* notmuch-config.c */\r
\r
+ typedef struct _notmuch_config notmuch_config_t;\r
+diff --git a/notmuch-search.c b/notmuch-search.c\r
+index dc44eb6..e243747 100644\r
+--- a/notmuch-search.c\r
++++ b/notmuch-search.c\r
+@@ -20,8 +20,120 @@\r
\r
+ #include "notmuch-client.h"\r
\r
++typedef struct search_format {\r
++    const char *results_start;\r
++    const char *thread_start;\r
++    void (*thread) (const void *ctx,\r
++                  const char *id,\r
++                  const time_t date,\r
++                  const int matched,\r
++                  const int total,\r
++                  const char *authors,\r
++                  const char *subject);\r
++    const char *tag_start;\r
++    const char *tag;\r
++    const char *tag_sep;\r
++    const char *tag_end;\r
++    const char *thread_sep;\r
++    const char *thread_end;\r
++    const char *results_end;\r
++} search_format_t;\r
++\r
++static void\r
++format_thread_text (const void *ctx,\r
++                  const char *id,\r
++                  const time_t date,\r
++                  const int matched,\r
++                  const int total,\r
++                  const char *authors,\r
++                  const char *subject);\r
++static const search_format_t format_text = {\r
++    "",\r
++      "",\r
++          format_thread_text,\r
++          " (",\r
++              "%s", " ",\r
++          ")", "",\r
++      "\n",\r
++    "",\r
++};\r
++\r
++static void\r
++format_thread_json (const void *ctx,\r
++                  const char *id,\r
++                  const time_t date,\r
++                  const int matched,\r
++                  const int total,\r
++                  const char *authors,\r
++                  const char *subject);\r
++static const search_format_t format_json = {\r
++    "[",\r
++      "{",\r
++          format_thread_json,\r
++          "\"tags\": [",\r
++              "\"%s\"", ", ",\r
++          "]", ",\n",\r
++      "}",\r
++    "]\n",\r
++};\r
++\r
++static void\r
++format_thread_text (const void *ctx,\r
++                  const char *id,\r
++                  const time_t date,\r
++                  const int matched,\r
++                  const int total,\r
++                  const char *authors,\r
++                  const char *subject)\r
++{\r
++    printf ("thread:%s %12s [%d/%d] %s; %s",\r
++          id,\r
++          notmuch_time_relative_date (ctx, date),\r
++          matched,\r
++          total,\r
++          authors,\r
++          subject);\r
++}\r
++\r
++static void\r
++format_thread_json (const void *ctx,\r
++                  const char *id,\r
++                  const time_t date,\r
++                  const int matched,\r
++                  const int total,\r
++                  const char *authors,\r
++                  const char *subject)\r
++{\r
++    struct tm *tm;\r
++    char timestamp[40];\r
++    void *ctx_quote = talloc_new (ctx);\r
++\r
++    tm = gmtime (&date);\r
++    if (tm == NULL)\r
++      INTERNAL_ERROR ("gmtime failed on thread %s.", id);\r
++\r
++    if (strftime (timestamp, sizeof (timestamp), "%s", tm) == 0)\r
++      INTERNAL_ERROR ("strftime failed on thread %s.", id);\r
++\r
++    printf ("\"id\": %s,\n"\r
++          "\"timestamp\": %s,\n"\r
++          "\"matched\": %d,\n"\r
++          "\"total\": %d,\n"\r
++          "\"authors\": %s,\n"\r
++          "\"subject\": %s,\n",\r
++          json_quote_str (ctx_quote, id),\r
++          timestamp,\r
++          matched,\r
++          total,\r
++          json_quote_str (ctx_quote, authors),\r
++          json_quote_str (ctx_quote, subject));\r
++\r
++    talloc_free (ctx_quote);\r
++}\r
++\r
+ static void\r
+ do_search_threads (const void *ctx,\r
++                 const search_format_t *format,\r
+                  notmuch_query_t *query,\r
+                  notmuch_sort_t sort)\r
+ {\r
+@@ -29,7 +141,9 @@ do_search_threads (const void *ctx,\r
+     notmuch_threads_t *threads;\r
+     notmuch_tags_t *tags;\r
+     time_t date;\r
+-    const char *relative_date;\r
++    int first_thread = 1;\r
++\r
++    fputs (format->results_start, stdout);\r
\r
+     for (threads = notmuch_query_search_threads (query);\r
+        notmuch_threads_has_more (threads);\r
+@@ -37,6 +151,9 @@ do_search_threads (const void *ctx,\r
+     {\r
+       int first_tag = 1;\r
\r
++      if (! first_thread)\r
++          fputs (format->thread_sep, stdout);\r
++\r
+       thread = notmuch_threads_get (threads);\r
\r
+       if (sort == NOTMUCH_SORT_OLDEST_FIRST)\r
+@@ -44,30 +161,37 @@ do_search_threads (const void *ctx,\r
+       else\r
+           date = notmuch_thread_get_newest_date (thread);\r
\r
+-      relative_date = notmuch_time_relative_date (ctx, date);\r
++      fputs (format->thread_start, stdout);\r
++\r
++      format->thread (ctx,\r
++                      notmuch_thread_get_thread_id (thread),\r
++                      date,\r
++                      notmuch_thread_get_matched_messages (thread),\r
++                      notmuch_thread_get_total_messages (thread),\r
++                      notmuch_thread_get_authors (thread),\r
++                      notmuch_thread_get_subject (thread));\r
\r
+-      printf ("thread:%s %12s [%d/%d] %s; %s",\r
+-              notmuch_thread_get_thread_id (thread),\r
+-              relative_date,\r
+-              notmuch_thread_get_matched_messages (thread),\r
+-              notmuch_thread_get_total_messages (thread),\r
+-              notmuch_thread_get_authors (thread),\r
+-              notmuch_thread_get_subject (thread));\r
++      fputs (format->tag_start, stdout);\r
\r
+-      printf (" (");\r
+       for (tags = notmuch_thread_get_tags (thread);\r
+            notmuch_tags_has_more (tags);\r
+            notmuch_tags_advance (tags))\r
+       {\r
+           if (! first_tag)\r
+-              printf (" ");\r
+-          printf ("%s", notmuch_tags_get (tags));\r
++              fputs (format->tag_sep, stdout);\r
++          printf (format->tag, notmuch_tags_get (tags));\r
+           first_tag = 0;\r
+       }\r
+-      printf (")\n");\r
++\r
++      fputs (format->tag_end, stdout);\r
++      fputs (format->thread_end, stdout);\r
++\r
++      first_thread = 0;\r
\r
+       notmuch_thread_destroy (thread);\r
+     }\r
++\r
++    fputs (format->results_end, stdout);\r
+ }\r
\r
+ int\r
+@@ -79,6 +203,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])\r
+     char *query_str;\r
+     char *opt;\r
+     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;\r
++    const search_format_t *format = &format_text;\r
+     int i;\r
\r
+     for (i = 0; i < argc && argv[i][0] == '-'; i++) {\r
+@@ -96,6 +221,16 @@ notmuch_search_command (void *ctx, int argc, char *argv[])\r
+               fprintf (stderr, "Invalid value for --sort: %s\n", opt);\r
+               return 1;\r
+           }\r
++      } else if (STRNCMP_LITERAL (argv[i], "--output=") == 0) {\r
++          opt = argv[i] + sizeof ("--output=") - 1;\r
++          if (strcmp (opt, "text") == 0) {\r
++              format = &format_text;\r
++          } else if (strcmp (opt, "json") == 0) {\r
++              format = &format_json;\r
++          } else {\r
++              fprintf (stderr, "Invalid value for --output: %s\n", opt);\r
++              return 1;\r
++          }\r
+       } else {\r
+           fprintf (stderr, "Unrecognized option: %s\n", argv[i]);\r
+           return 1;\r
+@@ -132,7 +267,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])\r
\r
+     notmuch_query_set_sort (query, sort);\r
\r
+-    do_search_threads (ctx, query, sort);\r
++    do_search_threads (ctx, format, query, sort);\r
\r
+     notmuch_query_destroy (query);\r
+     notmuch_database_close (notmuch);\r
+diff --git a/notmuch-show.c b/notmuch-show.c\r
+index 376aacd..b5b3eba 100644\r
+--- a/notmuch-show.c\r
++++ b/notmuch-show.c\r
+@@ -20,8 +20,65 @@\r
\r
+ #include "notmuch-client.h"\r
\r
++typedef struct show_format {\r
++    const char *message_set_start;\r
++    const char *message_start;\r
++    void (*message) (const void *ctx,\r
++                   notmuch_message_t *message,\r
++                   int indent);\r
++    const char *header_start;\r
++    void (*header) (const void *ctx,\r
++                  notmuch_message_t *message);\r
++    const char *header_end;\r
++    const char *body_start;\r
++    void (*part) (GMimeObject *part,\r
++                int *part_count);\r
++    const char *body_end;\r
++    const char *message_end;\r
++    const char *message_set_sep;\r
++    const char *message_set_end;\r
++} show_format_t;\r
++\r
++static void\r
++format_message_text (unused (const void *ctx),\r
++                   notmuch_message_t *message,\r
++                   int indent);\r
++static void\r
++format_headers_text (const void *ctx,\r
++                   notmuch_message_t *message);\r
++static void\r
++format_part_text (GMimeObject *part,\r
++                int *part_count);\r
++static const show_format_t format_text = {\r
++    "",\r
++      "\fmessage{ ", format_message_text,\r
++          "\fheader{\n", format_headers_text, "\fheader}\n",\r
++          "\fbody{\n", format_part_text, "\fbody}\n",\r
++      "\fmessage}\n", "",\r
++    ""\r
++};\r
++\r
++static void\r
++format_message_json (const void *ctx,\r
++                   notmuch_message_t *message,\r
++                   unused (int indent));\r
++static void\r
++format_headers_json (const void *ctx,\r
++                   notmuch_message_t *message);\r
++static void\r
++format_part_json (GMimeObject *part,\r
++                int *part_count);\r
++static const show_format_t format_json = {\r
++    "[",\r
++      "{", format_message_json,\r
++          ", \"headers\": {", format_headers_json, "}",\r
++          ", \"body\": [", format_part_json, "]",\r
++      "}", ", ",\r
++    "]"\r
++};\r
++\r
+ static const char *\r
+-_get_tags_as_string (void *ctx, notmuch_message_t *message)\r
++_get_tags_as_string (const void *ctx, notmuch_message_t *message)\r
+ {\r
+     notmuch_tags_t *tags;\r
+     int first = 1;\r
+@@ -48,7 +105,7 @@ _get_tags_as_string (void *ctx, notmuch_message_t *message)\r
\r
+ /* Get a nice, single-line summary of message. */\r
+ static const char *\r
+-_get_one_line_summary (void *ctx, notmuch_message_t *message)\r
++_get_one_line_summary (const void *ctx, notmuch_message_t *message)\r
+ {\r
+     const char *from;\r
+     time_t date;\r
+@@ -67,18 +124,87 @@ _get_one_line_summary (void *ctx, notmuch_message_t *message)\r
+ }\r
\r
+ static void\r
+-show_part_content (GMimeObject *part)\r
++format_message_text (unused (const void *ctx), notmuch_message_t *message, int indent)\r
++{\r
++    printf ("id:%s depth:%d match:%d filename:%s\n",\r
++          notmuch_message_get_message_id (message),\r
++          indent,\r
++          notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),\r
++          notmuch_message_get_filename (message));\r
++}\r
++\r
++static void\r
++format_message_json (const void *ctx, notmuch_message_t *message, unused (int indent))\r
++{\r
++    void *ctx_quote = talloc_new (ctx);\r
++\r
++    printf ("\"id\": %s, \"match\": %s, \"filename\": %s",\r
++          json_quote_str (ctx_quote, notmuch_message_get_message_id (message)),\r
++          notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? "true" : "false",\r
++          json_quote_str (ctx_quote, notmuch_message_get_filename (message)));\r
++\r
++    talloc_free (ctx_quote);\r
++}\r
++\r
++static void\r
++format_headers_text (const void *ctx, notmuch_message_t *message)\r
++{\r
++    const char *headers[] = {\r
++      "Subject", "From", "To", "Cc", "Bcc", "Date"\r
++    };\r
++    const char *name, *value;\r
++    unsigned int i;\r
++\r
++    printf ("%s\n", _get_one_line_summary (ctx, message));\r
++\r
++    for (i = 0; i < ARRAY_SIZE (headers); i++) {\r
++      name = headers[i];\r
++      value = notmuch_message_get_header (message, name);\r
++      if (value)\r
++          printf ("%s: %s\n", name, value);\r
++    }\r
++}\r
++\r
++static void\r
++format_headers_json (const void *ctx, notmuch_message_t *message)\r
++{\r
++    const char *headers[] = {\r
++      "Subject", "From", "To", "Cc", "Bcc", "Date"\r
++    };\r
++    const char *name, *value;\r
++    unsigned int i;\r
++    int first_header = 1;\r
++    void *ctx_quote = talloc_new (ctx);\r
++\r
++    for (i = 0; i < ARRAY_SIZE (headers); i++) {\r
++      name = headers[i];\r
++      value = notmuch_message_get_header (message, name);\r
++      if (value)\r
++      {\r
++          if (!first_header)\r
++              fputs (", ", stdout);\r
++          first_header = 0;\r
++\r
++          printf ("%s: %s",\r
++                  json_quote_str (ctx_quote, name),\r
++                  json_quote_str (ctx_quote, value));\r
++      }\r
++    }\r
++\r
++    talloc_free (ctx_quote);\r
++}\r
++\r
++static void\r
++show_part_content (GMimeObject *part, GMimeStream *stream_out)\r
+ {\r
+-    GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);\r
+     GMimeStream *stream_filter = NULL;\r
+     GMimeDataWrapper *wrapper;\r
+     const char *charset;\r
\r
+     charset = g_mime_object_get_content_type_parameter (part, "charset");\r
\r
+-    if (stream_stdout) {\r
+-      g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);\r
+-      stream_filter = g_mime_stream_filter_new(stream_stdout);\r
++    if (stream_out) {\r
++      stream_filter = g_mime_stream_filter_new(stream_out);\r
+       g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),\r
+                                g_mime_filter_crlf_new(FALSE, FALSE));\r
+         if (charset) {\r
+@@ -92,15 +218,16 @@ show_part_content (GMimeObject *part)\r
+       g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);\r
+     if (stream_filter)\r
+       g_object_unref(stream_filter);\r
+-    if (stream_stdout)\r
+-      g_object_unref(stream_stdout);\r
+ }\r
\r
+ static void\r
+-show_part (GMimeObject *part, int *part_count)\r
++format_part_text (GMimeObject *part, int *part_count)\r
+ {\r
+     GMimeContentDisposition *disposition;\r
+     GMimeContentType *content_type;\r
++    GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);\r
++\r
++    g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);\r
\r
+     disposition = g_mime_object_get_content_disposition (part);\r
+     if (disposition &&\r
+@@ -118,11 +245,14 @@ show_part (GMimeObject *part, int *part_count)\r
+       if (g_mime_content_type_is_type (content_type, "text", "*") &&\r
+           !g_mime_content_type_is_type (content_type, "text", "html"))\r
+       {\r
+-          show_part_content (part);\r
++          show_part_content (part, stream_stdout);\r
+       }\r
\r
+       printf ("\fattachment}\n");\r
\r
++      if (stream_stdout)\r
++          g_object_unref(stream_stdout);\r
++\r
+       return;\r
+     }\r
\r
+@@ -135,7 +265,7 @@ show_part (GMimeObject *part, int *part_count)\r
+     if (g_mime_content_type_is_type (content_type, "text", "*") &&\r
+       !g_mime_content_type_is_type (content_type, "text", "html"))\r
+     {\r
+-      show_part_content (part);\r
++      show_part_content (part, stream_stdout);\r
+     }\r
+     else\r
+     {\r
+@@ -144,57 +274,93 @@ show_part (GMimeObject *part, int *part_count)\r
+     }\r
\r
+     printf ("\fpart}\n");\r
++\r
++    if (stream_stdout)\r
++      g_object_unref(stream_stdout);\r
+ }\r
\r
+ static void\r
+-show_message (void *ctx, notmuch_message_t *message, int indent)\r
++format_part_json (GMimeObject *part, int *part_count)\r
+ {\r
+-    const char *headers[] = {\r
+-      "Subject", "From", "To", "Cc", "Bcc", "Date"\r
+-    };\r
+-    const char *name, *value;\r
+-    unsigned int i;\r
++    GMimeContentType *content_type;\r
++    GMimeContentDisposition *disposition;\r
++    void *ctx = talloc_new (NULL);\r
++    GMimeStream *stream_memory = g_mime_stream_mem_new ();\r
++    GByteArray *part_content;\r
\r
+-    printf ("\fmessage{ id:%s depth:%d match:%d filename:%s\n",\r
+-          notmuch_message_get_message_id (message),\r
+-          indent,\r
+-          notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),\r
+-          notmuch_message_get_filename (message));\r
++    content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));\r
\r
+-    printf ("\fheader{\n");\r
++    if (*part_count > 1)\r
++      fputs (", ", stdout);\r
\r
+-    printf ("%s\n", _get_one_line_summary (ctx, message));\r
++    printf ("{\"id\": %d, \"content-type\": %s",\r
++          *part_count,\r
++          json_quote_str (ctx, g_mime_content_type_to_string (content_type)));\r
\r
+-    for (i = 0; i < ARRAY_SIZE (headers); i++) {\r
+-      name = headers[i];\r
+-      value = notmuch_message_get_header (message, name);\r
+-      if (value)\r
+-          printf ("%s: %s\n", name, value);\r
++    disposition = g_mime_object_get_content_disposition (part);\r
++    if (disposition &&\r
++      strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)\r
++    {\r
++      const char *filename = g_mime_part_get_filename (GMIME_PART (part));\r
++\r
++      printf (", \"filename\": %s", json_quote_str (ctx, filename));\r
+     }\r
\r
+-    printf ("\fheader}\n");\r
+-    printf ("\fbody{\n");\r
++    if (g_mime_content_type_is_type (content_type, "text", "*") &&\r
++      !g_mime_content_type_is_type (content_type, "text", "html"))\r
++    {\r
++      show_part_content (part, stream_memory);\r
++      part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));\r
++\r
++      printf (", \"content\": %s", json_quote_str (ctx, (char *) part_content->data));\r
++    }\r
++\r
++    fputs ("}", stdout);\r
++\r
++    talloc_free (ctx);\r
++    if (stream_memory)\r
++      g_object_unref (stream_memory);\r
++}\r
++\r
++static void\r
++show_message (void *ctx, const show_format_t *format, notmuch_message_t *message, int indent)\r
++{\r
++    fputs (format->message_start, stdout);\r
++    format->message(ctx, message, indent);\r
\r
+-    show_message_body (notmuch_message_get_filename (message), show_part);\r
++    fputs (format->header_start, stdout);\r
++    format->header(ctx, message);\r
++    fputs (format->header_end, stdout);\r
\r
+-    printf ("\fbody}\n");\r
++    fputs (format->body_start, stdout);\r
++    show_message_body (notmuch_message_get_filename (message), format->part);\r
++    fputs (format->body_end, stdout);\r
\r
+-    printf ("\fmessage}\n");\r
++    fputs (format->message_end, stdout);\r
+ }\r
\r
\r
+ static void\r
+-show_messages (void *ctx, notmuch_messages_t *messages, int indent,\r
++show_messages (void *ctx, const show_format_t *format, notmuch_messages_t *messages, int indent,\r
+              notmuch_bool_t entire_thread)\r
+ {\r
+     notmuch_message_t *message;\r
+     notmuch_bool_t match;\r
++    int first_set = 1;\r
+     int next_indent;\r
\r
++    fputs (format->message_set_start, stdout);\r
++\r
+     for (;\r
+        notmuch_messages_has_more (messages);\r
+        notmuch_messages_advance (messages))\r
+     {\r
++      if (!first_set)\r
++          fputs (format->message_set_sep, stdout);\r
++      first_set = 0;\r
++\r
++      fputs (format->message_set_start, stdout);\r
++\r
+       message = notmuch_messages_get (messages);\r
\r
+       match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);\r
+@@ -202,15 +368,21 @@ show_messages (void *ctx, notmuch_messages_t *messages, int indent,\r
+       next_indent = indent;\r
\r
+       if (match || entire_thread) {\r
+-          show_message (ctx, message, indent);\r
++          show_message (ctx, format, message, indent);\r
+           next_indent = indent + 1;\r
++\r
++          fputs (format->message_set_sep, stdout);\r
+       }\r
\r
+-      show_messages (ctx, notmuch_message_get_replies (message),\r
++      show_messages (ctx, format, notmuch_message_get_replies (message),\r
+                      next_indent, entire_thread);\r
\r
+       notmuch_message_destroy (message);\r
++\r
++      fputs (format->message_set_end, stdout);\r
+     }\r
++\r
++    fputs (format->message_set_end, stdout);\r
+ }\r
\r
+ int\r
+@@ -223,15 +395,29 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))\r
+     notmuch_thread_t *thread;\r
+     notmuch_messages_t *messages;\r
+     char *query_string;\r
++    char *opt;\r
++    const show_format_t *format = &format_text;\r
+     int entire_thread = 0;\r
+     int i;\r
++    int first_toplevel = 1;\r
\r
+     for (i = 0; i < argc && argv[i][0] == '-'; i++) {\r
+       if (strcmp (argv[i], "--") == 0) {\r
+           i++;\r
+           break;\r
+       }\r
+-        if (strcmp(argv[i], "--entire-thread") == 0) {\r
++      if (STRNCMP_LITERAL (argv[i], "--output=") == 0) {\r
++          opt = argv[i] + sizeof ("--output=") - 1;\r
++          if (strcmp (opt, "text") == 0) {\r
++              format = &format_text;\r
++          } else if (strcmp (opt, "json") == 0) {\r
++              format = &format_json;\r
++              entire_thread = 1;\r
++          } else {\r
++              fprintf (stderr, "Invalid value for --output: %s\n", opt);\r
++              return 1;\r
++          }\r
++      } else if (STRNCMP_LITERAL (argv[i], "--entire-thread") == 0) {\r
+           entire_thread = 1;\r
+       } else {\r
+           fprintf (stderr, "Unrecognized option: %s\n", argv[i]);\r
+@@ -268,6 +454,8 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))\r
+       return 1;\r
+     }\r
\r
++    fputs (format->message_set_start, stdout);\r
++\r
+     for (threads = notmuch_query_search_threads (query);\r
+        notmuch_threads_has_more (threads);\r
+        notmuch_threads_advance (threads))\r
+@@ -280,11 +468,18 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))\r
+           INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",\r
+                           notmuch_thread_get_thread_id (thread));\r
\r
+-      show_messages (ctx, messages, 0, entire_thread);\r
++      if (!first_toplevel)\r
++          fputs (format->message_set_sep, stdout);\r
++      first_toplevel = 0;\r
++\r
++      show_messages (ctx, format, messages, 0, entire_thread);\r
\r
+       notmuch_thread_destroy (thread);\r
++\r
+     }\r
\r
++    fputs (format->message_set_end, stdout);\r
++\r
+     notmuch_query_destroy (query);\r
+     notmuch_database_close (notmuch);\r
\r
+diff --git a/notmuch.c b/notmuch.c\r
+index 2ac8a59..aa2fc12 100644\r
+--- a/notmuch.c\r
++++ b/notmuch.c\r
+@@ -162,6 +162,11 @@ command_t commands[] = {\r
+       "\n"\r
+       "\t\tSupported options for search include:\n"\r
+       "\n"\r
++      "\t\t--output=(json|text)\n"\r
++      "\n"\r
++      "\t\t\tPresents the results in either JSON or plain-text\n"\r
++      "\t\t\tformat, which is the default.\n"\r
++      "\n"\r
+       "\t\t--sort=(newest-first|oldest-first)\n"\r
+       "\n"\r
+       "\t\t\tPresent results in either chronological order\n"\r
+@@ -186,13 +191,18 @@ command_t commands[] = {\r
+       "\t\t\tall messages in the same thread as any matched\n"\r
+       "\t\t\tmessage will be displayed.\n"\r
+       "\n"\r
+-      "\t\tThe output format is plain-text, with all text-content\n"\r
+-      "\t\tMIME parts decoded. Various components in the output,\n"\r
+-      "\t\t('message', 'header', 'body', 'attachment', and MIME 'part')\n"\r
+-      "\t\tare delimited by easily-parsed markers. Each marker consists\n"\r
+-      "\t\tof a Control-L character (ASCII decimal 12), the name of\n"\r
+-      "\t\tthe marker, and then either an opening or closing brace,\n"\r
+-      "\t\t'{' or '}' to either open or close the component.\n"\r
++      "\t\t--output=(json|text)\n"\r
++      "\n"\r
++      "\t\t\tPresents the results in either JSON or plain-text\n"\r
++      "\t\t\tformat, which is the default.\n"\r
++      "\n"\r
++      "\t\tThe plain-text has all text-content MIME parts decoded.\n"\r
++      "\t\tVarious components in the output, ('message', 'header',\n"\r
++      "\t\t'body', 'attachment', and MIME 'part') are delimited by\n"\r
++      "\t\teasily-parsed markers. Each marker consists of a Control-L\n"\r
++      "\t\tcharacter (ASCII decimal 12), the name of the marker, and\n"\r
++      "\t\tthen either an opening or closing brace, '{' or '}' to\n"\r
++      "\t\teither open or close the component.\n"\r
+       "\n"\r
+       "\t\tA common use of \"notmuch show\" is to display a single\n"\r
+       "\t\tthread of email messages. For this, use a search term of\n"\r
+diff --git a/show-message.c b/show-message.c\r
+index 784981b..05ced9c 100644\r
+--- a/show-message.c\r
++++ b/show-message.c\r
+@@ -26,8 +26,6 @@ static void\r
+ show_message_part (GMimeObject *part, int *part_count,\r
+                  void (*show_part) (GMimeObject *part, int *part_count))\r
+ {\r
+-    *part_count = *part_count + 1;\r
+-\r
+     if (GMIME_IS_MULTIPART (part)) {\r
+       GMimeMultipart *multipart = GMIME_MULTIPART (part);\r
+       int i;\r
+@@ -56,6 +54,8 @@ show_message_part (GMimeObject *part, int *part_count,\r
+       return;\r
+     }\r
\r
++    *part_count = *part_count + 1;\r
++\r
+     (*show_part) (part, part_count);\r
+ }\r
\r
+-- \r
+1.6.5.3\r
+\r