Reduce some excessive indentation.
[notmuch] / notmuch-show.c
1 /* notmuch - Not much of an email program, (just index and search)
2  *
3  * Copyright © 2009 Carl Worth
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see http://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "notmuch-client.h"
22
23 static void
24 format_message_text (unused (const void *ctx),
25                      notmuch_message_t *message,
26                      int indent);
27 static void
28 format_headers_text (const void *ctx,
29                      notmuch_message_t *message);
30
31 static void
32 format_part_start_text (GMimeObject *part,
33                         int *part_count);
34
35 static void
36 format_part_content_text (GMimeObject *part);
37
38 static void
39 format_part_end_text (GMimeObject *part);
40
41 static const notmuch_show_format_t format_text = {
42     "",
43         "\fmessage{ ", format_message_text,
44             "\fheader{\n", format_headers_text, "\fheader}\n",
45             "\fbody{\n",
46                 format_part_start_text,
47                 NULL,
48                 NULL,
49                 format_part_content_text,
50                 format_part_end_text,
51                 "",
52             "\fbody}\n",
53         "\fmessage}\n", "",
54     ""
55 };
56
57 static void
58 format_message_json (const void *ctx,
59                      notmuch_message_t *message,
60                      unused (int indent));
61 static void
62 format_headers_json (const void *ctx,
63                      notmuch_message_t *message);
64
65 static void
66 format_part_start_json (unused (GMimeObject *part),
67                         int *part_count);
68
69 static void
70 format_part_encstatus_json (int status);
71
72 static void
73 format_part_sigstatus_json (const GMimeSignatureValidity* validity);
74
75 static void
76 format_part_content_json (GMimeObject *part);
77
78 static void
79 format_part_end_json (GMimeObject *part);
80
81 static const notmuch_show_format_t format_json = {
82     "[",
83         "{", format_message_json,
84             ", \"headers\": {", format_headers_json, "}",
85             ", \"body\": [",
86                 format_part_start_json,
87                 format_part_encstatus_json,
88                 format_part_sigstatus_json,
89                 format_part_content_json,
90                 format_part_end_json,
91                 ", ",
92             "]",
93         "}", ", ",
94     "]"
95 };
96
97 static void
98 format_message_mbox (const void *ctx,
99                      notmuch_message_t *message,
100                      unused (int indent));
101
102 static const notmuch_show_format_t format_mbox = {
103     "",
104         "", format_message_mbox,
105             "", NULL, "",
106             "",
107                 NULL,
108                 NULL,
109                 NULL,
110                 NULL,
111                 NULL,
112                 "",
113             "",
114         "", "",
115     ""
116 };
117
118 static void
119 format_part_content_raw (GMimeObject *part);
120
121 static const notmuch_show_format_t format_raw = {
122     "",
123         "", NULL,
124             "", NULL, "",
125             "",
126                 NULL,
127                 NULL,
128                 NULL,
129                 format_part_content_raw,
130                 NULL,
131                 "",
132             "",
133         "", "",
134     ""
135 };
136
137 static const char *
138 _get_tags_as_string (const void *ctx, notmuch_message_t *message)
139 {
140     notmuch_tags_t *tags;
141     int first = 1;
142     const char *tag;
143     char *result;
144
145     result = talloc_strdup (ctx, "");
146     if (result == NULL)
147         return NULL;
148
149     for (tags = notmuch_message_get_tags (message);
150          notmuch_tags_valid (tags);
151          notmuch_tags_move_to_next (tags))
152     {
153         tag = notmuch_tags_get (tags);
154
155         result = talloc_asprintf_append (result, "%s%s",
156                                          first ? "" : " ", tag);
157         first = 0;
158     }
159
160     return result;
161 }
162
163 /* Get a nice, single-line summary of message. */
164 static const char *
165 _get_one_line_summary (const void *ctx, notmuch_message_t *message)
166 {
167     const char *from;
168     time_t date;
169     const char *relative_date;
170     const char *tags;
171
172     from = notmuch_message_get_header (message, "from");
173
174     date = notmuch_message_get_date (message);
175     relative_date = notmuch_time_relative_date (ctx, date);
176
177     tags = _get_tags_as_string (ctx, message);
178
179     return talloc_asprintf (ctx, "%s (%s) (%s)",
180                             from, relative_date, tags);
181 }
182
183 static void
184 format_message_text (unused (const void *ctx), notmuch_message_t *message, int indent)
185 {
186     printf ("id:%s depth:%d match:%d filename:%s\n",
187             notmuch_message_get_message_id (message),
188             indent,
189             notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),
190             notmuch_message_get_filename (message));
191 }
192
193 static void
194 format_message_json (const void *ctx, notmuch_message_t *message, unused (int indent))
195 {
196     notmuch_tags_t *tags;
197     int first = 1;
198     void *ctx_quote = talloc_new (ctx);
199     time_t date;
200     const char *relative_date;
201
202     date = notmuch_message_get_date (message);
203     relative_date = notmuch_time_relative_date (ctx, date);
204
205     printf ("\"id\": %s, \"match\": %s, \"filename\": %s, \"timestamp\": %ld, \"date_relative\": \"%s\", \"tags\": [",
206             json_quote_str (ctx_quote, notmuch_message_get_message_id (message)),
207             notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? "true" : "false",
208             json_quote_str (ctx_quote, notmuch_message_get_filename (message)),
209             date, relative_date);
210
211     for (tags = notmuch_message_get_tags (message);
212          notmuch_tags_valid (tags);
213          notmuch_tags_move_to_next (tags))
214     {
215          printf("%s%s", first ? "" : ",",
216                json_quote_str (ctx_quote, notmuch_tags_get (tags)));
217          first = 0;
218     }
219     printf("]");
220     talloc_free (ctx_quote);
221 }
222
223 /* Extract just the email address from the contents of a From:
224  * header. */
225 static const char *
226 _extract_email_address (const void *ctx, const char *from)
227 {
228     InternetAddressList *addresses;
229     InternetAddress *address;
230     InternetAddressMailbox *mailbox;
231     const char *email = "MAILER-DAEMON";
232
233     addresses = internet_address_list_parse_string (from);
234
235     /* Bail if there is no address here. */
236     if (addresses == NULL || internet_address_list_length (addresses) < 1)
237         goto DONE;
238
239     /* Otherwise, just use the first address. */
240     address = internet_address_list_get_address (addresses, 0);
241
242     /* The From header should never contain an address group rather
243      * than a mailbox. So bail if it does. */
244     if (! INTERNET_ADDRESS_IS_MAILBOX (address))
245         goto DONE;
246
247     mailbox = INTERNET_ADDRESS_MAILBOX (address);
248     email = internet_address_mailbox_get_addr (mailbox);
249     email = talloc_strdup (ctx, email);
250
251   DONE:
252     /* XXX: How to free addresses here? */
253     return email;
254    }
255
256 /* Return 1 if 'line' is an mbox From_ line---that is, a line
257  * beginning with zero or more '>' characters followed by the
258  * characters 'F', 'r', 'o', 'm', and space.
259  *
260  * Any characters at all may appear after that in the line.
261  */
262 static int
263 _is_from_line (const char *line)
264 {
265     const char *s = line;
266
267     if (line == NULL)
268         return 0;
269
270     while (*s == '>')
271         s++;
272
273     if (STRNCMP_LITERAL (s, "From ") == 0)
274         return 1;
275     else
276         return 0;
277 }
278
279 /* Print a message in "mboxrd" format as documented, for example,
280  * here:
281  *
282  * http://qmail.org/qmail-manual-html/man5/mbox.html
283  */
284 static void
285 format_message_mbox (const void *ctx,
286                      notmuch_message_t *message,
287                      unused (int indent))
288 {
289     const char *filename;
290     FILE *file;
291     const char *from;
292
293     time_t date;
294     struct tm date_gmtime;
295     char date_asctime[26];
296
297     char *line = NULL;
298     size_t line_size;
299     ssize_t line_len;
300
301     filename = notmuch_message_get_filename (message);
302     file = fopen (filename, "r");
303     if (file == NULL) {
304         fprintf (stderr, "Failed to open %s: %s\n",
305                  filename, strerror (errno));
306         return;
307     }
308
309     from = notmuch_message_get_header (message, "from");
310     from = _extract_email_address (ctx, from);
311
312     date = notmuch_message_get_date (message);
313     gmtime_r (&date, &date_gmtime);
314     asctime_r (&date_gmtime, date_asctime);
315
316     printf ("From %s %s", from, date_asctime);
317
318     while ((line_len = getline (&line, &line_size, file)) != -1 ) {
319         if (_is_from_line (line))
320             putchar ('>');
321         printf ("%s", line);
322     }
323
324     printf ("\n");
325
326     fclose (file);
327 }
328
329 static void
330 format_headers_text (const void *ctx, notmuch_message_t *message)
331 {
332     const char *headers[] = {
333         "Subject", "From", "To", "Cc", "Bcc", "Date"
334     };
335     const char *name, *value;
336     unsigned int i;
337
338     printf ("%s\n", _get_one_line_summary (ctx, message));
339
340     for (i = 0; i < ARRAY_SIZE (headers); i++) {
341         name = headers[i];
342         value = notmuch_message_get_header (message, name);
343         if (value && strlen (value))
344             printf ("%s: %s\n", name, value);
345     }
346 }
347
348 static void
349 format_headers_json (const void *ctx, notmuch_message_t *message)
350 {
351     const char *headers[] = {
352         "Subject", "From", "To", "Cc", "Bcc", "Date"
353     };
354     const char *name, *value;
355     unsigned int i;
356     int first_header = 1;
357     void *ctx_quote = talloc_new (ctx);
358
359     for (i = 0; i < ARRAY_SIZE (headers); i++) {
360         name = headers[i];
361         value = notmuch_message_get_header (message, name);
362         if (value)
363         {
364             if (!first_header)
365                 fputs (", ", stdout);
366             first_header = 0;
367
368             printf ("%s: %s",
369                     json_quote_str (ctx_quote, name),
370                     json_quote_str (ctx_quote, value));
371         }
372     }
373
374     talloc_free (ctx_quote);
375 }
376
377 /* Write a MIME text part out to the given stream.
378  *
379  * Both line-ending conversion (CRLF->LF) and charset conversion ( ->
380  * UTF-8) will be performed, so it is inappropriate to call this
381  * function with a non-text part. Doing so will trigger an internal
382  * error.
383  */
384 static void
385 show_text_part_content (GMimeObject *part, GMimeStream *stream_out)
386 {
387     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
388     GMimeStream *stream_filter = NULL;
389     GMimeDataWrapper *wrapper;
390     const char *charset;
391
392     if (! g_mime_content_type_is_type (content_type, "text", "*"))
393         INTERNAL_ERROR ("Illegal request to format non-text part (%s) as text.",
394                         g_mime_content_type_to_string (content_type));
395
396     if (stream_out == NULL)
397         return;
398
399     stream_filter = g_mime_stream_filter_new (stream_out);
400     g_mime_stream_filter_add(GMIME_STREAM_FILTER (stream_filter),
401                              g_mime_filter_crlf_new (FALSE, FALSE));
402
403     charset = g_mime_object_get_content_type_parameter (part, "charset");
404     if (charset) {
405         GMimeFilter *charset_filter;
406         charset_filter = g_mime_filter_charset_new (charset, "UTF-8");
407         /* This result can be NULL for things like "unknown-8bit".
408          * Don't set a NULL filter as that makes GMime print
409          * annoying assertion-failure messages on stderr. */
410         if (charset_filter)
411             g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
412                                       charset_filter);
413     }
414
415     wrapper = g_mime_part_get_content_object (GMIME_PART (part));
416     if (wrapper && stream_filter)
417         g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
418     if (stream_filter)
419         g_object_unref(stream_filter);
420 }
421
422 static const char*
423 signerstatustostring (GMimeSignerStatus x)
424 {
425     switch (x) {
426     case GMIME_SIGNER_STATUS_NONE:
427         return "none";
428     case GMIME_SIGNER_STATUS_GOOD:
429         return "good";
430     case GMIME_SIGNER_STATUS_BAD:
431         return "bad";
432     case GMIME_SIGNER_STATUS_ERROR:
433         return "error";
434     }
435     return "unknown";
436 }
437
438 static void
439 format_part_start_text (GMimeObject *part, int *part_count)
440 {
441     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
442
443     if (disposition &&
444         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
445     {
446         printf ("\fattachment{ ID: %d", *part_count);
447
448     } else {
449
450         printf ("\fpart{ ID: %d", *part_count);
451     }
452 }
453
454 static void
455 format_part_content_text (GMimeObject *part)
456 {
457     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
458     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
459     GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
460
461     printf (", Content-type: %s\n", g_mime_content_type_to_string (content_type));
462
463     if (disposition &&
464         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
465     {
466         const char *filename = g_mime_part_get_filename (GMIME_PART (part));
467         printf ("Attachment: %s (%s)\n", filename,
468                 g_mime_content_type_to_string (content_type));
469     }
470
471     if (g_mime_content_type_is_type (content_type, "text", "*") &&
472         !g_mime_content_type_is_type (content_type, "text", "html"))
473     {
474         g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
475         show_text_part_content (part, stream_stdout);
476         g_object_unref(stream_stdout);
477     }
478     else if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
479              g_mime_content_type_is_type (content_type, "message", "rfc822"))
480     {
481         /* Do nothing for multipart since its content will be printed
482          * when recursing. */
483     }
484     else
485     {
486         printf ("Non-text part: %s\n",
487                 g_mime_content_type_to_string (content_type));
488     }
489 }
490
491 static void
492 format_part_end_text (GMimeObject *part)
493 {
494     GMimeContentDisposition *disposition;
495
496     disposition = g_mime_object_get_content_disposition (part);
497     if (disposition &&
498         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
499     {
500         printf ("\fattachment}\n");
501     }
502     else
503     {
504         printf ("\fpart}\n");
505     }
506 }
507
508 static void
509 format_part_start_json (unused (GMimeObject *part), int *part_count)
510 {
511     printf ("{\"id\": %d", *part_count);
512 }
513
514 static void
515 format_part_encstatus_json (int status)
516 {
517     printf (", \"encstatus\": [{\"status\": ");
518     if (status) {
519         printf ("\"good\"");
520     } else {
521         printf ("\"bad\"");
522     }
523     printf ("}]");
524 }
525
526 static void
527 format_part_sigstatus_json (const GMimeSignatureValidity* validity)
528 {
529     printf (", \"sigstatus\": [");
530
531     if (!validity) {
532         printf ("]");
533         return;
534     }
535
536     const GMimeSigner *signer = g_mime_signature_validity_get_signers (validity);
537     int first = 1;
538     void *ctx_quote = talloc_new (NULL);
539
540     while (signer) {
541         if (first)
542             first = 0;
543         else
544             printf (", ");
545
546         printf ("{");
547
548         /* status */
549         printf ("\"status\": %s", json_quote_str (ctx_quote, signerstatustostring(signer->status)));
550
551         if (signer->status == GMIME_SIGNER_STATUS_GOOD)
552         {
553             if (signer->fingerprint)
554                 printf (", \"fingerprint\": %s", json_quote_str (ctx_quote, signer->fingerprint));
555             /* these dates are seconds since the epoch; should we
556              * provide a more human-readable format string? */
557             if (signer->created)
558                 printf (", \"created\": %d", (int) signer->created);
559             if (signer->expires)
560                 printf (", \"expires\": %d", (int) signer->expires);
561             /* output user id only if validity is FULL or ULTIMATE. */
562             /* note that gmime is using the term "trust" here, which
563              * is WRONG.  It's actually user id "validity". */
564             if ((signer->name) && (signer->trust)) {
565                 if ((signer->trust == GMIME_SIGNER_TRUST_FULLY) || (signer->trust == GMIME_SIGNER_TRUST_ULTIMATE))
566                     printf (", \"userid\": %s", json_quote_str (ctx_quote, signer->name));
567            }
568        } else {
569            if (signer->keyid)
570                printf (", \"keyid\": %s", json_quote_str (ctx_quote, signer->keyid));
571        }
572        if (signer->errors != GMIME_SIGNER_ERROR_NONE) {
573            printf (", \"errors\": %x", signer->errors);
574        }
575
576        printf ("}");
577        signer = signer->next;
578     }
579
580     printf ("]");
581
582     talloc_free (ctx_quote);
583 }
584
585 static void
586 format_part_content_json (GMimeObject *part)
587 {
588     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
589     GMimeStream *stream_memory = g_mime_stream_mem_new ();
590     const char *cid = g_mime_object_get_content_id (part);
591     void *ctx = talloc_new (NULL);
592     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
593     GByteArray *part_content;
594
595     printf (", \"content-type\": %s",
596             json_quote_str (ctx, g_mime_content_type_to_string (content_type)));
597
598     if (cid != NULL)
599             printf(", \"content-id\": %s", json_quote_str (ctx, cid));
600
601     if (disposition &&
602         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
603     {
604         const char *filename = g_mime_part_get_filename (GMIME_PART (part));
605
606         printf (", \"filename\": %s", json_quote_str (ctx, filename));
607     }
608
609     if (g_mime_content_type_is_type (content_type, "text", "*") &&
610         !g_mime_content_type_is_type (content_type, "text", "html"))
611     {
612         show_text_part_content (part, stream_memory);
613         part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
614
615         printf (", \"content\": %s", json_quote_chararray (ctx, (char *) part_content->data, part_content->len));
616     }
617     else if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
618              g_mime_content_type_is_type (content_type, "message", "rfc822"))
619     {
620         printf (", \"content\": [");
621     }
622
623     talloc_free (ctx);
624     if (stream_memory)
625         g_object_unref (stream_memory);
626 }
627
628 static void
629 format_part_end_json (GMimeObject *part)
630 {
631     GMimeContentType *content_type;
632
633     content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
634
635     if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
636         g_mime_content_type_is_type (content_type, "message", "rfc822"))
637         printf ("]");
638
639     printf ("}");
640 }
641
642 static void
643 format_part_content_raw (GMimeObject *part)
644 {
645     GMimeStream *stream_stdout;
646     GMimeStream *stream_filter = NULL;
647     GMimeDataWrapper *wrapper;
648
649     stream_stdout = g_mime_stream_file_new (stdout);
650     g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
651
652     stream_filter = g_mime_stream_filter_new (stream_stdout);
653
654     wrapper = g_mime_part_get_content_object (GMIME_PART (part));
655
656     if (wrapper && stream_filter)
657         g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
658
659     if (stream_filter)
660         g_object_unref (stream_filter);
661
662     if (stream_stdout)
663         g_object_unref(stream_stdout);
664 }
665
666 static void
667 show_message (void *ctx,
668               const notmuch_show_format_t *format,
669               notmuch_message_t *message,
670               int indent,
671               notmuch_show_params_t *params)
672 {
673     if (params->part <= 0) {
674         fputs (format->message_start, stdout);
675         if (format->message)
676             format->message(ctx, message, indent);
677
678         fputs (format->header_start, stdout);
679         if (format->header)
680             format->header(ctx, message);
681         fputs (format->header_end, stdout);
682
683         fputs (format->body_start, stdout);
684     }
685
686     if (format->part_content)
687         show_message_body (notmuch_message_get_filename (message),
688                            format, params);
689
690     if (params->part <= 0) {
691         fputs (format->body_end, stdout);
692
693         fputs (format->message_end, stdout);
694     }
695 }
696
697 static void
698 show_messages (void *ctx,
699                const notmuch_show_format_t *format,
700                notmuch_messages_t *messages,
701                int indent,
702                notmuch_show_params_t *params)
703 {
704     notmuch_message_t *message;
705     notmuch_bool_t match;
706     int first_set = 1;
707     int next_indent;
708
709     fputs (format->message_set_start, stdout);
710
711     for (;
712          notmuch_messages_valid (messages);
713          notmuch_messages_move_to_next (messages))
714     {
715         if (!first_set)
716             fputs (format->message_set_sep, stdout);
717         first_set = 0;
718
719         fputs (format->message_set_start, stdout);
720
721         message = notmuch_messages_get (messages);
722
723         match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
724
725         next_indent = indent;
726
727         if (match || params->entire_thread) {
728             show_message (ctx, format, message, indent, params);
729             next_indent = indent + 1;
730
731             fputs (format->message_set_sep, stdout);
732         }
733
734         show_messages (ctx,
735                        format,
736                        notmuch_message_get_replies (message),
737                        next_indent,
738                        params);
739
740         notmuch_message_destroy (message);
741
742         fputs (format->message_set_end, stdout);
743     }
744
745     fputs (format->message_set_end, stdout);
746 }
747
748 /* Formatted output of single message */
749 static int
750 do_show_single (void *ctx,
751                 notmuch_query_t *query,
752                 const notmuch_show_format_t *format,
753                 notmuch_show_params_t *params)
754 {
755     notmuch_messages_t *messages;
756     notmuch_message_t *message;
757
758     if (notmuch_query_count_messages (query) != 1) {
759         fprintf (stderr, "Error: search term did not match precisely one message.\n");
760         return 1;
761     }
762
763     messages = notmuch_query_search_messages (query);
764     message = notmuch_messages_get (messages);
765
766     if (message == NULL) {
767         fprintf (stderr, "Error: Cannot find matching message.\n");
768         return 1;
769     }
770
771     notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
772
773     /* Special case for --format=raw of full single message, just cat out file */
774     if (params->raw && 0 == params->part) {
775
776         const char *filename;
777         FILE *file;
778         size_t size;
779         char buf[4096];
780
781         filename = notmuch_message_get_filename (message);
782         if (filename == NULL) {
783             fprintf (stderr, "Error: Cannot message filename.\n");
784             return 1;
785         }
786
787         file = fopen (filename, "r");
788         if (file == NULL) {
789             fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
790             return 1;
791         }
792
793         while (!feof (file)) {
794             size = fread (buf, 1, sizeof (buf), file);
795             fwrite (buf, size, 1, stdout);
796         }
797
798         fclose (file);
799
800     } else {
801
802         show_message (ctx, format, message, 0, params);
803
804     }
805
806     return 0;
807 }
808
809 /* Formatted output of threads */
810 static int
811 do_show (void *ctx,
812          notmuch_query_t *query,
813          const notmuch_show_format_t *format,
814          notmuch_show_params_t *params)
815 {
816     notmuch_threads_t *threads;
817     notmuch_thread_t *thread;
818     notmuch_messages_t *messages;
819     int first_toplevel = 1;
820
821     fputs (format->message_set_start, stdout);
822
823     for (threads = notmuch_query_search_threads (query);
824          notmuch_threads_valid (threads);
825          notmuch_threads_move_to_next (threads))
826     {
827         thread = notmuch_threads_get (threads);
828
829         messages = notmuch_thread_get_toplevel_messages (thread);
830
831         if (messages == NULL)
832             INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
833                             notmuch_thread_get_thread_id (thread));
834
835         if (!first_toplevel)
836             fputs (format->message_set_sep, stdout);
837         first_toplevel = 0;
838
839         show_messages (ctx, format, messages, 0, params);
840
841         notmuch_thread_destroy (thread);
842
843     }
844
845     fputs (format->message_set_end, stdout);
846
847     return 0;
848 }
849
850 int
851 notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
852 {
853     notmuch_config_t *config;
854     notmuch_database_t *notmuch;
855     notmuch_query_t *query;
856     char *query_string;
857     char *opt;
858     const notmuch_show_format_t *format = &format_text;
859     notmuch_show_params_t params;
860     int mbox = 0;
861     int format_specified = 0;
862     int i;
863
864     params.entire_thread = 0;
865     params.raw = 0;
866     params.part = -1;
867     params.cryptoctx = NULL;
868     params.decrypt = 0;
869
870     for (i = 0; i < argc && argv[i][0] == '-'; i++) {
871         if (strcmp (argv[i], "--") == 0) {
872             i++;
873             break;
874         }
875         if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
876             opt = argv[i] + sizeof ("--format=") - 1;
877             if (strcmp (opt, "text") == 0) {
878                 format = &format_text;
879             } else if (strcmp (opt, "json") == 0) {
880                 format = &format_json;
881                 params.entire_thread = 1;
882             } else if (strcmp (opt, "mbox") == 0) {
883                 format = &format_mbox;
884                 mbox = 1;
885             } else if (strcmp (opt, "raw") == 0) {
886                 format = &format_raw;
887                 params.raw = 1;
888             } else {
889                 fprintf (stderr, "Invalid value for --format: %s\n", opt);
890                 return 1;
891             }
892             format_specified = 1;
893         } else if (STRNCMP_LITERAL (argv[i], "--part=") == 0) {
894             params.part = atoi(argv[i] + sizeof ("--part=") - 1);
895         } else if (STRNCMP_LITERAL (argv[i], "--entire-thread") == 0) {
896             params.entire_thread = 1;
897         } else if ((STRNCMP_LITERAL (argv[i], "--verify") == 0) ||
898                    (STRNCMP_LITERAL (argv[i], "--decrypt") == 0)) {
899             if (params.cryptoctx == NULL) {
900                 GMimeSession* session = g_object_new(notmuch_gmime_session_get_type(), NULL);
901                 if (NULL == (params.cryptoctx = g_mime_gpg_context_new(session, "gpg")))
902                     fprintf (stderr, "Failed to construct gpg context.\n");
903                 else
904                     g_mime_gpg_context_set_always_trust((GMimeGpgContext*)params.cryptoctx, FALSE);
905                 g_object_unref (session);
906                 session = NULL;
907             }
908             if (STRNCMP_LITERAL (argv[i], "--decrypt") == 0)
909                 params.decrypt = 1;
910         } else {
911             fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
912             return 1;
913         }
914     }
915
916     argc -= i;
917     argv += i;
918
919     config = notmuch_config_open (ctx, NULL, NULL);
920     if (config == NULL)
921         return 1;
922
923     query_string = query_string_from_args (ctx, argc, argv);
924     if (query_string == NULL) {
925         fprintf (stderr, "Out of memory\n");
926         return 1;
927     }
928
929     if (mbox && params.part > 0) {
930         fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
931         return 1;
932     }
933
934     if (*query_string == '\0') {
935         fprintf (stderr, "Error: notmuch show requires at least one search term.\n");
936         return 1;
937     }
938
939     notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
940                                      NOTMUCH_DATABASE_MODE_READ_ONLY);
941     if (notmuch == NULL)
942         return 1;
943
944     query = notmuch_query_create (notmuch, query_string);
945     if (query == NULL) {
946         fprintf (stderr, "Out of memory\n");
947         return 1;
948     }
949
950     /* if part was requested and format was not specified, use format=raw */
951     if (params.part >= 0 && !format_specified)
952         format = &format_raw;
953
954     /* If --format=raw specified without specifying part, we can only
955      * output single message, so set part=0 */
956     if (params.raw && params.part < 0)
957         params.part = 0;
958
959     if (params.part >= 0)
960         return do_show_single (ctx, query, format, &params);
961     else
962         return do_show (ctx, query, format, &params);
963
964     notmuch_query_destroy (query);
965     notmuch_database_close (notmuch);
966
967     if (params.cryptoctx)
968         g_object_unref(params.cryptoctx);
969
970     return 0;
971 }