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