--- /dev/null
+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