cli/show: move formatter structs closer to where they're needed
[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                          notmuch_bool_t reply)
200 {
201     /* Any changes to the JSON or S-Expression format should be
202      * reflected in the file devel/schemata. */
203
204     InternetAddressList *recipients;
205     const char *recipients_string;
206     const char *reply_to_string;
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_sender (message));
215
216     recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO);
217     recipients_string = internet_address_list_to_string (recipients, 0);
218     if (recipients_string) {
219         sp->map_key (sp, "To");
220         sp->string (sp, recipients_string);
221     }
222
223     recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_CC);
224     recipients_string = internet_address_list_to_string (recipients, 0);
225     if (recipients_string) {
226         sp->map_key (sp, "Cc");
227         sp->string (sp, recipients_string);
228     }
229
230     recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_BCC);
231     recipients_string = internet_address_list_to_string (recipients, 0);
232     if (recipients_string) {
233         sp->map_key (sp, "Bcc");
234         sp->string (sp, recipients_string);
235     }
236
237     reply_to_string = g_mime_message_get_reply_to (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_as_string (message));
252     }
253
254     sp->end (sp);
255 }
256
257 /* Write a MIME text part out to the given stream.
258  *
259  * If (flags & NOTMUCH_SHOW_TEXT_PART_REPLY), this prepends "> " to
260  * each output line.
261  *
262  * Both line-ending conversion (CRLF->LF) and charset conversion ( ->
263  * UTF-8) will be performed, so it is inappropriate to call this
264  * function with a non-text part. Doing so will trigger an internal
265  * error.
266  */
267 void
268 show_text_part_content (GMimeObject *part, GMimeStream *stream_out,
269                         notmuch_show_text_part_flags flags)
270 {
271     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
272     GMimeStream *stream_filter = NULL;
273     GMimeDataWrapper *wrapper;
274     const char *charset;
275
276     if (! g_mime_content_type_is_type (content_type, "text", "*"))
277         INTERNAL_ERROR ("Illegal request to format non-text part (%s) as text.",
278                         g_mime_content_type_to_string (content_type));
279
280     if (stream_out == NULL)
281         return;
282
283     stream_filter = g_mime_stream_filter_new (stream_out);
284     g_mime_stream_filter_add(GMIME_STREAM_FILTER (stream_filter),
285                              g_mime_filter_crlf_new (FALSE, FALSE));
286
287     charset = g_mime_object_get_content_type_parameter (part, "charset");
288     if (charset) {
289         GMimeFilter *charset_filter;
290         charset_filter = g_mime_filter_charset_new (charset, "UTF-8");
291         /* This result can be NULL for things like "unknown-8bit".
292          * Don't set a NULL filter as that makes GMime print
293          * annoying assertion-failure messages on stderr. */
294         if (charset_filter) {
295             g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
296                                       charset_filter);
297             g_object_unref (charset_filter);
298         }
299
300     }
301
302     if (flags & NOTMUCH_SHOW_TEXT_PART_REPLY) {
303         GMimeFilter *reply_filter;
304         reply_filter = g_mime_filter_reply_new (TRUE);
305         if (reply_filter) {
306             g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
307                                       reply_filter);
308             g_object_unref (reply_filter);
309         }
310     }
311
312     wrapper = g_mime_part_get_content_object (GMIME_PART (part));
313     if (wrapper && stream_filter)
314         g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
315     if (stream_filter)
316         g_object_unref(stream_filter);
317 }
318
319 /* Get signature status string (GMime 2.6) */
320 static const char*
321 signature_status_to_string (GMimeSignatureStatus x)
322 {
323     switch (x) {
324     case GMIME_SIGNATURE_STATUS_GOOD:
325         return "good";
326     case GMIME_SIGNATURE_STATUS_BAD:
327         return "bad";
328     case GMIME_SIGNATURE_STATUS_ERROR:
329         return "error";
330     }
331     return "unknown";
332 }
333
334 /* Signature status sprinter (GMime 2.6) */
335 static void
336 format_part_sigstatus_sprinter (sprinter_t *sp, mime_node_t *node)
337 {
338     /* Any changes to the JSON or S-Expression format should be
339      * reflected in the file devel/schemata. */
340
341     GMimeSignatureList *siglist = node->sig_list;
342
343     sp->begin_list (sp);
344
345     if (!siglist) {
346         sp->end (sp);
347         return;
348     }
349
350     int i;
351     for (i = 0; i < g_mime_signature_list_length (siglist); i++) {
352         GMimeSignature *signature = g_mime_signature_list_get_signature (siglist, i);
353
354         sp->begin_map (sp);
355
356         /* status */
357         GMimeSignatureStatus status = g_mime_signature_get_status (signature);
358         sp->map_key (sp, "status");
359         sp->string (sp, signature_status_to_string (status));
360
361         GMimeCertificate *certificate = g_mime_signature_get_certificate (signature);
362         if (status == GMIME_SIGNATURE_STATUS_GOOD) {
363             if (certificate) {
364                 sp->map_key (sp, "fingerprint");
365                 sp->string (sp, g_mime_certificate_get_fingerprint (certificate));
366             }
367             /* these dates are seconds since the epoch; should we
368              * provide a more human-readable format string? */
369             time_t created = g_mime_signature_get_created (signature);
370             if (created != -1) {
371                 sp->map_key (sp, "created");
372                 sp->integer (sp, created);
373             }
374             time_t expires = g_mime_signature_get_expires (signature);
375             if (expires > 0) {
376                 sp->map_key (sp, "expires");
377                 sp->integer (sp, expires);
378             }
379             /* output user id only if validity is FULL or ULTIMATE. */
380             /* note that gmime is using the term "trust" here, which
381              * is WRONG.  It's actually user id "validity". */
382             if (certificate) {
383                 const char *name = g_mime_certificate_get_name (certificate);
384                 GMimeCertificateTrust trust = g_mime_certificate_get_trust (certificate);
385                 if (name && (trust == GMIME_CERTIFICATE_TRUST_FULLY || trust == GMIME_CERTIFICATE_TRUST_ULTIMATE)) {
386                     sp->map_key (sp, "userid");
387                     sp->string (sp, name);
388                 }
389             }
390         } else if (certificate) {
391             const char *key_id = g_mime_certificate_get_key_id (certificate);
392             if (key_id) {
393                 sp->map_key (sp, "keyid");
394                 sp->string (sp, key_id);
395             }
396         }
397
398         GMimeSignatureError errors = g_mime_signature_get_errors (signature);
399         if (errors != GMIME_SIGNATURE_ERROR_NONE) {
400             sp->map_key (sp, "errors");
401             sp->integer (sp, errors);
402         }
403
404         sp->end (sp);
405      }
406
407     sp->end (sp);
408 }
409
410 static notmuch_status_t
411 format_part_text (const void *ctx, sprinter_t *sp, mime_node_t *node,
412                   int indent, const notmuch_show_params_t *params)
413 {
414     /* The disposition and content-type metadata are associated with
415      * the envelope for message parts */
416     GMimeObject *meta = node->envelope_part ?
417         GMIME_OBJECT (node->envelope_part) : node->part;
418     GMimeContentType *content_type = g_mime_object_get_content_type (meta);
419     const notmuch_bool_t leaf = GMIME_IS_PART (node->part);
420     const char *part_type;
421     int i;
422
423     if (node->envelope_file) {
424         notmuch_message_t *message = node->envelope_file;
425
426         part_type = "message";
427         printf ("\f%s{ id:%s depth:%d match:%d excluded:%d filename:%s\n",
428                 part_type,
429                 notmuch_message_get_message_id (message),
430                 indent,
431                 notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? 1 : 0,
432                 notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED) ? 1 : 0,
433                 notmuch_message_get_filename (message));
434     } else {
435         const char *disposition = _get_disposition (meta);
436         const char *cid = g_mime_object_get_content_id (meta);
437         const char *filename = leaf ?
438             g_mime_part_get_filename (GMIME_PART (node->part)) : NULL;
439
440         if (disposition &&
441             strcasecmp (disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
442             part_type = "attachment";
443         else
444             part_type = "part";
445
446         printf ("\f%s{ ID: %d", part_type, node->part_num);
447         if (filename)
448             printf (", Filename: %s", filename);
449         if (cid)
450             printf (", Content-id: %s", cid);
451         printf (", Content-type: %s\n", g_mime_content_type_to_string (content_type));
452     }
453
454     if (GMIME_IS_MESSAGE (node->part)) {
455         GMimeMessage *message = GMIME_MESSAGE (node->part);
456         InternetAddressList *recipients;
457         const char *recipients_string;
458
459         printf ("\fheader{\n");
460         if (node->envelope_file)
461             printf ("%s\n", _get_one_line_summary (ctx, node->envelope_file));
462         printf ("Subject: %s\n", g_mime_message_get_subject (message));
463         printf ("From: %s\n", g_mime_message_get_sender (message));
464         recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO);
465         recipients_string = internet_address_list_to_string (recipients, 0);
466         if (recipients_string)
467             printf ("To: %s\n", recipients_string);
468         recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_CC);
469         recipients_string = internet_address_list_to_string (recipients, 0);
470         if (recipients_string)
471             printf ("Cc: %s\n", recipients_string);
472         printf ("Date: %s\n", g_mime_message_get_date_as_string (message));
473         printf ("\fheader}\n");
474
475         printf ("\fbody{\n");
476     }
477
478     if (leaf) {
479         if (g_mime_content_type_is_type (content_type, "text", "*") &&
480             !g_mime_content_type_is_type (content_type, "text", "html"))
481         {
482             GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
483             g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
484             show_text_part_content (node->part, stream_stdout, 0);
485             g_object_unref(stream_stdout);
486         } else {
487             printf ("Non-text part: %s\n",
488                     g_mime_content_type_to_string (content_type));
489         }
490     }
491
492     for (i = 0; i < node->nchildren; i++)
493         format_part_text (ctx, sp, mime_node_child (node, i), indent, params);
494
495     if (GMIME_IS_MESSAGE (node->part))
496         printf ("\fbody}\n");
497
498     printf ("\f%s}\n", part_type);
499
500     return NOTMUCH_STATUS_SUCCESS;
501 }
502
503 static void
504 format_omitted_part_meta_sprinter (sprinter_t *sp, GMimeObject *meta, GMimePart *part)
505 {
506     const char *content_charset = g_mime_object_get_content_type_parameter (meta, "charset");
507     const char *cte = g_mime_object_get_header (meta, "content-transfer-encoding");
508     GMimeDataWrapper *wrapper = g_mime_part_get_content_object (part);
509     GMimeStream *stream = g_mime_data_wrapper_get_stream (wrapper);
510     ssize_t content_length = g_mime_stream_length (stream);
511
512     if (content_charset != NULL) {
513         sp->map_key (sp, "content-charset");
514         sp->string (sp, content_charset);
515     }
516     if (cte != NULL) {
517         sp->map_key (sp, "content-transfer-encoding");
518         sp->string (sp, cte);
519     }
520     if (content_length >= 0) {
521         sp->map_key (sp, "content-length");
522         sp->integer (sp, content_length);
523     }
524 }
525
526 void
527 format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
528                       notmuch_bool_t first, notmuch_bool_t output_body,
529                       notmuch_bool_t include_html)
530 {
531     /* Any changes to the JSON or S-Expression format should be
532      * reflected in the file devel/schemata. */
533
534     if (node->envelope_file) {
535         sp->begin_map (sp);
536         format_message_sprinter (sp, node->envelope_file);
537
538         sp->map_key (sp, "headers");
539         format_headers_sprinter (sp, GMIME_MESSAGE (node->part), FALSE);
540
541         if (output_body) {
542             sp->map_key (sp, "body");
543             sp->begin_list (sp);
544             format_part_sprinter (ctx, sp, mime_node_child (node, 0), first, TRUE, include_html);
545             sp->end (sp);
546         }
547         sp->end (sp);
548         return;
549     }
550
551     /* The disposition and content-type metadata are associated with
552      * the envelope for message parts */
553     GMimeObject *meta = node->envelope_part ?
554         GMIME_OBJECT (node->envelope_part) : node->part;
555     GMimeContentType *content_type = g_mime_object_get_content_type (meta);
556     const char *disposition = _get_disposition (meta);
557     const char *cid = g_mime_object_get_content_id (meta);
558     const char *filename = GMIME_IS_PART (node->part) ?
559         g_mime_part_get_filename (GMIME_PART (node->part)) : NULL;
560     int nclose = 0;
561     int i;
562
563     sp->begin_map (sp);
564
565     sp->map_key (sp, "id");
566     sp->integer (sp, node->part_num);
567
568     if (node->decrypt_attempted) {
569         sp->map_key (sp, "encstatus");
570         sp->begin_list (sp);
571         sp->begin_map (sp);
572         sp->map_key (sp, "status");
573         sp->string (sp, node->decrypt_success ? "good" : "bad");
574         sp->end (sp);
575         sp->end (sp);
576     }
577
578     if (node->verify_attempted) {
579         sp->map_key (sp, "sigstatus");
580         format_part_sigstatus_sprinter (sp, node);
581     }
582
583     sp->map_key (sp, "content-type");
584     sp->string (sp, g_mime_content_type_to_string (content_type));
585
586     if (disposition) {
587         sp->map_key (sp, "content-disposition");
588         sp->string (sp, disposition);
589     }
590
591     if (cid) {
592         sp->map_key (sp, "content-id");
593         sp->string (sp, cid);
594     }
595
596     if (filename) {
597         sp->map_key (sp, "filename");
598         sp->string (sp, filename);
599     }
600
601     if (GMIME_IS_PART (node->part)) {
602         /* For non-HTML text parts, we include the content in the
603          * JSON. Since JSON must be Unicode, we handle charset
604          * decoding here and do not report a charset to the caller.
605          * For text/html parts, we do not include the content unless
606          * the --include-html option has been passed. If a html part
607          * is not included, it can be requested directly. This makes
608          * charset decoding the responsibility on the caller so we
609          * report the charset for text/html parts.
610          */
611         if (g_mime_content_type_is_type (content_type, "text", "*") &&
612             (include_html ||
613              ! g_mime_content_type_is_type (content_type, "text", "html")))
614         {
615             GMimeStream *stream_memory = g_mime_stream_mem_new ();
616             GByteArray *part_content;
617             show_text_part_content (node->part, stream_memory, 0);
618             part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
619             sp->map_key (sp, "content");
620             sp->string_len (sp, (char *) part_content->data, part_content->len);
621             g_object_unref (stream_memory);
622         } else {
623             format_omitted_part_meta_sprinter (sp, meta, GMIME_PART (node->part));
624         }
625     } else if (GMIME_IS_MULTIPART (node->part)) {
626         sp->map_key (sp, "content");
627         sp->begin_list (sp);
628         nclose = 1;
629     } else if (GMIME_IS_MESSAGE (node->part)) {
630         sp->map_key (sp, "content");
631         sp->begin_list (sp);
632         sp->begin_map (sp);
633
634         sp->map_key (sp, "headers");
635         format_headers_sprinter (sp, GMIME_MESSAGE (node->part), FALSE);
636
637         sp->map_key (sp, "body");
638         sp->begin_list (sp);
639         nclose = 3;
640     }
641
642     for (i = 0; i < node->nchildren; i++)
643         format_part_sprinter (ctx, sp, mime_node_child (node, i), i == 0, TRUE, include_html);
644
645     /* Close content structures */
646     for (i = 0; i < nclose; i++)
647         sp->end (sp);
648     /* Close part map */
649     sp->end (sp);
650 }
651
652 static notmuch_status_t
653 format_part_sprinter_entry (const void *ctx, sprinter_t *sp,
654                             mime_node_t *node, unused (int indent),
655                             const notmuch_show_params_t *params)
656 {
657     format_part_sprinter (ctx, sp, node, TRUE, params->output_body, params->include_html);
658
659     return NOTMUCH_STATUS_SUCCESS;
660 }
661
662 /* Print a message in "mboxrd" format as documented, for example,
663  * here:
664  *
665  * http://qmail.org/qmail-manual-html/man5/mbox.html
666  */
667 static notmuch_status_t
668 format_part_mbox (const void *ctx, unused (sprinter_t *sp), mime_node_t *node,
669                   unused (int indent),
670                   unused (const notmuch_show_params_t *params))
671 {
672     notmuch_message_t *message = node->envelope_file;
673
674     const char *filename;
675     FILE *file;
676     const char *from;
677
678     time_t date;
679     struct tm date_gmtime;
680     char date_asctime[26];
681
682     char *line = NULL;
683     size_t line_size;
684     ssize_t line_len;
685
686     if (!message)
687         INTERNAL_ERROR ("format_part_mbox requires a root part");
688
689     filename = notmuch_message_get_filename (message);
690     file = fopen (filename, "r");
691     if (file == NULL) {
692         fprintf (stderr, "Failed to open %s: %s\n",
693                  filename, strerror (errno));
694         return NOTMUCH_STATUS_FILE_ERROR;
695     }
696
697     from = notmuch_message_get_header (message, "from");
698     from = _extract_email_address (ctx, from);
699
700     date = notmuch_message_get_date (message);
701     gmtime_r (&date, &date_gmtime);
702     asctime_r (&date_gmtime, date_asctime);
703
704     printf ("From %s %s", from, date_asctime);
705
706     while ((line_len = getline (&line, &line_size, file)) != -1 ) {
707         if (_is_from_line (line))
708             putchar ('>');
709         printf ("%s", line);
710     }
711
712     printf ("\n");
713
714     fclose (file);
715
716     return NOTMUCH_STATUS_SUCCESS;
717 }
718
719 static notmuch_status_t
720 format_part_raw (unused (const void *ctx), unused (sprinter_t *sp),
721                  mime_node_t *node, unused (int indent),
722                  unused (const notmuch_show_params_t *params))
723 {
724     if (node->envelope_file) {
725         /* Special case the entire message to avoid MIME parsing. */
726         const char *filename;
727         FILE *file;
728         size_t size;
729         char buf[4096];
730
731         filename = notmuch_message_get_filename (node->envelope_file);
732         if (filename == NULL) {
733             fprintf (stderr, "Error: Cannot get message filename.\n");
734             return NOTMUCH_STATUS_FILE_ERROR;
735         }
736
737         file = fopen (filename, "r");
738         if (file == NULL) {
739             fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
740             return NOTMUCH_STATUS_FILE_ERROR;
741         }
742
743         while (!feof (file)) {
744             size = fread (buf, 1, sizeof (buf), file);
745             if (ferror (file)) {
746                 fprintf (stderr, "Error: Read failed from %s\n", filename);
747                 fclose (file);
748                 return NOTMUCH_STATUS_FILE_ERROR;
749             }
750
751             if (fwrite (buf, size, 1, stdout) != 1) {
752                 fprintf (stderr, "Error: Write failed\n");
753                 fclose (file);
754                 return NOTMUCH_STATUS_FILE_ERROR;
755             }
756         }
757
758         fclose (file);
759         return NOTMUCH_STATUS_SUCCESS;
760     }
761
762     GMimeStream *stream_stdout;
763     GMimeStream *stream_filter = NULL;
764
765     stream_stdout = g_mime_stream_file_new (stdout);
766     g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
767
768     stream_filter = g_mime_stream_filter_new (stream_stdout);
769
770     if (GMIME_IS_PART (node->part)) {
771         /* For leaf parts, we emit only the transfer-decoded
772          * body. */
773         GMimeDataWrapper *wrapper;
774         wrapper = g_mime_part_get_content_object (GMIME_PART (node->part));
775
776         if (wrapper && stream_filter)
777             g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
778     } else {
779         /* Write out the whole part.  For message parts (the root
780          * part and embedded message parts), this will be the
781          * message including its headers (but not the
782          * encapsulating part's headers).  For multipart parts,
783          * this will include the headers. */
784         if (stream_filter)
785             g_mime_object_write_to_stream (node->part, stream_filter);
786     }
787
788     if (stream_filter)
789         g_object_unref (stream_filter);
790
791     if (stream_stdout)
792         g_object_unref(stream_stdout);
793
794     return NOTMUCH_STATUS_SUCCESS;
795 }
796
797 static notmuch_status_t
798 show_message (void *ctx,
799               const notmuch_show_format_t *format,
800               sprinter_t *sp,
801               notmuch_message_t *message,
802               int indent,
803               notmuch_show_params_t *params)
804 {
805     void *local = talloc_new (ctx);
806     mime_node_t *root, *part;
807     notmuch_status_t status;
808
809     status = mime_node_open (local, message, &(params->crypto), &root);
810     if (status)
811         goto DONE;
812     part = mime_node_seek_dfs (root, (params->part < 0 ? 0 : params->part));
813     if (part)
814         status = format->part (local, sp, part, indent, params);
815   DONE:
816     talloc_free (local);
817     return status;
818 }
819
820 static notmuch_status_t
821 show_messages (void *ctx,
822                const notmuch_show_format_t *format,
823                sprinter_t *sp,
824                notmuch_messages_t *messages,
825                int indent,
826                notmuch_show_params_t *params)
827 {
828     notmuch_message_t *message;
829     notmuch_bool_t match;
830     notmuch_bool_t excluded;
831     int next_indent;
832     notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
833
834     sp->begin_list (sp);
835
836     for (;
837          notmuch_messages_valid (messages);
838          notmuch_messages_move_to_next (messages))
839     {
840         sp->begin_list (sp);
841
842         message = notmuch_messages_get (messages);
843
844         match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
845         excluded = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED);
846
847         next_indent = indent;
848
849         if ((match && (!excluded || !params->omit_excluded)) || params->entire_thread) {
850             status = show_message (ctx, format, sp, message, indent, params);
851             if (status && !res)
852                 res = status;
853             next_indent = indent + 1;
854         } else {
855             sp->null (sp);
856         }
857
858         status = show_messages (ctx,
859                                 format, sp,
860                                 notmuch_message_get_replies (message),
861                                 next_indent,
862                                 params);
863         if (status && !res)
864             res = status;
865
866         notmuch_message_destroy (message);
867
868         sp->end (sp);
869     }
870
871     sp->end (sp);
872
873     return res;
874 }
875
876 /* Formatted output of single message */
877 static int
878 do_show_single (void *ctx,
879                 notmuch_query_t *query,
880                 const notmuch_show_format_t *format,
881                 sprinter_t *sp,
882                 notmuch_show_params_t *params)
883 {
884     notmuch_messages_t *messages;
885     notmuch_message_t *message;
886     notmuch_status_t status;
887     unsigned int count;
888
889     status = notmuch_query_count_messages_st (query, &count);
890     if (print_status_query ("notmuch show", query, status))
891         return 1;
892
893     if (count != 1) {
894         fprintf (stderr, "Error: search term did not match precisely one message (matched %d messages).\n", count);
895         return 1;
896     }
897
898     status = notmuch_query_search_messages_st (query, &messages);
899     if (print_status_query ("notmuch show", query, status))
900         return 1;
901
902     message = notmuch_messages_get (messages);
903
904     if (message == NULL) {
905         fprintf (stderr, "Error: Cannot find matching message.\n");
906         return 1;
907     }
908
909     notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
910
911     return show_message (ctx, format, sp, message, 0, params)
912         != NOTMUCH_STATUS_SUCCESS;
913 }
914
915 /* Formatted output of threads */
916 static int
917 do_show (void *ctx,
918          notmuch_query_t *query,
919          const notmuch_show_format_t *format,
920          sprinter_t *sp,
921          notmuch_show_params_t *params)
922 {
923     notmuch_threads_t *threads;
924     notmuch_thread_t *thread;
925     notmuch_messages_t *messages;
926     notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
927
928     status= notmuch_query_search_threads_st (query, &threads);
929     if (print_status_query ("notmuch show", query, status))
930         return 1;
931
932     sp->begin_list (sp);
933
934     for ( ;
935          notmuch_threads_valid (threads);
936          notmuch_threads_move_to_next (threads))
937     {
938         thread = notmuch_threads_get (threads);
939
940         messages = notmuch_thread_get_toplevel_messages (thread);
941
942         if (messages == NULL)
943             INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
944                             notmuch_thread_get_thread_id (thread));
945
946         status = show_messages (ctx, format, sp, messages, 0, params);
947         if (status && !res)
948             res = status;
949
950         notmuch_thread_destroy (thread);
951
952     }
953
954     sp->end (sp);
955
956     return res != NOTMUCH_STATUS_SUCCESS;
957 }
958
959 enum {
960     NOTMUCH_FORMAT_NOT_SPECIFIED,
961     NOTMUCH_FORMAT_JSON,
962     NOTMUCH_FORMAT_SEXP,
963     NOTMUCH_FORMAT_TEXT,
964     NOTMUCH_FORMAT_MBOX,
965     NOTMUCH_FORMAT_RAW
966 };
967
968 static const notmuch_show_format_t format_json = {
969     .new_sprinter = sprinter_json_create,
970     .part = format_part_sprinter_entry,
971 };
972
973 static const notmuch_show_format_t format_sexp = {
974     .new_sprinter = sprinter_sexp_create,
975     .part = format_part_sprinter_entry,
976 };
977
978 static const notmuch_show_format_t format_text = {
979     .new_sprinter = sprinter_text_create,
980     .part = format_part_text,
981 };
982
983 static const notmuch_show_format_t format_mbox = {
984     .new_sprinter = sprinter_text_create,
985     .part = format_part_mbox,
986 };
987
988 static const notmuch_show_format_t format_raw = {
989     .new_sprinter = sprinter_text_create,
990     .part = format_part_raw,
991 };
992
993 static const notmuch_show_format_t *formatters[] = {
994     [NOTMUCH_FORMAT_JSON] = &format_json,
995     [NOTMUCH_FORMAT_SEXP] = &format_sexp,
996     [NOTMUCH_FORMAT_TEXT] = &format_text,
997     [NOTMUCH_FORMAT_MBOX] = &format_mbox,
998     [NOTMUCH_FORMAT_RAW] = &format_raw,
999 };
1000
1001 enum {
1002     ENTIRE_THREAD_DEFAULT = -1,
1003     ENTIRE_THREAD_FALSE = FALSE,
1004     ENTIRE_THREAD_TRUE = TRUE,
1005 };
1006
1007 /* The following is to allow future options to be added more easily */
1008 enum {
1009     EXCLUDE_TRUE,
1010     EXCLUDE_FALSE,
1011 };
1012
1013 int
1014 notmuch_show_command (notmuch_config_t *config, int argc, char *argv[])
1015 {
1016     notmuch_database_t *notmuch;
1017     notmuch_query_t *query;
1018     char *query_string;
1019     int opt_index, ret;
1020     const notmuch_show_format_t *formatter;
1021     sprinter_t *sprinter;
1022     notmuch_show_params_t params = {
1023         .part = -1,
1024         .omit_excluded = TRUE,
1025         .output_body = TRUE,
1026         .crypto = {
1027             .verify = FALSE,
1028             .decrypt = FALSE,
1029             .gpgpath = NULL
1030         },
1031         .include_html = FALSE
1032     };
1033     int format = NOTMUCH_FORMAT_NOT_SPECIFIED;
1034     int exclude = EXCLUDE_TRUE;
1035     int entire_thread = ENTIRE_THREAD_DEFAULT;
1036     notmuch_bool_t single_message;
1037
1038     notmuch_opt_desc_t options[] = {
1039         { NOTMUCH_OPT_KEYWORD, &format, "format", 'f',
1040           (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
1041                                   { "text", NOTMUCH_FORMAT_TEXT },
1042                                   { "sexp", NOTMUCH_FORMAT_SEXP },
1043                                   { "mbox", NOTMUCH_FORMAT_MBOX },
1044                                   { "raw", NOTMUCH_FORMAT_RAW },
1045                                   { 0, 0 } } },
1046         { NOTMUCH_OPT_INT, &notmuch_format_version, "format-version", 0, 0 },
1047         { NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
1048           (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE },
1049                                   { "false", EXCLUDE_FALSE },
1050                                   { 0, 0 } } },
1051         { NOTMUCH_OPT_KEYWORD, &entire_thread, "entire-thread", 't',
1052           (notmuch_keyword_t []){ { "true", ENTIRE_THREAD_TRUE },
1053                                   { "false", ENTIRE_THREAD_FALSE },
1054                                   { "", ENTIRE_THREAD_TRUE },
1055                                   { 0, 0 } } },
1056         { NOTMUCH_OPT_INT, &params.part, "part", 'p', 0 },
1057         { NOTMUCH_OPT_BOOLEAN, &params.crypto.decrypt, "decrypt", 'd', 0 },
1058         { NOTMUCH_OPT_BOOLEAN, &params.crypto.verify, "verify", 'v', 0 },
1059         { NOTMUCH_OPT_BOOLEAN, &params.output_body, "body", 'b', 0 },
1060         { NOTMUCH_OPT_BOOLEAN, &params.include_html, "include-html", 0, 0 },
1061         { NOTMUCH_OPT_INHERIT, (void *) &notmuch_shared_options, NULL, 0, 0 },
1062         { 0, 0, 0, 0, 0 }
1063     };
1064
1065     opt_index = parse_arguments (argc, argv, options, 1);
1066     if (opt_index < 0)
1067         return EXIT_FAILURE;
1068
1069     notmuch_process_shared_options (argv[0]);
1070
1071     /* decryption implies verification */
1072     if (params.crypto.decrypt)
1073         params.crypto.verify = TRUE;
1074
1075     /* specifying a part implies single message display */
1076     single_message = params.part >= 0;
1077
1078     if (format == NOTMUCH_FORMAT_NOT_SPECIFIED) {
1079         /* if part was requested and format was not specified, use format=raw */
1080         if (params.part >= 0)
1081             format = NOTMUCH_FORMAT_RAW;
1082         else
1083             format = NOTMUCH_FORMAT_TEXT;
1084     }
1085
1086     if (format == NOTMUCH_FORMAT_MBOX) {
1087         if (params.part > 0) {
1088             fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
1089             return EXIT_FAILURE;
1090         }
1091     } else if (format == NOTMUCH_FORMAT_RAW) {
1092         /* raw format only supports single message display */
1093         single_message = TRUE;
1094     }
1095
1096     notmuch_exit_if_unsupported_format ();
1097
1098     /* Default is entire-thread = FALSE except for format=json and
1099      * format=sexp. */
1100     if (entire_thread == ENTIRE_THREAD_DEFAULT) {
1101         if (format == NOTMUCH_FORMAT_JSON || format == NOTMUCH_FORMAT_SEXP)
1102             params.entire_thread = TRUE;
1103         else
1104             params.entire_thread = FALSE;
1105     } else {
1106         params.entire_thread = entire_thread;
1107     }
1108
1109     if (!params.output_body) {
1110         if (params.part > 0) {
1111             fprintf (stderr, "Warning: --body=false is incompatible with --part > 0. Disabling.\n");
1112             params.output_body = TRUE;
1113         } else {
1114             if (format != NOTMUCH_FORMAT_JSON && format != NOTMUCH_FORMAT_SEXP)
1115                 fprintf (stderr,
1116                          "Warning: --body=false only implemented for format=json and format=sexp\n");
1117         }
1118     }
1119
1120     if (params.include_html &&
1121         (format != NOTMUCH_FORMAT_JSON && format != NOTMUCH_FORMAT_SEXP)) {
1122         fprintf (stderr, "Warning: --include-html only implemented for format=json and format=sexp\n");
1123     }
1124
1125     query_string = query_string_from_args (config, argc-opt_index, argv+opt_index);
1126     if (query_string == NULL) {
1127         fprintf (stderr, "Out of memory\n");
1128         return EXIT_FAILURE;
1129     }
1130
1131     if (*query_string == '\0') {
1132         fprintf (stderr, "Error: notmuch show requires at least one search term.\n");
1133         return EXIT_FAILURE;
1134     }
1135
1136     params.crypto.gpgpath = notmuch_config_get_crypto_gpg_path (config);
1137
1138     if (notmuch_database_open (notmuch_config_get_database_path (config),
1139                                NOTMUCH_DATABASE_MODE_READ_ONLY, &notmuch))
1140         return EXIT_FAILURE;
1141
1142     notmuch_exit_if_unmatched_db_uuid (notmuch);
1143
1144     query = notmuch_query_create (notmuch, query_string);
1145     if (query == NULL) {
1146         fprintf (stderr, "Out of memory\n");
1147         return EXIT_FAILURE;
1148     }
1149
1150     /* Create structure printer. */
1151     formatter = formatters[format];
1152     sprinter = formatter->new_sprinter(config, stdout);
1153
1154     /* If a single message is requested we do not use search_excludes. */
1155     if (single_message) {
1156         ret = do_show_single (config, query, formatter, sprinter, &params);
1157     } else {
1158         /* We always apply set the exclude flag. The
1159          * exclude=true|false option controls whether or not we return
1160          * threads that only match in an excluded message */
1161         const char **search_exclude_tags;
1162         size_t search_exclude_tags_length;
1163         unsigned int i;
1164
1165         search_exclude_tags = notmuch_config_get_search_exclude_tags
1166             (config, &search_exclude_tags_length);
1167         for (i = 0; i < search_exclude_tags_length; i++)
1168             notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
1169
1170         if (exclude == EXCLUDE_FALSE) {
1171             notmuch_query_set_omit_excluded (query, FALSE);
1172             params.omit_excluded = FALSE;
1173         }
1174
1175         ret = do_show (config, query, formatter, sprinter, &params);
1176     }
1177
1178     notmuch_crypto_cleanup (&params.crypto);
1179     notmuch_query_destroy (query);
1180     notmuch_database_destroy (notmuch);
1181
1182     return ret ? EXIT_FAILURE : EXIT_SUCCESS;
1183 }