Back to index

citadel  8.12
imap_search.c
Go to the documentation of this file.
00001 /*
00002  * Implements IMAP's gratuitously complex SEARCH command.
00003  *
00004  * Copyright (c) 2001-2012 by the citadel.org team
00005  *
00006  *  This program is open source software; you can redistribute it and/or modify
00007  *  it under the terms of the GNU General Public License version 3.
00008  *  
00009  *  
00010  *
00011  *  This program is distributed in the hope that it will be useful,
00012  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
00013  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00014  *  GNU General Public License for more details.
00015  *
00016  *  
00017  *  
00018  *  
00019  */
00020 
00021 #include "ctdl_module.h"
00022 
00023 
00024 #include "sysdep.h"
00025 #include <stdlib.h>
00026 #include <unistd.h>
00027 #include <stdio.h>
00028 #include <fcntl.h>
00029 #include <signal.h>
00030 #include <pwd.h>
00031 #include <errno.h>
00032 #include <sys/types.h>
00033 
00034 #if TIME_WITH_SYS_TIME
00035 # include <sys/time.h>
00036 # include <time.h>
00037 #else
00038 # if HAVE_SYS_TIME_H
00039 #  include <sys/time.h>
00040 # else
00041 #  include <time.h>
00042 # endif
00043 #endif
00044 
00045 #include <sys/wait.h>
00046 #include <ctype.h>
00047 #include <string.h>
00048 #include <limits.h>
00049 #include <libcitadel.h>
00050 #include "citadel.h"
00051 #include "server.h"
00052 #include "sysdep_decls.h"
00053 #include "citserver.h"
00054 #include "support.h"
00055 #include "config.h"
00056 #include "user_ops.h"
00057 #include "database.h"
00058 #include "msgbase.h"
00059 #include "internet_addressing.h"
00060 #include "serv_imap.h"
00061 #include "imap_tools.h"
00062 #include "imap_fetch.h"
00063 #include "imap_search.h"
00064 #include "genstamp.h"
00065 
00066 
00067 /*
00068  * imap_do_search() calls imap_do_search_msg() to search an individual
00069  * message after it has been fetched from the disk.  This function returns
00070  * nonzero if there is a match.
00071  *
00072  * supplied_msg MAY be used to pass a pointer to the message in memory,
00073  * if for some reason it's already been loaded.  If not, the message will
00074  * be loaded only if one or more search criteria require it.
00075  */
00076 int imap_do_search_msg(int seq, struct CtdlMessage *supplied_msg,
00077                      int num_items, ConstStr *itemlist, int is_uid) {
00078 
00079        citimap *Imap = IMAP;
00080        int match = 0;
00081        int is_not = 0;
00082        int is_or = 0;
00083        int pos = 0;
00084        int i;
00085        char *fieldptr;
00086        struct CtdlMessage *msg = NULL;
00087        int need_to_free_msg = 0;
00088 
00089        if (num_items == 0) {
00090               return(0);
00091        }
00092        msg = supplied_msg;
00093 
00094        /* Initially we start at the beginning. */
00095        pos = 0;
00096 
00097        /* Check for the dreaded NOT criterion. */
00098        if (!strcasecmp(itemlist[0].Key, "NOT")) {
00099               is_not = 1;
00100               pos = 1;
00101        }
00102 
00103        /* Check for the dreaded OR criterion. */
00104        if (!strcasecmp(itemlist[0].Key, "OR")) {
00105               is_or = 1;
00106               pos = 1;
00107        }
00108 
00109        /* Now look for criteria. */
00110        if (!strcasecmp(itemlist[pos].Key, "ALL")) {
00111               match = 1;
00112               ++pos;
00113        }
00114        
00115        else if (!strcasecmp(itemlist[pos].Key, "ANSWERED")) {
00116               if (Imap->flags[seq-1] & IMAP_ANSWERED) {
00117                      match = 1;
00118               }
00119               ++pos;
00120        }
00121 
00122        else if (!strcasecmp(itemlist[pos].Key, "BCC")) {
00123               if (msg == NULL) {
00124                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00125                      need_to_free_msg = 1;
00126               }
00127               if (msg != NULL) {
00128                      fieldptr = rfc822_fetch_field(msg->cm_fields['M'], "Bcc");
00129                      if (fieldptr != NULL) {
00130                             if (bmstrcasestr(fieldptr, itemlist[pos+1].Key)) {
00131                                    match = 1;
00132                             }
00133                             free(fieldptr);
00134                      }
00135               }
00136               pos += 2;
00137        }
00138 
00139        else if (!strcasecmp(itemlist[pos].Key, "BEFORE")) {
00140               if (msg == NULL) {
00141                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00142                      need_to_free_msg = 1;
00143               }
00144               if (msg != NULL) {
00145                      if (msg->cm_fields['T'] != NULL) {
00146                             if (imap_datecmp(itemlist[pos+1].Key,
00147                                           atol(msg->cm_fields['T'])) < 0) {
00148                                    match = 1;
00149                             }
00150                      }
00151               }
00152               pos += 2;
00153        }
00154 
00155        else if (!strcasecmp(itemlist[pos].Key, "BODY")) {
00156 
00157               /* If fulltext indexing is active, on this server,
00158                *  all messages have already been qualified.
00159                */
00160               if (config.c_enable_fulltext) {
00161                      match = 1;
00162               }
00163 
00164               /* Otherwise, we have to do a slow search. */
00165               else {
00166                      if (msg == NULL) {
00167                             msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00168                             need_to_free_msg = 1;
00169                      }
00170                      if (msg != NULL) {
00171                             if (bmstrcasestr(msg->cm_fields['M'], itemlist[pos+1].Key)) {
00172                                    match = 1;
00173                             }
00174                      }
00175               }
00176 
00177               pos += 2;
00178        }
00179 
00180        else if (!strcasecmp(itemlist[pos].Key, "CC")) {
00181               if (msg == NULL) {
00182                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00183                      need_to_free_msg = 1;
00184               }
00185               if (msg != NULL) {
00186                      fieldptr = msg->cm_fields['Y'];
00187                      if (fieldptr != NULL) {
00188                             if (bmstrcasestr(fieldptr, itemlist[pos+1].Key)) {
00189                                    match = 1;
00190                             }
00191                      }
00192                      else {
00193                             fieldptr = rfc822_fetch_field(msg->cm_fields['M'], "Cc");
00194                             if (fieldptr != NULL) {
00195                                    if (bmstrcasestr(fieldptr, itemlist[pos+1].Key)) {
00196                                           match = 1;
00197                                    }
00198                                    free(fieldptr);
00199                             }
00200                      }
00201               }
00202               pos += 2;
00203        }
00204 
00205        else if (!strcasecmp(itemlist[pos].Key, "DELETED")) {
00206               if (Imap->flags[seq-1] & IMAP_DELETED) {
00207                      match = 1;
00208               }
00209               ++pos;
00210        }
00211 
00212        else if (!strcasecmp(itemlist[pos].Key, "DRAFT")) {
00213               if (Imap->flags[seq-1] & IMAP_DRAFT) {
00214                      match = 1;
00215               }
00216               ++pos;
00217        }
00218 
00219        else if (!strcasecmp(itemlist[pos].Key, "FLAGGED")) {
00220               if (Imap->flags[seq-1] & IMAP_FLAGGED) {
00221                      match = 1;
00222               }
00223               ++pos;
00224        }
00225 
00226        else if (!strcasecmp(itemlist[pos].Key, "FROM")) {
00227               if (msg == NULL) {
00228                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00229                      need_to_free_msg = 1;
00230               }
00231               if (msg != NULL) {
00232                      if (bmstrcasestr(msg->cm_fields['A'], itemlist[pos+1].Key)) {
00233                             match = 1;
00234                      }
00235                      if (bmstrcasestr(msg->cm_fields['F'], itemlist[pos+1].Key)) {
00236                             match = 1;
00237                      }
00238               }
00239               pos += 2;
00240        }
00241 
00242        else if (!strcasecmp(itemlist[pos].Key, "HEADER")) {
00243 
00244               /* We've got to do a slow search for this because the client
00245                * might be asking for an RFC822 header field that has not been
00246                * converted into a Citadel header field.  That requires
00247                * examining the message body.
00248                */
00249               if (msg == NULL) {
00250                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00251                      need_to_free_msg = 1;
00252               }
00253 
00254               if (msg != NULL) {
00255        
00256                      CC->redirect_buffer = NewStrBufPlain(NULL, SIZ);
00257                      CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_FAST, 0, 1, 0);
00258        
00259                      fieldptr = rfc822_fetch_field(ChrPtr(CC->redirect_buffer), itemlist[pos+1].Key);
00260                      if (fieldptr != NULL) {
00261                             if (bmstrcasestr(fieldptr, itemlist[pos+2].Key)) {
00262                                    match = 1;
00263                             }
00264                             free(fieldptr);
00265                      }
00266        
00267                      FreeStrBuf(&CC->redirect_buffer);
00268               }
00269 
00270               pos += 3;     /* Yes, three */
00271        }
00272 
00273        else if (!strcasecmp(itemlist[pos].Key, "KEYWORD")) {
00274               /* not implemented */
00275               pos += 2;
00276        }
00277 
00278        else if (!strcasecmp(itemlist[pos].Key, "LARGER")) {
00279               if (msg == NULL) {
00280                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00281                      need_to_free_msg = 1;
00282               }
00283               if (msg != NULL) {
00284                      if (strlen(msg->cm_fields['M']) > atoi(itemlist[pos+1].Key)) {
00285                             match = 1;
00286                      }
00287               }
00288               pos += 2;
00289        }
00290 
00291        else if (!strcasecmp(itemlist[pos].Key, "NEW")) {
00292               if ( (Imap->flags[seq-1] & IMAP_RECENT) && (!(Imap->flags[seq-1] & IMAP_SEEN))) {
00293                      match = 1;
00294               }
00295               ++pos;
00296        }
00297 
00298        else if (!strcasecmp(itemlist[pos].Key, "OLD")) {
00299               if (!(Imap->flags[seq-1] & IMAP_RECENT)) {
00300                      match = 1;
00301               }
00302               ++pos;
00303        }
00304 
00305        else if (!strcasecmp(itemlist[pos].Key, "ON")) {
00306               if (msg == NULL) {
00307                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00308                      need_to_free_msg = 1;
00309               }
00310               if (msg != NULL) {
00311                      if (msg->cm_fields['T'] != NULL) {
00312                             if (imap_datecmp(itemlist[pos+1].Key,
00313                                           atol(msg->cm_fields['T'])) == 0) {
00314                                    match = 1;
00315                             }
00316                      }
00317               }
00318               pos += 2;
00319        }
00320 
00321        else if (!strcasecmp(itemlist[pos].Key, "RECENT")) {
00322               if (Imap->flags[seq-1] & IMAP_RECENT) {
00323                      match = 1;
00324               }
00325               ++pos;
00326        }
00327 
00328        else if (!strcasecmp(itemlist[pos].Key, "SEEN")) {
00329               if (Imap->flags[seq-1] & IMAP_SEEN) {
00330                      match = 1;
00331               }
00332               ++pos;
00333        }
00334 
00335        else if (!strcasecmp(itemlist[pos].Key, "SENTBEFORE")) {
00336               if (msg == NULL) {
00337                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00338                      need_to_free_msg = 1;
00339               }
00340               if (msg != NULL) {
00341                      if (msg->cm_fields['T'] != NULL) {
00342                             if (imap_datecmp(itemlist[pos+1].Key,
00343                                           atol(msg->cm_fields['T'])) < 0) {
00344                                    match = 1;
00345                             }
00346                      }
00347               }
00348               pos += 2;
00349        }
00350 
00351        else if (!strcasecmp(itemlist[pos].Key, "SENTON")) {
00352               if (msg == NULL) {
00353                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00354                      need_to_free_msg = 1;
00355               }
00356               if (msg != NULL) {
00357                      if (msg->cm_fields['T'] != NULL) {
00358                             if (imap_datecmp(itemlist[pos+1].Key,
00359                                           atol(msg->cm_fields['T'])) == 0) {
00360                                    match = 1;
00361                             }
00362                      }
00363               }
00364               pos += 2;
00365        }
00366 
00367        else if (!strcasecmp(itemlist[pos].Key, "SENTSINCE")) {
00368               if (msg == NULL) {
00369                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00370                      need_to_free_msg = 1;
00371               }
00372               if (msg != NULL) {
00373                      if (msg->cm_fields['T'] != NULL) {
00374                             if (imap_datecmp(itemlist[pos+1].Key,
00375                                           atol(msg->cm_fields['T'])) >= 0) {
00376                                    match = 1;
00377                             }
00378                      }
00379               }
00380               pos += 2;
00381        }
00382 
00383        else if (!strcasecmp(itemlist[pos].Key, "SINCE")) {
00384               if (msg == NULL) {
00385                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00386                      need_to_free_msg = 1;
00387               }
00388               if (msg != NULL) {
00389                      if (msg->cm_fields['T'] != NULL) {
00390                             if (imap_datecmp(itemlist[pos+1].Key,
00391                                           atol(msg->cm_fields['T'])) >= 0) {
00392                                    match = 1;
00393                             }
00394                      }
00395               }
00396               pos += 2;
00397        }
00398 
00399        else if (!strcasecmp(itemlist[pos].Key, "SMALLER")) {
00400               if (msg == NULL) {
00401                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00402                      need_to_free_msg = 1;
00403               }
00404               if (msg != NULL) {
00405                      if (strlen(msg->cm_fields['M']) < atoi(itemlist[pos+1].Key)) {
00406                             match = 1;
00407                      }
00408               }
00409               pos += 2;
00410        }
00411 
00412        else if (!strcasecmp(itemlist[pos].Key, "SUBJECT")) {
00413               if (msg == NULL) {
00414                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00415                      need_to_free_msg = 1;
00416               }
00417               if (msg != NULL) {
00418                      if (bmstrcasestr(msg->cm_fields['U'], itemlist[pos+1].Key)) {
00419                             match = 1;
00420                      }
00421               }
00422               pos += 2;
00423        }
00424 
00425        else if (!strcasecmp(itemlist[pos].Key, "TEXT")) {
00426               if (msg == NULL) {
00427                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00428                      need_to_free_msg = 1;
00429               }
00430               if (msg != NULL) {
00431                      for (i='A'; i<='Z'; ++i) {
00432                             if (bmstrcasestr(msg->cm_fields[i], itemlist[pos+1].Key)) {
00433                                    match = 1;
00434                             }
00435                      }
00436               }
00437               pos += 2;
00438        }
00439 
00440        else if (!strcasecmp(itemlist[pos].Key, "TO")) {
00441               if (msg == NULL) {
00442                      msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
00443                      need_to_free_msg = 1;
00444               }
00445               if (msg != NULL) {
00446                      if (bmstrcasestr(msg->cm_fields['R'], itemlist[pos+1].Key)) {
00447                             match = 1;
00448                      }
00449               }
00450               pos += 2;
00451        }
00452 
00453        /* FIXME this is b0rken.  fix it. */
00454        else if (imap_is_message_set(itemlist[pos].Key)) {
00455               if (is_msg_in_sequence_set(itemlist[pos].Key, seq)) {
00456                      match = 1;
00457               }
00458               pos += 1;
00459        }
00460 
00461        /* FIXME this is b0rken.  fix it. */
00462        else if (!strcasecmp(itemlist[pos].Key, "UID")) {
00463               if (is_msg_in_sequence_set(itemlist[pos+1].Key, Imap->msgids[seq-1])) {
00464                      match = 1;
00465               }
00466               pos += 2;
00467        }
00468 
00469        /* Now here come the 'UN' criteria.  Why oh why do we have to
00470         * implement *both* the 'UN' criteria *and* the 'NOT' keyword?  Why
00471         * can't there be *one* way to do things?  More gratuitous complexity.
00472         */
00473 
00474        else if (!strcasecmp(itemlist[pos].Key, "UNANSWERED")) {
00475               if ((Imap->flags[seq-1] & IMAP_ANSWERED) == 0) {
00476                      match = 1;
00477               }
00478               ++pos;
00479        }
00480 
00481        else if (!strcasecmp(itemlist[pos].Key, "UNDELETED")) {
00482               if ((Imap->flags[seq-1] & IMAP_DELETED) == 0) {
00483                      match = 1;
00484               }
00485               ++pos;
00486        }
00487 
00488        else if (!strcasecmp(itemlist[pos].Key, "UNDRAFT")) {
00489               if ((Imap->flags[seq-1] & IMAP_DRAFT) == 0) {
00490                      match = 1;
00491               }
00492               ++pos;
00493        }
00494 
00495        else if (!strcasecmp(itemlist[pos].Key, "UNFLAGGED")) {
00496               if ((Imap->flags[seq-1] & IMAP_FLAGGED) == 0) {
00497                      match = 1;
00498               }
00499               ++pos;
00500        }
00501 
00502        else if (!strcasecmp(itemlist[pos].Key, "UNKEYWORD")) {
00503               /* FIXME */
00504               pos += 2;
00505        }
00506 
00507        else if (!strcasecmp(itemlist[pos].Key, "UNSEEN")) {
00508               if ((Imap->flags[seq-1] & IMAP_SEEN) == 0) {
00509                      match = 1;
00510               }
00511               ++pos;
00512        }
00513 
00514        /* Remember to negate if we were told to */
00515        if (is_not) {
00516               match = !match;
00517        }
00518 
00519        /* Keep going if there are more criteria! */
00520        if (pos < num_items) {
00521 
00522               if (is_or) {
00523                      match = (match || imap_do_search_msg(seq, msg,
00524                             num_items - pos, &itemlist[pos], is_uid));
00525               }
00526               else {
00527                      match = (match && imap_do_search_msg(seq, msg,
00528                             num_items - pos, &itemlist[pos], is_uid));
00529               }
00530 
00531        }
00532 
00533        if (need_to_free_msg) {
00534               CtdlFreeMessage(msg);
00535        }
00536        return(match);
00537 }
00538 
00539 
00540 /*
00541  * imap_search() calls imap_do_search() to do its actual work, once it's
00542  * validated and boiled down the request a bit.
00543  */
00544 void imap_do_search(int num_items, ConstStr *itemlist, int is_uid) {
00545        citimap *Imap = IMAP;
00546        int i, j, k;
00547        int fts_num_msgs = 0;
00548        long *fts_msgs = NULL;
00549        int is_in_list = 0;
00550        int num_results = 0;
00551 
00552        /* Strip parentheses.  We realize that this method will not work
00553         * in all cases, but it seems to work with all currently available
00554         * client software.  Revisit later...
00555         */
00556        for (i=0; i<num_items; ++i) {
00557               if (itemlist[i].Key[0] == '(') {
00558                      
00559                      TokenCutLeft(&Imap->Cmd, 
00560                                  &itemlist[i], 
00561                                  1);
00562               }
00563               if (itemlist[i].Key[itemlist[i].len-1] == ')') {
00564                      TokenCutRight(&Imap->Cmd, 
00565                                   &itemlist[i], 
00566                                   1);
00567               }
00568        }
00569 
00570        /* If there is a BODY search criterion in the query, use our full
00571         * text index to disqualify messages that don't have any chance of
00572         * matching.  (Only do this if the index is enabled!!)
00573         */
00574        if (config.c_enable_fulltext) for (i=0; i<(num_items-1); ++i) {
00575               if (!strcasecmp(itemlist[i].Key, "BODY")) {
00576                      CtdlModuleDoSearch(&fts_num_msgs, &fts_msgs, itemlist[i+1].Key, "fulltext");
00577                      if (fts_num_msgs > 0) {
00578                             for (j=0; j < Imap->num_msgs; ++j) {
00579                                    if (Imap->flags[j] & IMAP_SELECTED) {
00580                                           is_in_list = 0;
00581                                           for (k=0; k<fts_num_msgs; ++k) {
00582                                                  if (Imap->msgids[j] == fts_msgs[k]) {
00583                                                         ++is_in_list;
00584                                                  }
00585                                           }
00586                                    }
00587                                    if (!is_in_list) {
00588                                           Imap->flags[j] = Imap->flags[j] & ~IMAP_SELECTED;
00589                                    }
00590                             }
00591                      }
00592                      else {        /* no hits on the index; disqualify every message */
00593                             for (j=0; j < Imap->num_msgs; ++j) {
00594                                    Imap->flags[j] = Imap->flags[j] & ~IMAP_SELECTED;
00595                             }
00596                      }
00597                      if (fts_msgs) {
00598                             free(fts_msgs);
00599                      }
00600               }
00601        }
00602 
00603        /* Now go through the messages and apply all search criteria. */
00604        buffer_output();
00605        IAPuts("* SEARCH ");
00606        if (Imap->num_msgs > 0)
00607         for (i = 0; i < Imap->num_msgs; ++i)
00608          if (Imap->flags[i] & IMAP_SELECTED) {
00609               if (imap_do_search_msg(i+1, NULL, num_items, itemlist, is_uid)) {
00610                      if (num_results != 0) {
00611                             IAPuts(" ");
00612                      }
00613                      if (is_uid) {
00614                             IAPrintf("%ld", Imap->msgids[i]);
00615                      }
00616                      else {
00617                             IAPrintf("%d", i+1);
00618                      }
00619                      ++num_results;
00620               }
00621        }
00622        IAPuts("\r\n");
00623        unbuffer_output();
00624 }
00625 
00626 
00627 /*
00628  * This function is called by the main command loop.
00629  */
00630 void imap_search(int num_parms, ConstStr *Params) {
00631        int i;
00632 
00633        if (num_parms < 3) {
00634               IReply("BAD invalid parameters");
00635               return;
00636        }
00637 
00638        for (i = 0; i < IMAP->num_msgs; ++i) {
00639               IMAP->flags[i] |= IMAP_SELECTED;
00640        }
00641 
00642        imap_do_search(num_parms-2, &Params[2], 0);
00643        IReply("OK SEARCH completed");
00644 }
00645 
00646 /*
00647  * This function is called by the main command loop.
00648  */
00649 void imap_uidsearch(int num_parms, ConstStr *Params) {
00650        int i;
00651 
00652        if (num_parms < 4) {
00653               IReply("BAD invalid parameters");
00654               return;
00655        }
00656 
00657        for (i = 0; i < IMAP->num_msgs; ++i) {
00658               IMAP->flags[i] |= IMAP_SELECTED;
00659        }
00660 
00661        imap_do_search(num_parms-3, &Params[3], 1);
00662        IReply("OK UID SEARCH completed");
00663 }
00664 
00665