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"
16 #include "../../sysdep.h"
24 #include <sys/types.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"
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.
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.
58 int imap_do_search_msg(int seq, struct CtdlMessage *supplied_msg,
59 int num_items, ConstStr *itemlist, int is_uid) {
68 struct CtdlMessage *msg = NULL;
69 int need_to_free_msg = 0;
76 /* Initially we start at the beginning. */
79 /* Check for the dreaded NOT criterion. */
80 if (!strcasecmp(itemlist[0].Key, "NOT")) {
85 /* Check for the dreaded OR criterion. */
86 if (!strcasecmp(itemlist[0].Key, "OR")) {
91 /* Now look for criteria. */
92 if (!strcasecmp(itemlist[pos].Key, "ALL")) {
97 else if (!strcasecmp(itemlist[pos].Key, "ANSWERED")) {
98 if (Imap->flags[seq-1] & IMAP_ANSWERED) {
104 else if (!strcasecmp(itemlist[pos].Key, "BCC")) {
106 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
107 need_to_free_msg = 1;
110 fieldptr = rfc822_fetch_field(msg->cm_fields[eMessageText], "Bcc");
111 if (fieldptr != NULL) {
112 if (bmstrcasestr(fieldptr, itemlist[pos+1].Key)) {
121 else if (!strcasecmp(itemlist[pos].Key, "BEFORE")) {
123 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
124 need_to_free_msg = 1;
127 if (!CM_IsEmpty(msg, eTimestamp)) {
128 if (imap_datecmp(itemlist[pos+1].Key,
129 atol(msg->cm_fields[eTimestamp])) < 0) {
137 else if (!strcasecmp(itemlist[pos].Key, "BODY")) {
139 /* If fulltext indexing is active, on this server,
140 * all messages have already been qualified.
142 if (CtdlGetConfigInt("c_enable_fulltext")) {
146 /* Otherwise, we have to do a slow search. */
149 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
150 need_to_free_msg = 1;
153 if (bmstrcasestr(msg->cm_fields[eMessageText], itemlist[pos+1].Key)) {
162 else if (!strcasecmp(itemlist[pos].Key, "CC")) {
164 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
165 need_to_free_msg = 1;
168 fieldptr = msg->cm_fields[eCarbonCopY];
169 if (fieldptr != NULL) {
170 if (bmstrcasestr(fieldptr, itemlist[pos+1].Key)) {
175 fieldptr = rfc822_fetch_field(msg->cm_fields[eMessageText], "Cc");
176 if (fieldptr != NULL) {
177 if (bmstrcasestr(fieldptr, itemlist[pos+1].Key)) {
187 else if (!strcasecmp(itemlist[pos].Key, "DELETED")) {
188 if (Imap->flags[seq-1] & IMAP_DELETED) {
194 else if (!strcasecmp(itemlist[pos].Key, "DRAFT")) {
195 if (Imap->flags[seq-1] & IMAP_DRAFT) {
201 else if (!strcasecmp(itemlist[pos].Key, "FLAGGED")) {
202 if (Imap->flags[seq-1] & IMAP_FLAGGED) {
208 else if (!strcasecmp(itemlist[pos].Key, "FROM")) {
210 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
211 need_to_free_msg = 1;
214 if (bmstrcasestr(msg->cm_fields[eAuthor], itemlist[pos+1].Key)) {
217 if (bmstrcasestr(msg->cm_fields[erFc822Addr], itemlist[pos+1].Key)) {
224 else if (!strcasecmp(itemlist[pos].Key, "HEADER")) {
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.
232 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
233 need_to_free_msg = 1;
238 CC->redirect_buffer = NewStrBufPlain(NULL, SIZ);
239 CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_FAST, 0, 1, 0);
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)) {
249 FreeStrBuf(&CC->redirect_buffer);
252 pos += 3; /* Yes, three */
255 else if (!strcasecmp(itemlist[pos].Key, "KEYWORD")) {
256 /* not implemented */
260 else if (!strcasecmp(itemlist[pos].Key, "LARGER")) {
262 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
263 need_to_free_msg = 1;
266 if (msg->cm_lengths[eMessageText] > atoi(itemlist[pos+1].Key)) {
273 else if (!strcasecmp(itemlist[pos].Key, "NEW")) {
274 if ( (Imap->flags[seq-1] & IMAP_RECENT) && (!(Imap->flags[seq-1] & IMAP_SEEN))) {
280 else if (!strcasecmp(itemlist[pos].Key, "OLD")) {
281 if (!(Imap->flags[seq-1] & IMAP_RECENT)) {
287 else if (!strcasecmp(itemlist[pos].Key, "ON")) {
289 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
290 need_to_free_msg = 1;
293 if (!CM_IsEmpty(msg, eTimestamp)) {
294 if (imap_datecmp(itemlist[pos+1].Key,
295 atol(msg->cm_fields[eTimestamp])) == 0) {
303 else if (!strcasecmp(itemlist[pos].Key, "RECENT")) {
304 if (Imap->flags[seq-1] & IMAP_RECENT) {
310 else if (!strcasecmp(itemlist[pos].Key, "SEEN")) {
311 if (Imap->flags[seq-1] & IMAP_SEEN) {
317 else if (!strcasecmp(itemlist[pos].Key, "SENTBEFORE")) {
319 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
320 need_to_free_msg = 1;
323 if (!CM_IsEmpty(msg, eTimestamp)) {
324 if (imap_datecmp(itemlist[pos+1].Key,
325 atol(msg->cm_fields[eTimestamp])) < 0) {
333 else if (!strcasecmp(itemlist[pos].Key, "SENTON")) {
335 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
336 need_to_free_msg = 1;
339 if (!CM_IsEmpty(msg, eTimestamp)) {
340 if (imap_datecmp(itemlist[pos+1].Key,
341 atol(msg->cm_fields[eTimestamp])) == 0) {
349 else if (!strcasecmp(itemlist[pos].Key, "SENTSINCE")) {
351 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
352 need_to_free_msg = 1;
355 if (!CM_IsEmpty(msg, eTimestamp)) {
356 if (imap_datecmp(itemlist[pos+1].Key,
357 atol(msg->cm_fields[eTimestamp])) >= 0) {
365 else if (!strcasecmp(itemlist[pos].Key, "SINCE")) {
367 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
368 need_to_free_msg = 1;
371 if (!CM_IsEmpty(msg, eTimestamp)) {
372 if (imap_datecmp(itemlist[pos+1].Key,
373 atol(msg->cm_fields[eTimestamp])) >= 0) {
381 else if (!strcasecmp(itemlist[pos].Key, "SMALLER")) {
383 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
384 need_to_free_msg = 1;
387 if (msg->cm_lengths[eMessageText] < atoi(itemlist[pos+1].Key)) {
394 else if (!strcasecmp(itemlist[pos].Key, "SUBJECT")) {
396 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
397 need_to_free_msg = 1;
400 if (bmstrcasestr(msg->cm_fields[eMsgSubject], itemlist[pos+1].Key)) {
407 else if (!strcasecmp(itemlist[pos].Key, "TEXT")) {
409 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
410 need_to_free_msg = 1;
413 for (i='A'; i<='Z'; ++i) {
414 if (bmstrcasestr(msg->cm_fields[i], itemlist[pos+1].Key)) {
422 else if (!strcasecmp(itemlist[pos].Key, "TO")) {
424 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
425 need_to_free_msg = 1;
428 if (bmstrcasestr(msg->cm_fields[eRecipient], itemlist[pos+1].Key)) {
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)) {
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])) {
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.
456 else if (!strcasecmp(itemlist[pos].Key, "UNANSWERED")) {
457 if ((Imap->flags[seq-1] & IMAP_ANSWERED) == 0) {
463 else if (!strcasecmp(itemlist[pos].Key, "UNDELETED")) {
464 if ((Imap->flags[seq-1] & IMAP_DELETED) == 0) {
470 else if (!strcasecmp(itemlist[pos].Key, "UNDRAFT")) {
471 if ((Imap->flags[seq-1] & IMAP_DRAFT) == 0) {
477 else if (!strcasecmp(itemlist[pos].Key, "UNFLAGGED")) {
478 if ((Imap->flags[seq-1] & IMAP_FLAGGED) == 0) {
484 else if (!strcasecmp(itemlist[pos].Key, "UNKEYWORD")) {
489 else if (!strcasecmp(itemlist[pos].Key, "UNSEEN")) {
490 if ((Imap->flags[seq-1] & IMAP_SEEN) == 0) {
496 /* Remember to negate if we were told to */
501 /* Keep going if there are more criteria! */
502 if (pos < num_items) {
505 match = (match || imap_do_search_msg(seq, msg, num_items - pos, &itemlist[pos], is_uid));
508 match = (match && imap_do_search_msg(seq, msg, num_items - pos, &itemlist[pos], is_uid));
513 if (need_to_free_msg) {
521 * imap_search() calls imap_do_search() to do its actual work, once it's
522 * validated and boiled down the request a bit.
524 void imap_do_search(int num_items, ConstStr *itemlist, int is_uid) {
525 citimap *Imap = IMAP;
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...
535 for (i=0; i<num_items; ++i) {
536 if (itemlist[i].len && (itemlist[i].Key[0] == '(')) {
537 TokenCutLeft(&Imap->Cmd, &itemlist[i], 1);
539 if (itemlist[i].len && (itemlist[i].Key[itemlist[i].len-1] == ')')) {
540 TokenCutRight(&Imap->Cmd, &itemlist[i], 1);
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!!)
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) {
555 for (k=0; k<array_len(fts); ++k) {
557 memcpy(&smsgnum, array_get_element_at(fts, k), sizeof(long));
558 if (Imap->msgids[j] == smsgnum) {
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");