test: copy files in test_expect_equal_file instead of moving them
[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 static void
378 show_part_content (GMimeObject *part, GMimeStream *stream_out)
379 {
380     GMimeStream *stream_filter = NULL;
381     GMimeDataWrapper *wrapper;
382     const char *charset;
383
384     /* do nothing if this is a multipart */
385     if (GMIME_IS_MULTIPART (part) || GMIME_IS_MESSAGE_PART (part))
386         return;
387
388     charset = g_mime_object_get_content_type_parameter (part, "charset");
389
390     if (stream_out) {
391         stream_filter = g_mime_stream_filter_new (stream_out);
392         g_mime_stream_filter_add(GMIME_STREAM_FILTER (stream_filter),
393                                  g_mime_filter_crlf_new (FALSE, FALSE));
394         if (charset) {
395             GMimeFilter *charset_filter;
396             charset_filter = g_mime_filter_charset_new (charset, "UTF-8");
397             /* This result can be NULL for things like "unknown-8bit".
398              * Don't set a NULL filter as that makes GMime print
399              * annoying assertion-failure messages on stderr. */
400             if (charset_filter)
401                 g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
402                                           charset_filter);
403         }
404     }
405
406     wrapper = g_mime_part_get_content_object (GMIME_PART (part));
407     if (wrapper && stream_filter)
408         g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
409     if (stream_filter)
410         g_object_unref(stream_filter);
411 }
412
413 static const char*
414 signerstatustostring (GMimeSignerStatus x)
415 {
416     switch (x) {
417     case GMIME_SIGNER_STATUS_NONE:
418         return "none";
419     case GMIME_SIGNER_STATUS_GOOD:
420         return "good";
421     case GMIME_SIGNER_STATUS_BAD:
422         return "bad";
423     case GMIME_SIGNER_STATUS_ERROR:
424         return "error";
425     }
426     return "unknown";
427 }
428
429 static void
430 format_part_start_text (GMimeObject *part, int *part_count)
431 {
432     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
433
434     if (disposition &&
435         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
436     {
437         printf ("\fattachment{ ID: %d", *part_count);
438
439     } else {
440
441         printf ("\fpart{ ID: %d", *part_count);
442     }
443 }
444
445 static void
446 format_part_content_text (GMimeObject *part)
447 {
448     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
449     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
450     GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
451
452     printf (", Content-type: %s\n", g_mime_content_type_to_string (content_type));
453
454     if (disposition &&
455         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
456     {
457         const char *filename = g_mime_part_get_filename (GMIME_PART (part));
458         printf ("Attachment: %s (%s)\n", filename,
459                 g_mime_content_type_to_string (content_type));
460     }
461
462     if (g_mime_content_type_is_type (content_type, "text", "*") &&
463         !g_mime_content_type_is_type (content_type, "text", "html"))
464     {
465         g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
466         show_part_content (part, stream_stdout);
467         g_object_unref(stream_stdout);
468     }
469     else if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
470              g_mime_content_type_is_type (content_type, "message", "rfc822"))
471     {
472         /* Do nothing for multipart since its content will be printed
473          * when recursing. */
474     }
475     else
476     {
477         printf ("Non-text part: %s\n",
478                 g_mime_content_type_to_string (content_type));
479     }
480 }
481
482 static void
483 format_part_end_text (GMimeObject *part)
484 {
485     GMimeContentDisposition *disposition;
486
487     disposition = g_mime_object_get_content_disposition (part);
488     if (disposition &&
489         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
490     {
491         printf ("\fattachment}\n");
492     }
493     else
494     {
495         printf ("\fpart}\n");
496     }
497 }
498
499 static void
500 format_part_start_json (unused (GMimeObject *part), int *part_count)
501 {
502     printf ("{\"id\": %d", *part_count);
503 }
504
505 static void
506 format_part_encstatus_json (int status)
507 {
508     printf (", \"encstatus\": [{\"status\": ");
509     if (status) {
510         printf ("\"good\"");
511     } else {
512         printf ("\"bad\"");
513     }
514     printf ("}]");
515 }
516
517 static void
518 format_part_sigstatus_json (const GMimeSignatureValidity* validity)
519 {
520     printf (", \"sigstatus\": [");
521
522     if (!validity) {
523         printf ("]");
524         return;
525     }
526
527     const GMimeSigner *signer = g_mime_signature_validity_get_signers (validity);
528     int first = 1;
529     void *ctx_quote = talloc_new (NULL);
530
531     while (signer) {
532         if (first)
533             first = 0;
534         else
535             printf (", ");
536
537         printf ("{");
538
539         /* status */
540         printf ("\"status\": %s", json_quote_str (ctx_quote, signerstatustostring(signer->status)));
541
542         if (signer->status == GMIME_SIGNER_STATUS_GOOD)
543         {
544             if (signer->fingerprint)
545                 printf (", \"fingerprint\": %s", json_quote_str (ctx_quote, signer->fingerprint));
546             /* these dates are seconds since the epoch; should we
547              * provide a more human-readable format string? */
548             if (signer->created)
549                 printf (", \"created\": %d", (int) signer->created);
550             if (signer->expires)
551                 printf (", \"expires\": %d", (int) signer->expires);
552             /* output user id only if validity is FULL or ULTIMATE. */
553             /* note that gmime is using the term "trust" here, which
554              * is WRONG.  It's actually user id "validity". */
555             if ((signer->name) && (signer->trust)) {
556                 if ((signer->trust == GMIME_SIGNER_TRUST_FULLY) || (signer->trust == GMIME_SIGNER_TRUST_ULTIMATE))
557                     printf (", \"userid\": %s", json_quote_str (ctx_quote, signer->name));
558            }
559        } else {
560            if (signer->keyid)
561                printf (", \"keyid\": %s", json_quote_str (ctx_quote, signer->keyid));
562        }
563        if (signer->errors != GMIME_SIGNER_ERROR_NONE) {
564            printf (", \"errors\": %x", signer->errors);
565        }
566
567        printf ("}");
568        signer = signer->next;
569     }
570
571     printf ("]");
572
573     talloc_free (ctx_quote);
574 }
575
576 static void
577 format_part_content_json (GMimeObject *part)
578 {
579     GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
580     GMimeStream *stream_memory = g_mime_stream_mem_new ();
581     const char *cid = g_mime_object_get_content_id (part);
582     void *ctx = talloc_new (NULL);
583     GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
584     GByteArray *part_content;
585
586     printf (", \"content-type\": %s",
587             json_quote_str (ctx, g_mime_content_type_to_string (content_type)));
588
589     if (cid != NULL)
590             printf(", \"content-id\": %s", json_quote_str (ctx, cid));
591
592     if (disposition &&
593         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
594     {
595         const char *filename = g_mime_part_get_filename (GMIME_PART (part));
596
597         printf (", \"filename\": %s", json_quote_str (ctx, filename));
598     }
599
600     if (g_mime_content_type_is_type (content_type, "text", "*") &&
601         !g_mime_content_type_is_type (content_type, "text", "html"))
602     {
603         show_part_content (part, stream_memory);
604         part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
605
606         printf (", \"content\": %s", json_quote_chararray (ctx, (char *) part_content->data, part_content->len));
607     }
608     else if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
609              g_mime_content_type_is_type (content_type, "message", "rfc822"))
610     {
611         printf (", \"content\": [");
612     }
613
614     talloc_free (ctx);
615     if (stream_memory)
616         g_object_unref (stream_memory);
617 }
618
619 static void
620 format_part_end_json (GMimeObject *part)
621 {
622     GMimeContentType *content_type;
623
624     content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
625
626     if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
627         g_mime_content_type_is_type (content_type, "message", "rfc822"))
628         printf ("]");
629
630     printf ("}");
631 }
632
633 static void
634 format_part_content_raw (GMimeObject *part)
635 {
636     GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
637     g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
638     show_part_content (part, stream_stdout);
639     g_object_unref(stream_stdout);
640 }
641
642 static void
643 show_message (void *ctx,
644               const notmuch_show_format_t *format,
645               notmuch_message_t *message,
646               int indent,
647               notmuch_show_params_t *params)
648 {
649     if (params->part <= 0) {
650         fputs (format->message_start, stdout);
651         if (format->message)
652             format->message(ctx, message, indent);
653
654         fputs (format->header_start, stdout);
655         if (format->header)
656             format->header(ctx, message);
657         fputs (format->header_end, stdout);
658
659         fputs (format->body_start, stdout);
660     }
661
662     if (format->part_content)
663         show_message_body (notmuch_message_get_filename (message),
664                            format, params);
665
666     if (params->part <= 0) {
667         fputs (format->body_end, stdout);
668
669         fputs (format->message_end, stdout);
670     }
671 }
672
673 static void
674 show_messages (void *ctx,
675                const notmuch_show_format_t *format,
676                notmuch_messages_t *messages,
677                int indent,
678                notmuch_show_params_t *params)
679 {
680     notmuch_message_t *message;
681     notmuch_bool_t match;
682     int first_set = 1;
683     int next_indent;
684
685     fputs (format->message_set_start, stdout);
686
687     for (;
688          notmuch_messages_valid (messages);
689          notmuch_messages_move_to_next (messages))
690     {
691         if (!first_set)
692             fputs (format->message_set_sep, stdout);
693         first_set = 0;
694
695         fputs (format->message_set_start, stdout);
696
697         message = notmuch_messages_get (messages);
698
699         match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
700
701         next_indent = indent;
702
703         if (match || params->entire_thread) {
704             show_message (ctx, format, message, indent, params);
705             next_indent = indent + 1;
706
707             fputs (format->message_set_sep, stdout);
708         }
709
710         show_messages (ctx,
711                        format,
712                        notmuch_message_get_replies (message),
713                        next_indent,
714                        params);
715
716         notmuch_message_destroy (message);
717
718         fputs (format->message_set_end, stdout);
719     }
720
721     fputs (format->message_set_end, stdout);
722 }
723
724 /* Formatted output of single message */
725 static int
726 do_show_single (void *ctx,
727                 notmuch_query_t *query,
728                 const notmuch_show_format_t *format,
729                 notmuch_show_params_t *params)
730 {
731     notmuch_messages_t *messages;
732     notmuch_message_t *message;
733
734     if (notmuch_query_count_messages (query) != 1) {
735         fprintf (stderr, "Error: search term did not match precisely one message.\n");
736         return 1;
737     }
738
739     messages = notmuch_query_search_messages (query);
740     message = notmuch_messages_get (messages);
741
742     if (message == NULL) {
743         fprintf (stderr, "Error: Cannot find matching message.\n");
744         return 1;
745     }
746
747     notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
748
749     /* Special case for --format=raw of full single message, just cat out file */
750     if (params->raw && 0 == params->part) {
751
752         const char *filename;
753         FILE *file;
754         size_t size;
755         char buf[4096];
756
757         filename = notmuch_message_get_filename (message);
758         if (filename == NULL) {
759             fprintf (stderr, "Error: Cannot message filename.\n");
760             return 1;
761         }
762
763         file = fopen (filename, "r");
764         if (file == NULL) {
765             fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
766             return 1;
767         }
768
769         while (!feof (file)) {
770             size = fread (buf, 1, sizeof (buf), file);
771             fwrite (buf, size, 1, stdout);
772         }
773
774         fclose (file);
775
776     } else {
777
778         show_message (ctx, format, message, 0, params);
779
780     }
781
782     return 0;
783 }
784
785 /* Formatted output of threads */
786 static int
787 do_show (void *ctx,
788          notmuch_query_t *query,
789          const notmuch_show_format_t *format,
790          notmuch_show_params_t *params)
791 {
792     notmuch_threads_t *threads;
793     notmuch_thread_t *thread;
794     notmuch_messages_t *messages;
795     int first_toplevel = 1;
796
797     fputs (format->message_set_start, stdout);
798
799     for (threads = notmuch_query_search_threads (query);
800          notmuch_threads_valid (threads);
801          notmuch_threads_move_to_next (threads))
802     {
803         thread = notmuch_threads_get (threads);
804
805         messages = notmuch_thread_get_toplevel_messages (thread);
806
807         if (messages == NULL)
808             INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
809                             notmuch_thread_get_thread_id (thread));
810
811         if (!first_toplevel)
812             fputs (format->message_set_sep, stdout);
813         first_toplevel = 0;
814
815         show_messages (ctx, format, messages, 0, params);
816
817         notmuch_thread_destroy (thread);
818
819     }
820
821     fputs (format->message_set_end, stdout);
822
823     return 0;
824 }
825
826 int
827 notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
828 {
829     notmuch_config_t *config;
830     notmuch_database_t *notmuch;
831     notmuch_query_t *query;
832     char *query_string;
833     char *opt;
834     const notmuch_show_format_t *format = &format_text;
835     notmuch_show_params_t params;
836     int mbox = 0;
837     int format_specified = 0;
838     int i;
839
840     params.entire_thread = 0;
841     params.raw = 0;
842     params.part = -1;
843     params.cryptoctx = NULL;
844     params.decrypt = 0;
845
846     for (i = 0; i < argc && argv[i][0] == '-'; i++) {
847         if (strcmp (argv[i], "--") == 0) {
848             i++;
849             break;
850         }
851         if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
852             opt = argv[i] + sizeof ("--format=") - 1;
853             if (strcmp (opt, "text") == 0) {
854                 format = &format_text;
855             } else if (strcmp (opt, "json") == 0) {
856                 format = &format_json;
857                 params.entire_thread = 1;
858             } else if (strcmp (opt, "mbox") == 0) {
859                 format = &format_mbox;
860                 mbox = 1;
861             } else if (strcmp (opt, "raw") == 0) {
862                 format = &format_raw;
863                 params.raw = 1;
864             } else {
865                 fprintf (stderr, "Invalid value for --format: %s\n", opt);
866                 return 1;
867             }
868             format_specified = 1;
869         } else if (STRNCMP_LITERAL (argv[i], "--part=") == 0) {
870             params.part = atoi(argv[i] + sizeof ("--part=") - 1);
871         } else if (STRNCMP_LITERAL (argv[i], "--entire-thread") == 0) {
872             params.entire_thread = 1;
873         } else if ((STRNCMP_LITERAL (argv[i], "--verify") == 0) ||
874                    (STRNCMP_LITERAL (argv[i], "--decrypt") == 0)) {
875             if (params.cryptoctx == NULL) {
876                 GMimeSession* session = g_object_new(notmuch_gmime_session_get_type(), NULL);
877                 if (NULL == (params.cryptoctx = g_mime_gpg_context_new(session, "gpg")))
878                     fprintf (stderr, "Failed to construct gpg context.\n");
879                 else
880                     g_mime_gpg_context_set_always_trust((GMimeGpgContext*)params.cryptoctx, FALSE);
881                 g_object_unref (session);
882                 session = NULL;
883             }
884             if (STRNCMP_LITERAL (argv[i], "--decrypt") == 0)
885                 params.decrypt = 1;
886         } else {
887             fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
888             return 1;
889         }
890     }
891
892     argc -= i;
893     argv += i;
894
895     config = notmuch_config_open (ctx, NULL, NULL);
896     if (config == NULL)
897         return 1;
898
899     query_string = query_string_from_args (ctx, argc, argv);
900     if (query_string == NULL) {
901         fprintf (stderr, "Out of memory\n");
902         return 1;
903     }
904
905     if (mbox && params.part > 0) {
906         fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
907         return 1;
908     }
909
910     if (*query_string == '\0') {
911         fprintf (stderr, "Error: notmuch show requires at least one search term.\n");
912         return 1;
913     }
914
915     notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
916                                      NOTMUCH_DATABASE_MODE_READ_ONLY);
917     if (notmuch == NULL)
918         return 1;
919
920     query = notmuch_query_create (notmuch, query_string);
921     if (query == NULL) {
922         fprintf (stderr, "Out of memory\n");
923         return 1;
924     }
925
926     /* if part was requested and format was not specified, use format=raw */
927     if (params.part >= 0 && !format_specified)
928         format = &format_raw;
929
930     /* If --format=raw specified without specifying part, we can only
931      * output single message, so set part=0 */
932     if (params.raw && params.part < 0)
933         params.part = 0;
934
935     if (params.part >= 0)
936         return do_show_single (ctx, query, format, &params);
937     else
938         return do_show (ctx, query, format, &params);
939
940     notmuch_query_destroy (query);
941     notmuch_database_close (notmuch);
942
943     if (params.cryptoctx)
944         g_object_unref(params.cryptoctx);
945
946     return 0;
947 }