notmuch show: Don't do text conversions for non-text parts
[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     /* do nothing if this is a multipart */
397     if (GMIME_IS_MULTIPART (part) || GMIME_IS_MESSAGE_PART (part))
398         return;
399
400     charset = g_mime_object_get_content_type_parameter (part, "charset");
401
402     if (stream_out) {
403         stream_filter = g_mime_stream_filter_new (stream_out);
404         g_mime_stream_filter_add(GMIME_STREAM_FILTER (stream_filter),
405                                  g_mime_filter_crlf_new (FALSE, FALSE));
406         if (charset) {
407             GMimeFilter *charset_filter;
408             charset_filter = g_mime_filter_charset_new (charset, "UTF-8");
409             /* This result can be NULL for things like "unknown-8bit".
410              * Don't set a NULL filter as that makes GMime print
411              * annoying assertion-failure messages on stderr. */
412             if (charset_filter)
413                 g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
414                                           charset_filter);
415         }
416     }
417
418     wrapper = g_mime_part_get_content_object (GMIME_PART (part));
419     if (wrapper && stream_filter)
420         g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
421     if (stream_filter)
422         g_object_unref(stream_filter);
423 }
424
425 static const char*
426 signerstatustostring (GMimeSignerStatus x)
427 {
428     switch (x) {
429     case GMIME_SIGNER_STATUS_NONE:
430         return "none";
431     case GMIME_SIGNER_STATUS_GOOD:
432         return "good";
433     case GMIME_SIGNER_STATUS_BAD:
434         return "bad";
435     case GMIME_SIGNER_STATUS_ERROR:
436         return "error";
437     }
438     return "unknown";
439 }
440
441 static void
442 format_part_start_text (GMimeObject *part, int *part_count)
443 {
444     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
445
446     if (disposition &&
447         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
448     {
449         printf ("\fattachment{ ID: %d", *part_count);
450
451     } else {
452
453         printf ("\fpart{ ID: %d", *part_count);
454     }
455 }
456
457 static void
458 format_part_content_text (GMimeObject *part)
459 {
460     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
461     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
462     GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
463
464     printf (", Content-type: %s\n", g_mime_content_type_to_string (content_type));
465
466     if (disposition &&
467         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
468     {
469         const char *filename = g_mime_part_get_filename (GMIME_PART (part));
470         printf ("Attachment: %s (%s)\n", filename,
471                 g_mime_content_type_to_string (content_type));
472     }
473
474     if (g_mime_content_type_is_type (content_type, "text", "*") &&
475         !g_mime_content_type_is_type (content_type, "text", "html"))
476     {
477         g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
478         show_text_part_content (part, stream_stdout);
479         g_object_unref(stream_stdout);
480     }
481     else if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
482              g_mime_content_type_is_type (content_type, "message", "rfc822"))
483     {
484         /* Do nothing for multipart since its content will be printed
485          * when recursing. */
486     }
487     else
488     {
489         printf ("Non-text part: %s\n",
490                 g_mime_content_type_to_string (content_type));
491     }
492 }
493
494 static void
495 format_part_end_text (GMimeObject *part)
496 {
497     GMimeContentDisposition *disposition;
498
499     disposition = g_mime_object_get_content_disposition (part);
500     if (disposition &&
501         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
502     {
503         printf ("\fattachment}\n");
504     }
505     else
506     {
507         printf ("\fpart}\n");
508     }
509 }
510
511 static void
512 format_part_start_json (unused (GMimeObject *part), int *part_count)
513 {
514     printf ("{\"id\": %d", *part_count);
515 }
516
517 static void
518 format_part_encstatus_json (int status)
519 {
520     printf (", \"encstatus\": [{\"status\": ");
521     if (status) {
522         printf ("\"good\"");
523     } else {
524         printf ("\"bad\"");
525     }
526     printf ("}]");
527 }
528
529 static void
530 format_part_sigstatus_json (const GMimeSignatureValidity* validity)
531 {
532     printf (", \"sigstatus\": [");
533
534     if (!validity) {
535         printf ("]");
536         return;
537     }
538
539     const GMimeSigner *signer = g_mime_signature_validity_get_signers (validity);
540     int first = 1;
541     void *ctx_quote = talloc_new (NULL);
542
543     while (signer) {
544         if (first)
545             first = 0;
546         else
547             printf (", ");
548
549         printf ("{");
550
551         /* status */
552         printf ("\"status\": %s", json_quote_str (ctx_quote, signerstatustostring(signer->status)));
553
554         if (signer->status == GMIME_SIGNER_STATUS_GOOD)
555         {
556             if (signer->fingerprint)
557                 printf (", \"fingerprint\": %s", json_quote_str (ctx_quote, signer->fingerprint));
558             /* these dates are seconds since the epoch; should we
559              * provide a more human-readable format string? */
560             if (signer->created)
561                 printf (", \"created\": %d", (int) signer->created);
562             if (signer->expires)
563                 printf (", \"expires\": %d", (int) signer->expires);
564             /* output user id only if validity is FULL or ULTIMATE. */
565             /* note that gmime is using the term "trust" here, which
566              * is WRONG.  It's actually user id "validity". */
567             if ((signer->name) && (signer->trust)) {
568                 if ((signer->trust == GMIME_SIGNER_TRUST_FULLY) || (signer->trust == GMIME_SIGNER_TRUST_ULTIMATE))
569                     printf (", \"userid\": %s", json_quote_str (ctx_quote, signer->name));
570            }
571        } else {
572            if (signer->keyid)
573                printf (", \"keyid\": %s", json_quote_str (ctx_quote, signer->keyid));
574        }
575        if (signer->errors != GMIME_SIGNER_ERROR_NONE) {
576            printf (", \"errors\": %x", signer->errors);
577        }
578
579        printf ("}");
580        signer = signer->next;
581     }
582
583     printf ("]");
584
585     talloc_free (ctx_quote);
586 }
587
588 static void
589 format_part_content_json (GMimeObject *part)
590 {
591     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
592     GMimeStream *stream_memory = g_mime_stream_mem_new ();
593     const char *cid = g_mime_object_get_content_id (part);
594     void *ctx = talloc_new (NULL);
595     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
596     GByteArray *part_content;
597
598     printf (", \"content-type\": %s",
599             json_quote_str (ctx, g_mime_content_type_to_string (content_type)));
600
601     if (cid != NULL)
602             printf(", \"content-id\": %s", json_quote_str (ctx, cid));
603
604     if (disposition &&
605         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
606     {
607         const char *filename = g_mime_part_get_filename (GMIME_PART (part));
608
609         printf (", \"filename\": %s", json_quote_str (ctx, filename));
610     }
611
612     if (g_mime_content_type_is_type (content_type, "text", "*") &&
613         !g_mime_content_type_is_type (content_type, "text", "html"))
614     {
615         show_text_part_content (part, stream_memory);
616         part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
617
618         printf (", \"content\": %s", json_quote_chararray (ctx, (char *) part_content->data, part_content->len));
619     }
620     else if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
621              g_mime_content_type_is_type (content_type, "message", "rfc822"))
622     {
623         printf (", \"content\": [");
624     }
625
626     talloc_free (ctx);
627     if (stream_memory)
628         g_object_unref (stream_memory);
629 }
630
631 static void
632 format_part_end_json (GMimeObject *part)
633 {
634     GMimeContentType *content_type;
635
636     content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
637
638     if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
639         g_mime_content_type_is_type (content_type, "message", "rfc822"))
640         printf ("]");
641
642     printf ("}");
643 }
644
645 static void
646 format_part_content_raw (GMimeObject *part)
647 {
648     GMimeStream *stream_stdout;
649     GMimeStream *stream_filter = NULL;
650     GMimeDataWrapper *wrapper;
651
652     stream_stdout = g_mime_stream_file_new (stdout);
653     g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
654
655     stream_filter = g_mime_stream_filter_new (stream_stdout);
656
657     wrapper = g_mime_part_get_content_object (GMIME_PART (part));
658
659     if (wrapper && stream_filter)
660         g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
661
662     if (stream_filter)
663         g_object_unref (stream_filter);
664
665     if (stream_stdout)
666         g_object_unref(stream_stdout);
667 }
668
669 static void
670 show_message (void *ctx,
671               const notmuch_show_format_t *format,
672               notmuch_message_t *message,
673               int indent,
674               notmuch_show_params_t *params)
675 {
676     if (params->part <= 0) {
677         fputs (format->message_start, stdout);
678         if (format->message)
679             format->message(ctx, message, indent);
680
681         fputs (format->header_start, stdout);
682         if (format->header)
683             format->header(ctx, message);
684         fputs (format->header_end, stdout);
685
686         fputs (format->body_start, stdout);
687     }
688
689     if (format->part_content)
690         show_message_body (notmuch_message_get_filename (message),
691                            format, params);
692
693     if (params->part <= 0) {
694         fputs (format->body_end, stdout);
695
696         fputs (format->message_end, stdout);
697     }
698 }
699
700 static void
701 show_messages (void *ctx,
702                const notmuch_show_format_t *format,
703                notmuch_messages_t *messages,
704                int indent,
705                notmuch_show_params_t *params)
706 {
707     notmuch_message_t *message;
708     notmuch_bool_t match;
709     int first_set = 1;
710     int next_indent;
711
712     fputs (format->message_set_start, stdout);
713
714     for (;
715          notmuch_messages_valid (messages);
716          notmuch_messages_move_to_next (messages))
717     {
718         if (!first_set)
719             fputs (format->message_set_sep, stdout);
720         first_set = 0;
721
722         fputs (format->message_set_start, stdout);
723
724         message = notmuch_messages_get (messages);
725
726         match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
727
728         next_indent = indent;
729
730         if (match || params->entire_thread) {
731             show_message (ctx, format, message, indent, params);
732             next_indent = indent + 1;
733
734             fputs (format->message_set_sep, stdout);
735         }
736
737         show_messages (ctx,
738                        format,
739                        notmuch_message_get_replies (message),
740                        next_indent,
741                        params);
742
743         notmuch_message_destroy (message);
744
745         fputs (format->message_set_end, stdout);
746     }
747
748     fputs (format->message_set_end, stdout);
749 }
750
751 /* Formatted output of single message */
752 static int
753 do_show_single (void *ctx,
754                 notmuch_query_t *query,
755                 const notmuch_show_format_t *format,
756                 notmuch_show_params_t *params)
757 {
758     notmuch_messages_t *messages;
759     notmuch_message_t *message;
760
761     if (notmuch_query_count_messages (query) != 1) {
762         fprintf (stderr, "Error: search term did not match precisely one message.\n");
763         return 1;
764     }
765
766     messages = notmuch_query_search_messages (query);
767     message = notmuch_messages_get (messages);
768
769     if (message == NULL) {
770         fprintf (stderr, "Error: Cannot find matching message.\n");
771         return 1;
772     }
773
774     notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
775
776     /* Special case for --format=raw of full single message, just cat out file */
777     if (params->raw && 0 == params->part) {
778
779         const char *filename;
780         FILE *file;
781         size_t size;
782         char buf[4096];
783
784         filename = notmuch_message_get_filename (message);
785         if (filename == NULL) {
786             fprintf (stderr, "Error: Cannot message filename.\n");
787             return 1;
788         }
789
790         file = fopen (filename, "r");
791         if (file == NULL) {
792             fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
793             return 1;
794         }
795
796         while (!feof (file)) {
797             size = fread (buf, 1, sizeof (buf), file);
798             fwrite (buf, size, 1, stdout);
799         }
800
801         fclose (file);
802
803     } else {
804
805         show_message (ctx, format, message, 0, params);
806
807     }
808
809     return 0;
810 }
811
812 /* Formatted output of threads */
813 static int
814 do_show (void *ctx,
815          notmuch_query_t *query,
816          const notmuch_show_format_t *format,
817          notmuch_show_params_t *params)
818 {
819     notmuch_threads_t *threads;
820     notmuch_thread_t *thread;
821     notmuch_messages_t *messages;
822     int first_toplevel = 1;
823
824     fputs (format->message_set_start, stdout);
825
826     for (threads = notmuch_query_search_threads (query);
827          notmuch_threads_valid (threads);
828          notmuch_threads_move_to_next (threads))
829     {
830         thread = notmuch_threads_get (threads);
831
832         messages = notmuch_thread_get_toplevel_messages (thread);
833
834         if (messages == NULL)
835             INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
836                             notmuch_thread_get_thread_id (thread));
837
838         if (!first_toplevel)
839             fputs (format->message_set_sep, stdout);
840         first_toplevel = 0;
841
842         show_messages (ctx, format, messages, 0, params);
843
844         notmuch_thread_destroy (thread);
845
846     }
847
848     fputs (format->message_set_end, stdout);
849
850     return 0;
851 }
852
853 int
854 notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
855 {
856     notmuch_config_t *config;
857     notmuch_database_t *notmuch;
858     notmuch_query_t *query;
859     char *query_string;
860     char *opt;
861     const notmuch_show_format_t *format = &format_text;
862     notmuch_show_params_t params;
863     int mbox = 0;
864     int format_specified = 0;
865     int i;
866
867     params.entire_thread = 0;
868     params.raw = 0;
869     params.part = -1;
870     params.cryptoctx = NULL;
871     params.decrypt = 0;
872
873     for (i = 0; i < argc && argv[i][0] == '-'; i++) {
874         if (strcmp (argv[i], "--") == 0) {
875             i++;
876             break;
877         }
878         if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
879             opt = argv[i] + sizeof ("--format=") - 1;
880             if (strcmp (opt, "text") == 0) {
881                 format = &format_text;
882             } else if (strcmp (opt, "json") == 0) {
883                 format = &format_json;
884                 params.entire_thread = 1;
885             } else if (strcmp (opt, "mbox") == 0) {
886                 format = &format_mbox;
887                 mbox = 1;
888             } else if (strcmp (opt, "raw") == 0) {
889                 format = &format_raw;
890                 params.raw = 1;
891             } else {
892                 fprintf (stderr, "Invalid value for --format: %s\n", opt);
893                 return 1;
894             }
895             format_specified = 1;
896         } else if (STRNCMP_LITERAL (argv[i], "--part=") == 0) {
897             params.part = atoi(argv[i] + sizeof ("--part=") - 1);
898         } else if (STRNCMP_LITERAL (argv[i], "--entire-thread") == 0) {
899             params.entire_thread = 1;
900         } else if ((STRNCMP_LITERAL (argv[i], "--verify") == 0) ||
901                    (STRNCMP_LITERAL (argv[i], "--decrypt") == 0)) {
902             if (params.cryptoctx == NULL) {
903                 GMimeSession* session = g_object_new(notmuch_gmime_session_get_type(), NULL);
904                 if (NULL == (params.cryptoctx = g_mime_gpg_context_new(session, "gpg")))
905                     fprintf (stderr, "Failed to construct gpg context.\n");
906                 else
907                     g_mime_gpg_context_set_always_trust((GMimeGpgContext*)params.cryptoctx, FALSE);
908                 g_object_unref (session);
909                 session = NULL;
910             }
911             if (STRNCMP_LITERAL (argv[i], "--decrypt") == 0)
912                 params.decrypt = 1;
913         } else {
914             fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
915             return 1;
916         }
917     }
918
919     argc -= i;
920     argv += i;
921
922     config = notmuch_config_open (ctx, NULL, NULL);
923     if (config == NULL)
924         return 1;
925
926     query_string = query_string_from_args (ctx, argc, argv);
927     if (query_string == NULL) {
928         fprintf (stderr, "Out of memory\n");
929         return 1;
930     }
931
932     if (mbox && params.part > 0) {
933         fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
934         return 1;
935     }
936
937     if (*query_string == '\0') {
938         fprintf (stderr, "Error: notmuch show requires at least one search term.\n");
939         return 1;
940     }
941
942     notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
943                                      NOTMUCH_DATABASE_MODE_READ_ONLY);
944     if (notmuch == NULL)
945         return 1;
946
947     query = notmuch_query_create (notmuch, query_string);
948     if (query == NULL) {
949         fprintf (stderr, "Out of memory\n");
950         return 1;
951     }
952
953     /* if part was requested and format was not specified, use format=raw */
954     if (params.part >= 0 && !format_specified)
955         format = &format_raw;
956
957     /* If --format=raw specified without specifying part, we can only
958      * output single message, so set part=0 */
959     if (params.raw && params.part < 0)
960         params.part = 0;
961
962     if (params.part >= 0)
963         return do_show_single (ctx, query, format, &params);
964     else
965         return do_show (ctx, query, format, &params);
966
967     notmuch_query_destroy (query);
968     notmuch_database_close (notmuch);
969
970     if (params.cryptoctx)
971         g_object_unref(params.cryptoctx);
972
973     return 0;
974 }