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