]> git.notmuchmail.org Git - notmuch/blob - notmuch-show.c
1ed49d995efad1fac8dd1c5e9b37532f18734b32
[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 http://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "notmuch-client.h"
22
23 static void
24 format_message_text (unused (const void *ctx),
25                      notmuch_message_t *message,
26                      int indent);
27 static void
28 format_headers_text (const void *ctx,
29                      notmuch_message_t *message);
30
31 static void
32 format_part_start_text (GMimeObject *part,
33                         int *part_count);
34
35 static void
36 format_part_content_text (GMimeObject *part);
37
38 static void
39 format_part_end_text (GMimeObject *part);
40
41 static const notmuch_show_format_t format_text = {
42     "",
43         "\fmessage{ ", format_message_text,
44             "\fheader{\n", format_headers_text, "\fheader}\n",
45             "\fbody{\n",
46                 format_part_start_text,
47                 NULL,
48                 NULL,
49                 format_part_content_text,
50                 format_part_end_text,
51                 "",
52             "\fbody}\n",
53         "\fmessage}\n", "",
54     ""
55 };
56
57 static void
58 format_message_json (const void *ctx,
59                      notmuch_message_t *message,
60                      unused (int indent));
61 static void
62 format_headers_json (const void *ctx,
63                      notmuch_message_t *message);
64
65 static void
66 format_part_start_json (unused (GMimeObject *part),
67                         int *part_count);
68
69 static void
70 format_part_encstatus_json (int status);
71
72 static void
73 format_part_sigstatus_json (const GMimeSignatureValidity* validity);
74
75 static void
76 format_part_content_json (GMimeObject *part);
77
78 static void
79 format_part_end_json (GMimeObject *part);
80
81 static const notmuch_show_format_t format_json = {
82     "[",
83         "{", format_message_json,
84             ", \"headers\": {", format_headers_json, "}",
85             ", \"body\": [",
86                 format_part_start_json,
87                 format_part_encstatus_json,
88                 format_part_sigstatus_json,
89                 format_part_content_json,
90                 format_part_end_json,
91                 ", ",
92             "]",
93         "}", ", ",
94     "]"
95 };
96
97 static void
98 format_message_mbox (const void *ctx,
99                      notmuch_message_t *message,
100                      unused (int indent));
101
102 static const notmuch_show_format_t format_mbox = {
103     "",
104         "", format_message_mbox,
105             "", NULL, "",
106             "",
107                 NULL,
108                 NULL,
109                 NULL,
110                 NULL,
111                 NULL,
112                 "",
113             "",
114         "", "",
115     ""
116 };
117
118 static void
119 format_part_content_raw (GMimeObject *part);
120
121 static const notmuch_show_format_t format_raw = {
122     "",
123         "", NULL,
124             "", NULL, "",
125             "",
126                 NULL,
127                 NULL,
128                 NULL,
129                 format_part_content_raw,
130                 NULL,
131                 "",
132             "",
133         "", "",
134     ""
135 };
136
137 static const char *
138 _get_tags_as_string (const void *ctx, notmuch_message_t *message)
139 {
140     notmuch_tags_t *tags;
141     int first = 1;
142     const char *tag;
143     char *result;
144
145     result = talloc_strdup (ctx, "");
146     if (result == NULL)
147         return NULL;
148
149     for (tags = notmuch_message_get_tags (message);
150          notmuch_tags_valid (tags);
151          notmuch_tags_move_to_next (tags))
152     {
153         tag = notmuch_tags_get (tags);
154
155         result = talloc_asprintf_append (result, "%s%s",
156                                          first ? "" : " ", tag);
157         first = 0;
158     }
159
160     return result;
161 }
162
163 /* Get a nice, single-line summary of message. */
164 static const char *
165 _get_one_line_summary (const void *ctx, notmuch_message_t *message)
166 {
167     const char *from;
168     time_t date;
169     const char *relative_date;
170     const char *tags;
171
172     from = notmuch_message_get_header (message, "from");
173
174     date = notmuch_message_get_date (message);
175     relative_date = notmuch_time_relative_date (ctx, date);
176
177     tags = _get_tags_as_string (ctx, message);
178
179     return talloc_asprintf (ctx, "%s (%s) (%s)",
180                             from, relative_date, tags);
181 }
182
183 static void
184 format_message_text (unused (const void *ctx), notmuch_message_t *message, int indent)
185 {
186     printf ("id:%s depth:%d match:%d filename:%s\n",
187             notmuch_message_get_message_id (message),
188             indent,
189             notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),
190             notmuch_message_get_filename (message));
191 }
192
193 static void
194 format_message_json (const void *ctx, notmuch_message_t *message, unused (int indent))
195 {
196     notmuch_tags_t *tags;
197     int first = 1;
198     void *ctx_quote = talloc_new (ctx);
199     time_t date;
200     const char *relative_date;
201
202     date = notmuch_message_get_date (message);
203     relative_date = notmuch_time_relative_date (ctx, date);
204
205     printf ("\"id\": %s, \"match\": %s, \"filename\": %s, \"timestamp\": %ld, \"date_relative\": \"%s\", \"tags\": [",
206             json_quote_str (ctx_quote, notmuch_message_get_message_id (message)),
207             notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? "true" : "false",
208             json_quote_str (ctx_quote, notmuch_message_get_filename (message)),
209             date, relative_date);
210
211     for (tags = notmuch_message_get_tags (message);
212          notmuch_tags_valid (tags);
213          notmuch_tags_move_to_next (tags))
214     {
215          printf("%s%s", first ? "" : ",",
216                json_quote_str (ctx_quote, notmuch_tags_get (tags)));
217          first = 0;
218     }
219     printf("]");
220     talloc_free (ctx_quote);
221 }
222
223 /* Extract just the email address from the contents of a From:
224  * header. */
225 static const char *
226 _extract_email_address (const void *ctx, const char *from)
227 {
228     InternetAddressList *addresses;
229     InternetAddress *address;
230     InternetAddressMailbox *mailbox;
231     const char *email = "MAILER-DAEMON";
232
233     addresses = internet_address_list_parse_string (from);
234
235     /* Bail if there is no address here. */
236     if (addresses == NULL || internet_address_list_length (addresses) < 1)
237         goto DONE;
238
239     /* Otherwise, just use the first address. */
240     address = internet_address_list_get_address (addresses, 0);
241
242     /* The From header should never contain an address group rather
243      * than a mailbox. So bail if it does. */
244     if (! INTERNET_ADDRESS_IS_MAILBOX (address))
245         goto DONE;
246
247     mailbox = INTERNET_ADDRESS_MAILBOX (address);
248     email = internet_address_mailbox_get_addr (mailbox);
249     email = talloc_strdup (ctx, email);
250
251   DONE:
252     /* XXX: How to free addresses here? */
253     return email;
254    }
255
256 /* Return 1 if 'line' is an mbox From_ line---that is, a line
257  * beginning with zero or more '>' characters followed by the
258  * characters 'F', 'r', 'o', 'm', and space.
259  *
260  * Any characters at all may appear after that in the line.
261  */
262 static int
263 _is_from_line (const char *line)
264 {
265     const char *s = line;
266
267     if (line == NULL)
268         return 0;
269
270     while (*s == '>')
271         s++;
272
273     if (STRNCMP_LITERAL (s, "From ") == 0)
274         return 1;
275     else
276         return 0;
277 }
278
279 /* Print a message in "mboxrd" format as documented, for example,
280  * here:
281  *
282  * http://qmail.org/qmail-manual-html/man5/mbox.html
283  */
284 static void
285 format_message_mbox (const void *ctx,
286                      notmuch_message_t *message,
287                      unused (int indent))
288 {
289     const char *filename;
290     FILE *file;
291     const char *from;
292
293     time_t date;
294     struct tm date_gmtime;
295     char date_asctime[26];
296
297     char *line = NULL;
298     size_t line_size;
299     ssize_t line_len;
300
301     filename = notmuch_message_get_filename (message);
302     file = fopen (filename, "r");
303     if (file == NULL) {
304         fprintf (stderr, "Failed to open %s: %s\n",
305                  filename, strerror (errno));
306         return;
307     }
308
309     from = notmuch_message_get_header (message, "from");
310     from = _extract_email_address (ctx, from);
311
312     date = notmuch_message_get_date (message);
313     gmtime_r (&date, &date_gmtime);
314     asctime_r (&date_gmtime, date_asctime);
315
316     printf ("From %s %s", from, date_asctime);
317
318     while ((line_len = getline (&line, &line_size, file)) != -1 ) {
319         if (_is_from_line (line))
320             putchar ('>');
321         printf ("%s", line);
322     }
323
324     printf ("\n");
325
326     fclose (file);
327 }
328
329 static void
330 format_headers_text (const void *ctx, notmuch_message_t *message)
331 {
332     const char *headers[] = {
333         "Subject", "From", "To", "Cc", "Bcc", "Date"
334     };
335     const char *name, *value;
336     unsigned int i;
337
338     printf ("%s\n", _get_one_line_summary (ctx, message));
339
340     for (i = 0; i < ARRAY_SIZE (headers); i++) {
341         name = headers[i];
342         value = notmuch_message_get_header (message, name);
343         if (value && strlen (value))
344             printf ("%s: %s\n", name, value);
345     }
346 }
347
348 static void
349 format_headers_json (const void *ctx, notmuch_message_t *message)
350 {
351     const char *headers[] = {
352         "Subject", "From", "To", "Cc", "Bcc", "Date"
353     };
354     const char *name, *value;
355     unsigned int i;
356     int first_header = 1;
357     void *ctx_quote = talloc_new (ctx);
358
359     for (i = 0; i < ARRAY_SIZE (headers); i++) {
360         name = headers[i];
361         value = notmuch_message_get_header (message, name);
362         if (value)
363         {
364             if (!first_header)
365                 fputs (", ", stdout);
366             first_header = 0;
367
368             printf ("%s: %s",
369                     json_quote_str (ctx_quote, name),
370                     json_quote_str (ctx_quote, value));
371         }
372     }
373
374     talloc_free (ctx_quote);
375 }
376
377 /* Write a MIME text part out to the given stream.
378  *
379  * Both line-ending conversion (CRLF->LF) and charset conversion ( ->
380  * UTF-8) will be performed, so it is inappropriate to call this
381  * function with a non-text part. Doing so will trigger an internal
382  * error.
383  */
384 static void
385 show_text_part_content (GMimeObject *part, GMimeStream *stream_out)
386 {
387     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
388     GMimeStream *stream_filter = NULL;
389     GMimeDataWrapper *wrapper;
390     const char *charset;
391
392     if (! g_mime_content_type_is_type (content_type, "text", "*"))
393         INTERNAL_ERROR ("Illegal request to format non-text part (%s) as text.",
394                         g_mime_content_type_to_string (content_type));
395
396     charset = g_mime_object_get_content_type_parameter (part, "charset");
397
398     if (stream_out) {
399         stream_filter = g_mime_stream_filter_new (stream_out);
400         g_mime_stream_filter_add(GMIME_STREAM_FILTER (stream_filter),
401                                  g_mime_filter_crlf_new (FALSE, FALSE));
402         if (charset) {
403             GMimeFilter *charset_filter;
404             charset_filter = g_mime_filter_charset_new (charset, "UTF-8");
405             /* This result can be NULL for things like "unknown-8bit".
406              * Don't set a NULL filter as that makes GMime print
407              * annoying assertion-failure messages on stderr. */
408             if (charset_filter)
409                 g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
410                                           charset_filter);
411         }
412     }
413
414     wrapper = g_mime_part_get_content_object (GMIME_PART (part));
415     if (wrapper && stream_filter)
416         g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
417     if (stream_filter)
418         g_object_unref(stream_filter);
419 }
420
421 static const char*
422 signerstatustostring (GMimeSignerStatus x)
423 {
424     switch (x) {
425     case GMIME_SIGNER_STATUS_NONE:
426         return "none";
427     case GMIME_SIGNER_STATUS_GOOD:
428         return "good";
429     case GMIME_SIGNER_STATUS_BAD:
430         return "bad";
431     case GMIME_SIGNER_STATUS_ERROR:
432         return "error";
433     }
434     return "unknown";
435 }
436
437 static void
438 format_part_start_text (GMimeObject *part, int *part_count)
439 {
440     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
441
442     if (disposition &&
443         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
444     {
445         printf ("\fattachment{ ID: %d", *part_count);
446
447     } else {
448
449         printf ("\fpart{ ID: %d", *part_count);
450     }
451 }
452
453 static void
454 format_part_content_text (GMimeObject *part)
455 {
456     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
457     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
458     GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
459
460     printf (", Content-type: %s\n", g_mime_content_type_to_string (content_type));
461
462     if (disposition &&
463         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
464     {
465         const char *filename = g_mime_part_get_filename (GMIME_PART (part));
466         printf ("Attachment: %s (%s)\n", filename,
467                 g_mime_content_type_to_string (content_type));
468     }
469
470     if (g_mime_content_type_is_type (content_type, "text", "*") &&
471         !g_mime_content_type_is_type (content_type, "text", "html"))
472     {
473         g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
474         show_text_part_content (part, stream_stdout);
475         g_object_unref(stream_stdout);
476     }
477     else if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
478              g_mime_content_type_is_type (content_type, "message", "rfc822"))
479     {
480         /* Do nothing for multipart since its content will be printed
481          * when recursing. */
482     }
483     else
484     {
485         printf ("Non-text part: %s\n",
486                 g_mime_content_type_to_string (content_type));
487     }
488 }
489
490 static void
491 format_part_end_text (GMimeObject *part)
492 {
493     GMimeContentDisposition *disposition;
494
495     disposition = g_mime_object_get_content_disposition (part);
496     if (disposition &&
497         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
498     {
499         printf ("\fattachment}\n");
500     }
501     else
502     {
503         printf ("\fpart}\n");
504     }
505 }
506
507 static void
508 format_part_start_json (unused (GMimeObject *part), int *part_count)
509 {
510     printf ("{\"id\": %d", *part_count);
511 }
512
513 static void
514 format_part_encstatus_json (int status)
515 {
516     printf (", \"encstatus\": [{\"status\": ");
517     if (status) {
518         printf ("\"good\"");
519     } else {
520         printf ("\"bad\"");
521     }
522     printf ("}]");
523 }
524
525 static void
526 format_part_sigstatus_json (const GMimeSignatureValidity* validity)
527 {
528     printf (", \"sigstatus\": [");
529
530     if (!validity) {
531         printf ("]");
532         return;
533     }
534
535     const GMimeSigner *signer = g_mime_signature_validity_get_signers (validity);
536     int first = 1;
537     void *ctx_quote = talloc_new (NULL);
538
539     while (signer) {
540         if (first)
541             first = 0;
542         else
543             printf (", ");
544
545         printf ("{");
546
547         /* status */
548         printf ("\"status\": %s", json_quote_str (ctx_quote, signerstatustostring(signer->status)));
549
550         if (signer->status == GMIME_SIGNER_STATUS_GOOD)
551         {
552             if (signer->fingerprint)
553                 printf (", \"fingerprint\": %s", json_quote_str (ctx_quote, signer->fingerprint));
554             /* these dates are seconds since the epoch; should we
555              * provide a more human-readable format string? */
556             if (signer->created)
557                 printf (", \"created\": %d", (int) signer->created);
558             if (signer->expires)
559                 printf (", \"expires\": %d", (int) signer->expires);
560             /* output user id only if validity is FULL or ULTIMATE. */
561             /* note that gmime is using the term "trust" here, which
562              * is WRONG.  It's actually user id "validity". */
563             if ((signer->name) && (signer->trust)) {
564                 if ((signer->trust == GMIME_SIGNER_TRUST_FULLY) || (signer->trust == GMIME_SIGNER_TRUST_ULTIMATE))
565                     printf (", \"userid\": %s", json_quote_str (ctx_quote, signer->name));
566            }
567        } else {
568            if (signer->keyid)
569                printf (", \"keyid\": %s", json_quote_str (ctx_quote, signer->keyid));
570        }
571        if (signer->errors != GMIME_SIGNER_ERROR_NONE) {
572            printf (", \"errors\": %x", signer->errors);
573        }
574
575        printf ("}");
576        signer = signer->next;
577     }
578
579     printf ("]");
580
581     talloc_free (ctx_quote);
582 }
583
584 static void
585 format_part_content_json (GMimeObject *part)
586 {
587     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
588     GMimeStream *stream_memory = g_mime_stream_mem_new ();
589     const char *cid = g_mime_object_get_content_id (part);
590     void *ctx = talloc_new (NULL);
591     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
592     GByteArray *part_content;
593
594     printf (", \"content-type\": %s",
595             json_quote_str (ctx, g_mime_content_type_to_string (content_type)));
596
597     if (cid != NULL)
598             printf(", \"content-id\": %s", json_quote_str (ctx, cid));
599
600     if (disposition &&
601         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
602     {
603         const char *filename = g_mime_part_get_filename (GMIME_PART (part));
604
605         printf (", \"filename\": %s", json_quote_str (ctx, filename));
606     }
607
608     if (g_mime_content_type_is_type (content_type, "text", "*") &&
609         !g_mime_content_type_is_type (content_type, "text", "html"))
610     {
611         show_text_part_content (part, stream_memory);
612         part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
613
614         printf (", \"content\": %s", json_quote_chararray (ctx, (char *) part_content->data, part_content->len));
615     }
616     else if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
617              g_mime_content_type_is_type (content_type, "message", "rfc822"))
618     {
619         printf (", \"content\": [");
620     }
621
622     talloc_free (ctx);
623     if (stream_memory)
624         g_object_unref (stream_memory);
625 }
626
627 static void
628 format_part_end_json (GMimeObject *part)
629 {
630     GMimeContentType *content_type;
631
632     content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
633
634     if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
635         g_mime_content_type_is_type (content_type, "message", "rfc822"))
636         printf ("]");
637
638     printf ("}");
639 }
640
641 static void
642 format_part_content_raw (GMimeObject *part)
643 {
644     GMimeStream *stream_stdout;
645     GMimeStream *stream_filter = NULL;
646     GMimeDataWrapper *wrapper;
647
648     stream_stdout = g_mime_stream_file_new (stdout);
649     g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
650
651     stream_filter = g_mime_stream_filter_new (stream_stdout);
652
653     wrapper = g_mime_part_get_content_object (GMIME_PART (part));
654
655     if (wrapper && stream_filter)
656         g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
657
658     if (stream_filter)
659         g_object_unref (stream_filter);
660
661     if (stream_stdout)
662         g_object_unref(stream_stdout);
663 }
664
665 static void
666 show_message (void *ctx,
667               const notmuch_show_format_t *format,
668               notmuch_message_t *message,
669               int indent,
670               notmuch_show_params_t *params)
671 {
672     if (params->part <= 0) {
673         fputs (format->message_start, stdout);
674         if (format->message)
675             format->message(ctx, message, indent);
676
677         fputs (format->header_start, stdout);
678         if (format->header)
679             format->header(ctx, message);
680         fputs (format->header_end, stdout);
681
682         fputs (format->body_start, stdout);
683     }
684
685     if (format->part_content)
686         show_message_body (notmuch_message_get_filename (message),
687                            format, params);
688
689     if (params->part <= 0) {
690         fputs (format->body_end, stdout);
691
692         fputs (format->message_end, stdout);
693     }
694 }
695
696 static void
697 show_messages (void *ctx,
698                const notmuch_show_format_t *format,
699                notmuch_messages_t *messages,
700                int indent,
701                notmuch_show_params_t *params)
702 {
703     notmuch_message_t *message;
704     notmuch_bool_t match;
705     int first_set = 1;
706     int next_indent;
707
708     fputs (format->message_set_start, stdout);
709
710     for (;
711          notmuch_messages_valid (messages);
712          notmuch_messages_move_to_next (messages))
713     {
714         if (!first_set)
715             fputs (format->message_set_sep, stdout);
716         first_set = 0;
717
718         fputs (format->message_set_start, stdout);
719
720         message = notmuch_messages_get (messages);
721
722         match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
723
724         next_indent = indent;
725
726         if (match || params->entire_thread) {
727             show_message (ctx, format, message, indent, params);
728             next_indent = indent + 1;
729
730             fputs (format->message_set_sep, stdout);
731         }
732
733         show_messages (ctx,
734                        format,
735                        notmuch_message_get_replies (message),
736                        next_indent,
737                        params);
738
739         notmuch_message_destroy (message);
740
741         fputs (format->message_set_end, stdout);
742     }
743
744     fputs (format->message_set_end, stdout);
745 }
746
747 /* Formatted output of single message */
748 static int
749 do_show_single (void *ctx,
750                 notmuch_query_t *query,
751                 const notmuch_show_format_t *format,
752                 notmuch_show_params_t *params)
753 {
754     notmuch_messages_t *messages;
755     notmuch_message_t *message;
756
757     if (notmuch_query_count_messages (query) != 1) {
758         fprintf (stderr, "Error: search term did not match precisely one message.\n");
759         return 1;
760     }
761
762     messages = notmuch_query_search_messages (query);
763     message = notmuch_messages_get (messages);
764
765     if (message == NULL) {
766         fprintf (stderr, "Error: Cannot find matching message.\n");
767         return 1;
768     }
769
770     notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
771
772     /* Special case for --format=raw of full single message, just cat out file */
773     if (params->raw && 0 == params->part) {
774
775         const char *filename;
776         FILE *file;
777         size_t size;
778         char buf[4096];
779
780         filename = notmuch_message_get_filename (message);
781         if (filename == NULL) {
782             fprintf (stderr, "Error: Cannot message filename.\n");
783             return 1;
784         }
785
786         file = fopen (filename, "r");
787         if (file == NULL) {
788             fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
789             return 1;
790         }
791
792         while (!feof (file)) {
793             size = fread (buf, 1, sizeof (buf), file);
794             fwrite (buf, size, 1, stdout);
795         }
796
797         fclose (file);
798
799     } else {
800
801         show_message (ctx, format, message, 0, params);
802
803     }
804
805     return 0;
806 }
807
808 /* Formatted output of threads */
809 static int
810 do_show (void *ctx,
811          notmuch_query_t *query,
812          const notmuch_show_format_t *format,
813          notmuch_show_params_t *params)
814 {
815     notmuch_threads_t *threads;
816     notmuch_thread_t *thread;
817     notmuch_messages_t *messages;
818     int first_toplevel = 1;
819
820     fputs (format->message_set_start, stdout);
821
822     for (threads = notmuch_query_search_threads (query);
823          notmuch_threads_valid (threads);
824          notmuch_threads_move_to_next (threads))
825     {
826         thread = notmuch_threads_get (threads);
827
828         messages = notmuch_thread_get_toplevel_messages (thread);
829
830         if (messages == NULL)
831             INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
832                             notmuch_thread_get_thread_id (thread));
833
834         if (!first_toplevel)
835             fputs (format->message_set_sep, stdout);
836         first_toplevel = 0;
837
838         show_messages (ctx, format, messages, 0, params);
839
840         notmuch_thread_destroy (thread);
841
842     }
843
844     fputs (format->message_set_end, stdout);
845
846     return 0;
847 }
848
849 int
850 notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
851 {
852     notmuch_config_t *config;
853     notmuch_database_t *notmuch;
854     notmuch_query_t *query;
855     char *query_string;
856     char *opt;
857     const notmuch_show_format_t *format = &format_text;
858     notmuch_show_params_t params;
859     int mbox = 0;
860     int format_specified = 0;
861     int i;
862
863     params.entire_thread = 0;
864     params.raw = 0;
865     params.part = -1;
866     params.cryptoctx = NULL;
867     params.decrypt = 0;
868
869     for (i = 0; i < argc && argv[i][0] == '-'; i++) {
870         if (strcmp (argv[i], "--") == 0) {
871             i++;
872             break;
873         }
874         if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
875             opt = argv[i] + sizeof ("--format=") - 1;
876             if (strcmp (opt, "text") == 0) {
877                 format = &format_text;
878             } else if (strcmp (opt, "json") == 0) {
879                 format = &format_json;
880                 params.entire_thread = 1;
881             } else if (strcmp (opt, "mbox") == 0) {
882                 format = &format_mbox;
883                 mbox = 1;
884             } else if (strcmp (opt, "raw") == 0) {
885                 format = &format_raw;
886                 params.raw = 1;
887             } else {
888                 fprintf (stderr, "Invalid value for --format: %s\n", opt);
889                 return 1;
890             }
891             format_specified = 1;
892         } else if (STRNCMP_LITERAL (argv[i], "--part=") == 0) {
893             params.part = atoi(argv[i] + sizeof ("--part=") - 1);
894         } else if (STRNCMP_LITERAL (argv[i], "--entire-thread") == 0) {
895             params.entire_thread = 1;
896         } else if ((STRNCMP_LITERAL (argv[i], "--verify") == 0) ||
897                    (STRNCMP_LITERAL (argv[i], "--decrypt") == 0)) {
898             if (params.cryptoctx == NULL) {
899                 GMimeSession* session = g_object_new(notmuch_gmime_session_get_type(), NULL);
900                 if (NULL == (params.cryptoctx = g_mime_gpg_context_new(session, "gpg")))
901                     fprintf (stderr, "Failed to construct gpg context.\n");
902                 else
903                     g_mime_gpg_context_set_always_trust((GMimeGpgContext*)params.cryptoctx, FALSE);
904                 g_object_unref (session);
905                 session = NULL;
906             }
907             if (STRNCMP_LITERAL (argv[i], "--decrypt") == 0)
908                 params.decrypt = 1;
909         } else {
910             fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
911             return 1;
912         }
913     }
914
915     argc -= i;
916     argv += i;
917
918     config = notmuch_config_open (ctx, NULL, NULL);
919     if (config == NULL)
920         return 1;
921
922     query_string = query_string_from_args (ctx, argc, argv);
923     if (query_string == NULL) {
924         fprintf (stderr, "Out of memory\n");
925         return 1;
926     }
927
928     if (mbox && params.part > 0) {
929         fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
930         return 1;
931     }
932
933     if (*query_string == '\0') {
934         fprintf (stderr, "Error: notmuch show requires at least one search term.\n");
935         return 1;
936     }
937
938     notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
939                                      NOTMUCH_DATABASE_MODE_READ_ONLY);
940     if (notmuch == NULL)
941         return 1;
942
943     query = notmuch_query_create (notmuch, query_string);
944     if (query == NULL) {
945         fprintf (stderr, "Out of memory\n");
946         return 1;
947     }
948
949     /* if part was requested and format was not specified, use format=raw */
950     if (params.part >= 0 && !format_specified)
951         format = &format_raw;
952
953     /* If --format=raw specified without specifying part, we can only
954      * output single message, so set part=0 */
955     if (params.raw && params.part < 0)
956         params.part = 0;
957
958     if (params.part >= 0)
959         return do_show_single (ctx, query, format, &params);
960     else
961         return do_show (ctx, query, format, &params);
962
963     notmuch_query_destroy (query);
964     notmuch_database_close (notmuch);
965
966     if (params.cryptoctx)
967         g_object_unref(params.cryptoctx);
968
969     return 0;
970 }