0b2db46acc26f7a4034b99c3070ba5288fb82940
[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
47 #include <libmodest-dbus-client/libmodest-dbus-client.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-search.h"
54 #include "modest-runtime.h"
55
56 static gchar *
57 g_strdup_or_null (const gchar *str)
58 {
59         gchar *string = NULL;
60
61         if  (str != NULL) {
62                 string = g_strdup (str);
63         }
64
65         return string;
66 }
67
68 typedef struct
69 {
70         GMainLoop* loop;
71         TnyAccount *account;
72         gboolean is_online;
73         gint count_tries;
74 } UtilIdleData;
75
76 #define NUMBER_OF_TRIES 10 /* Try approx every second, ten times. */
77
78 static gboolean 
79 on_timeout_check_account_is_online(gpointer user_data)
80 {
81         printf ("DEBUG: %s:\n", __FUNCTION__);
82         UtilIdleData *data = (UtilIdleData*)user_data;
83         
84         if (!data) {
85                 g_warning ("%s: data is NULL.\n", __FUNCTION__);
86         }
87         
88         if (!(data->account)) {
89                 g_warning ("%s: data->account is NULL.\n", __FUNCTION__);
90         }
91         
92         if (data && data->account) {
93                 printf ("%s: tny_account_get_connection_status()==%d\n", __FUNCTION__, tny_account_get_connection_status (data->account));      
94         }
95         
96         gboolean stop_trying = FALSE;
97         if (data && data->account && 
98                 (tny_account_get_connection_status (data->account) == TNY_CONNECTION_STATUS_CONNECTED) )
99         {
100                 data->is_online = TRUE;
101                 
102                 stop_trying = TRUE;
103         }
104         else {
105                 /* Give up if we have tried too many times: */
106                 if (data->count_tries >= NUMBER_OF_TRIES)
107                 {
108                         stop_trying = TRUE;
109                 }
110                 else {
111                         /* Wait for another timeout: */
112                         ++(data->count_tries);
113                 }
114         }
115         
116         if (stop_trying) {
117                 /* Allow the function that requested this idle callback to continue: */
118                 if (data->loop)
119                         g_main_loop_quit (data->loop);
120                 
121                 return FALSE; /* Don't call this again. */
122         } else {
123                 return TRUE; /* Call this timeout callback again. */
124         }
125 }
126
127 /* Return TRUE immediately if the account is already online,
128  * otherwise check every second for NUMBER_OF_TRIES seconds and return TRUE as 
129  * soon as the account is online, or FALSE if the account does 
130  * not become online in the NUMBER_OF_TRIES seconds.
131  * This is useful when the D-Bus method was run immediately after 
132  * the application was started (when using D-Bus activation), 
133  * because the account usually takes a short time to go online.
134  * The return value is maybe not very useful.
135  */
136 static gboolean
137 check_and_wait_for_account_is_online(TnyAccount *account)
138 {
139         if (!tny_device_is_online (modest_runtime_get_device())) {
140                 printf ("%s: device is offline.\n", __FUNCTION__);
141                 return FALSE;
142         }
143                 
144         printf ("%s: tny_account_get_connection_status()==%d\n", __FUNCTION__, tny_account_get_connection_status (account));
145         
146         if (tny_account_get_connection_status (account) == TNY_CONNECTION_STATUS_CONNECTED)
147                 return TRUE;
148                 
149         /* This blocks on the result: */
150         UtilIdleData *data = g_slice_new0 (UtilIdleData);
151         data->is_online = FALSE;
152                 
153         GMainContext *context = NULL; /* g_main_context_new (); */
154         data->loop = g_main_loop_new (context, FALSE /* not running */);
155
156         g_timeout_add (1000, &on_timeout_check_account_is_online, data);
157
158         /* This main loop will run until the idle handler has stopped it: */
159         g_main_loop_run (data->loop);
160
161         g_main_loop_unref (data->loop);
162         /* g_main_context_unref (context); */
163
164         g_slice_free (UtilIdleData, data);
165         
166         return data->is_online; 
167 }
168
169 static GList*
170 add_hit (GList *list, TnyHeader *header, TnyFolder *folder)
171 {
172         ModestSearchHit *hit;
173         TnyHeaderFlags   flags;
174         char            *furl;
175         char            *msg_url;
176         const char      *uid;
177         const char      *subject;
178         const char      *sender;
179
180         hit = g_slice_new0 (ModestSearchHit);
181
182         furl = tny_folder_get_url_string (folder);
183         printf ("DEBUG: %s: folder URL=%s\n", __FUNCTION__, furl);
184         if (!furl) {
185                 g_warning ("%s: tny_folder_get_url_string(): returned NULL for folder. Folder name=%s\n", __FUNCTION__, tny_folder_get_name (folder));
186         }
187         
188         /* Make sure that we use the short UID instead of the long UID,
189          * and/or find out what UID form is used when finding, in camel_data_cache_get().
190          * so we can find what we get. Philip is working on this.
191          */
192         uid = tny_header_get_uid (header);
193         if (!furl) {
194                 g_warning ("%s: tny_header_get_uid(): returned NULL for message with subject=%s\n", __FUNCTION__, tny_header_get_subject (header));
195         }
196         
197         msg_url = g_strdup_printf ("%s/%s", furl, uid);
198         g_free (furl);
199         
200         subject = tny_header_get_subject (header);
201         sender = tny_header_get_from (header);
202         
203         flags = tny_header_get_flags (header);
204
205         hit->msgid = msg_url;
206         hit->subject = g_strdup_or_null (subject);
207         hit->sender = g_strdup_or_null (sender);
208         hit->folder = g_strdup_or_null (tny_folder_get_name (folder));
209         hit->msize = tny_header_get_message_size (header);
210         hit->has_attachment = flags & TNY_HEADER_FLAG_ATTACHMENTS;
211         hit->is_unread = ! (flags & TNY_HEADER_FLAG_SEEN);
212         hit->timestamp = tny_header_get_date_received (header);
213         
214         return g_list_prepend (list, hit);
215 }
216
217 /** Call this until it returns FALSE or nread is set to 0.
218  * 
219  * @result: FALSE is something failed. */
220 static gboolean
221 read_chunk (TnyStream *stream, char *buffer, gsize count, gsize *nread)
222 {
223         gsize _nread = 0;
224         gssize res = 0;
225
226         while (_nread < count) {
227                 res = tny_stream_read (stream,
228                                        buffer + _nread, 
229                                        count - _nread);
230                 if (res == -1) { /* error */
231                         *nread = _nread;
232                         return FALSE;
233                 }
234
235                 _nread += res;
236                 
237                 if (res == 0) { /* no more bytes read. */
238                         *nread = _nread;
239                         return TRUE;
240                 }
241         }
242
243         *nread = _nread;
244         return TRUE;
245 }
246
247 #ifdef MODEST_HAVE_OGS
248 static gboolean
249 search_mime_part_ogs (TnyMimePart *part, ModestSearch *search)
250 {
251         TnyStream *stream = NULL;
252         char       buffer[4096];
253         const gsize len = sizeof (buffer);
254         gsize      nread = 0;
255         gboolean   is_text_html = FALSE;
256         gboolean   found = FALSE;
257         gboolean   res = FALSE;
258
259         gboolean is_text = tny_mime_part_content_type_is (part, "text/*");
260         if (!is_text) {
261                 g_debug ("%s: tny_mime_part_content_type_is() failed to find a "
262                         "text/* MIME part. Content type is %s", 
263                 __FUNCTION__, "Unknown (calling tny_mime_part_get_content_type(part) causes a deadlock)");
264                 
265             /* Retry with specific MIME types, because the wildcard seems to fail
266              * in tinymail.
267              * Actually I'm not sure anymore that it fails, so we could probalby 
268              * remove this later: murrayc */
269             is_text = (
270                 tny_mime_part_content_type_is (part, "text/plain") ||
271                 tny_mime_part_content_type_is (part, "text/html") );
272                 
273                 if (is_text) {
274                   g_debug ("%s: Retryting with text/plain or text/html succeeded", 
275                         __FUNCTION__);  
276                 }
277         }
278         
279         if (!is_text) {
280             return FALSE;
281         }
282         
283         is_text_html = tny_mime_part_content_type_is (part, "text/html");
284
285         stream = tny_mime_part_get_stream (part);
286
287         res = read_chunk (stream, buffer, len, &nread);
288         while (res && (nread > 0)) {
289                 /* search->text_searcher was instantiated in modest_search_folder(). */
290                 
291                 if (is_text_html) {
292
293                         found = ogs_text_searcher_search_html (search->text_searcher,
294                                                                buffer,
295                                                                nread,
296                                                                nread < len);
297                 } else {
298                         found = ogs_text_searcher_search_text (search->text_searcher,
299                                                                buffer,
300                                                                nread);
301                 }
302
303                 if (found) {
304                         break;
305                 }
306                 
307                 nread = 0;
308                 res = read_chunk (stream, buffer, len, &nread);
309         }
310
311         if (!found) {
312                 found = ogs_text_searcher_search_done (search->text_searcher);
313         }
314
315         ogs_text_searcher_reset (search->text_searcher);
316         
317         /* debug stuff:
318         if (!found) {
319                 buffer[len -1] = 0;
320                 printf ("DEBUG: %s: query %s was not found in message text: %s\n", 
321                         __FUNCTION__, search->query, buffer);   
322                 
323         } else {
324                 printf ("DEBUG: %s: found.\n", __FUNCTION__);   
325         }
326         */
327
328         return found;
329 }
330
331 #else
332
333 static gboolean
334 search_mime_part_strcmp (TnyMimePart *part, ModestSearch *search)
335 {
336         TnyStream *stream;
337         char       buffer[8193];
338         char      *chunk[2];
339         gssize     len;
340         gsize     nread;
341         gboolean   found;
342         gboolean   res;
343
344         if (! tny_mime_part_content_type_is (part, "text/*")) {
345                 g_debug ("%s: No text MIME part found.\n", __FUNCTION__);
346                 return FALSE;
347         }
348
349         found = FALSE;
350         len = (sizeof (buffer) - 1) / 2;
351
352         if (strlen (search->body) > len) {
353                 g_warning ("Search term bigger then chunk."
354                            "We might not find everything");     
355         }
356
357         stream = tny_mime_part_get_stream (part);
358
359         memset (buffer, 0, sizeof (buffer));
360         chunk[0] = buffer;
361         chunk[1] = buffer + len;
362
363         res = read_chunk (stream, chunk[0], len, &nread);
364
365         if (res == FALSE) {
366                 goto done;
367         }
368
369         found = !modest_text_utils_utf8_strcmp (search->body,
370                                                 buffer,
371                                                 TRUE);
372         if (found) {
373                 goto done;
374         }
375
376         /* This works like this:
377          * buffer: [ooooooooooo|xxxxxxxxxxxx|\0] 
378          *          ^chunk[0]  ^chunk[1]
379          * we have prefilled chunk[0] now we always read into chunk[1]
380          * and then move the content of chunk[1] to chunk[0].
381          * The idea is to prevent not finding search terms that are
382          * spread across 2 reads:        
383          * buffer: [ooooooooTES|Txxxxxxxxxxx|\0] 
384          * We should catch that because we always search the whole
385          * buffer not only the chunks.
386          *
387          * Of course that breaks for search terms > sizeof (chunk)
388          * but sizeof (chunk) should be big enough I guess (see
389          * the g_warning in this function)
390          * */   
391         while ((res = read_chunk (stream, chunk[1], len, &nread))) {
392                 buffer[len + nread] = '\0';
393
394                 found = !modest_text_utils_utf8_strcmp (search->body,
395                                                         buffer,
396                                                         TRUE);
397
398                 if (found) {
399                         break;
400                 }
401
402                 /* also move the \0 */
403                 g_memmove (chunk[0], chunk[1], len + 1);
404         }
405
406 done:
407         g_object_unref (stream);
408         return found;
409 }
410 #endif /*MODEST_HAVE_OGS*/
411
412 static gboolean
413 search_string (const char      *what,
414                const char      *where,
415                ModestSearch    *search)
416 {
417         gboolean found;
418 #ifdef MODEST_HAVE_OGS
419         if (search->flags & MODEST_SEARCH_USE_OGS) {
420                 found = ogs_text_searcher_search_text (search->text_searcher,
421                                                        where,
422                                                        strlen (where));
423
424                 ogs_text_searcher_reset (search->text_searcher);
425         } else {
426 #endif
427                 if (what == NULL || where == NULL) {
428                         return FALSE;
429                 }
430
431                 found = !modest_text_utils_utf8_strcmp (what, where, TRUE);
432 #ifdef MODEST_HAVE_OGS
433         }
434 #endif
435         return found;
436 }
437
438
439 static gboolean search_mime_part_and_child_parts (TnyMimePart *part, ModestSearch *search)
440 {
441         gboolean found = FALSE;
442         #ifdef MODEST_HAVE_OGS
443         found = search_mime_part_ogs (part, search);
444         #else
445         found = search_mime_part_strcmp (part, search);
446         #endif
447
448         if (found) {    
449                 return found;           
450         }
451         
452         /* Check the child part too, recursively: */
453         TnyList *child_parts = tny_simple_list_new ();
454         tny_mime_part_get_parts (TNY_MIME_PART (part), child_parts);
455
456         TnyIterator *piter = tny_list_create_iterator (child_parts);
457         while (!found && !tny_iterator_is_done (piter)) {
458                 TnyMimePart *pcur = (TnyMimePart *) tny_iterator_get_current (piter);
459                 if (pcur) {
460                         found = search_mime_part_and_child_parts (pcur, search);
461
462                         g_object_unref (pcur);
463                 }
464
465                 tny_iterator_next (piter);
466         }
467
468         g_object_unref (piter);
469         g_object_unref (child_parts);
470         
471         return found;
472 }
473
474 /**
475  * modest_search:
476  * @folder: a #TnyFolder instance
477  * @search: a #ModestSearch query
478  *
479  * This operation will search @folder for headers that match the query @search,
480  * if the folder itself matches the query.
481  * It will return a doubly linked list with URIs that point to the message.
482  **/
483 GList *
484 modest_search_folder (TnyFolder *folder, ModestSearch *search)
485 {
486         /* Check that we should be searching this folder. */
487         /* Note that we don't try to search sub-folders. 
488          * Maybe we should, but that should be specified. */
489         if (search->folder && strlen (search->folder) && (strcmp (tny_folder_get_id (folder), search->folder) != 0))
490                 return NULL;
491         
492         GList *retval = NULL;
493         TnyIterator *iter = NULL;
494         TnyList *list = NULL;
495         
496 #ifdef MODEST_HAVE_OGS
497         if (search->flags & MODEST_SEARCH_USE_OGS) {
498         
499                 if (search->text_searcher == NULL && search->query != NULL) {
500                         OgsTextSearcher *text_searcher; 
501
502                         text_searcher = ogs_text_searcher_new (FALSE);
503                         ogs_text_searcher_parse_query (text_searcher, search->query);
504                         search->text_searcher = text_searcher;
505                 }
506         }
507 #endif
508
509         list = tny_simple_list_new ();
510         GError *error = NULL;
511         tny_folder_get_headers (folder, list, FALSE /* don't refresh */, &error);
512         if (error) {
513                 g_warning ("%s: tny_folder_get_headers() failed with error=%s.\n", 
514                 __FUNCTION__, error->message);
515                 g_error_free (error);
516                 error = NULL;   
517         }
518
519         iter = tny_list_create_iterator (list);
520
521         while (!tny_iterator_is_done (iter)) {
522                 TnyHeader *cur = (TnyHeader *) tny_iterator_get_current (iter);
523                 const time_t t = tny_header_get_date_sent (cur);
524                 gboolean found = FALSE;
525                 
526                 /* Ignore deleted (not yet expunged) emails: */
527                 if (tny_header_get_flags(cur) & TNY_HEADER_FLAG_DELETED)
528                         goto go_next;
529                         
530                 if (search->flags & MODEST_SEARCH_BEFORE)
531                         if (!(t <= search->end_date))
532                                 goto go_next;
533
534                 if (search->flags & MODEST_SEARCH_AFTER)
535                         if (!(t >= search->start_date))
536                                 goto go_next;
537
538                 if (search->flags & MODEST_SEARCH_SIZE)
539                         if (tny_header_get_message_size (cur) < search->minsize)
540                                 goto go_next;
541
542                 if (search->flags & MODEST_SEARCH_SUBJECT) {
543                         const char *str = tny_header_get_subject (cur);
544
545                         if ((found = search_string (search->subject, str, search))) {
546                             retval = add_hit (retval, cur, folder);
547                         }
548                 }
549                 
550                 if (!found && search->flags & MODEST_SEARCH_SENDER) {
551                         char *str = g_strdup (tny_header_get_from (cur));
552
553                         if ((found = search_string (search->from, (const gchar *) str, search))) {
554                                 retval = add_hit (retval, cur, folder);
555                         }
556                         g_free (str);
557                 }
558                 
559                 if (!found && search->flags & MODEST_SEARCH_RECIPIENT) {
560                         const char *str = tny_header_get_to (cur);
561
562                         if ((found = search_string (search->recipient, str, search))) {
563                                 retval = add_hit (retval, cur, folder);
564                         }
565                 }
566         
567                 if (!found && search->flags & MODEST_SEARCH_BODY) {
568                         TnyHeaderFlags flags;
569                         GError      *err = NULL;
570                         TnyMsg      *msg = NULL;
571
572                         flags = tny_header_get_flags (cur);
573
574                         if (!(flags & TNY_HEADER_FLAG_CACHED)) {
575                                 goto go_next;
576                         }
577
578                         msg = tny_folder_get_msg (folder, cur, &err);
579
580                         if (err != NULL || msg == NULL) {
581                                 g_warning ("%s: Could not get message.\n", __FUNCTION__);
582                                 g_error_free (err);
583
584                                 if (msg) {
585                                         g_object_unref (msg);
586                                 }
587                         } else {        
588                         
589                                 found = search_mime_part_and_child_parts (TNY_MIME_PART (msg), 
590                                                                           search);
591                                 if (found) {
592                                         retval = add_hit (retval, cur, folder);
593                                 }
594                         }
595                         
596                         if (msg)
597                                 g_object_unref (msg);
598                 }
599
600 go_next:
601                 g_object_unref (cur);
602                 tny_iterator_next (iter);
603         }
604
605         g_object_unref (iter);
606         g_object_unref (list);
607         return retval;
608 }
609
610 GList *
611 modest_search_account (TnyAccount *account, ModestSearch *search)
612 {
613         TnyFolderStore      *store;
614         TnyIterator         *iter;
615         TnyList             *folders;
616         GList               *hits;
617         GError              *error;
618
619         error = NULL;
620         hits = NULL;
621
622         store = TNY_FOLDER_STORE (account);
623
624         folders = tny_simple_list_new ();
625         tny_folder_store_get_folders (store, folders, NULL, &error);
626         
627         if (error != NULL) {
628                 g_object_unref (folders);
629                 return NULL;
630         }
631
632         iter = tny_list_create_iterator (folders);
633         while (!tny_iterator_is_done (iter)) {
634                 TnyFolder *folder = NULL;
635                 GList     *res = NULL;
636
637                 folder = TNY_FOLDER (tny_iterator_get_current (iter));
638                 if (folder) {
639                         /* g_debug ("DEBUG: %s: searching folder %s.", 
640                                 __FUNCTION__, tny_folder_get_name (folder)); */
641                 
642                         res = modest_search_folder (folder, search);
643
644                         if (res != NULL) {
645                                 if (hits == NULL) {
646                                         hits = res;
647                                 } else {
648                                         hits = g_list_concat (hits, res);
649                                 }
650                         }
651
652                         g_object_unref (folder);
653                 }
654
655                 tny_iterator_next (iter);
656         }
657
658         g_object_unref (iter);
659         g_object_unref (folders);
660
661         /* printf ("DEBUG: %s: hits length = %d\n", __FUNCTION__, g_list_length (hits)); */
662         return hits;
663 }
664
665 GList *
666 modest_search_all_accounts (ModestSearch *search)
667 {
668         /* printf ("DEBUG: %s: query=%s\n", __FUNCTION__, search->query); */
669         ModestTnyAccountStore *astore;
670         TnyList               *accounts;
671         TnyIterator           *iter;
672         GList                 *hits;
673
674         hits = NULL;
675         astore = modest_runtime_get_account_store ();
676
677         accounts = tny_simple_list_new ();
678         tny_account_store_get_accounts (TNY_ACCOUNT_STORE (astore),
679                                         accounts,
680                                         TNY_ACCOUNT_STORE_STORE_ACCOUNTS);
681
682         iter = tny_list_create_iterator (accounts);
683         while (!tny_iterator_is_done (iter)) {
684                 TnyAccount *account = NULL;
685                 GList      *res = NULL;
686
687                 account = TNY_ACCOUNT (tny_iterator_get_current (iter));
688                 if (account) {
689                         /* g_debug ("DEBUG: %s: Searching account %s",
690                          __FUNCTION__, tny_account_get_name (account)); */
691                          
692                         /* Give the account time to go online if necessary, 
693                          * for instance if this is immediately after startup,
694                          * after D-Bus activation: */
695                         check_and_wait_for_account_is_online (account);
696                         
697                         /* Search: */
698                         res = modest_search_account (account, search);
699                         
700                         if (res != NULL) {      
701                                 if (hits == NULL) {
702                                         hits = res;
703                                 } else {
704                                         hits = g_list_concat (hits, res);
705                                 }
706                         }
707                         
708                         g_object_unref (account);
709                 }
710
711                 tny_iterator_next (iter);
712         }
713
714         g_object_unref (accounts);
715         g_object_unref (iter);
716
717         /* printf ("DEBUG: %s: end: hits length=%d\n", __FUNCTION__, g_list_length(hits)); */
718         return hits;
719 }
720
721