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