diff options
Diffstat (limited to 'src/imap/imap-client.c')
-rw-r--r-- | src/imap/imap-client.c | 1681 |
1 files changed, 1681 insertions, 0 deletions
diff --git a/src/imap/imap-client.c b/src/imap/imap-client.c new file mode 100644 index 0000000..f3c1643 --- /dev/null +++ b/src/imap/imap-client.c @@ -0,0 +1,1681 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "ioloop.h" +#include "llist.h" +#include "str.h" +#include "hostpid.h" +#include "net.h" +#include "iostream.h" +#include "iostream-rawlog.h" +#include "istream.h" +#include "istream-concat.h" +#include "ostream.h" +#include "time-util.h" +#include "var-expand.h" +#include "master-service.h" +#include "imap-resp-code.h" +#include "imap-util.h" +#include "imap-urlauth.h" +#include "mail-error.h" +#include "mail-namespace.h" +#include "mail-storage-service.h" +#include "mail-autoexpunge.h" +#include "imap-state.h" +#include "imap-search.h" +#include "imap-notify.h" +#include "imap-commands.h" +#include "imap-feature.h" + +#include <unistd.h> + +/* If the last command took longer than this to run, log statistics on + where the time was spent. */ +#define IMAP_CLIENT_DISCONNECT_LOG_STATS_CMD_MIN_RUNNING_MSECS 1000 + +extern struct mail_storage_callbacks mail_storage_callbacks; +extern struct imap_client_vfuncs imap_client_vfuncs; + +struct imap_module_register imap_module_register = { 0 }; + +struct client *imap_clients = NULL; +unsigned int imap_client_count = 0; + +unsigned int imap_feature_condstore = UINT_MAX; +unsigned int imap_feature_qresync = UINT_MAX; + +static const char *client_command_state_names[CLIENT_COMMAND_STATE_DONE+1] = { + "wait-input", + "wait-output", + "wait-external", + "wait-unambiguity", + "wait-sync", + "done" +}; + +static void client_idle_timeout(struct client *client) +{ + if (client->output_cmd_lock == NULL) + client_send_line(client, "* BYE Disconnected for inactivity."); + client_destroy(client, t_strdup_printf( + "Inactivity - no input for %"PRIdTIME_T" secs", + ioloop_time - client->last_input)); +} + +static void client_init_urlauth(struct client *client) +{ + struct imap_urlauth_config config; + + i_zero(&config); + config.url_host = client->set->imap_urlauth_host; + config.url_port = client->set->imap_urlauth_port; + config.socket_path = t_strconcat(client->user->set->base_dir, + "/"IMAP_URLAUTH_SOCKET_NAME, NULL); + config.session_id = client->user->session_id; + config.access_user = client->user->username; + config.access_service = "imap"; + config.access_anonymous = client->user->anonymous; + + client->urlauth_ctx = imap_urlauth_init(client->user, &config); +} + +static bool user_has_special_use_mailboxes(struct mail_user *user) +{ + struct mail_namespace_settings *ns_set; + + /* + * We have to iterate over namespace and mailbox *settings* since + * the namespaces haven't been set up yet. The namespaces haven't + * been set up so that we don't hold up the OK response to LOGIN + * when using slow lib-storage backends. + */ + + /* no namespaces => no special use flags */ + if (!array_is_created(&user->set->namespaces)) + return FALSE; + + array_foreach_elem(&user->set->namespaces, ns_set) { + struct mailbox_settings *box_set; + + /* no mailboxes => no special use flags */ + if (!array_is_created(&ns_set->mailboxes)) + continue; + + array_foreach_elem(&ns_set->mailboxes, box_set) { + if (box_set->special_use != NULL) + return TRUE; + } + } + + return FALSE; +} + +struct client *client_create(int fd_in, int fd_out, bool unhibernated, + struct event *event, struct mail_user *user, + struct mail_storage_service_user *service_user, + const struct imap_settings *set, + const struct smtp_submit_settings *smtp_set) +{ + const struct mail_storage_settings *mail_set; + struct client *client; + const char *ident; + pool_t pool; + + /* always use nonblocking I/O */ + net_set_nonblock(fd_in, TRUE); + net_set_nonblock(fd_out, TRUE); + + pool = pool_alloconly_create("imap client", 2048); + client = p_new(pool, struct client, 1); + client->pool = pool; + client->v = imap_client_vfuncs; + client->event = event; + event_ref(client->event); + client->unhibernated = unhibernated; + client->set = set; + client->smtp_set = smtp_set; + client->service_user = service_user; + client->fd_in = fd_in; + client->fd_out = fd_out; + client->input = i_stream_create_fd(fd_in, + set->imap_max_line_length); + client->output = o_stream_create_fd(fd_out, SIZE_MAX); + o_stream_set_no_error_handling(client->output, TRUE); + i_stream_set_name(client->input, "<imap client>"); + o_stream_set_name(client->output, "<imap client>"); + + o_stream_set_flush_callback(client->output, client_output, client); + + p_array_init(&client->module_contexts, client->pool, 5); + client->last_input = ioloop_time; + client->to_idle = timeout_add(CLIENT_IDLE_TIMEOUT_MSECS, + client_idle_timeout, client); + + client->command_pool = + pool_alloconly_create(MEMPOOL_GROWING"client command", 1024*2); + client->user = user; + client->notify_count_changes = TRUE; + client->notify_flag_changes = TRUE; + p_array_init(&client->enabled_features, client->pool, 8); + + client->capability_string = + str_new(client->pool, sizeof(CAPABILITY_STRING)+64); + + if (*set->imap_capability == '\0') + str_append(client->capability_string, CAPABILITY_STRING); + else if (*set->imap_capability != '+') { + str_append(client->capability_string, set->imap_capability); + } else { + str_append(client->capability_string, CAPABILITY_STRING); + str_append_c(client->capability_string, ' '); + str_append(client->capability_string, set->imap_capability + 1); + } + if (client->set->imap_literal_minus) + client_add_capability(client, "LITERAL-"); + else + client_add_capability(client, "LITERAL+"); + if (user->fuzzy_search) { + /* Enable FUZZY capability only when it actually has + a chance of working */ + client_add_capability(client, "SEARCH=FUZZY"); + } + + mail_set = mail_user_set_get_storage_set(user); + if (mail_set->mailbox_list_index) { + /* NOTIFY is enabled only when mailbox list indexes are + enabled, although even that doesn't necessarily guarantee + it always */ + client_add_capability(client, "NOTIFY"); + } + + if (*set->imap_urlauth_host != '\0' && + *mail_set->mail_attribute_dict != '\0') { + /* Enable URLAUTH capability only when dict is + configured correctly */ + client_init_urlauth(client); + client_add_capability(client, "URLAUTH"); + client_add_capability(client, "URLAUTH=BINARY"); + } + if (set->imap_metadata && *mail_set->mail_attribute_dict != '\0') + client_add_capability(client, "METADATA"); + if (user_has_special_use_mailboxes(user)) { + /* Advertise SPECIAL-USE only if there are actually some + SPECIAL-USE flags in mailbox configuration. */ + client_add_capability(client, "SPECIAL-USE"); + } + + ident = mail_user_get_anvil_userip_ident(client->user); + if (ident != NULL) { + master_service_anvil_send(master_service, t_strconcat( + "CONNECT\t", my_pid, "\timap/", ident, "\n", NULL)); + client->anvil_sent = TRUE; + } + + imap_client_count++; + DLLIST_PREPEND(&imap_clients, client); + if (hook_client_created != NULL) + hook_client_created(&client); + + imap_refresh_proctitle(); + return client; +} + +void client_create_finish_io(struct client *client) +{ + if (client->set->rawlog_dir[0] != '\0') { + (void)iostream_rawlog_create(client->set->rawlog_dir, + &client->input, &client->output); + } + client->io = io_add_istream(client->input, client_input, client); +} + +int client_create_finish(struct client *client, const char **error_r) +{ + if (mail_namespaces_init(client->user, error_r) < 0) + return -1; + mail_namespaces_set_storage_callbacks(client->user->namespaces, + &mail_storage_callbacks, client); + client->v.init(client); + return 0; +} + +void client_add_istream_prefix(struct client *client, + const unsigned char *data, size_t size) +{ + i_assert(client->io == NULL); + + struct istream *inputs[] = { + i_stream_create_copy_from_data(data, size), + client->input, + NULL + }; + client->input = i_stream_create_concat(inputs); + i_stream_copy_fd(client->input, inputs[1]); + i_stream_unref(&inputs[0]); + i_stream_unref(&inputs[1]); + + i_stream_set_input_pending(client->input, TRUE); +} + +static void client_default_init(struct client *client ATTR_UNUSED) +{ + /* nothing */ +} + +void client_command_cancel(struct client_command_context **_cmd) +{ + struct client_command_context *cmd = *_cmd; + bool cmd_ret; + + switch (cmd->state) { + case CLIENT_COMMAND_STATE_WAIT_INPUT: + /* a bit kludgy check: cancel command only if it has context + set. currently only append command matches this check. all + other commands haven't even started the processing yet. */ + if (cmd->context == NULL) + break; + /* fall through */ + case CLIENT_COMMAND_STATE_WAIT_EXTERNAL: + case CLIENT_COMMAND_STATE_WAIT_OUTPUT: + cmd->cancel = TRUE; + break; + case CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY: + case CLIENT_COMMAND_STATE_WAIT_SYNC: + /* commands haven't started yet */ + break; + case CLIENT_COMMAND_STATE_DONE: + i_unreached(); + } + + cmd_ret = !cmd->cancel || cmd->func == NULL ? TRUE : + command_exec(cmd); + if (!cmd_ret) { + if (cmd->client->output->closed) + i_panic("command didn't cancel itself: %s", cmd->name); + } else { + client_command_free(*_cmd != NULL ? _cmd : &cmd); + } +} + +const char *client_stats(struct client *client) +{ + const struct var_expand_table logout_tab[] = { + { 'i', dec2str(i_stream_get_absolute_offset(client->input)), "input" }, + { 'o', dec2str(client->output->offset), "output" }, + { '\0', client->user->session_id, "session" }, + { '\0', dec2str(client->fetch_hdr_count), "fetch_hdr_count" }, + { '\0', dec2str(client->fetch_hdr_bytes), "fetch_hdr_bytes" }, + { '\0', dec2str(client->fetch_body_count), "fetch_body_count" }, + { '\0', dec2str(client->fetch_body_bytes), "fetch_body_bytes" }, + { '\0', dec2str(client->deleted_count), "deleted" }, + { '\0', dec2str(client->expunged_count), "expunged" }, + { '\0', dec2str(client->trashed_count), "trashed" }, + { '\0', dec2str(client->autoexpunged_count), "autoexpunged" }, + { '\0', dec2str(client->append_count), "appended" }, + { '\0', NULL, NULL } + }; + const struct var_expand_table *user_tab = + mail_user_var_expand_table(client->user); + const struct var_expand_table *tab = + t_var_expand_merge_tables(logout_tab, user_tab); + string_t *str; + const char *error; + + str = t_str_new(128); + if (var_expand_with_funcs(str, client->set->imap_logout_format, + tab, mail_user_var_expand_func_table, + client->user, &error) < 0) { + e_error(client->event, + "Failed to expand imap_logout_format=%s: %s", + client->set->imap_logout_format, error); + } + return str_c(str); +} + +void client_destroy(struct client *client, const char *reason) +{ + client->v.destroy(client, reason); +} + +static void +client_command_stats_append(string_t *str, + const struct client_command_stats *stats, + const char *wait_condition, + size_t buffered_size) +{ + uint64_t ioloop_wait_usecs; + unsigned int msecs_in_ioloop; + + ioloop_wait_usecs = io_loop_get_wait_usecs(current_ioloop); + msecs_in_ioloop = (ioloop_wait_usecs - + stats->start_ioloop_wait_usecs + 999) / 1000; + str_printfa(str, "running for %d.%03d + waiting ", + (int)((stats->running_usecs+999)/1000 / 1000), + (int)((stats->running_usecs+999)/1000 % 1000)); + if (wait_condition[0] != '\0') + str_printfa(str, "%s ", wait_condition); + str_printfa(str, "for %d.%03d secs", + msecs_in_ioloop / 1000, msecs_in_ioloop % 1000); + if (stats->lock_wait_usecs > 0) { + int lock_wait_msecs = (stats->lock_wait_usecs+999)/1000; + str_printfa(str, ", %d.%03d in locks", + lock_wait_msecs/1000, lock_wait_msecs%1000); + } + str_printfa(str, ", %"PRIu64" B in + %"PRIu64, + stats->bytes_in, stats->bytes_out); + if (buffered_size > 0) + str_printfa(str, "+%zu", buffered_size); + str_append(str, " B out"); +} + +static const char *client_get_last_command_status(struct client *client) +{ + if (client->logged_out) + return ""; + if (client->last_cmd_name == NULL) { + if (client->unhibernated) + return " (No commands sent after unhibernation)"; + else + return " (No commands sent)"; + } + + /* client disconnected without sending LOGOUT. if the last command + took over 1 second to run, log it. */ + const struct client_command_stats *stats = &client->last_cmd_stats; + + string_t *str = t_str_new(128); + int last_run_secs = timeval_diff_msecs(&ioloop_timeval, + &stats->last_run_timeval); + str_printfa(str, " (%s finished %d.%03d secs ago", + client->last_cmd_name, last_run_secs/1000, + last_run_secs%1000); + + if (timeval_diff_msecs(&stats->last_run_timeval, &stats->start_time) >= + IMAP_CLIENT_DISCONNECT_LOG_STATS_CMD_MIN_RUNNING_MSECS) { + str_append(str, " - "); + client_command_stats_append(str, stats, "", 0); + } + str_append_c(str, ')'); + return str_c(str); +} + +static const char *client_get_commands_status(struct client *client) +{ + struct client_command_context *cmd, *last_cmd = NULL; + struct client_command_stats all_stats; + string_t *str; + enum io_condition cond; + const char *cond_str; + + if (client->command_queue == NULL) + return client_get_last_command_status(client); + + i_zero(&all_stats); + str = t_str_new(128); + str_append(str, " ("); + for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) { + if (cmd->name == NULL) { + /* (parts of a) tag were received, but not yet + the command name */ + continue; + } + str_append(str, cmd->name); + if (cmd->next != NULL) + str_append_c(str, ','); + all_stats.running_usecs += cmd->stats.running_usecs; + all_stats.lock_wait_usecs += cmd->stats.lock_wait_usecs; + all_stats.bytes_in += cmd->stats.bytes_in; + all_stats.bytes_out += cmd->stats.bytes_out; + last_cmd = cmd; + } + if (last_cmd == NULL) + return client_get_last_command_status(client); + + cond = io_loop_find_fd_conditions(current_ioloop, client->fd_out); + if ((cond & (IO_READ | IO_WRITE)) == (IO_READ | IO_WRITE)) + cond_str = "input/output"; + else if ((cond & IO_READ) != 0) + cond_str = "input"; + else if ((cond & IO_WRITE) != 0) + cond_str = "output"; + else + cond_str = "nothing"; + + all_stats.start_ioloop_wait_usecs = + last_cmd->stats.start_ioloop_wait_usecs; + str_append_c(str, ' '); + client_command_stats_append(str, &all_stats, cond_str, + o_stream_get_buffer_used_size(client->output)); + str_printfa(str, ", state=%s)", + client_command_state_names[last_cmd->state]); + return str_c(str); +} + +static void client_log_disconnect(struct client *client, const char *reason) +{ + e_info(client->event, "Disconnected: %s %s", reason, client_stats(client)); +} + +static void client_default_destroy(struct client *client, const char *reason) +{ + struct client_command_context *cmd; + + i_assert(!client->destroyed); + client->destroyed = TRUE; + client->disconnected = TRUE; + + if (client->disconnect_reason != NULL) + reason = client->disconnect_reason; + if (reason == NULL) + reason = t_strconcat( + io_stream_get_disconnect_reason(client->input, + client->output), + client_get_commands_status(client), NULL); + + i_stream_close(client->input); + o_stream_close(client->output); + + /* finish off all the queued commands. */ + if (client->output_cmd_lock != NULL) + client_command_cancel(&client->output_cmd_lock); + while (client->command_queue != NULL) { + cmd = client->command_queue; + client_command_cancel(&cmd); + } + /* handle the input_lock command last. it might have been waiting on + other queued commands (although we probably should just drop the + command at that point since it hasn't started running. but this may + change in future). */ + if (client->input_lock != NULL) + client_command_cancel(&client->input_lock); + + if (client->notify_ctx != NULL) + imap_notify_deinit(&client->notify_ctx); + if (client->urlauth_ctx != NULL) + imap_urlauth_deinit(&client->urlauth_ctx); + /* Keep mailbox closing close to last, so anything that could + potentially have transactions open will close them first. */ + if (client->mailbox != NULL) + imap_client_close_mailbox(client); + if (client->anvil_sent) { + master_service_anvil_send(master_service, t_strconcat( + "DISCONNECT\t", my_pid, "\timap/", + mail_user_get_anvil_userip_ident(client->user), + "\n", NULL)); + } + + if (client->free_parser != NULL) + imap_parser_unref(&client->free_parser); + io_remove(&client->io); + timeout_remove(&client->to_idle_output); + timeout_remove(&client->to_idle); + + /* i/ostreams are already closed at this stage, so fd can be closed */ + fd_close_maybe_stdio(&client->fd_in, &client->fd_out); + + /* Autoexpunging might run for a long time. Disconnect the client + before it starts, and refresh proctitle so it's clear that it's + doing autoexpunging. We've also sent DISCONNECT to anvil already, + because this is background work and shouldn't really be counted + as an active IMAP session for the user. + + Don't autoexpunge if the client is hibernated - it shouldn't be any + different from the non-hibernating IDLE case. For frequent + hibernations it could also be doing unnecessarily much work. */ + imap_refresh_proctitle(); + if (!client->hibernated) { + client->autoexpunged_count = mail_user_autoexpunge(client->user); + client_log_disconnect(client, reason); + } + mail_user_deinit(&client->user); + + /* free the i/ostreams after mail_user_unref(), which could trigger + mail_storage_callbacks notifications that write to the ostream. */ + i_stream_destroy(&client->input); + o_stream_destroy(&client->output); + + if (array_is_created(&client->search_saved_uidset)) + array_free(&client->search_saved_uidset); + if (array_is_created(&client->search_updates)) + array_free(&client->search_updates); + pool_unref(&client->command_pool); + mail_storage_service_user_unref(&client->service_user); + + imap_client_count--; + DLLIST_REMOVE(&imap_clients, client); + + event_unref(&client->event); + i_free(client->last_cmd_name); + pool_unref(&client->pool); + + master_service_client_connection_destroyed(master_service); + imap_refresh_proctitle(); +} + +static void client_destroy_timeout(struct client *client) +{ + client_destroy(client, NULL); +} + +void client_disconnect(struct client *client, const char *reason) +{ + if (client->disconnected) + return; + + client->disconnected = TRUE; + client->disconnect_reason = p_strdup(client->pool, reason); + /* Finish the ostream. With IMAP COMPRESS this sends the EOF marker. */ + (void)o_stream_finish(client->output); + o_stream_uncork(client->output); + + i_stream_close(client->input); + o_stream_close(client->output); + + timeout_remove(&client->to_idle); + client->to_idle = timeout_add(0, client_destroy_timeout, client); +} + +void client_disconnect_with_error(struct client *client, + const char *client_error) +{ + client_send_line(client, t_strconcat("* BYE ", client_error, NULL)); + client_disconnect(client, client_error); +} + +void client_add_capability(struct client *client, const char *capability) +{ + /* require a single capability at a time (feels cleaner) */ + i_assert(strchr(capability, ' ') == NULL); + + if (client->set->imap_capability[0] != '\0' && + client->set->imap_capability[0] != '+') { + /* explicit capability - don't change it */ + return; + } + str_append_c(client->capability_string, ' '); + str_append(client->capability_string, capability); +} + +void client_send_line(struct client *client, const char *data) +{ + (void)client_send_line_next(client, data); +} + +int client_send_line_next(struct client *client, const char *data) +{ + struct const_iovec iov[2]; + + if (client->output->closed) + return -1; + + iov[0].iov_base = data; + iov[0].iov_len = strlen(data); + iov[1].iov_base = "\r\n"; + iov[1].iov_len = 2; + + if (o_stream_sendv(client->output, iov, 2) < 0) + return -1; + client->last_output = ioloop_time; + + if (o_stream_get_buffer_used_size(client->output) >= + CLIENT_OUTPUT_OPTIMAL_SIZE) { + /* buffer full, try flushing */ + return o_stream_flush(client->output); + } + return 1; +} + +static void +client_cmd_append_timing_stats(struct client_command_context *cmd, + string_t *str) +{ + unsigned int msecs_in_cmd, msecs_in_ioloop; + uint64_t ioloop_wait_usecs; + unsigned int msecs_since_cmd; + + if (cmd->stats.start_time.tv_sec == 0) + return; + command_stats_flush(cmd); + + ioloop_wait_usecs = io_loop_get_wait_usecs(current_ioloop); + msecs_in_cmd = (cmd->stats.running_usecs + 999) / 1000; + msecs_in_ioloop = (ioloop_wait_usecs - + cmd->stats.start_ioloop_wait_usecs + 999) / 1000; + msecs_since_cmd = timeval_diff_msecs(&ioloop_timeval, + &cmd->stats.last_run_timeval); + + if (str_data(str)[str_len(str)-1] == '.') + str_truncate(str, str_len(str)-1); + str_printfa(str, " (%d.%03d + %d.%03d ", + msecs_in_cmd / 1000, msecs_in_cmd % 1000, + msecs_in_ioloop / 1000, msecs_in_ioloop % 1000); + if (msecs_since_cmd > 0) { + str_printfa(str, "+ %d.%03d ", + msecs_since_cmd / 1000, msecs_since_cmd % 1000); + } + str_append(str, "secs)."); +} + +void client_send_tagline(struct client_command_context *cmd, const char *data) +{ + cmd->client->v.send_tagline(cmd, data); +} + +static void +client_default_send_tagline(struct client_command_context *cmd, const char *data) +{ + struct client *client = cmd->client; + const char *tag = cmd->tag; + + if (client->output->closed || cmd->cancel) + return; + + i_assert(!cmd->tagline_sent); + cmd->tagline_sent = TRUE; + cmd->tagline_reply = p_strdup(cmd->pool, data); + + if (tag == NULL || *tag == '\0') + tag = "*"; + + T_BEGIN { + string_t *str = t_str_new(256); + str_printfa(str, "%s %s", tag, data); + client_cmd_append_timing_stats(cmd, str); + str_append(str, "\r\n"); + o_stream_nsend(client->output, str_data(str), str_len(str)); + } T_END; + + client->last_output = ioloop_time; +} + +static int +client_default_sync_notify_more(struct imap_sync_context *ctx ATTR_UNUSED) +{ + return 1; +} + +void client_send_command_error(struct client_command_context *cmd, + const char *client_error) +{ + struct client *client = cmd->client; + const char *error, *cmd_name; + enum imap_parser_error parse_error; + + if (client_error == NULL) { + client_error = imap_parser_get_error(cmd->parser, &parse_error); + switch (parse_error) { + case IMAP_PARSE_ERROR_NONE: + i_unreached(); + case IMAP_PARSE_ERROR_LITERAL_TOO_BIG: + client_disconnect_with_error(client, client_error); + return; + default: + break; + } + } + + if (cmd->tag == NULL) + error = t_strconcat("BAD Error in IMAP tag: ", client_error, NULL); + else if (cmd->name == NULL) + error = t_strconcat("BAD Error in IMAP command: ", client_error, NULL); + else { + cmd_name = t_str_ucase(cmd->name); + error = t_strconcat("BAD Error in IMAP command ", + cmd_name, ": ", client_error, NULL); + } + + client_send_tagline(cmd, error); + + if (++client->bad_counter >= CLIENT_MAX_BAD_COMMANDS) { + client_disconnect_with_error(client, + "Too many invalid IMAP commands."); + } + + cmd->param_error = TRUE; + /* client_read_args() failures rely on this being set, so that the + command processing is stopped even while command function returns + FALSE. */ + cmd->state = CLIENT_COMMAND_STATE_DONE; +} + +void client_send_internal_error(struct client_command_context *cmd) +{ + client_send_tagline(cmd, + t_strflocaltime("NO "MAIL_ERRSTR_CRITICAL_MSG_STAMP, ioloop_time)); +} + +bool client_read_args(struct client_command_context *cmd, unsigned int count, + unsigned int flags, const struct imap_arg **args_r) +{ + int ret; + + i_assert(count <= INT_MAX); + + ret = imap_parser_read_args(cmd->parser, count, flags, args_r); + if (ret >= (int)count) { + /* all parameters read successfully */ + i_assert(cmd->client->input_lock == NULL || + cmd->client->input_lock == cmd); + + client_args_finished(cmd, *args_r); + cmd->client->input_lock = NULL; + return TRUE; + } else if (ret == -2) { + /* need more data */ + if (cmd->client->input->closed) { + /* disconnected */ + cmd->state = CLIENT_COMMAND_STATE_DONE; + } + return FALSE; + } else { + /* error, or missing arguments */ + client_send_command_error(cmd, ret < 0 ? NULL : + "Missing arguments"); + return FALSE; + } +} + +bool client_read_string_args(struct client_command_context *cmd, + unsigned int count, ...) +{ + const struct imap_arg *imap_args; + va_list va; + const char *str; + unsigned int i; + + if (!client_read_args(cmd, count, 0, &imap_args)) + return FALSE; + + va_start(va, count); + for (i = 0; i < count; i++) { + const char **ret = va_arg(va, const char **); + + if (IMAP_ARG_IS_EOL(&imap_args[i])) { + client_send_command_error(cmd, "Missing arguments."); + break; + } + + if (!imap_arg_get_astring(&imap_args[i], &str)) { + client_send_command_error(cmd, "Invalid arguments."); + break; + } + + if (ret != NULL) + *ret = str; + } + va_end(va); + + return i == count; +} + +void client_args_finished(struct client_command_context *cmd, + const struct imap_arg *args) +{ + string_t *str = t_str_new(256); + + if (cmd->args != NULL && cmd->args[0] != '\0') { + str_append(str, cmd->args); + str_append_c(str, ' '); + } + imap_write_args(str, args); + cmd->args = p_strdup(cmd->pool, str_c(str)); + event_add_str(cmd->event, "cmd_args", cmd->args); + + str_truncate(str, 0); + if (cmd->human_args != NULL && cmd->human_args[0] != '\0') { + str_append(str, cmd->human_args); + str_append_c(str, ' '); + } + imap_write_args_for_human(str, args); + cmd->human_args = p_strdup(cmd->pool, str_c(str)); + event_add_str(cmd->event, "cmd_human_args", cmd->human_args); +} + +static struct client_command_context * +client_command_find_with_flags(struct client_command_context *new_cmd, + enum command_flags flags, + enum client_command_state max_state) +{ + struct client_command_context *cmd; + + cmd = new_cmd->client->command_queue; + for (; cmd != NULL; cmd = cmd->next) { + /* The tagline_sent check is a bit kludgy here. Plugins may + hook into sync_notify_more() and send the tagline before + finishing the command. During this stage the state was been + dropped from _WAIT_SYNC to _WAIT_OUTPUT, so the <= max_state + check doesn't work correctly here. (Perhaps we should add + a new _WAIT_SYNC_OUTPUT?) */ + if (cmd->state <= max_state && !cmd->tagline_sent && + cmd != new_cmd && (cmd->cmd_flags & flags) != 0) + return cmd; + } + return NULL; +} + +static bool client_command_is_ambiguous(struct client_command_context *cmd) +{ + enum command_flags flags; + enum client_command_state max_state = + CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY; + bool broken_client = FALSE; + + if ((cmd->cmd_flags & COMMAND_FLAG_REQUIRES_SYNC) != 0 && + !imap_sync_is_allowed(cmd->client)) + return TRUE; + + if (cmd->search_save_result_used) { + /* if there are pending commands that update the search + save result, wait */ + struct client_command_context *old_cmd = cmd->next; + + for (; old_cmd != NULL; old_cmd = old_cmd->next) { + if (old_cmd->search_save_result) + return TRUE; + } + } + + if ((cmd->cmd_flags & COMMAND_FLAG_BREAKS_MAILBOX) == + COMMAND_FLAG_BREAKS_MAILBOX) { + /* there must be no other command running that uses the + selected mailbox */ + flags = COMMAND_FLAG_USES_MAILBOX; + max_state = CLIENT_COMMAND_STATE_DONE; + } else if ((cmd->cmd_flags & COMMAND_FLAG_USES_SEQS) != 0) { + /* no existing command must be breaking sequences */ + flags = COMMAND_FLAG_BREAKS_SEQS; + broken_client = TRUE; + } else if ((cmd->cmd_flags & COMMAND_FLAG_BREAKS_SEQS) != 0) { + /* if existing command uses sequences, we'll have to block */ + flags = COMMAND_FLAG_USES_SEQS; + } else { + return FALSE; + } + + if (client_command_find_with_flags(cmd, flags, max_state) == NULL) { + if (cmd->client->syncing) { + /* don't do anything until syncing is finished */ + return TRUE; + } + if (cmd->client->mailbox_change_lock != NULL && + cmd->client->mailbox_change_lock != cmd) { + /* don't do anything until mailbox is fully + opened/closed */ + return TRUE; + } + return FALSE; + } + + if (broken_client) { + client_send_line(cmd->client, + "* BAD ["IMAP_RESP_CODE_CLIENTBUG"] " + "Command pipelining results in ambiguity."); + } + + return TRUE; +} + +struct client_command_context *client_command_alloc(struct client *client) +{ + struct client_command_context *cmd; + + cmd = p_new(client->command_pool, struct client_command_context, 1); + cmd->client = client; + cmd->pool = client->command_pool; + cmd->global_event = event_create(client->event); + cmd->event = event_create(cmd->global_event); + cmd->stats.start_time = ioloop_timeval; + cmd->stats.last_run_timeval = ioloop_timeval; + cmd->stats.start_ioloop_wait_usecs = + io_loop_get_wait_usecs(current_ioloop); + p_array_init(&cmd->module_contexts, cmd->pool, 5); + + DLLIST_PREPEND(&client->command_queue, cmd); + client->command_queue_size++; + + imap_client_notify_command_allocated(client); + return cmd; +} + +void client_command_init_finished(struct client_command_context *cmd) +{ + event_add_str(cmd->event, "cmd_tag", cmd->tag); + /* use "unknown" until we checked that the command name is known/valid */ + event_add_str(cmd->event, "cmd_name", "unknown"); + /* the actual command name received from client - as-is */ + event_add_str(cmd->event, "cmd_input_name", cmd->name); +} + +static struct client_command_context * +client_command_new(struct client *client) +{ + struct client_command_context *cmd; + + cmd = client_command_alloc(client); + if (client->free_parser != NULL) { + cmd->parser = client->free_parser; + client->free_parser = NULL; + } else { + cmd->parser = + imap_parser_create(client->input, client->output, + client->set->imap_max_line_length); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(cmd->parser); + } + return cmd; +} + +void client_add_missing_io(struct client *client) +{ + if (client->io == NULL && !client->disconnected) + client->io = io_add_istream(client->input, client_input, client); +} + +void client_command_free(struct client_command_context **_cmd) +{ + struct client_command_context *cmd = *_cmd; + struct client *client = cmd->client; + enum client_command_state state = cmd->state; + + *_cmd = NULL; + + i_assert(!cmd->executing); + i_assert(client->output_cmd_lock == NULL); + + /* reset input idle time because command output might have taken a + long time and we don't want to disconnect client immediately then */ + client->last_input = ioloop_time; + timeout_reset(client->to_idle); + + if (cmd->cancel) { + cmd->cancel = FALSE; + client_send_tagline(cmd, "NO Command cancelled."); + } + + i_free(client->last_cmd_name); + client->last_cmd_name = i_strdup(cmd->name); + client->last_cmd_stats = cmd->stats; + + if (!cmd->param_error) + client->bad_counter = 0; + + if (client->input_lock == cmd) + client->input_lock = NULL; + if (client->mailbox_change_lock == cmd) + client->mailbox_change_lock = NULL; + + event_set_name(cmd->event, "imap_command_finished"); + if (cmd->tagline_reply != NULL) { + event_add_str(cmd->event, "tagged_reply_state", + t_strcut(cmd->tagline_reply, ' ')); + event_add_str(cmd->event, "tagged_reply", cmd->tagline_reply); + } + event_add_timeval(cmd->event, "last_run_time", + &cmd->stats.last_run_timeval); + event_add_int(cmd->event, "running_usecs", cmd->stats.running_usecs); + event_add_int(cmd->event, "lock_wait_usecs", cmd->stats.lock_wait_usecs); + event_add_int(cmd->event, "bytes_in", cmd->stats.bytes_in); + event_add_int(cmd->event, "bytes_out", cmd->stats.bytes_out); + + e_debug(cmd->event, "Command finished: %s %s", cmd->name, + cmd->human_args != NULL ? cmd->human_args : ""); + event_unref(&cmd->event); + event_unref(&cmd->global_event); + + if (cmd->parser != NULL) { + if (client->free_parser == NULL) { + imap_parser_reset(cmd->parser); + client->free_parser = cmd->parser; + } else { + imap_parser_unref(&cmd->parser); + } + } + + client->command_queue_size--; + DLLIST_REMOVE(&client->command_queue, cmd); + cmd = NULL; + + if (client->command_queue == NULL) { + /* no commands left in the queue, we can clear the pool */ + p_clear(client->command_pool); + timeout_remove(&client->to_idle_output); + } + imap_client_notify_command_freed(client); + imap_refresh_proctitle(); + + /* if command finished from external event, check input for more + unhandled commands since we may not be executing from client_input + or client_output. */ + if (state == CLIENT_COMMAND_STATE_WAIT_EXTERNAL && + !client->disconnected) { + client_add_missing_io(client); + io_set_pending(client->io); + } +} + +static void client_check_command_hangs(struct client *client) +{ + struct client_command_context *cmd; + unsigned int unfinished_count = 0; + bool have_wait_unfinished = FALSE; + + for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) { + switch (cmd->state) { + case CLIENT_COMMAND_STATE_WAIT_INPUT: + /* We need to be reading input for this command. + However, if there is already an output lock for + another command we'll wait for it to finish first. + This is needed because if there are any literals + we'd need to send "+ OK" responses. */ + i_assert(client->io != NULL || + (client->output_cmd_lock != NULL && + client->output_cmd_lock != client->input_lock)); + unfinished_count++; + break; + case CLIENT_COMMAND_STATE_WAIT_OUTPUT: + i_assert((io_loop_find_fd_conditions(current_ioloop, client->fd_out) & IO_WRITE) != 0); + unfinished_count++; + break; + case CLIENT_COMMAND_STATE_WAIT_EXTERNAL: + unfinished_count++; + break; + case CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY: + have_wait_unfinished = TRUE; + break; + case CLIENT_COMMAND_STATE_WAIT_SYNC: + if ((io_loop_find_fd_conditions(current_ioloop, client->fd_out) & IO_WRITE) == 0) + have_wait_unfinished = TRUE; + else { + /* we have an output callback, which will be + called soon and it'll run cmd_sync_delayed(). + FIXME: is this actually wanted? */ + } + break; + case CLIENT_COMMAND_STATE_DONE: + i_unreached(); + } + } + i_assert(!have_wait_unfinished || unfinished_count > 0); +} + +static bool client_remove_pending_unambiguity(struct client *client) +{ + if (client->input_lock != NULL) { + /* there's a command that has locked the input */ + struct client_command_context *cmd = client->input_lock; + + if (cmd->state != CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY) + return TRUE; + + /* the command is waiting for existing ambiguity causing + commands to finish. */ + if (client_command_is_ambiguous(cmd)) { + /* we could be waiting for existing sync to finish */ + if (!cmd_sync_delayed(client)) + return FALSE; + if (client_command_is_ambiguous(cmd)) + return FALSE; + } + cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT; + } + return TRUE; +} + +void client_continue_pending_input(struct client *client) +{ + i_assert(!client->handling_input); + + if (client->disconnected) { + client_destroy(client, NULL); + return; + } + + /* this function is called at the end of I/O callbacks (and only there). + fix up the command states and verify that they're correct. */ + while (client_remove_pending_unambiguity(client)) { + client_add_missing_io(client); + + /* if there's unread data in buffer, handle it. */ + if (i_stream_get_data_size(client->input) == 0 || + client->disconnected) + break; + + struct ostream *output = client->output; + o_stream_ref(output); + o_stream_cork(output); + bool ret = client_handle_input(client); + o_stream_uncork(output); + o_stream_unref(&output); + if (!ret) + break; + } + if (client->input->closed || client->output->closed) + client_destroy(client, NULL); + else + client_check_command_hangs(client); +} + +/* Skip incoming data until newline is found, + returns TRUE if newline was found. */ +static bool client_skip_line(struct client *client) +{ + const unsigned char *data; + size_t i, data_size; + + data = i_stream_get_data(client->input, &data_size); + + for (i = 0; i < data_size; i++) { + if (data[i] == '\n') { + client->input_skip_line = FALSE; + i++; + break; + } + } + + i_stream_skip(client->input, i); + return !client->input_skip_line; +} + +static void client_idle_output_timeout(struct client *client) +{ + client_destroy(client, t_strdup_printf( + "Client has not read server output for for %"PRIdTIME_T" secs", + ioloop_time - client->last_output)); +} + +bool client_handle_unfinished_cmd(struct client_command_context *cmd) +{ + if (cmd->state == CLIENT_COMMAND_STATE_WAIT_INPUT) { + /* need more input */ + return FALSE; + } + if (cmd->state != CLIENT_COMMAND_STATE_WAIT_OUTPUT) { + /* waiting for something */ + if (cmd->state == CLIENT_COMMAND_STATE_WAIT_SYNC) { + /* this is mainly for APPEND. */ + client_add_missing_io(cmd->client); + } + return TRUE; + } + + /* output is blocking, we can execute more commands */ + o_stream_set_flush_pending(cmd->client->output, TRUE); + if (cmd->client->to_idle_output == NULL) { + /* disconnect sooner if client isn't reading our output */ + cmd->client->to_idle_output = + timeout_add(CLIENT_OUTPUT_TIMEOUT_MSECS, + client_idle_output_timeout, cmd->client); + } + return TRUE; +} + +static void +client_command_failed_early(struct client_command_context **_cmd, + const char *error) +{ + struct client_command_context *cmd = *_cmd; + + /* ignore the rest of this line */ + cmd->client->input_skip_line = TRUE; + + io_loop_time_refresh(); + command_stats_start(cmd); + client_send_command_error(cmd, error); + cmd->param_error = TRUE; + client_command_free(_cmd); +} + +static bool client_command_input(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct command *command; + const char *tag, *name; + int ret; + + if (cmd->func != NULL) { + /* command is being executed - continue it */ + if (command_exec(cmd)) { + /* command execution was finished */ + client_command_free(&cmd); + client_add_missing_io(client); + return TRUE; + } + + return client_handle_unfinished_cmd(cmd); + } + + if (cmd->tag == NULL) { + ret = imap_parser_read_tag(cmd->parser, &tag); + if (ret == 0) + return FALSE; /* need more data */ + if (ret < 0) { + client_command_failed_early(&cmd, "Invalid tag."); + return TRUE; + } + cmd->tag = p_strdup(cmd->pool, tag); + } + + if (cmd->name == NULL) { + ret = imap_parser_read_command_name(cmd->parser, &name); + if (ret == 0) + return FALSE; /* need more data */ + if (ret < 0) { + client_command_failed_early(&cmd, "Invalid command name."); + return TRUE; + } + + /* UID commands are a special case. better to handle them + here. */ + if (!cmd->uid && strcasecmp(name, "UID") == 0) { + cmd->uid = TRUE; + return client_command_input(cmd); + } + cmd->name = !cmd->uid ? p_strdup(cmd->pool, name) : + p_strconcat(cmd->pool, "UID ", name, NULL); + client_command_init_finished(cmd); + imap_refresh_proctitle(); + } + + client->input_skip_line = TRUE; + + if (cmd->name[0] == '\0') { + /* command not given - cmd->func is already NULL. */ + } else if ((command = command_find(cmd->name)) != NULL) { + cmd->func = command->func; + cmd->cmd_flags = command->flags; + /* valid command - overwrite the "unknown" string set earlier */ + event_add_str(cmd->global_event, "cmd_name", command->name); + event_strlist_append(cmd->global_event, "reason_code", + event_reason_code_prefix("imap", "cmd_", command->name)); + event_add_str(cmd->event, "cmd_name", command->name); + if (client_command_is_ambiguous(cmd)) { + /* do nothing until existing commands are finished */ + i_assert(cmd->state == CLIENT_COMMAND_STATE_WAIT_INPUT); + cmd->state = CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY; + io_remove(&client->io); + return FALSE; + } + } + + if (cmd->func == NULL) { + /* unknown command */ + client_command_failed_early(&cmd, "Unknown command."); + return TRUE; + } else { + i_assert(!client->disconnected); + + return client_command_input(cmd); + } +} + +static bool client_handle_next_command(struct client *client, bool *remove_io_r) +{ + *remove_io_r = FALSE; + + if (client->input_lock != NULL) { + if (client->input_lock->state == + CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY || + /* we can't send literal "+ OK" replies if output is + locked by another command. */ + (client->output_cmd_lock != NULL && + client->output_cmd_lock != client->input_lock)) { + *remove_io_r = TRUE; + return FALSE; + } + return client_command_input(client->input_lock); + } + + if (client->input_skip_line) { + /* first eat the previous command line */ + if (!client_skip_line(client)) + return FALSE; + client->input_skip_line = FALSE; + } + + /* don't bother creating a new client command before there's at least + some input */ + if (i_stream_get_data_size(client->input) == 0) + return FALSE; + + /* beginning a new command */ + if (client->command_queue_size >= CLIENT_COMMAND_QUEUE_MAX_SIZE || + client->output_cmd_lock != NULL) { + /* wait for some of the commands to finish */ + *remove_io_r = TRUE; + return FALSE; + } + + client->input_lock = client_command_new(client); + return client_command_input(client->input_lock); +} + +bool client_handle_input(struct client *client) +{ + bool ret, remove_io, handled_commands = FALSE; + + i_assert(o_stream_is_corked(client->output) || + client->output->stream_errno != 0); + i_assert(!client->disconnected); + + client->handling_input = TRUE; + do { + T_BEGIN { + ret = client_handle_next_command(client, &remove_io); + } T_END; + if (ret) + handled_commands = TRUE; + } while (ret && !client->disconnected && client->io != NULL); + client->handling_input = FALSE; + + if (remove_io) + io_remove(&client->io); + else + client_add_missing_io(client); + if (!handled_commands) + return FALSE; + + if (client->input_lock == NULL) { + /* finished handling all commands. sync them all at once now. */ + cmd_sync_delayed(client); + } else if (client->input_lock->state == CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY) { + /* the command may be waiting for previous command to sync. */ + cmd_sync_delayed(client); + } + return TRUE; +} + +void client_input(struct client *client) +{ + struct client_command_context *cmd; + struct ostream *output = client->output; + ssize_t bytes; + + i_assert(client->io != NULL); + + client->last_input = ioloop_time; + timeout_reset(client->to_idle); + + bytes = i_stream_read(client->input); + if (bytes == -1) { + /* disconnected */ + client_destroy(client, NULL); + return; + } + + o_stream_ref(output); + o_stream_cork(output); + if (!client_handle_input(client) && bytes == -2) { + /* parameter word is longer than max. input buffer size. + this is most likely an error, so skip the new data + until newline is found. */ + client->input_skip_line = TRUE; + + cmd = client->input_lock != NULL ? client->input_lock : + client_command_new(client); + cmd->param_error = TRUE; + client_send_command_error(cmd, "Too long argument."); + client_command_free(&cmd); + } + o_stream_uncork(output); + o_stream_unref(&output); + imap_refresh_proctitle(); + + client_continue_pending_input(client); +} + +static void client_output_cmd(struct client_command_context *cmd) +{ + bool finished; + + /* continue processing command */ + finished = command_exec(cmd); + + if (!finished) + (void)client_handle_unfinished_cmd(cmd); + else { + /* command execution was finished */ + client_command_free(&cmd); + } +} + +static void client_output_commands(struct client *client) +{ + struct client_command_context *cmd; + + /* mark all commands non-executed */ + for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) + cmd->temp_executed = FALSE; + + if (client->output_cmd_lock != NULL) { + client->output_cmd_lock->temp_executed = TRUE; + client_output_cmd(client->output_cmd_lock); + } + while (client->output_cmd_lock == NULL) { + /* go through the entire commands list every time in case + multiple commands were freed. temp_executed keeps track of + which messages we've called so far */ + cmd = client->command_queue; + for (; cmd != NULL; cmd = cmd->next) { + if (!cmd->temp_executed && + cmd->state == CLIENT_COMMAND_STATE_WAIT_OUTPUT) { + cmd->temp_executed = TRUE; + client_output_cmd(cmd); + break; + } + } + if (cmd == NULL) { + /* all commands executed */ + break; + } + } +} + +int client_output(struct client *client) +{ + int ret; + + i_assert(!client->destroyed); + + client->last_output = ioloop_time; + timeout_reset(client->to_idle); + if (client->to_idle_output != NULL) + timeout_reset(client->to_idle_output); + + if ((ret = o_stream_flush(client->output)) < 0) { + client_destroy(client, NULL); + return 1; + } + + client_output_commands(client); + (void)cmd_sync_delayed(client); + + imap_refresh_proctitle_delayed(); + if (client->output->closed) + client_destroy(client, NULL); + else { + /* corking is added automatically by ostream-file. we need to + uncork here before client_check_command_hangs() is called, + because otherwise it can assert-crash due to ioloop not + having IO_WRITE callback set for the ostream. */ + o_stream_uncork(client->output); + client_continue_pending_input(client); + } + return ret; +} + +bool client_handle_search_save_ambiguity(struct client_command_context *cmd) +{ + struct client_command_context *old_cmd = cmd->next; + + /* search only commands that were added before this command + (commands are prepended to the queue, so they're after ourself) */ + for (; old_cmd != NULL; old_cmd = old_cmd->next) { + if (old_cmd->search_save_result) + break; + } + if (old_cmd == NULL) + return FALSE; + + /* ambiguity, wait until it's over */ + i_assert(cmd->state == CLIENT_COMMAND_STATE_WAIT_INPUT); + cmd->client->input_lock = cmd; + cmd->state = CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY; + cmd->search_save_result_used = TRUE; + io_remove(&cmd->client->io); + return TRUE; +} + +void client_enable(struct client *client, unsigned int feature_idx) +{ + if (client_has_enabled(client, feature_idx)) + return; + + const struct imap_feature *feat = imap_feature_idx(feature_idx); + feat->callback(client); + /* set after the callback, so the callback can see what features were + previously set */ + bool value = TRUE; + array_idx_set(&client->enabled_features, feature_idx, &value); +} + +bool client_has_enabled(struct client *client, unsigned int feature_idx) +{ + if (feature_idx >= array_count(&client->enabled_features)) + return FALSE; + const bool *featurep = + array_idx(&client->enabled_features, feature_idx); + return *featurep; +} + +static void imap_client_enable_condstore(struct client *client) +{ + struct mailbox_status status; + int ret; + + if (client->mailbox == NULL) + return; + + if ((client_enabled_mailbox_features(client) & MAILBOX_FEATURE_CONDSTORE) != 0) + return; + + ret = mailbox_enable(client->mailbox, MAILBOX_FEATURE_CONDSTORE); + if (ret == 0) { + /* CONDSTORE being enabled while mailbox is selected. + Notify client of the latest HIGHESTMODSEQ. */ + ret = mailbox_get_status(client->mailbox, + STATUS_HIGHESTMODSEQ, &status); + if (ret == 0) { + client_send_line(client, t_strdup_printf( + "* OK [HIGHESTMODSEQ %"PRIu64"] Highest", + status.highest_modseq)); + } + } + if (ret < 0) { + client_send_untagged_storage_error(client, + mailbox_get_storage(client->mailbox)); + } +} + +static void imap_client_enable_qresync(struct client *client) +{ + /* enable also CONDSTORE */ + client_enable(client, imap_feature_condstore); +} + +enum mailbox_feature client_enabled_mailbox_features(struct client *client) +{ + enum mailbox_feature mailbox_features = 0; + const struct imap_feature *feature; + const bool *client_enabled; + unsigned int count; + + client_enabled = array_get(&client->enabled_features, &count); + for (unsigned int idx = 0; idx < count; idx++) { + if (client_enabled[idx]) { + feature = imap_feature_idx(idx); + mailbox_features |= feature->mailbox_features; + } + } + return mailbox_features; +} + +const char *const *client_enabled_features(struct client *client) +{ + ARRAY_TYPE(const_string) feature_strings; + const struct imap_feature *feature; + const bool *client_enabled; + unsigned int count; + + t_array_init(&feature_strings, 8); + client_enabled = array_get(&client->enabled_features, &count); + for (unsigned int idx = 0; idx < count; idx++) { + if (client_enabled[idx]) { + feature = imap_feature_idx(idx); + array_push_back(&feature_strings, &feature->feature); + } + } + array_append_zero(&feature_strings); + return array_front(&feature_strings); +} + +struct imap_search_update * +client_search_update_lookup(struct client *client, const char *tag, + unsigned int *idx_r) +{ + struct imap_search_update *updates; + unsigned int i, count; + + if (!array_is_created(&client->search_updates)) + return NULL; + + updates = array_get_modifiable(&client->search_updates, &count); + for (i = 0; i < count; i++) { + if (strcmp(updates[i].tag, tag) == 0) { + *idx_r = i; + return &updates[i]; + } + } + return NULL; +} + +void client_search_updates_free(struct client *client) +{ + struct imap_search_update *update; + + if (!array_is_created(&client->search_updates)) + return; + + array_foreach_modifiable(&client->search_updates, update) + imap_search_update_free(update); + array_clear(&client->search_updates); +} + +void clients_init(void) +{ + imap_feature_condstore = + imap_feature_register("CONDSTORE", MAILBOX_FEATURE_CONDSTORE, + imap_client_enable_condstore); + imap_feature_qresync = + imap_feature_register("QRESYNC", MAILBOX_FEATURE_CONDSTORE, + imap_client_enable_qresync); +} + +void clients_destroy_all(void) +{ + while (imap_clients != NULL) { + mail_storage_service_io_activate_user(imap_clients->service_user); + client_send_line(imap_clients, "* BYE Server shutting down."); + client_destroy(imap_clients, "Server shutting down."); + } +} + +struct imap_client_vfuncs imap_client_vfuncs = { + .init = client_default_init, + .destroy = client_default_destroy, + + .send_tagline = client_default_send_tagline, + .sync_notify_more = client_default_sync_notify_more, + + .state_export = imap_state_export_base, + .state_import = imap_state_import_base, +}; |