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