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