]> git.notmuchmail.org Git - notmuch/blob - notmuch-show.c
CLI/show: initial support for --duplicate for (raw output only)
[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 https://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "notmuch-client.h"
22 #include "gmime-filter-reply.h"
23 #include "sprinter.h"
24 #include "zlib-extra.h"
25
26 static const char *
27 _get_filename (notmuch_message_t *message, int index)
28 {
29     notmuch_filenames_t *filenames = notmuch_message_get_filenames (message);
30     int i = 1;
31
32     for (;
33          notmuch_filenames_valid (filenames);
34          notmuch_filenames_move_to_next (filenames), i++) {
35         if (i >= index)
36             return notmuch_filenames_get (filenames);
37     }
38     return NULL;
39 }
40
41 static const char *
42 _get_tags_as_string (const void *ctx, notmuch_message_t *message)
43 {
44     notmuch_tags_t *tags;
45     int first = 1;
46     const char *tag;
47     char *result;
48
49     result = talloc_strdup (ctx, "");
50     if (result == NULL)
51         return NULL;
52
53     for (tags = notmuch_message_get_tags (message);
54          notmuch_tags_valid (tags);
55          notmuch_tags_move_to_next (tags)) {
56         tag = notmuch_tags_get (tags);
57
58         result = talloc_asprintf_append (result, "%s%s",
59                                          first ? "" : " ", tag);
60         first = 0;
61     }
62
63     return result;
64 }
65
66 /* Get a nice, single-line summary of message. */
67 static const char *
68 _get_one_line_summary (const void *ctx, notmuch_message_t *message)
69 {
70     const char *from;
71     time_t date;
72     const char *relative_date;
73     const char *tags;
74
75     from = notmuch_message_get_header (message, "from");
76
77     date = notmuch_message_get_date (message);
78     relative_date = notmuch_time_relative_date (ctx, date);
79
80     tags = _get_tags_as_string (ctx, message);
81
82     return talloc_asprintf (ctx, "%s (%s) (%s)",
83                             from, relative_date, tags);
84 }
85
86 static const char *
87 _get_disposition (GMimeObject *meta)
88 {
89     GMimeContentDisposition *disposition;
90
91     disposition = g_mime_object_get_content_disposition (meta);
92     if (! disposition)
93         return NULL;
94
95     return g_mime_content_disposition_get_disposition (disposition);
96 }
97
98 static bool
99 _get_message_flag (notmuch_message_t *message, notmuch_message_flag_t flag)
100 {
101     notmuch_bool_t is_set;
102     notmuch_status_t status;
103
104     status = notmuch_message_get_flag_st (message, flag, &is_set);
105
106     if (print_status_message ("notmuch show", message, status))
107         INTERNAL_ERROR ("unexpected error getting message flag\n");
108
109     return is_set;
110 }
111
112 /* Emit a sequence of key/value pairs for the metadata of message.
113  * The caller should begin a map before calling this. */
114 static void
115 format_message_sprinter (sprinter_t *sp, notmuch_message_t *message)
116 {
117     /* Any changes to the JSON or S-Expression format should be
118      * reflected in the file devel/schemata. */
119
120     void *local = talloc_new (NULL);
121     notmuch_tags_t *tags;
122     time_t date;
123     const char *relative_date;
124
125     sp->map_key (sp, "id");
126     sp->string (sp, notmuch_message_get_message_id (message));
127
128     sp->map_key (sp, "match");
129     sp->boolean (sp, _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH));
130
131     sp->map_key (sp, "excluded");
132     sp->boolean (sp, _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED));
133
134     sp->map_key (sp, "filename");
135     if (notmuch_format_version >= 3) {
136         notmuch_filenames_t *filenames;
137
138         sp->begin_list (sp);
139         for (filenames = notmuch_message_get_filenames (message);
140              notmuch_filenames_valid (filenames);
141              notmuch_filenames_move_to_next (filenames)) {
142             sp->string (sp, notmuch_filenames_get (filenames));
143         }
144         notmuch_filenames_destroy (filenames);
145         sp->end (sp);
146     } else {
147         sp->string (sp, notmuch_message_get_filename (message));
148     }
149
150     sp->map_key (sp, "timestamp");
151     date = notmuch_message_get_date (message);
152     sp->integer (sp, date);
153
154     sp->map_key (sp, "date_relative");
155     relative_date = notmuch_time_relative_date (local, date);
156     sp->string (sp, relative_date);
157
158     sp->map_key (sp, "tags");
159     sp->begin_list (sp);
160     for (tags = notmuch_message_get_tags (message);
161          notmuch_tags_valid (tags);
162          notmuch_tags_move_to_next (tags))
163         sp->string (sp, notmuch_tags_get (tags));
164     sp->end (sp);
165
166     talloc_free (local);
167 }
168
169 /* Extract just the email address from the contents of a From:
170  * header. */
171 static const char *
172 _extract_email_address (const void *ctx, const char *from)
173 {
174     InternetAddressList *addresses;
175     InternetAddress *address;
176     InternetAddressMailbox *mailbox;
177     const char *email = "MAILER-DAEMON";
178
179     addresses = internet_address_list_parse (NULL, from);
180
181     /* Bail if there is no address here. */
182     if (addresses == NULL || internet_address_list_length (addresses) < 1)
183         goto DONE;
184
185     /* Otherwise, just use the first address. */
186     address = internet_address_list_get_address (addresses, 0);
187
188     /* The From header should never contain an address group rather
189      * than a mailbox. So bail if it does. */
190     if (! INTERNET_ADDRESS_IS_MAILBOX (address))
191         goto DONE;
192
193     mailbox = INTERNET_ADDRESS_MAILBOX (address);
194     email = internet_address_mailbox_get_addr (mailbox);
195     email = talloc_strdup (ctx, email);
196
197   DONE:
198     if (addresses)
199         g_object_unref (addresses);
200
201     return email;
202 }
203
204 /* Return 1 if 'line' is an mbox From_ line---that is, a line
205  * beginning with zero or more '>' characters followed by the
206  * characters 'F', 'r', 'o', 'm', and space.
207  *
208  * Any characters at all may appear after that in the line.
209  */
210 static int
211 _is_from_line (const char *line)
212 {
213     const char *s = line;
214
215     if (line == NULL)
216         return 0;
217
218     while (*s == '>')
219         s++;
220
221     if (STRNCMP_LITERAL (s, "From ") == 0)
222         return 1;
223     else
224         return 0;
225 }
226
227 /* Output extra headers if configured with the `show.extra_headers'
228  * configuration option
229  */
230 static void
231 format_extra_headers_sprinter (sprinter_t *sp, GMimeMessage *message)
232 {
233     GMimeHeaderList *header_list = g_mime_object_get_header_list (GMIME_OBJECT (message));
234
235     for (notmuch_config_values_t *extra_headers = notmuch_config_get_values (
236              sp->notmuch, NOTMUCH_CONFIG_EXTRA_HEADERS);
237          notmuch_config_values_valid (extra_headers);
238          notmuch_config_values_move_to_next (extra_headers)) {
239         GMimeHeader *header;
240         const char *header_name = notmuch_config_values_get (extra_headers);
241
242         header = g_mime_header_list_get_header (header_list, header_name);
243         if (header == NULL)
244             continue;
245
246         sp->map_key (sp, g_mime_header_get_name (header));
247         sp->string (sp, g_mime_header_get_value (header));
248     }
249 }
250
251 void
252 format_headers_sprinter (sprinter_t *sp, GMimeMessage *message,
253                          bool reply, const _notmuch_message_crypto_t *msg_crypto)
254 {
255     /* Any changes to the JSON or S-Expression format should be
256      * reflected in the file devel/schemata. */
257
258     char *recipients_string;
259     const char *reply_to_string;
260     void *local = talloc_new (sp);
261
262     sp->begin_map (sp);
263
264     sp->map_key (sp, "Subject");
265     if (msg_crypto && msg_crypto->payload_subject) {
266         sp->string (sp, msg_crypto->payload_subject);
267     } else
268         sp->string (sp, g_mime_message_get_subject (message));
269
270     sp->map_key (sp, "From");
271     sp->string (sp, g_mime_message_get_from_string (message));
272
273     recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_TO);
274     if (recipients_string) {
275         sp->map_key (sp, "To");
276         sp->string (sp, recipients_string);
277         g_free (recipients_string);
278     }
279
280     recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_CC);
281     if (recipients_string) {
282         sp->map_key (sp, "Cc");
283         sp->string (sp, recipients_string);
284         g_free (recipients_string);
285     }
286
287     recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_BCC);
288     if (recipients_string) {
289         sp->map_key (sp, "Bcc");
290         sp->string (sp, recipients_string);
291         g_free (recipients_string);
292     }
293
294     reply_to_string = g_mime_message_get_reply_to_string (local, message);
295     if (reply_to_string) {
296         sp->map_key (sp, "Reply-To");
297         sp->string (sp, reply_to_string);
298     }
299
300     if (reply) {
301         sp->map_key (sp, "In-reply-to");
302         sp->string (sp, g_mime_object_get_header (GMIME_OBJECT (message), "In-reply-to"));
303
304         sp->map_key (sp, "References");
305         sp->string (sp, g_mime_object_get_header (GMIME_OBJECT (message), "References"));
306     } else {
307         sp->map_key (sp, "Date");
308         sp->string (sp, g_mime_message_get_date_string (sp, message));
309     }
310
311     /* Output extra headers the user has configured, if any */
312     if (! reply)
313         format_extra_headers_sprinter (sp, message);
314     sp->end (sp);
315     talloc_free (local);
316 }
317
318 /* Write a MIME text part out to the given stream.
319  *
320  * If (flags & NOTMUCH_SHOW_TEXT_PART_REPLY), this prepends "> " to
321  * each output line.
322  *
323  * Both line-ending conversion (CRLF->LF) and charset conversion ( ->
324  * UTF-8) will be performed, so it is inappropriate to call this
325  * function with a non-text part. Doing so will trigger an internal
326  * error.
327  */
328 void
329 show_text_part_content (GMimeObject *part, GMimeStream *stream_out,
330                         notmuch_show_text_part_flags flags)
331 {
332     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
333     GMimeStream *stream_filter = NULL;
334     GMimeFilter *crlf_filter = NULL;
335     GMimeFilter *windows_filter = NULL;
336     GMimeDataWrapper *wrapper;
337     const char *charset;
338
339     if (! g_mime_content_type_is_type (content_type, "text", "*"))
340         INTERNAL_ERROR ("Illegal request to format non-text part (%s) as text.",
341                         g_mime_content_type_get_mime_type (content_type));
342
343     if (stream_out == NULL)
344         return;
345
346     charset = g_mime_object_get_content_type_parameter (part, "charset");
347     charset = charset ? g_mime_charset_canon_name (charset) : NULL;
348     wrapper = g_mime_part_get_content (GMIME_PART (part));
349     if (wrapper && charset && ! g_ascii_strncasecmp (charset, "iso-8859-", 9)) {
350         GMimeStream *null_stream = NULL;
351         GMimeStream *null_stream_filter = NULL;
352
353         /* Check for mislabeled Windows encoding */
354         null_stream = g_mime_stream_null_new ();
355         null_stream_filter = g_mime_stream_filter_new (null_stream);
356         windows_filter = g_mime_filter_windows_new (charset);
357         g_mime_stream_filter_add (GMIME_STREAM_FILTER (null_stream_filter),
358                                   windows_filter);
359         g_mime_data_wrapper_write_to_stream (wrapper, null_stream_filter);
360         charset = g_mime_filter_windows_real_charset (
361             (GMimeFilterWindows *) windows_filter);
362
363         if (null_stream_filter)
364             g_object_unref (null_stream_filter);
365         if (null_stream)
366             g_object_unref (null_stream);
367         /* Keep a reference to windows_filter in order to prevent the
368          * charset string from deallocation. */
369     }
370
371     stream_filter = g_mime_stream_filter_new (stream_out);
372     crlf_filter = g_mime_filter_dos2unix_new (false);
373     g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
374                               crlf_filter);
375     g_object_unref (crlf_filter);
376
377     if (charset) {
378         GMimeFilter *charset_filter;
379         charset_filter = g_mime_filter_charset_new (charset, "UTF-8");
380         /* This result can be NULL for things like "unknown-8bit".
381          * Don't set a NULL filter as that makes GMime print
382          * annoying assertion-failure messages on stderr. */
383         if (charset_filter) {
384             g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
385                                       charset_filter);
386             g_object_unref (charset_filter);
387         }
388
389     }
390
391     if (flags & NOTMUCH_SHOW_TEXT_PART_REPLY) {
392         GMimeFilter *reply_filter;
393         reply_filter = g_mime_filter_reply_new (true);
394         if (reply_filter) {
395             g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
396                                       reply_filter);
397             g_object_unref (reply_filter);
398         }
399     }
400
401     if (wrapper && stream_filter)
402         g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
403     if (stream_filter)
404         g_object_unref (stream_filter);
405     if (windows_filter)
406         g_object_unref (windows_filter);
407 }
408
409 static const char *
410 signature_status_to_string (GMimeSignatureStatus status)
411 {
412     if (g_mime_signature_status_bad (status))
413         return "bad";
414
415     if (g_mime_signature_status_error (status))
416         return "error";
417
418     if (g_mime_signature_status_good (status))
419         return "good";
420
421     return "unknown";
422 }
423
424 /* Print signature flags */
425 struct key_map_struct {
426     GMimeSignatureStatus bit;
427     const char *string;
428 };
429
430 static void
431 do_format_signature_errors (sprinter_t *sp, struct key_map_struct *key_map,
432                             unsigned int array_map_len, GMimeSignatureStatus errors)
433 {
434     sp->map_key (sp, "errors");
435     sp->begin_map (sp);
436
437     for (unsigned int i = 0; i < array_map_len; i++) {
438         if (errors & key_map[i].bit) {
439             sp->map_key (sp, key_map[i].string);
440             sp->boolean (sp, true);
441         }
442     }
443
444     sp->end (sp);
445 }
446
447 static void
448 format_signature_errors (sprinter_t *sp, GMimeSignature *signature)
449 {
450     GMimeSignatureStatus errors = g_mime_signature_get_status (signature);
451
452     if (! (errors & GMIME_SIGNATURE_STATUS_ERROR_MASK))
453         return;
454
455     struct key_map_struct key_map[] = {
456         { GMIME_SIGNATURE_STATUS_KEY_REVOKED, "key-revoked" },
457         { GMIME_SIGNATURE_STATUS_KEY_EXPIRED, "key-expired" },
458         { GMIME_SIGNATURE_STATUS_SIG_EXPIRED, "sig-expired" },
459         { GMIME_SIGNATURE_STATUS_KEY_MISSING, "key-missing" },
460         { GMIME_SIGNATURE_STATUS_CRL_MISSING, "crl-missing" },
461         { GMIME_SIGNATURE_STATUS_CRL_TOO_OLD, "crl-too-old" },
462         { GMIME_SIGNATURE_STATUS_BAD_POLICY, "bad-policy" },
463         { GMIME_SIGNATURE_STATUS_SYS_ERROR, "sys-error" },
464         { GMIME_SIGNATURE_STATUS_TOFU_CONFLICT, "tofu-conflict" },
465     };
466
467     do_format_signature_errors (sp, key_map, ARRAY_SIZE (key_map), errors);
468 }
469
470 /* Signature status sprinter */
471 static void
472 format_part_sigstatus_sprinter (sprinter_t *sp, GMimeSignatureList *siglist)
473 {
474     /* Any changes to the JSON or S-Expression format should be
475      * reflected in the file devel/schemata. */
476
477     sp->begin_list (sp);
478
479     if (! siglist) {
480         sp->end (sp);
481         return;
482     }
483
484     int i;
485
486     for (i = 0; i < g_mime_signature_list_length (siglist); i++) {
487         GMimeSignature *signature = g_mime_signature_list_get_signature (siglist, i);
488
489         sp->begin_map (sp);
490
491         /* status */
492         GMimeSignatureStatus status = g_mime_signature_get_status (signature);
493         sp->map_key (sp, "status");
494         sp->string (sp, signature_status_to_string (status));
495
496         GMimeCertificate *certificate = g_mime_signature_get_certificate (signature);
497         if (g_mime_signature_status_good (status)) {
498             if (certificate) {
499                 sp->map_key (sp, "fingerprint");
500                 sp->string (sp, g_mime_certificate_get_fingerprint (certificate));
501             }
502             /* these dates are seconds since the epoch; should we
503              * provide a more human-readable format string? */
504             time_t created = g_mime_signature_get_created (signature);
505             if (created != -1) {
506                 sp->map_key (sp, "created");
507                 sp->integer (sp, created);
508             }
509             time_t expires = g_mime_signature_get_expires (signature);
510             if (expires > 0) {
511                 sp->map_key (sp, "expires");
512                 sp->integer (sp, expires);
513             }
514             if (certificate) {
515                 const char *uid = g_mime_certificate_get_valid_userid (certificate);
516                 if (uid) {
517                     sp->map_key (sp, "userid");
518                     sp->string (sp, uid);
519                 }
520                 const char *email = g_mime_certificate_get_valid_email (certificate);
521                 if (email) {
522                     sp->map_key (sp, "email");
523                     sp->string (sp, email);
524                 }
525             }
526         } else if (certificate) {
527             const char *key_id = g_mime_certificate_get_fpr16 (certificate);
528             if (key_id) {
529                 sp->map_key (sp, "keyid");
530                 sp->string (sp, key_id);
531             }
532         }
533
534         if (notmuch_format_version <= 3) {
535             GMimeSignatureStatus errors = g_mime_signature_get_status (signature);
536             if (g_mime_signature_status_error (errors)) {
537                 sp->map_key (sp, "errors");
538                 sp->integer (sp, errors);
539             }
540         } else {
541             format_signature_errors (sp, signature);
542         }
543
544         sp->end (sp);
545     }
546
547     sp->end (sp);
548 }
549
550 static notmuch_status_t
551 format_part_text (const void *ctx, sprinter_t *sp, mime_node_t *node,
552                   int indent, const notmuch_show_params_t *params)
553 {
554     /* The disposition and content-type metadata are associated with
555      * the envelope for message parts */
556     GMimeObject *meta = node->envelope_part ? (
557         GMIME_OBJECT (node->envelope_part) ) : node->part;
558     GMimeContentType *content_type = g_mime_object_get_content_type (meta);
559     const bool leaf = GMIME_IS_PART (node->part);
560     GMimeStream *stream = params->out_stream;
561     const char *part_type;
562     int i;
563
564     if (node->envelope_file) {
565         notmuch_message_t *message = node->envelope_file;
566
567         part_type = "message";
568         g_mime_stream_printf (stream, "\f%s{ id:%s depth:%d match:%d excluded:%d filename:%s\n",
569                               part_type,
570                               notmuch_message_get_message_id (message),
571                               indent,
572                               _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? 1 : 0,
573                               _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED) ? 1 : 0,
574                               notmuch_message_get_filename (message));
575     } else {
576         char *content_string;
577         const char *disposition = _get_disposition (meta);
578         const char *cid = g_mime_object_get_content_id (meta);
579         const char *filename = leaf ? (
580             g_mime_part_get_filename (GMIME_PART (node->part)) ) : NULL;
581
582         if (disposition &&
583             strcasecmp (disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
584             part_type = "attachment";
585         else
586             part_type = "part";
587
588         g_mime_stream_printf (stream, "\f%s{ ID: %d", part_type, node->part_num);
589         if (filename)
590             g_mime_stream_printf (stream, ", Filename: %s", filename);
591         if (cid)
592             g_mime_stream_printf (stream, ", Content-id: %s", cid);
593
594         content_string = g_mime_content_type_get_mime_type (content_type);
595         g_mime_stream_printf (stream, ", Content-type: %s\n", content_string);
596         g_free (content_string);
597     }
598
599     if (GMIME_IS_MESSAGE (node->part)) {
600         GMimeMessage *message = GMIME_MESSAGE (node->part);
601         char *recipients_string;
602         char *date_string;
603
604         g_mime_stream_printf (stream, "\fheader{\n");
605         if (node->envelope_file)
606             g_mime_stream_printf (stream, "%s\n", _get_one_line_summary (ctx, node->envelope_file));
607         g_mime_stream_printf (stream, "Subject: %s\n", g_mime_message_get_subject (message));
608         g_mime_stream_printf (stream, "From: %s\n", g_mime_message_get_from_string (message));
609         recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_TO);
610         if (recipients_string)
611             g_mime_stream_printf (stream, "To: %s\n", recipients_string);
612         g_free (recipients_string);
613         recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_CC);
614         if (recipients_string)
615             g_mime_stream_printf (stream, "Cc: %s\n", recipients_string);
616         g_free (recipients_string);
617         date_string = g_mime_message_get_date_string (node, message);
618         g_mime_stream_printf (stream, "Date: %s\n", date_string);
619         g_mime_stream_printf (stream, "\fheader}\n");
620
621         if (! params->output_body) {
622             g_mime_stream_printf (stream, "\f%s}\n", part_type);
623             return NOTMUCH_STATUS_SUCCESS;
624         }
625         g_mime_stream_printf (stream, "\fbody{\n");
626     }
627
628     if (leaf) {
629         if (g_mime_content_type_is_type (content_type, "text", "*") &&
630             (params->include_html ||
631              ! g_mime_content_type_is_type (content_type, "text", "html"))) {
632             show_text_part_content (node->part, stream, 0);
633         } else {
634             char *content_string = g_mime_content_type_get_mime_type (content_type);
635             g_mime_stream_printf (stream, "Non-text part: %s\n", content_string);
636             g_free (content_string);
637         }
638     }
639
640     for (i = 0; i < node->nchildren; i++)
641         format_part_text (ctx, sp, mime_node_child (node, i), indent, params);
642
643     if (GMIME_IS_MESSAGE (node->part))
644         g_mime_stream_printf (stream, "\fbody}\n");
645
646     g_mime_stream_printf (stream, "\f%s}\n", part_type);
647
648     return NOTMUCH_STATUS_SUCCESS;
649 }
650
651 static void
652 format_omitted_part_meta_sprinter (sprinter_t *sp, GMimeObject *meta, GMimePart *part)
653 {
654     const char *content_charset = g_mime_object_get_content_type_parameter (meta, "charset");
655     const char *cte = g_mime_object_get_header (meta, "content-transfer-encoding");
656     GMimeDataWrapper *wrapper = g_mime_part_get_content (part);
657     GMimeStream *stream = g_mime_data_wrapper_get_stream (wrapper);
658     ssize_t content_length = g_mime_stream_length (stream);
659
660     if (content_charset != NULL) {
661         sp->map_key (sp, "content-charset");
662         sp->string (sp, content_charset);
663     }
664     if (cte != NULL) {
665         sp->map_key (sp, "content-transfer-encoding");
666         sp->string (sp, cte);
667     }
668     if (content_length >= 0) {
669         sp->map_key (sp, "content-length");
670         sp->integer (sp, content_length);
671     }
672 }
673
674 void
675 format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
676                       bool output_body,
677                       bool include_html)
678 {
679     /* Any changes to the JSON or S-Expression format should be
680      * reflected in the file devel/schemata. */
681
682     if (node->envelope_file) {
683         const _notmuch_message_crypto_t *msg_crypto = NULL;
684         sp->begin_map (sp);
685         format_message_sprinter (sp, node->envelope_file);
686
687         if (output_body) {
688             sp->map_key (sp, "body");
689             sp->begin_list (sp);
690             format_part_sprinter (ctx, sp, mime_node_child (node, 0), true, include_html);
691             sp->end (sp);
692         }
693
694         msg_crypto = mime_node_get_message_crypto_status (node);
695         if (notmuch_format_version >= 4) {
696             sp->map_key (sp, "crypto");
697             sp->begin_map (sp);
698             if (msg_crypto->sig_list ||
699                 msg_crypto->decryption_status != NOTMUCH_MESSAGE_DECRYPTED_NONE) {
700                 if (msg_crypto->sig_list) {
701                     sp->map_key (sp, "signed");
702                     sp->begin_map (sp);
703                     sp->map_key (sp, "status");
704                     format_part_sigstatus_sprinter (sp, msg_crypto->sig_list);
705                     if (msg_crypto->signature_encrypted) {
706                         sp->map_key (sp, "encrypted");
707                         sp->boolean (sp, msg_crypto->signature_encrypted);
708                     }
709                     if (msg_crypto->payload_subject) {
710                         sp->map_key (sp, "headers");
711                         sp->begin_list (sp);
712                         sp->string (sp, "Subject");
713                         sp->end (sp);
714                     }
715                     sp->end (sp);
716                 }
717                 if (msg_crypto->decryption_status != NOTMUCH_MESSAGE_DECRYPTED_NONE) {
718                     sp->map_key (sp, "decrypted");
719                     sp->begin_map (sp);
720                     sp->map_key (sp, "status");
721                     sp->string (sp, msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL ?
722                                 "full" : "partial");
723
724                     if (msg_crypto->payload_subject) {
725                         const char *subject = g_mime_message_get_subject GMIME_MESSAGE (node->part);
726                         if (subject == NULL || strcmp (subject, msg_crypto->payload_subject)) {
727                             /* protected subject differs from the external header */
728                             sp->map_key (sp, "header-mask");
729                             sp->begin_map (sp);
730                             sp->map_key (sp, "Subject");
731                             if (subject == NULL)
732                                 sp->null (sp);
733                             else
734                                 sp->string (sp, subject);
735                             sp->end (sp);
736                         }
737                     }
738                     sp->end (sp);
739                 }
740             }
741             sp->end (sp);
742         }
743
744         sp->map_key (sp, "headers");
745         format_headers_sprinter (sp, GMIME_MESSAGE (node->part), false, msg_crypto);
746
747         sp->end (sp);
748         return;
749     }
750
751     /* The disposition and content-type metadata are associated with
752      * the envelope for message parts */
753     GMimeObject *meta = node->envelope_part ? (
754         GMIME_OBJECT (node->envelope_part) ) : node->part;
755     GMimeContentType *content_type = g_mime_object_get_content_type (meta);
756     char *content_string;
757     const char *disposition = _get_disposition (meta);
758     const char *cid = g_mime_object_get_content_id (meta);
759     const char *filename = GMIME_IS_PART (node->part) ? (
760         g_mime_part_get_filename (GMIME_PART (node->part) ) ) : NULL;
761     int nclose = 0;
762     int i;
763
764     sp->begin_map (sp);
765
766     sp->map_key (sp, "id");
767     sp->integer (sp, node->part_num);
768
769     if (node->decrypt_attempted) {
770         sp->map_key (sp, "encstatus");
771         sp->begin_list (sp);
772         sp->begin_map (sp);
773         sp->map_key (sp, "status");
774         sp->string (sp, node->decrypt_success ? "good" : "bad");
775         sp->end (sp);
776         sp->end (sp);
777     }
778
779     if (node->verify_attempted) {
780         sp->map_key (sp, "sigstatus");
781         format_part_sigstatus_sprinter (sp, node->sig_list);
782     }
783
784     sp->map_key (sp, "content-type");
785     content_string = g_mime_content_type_get_mime_type (content_type);
786     sp->string (sp, content_string);
787     g_free (content_string);
788
789     if (disposition) {
790         sp->map_key (sp, "content-disposition");
791         sp->string (sp, disposition);
792     }
793
794     if (cid) {
795         sp->map_key (sp, "content-id");
796         sp->string (sp, cid);
797     }
798
799     if (filename) {
800         sp->map_key (sp, "filename");
801         sp->string (sp, filename);
802     }
803
804     if (GMIME_IS_PART (node->part)) {
805         /* For non-HTML text parts, we include the content in the
806          * JSON. Since JSON must be Unicode, we handle charset
807          * decoding here and do not report a charset to the caller.
808          * For text/html parts, we do not include the content unless
809          * the --include-html option has been passed. If a html part
810          * is not included, it can be requested directly. This makes
811          * charset decoding the responsibility on the caller so we
812          * report the charset for text/html parts.
813          */
814         if (g_mime_content_type_is_type (content_type, "text", "*") &&
815             (include_html ||
816              ! g_mime_content_type_is_type (content_type, "text", "html"))) {
817             GMimeStream *stream_memory = g_mime_stream_mem_new ();
818             GByteArray *part_content;
819             show_text_part_content (node->part, stream_memory, 0);
820             part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
821             sp->map_key (sp, "content");
822             sp->string_len (sp, (char *) part_content->data, part_content->len);
823             g_object_unref (stream_memory);
824         } else {
825             /* if we have a child part despite being a standard
826              * (non-multipart) MIME part, that means there is
827              * something to unwrap, which we will present in
828              * content: */
829             if (node->nchildren) {
830                 sp->map_key (sp, "content");
831                 sp->begin_list (sp);
832                 nclose = 1;
833             } else
834                 format_omitted_part_meta_sprinter (sp, meta, GMIME_PART (node->part));
835         }
836     } else if (GMIME_IS_MULTIPART (node->part)) {
837         sp->map_key (sp, "content");
838         sp->begin_list (sp);
839         nclose = 1;
840     } else if (GMIME_IS_MESSAGE (node->part)) {
841         sp->map_key (sp, "content");
842         sp->begin_list (sp);
843         sp->begin_map (sp);
844
845         sp->map_key (sp, "headers");
846         format_headers_sprinter (sp, GMIME_MESSAGE (node->part), false, NULL);
847
848         sp->map_key (sp, "body");
849         sp->begin_list (sp);
850         nclose = 3;
851     }
852
853     for (i = 0; i < node->nchildren; i++)
854         format_part_sprinter (ctx, sp, mime_node_child (node, i), true, include_html);
855
856     /* Close content structures */
857     for (i = 0; i < nclose; i++)
858         sp->end (sp);
859     /* Close part map */
860     sp->end (sp);
861 }
862
863 static notmuch_status_t
864 format_part_sprinter_entry (const void *ctx, sprinter_t *sp,
865                             mime_node_t *node, unused (int indent),
866                             const notmuch_show_params_t *params)
867 {
868     format_part_sprinter (ctx, sp, node, params->output_body, params->include_html);
869
870     return NOTMUCH_STATUS_SUCCESS;
871 }
872
873 /* Print a message in "mboxrd" format as documented, for example,
874  * here:
875  *
876  * http://qmail.org/qmail-manual-html/man5/mbox.html
877  */
878 static notmuch_status_t
879 format_part_mbox (const void *ctx, unused (sprinter_t *sp), mime_node_t *node,
880                   unused (int indent),
881                   unused (const notmuch_show_params_t *params))
882 {
883     notmuch_message_t *message = node->envelope_file;
884
885     const char *filename;
886     gzFile file;
887     const char *from;
888
889     time_t date;
890     struct tm date_gmtime;
891     char date_asctime[26];
892
893     char *line = NULL;
894     ssize_t line_size;
895     ssize_t line_len;
896
897     if (! message)
898         INTERNAL_ERROR ("format_part_mbox requires a root part");
899
900     filename = notmuch_message_get_filename (message);
901     file = gzopen (filename, "r");
902     if (file == NULL) {
903         fprintf (stderr, "Failed to open %s: %s\n",
904                  filename, strerror (errno));
905         return NOTMUCH_STATUS_FILE_ERROR;
906     }
907
908     from = notmuch_message_get_header (message, "from");
909     from = _extract_email_address (ctx, from);
910
911     date = notmuch_message_get_date (message);
912     gmtime_r (&date, &date_gmtime);
913     asctime_r (&date_gmtime, date_asctime);
914
915     printf ("From %s %s", from, date_asctime);
916
917     while ((line_len = gz_getline (message, &line, &line_size, file)) != UTIL_EOF ) {
918         if (_is_from_line (line))
919             putchar ('>');
920         printf ("%s", line);
921     }
922
923     printf ("\n");
924
925     gzclose (file);
926
927     return NOTMUCH_STATUS_SUCCESS;
928 }
929
930 static notmuch_status_t
931 format_part_raw (unused (const void *ctx), unused (sprinter_t *sp),
932                  mime_node_t *node, unused (int indent),
933                  const notmuch_show_params_t *params)
934 {
935     if (node->envelope_file) {
936         /* Special case the entire message to avoid MIME parsing. */
937         const char *filename;
938         GMimeStream *stream = NULL;
939         ssize_t ssize;
940         char buf[4096];
941         notmuch_status_t ret = NOTMUCH_STATUS_FILE_ERROR;
942
943         filename = _get_filename (node->envelope_file, params->duplicate);
944         if (filename == NULL) {
945             fprintf (stderr, "Error: Cannot get message filename.\n");
946             goto DONE;
947         }
948
949         stream = g_mime_stream_gzfile_open (filename);
950         if (stream == NULL) {
951             fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
952             goto DONE;
953         }
954
955         while (! g_mime_stream_eos (stream)) {
956             ssize = g_mime_stream_read (stream, buf, sizeof (buf));
957             if (ssize < 0) {
958                 fprintf (stderr, "Error: Read failed from %s\n", filename);
959                 goto DONE;
960             }
961
962             if (ssize > 0 && fwrite (buf, ssize, 1, stdout) != 1) {
963                 fprintf (stderr, "Error: Write %zd chars to stdout failed\n", ssize);
964                 goto DONE;
965             }
966         }
967
968         ret = NOTMUCH_STATUS_SUCCESS;
969
970         /* XXX This DONE is just for the special case of a node in a single file */
971       DONE:
972         if (stream)
973             g_object_unref (stream);
974
975         return ret;
976     }
977
978     GMimeStream *stream_filter = g_mime_stream_filter_new (params->out_stream);
979
980     if (GMIME_IS_PART (node->part)) {
981         /* For leaf parts, we emit only the transfer-decoded
982          * body. */
983         GMimeDataWrapper *wrapper;
984         wrapper = g_mime_part_get_content (GMIME_PART (node->part));
985
986         if (wrapper && stream_filter)
987             g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
988     } else {
989         /* Write out the whole part.  For message parts (the root
990          * part and embedded message parts), this will be the
991          * message including its headers (but not the
992          * encapsulating part's headers).  For multipart parts,
993          * this will include the headers. */
994         if (stream_filter)
995             g_mime_object_write_to_stream (node->part, NULL, stream_filter);
996     }
997
998     if (stream_filter)
999         g_object_unref (stream_filter);
1000
1001     return NOTMUCH_STATUS_SUCCESS;
1002 }
1003
1004 static notmuch_status_t
1005 show_message (void *ctx,
1006               const notmuch_show_format_t *format,
1007               sprinter_t *sp,
1008               notmuch_message_t *message,
1009               int indent,
1010               notmuch_show_params_t *params)
1011 {
1012     void *local = talloc_new (ctx);
1013     mime_node_t *root, *part;
1014     notmuch_status_t status;
1015     unsigned int session_keys = 0;
1016     notmuch_status_t session_key_count_error = NOTMUCH_STATUS_SUCCESS;
1017
1018     if (params->crypto.decrypt == NOTMUCH_DECRYPT_TRUE)
1019         session_key_count_error = notmuch_message_count_properties (message, "session-key",
1020                                                                     &session_keys);
1021
1022     status = mime_node_open (local, message, &(params->crypto), &root);
1023     if (status)
1024         goto DONE;
1025     part = mime_node_seek_dfs (root, (params->part < 0 ? 0 : params->part));
1026     if (part)
1027         status = format->part (local, sp, part, indent, params);
1028     if (params->crypto.decrypt == NOTMUCH_DECRYPT_TRUE && session_key_count_error ==
1029         NOTMUCH_STATUS_SUCCESS) {
1030         unsigned int new_session_keys = 0;
1031         if (notmuch_message_count_properties (message, "session-key", &new_session_keys) ==
1032             NOTMUCH_STATUS_SUCCESS &&
1033             new_session_keys > session_keys) {
1034             /* try a quiet re-indexing */
1035             notmuch_indexopts_t *indexopts = notmuch_database_get_default_indexopts (
1036                 notmuch_message_get_database (message));
1037             if (indexopts) {
1038                 notmuch_indexopts_set_decrypt_policy (indexopts, NOTMUCH_DECRYPT_AUTO);
1039                 print_status_message ("Error re-indexing message with --decrypt=stash",
1040                                       message, notmuch_message_reindex (message, indexopts));
1041             }
1042         }
1043     }
1044   DONE:
1045     talloc_free (local);
1046     return status;
1047 }
1048
1049 static notmuch_status_t
1050 show_messages (void *ctx,
1051                const notmuch_show_format_t *format,
1052                sprinter_t *sp,
1053                notmuch_messages_t *messages,
1054                int indent,
1055                notmuch_show_params_t *params)
1056 {
1057     notmuch_message_t *message;
1058     bool match;
1059     bool excluded;
1060     int next_indent;
1061     notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
1062
1063     sp->begin_list (sp);
1064
1065     for (;
1066          notmuch_messages_valid (messages);
1067          notmuch_messages_move_to_next (messages)) {
1068         sp->begin_list (sp);
1069
1070         message = notmuch_messages_get (messages);
1071
1072         match = _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
1073         excluded = _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED);
1074
1075         next_indent = indent;
1076
1077         if ((match && (! excluded || ! params->omit_excluded)) || params->entire_thread) {
1078             status = show_message (ctx, format, sp, message, indent, params);
1079             if (status && ! res)
1080                 res = status;
1081             next_indent = indent + 1;
1082         } else {
1083             sp->null (sp);
1084         }
1085
1086         status = show_messages (ctx,
1087                                 format, sp,
1088                                 notmuch_message_get_replies (message),
1089                                 next_indent,
1090                                 params);
1091         if (status && ! res)
1092             res = status;
1093
1094         notmuch_message_destroy (message);
1095
1096         sp->end (sp);
1097     }
1098
1099     sp->end (sp);
1100
1101     return res;
1102 }
1103
1104 /* Formatted output of single message */
1105 static int
1106 do_show_single (void *ctx,
1107                 notmuch_query_t *query,
1108                 const notmuch_show_format_t *format,
1109                 sprinter_t *sp,
1110                 notmuch_show_params_t *params)
1111 {
1112     notmuch_messages_t *messages;
1113     notmuch_message_t *message;
1114     notmuch_status_t status;
1115     unsigned int count;
1116
1117     status = notmuch_query_count_messages (query, &count);
1118     if (print_status_query ("notmuch show", query, status))
1119         return 1;
1120
1121     if (count != 1) {
1122         fprintf (stderr,
1123                  "Error: search term did not match precisely one message (matched %u messages).\n",
1124                  count);
1125         return 1;
1126     }
1127
1128     status = notmuch_query_search_messages (query, &messages);
1129     if (print_status_query ("notmuch show", query, status))
1130         return 1;
1131
1132     message = notmuch_messages_get (messages);
1133
1134     if (message == NULL) {
1135         fprintf (stderr, "Error: Cannot find matching message.\n");
1136         return 1;
1137     }
1138
1139     notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
1140
1141     return show_message (ctx, format, sp, message, 0, params)
1142            != NOTMUCH_STATUS_SUCCESS;
1143 }
1144
1145 /* Formatted output of threads */
1146 static int
1147 do_show_threaded (void *ctx,
1148                   notmuch_query_t *query,
1149                   const notmuch_show_format_t *format,
1150                   sprinter_t *sp,
1151                   notmuch_show_params_t *params)
1152 {
1153     notmuch_threads_t *threads;
1154     notmuch_thread_t *thread;
1155     notmuch_messages_t *messages;
1156     notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
1157
1158     status = notmuch_query_search_threads (query, &threads);
1159     if (print_status_query ("notmuch show", query, status))
1160         return 1;
1161
1162     sp->begin_list (sp);
1163
1164     for (;
1165          notmuch_threads_valid (threads);
1166          notmuch_threads_move_to_next (threads)) {
1167         thread = notmuch_threads_get (threads);
1168
1169         messages = notmuch_thread_get_toplevel_messages (thread);
1170
1171         if (messages == NULL)
1172             INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
1173                             notmuch_thread_get_thread_id (thread));
1174
1175         status = show_messages (ctx, format, sp, messages, 0, params);
1176         if (status && ! res)
1177             res = status;
1178
1179         notmuch_thread_destroy (thread);
1180
1181     }
1182
1183     sp->end (sp);
1184
1185     return res != NOTMUCH_STATUS_SUCCESS;
1186 }
1187
1188 static int
1189 do_show_unthreaded (void *ctx,
1190                     notmuch_query_t *query,
1191                     const notmuch_show_format_t *format,
1192                     sprinter_t *sp,
1193                     notmuch_show_params_t *params)
1194 {
1195     notmuch_messages_t *messages;
1196     notmuch_message_t *message;
1197     notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
1198     notmuch_bool_t excluded;
1199
1200     status = notmuch_query_search_messages (query, &messages);
1201     if (print_status_query ("notmuch show", query, status))
1202         return 1;
1203
1204     sp->begin_list (sp);
1205
1206     for (;
1207          notmuch_messages_valid (messages);
1208          notmuch_messages_move_to_next (messages)) {
1209         sp->begin_list (sp);
1210         sp->begin_list (sp);
1211
1212         message = notmuch_messages_get (messages);
1213
1214         notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, TRUE);
1215         excluded = _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED);
1216
1217         if (! excluded || ! params->omit_excluded) {
1218             status = show_message (ctx, format, sp, message, 0, params);
1219             if (status && ! res)
1220                 res = status;
1221         } else {
1222             sp->null (sp);
1223         }
1224         notmuch_message_destroy (message);
1225         sp->end (sp);
1226         sp->end (sp);
1227     }
1228     sp->end (sp);
1229     return res;
1230 }
1231
1232 enum {
1233     NOTMUCH_FORMAT_NOT_SPECIFIED,
1234     NOTMUCH_FORMAT_JSON,
1235     NOTMUCH_FORMAT_SEXP,
1236     NOTMUCH_FORMAT_TEXT,
1237     NOTMUCH_FORMAT_MBOX,
1238     NOTMUCH_FORMAT_RAW
1239 };
1240
1241 static const notmuch_show_format_t format_json = {
1242     .new_sprinter = sprinter_json_create,
1243     .part = format_part_sprinter_entry,
1244 };
1245
1246 static const notmuch_show_format_t format_sexp = {
1247     .new_sprinter = sprinter_sexp_create,
1248     .part = format_part_sprinter_entry,
1249 };
1250
1251 static const notmuch_show_format_t format_text = {
1252     .new_sprinter = sprinter_text_create,
1253     .part = format_part_text,
1254 };
1255
1256 static const notmuch_show_format_t format_mbox = {
1257     .new_sprinter = sprinter_text_create,
1258     .part = format_part_mbox,
1259 };
1260
1261 static const notmuch_show_format_t format_raw = {
1262     .new_sprinter = sprinter_text_create,
1263     .part = format_part_raw,
1264 };
1265
1266 static const notmuch_show_format_t *formatters[] = {
1267     [NOTMUCH_FORMAT_JSON] = &format_json,
1268     [NOTMUCH_FORMAT_SEXP] = &format_sexp,
1269     [NOTMUCH_FORMAT_TEXT] = &format_text,
1270     [NOTMUCH_FORMAT_MBOX] = &format_mbox,
1271     [NOTMUCH_FORMAT_RAW] = &format_raw,
1272 };
1273
1274 int
1275 notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[])
1276 {
1277     notmuch_query_t *query;
1278     char *query_string;
1279     int opt_index, ret;
1280     const notmuch_show_format_t *formatter;
1281     sprinter_t *sprinter;
1282     notmuch_show_params_t params = {
1283         .part = -1,
1284         .duplicate = 0,
1285         .omit_excluded = true,
1286         .output_body = true,
1287         .crypto = { .decrypt = NOTMUCH_DECRYPT_AUTO },
1288     };
1289     int format = NOTMUCH_FORMAT_NOT_SPECIFIED;
1290     bool exclude = true;
1291     bool entire_thread_set = false;
1292     bool single_message;
1293     bool unthreaded = FALSE;
1294     notmuch_status_t status;
1295     int sort = NOTMUCH_SORT_NEWEST_FIRST;
1296
1297     notmuch_opt_desc_t options[] = {
1298         { .opt_keyword = &sort, .name = "sort", .keywords =
1299               (notmuch_keyword_t []){ { "oldest-first", NOTMUCH_SORT_OLDEST_FIRST },
1300                                       { "newest-first", NOTMUCH_SORT_NEWEST_FIRST },
1301                                       { 0, 0 } } },
1302         { .opt_keyword = &format, .name = "format", .keywords =
1303               (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
1304                                       { "text", NOTMUCH_FORMAT_TEXT },
1305                                       { "sexp", NOTMUCH_FORMAT_SEXP },
1306                                       { "mbox", NOTMUCH_FORMAT_MBOX },
1307                                       { "raw", NOTMUCH_FORMAT_RAW },
1308                                       { 0, 0 } } },
1309         { .opt_int = &notmuch_format_version, .name = "format-version" },
1310         { .opt_bool = &exclude, .name = "exclude" },
1311         { .opt_bool = &params.entire_thread, .name = "entire-thread",
1312           .present = &entire_thread_set },
1313         { .opt_bool = &unthreaded, .name = "unthreaded" },
1314         { .opt_int = &params.part, .name = "part" },
1315         { .opt_keyword = (int *) (&params.crypto.decrypt), .name = "decrypt",
1316           .keyword_no_arg_value = "true", .keywords =
1317               (notmuch_keyword_t []){ { "false", NOTMUCH_DECRYPT_FALSE },
1318                                       { "auto", NOTMUCH_DECRYPT_AUTO },
1319                                       { "true", NOTMUCH_DECRYPT_NOSTASH },
1320                                       { "stash", NOTMUCH_DECRYPT_TRUE },
1321                                       { 0, 0 } } },
1322         { .opt_bool = &params.crypto.verify, .name = "verify" },
1323         { .opt_bool = &params.output_body, .name = "body" },
1324         { .opt_bool = &params.include_html, .name = "include-html" },
1325         { .opt_int = &params.duplicate, .name = "duplicate" },
1326         { .opt_inherit = notmuch_shared_options },
1327         { }
1328     };
1329
1330     opt_index = parse_arguments (argc, argv, options, 1);
1331     if (opt_index < 0)
1332         return EXIT_FAILURE;
1333
1334     notmuch_process_shared_options (notmuch, argv[0]);
1335
1336     /* explicit decryption implies verification */
1337     if (params.crypto.decrypt == NOTMUCH_DECRYPT_NOSTASH ||
1338         params.crypto.decrypt == NOTMUCH_DECRYPT_TRUE)
1339         params.crypto.verify = true;
1340
1341     /* specifying a part implies single message display */
1342     single_message = params.part >= 0;
1343
1344     /* specifying a duplicate also implies single message display */
1345     single_message = single_message || (params.duplicate > 0);
1346
1347     if (format == NOTMUCH_FORMAT_NOT_SPECIFIED) {
1348         /* if part was requested and format was not specified, use format=raw */
1349         if (params.part >= 0)
1350             format = NOTMUCH_FORMAT_RAW;
1351         else
1352             format = NOTMUCH_FORMAT_TEXT;
1353     }
1354
1355     if (format == NOTMUCH_FORMAT_MBOX) {
1356         if (params.part > 0) {
1357             fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
1358             return EXIT_FAILURE;
1359         }
1360     } else if (format == NOTMUCH_FORMAT_RAW) {
1361         /* raw format only supports single message display */
1362         single_message = true;
1363     }
1364
1365     notmuch_exit_if_unsupported_format ();
1366
1367     /* Default is entire-thread = false except for format=json and
1368      * format=sexp. */
1369     if (! entire_thread_set &&
1370         (format == NOTMUCH_FORMAT_JSON || format == NOTMUCH_FORMAT_SEXP))
1371         params.entire_thread = true;
1372
1373     if (! params.output_body) {
1374         if (params.part > 0) {
1375             fprintf (stderr, "Warning: --body=false is incompatible with --part > 0. Disabling.\n");
1376             params.output_body = true;
1377         } else {
1378             if (format != NOTMUCH_FORMAT_TEXT &&
1379                 format != NOTMUCH_FORMAT_JSON &&
1380                 format != NOTMUCH_FORMAT_SEXP)
1381                 fprintf (stderr,
1382                          "Warning: --body=false only implemented for format=text, format=json and format=sexp\n");
1383         }
1384     }
1385
1386     if (params.include_html &&
1387         (format != NOTMUCH_FORMAT_TEXT &&
1388          format != NOTMUCH_FORMAT_JSON &&
1389          format != NOTMUCH_FORMAT_SEXP)) {
1390         fprintf (stderr,
1391                  "Warning: --include-html only implemented for format=text, format=json and format=sexp\n");
1392     }
1393
1394     if (params.crypto.decrypt == NOTMUCH_DECRYPT_TRUE) {
1395         status = notmuch_database_reopen (notmuch, NOTMUCH_DATABASE_MODE_READ_WRITE);
1396         if (status) {
1397             fprintf (stderr, "Error reopening database for READ_WRITE: %s\n",
1398                      notmuch_status_to_string (status));
1399             return EXIT_FAILURE;
1400         }
1401     }
1402
1403     query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
1404     if (query_string == NULL) {
1405         fprintf (stderr, "Out of memory\n");
1406         return EXIT_FAILURE;
1407     }
1408
1409     if (*query_string == '\0') {
1410         fprintf (stderr, "Error: notmuch show requires at least one search term.\n");
1411         return EXIT_FAILURE;
1412     }
1413
1414     status = notmuch_query_create_with_syntax (notmuch, query_string,
1415                                                shared_option_query_syntax (),
1416                                                &query);
1417     if (print_status_database ("notmuch show", notmuch, status))
1418         return EXIT_FAILURE;
1419
1420     notmuch_query_set_sort (query, sort);
1421
1422     /* Create structure printer. */
1423     formatter = formatters[format];
1424     sprinter = formatter->new_sprinter (notmuch, stdout);
1425
1426     params.out_stream = g_mime_stream_stdout_new ();
1427
1428     /* If a single message is requested we do not use search_excludes. */
1429     if (single_message) {
1430         ret = do_show_single (notmuch, query, formatter, sprinter, &params);
1431     } else {
1432         /* We always apply set the exclude flag. The
1433          * exclude=true|false option controls whether or not we return
1434          * threads that only match in an excluded message */
1435         notmuch_config_values_t *exclude_tags;
1436         notmuch_status_t status;
1437
1438         for (exclude_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS);
1439              notmuch_config_values_valid (exclude_tags);
1440              notmuch_config_values_move_to_next (exclude_tags)) {
1441
1442             status = notmuch_query_add_tag_exclude (query,
1443                                                     notmuch_config_values_get (exclude_tags));
1444             if (status && status != NOTMUCH_STATUS_IGNORED) {
1445                 print_status_query ("notmuch show", query, status);
1446                 ret = -1;
1447                 goto DONE;
1448             }
1449         }
1450
1451         if (exclude == false) {
1452             notmuch_query_set_omit_excluded (query, false);
1453             params.omit_excluded = false;
1454         }
1455
1456         if (unthreaded)
1457             ret = do_show_unthreaded (notmuch, query, formatter, sprinter, &params);
1458         else
1459             ret = do_show_threaded (notmuch, query, formatter, sprinter, &params);
1460     }
1461
1462   DONE:
1463     g_mime_stream_flush (params.out_stream);
1464     g_object_unref (params.out_stream);
1465
1466     _notmuch_crypto_cleanup (&params.crypto);
1467     notmuch_query_destroy (query);
1468     notmuch_database_destroy (notmuch);
1469
1470     return ret ? EXIT_FAILURE : EXIT_SUCCESS;
1471 }