Rewrite IMAP-Module to buffer its output
[citadel.git] / citadel / modules / imap / imap_search.c
1 /*
2  * Implements IMAP's gratuitously complex SEARCH command.
3  *
4  *
5  * Copyright (c) 2001-2009 by the citadel.org team
6  *
7  *  This program is free software; you can redistribute it and/or modify
8  *  it under the terms of the GNU General Public License as published by
9  *  the Free Software Foundation; either version 3 of the License, or
10  *  (at your option) any later version.
11  *
12  *  This program is distributed in the hope that it will be useful,
13  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  *  GNU General Public License for more details.
16  *
17  *  You should have received a copy of the GNU General Public License
18  *  along with this program; if not, write to the Free Software
19  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20  */
21
22 #include "ctdl_module.h"
23
24
25 #include "sysdep.h"
26 #include <stdlib.h>
27 #include <unistd.h>
28 #include <stdio.h>
29 #include <fcntl.h>
30 #include <signal.h>
31 #include <pwd.h>
32 #include <errno.h>
33 #include <sys/types.h>
34
35 #if TIME_WITH_SYS_TIME
36 # include <sys/time.h>
37 # include <time.h>
38 #else
39 # if HAVE_SYS_TIME_H
40 #  include <sys/time.h>
41 # else
42 #  include <time.h>
43 # endif
44 #endif
45
46 #include <sys/wait.h>
47 #include <ctype.h>
48 #include <string.h>
49 #include <limits.h>
50 #include <libcitadel.h>
51 #include "citadel.h"
52 #include "server.h"
53 #include "sysdep_decls.h"
54 #include "citserver.h"
55 #include "support.h"
56 #include "config.h"
57 #include "user_ops.h"
58 #include "database.h"
59 #include "msgbase.h"
60 #include "internet_addressing.h"
61 #include "serv_imap.h"
62 #include "imap_tools.h"
63 #include "imap_fetch.h"
64 #include "imap_search.h"
65 #include "genstamp.h"
66
67
68 /*
69  * imap_do_search() calls imap_do_search_msg() to search an individual
70  * message after it has been fetched from the disk.  This function returns
71  * nonzero if there is a match.
72  *
73  * supplied_msg MAY be used to pass a pointer to the message in memory,
74  * if for some reason it's already been loaded.  If not, the message will
75  * be loaded only if one or more search criteria require it.
76  */
77 int imap_do_search_msg(int seq, struct CtdlMessage *supplied_msg,
78                         int num_items, ConstStr *itemlist, int is_uid) {
79
80         citimap *Imap = IMAP;
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].Key, "NOT")) {
100                 is_not = 1;
101                 pos = 1;
102         }
103
104         /* Check for the dreaded OR criterion. */
105         if (!strcasecmp(itemlist[0].Key, "OR")) {
106                 is_or = 1;
107                 pos = 1;
108         }
109
110         /* Now look for criteria. */
111         if (!strcasecmp(itemlist[pos].Key, "ALL")) {
112                 match = 1;
113                 ++pos;
114         }
115         
116         else if (!strcasecmp(itemlist[pos].Key, "ANSWERED")) {
117                 if (Imap->flags[seq-1] & IMAP_ANSWERED) {
118                         match = 1;
119                 }
120                 ++pos;
121         }
122
123         else if (!strcasecmp(itemlist[pos].Key, "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].Key)) {
132                                         match = 1;
133                                 }
134                                 free(fieldptr);
135                         }
136                 }
137                 pos += 2;
138         }
139
140         else if (!strcasecmp(itemlist[pos].Key, "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].Key,
148                                                 atol(msg->cm_fields['T'])) < 0) {
149                                         match = 1;
150                                 }
151                         }
152                 }
153                 pos += 2;
154         }
155
156         else if (!strcasecmp(itemlist[pos].Key, "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].Key)) {
173                                         match = 1;
174                                 }
175                         }
176                 }
177
178                 pos += 2;
179         }
180
181         else if (!strcasecmp(itemlist[pos].Key, "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].Key)) {
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].Key)) {
197                                                 match = 1;
198                                         }
199                                         free(fieldptr);
200                                 }
201                         }
202                 }
203                 pos += 2;
204         }
205
206         else if (!strcasecmp(itemlist[pos].Key, "DELETED")) {
207                 if (Imap->flags[seq-1] & IMAP_DELETED) {
208                         match = 1;
209                 }
210                 ++pos;
211         }
212
213         else if (!strcasecmp(itemlist[pos].Key, "DRAFT")) {
214                 if (Imap->flags[seq-1] & IMAP_DRAFT) {
215                         match = 1;
216                 }
217                 ++pos;
218         }
219
220         else if (!strcasecmp(itemlist[pos].Key, "FLAGGED")) {
221                 if (Imap->flags[seq-1] & IMAP_FLAGGED) {
222                         match = 1;
223                 }
224                 ++pos;
225         }
226
227         else if (!strcasecmp(itemlist[pos].Key, "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].Key)) {
234                                 match = 1;
235                         }
236                         if (bmstrcasestr(msg->cm_fields['F'], itemlist[pos+1].Key)) {
237                                 match = 1;
238                         }
239                 }
240                 pos += 2;
241         }
242
243         else if (!strcasecmp(itemlist[pos].Key, "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 = NewStrBufPlain(NULL, SIZ);
258                         CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_FAST, 0, 1, 0);
259         
260                         fieldptr = rfc822_fetch_field(ChrPtr(CC->redirect_buffer), itemlist[pos+1].Key);
261                         if (fieldptr != NULL) {
262                                 if (bmstrcasestr(fieldptr, itemlist[pos+2].Key)) {
263                                         match = 1;
264                                 }
265                                 free(fieldptr);
266                         }
267         
268                         FreeStrBuf(&CC->redirect_buffer);
269                 }
270
271                 pos += 3;       /* Yes, three */
272         }
273
274         else if (!strcasecmp(itemlist[pos].Key, "KEYWORD")) {
275                 /* not implemented */
276                 pos += 2;
277         }
278
279         else if (!strcasecmp(itemlist[pos].Key, "LARGER")) {
280                 if (msg == NULL) {
281                         msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
282                         need_to_free_msg = 1;
283                 }
284                 if (msg != NULL) {
285                         if (strlen(msg->cm_fields['M']) > atoi(itemlist[pos+1].Key)) {
286                                 match = 1;
287                         }
288                 }
289                 pos += 2;
290         }
291
292         else if (!strcasecmp(itemlist[pos].Key, "NEW")) {
293                 if ( (Imap->flags[seq-1] & IMAP_RECENT) && (!(Imap->flags[seq-1] & IMAP_SEEN))) {
294                         match = 1;
295                 }
296                 ++pos;
297         }
298
299         else if (!strcasecmp(itemlist[pos].Key, "OLD")) {
300                 if (!(Imap->flags[seq-1] & IMAP_RECENT)) {
301                         match = 1;
302                 }
303                 ++pos;
304         }
305
306         else if (!strcasecmp(itemlist[pos].Key, "ON")) {
307                 if (msg == NULL) {
308                         msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
309                         need_to_free_msg = 1;
310                 }
311                 if (msg != NULL) {
312                         if (msg->cm_fields['T'] != NULL) {
313                                 if (imap_datecmp(itemlist[pos+1].Key,
314                                                 atol(msg->cm_fields['T'])) == 0) {
315                                         match = 1;
316                                 }
317                         }
318                 }
319                 pos += 2;
320         }
321
322         else if (!strcasecmp(itemlist[pos].Key, "RECENT")) {
323                 if (Imap->flags[seq-1] & IMAP_RECENT) {
324                         match = 1;
325                 }
326                 ++pos;
327         }
328
329         else if (!strcasecmp(itemlist[pos].Key, "SEEN")) {
330                 if (Imap->flags[seq-1] & IMAP_SEEN) {
331                         match = 1;
332                 }
333                 ++pos;
334         }
335
336         else if (!strcasecmp(itemlist[pos].Key, "SENTBEFORE")) {
337                 if (msg == NULL) {
338                         msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
339                         need_to_free_msg = 1;
340                 }
341                 if (msg != NULL) {
342                         if (msg->cm_fields['T'] != NULL) {
343                                 if (imap_datecmp(itemlist[pos+1].Key,
344                                                 atol(msg->cm_fields['T'])) < 0) {
345                                         match = 1;
346                                 }
347                         }
348                 }
349                 pos += 2;
350         }
351
352         else if (!strcasecmp(itemlist[pos].Key, "SENTON")) {
353                 if (msg == NULL) {
354                         msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
355                         need_to_free_msg = 1;
356                 }
357                 if (msg != NULL) {
358                         if (msg->cm_fields['T'] != NULL) {
359                                 if (imap_datecmp(itemlist[pos+1].Key,
360                                                 atol(msg->cm_fields['T'])) == 0) {
361                                         match = 1;
362                                 }
363                         }
364                 }
365                 pos += 2;
366         }
367
368         else if (!strcasecmp(itemlist[pos].Key, "SENTSINCE")) {
369                 if (msg == NULL) {
370                         msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
371                         need_to_free_msg = 1;
372                 }
373                 if (msg != NULL) {
374                         if (msg->cm_fields['T'] != NULL) {
375                                 if (imap_datecmp(itemlist[pos+1].Key,
376                                                 atol(msg->cm_fields['T'])) >= 0) {
377                                         match = 1;
378                                 }
379                         }
380                 }
381                 pos += 2;
382         }
383
384         else if (!strcasecmp(itemlist[pos].Key, "SINCE")) {
385                 if (msg == NULL) {
386                         msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
387                         need_to_free_msg = 1;
388                 }
389                 if (msg != NULL) {
390                         if (msg->cm_fields['T'] != NULL) {
391                                 if (imap_datecmp(itemlist[pos+1].Key,
392                                                 atol(msg->cm_fields['T'])) >= 0) {
393                                         match = 1;
394                                 }
395                         }
396                 }
397                 pos += 2;
398         }
399
400         else if (!strcasecmp(itemlist[pos].Key, "SMALLER")) {
401                 if (msg == NULL) {
402                         msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
403                         need_to_free_msg = 1;
404                 }
405                 if (msg != NULL) {
406                         if (strlen(msg->cm_fields['M']) < atoi(itemlist[pos+1].Key)) {
407                                 match = 1;
408                         }
409                 }
410                 pos += 2;
411         }
412
413         else if (!strcasecmp(itemlist[pos].Key, "SUBJECT")) {
414                 if (msg == NULL) {
415                         msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
416                         need_to_free_msg = 1;
417                 }
418                 if (msg != NULL) {
419                         if (bmstrcasestr(msg->cm_fields['U'], itemlist[pos+1].Key)) {
420                                 match = 1;
421                         }
422                 }
423                 pos += 2;
424         }
425
426         else if (!strcasecmp(itemlist[pos].Key, "TEXT")) {
427                 if (msg == NULL) {
428                         msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
429                         need_to_free_msg = 1;
430                 }
431                 if (msg != NULL) {
432                         for (i='A'; i<='Z'; ++i) {
433                                 if (bmstrcasestr(msg->cm_fields[i], itemlist[pos+1].Key)) {
434                                         match = 1;
435                                 }
436                         }
437                 }
438                 pos += 2;
439         }
440
441         else if (!strcasecmp(itemlist[pos].Key, "TO")) {
442                 if (msg == NULL) {
443                         msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
444                         need_to_free_msg = 1;
445                 }
446                 if (msg != NULL) {
447                         if (bmstrcasestr(msg->cm_fields['R'], itemlist[pos+1].Key)) {
448                                 match = 1;
449                         }
450                 }
451                 pos += 2;
452         }
453
454         /* FIXME this is b0rken.  fix it. */
455         else if (imap_is_message_set(itemlist[pos].Key)) {
456                 if (is_msg_in_sequence_set(itemlist[pos].Key, seq)) {
457                         match = 1;
458                 }
459                 pos += 1;
460         }
461
462         /* FIXME this is b0rken.  fix it. */
463         else if (!strcasecmp(itemlist[pos].Key, "UID")) {
464                 if (is_msg_in_sequence_set(itemlist[pos+1].Key, Imap->msgids[seq-1])) {
465                         match = 1;
466                 }
467                 pos += 2;
468         }
469
470         /* Now here come the 'UN' criteria.  Why oh why do we have to
471          * implement *both* the 'UN' criteria *and* the 'NOT' keyword?  Why
472          * can't there be *one* way to do things?  More gratuitous complexity.
473          */
474
475         else if (!strcasecmp(itemlist[pos].Key, "UNANSWERED")) {
476                 if ((Imap->flags[seq-1] & IMAP_ANSWERED) == 0) {
477                         match = 1;
478                 }
479                 ++pos;
480         }
481
482         else if (!strcasecmp(itemlist[pos].Key, "UNDELETED")) {
483                 if ((Imap->flags[seq-1] & IMAP_DELETED) == 0) {
484                         match = 1;
485                 }
486                 ++pos;
487         }
488
489         else if (!strcasecmp(itemlist[pos].Key, "UNDRAFT")) {
490                 if ((Imap->flags[seq-1] & IMAP_DRAFT) == 0) {
491                         match = 1;
492                 }
493                 ++pos;
494         }
495
496         else if (!strcasecmp(itemlist[pos].Key, "UNFLAGGED")) {
497                 if ((Imap->flags[seq-1] & IMAP_FLAGGED) == 0) {
498                         match = 1;
499                 }
500                 ++pos;
501         }
502
503         else if (!strcasecmp(itemlist[pos].Key, "UNKEYWORD")) {
504                 /* FIXME */
505                 pos += 2;
506         }
507
508         else if (!strcasecmp(itemlist[pos].Key, "UNSEEN")) {
509                 if ((Imap->flags[seq-1] & IMAP_SEEN) == 0) {
510                         match = 1;
511                 }
512                 ++pos;
513         }
514
515         /* Remember to negate if we were told to */
516         if (is_not) {
517                 match = !match;
518         }
519
520         /* Keep going if there are more criteria! */
521         if (pos < num_items) {
522
523                 if (is_or) {
524                         match = (match || imap_do_search_msg(seq, msg,
525                                 num_items - pos, &itemlist[pos], is_uid));
526                 }
527                 else {
528                         match = (match && imap_do_search_msg(seq, msg,
529                                 num_items - pos, &itemlist[pos], is_uid));
530                 }
531
532         }
533
534         if (need_to_free_msg) {
535                 CtdlFreeMessage(msg);
536         }
537         return(match);
538 }
539
540
541 /*
542  * imap_search() calls imap_do_search() to do its actual work, once it's
543  * validated and boiled down the request a bit.
544  */
545 void imap_do_search(int num_items, ConstStr *itemlist, int is_uid) {
546         citimap *Imap = IMAP;
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         IAPuts("* 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                                 IAPuts(" ");
613                         }
614                         if (is_uid) {
615                                 IAPrintf("%ld", Imap->msgids[i]);
616                         }
617                         else {
618                                 IAPrintf("%d", i+1);
619                         }
620                         ++num_results;
621                 }
622         }
623         IAPuts("\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                 IReply("BAD invalid parameters");
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         IReply("OK SEARCH completed");
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                 IReply("BAD invalid parameters");
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         IReply("OK UID SEARCH completed");
664 }
665
666