/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ #include "imap-common.h" #include "ostream.h" #include "imap-resp-code.h" #include "imap-commands.h" #include "imap-fetch.h" #include "imap-search-args.h" #include "mail-search.h" static const char *all_macro[] = { "FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE", NULL }; static const char *fast_macro[] = { "FLAGS", "INTERNALDATE", "RFC822.SIZE", NULL }; static const char *full_macro[] = { "FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE", "BODY", NULL }; static bool imap_fetch_cmd_init_handler(struct imap_fetch_context *ctx, struct client_command_context *cmd, const char *name, const struct imap_arg **args) { struct imap_fetch_init_context init_ctx; i_zero(&init_ctx); init_ctx.fetch_ctx = ctx; init_ctx.pool = ctx->ctx_pool; init_ctx.name = name; init_ctx.args = *args; if (!imap_fetch_init_handler(&init_ctx)) { i_assert(init_ctx.error != NULL); client_send_command_error(cmd, init_ctx.error); return FALSE; } *args = init_ctx.args; return TRUE; } static bool fetch_parse_args(struct imap_fetch_context *ctx, struct client_command_context *cmd, const struct imap_arg *arg, const struct imap_arg **next_arg_r) { const char *str, *const *macro; if (cmd->uid) { if (!imap_fetch_cmd_init_handler(ctx, cmd, "UID", &arg)) return FALSE; } if (imap_arg_get_atom(arg, &str)) { str = t_str_ucase(str); arg++; /* handle macros first */ if (strcmp(str, "ALL") == 0) macro = all_macro; else if (strcmp(str, "FAST") == 0) macro = fast_macro; else if (strcmp(str, "FULL") == 0) macro = full_macro; else { macro = NULL; if (!imap_fetch_cmd_init_handler(ctx, cmd, str, &arg)) return FALSE; } if (macro != NULL) { while (*macro != NULL) { if (!imap_fetch_cmd_init_handler(ctx, cmd, *macro, &arg)) return FALSE; macro++; } } *next_arg_r = arg; } else { *next_arg_r = arg + 1; arg = imap_arg_as_list(arg); if (IMAP_ARG_IS_EOL(arg)) { client_send_command_error(cmd, "FETCH list is empty."); return FALSE; } while (imap_arg_get_atom(arg, &str)) { str = t_str_ucase(str); arg++; if (!imap_fetch_cmd_init_handler(ctx, cmd, str, &arg)) return FALSE; } if (!IMAP_ARG_IS_EOL(arg)) { client_send_command_error(cmd, "FETCH list contains non-atoms."); return FALSE; } } return TRUE; } static bool fetch_parse_modifier(struct imap_fetch_context *ctx, struct client_command_context *cmd, struct mail_search_args *search_args, const char *name, const struct imap_arg **args, bool *send_vanished) { const char *str; uint64_t modseq; if (strcmp(name, "CHANGEDSINCE") == 0) { if (cmd->client->nonpermanent_modseqs) { client_send_command_error(cmd, "FETCH CHANGEDSINCE can't be used with non-permanent modseqs"); return FALSE; } if (!imap_arg_get_atom(*args, &str) || str_to_uint64(str, &modseq) < 0) { client_send_command_error(cmd, "Invalid CHANGEDSINCE modseq."); return FALSE; } *args += 1; imap_search_add_changed_since(search_args, modseq); imap_fetch_init_nofail_handler(ctx, imap_fetch_modseq_init); return TRUE; } if (strcmp(name, "VANISHED") == 0 && cmd->uid) { if (!client_has_enabled(ctx->client, imap_feature_qresync)) { client_send_command_error(cmd, "QRESYNC not enabled"); return FALSE; } *send_vanished = TRUE; return TRUE; } client_send_command_error(cmd, "Unknown FETCH modifier"); return FALSE; } static bool fetch_parse_modifiers(struct imap_fetch_context *ctx, struct client_command_context *cmd, struct mail_search_args *search_args, const struct imap_arg *args, bool *send_vanished_r) { const char *name; *send_vanished_r = FALSE; while (!IMAP_ARG_IS_EOL(args)) { if (!imap_arg_get_atom(args, &name)) { client_send_command_error(cmd, "FETCH modifiers contain non-atoms."); return FALSE; } args++; if (!fetch_parse_modifier(ctx, cmd, search_args, t_str_ucase(name), &args, send_vanished_r)) return FALSE; } if (*send_vanished_r && (search_args->args->next == NULL || search_args->args->next->type != SEARCH_MODSEQ)) { client_send_command_error(cmd, "VANISHED used without CHANGEDSINCE"); return FALSE; } return TRUE; } static bool cmd_fetch_finished(struct client_command_context *cmd ATTR_UNUSED) { return TRUE; } static bool imap_fetch_is_failed_retry(struct imap_fetch_context *ctx) { if (!array_is_created(&ctx->client->fetch_failed_uids) || !array_is_created(&ctx->fetch_failed_uids)) return FALSE; return seq_range_array_have_common(&ctx->client->fetch_failed_uids, &ctx->fetch_failed_uids); } static void imap_fetch_add_failed_uids(struct imap_fetch_context *ctx) { if (!array_is_created(&ctx->fetch_failed_uids)) return; if (!array_is_created(&ctx->client->fetch_failed_uids)) { p_array_init(&ctx->client->fetch_failed_uids, ctx->client->pool, array_count(&ctx->fetch_failed_uids)); } seq_range_array_merge(&ctx->client->fetch_failed_uids, &ctx->fetch_failed_uids); } static bool cmd_fetch_finish(struct imap_fetch_context *ctx, struct client_command_context *cmd) { static const char *ok_message = "OK Fetch completed."; const char *tagged_reply = ok_message; enum mail_error error; bool seen_flags_changed = ctx->state.seen_flags_changed; if (ctx->state.skipped_expunged_msgs) { tagged_reply = "OK ["IMAP_RESP_CODE_EXPUNGEISSUED"] " "Some messages were already expunged."; } if (imap_fetch_end(ctx) < 0) { const char *client_error; if (cmd->client->output->closed) { /* If we're canceling we need to finish this command or we'll assert crash. But normally we want to return FALSE so that the disconnect message logs about this fetch command and that these latest output bytes are included in it (which wouldn't happen if we called client_disconnect() here directly). */ cmd->func = cmd_fetch_finished; imap_fetch_free(&ctx); return cmd->cancel; } if (ctx->error == MAIL_ERROR_NONE) client_error = mailbox_get_last_error(cmd->client->mailbox, &error); else { client_error = ctx->errstr; error = ctx->error; } if (error == MAIL_ERROR_CONVERSION) { /* BINARY found unsupported Content-Transfer-Encoding */ tagged_reply = t_strdup_printf( "NO ["IMAP_RESP_CODE_UNKNOWN_CTE"] %s", client_error); } else if (error == MAIL_ERROR_INVALIDDATA) { /* Content was invalid */ tagged_reply = t_strdup_printf( "NO ["IMAP_RESP_CODE_PARSE"] %s", client_error); } else if (cmd->client->set->parsed_fetch_failure != IMAP_CLIENT_FETCH_FAILURE_NO_AFTER || imap_fetch_is_failed_retry(ctx)) { /* By default we never want to reply NO to FETCH requests, because many IMAP clients become confused about what they should on NO. A disconnection causes less confusion. */ const char *internal_error = mailbox_get_last_internal_error(cmd->client->mailbox, NULL); client_send_line(cmd->client, t_strconcat( "* BYE FETCH failed: ", client_error, NULL)); client_disconnect(cmd->client, t_strconcat( "FETCH failed: ", internal_error, NULL)); imap_fetch_free(&ctx); return TRUE; } else { /* Use a tagged NO to FETCH failure, but only if client hasn't repeated the FETCH to the same email (so that we avoid infinitely retries from client.) */ imap_fetch_add_failed_uids(ctx); tagged_reply = t_strdup_printf( "NO ["IMAP_RESP_CODE_SERVERBUG"] %s", client_error); } } imap_fetch_free(&ctx); return cmd_sync(cmd, (seen_flags_changed ? 0 : MAILBOX_SYNC_FLAG_FAST) | (cmd->uid ? 0 : MAILBOX_SYNC_FLAG_NO_EXPUNGES), 0, tagged_reply); } static bool cmd_fetch_continue(struct client_command_context *cmd) { struct imap_fetch_context *ctx = cmd->context; if (imap_fetch_more(ctx, cmd) == 0) { /* unfinished */ return FALSE; } return cmd_fetch_finish(ctx, cmd); } static void cmd_fetch_set_reason_codes(struct client_command_context *cmd, struct imap_fetch_context *ctx) { /* Fetching body or header always causes the message to be opened. Use them as the primary reason. */ if ((ctx->fetch_data & MAIL_FETCH_STREAM_BODY) != 0) { event_strlist_append(cmd->global_event, EVENT_REASON_CODE, "imap:fetch_body"); return; } if ((ctx->fetch_data & MAIL_FETCH_STREAM_HEADER) != 0) { event_strlist_append(cmd->global_event, EVENT_REASON_CODE, "imap:fetch_header"); return; } /* The rest of these can come from cache. Since especially with prefetching we can't really know which one of them specifically triggered opening the mail, just use all of them as the reasons. */ if (ctx->fetch_header_fields || HAS_ANY_BITS(ctx->fetch_data, MAIL_FETCH_IMAP_ENVELOPE | MAIL_FETCH_DATE)) { event_strlist_append(cmd->global_event, EVENT_REASON_CODE, "imap:fetch_header_fields"); } if (HAS_ANY_BITS(ctx->fetch_data, MAIL_FETCH_IMAP_BODY | MAIL_FETCH_IMAP_BODYSTRUCTURE)) { event_strlist_append(cmd->global_event, EVENT_REASON_CODE, "imap:fetch_bodystructure"); } if (HAS_ANY_BITS(ctx->fetch_data, MAIL_FETCH_PHYSICAL_SIZE | MAIL_FETCH_VIRTUAL_SIZE)) { event_strlist_append(cmd->global_event, EVENT_REASON_CODE, "imap:fetch_size"); } } bool cmd_fetch(struct client_command_context *cmd) { struct client *client = cmd->client; struct imap_fetch_context *ctx; const struct imap_arg *args, *next_arg, *list_arg; struct mail_search_args *search_args; struct imap_fetch_qresync_args qresync_args; const char *messageset; bool send_vanished = FALSE; int ret; if (!client_read_args(cmd, 0, 0, &args)) return FALSE; if (!client_verify_open_mailbox(cmd)) return TRUE; /* [(modifiers)] */ if (!imap_arg_get_atom(&args[0], &messageset) || (args[1].type != IMAP_ARG_LIST && args[1].type != IMAP_ARG_ATOM) || (!IMAP_ARG_IS_EOL(&args[2]) && args[2].type != IMAP_ARG_LIST)) { client_send_command_error(cmd, "Invalid arguments."); return TRUE; } /* UID FETCH VANISHED needs the uidset, so convert it to sequence set later */ ret = imap_search_get_anyset(cmd, messageset, cmd->uid, &search_args); if (ret <= 0) return ret < 0; ctx = imap_fetch_alloc(client, cmd->pool, imap_client_command_get_reason(cmd)); if (!fetch_parse_args(ctx, cmd, &args[1], &next_arg) || (imap_arg_get_list(next_arg, &list_arg) && !fetch_parse_modifiers(ctx, cmd, search_args, list_arg, &send_vanished))) { imap_fetch_free(&ctx); mail_search_args_unref(&search_args); return TRUE; } if (send_vanished) { i_zero(&qresync_args); if (imap_fetch_send_vanished(client, client->mailbox, search_args, &qresync_args) < 0) { mail_search_args_unref(&search_args); return cmd_fetch_finish(ctx, cmd); } } cmd_fetch_set_reason_codes(cmd, ctx); imap_fetch_begin(ctx, client->mailbox, search_args); mail_search_args_unref(&search_args); if (imap_fetch_more(ctx, cmd) == 0) { /* unfinished */ cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT; cmd->func = cmd_fetch_continue; cmd->context = ctx; return FALSE; } return cmd_fetch_finish(ctx, cmd); }