2 * Implements IMAP's gratuitously complex SEARCH command.
4 * Copyright (c) 2001-2020 by the citadel.org team
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.
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.
15 #include "../../ctdl_module.h"
18 #include "../../sysdep.h"
26 #include <sys/types.h>
32 #include <libcitadel.h>
33 #include "../../citadel_defs.h"
34 #include "../../server.h"
35 #include "../../sysdep_decls.h"
36 #include "../../citserver.h"
37 #include "../../support.h"
38 #include "../../config.h"
39 #include "../../user_ops.h"
40 #include "../../database.h"
41 #include "../../msgbase.h"
42 #include "../../internet_addressing.h"
43 #include "serv_imap.h"
44 #include "imap_tools.h"
45 #include "imap_fetch.h"
46 #include "imap_search.h"
47 #include "../../genstamp.h"
51 * imap_do_search() calls imap_do_search_msg() to search an individual
52 * message after it has been fetched from the disk. This function returns
53 * nonzero if there is a match.
55 * supplied_msg MAY be used to pass a pointer to the message in memory,
56 * if for some reason it's already been loaded. If not, the message will
57 * be loaded only if one or more search criteria require it.
59 int imap_do_search_msg(int seq, struct CtdlMessage *supplied_msg,
60 int num_items, ConstStr *itemlist, int is_uid) {
69 struct CtdlMessage *msg = NULL;
70 int need_to_free_msg = 0;
77 /* Initially we start at the beginning. */
80 /* Check for the dreaded NOT criterion. */
81 if (!strcasecmp(itemlist[0].Key, "NOT")) {
86 /* Check for the dreaded OR criterion. */
87 if (!strcasecmp(itemlist[0].Key, "OR")) {
92 /* Now look for criteria. */
93 if (!strcasecmp(itemlist[pos].Key, "ALL")) {
98 else if (!strcasecmp(itemlist[pos].Key, "ANSWERED")) {
99 if (Imap->flags[seq-1] & IMAP_ANSWERED) {
105 else if (!strcasecmp(itemlist[pos].Key, "BCC")) {
107 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
108 need_to_free_msg = 1;
111 fieldptr = rfc822_fetch_field(msg->cm_fields[eMesageText], "Bcc");
112 if (fieldptr != NULL) {
113 if (bmstrcasestr(fieldptr, itemlist[pos+1].Key)) {
122 else if (!strcasecmp(itemlist[pos].Key, "BEFORE")) {
124 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
125 need_to_free_msg = 1;
128 if (!CM_IsEmpty(msg, eTimestamp)) {
129 if (imap_datecmp(itemlist[pos+1].Key,
130 atol(msg->cm_fields[eTimestamp])) < 0) {
138 else if (!strcasecmp(itemlist[pos].Key, "BODY")) {
140 /* If fulltext indexing is active, on this server,
141 * all messages have already been qualified.
143 if (CtdlGetConfigInt("c_enable_fulltext")) {
147 /* Otherwise, we have to do a slow search. */
150 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
151 need_to_free_msg = 1;
154 if (bmstrcasestr(msg->cm_fields[eMesageText], itemlist[pos+1].Key)) {
163 else if (!strcasecmp(itemlist[pos].Key, "CC")) {
165 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
166 need_to_free_msg = 1;
169 fieldptr = msg->cm_fields[eCarbonCopY];
170 if (fieldptr != NULL) {
171 if (bmstrcasestr(fieldptr, itemlist[pos+1].Key)) {
176 fieldptr = rfc822_fetch_field(msg->cm_fields[eMesageText], "Cc");
177 if (fieldptr != NULL) {
178 if (bmstrcasestr(fieldptr, itemlist[pos+1].Key)) {
188 else if (!strcasecmp(itemlist[pos].Key, "DELETED")) {
189 if (Imap->flags[seq-1] & IMAP_DELETED) {
195 else if (!strcasecmp(itemlist[pos].Key, "DRAFT")) {
196 if (Imap->flags[seq-1] & IMAP_DRAFT) {
202 else if (!strcasecmp(itemlist[pos].Key, "FLAGGED")) {
203 if (Imap->flags[seq-1] & IMAP_FLAGGED) {
209 else if (!strcasecmp(itemlist[pos].Key, "FROM")) {
211 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
212 need_to_free_msg = 1;
215 if (bmstrcasestr(msg->cm_fields[eAuthor], itemlist[pos+1].Key)) {
218 if (bmstrcasestr(msg->cm_fields[erFc822Addr], itemlist[pos+1].Key)) {
225 else if (!strcasecmp(itemlist[pos].Key, "HEADER")) {
227 /* We've got to do a slow search for this because the client
228 * might be asking for an RFC822 header field that has not been
229 * converted into a Citadel header field. That requires
230 * examining the message body.
233 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
234 need_to_free_msg = 1;
239 CC->redirect_buffer = NewStrBufPlain(NULL, SIZ);
240 CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_FAST, 0, 1, 0);
242 fieldptr = rfc822_fetch_field(ChrPtr(CC->redirect_buffer), itemlist[pos+1].Key);
243 if (fieldptr != NULL) {
244 if (bmstrcasestr(fieldptr, itemlist[pos+2].Key)) {
250 FreeStrBuf(&CC->redirect_buffer);
253 pos += 3; /* Yes, three */
256 else if (!strcasecmp(itemlist[pos].Key, "KEYWORD")) {
257 /* not implemented */
261 else if (!strcasecmp(itemlist[pos].Key, "LARGER")) {
263 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
264 need_to_free_msg = 1;
267 if (msg->cm_lengths[eMesageText] > atoi(itemlist[pos+1].Key)) {
274 else if (!strcasecmp(itemlist[pos].Key, "NEW")) {
275 if ( (Imap->flags[seq-1] & IMAP_RECENT) && (!(Imap->flags[seq-1] & IMAP_SEEN))) {
281 else if (!strcasecmp(itemlist[pos].Key, "OLD")) {
282 if (!(Imap->flags[seq-1] & IMAP_RECENT)) {
288 else if (!strcasecmp(itemlist[pos].Key, "ON")) {
290 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
291 need_to_free_msg = 1;
294 if (!CM_IsEmpty(msg, eTimestamp)) {
295 if (imap_datecmp(itemlist[pos+1].Key,
296 atol(msg->cm_fields[eTimestamp])) == 0) {
304 else if (!strcasecmp(itemlist[pos].Key, "RECENT")) {
305 if (Imap->flags[seq-1] & IMAP_RECENT) {
311 else if (!strcasecmp(itemlist[pos].Key, "SEEN")) {
312 if (Imap->flags[seq-1] & IMAP_SEEN) {
318 else if (!strcasecmp(itemlist[pos].Key, "SENTBEFORE")) {
320 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
321 need_to_free_msg = 1;
324 if (!CM_IsEmpty(msg, eTimestamp)) {
325 if (imap_datecmp(itemlist[pos+1].Key,
326 atol(msg->cm_fields[eTimestamp])) < 0) {
334 else if (!strcasecmp(itemlist[pos].Key, "SENTON")) {
336 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
337 need_to_free_msg = 1;
340 if (!CM_IsEmpty(msg, eTimestamp)) {
341 if (imap_datecmp(itemlist[pos+1].Key,
342 atol(msg->cm_fields[eTimestamp])) == 0) {
350 else if (!strcasecmp(itemlist[pos].Key, "SENTSINCE")) {
352 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
353 need_to_free_msg = 1;
356 if (!CM_IsEmpty(msg, eTimestamp)) {
357 if (imap_datecmp(itemlist[pos+1].Key,
358 atol(msg->cm_fields[eTimestamp])) >= 0) {
366 else if (!strcasecmp(itemlist[pos].Key, "SINCE")) {
368 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
369 need_to_free_msg = 1;
372 if (!CM_IsEmpty(msg, eTimestamp)) {
373 if (imap_datecmp(itemlist[pos+1].Key,
374 atol(msg->cm_fields[eTimestamp])) >= 0) {
382 else if (!strcasecmp(itemlist[pos].Key, "SMALLER")) {
384 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
385 need_to_free_msg = 1;
388 if (msg->cm_lengths[eMesageText] < atoi(itemlist[pos+1].Key)) {
395 else if (!strcasecmp(itemlist[pos].Key, "SUBJECT")) {
397 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
398 need_to_free_msg = 1;
401 if (bmstrcasestr(msg->cm_fields[eMsgSubject], itemlist[pos+1].Key)) {
408 else if (!strcasecmp(itemlist[pos].Key, "TEXT")) {
410 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
411 need_to_free_msg = 1;
414 for (i='A'; i<='Z'; ++i) {
415 if (bmstrcasestr(msg->cm_fields[i], itemlist[pos+1].Key)) {
423 else if (!strcasecmp(itemlist[pos].Key, "TO")) {
425 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
426 need_to_free_msg = 1;
429 if (bmstrcasestr(msg->cm_fields[eRecipient], itemlist[pos+1].Key)) {
436 /* FIXME this is b0rken. fix it. */
437 else if (imap_is_message_set(itemlist[pos].Key)) {
438 if (is_msg_in_sequence_set(itemlist[pos].Key, seq)) {
444 /* FIXME this is b0rken. fix it. */
445 else if (!strcasecmp(itemlist[pos].Key, "UID")) {
446 if (is_msg_in_sequence_set(itemlist[pos+1].Key, Imap->msgids[seq-1])) {
452 /* Now here come the 'UN' criteria. Why oh why do we have to
453 * implement *both* the 'UN' criteria *and* the 'NOT' keyword? Why
454 * can't there be *one* way to do things? More gratuitous complexity.
457 else if (!strcasecmp(itemlist[pos].Key, "UNANSWERED")) {
458 if ((Imap->flags[seq-1] & IMAP_ANSWERED) == 0) {
464 else if (!strcasecmp(itemlist[pos].Key, "UNDELETED")) {
465 if ((Imap->flags[seq-1] & IMAP_DELETED) == 0) {
471 else if (!strcasecmp(itemlist[pos].Key, "UNDRAFT")) {
472 if ((Imap->flags[seq-1] & IMAP_DRAFT) == 0) {
478 else if (!strcasecmp(itemlist[pos].Key, "UNFLAGGED")) {
479 if ((Imap->flags[seq-1] & IMAP_FLAGGED) == 0) {
485 else if (!strcasecmp(itemlist[pos].Key, "UNKEYWORD")) {
490 else if (!strcasecmp(itemlist[pos].Key, "UNSEEN")) {
491 if ((Imap->flags[seq-1] & IMAP_SEEN) == 0) {
497 /* Remember to negate if we were told to */
502 /* Keep going if there are more criteria! */
503 if (pos < num_items) {
506 match = (match || imap_do_search_msg(seq, msg, num_items - pos, &itemlist[pos], is_uid));
509 match = (match && imap_do_search_msg(seq, msg, num_items - pos, &itemlist[pos], is_uid));
514 if (need_to_free_msg) {
522 * imap_search() calls imap_do_search() to do its actual work, once it's
523 * validated and boiled down the request a bit.
525 void imap_do_search(int num_items, ConstStr *itemlist, int is_uid) {
526 citimap *Imap = IMAP;
528 int fts_num_msgs = 0;
529 long *fts_msgs = NULL;
533 /* Strip parentheses. We realize that this method will not work
534 * in all cases, but it seems to work with all currently available
535 * client software. Revisit later...
537 for (i=0; i<num_items; ++i) {
538 if (itemlist[i].Key[0] == '(') {
539 TokenCutLeft(&Imap->Cmd, &itemlist[i], 1);
541 if (itemlist[i].Key[itemlist[i].len-1] == ')') {
542 TokenCutRight(&Imap->Cmd, &itemlist[i], 1);
546 /* If there is a BODY search criterion in the query, use our full
547 * text index to disqualify messages that don't have any chance of
548 * matching. (Only do this if the index is enabled!!)
550 if (CtdlGetConfigInt("c_enable_fulltext")) for (i=0; i<(num_items-1); ++i) {
551 if (!strcasecmp(itemlist[i].Key, "BODY")) {
552 CtdlModuleDoSearch(&fts_num_msgs, &fts_msgs, itemlist[i+1].Key, "fulltext");
553 if (fts_num_msgs > 0) {
554 for (j=0; j < Imap->num_msgs; ++j) {
555 if (Imap->flags[j] & IMAP_SELECTED) {
557 for (k=0; k<fts_num_msgs; ++k) {
558 if (Imap->msgids[j] == fts_msgs[k]) {
564 Imap->flags[j] = Imap->flags[j] & ~IMAP_SELECTED;
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;
579 /* Now go through the messages and apply all search criteria. */
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) {
590 IAPrintf("%ld", Imap->msgids[i]);
604 * This function is called by the main command loop.
606 void imap_search(int num_parms, ConstStr *Params) {
610 IReply("BAD invalid parameters");
614 for (i = 0; i < IMAP->num_msgs; ++i) {
615 IMAP->flags[i] |= IMAP_SELECTED;
618 imap_do_search(num_parms-2, &Params[2], 0);
619 IReply("OK SEARCH completed");
623 * This function is called by the main command loop.
625 void imap_uidsearch(int num_parms, ConstStr *Params) {
629 IReply("BAD invalid parameters");
633 for (i = 0; i < IMAP->num_msgs; ++i) {
634 IMAP->flags[i] |= IMAP_SELECTED;
637 imap_do_search(num_parms-3, &Params[3], 1);
638 IReply("OK UID SEARCH completed");