[PATCH 1/9] lib: read "property" terms from messages.
[notmuch-archives.git] / 95 / 8bcef4fe4de6c23ab169072531a0c6c0e8f3c9
1 Return-Path: <bremner@pivot.cs.unb.ca>\r
2 X-Original-To: notmuch@notmuchmail.org\r
3 Delivered-To: notmuch@notmuchmail.org\r
4 Received: from localhost (localhost [127.0.0.1])\r
5         by olra.theworths.org (Postfix) with ESMTP id AF715431FBC\r
6         for <notmuch@notmuchmail.org>; Thu, 31 Dec 2009 07:19:52 -0800 (PST)\r
7 X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
8 Received: from olra.theworths.org ([127.0.0.1])\r
9         by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
10         with ESMTP id 554aUXUBRgQV for <notmuch@notmuchmail.org>;\r
11         Thu, 31 Dec 2009 07:19:50 -0800 (PST)\r
12 Received: from pivot.cs.unb.ca (pivot.cs.unb.ca [131.202.240.57])\r
13         by olra.theworths.org (Postfix) with ESMTP id 10DBE431FAE\r
14         for <notmuch@notmuchmail.org>; Thu, 31 Dec 2009 07:19:50 -0800 (PST)\r
15 Received: from\r
16         fctnnbsc30w-142167182194.pppoe-dynamic.high-speed.nb.bellaliant.net\r
17         ([142.167.182.194] helo=rocinante.cs.unb.ca)\r
18         by pivot.cs.unb.ca with esmtpsa (TLS1.0:RSA_AES_256_CBC_SHA1:32)\r
19         (Exim 4.69) (envelope-from <bremner@pivot.cs.unb.ca>)\r
20         id 1NQMoW-0000VT-Ef; Thu, 31 Dec 2009 11:19:49 -0400\r
21 Received: from bremner by rocinante.cs.unb.ca with local (Exim 4.71)\r
22         (envelope-from <bremner@rocinante.cs.unb.ca>)\r
23         id 1NQMmd-0004bv-6y; Thu, 31 Dec 2009 11:17:51 -0400\r
24 From: david@tethera.net\r
25 To: notmuch@notmuchmail.org\r
26 Date: Thu, 31 Dec 2009 11:17:40 -0400\r
27 Message-Id: <1262272660-16786-1-git-send-email-david@tethera.net>\r
28 X-Mailer: git-send-email 1.6.5\r
29 In-Reply-To: <id:1262249616-sup-3911@elise>\r
30 References: <id:1262249616-sup-3911@elise>\r
31 X-Sender-Verified: bremner@pivot.cs.unb.ca\r
32 Subject: [notmuch] [PATCH] Add an "--format=(json|text)" command-line option\r
33         to both notmuch-search and notmuch-show.\r
34 X-BeenThere: notmuch@notmuchmail.org\r
35 X-Mailman-Version: 2.1.12\r
36 Precedence: list\r
37 List-Id: "Use and development of the notmuch mail system."\r
38         <notmuch.notmuchmail.org>\r
39 List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
40         <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
41 List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
42 List-Post: <mailto:notmuch@notmuchmail.org>\r
43 List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
44 List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
45         <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
46 X-List-Received-Date: Thu, 31 Dec 2009 15:19:52 -0000\r
47 \r
48 From: Scott Robinson <scott@quadhome.com>\r
49 \r
50 In the case of notmuch-show, "--format=json" also implies\r
51 "--entire-thread" as the thread structure is implicit in the emitted\r
52 document tree.\r
53 \r
54 As a coincidence to the implementation, multipart message ID numbers are\r
55 now incremented with each part printed. This changes the previous\r
56 semantics, which were unclear and not necessary related to the actual\r
57 ordering of the message parts.\r
58 ---\r
59 \r
60 This is an updated version of Scott Robinson's patch of\r
61 id:1261114167-sup-8228@lisa. All of the hard work is Scott's. My\r
62 changes to Scotts patch are \r
63  - change option to --format from --output\r
64  - make a couple of calls to function points conditional on the \r
65    function pointers being non-null\r
66  - updated the built-in documentation and the man page\r
67 \r
68 I also changed the copyright of json.c to Scott. Hope that's OK with\r
69 Scott.\r
70 \r
71  Makefile.local   |    3 +-\r
72  json.c           |   72 ++++++++++++++\r
73  notmuch-client.h |    3 +\r
74  notmuch-search.c |  163 +++++++++++++++++++++++++++++---\r
75  notmuch-show.c   |  278 ++++++++++++++++++++++++++++++++++++++++++++++--------\r
76  notmuch.1        |   27 +++++-\r
77  notmuch.c        |   29 +++++--\r
78  show-message.c   |    4 +-\r
79  8 files changed, 514 insertions(+), 65 deletions(-)\r
80  create mode 100644 json.c\r
81 \r
82 diff --git a/Makefile.local b/Makefile.local\r
83 index 933ff4c..53b474b 100644\r
84 --- a/Makefile.local\r
85 +++ b/Makefile.local\r
86 @@ -18,7 +18,8 @@ notmuch_client_srcs =         \\r
87         notmuch-tag.c           \\r
88         notmuch-time.c          \\r
89         query-string.c          \\r
90 -       show-message.c\r
91 +       show-message.c          \\r
92 +       json.c\r
93  \r
94  notmuch_client_modules = $(notmuch_client_srcs:.c=.o)\r
95  notmuch: $(notmuch_client_modules) lib/notmuch.a\r
96 diff --git a/json.c b/json.c\r
97 new file mode 100644\r
98 index 0000000..3960750\r
99 --- /dev/null\r
100 +++ b/json.c\r
101 @@ -0,0 +1,72 @@\r
102 +/* notmuch - Not much of an email program, (just index and search)\r
103 + *\r
104 + * Copyright © 2009 Scott Robinson\r
105 + *\r
106 + * This program is free software: you can redistribute it and/or modify\r
107 + * it under the terms of the GNU General Public License as published by\r
108 + * the Free Software Foundation, either version 3 of the License, or\r
109 + * (at your option) any later version.\r
110 + *\r
111 + * This program is distributed in the hope that it will be useful,\r
112 + * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
113 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
114 + * GNU General Public License for more details.\r
115 + *\r
116 + * You should have received a copy of the GNU General Public License\r
117 + * along with this program.  If not, see http://www.gnu.org/licenses/ .\r
118 + *\r
119 + * Authors: Scott Robinson <scott@quadhome.com>\r
120 + *\r
121 + */\r
122 +\r
123 +#include "notmuch-client.h"\r
124 +\r
125 +/*\r
126 + * json_quote_str derived from cJSON's print_string_ptr,\r
127 + * Copyright (c) 2009 Dave Gamble\r
128 + */\r
129 +\r
130 +char *\r
131 +json_quote_str(const void *ctx, const char *str)\r
132 +{\r
133 +    const char *ptr;\r
134 +    char *ptr2;\r
135 +    char *out;\r
136 +    int len = 0;\r
137 +\r
138 +    if (!str)\r
139 +       return NULL;\r
140 +\r
141 +    for (ptr = str; *ptr; len++, ptr++) {\r
142 +       if (*ptr < 32 || *ptr == '\"' || *ptr == '\\')\r
143 +           len++;\r
144 +    }\r
145 +\r
146 +    out = talloc_array (ctx, char, len + 3);\r
147 +\r
148 +    ptr = str;\r
149 +    ptr2 = out;\r
150 +\r
151 +    *ptr2++ = '\"';\r
152 +    while (*ptr) {\r
153 +           if (*ptr > 31 && *ptr != '\"' && *ptr != '\\') {\r
154 +               *ptr2++ = *ptr++;\r
155 +           } else {\r
156 +               *ptr2++ = '\\';\r
157 +               switch (*ptr++) {\r
158 +                   case '\"':  *ptr2++ = '\"'; break;\r
159 +                   case '\\':  *ptr2++ = '\\'; break;\r
160 +                   case '\b':  *ptr2++ = 'b';  break;\r
161 +                   case '\f':  *ptr2++ = 'f';  break;\r
162 +                   case '\n':  *ptr2++ = 'n';  break;\r
163 +                   case '\r':  *ptr2++ = 'r';  break;\r
164 +                   case '\t':  *ptr2++ = 't';  break;\r
165 +                   default:     ptr2--;        break;\r
166 +               }\r
167 +           }\r
168 +    }\r
169 +    *ptr2++ = '\"';\r
170 +    *ptr2++ = '\0';\r
171 +\r
172 +    return out;\r
173 +}\r
174 diff --git a/notmuch-client.h b/notmuch-client.h\r
175 index 50a30fe..7b844b9 100644\r
176 --- a/notmuch-client.h\r
177 +++ b/notmuch-client.h\r
178 @@ -143,6 +143,9 @@ notmuch_status_t\r
179  show_message_body (const char *filename,\r
180                    void (*show_part) (GMimeObject *part, int *part_count));\r
181  \r
182 +char *\r
183 +json_quote_str (const void *ctx, const char *str);\r
184 +\r
185  /* notmuch-config.c */\r
186  \r
187  typedef struct _notmuch_config notmuch_config_t;\r
188 diff --git a/notmuch-search.c b/notmuch-search.c\r
189 index dc44eb6..482c6e8 100644\r
190 --- a/notmuch-search.c\r
191 +++ b/notmuch-search.c\r
192 @@ -20,8 +20,120 @@\r
193  \r
194  #include "notmuch-client.h"\r
195  \r
196 +typedef struct search_format {\r
197 +    const char *results_start;\r
198 +    const char *thread_start;\r
199 +    void (*thread) (const void *ctx,\r
200 +                   const char *id,\r
201 +                   const time_t date,\r
202 +                   const int matched,\r
203 +                   const int total,\r
204 +                   const char *authors,\r
205 +                   const char *subject);\r
206 +    const char *tag_start;\r
207 +    const char *tag;\r
208 +    const char *tag_sep;\r
209 +    const char *tag_end;\r
210 +    const char *thread_sep;\r
211 +    const char *thread_end;\r
212 +    const char *results_end;\r
213 +} search_format_t;\r
214 +\r
215 +static void\r
216 +format_thread_text (const void *ctx,\r
217 +                   const char *id,\r
218 +                   const time_t date,\r
219 +                   const int matched,\r
220 +                   const int total,\r
221 +                   const char *authors,\r
222 +                   const char *subject);\r
223 +static const search_format_t format_text = {\r
224 +    "",\r
225 +       "",\r
226 +           format_thread_text,\r
227 +           " (",\r
228 +               "%s", " ",\r
229 +           ")", "",\r
230 +       "\n",\r
231 +    "",\r
232 +};\r
233 +\r
234 +static void\r
235 +format_thread_json (const void *ctx,\r
236 +                   const char *id,\r
237 +                   const time_t date,\r
238 +                   const int matched,\r
239 +                   const int total,\r
240 +                   const char *authors,\r
241 +                   const char *subject);\r
242 +static const search_format_t format_json = {\r
243 +    "[",\r
244 +       "{",\r
245 +           format_thread_json,\r
246 +           "\"tags\": [",\r
247 +               "\"%s\"", ", ",\r
248 +           "]", ",\n",\r
249 +       "}",\r
250 +    "]\n",\r
251 +};\r
252 +\r
253 +static void\r
254 +format_thread_text (const void *ctx,\r
255 +                   const char *id,\r
256 +                   const time_t date,\r
257 +                   const int matched,\r
258 +                   const int total,\r
259 +                   const char *authors,\r
260 +                   const char *subject)\r
261 +{\r
262 +    printf ("thread:%s %12s [%d/%d] %s; %s",\r
263 +           id,\r
264 +           notmuch_time_relative_date (ctx, date),\r
265 +           matched,\r
266 +           total,\r
267 +           authors,\r
268 +           subject);\r
269 +}\r
270 +\r
271 +static void\r
272 +format_thread_json (const void *ctx,\r
273 +                   const char *id,\r
274 +                   const time_t date,\r
275 +                   const int matched,\r
276 +                   const int total,\r
277 +                   const char *authors,\r
278 +                   const char *subject)\r
279 +{\r
280 +    struct tm *tm;\r
281 +    char timestamp[40];\r
282 +    void *ctx_quote = talloc_new (ctx);\r
283 +\r
284 +    tm = gmtime (&date);\r
285 +    if (tm == NULL)\r
286 +       INTERNAL_ERROR ("gmtime failed on thread %s.", id);\r
287 +\r
288 +    if (strftime (timestamp, sizeof (timestamp), "%s", tm) == 0)\r
289 +       INTERNAL_ERROR ("strftime failed on thread %s.", id);\r
290 +\r
291 +    printf ("\"id\": %s,\n"\r
292 +           "\"timestamp\": %s,\n"\r
293 +           "\"matched\": %d,\n"\r
294 +           "\"total\": %d,\n"\r
295 +           "\"authors\": %s,\n"\r
296 +           "\"subject\": %s,\n",\r
297 +           json_quote_str (ctx_quote, id),\r
298 +           timestamp,\r
299 +           matched,\r
300 +           total,\r
301 +           json_quote_str (ctx_quote, authors),\r
302 +           json_quote_str (ctx_quote, subject));\r
303 +\r
304 +    talloc_free (ctx_quote);\r
305 +}\r
306 +\r
307  static void\r
308  do_search_threads (const void *ctx,\r
309 +                  const search_format_t *format,\r
310                    notmuch_query_t *query,\r
311                    notmuch_sort_t sort)\r
312  {\r
313 @@ -29,7 +141,9 @@ do_search_threads (const void *ctx,\r
314      notmuch_threads_t *threads;\r
315      notmuch_tags_t *tags;\r
316      time_t date;\r
317 -    const char *relative_date;\r
318 +    int first_thread = 1;\r
319 +\r
320 +    fputs (format->results_start, stdout);\r
321  \r
322      for (threads = notmuch_query_search_threads (query);\r
323          notmuch_threads_has_more (threads);\r
324 @@ -37,6 +151,9 @@ do_search_threads (const void *ctx,\r
325      {\r
326         int first_tag = 1;\r
327  \r
328 +       if (! first_thread)\r
329 +           fputs (format->thread_sep, stdout);\r
330 +\r
331         thread = notmuch_threads_get (threads);\r
332  \r
333         if (sort == NOTMUCH_SORT_OLDEST_FIRST)\r
334 @@ -44,30 +161,37 @@ do_search_threads (const void *ctx,\r
335         else\r
336             date = notmuch_thread_get_newest_date (thread);\r
337  \r
338 -       relative_date = notmuch_time_relative_date (ctx, date);\r
339 +       fputs (format->thread_start, stdout);\r
340 +\r
341 +       format->thread (ctx,\r
342 +                       notmuch_thread_get_thread_id (thread),\r
343 +                       date,\r
344 +                       notmuch_thread_get_matched_messages (thread),\r
345 +                       notmuch_thread_get_total_messages (thread),\r
346 +                       notmuch_thread_get_authors (thread),\r
347 +                       notmuch_thread_get_subject (thread));\r
348  \r
349 -       printf ("thread:%s %12s [%d/%d] %s; %s",\r
350 -               notmuch_thread_get_thread_id (thread),\r
351 -               relative_date,\r
352 -               notmuch_thread_get_matched_messages (thread),\r
353 -               notmuch_thread_get_total_messages (thread),\r
354 -               notmuch_thread_get_authors (thread),\r
355 -               notmuch_thread_get_subject (thread));\r
356 +       fputs (format->tag_start, stdout);\r
357  \r
358 -       printf (" (");\r
359         for (tags = notmuch_thread_get_tags (thread);\r
360              notmuch_tags_has_more (tags);\r
361              notmuch_tags_advance (tags))\r
362         {\r
363             if (! first_tag)\r
364 -               printf (" ");\r
365 -           printf ("%s", notmuch_tags_get (tags));\r
366 +               fputs (format->tag_sep, stdout);\r
367 +           printf (format->tag, notmuch_tags_get (tags));\r
368             first_tag = 0;\r
369         }\r
370 -       printf (")\n");\r
371 +\r
372 +       fputs (format->tag_end, stdout);\r
373 +       fputs (format->thread_end, stdout);\r
374 +\r
375 +       first_thread = 0;\r
376  \r
377         notmuch_thread_destroy (thread);\r
378      }\r
379 +\r
380 +    fputs (format->results_end, stdout);\r
381  }\r
382  \r
383  int\r
384 @@ -79,6 +203,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])\r
385      char *query_str;\r
386      char *opt;\r
387      notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;\r
388 +    const search_format_t *format = &format_text;\r
389      int i;\r
390  \r
391      for (i = 0; i < argc && argv[i][0] == '-'; i++) {\r
392 @@ -96,6 +221,16 @@ notmuch_search_command (void *ctx, int argc, char *argv[])\r
393                 fprintf (stderr, "Invalid value for --sort: %s\n", opt);\r
394                 return 1;\r
395             }\r
396 +       } else if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {\r
397 +           opt = argv[i] + sizeof ("--format=") - 1;\r
398 +           if (strcmp (opt, "text") == 0) {\r
399 +               format = &format_text;\r
400 +           } else if (strcmp (opt, "json") == 0) {\r
401 +               format = &format_json;\r
402 +           } else {\r
403 +               fprintf (stderr, "Invalid value for --format: %s\n", opt);\r
404 +               return 1;\r
405 +           }\r
406         } else {\r
407             fprintf (stderr, "Unrecognized option: %s\n", argv[i]);\r
408             return 1;\r
409 @@ -132,7 +267,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])\r
410  \r
411      notmuch_query_set_sort (query, sort);\r
412  \r
413 -    do_search_threads (ctx, query, sort);\r
414 +    do_search_threads (ctx, format, query, sort);\r
415  \r
416      notmuch_query_destroy (query);\r
417      notmuch_database_close (notmuch);\r
418 diff --git a/notmuch-show.c b/notmuch-show.c\r
419 index 376aacd..1a1d601 100644\r
420 --- a/notmuch-show.c\r
421 +++ b/notmuch-show.c\r
422 @@ -20,8 +20,65 @@\r
423  \r
424  #include "notmuch-client.h"\r
425  \r
426 +typedef struct show_format {\r
427 +    const char *message_set_start;\r
428 +    const char *message_start;\r
429 +    void (*message) (const void *ctx,\r
430 +                    notmuch_message_t *message,\r
431 +                    int indent);\r
432 +    const char *header_start;\r
433 +    void (*header) (const void *ctx,\r
434 +                   notmuch_message_t *message);\r
435 +    const char *header_end;\r
436 +    const char *body_start;\r
437 +    void (*part) (GMimeObject *part,\r
438 +                 int *part_count);\r
439 +    const char *body_end;\r
440 +    const char *message_end;\r
441 +    const char *message_set_sep;\r
442 +    const char *message_set_end;\r
443 +} show_format_t;\r
444 +\r
445 +static void\r
446 +format_message_text (unused (const void *ctx),\r
447 +                    notmuch_message_t *message,\r
448 +                    int indent);\r
449 +static void\r
450 +format_headers_text (const void *ctx,\r
451 +                    notmuch_message_t *message);\r
452 +static void\r
453 +format_part_text (GMimeObject *part,\r
454 +                 int *part_count);\r
455 +static const show_format_t format_text = {\r
456 +    "",\r
457 +       "\fmessage{ ", format_message_text,\r
458 +           "\fheader{\n", format_headers_text, "\fheader}\n",\r
459 +           "\fbody{\n", format_part_text, "\fbody}\n",\r
460 +       "\fmessage}\n", "",\r
461 +    ""\r
462 +};\r
463 +\r
464 +static void\r
465 +format_message_json (const void *ctx,\r
466 +                    notmuch_message_t *message,\r
467 +                    unused (int indent));\r
468 +static void\r
469 +format_headers_json (const void *ctx,\r
470 +                    notmuch_message_t *message);\r
471 +static void\r
472 +format_part_json (GMimeObject *part,\r
473 +                 int *part_count);\r
474 +static const show_format_t format_json = {\r
475 +    "[",\r
476 +       "{", format_message_json,\r
477 +           ", \"headers\": {", format_headers_json, "}",\r
478 +           ", \"body\": [", format_part_json, "]",\r
479 +       "}", ", ",\r
480 +    "]"\r
481 +};\r
482 +\r
483  static const char *\r
484 -_get_tags_as_string (void *ctx, notmuch_message_t *message)\r
485 +_get_tags_as_string (const void *ctx, notmuch_message_t *message)\r
486  {\r
487      notmuch_tags_t *tags;\r
488      int first = 1;\r
489 @@ -48,7 +105,7 @@ _get_tags_as_string (void *ctx, notmuch_message_t *message)\r
490  \r
491  /* Get a nice, single-line summary of message. */\r
492  static const char *\r
493 -_get_one_line_summary (void *ctx, notmuch_message_t *message)\r
494 +_get_one_line_summary (const void *ctx, notmuch_message_t *message)\r
495  {\r
496      const char *from;\r
497      time_t date;\r
498 @@ -67,18 +124,87 @@ _get_one_line_summary (void *ctx, notmuch_message_t *message)\r
499  }\r
500  \r
501  static void\r
502 -show_part_content (GMimeObject *part)\r
503 +format_message_text (unused (const void *ctx), notmuch_message_t *message, int indent)\r
504 +{\r
505 +    printf ("id:%s depth:%d match:%d filename:%s\n",\r
506 +           notmuch_message_get_message_id (message),\r
507 +           indent,\r
508 +           notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),\r
509 +           notmuch_message_get_filename (message));\r
510 +}\r
511 +\r
512 +static void\r
513 +format_message_json (const void *ctx, notmuch_message_t *message, unused (int indent))\r
514 +{\r
515 +    void *ctx_quote = talloc_new (ctx);\r
516 +\r
517 +    printf ("\"id\": %s, \"match\": %s, \"filename\": %s",\r
518 +           json_quote_str (ctx_quote, notmuch_message_get_message_id (message)),\r
519 +           notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? "true" : "false",\r
520 +           json_quote_str (ctx_quote, notmuch_message_get_filename (message)));\r
521 +\r
522 +    talloc_free (ctx_quote);\r
523 +}\r
524 +\r
525 +static void\r
526 +format_headers_text (const void *ctx, notmuch_message_t *message)\r
527 +{\r
528 +    const char *headers[] = {\r
529 +       "Subject", "From", "To", "Cc", "Bcc", "Date"\r
530 +    };\r
531 +    const char *name, *value;\r
532 +    unsigned int i;\r
533 +\r
534 +    printf ("%s\n", _get_one_line_summary (ctx, message));\r
535 +\r
536 +    for (i = 0; i < ARRAY_SIZE (headers); i++) {\r
537 +       name = headers[i];\r
538 +       value = notmuch_message_get_header (message, name);\r
539 +       if (value)\r
540 +           printf ("%s: %s\n", name, value);\r
541 +    }\r
542 +}\r
543 +\r
544 +static void\r
545 +format_headers_json (const void *ctx, notmuch_message_t *message)\r
546 +{\r
547 +    const char *headers[] = {\r
548 +       "Subject", "From", "To", "Cc", "Bcc", "Date"\r
549 +    };\r
550 +    const char *name, *value;\r
551 +    unsigned int i;\r
552 +    int first_header = 1;\r
553 +    void *ctx_quote = talloc_new (ctx);\r
554 +\r
555 +    for (i = 0; i < ARRAY_SIZE (headers); i++) {\r
556 +       name = headers[i];\r
557 +       value = notmuch_message_get_header (message, name);\r
558 +       if (value)\r
559 +       {\r
560 +           if (!first_header)\r
561 +               fputs (", ", stdout);\r
562 +           first_header = 0;\r
563 +\r
564 +           printf ("%s: %s",\r
565 +                   json_quote_str (ctx_quote, name),\r
566 +                   json_quote_str (ctx_quote, value));\r
567 +       }\r
568 +    }\r
569 +\r
570 +    talloc_free (ctx_quote);\r
571 +}\r
572 +\r
573 +static void\r
574 +show_part_content (GMimeObject *part, GMimeStream *stream_out)\r
575  {\r
576 -    GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);\r
577      GMimeStream *stream_filter = NULL;\r
578      GMimeDataWrapper *wrapper;\r
579      const char *charset;\r
580  \r
581      charset = g_mime_object_get_content_type_parameter (part, "charset");\r
582  \r
583 -    if (stream_stdout) {\r
584 -       g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);\r
585 -       stream_filter = g_mime_stream_filter_new(stream_stdout);\r
586 +    if (stream_out) {\r
587 +       stream_filter = g_mime_stream_filter_new(stream_out);\r
588         g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),\r
589                                  g_mime_filter_crlf_new(FALSE, FALSE));\r
590          if (charset) {\r
591 @@ -92,15 +218,16 @@ show_part_content (GMimeObject *part)\r
592         g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);\r
593      if (stream_filter)\r
594         g_object_unref(stream_filter);\r
595 -    if (stream_stdout)\r
596 -       g_object_unref(stream_stdout);\r
597  }\r
598  \r
599  static void\r
600 -show_part (GMimeObject *part, int *part_count)\r
601 +format_part_text (GMimeObject *part, int *part_count)\r
602  {\r
603      GMimeContentDisposition *disposition;\r
604      GMimeContentType *content_type;\r
605 +    GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);\r
606 +\r
607 +    g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);\r
608  \r
609      disposition = g_mime_object_get_content_disposition (part);\r
610      if (disposition &&\r
611 @@ -118,11 +245,14 @@ show_part (GMimeObject *part, int *part_count)\r
612         if (g_mime_content_type_is_type (content_type, "text", "*") &&\r
613             !g_mime_content_type_is_type (content_type, "text", "html"))\r
614         {\r
615 -           show_part_content (part);\r
616 +           show_part_content (part, stream_stdout);\r
617         }\r
618  \r
619         printf ("\fattachment}\n");\r
620  \r
621 +       if (stream_stdout)\r
622 +           g_object_unref(stream_stdout);\r
623 +\r
624         return;\r
625      }\r
626  \r
627 @@ -135,7 +265,7 @@ show_part (GMimeObject *part, int *part_count)\r
628      if (g_mime_content_type_is_type (content_type, "text", "*") &&\r
629         !g_mime_content_type_is_type (content_type, "text", "html"))\r
630      {\r
631 -       show_part_content (part);\r
632 +       show_part_content (part, stream_stdout);\r
633      }\r
634      else\r
635      {\r
636 @@ -144,57 +274,96 @@ show_part (GMimeObject *part, int *part_count)\r
637      }\r
638  \r
639      printf ("\fpart}\n");\r
640 +\r
641 +    if (stream_stdout)\r
642 +       g_object_unref(stream_stdout);\r
643  }\r
644  \r
645  static void\r
646 -show_message (void *ctx, notmuch_message_t *message, int indent)\r
647 +format_part_json (GMimeObject *part, int *part_count)\r
648  {\r
649 -    const char *headers[] = {\r
650 -       "Subject", "From", "To", "Cc", "Bcc", "Date"\r
651 -    };\r
652 -    const char *name, *value;\r
653 -    unsigned int i;\r
654 +    GMimeContentType *content_type;\r
655 +    GMimeContentDisposition *disposition;\r
656 +    void *ctx = talloc_new (NULL);\r
657 +    GMimeStream *stream_memory = g_mime_stream_mem_new ();\r
658 +    GByteArray *part_content;\r
659  \r
660 -    printf ("\fmessage{ id:%s depth:%d match:%d filename:%s\n",\r
661 -           notmuch_message_get_message_id (message),\r
662 -           indent,\r
663 -           notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),\r
664 -           notmuch_message_get_filename (message));\r
665 +    content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));\r
666  \r
667 -    printf ("\fheader{\n");\r
668 +    if (*part_count > 1)\r
669 +       fputs (", ", stdout);\r
670  \r
671 -    printf ("%s\n", _get_one_line_summary (ctx, message));\r
672 +    printf ("{\"id\": %d, \"content-type\": %s",\r
673 +           *part_count,\r
674 +           json_quote_str (ctx, g_mime_content_type_to_string (content_type)));\r
675  \r
676 -    for (i = 0; i < ARRAY_SIZE (headers); i++) {\r
677 -       name = headers[i];\r
678 -       value = notmuch_message_get_header (message, name);\r
679 -       if (value)\r
680 -           printf ("%s: %s\n", name, value);\r
681 +    disposition = g_mime_object_get_content_disposition (part);\r
682 +    if (disposition &&\r
683 +       strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)\r
684 +    {\r
685 +       const char *filename = g_mime_part_get_filename (GMIME_PART (part));\r
686 +\r
687 +       printf (", \"filename\": %s", json_quote_str (ctx, filename));\r
688      }\r
689  \r
690 -    printf ("\fheader}\n");\r
691 -    printf ("\fbody{\n");\r
692 +    if (g_mime_content_type_is_type (content_type, "text", "*") &&\r
693 +       !g_mime_content_type_is_type (content_type, "text", "html"))\r
694 +    {\r
695 +       show_part_content (part, stream_memory);\r
696 +       part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));\r
697 +\r
698 +       printf (", \"content\": %s", json_quote_str (ctx, (char *) part_content->data));\r
699 +    }\r
700 +\r
701 +    fputs ("}", stdout);\r
702 +\r
703 +    talloc_free (ctx);\r
704 +    if (stream_memory)\r
705 +       g_object_unref (stream_memory);\r
706 +}\r
707 +\r
708 +static void\r
709 +show_message (void *ctx, const show_format_t *format, notmuch_message_t *message, int indent)\r
710 +{\r
711 +    fputs (format->message_start, stdout);\r
712 +    if (format->message)\r
713 +       format->message(ctx, message, indent);\r
714  \r
715 -    show_message_body (notmuch_message_get_filename (message), show_part);\r
716 +    fputs (format->header_start, stdout);\r
717 +    if (format->header)\r
718 +       format->header(ctx, message);\r
719 +    fputs (format->header_end, stdout);\r
720  \r
721 -    printf ("\fbody}\n");\r
722 +    fputs (format->body_start, stdout);\r
723 +    if (format->part)\r
724 +       show_message_body (notmuch_message_get_filename (message), format->part);\r
725 +    fputs (format->body_end, stdout);\r
726  \r
727 -    printf ("\fmessage}\n");\r
728 +    fputs (format->message_end, stdout);\r
729  }\r
730  \r
731  \r
732  static void\r
733 -show_messages (void *ctx, notmuch_messages_t *messages, int indent,\r
734 +show_messages (void *ctx, const show_format_t *format, notmuch_messages_t *messages, int indent,\r
735                notmuch_bool_t entire_thread)\r
736  {\r
737      notmuch_message_t *message;\r
738      notmuch_bool_t match;\r
739 +    int first_set = 1;\r
740      int next_indent;\r
741  \r
742 +    fputs (format->message_set_start, stdout);\r
743 +\r
744      for (;\r
745          notmuch_messages_has_more (messages);\r
746          notmuch_messages_advance (messages))\r
747      {\r
748 +       if (!first_set)\r
749 +           fputs (format->message_set_sep, stdout);\r
750 +       first_set = 0;\r
751 +\r
752 +       fputs (format->message_set_start, stdout);\r
753 +\r
754         message = notmuch_messages_get (messages);\r
755  \r
756         match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);\r
757 @@ -202,15 +371,21 @@ show_messages (void *ctx, notmuch_messages_t *messages, int indent,\r
758         next_indent = indent;\r
759  \r
760         if (match || entire_thread) {\r
761 -           show_message (ctx, message, indent);\r
762 +           show_message (ctx, format, message, indent);\r
763             next_indent = indent + 1;\r
764 +\r
765 +           fputs (format->message_set_sep, stdout);\r
766         }\r
767  \r
768 -       show_messages (ctx, notmuch_message_get_replies (message),\r
769 +       show_messages (ctx, format, notmuch_message_get_replies (message),\r
770                        next_indent, entire_thread);\r
771  \r
772         notmuch_message_destroy (message);\r
773 +\r
774 +       fputs (format->message_set_end, stdout);\r
775      }\r
776 +\r
777 +    fputs (format->message_set_end, stdout);\r
778  }\r
779  \r
780  int\r
781 @@ -223,15 +398,29 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))\r
782      notmuch_thread_t *thread;\r
783      notmuch_messages_t *messages;\r
784      char *query_string;\r
785 +    char *opt;\r
786 +    const show_format_t *format = &format_text;\r
787      int entire_thread = 0;\r
788      int i;\r
789 +    int first_toplevel = 1;\r
790  \r
791      for (i = 0; i < argc && argv[i][0] == '-'; i++) {\r
792         if (strcmp (argv[i], "--") == 0) {\r
793             i++;\r
794             break;\r
795         }\r
796 -        if (strcmp(argv[i], "--entire-thread") == 0) {\r
797 +       if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {\r
798 +           opt = argv[i] + sizeof ("--format=") - 1;\r
799 +           if (strcmp (opt, "text") == 0) {\r
800 +               format = &format_text;\r
801 +           } else if (strcmp (opt, "json") == 0) {\r
802 +               format = &format_json;\r
803 +               entire_thread = 1;\r
804 +           } else {\r
805 +               fprintf (stderr, "Invalid value for --format: %s\n", opt);\r
806 +               return 1;\r
807 +           }\r
808 +       } else if (STRNCMP_LITERAL (argv[i], "--entire-thread") == 0) {\r
809             entire_thread = 1;\r
810         } else {\r
811             fprintf (stderr, "Unrecognized option: %s\n", argv[i]);\r
812 @@ -268,6 +457,8 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))\r
813         return 1;\r
814      }\r
815  \r
816 +    fputs (format->message_set_start, stdout);\r
817 +\r
818      for (threads = notmuch_query_search_threads (query);\r
819          notmuch_threads_has_more (threads);\r
820          notmuch_threads_advance (threads))\r
821 @@ -280,11 +471,18 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))\r
822             INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",\r
823                             notmuch_thread_get_thread_id (thread));\r
824  \r
825 -       show_messages (ctx, messages, 0, entire_thread);\r
826 +       if (!first_toplevel)\r
827 +           fputs (format->message_set_sep, stdout);\r
828 +       first_toplevel = 0;\r
829 +\r
830 +       show_messages (ctx, format, messages, 0, entire_thread);\r
831  \r
832         notmuch_thread_destroy (thread);\r
833 +\r
834      }\r
835  \r
836 +    fputs (format->message_set_end, stdout);\r
837 +\r
838      notmuch_query_destroy (query);\r
839      notmuch_database_close (notmuch);\r
840  \r
841 diff --git a/notmuch.1 b/notmuch.1\r
842 index 369ecba..e3d5acb 100644\r
843 --- a/notmuch.1\r
844 +++ b/notmuch.1\r
845 @@ -154,6 +154,12 @@ Supported options for\r
846  include\r
847  .RS 4\r
848  .TP 4\r
849 +.BR \-\-format= ( json | text )\r
850 +\r
851 +Presents the results in either JSON or plain-text (default).\r
852 +.RE\r
853 +.RS 4\r
854 +.TP 4\r
855  .BR \-\-sort= ( newest\-first | oldest\-first )\r
856  \r
857  This option can be used to present results in either chronological order\r
858 @@ -202,7 +208,14 @@ matched message will be displayed.\r
859  .RE\r
860  \r
861  .RS 4\r
862 -The  output format  is plain-text,  with all  text-content  MIME parts\r
863 +.TP 4\r
864 +.B \-\-format=(json|text)\r
865 +\r
866 +.RS 4\r
867 +.TP 4\r
868 +.B text\r
869 +\r
870 +The default plain-text format  has  text-content  MIME parts\r
871  decoded. Various components in the output,\r
872  .RB ( message ", " header ", " body ", " attachment ", and MIME " part ),\r
873  will be delimited by easily-parsed markers. Each marker consists of a\r
874 @@ -210,6 +223,18 @@ Control-L character (ASCII decimal 12), the name of the marker, and\r
875  then either an opening or closing brace, ('{' or '}'), to either open\r
876  or close the component.\r
877  \r
878 +.RE\r
879 +.RS 4\r
880 +.TP 4\r
881 +.B json\r
882 +\r
883 +Format output as Javascript Object Notation (JSON). JSON output always\r
884 +includes all messages in a matching thread; in effect\r
885 +.B \-\-format=json\r
886 +implies\r
887 +.B \-\-entire\-thread\r
888 +\r
889 +.RE\r
890  A common use of\r
891  .B notmuch show\r
892  is to display a single thread of email messages. For this, use a\r
893 diff --git a/notmuch.c b/notmuch.c\r
894 index 2ac8a59..eaa6308 100644\r
895 --- a/notmuch.c\r
896 +++ b/notmuch.c\r
897 @@ -162,6 +162,11 @@ command_t commands[] = {\r
898        "\n"\r
899        "\t\tSupported options for search include:\n"\r
900        "\n"\r
901 +      "\t\t--format=(json|text)\n"\r
902 +      "\n"\r
903 +      "\t\t\tPresents the results in either JSON or\n"\r
904 +      "\t\t\tplain-text (default)\n"\r
905 +      "\n"\r
906        "\t\t--sort=(newest-first|oldest-first)\n"\r
907        "\n"\r
908        "\t\t\tPresent results in either chronological order\n"\r
909 @@ -186,13 +191,23 @@ command_t commands[] = {\r
910        "\t\t\tall messages in the same thread as any matched\n"\r
911        "\t\t\tmessage will be displayed.\n"\r
912        "\n"\r
913 -      "\t\tThe output format is plain-text, with all text-content\n"\r
914 -      "\t\tMIME parts decoded. Various components in the output,\n"\r
915 -      "\t\t('message', 'header', 'body', 'attachment', and MIME 'part')\n"\r
916 -      "\t\tare delimited by easily-parsed markers. Each marker consists\n"\r
917 -      "\t\tof a Control-L character (ASCII decimal 12), the name of\n"\r
918 -      "\t\tthe marker, and then either an opening or closing brace,\n"\r
919 -      "\t\t'{' or '}' to either open or close the component.\n"\r
920 +      "\t\t--format=(json|text)\n"\r
921 +      "\n"\r
922 +      "\t\t\ttext\t(default)\n"\r
923 +      "\n"\r
924 +      "\t\t\tThe plain-text has all text-content MIME parts decoded.\n"\r
925 +      "\t\t\tVarious components in the output, ('message', 'header',\n"\r
926 +      "\t\t\t'body', 'attachment', and MIME 'part') are delimited by\n"\r
927 +      "\t\t\teasily-parsed markers. Each marker consists of a Control-L\n"\r
928 +      "\t\t\tcharacter (ASCII decimal 12), the name of the marker, and\n"\r
929 +      "\t\t\tthen either an opening or closing brace, '{' or '}' to\n"\r
930 +      "\t\t\teither open or close the component.\n"\r
931 +      "\n"\r
932 +      "\t\t\tjson\n"\r
933 +      "\n"\r
934 +      "\t\t\tFormat output as Javascript Object Notation (JSON).\n"\r
935 +      "\t\t\tJSON output always includes all messages in a matching,\n"\r
936 +      "\t\t\tthread i.e. '--output=json' implies '--entire-thread'\n"\r
937        "\n"\r
938        "\t\tA common use of \"notmuch show\" is to display a single\n"\r
939        "\t\tthread of email messages. For this, use a search term of\n"\r
940 diff --git a/show-message.c b/show-message.c\r
941 index 784981b..05ced9c 100644\r
942 --- a/show-message.c\r
943 +++ b/show-message.c\r
944 @@ -26,8 +26,6 @@ static void\r
945  show_message_part (GMimeObject *part, int *part_count,\r
946                    void (*show_part) (GMimeObject *part, int *part_count))\r
947  {\r
948 -    *part_count = *part_count + 1;\r
949 -\r
950      if (GMIME_IS_MULTIPART (part)) {\r
951         GMimeMultipart *multipart = GMIME_MULTIPART (part);\r
952         int i;\r
953 @@ -56,6 +54,8 @@ show_message_part (GMimeObject *part, int *part_count,\r
954         return;\r
955      }\r
956  \r
957 +    *part_count = *part_count + 1;\r
958 +\r
959      (*show_part) (part, part_count);\r
960  }\r
961  \r
962 -- \r
963 1.6.5\r
964 \r