* More license declarations
[citadel.git] / citadel / modules / imap / imap_search.c
1 /*
2  * $Id$
3  *
4  * Implements IMAP's gratuitously complex SEARCH command.
5  *
6  *
7  * Copyright (c) 2001-2009 by the citadel.org team
8  *
9  *  This program is free software; you can redistribute it and/or modify
10  *  it under the terms of the GNU General Public License as published by
11  *  the Free Software Foundation; either version 3 of the License, or
12  *  (at your option) any later version.
13  *
14  *  This program is distributed in the hope that it will be useful,
15  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
16  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  *  GNU General Public License for more details.
18  *
19  *  You should have received a copy of the GNU General Public License
20  *  along with this program; if not, write to the Free Software
21  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22  */
23
24 #include "sysdep.h"
25 #include <stdlib.h>
26 #include <unistd.h>
27 #include <stdio.h>
28 #include <fcntl.h>
29 #include <signal.h>
30 #include <pwd.h>
31 #include <errno.h>
32 #include <sys/types.h>
33
34 #if TIME_WITH_SYS_TIME
35 # include <sys/time.h>
36 # include <time.h>
37 #else
38 # if HAVE_SYS_TIME_H
39 #  include <sys/time.h>
40 # else
41 #  include <time.h>
42 # endif
43 #endif
44
45 #include <sys/wait.h>
46 #include <ctype.h>
47 #include <string.h>
48 #include <limits.h>
49 #include <libcitadel.h>
50 #include "citadel.h"
51 #include "server.h"
52 #include "sysdep_decls.h"
53 #include "citserver.h"
54 #include "support.h"
55 #include "config.h"
56 #include "room_ops.h"
57 #include "user_ops.h"
58 #include "policy.h"
59 #include "database.h"
60 #include "msgbase.h"
61 #include "internet_addressing.h"
62 #include "serv_imap.h"
63 #include "imap_tools.h"
64 #include "imap_fetch.h"
65 #include "imap_search.h"
66 #include "genstamp.h"
67
68
69 /*
70  * imap_do_search() calls imap_do_search_msg() to search an individual
71  * message after it has been fetched from the disk.  This function returns
72  * nonzero if there is a match.
73  *
74  * supplied_msg MAY be used to pass a pointer to the message in memory,
75  * if for some reason it's already been loaded.  If not, the message will
76  * be loaded only if one or more search criteria require it.
77  */
78 int imap_do_search_msg(int seq, struct CtdlMessage *supplied_msg,
79                         int num_items, char **itemlist, int is_uid) {
80
81         int match = 0;
82         int is_not = 0;
83         int is_or = 0;
84         int pos = 0;
85         int i;
86         char *fieldptr;
87         struct CtdlMessage *msg = NULL;
88         int need_to_free_msg = 0;
89
90         if (num_items == 0) {
91                 return(0);
92         }
93         msg = supplied_msg;
94
95         /* Initially we start at the beginning. */
96         pos = 0;
97
98         /* Check for the dreaded NOT criterion. */
99         if (!strcasecmp(itemlist[0], "NOT")) {
100                 is_not = 1;
101                 pos = 1;
102         }
103
104         /* Check for the dreaded OR criterion. */
105         if (!strcasecmp(itemlist[0], "OR")) {
106                 is_or = 1;
107                 pos = 1;
108         }
109
110         /* Now look for criteria. */
111         if (!strcasecmp(itemlist[pos], "ALL")) {
112                 match = 1;
113                 ++pos;
114         }
115         
116         else if (!strcasecmp(itemlist[pos], "ANSWERED")) {
117                 if (IMAP->flags[seq-1] & IMAP_ANSWERED) {
118                         match = 1;
119                 }
120                 ++pos;
121         }
122
123         else if (!strcasecmp(itemlist[pos], "BCC")) {
124                 if (msg == NULL) {
125                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
126                         need_to_free_msg = 1;
127                 }
128                 if (msg != NULL) {
129                         fieldptr = rfc822_fetch_field(msg->cm_fields['M'], "Bcc");
130                         if (fieldptr != NULL) {
131                                 if (bmstrcasestr(fieldptr, itemlist[pos+1])) {
132                                         match = 1;
133                                 }
134                                 free(fieldptr);
135                         }
136                 }
137                 pos += 2;
138         }
139
140         else if (!strcasecmp(itemlist[pos], "BEFORE")) {
141                 if (msg == NULL) {
142                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
143                         need_to_free_msg = 1;
144                 }
145                 if (msg != NULL) {
146                         if (msg->cm_fields['T'] != NULL) {
147                                 if (imap_datecmp(itemlist[pos+1],
148                                                 atol(msg->cm_fields['T'])) < 0) {
149                                         match = 1;
150                                 }
151                         }
152                 }
153                 pos += 2;
154         }
155
156         else if (!strcasecmp(itemlist[pos], "BODY")) {
157
158                 /* If fulltext indexing is active, on this server,
159                  *  all messages have already been qualified.
160                  */
161                 if (config.c_enable_fulltext) {
162                         match = 1;
163                 }
164
165                 /* Otherwise, we have to do a slow search. */
166                 else {
167                         if (msg == NULL) {
168                                 msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
169                                 need_to_free_msg = 1;
170                         }
171                         if (msg != NULL) {
172                                 if (bmstrcasestr(msg->cm_fields['M'], itemlist[pos+1])) {
173                                         match = 1;
174                                 }
175                         }
176                 }
177
178                 pos += 2;
179         }
180
181         else if (!strcasecmp(itemlist[pos], "CC")) {
182                 if (msg == NULL) {
183                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
184                         need_to_free_msg = 1;
185                 }
186                 if (msg != NULL) {
187                         fieldptr = msg->cm_fields['Y'];
188                         if (fieldptr != NULL) {
189                                 if (bmstrcasestr(fieldptr, itemlist[pos+1])) {
190                                         match = 1;
191                                 }
192                         }
193                         else {
194                                 fieldptr = rfc822_fetch_field(msg->cm_fields['M'], "Cc");
195                                 if (fieldptr != NULL) {
196                                         if (bmstrcasestr(fieldptr, itemlist[pos+1])) {
197                                                 match = 1;
198                                         }
199                                         free(fieldptr);
200                                 }
201                         }
202                 }
203                 pos += 2;
204         }
205
206         else if (!strcasecmp(itemlist[pos], "DELETED")) {
207                 if (IMAP->flags[seq-1] & IMAP_DELETED) {
208                         match = 1;
209                 }
210                 ++pos;
211         }
212
213         else if (!strcasecmp(itemlist[pos], "DRAFT")) {
214                 if (IMAP->flags[seq-1] & IMAP_DRAFT) {
215                         match = 1;
216                 }
217                 ++pos;
218         }
219
220         else if (!strcasecmp(itemlist[pos], "FLAGGED")) {
221                 if (IMAP->flags[seq-1] & IMAP_FLAGGED) {
222                         match = 1;
223                 }
224                 ++pos;
225         }
226
227         else if (!strcasecmp(itemlist[pos], "FROM")) {
228                 if (msg == NULL) {
229                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
230                         need_to_free_msg = 1;
231                 }
232                 if (msg != NULL) {
233                         if (bmstrcasestr(msg->cm_fields['A'], itemlist[pos+1])) {
234                                 match = 1;
235                         }
236                         if (bmstrcasestr(msg->cm_fields['F'], itemlist[pos+1])) {
237                                 match = 1;
238                         }
239                 }
240                 pos += 2;
241         }
242
243         else if (!strcasecmp(itemlist[pos], "HEADER")) {
244
245                 /* We've got to do a slow search for this because the client
246                  * might be asking for an RFC822 header field that has not been
247                  * converted into a Citadel header field.  That requires
248                  * examining the message body.
249                  */
250                 if (msg == NULL) {
251                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
252                         need_to_free_msg = 1;
253                 }
254
255                 if (msg != NULL) {
256         
257                         CC->redirect_buffer = malloc(SIZ);
258                         CC->redirect_len = 0;
259                         CC->redirect_alloc = SIZ;
260                         CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_FAST, 0, 1, 0);
261         
262                         fieldptr = rfc822_fetch_field(CC->redirect_buffer, itemlist[pos+1]);
263                         if (fieldptr != NULL) {
264                                 if (bmstrcasestr(fieldptr, itemlist[pos+2])) {
265                                         match = 1;
266                                 }
267                                 free(fieldptr);
268                         }
269         
270                         free(CC->redirect_buffer);
271                         CC->redirect_buffer = NULL;
272                         CC->redirect_len = 0;
273                         CC->redirect_alloc = 0;
274                 }
275
276                 pos += 3;       /* Yes, three */
277         }
278
279         else if (!strcasecmp(itemlist[pos], "KEYWORD")) {
280                 /* not implemented */
281                 pos += 2;
282         }
283
284         else if (!strcasecmp(itemlist[pos], "LARGER")) {
285                 if (msg == NULL) {
286                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
287                         need_to_free_msg = 1;
288                 }
289                 if (msg != NULL) {
290                         if (strlen(msg->cm_fields['M']) > atoi(itemlist[pos+1])) {
291                                 match = 1;
292                         }
293                 }
294                 pos += 2;
295         }
296
297         else if (!strcasecmp(itemlist[pos], "NEW")) {
298                 if ( (IMAP->flags[seq-1] & IMAP_RECENT) && (!(IMAP->flags[seq-1] & IMAP_SEEN))) {
299                         match = 1;
300                 }
301                 ++pos;
302         }
303
304         else if (!strcasecmp(itemlist[pos], "OLD")) {
305                 if (!(IMAP->flags[seq-1] & IMAP_RECENT)) {
306                         match = 1;
307                 }
308                 ++pos;
309         }
310
311         else if (!strcasecmp(itemlist[pos], "ON")) {
312                 if (msg == NULL) {
313                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
314                         need_to_free_msg = 1;
315                 }
316                 if (msg != NULL) {
317                         if (msg->cm_fields['T'] != NULL) {
318                                 if (imap_datecmp(itemlist[pos+1],
319                                                 atol(msg->cm_fields['T'])) == 0) {
320                                         match = 1;
321                                 }
322                         }
323                 }
324                 pos += 2;
325         }
326
327         else if (!strcasecmp(itemlist[pos], "RECENT")) {
328                 if (IMAP->flags[seq-1] & IMAP_RECENT) {
329                         match = 1;
330                 }
331                 ++pos;
332         }
333
334         else if (!strcasecmp(itemlist[pos], "SEEN")) {
335                 if (IMAP->flags[seq-1] & IMAP_SEEN) {
336                         match = 1;
337                 }
338                 ++pos;
339         }
340
341         else if (!strcasecmp(itemlist[pos], "SENTBEFORE")) {
342                 if (msg == NULL) {
343                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
344                         need_to_free_msg = 1;
345                 }
346                 if (msg != NULL) {
347                         if (msg->cm_fields['T'] != NULL) {
348                                 if (imap_datecmp(itemlist[pos+1],
349                                                 atol(msg->cm_fields['T'])) < 0) {
350                                         match = 1;
351                                 }
352                         }
353                 }
354                 pos += 2;
355         }
356
357         else if (!strcasecmp(itemlist[pos], "SENTON")) {
358                 if (msg == NULL) {
359                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
360                         need_to_free_msg = 1;
361                 }
362                 if (msg != NULL) {
363                         if (msg->cm_fields['T'] != NULL) {
364                                 if (imap_datecmp(itemlist[pos+1],
365                                                 atol(msg->cm_fields['T'])) == 0) {
366                                         match = 1;
367                                 }
368                         }
369                 }
370                 pos += 2;
371         }
372
373         else if (!strcasecmp(itemlist[pos], "SENTSINCE")) {
374                 if (msg == NULL) {
375                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
376                         need_to_free_msg = 1;
377                 }
378                 if (msg != NULL) {
379                         if (msg->cm_fields['T'] != NULL) {
380                                 if (imap_datecmp(itemlist[pos+1],
381                                                 atol(msg->cm_fields['T'])) >= 0) {
382                                         match = 1;
383                                 }
384                         }
385                 }
386                 pos += 2;
387         }
388
389         else if (!strcasecmp(itemlist[pos], "SINCE")) {
390                 if (msg == NULL) {
391                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
392                         need_to_free_msg = 1;
393                 }
394                 if (msg != NULL) {
395                         if (msg->cm_fields['T'] != NULL) {
396                                 if (imap_datecmp(itemlist[pos+1],
397                                                 atol(msg->cm_fields['T'])) >= 0) {
398                                         match = 1;
399                                 }
400                         }
401                 }
402                 pos += 2;
403         }
404
405         else if (!strcasecmp(itemlist[pos], "SMALLER")) {
406                 if (msg == NULL) {
407                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
408                         need_to_free_msg = 1;
409                 }
410                 if (msg != NULL) {
411                         if (strlen(msg->cm_fields['M']) < atoi(itemlist[pos+1])) {
412                                 match = 1;
413                         }
414                 }
415                 pos += 2;
416         }
417
418         else if (!strcasecmp(itemlist[pos], "SUBJECT")) {
419                 if (msg == NULL) {
420                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
421                         need_to_free_msg = 1;
422                 }
423                 if (msg != NULL) {
424                         if (bmstrcasestr(msg->cm_fields['U'], itemlist[pos+1])) {
425                                 match = 1;
426                         }
427                 }
428                 pos += 2;
429         }
430
431         else if (!strcasecmp(itemlist[pos], "TEXT")) {
432                 if (msg == NULL) {
433                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
434                         need_to_free_msg = 1;
435                 }
436                 if (msg != NULL) {
437                         for (i='A'; i<='Z'; ++i) {
438                                 if (bmstrcasestr(msg->cm_fields[i], itemlist[pos+1])) {
439                                         match = 1;
440                                 }
441                         }
442                 }
443                 pos += 2;
444         }
445
446         else if (!strcasecmp(itemlist[pos], "TO")) {
447                 if (msg == NULL) {
448                         msg = CtdlFetchMessage(IMAP->msgids[seq-1], 1);
449                         need_to_free_msg = 1;
450                 }
451                 if (msg != NULL) {
452                         if (bmstrcasestr(msg->cm_fields['R'], itemlist[pos+1])) {
453                                 match = 1;
454                         }
455                 }
456                 pos += 2;
457         }
458
459         /* FIXME this is b0rken.  fix it. */
460         else if (imap_is_message_set(itemlist[pos])) {
461                 if (is_msg_in_sequence_set(itemlist[pos], seq)) {
462                         match = 1;
463                 }
464                 pos += 1;
465         }
466
467         /* FIXME this is b0rken.  fix it. */
468         else if (!strcasecmp(itemlist[pos], "UID")) {
469                 if (is_msg_in_sequence_set(itemlist[pos+1], IMAP->msgids[seq-1])) {
470                         match = 1;
471                 }
472                 pos += 2;
473         }
474
475         /* Now here come the 'UN' criteria.  Why oh why do we have to
476          * implement *both* the 'UN' criteria *and* the 'NOT' keyword?  Why
477          * can't there be *one* way to do things?  More gratuitous complexity.
478          */
479
480         else if (!strcasecmp(itemlist[pos], "UNANSWERED")) {
481                 if ((IMAP->flags[seq-1] & IMAP_ANSWERED) == 0) {
482                         match = 1;
483                 }
484                 ++pos;
485         }
486
487         else if (!strcasecmp(itemlist[pos], "UNDELETED")) {
488                 if ((IMAP->flags[seq-1] & IMAP_DELETED) == 0) {
489                         match = 1;
490                 }
491                 ++pos;
492         }
493
494         else if (!strcasecmp(itemlist[pos], "UNDRAFT")) {
495                 if ((IMAP->flags[seq-1] & IMAP_DRAFT) == 0) {
496                         match = 1;
497                 }
498                 ++pos;
499         }
500
501         else if (!strcasecmp(itemlist[pos], "UNFLAGGED")) {
502                 if ((IMAP->flags[seq-1] & IMAP_FLAGGED) == 0) {
503                         match = 1;
504                 }
505                 ++pos;
506         }
507
508         else if (!strcasecmp(itemlist[pos], "UNKEYWORD")) {
509                 /* FIXME */
510                 pos += 2;
511         }
512
513         else if (!strcasecmp(itemlist[pos], "UNSEEN")) {
514                 if ((IMAP->flags[seq-1] & IMAP_SEEN) == 0) {
515                         match = 1;
516                 }
517                 ++pos;
518         }
519
520         /* Remember to negate if we were told to */
521         if (is_not) {
522                 match = !match;
523         }
524
525         /* Keep going if there are more criteria! */
526         if (pos < num_items) {
527
528                 if (is_or) {
529                         match = (match || imap_do_search_msg(seq, msg,
530                                 num_items - pos, &itemlist[pos], is_uid));
531                 }
532                 else {
533                         match = (match && imap_do_search_msg(seq, msg,
534                                 num_items - pos, &itemlist[pos], is_uid));
535                 }
536
537         }
538
539         if (need_to_free_msg) {
540                 CtdlFreeMessage(msg);
541         }
542         return(match);
543 }
544
545
546 /*
547  * imap_search() calls imap_do_search() to do its actual work, once it's
548  * validated and boiled down the request a bit.
549  */
550 void imap_do_search(int num_items, char **itemlist, int is_uid) {
551         int i, j, k;
552         int fts_num_msgs = 0;
553         long *fts_msgs = NULL;
554         int is_in_list = 0;
555         int num_results = 0;
556
557         /* Strip parentheses.  We realize that this method will not work
558          * in all cases, but it seems to work with all currently available
559          * client software.  Revisit later...
560          */
561         for (i=0; i<num_items; ++i) {
562                 if (itemlist[i][0] == '(') {
563                         strcpy(&itemlist[i][0], &itemlist[i][1]);
564                 }
565                 if (itemlist[i][strlen(itemlist[i])-1] == ')') {
566                         itemlist[i][strlen(itemlist[i])-1] = 0;
567                 }
568         }
569
570         /* If there is a BODY search criterion in the query, use our full
571          * text index to disqualify messages that don't have any chance of
572          * matching.  (Only do this if the index is enabled!!)
573          */
574         if (config.c_enable_fulltext) for (i=0; i<(num_items-1); ++i) {
575                 if (!strcasecmp(itemlist[i], "BODY")) {
576                         CtdlModuleDoSearch(&fts_num_msgs, &fts_msgs, itemlist[i+1], "fulltext");
577                         if (fts_num_msgs > 0) {
578                                 for (j=0; j < IMAP->num_msgs; ++j) {
579                                         if (IMAP->flags[j] & IMAP_SELECTED) {
580                                                 is_in_list = 0;
581                                                 for (k=0; k<fts_num_msgs; ++k) {
582                                                         if (IMAP->msgids[j] == fts_msgs[k]) {
583                                                                 ++is_in_list;
584                                                         }
585                                                 }
586                                         }
587                                         if (!is_in_list) {
588                                                 IMAP->flags[j] = IMAP->flags[j] & ~IMAP_SELECTED;
589                                         }
590                                 }
591                         }
592                         else {          /* no hits on the index; disqualify every message */
593                                 for (j=0; j < IMAP->num_msgs; ++j) {
594                                         IMAP->flags[j] = IMAP->flags[j] & ~IMAP_SELECTED;
595                                 }
596                         }
597                         if (fts_msgs) {
598                                 free(fts_msgs);
599                         }
600                 }
601         }
602
603         /* Now go through the messages and apply all search criteria. */
604         buffer_output();
605         cprintf("* SEARCH ");
606         if (IMAP->num_msgs > 0)
607          for (i = 0; i < IMAP->num_msgs; ++i)
608           if (IMAP->flags[i] & IMAP_SELECTED) {
609                 if (imap_do_search_msg(i+1, NULL, num_items, itemlist, is_uid)) {
610                         if (num_results != 0) {
611                                 cprintf(" ");
612                         }
613                         if (is_uid) {
614                                 cprintf("%ld", IMAP->msgids[i]);
615                         }
616                         else {
617                                 cprintf("%d", i+1);
618                         }
619                         ++num_results;
620                 }
621         }
622         cprintf("\r\n");
623         unbuffer_output();
624 }
625
626
627 /*
628  * This function is called by the main command loop.
629  */
630 void imap_search(int num_parms, char *parms[]) {
631         int i;
632
633         if (num_parms < 3) {
634                 cprintf("%s BAD invalid parameters\r\n", parms[0]);
635                 return;
636         }
637
638         for (i = 0; i < IMAP->num_msgs; ++i) {
639                 IMAP->flags[i] |= IMAP_SELECTED;
640         }
641
642         imap_do_search(num_parms-2, &parms[2], 0);
643         cprintf("%s OK SEARCH completed\r\n", parms[0]);
644 }
645
646 /*
647  * This function is called by the main command loop.
648  */
649 void imap_uidsearch(int num_parms, char *parms[]) {
650         int i;
651
652         if (num_parms < 4) {
653                 cprintf("%s BAD invalid parameters\r\n", parms[0]);
654                 return;
655         }
656
657         for (i = 0; i < IMAP->num_msgs; ++i) {
658                 IMAP->flags[i] |= IMAP_SELECTED;
659         }
660
661         imap_do_search(num_parms-3, &parms[3], 1);
662         cprintf("%s OK UID SEARCH completed\r\n", parms[0]);
663 }
664
665