]> git.notmuchmail.org Git - notmuch/blob - notmuch-show.c
7fb40ce9ab5d3f0179707a7c0ea88f9440673f25
[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                       int duplicate,
677                       bool output_body,
678                       bool include_html)
679 {
680     /* Any changes to the JSON or S-Expression format should be
681      * reflected in the file devel/schemata. */
682
683     if (node->envelope_file) {
684         const _notmuch_message_crypto_t *msg_crypto = NULL;
685         sp->begin_map (sp);
686         format_message_sprinter (sp, node->envelope_file);
687
688         sp->map_key (sp, "duplicate");
689         sp->integer (sp, duplicate > 0 ? duplicate : 1);
690
691         if (output_body) {
692             sp->map_key (sp, "body");
693             sp->begin_list (sp);
694             format_part_sprinter (ctx, sp, mime_node_child (node, 0), -1, true, include_html);
695             sp->end (sp);
696         }
697
698         msg_crypto = mime_node_get_message_crypto_status (node);
699         if (notmuch_format_version >= 4) {
700             sp->map_key (sp, "crypto");
701             sp->begin_map (sp);
702             if (msg_crypto->sig_list ||
703                 msg_crypto->decryption_status != NOTMUCH_MESSAGE_DECRYPTED_NONE) {
704                 if (msg_crypto->sig_list) {
705                     sp->map_key (sp, "signed");
706                     sp->begin_map (sp);
707                     sp->map_key (sp, "status");
708                     format_part_sigstatus_sprinter (sp, msg_crypto->sig_list);
709                     if (msg_crypto->signature_encrypted) {
710                         sp->map_key (sp, "encrypted");
711                         sp->boolean (sp, msg_crypto->signature_encrypted);
712                     }
713                     if (msg_crypto->payload_subject) {
714                         sp->map_key (sp, "headers");
715                         sp->begin_list (sp);
716                         sp->string (sp, "Subject");
717                         sp->end (sp);
718                     }
719                     sp->end (sp);
720                 }
721                 if (msg_crypto->decryption_status != NOTMUCH_MESSAGE_DECRYPTED_NONE) {
722                     sp->map_key (sp, "decrypted");
723                     sp->begin_map (sp);
724                     sp->map_key (sp, "status");
725                     sp->string (sp, msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL ?
726                                 "full" : "partial");
727
728                     if (msg_crypto->payload_subject) {
729                         const char *subject = g_mime_message_get_subject GMIME_MESSAGE (node->part);
730                         if (subject == NULL || strcmp (subject, msg_crypto->payload_subject)) {
731                             /* protected subject differs from the external header */
732                             sp->map_key (sp, "header-mask");
733                             sp->begin_map (sp);
734                             sp->map_key (sp, "Subject");
735                             if (subject == NULL)
736                                 sp->null (sp);
737                             else
738                                 sp->string (sp, subject);
739                             sp->end (sp);
740                         }
741                     }
742                     sp->end (sp);
743                 }
744             }
745             sp->end (sp);
746         }
747
748         sp->map_key (sp, "headers");
749         format_headers_sprinter (sp, GMIME_MESSAGE (node->part), false, msg_crypto);
750
751         sp->end (sp);
752         return;
753     }
754
755     /* The disposition and content-type metadata are associated with
756      * the envelope for message parts */
757     GMimeObject *meta = node->envelope_part ? (
758         GMIME_OBJECT (node->envelope_part) ) : node->part;
759     GMimeContentType *content_type = g_mime_object_get_content_type (meta);
760     char *content_string;
761     const char *disposition = _get_disposition (meta);
762     const char *cid = g_mime_object_get_content_id (meta);
763     const char *filename = GMIME_IS_PART (node->part) ? (
764         g_mime_part_get_filename (GMIME_PART (node->part) ) ) : NULL;
765     int nclose = 0;
766     int i;
767
768     sp->begin_map (sp);
769
770     sp->map_key (sp, "id");
771     sp->integer (sp, node->part_num);
772
773     if (node->decrypt_attempted) {
774         sp->map_key (sp, "encstatus");
775         sp->begin_list (sp);
776         sp->begin_map (sp);
777         sp->map_key (sp, "status");
778         sp->string (sp, node->decrypt_success ? "good" : "bad");
779         sp->end (sp);
780         sp->end (sp);
781     }
782
783     if (node->verify_attempted) {
784         sp->map_key (sp, "sigstatus");
785         format_part_sigstatus_sprinter (sp, node->sig_list);
786     }
787
788     sp->map_key (sp, "content-type");
789     content_string = g_mime_content_type_get_mime_type (content_type);
790     sp->string (sp, content_string);
791     g_free (content_string);
792
793     if (disposition) {
794         sp->map_key (sp, "content-disposition");
795         sp->string (sp, disposition);
796     }
797
798     if (cid) {
799         sp->map_key (sp, "content-id");
800         sp->string (sp, cid);
801     }
802
803     if (filename) {
804         sp->map_key (sp, "filename");
805         sp->string (sp, filename);
806     }
807
808     if (GMIME_IS_PART (node->part)) {
809         /* For non-HTML text parts, we include the content in the
810          * JSON. Since JSON must be Unicode, we handle charset
811          * decoding here and do not report a charset to the caller.
812          * For text/html parts, we do not include the content unless
813          * the --include-html option has been passed. If a html part
814          * is not included, it can be requested directly. This makes
815          * charset decoding the responsibility on the caller so we
816          * report the charset for text/html parts.
817          */
818         if (g_mime_content_type_is_type (content_type, "text", "*") &&
819             (include_html ||
820              ! g_mime_content_type_is_type (content_type, "text", "html"))) {
821             GMimeStream *stream_memory = g_mime_stream_mem_new ();
822             GByteArray *part_content;
823             show_text_part_content (node->part, stream_memory, 0);
824             part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
825             sp->map_key (sp, "content");
826             sp->string_len (sp, (char *) part_content->data, part_content->len);
827             g_object_unref (stream_memory);
828         } else {
829             /* if we have a child part despite being a standard
830              * (non-multipart) MIME part, that means there is
831              * something to unwrap, which we will present in
832              * content: */
833             if (node->nchildren) {
834                 sp->map_key (sp, "content");
835                 sp->begin_list (sp);
836                 nclose = 1;
837             } else
838                 format_omitted_part_meta_sprinter (sp, meta, GMIME_PART (node->part));
839         }
840     } else if (GMIME_IS_MULTIPART (node->part)) {
841         sp->map_key (sp, "content");
842         sp->begin_list (sp);
843         nclose = 1;
844     } else if (GMIME_IS_MESSAGE (node->part)) {
845         sp->map_key (sp, "content");
846         sp->begin_list (sp);
847         sp->begin_map (sp);
848
849         sp->map_key (sp, "headers");
850         format_headers_sprinter (sp, GMIME_MESSAGE (node->part), false, NULL);
851
852         sp->map_key (sp, "body");
853         sp->begin_list (sp);
854         nclose = 3;
855     }
856
857     for (i = 0; i < node->nchildren; i++)
858         format_part_sprinter (ctx, sp, mime_node_child (node, i), -1, true, include_html);
859
860     /* Close content structures */
861     for (i = 0; i < nclose; i++)
862         sp->end (sp);
863     /* Close part map */
864     sp->end (sp);
865 }
866
867 static notmuch_status_t
868 format_part_sprinter_entry (const void *ctx, sprinter_t *sp,
869                             mime_node_t *node, unused (int indent),
870                             const notmuch_show_params_t *params)
871 {
872     format_part_sprinter (ctx, sp, node, params->duplicate, params->output_body,
873                           params->include_html);
874
875     return NOTMUCH_STATUS_SUCCESS;
876 }
877
878 /* Print a message in "mboxrd" format as documented, for example,
879  * here:
880  *
881  * http://qmail.org/qmail-manual-html/man5/mbox.html
882  */
883 static notmuch_status_t
884 format_part_mbox (const void *ctx, unused (sprinter_t *sp), mime_node_t *node,
885                   unused (int indent),
886                   unused (const notmuch_show_params_t *params))
887 {
888     notmuch_message_t *message = node->envelope_file;
889
890     const char *filename;
891     gzFile file;
892     const char *from;
893
894     time_t date;
895     struct tm date_gmtime;
896     char date_asctime[26];
897
898     char *line = NULL;
899     ssize_t line_size;
900     ssize_t line_len;
901
902     if (! message)
903         INTERNAL_ERROR ("format_part_mbox requires a root part");
904
905     filename = notmuch_message_get_filename (message);
906     file = gzopen (filename, "r");
907     if (file == NULL) {
908         fprintf (stderr, "Failed to open %s: %s\n",
909                  filename, strerror (errno));
910         return NOTMUCH_STATUS_FILE_ERROR;
911     }
912
913     from = notmuch_message_get_header (message, "from");
914     from = _extract_email_address (ctx, from);
915
916     date = notmuch_message_get_date (message);
917     gmtime_r (&date, &date_gmtime);
918     asctime_r (&date_gmtime, date_asctime);
919
920     printf ("From %s %s", from, date_asctime);
921
922     while ((line_len = gz_getline (message, &line, &line_size, file)) != UTIL_EOF ) {
923         if (_is_from_line (line))
924             putchar ('>');
925         printf ("%s", line);
926     }
927
928     printf ("\n");
929
930     gzclose (file);
931
932     return NOTMUCH_STATUS_SUCCESS;
933 }
934
935 static notmuch_status_t
936 format_part_raw (unused (const void *ctx), unused (sprinter_t *sp),
937                  mime_node_t *node, unused (int indent),
938                  const notmuch_show_params_t *params)
939 {
940     if (node->envelope_file) {
941         /* Special case the entire message to avoid MIME parsing. */
942         const char *filename;
943         GMimeStream *stream = NULL;
944         ssize_t ssize;
945         char buf[4096];
946         notmuch_status_t ret = NOTMUCH_STATUS_FILE_ERROR;
947
948         filename = _get_filename (node->envelope_file, params->duplicate);
949         if (filename == NULL) {
950             fprintf (stderr, "Error: Cannot get message filename.\n");
951             goto DONE;
952         }
953
954         stream = g_mime_stream_gzfile_open (filename);
955         if (stream == NULL) {
956             fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
957             goto DONE;
958         }
959
960         while (! g_mime_stream_eos (stream)) {
961             ssize = g_mime_stream_read (stream, buf, sizeof (buf));
962             if (ssize < 0) {
963                 fprintf (stderr, "Error: Read failed from %s\n", filename);
964                 goto DONE;
965             }
966
967             if (ssize > 0 && fwrite (buf, ssize, 1, stdout) != 1) {
968                 fprintf (stderr, "Error: Write %zd chars to stdout failed\n", ssize);
969                 goto DONE;
970             }
971         }
972
973         ret = NOTMUCH_STATUS_SUCCESS;
974
975         /* XXX This DONE is just for the special case of a node in a single file */
976       DONE:
977         if (stream)
978             g_object_unref (stream);
979
980         return ret;
981     }
982
983     GMimeStream *stream_filter = g_mime_stream_filter_new (params->out_stream);
984
985     if (GMIME_IS_PART (node->part)) {
986         /* For leaf parts, we emit only the transfer-decoded
987          * body. */
988         GMimeDataWrapper *wrapper;
989         wrapper = g_mime_part_get_content (GMIME_PART (node->part));
990
991         if (wrapper && stream_filter)
992             g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
993     } else {
994         /* Write out the whole part.  For message parts (the root
995          * part and embedded message parts), this will be the
996          * message including its headers (but not the
997          * encapsulating part's headers).  For multipart parts,
998          * this will include the headers. */
999         if (stream_filter)
1000             g_mime_object_write_to_stream (node->part, NULL, stream_filter);
1001     }
1002
1003     if (stream_filter)
1004         g_object_unref (stream_filter);
1005
1006     return NOTMUCH_STATUS_SUCCESS;
1007 }
1008
1009 static notmuch_status_t
1010 show_message (void *ctx,
1011               const notmuch_show_format_t *format,
1012               sprinter_t *sp,
1013               notmuch_message_t *message,
1014               int indent,
1015               notmuch_show_params_t *params)
1016 {
1017     void *local = talloc_new (ctx);
1018     mime_node_t *root, *part;
1019     notmuch_status_t status;
1020     unsigned int session_keys = 0;
1021     notmuch_status_t session_key_count_error = NOTMUCH_STATUS_SUCCESS;
1022
1023     if (params->crypto.decrypt == NOTMUCH_DECRYPT_TRUE)
1024         session_key_count_error = notmuch_message_count_properties (message, "session-key",
1025                                                                     &session_keys);
1026
1027     status = mime_node_open (local, message, params->duplicate, &(params->crypto), &root);
1028     if (status)
1029         goto DONE;
1030     part = mime_node_seek_dfs (root, (params->part < 0 ? 0 : params->part));
1031     if (part)
1032         status = format->part (local, sp, part, indent, params);
1033     if (params->crypto.decrypt == NOTMUCH_DECRYPT_TRUE && session_key_count_error ==
1034         NOTMUCH_STATUS_SUCCESS) {
1035         unsigned int new_session_keys = 0;
1036         if (notmuch_message_count_properties (message, "session-key", &new_session_keys) ==
1037             NOTMUCH_STATUS_SUCCESS &&
1038             new_session_keys > session_keys) {
1039             /* try a quiet re-indexing */
1040             notmuch_indexopts_t *indexopts = notmuch_database_get_default_indexopts (
1041                 notmuch_message_get_database (message));
1042             if (indexopts) {
1043                 notmuch_indexopts_set_decrypt_policy (indexopts, NOTMUCH_DECRYPT_AUTO);
1044                 print_status_message ("Error re-indexing message with --decrypt=stash",
1045                                       message, notmuch_message_reindex (message, indexopts));
1046             }
1047         }
1048     }
1049   DONE:
1050     talloc_free (local);
1051     return status;
1052 }
1053
1054 static notmuch_status_t
1055 show_messages (void *ctx,
1056                const notmuch_show_format_t *format,
1057                sprinter_t *sp,
1058                notmuch_messages_t *messages,
1059                int indent,
1060                notmuch_show_params_t *params)
1061 {
1062     notmuch_message_t *message;
1063     bool match;
1064     bool excluded;
1065     int next_indent;
1066     notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
1067
1068     sp->begin_list (sp);
1069
1070     for (;
1071          notmuch_messages_valid (messages);
1072          notmuch_messages_move_to_next (messages)) {
1073         sp->begin_list (sp);
1074
1075         message = notmuch_messages_get (messages);
1076
1077         match = _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
1078         excluded = _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED);
1079
1080         next_indent = indent;
1081
1082         if ((match && (! excluded || ! params->omit_excluded)) || params->entire_thread) {
1083             status = show_message (ctx, format, sp, message, indent, params);
1084             if (status && ! res)
1085                 res = status;
1086             next_indent = indent + 1;
1087         } else {
1088             sp->null (sp);
1089         }
1090
1091         status = show_messages (ctx,
1092                                 format, sp,
1093                                 notmuch_message_get_replies (message),
1094                                 next_indent,
1095                                 params);
1096         if (status && ! res)
1097             res = status;
1098
1099         notmuch_message_destroy (message);
1100
1101         sp->end (sp);
1102     }
1103
1104     sp->end (sp);
1105
1106     return res;
1107 }
1108
1109 /* Formatted output of single message */
1110 static int
1111 do_show_single (void *ctx,
1112                 notmuch_query_t *query,
1113                 const notmuch_show_format_t *format,
1114                 sprinter_t *sp,
1115                 notmuch_show_params_t *params)
1116 {
1117     notmuch_messages_t *messages;
1118     notmuch_message_t *message;
1119     notmuch_status_t status;
1120     unsigned int count;
1121
1122     status = notmuch_query_count_messages (query, &count);
1123     if (print_status_query ("notmuch show", query, status))
1124         return 1;
1125
1126     if (count != 1) {
1127         fprintf (stderr,
1128                  "Error: search term did not match precisely one message (matched %u messages).\n",
1129                  count);
1130         return 1;
1131     }
1132
1133     status = notmuch_query_search_messages (query, &messages);
1134     if (print_status_query ("notmuch show", query, status))
1135         return 1;
1136
1137     message = notmuch_messages_get (messages);
1138
1139     if (message == NULL) {
1140         fprintf (stderr, "Error: Cannot find matching message.\n");
1141         return 1;
1142     }
1143
1144     notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
1145
1146     return show_message (ctx, format, sp, message, 0, params)
1147            != NOTMUCH_STATUS_SUCCESS;
1148 }
1149
1150 /* Formatted output of threads */
1151 static int
1152 do_show_threaded (void *ctx,
1153                   notmuch_query_t *query,
1154                   const notmuch_show_format_t *format,
1155                   sprinter_t *sp,
1156                   notmuch_show_params_t *params)
1157 {
1158     notmuch_threads_t *threads;
1159     notmuch_thread_t *thread;
1160     notmuch_messages_t *messages;
1161     notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
1162     int i;
1163
1164     if (params->offset < 0) {
1165         unsigned count;
1166         notmuch_status_t s = notmuch_query_count_threads (query, &count);
1167         if (print_status_query ("notmuch show", query, s))
1168             return 1;
1169
1170         params->offset += count;
1171         if (params->offset < 0)
1172             params->offset = 0;
1173     }
1174
1175     status = notmuch_query_search_threads (query, &threads);
1176     if (print_status_query ("notmuch show", query, status))
1177         return 1;
1178
1179     sp->begin_list (sp);
1180
1181     for (i = 0;
1182          notmuch_threads_valid (threads) && (params->limit < 0 || i < params->offset + params->limit);
1183          notmuch_threads_move_to_next (threads), i++) {
1184         thread = notmuch_threads_get (threads);
1185
1186         if (i < params->offset) {
1187             notmuch_thread_destroy (thread);
1188             continue;
1189         }
1190
1191         messages = notmuch_thread_get_toplevel_messages (thread);
1192
1193         if (messages == NULL)
1194             INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
1195                             notmuch_thread_get_thread_id (thread));
1196
1197         status = show_messages (ctx, format, sp, messages, 0, params);
1198         if (status && ! res)
1199             res = status;
1200
1201         notmuch_thread_destroy (thread);
1202
1203     }
1204
1205     sp->end (sp);
1206
1207     return res != NOTMUCH_STATUS_SUCCESS;
1208 }
1209
1210 static int
1211 do_show_unthreaded (void *ctx,
1212                     notmuch_query_t *query,
1213                     const notmuch_show_format_t *format,
1214                     sprinter_t *sp,
1215                     notmuch_show_params_t *params)
1216 {
1217     notmuch_messages_t *messages;
1218     notmuch_message_t *message;
1219     notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
1220     notmuch_bool_t excluded;
1221     int i;
1222
1223     if (params->offset < 0) {
1224         unsigned count;
1225         notmuch_status_t s = notmuch_query_count_messages (query, &count);
1226         if (print_status_query ("notmuch show", query, s))
1227             return 1;
1228
1229         params->offset += count;
1230         if (params->offset < 0)
1231             params->offset = 0;
1232     }
1233
1234     status = notmuch_query_search_messages (query, &messages);
1235     if (print_status_query ("notmuch show", query, status))
1236         return 1;
1237
1238     sp->begin_list (sp);
1239
1240     for (i = 0;
1241          notmuch_messages_valid (messages) && (params->limit < 0 || i < params->offset + params->limit);
1242          notmuch_messages_move_to_next (messages), i++) {
1243         if (i < params->offset) {
1244             continue;
1245         }
1246
1247         sp->begin_list (sp);
1248         sp->begin_list (sp);
1249
1250         message = notmuch_messages_get (messages);
1251
1252         notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, TRUE);
1253         excluded = _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED);
1254
1255         if (! excluded || ! params->omit_excluded) {
1256             status = show_message (ctx, format, sp, message, 0, params);
1257             if (status && ! res)
1258                 res = status;
1259         } else {
1260             sp->null (sp);
1261         }
1262         notmuch_message_destroy (message);
1263         sp->end (sp);
1264         sp->end (sp);
1265     }
1266     sp->end (sp);
1267     return res;
1268 }
1269
1270 enum {
1271     NOTMUCH_FORMAT_NOT_SPECIFIED,
1272     NOTMUCH_FORMAT_JSON,
1273     NOTMUCH_FORMAT_SEXP,
1274     NOTMUCH_FORMAT_TEXT,
1275     NOTMUCH_FORMAT_MBOX,
1276     NOTMUCH_FORMAT_RAW
1277 };
1278
1279 static const notmuch_show_format_t format_json = {
1280     .new_sprinter = sprinter_json_create,
1281     .part = format_part_sprinter_entry,
1282 };
1283
1284 static const notmuch_show_format_t format_sexp = {
1285     .new_sprinter = sprinter_sexp_create,
1286     .part = format_part_sprinter_entry,
1287 };
1288
1289 static const notmuch_show_format_t format_text = {
1290     .new_sprinter = sprinter_text_create,
1291     .part = format_part_text,
1292 };
1293
1294 static const notmuch_show_format_t format_mbox = {
1295     .new_sprinter = sprinter_text_create,
1296     .part = format_part_mbox,
1297 };
1298
1299 static const notmuch_show_format_t format_raw = {
1300     .new_sprinter = sprinter_text_create,
1301     .part = format_part_raw,
1302 };
1303
1304 static const notmuch_show_format_t *formatters[] = {
1305     [NOTMUCH_FORMAT_JSON] = &format_json,
1306     [NOTMUCH_FORMAT_SEXP] = &format_sexp,
1307     [NOTMUCH_FORMAT_TEXT] = &format_text,
1308     [NOTMUCH_FORMAT_MBOX] = &format_mbox,
1309     [NOTMUCH_FORMAT_RAW] = &format_raw,
1310 };
1311
1312 int
1313 notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[])
1314 {
1315     notmuch_query_t *query;
1316     char *query_string;
1317     int opt_index, ret;
1318     const notmuch_show_format_t *formatter;
1319     sprinter_t *sprinter;
1320     notmuch_show_params_t params = {
1321         .part = -1,
1322         .duplicate = 0,
1323         .offset = 0,
1324         .limit = -1, /* unlimited */
1325         .omit_excluded = true,
1326         .output_body = true,
1327         .crypto = { .decrypt = NOTMUCH_DECRYPT_AUTO },
1328     };
1329     int format = NOTMUCH_FORMAT_NOT_SPECIFIED;
1330     bool exclude = true;
1331     bool entire_thread_set = false;
1332     bool single_message;
1333     bool unthreaded = FALSE;
1334     notmuch_status_t status;
1335     int sort = NOTMUCH_SORT_NEWEST_FIRST;
1336
1337     notmuch_opt_desc_t options[] = {
1338         { .opt_keyword = &sort, .name = "sort", .keywords =
1339               (notmuch_keyword_t []){ { "oldest-first", NOTMUCH_SORT_OLDEST_FIRST },
1340                                       { "newest-first", NOTMUCH_SORT_NEWEST_FIRST },
1341                                       { 0, 0 } } },
1342         { .opt_keyword = &format, .name = "format", .keywords =
1343               (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
1344                                       { "text", NOTMUCH_FORMAT_TEXT },
1345                                       { "sexp", NOTMUCH_FORMAT_SEXP },
1346                                       { "mbox", NOTMUCH_FORMAT_MBOX },
1347                                       { "raw", NOTMUCH_FORMAT_RAW },
1348                                       { 0, 0 } } },
1349         { .opt_int = &notmuch_format_version, .name = "format-version" },
1350         { .opt_bool = &exclude, .name = "exclude" },
1351         { .opt_bool = &params.entire_thread, .name = "entire-thread",
1352           .present = &entire_thread_set },
1353         { .opt_bool = &unthreaded, .name = "unthreaded" },
1354         { .opt_int = &params.part, .name = "part" },
1355         { .opt_keyword = (int *) (&params.crypto.decrypt), .name = "decrypt",
1356           .keyword_no_arg_value = "true", .keywords =
1357               (notmuch_keyword_t []){ { "false", NOTMUCH_DECRYPT_FALSE },
1358                                       { "auto", NOTMUCH_DECRYPT_AUTO },
1359                                       { "true", NOTMUCH_DECRYPT_NOSTASH },
1360                                       { "stash", NOTMUCH_DECRYPT_TRUE },
1361                                       { 0, 0 } } },
1362         { .opt_bool = &params.crypto.verify, .name = "verify" },
1363         { .opt_bool = &params.output_body, .name = "body" },
1364         { .opt_bool = &params.include_html, .name = "include-html" },
1365         { .opt_int = &params.duplicate, .name = "duplicate" },
1366         { .opt_int = &params.limit, .name = "limit" },
1367         { .opt_int = &params.offset, .name = "offset" },
1368         { .opt_inherit = notmuch_shared_options },
1369         { }
1370     };
1371
1372     opt_index = parse_arguments (argc, argv, options, 1);
1373     if (opt_index < 0)
1374         return EXIT_FAILURE;
1375
1376     notmuch_process_shared_options (notmuch, argv[0]);
1377
1378     /* explicit decryption implies verification */
1379     if (params.crypto.decrypt == NOTMUCH_DECRYPT_NOSTASH ||
1380         params.crypto.decrypt == NOTMUCH_DECRYPT_TRUE)
1381         params.crypto.verify = true;
1382
1383     /* specifying a part implies single message display */
1384     single_message = params.part >= 0;
1385
1386     /* specifying a duplicate also implies single message display */
1387     single_message = single_message || (params.duplicate > 0);
1388
1389     if (format == NOTMUCH_FORMAT_NOT_SPECIFIED) {
1390         /* if part was requested and format was not specified, use format=raw */
1391         if (params.part >= 0)
1392             format = NOTMUCH_FORMAT_RAW;
1393         else
1394             format = NOTMUCH_FORMAT_TEXT;
1395     }
1396
1397     if (format == NOTMUCH_FORMAT_MBOX) {
1398         if (params.part > 0) {
1399             fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
1400             return EXIT_FAILURE;
1401         }
1402     } else if (format == NOTMUCH_FORMAT_RAW) {
1403         /* raw format only supports single message display */
1404         single_message = true;
1405     }
1406
1407     notmuch_exit_if_unsupported_format ();
1408
1409     /* Default is entire-thread = false except for format=json and
1410      * format=sexp. */
1411     if (! entire_thread_set &&
1412         (format == NOTMUCH_FORMAT_JSON || format == NOTMUCH_FORMAT_SEXP))
1413         params.entire_thread = true;
1414
1415     if (! params.output_body) {
1416         if (params.part > 0) {
1417             fprintf (stderr, "Warning: --body=false is incompatible with --part > 0. Disabling.\n");
1418             params.output_body = true;
1419         } else {
1420             if (format != NOTMUCH_FORMAT_TEXT &&
1421                 format != NOTMUCH_FORMAT_JSON &&
1422                 format != NOTMUCH_FORMAT_SEXP)
1423                 fprintf (stderr,
1424                          "Warning: --body=false only implemented for format=text, format=json and format=sexp\n");
1425         }
1426     }
1427
1428     if (params.include_html &&
1429         (format != NOTMUCH_FORMAT_TEXT &&
1430          format != NOTMUCH_FORMAT_JSON &&
1431          format != NOTMUCH_FORMAT_SEXP)) {
1432         fprintf (stderr,
1433                  "Warning: --include-html only implemented for format=text, format=json and format=sexp\n");
1434     }
1435
1436     if (params.crypto.decrypt == NOTMUCH_DECRYPT_TRUE) {
1437         status = notmuch_database_reopen (notmuch, NOTMUCH_DATABASE_MODE_READ_WRITE);
1438         if (status) {
1439             fprintf (stderr, "Error reopening database for READ_WRITE: %s\n",
1440                      notmuch_status_to_string (status));
1441             return EXIT_FAILURE;
1442         }
1443     }
1444
1445     query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
1446     if (query_string == NULL) {
1447         fprintf (stderr, "Out of memory\n");
1448         return EXIT_FAILURE;
1449     }
1450
1451     if (*query_string == '\0') {
1452         fprintf (stderr, "Error: notmuch show requires at least one search term.\n");
1453         return EXIT_FAILURE;
1454     }
1455
1456     status = notmuch_query_create_with_syntax (notmuch, query_string,
1457                                                shared_option_query_syntax (),
1458                                                &query);
1459     if (print_status_database ("notmuch show", notmuch, status))
1460         return EXIT_FAILURE;
1461
1462     notmuch_query_set_sort (query, sort);
1463
1464     /* Create structure printer. */
1465     formatter = formatters[format];
1466     sprinter = formatter->new_sprinter (notmuch, stdout);
1467
1468     params.out_stream = g_mime_stream_stdout_new ();
1469
1470     /* If a single message is requested we do not use search_excludes. */
1471     if (single_message) {
1472         ret = do_show_single (notmuch, query, formatter, sprinter, &params);
1473     } else {
1474         /* We always apply set the exclude flag. The
1475          * exclude=true|false option controls whether or not we return
1476          * threads that only match in an excluded message */
1477         notmuch_config_values_t *exclude_tags;
1478         notmuch_status_t status;
1479
1480         for (exclude_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS);
1481              notmuch_config_values_valid (exclude_tags);
1482              notmuch_config_values_move_to_next (exclude_tags)) {
1483
1484             status = notmuch_query_add_tag_exclude (query,
1485                                                     notmuch_config_values_get (exclude_tags));
1486             if (status && status != NOTMUCH_STATUS_IGNORED) {
1487                 print_status_query ("notmuch show", query, status);
1488                 ret = -1;
1489                 goto DONE;
1490             }
1491         }
1492
1493         if (exclude == false) {
1494             notmuch_query_set_omit_excluded (query, false);
1495             params.omit_excluded = false;
1496         }
1497
1498         if (unthreaded)
1499             ret = do_show_unthreaded (notmuch, query, formatter, sprinter, &params);
1500         else
1501             ret = do_show_threaded (notmuch, query, formatter, sprinter, &params);
1502     }
1503
1504   DONE:
1505     g_mime_stream_flush (params.out_stream);
1506     g_object_unref (params.out_stream);
1507
1508     _notmuch_crypto_cleanup (&params.crypto);
1509     notmuch_query_destroy (query);
1510     notmuch_database_destroy (notmuch);
1511
1512     return ret ? EXIT_FAILURE : EXIT_SUCCESS;
1513 }