test emacs: Add tests for hiding messages in notmuch-show view
[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 signer_status_to_string (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
460     printf (", Content-type: %s\n", g_mime_content_type_to_string (content_type));
461
462     if (disposition &&
463         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
464     {
465         const char *filename = g_mime_part_get_filename (GMIME_PART (part));
466         printf ("Attachment: %s (%s)\n", filename,
467                 g_mime_content_type_to_string (content_type));
468     }
469
470     if (g_mime_content_type_is_type (content_type, "text", "*") &&
471         !g_mime_content_type_is_type (content_type, "text", "html"))
472     {
473         GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
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",
550                 json_quote_str (ctx_quote,
551                                 signer_status_to_string (signer->status)));
552
553         if (signer->status == GMIME_SIGNER_STATUS_GOOD)
554         {
555             if (signer->fingerprint)
556                 printf (", \"fingerprint\": %s", json_quote_str (ctx_quote, signer->fingerprint));
557             /* these dates are seconds since the epoch; should we
558              * provide a more human-readable format string? */
559             if (signer->created)
560                 printf (", \"created\": %d", (int) signer->created);
561             if (signer->expires)
562                 printf (", \"expires\": %d", (int) signer->expires);
563             /* output user id only if validity is FULL or ULTIMATE. */
564             /* note that gmime is using the term "trust" here, which
565              * is WRONG.  It's actually user id "validity". */
566             if ((signer->name) && (signer->trust)) {
567                 if ((signer->trust == GMIME_SIGNER_TRUST_FULLY) || (signer->trust == GMIME_SIGNER_TRUST_ULTIMATE))
568                     printf (", \"userid\": %s", json_quote_str (ctx_quote, signer->name));
569            }
570        } else {
571            if (signer->keyid)
572                printf (", \"keyid\": %s", json_quote_str (ctx_quote, signer->keyid));
573        }
574        if (signer->errors != GMIME_SIGNER_ERROR_NONE) {
575            printf (", \"errors\": %x", signer->errors);
576        }
577
578        printf ("}");
579        signer = signer->next;
580     }
581
582     printf ("]");
583
584     talloc_free (ctx_quote);
585 }
586
587 static void
588 format_part_content_json (GMimeObject *part)
589 {
590     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
591     GMimeStream *stream_memory = g_mime_stream_mem_new ();
592     const char *cid = g_mime_object_get_content_id (part);
593     void *ctx = talloc_new (NULL);
594     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
595     GByteArray *part_content;
596
597     printf (", \"content-type\": %s",
598             json_quote_str (ctx, g_mime_content_type_to_string (content_type)));
599
600     if (cid != NULL)
601             printf(", \"content-id\": %s", json_quote_str (ctx, cid));
602
603     if (disposition &&
604         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
605     {
606         const char *filename = g_mime_part_get_filename (GMIME_PART (part));
607
608         printf (", \"filename\": %s", json_quote_str (ctx, filename));
609     }
610
611     if (g_mime_content_type_is_type (content_type, "text", "*") &&
612         !g_mime_content_type_is_type (content_type, "text", "html"))
613     {
614         show_text_part_content (part, stream_memory);
615         part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
616
617         printf (", \"content\": %s", json_quote_chararray (ctx, (char *) part_content->data, part_content->len));
618     }
619     else if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
620              g_mime_content_type_is_type (content_type, "message", "rfc822"))
621     {
622         printf (", \"content\": [");
623     }
624
625     talloc_free (ctx);
626     if (stream_memory)
627         g_object_unref (stream_memory);
628 }
629
630 static void
631 format_part_end_json (GMimeObject *part)
632 {
633     GMimeContentType *content_type;
634
635     content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
636
637     if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
638         g_mime_content_type_is_type (content_type, "message", "rfc822"))
639         printf ("]");
640
641     printf ("}");
642 }
643
644 static void
645 format_part_content_raw (GMimeObject *part)
646 {
647     GMimeStream *stream_stdout;
648     GMimeStream *stream_filter = NULL;
649     GMimeDataWrapper *wrapper;
650
651     stream_stdout = g_mime_stream_file_new (stdout);
652     g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
653
654     stream_filter = g_mime_stream_filter_new (stream_stdout);
655
656     wrapper = g_mime_part_get_content_object (GMIME_PART (part));
657
658     if (wrapper && stream_filter)
659         g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
660
661     if (stream_filter)
662         g_object_unref (stream_filter);
663
664     if (stream_stdout)
665         g_object_unref(stream_stdout);
666 }
667
668 static void
669 show_message (void *ctx,
670               const notmuch_show_format_t *format,
671               notmuch_message_t *message,
672               int indent,
673               notmuch_show_params_t *params)
674 {
675     if (params->part <= 0) {
676         fputs (format->message_start, stdout);
677         if (format->message)
678             format->message(ctx, message, indent);
679
680         fputs (format->header_start, stdout);
681         if (format->header)
682             format->header(ctx, message);
683         fputs (format->header_end, stdout);
684
685         fputs (format->body_start, stdout);
686     }
687
688     if (format->part_content)
689         show_message_body (notmuch_message_get_filename (message),
690                            format, params);
691
692     if (params->part <= 0) {
693         fputs (format->body_end, stdout);
694
695         fputs (format->message_end, stdout);
696     }
697 }
698
699 static void
700 show_messages (void *ctx,
701                const notmuch_show_format_t *format,
702                notmuch_messages_t *messages,
703                int indent,
704                notmuch_show_params_t *params)
705 {
706     notmuch_message_t *message;
707     notmuch_bool_t match;
708     int first_set = 1;
709     int next_indent;
710
711     fputs (format->message_set_start, stdout);
712
713     for (;
714          notmuch_messages_valid (messages);
715          notmuch_messages_move_to_next (messages))
716     {
717         if (!first_set)
718             fputs (format->message_set_sep, stdout);
719         first_set = 0;
720
721         fputs (format->message_set_start, stdout);
722
723         message = notmuch_messages_get (messages);
724
725         match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
726
727         next_indent = indent;
728
729         if (match || params->entire_thread) {
730             show_message (ctx, format, message, indent, params);
731             next_indent = indent + 1;
732
733             fputs (format->message_set_sep, stdout);
734         }
735
736         show_messages (ctx,
737                        format,
738                        notmuch_message_get_replies (message),
739                        next_indent,
740                        params);
741
742         notmuch_message_destroy (message);
743
744         fputs (format->message_set_end, stdout);
745     }
746
747     fputs (format->message_set_end, stdout);
748 }
749
750 /* Formatted output of single message */
751 static int
752 do_show_single (void *ctx,
753                 notmuch_query_t *query,
754                 const notmuch_show_format_t *format,
755                 notmuch_show_params_t *params)
756 {
757     notmuch_messages_t *messages;
758     notmuch_message_t *message;
759
760     if (notmuch_query_count_messages (query) != 1) {
761         fprintf (stderr, "Error: search term did not match precisely one message.\n");
762         return 1;
763     }
764
765     messages = notmuch_query_search_messages (query);
766     message = notmuch_messages_get (messages);
767
768     if (message == NULL) {
769         fprintf (stderr, "Error: Cannot find matching message.\n");
770         return 1;
771     }
772
773     notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
774
775     /* Special case for --format=raw of full single message, just cat out file */
776     if (params->raw && 0 == params->part) {
777
778         const char *filename;
779         FILE *file;
780         size_t size;
781         char buf[4096];
782
783         filename = notmuch_message_get_filename (message);
784         if (filename == NULL) {
785             fprintf (stderr, "Error: Cannot message filename.\n");
786             return 1;
787         }
788
789         file = fopen (filename, "r");
790         if (file == NULL) {
791             fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
792             return 1;
793         }
794
795         while (!feof (file)) {
796             size = fread (buf, 1, sizeof (buf), file);
797             fwrite (buf, size, 1, stdout);
798         }
799
800         fclose (file);
801
802     } else {
803
804         show_message (ctx, format, message, 0, params);
805
806     }
807
808     return 0;
809 }
810
811 /* Formatted output of threads */
812 static int
813 do_show (void *ctx,
814          notmuch_query_t *query,
815          const notmuch_show_format_t *format,
816          notmuch_show_params_t *params)
817 {
818     notmuch_threads_t *threads;
819     notmuch_thread_t *thread;
820     notmuch_messages_t *messages;
821     int first_toplevel = 1;
822
823     fputs (format->message_set_start, stdout);
824
825     for (threads = notmuch_query_search_threads (query);
826          notmuch_threads_valid (threads);
827          notmuch_threads_move_to_next (threads))
828     {
829         thread = notmuch_threads_get (threads);
830
831         messages = notmuch_thread_get_toplevel_messages (thread);
832
833         if (messages == NULL)
834             INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
835                             notmuch_thread_get_thread_id (thread));
836
837         if (!first_toplevel)
838             fputs (format->message_set_sep, stdout);
839         first_toplevel = 0;
840
841         show_messages (ctx, format, messages, 0, params);
842
843         notmuch_thread_destroy (thread);
844
845     }
846
847     fputs (format->message_set_end, stdout);
848
849     return 0;
850 }
851
852 int
853 notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
854 {
855     notmuch_config_t *config;
856     notmuch_database_t *notmuch;
857     notmuch_query_t *query;
858     char *query_string;
859     char *opt;
860     const notmuch_show_format_t *format = &format_text;
861     notmuch_show_params_t params;
862     int mbox = 0;
863     int format_specified = 0;
864     int i;
865
866     params.entire_thread = 0;
867     params.raw = 0;
868     params.part = -1;
869     params.cryptoctx = NULL;
870     params.decrypt = 0;
871
872     for (i = 0; i < argc && argv[i][0] == '-'; i++) {
873         if (strcmp (argv[i], "--") == 0) {
874             i++;
875             break;
876         }
877         if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
878             opt = argv[i] + sizeof ("--format=") - 1;
879             if (strcmp (opt, "text") == 0) {
880                 format = &format_text;
881             } else if (strcmp (opt, "json") == 0) {
882                 format = &format_json;
883                 params.entire_thread = 1;
884             } else if (strcmp (opt, "mbox") == 0) {
885                 format = &format_mbox;
886                 mbox = 1;
887             } else if (strcmp (opt, "raw") == 0) {
888                 format = &format_raw;
889                 params.raw = 1;
890             } else {
891                 fprintf (stderr, "Invalid value for --format: %s\n", opt);
892                 return 1;
893             }
894             format_specified = 1;
895         } else if (STRNCMP_LITERAL (argv[i], "--part=") == 0) {
896             params.part = atoi(argv[i] + sizeof ("--part=") - 1);
897         } else if (STRNCMP_LITERAL (argv[i], "--entire-thread") == 0) {
898             params.entire_thread = 1;
899         } else if ((STRNCMP_LITERAL (argv[i], "--verify") == 0) ||
900                    (STRNCMP_LITERAL (argv[i], "--decrypt") == 0)) {
901             if (params.cryptoctx == NULL) {
902                 GMimeSession* session = g_object_new(g_mime_session_get_type(), NULL);
903                 if (NULL == (params.cryptoctx = g_mime_gpg_context_new(session, "gpg")))
904                     fprintf (stderr, "Failed to construct gpg context.\n");
905                 else
906                     g_mime_gpg_context_set_always_trust((GMimeGpgContext*)params.cryptoctx, FALSE);
907                 g_object_unref (session);
908                 session = NULL;
909             }
910             if (STRNCMP_LITERAL (argv[i], "--decrypt") == 0)
911                 params.decrypt = 1;
912         } else {
913             fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
914             return 1;
915         }
916     }
917
918     argc -= i;
919     argv += i;
920
921     config = notmuch_config_open (ctx, NULL, NULL);
922     if (config == NULL)
923         return 1;
924
925     query_string = query_string_from_args (ctx, argc, argv);
926     if (query_string == NULL) {
927         fprintf (stderr, "Out of memory\n");
928         return 1;
929     }
930
931     if (mbox && params.part > 0) {
932         fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
933         return 1;
934     }
935
936     if (*query_string == '\0') {
937         fprintf (stderr, "Error: notmuch show requires at least one search term.\n");
938         return 1;
939     }
940
941     notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
942                                      NOTMUCH_DATABASE_MODE_READ_ONLY);
943     if (notmuch == NULL)
944         return 1;
945
946     query = notmuch_query_create (notmuch, query_string);
947     if (query == NULL) {
948         fprintf (stderr, "Out of memory\n");
949         return 1;
950     }
951
952     /* if part was requested and format was not specified, use format=raw */
953     if (params.part >= 0 && !format_specified)
954         format = &format_raw;
955
956     /* If --format=raw specified without specifying part, we can only
957      * output single message, so set part=0 */
958     if (params.raw && params.part < 0)
959         params.part = 0;
960
961     if (params.part >= 0)
962         return do_show_single (ctx, query, format, &params);
963     else
964         return do_show (ctx, query, format, &params);
965
966     notmuch_query_destroy (query);
967     notmuch_database_close (notmuch);
968
969     if (params.cryptoctx)
970         g_object_unref(params.cryptoctx);
971
972     return 0;
973 }