summaryrefslogtreecommitdiffstats
path: root/src/imap/imap-search.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
commitf7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch)
treea3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /src/imap/imap-search.c
parentInitial commit. (diff)
downloaddovecot-upstream.tar.xz
dovecot-upstream.zip
Adding upstream version 1:2.3.19.1+dfsg1.upstream/1%2.3.19.1+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/imap/imap-search.c')
-rw-r--r--src/imap/imap-search.c612
1 files changed, 612 insertions, 0 deletions
diff --git a/src/imap/imap-search.c b/src/imap/imap-search.c
new file mode 100644
index 0000000..8b5d660
--- /dev/null
+++ b/src/imap/imap-search.c
@@ -0,0 +1,612 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "ostream.h"
+#include "str.h"
+#include "seq-range-array.h"
+#include "time-util.h"
+#include "imap-resp-code.h"
+#include "imap-quote.h"
+#include "imap-seqset.h"
+#include "imap-util.h"
+#include "mail-search-build.h"
+#include "imap-fetch.h"
+#include "imap-commands.h"
+#include "imap-search-args.h"
+#include "imap-search.h"
+
+
+static int imap_search_deinit(struct imap_search_context *ctx);
+
+static int
+imap_partial_range_parse(struct imap_search_context *ctx, const char *str)
+{
+ ctx->partial1 = 0;
+ ctx->partial2 = 0;
+ for (; *str >= '0' && *str <= '9'; str++)
+ ctx->partial1 = ctx->partial1 * 10 + *str-'0';
+ if (*str != ':' || ctx->partial1 == 0)
+ return -1;
+ for (str++; *str >= '0' && *str <= '9'; str++)
+ ctx->partial2 = ctx->partial2 * 10 + *str-'0';
+ if (*str != '\0' || ctx->partial2 == 0)
+ return -1;
+
+ if (ctx->partial1 > ctx->partial2) {
+ uint32_t temp = ctx->partial2;
+ ctx->partial2 = ctx->partial1;
+ ctx->partial1 = temp;
+ }
+
+ return 0;
+}
+
+static bool
+search_parse_fetch_att(struct imap_search_context *ctx,
+ const struct imap_arg *update_args)
+{
+ const char *client_error;
+
+ ctx->fetch_pool = pool_alloconly_create("search update fetch", 512);
+ if (imap_fetch_att_list_parse(ctx->cmd->client, ctx->fetch_pool,
+ update_args, &ctx->fetch_ctx,
+ &client_error) < 0) {
+ client_send_command_error(ctx->cmd, t_strconcat(
+ "SEARCH UPDATE fetch-att: ", client_error, NULL));
+ pool_unref(&ctx->fetch_pool);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+search_parse_return_options(struct imap_search_context *ctx,
+ const struct imap_arg *args)
+{
+ struct client_command_context *cmd = ctx->cmd;
+ const struct imap_arg *update_args;
+ const char *name, *str;
+ unsigned int idx;
+
+ while (!IMAP_ARG_IS_EOL(args)) {
+ if (!imap_arg_get_atom(args, &name)) {
+ client_send_command_error(cmd,
+ "SEARCH return options contain non-atoms.");
+ return FALSE;
+ }
+ name = t_str_ucase(name);
+ args++;
+ if (strcmp(name, "MIN") == 0)
+ ctx->return_options |= SEARCH_RETURN_MIN;
+ else if (strcmp(name, "MAX") == 0)
+ ctx->return_options |= SEARCH_RETURN_MAX;
+ else if (strcmp(name, "ALL") == 0)
+ ctx->return_options |= SEARCH_RETURN_ALL;
+ else if (strcmp(name, "COUNT") == 0)
+ ctx->return_options |= SEARCH_RETURN_COUNT;
+ else if (strcmp(name, "SAVE") == 0)
+ ctx->return_options |= SEARCH_RETURN_SAVE;
+ else if (strcmp(name, "CONTEXT") == 0) {
+ /* no-op */
+ } else if (strcmp(name, "UPDATE") == 0) {
+ if ((ctx->return_options & SEARCH_RETURN_UPDATE) != 0) {
+ client_send_command_error(cmd,
+ "SEARCH return options have duplicate UPDATE.");
+ return FALSE;
+ }
+ ctx->return_options |= SEARCH_RETURN_UPDATE;
+ if (imap_arg_get_list(args, &update_args)) {
+ if (!search_parse_fetch_att(ctx, update_args))
+ return FALSE;
+ args++;
+ }
+ } else if (strcmp(name, "RELEVANCY") == 0)
+ ctx->return_options |= SEARCH_RETURN_RELEVANCY;
+ else if (strcmp(name, "PARTIAL") == 0) {
+ if (ctx->partial1 != 0) {
+ client_send_command_error(cmd,
+ "PARTIAL can be used only once.");
+ return FALSE;
+ }
+ ctx->return_options |= SEARCH_RETURN_PARTIAL;
+ if (!imap_arg_get_atom(args, &str)) {
+ client_send_command_error(cmd,
+ "PARTIAL range missing.");
+ return FALSE;
+ }
+ if (imap_partial_range_parse(ctx, str) < 0) {
+ client_send_command_error(cmd,
+ "PARTIAL range broken.");
+ return FALSE;
+ }
+ args++;
+ } else {
+ client_send_command_error(cmd,
+ "Unknown SEARCH return option");
+ return FALSE;
+ }
+ }
+
+ if ((ctx->return_options & SEARCH_RETURN_UPDATE) != 0 &&
+ client_search_update_lookup(cmd->client, cmd->tag, &idx) != NULL) {
+ client_send_command_error(cmd, "Duplicate search update tag");
+ return FALSE;
+ }
+ if ((ctx->return_options & SEARCH_RETURN_PARTIAL) != 0 &&
+ (ctx->return_options & SEARCH_RETURN_ALL) != 0) {
+ client_send_command_error(cmd, "PARTIAL conflicts with ALL");
+ return FALSE;
+ }
+
+ if (ctx->return_options == 0)
+ ctx->return_options = SEARCH_RETURN_ALL;
+ ctx->return_options |= SEARCH_RETURN_ESEARCH;
+ return TRUE;
+}
+
+static void imap_search_args_check(struct imap_search_context *ctx,
+ const struct mail_search_arg *sargs)
+{
+ for (; sargs != NULL; sargs = sargs->next) {
+ switch (sargs->type) {
+ case SEARCH_SEQSET:
+ ctx->have_seqsets = TRUE;
+ break;
+ case SEARCH_MODSEQ:
+ ctx->have_modseqs = TRUE;
+ break;
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ imap_search_args_check(ctx, sargs->value.subargs);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+static void imap_search_result_save(struct imap_search_context *ctx)
+{
+ struct client *client = ctx->cmd->client;
+ struct mail_search_result *result;
+ struct imap_search_update *update;
+
+ if (!array_is_created(&client->search_updates))
+ i_array_init(&client->search_updates, 32);
+ else if (array_count(&client->search_updates) >=
+ CLIENT_MAX_SEARCH_UPDATES) {
+ /* too many updates */
+ string_t *str = t_str_new(256);
+ str_append(str, "* NO [NOUPDATE ");
+ imap_append_quoted(str, ctx->cmd->tag);
+ str_append_c(str, ']');
+ client_send_line(client, str_c(str));
+ ctx->return_options &= ENUM_NEGATE(SEARCH_RETURN_UPDATE);
+ imap_search_context_free(ctx);
+ return;
+ }
+ result = mailbox_search_result_save(ctx->search_ctx,
+ MAILBOX_SEARCH_RESULT_FLAG_UPDATE |
+ MAILBOX_SEARCH_RESULT_FLAG_QUEUE_SYNC);
+
+ update = array_append_space(&client->search_updates);
+ update->tag = i_strdup(ctx->cmd->tag);
+ update->result = result;
+ update->return_uids = ctx->cmd->uid;
+ update->fetch_pool = ctx->fetch_pool;
+ update->fetch_ctx = ctx->fetch_ctx;
+ ctx->fetch_pool = NULL;
+ ctx->fetch_ctx = NULL;
+}
+
+static void imap_search_send_result_standard(struct imap_search_context *ctx)
+{
+ const struct seq_range *range;
+ string_t *str;
+ uint32_t seq;
+
+ str = t_str_new(1024);
+ str_append(str, ctx->sorting ? "* SORT" : "* SEARCH");
+ array_foreach(&ctx->result, range) {
+ for (seq = range->seq1; seq <= range->seq2; seq++)
+ str_printfa(str, " %u", seq);
+ if (str_len(str) >= 1024-32) {
+ o_stream_nsend(ctx->cmd->client->output,
+ str_data(str), str_len(str));
+ str_truncate(str, 0);
+ }
+ }
+
+ if (ctx->highest_seen_modseq != 0) {
+ str_printfa(str, " (MODSEQ %"PRIu64")",
+ ctx->highest_seen_modseq);
+ }
+ str_append(str, "\r\n");
+ o_stream_nsend(ctx->cmd->client->output, str_data(str), str_len(str));
+}
+
+static void
+imap_search_send_partial(struct imap_search_context *ctx, string_t *str)
+{
+ str_printfa(str, " PARTIAL (%u:%u ", ctx->partial1, ctx->partial2);
+ if (array_count(&ctx->result) == 0) {
+ /* no results (in range) */
+ str_append(str, "NIL");
+ } else {
+ imap_write_seq_range(str, &ctx->result);
+ }
+ str_append_c(str, ')');
+}
+
+static void
+imap_search_send_relevancy(struct imap_search_context *ctx, string_t *dest)
+{
+ const float *scores;
+ unsigned int i, count;
+ float diff, imap_score;
+
+ scores = array_get(&ctx->relevancy_scores, &count);
+ if (count == 0)
+ return;
+
+ /* we'll need to convert float scores to numbers 1..100
+ FIXME: would be a good idea to try to detect non-linear score
+ mappings and convert them better.. */
+ diff = ctx->max_relevancy - ctx->min_relevancy;
+ if (diff == 0)
+ diff = 1.0;
+ for (i = 0; i < count; i++) {
+ if (i > 0)
+ str_append_c(dest, ' ');
+ imap_score = (scores[i] - ctx->min_relevancy) / diff * 100.0;
+ if (imap_score < 1)
+ str_append(dest, "1");
+ else
+ str_printfa(dest, "%u", (unsigned int)imap_score);
+ }
+}
+
+static void imap_search_send_result(struct imap_search_context *ctx)
+{
+ struct client *client = ctx->cmd->client;
+ string_t *str;
+
+ if ((ctx->return_options & SEARCH_RETURN_ESEARCH) == 0) {
+ imap_search_send_result_standard(ctx);
+ return;
+ }
+
+ if (ctx->return_options ==
+ (SEARCH_RETURN_ESEARCH | SEARCH_RETURN_SAVE)) {
+ /* we only wanted to save the result, don't return
+ ESEARCH result. */
+ return;
+ }
+
+ str = str_new(default_pool, 1024);
+ str_append(str, "* ESEARCH (TAG ");
+ imap_append_string(str, ctx->cmd->tag);
+ str_append_c(str, ')');
+
+ if (ctx->cmd->uid)
+ str_append(str, " UID");
+
+ if ((ctx->return_options & SEARCH_RETURN_MIN) != 0 && ctx->min_id != 0)
+ str_printfa(str, " MIN %u", ctx->min_id);
+ if ((ctx->return_options & SEARCH_RETURN_MAX) != 0 &&
+ ctx->max_seq != 0) {
+ uint32_t id = ctx->cmd->uid ? ctx->max_uid : ctx->max_seq;
+ str_printfa(str, " MAX %u", id);
+ }
+
+ if ((ctx->return_options & SEARCH_RETURN_ALL) != 0 &&
+ array_count(&ctx->result) > 0) {
+ str_append(str, " ALL ");
+ imap_write_seq_range(str, &ctx->result);
+ }
+ if ((ctx->return_options & SEARCH_RETURN_RELEVANCY) != 0) {
+ str_append(str, " RELEVANCY (");
+ imap_search_send_relevancy(ctx, str);
+ str_append_c(str, ')');
+ }
+
+ if ((ctx->return_options & SEARCH_RETURN_PARTIAL) != 0)
+ imap_search_send_partial(ctx, str);
+
+ if ((ctx->return_options & SEARCH_RETURN_COUNT) != 0)
+ str_printfa(str, " COUNT %u", ctx->result_count);
+ if (ctx->highest_seen_modseq != 0) {
+ str_printfa(str, " MODSEQ %"PRIu64,
+ ctx->highest_seen_modseq);
+ }
+ str_append(str, "\r\n");
+ o_stream_nsend(client->output, str_data(str), str_len(str));
+ str_free(&str);
+}
+
+static void
+search_update_mail(struct imap_search_context *ctx, struct mail *mail)
+{
+ uint64_t modseq;
+
+ if (ctx->max_update_seq == mail->seq) {
+ /* MIN already handled this mail */
+ return;
+ }
+ ctx->max_update_seq = mail->seq;
+
+ if ((ctx->return_options & SEARCH_RETURN_MODSEQ) != 0) {
+ modseq = mail_get_modseq(mail);
+ if (ctx->highest_seen_modseq < modseq)
+ ctx->highest_seen_modseq = modseq;
+ }
+ if ((ctx->return_options & SEARCH_RETURN_SAVE) != 0) {
+ seq_range_array_add(&ctx->cmd->client->search_saved_uidset,
+ mail->uid);
+ }
+ if ((ctx->return_options & SEARCH_RETURN_RELEVANCY) != 0) {
+ const char *str;
+ float score;
+
+ if (mail_get_special(mail, MAIL_FETCH_SEARCH_RELEVANCY, &str) < 0)
+ score = 0;
+ else
+ score = strtod(str, NULL);
+ array_push_back(&ctx->relevancy_scores, &score);
+ if (ctx->min_relevancy > score)
+ ctx->min_relevancy = score;
+ if (ctx->max_relevancy < score)
+ ctx->max_relevancy = score;
+ }
+}
+
+static void search_add_result_id(struct imap_search_context *ctx, uint32_t id)
+{
+ struct seq_range *range;
+ unsigned int count;
+
+ /* only append the data. this is especially important when we're
+ returning a sort result. */
+ range = array_get_modifiable(&ctx->result, &count);
+ if (count > 0 && id == range[count-1].seq2 + 1) {
+ range[count-1].seq2++;
+ } else {
+ range = array_append_space(&ctx->result);
+ range->seq1 = range->seq2 = id;
+ }
+}
+
+static bool cmd_search_more(struct client_command_context *cmd)
+{
+ struct imap_search_context *ctx = cmd->context;
+ enum search_return_options opts = ctx->return_options;
+ struct mail *mail;
+ enum mailbox_sync_flags sync_flags;
+ uint32_t id;
+ const char *ok_reply;
+ bool tryagain, lost_data;
+
+ if (cmd->cancel) {
+ (void)imap_search_deinit(ctx);
+ return TRUE;
+ }
+
+ while (mailbox_search_next_nonblock(ctx->search_ctx,
+ &mail, &tryagain)) {
+ id = cmd->uid ? mail->uid : mail->seq;
+ ctx->result_count++;
+
+ ctx->max_seq = mail->seq;
+ ctx->max_uid = mail->uid;
+ if (HAS_ANY_BITS(opts, SEARCH_RETURN_MIN) && ctx->min_id == 0) {
+ /* MIN not set yet */
+ ctx->min_id = id;
+ search_update_mail(ctx, mail);
+ }
+ if (HAS_ANY_BITS(opts, SEARCH_RETURN_ALL)) {
+ /* ALL and PARTIAL are mutually exclusive */
+ i_assert(HAS_NO_BITS(opts, SEARCH_RETURN_PARTIAL));
+ search_add_result_id(ctx, id);
+ } else if ((opts & SEARCH_RETURN_PARTIAL) != 0) {
+ /* only update if it's within range */
+ i_assert(HAS_NO_BITS(opts, SEARCH_RETURN_ALL));
+ if (ctx->partial1 <= ctx->result_count &&
+ ctx->partial2 >= ctx->result_count)
+ search_add_result_id(ctx, id);
+ else if (HAS_ALL_BITS(opts, SEARCH_RETURN_COUNT |
+ SEARCH_RETURN_SAVE)) {
+ /* (SAVE COUNT PARTIAL n:m) must include all
+ results in SAVE, but not include mails
+ outside the PARTIAL range in MODSEQ or
+ RELEVANCY */
+ seq_range_array_add(&cmd->client->search_saved_uidset,
+ mail->uid);
+ continue;
+ } else {
+ continue;
+ }
+ } else if (HAS_ANY_BITS(opts, SEARCH_RETURN_COUNT)) {
+ /* with COUNT don't add it to results, but handle
+ SAVE and MODSEQ */
+ } else if (HAS_ANY_BITS(opts, SEARCH_RETURN_MIN |
+ SEARCH_RETURN_MAX)) {
+ /* MIN and/or MAX only requested, but we don't know if
+ this is MAX until the search is finished. */
+ continue;
+ } else if (HAS_ANY_BITS(opts, SEARCH_RETURN_SAVE)) {
+ /* Only SAVE used */
+ }
+ search_update_mail(ctx, mail);
+ }
+ if (tryagain)
+ return FALSE;
+
+ if ((opts & SEARCH_RETURN_MAX) != 0 && ctx->max_seq != 0 &&
+ ctx->max_update_seq != ctx->max_seq &&
+ HAS_ANY_BITS(opts, SEARCH_RETURN_MODSEQ |
+ SEARCH_RETURN_SAVE | SEARCH_RETURN_RELEVANCY)) {
+ /* finish handling MAX */
+ mail = mail_alloc(ctx->trans, 0, NULL);
+ mail_set_seq(mail, ctx->max_seq);
+ search_update_mail(ctx, mail);
+ mail_free(&mail);
+ }
+
+ lost_data = mailbox_search_seen_lost_data(ctx->search_ctx);
+ if (imap_search_deinit(ctx) < 0) {
+ client_send_box_error(cmd, cmd->client->mailbox);
+ return TRUE;
+ }
+
+ sync_flags = MAILBOX_SYNC_FLAG_FAST;
+ if (!cmd->uid || ctx->have_seqsets)
+ sync_flags |= MAILBOX_SYNC_FLAG_NO_EXPUNGES;
+ ok_reply = t_strdup_printf("OK %s%s completed",
+ lost_data ? "["IMAP_RESP_CODE_EXPUNGEISSUED"] " : "",
+ !ctx->sorting ? "Search" : "Sort");
+ return cmd_sync(cmd, sync_flags, 0, ok_reply);
+}
+
+static void cmd_search_more_callback(struct client_command_context *cmd)
+{
+ struct client *client = cmd->client;
+ bool finished;
+
+ o_stream_cork(client->output);
+ finished = command_exec(cmd);
+ o_stream_uncork(client->output);
+
+ if (!finished)
+ (void)client_handle_unfinished_cmd(cmd);
+ else
+ client_command_free(&cmd);
+ cmd_sync_delayed(client);
+
+ client_continue_pending_input(client);
+}
+
+int cmd_search_parse_return_if_found(struct imap_search_context *ctx,
+ const struct imap_arg **_args)
+{
+ const struct imap_arg *list_args, *args = *_args;
+ struct client_command_context *cmd = ctx->cmd;
+
+ if (!imap_arg_atom_equals(&args[0], "RETURN") ||
+ !imap_arg_get_list(&args[1], &list_args)) {
+ ctx->return_options = SEARCH_RETURN_ALL;
+ return 1;
+ }
+
+ if (!search_parse_return_options(ctx, list_args)) {
+ imap_search_context_free(ctx);
+ return -1;
+ }
+
+ if ((ctx->return_options & SEARCH_RETURN_SAVE) != 0) {
+ /* wait if there is another SEARCH SAVE command running. */
+ if (client_handle_search_save_ambiguity(cmd)) {
+ imap_search_context_free(ctx);
+ return 0;
+ }
+
+ cmd->search_save_result = TRUE;
+ }
+
+ *_args = args + 2;
+ return 1;
+}
+
+bool imap_search_start(struct imap_search_context *ctx,
+ struct mail_search_args *sargs,
+ const enum mail_sort_type *sort_program)
+{
+ struct client_command_context *cmd = ctx->cmd;
+
+ imap_search_args_check(ctx, sargs->args);
+
+ if (ctx->have_modseqs) {
+ ctx->return_options |= SEARCH_RETURN_MODSEQ;
+ client_enable(cmd->client, imap_feature_condstore);
+ }
+
+ ctx->box = cmd->client->mailbox;
+ ctx->trans = mailbox_transaction_begin(ctx->box, 0,
+ imap_client_command_get_reason(cmd));
+ ctx->sargs = sargs;
+ ctx->search_ctx =
+ mailbox_search_init(ctx->trans, sargs, sort_program, 0, NULL);
+ ctx->sorting = sort_program != NULL;
+ i_array_init(&ctx->result, 128);
+ if ((ctx->return_options & SEARCH_RETURN_UPDATE) != 0)
+ imap_search_result_save(ctx);
+ else {
+ i_assert(ctx->fetch_ctx == NULL);
+ }
+ if ((ctx->return_options & SEARCH_RETURN_RELEVANCY) != 0)
+ i_array_init(&ctx->relevancy_scores, 128);
+
+ cmd->func = cmd_search_more;
+ cmd->context = ctx;
+
+ if (cmd_search_more(cmd))
+ return TRUE;
+
+ /* we may have moved onto syncing by now */
+ if (cmd->func == cmd_search_more) {
+ ctx->to = timeout_add(0, cmd_search_more_callback, cmd);
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_EXTERNAL;
+ }
+ return FALSE;
+}
+
+static int imap_search_deinit(struct imap_search_context *ctx)
+{
+ int ret = 0;
+
+ if (mailbox_search_deinit(&ctx->search_ctx) < 0)
+ ret = -1;
+
+ /* Send the result also after failing. It might have something useful,
+ even though it didn't fully succeed. The client should be able to
+ realize that there was some failure because NO is returned. */
+ if (!ctx->cmd->cancel &&
+ (ret == 0 || array_count(&ctx->result) > 0))
+ imap_search_send_result(ctx);
+
+ if (ret < 0 || ctx->cmd->cancel) {
+ /* search failed */
+ if ((ctx->return_options & SEARCH_RETURN_SAVE) != 0)
+ array_clear(&ctx->cmd->client->search_saved_uidset);
+ }
+
+ (void)mailbox_transaction_commit(&ctx->trans);
+
+ timeout_remove(&ctx->to);
+ if (array_is_created(&ctx->relevancy_scores))
+ array_free(&ctx->relevancy_scores);
+ array_free(&ctx->result);
+ mail_search_args_deinit(ctx->sargs);
+ mail_search_args_unref(&ctx->sargs);
+ imap_search_context_free(ctx);
+
+ ctx->cmd->context = NULL;
+ return ret;
+}
+
+void imap_search_context_free(struct imap_search_context *ctx)
+{
+ if (ctx->fetch_ctx != NULL) {
+ imap_fetch_free(&ctx->fetch_ctx);
+ pool_unref(&ctx->fetch_pool);
+ }
+}
+
+void imap_search_update_free(struct imap_search_update *update)
+{
+ if (update->fetch_ctx != NULL) {
+ imap_fetch_free(&update->fetch_ctx);
+ pool_unref(&update->fetch_pool);
+ }
+ mailbox_search_result_free(&update->result);
+ i_free(update->tag);
+}