notmuch search: Print all authors contributing to a thread.
[notmuch] / lib / query.cc
1 /* query.cc - Support for searching 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 <glib.h> /* GHashTable, GPtrArray */
25
26 #include <xapian.h>
27
28 struct _notmuch_query {
29     notmuch_database_t *notmuch;
30     const char *query_string;
31     notmuch_sort_t sort;
32 };
33
34 struct _notmuch_messages {
35     notmuch_database_t *notmuch;
36     Xapian::MSetIterator iterator;
37     Xapian::MSetIterator iterator_end;
38 };
39
40 struct _notmuch_threads {
41     notmuch_database_t *notmuch;
42     GPtrArray *threads;
43     unsigned int index;
44 };
45
46 notmuch_query_t *
47 notmuch_query_create (notmuch_database_t *notmuch,
48                       const char *query_string)
49 {
50     notmuch_query_t *query;
51
52 #ifdef DEBUG_QUERY
53     fprintf (stderr, "Query string is:\n%s\n", query_string);
54 #endif
55
56     query = talloc (NULL, notmuch_query_t);
57     if (unlikely (query == NULL))
58         return NULL;
59
60     query->notmuch = notmuch;
61
62     query->query_string = talloc_strdup (query, query_string);
63
64     query->sort = NOTMUCH_SORT_DATE_OLDEST_FIRST;
65
66     return query;
67 }
68
69 void
70 notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort)
71 {
72     query->sort = sort;
73 }
74
75 /* We end up having to call the destructors explicitly because we had
76  * to use "placement new" in order to initialize C++ objects within a
77  * block that we allocated with talloc. So C++ is making talloc
78  * slightly less simple to use, (we wouldn't need
79  * talloc_set_destructor at all otherwise).
80  */
81 static int
82 _notmuch_messages_destructor (notmuch_messages_t *messages)
83 {
84     messages->iterator.~MSetIterator ();
85     messages->iterator_end.~MSetIterator ();
86
87     return 0;
88 }
89
90 notmuch_messages_t *
91 notmuch_query_search_messages (notmuch_query_t *query,
92                                int first,
93                                int max_messages)
94 {
95     notmuch_database_t *notmuch = query->notmuch;
96     const char *query_string = query->query_string;
97     notmuch_messages_t *messages;
98
99     messages = talloc (query, notmuch_messages_t);
100     if (unlikely (messages == NULL))
101         return NULL;
102
103     try {
104         Xapian::Enquire enquire (*notmuch->xapian_db);
105         Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
106                                                    _find_prefix ("type"),
107                                                    "mail"));
108         Xapian::Query string_query, final_query;
109         Xapian::MSet mset;
110         unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN |
111                               Xapian::QueryParser::FLAG_PHRASE |
112                               Xapian::QueryParser::FLAG_LOVEHATE |
113                               Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE |
114                               Xapian::QueryParser::FLAG_WILDCARD);
115
116         if (strcmp (query_string, "") == 0) {
117             final_query = mail_query;
118         } else {
119             string_query = notmuch->query_parser->
120                 parse_query (query_string, flags);
121             final_query = Xapian::Query (Xapian::Query::OP_AND,
122                                          mail_query, string_query);
123         }
124
125         switch (query->sort) {
126         case NOTMUCH_SORT_DATE_OLDEST_FIRST:
127             enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, FALSE);
128             break;
129         case NOTMUCH_SORT_DATE_NEWEST_FIRST:
130             enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, TRUE);
131             break;
132         case NOTMUCH_SORT_MESSAGE_ID:
133             enquire.set_sort_by_value (NOTMUCH_VALUE_MESSAGE_ID, FALSE);
134             break;
135         }
136
137 #if DEBUG_QUERY
138         fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str());
139 #endif
140
141         enquire.set_query (final_query);
142
143         if (max_messages == -1)
144             max_messages = notmuch->xapian_db->get_doccount ();
145         mset = enquire.get_mset (first, max_messages);
146
147         messages->notmuch = notmuch;
148
149         new (&messages->iterator) Xapian::MSetIterator ();
150         new (&messages->iterator_end) Xapian::MSetIterator ();
151
152         talloc_set_destructor (messages, _notmuch_messages_destructor);
153
154         messages->iterator = mset.begin ();
155         messages->iterator_end = mset.end ();
156
157     } catch (const Xapian::Error &error) {
158         fprintf (stderr, "A Xapian exception occurred: %s\n",
159                  error.get_msg().c_str());
160     }
161
162     return messages;
163 }
164
165 /* Glib objects force use to use a talloc destructor as well, (but not
166  * nearly as ugly as the for messages due to C++ objects). At
167  * this point, I'd really like to have some talloc-friendly
168  * equivalents for the few pieces of glib that I'm using. */
169 static int
170 _notmuch_threads_destructor (notmuch_threads_t *threads)
171 {
172     g_ptr_array_free (threads->threads, TRUE);
173
174     return 0;
175 }
176
177 notmuch_threads_t *
178 notmuch_query_search_threads (notmuch_query_t *query,
179                               int first,
180                               int max_threads)
181 {
182     notmuch_threads_t *threads;
183     notmuch_thread_t *thread;
184     const char *thread_id;
185     notmuch_messages_t *messages;
186     notmuch_message_t *message;
187     GHashTable *seen;
188     int messages_seen = 0, threads_seen = 0;
189
190     threads = talloc (query, notmuch_threads_t);
191     if (threads == NULL)
192         return NULL;
193
194     threads->notmuch = query->notmuch;
195     threads->threads = g_ptr_array_new ();
196     threads->index = 0;
197
198     talloc_set_destructor (threads, _notmuch_threads_destructor);
199
200     seen = g_hash_table_new_full (g_str_hash, g_str_equal,
201                                   free, NULL);
202
203     while (max_threads < 0 || threads_seen < first + max_threads)
204     {
205         int messages_seen_previously = messages_seen;
206
207         for (messages = notmuch_query_search_messages (query,
208                                                        messages_seen,
209                                                        max_threads);
210              notmuch_messages_has_more (messages);
211              notmuch_messages_advance (messages))
212         {
213             message = notmuch_messages_get (messages);
214
215             thread_id = notmuch_message_get_thread_id (message);
216
217             if (! g_hash_table_lookup_extended (seen,
218                                                 thread_id, NULL,
219                                                 (void **) &thread))
220             {
221                 if (threads_seen >= first) {
222                     thread = _notmuch_thread_create (query, query->notmuch,
223                                                      thread_id);
224                     g_ptr_array_add (threads->threads, thread);
225                 } else {
226                     thread = NULL;
227                 }
228
229                 g_hash_table_insert (seen, xstrdup (thread_id), thread);
230
231                 threads_seen++;
232             }
233
234             notmuch_message_destroy (message);
235
236             messages_seen++;
237
238             if (max_threads >= 0 && threads_seen >= first + max_threads)
239                 break;
240         }
241
242         /* Stop if we're not seeing any more messages. */
243         if (messages_seen == messages_seen_previously)
244             break;
245     }
246
247     g_hash_table_unref (seen);
248
249     return threads;
250 }
251
252 void
253 notmuch_query_destroy (notmuch_query_t *query)
254 {
255     talloc_free (query);
256 }
257
258 notmuch_bool_t
259 notmuch_messages_has_more (notmuch_messages_t *messages)
260 {
261     return (messages->iterator != messages->iterator_end);
262 }
263
264 notmuch_message_t *
265 notmuch_messages_get (notmuch_messages_t *messages)
266 {
267     notmuch_message_t *message;
268     Xapian::docid doc_id;
269     notmuch_private_status_t status;
270
271     if (! notmuch_messages_has_more (messages))
272         return NULL;
273
274     doc_id = *messages->iterator;
275
276     message = _notmuch_message_create (messages,
277                                        messages->notmuch, doc_id,
278                                        &status);
279
280     if (message == NULL &&
281         status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
282     {
283         INTERNAL_ERROR ("a messages iterator contains a non-existent document ID.\n");
284     }
285
286     return message;
287 }
288
289 void
290 notmuch_messages_advance (notmuch_messages_t *messages)
291 {
292     messages->iterator++;
293 }
294
295 void
296 notmuch_messages_destroy (notmuch_messages_t *messages)
297 {
298     talloc_free (messages);
299 }
300
301 notmuch_bool_t
302 notmuch_threads_has_more (notmuch_threads_t *threads)
303 {
304     return (threads->index < threads->threads->len);
305 }
306
307 notmuch_thread_t *
308 notmuch_threads_get (notmuch_threads_t *threads)
309 {
310     if (! notmuch_threads_has_more (threads))
311         return NULL;
312
313     return (notmuch_thread_t *) g_ptr_array_index (threads->threads,
314                                                    threads->index);
315 }
316
317 void
318 notmuch_threads_advance (notmuch_threads_t *threads)
319 {
320     threads->index++;
321 }
322
323 void
324 notmuch_threads_destroy (notmuch_threads_t *threads)
325 {
326     talloc_free (threads);
327 }