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