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