test: Test upgrade to ghost messages feature
[notmuch] / notmuch-reply.c
1 /* notmuch - Not much of an email program, (just index and search)
2  *
3  * Copyright © 2009 Carl Worth
4  * Copyright © 2009 Keith Packard
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see http://www.gnu.org/licenses/ .
18  *
19  * Authors: Carl Worth <cworth@cworth.org>
20  *          Keith Packard <keithp@keithp.com>
21  */
22
23 #include "notmuch-client.h"
24 #include "string-util.h"
25 #include "sprinter.h"
26
27 static void
28 show_reply_headers (GMimeMessage *message)
29 {
30     GMimeStream *stream_stdout = NULL;
31
32     stream_stdout = g_mime_stream_file_new (stdout);
33     if (stream_stdout) {
34         g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
35         /* Output RFC 2822 formatted (and RFC 2047 encoded) headers. */
36         g_mime_object_write_to_stream (GMIME_OBJECT(message), stream_stdout);
37         g_object_unref(stream_stdout);
38     }
39 }
40
41 static void
42 format_part_reply (mime_node_t *node)
43 {
44     int i;
45
46     if (node->envelope_file) {
47         printf ("On %s, %s wrote:\n",
48                 notmuch_message_get_header (node->envelope_file, "date"),
49                 notmuch_message_get_header (node->envelope_file, "from"));
50     } else if (GMIME_IS_MESSAGE (node->part)) {
51         GMimeMessage *message = GMIME_MESSAGE (node->part);
52         InternetAddressList *recipients;
53         const char *recipients_string;
54
55         printf ("> From: %s\n", g_mime_message_get_sender (message));
56         recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO);
57         recipients_string = internet_address_list_to_string (recipients, 0);
58         if (recipients_string)
59             printf ("> To: %s\n",
60                     recipients_string);
61         recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_CC);
62         recipients_string = internet_address_list_to_string (recipients, 0);
63         if (recipients_string)
64             printf ("> Cc: %s\n",
65                     recipients_string);
66         printf ("> Subject: %s\n", g_mime_message_get_subject (message));
67         printf ("> Date: %s\n", g_mime_message_get_date_as_string (message));
68         printf (">\n");
69     } else if (GMIME_IS_PART (node->part)) {
70         GMimeContentType *content_type = g_mime_object_get_content_type (node->part);
71         GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (node->part);
72
73         if (g_mime_content_type_is_type (content_type, "application", "pgp-encrypted") ||
74             g_mime_content_type_is_type (content_type, "application", "pgp-signature")) {
75             /* Ignore PGP/MIME cruft parts */
76         } else if (g_mime_content_type_is_type (content_type, "text", "*") &&
77                    !g_mime_content_type_is_type (content_type, "text", "html")) {
78             GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
79             g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
80             show_text_part_content (node->part, stream_stdout, NOTMUCH_SHOW_TEXT_PART_REPLY);
81             g_object_unref(stream_stdout);
82         } else if (disposition &&
83                    strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0) {
84             const char *filename = g_mime_part_get_filename (GMIME_PART (node->part));
85             printf ("Attachment: %s (%s)\n", filename,
86                     g_mime_content_type_to_string (content_type));
87         } else {
88             printf ("Non-text part: %s\n",
89                     g_mime_content_type_to_string (content_type));
90         }
91     }
92
93     for (i = 0; i < node->nchildren; i++)
94         format_part_reply (mime_node_child (node, i));
95 }
96
97 typedef enum {
98     USER_ADDRESS_IN_STRING,
99     STRING_IN_USER_ADDRESS,
100     STRING_IS_USER_ADDRESS,
101 } address_match_t;
102
103 /* Match given string against given address according to mode. */
104 static notmuch_bool_t
105 match_address (const char *str, const char *address, address_match_t mode)
106 {
107     switch (mode) {
108     case USER_ADDRESS_IN_STRING:
109         return strcasestr (str, address) != NULL;
110     case STRING_IN_USER_ADDRESS:
111         return strcasestr (address, str) != NULL;
112     case STRING_IS_USER_ADDRESS:
113         return strcasecmp (address, str) == 0;
114     }
115
116     return FALSE;
117 }
118
119 /* Match given string against user's configured "primary" and "other"
120  * addresses according to mode. */
121 static const char *
122 address_match (const char *str, notmuch_config_t *config, address_match_t mode)
123 {
124     const char *primary;
125     const char **other;
126     size_t i, other_len;
127
128     if (!str || *str == '\0')
129         return NULL;
130
131     primary = notmuch_config_get_user_primary_email (config);
132     if (match_address (str, primary, mode))
133         return primary;
134
135     other = notmuch_config_get_user_other_email (config, &other_len);
136     for (i = 0; i < other_len; i++) {
137         if (match_address (str, other[i], mode))
138             return other[i];
139     }
140
141     return NULL;
142 }
143
144 /* Does the given string contain an address configured as one of the
145  * user's "primary" or "other" addresses. If so, return the matching
146  * address, NULL otherwise. */
147 static const char *
148 user_address_in_string (const char *str, notmuch_config_t *config)
149 {
150     return address_match (str, config, USER_ADDRESS_IN_STRING);
151 }
152
153 /* Do any of the addresses configured as one of the user's "primary"
154  * or "other" addresses contain the given string. If so, return the
155  * matching address, NULL otherwise. */
156 static const char *
157 string_in_user_address (const char *str, notmuch_config_t *config)
158 {
159     return address_match (str, config, STRING_IN_USER_ADDRESS);
160 }
161
162 /* Is the given address configured as one of the user's "primary" or
163  * "other" addresses. */
164 static notmuch_bool_t
165 address_is_users (const char *address, notmuch_config_t *config)
166 {
167     return address_match (address, config, STRING_IS_USER_ADDRESS) != NULL;
168 }
169
170 /* Scan addresses in 'list'.
171  *
172  * If 'message' is non-NULL, then for each address in 'list' that is
173  * not configured as one of the user's addresses in 'config', add that
174  * address to 'message' as an address of 'type'.
175  *
176  * If 'user_from' is non-NULL and *user_from is NULL, *user_from will
177  * be set to the first address encountered in 'list' that is the
178  * user's address.
179  *
180  * Return the number of addresses added to 'message'. (If 'message' is
181  * NULL, the function returns 0 by definition.)
182  */
183 static unsigned int
184 scan_address_list (InternetAddressList *list,
185                    notmuch_config_t *config,
186                    GMimeMessage *message,
187                    GMimeRecipientType type,
188                    const char **user_from)
189 {
190     InternetAddress *address;
191     int i;
192     unsigned int n = 0;
193
194     for (i = 0; i < internet_address_list_length (list); i++) {
195         address = internet_address_list_get_address (list, i);
196         if (INTERNET_ADDRESS_IS_GROUP (address)) {
197             InternetAddressGroup *group;
198             InternetAddressList *group_list;
199
200             group = INTERNET_ADDRESS_GROUP (address);
201             group_list = internet_address_group_get_members (group);
202             if (group_list == NULL)
203                 continue;
204
205             n += scan_address_list (group_list, config, message, type, user_from);
206         } else {
207             InternetAddressMailbox *mailbox;
208             const char *name;
209             const char *addr;
210
211             mailbox = INTERNET_ADDRESS_MAILBOX (address);
212
213             name = internet_address_get_name (address);
214             addr = internet_address_mailbox_get_addr (mailbox);
215
216             if (address_is_users (addr, config)) {
217                 if (user_from && *user_from == NULL)
218                     *user_from = addr;
219             } else if (message) {
220                 g_mime_message_add_recipient (message, type, name, addr);
221                 n++;
222             }
223         }
224     }
225
226     return n;
227 }
228
229 /* Scan addresses in 'recipients'.
230  *
231  * See the documentation of scan_address_list() above. This function
232  * does exactly the same, but converts 'recipients' to an
233  * InternetAddressList first.
234  */
235 static unsigned int
236 scan_address_string (const char *recipients,
237                      notmuch_config_t *config,
238                      GMimeMessage *message,
239                      GMimeRecipientType type,
240                      const char **user_from)
241 {
242     InternetAddressList *list;
243
244     if (recipients == NULL)
245         return 0;
246
247     list = internet_address_list_parse_string (recipients);
248     if (list == NULL)
249         return 0;
250
251     return scan_address_list (list, config, message, type, user_from);
252 }
253
254 /* Does the address in the Reply-To header of 'message' already appear
255  * in either the 'To' or 'Cc' header of the message?
256  */
257 static int
258 reply_to_header_is_redundant (notmuch_message_t *message)
259 {
260     const char *reply_to, *to, *cc, *addr;
261     InternetAddressList *list;
262     InternetAddress *address;
263     InternetAddressMailbox *mailbox;
264
265     reply_to = notmuch_message_get_header (message, "reply-to");
266     if (reply_to == NULL || *reply_to == '\0')
267         return 0;
268
269     list = internet_address_list_parse_string (reply_to);
270
271     if (internet_address_list_length (list) != 1)
272         return 0;
273
274     address = internet_address_list_get_address (list, 0);
275     if (INTERNET_ADDRESS_IS_GROUP (address))
276         return 0;
277
278     mailbox = INTERNET_ADDRESS_MAILBOX (address);
279     addr = internet_address_mailbox_get_addr (mailbox);
280
281     to = notmuch_message_get_header (message, "to");
282     cc = notmuch_message_get_header (message, "cc");
283
284     if ((to && strstr (to, addr) != 0) ||
285         (cc && strstr (cc, addr) != 0))
286     {
287         return 1;
288     }
289
290     return 0;
291 }
292
293 /* Augment the recipients of 'reply' from the "Reply-to:", "From:",
294  * "To:", "Cc:", and "Bcc:" headers of 'message'.
295  *
296  * If 'reply_all' is true, use sender and all recipients, otherwise
297  * scan the headers for the first that contains something other than
298  * the user's addresses and add the recipients from this header
299  * (typically this would be reply-to-sender, but also handles reply to
300  * user's own message in a sensible way).
301  *
302  * If any of the user's addresses were found in these headers, the
303  * first of these returned, otherwise NULL is returned.
304  */
305 static const char *
306 add_recipients_from_message (GMimeMessage *reply,
307                              notmuch_config_t *config,
308                              notmuch_message_t *message,
309                              notmuch_bool_t reply_all)
310 {
311     struct {
312         const char *header;
313         const char *fallback;
314         GMimeRecipientType recipient_type;
315     } reply_to_map[] = {
316         { "reply-to", "from", GMIME_RECIPIENT_TYPE_TO  },
317         { "to",         NULL, GMIME_RECIPIENT_TYPE_TO  },
318         { "cc",         NULL, GMIME_RECIPIENT_TYPE_CC  },
319         { "bcc",        NULL, GMIME_RECIPIENT_TYPE_BCC }
320     };
321     const char *from_addr = NULL;
322     unsigned int i;
323     unsigned int n = 0;
324
325     /* Some mailing lists munge the Reply-To header despite it being A Bad
326      * Thing, see http://www.unicom.com/pw/reply-to-harmful.html
327      *
328      * The munging is easy to detect, because it results in a
329      * redundant reply-to header, (with an address that already exists
330      * in either To or Cc). So in this case, we ignore the Reply-To
331      * field and use the From header. This ensures the original sender
332      * will get the reply even if not subscribed to the list. Note
333      * that the address in the Reply-To header will always appear in
334      * the reply.
335      */
336     if (reply_to_header_is_redundant (message)) {
337         reply_to_map[0].header = "from";
338         reply_to_map[0].fallback = NULL;
339     }
340
341     for (i = 0; i < ARRAY_SIZE (reply_to_map); i++) {
342         const char *recipients;
343
344         recipients = notmuch_message_get_header (message,
345                                                  reply_to_map[i].header);
346         if ((recipients == NULL || recipients[0] == '\0') && reply_to_map[i].fallback)
347             recipients = notmuch_message_get_header (message,
348                                                      reply_to_map[i].fallback);
349
350         n += scan_address_string (recipients, config, reply,
351                                   reply_to_map[i].recipient_type, &from_addr);
352
353         if (!reply_all && n) {
354             /* Stop adding new recipients in reply-to-sender mode if
355              * we have added some recipient(s) above.
356              *
357              * This also handles the case of user replying to his own
358              * message, where reply-to/from is not a recipient. In
359              * this case there may be more than one recipient even if
360              * not replying to all.
361              */
362             reply = NULL;
363
364             /* From address and some recipients are enough, bail out. */
365             if (from_addr)
366                 break;
367         }
368     }
369
370     return from_addr;
371 }
372
373 /*
374  * Look for the user's address in " for <email@add.res>" in the
375  * received headers.
376  *
377  * Return the address that was found, if any, and NULL otherwise.
378  */
379 static const char *
380 guess_from_in_received_for (notmuch_config_t *config, const char *received)
381 {
382     const char *ptr;
383
384     ptr = strstr (received, " for ");
385     if (! ptr)
386         return NULL;
387
388     return user_address_in_string (ptr, config);
389 }
390
391 /*
392  * Parse all the " by MTA ..." parts in received headers to guess the
393  * email address that this was originally delivered to.
394  *
395  * Extract just the MTA here by removing leading whitespace and
396  * assuming that the MTA name ends at the next whitespace. Test for
397  * *(by+4) to be non-'\0' to make sure there's something there at all
398  * - and then assume that the first whitespace delimited token that
399  * follows is the receiving system in this step of the receive chain.
400  *
401  * Return the address that was found, if any, and NULL otherwise.
402  */
403 static const char *
404 guess_from_in_received_by (notmuch_config_t *config, const char *received)
405 {
406     const char *addr;
407     const char *by = received;
408     char *domain, *tld, *mta, *ptr, *token;
409
410     while ((by = strstr (by, " by ")) != NULL) {
411         by += 4;
412         if (*by == '\0')
413             break;
414         mta = xstrdup (by);
415         token = strtok(mta," \t");
416         if (token == NULL) {
417             free (mta);
418             break;
419         }
420         /*
421          * Now extract the last two components of the MTA host name as
422          * domain and tld.
423          */
424         domain = tld = NULL;
425         while ((ptr = strsep (&token, ". \t")) != NULL) {
426             if (*ptr == '\0')
427                 continue;
428             domain = tld;
429             tld = ptr;
430         }
431
432         if (domain) {
433             /*
434              * Recombine domain and tld and look for it among the
435              * configured email addresses. This time we have a known
436              * domain name and nothing else - so the test is the other
437              * way around: we check if this is a substring of one of
438              * the email addresses.
439              */
440             *(tld - 1) = '.';
441
442             addr = string_in_user_address (domain, config);
443             if (addr) {
444                 free (mta);
445                 return addr;
446             }
447         }
448         free (mta);
449     }
450
451     return NULL;
452 }
453
454 /*
455  * Get the concatenated Received: headers and search from the front
456  * (last Received: header added) and try to extract from them
457  * indications to which email address this message was delivered.
458  *
459  * The Received: header is special in our get_header function and is
460  * always concatenated.
461  *
462  * Return the address that was found, if any, and NULL otherwise.
463  */
464 static const char *
465 guess_from_in_received_headers (notmuch_config_t *config,
466                                 notmuch_message_t *message)
467 {
468     const char *received, *addr;
469     char *sanitized;
470
471     received = notmuch_message_get_header (message, "received");
472     if (! received)
473         return NULL;
474
475     sanitized = sanitize_string (NULL, received);
476     if (! sanitized)
477         return NULL;
478
479     addr = guess_from_in_received_for (config, sanitized);
480     if (! addr)
481         addr = guess_from_in_received_by (config, sanitized);
482
483     talloc_free (sanitized);
484
485     return addr;
486 }
487
488 /*
489  * Try to find user's email address in one of the extra To-like
490  * headers: Envelope-To, X-Original-To, and Delivered-To (searched in
491  * that order).
492  *
493  * Return the address that was found, if any, and NULL otherwise.
494  */
495 static const char *
496 get_from_in_to_headers (notmuch_config_t *config, notmuch_message_t *message)
497 {
498     size_t i;
499     const char *tohdr, *addr;
500     const char *to_headers[] = {
501         "Envelope-to",
502         "X-Original-To",
503         "Delivered-To",
504     };
505
506     for (i = 0; i < ARRAY_SIZE (to_headers); i++) {
507         tohdr = notmuch_message_get_header (message, to_headers[i]);
508
509         /* Note: tohdr potentially contains a list of email addresses. */
510         addr = user_address_in_string (tohdr, config);
511         if (addr)
512             return addr;
513     }
514
515     return NULL;
516 }
517
518 static GMimeMessage *
519 create_reply_message(void *ctx,
520                      notmuch_config_t *config,
521                      notmuch_message_t *message,
522                      notmuch_bool_t reply_all)
523 {
524     const char *subject, *from_addr = NULL;
525     const char *in_reply_to, *orig_references, *references;
526
527     /* The 1 means we want headers in a "pretty" order. */
528     GMimeMessage *reply = g_mime_message_new (1);
529     if (reply == NULL) {
530         fprintf (stderr, "Out of memory\n");
531         return NULL;
532     }
533
534     subject = notmuch_message_get_header (message, "subject");
535     if (subject) {
536         if (strncasecmp (subject, "Re:", 3))
537             subject = talloc_asprintf (ctx, "Re: %s", subject);
538         g_mime_message_set_subject (reply, subject);
539     }
540
541     from_addr = add_recipients_from_message (reply, config,
542                                              message, reply_all);
543
544     /*
545      * Sadly, there is no standard way to find out to which email
546      * address a mail was delivered - what is in the headers depends
547      * on the MTAs used along the way.
548      *
549      * If none of the user's email addresses are in the To: or Cc:
550      * headers, we try a number of heuristics which hopefully will
551      * answer this question.
552      *
553      * First, check for Envelope-To:, X-Original-To:, and
554      * Delivered-To: headers.
555      */
556     if (from_addr == NULL)
557         from_addr = get_from_in_to_headers (config, message);
558
559     /*
560      * Check for a (for <email@add.res>) clause in Received: headers,
561      * and the domain part of known email addresses in the 'by' part
562      * of Received: headers
563      */
564     if (from_addr == NULL)
565         from_addr = guess_from_in_received_headers (config, message);
566
567     /* Default to user's primary address. */
568     if (from_addr == NULL)
569         from_addr = notmuch_config_get_user_primary_email (config);
570
571     from_addr = talloc_asprintf (ctx, "%s <%s>",
572                                  notmuch_config_get_user_name (config),
573                                  from_addr);
574     g_mime_object_set_header (GMIME_OBJECT (reply),
575                               "From", from_addr);
576
577     in_reply_to = talloc_asprintf (ctx, "<%s>",
578                                    notmuch_message_get_message_id (message));
579
580     g_mime_object_set_header (GMIME_OBJECT (reply),
581                               "In-Reply-To", in_reply_to);
582
583     orig_references = notmuch_message_get_header (message, "references");
584     if (!orig_references)
585         /* Treat errors like missing References headers. */
586         orig_references = "";
587     references = talloc_asprintf (ctx, "%s%s%s",
588                                   *orig_references ? orig_references : "",
589                                   *orig_references ? " " : "",
590                                   in_reply_to);
591     g_mime_object_set_header (GMIME_OBJECT (reply),
592                               "References", references);
593
594     return reply;
595 }
596
597 static int
598 notmuch_reply_format_default(void *ctx,
599                              notmuch_config_t *config,
600                              notmuch_query_t *query,
601                              notmuch_show_params_t *params,
602                              notmuch_bool_t reply_all,
603                              unused (sprinter_t *sp))
604 {
605     GMimeMessage *reply;
606     notmuch_messages_t *messages;
607     notmuch_message_t *message;
608     mime_node_t *root;
609
610     for (messages = notmuch_query_search_messages (query);
611          notmuch_messages_valid (messages);
612          notmuch_messages_move_to_next (messages))
613     {
614         message = notmuch_messages_get (messages);
615
616         reply = create_reply_message (ctx, config, message, reply_all);
617
618         /* If reply creation failed, we're out of memory, so don't
619          * bother trying any more messages.
620          */
621         if (!reply) {
622             notmuch_message_destroy (message);
623             return 1;
624         }
625
626         show_reply_headers (reply);
627
628         g_object_unref (G_OBJECT (reply));
629         reply = NULL;
630
631         if (mime_node_open (ctx, message, &(params->crypto), &root) == NOTMUCH_STATUS_SUCCESS) {
632             format_part_reply (root);
633             talloc_free (root);
634         }
635
636         notmuch_message_destroy (message);
637     }
638     return 0;
639 }
640
641 static int
642 notmuch_reply_format_sprinter(void *ctx,
643                               notmuch_config_t *config,
644                               notmuch_query_t *query,
645                               notmuch_show_params_t *params,
646                               notmuch_bool_t reply_all,
647                               sprinter_t *sp)
648 {
649     GMimeMessage *reply;
650     notmuch_messages_t *messages;
651     notmuch_message_t *message;
652     mime_node_t *node;
653
654     if (notmuch_query_count_messages (query) != 1) {
655         fprintf (stderr, "Error: search term did not match precisely one message.\n");
656         return 1;
657     }
658
659     messages = notmuch_query_search_messages (query);
660     message = notmuch_messages_get (messages);
661     if (mime_node_open (ctx, message, &(params->crypto), &node) != NOTMUCH_STATUS_SUCCESS)
662         return 1;
663
664     reply = create_reply_message (ctx, config, message, reply_all);
665     if (!reply)
666         return 1;
667
668     sp->begin_map (sp);
669
670     /* The headers of the reply message we've created */
671     sp->map_key (sp, "reply-headers");
672     format_headers_sprinter (sp, reply, TRUE);
673     g_object_unref (G_OBJECT (reply));
674     reply = NULL;
675
676     /* Start the original */
677     sp->map_key (sp, "original");
678     format_part_sprinter (ctx, sp, node, TRUE, TRUE, FALSE);
679
680     /* End */
681     sp->end (sp);
682     notmuch_message_destroy (message);
683
684     return 0;
685 }
686
687 /* This format is currently tuned for a git send-email --notmuch hook */
688 static int
689 notmuch_reply_format_headers_only(void *ctx,
690                                   notmuch_config_t *config,
691                                   notmuch_query_t *query,
692                                   unused (notmuch_show_params_t *params),
693                                   notmuch_bool_t reply_all,
694                                   unused (sprinter_t *sp))
695 {
696     GMimeMessage *reply;
697     notmuch_messages_t *messages;
698     notmuch_message_t *message;
699     const char *in_reply_to, *orig_references, *references;
700     char *reply_headers;
701
702     for (messages = notmuch_query_search_messages (query);
703          notmuch_messages_valid (messages);
704          notmuch_messages_move_to_next (messages))
705     {
706         message = notmuch_messages_get (messages);
707
708         /* The 0 means we do not want headers in a "pretty" order. */
709         reply = g_mime_message_new (0);
710         if (reply == NULL) {
711             fprintf (stderr, "Out of memory\n");
712             return 1;
713         }
714
715         in_reply_to = talloc_asprintf (ctx, "<%s>",
716                              notmuch_message_get_message_id (message));
717
718         g_mime_object_set_header (GMIME_OBJECT (reply),
719                                   "In-Reply-To", in_reply_to);
720
721
722         orig_references = notmuch_message_get_header (message, "references");
723
724         /* We print In-Reply-To followed by References because git format-patch treats them
725          * specially.  Git does not interpret the other headers specially
726          */
727         references = talloc_asprintf (ctx, "%s%s%s",
728                                       orig_references ? orig_references : "",
729                                       orig_references ? " " : "",
730                                       in_reply_to);
731         g_mime_object_set_header (GMIME_OBJECT (reply),
732                                   "References", references);
733
734         (void)add_recipients_from_message (reply, config, message, reply_all);
735
736         reply_headers = g_mime_object_to_string (GMIME_OBJECT (reply));
737         printf ("%s", reply_headers);
738         free (reply_headers);
739
740         g_object_unref (G_OBJECT (reply));
741         reply = NULL;
742
743         notmuch_message_destroy (message);
744     }
745     return 0;
746 }
747
748 enum {
749     FORMAT_DEFAULT,
750     FORMAT_JSON,
751     FORMAT_SEXP,
752     FORMAT_HEADERS_ONLY,
753 };
754
755 int
756 notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[])
757 {
758     notmuch_database_t *notmuch;
759     notmuch_query_t *query;
760     char *query_string;
761     int opt_index;
762     int (*reply_format_func) (void *ctx,
763                               notmuch_config_t *config,
764                               notmuch_query_t *query,
765                               notmuch_show_params_t *params,
766                               notmuch_bool_t reply_all,
767                               struct sprinter *sp);
768     notmuch_show_params_t params = {
769         .part = -1,
770         .crypto = {
771             .verify = FALSE,
772             .decrypt = FALSE
773         }
774     };
775     int format = FORMAT_DEFAULT;
776     int reply_all = TRUE;
777     struct sprinter *sp = NULL;
778
779     notmuch_opt_desc_t options[] = {
780         { NOTMUCH_OPT_KEYWORD, &format, "format", 'f',
781           (notmuch_keyword_t []){ { "default", FORMAT_DEFAULT },
782                                   { "json", FORMAT_JSON },
783                                   { "sexp", FORMAT_SEXP },
784                                   { "headers-only", FORMAT_HEADERS_ONLY },
785                                   { 0, 0 } } },
786         { NOTMUCH_OPT_INT, &notmuch_format_version, "format-version", 0, 0 },
787         { NOTMUCH_OPT_KEYWORD, &reply_all, "reply-to", 'r',
788           (notmuch_keyword_t []){ { "all", TRUE },
789                                   { "sender", FALSE },
790                                   { 0, 0 } } },
791         { NOTMUCH_OPT_BOOLEAN, &params.crypto.decrypt, "decrypt", 'd', 0 },
792         { 0, 0, 0, 0, 0 }
793     };
794
795     opt_index = parse_arguments (argc, argv, options, 1);
796     if (opt_index < 0)
797         return EXIT_FAILURE;
798
799     if (format == FORMAT_HEADERS_ONLY) {
800         reply_format_func = notmuch_reply_format_headers_only;
801     } else if (format == FORMAT_JSON) {
802         reply_format_func = notmuch_reply_format_sprinter;
803         sp = sprinter_json_create (config, stdout);
804     } else if (format == FORMAT_SEXP) {
805         reply_format_func = notmuch_reply_format_sprinter;
806         sp = sprinter_sexp_create (config, stdout);
807     } else {
808         reply_format_func = notmuch_reply_format_default;
809     }
810
811     notmuch_exit_if_unsupported_format ();
812
813     query_string = query_string_from_args (config, argc-opt_index, argv+opt_index);
814     if (query_string == NULL) {
815         fprintf (stderr, "Out of memory\n");
816         return EXIT_FAILURE;
817     }
818
819     if (*query_string == '\0') {
820         fprintf (stderr, "Error: notmuch reply requires at least one search term.\n");
821         return EXIT_FAILURE;
822     }
823
824     if (notmuch_database_open (notmuch_config_get_database_path (config),
825                                NOTMUCH_DATABASE_MODE_READ_ONLY, &notmuch))
826         return EXIT_FAILURE;
827
828     query = notmuch_query_create (notmuch, query_string);
829     if (query == NULL) {
830         fprintf (stderr, "Out of memory\n");
831         return EXIT_FAILURE;
832     }
833
834     if (reply_format_func (config, config, query, &params, reply_all, sp) != 0)
835         return EXIT_FAILURE;
836
837     notmuch_crypto_cleanup (&params.crypto);
838     notmuch_query_destroy (query);
839     notmuch_database_destroy (notmuch);
840
841     return EXIT_SUCCESS;
842 }