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