]> git.notmuchmail.org Git - notmuch/blob - notmuch-reply.c
configure: Move getlinetest.c down into config/have_getline.c.
[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-reply.h"
25
26 static const struct {
27     const char *header;
28     const char *fallback;
29     GMimeRecipientType recipient_type;
30 } reply_to_map[] = {
31     { "reply-to", "from", GMIME_RECIPIENT_TYPE_TO  },
32     { "to",         NULL, GMIME_RECIPIENT_TYPE_TO  },
33     { "cc",         NULL, GMIME_RECIPIENT_TYPE_CC  },
34     { "bcc",        NULL, GMIME_RECIPIENT_TYPE_BCC }
35 };
36
37 static void
38 reply_part_content (GMimeObject *part)
39 {
40     GMimeStream *stream_stdout = NULL, *stream_filter = NULL;
41     GMimeDataWrapper *wrapper;
42
43     stream_stdout = g_mime_stream_file_new (stdout);
44     if (stream_stdout) {
45         g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
46         stream_filter = g_mime_stream_filter_new(stream_stdout);
47     }
48     g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),
49                              g_mime_filter_reply_new(TRUE));
50     wrapper = g_mime_part_get_content_object (GMIME_PART (part));
51     if (wrapper && stream_filter)
52         g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
53     if (stream_filter)
54         g_object_unref(stream_filter);
55     if (stream_stdout)
56         g_object_unref(stream_stdout);
57 }
58
59 static void
60 reply_part (GMimeObject *part, int *part_count)
61 {
62     GMimeContentDisposition *disposition;
63     GMimeContentType *content_type;
64
65     (void) part_count;
66     disposition = g_mime_object_get_content_disposition (part);
67     if (disposition &&
68         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
69     {
70         const char *filename = g_mime_part_get_filename (GMIME_PART (part));
71         content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
72
73         if (g_mime_content_type_is_type (content_type, "text", "*") &&
74             !g_mime_content_type_is_type (content_type, "text", "html"))
75         {
76             reply_part_content (part);
77         }
78         else
79         {
80             printf ("Attachment: %s (%s)\n", filename,
81                     g_mime_content_type_to_string (content_type));
82         }
83
84         return;
85     }
86
87     content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
88
89     if (g_mime_content_type_is_type (content_type, "text", "*") &&
90         !g_mime_content_type_is_type (content_type, "text", "html"))
91     {
92         reply_part_content (part);
93     }
94     else
95     {
96         printf ("Non-text part: %s\n",
97                 g_mime_content_type_to_string (content_type));
98     }
99 }
100
101 /* Is the given address configured as one of the user's "personal" or
102  * "other" addresses. */
103 static int
104 address_is_users (const char *address, notmuch_config_t *config)
105 {
106     const char *primary;
107     char **other;
108     size_t i, other_len;
109
110     primary = notmuch_config_get_user_primary_email (config);
111     if (strcasecmp (primary, address) == 0)
112         return 1;
113
114     other = notmuch_config_get_user_other_email (config, &other_len);
115     for (i = 0; i < other_len; i++)
116         if (strcasecmp (other[i], address) == 0)
117             return 1;
118
119     return 0;
120 }
121
122 /* For each address in 'list' that is not configured as one of the
123  * user's addresses in 'config', add that address to 'message' as an
124  * address of 'type'.
125  *
126  * The first address encountered that *is* the user's address will be
127  * returned, (otherwise NULL is returned).
128  */
129 static const char *
130 add_recipients_for_address_list (GMimeMessage *message,
131                                  notmuch_config_t *config,
132                                  GMimeRecipientType type,
133                                  InternetAddressList *list)
134 {
135     InternetAddress *address;
136     int i;
137     const char *ret = NULL;
138
139     for (i = 0; i < internet_address_list_length (list); i++) {
140         address = internet_address_list_get_address (list, i);
141         if (INTERNET_ADDRESS_IS_GROUP (address)) {
142             InternetAddressGroup *group;
143             InternetAddressList *group_list;
144
145             group = INTERNET_ADDRESS_GROUP (address);
146             group_list = internet_address_group_get_members (group);
147             if (group_list == NULL)
148                 continue;
149
150             add_recipients_for_address_list (message, config,
151                                              type, group_list);
152         } else {
153             InternetAddressMailbox *mailbox;
154             const char *name;
155             const char *addr;
156
157             mailbox = INTERNET_ADDRESS_MAILBOX (address);
158
159             name = internet_address_get_name (address);
160             addr = internet_address_mailbox_get_addr (mailbox);
161
162             if (address_is_users (addr, config)) {
163                 if (ret == NULL)
164                     ret = addr;
165             } else {
166                 g_mime_message_add_recipient (message, type, name, addr);
167             }
168         }
169     }
170
171     return ret;
172 }
173
174 /* For each address in 'recipients' that is not configured as one of
175  * the user's addresses in 'config', add that address to 'message' as
176  * an address of 'type'.
177  *
178  * The first address encountered that *is* the user's address will be
179  * returned, (otherwise NULL is returned).
180  */
181 static const char *
182 add_recipients_for_string (GMimeMessage *message,
183                            notmuch_config_t *config,
184                            GMimeRecipientType type,
185                            const char *recipients)
186 {
187     InternetAddressList *list;
188
189     list = internet_address_list_parse_string (recipients);
190     if (list == NULL)
191         return NULL;
192
193     return add_recipients_for_address_list (message, config, type, list);
194 }
195
196 static int
197 notmuch_reply_format_default(void *ctx, notmuch_config_t *config, notmuch_query_t *query)
198 {
199     GMimeMessage *reply;
200     notmuch_messages_t *messages;
201     notmuch_message_t *message;
202     const char *subject, *recipients, *from_addr = NULL;
203     const char *in_reply_to, *orig_references, *references;
204     char *reply_headers;
205     unsigned int i;
206
207     for (messages = notmuch_query_search_messages (query);
208          notmuch_messages_has_more (messages);
209          notmuch_messages_advance (messages))
210     {
211         message = notmuch_messages_get (messages);
212
213         /* The 1 means we want headers in a "pretty" order. */
214         reply = g_mime_message_new (1);
215         if (reply == NULL) {
216             fprintf (stderr, "Out of memory\n");
217             return 1;
218         }
219
220         subject = notmuch_message_get_header (message, "subject");
221
222         if (strncasecmp (subject, "Re:", 3))
223             subject = talloc_asprintf (ctx, "Re: %s", subject);
224         g_mime_message_set_subject (reply, subject);
225
226         for (i = 0; i < ARRAY_SIZE (reply_to_map); i++) {
227             const char *addr;
228
229             recipients = notmuch_message_get_header (message,
230                                                      reply_to_map[i].header);
231             if ((recipients == NULL || recipients[0] == '\0') && reply_to_map[i].fallback)
232                 recipients = notmuch_message_get_header (message,
233                                                          reply_to_map[i].fallback);
234
235             addr = add_recipients_for_string (reply, config,
236                                               reply_to_map[i].recipient_type,
237                                               recipients);
238             if (from_addr == NULL)
239                 from_addr = addr;
240         }
241
242         if (from_addr == NULL)
243             from_addr = notmuch_config_get_user_primary_email (config);
244
245         from_addr = talloc_asprintf (ctx, "%s <%s>",
246                                      notmuch_config_get_user_name (config),
247                                      from_addr);
248         g_mime_object_set_header (GMIME_OBJECT (reply),
249                                   "From", from_addr);
250
251         g_mime_object_set_header (GMIME_OBJECT (reply), "Bcc",
252                            notmuch_config_get_user_primary_email (config));
253
254         in_reply_to = talloc_asprintf (ctx, "<%s>",
255                              notmuch_message_get_message_id (message));
256
257         g_mime_object_set_header (GMIME_OBJECT (reply),
258                                   "In-Reply-To", in_reply_to);
259
260         orig_references = notmuch_message_get_header (message, "references");
261         references = talloc_asprintf (ctx, "%s%s%s",
262                                       orig_references ? orig_references : "",
263                                       orig_references ? " " : "",
264                                       in_reply_to);
265         g_mime_object_set_header (GMIME_OBJECT (reply),
266                                   "References", references);
267
268         reply_headers = g_mime_object_to_string (GMIME_OBJECT (reply));
269         printf ("%s", reply_headers);
270         free (reply_headers);
271
272         g_object_unref (G_OBJECT (reply));
273         reply = NULL;
274
275         printf ("On %s, %s wrote:\n",
276                 notmuch_message_get_header (message, "date"),
277                 notmuch_message_get_header (message, "from"));
278
279         show_message_body (notmuch_message_get_filename (message), reply_part);
280
281         notmuch_message_destroy (message);
282     }
283     return 0;
284 }
285
286 /* This format is currently tuned for a git send-email --notmuch hook */
287 static int
288 notmuch_reply_format_headers_only(void *ctx, notmuch_config_t *config, notmuch_query_t *query)
289 {
290     GMimeMessage *reply;
291     notmuch_messages_t *messages;
292     notmuch_message_t *message;
293     const char *recipients, *in_reply_to, *orig_references, *references;
294     char *reply_headers;
295     unsigned int i;
296
297     for (messages = notmuch_query_search_messages (query);
298          notmuch_messages_has_more (messages);
299          notmuch_messages_advance (messages))
300     {
301         message = notmuch_messages_get (messages);
302
303         /* The 0 means we do not want headers in a "pretty" order. */
304         reply = g_mime_message_new (0);
305         if (reply == NULL) {
306             fprintf (stderr, "Out of memory\n");
307             return 1;
308         }
309
310         in_reply_to = talloc_asprintf (ctx, "<%s>",
311                              notmuch_message_get_message_id (message));
312
313         g_mime_object_set_header (GMIME_OBJECT (reply),
314                                   "In-Reply-To", in_reply_to);
315
316
317         orig_references = notmuch_message_get_header (message, "references");
318
319         /* We print In-Reply-To followed by References because git format-patch treats them
320          * specially.  Git does not interpret the other headers specially
321          */
322         references = talloc_asprintf (ctx, "%s%s%s",
323                                       orig_references ? orig_references : "",
324                                       orig_references ? " " : "",
325                                       in_reply_to);
326         g_mime_object_set_header (GMIME_OBJECT (reply),
327                                   "References", references);
328
329         for (i = 0; i < ARRAY_SIZE (reply_to_map); i++) {
330             const char *addr;
331
332             recipients = notmuch_message_get_header (message,
333                                                      reply_to_map[i].header);
334             if ((recipients == NULL || recipients[0] == '\0') && reply_to_map[i].fallback)
335                 recipients = notmuch_message_get_header (message,
336                                                          reply_to_map[i].fallback);
337
338             addr = add_recipients_for_string (reply, config,
339                                               reply_to_map[i].recipient_type,
340                                               recipients);
341         }
342
343         g_mime_object_set_header (GMIME_OBJECT (reply), "Bcc",
344                            notmuch_config_get_user_primary_email (config));
345
346         reply_headers = g_mime_object_to_string (GMIME_OBJECT (reply));
347         printf ("%s", reply_headers);
348         free (reply_headers);
349
350         g_object_unref (G_OBJECT (reply));
351         reply = NULL;
352
353         notmuch_message_destroy (message);
354     }
355     return 0;
356 }
357
358 int
359 notmuch_reply_command (void *ctx, int argc, char *argv[])
360 {
361     notmuch_config_t *config;
362     notmuch_database_t *notmuch;
363     notmuch_query_t *query;
364     char *opt, *query_string;
365     int i, ret = 0;
366     int (*reply_format_func)(void *ctx, notmuch_config_t *config, notmuch_query_t *query);
367
368     reply_format_func = notmuch_reply_format_default;
369
370     for (i = 0; i < argc && argv[i][0] == '-'; i++) {
371         if (strcmp (argv[i], "--") == 0) {
372             i++;
373             break;
374         }
375         if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
376             opt = argv[i] + sizeof ("--format=") - 1;
377             if (strcmp (opt, "default") == 0) {
378                 reply_format_func = notmuch_reply_format_default;
379             } else if (strcmp (opt, "headers-only") == 0) {
380                 reply_format_func = notmuch_reply_format_headers_only;
381             } else {
382                 fprintf (stderr, "Invalid value for --format: %s\n", opt);
383                 return 1;
384             }
385         } else {
386             fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
387             return 1;
388         }
389     }
390
391     argc -= i;
392     argv += i;
393
394     config = notmuch_config_open (ctx, NULL, NULL);
395     if (config == NULL)
396         return 1;
397
398     query_string = query_string_from_args (ctx, argc, argv);
399     if (query_string == NULL) {
400         fprintf (stderr, "Out of memory\n");
401         return 1;
402     }
403
404     if (*query_string == '\0') {
405         fprintf (stderr, "Error: notmuch reply requires at least one search term.\n");
406         return 1;
407     }
408
409     notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
410                                      NOTMUCH_DATABASE_MODE_READ_ONLY);
411     if (notmuch == NULL)
412         return 1;
413
414     query = notmuch_query_create (notmuch, query_string);
415     if (query == NULL) {
416         fprintf (stderr, "Out of memory\n");
417         return 1;
418     }
419
420     if (reply_format_func (ctx, config, query) != 0)
421         return 1;
422
423     notmuch_query_destroy (query);
424     notmuch_database_close (notmuch);
425
426     return ret;
427 }