1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /* camel-imap-search.c: IMAP folder search */
6 * Dan Winship <danw@ximian.com>
7 * Michael Zucchi <notzed@ximian.com>
9 * Copyright 2000, 2001, 2002 Ximian, Inc.
11 * This program is free software; you can redistribute it and/or
12 * modify it under the terms of version 2 of the GNU Lesser General Public
13 * License as published by the Free Software Foundation.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 * General Public License for more details.
20 * You should have received a copy of the GNU Lesser General Public
21 * License along with this program; if not, write to the
22 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
23 * Boston, MA 02110-1301, USA.
33 #include <libedataserver/md5-utils.h> /* md5 hash building */
35 #include "camel-mime-utils.h" /* base64 encoding */
36 #include "camel-search-private.h"
37 #include "camel-seekable-stream.h"
39 #include "camel-imap-command.h"
40 #include "camel-imap-folder.h"
41 #include "camel-imap-search.h"
42 #include "camel-imap-store.h"
43 #include "camel-imap-summary.h"
44 #include "camel-imap-utils.h"
49 /* The strtok() in Microsoft's C library is MT-safe (but still uses
50 * only one buffer pointer per thread, but for the use of strtok_r()
51 * here that's enough).
53 #define strtok_r(s,sep,lasts) (*(lasts)=strtok((s),(sep)))
58 BODY (as in body search)
59 Last uid when search performed
60 termcount: number of search terms
61 matchcount: number of matches
63 match0, match1, match2, ...
66 /* size of in-memory cache */
67 #define MATCH_CACHE_SIZE (32)
69 /* Also takes care of 'endianness' file magic */
70 #define MATCH_MARK (('B' << 24) | ('O' << 16) | ('D' << 8) | 'Y')
72 /* on-disk header, in native endianness format, matches follow */
73 struct _match_header {
75 guint32 validity; /* uidvalidity for this folder */
81 /* in-memory record */
82 struct _match_record {
83 struct _match_record *next;
84 struct _match_record *prev;
91 unsigned int termcount;
97 static void free_match(CamelImapSearch *is, struct _match_record *mr);
98 static ESExpResult *imap_body_contains (struct _ESExp *f, int argc, struct _ESExpResult **argv, CamelFolderSearch *s);
100 static CamelFolderSearchClass *imap_search_parent_class;
103 camel_imap_search_class_init (CamelImapSearchClass *camel_imap_search_class)
105 /* virtual method overload */
106 CamelFolderSearchClass *camel_folder_search_class =
107 CAMEL_FOLDER_SEARCH_CLASS (camel_imap_search_class);
109 imap_search_parent_class = (CamelFolderSearchClass *)camel_type_get_global_classfuncs (camel_folder_search_get_type ());
111 /* virtual method overload */
112 camel_folder_search_class->body_contains = imap_body_contains;
116 camel_imap_search_init(CamelImapSearch *is)
118 e_dlist_init(&is->matches);
119 is->matches_hash = g_hash_table_new(g_str_hash, g_str_equal);
120 is->matches_count = 0;
125 camel_imap_search_finalise(CamelImapSearch *is)
127 struct _match_record *mr;
129 while ( (mr = (struct _match_record *)e_dlist_remtail(&is->matches)) )
131 g_hash_table_destroy(is->matches_hash);
133 camel_object_unref((CamelObject *)is->cache);
137 camel_imap_search_get_type (void)
139 static CamelType camel_imap_search_type = CAMEL_INVALID_TYPE;
141 if (camel_imap_search_type == CAMEL_INVALID_TYPE) {
142 camel_imap_search_type = camel_type_register (
143 CAMEL_FOLDER_SEARCH_TYPE, "CamelImapSearch",
144 sizeof (CamelImapSearch),
145 sizeof (CamelImapSearchClass),
146 (CamelObjectClassInitFunc) camel_imap_search_class_init, NULL,
147 (CamelObjectInitFunc) camel_imap_search_init,
148 (CamelObjectFinalizeFunc) camel_imap_search_finalise);
151 return camel_imap_search_type;
155 * camel_imap_search_new:
157 * Return value: A new CamelImapSearch widget.
160 camel_imap_search_new (const char *cachedir)
162 CamelFolderSearch *new = CAMEL_FOLDER_SEARCH (camel_object_new (camel_imap_search_get_type ()));
163 CamelImapSearch *is = (CamelImapSearch *)new;
165 camel_folder_search_construct (new);
167 is->cache = camel_data_cache_new(cachedir, 0, NULL);
169 /* Expire entries after 14 days of inactivity */
170 camel_data_cache_set_expire_access(is->cache, 60*60*24*14);
178 hash_match(char hash[17], int argc, struct _ESExpResult **argv)
181 unsigned char digest[16];
182 unsigned int state = 0, save = 0;
186 for (i=0;i<argc;i++) {
187 if (argv[i]->type == ESEXP_RES_STRING)
188 md5_update(&ctx, argv[i]->value.string, strlen(argv[i]->value.string));
190 md5_final(&ctx, digest);
192 camel_base64_encode_close(digest, 12, FALSE, hash, &state, &save);
205 save_match(CamelImapSearch *is, struct _match_record *mr)
207 guint32 mark = MATCH_MARK;
209 struct _match_header header;
212 /* since its a cache, doesn't matter if it doesn't save, at least we have the in-memory cache
214 if (is->cache == NULL)
217 stream = camel_data_cache_add(is->cache, "search/body-contains", mr->hash, NULL);
221 d(printf("Saving search cache entry to '%s': %s\n", mr->hash, mr->terms[0]));
223 /* we write the whole thing, then re-write the header magic, saves fancy sync code */
224 memcpy(&header.mark, " ", 4);
225 header.termcount = 0;
226 header.matchcount = mr->matches->len;
227 header.lastuid = mr->lastuid;
228 header.validity = mr->validity;
230 if (camel_stream_write(stream, (char *)&header, sizeof(header)) != sizeof(header)
231 || camel_stream_write(stream, mr->matches->data, mr->matches->len*sizeof(guint32)) != mr->matches->len*sizeof(guint32)
232 || camel_seekable_stream_seek((CamelSeekableStream *)stream, 0, CAMEL_STREAM_SET) == -1
233 || camel_stream_write(stream, (char *)&mark, sizeof(mark)) != sizeof(mark)) {
234 d(printf(" saving failed, removing cache entry\n"));
235 camel_data_cache_remove(is->cache, "search/body-contains", mr->hash, NULL);
239 camel_object_unref((CamelObject *)stream);
244 free_match(CamelImapSearch *is, struct _match_record *mr)
248 for (i=0;i<mr->termcount;i++)
249 g_free(mr->terms[i]);
251 g_array_free(mr->matches, TRUE);
255 static struct _match_record *
256 load_match(CamelImapSearch *is, char hash[17], int argc, struct _ESExpResult **argv)
258 struct _match_record *mr;
259 CamelStream *stream = NULL;
260 struct _match_header header;
263 mr = g_malloc0(sizeof(*mr));
264 mr->matches = g_array_new(0, 0, sizeof(guint32));
265 g_assert(strlen(hash) == 16);
266 strcpy(mr->hash, hash);
267 mr->terms = g_malloc0(sizeof(mr->terms[0]) * argc);
268 for (i=0;i<argc;i++) {
269 if (argv[i]->type == ESEXP_RES_STRING) {
271 mr->terms[i] = g_strdup(argv[i]->value.string);
275 d(printf("Loading search cache entry to '%s': %s\n", mr->hash, mr->terms[0]));
277 memset(&header, 0, sizeof(header));
279 stream = camel_data_cache_get(is->cache, "search/body-contains", mr->hash, NULL);
280 if (stream != NULL) {
281 /* 'cause i'm gonna be lazy, i'm going to have the termcount == 0 for now,
282 and not load or save them since i can't think of a nice way to do it, the hash
283 should be sufficient to key it */
284 /* This check should also handle endianness changes, we just throw away
285 the data (its only a cache) */
286 if (camel_stream_read(stream, (char *)&header, sizeof(header)) == sizeof(header)
287 && header.validity == is->validity
288 && header.mark == MATCH_MARK
289 && header.termcount == 0) {
290 d(printf(" found %d matches\n", header.matchcount));
291 g_array_set_size(mr->matches, header.matchcount);
292 camel_stream_read(stream, mr->matches->data, sizeof(guint32)*header.matchcount);
294 d(printf(" file format invalid/validity changed\n"));
295 memset(&header, 0, sizeof(header));
297 camel_object_unref((CamelObject *)stream);
299 d(printf(" no cache entry found\n"));
302 mr->validity = header.validity;
303 if (mr->validity != is->validity)
306 mr->lastuid = header.lastuid;
312 sync_match(CamelImapSearch *is, struct _match_record *mr)
314 char *p, *result, *lasts = NULL;
315 CamelImapResponse *response = NULL;
317 CamelFolder *folder = ((CamelFolderSearch *)is)->folder;
318 CamelImapStore *store = (CamelImapStore *)folder->parent_store;
319 struct _camel_search_words *words;
323 if (mr->lastuid >= is->lastuid && mr->validity == is->validity)
326 d(printf ("updating match record for uid's %d:%d\n", mr->lastuid+1, is->lastuid));
328 /* TODO: Handle multiple search terms */
330 /* This handles multiple search words within a single term */
331 words = camel_search_words_split (mr->terms[0]);
332 search = g_string_new ("");
333 g_string_append_printf (search, "UID %d:%d", mr->lastuid + 1, is->lastuid);
334 for (i = 0; i < words->len; i++) {
335 char *w = words->words[i]->word, c;
337 g_string_append_printf (search, " BODY \"");
339 if (c == '\\' || c == '"')
340 g_string_append_c (search, '\\');
341 g_string_append_c (search, c);
343 g_string_append_c (search, '"');
345 camel_search_words_free (words);
347 /* We only try search using utf8 if its non us-ascii text? */
348 if ((words->type & CAMEL_SEARCH_WORD_8BIT) && (store->capabilities & IMAP_CAPABILITY_utf8_search)) {
349 response = camel_imap_command (store, folder, NULL,
350 "UID SEARCH CHARSET UTF-8 %s", search->str);
351 /* We can't actually tell if we got a NO response, so assume always */
352 if (response == NULL)
353 store->capabilities &= ~IMAP_CAPABILITY_utf8_search;
355 if (response == NULL)
356 response = camel_imap_command (store, folder, NULL,
357 "UID SEARCH %s", search->str);
358 g_string_free(search, TRUE);
362 result = camel_imap_response_extract (store, response, "SEARCH", NULL);
366 p = result + sizeof ("* SEARCH");
367 for (p = strtok_r (p, " ", &lasts); p; p = strtok_r (NULL, " ", &lasts)) {
368 uid = strtoul(p, NULL, 10);
369 g_array_append_vals(mr->matches, &uid, 1);
373 mr->validity = is->validity;
374 mr->lastuid = is->lastuid;
380 static struct _match_record *
381 get_match(CamelImapSearch *is, int argc, struct _ESExpResult **argv)
384 struct _match_record *mr;
386 hash_match(hash, argc, argv);
388 mr = g_hash_table_lookup(is->matches_hash, hash);
390 while (is->matches_count >= MATCH_CACHE_SIZE) {
391 mr = (struct _match_record *)e_dlist_remtail(&is->matches);
393 printf("expiring match '%s' (%s)\n", mr->hash, mr->terms[0]);
394 g_hash_table_remove(is->matches_hash, mr->hash);
398 is->matches_count = 0;
401 mr = load_match(is, hash, argc, argv);
402 g_hash_table_insert(is->matches_hash, mr->hash, mr);
405 e_dlist_remove((EDListNode *)mr);
408 e_dlist_addhead(&is->matches, (EDListNode *)mr);
410 /* what about offline mode? */
411 /* We could cache those results too, or should we cache them elsewhere? */
418 imap_body_contains (struct _ESExp *f, int argc, struct _ESExpResult **argv, CamelFolderSearch *s)
420 CamelImapStore *store = CAMEL_IMAP_STORE (s->folder->parent_store);
421 CamelImapSearch *is = (CamelImapSearch *)s;
424 CamelMessageInfo *info;
425 GHashTable *uid_hash = NULL;
428 struct _match_record *mr;
431 d(printf("Performing body search '%s'\n", argv[0]->value.string));
433 /* TODO: Cache offline searches too? */
435 /* If offline, search using the parent class, which can handle this manually */
436 if (!camel_disco_store_check_online (CAMEL_DISCO_STORE (store), NULL))
437 return imap_search_parent_class->body_contains(f, argc, argv, s);
439 /* optimise the match "" case - match everything */
440 if (argc == 1 && argv[0]->value.string[0] == '\0') {
442 r = e_sexp_result_new(f, ESEXP_RES_BOOL);
443 r->value.bool = TRUE;
445 r = e_sexp_result_new(f, ESEXP_RES_ARRAY_PTR);
446 r->value.ptrarray = g_ptr_array_new ();
447 for (i = 0; i < s->summary->len; i++) {
448 info = g_ptr_array_index(s->summary, i);
449 g_ptr_array_add(r->value.ptrarray, (char *)camel_message_info_uid(info));
452 } else if (argc == 0 || s->summary->len == 0) {
453 /* nothing to match case, do nothing (should be handled higher up?) */
455 r = e_sexp_result_new(f, ESEXP_RES_BOOL);
456 r->value.bool = FALSE;
458 r = e_sexp_result_new(f, ESEXP_RES_ARRAY_PTR);
459 r->value.ptrarray = g_ptr_array_new ();
464 /* setup lastuid/validity for synchronising */
465 info = g_ptr_array_index(s->summary, s->summary->len-1);
466 is->lastuid = strtoul(camel_message_info_uid(info), NULL, 10);
467 is->validity = ((CamelImapSummary *)(s->folder->summary))->validity;
469 mr = get_match(is, argc, argv);
472 uidn = strtoul(camel_message_info_uid(s->current), NULL, 10);
473 uidp = (guint32 *)mr->matches->data;
474 j = mr->matches->len;
475 for (i=0;i<j && !truth;i++)
476 truth = *uidp++ == uidn;
477 r = e_sexp_result_new(f, ESEXP_RES_BOOL);
478 r->value.bool = truth;
480 r = e_sexp_result_new(f, ESEXP_RES_ARRAY_PTR);
481 array = r->value.ptrarray = g_ptr_array_new();
483 /* We use a hash to map the uid numbers to uid strings as required by the search api */
484 /* We use the summary's strings so we dont need to alloc more */
485 uid_hash = g_hash_table_new(NULL, NULL);
486 for (i = 0; i < s->summary->len; i++) {
487 info = s->summary->pdata[i];
488 uid = (char *)camel_message_info_uid(info);
489 uidn = strtoul(uid, NULL, 10);
490 g_hash_table_insert(uid_hash, GUINT_TO_POINTER(uidn), uid);
493 uidp = (guint32 *)mr->matches->data;
494 j = mr->matches->len;
495 for (i=0;i<j && !truth;i++) {
496 uid = g_hash_table_lookup(uid_hash, GUINT_TO_POINTER(*uidp++));
498 g_ptr_array_add(array, uid);
501 g_hash_table_destroy(uid_hash);