* Fixes NB#82177 modest no longer hangs when canceling a search
[modest] / src / modest-search.c
1 /* Copyright (c) 2006, Nokia Corporation
2  * All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  * * Redistributions of source code must retain the above copyright
9  *   notice, this list of conditions and the following disclaimer.
10  * * Redistributions in binary form must reproduce the above copyright
11  *   notice, this list of conditions and the following disclaimer in the
12  *   documentation and/or other materials provided with the distribution.
13  * * Neither the name of the Nokia Corporation nor the names of its
14  *   contributors may be used to endorse or promote products derived from
15  *   this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
18  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
19  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
20  * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
21  * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 #ifndef _GNU_SOURCE
31 #define _GNU_SOURCE
32 #endif
33
34 #ifdef HAVE_CONFIG_H
35 #include <config.h>
36 #endif
37
38 #include <string.h>
39
40 #include <tny-shared.h>
41 #include <tny-folder.h>
42 #include <tny-folder-store.h>
43 #include <tny-list.h>
44 #include <tny-iterator.h>
45 #include <tny-simple-list.h>
46 #include <tny-camel-imap-store-account.h>
47 #include <tny-camel-pop-store-account.h>
48
49 #include "modest-text-utils.h"
50 #include "modest-account-mgr.h"
51 #include "modest-tny-account-store.h"
52 #include "modest-tny-account.h"
53 #include "modest-tny-mime-part.h"
54 #include "modest-tny-folder.h"
55 #include "modest-search.h"
56 #include "modest-runtime.h"
57 #include "modest-platform.h"
58
59 typedef struct 
60 {
61         guint folder_count;
62         guint folder_total;
63         GList *msg_hits;
64         ModestSearch *search;
65         ModestSearchCallback callback;
66         gpointer user_data;
67 } SearchHelper;
68
69 static SearchHelper *create_helper (ModestSearchCallback callback, 
70                                     ModestSearch *search,
71                                     gpointer user_data);
72
73 static void          check_search_finished (SearchHelper *helper);
74
75 static gchar *
76 g_strdup_or_null (const gchar *str)
77 {
78         gchar *string = NULL;
79
80         if  (str != NULL) {
81                 string = g_strdup (str);
82         }
83
84         return string;
85 }
86
87 static GList*
88 add_hit (GList *list, TnyHeader *header, TnyFolder *folder)
89 {
90         ModestSearchResultHit *hit;
91         TnyHeaderFlags   flags;
92         char            *furl;
93         char            *msg_url;
94         const char      *uid;
95         const char      *subject;
96         const char      *sender;
97
98         hit = g_slice_new0 (ModestSearchResultHit);
99
100         furl = tny_folder_get_url_string (folder);
101         g_debug ("%s: folder URL=%s\n", __FUNCTION__, furl);
102         if (!furl) {
103                 g_warning ("%s: tny_folder_get_url_string(): returned NULL for folder. Folder name=%s\n", __FUNCTION__, tny_folder_get_name (folder));
104         }
105         
106         /* Make sure that we use the short UID instead of the long UID,
107          * and/or find out what UID form is used when finding, in camel_data_cache_get().
108          * so we can find what we get. Philip is working on this.
109          */
110         uid = tny_header_get_uid (header);
111         if (!furl) {
112                 g_warning ("%s: tny_header_get_uid(): returned NULL for message with subject=%s\n", __FUNCTION__, tny_header_get_subject (header));
113         }
114         
115         msg_url = g_strdup_printf ("%s/%s", furl, uid);
116         g_free (furl);
117         
118         subject = tny_header_get_subject (header);
119         sender = tny_header_get_from (header);
120         
121         flags = tny_header_get_flags (header);
122
123         hit->msgid = msg_url;
124         hit->subject = g_strdup_or_null (subject);
125         hit->sender = g_strdup_or_null (sender);
126         hit->folder = g_strdup_or_null (tny_folder_get_name (folder));
127         hit->msize = tny_header_get_message_size (header);
128         hit->has_attachment = flags & TNY_HEADER_FLAG_ATTACHMENTS;
129         hit->is_unread = ! (flags & TNY_HEADER_FLAG_SEEN);
130         hit->timestamp = MIN (tny_header_get_date_received (header), tny_header_get_date_sent (header));
131         
132         return g_list_prepend (list, hit);
133 }
134
135 /** Call this until it returns FALSE or nread is set to 0.
136  * 
137  * @result: FALSE is something failed. */
138 static gboolean
139 read_chunk (TnyStream *stream, char *buffer, gsize count, gsize *nread)
140 {
141         gsize _nread = 0;
142         gssize res = 0;
143
144         while (_nread < count) {
145                 res = tny_stream_read (stream,
146                                        buffer + _nread, 
147                                        count - _nread);
148                 if (res == -1) { /* error */
149                         *nread = _nread;
150                         return FALSE;
151                 }
152
153                 _nread += res;
154                 
155                 if (res == 0) { /* no more bytes read. */
156                         *nread = _nread;
157                         return TRUE;
158                 }
159         }
160
161         *nread = _nread;
162         return TRUE;
163 }
164
165 #ifdef MODEST_HAVE_OGS
166 /*
167  * This function assumes that the mime part is of type "text / *"
168  */
169 static gboolean
170 search_mime_part_ogs (TnyMimePart *part, ModestSearch *search)
171 {
172         TnyStream *stream = NULL;
173         char       buffer[4096];
174         const gsize len = sizeof (buffer);
175         gsize      nread = 0;
176         gboolean   is_text_html = FALSE;
177         gboolean   found = FALSE;
178         gboolean   res = FALSE;
179         
180         is_text_html = tny_mime_part_content_type_is (part, "text/html");
181
182         stream = tny_mime_part_get_stream (part);
183
184         res = read_chunk (stream, buffer, len, &nread);
185         while (res && (nread > 0)) {
186                 /* search->text_searcher was instantiated in modest_search_folder(). */
187                 
188                 if (is_text_html) {
189                         found = ogs_text_searcher_search_html (search->text_searcher,
190                                                                buffer,
191                                                                nread,
192                                                                nread < len);
193                 } else {
194                         found = ogs_text_searcher_search_text (search->text_searcher,
195                                                                buffer,
196                                                                nread);
197                 }
198
199                 /* HACK: this helps UI refreshes because the search
200                    operations could be heavy */
201                 while (gtk_events_pending ())
202                         gtk_main_iteration ();
203
204                 if (found) {
205                         break;
206                 }
207                 
208                 nread = 0;
209                 res = read_chunk (stream, buffer, len, &nread);
210         }
211         g_object_unref (stream);
212
213         if (!found) {
214                 found = ogs_text_searcher_search_done (search->text_searcher);
215         }
216
217         ogs_text_searcher_reset (search->text_searcher);
218
219         return found;
220 }
221
222 #else
223
224 /*
225  * This function assumes that the mime part is of type "text / *"
226  */
227 static gboolean
228 search_mime_part_strcmp (TnyMimePart *part, ModestSearch *search)
229 {
230         TnyStream *stream;
231         char       buffer[8193];
232         char      *chunk[2];
233         gssize     len;
234         gsize     nread;
235         gboolean   found;
236         gboolean   res;
237
238         found = FALSE;
239         len = (sizeof (buffer) - 1) / 2;
240
241         if (strlen (search->body) > len) {
242                 g_warning ("Search term bigger then chunk."
243                            "We might not find everything");     
244         }
245
246         stream = tny_mime_part_get_stream (part);
247
248         memset (buffer, 0, sizeof (buffer));
249         chunk[0] = buffer;
250         chunk[1] = buffer + len;
251
252         res = read_chunk (stream, chunk[0], len, &nread);
253
254         if (res == FALSE) {
255                 goto done;
256         }
257
258         found = !modest_text_utils_utf8_strcmp (search->body,
259                                                 buffer,
260                                                 TRUE);
261         if (found) {
262                 goto done;
263         }
264
265         /* This works like this:
266          * buffer: [ooooooooooo|xxxxxxxxxxxx|\0] 
267          *          ^chunk[0]  ^chunk[1]
268          * we have prefilled chunk[0] now we always read into chunk[1]
269          * and then move the content of chunk[1] to chunk[0].
270          * The idea is to prevent not finding search terms that are
271          * spread across 2 reads:        
272          * buffer: [ooooooooTES|Txxxxxxxxxxx|\0] 
273          * We should catch that because we always search the whole
274          * buffer not only the chunks.
275          *
276          * Of course that breaks for search terms > sizeof (chunk)
277          * but sizeof (chunk) should be big enough I guess (see
278          * the g_warning in this function)
279          * */   
280         while ((res = read_chunk (stream, chunk[1], len, &nread))) {
281                 buffer[len + nread] = '\0';
282
283                 found = !modest_text_utils_utf8_strcmp (search->body,
284                                                         buffer,
285                                                         TRUE);
286
287                 /* HACK: this helps UI refreshes because the search
288                    operations could be heavy */
289                 while (gtk_events_pending ())
290                         gtk_main_iteration ();
291
292                 if ((found)||(nread == 0)) {
293                         break;
294                 }
295
296                 /* also move the \0 */
297                 g_memmove (chunk[0], chunk[1], len + 1);
298         }
299
300 done:
301         g_object_unref (stream);
302         return found;
303 }
304 #endif /*MODEST_HAVE_OGS*/
305
306 static gboolean
307 search_string (const char      *what,
308                const char      *where,
309                ModestSearch    *search)
310 {
311         gboolean found = FALSE;
312 #ifdef MODEST_HAVE_OGS
313         if (search->flags & MODEST_SEARCH_USE_OGS) {
314                 found = ogs_text_searcher_search_text (search->text_searcher,
315                                                        where,
316                                                        strlen (where));
317
318                 ogs_text_searcher_reset (search->text_searcher);
319         } else {
320 #endif
321                 if (what == NULL || where == NULL) {
322                         return FALSE;
323                 }
324
325                 found = !modest_text_utils_utf8_strcmp (what, where, TRUE);
326 #ifdef MODEST_HAVE_OGS
327         }
328 #endif
329
330         /* HACK: this helps UI refreshes because the search
331            operations could be heavy */
332         while (gtk_events_pending ())
333                 gtk_main_iteration ();
334
335         return found;
336 }
337
338
339 static gboolean 
340 search_mime_part_and_child_parts (TnyMimePart *part, ModestSearch *search)
341 {
342         gboolean found = FALSE;
343
344         /* Do not search into attachments */
345         if (modest_tny_mime_part_is_attachment_for_modest (part))
346                 return FALSE;
347
348         #ifdef MODEST_HAVE_OGS
349         found = search_mime_part_ogs (part, search);
350         #else
351         found = search_mime_part_strcmp (part, search);
352         #endif
353
354         if (found) {    
355                 return found;           
356         }
357         
358         /* Check the child part too, recursively: */
359         TnyList *child_parts = tny_simple_list_new ();
360         tny_mime_part_get_parts (TNY_MIME_PART (part), child_parts);
361
362         TnyIterator *piter = tny_list_create_iterator (child_parts);
363         while (!found && !tny_iterator_is_done (piter)) {
364                 TnyMimePart *pcur = (TnyMimePart *) tny_iterator_get_current (piter);
365                 if (pcur) {
366                         found = search_mime_part_and_child_parts (pcur, search);
367
368                         g_object_unref (pcur);
369                 }
370
371                 tny_iterator_next (piter);
372         }
373
374         g_object_unref (piter);
375         g_object_unref (child_parts);
376         
377         return found;
378 }
379
380 static void 
381 modest_search_folder_get_headers_cb (TnyFolder *folder, 
382                                      gboolean cancelled, 
383                                      TnyList *headers, 
384                                      GError *err, 
385                                      gpointer user_data)
386 {
387         TnyIterator *iter = NULL;
388         SearchHelper *helper;
389
390         helper = (SearchHelper *) user_data;
391
392         if (err || cancelled) {
393                 goto end;
394         }
395
396         iter = tny_list_create_iterator (headers);
397
398         while (!tny_iterator_is_done (iter)) {
399
400                 TnyHeader *cur = (TnyHeader *) tny_iterator_get_current (iter);
401                 const time_t t = tny_header_get_date_sent (cur);
402                 gboolean found = FALSE;
403                 
404                 /* Ignore deleted (not yet expunged) emails: */
405                 if (tny_header_get_flags(cur) & TNY_HEADER_FLAG_DELETED)
406                         goto go_next;
407                         
408                 if (helper->search->flags & MODEST_SEARCH_BEFORE)
409                         if (!(t <= helper->search->end_date))
410                                 goto go_next;
411
412                 if (helper->search->flags & MODEST_SEARCH_AFTER)
413                         if (!(t >= helper->search->start_date))
414                                 goto go_next;
415
416                 if (helper->search->flags & MODEST_SEARCH_SIZE)
417                         if (tny_header_get_message_size (cur) < helper->search->minsize)
418                                 goto go_next;
419
420                 if (helper->search->flags & MODEST_SEARCH_SUBJECT) {
421                         const char *str = tny_header_get_subject (cur);
422
423                         if ((found = search_string (helper->search->subject, str, helper->search))) {
424                             helper->msg_hits = add_hit (helper->msg_hits, cur, folder);
425                         }
426                 }
427                 
428                 if (!found && helper->search->flags & MODEST_SEARCH_SENDER) {
429                         char *str = g_strdup (tny_header_get_from (cur));
430
431                         if ((found = search_string (helper->search->from, (const gchar *) str, helper->search))) {
432                                 helper->msg_hits = add_hit (helper->msg_hits, cur, folder);
433                         }
434                         g_free (str);
435                 }
436                 
437                 if (!found && helper->search->flags & MODEST_SEARCH_RECIPIENT) {
438                         const char *str = tny_header_get_to (cur);
439
440                         if ((found = search_string (helper->search->recipient, str, helper->search))) {
441                                 helper->msg_hits = add_hit (helper->msg_hits, cur, folder);
442                         }
443                 }
444         
445                 if (!found && helper->search->flags & MODEST_SEARCH_BODY) {
446                         TnyHeaderFlags flags;
447                         GError      *err = NULL;
448                         TnyMsg      *msg = NULL;
449
450                         flags = tny_header_get_flags (cur);
451
452                         if (!(flags & TNY_HEADER_FLAG_CACHED)) {
453                                 goto go_next;
454                         }
455
456                         msg = tny_folder_get_msg (folder, cur, &err);
457
458                         if (err != NULL || msg == NULL) {
459                                 g_warning ("%s: Could not get message.\n", __FUNCTION__);
460                                 g_error_free (err);
461
462                                 if (msg) {
463                                         g_object_unref (msg);
464                                 }
465                         } else {        
466                                 g_debug ("Searching in %s\n", tny_header_get_subject (cur));
467                         
468                                 found = search_mime_part_and_child_parts (TNY_MIME_PART (msg),
469                                                                           helper->search);
470                                 if (found) {
471                                         helper->msg_hits = add_hit (helper->msg_hits, cur, folder);
472                                 }
473                         }
474                         
475                         if (msg)
476                                 g_object_unref (msg);
477                 }
478         go_next:
479                 g_object_unref (cur);
480                 tny_iterator_next (iter);
481         }
482
483         /* Frees */
484         g_object_unref (iter);
485  end:
486         if (headers)
487                 g_object_unref (headers);
488
489         /* Check search finished */
490         helper->folder_count++;
491         check_search_finished (helper);
492 }
493
494 static void
495 _search_folder (TnyFolder *folder, 
496                 SearchHelper *helper)
497 {
498         TnyList *list = NULL;
499
500         g_debug ("%s: searching folder %s.", __FUNCTION__, tny_folder_get_name (folder));
501         
502         /* Check that we should be searching this folder. */
503         /* Note that we don't try to search sub-folders. 
504          * Maybe we should, but that should be specified. */
505         if (helper->search->folder && strlen (helper->search->folder)) {
506                 if (!strcmp (helper->search->folder, "outbox")) {
507                         if (modest_tny_folder_guess_folder_type (folder) != TNY_FOLDER_TYPE_OUTBOX) {
508                                 modest_search_folder_get_headers_cb (folder, TRUE, NULL, NULL, helper); 
509                                 return;
510                         }
511                 } else if (strcmp (tny_folder_get_id (folder), helper->search->folder) != 0) {
512                         modest_search_folder_get_headers_cb (folder, TRUE, NULL, NULL, helper); 
513                         return;
514                 }
515         }
516         
517 #ifdef MODEST_HAVE_OGS
518         if (helper->search->flags & MODEST_SEARCH_USE_OGS) {
519         
520                 if (helper->search->text_searcher == NULL && helper->search->query != NULL) {
521                         OgsTextSearcher *text_searcher; 
522
523                         text_searcher = ogs_text_searcher_new (FALSE);
524                         ogs_text_searcher_parse_query (text_searcher, helper->search->query);
525                         helper->search->text_searcher = text_searcher;
526                 }
527         }
528 #endif
529         list = tny_simple_list_new ();
530         /* Get the headers */
531         tny_folder_get_headers_async (folder, list, FALSE, 
532                                       modest_search_folder_get_headers_cb, 
533                                       NULL, helper);
534 }
535
536 void
537 modest_search_folder (TnyFolder *folder, 
538                       ModestSearch *search,
539                       ModestSearchCallback callback,
540                       gpointer user_data)
541 {
542         SearchHelper *helper;
543
544         /* Create the helper */
545         helper = create_helper (callback, search, user_data);
546
547         /* Search */
548         _search_folder (folder, helper);
549 }
550
551 static void
552 modest_search_account_get_folders_cb (TnyFolderStore *self, 
553                                       gboolean cancelled, 
554                                       TnyList *folders, 
555                                       GError *err, 
556                                       gpointer user_data)
557 {
558         TnyIterator *iter;
559         SearchHelper *helper;
560
561         helper = (SearchHelper *) user_data;
562
563         if (err || cancelled) {
564                 goto end;
565         }
566
567         iter = tny_list_create_iterator (folders);
568         while (!tny_iterator_is_done (iter)) {
569                 TnyFolder *folder = NULL;
570
571                 /* Search into folder */
572                 folder = TNY_FOLDER (tny_iterator_get_current (iter));  
573                 helper->folder_total++;
574                 _search_folder (folder, (SearchHelper *) user_data);
575                 g_object_unref (folder);
576
577                 tny_iterator_next (iter);
578         }
579         g_object_unref (iter);
580  end:
581         if (folders)
582                 g_object_unref (folders);
583
584         /* Check search finished */
585         check_search_finished (helper);
586 }
587
588 static void
589 _search_account (TnyAccount *account, 
590                  SearchHelper *helper)
591 {
592         TnyList *folders = tny_simple_list_new ();
593
594         g_debug ("%s: Searching account %s", __FUNCTION__, tny_account_get_name (account));
595
596         /* Get folders */
597         tny_folder_store_get_folders_async (TNY_FOLDER_STORE (account), folders, NULL, 
598                                             modest_search_account_get_folders_cb, 
599                                             NULL, helper);
600 }
601
602 void
603 modest_search_account (TnyAccount *account, 
604                        ModestSearch *search,
605                        ModestSearchCallback callback,
606                        gpointer user_data)
607 {
608         SearchHelper *helper;
609
610         /* Create the helper */
611         helper = create_helper (callback, search, user_data);
612
613         /* Search */
614         _search_account (account, helper);
615 }
616
617 void
618 modest_search_all_accounts (ModestSearch *search,
619                             ModestSearchCallback callback,
620                             gpointer user_data)
621 {
622         ModestTnyAccountStore *astore;
623         TnyList *accounts;
624         TnyIterator *iter;
625         GList *hits;
626         SearchHelper *helper;
627
628         hits = NULL;
629         astore = modest_runtime_get_account_store ();
630
631         accounts = tny_simple_list_new ();
632         tny_account_store_get_accounts (TNY_ACCOUNT_STORE (astore),
633                                         accounts,
634                                         TNY_ACCOUNT_STORE_STORE_ACCOUNTS);
635
636         /* Create the helper */
637         helper = create_helper (callback, search, user_data);
638
639         /* Search through all accounts */
640         iter = tny_list_create_iterator (accounts);
641         while (!tny_iterator_is_done (iter)) {
642                 TnyAccount *account = NULL;
643
644                 account = TNY_ACCOUNT (tny_iterator_get_current (iter));
645                 _search_account (account, helper);
646                 g_object_unref (account);
647
648                 tny_iterator_next (iter);
649         }
650         g_object_unref (iter);
651         g_object_unref (accounts);
652 }
653
654 static SearchHelper *
655 create_helper (ModestSearchCallback callback, 
656                ModestSearch *search,
657                gpointer user_data)
658 {
659         SearchHelper *helper;
660
661         helper = g_slice_new0 (SearchHelper);
662         helper->folder_count = 0;
663         helper->folder_total = 0;
664         helper->search = search;
665         helper->callback = callback;
666         helper->user_data = user_data;
667         helper->msg_hits = NULL;
668
669         return helper;
670 }
671
672 void 
673 modest_search_free (ModestSearch *search)
674 {
675         if (search->folder)
676                 g_free (search->folder);
677         if (search->subject)
678                 g_free (search->subject);
679         if (search->from)
680                 g_free (search->from);
681         if (search->recipient)
682                 g_free (search->recipient);
683         if (search->body)
684                 g_free (search->body);
685
686 #ifdef MODEST_HAVE_OGS
687         if (search->query)
688                 g_free (search->query);
689         if (search->text_searcher)
690                 ogs_text_searcher_free (search->text_searcher); 
691 #endif
692 }
693
694 static void
695 check_search_finished (SearchHelper *helper)
696 {
697         /* If there are no more folders to check the account search has finished */
698         if (helper->folder_count == helper->folder_total) {
699                 /* callback */
700                 helper->callback (helper->msg_hits, helper->user_data);
701                 
702                 /* free helper */
703                 g_list_free (helper->msg_hits);
704                 g_slice_free (SearchHelper, helper);
705         }
706 }