summaryrefslogtreecommitdiffstats
path: root/src/lib-imap-client/imapc-connection.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib-imap-client/imapc-connection.c')
-rw-r--r--src/lib-imap-client/imapc-connection.c2557
1 files changed, 2557 insertions, 0 deletions
diff --git a/src/lib-imap-client/imapc-connection.c b/src/lib-imap-client/imapc-connection.c
new file mode 100644
index 0000000..f025403
--- /dev/null
+++ b/src/lib-imap-client/imapc-connection.c
@@ -0,0 +1,2557 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "base64.h"
+#include "write-full.h"
+#include "str.h"
+#include "time-util.h"
+#include "dns-lookup.h"
+#include "dsasl-client.h"
+#include "iostream-rawlog.h"
+#include "iostream-ssl.h"
+#include "imap-quote.h"
+#include "imap-util.h"
+#include "imap-parser.h"
+#include "imapc-client-private.h"
+#include "imapc-connection.h"
+
+#include <unistd.h>
+#include <ctype.h>
+
+#define IMAPC_COMMAND_STATE_AUTHENTICATE_CONTINUE 10000
+#define IMAPC_MAX_INLINE_LITERAL_SIZE (1024*32)
+/* If LOGOUT reply takes longer than this, disconnect. */
+#define IMAPC_LOGOUT_TIMEOUT_MSECS 5000
+
+enum imapc_input_state {
+ IMAPC_INPUT_STATE_NONE = 0,
+ IMAPC_INPUT_STATE_PLUS,
+ IMAPC_INPUT_STATE_UNTAGGED,
+ IMAPC_INPUT_STATE_UNTAGGED_NUM,
+ IMAPC_INPUT_STATE_TAGGED
+};
+
+struct imapc_command_stream {
+ unsigned int pos;
+ uoff_t size;
+ struct istream *input;
+};
+
+struct imapc_command {
+ pool_t pool;
+ buffer_t *data;
+ unsigned int send_pos;
+ unsigned int tag;
+
+ enum imapc_command_flags flags;
+ struct imapc_connection *conn;
+ /* If non-NULL, points to the mailbox where this command should be
+ executed */
+ struct imapc_client_mailbox *box;
+
+ ARRAY(struct imapc_command_stream) streams;
+
+ imapc_command_callback_t *callback;
+ void *context;
+
+ /* This is the AUTHENTICATE command */
+ bool authenticate:1;
+ /* This is the IDLE command */
+ bool idle:1;
+ /* Waiting for '+' literal reply before we can continue */
+ bool wait_for_literal:1;
+ /* Command is fully sent to server */
+ bool sent:1;
+};
+ARRAY_DEFINE_TYPE(imapc_command, struct imapc_command *);
+
+struct imapc_connection_literal {
+ char *temp_path;
+ int fd;
+ uoff_t bytes_left;
+
+ const struct imap_arg *parent_arg;
+ unsigned int list_idx;
+};
+
+struct imapc_connection {
+ struct imapc_client *client;
+ char *name;
+ int refcount;
+
+ int fd;
+ struct io *io;
+ struct istream *input, *raw_input;
+ struct ostream *output, *raw_output;
+ struct imap_parser *parser;
+ struct timeout *to;
+ struct timeout *to_output;
+ struct dns_lookup *dns_lookup;
+ struct dsasl_client *sasl_client;
+
+ struct ssl_iostream *ssl_iostream;
+
+ int (*input_callback)(struct imapc_connection *conn);
+ enum imapc_input_state input_state;
+ unsigned int cur_tag;
+ uint32_t cur_num;
+ struct timeval last_connect;
+ unsigned int reconnect_count;
+
+ /* If QRESYNC isn't used, this is set immediately after issuing
+ SELECT/EXAMINE. We could differentiate better whether a mailbox is
+ "being selected" vs "fully selected", but that code is already in
+ the imapc-storage side so it would have to be moved or duplicated
+ here. And since nothing actually cares about this distinction (yet),
+ don't bother with it for now. This is set to NULL when the mailbox
+ is closed from imapc-storage point of view, even if the server is
+ still in selected state (see selected_on_server). */
+ struct imapc_client_mailbox *selected_box;
+ /* If QRESYNC is used, this is set when SELECT/EXAMINE is issued.
+ If the server is already in selected state, the selected_box is most
+ likely already NULL at this point, because imapc-storage has closed
+ it. */
+ struct imapc_client_mailbox *qresync_selecting_box;
+ enum imapc_connection_state state;
+ char *disconnect_reason;
+
+ enum imapc_capability capabilities;
+ char **capabilities_list;
+
+ imapc_command_callback_t *login_callback;
+ void *login_context;
+
+ /* commands pending in queue to be sent */
+ ARRAY_TYPE(imapc_command) cmd_send_queue;
+ /* commands that have been sent, waiting for their tagged reply */
+ ARRAY_TYPE(imapc_command) cmd_wait_list;
+ /* commands that were already sent, but were aborted since (due to
+ unselecting mailbox). */
+ ARRAY_TYPE(seq_range) aborted_cmd_tags;
+ unsigned int reconnect_command_count;
+
+ unsigned int ips_count, prev_connect_idx;
+ struct ip_addr *ips;
+
+ struct imapc_connection_literal literal;
+ ARRAY(struct imapc_arg_file) literal_files;
+
+ unsigned int throttle_msecs;
+ unsigned int throttle_shrink_msecs;
+ unsigned int last_successful_throttle_msecs;
+ bool throttle_pending;
+ struct timeval throttle_end_timeval;
+ struct timeout *to_throttle, *to_throttle_shrink;
+
+ bool reconnecting:1;
+ bool reconnect_waiting:1;
+ bool reconnect_ok:1;
+ bool idling:1;
+ bool idle_stopping:1;
+ bool idle_plus_waiting:1;
+ bool select_waiting_reply:1;
+ /* TRUE if IMAP server is in SELECTED state. select_box may be NULL
+ though, if we already closed the mailbox from client point of
+ view. */
+ bool selected_on_server:1;
+};
+
+static void imapc_connection_capability_cb(const struct imapc_command_reply *reply,
+ void *context);
+static int imapc_connection_output(struct imapc_connection *conn);
+static int imapc_connection_ssl_init(struct imapc_connection *conn);
+static void imapc_command_free(struct imapc_command *cmd);
+static void imapc_command_send_more(struct imapc_connection *conn);
+static void
+imapc_login_callback(struct imapc_connection *conn,
+ const struct imapc_command_reply *reply);
+
+static void
+imapc_auth_ok(struct imapc_connection *conn)
+{
+ if (conn->client->set.debug)
+ i_debug("imapc(%s): Authenticated successfully", conn->name);
+
+ if (conn->client->state_change_callback == NULL)
+ return;
+
+ conn->client->state_change_callback(conn->client->state_change_context,
+ IMAPC_STATE_CHANGE_AUTH_OK, NULL);
+}
+
+static void
+imapc_auth_failed(struct imapc_connection *conn, const struct imapc_command_reply *_reply,
+ const char *error)
+{
+ struct imapc_command_reply reply = *_reply;
+
+ reply.text_without_resp = reply.text_full =
+ t_strdup_printf("Authentication failed: %s", error);
+ if (reply.state != IMAPC_COMMAND_STATE_DISCONNECTED) {
+ reply.state = IMAPC_COMMAND_STATE_AUTH_FAILED;
+ i_error("imapc(%s): %s", conn->name, reply.text_full);
+ }
+ imapc_login_callback(conn, &reply);
+
+ if (conn->client->state_change_callback == NULL)
+ return;
+
+ conn->client->state_change_callback(conn->client->state_change_context,
+ IMAPC_STATE_CHANGE_AUTH_FAILED,
+ error);
+}
+
+struct imapc_connection *
+imapc_connection_init(struct imapc_client *client,
+ imapc_command_callback_t *login_callback,
+ void *login_context)
+{
+ struct imapc_connection *conn;
+
+ conn = i_new(struct imapc_connection, 1);
+ conn->refcount = 1;
+ conn->client = client;
+ conn->login_callback = login_callback;
+ conn->login_context = login_context;
+ conn->fd = -1;
+ conn->name = i_strdup_printf("%s:%u", client->set.host,
+ client->set.port);
+ conn->literal.fd = -1;
+ conn->reconnect_ok = (client->set.connect_retry_count>0);
+ i_array_init(&conn->cmd_send_queue, 8);
+ i_array_init(&conn->cmd_wait_list, 32);
+ i_array_init(&conn->literal_files, 4);
+ i_array_init(&conn->aborted_cmd_tags, 8);
+
+ if (client->set.debug)
+ i_debug("imapc(%s): Created new connection", conn->name);
+
+ imapc_client_ref(client);
+ return conn;
+}
+
+static void imapc_connection_ref(struct imapc_connection *conn)
+{
+ i_assert(conn->refcount > 0);
+
+ conn->refcount++;
+}
+
+static void imapc_connection_unref(struct imapc_connection **_conn)
+{
+ struct imapc_connection *conn = *_conn;
+
+ i_assert(conn->refcount > 0);
+
+ *_conn = NULL;
+ if (--conn->refcount > 0)
+ return;
+
+ i_assert(conn->disconnect_reason == NULL);
+
+ if (conn->capabilities_list != NULL)
+ p_strsplit_free(default_pool, conn->capabilities_list);
+ array_free(&conn->cmd_send_queue);
+ array_free(&conn->cmd_wait_list);
+ array_free(&conn->literal_files);
+ array_free(&conn->aborted_cmd_tags);
+ imapc_client_unref(&conn->client);
+ i_free(conn->ips);
+ i_free(conn->name);
+ i_free(conn);
+}
+
+void imapc_connection_deinit(struct imapc_connection **_conn)
+{
+ imapc_connection_disconnect(*_conn);
+ imapc_connection_unref(_conn);
+}
+
+void imapc_connection_ioloop_changed(struct imapc_connection *conn)
+{
+ if (conn->io != NULL)
+ conn->io = io_loop_move_io(&conn->io);
+ if (conn->to != NULL)
+ conn->to = io_loop_move_timeout(&conn->to);
+ if (conn->to_throttle != NULL)
+ conn->to_throttle = io_loop_move_timeout(&conn->to_throttle);
+ if (conn->to_throttle_shrink != NULL)
+ conn->to_throttle_shrink = io_loop_move_timeout(&conn->to_throttle_shrink);
+ if (conn->output != NULL)
+ o_stream_switch_ioloop(conn->output);
+ if (conn->dns_lookup != NULL)
+ dns_lookup_switch_ioloop(conn->dns_lookup);
+
+ if (conn->client->ioloop == NULL && conn->to_output != NULL) {
+ /* we're only once moving the to_output to the main ioloop,
+ since timeout moves currently also reset the timeout.
+ (the rest of the times this is a no-op) */
+ conn->to_output = io_loop_move_timeout(&conn->to_output);
+ }
+}
+
+static const char *imapc_command_get_readable(struct imapc_command *cmd)
+{
+ string_t *str = t_str_new(256);
+ const unsigned char *data = cmd->data->data;
+ unsigned int i;
+
+ for (i = 0; i < cmd->data->used; i++) {
+ if (data[i] != '\r' && data[i] != '\n')
+ str_append_c(str, data[i]);
+ }
+ return str_c(str);
+}
+
+static void
+imapc_connection_abort_commands_array(ARRAY_TYPE(imapc_command) *cmd_array,
+ ARRAY_TYPE(imapc_command) *dest_array,
+ struct imapc_client_mailbox *only_box,
+ bool keep_retriable)
+{
+ struct imapc_command *cmd;
+ unsigned int i;
+
+ for (i = 0; i < array_count(cmd_array); ) {
+ cmd = array_idx_elem(cmd_array, i);
+
+ if (cmd->box != only_box && only_box != NULL)
+ i++;
+ else if (keep_retriable &&
+ (cmd->flags & IMAPC_COMMAND_FLAG_RETRIABLE) != 0) {
+ cmd->send_pos = 0;
+ cmd->wait_for_literal = 0;
+ cmd->flags |= IMAPC_COMMAND_FLAG_RECONNECTED;
+ i++;
+ } else {
+ array_delete(cmd_array, i, 1);
+ array_push_back(dest_array, &cmd);
+ }
+ }
+}
+
+void imapc_connection_abort_commands(struct imapc_connection *conn,
+ struct imapc_client_mailbox *only_box,
+ bool keep_retriable)
+{
+ struct imapc_command *cmd;
+ ARRAY_TYPE(imapc_command) tmp_array;
+ struct imapc_command_reply reply;
+
+ t_array_init(&tmp_array, 8);
+ imapc_connection_abort_commands_array(&conn->cmd_wait_list, &tmp_array,
+ only_box, keep_retriable);
+ imapc_connection_abort_commands_array(&conn->cmd_send_queue, &tmp_array,
+ only_box, keep_retriable);
+
+ if (array_count(&conn->cmd_wait_list) > 0 && only_box == NULL) {
+ /* need to move all the waiting commands to send queue */
+ array_append_array(&conn->cmd_wait_list,
+ &conn->cmd_send_queue);
+ array_clear(&conn->cmd_send_queue);
+ array_append_array(&conn->cmd_send_queue,
+ &conn->cmd_wait_list);
+ array_clear(&conn->cmd_wait_list);
+ }
+
+ /* abort the commands. we'll do it here later so that if the
+ callback recurses us back here we don't crash */
+ i_zero(&reply);
+ reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
+ if (only_box != NULL) {
+ reply.text_without_resp = reply.text_full =
+ "Unselecting mailbox";
+ } else {
+ reply.text_without_resp = reply.text_full =
+ "Disconnected from server";
+ }
+ array_foreach_elem(&tmp_array, cmd) {
+ if (cmd->sent && conn->state == IMAPC_CONNECTION_STATE_DONE) {
+ /* We're not disconnected, so the reply will still
+ come. Remember that it needs to be ignored. */
+ seq_range_array_add(&conn->aborted_cmd_tags, cmd->tag);
+ }
+ cmd->callback(&reply, cmd->context);
+ imapc_command_free(cmd);
+ }
+ if (array_count(&conn->cmd_wait_list) == 0)
+ timeout_remove(&conn->to);
+}
+
+static void
+imapc_login_callback(struct imapc_connection *conn,
+ const struct imapc_command_reply *reply)
+{
+ if (conn->login_callback != NULL)
+ conn->login_callback(reply, conn->login_context);
+}
+
+static void imapc_connection_set_state(struct imapc_connection *conn,
+ enum imapc_connection_state state)
+{
+ struct imapc_command_reply reply;
+
+ conn->state = state;
+
+ switch (state) {
+ case IMAPC_CONNECTION_STATE_DISCONNECTED:
+ i_zero(&reply);
+ reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
+ reply.text_full = "Disconnected from server";
+ if (conn->disconnect_reason != NULL) {
+ reply.text_full = t_strdup_printf("%s: %s",
+ reply.text_full, conn->disconnect_reason);
+ i_free_and_null(conn->disconnect_reason);
+ }
+ reply.text_without_resp = reply.text_full;
+ if (!conn->reconnecting) {
+ imapc_login_callback(conn, &reply);
+ i_free(conn->ips);
+ conn->ips_count = 0;
+ }
+ array_clear(&conn->aborted_cmd_tags);
+ conn->idling = FALSE;
+ conn->idle_plus_waiting = FALSE;
+ conn->idle_stopping = FALSE;
+
+ conn->select_waiting_reply = FALSE;
+ conn->qresync_selecting_box = NULL;
+ conn->selected_box = NULL;
+ conn->selected_on_server = FALSE;
+ /* fall through */
+ case IMAPC_CONNECTION_STATE_DONE:
+ /* if we came from imapc_client_get_capabilities(), stop so
+ it can finish up and not just hang indefinitely. */
+ if (conn->client->stop_on_state_finish && !conn->reconnecting)
+ imapc_client_stop(conn->client);
+ break;
+ default:
+ break;
+ }
+}
+
+static void imapc_connection_lfiles_free(struct imapc_connection *conn)
+{
+ struct imapc_arg_file *lfile;
+
+ array_foreach_modifiable(&conn->literal_files, lfile) {
+ if (close(lfile->fd) < 0)
+ i_error("imapc: close(literal file) failed: %m");
+ }
+ array_clear(&conn->literal_files);
+}
+
+static void
+imapc_connection_literal_reset(struct imapc_connection_literal *literal)
+{
+ i_close_fd_path(&literal->fd, literal->temp_path);
+ i_free_and_null(literal->temp_path);
+
+ i_zero(literal);
+ literal->fd = -1;
+}
+
+void imapc_connection_disconnect_full(struct imapc_connection *conn,
+ bool reconnecting)
+{
+ /* timeout may be set also in disconnected state */
+ timeout_remove(&conn->to);
+ conn->reconnecting = reconnecting;
+
+ if (conn->state == IMAPC_CONNECTION_STATE_DISCONNECTED) {
+ i_assert(array_count(&conn->cmd_wait_list) == 0);
+ if (conn->reconnect_command_count == 0)
+ imapc_connection_abort_commands(conn, NULL,
+ reconnecting);
+ return;
+ }
+
+ if (conn->client->set.debug)
+ i_debug("imapc(%s): Disconnected", conn->name);
+
+ if (conn->dns_lookup != NULL)
+ dns_lookup_abort(&conn->dns_lookup);
+ imapc_connection_lfiles_free(conn);
+ imapc_connection_literal_reset(&conn->literal);
+ timeout_remove(&conn->to_output);
+ timeout_remove(&conn->to_throttle);
+ timeout_remove(&conn->to_throttle_shrink);
+ if (conn->parser != NULL)
+ imap_parser_unref(&conn->parser);
+ io_remove(&conn->io);
+ ssl_iostream_destroy(&conn->ssl_iostream);
+ if (conn->fd != -1) {
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ net_disconnect(conn->fd);
+ conn->fd = -1;
+ }
+
+ /* get capabilities again after reconnection. this is especially
+ important because post-login capabilities often do not contain AUTH=
+ capabilities. */
+ conn->capabilities = 0;
+ if (conn->capabilities_list != NULL) {
+ p_strsplit_free(default_pool, conn->capabilities_list);
+ conn->capabilities_list = NULL;
+ }
+
+ imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_DISCONNECTED);
+ imapc_connection_abort_commands(conn, NULL, reconnecting);
+
+ if (!reconnecting) {
+ imapc_client_try_stop(conn->client);
+ }
+}
+
+void imapc_connection_set_no_reconnect(struct imapc_connection *conn)
+{
+ conn->reconnect_ok = FALSE;
+}
+
+void imapc_connection_disconnect(struct imapc_connection *conn)
+{
+ imapc_connection_disconnect_full(conn, FALSE);
+}
+
+static void imapc_connection_set_disconnected(struct imapc_connection *conn)
+{
+ imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_DISCONNECTED);
+ imapc_connection_abort_commands(conn, NULL, FALSE);
+}
+
+static bool imapc_connection_can_reconnect(struct imapc_connection *conn)
+{
+ if (conn->client->logging_out)
+ return FALSE;
+ if (conn->client->set.connect_retry_count == 0 ||
+ (conn->client->set.connect_retry_count < UINT_MAX &&
+ conn->reconnect_count >= conn->client->set.connect_retry_count))
+ return FALSE;
+
+ if (conn->selected_box != NULL)
+ return imapc_client_mailbox_can_reconnect(conn->selected_box);
+ else {
+ return conn->reconnect_command_count == 0 &&
+ conn->reconnect_ok;
+ }
+}
+
+static void imapc_connection_reconnect(struct imapc_connection *conn)
+{
+ conn->reconnect_ok = FALSE;
+ conn->reconnect_waiting = FALSE;
+
+ if (conn->selected_box != NULL) {
+ i_assert(!conn->selected_box->reconnecting);
+ conn->selected_box->reconnecting = TRUE;
+ /* if we fail again, avoid reconnecting immediately. if the
+ server is broken we could just get into an infinitely
+ failing reconnection loop. */
+ conn->selected_box->reconnect_ok = FALSE;
+ }
+ imapc_connection_disconnect_full(conn, TRUE);
+ imapc_connection_connect(conn);
+}
+
+void imapc_connection_try_reconnect(struct imapc_connection *conn,
+ const char *errstr,
+ unsigned int delay_msecs,
+ bool connect_error)
+{
+ /* Try the next IP address only for connect() problems. */
+ if (conn->prev_connect_idx + 1 < conn->ips_count && connect_error) {
+ i_warning("imapc(%s): %s - trying the next IP", conn->name, errstr);
+ conn->reconnect_ok = TRUE;
+ imapc_connection_disconnect_full(conn, TRUE);
+ imapc_connection_connect(conn);
+ return;
+ }
+
+ if (!imapc_connection_can_reconnect(conn)) {
+ i_error("imapc(%s): %s - disconnecting", conn->name, errstr);
+ imapc_connection_disconnect(conn);
+ } else {
+ conn->reconnecting = TRUE;
+ i_warning("imapc(%s): %s - reconnecting (delay %u ms)", conn->name, errstr, delay_msecs);
+ if (delay_msecs == 0)
+ imapc_connection_reconnect(conn);
+ else {
+ imapc_connection_disconnect_full(conn, TRUE);
+ conn->to = timeout_add(delay_msecs, imapc_connection_reconnect, conn);
+ conn->reconnect_count++;
+ conn->reconnect_waiting = TRUE;
+ }
+ }
+}
+
+static void ATTR_FORMAT(2, 3)
+imapc_connection_input_error(struct imapc_connection *conn,
+ const char *fmt, ...)
+{
+ va_list va;
+
+ va_start(va, fmt);
+ i_error("imapc(%s): Server sent invalid input: %s",
+ conn->name, t_strdup_vprintf(fmt, va));
+ imapc_connection_disconnect(conn);
+ va_end(va);
+}
+
+static bool last_arg_is_fetch_body(const struct imap_arg *args,
+ const struct imap_arg **parent_arg_r,
+ unsigned int *idx_r)
+{
+ const struct imap_arg *list;
+ const char *name;
+ unsigned int count;
+
+ if (args[0].type == IMAP_ARG_ATOM &&
+ imap_arg_atom_equals(&args[1], "FETCH") &&
+ imap_arg_get_list_full(&args[2], &list, &count) && count >= 2 &&
+ list[count].type == IMAP_ARG_LITERAL_SIZE &&
+ imap_arg_get_atom(&list[count-1], &name) &&
+ strncasecmp(name, "BODY[", 5) == 0) {
+ *parent_arg_r = &args[2];
+ *idx_r = count;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int
+imapc_connection_read_literal_init(struct imapc_connection *conn, uoff_t size,
+ const struct imap_arg *args)
+{
+ const char *path;
+ const struct imap_arg *parent_arg;
+ unsigned int idx;
+
+ i_assert(conn->literal.fd == -1);
+
+ if (size <= IMAPC_MAX_INLINE_LITERAL_SIZE ||
+ !last_arg_is_fetch_body(args, &parent_arg, &idx)) {
+ /* read the literal directly into parser */
+ return 0;
+ }
+
+ conn->literal.fd = imapc_client_create_temp_fd(conn->client, &path);
+ if (conn->literal.fd == -1)
+ return -1;
+ conn->literal.temp_path = i_strdup(path);
+ conn->literal.bytes_left = size;
+ conn->literal.parent_arg = parent_arg;
+ conn->literal.list_idx = idx;
+ return 1;
+}
+
+static int imapc_connection_read_literal(struct imapc_connection *conn)
+{
+ struct imapc_arg_file *lfile;
+ const unsigned char *data;
+ size_t size;
+
+ if (conn->literal.bytes_left == 0)
+ return 1;
+
+ data = i_stream_get_data(conn->input, &size);
+ if (size > conn->literal.bytes_left)
+ size = conn->literal.bytes_left;
+ if (size > 0) {
+ if (write_full(conn->literal.fd, data, size) < 0) {
+ i_error("imapc(%s): write(%s) failed: %m",
+ conn->name, conn->literal.temp_path);
+ imapc_connection_disconnect(conn);
+ return -1;
+ }
+ i_stream_skip(conn->input, size);
+ conn->literal.bytes_left -= size;
+ }
+ if (conn->literal.bytes_left > 0)
+ return 0;
+
+ /* finished */
+ lfile = array_append_space(&conn->literal_files);
+ lfile->fd = conn->literal.fd;
+ lfile->parent_arg = conn->literal.parent_arg;
+ lfile->list_idx = conn->literal.list_idx;
+
+ conn->literal.fd = -1;
+ imapc_connection_literal_reset(&conn->literal);
+ return 1;
+}
+
+static int
+imapc_connection_read_line_more(struct imapc_connection *conn,
+ const struct imap_arg **imap_args_r)
+{
+ uoff_t literal_size;
+ int ret;
+
+ if ((ret = imapc_connection_read_literal(conn)) <= 0)
+ return ret;
+
+ ret = imap_parser_read_args(conn->parser, 0,
+ IMAP_PARSE_FLAG_LITERAL_SIZE |
+ IMAP_PARSE_FLAG_ATOM_ALLCHARS |
+ IMAP_PARSE_FLAG_LITERAL8 |
+ IMAP_PARSE_FLAG_SERVER_TEXT, imap_args_r);
+ if (ret == -2) {
+ /* need more data */
+ return 0;
+ }
+ if (ret < 0) {
+ enum imap_parser_error parser_error;
+ const char *err_msg = imap_parser_get_error(conn->parser, &parser_error);
+ if (parser_error != IMAP_PARSE_ERROR_BAD_SYNTAX)
+ imapc_connection_input_error(conn, "Error parsing input: %s", err_msg);
+ else
+ i_error("Error parsing input: %s", err_msg);
+ return -1;
+ }
+
+ if (imap_parser_get_literal_size(conn->parser, &literal_size)) {
+ if (imapc_connection_read_literal_init(conn, literal_size,
+ *imap_args_r) <= 0) {
+ imap_parser_read_last_literal(conn->parser);
+ return 2;
+ }
+ return imapc_connection_read_line_more(conn, imap_args_r);
+ }
+ return 1;
+}
+
+static int
+imapc_connection_read_line(struct imapc_connection *conn,
+ const struct imap_arg **imap_args_r)
+{
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ while ((ret = imapc_connection_read_line_more(conn, imap_args_r)) == 2)
+ ;
+
+ if (ret > 0) {
+ data = i_stream_get_data(conn->input, &size);
+ if (size >= 2 && data[0] == '\r' && data[1] == '\n')
+ i_stream_skip(conn->input, 2);
+ else if (size >= 1 && data[0] == '\n')
+ i_stream_skip(conn->input, 1);
+ else
+ i_panic("imapc: Missing LF from input line");
+ } else if (ret < 0) {
+ data = i_stream_get_data(conn->input, &size);
+ unsigned char *lf = memchr(data, '\n', size);
+ if (lf != NULL)
+ i_stream_skip(conn->input, (lf - data) + 1);
+ }
+ return ret;
+}
+
+static int
+imapc_connection_parse_capability(struct imapc_connection *conn,
+ const char *value)
+{
+ const char *const *tmp;
+ unsigned int i;
+
+ if (conn->client->set.debug) {
+ i_debug("imapc(%s): Server capabilities: %s",
+ conn->name, value);
+ }
+
+ conn->capabilities = 0;
+ if (conn->capabilities_list != NULL)
+ p_strsplit_free(default_pool, conn->capabilities_list);
+ conn->capabilities_list = p_strsplit(default_pool, value, " ");
+
+ for (tmp = t_strsplit(value, " "); *tmp != NULL; tmp++) {
+ for (i = 0; imapc_capability_names[i].name != NULL; i++) {
+ const struct imapc_capability_name *cap =
+ &imapc_capability_names[i];
+
+ if (strcasecmp(*tmp, cap->name) == 0) {
+ conn->capabilities |= cap->capability;
+ break;
+ }
+ }
+ }
+
+ if ((conn->capabilities & IMAPC_CAPABILITY_IMAP4REV1) == 0) {
+ imapc_connection_input_error(conn,
+ "CAPABILITY list is missing IMAP4REV1");
+ return -1;
+ }
+ return 0;
+}
+
+static int
+imapc_connection_handle_resp_text_code(struct imapc_connection *conn,
+ const char *key, const char *value)
+{
+ if (strcasecmp(key, "CAPABILITY") == 0) {
+ if (imapc_connection_parse_capability(conn, value) < 0)
+ return -1;
+ }
+ if (strcasecmp(key, "CLOSED") == 0) {
+ /* QRESYNC: SELECTing another mailbox */
+ if (conn->qresync_selecting_box != NULL) {
+ conn->selected_box = conn->qresync_selecting_box;
+ conn->qresync_selecting_box = NULL;
+ } else {
+ conn->selected_on_server = FALSE;
+ }
+ }
+ return 0;
+}
+
+static int
+imapc_connection_handle_resp_text(struct imapc_connection *conn,
+ const char *text,
+ const char **key_r, const char **value_r)
+{
+ const char *p, *value;
+
+ i_assert(text[0] == '[');
+
+ p = strchr(text, ']');
+ if (p == NULL) {
+ imapc_connection_input_error(conn, "Missing ']' in resp-text");
+ return -1;
+ }
+ text = t_strdup_until(text + 1, p);
+ value = strchr(text, ' ');
+ if (value != NULL) {
+ *key_r = t_strdup_until(text, value);
+ *value_r = value + 1;
+ } else {
+ *key_r = text;
+ *value_r = "";
+ }
+ return imapc_connection_handle_resp_text_code(conn, *key_r, *value_r);
+}
+
+static int
+imapc_connection_handle_imap_resp_text(struct imapc_connection *conn,
+ const struct imap_arg *args,
+ const char **key_r, const char **value_r)
+{
+ const char *text;
+
+ if (args->type != IMAP_ARG_ATOM)
+ return 0;
+
+ text = imap_args_to_str(args);
+ if (*text != '[') {
+ if (*text == '\0') {
+ imapc_connection_input_error(conn,
+ "Missing text in resp-text");
+ return -1;
+ }
+ return 0;
+ }
+ return imapc_connection_handle_resp_text(conn, text, key_r, value_r);
+}
+
+static bool need_literal(const char *str)
+{
+ unsigned int i;
+
+ for (i = 0; str[i] != '\0'; i++) {
+ unsigned char c = str[i];
+
+ if ((c & 0x80) != 0 || c == '\r' || c == '\n')
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void imapc_connection_input_reset(struct imapc_connection *conn)
+{
+ conn->input_state = IMAPC_INPUT_STATE_NONE;
+ conn->cur_tag = 0;
+ conn->cur_num = 0;
+ if (conn->parser != NULL)
+ imap_parser_reset(conn->parser);
+ imapc_connection_lfiles_free(conn);
+}
+
+static void
+imapc_connection_auth_finish(struct imapc_connection *conn,
+ const struct imapc_command_reply *reply)
+{
+ if (reply->state != IMAPC_COMMAND_STATE_OK) {
+ imapc_auth_failed(conn, reply, reply->text_full);
+ imapc_connection_disconnect(conn);
+ return;
+ }
+
+ imapc_auth_ok(conn);
+
+ i_assert(array_count(&conn->cmd_wait_list) == 0);
+ timeout_remove(&conn->to);
+ imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_DONE);
+ imapc_login_callback(conn, reply);
+
+ imapc_command_send_more(conn);
+}
+
+static void imapc_connection_login_cb(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+
+ imapc_connection_auth_finish(conn, reply);
+}
+
+static void
+imapc_connection_proxyauth_login_cb(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+ const struct imapc_client_settings *set = &conn->client->set;
+ struct imapc_command *cmd;
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK) {
+ cmd = imapc_connection_cmd(conn, imapc_connection_login_cb,
+ conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_sendf(cmd, "PROXYAUTH %s", set->username);
+ imapc_command_send_more(conn);
+ } else {
+ imapc_connection_auth_finish(conn, reply);
+ }
+}
+
+static void
+imapc_connection_authenticate_cb(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+ const unsigned char *sasl_output;
+ size_t input_len, sasl_output_len;
+ buffer_t *buf;
+ const char *error;
+
+ if ((int)reply->state != IMAPC_COMMAND_STATE_AUTHENTICATE_CONTINUE) {
+ dsasl_client_free(&conn->sasl_client);
+ imapc_connection_auth_finish(conn, reply);
+ return;
+ }
+
+ input_len = strlen(reply->text_full);
+ buf = t_buffer_create(MAX_BASE64_DECODED_SIZE(input_len));
+ if (base64_decode(reply->text_full, input_len, NULL, buf) < 0) {
+ imapc_auth_failed(conn, reply,
+ t_strdup_printf("Server sent non-base64 input for AUTHENTICATE: %s",
+ reply->text_full));
+ } else if (dsasl_client_input(conn->sasl_client, buf->data, buf->used, &error) < 0) {
+ imapc_auth_failed(conn, reply, error);
+ } else if (dsasl_client_output(conn->sasl_client, &sasl_output,
+ &sasl_output_len, &error) < 0) {
+ imapc_auth_failed(conn, reply, error);
+ } else {
+ string_t *imap_output =
+ t_str_new(MAX_BASE64_ENCODED_SIZE(sasl_output_len)+2);
+ base64_encode(sasl_output, sasl_output_len, imap_output);
+ str_append(imap_output, "\r\n");
+ o_stream_nsend(conn->output, str_data(imap_output),
+ str_len(imap_output));
+ return;
+ }
+ imapc_connection_disconnect(conn);
+}
+
+static bool imapc_connection_have_auth(struct imapc_connection *conn,
+ const char *mech_name)
+{
+ char *const *capa;
+
+ for (capa = conn->capabilities_list; *capa != NULL; capa++) {
+ if (strncasecmp(*capa, "AUTH=", 5) == 0 &&
+ strcasecmp((*capa)+5, mech_name) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int
+imapc_connection_get_sasl_mech(struct imapc_connection *conn,
+ const struct dsasl_client_mech **mech_r,
+ const char **error_r)
+{
+ const struct imapc_client_settings *set = &conn->client->set;
+ const char *const *mechanisms =
+ t_strsplit_spaces(set->sasl_mechanisms, ", ");
+
+ /* find one of the specified SASL mechanisms */
+ for (; *mechanisms != NULL; mechanisms++) {
+ if (imapc_connection_have_auth(conn, *mechanisms)) {
+ *mech_r = dsasl_client_mech_find(*mechanisms);
+ if (*mech_r != NULL)
+ return 0;
+
+ *error_r = t_strdup_printf(
+ "Support for SASL method '%s' is missing", *mechanisms);
+ return -1;
+ }
+ }
+ *error_r = t_strdup_printf("IMAP server doesn't support any of the requested SASL mechanisms: %s",
+ set->sasl_mechanisms);
+ return -1;
+}
+
+static void imapc_connection_authenticate(struct imapc_connection *conn)
+{
+ const struct imapc_client_settings *set = &conn->client->set;
+ struct imapc_command *cmd;
+ struct dsasl_client_settings sasl_set;
+ const struct dsasl_client_mech *sasl_mech = NULL;
+ const char *error;
+
+ if (conn->client->set.debug) {
+ if (set->master_user == NULL) {
+ i_debug("imapc(%s): Authenticating as %s",
+ conn->name, set->username);
+ } else {
+ i_debug("imapc(%s): Authenticating as %s for user %s",
+ conn->name, set->master_user, set->username);
+ }
+ }
+
+ if (set->sasl_mechanisms != NULL && set->sasl_mechanisms[0] != '\0') {
+ if (imapc_connection_get_sasl_mech(conn, &sasl_mech, &error) < 0) {
+ struct imapc_command_reply reply;
+ i_zero(&reply);
+ reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
+ reply.text_full = "";
+ imapc_auth_failed(conn, &reply, error);
+ imapc_connection_disconnect(conn);
+ return;
+ }
+ }
+
+ if (set->use_proxyauth && set->master_user != NULL) {
+ /* We can use LOGIN command */
+ cmd = imapc_connection_cmd(conn, imapc_connection_proxyauth_login_cb,
+ conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_sendf(cmd, "LOGIN %s %s",
+ set->master_user, set->password);
+ return;
+ }
+ if (sasl_mech == NULL &&
+ ((set->master_user == NULL &&
+ !need_literal(set->username) && !need_literal(set->password)) ||
+ (conn->capabilities & IMAPC_CAPABILITY_AUTH_PLAIN) == 0)) {
+ /* We can use LOGIN command */
+ cmd = imapc_connection_cmd(conn, imapc_connection_login_cb,
+ conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_sendf(cmd, "LOGIN %s %s",
+ set->username, set->password);
+ return;
+ }
+
+ i_zero(&sasl_set);
+ if (set->master_user == NULL)
+ sasl_set.authid = set->username;
+ else {
+ sasl_set.authid = set->master_user;
+ sasl_set.authzid = set->username;
+ }
+ sasl_set.password = set->password;
+
+ if (sasl_mech == NULL)
+ sasl_mech = &dsasl_client_mech_plain;
+ conn->sasl_client = dsasl_client_new(sasl_mech, &sasl_set);
+
+ cmd = imapc_connection_cmd(conn, imapc_connection_authenticate_cb, conn);
+ cmd->authenticate = TRUE;
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+
+ if ((conn->capabilities & IMAPC_CAPABILITY_SASL_IR) != 0) {
+ const unsigned char *sasl_output;
+ size_t sasl_output_len;
+ string_t *sasl_output_base64;
+ const char *error;
+
+ if (dsasl_client_output(conn->sasl_client, &sasl_output,
+ &sasl_output_len, &error) < 0) {
+ i_error("imapc(%s): Failed to create initial SASL reply: %s",
+ conn->name, error);
+ imapc_connection_disconnect(conn);
+ return;
+ }
+ sasl_output_base64 = t_str_new(MAX_BASE64_ENCODED_SIZE(sasl_output_len));
+ base64_encode(sasl_output, sasl_output_len, sasl_output_base64);
+
+ imapc_command_sendf(cmd, "AUTHENTICATE %1s %1s",
+ dsasl_client_mech_get_name(sasl_mech),
+ str_c(sasl_output_base64));
+ } else {
+ imapc_command_sendf(cmd, "AUTHENTICATE %1s",
+ dsasl_client_mech_get_name(sasl_mech));
+ }
+}
+
+static void
+imapc_connection_starttls_cb(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+ struct imapc_command *cmd;
+
+ if (reply->state != IMAPC_COMMAND_STATE_OK) {
+ imapc_connection_input_error(conn, "STARTTLS failed: %s",
+ reply->text_full);
+ return;
+ }
+
+ if (imapc_connection_ssl_init(conn) < 0)
+ imapc_connection_disconnect(conn);
+ else {
+ /* get updated capabilities */
+ cmd = imapc_connection_cmd(conn, imapc_connection_capability_cb,
+ conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_send(cmd, "CAPABILITY");
+ }
+}
+
+static void
+imapc_connection_id_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+}
+
+static void imapc_connection_send_id(struct imapc_connection *conn)
+{
+ static unsigned int global_id_counter = 0;
+ struct imapc_command *cmd;
+
+ if ((conn->capabilities & IMAPC_CAPABILITY_ID) == 0 ||
+ conn->client->set.session_id_prefix == NULL)
+ return;
+
+ cmd = imapc_connection_cmd(conn, imapc_connection_id_callback, conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_send(cmd, t_strdup_printf(
+ "ID (\"name\" \"Dovecot\" \"x-session-ext-id\" \"%s-%u\")",
+ conn->client->set.session_id_prefix, ++global_id_counter));
+}
+
+static void imapc_connection_starttls(struct imapc_connection *conn)
+{
+ struct imapc_command *cmd;
+
+ if (conn->client->set.ssl_mode == IMAPC_CLIENT_SSL_MODE_STARTTLS &&
+ conn->ssl_iostream == NULL) {
+ if ((conn->capabilities & IMAPC_CAPABILITY_STARTTLS) == 0) {
+ i_error("imapc(%s): Requested STARTTLS, "
+ "but server doesn't support it",
+ conn->name);
+ imapc_connection_disconnect(conn);
+ return;
+ }
+ cmd = imapc_connection_cmd(conn, imapc_connection_starttls_cb,
+ conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_send(cmd, "STARTTLS");
+ return;
+ }
+ imapc_connection_send_id(conn);
+ imapc_connection_authenticate(conn);
+}
+
+static void
+imapc_connection_capability_cb(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+
+ if (reply->state != IMAPC_COMMAND_STATE_OK) {
+ imapc_connection_input_error(conn,
+ "Failed to get capabilities: %s", reply->text_full);
+ } else if (conn->capabilities == 0) {
+ imapc_connection_input_error(conn,
+ "Capabilities not returned by server");
+ } else {
+ imapc_connection_starttls(conn);
+ }
+}
+
+static int imapc_connection_input_banner(struct imapc_connection *conn)
+{
+ const struct imap_arg *imap_args;
+ const char *key, *value;
+ struct imapc_command *cmd;
+ int ret;
+
+ if ((ret = imapc_connection_read_line(conn, &imap_args)) <= 0)
+ return ret;
+ /* we already verified that the banner beigns with OK */
+ i_assert(imap_arg_atom_equals(imap_args, "OK"));
+ imap_args++;
+
+ if (imapc_connection_handle_imap_resp_text(conn, imap_args,
+ &key, &value) < 0)
+ return -1;
+ imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_AUTHENTICATING);
+
+ if (conn->capabilities == 0) {
+ /* capabilities weren't sent in the banner. ask for them. */
+ cmd = imapc_connection_cmd(conn, imapc_connection_capability_cb,
+ conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_send(cmd, "CAPABILITY");
+ } else {
+ imapc_connection_starttls(conn);
+ }
+ conn->input_callback = NULL;
+ imapc_connection_input_reset(conn);
+ return 1;
+}
+
+static int imapc_connection_input_untagged(struct imapc_connection *conn)
+{
+ const struct imap_arg *imap_args;
+ const unsigned char *data;
+ size_t size;
+ const char *name, *value;
+ struct imap_parser *parser;
+ struct imapc_untagged_reply reply;
+ int ret;
+
+ if (conn->state == IMAPC_CONNECTION_STATE_CONNECTING) {
+ /* input banner */
+ data = i_stream_get_data(conn->input, &size);
+ if (size < 3 && memchr(data, '\n', size) == NULL)
+ return 0;
+ if (i_memcasecmp(data, "OK ", 3) != 0) {
+ imapc_connection_input_error(conn,
+ "Banner doesn't begin with OK: %s",
+ t_strcut(t_strndup(data, size), '\n'));
+ return -1;
+ }
+ conn->input_callback = imapc_connection_input_banner;
+ return 1;
+ }
+
+ if ((ret = imapc_connection_read_line(conn, &imap_args)) == 0)
+ return 0;
+ else if (ret < 0) {
+ imapc_connection_input_reset(conn);
+ return 1;
+ }
+ if (!imap_arg_get_atom(&imap_args[0], &name)) {
+ imapc_connection_input_error(conn, "Invalid untagged reply");
+ return -1;
+ }
+ imap_args++;
+
+ if (conn->input_state == IMAPC_INPUT_STATE_UNTAGGED &&
+ str_to_uint32(name, &conn->cur_num) == 0) {
+ /* <seq> <event> */
+ conn->input_state = IMAPC_INPUT_STATE_UNTAGGED_NUM;
+ if (!imap_arg_get_atom(&imap_args[0], &name)) {
+ imapc_connection_input_error(conn,
+ "Invalid untagged reply");
+ return -1;
+ }
+ imap_args++;
+ }
+ i_zero(&reply);
+
+ if (strcasecmp(name, "OK") == 0) {
+ if (imapc_connection_handle_imap_resp_text(conn, imap_args,
+ &reply.resp_text_key,
+ &reply.resp_text_value) < 0)
+ return -1;
+ } else if (strcasecmp(name, "CAPABILITY") == 0) {
+ value = imap_args_to_str(imap_args);
+ if (imapc_connection_parse_capability(conn, value) < 0)
+ return -1;
+ } else if (strcasecmp(name, "BYE") == 0) {
+ i_free(conn->disconnect_reason);
+ conn->disconnect_reason = i_strdup(imap_args_to_str(imap_args));
+ }
+
+ reply.name = name;
+ reply.num = conn->cur_num;
+ reply.args = imap_args;
+ reply.file_args = array_get(&conn->literal_files,
+ &reply.file_args_count);
+
+ if (conn->selected_box != NULL) {
+ reply.untagged_box_context =
+ conn->selected_box->untagged_box_context;
+ }
+
+ /* the callback may disconnect and destroy the parser */
+ parser = conn->parser;
+ imap_parser_ref(parser);
+ conn->client->untagged_callback(&reply, conn->client->untagged_context);
+ imap_parser_unref(&parser);
+ imapc_connection_input_reset(conn);
+ return 1;
+}
+
+static int imapc_connection_input_plus(struct imapc_connection *conn)
+{
+ struct imapc_command *const *cmds;
+ unsigned int cmds_count;
+ const char *line;
+
+ if ((line = i_stream_next_line(conn->input)) == NULL)
+ return 0;
+
+ cmds = array_get(&conn->cmd_send_queue, &cmds_count);
+ if (conn->idle_plus_waiting) {
+ /* "+ idling" reply for IDLE command */
+ conn->idle_plus_waiting = FALSE;
+ conn->idling = TRUE;
+ /* no timing out while IDLEing */
+ if (conn->to != NULL && !conn->idle_stopping)
+ timeout_remove(&conn->to);
+ } else if (cmds_count > 0 && cmds[0]->wait_for_literal) {
+ /* reply for literal */
+ cmds[0]->wait_for_literal = FALSE;
+ imapc_command_send_more(conn);
+ } else {
+ cmds = array_get(&conn->cmd_wait_list, &cmds_count);
+ if (cmds_count > 0 && cmds[0]->authenticate) {
+ /* continue AUTHENTICATE */
+ struct imapc_command_reply reply;
+
+ i_zero(&reply);
+ reply.state = (enum imapc_command_state)IMAPC_COMMAND_STATE_AUTHENTICATE_CONTINUE;
+ reply.text_full = line;
+ cmds[0]->callback(&reply, cmds[0]->context);
+ } else {
+ imapc_connection_input_error(conn, "Unexpected '+': %s", line);
+ return -1;
+ }
+ }
+
+ imapc_connection_input_reset(conn);
+ return 1;
+}
+
+static void
+imapc_connection_throttle_shrink_timeout(struct imapc_connection *conn)
+{
+ if (conn->throttle_msecs <= 1)
+ conn->throttle_msecs = 0;
+ else
+ conn->throttle_msecs = conn->throttle_msecs*3 / 4;
+
+ if (conn->throttle_shrink_msecs <= conn->client->set.throttle_set.shrink_min_msecs)
+ conn->throttle_shrink_msecs = 0;
+ else
+ conn->throttle_shrink_msecs = conn->throttle_shrink_msecs*3 / 4;
+
+ timeout_remove(&conn->to_throttle_shrink);
+ if (conn->throttle_shrink_msecs > 0) {
+ conn->to_throttle_shrink =
+ timeout_add(conn->throttle_shrink_msecs,
+ imapc_connection_throttle_shrink_timeout, conn);
+ }
+}
+
+static void
+imapc_connection_throttle(struct imapc_connection *conn,
+ const struct imapc_command_reply *reply)
+{
+ timeout_remove(&conn->to_throttle);
+
+ /* If GMail returns [THROTTLED], start slowing down commands.
+ Unfortunately this isn't a nice resp-text-code, but just
+ appended at the end of the line (although we kind of support
+ it as resp-text-code also in here if it's uppercased). */
+ if (strstr(reply->text_full, "[THROTTLED]") != NULL) {
+ if (conn->throttle_msecs == 0)
+ conn->throttle_msecs = conn->client->set.throttle_set.init_msecs;
+ else if (conn->throttle_msecs < conn->last_successful_throttle_msecs)
+ conn->throttle_msecs = conn->last_successful_throttle_msecs;
+ else {
+ conn->throttle_msecs *= 2;
+ if (conn->throttle_msecs > conn->client->set.throttle_set.max_msecs)
+ conn->throttle_msecs = conn->client->set.throttle_set.max_msecs;
+ }
+ if (conn->throttle_shrink_msecs == 0)
+ conn->throttle_shrink_msecs = conn->client->set.throttle_set.shrink_min_msecs;
+ else
+ conn->throttle_shrink_msecs *= 2;
+ if (conn->to_throttle_shrink != NULL)
+ timeout_reset(conn->to_throttle_shrink);
+ } else {
+ if (conn->throttle_shrink_msecs > 0 &&
+ conn->to_throttle_shrink == NULL) {
+ conn->to_throttle_shrink =
+ timeout_add(conn->throttle_shrink_msecs,
+ imapc_connection_throttle_shrink_timeout, conn);
+ }
+ conn->last_successful_throttle_msecs = conn->throttle_msecs;
+ }
+
+ if (conn->throttle_msecs > 0) {
+ conn->throttle_end_timeval = ioloop_timeval;
+ timeval_add_msecs(&conn->throttle_end_timeval,
+ conn->throttle_msecs);
+ conn->throttle_pending = TRUE;
+ }
+}
+
+static void
+imapc_command_reply_free(struct imapc_command *cmd,
+ const struct imapc_command_reply *reply)
+{
+ cmd->callback(reply, cmd->context);
+ imapc_command_free(cmd);
+}
+
+static int imapc_connection_input_tagged(struct imapc_connection *conn)
+{
+ struct imapc_command *const *cmds, *cmd = NULL;
+ unsigned int i, count;
+ char *line, *linep;
+ const char *p;
+ struct imapc_command_reply reply;
+
+ line = i_stream_next_line(conn->input);
+ if (line == NULL)
+ return 0;
+ /* make sure reply texts stays valid if input stream gets freed */
+ line = t_strdup_noconst(line);
+
+ i_zero(&reply);
+
+ linep = strchr(line, ' ');
+ if (linep == NULL)
+ reply.text_full = "";
+ else {
+ *linep = '\0';
+ reply.text_full = linep + 1;
+ }
+
+ if (strcasecmp(line, "ok") == 0)
+ reply.state = IMAPC_COMMAND_STATE_OK;
+ else if (strcasecmp(line, "no") == 0)
+ reply.state = IMAPC_COMMAND_STATE_NO;
+ else if (strcasecmp(line, "bad") == 0)
+ reply.state = IMAPC_COMMAND_STATE_BAD;
+ else {
+ imapc_connection_input_error(conn,
+ "Invalid state in tagged reply: %u %s %s",
+ conn->cur_tag, line, reply.text_full);
+ return -1;
+ }
+
+ if (reply.text_full[0] == '[') {
+ /* get resp-text */
+ if (imapc_connection_handle_resp_text(conn, reply.text_full,
+ &reply.resp_text_key,
+ &reply.resp_text_value) < 0)
+ return -1;
+
+ p = i_strchr_to_next(reply.text_full, ']');
+ i_assert(p != NULL);
+ reply.text_without_resp = p;
+ if (reply.text_without_resp[0] == ' ')
+ reply.text_without_resp++;
+ } else {
+ reply.text_without_resp = reply.text_full;
+ }
+ /* if we've pipelined multiple commands, handle [THROTTLED] reply
+ from only one of them */
+ if (!conn->throttle_pending)
+ imapc_connection_throttle(conn, &reply);
+
+ /* find the command. it's either the first command in send queue
+ (literal failed) or somewhere in wait list. */
+ cmds = array_get(&conn->cmd_send_queue, &count);
+ if (count > 0 && cmds[0]->tag == conn->cur_tag) {
+ cmd = cmds[0];
+ array_pop_front(&conn->cmd_send_queue);
+ } else {
+ cmds = array_get(&conn->cmd_wait_list, &count);
+ for (i = 0; i < count; i++) {
+ if (cmds[i]->tag == conn->cur_tag) {
+ cmd = cmds[i];
+ array_delete(&conn->cmd_wait_list, i, 1);
+ break;
+ }
+ }
+ }
+ if (array_count(&conn->cmd_wait_list) == 0 &&
+ array_count(&conn->cmd_send_queue) == 0 &&
+ conn->state == IMAPC_CONNECTION_STATE_DONE && conn->to != NULL)
+ timeout_remove(&conn->to);
+
+ if (cmd == NULL) {
+ if (seq_range_exists(&conn->aborted_cmd_tags, conn->cur_tag)) {
+ /* sent command was already aborted - ignore it */
+ seq_range_array_remove(&conn->aborted_cmd_tags,
+ conn->cur_tag);
+ imapc_connection_input_reset(conn);
+ return 1;
+ }
+ imapc_connection_input_error(conn,
+ "Unknown tag in a reply: %u %s %s",
+ conn->cur_tag, line, reply.text_full);
+ return -1;
+ }
+ if ((cmd->flags & IMAPC_COMMAND_FLAG_SELECT) != 0)
+ conn->select_waiting_reply = FALSE;
+
+ if (reply.state == IMAPC_COMMAND_STATE_BAD) {
+ i_error("imapc(%s): Command '%s' failed with BAD: %u %s",
+ conn->name, imapc_command_get_readable(cmd),
+ conn->cur_tag, reply.text_full);
+ imapc_connection_disconnect(conn);
+ }
+
+ if (reply.state == IMAPC_COMMAND_STATE_NO &&
+ (cmd->flags & IMAPC_COMMAND_FLAG_SELECT) != 0 &&
+ conn->selected_box != NULL) {
+ /* EXAMINE/SELECT failed: mailbox is no longer selected */
+ imapc_connection_unselect(conn->selected_box, TRUE);
+ }
+
+ if (conn->reconnect_command_count > 0 &&
+ (cmd->flags & IMAPC_COMMAND_FLAG_RECONNECTED) != 0) {
+ i_assert(conn->reconnect_command_count > 0);
+ if (--conn->reconnect_command_count == 0) {
+ /* we've received replies for all the commands started
+ before reconnection. if we get disconnected now, we
+ can safely reconnect without worrying about infinite
+ reconnect loops. */
+ if (conn->selected_box != NULL)
+ conn->selected_box->reconnect_ok = TRUE;
+ }
+ }
+ if (conn->reconnect_command_count == 0) {
+ /* we've successfully received replies to some commands. */
+ conn->reconnect_ok = TRUE;
+ }
+ imapc_connection_input_reset(conn);
+ imapc_command_reply_free(cmd, &reply);
+ imapc_command_send_more(conn);
+ return 1;
+}
+
+static int imapc_connection_input_one(struct imapc_connection *conn)
+{
+ const char *tag;
+ int ret = -1;
+
+ if (conn->input_callback != NULL)
+ return conn->input_callback(conn);
+
+ switch (conn->input_state) {
+ case IMAPC_INPUT_STATE_NONE:
+ tag = imap_parser_read_word(conn->parser);
+ if (tag == NULL)
+ return 0;
+
+ if (strcmp(tag, "*") == 0) {
+ conn->input_state = IMAPC_INPUT_STATE_UNTAGGED;
+ conn->cur_num = 0;
+ ret = imapc_connection_input_untagged(conn);
+ } else if (strcmp(tag, "+") == 0) {
+ conn->input_state = IMAPC_INPUT_STATE_PLUS;
+ ret = imapc_connection_input_plus(conn);
+ } else {
+ conn->input_state = IMAPC_INPUT_STATE_TAGGED;
+ if (str_to_uint(tag, &conn->cur_tag) < 0 ||
+ conn->cur_tag == 0) {
+ imapc_connection_input_error(conn,
+ "Invalid command tag: %s", tag);
+ ret = -1;
+ } else {
+ ret = imapc_connection_input_tagged(conn);
+ }
+ }
+ break;
+ case IMAPC_INPUT_STATE_PLUS:
+ ret = imapc_connection_input_plus(conn);
+ break;
+ case IMAPC_INPUT_STATE_UNTAGGED:
+ case IMAPC_INPUT_STATE_UNTAGGED_NUM:
+ ret = imapc_connection_input_untagged(conn);
+ break;
+ case IMAPC_INPUT_STATE_TAGGED:
+ ret = imapc_connection_input_tagged(conn);
+ break;
+ }
+ return ret;
+}
+
+static void imapc_connection_input(struct imapc_connection *conn)
+{
+ const char *errstr;
+ string_t *str;
+ ssize_t ret = 0;
+
+ /* we need to read as much as we can with SSL streams to avoid
+ hanging */
+ imapc_connection_ref(conn);
+ while (conn->input != NULL && (ret = i_stream_read(conn->input)) > 0)
+ imapc_connection_input_pending(conn);
+
+ if (ret < 0 && conn->client->logging_out &&
+ conn->disconnect_reason != NULL) {
+ /* expected disconnection */
+ imapc_connection_disconnect(conn);
+ } else if (ret < 0) {
+ /* disconnected or buffer full */
+ str = t_str_new(128);
+ if (conn->disconnect_reason != NULL) {
+ str_printfa(str, "Server disconnected with message: %s",
+ conn->disconnect_reason);
+ } else if (ret == -2) {
+ str_printfa(str, "Server sent too large input "
+ "(buffer full at %zu)",
+ i_stream_get_data_size(conn->input));
+ } else if (conn->ssl_iostream == NULL) {
+ errstr = conn->input->stream_errno == 0 ? "EOF" :
+ i_stream_get_error(conn->input);
+ str_printfa(str, "Server disconnected unexpectedly: %s",
+ errstr);
+ } else {
+ errstr = ssl_iostream_get_last_error(conn->ssl_iostream);
+ if (errstr == NULL) {
+ errstr = conn->input->stream_errno == 0 ? "EOF" :
+ i_stream_get_error(conn->input);
+ }
+ str_printfa(str, "Server disconnected unexpectedly: %s",
+ errstr);
+ }
+ imapc_connection_try_reconnect(conn, str_c(str), 0, FALSE);
+ }
+ imapc_connection_unref(&conn);
+}
+
+static int imapc_connection_ssl_handshaked(const char **error_r, void *context)
+{
+ struct imapc_connection *conn = context;
+ const char *error;
+
+ if (ssl_iostream_check_cert_validity(conn->ssl_iostream,
+ conn->client->set.host, &error) == 0) {
+ if (conn->client->set.debug) {
+ i_debug("imapc(%s): SSL handshake successful",
+ conn->name);
+ }
+ return 0;
+ } else if (conn->client->set.ssl_set.allow_invalid_cert) {
+ if (conn->client->set.debug) {
+ i_debug("imapc(%s): SSL handshake successful, "
+ "ignoring invalid certificate: %s",
+ conn->name, error);
+ }
+ return 0;
+ } else {
+ *error_r = error;
+ return -1;
+ }
+}
+
+static int imapc_connection_ssl_init(struct imapc_connection *conn)
+{
+ const char *error;
+
+ if (conn->client->ssl_ctx == NULL) {
+ i_error("imapc(%s): No SSL context", conn->name);
+ return -1;
+ }
+
+ if (conn->client->set.debug)
+ i_debug("imapc(%s): Starting SSL handshake", conn->name);
+
+ if (conn->raw_input != conn->input) {
+ /* recreate rawlog after STARTTLS */
+ i_stream_ref(conn->raw_input);
+ o_stream_ref(conn->raw_output);
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ conn->input = conn->raw_input;
+ conn->output = conn->raw_output;
+ }
+
+ io_remove(&conn->io);
+ if (io_stream_create_ssl_client(conn->client->ssl_ctx,
+ conn->client->set.host,
+ &conn->client->set.ssl_set,
+ &conn->input, &conn->output,
+ &conn->ssl_iostream, &error) < 0) {
+ i_error("imapc(%s): Couldn't initialize SSL client: %s",
+ conn->name, error);
+ return -1;
+ }
+ conn->io = io_add_istream(conn->input, imapc_connection_input, conn);
+ ssl_iostream_set_handshake_callback(conn->ssl_iostream,
+ imapc_connection_ssl_handshaked,
+ conn);
+ if (ssl_iostream_handshake(conn->ssl_iostream) < 0) {
+ i_error("imapc(%s): SSL handshake failed: %s", conn->name,
+ ssl_iostream_get_last_error(conn->ssl_iostream));
+ return -1;
+ }
+
+ if (*conn->client->set.rawlog_dir != '\0') {
+ iostream_rawlog_create(conn->client->set.rawlog_dir,
+ &conn->input, &conn->output);
+ }
+
+ imap_parser_set_streams(conn->parser, conn->input, NULL);
+ return 0;
+}
+
+static int imapc_connection_connected(struct imapc_connection *conn)
+{
+ const struct ip_addr *ip = &conn->ips[conn->prev_connect_idx];
+ struct ip_addr local_ip;
+ in_port_t local_port;
+ int err;
+
+ i_assert(conn->io == NULL);
+
+ err = net_geterror(conn->fd);
+ if (err != 0) {
+ imapc_connection_try_reconnect(conn, t_strdup_printf(
+ "connect(%s, %u) failed: %s",
+ net_ip2addr(ip), conn->client->set.port,
+ strerror(err)), conn->client->set.connect_retry_interval_msecs, TRUE);
+ return -1;
+ }
+ if (net_getsockname(conn->fd, &local_ip, &local_port) < 0)
+ local_port = 0;
+ i_info("imapc(%s): Connected to %s:%u (local %s:%u)", conn->name,
+ net_ip2addr(ip), conn->client->set.port,
+ net_ip2addr(&local_ip), local_port);
+ conn->io = io_add(conn->fd, IO_READ, imapc_connection_input, conn);
+ o_stream_set_flush_callback(conn->output, imapc_connection_output,
+ conn);
+
+ if (conn->client->set.ssl_mode == IMAPC_CLIENT_SSL_MODE_IMMEDIATE) {
+ if (imapc_connection_ssl_init(conn) < 0)
+ imapc_connection_disconnect(conn);
+ }
+ return imapc_connection_output(conn);
+}
+
+static void imapc_connection_timeout(struct imapc_connection *conn)
+{
+ const struct ip_addr *ip = &conn->ips[conn->prev_connect_idx];
+ const char *errstr;
+ bool connect_error = FALSE;
+
+ switch (conn->state) {
+ case IMAPC_CONNECTION_STATE_CONNECTING:
+ errstr = t_strdup_printf("connect(%s, %u) timed out after %u seconds",
+ net_ip2addr(ip), conn->client->set.port,
+ conn->client->set.connect_timeout_msecs/1000);
+ connect_error = TRUE;
+ break;
+ case IMAPC_CONNECTION_STATE_AUTHENTICATING:
+ errstr = t_strdup_printf("Authentication timed out after %u seconds",
+ conn->client->set.connect_timeout_msecs/1000);
+ break;
+ default:
+ i_unreached();
+ }
+ imapc_connection_try_reconnect(conn, errstr, 0, connect_error);
+}
+
+static void
+imapc_noop_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+}
+
+static void
+imapc_reidle_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+
+ imapc_connection_idle(conn);
+}
+
+static void imapc_connection_reset_idle(struct imapc_connection *conn)
+{
+ struct imapc_command *cmd;
+
+ if (conn->idling)
+ cmd = imapc_connection_cmd(conn, imapc_reidle_callback, conn);
+ else if (array_count(&conn->cmd_wait_list) == 0)
+ cmd = imapc_connection_cmd(conn, imapc_noop_callback, NULL);
+ else {
+ /* IMAP command reply is taking a long time */
+ return;
+ }
+ imapc_command_send(cmd, "NOOP");
+}
+
+static void imapc_connection_connect_next_ip(struct imapc_connection *conn)
+{
+ const struct ip_addr *ip = NULL;
+ unsigned int i;
+ int fd;
+
+ i_assert(conn->client->set.max_idle_time > 0);
+
+ for (i = 0; i<conn->ips_count;) {
+ conn->prev_connect_idx = (conn->prev_connect_idx+1) % conn->ips_count;
+ ip = &conn->ips[conn->prev_connect_idx];
+ fd = net_connect_ip(ip, conn->client->set.port, NULL);
+ if (fd != -1)
+ break;
+
+ /* failed to connect to one of the IPs immediately
+ (e.g. IPv6 address without connectivity). try all IPs
+ before failing completely. */
+ i_error("net_connect_ip(%s:%u) failed: %m",
+ net_ip2addr(ip), conn->client->set.port);
+ if (conn->prev_connect_idx+1 == conn->ips_count) {
+ imapc_connection_try_reconnect(conn, "No more IP address(es) to try",
+ conn->client->set.connect_retry_interval_msecs, TRUE);
+ return;
+ }
+ }
+
+ i_assert(ip != NULL);
+
+ conn->fd = fd;
+ conn->input = conn->raw_input =
+ i_stream_create_fd(fd, conn->client->set.max_line_length);
+ conn->output = conn->raw_output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+
+ if (*conn->client->set.rawlog_dir != '\0' &&
+ conn->client->set.ssl_mode != IMAPC_CLIENT_SSL_MODE_IMMEDIATE) {
+ iostream_rawlog_create(conn->client->set.rawlog_dir,
+ &conn->input, &conn->output);
+ }
+
+ o_stream_set_flush_pending(conn->output, TRUE);
+ o_stream_set_flush_callback(conn->output, imapc_connection_connected,
+ conn);
+ conn->parser = imap_parser_create(conn->input, NULL,
+ conn->client->set.max_line_length);
+ conn->to = timeout_add(conn->client->set.connect_timeout_msecs,
+ imapc_connection_timeout, conn);
+ conn->to_output = timeout_add(conn->client->set.max_idle_time*1000,
+ imapc_connection_reset_idle, conn);
+ if (conn->client->set.debug) {
+ i_debug("imapc(%s): Connecting to %s:%u", conn->name,
+ net_ip2addr(ip), conn->client->set.port);
+ }
+}
+
+static void
+imapc_connection_dns_callback(const struct dns_lookup_result *result,
+ struct imapc_connection *conn)
+{
+ conn->dns_lookup = NULL;
+
+ if (result->ret != 0) {
+ i_error("imapc(%s): dns_lookup(%s) failed: %s",
+ conn->name, conn->client->set.host, result->error);
+ imapc_connection_set_disconnected(conn);
+ return;
+ }
+
+ i_assert(result->ips_count > 0);
+ conn->ips_count = result->ips_count;
+ conn->ips = i_new(struct ip_addr, conn->ips_count);
+ memcpy(conn->ips, result->ips, sizeof(*conn->ips) * conn->ips_count);
+ conn->prev_connect_idx = conn->ips_count - 1;
+
+ imapc_connection_connect_next_ip(conn);
+}
+
+void imapc_connection_connect(struct imapc_connection *conn)
+{
+ struct dns_lookup_settings dns_set;
+ struct ip_addr ip, *ips;
+ unsigned int ips_count;
+ int ret;
+
+ if (conn->fd != -1 || conn->dns_lookup != NULL)
+ return;
+ if (conn->reconnect_waiting) {
+ /* wait for the reconnection delay to finish before
+ doing anything. */
+ return;
+ }
+
+ conn->reconnecting = FALSE;
+ /* if we get disconnected before we've finished all the pending
+ commands, don't reconnect */
+ conn->reconnect_command_count = array_count(&conn->cmd_wait_list) +
+ array_count(&conn->cmd_send_queue);
+
+ imapc_connection_input_reset(conn);
+ conn->last_connect = ioloop_timeval;
+
+ if (conn->client->set.debug) {
+ i_debug("imapc(%s): Looking up IP address "
+ "(reconnect_ok=%s, last_connect=%ld)", conn->name,
+ (conn->reconnect_ok ? "true" : "false"),
+ (long)conn->last_connect.tv_sec);
+ }
+
+ i_zero(&dns_set);
+ dns_set.dns_client_socket_path =
+ conn->client->set.dns_client_socket_path;
+ dns_set.timeout_msecs = conn->client->set.connect_timeout_msecs;
+ dns_set.event_parent = conn->client->event;
+
+ imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_CONNECTING);
+ if (conn->ips_count > 0) {
+ /* do nothing */
+ } else if (net_addr2ip(conn->client->set.host, &ip) == 0) {
+ conn->ips_count = 1;
+ conn->ips = i_new(struct ip_addr, conn->ips_count);
+ conn->ips[0] = ip;
+ } else if (*dns_set.dns_client_socket_path == '\0') {
+ ret = net_gethostbyname(conn->client->set.host,
+ &ips, &ips_count);
+ if (ret != 0) {
+ i_error("imapc(%s): net_gethostbyname(%s) failed: %s",
+ conn->name, conn->client->set.host,
+ net_gethosterror(ret));
+ imapc_connection_set_disconnected(conn);
+ return;
+ }
+ conn->ips_count = ips_count;
+ conn->ips = i_new(struct ip_addr, ips_count);
+ memcpy(conn->ips, ips, ips_count * sizeof(*ips));
+ } else {
+ (void)dns_lookup(conn->client->set.host, &dns_set,
+ imapc_connection_dns_callback, conn,
+ &conn->dns_lookup);
+ return;
+ }
+ imapc_connection_connect_next_ip(conn);
+}
+
+void imapc_connection_input_pending(struct imapc_connection *conn)
+{
+ int ret = 1;
+
+ if (conn->input == NULL)
+ return;
+
+ if (conn->to != NULL && !conn->idle_stopping)
+ timeout_reset(conn->to);
+
+ o_stream_cork(conn->output);
+ while (ret > 0 && conn->input != NULL) {
+ T_BEGIN {
+ ret = imapc_connection_input_one(conn);
+ } T_END;
+ }
+
+ if (conn->output != NULL)
+ o_stream_uncork(conn->output);
+}
+
+static struct imapc_command *
+imapc_command_begin(imapc_command_callback_t *callback, void *context)
+{
+ struct imapc_command *cmd;
+ pool_t pool;
+
+ i_assert(callback != NULL);
+
+ pool = pool_alloconly_create("imapc command", 2048);
+ cmd = p_new(pool, struct imapc_command, 1);
+ cmd->pool = pool;
+ cmd->callback = callback;
+ cmd->context = context;
+
+ /* use a globally unique tag counter so looking at rawlogs is
+ somewhat easier */
+ if (++imapc_client_cmd_tag_counter == 0)
+ imapc_client_cmd_tag_counter++;
+ cmd->tag = imapc_client_cmd_tag_counter;
+ return cmd;
+}
+
+static void imapc_command_free(struct imapc_command *cmd)
+{
+ struct imapc_command_stream *stream;
+
+ if (array_is_created(&cmd->streams)) {
+ array_foreach_modifiable(&cmd->streams, stream)
+ i_stream_unref(&stream->input);
+ }
+ pool_unref(&cmd->pool);
+}
+
+const char *imapc_command_get_tag(struct imapc_command *cmd)
+{
+ return t_strdup_printf("%u", cmd->tag);
+}
+
+void imapc_command_abort(struct imapc_command **_cmd)
+{
+ struct imapc_command *cmd = *_cmd;
+
+ *_cmd = NULL;
+ imapc_command_free(cmd);
+}
+
+static void imapc_command_timeout(struct imapc_connection *conn)
+{
+ struct imapc_command *const *cmds;
+ unsigned int count;
+
+ cmds = array_get(&conn->cmd_wait_list, &count);
+ i_assert(count > 0);
+
+ imapc_connection_try_reconnect(conn, t_strdup_printf(
+ "Command '%s' timed out", imapc_command_get_readable(cmds[0])), 0, FALSE);
+}
+
+static bool
+parse_sync_literal(const unsigned char *data, unsigned int pos,
+ unsigned int *value_r)
+{
+ unsigned int value = 0, mul = 1;
+
+ /* data should contain "{size}\r\n" and pos points after \n */
+ if (pos <= 4 || data[pos-1] != '\n' || data[pos-2] != '\r' ||
+ data[pos-3] != '}' || !i_isdigit(data[pos-4]))
+ return FALSE;
+ pos -= 4;
+
+ do {
+ value += (data[pos] - '0') * mul;
+ mul = mul*10;
+ pos--;
+ } while (pos > 0 && i_isdigit(data[pos]));
+
+ if (pos == 0 || data[pos] != '{')
+ return FALSE;
+
+ *value_r = value;
+ return TRUE;
+}
+
+static void imapc_command_send_finished(struct imapc_connection *conn,
+ struct imapc_command *cmd)
+{
+ struct imapc_command *const *cmdp;
+
+ i_assert(conn->to != NULL);
+
+ if (cmd->idle)
+ conn->idle_plus_waiting = TRUE;
+ cmd->sent = TRUE;
+
+ /* everything sent. move command to wait list. */
+ cmdp = array_front(&conn->cmd_send_queue);
+ i_assert(*cmdp == cmd);
+ array_pop_front(&conn->cmd_send_queue);
+ array_push_back(&conn->cmd_wait_list, &cmd);
+
+ /* send the next command in queue */
+ imapc_command_send_more(conn);
+}
+
+static struct imapc_command_stream *
+imapc_command_get_sending_stream(struct imapc_command *cmd)
+{
+ struct imapc_command_stream *stream;
+
+ if (!array_is_created(&cmd->streams) || array_count(&cmd->streams) == 0)
+ return NULL;
+
+ stream = array_front_modifiable(&cmd->streams);
+ if (stream->pos != cmd->send_pos)
+ return NULL;
+ return stream;
+}
+
+static int imapc_command_try_send_stream(struct imapc_connection *conn,
+ struct imapc_command *cmd)
+{
+ struct imapc_command_stream *stream;
+ enum ostream_send_istream_result res;
+
+ stream = imapc_command_get_sending_stream(cmd);
+ if (stream == NULL)
+ return -2;
+
+ /* we're sending the stream now */
+ o_stream_set_max_buffer_size(conn->output, 0);
+ res = o_stream_send_istream(conn->output, stream->input);
+ o_stream_set_max_buffer_size(conn->output, SIZE_MAX);
+
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ i_assert(stream->input->v_offset < stream->size);
+ return 0;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_error("imapc: read(%s) failed: %s",
+ i_stream_get_name(stream->input),
+ i_stream_get_error(stream->input));
+ return -1;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ /* disconnected */
+ return -1;
+ }
+ i_assert(stream->input->v_offset == stream->size);
+
+ /* finished with the stream */
+ i_stream_unref(&stream->input);
+ array_pop_front(&cmd->streams);
+
+ i_assert(cmd->send_pos != cmd->data->used);
+ return 1;
+}
+
+static void imapc_connection_set_selecting(struct imapc_client_mailbox *box)
+{
+ struct imapc_connection *conn = box->conn;
+
+ i_assert(conn->qresync_selecting_box == NULL);
+
+ if (conn->selected_on_server &&
+ (conn->capabilities & IMAPC_CAPABILITY_QRESYNC) != 0) {
+ /* server will send a [CLOSED] once selected mailbox is
+ closed */
+ conn->qresync_selecting_box = box;
+ } else {
+ /* we'll have to assume that all the future untagged messages
+ are for the mailbox we're selecting */
+ conn->selected_box = box;
+ conn->selected_on_server = TRUE;
+ }
+ conn->select_waiting_reply = TRUE;
+}
+
+static bool imapc_connection_is_throttled(struct imapc_connection *conn)
+{
+ timeout_remove(&conn->to_throttle);
+
+ if (conn->throttle_msecs == 0) {
+ /* we haven't received [THROTTLED] recently */
+ return FALSE;
+ }
+ if (array_count(&conn->cmd_wait_list) > 0) {
+ /* wait until we have received the existing commands' tagged
+ replies to see if we're still throttled */
+ return TRUE;
+ }
+ if (timeval_cmp(&ioloop_timeval, &conn->throttle_end_timeval) >= 0) {
+ /* we reached the throttle timeout - send the next command */
+ conn->throttle_pending = FALSE;
+ return FALSE;
+ }
+
+ /* we're still being throttled - wait for it to end */
+ conn->to_throttle = timeout_add_absolute(&conn->throttle_end_timeval,
+ imapc_command_send_more, conn);
+ return TRUE;
+}
+
+static void imapc_command_send_more(struct imapc_connection *conn)
+{
+ struct imapc_command *const *cmds, *cmd;
+ struct imapc_command_reply reply;
+ const unsigned char *p, *data;
+ unsigned int count, size;
+ size_t seek_pos, start_pos, end_pos;
+ int ret;
+
+ if (imapc_connection_is_throttled(conn))
+ return;
+
+ cmds = array_get(&conn->cmd_send_queue, &count);
+ if (count == 0)
+ return;
+ cmd = cmds[0];
+
+ if ((cmd->flags & IMAPC_COMMAND_FLAG_PRELOGIN) == 0 &&
+ conn->state != IMAPC_CONNECTION_STATE_DONE) {
+ /* wait until we're fully connected */
+ return;
+ }
+ if ((cmd->flags & IMAPC_COMMAND_FLAG_LOGOUT) != 0 &&
+ array_count(&conn->cmd_wait_list) > 0) {
+ /* wait until existing commands have finished */
+ return;
+ }
+ if (conn->select_waiting_reply) {
+ /* wait for SELECT to finish */
+ return;
+ }
+ if (cmd->wait_for_literal) {
+ /* wait until we received '+' */
+ return;
+ }
+
+ i_assert(cmd->send_pos < cmd->data->used);
+
+ if (cmd->box == NULL) {
+ /* non-mailbox command */
+ } else if (cmd->send_pos == 0 &&
+ (cmd->flags & IMAPC_COMMAND_FLAG_SELECT) != 0) {
+ /* SELECT/EXAMINE command */
+ imapc_connection_set_selecting(cmd->box);
+ } else if (!imapc_client_mailbox_is_opened(cmd->box)) {
+ if (cmd->box->reconnecting) {
+ /* wait for SELECT/EXAMINE */
+ return;
+ }
+ /* shouldn't normally happen */
+ i_zero(&reply);
+ reply.text_without_resp = reply.text_full = "Mailbox not open";
+ reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
+
+ array_pop_front(&conn->cmd_send_queue);
+ imapc_command_reply_free(cmd, &reply);
+ imapc_command_send_more(conn);
+ return;
+ }
+
+ /* add timeout for commands if there's not one yet
+ (pre-login has its own timeout) */
+ if ((cmd->flags & IMAPC_COMMAND_FLAG_LOGOUT) != 0) {
+ /* LOGOUT has a shorter timeout */
+ timeout_remove(&conn->to);
+ conn->to = timeout_add(IMAPC_LOGOUT_TIMEOUT_MSECS,
+ imapc_command_timeout, conn);
+ } else if (conn->to == NULL) {
+ conn->to = timeout_add(conn->client->set.cmd_timeout_msecs,
+ imapc_command_timeout, conn);
+ }
+
+ timeout_reset(conn->to_output);
+ if ((ret = imapc_command_try_send_stream(conn, cmd)) == 0)
+ return;
+ if (ret == -1) {
+ i_zero(&reply);
+ reply.text_without_resp = reply.text_full = "Mailbox not open";
+ reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
+
+ array_pop_front(&conn->cmd_send_queue);
+ imapc_command_reply_free(cmd, &reply);
+ imapc_command_send_more(conn);
+ return;
+ }
+
+ seek_pos = cmd->send_pos;
+ if (seek_pos != 0 && ret == -2) {
+ /* skip over the literal. we can also get here from
+ AUTHENTICATE command, which doesn't use a literal */
+ if (parse_sync_literal(cmd->data->data, seek_pos, &size)) {
+ seek_pos += size;
+ i_assert(seek_pos <= cmd->data->used);
+ }
+ }
+
+ do {
+ start_pos = seek_pos;
+ p = memchr(CONST_PTR_OFFSET(cmd->data->data, seek_pos), '\n',
+ cmd->data->used - seek_pos);
+ i_assert(p != NULL);
+
+ seek_pos = p - (const unsigned char *)cmd->data->data + 1;
+ /* keep going for LITERAL+ command */
+ } while (start_pos + 3 < seek_pos &&
+ p[-1] == '\r' && p[-2] == '}' && p[-3] == '+');
+ end_pos = seek_pos;
+
+ data = CONST_PTR_OFFSET(cmd->data->data, cmd->send_pos);
+ size = end_pos - cmd->send_pos;
+ o_stream_nsend(conn->output, data, size);
+ cmd->send_pos = end_pos;
+
+ if (cmd->send_pos == cmd->data->used) {
+ i_assert(!array_is_created(&cmd->streams) ||
+ array_count(&cmd->streams) == 0);
+ imapc_command_send_finished(conn, cmd);
+ } else {
+ cmd->wait_for_literal = TRUE;
+ }
+}
+
+static void imapc_connection_send_idle_done(struct imapc_connection *conn)
+{
+ if ((conn->idling || conn->idle_plus_waiting) && !conn->idle_stopping) {
+ conn->idle_stopping = TRUE;
+ o_stream_nsend_str(conn->output, "DONE\r\n");
+ if (conn->to == NULL) {
+ conn->to = timeout_add(conn->client->set.cmd_timeout_msecs,
+ imapc_command_timeout, conn);
+ }
+ }
+}
+
+static void imapc_connection_cmd_send(struct imapc_command *cmd)
+{
+ struct imapc_connection *conn = cmd->conn;
+ struct imapc_command *const *cmds;
+ unsigned int i, count;
+
+ imapc_connection_send_idle_done(conn);
+
+ i_assert((cmd->flags & IMAPC_COMMAND_FLAG_RECONNECTED) == 0);
+
+ if ((cmd->flags & IMAPC_COMMAND_FLAG_PRELOGIN) != 0 &&
+ conn->state == IMAPC_CONNECTION_STATE_AUTHENTICATING) {
+ /* pre-login commands get inserted before everything else */
+ array_push_front(&conn->cmd_send_queue, &cmd);
+ imapc_command_send_more(conn);
+ return;
+ }
+
+ /* add the command just before retried commands */
+ cmds = array_get(&conn->cmd_send_queue, &count);
+ for (i = count; i > 0; i--) {
+ if ((cmds[i-1]->flags & IMAPC_COMMAND_FLAG_RECONNECTED) == 0)
+ break;
+ }
+ array_insert(&conn->cmd_send_queue, i, &cmd, 1);
+ imapc_command_send_more(conn);
+}
+
+static int imapc_connection_output(struct imapc_connection *conn)
+{
+ struct imapc_command *const *cmds;
+ unsigned int count;
+ int ret;
+
+ if (conn->to != NULL)
+ timeout_reset(conn->to);
+
+ if ((ret = o_stream_flush(conn->output)) < 0)
+ return 1;
+
+ imapc_connection_ref(conn);
+ cmds = array_get(&conn->cmd_send_queue, &count);
+ if (count > 0) {
+ if (imapc_command_get_sending_stream(cmds[0]) != NULL &&
+ !cmds[0]->wait_for_literal) {
+ /* we're sending a stream. send more. */
+ imapc_command_send_more(conn);
+ }
+ }
+ imapc_connection_unref(&conn);
+ return ret;
+}
+
+struct imapc_command *
+imapc_connection_cmd(struct imapc_connection *conn,
+ imapc_command_callback_t *callback, void *context)
+{
+ struct imapc_command *cmd;
+
+ cmd = imapc_command_begin(callback, context);
+ cmd->conn = conn;
+ return cmd;
+}
+
+void imapc_command_set_flags(struct imapc_command *cmd,
+ enum imapc_command_flags flags)
+{
+ cmd->flags = flags;
+}
+
+void imapc_command_set_mailbox(struct imapc_command *cmd,
+ struct imapc_client_mailbox *box)
+{
+ cmd->box = box;
+}
+
+bool imapc_command_connection_is_selected(struct imapc_command *cmd)
+{
+ return cmd->conn->selected_box != NULL ||
+ cmd->conn->qresync_selecting_box != NULL;
+}
+
+void imapc_command_send(struct imapc_command *cmd, const char *cmd_str)
+{
+ size_t len = strlen(cmd_str);
+
+ cmd->data = str_new(cmd->pool, 6 + len + 2);
+ str_printfa(cmd->data, "%u %s\r\n", cmd->tag, cmd_str);
+ imapc_connection_cmd_send(cmd);
+}
+
+void imapc_command_sendf(struct imapc_command *cmd, const char *cmd_fmt, ...)
+{
+ va_list args;
+
+ va_start(args, cmd_fmt);
+ imapc_command_sendvf(cmd, cmd_fmt, args);
+ va_end(args);
+}
+
+void imapc_command_sendvf(struct imapc_command *cmd,
+ const char *cmd_fmt, va_list args)
+{
+ unsigned int i;
+
+ cmd->data = str_new(cmd->pool, 128);
+ str_printfa(cmd->data, "%u ", cmd->tag);
+
+ for (i = 0; cmd_fmt[i] != '\0'; i++) {
+ if (cmd_fmt[i] != '%') {
+ str_append_c(cmd->data, cmd_fmt[i]);
+ continue;
+ }
+
+ switch (cmd_fmt[++i]) {
+ case '\0':
+ i_unreached();
+ case 'u': {
+ unsigned int arg = va_arg(args, unsigned int);
+
+ str_printfa(cmd->data, "%u", arg);
+ break;
+ }
+ case 'p': {
+ struct istream *input = va_arg(args, struct istream *);
+ struct imapc_command_stream *s;
+ uoff_t size;
+
+ if (!array_is_created(&cmd->streams))
+ p_array_init(&cmd->streams, cmd->pool, 2);
+ if (i_stream_get_size(input, TRUE, &size) < 0)
+ size = 0;
+ str_printfa(cmd->data, "{%"PRIuUOFF_T"}\r\n", size);
+ s = array_append_space(&cmd->streams);
+ s->pos = str_len(cmd->data);
+ s->size = size;
+ s->input = input;
+ i_stream_ref(input);
+ break;
+ }
+ case 's': {
+ const char *arg = va_arg(args, const char *);
+
+ if (!need_literal(arg))
+ imap_append_quoted(cmd->data, arg);
+ else if ((cmd->conn->capabilities &
+ IMAPC_CAPABILITY_LITERALPLUS) != 0) {
+ str_printfa(cmd->data, "{%zu+}\r\n%s",
+ strlen(arg), arg);
+ } else {
+ str_printfa(cmd->data, "{%zu}\r\n%s",
+ strlen(arg), arg);
+ }
+ break;
+ }
+ case '1': {
+ /* %1s - no quoting */
+ const char *arg = va_arg(args, const char *);
+
+ i++;
+ i_assert(cmd_fmt[i] == 's');
+ str_append(cmd->data, arg);
+ break;
+ }
+ }
+ }
+ str_append(cmd->data, "\r\n");
+
+ imapc_connection_cmd_send(cmd);
+}
+
+enum imapc_connection_state
+imapc_connection_get_state(struct imapc_connection *conn)
+{
+ return conn->state;
+}
+
+enum imapc_capability
+imapc_connection_get_capabilities(struct imapc_connection *conn)
+{
+ return conn->capabilities;
+}
+
+void imapc_connection_unselect(struct imapc_client_mailbox *box,
+ bool via_tagged_reply)
+{
+ struct imapc_connection *conn = box->conn;
+
+ if (conn->select_waiting_reply) {
+ /* Mailbox closing was requested before SELECT/EXAMINE
+ replied. The connection state is now unknown and
+ shouldn't be used anymore. */
+ imapc_connection_disconnect(conn);
+ } else if (conn->qresync_selecting_box == NULL &&
+ conn->selected_box == NULL) {
+ /* There is no mailbox selected currently. */
+ i_assert(!via_tagged_reply);
+ } else {
+ /* Mailbox was closed in a known state. Either due to
+ SELECT/EXAMINE failing (via_tagged_reply) or by
+ imapc-storage after the mailbox was already fully
+ selected. */
+ i_assert(conn->qresync_selecting_box == box ||
+ conn->selected_box == box);
+ conn->qresync_selecting_box = NULL;
+ conn->selected_box = NULL;
+ if (via_tagged_reply)
+ conn->selected_on_server = FALSE;
+ else {
+ /* We didn't actually send UNSELECT command, so don't
+ touch selected_on_server state. */
+ }
+ }
+
+ imapc_connection_send_idle_done(conn);
+ imapc_connection_abort_commands(conn, box, FALSE);
+}
+
+struct imapc_client_mailbox *
+imapc_connection_get_mailbox(struct imapc_connection *conn)
+{
+ if (conn->qresync_selecting_box != NULL)
+ return conn->qresync_selecting_box;
+ return conn->selected_box;
+}
+
+static void
+imapc_connection_idle_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+
+ conn->idling = FALSE;
+ conn->idle_plus_waiting = FALSE;
+ conn->idle_stopping = FALSE;
+}
+
+void imapc_connection_idle(struct imapc_connection *conn)
+{
+ struct imapc_command *cmd;
+
+ if (array_count(&conn->cmd_send_queue) != 0 ||
+ array_count(&conn->cmd_wait_list) != 0 ||
+ conn->idling || conn->idle_plus_waiting ||
+ (conn->capabilities & IMAPC_CAPABILITY_IDLE) == 0)
+ return;
+
+ cmd = imapc_connection_cmd(conn, imapc_connection_idle_callback, conn);
+ cmd->idle = TRUE;
+ imapc_command_send(cmd, "IDLE");
+}