imapc-connection.c revision 78aafd062427cb22e9cf57ff08ced5b08baf55b3
/* Copyright (c) 2011-2017 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
/* If LOGOUT reply takes longer than this, disconnect. */
#define IMAPC_LOGOUT_TIMEOUT_MSECS 5000
enum imapc_input_state {
};
struct imapc_command_stream {
unsigned int pos;
};
struct imapc_command {
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;
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;
};
struct imapc_connection_literal {
char *temp_path;
int fd;
const struct imap_arg *parent_arg;
unsigned int list_idx;
};
struct imapc_connection {
struct imapc_client *client;
char *name;
int refcount;
int fd;
struct imap_parser *parser;
struct dns_lookup *dns_lookup;
struct dsasl_client *sasl_client;
struct ssl_iostream *ssl_iostream;
enum imapc_input_state input_state;
unsigned int cur_tag;
struct timeval last_connect;
unsigned int reconnect_count;
enum imapc_connection_state state;
char *disconnect_reason;
enum imapc_capability capabilities;
char **capabilities_list;
void *login_context;
/* commands pending in queue to be sent */
/* commands that have been sent, waiting for their tagged reply */
unsigned int reconnect_command_count;
unsigned int ips_count, prev_connect_idx;
struct imapc_connection_literal literal;
unsigned int throttle_msecs;
unsigned int throttle_shrink_msecs;
unsigned int last_successful_throttle_msecs;
bool throttle_pending;
struct timeval throttle_end_timeval;
bool reconnecting:1;
bool reconnect_waiting:1;
bool reconnect_ok:1;
bool idling:1;
bool idle_stopping:1;
bool idle_plus_waiting:1;
};
void *context);
static void
const struct imapc_command_reply *reply);
static void
{
return;
}
static void
const char *error)
{
return;
error);
}
struct imapc_connection *
void *login_context)
{
struct imapc_connection *conn;
return conn;
}
{
}
{
return;
}
{
}
{
/* 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) */
}
}
{
unsigned int i;
}
}
static void
struct imapc_client_mailbox *only_box,
bool keep_retriable)
{
unsigned int i;
for (i = 0; i < array_count(cmd_array); ) {
i++;
else if (keep_retriable &&
cmd->wait_for_literal = 0;
i++;
} else {
}
}
}
struct imapc_client_mailbox *only_box,
bool keep_retriable)
{
struct imapc_command_reply reply;
/* need to move all the waiting commands to send queue */
&conn->cmd_send_queue);
&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 */
"Disconnected from server";
}
}
static void
const struct imapc_command_reply *reply)
{
}
enum imapc_connection_state state)
{
struct imapc_command_reply reply;
switch (state) {
}
if (!conn->reconnecting) {
}
break;
default:
break;
}
}
{
struct imapc_arg_file *lfile;
i_error("imapc: close(literal file) failed: %m");
}
}
static void
{
}
}
bool reconnecting)
{
/* timeout may be set also in disconnected state */
return;
}
/* get capabilities again after reconnection. this is especially
important because post-login capabilities often do not contain AUTH=
capabilities. */
conn->capabilities = 0;
}
if (!reconnecting) {
}
}
{
}
{
}
{
}
{
return FALSE;
return FALSE;
else {
return conn->reconnect_command_count == 0 &&
}
}
{
/* if we fail again, avoid reconnecting immediately. if the
server is broken we could just get into an infinitely
failing reconnection loop. */
}
}
const char *errstr,
unsigned int delay_msecs)
{
return;
}
if (!imapc_connection_can_reconnect(conn)) {
} else {
if (delay_msecs == 0)
else {
conn->reconnect_count++;
}
}
}
const char *fmt, ...)
{
i_error("imapc(%s): Server sent invalid input: %s",
}
const struct imap_arg **parent_arg_r,
unsigned int *idx_r)
{
const char *name;
unsigned int count;
return TRUE;
}
return FALSE;
}
static int
{
const char *path;
const struct imap_arg *parent_arg;
unsigned int idx;
if (size <= IMAPC_MAX_INLINE_LITERAL_SIZE ||
/* read the literal directly into parser */
return 0;
}
return -1;
return 1;
}
{
struct imapc_arg_file *lfile;
const unsigned char *data;
return 1;
if (size > 0) {
i_error("imapc(%s): write(%s) failed: %m",
return -1;
}
}
return 0;
/* finished */
return 1;
}
static int
const struct imap_arg **imap_args_r)
{
int ret;
return ret;
if (ret == -2) {
/* need more data */
return 0;
}
if (ret < 0) {
return -1;
}
*imap_args_r) <= 0) {
return 2;
}
}
return 1;
}
static int
const struct imap_arg **imap_args_r)
{
const unsigned char *data;
int ret;
;
if (ret > 0) {
else
i_panic("imapc: Missing LF from input line");
}
return ret;
}
static int
const char *value)
{
const char *const *tmp;
unsigned int i;
i_debug("imapc(%s): Server capabilities: %s",
}
conn->capabilities = 0;
const struct imapc_capability_name *cap =
break;
}
}
}
"CAPABILITY list is missing IMAP4REV1");
return -1;
}
return 0;
}
static int
{
return -1;
}
/* QRESYNC: SELECTing another mailbox */
}
}
return 0;
}
static int
const char *text,
{
const char *p, *value;
if (p == NULL) {
return -1;
}
} else {
*value_r = "";
}
}
static int
{
const char *text;
return 0;
if (*text != '[') {
if (*text == '\0') {
"Missing text in resp-text");
return -1;
}
return 0;
}
}
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
const struct imapc_command_reply *reply)
{
return;
}
}
void *context)
{
}
static void
void *context)
{
struct imapc_command *cmd;
conn);
} else {
}
}
static void
void *context)
{
const unsigned char *sasl_output;
const char *error;
return;
}
t_strdup_printf("Server sent non-base64 input for AUTHENTICATE: %s",
&sasl_output_len, &error) < 0) {
} else {
return;
}
}
const char *mech_name)
{
char *const *capa;
return TRUE;
}
return FALSE;
}
static int
const struct dsasl_client_mech **mech_r,
const char **error_r)
{
const char *const *mechanisms =
/* find one of the specified SASL mechanisms */
return 0;
"Support for SASL method '%s' is missing", *mechanisms);
return -1;
}
}
return -1;
}
{
struct imapc_command *cmd;
struct dsasl_client_settings sasl_set;
const char *error;
i_debug("imapc(%s): Authenticating as %s",
} else {
i_debug("imapc(%s): Authenticating as %s for user %s",
}
}
struct imapc_command_reply reply;
return;
}
}
/* We can use LOGIN command */
conn);
return;
}
/* We can use LOGIN command */
conn);
return;
}
else {
}
const unsigned char *sasl_output;
const char *error;
&sasl_output_len, &error) < 0) {
i_error("imapc(%s): Failed to create initial SASL reply: %s",
return;
}
} else {
}
}
static void
void *context)
{
struct imapc_command *cmd;
return;
}
if (imapc_connection_ssl_init(conn) < 0)
else {
/* get updated capabilities */
conn);
}
}
{
struct imapc_command *cmd;
i_error("imapc(%s): Requested STARTTLS, "
"but server doesn't support it",
return;
}
conn);
return;
}
}
static void
void *context)
{
} else if (conn->capabilities == 0) {
"Capabilities not returned by server");
} else {
}
}
{
struct imapc_command *cmd;
int ret;
return ret;
/* we already verified that the banner beigns with OK */
imap_args++;
return -1;
if (conn->capabilities == 0) {
/* capabilities weren't sent in the banner. ask for them. */
conn);
} else {
}
return 1;
}
{
const unsigned char *data;
struct imap_parser *parser;
struct imapc_untagged_reply reply;
int ret;
/* input banner */
return 0;
"Banner doesn't begin with OK: %s",
return -1;
}
return 1;
}
return ret;
return -1;
}
imap_args++;
/* <seq> <event> */
"Invalid untagged reply");
return -1;
}
imap_args++;
}
&reply.resp_text_value) < 0)
return -1;
return -1;
}
}
/* the callback may disconnect and destroy the parser */
return 1;
}
{
struct imapc_command *const *cmds;
unsigned int cmds_count;
const char *line;
return 0;
if (conn->idle_plus_waiting) {
/* "+ idling" reply for IDLE command */
/* no timeouting while IDLEing */
/* reply for literal */
} else {
/* continue AUTHENTICATE */
struct imapc_command_reply reply;
} else {
return -1;
}
}
return 1;
}
static void
{
conn->throttle_msecs = 0;
else
conn->throttle_shrink_msecs = 0;
else
if (conn->throttle_shrink_msecs > 0) {
}
}
static void
const struct imapc_command_reply *reply)
{
/* 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 (conn->throttle_msecs == 0)
else {
}
if (conn->throttle_shrink_msecs == 0)
else
} else {
if (conn->throttle_shrink_msecs > 0 &&
}
}
if (conn->throttle_msecs > 0) {
}
}
static void
const struct imapc_command_reply *reply)
{
}
{
unsigned int i, count;
const char *p;
struct imapc_command_reply reply;
return 0;
/* make sure reply texts stays valid if input stream gets freed */
else {
*linep = '\0';
}
else {
"Invalid state in tagged reply: %u %s %s",
return -1;
}
/* get resp-text */
&reply.resp_text_value) < 0)
return -1;
reply.text_without_resp = p;
} else {
}
/* if we've pipelined multiple commands, handle [THROTTLED] reply
from only one of them */
if (!conn->throttle_pending)
/* find the command. it's either the first command in send queue
(literal failed) or somewhere in wait list. */
} else {
for (i = 0; i < count; i++) {
break;
}
}
}
"Unknown tag in a reply: %u %s %s",
return -1;
}
i_error("imapc(%s): Command '%s' failed with BAD: %u %s",
}
}
if (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->reconnect_command_count == 0) {
/* we've successfully received replies to some commands. */
}
return 1;
}
{
const char *tag;
int ret = -1;
switch (conn->input_state) {
case IMAPC_INPUT_STATE_NONE:
return 0;
} else {
"Invalid command tag: %s", tag);
ret = -1;
} else {
}
}
break;
case IMAPC_INPUT_STATE_PLUS:
break;
break;
case IMAPC_INPUT_STATE_TAGGED:
break;
}
return ret;
}
{
const char *errstr;
/* we need to read as much as we can with SSL streams to avoid
hanging */
/* expected disconnection */
} else if (ret < 0) {
/* disconnected or buffer full */
} else if (ret == -2) {
errstr);
} else {
}
errstr);
}
}
}
{
const char *error;
i_debug("imapc(%s): SSL handshake successful",
}
return 0;
i_debug("imapc(%s): SSL handshake successful, "
"ignoring invalid certificate: %s",
}
return 0;
} else {
return -1;
}
}
{
struct ssl_iostream_settings ssl_set;
const char *error;
return -1;
}
} else {
}
/* recreate rawlog after STARTTLS */
}
i_error("imapc(%s): Couldn't initialize SSL client: %s",
return -1;
}
conn);
return -1;
}
}
return 0;
}
{
int err;
if (err != 0) {
"connect(%s, %u) failed: %s",
return;
}
if (imapc_connection_ssl_init(conn) < 0)
}
}
{
const char *errstr;
break;
break;
default:
i_unreached();
}
}
static void
void *context ATTR_UNUSED)
{
}
static void
void *context)
{
}
{
struct imapc_command *cmd;
else {
/* IMAP command reply is taking a long time */
return;
}
}
{
unsigned int i;
int fd;
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",
return;
}
}
}
conn);
}
}
static void
struct imapc_connection *conn)
{
i_error("imapc(%s): dns_lookup(%s) failed: %s",
return;
}
}
{
struct dns_lookup_settings dns_set;
unsigned int ips_count;
int ret;
return;
if (conn->reconnect_waiting) {
/* wait for the reconnection delay to finish before
doing anything. */
return;
}
/* if we get disconnected before we've finished all the pending
commands, don't reconnect */
i_debug("imapc(%s): Looking up IP address "
}
/* do nothing */
if (ret != 0) {
i_error("imapc(%s): net_gethostbyname(%s) failed: %s",
return;
}
} else {
&conn->dns_lookup);
return;
}
}
{
int ret = 1;
return;
T_BEGIN {
} T_END;
}
}
static struct imapc_command *
{
struct imapc_command *cmd;
/* use a globally unique tag counter so looking at rawlogs is
somewhat easier */
if (++imapc_client_cmd_tag_counter == 0)
return cmd;
}
{
struct imapc_command_stream *stream;
}
}
{
}
{
}
{
struct imapc_command *const *cmds;
unsigned int count;
}
static bool
unsigned int *value_r)
{
/* data should contain "{size}\r\n" and pos points after \n */
return FALSE;
pos -= 4;
do {
pos--;
return FALSE;
return TRUE;
}
struct imapc_command *cmd)
{
struct imapc_command *const *cmdp;
/* everything sent. move command to wait list. */
/* send the next command in queue */
}
static struct imapc_command_stream *
{
struct imapc_command_stream *stream;
return NULL;
return NULL;
return stream;
}
struct imapc_command *cmd)
{
struct imapc_command_stream *stream;
return -2;
/* we're sending the stream now */
switch (res) {
break;
i_unreached();
return 0;
i_error("imapc: read(%s) failed: %s",
return -1;
/* disconnected */
return -1;
}
/* finished with the stream */
return 1;
}
{
/* server will send a [CLOSED] once selected mailbox is
closed */
} else {
/* we'll have to assume that all the future untagged messages
are for the mailbox we're selecting */
}
}
{
if (conn->throttle_msecs == 0) {
/* we haven't received [THROTTLED] recently */
return FALSE;
}
/* wait until we have received the existing commands' tagged
replies to see if we're still throttled */
return TRUE;
}
/* we reached the throttle timeout - send the next command */
return FALSE;
}
/* we're still being throttled - wait for it to end */
return TRUE;
}
{
struct imapc_command_reply reply;
const unsigned char *p, *data;
int ret;
return;
if (count == 0)
return;
/* wait until we're fully connected */
return;
}
/* wait until existing commands have finished */
return;
}
if (cmd->wait_for_literal) {
/* wait until we received '+' */
return;
}
/* non-mailbox command */
return;
}
/* shouldn't normally happen */
return;
}
/* add timeout for commands if there's not one yet
(pre-login has its own timeout) */
/* LOGOUT has a shorter timeout */
}
return;
if (ret == -1) {
return;
}
/* skip over the literal. we can also get here from
AUTHENTICATE command, which doesn't use a literal */
}
}
do {
/* keep going for LITERAL+ command */
p[-1] == '\r' && p[-2] == '}' && p[-3] == '+');
} else {
}
}
{
}
}
}
{
/* pre-login commands get inserted before everything else */
return;
}
/* reopening the mailbox. add it before other
queued commands. */
} else {
}
}
{
struct imapc_command *const *cmds;
unsigned int count;
int ret;
return 1;
if (count > 0) {
!cmds[0]->wait_for_literal) {
/* we're sending a stream. send more. */
}
}
return ret;
}
struct imapc_command *
{
struct imapc_command *cmd;
return cmd;
}
enum imapc_command_flags flags)
{
}
struct imapc_client_mailbox *box)
{
}
{
}
{
}
{
}
{
unsigned int i;
for (i = 0; cmd_fmt[i] != '\0'; i++) {
if (cmd_fmt[i] != '%') {
continue;
}
switch (cmd_fmt[++i]) {
case '\0':
i_unreached();
case 'u': {
break;
}
case 'p': {
struct imapc_command_stream *s;
size = 0;
break;
}
case 's': {
if (!need_literal(arg))
IMAPC_CAPABILITY_LITERALPLUS) != 0) {
} else {
}
break;
}
case '1': {
/* %1s - no quoting */
i++;
break;
}
}
}
}
{
}
enum imapc_capability
{
return conn->capabilities;
}
{
}
}
struct imapc_client_mailbox *
{
return conn->selecting_box;
return conn->selected_box;
}
static void
void *context)
{
}
{
struct imapc_command *cmd;
return;
}