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