notmuch search: Print the number of matched/total messages for each thread.
[notmuch] / lib / thread.cc
1 /* thread.cc - Results of thread-based searches from a notmuch database
2  *
3  * Copyright © 2009 Carl Worth
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see http://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "notmuch-private.h"
22 #include "database-private.h"
23
24 #include <xapian.h>
25
26 #include <gmime/gmime.h>
27 #include <glib.h> /* GHashTable */
28
29 struct _notmuch_thread {
30     notmuch_database_t *notmuch;
31     char *thread_id;
32     char *subject;
33     GHashTable *authors_hash;
34     char *authors;
35     GHashTable *tags;
36
37     int total_messages;
38     int matched_messages;
39     time_t oldest;
40     time_t newest;
41 };
42
43 static int
44 _notmuch_thread_destructor (notmuch_thread_t *thread)
45 {
46     g_hash_table_unref (thread->authors_hash);
47     g_hash_table_unref (thread->tags);
48
49     return 0;
50 }
51
52 static void
53 _thread_add_author (notmuch_thread_t *thread,
54                     const char *author)
55 {
56     if (author == NULL)
57         return;
58
59     if (g_hash_table_lookup_extended (thread->authors_hash,
60                                       author, NULL, NULL))
61         return;
62
63     g_hash_table_insert (thread->authors_hash, xstrdup (author), NULL);
64
65     if (thread->authors)
66         thread->authors = talloc_asprintf (thread, "%s, %s",
67                                            thread->authors,
68                                            author);
69     else
70         thread->authors = talloc_strdup (thread, author);
71 }
72
73 static void
74 _thread_add_message (notmuch_thread_t *thread,
75                      notmuch_message_t *message)
76 {
77     notmuch_tags_t *tags;
78     const char *tag;
79     time_t date;
80     InternetAddressList *list;
81     InternetAddress *address;
82     const char *from, *author;
83
84     from = notmuch_message_get_header (message, "from");
85     list = internet_address_list_parse_string (from);
86     if (list) {
87         address = internet_address_list_get_address (list, 0);
88         if (address) {
89             author = internet_address_get_name (address);
90             if (author == NULL) {
91                 InternetAddressMailbox *mailbox;
92                 mailbox = INTERNET_ADDRESS_MAILBOX (address);
93                 author = internet_address_mailbox_get_addr (mailbox);
94             }
95             _thread_add_author (thread, author);
96         }
97         g_object_unref (G_OBJECT (list));
98     }
99
100     if (! thread->subject) {
101         const char *subject;
102         subject = notmuch_message_get_header (message, "subject");
103         thread->subject = talloc_strdup (thread, subject);
104     }
105
106     for (tags = notmuch_message_get_tags (message);
107          notmuch_tags_has_more (tags);
108          notmuch_tags_advance (tags))
109     {
110         tag = notmuch_tags_get (tags);
111         g_hash_table_insert (thread->tags, xstrdup (tag), NULL);
112     }
113
114     date = notmuch_message_get_date (message);
115
116     if (date < thread->oldest || ! thread->total_messages)
117         thread->oldest = date;
118
119     if (date > thread->newest || ! thread->total_messages)
120         thread->newest = date;
121
122     thread->total_messages++;
123 }
124
125 /* Create a new notmuch_thread_t object for the given thread ID,
126  * treating any messages matching 'query_string' as "matched".
127  *
128  * Creating the thread will trigger two database searches. The first
129  * is for all messages belonging to the thread, (to get the first
130  * subject line, the total count of messages, and all authors). The
131  * second search is for all messages that are in the thread and that
132  * also match the given query_string. This is to allow for a separate
133  * count of matched messages, and to allow a viewer to diplay these
134  * messages differently.
135  *
136  * Here, 'ctx' is talloc context for the resulting thread object.
137  *
138  * This function returns NULL in the case of any error.
139  */
140 notmuch_thread_t *
141 _notmuch_thread_create (void *ctx,
142                         notmuch_database_t *notmuch,
143                         const char *thread_id,
144                         const char *query_string)
145 {
146     notmuch_thread_t *thread;
147     const char *thread_id_query_string, *matched_query_string;
148     notmuch_query_t *thread_id_query, *matched_query;
149     notmuch_messages_t *messages;
150
151     thread_id_query_string = talloc_asprintf (ctx, "thread:%s", thread_id);
152     if (unlikely (query_string == NULL))
153         return NULL;
154
155     matched_query_string = talloc_asprintf (ctx, "%s AND (%s)",
156                                             thread_id_query_string,
157                                             query_string);
158     if (unlikely (matched_query_string == NULL))
159         return NULL;
160
161     thread_id_query = notmuch_query_create (notmuch, thread_id_query_string);
162     if (unlikely (thread_id_query == NULL))
163         return NULL;
164
165     matched_query = notmuch_query_create (notmuch, matched_query_string);
166     if (unlikely (thread_id_query == NULL))
167         return NULL;
168
169     thread = talloc (ctx, notmuch_thread_t);
170     if (unlikely (thread == NULL))
171         return NULL;
172
173     talloc_set_destructor (thread, _notmuch_thread_destructor);
174
175     thread->notmuch = notmuch;
176     thread->thread_id = talloc_strdup (thread, thread_id);
177     thread->subject = NULL;
178     thread->authors_hash = g_hash_table_new_full (g_str_hash, g_str_equal,
179                                                   free, NULL);
180     thread->authors = NULL;
181     thread->tags = g_hash_table_new_full (g_str_hash, g_str_equal,
182                                           free, NULL);
183
184     thread->total_messages = 0;
185     thread->matched_messages = 0;
186     thread->oldest = 0;
187     thread->newest = 0;
188
189     notmuch_query_set_sort (thread_id_query, NOTMUCH_SORT_DATE_OLDEST_FIRST);
190
191     for (messages = notmuch_query_search_messages (thread_id_query, 0, -1);
192          notmuch_messages_has_more (messages);
193          notmuch_messages_advance (messages))
194     {
195         _thread_add_message (thread, notmuch_messages_get (messages));
196     }
197
198     notmuch_query_destroy (thread_id_query);
199
200     for (messages = notmuch_query_search_messages (matched_query, 0, -1);
201          notmuch_messages_has_more (messages);
202          notmuch_messages_advance (messages))
203     {
204         thread->matched_messages++;
205     }
206
207     notmuch_query_destroy (matched_query);
208
209     return thread;
210 }
211
212 const char *
213 notmuch_thread_get_thread_id (notmuch_thread_t *thread)
214 {
215     return thread->thread_id;
216 }
217
218 int
219 notmuch_thread_get_total_messages (notmuch_thread_t *thread)
220 {
221     return thread->total_messages;
222 }
223
224 int
225 notmuch_thread_get_matched_messages (notmuch_thread_t *thread)
226 {
227     return thread->matched_messages;
228 }
229
230 const char *
231 notmuch_thread_get_authors (notmuch_thread_t *thread)
232 {
233     return thread->authors;
234 }
235
236 const char *
237 notmuch_thread_get_subject (notmuch_thread_t *thread)
238 {
239     return thread->subject;
240 }
241
242 time_t
243 notmuch_thread_get_oldest_date (notmuch_thread_t *thread)
244 {
245     return thread->oldest;
246 }
247
248 time_t
249 notmuch_thread_get_newest_date (notmuch_thread_t *thread)
250 {
251     return thread->newest;
252 }
253
254 notmuch_tags_t *
255 notmuch_thread_get_tags (notmuch_thread_t *thread)
256 {
257     notmuch_tags_t *tags;
258     GList *keys, *l;
259
260     tags = _notmuch_tags_create (thread);
261     if (unlikely (tags == NULL))
262         return NULL;
263
264     keys = g_hash_table_get_keys (thread->tags);
265
266     for (l = keys; l; l = l->next)
267         _notmuch_tags_add_tag (tags, (char *) l->data);
268
269     g_list_free (keys);
270
271     _notmuch_tags_prepare_iterator (tags);
272
273     return tags;
274 }
275
276 void
277 notmuch_thread_destroy (notmuch_thread_t *thread)
278 {
279     talloc_free (thread);
280 }