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