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