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