]> git.cworth.org Git - notmuch/blob - notmuch-show.c
emacs: limit search for attachment to stop at first mime-part
[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             format_omitted_part_meta_sprinter (sp, meta, GMIME_PART (node->part));
763         }
764     } else if (GMIME_IS_MULTIPART (node->part)) {
765         sp->map_key (sp, "content");
766         sp->begin_list (sp);
767         nclose = 1;
768     } else if (GMIME_IS_MESSAGE (node->part)) {
769         sp->map_key (sp, "content");
770         sp->begin_list (sp);
771         sp->begin_map (sp);
772
773         sp->map_key (sp, "headers");
774         format_headers_sprinter (sp, GMIME_MESSAGE (node->part), false, NULL);
775
776         sp->map_key (sp, "body");
777         sp->begin_list (sp);
778         nclose = 3;
779     }
780
781     for (i = 0; i < node->nchildren; i++)
782         format_part_sprinter (ctx, sp, mime_node_child (node, i), true, include_html);
783
784     /* Close content structures */
785     for (i = 0; i < nclose; i++)
786         sp->end (sp);
787     /* Close part map */
788     sp->end (sp);
789 }
790
791 static notmuch_status_t
792 format_part_sprinter_entry (const void *ctx, sprinter_t *sp,
793                             mime_node_t *node, unused (int indent),
794                             const notmuch_show_params_t *params)
795 {
796     format_part_sprinter (ctx, sp, node, params->output_body, params->include_html);
797
798     return NOTMUCH_STATUS_SUCCESS;
799 }
800
801 /* Print a message in "mboxrd" format as documented, for example,
802  * here:
803  *
804  * http://qmail.org/qmail-manual-html/man5/mbox.html
805  */
806 static notmuch_status_t
807 format_part_mbox (const void *ctx, unused (sprinter_t *sp), mime_node_t *node,
808                   unused (int indent),
809                   unused (const notmuch_show_params_t *params))
810 {
811     notmuch_message_t *message = node->envelope_file;
812
813     const char *filename;
814     gzFile file;
815     const char *from;
816
817     time_t date;
818     struct tm date_gmtime;
819     char date_asctime[26];
820
821     char *line = NULL;
822     ssize_t line_size;
823     ssize_t line_len;
824
825     if (! message)
826         INTERNAL_ERROR ("format_part_mbox requires a root part");
827
828     filename = notmuch_message_get_filename (message);
829     file = gzopen (filename, "r");
830     if (file == NULL) {
831         fprintf (stderr, "Failed to open %s: %s\n",
832                  filename, strerror (errno));
833         return NOTMUCH_STATUS_FILE_ERROR;
834     }
835
836     from = notmuch_message_get_header (message, "from");
837     from = _extract_email_address (ctx, from);
838
839     date = notmuch_message_get_date (message);
840     gmtime_r (&date, &date_gmtime);
841     asctime_r (&date_gmtime, date_asctime);
842
843     printf ("From %s %s", from, date_asctime);
844
845     while ((line_len = gz_getline (message, &line, &line_size, file)) != UTIL_EOF ) {
846         if (_is_from_line (line))
847             putchar ('>');
848         printf ("%s", line);
849     }
850
851     printf ("\n");
852
853     gzclose (file);
854
855     return NOTMUCH_STATUS_SUCCESS;
856 }
857
858 static notmuch_status_t
859 format_part_raw (unused (const void *ctx), unused (sprinter_t *sp),
860                  mime_node_t *node, unused (int indent),
861                  const notmuch_show_params_t *params)
862 {
863     if (node->envelope_file) {
864         /* Special case the entire message to avoid MIME parsing. */
865         const char *filename;
866         GMimeStream *stream = NULL;
867         ssize_t ssize;
868         char buf[4096];
869         notmuch_status_t ret = NOTMUCH_STATUS_FILE_ERROR;
870
871         filename = notmuch_message_get_filename (node->envelope_file);
872         if (filename == NULL) {
873             fprintf (stderr, "Error: Cannot get message filename.\n");
874             goto DONE;
875         }
876
877         stream = g_mime_stream_gzfile_open (filename);
878         if (stream == NULL) {
879             fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
880             goto DONE;
881         }
882
883         while (! g_mime_stream_eos (stream)) {
884             ssize = g_mime_stream_read (stream, buf, sizeof (buf));
885             if (ssize < 0) {
886                 fprintf (stderr, "Error: Read failed from %s\n", filename);
887                 goto DONE;
888             }
889
890             if (ssize > 0 && fwrite (buf, ssize, 1, stdout) != 1) {
891                 fprintf (stderr, "Error: Write %ld chars to stdout failed\n", ssize);
892                 goto DONE;
893             }
894         }
895
896         ret = NOTMUCH_STATUS_SUCCESS;
897
898         /* XXX This DONE is just for the special case of a node in a single file */
899       DONE:
900         if (stream)
901             g_object_unref (stream);
902
903         return ret;
904     }
905
906     GMimeStream *stream_filter = g_mime_stream_filter_new (params->out_stream);
907
908     if (GMIME_IS_PART (node->part)) {
909         /* For leaf parts, we emit only the transfer-decoded
910          * body. */
911         GMimeDataWrapper *wrapper;
912         wrapper = g_mime_part_get_content (GMIME_PART (node->part));
913
914         if (wrapper && stream_filter)
915             g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
916     } else {
917         /* Write out the whole part.  For message parts (the root
918          * part and embedded message parts), this will be the
919          * message including its headers (but not the
920          * encapsulating part's headers).  For multipart parts,
921          * this will include the headers. */
922         if (stream_filter)
923             g_mime_object_write_to_stream (node->part, NULL, stream_filter);
924     }
925
926     if (stream_filter)
927         g_object_unref (stream_filter);
928
929     return NOTMUCH_STATUS_SUCCESS;
930 }
931
932 static notmuch_status_t
933 show_message (void *ctx,
934               const notmuch_show_format_t *format,
935               sprinter_t *sp,
936               notmuch_message_t *message,
937               int indent,
938               notmuch_show_params_t *params)
939 {
940     void *local = talloc_new (ctx);
941     mime_node_t *root, *part;
942     notmuch_status_t status;
943     unsigned int session_keys = 0;
944     notmuch_status_t session_key_count_error = NOTMUCH_STATUS_SUCCESS;
945
946     if (params->crypto.decrypt == NOTMUCH_DECRYPT_TRUE)
947         session_key_count_error = notmuch_message_count_properties (message, "session-key", &session_keys);
948
949     status = mime_node_open (local, message, &(params->crypto), &root);
950     if (status)
951         goto DONE;
952     part = mime_node_seek_dfs (root, (params->part < 0 ? 0 : params->part));
953     if (part)
954         status = format->part (local, sp, part, indent, params);
955     if (params->crypto.decrypt == NOTMUCH_DECRYPT_TRUE && session_key_count_error == NOTMUCH_STATUS_SUCCESS) {
956         unsigned int new_session_keys = 0;
957         if (notmuch_message_count_properties (message, "session-key", &new_session_keys) == NOTMUCH_STATUS_SUCCESS &&
958             new_session_keys > session_keys) {
959             /* try a quiet re-indexing */
960             notmuch_indexopts_t *indexopts = notmuch_database_get_default_indexopts (notmuch_message_get_database (message));
961             if (indexopts) {
962                 notmuch_indexopts_set_decrypt_policy (indexopts, NOTMUCH_DECRYPT_AUTO);
963                 print_status_message ("Error re-indexing message with --decrypt=stash",
964                                       message, notmuch_message_reindex (message, indexopts));
965             }
966         }
967     }
968   DONE:
969     talloc_free (local);
970     return status;
971 }
972
973 static notmuch_status_t
974 show_messages (void *ctx,
975                const notmuch_show_format_t *format,
976                sprinter_t *sp,
977                notmuch_messages_t *messages,
978                int indent,
979                notmuch_show_params_t *params)
980 {
981     notmuch_message_t *message;
982     bool match;
983     bool excluded;
984     int next_indent;
985     notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
986
987     sp->begin_list (sp);
988
989     for (;
990          notmuch_messages_valid (messages);
991          notmuch_messages_move_to_next (messages)) {
992         sp->begin_list (sp);
993
994         message = notmuch_messages_get (messages);
995
996         match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
997         excluded = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED);
998
999         next_indent = indent;
1000
1001         if ((match && (! excluded || ! params->omit_excluded)) || params->entire_thread) {
1002             status = show_message (ctx, format, sp, message, indent, params);
1003             if (status && ! res)
1004                 res = status;
1005             next_indent = indent + 1;
1006         } else {
1007             sp->null (sp);
1008         }
1009
1010         status = show_messages (ctx,
1011                                 format, sp,
1012                                 notmuch_message_get_replies (message),
1013                                 next_indent,
1014                                 params);
1015         if (status && ! res)
1016             res = status;
1017
1018         notmuch_message_destroy (message);
1019
1020         sp->end (sp);
1021     }
1022
1023     sp->end (sp);
1024
1025     return res;
1026 }
1027
1028 /* Formatted output of single message */
1029 static int
1030 do_show_single (void *ctx,
1031                 notmuch_query_t *query,
1032                 const notmuch_show_format_t *format,
1033                 sprinter_t *sp,
1034                 notmuch_show_params_t *params)
1035 {
1036     notmuch_messages_t *messages;
1037     notmuch_message_t *message;
1038     notmuch_status_t status;
1039     unsigned int count;
1040
1041     status = notmuch_query_count_messages (query, &count);
1042     if (print_status_query ("notmuch show", query, status))
1043         return 1;
1044
1045     if (count != 1) {
1046         fprintf (stderr, "Error: search term did not match precisely one message (matched %u messages).\n", count);
1047         return 1;
1048     }
1049
1050     status = notmuch_query_search_messages (query, &messages);
1051     if (print_status_query ("notmuch show", query, status))
1052         return 1;
1053
1054     message = notmuch_messages_get (messages);
1055
1056     if (message == NULL) {
1057         fprintf (stderr, "Error: Cannot find matching message.\n");
1058         return 1;
1059     }
1060
1061     notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
1062
1063     return show_message (ctx, format, sp, message, 0, params)
1064            != NOTMUCH_STATUS_SUCCESS;
1065 }
1066
1067 /* Formatted output of threads */
1068 static int
1069 do_show (void *ctx,
1070          notmuch_query_t *query,
1071          const notmuch_show_format_t *format,
1072          sprinter_t *sp,
1073          notmuch_show_params_t *params)
1074 {
1075     notmuch_threads_t *threads;
1076     notmuch_thread_t *thread;
1077     notmuch_messages_t *messages;
1078     notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
1079
1080     status = notmuch_query_search_threads (query, &threads);
1081     if (print_status_query ("notmuch show", query, status))
1082         return 1;
1083
1084     sp->begin_list (sp);
1085
1086     for (;
1087          notmuch_threads_valid (threads);
1088          notmuch_threads_move_to_next (threads)) {
1089         thread = notmuch_threads_get (threads);
1090
1091         messages = notmuch_thread_get_toplevel_messages (thread);
1092
1093         if (messages == NULL)
1094             INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
1095                             notmuch_thread_get_thread_id (thread));
1096
1097         status = show_messages (ctx, format, sp, messages, 0, params);
1098         if (status && ! res)
1099             res = status;
1100
1101         notmuch_thread_destroy (thread);
1102
1103     }
1104
1105     sp->end (sp);
1106
1107     return res != NOTMUCH_STATUS_SUCCESS;
1108 }
1109
1110 enum {
1111     NOTMUCH_FORMAT_NOT_SPECIFIED,
1112     NOTMUCH_FORMAT_JSON,
1113     NOTMUCH_FORMAT_SEXP,
1114     NOTMUCH_FORMAT_TEXT,
1115     NOTMUCH_FORMAT_MBOX,
1116     NOTMUCH_FORMAT_RAW
1117 };
1118
1119 static const notmuch_show_format_t format_json = {
1120     .new_sprinter = sprinter_json_create,
1121     .part = format_part_sprinter_entry,
1122 };
1123
1124 static const notmuch_show_format_t format_sexp = {
1125     .new_sprinter = sprinter_sexp_create,
1126     .part = format_part_sprinter_entry,
1127 };
1128
1129 static const notmuch_show_format_t format_text = {
1130     .new_sprinter = sprinter_text_create,
1131     .part = format_part_text,
1132 };
1133
1134 static const notmuch_show_format_t format_mbox = {
1135     .new_sprinter = sprinter_text_create,
1136     .part = format_part_mbox,
1137 };
1138
1139 static const notmuch_show_format_t format_raw = {
1140     .new_sprinter = sprinter_text_create,
1141     .part = format_part_raw,
1142 };
1143
1144 static const notmuch_show_format_t *formatters[] = {
1145     [NOTMUCH_FORMAT_JSON] = &format_json,
1146     [NOTMUCH_FORMAT_SEXP] = &format_sexp,
1147     [NOTMUCH_FORMAT_TEXT] = &format_text,
1148     [NOTMUCH_FORMAT_MBOX] = &format_mbox,
1149     [NOTMUCH_FORMAT_RAW] = &format_raw,
1150 };
1151
1152 int
1153 notmuch_show_command (notmuch_config_t *config, int argc, char *argv[])
1154 {
1155     notmuch_database_t *notmuch;
1156     notmuch_query_t *query;
1157     char *query_string;
1158     int opt_index, ret;
1159     const notmuch_show_format_t *formatter;
1160     sprinter_t *sprinter;
1161     notmuch_show_params_t params = {
1162         .part = -1,
1163         .omit_excluded = true,
1164         .output_body = true,
1165         .crypto = { .decrypt = NOTMUCH_DECRYPT_AUTO },
1166     };
1167     int format = NOTMUCH_FORMAT_NOT_SPECIFIED;
1168     bool exclude = true;
1169     bool entire_thread_set = false;
1170     bool single_message;
1171
1172     notmuch_opt_desc_t options[] = {
1173         { .opt_keyword = &format, .name = "format", .keywords =
1174               (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
1175                                       { "text", NOTMUCH_FORMAT_TEXT },
1176                                       { "sexp", NOTMUCH_FORMAT_SEXP },
1177                                       { "mbox", NOTMUCH_FORMAT_MBOX },
1178                                       { "raw", NOTMUCH_FORMAT_RAW },
1179                                       { 0, 0 } } },
1180         { .opt_int = &notmuch_format_version, .name = "format-version" },
1181         { .opt_bool = &exclude, .name = "exclude" },
1182         { .opt_bool = &params.entire_thread, .name = "entire-thread",
1183           .present = &entire_thread_set },
1184         { .opt_int = &params.part, .name = "part" },
1185         { .opt_keyword = (int *) (&params.crypto.decrypt), .name = "decrypt",
1186           .keyword_no_arg_value = "true", .keywords =
1187               (notmuch_keyword_t []){ { "false", NOTMUCH_DECRYPT_FALSE },
1188                                       { "auto", NOTMUCH_DECRYPT_AUTO },
1189                                       { "true", NOTMUCH_DECRYPT_NOSTASH },
1190                                       { "stash", NOTMUCH_DECRYPT_TRUE },
1191                                       { 0, 0 } } },
1192         { .opt_bool = &params.crypto.verify, .name = "verify" },
1193         { .opt_bool = &params.output_body, .name = "body" },
1194         { .opt_bool = &params.include_html, .name = "include-html" },
1195         { .opt_inherit = notmuch_shared_options },
1196         { }
1197     };
1198
1199     opt_index = parse_arguments (argc, argv, options, 1);
1200     if (opt_index < 0)
1201         return EXIT_FAILURE;
1202
1203     notmuch_process_shared_options (argv[0]);
1204
1205     /* explicit decryption implies verification */
1206     if (params.crypto.decrypt == NOTMUCH_DECRYPT_NOSTASH ||
1207         params.crypto.decrypt == NOTMUCH_DECRYPT_TRUE)
1208         params.crypto.verify = true;
1209
1210     /* specifying a part implies single message display */
1211     single_message = params.part >= 0;
1212
1213     if (format == NOTMUCH_FORMAT_NOT_SPECIFIED) {
1214         /* if part was requested and format was not specified, use format=raw */
1215         if (params.part >= 0)
1216             format = NOTMUCH_FORMAT_RAW;
1217         else
1218             format = NOTMUCH_FORMAT_TEXT;
1219     }
1220
1221     if (format == NOTMUCH_FORMAT_MBOX) {
1222         if (params.part > 0) {
1223             fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
1224             return EXIT_FAILURE;
1225         }
1226     } else if (format == NOTMUCH_FORMAT_RAW) {
1227         /* raw format only supports single message display */
1228         single_message = true;
1229     }
1230
1231     notmuch_exit_if_unsupported_format ();
1232
1233     /* Default is entire-thread = false except for format=json and
1234      * format=sexp. */
1235     if (! entire_thread_set &&
1236         (format == NOTMUCH_FORMAT_JSON || format == NOTMUCH_FORMAT_SEXP))
1237         params.entire_thread = true;
1238
1239     if (! params.output_body) {
1240         if (params.part > 0) {
1241             fprintf (stderr, "Warning: --body=false is incompatible with --part > 0. Disabling.\n");
1242             params.output_body = true;
1243         } else {
1244             if (format != NOTMUCH_FORMAT_TEXT &&
1245                 format != NOTMUCH_FORMAT_JSON &&
1246                 format != NOTMUCH_FORMAT_SEXP)
1247                 fprintf (stderr,
1248                          "Warning: --body=false only implemented for format=text, format=json and format=sexp\n");
1249         }
1250     }
1251
1252     if (params.include_html &&
1253         (format != NOTMUCH_FORMAT_TEXT &&
1254          format != NOTMUCH_FORMAT_JSON &&
1255          format != NOTMUCH_FORMAT_SEXP)) {
1256         fprintf (stderr, "Warning: --include-html only implemented for format=text, format=json and format=sexp\n");
1257     }
1258
1259     query_string = query_string_from_args (config, argc - opt_index, argv + opt_index);
1260     if (query_string == NULL) {
1261         fprintf (stderr, "Out of memory\n");
1262         return EXIT_FAILURE;
1263     }
1264
1265     if (*query_string == '\0') {
1266         fprintf (stderr, "Error: notmuch show requires at least one search term.\n");
1267         return EXIT_FAILURE;
1268     }
1269
1270     notmuch_database_mode_t mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
1271     if (params.crypto.decrypt == NOTMUCH_DECRYPT_TRUE)
1272         mode = NOTMUCH_DATABASE_MODE_READ_WRITE;
1273     if (notmuch_database_open (notmuch_config_get_database_path (config),
1274                                mode, &notmuch))
1275         return EXIT_FAILURE;
1276
1277     notmuch_exit_if_unmatched_db_uuid (notmuch);
1278
1279     query = notmuch_query_create (notmuch, query_string);
1280     if (query == NULL) {
1281         fprintf (stderr, "Out of memory\n");
1282         return EXIT_FAILURE;
1283     }
1284
1285     /* Create structure printer. */
1286     formatter = formatters[format];
1287     sprinter = formatter->new_sprinter (config, stdout);
1288
1289     params.out_stream = g_mime_stream_stdout_new ();
1290
1291     /* If a single message is requested we do not use search_excludes. */
1292     if (single_message) {
1293         ret = do_show_single (config, query, formatter, sprinter, &params);
1294     } else {
1295         /* We always apply set the exclude flag. The
1296          * exclude=true|false option controls whether or not we return
1297          * threads that only match in an excluded message */
1298         const char **search_exclude_tags;
1299         size_t search_exclude_tags_length;
1300         unsigned int i;
1301         notmuch_status_t status;
1302
1303         search_exclude_tags = notmuch_config_get_search_exclude_tags
1304                                   (config, &search_exclude_tags_length);
1305
1306         for (i = 0; i < search_exclude_tags_length; i++) {
1307             status = notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
1308             if (status && status != NOTMUCH_STATUS_IGNORED) {
1309                 print_status_query ("notmuch show", query, status);
1310                 ret = -1;
1311                 goto DONE;
1312             }
1313         }
1314
1315         if (exclude == false) {
1316             notmuch_query_set_omit_excluded (query, false);
1317             params.omit_excluded = false;
1318         }
1319
1320         ret = do_show (config, query, formatter, sprinter, &params);
1321     }
1322
1323   DONE:
1324     g_mime_stream_flush (params.out_stream);
1325     g_object_unref (params.out_stream);
1326
1327     _notmuch_crypto_cleanup (&params.crypto);
1328     notmuch_query_destroy (query);
1329     notmuch_database_destroy (notmuch);
1330
1331     return ret ? EXIT_FAILURE : EXIT_SUCCESS;
1332 }