diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:36:47 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:36:47 +0000 |
commit | 0441d265f2bb9da249c7abf333f0f771fadb4ab5 (patch) | |
tree | 3f3789daa2f6db22da6e55e92bee0062a7d613fe /src/lib-smtp/smtp-client-connection.c | |
parent | Initial commit. (diff) | |
download | dovecot-0441d265f2bb9da249c7abf333f0f771fadb4ab5.tar.xz dovecot-0441d265f2bb9da249c7abf333f0f771fadb4ab5.zip |
Adding upstream version 1:2.3.21+dfsg1.upstream/1%2.3.21+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/lib-smtp/smtp-client-connection.c')
-rw-r--r-- | src/lib-smtp/smtp-client-connection.c | 2522 |
1 files changed, 2522 insertions, 0 deletions
diff --git a/src/lib-smtp/smtp-client-connection.c b/src/lib-smtp/smtp-client-connection.c new file mode 100644 index 0000000..47862ad --- /dev/null +++ b/src/lib-smtp/smtp-client-connection.c @@ -0,0 +1,2522 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "llist.h" +#include "array.h" +#include "safe-memset.h" +#include "ioloop.h" +#include "net.h" +#include "base64.h" +#include "istream.h" +#include "ostream.h" +#include "ostream-dot.h" +#include "iostream-rawlog.h" +#include "iostream-ssl.h" +#include "str.h" +#include "dsasl-client.h" +#include "dns-lookup.h" +#include "smtp-syntax.h" +#include "smtp-reply-parser.h" +#include "smtp-client-private.h" + +#include <ctype.h> + +#define SMTP_CLIENT_ERROR_TEXT_CONNECT_FAILED \ + "Failed to connect to remote server" + +const char *const smtp_client_connection_state_names[] = { + "disconnected", + "connecting", + "handshaking", + "authenticating", + "ready", + "transaction" +}; + +static int +smtp_client_connection_ssl_init(struct smtp_client_connection *conn, + const char **error_r); +static void +smtp_client_connection_handshake(struct smtp_client_connection *conn); +static void +smtp_client_connection_established(struct smtp_client_connection *conn); +static void +smtp_client_connection_start_transaction(struct smtp_client_connection *conn); +static void +smtp_client_connection_connect_next_ip(struct smtp_client_connection *conn); +static bool +smtp_client_connection_last_ip(struct smtp_client_connection *conn); + +/* + * Capabilities + */ + +enum smtp_capability +smtp_client_connection_get_capabilities(struct smtp_client_connection *conn) +{ + return conn->caps.standard; +} + +uoff_t smtp_client_connection_get_size_capability( + struct smtp_client_connection *conn) +{ + return conn->caps.size; +} + +static const struct smtp_client_capability_extra * +smtp_client_connection_find_extra_capability( + struct smtp_client_connection *conn, const char *cap_name) +{ + const struct smtp_client_capability_extra *cap; + + if (!array_is_created(&conn->extra_capabilities)) + return NULL; + array_foreach(&conn->extra_capabilities, cap) { + if (strcasecmp(cap->name, cap_name) == 0) + return cap; + } + return NULL; +} + +void smtp_client_connection_accept_extra_capability( + struct smtp_client_connection *conn, + const struct smtp_client_capability_extra *cap) +{ + i_assert(smtp_client_connection_find_extra_capability(conn, cap->name) + == NULL); + + if (!array_is_created(&conn->extra_capabilities)) + p_array_init(&conn->extra_capabilities, conn->pool, 8); + + struct smtp_client_capability_extra cap_new = { + .name = p_strdup(conn->pool, cap->name), + }; + + if (cap->mail_param_extensions != NULL) { + cap_new.mail_param_extensions = + p_strarray_dup(conn->pool, cap->mail_param_extensions); + } + if (cap->rcpt_param_extensions != NULL) { + cap_new.rcpt_param_extensions = + p_strarray_dup(conn->pool, cap->rcpt_param_extensions); + } + + array_push_back(&conn->extra_capabilities, &cap_new); +} + +const struct smtp_capability_extra * +smtp_client_connection_get_extra_capability(struct smtp_client_connection *conn, + const char *name) +{ + const struct smtp_capability_extra *cap; + + if (!array_is_created(&conn->caps.extra)) + return NULL; + + array_foreach(&conn->caps.extra, cap) { + if (strcasecmp(cap->name, name) == 0) + return cap; + } + + return NULL; +} + +/* + * + */ + +static void +smtp_client_connection_commands_abort(struct smtp_client_connection *conn) +{ + smtp_client_commands_list_abort(conn->cmd_wait_list_head, + conn->cmd_wait_list_count); + smtp_client_commands_list_abort(conn->cmd_send_queue_head, + conn->cmd_send_queue_count); + smtp_client_commands_abort_delayed(conn); +} + +static void +smtp_client_connection_commands_fail_reply(struct smtp_client_connection *conn, + const struct smtp_reply *reply) +{ + smtp_client_commands_list_fail_reply(conn->cmd_wait_list_head, + conn->cmd_wait_list_count, reply); + smtp_client_commands_list_fail_reply(conn->cmd_send_queue_head, + conn->cmd_send_queue_count, reply); + smtp_client_commands_fail_delayed(conn); +} + +static void +smtp_client_connection_commands_fail(struct smtp_client_connection *conn, + unsigned int status, const char *error) +{ + struct smtp_reply reply; + + smtp_reply_init(&reply, status, error); + reply.enhanced_code.x = 9; + + smtp_client_connection_commands_fail_reply(conn, &reply); +} + +static void +smtp_client_connection_transactions_abort(struct smtp_client_connection *conn) +{ + struct smtp_client_transaction *trans; + + trans = conn->transactions_head; + while (trans != NULL) { + struct smtp_client_transaction *trans_next = trans->next; + smtp_client_transaction_abort(trans); + trans = trans_next; + } +} + +static void +smtp_client_connection_transactions_fail_reply( + struct smtp_client_connection *conn, const struct smtp_reply *reply) +{ + struct smtp_client_transaction *trans; + + trans = conn->transactions_head; + while (trans != NULL) { + struct smtp_client_transaction *trans_next = trans->next; + smtp_client_transaction_connection_result(trans, reply); + trans = trans_next; + } +} + +static void +smtp_client_connection_transactions_fail(struct smtp_client_connection *conn, + unsigned int status, const char *error) +{ + struct smtp_reply reply; + + smtp_reply_init(&reply, status, error); + reply.enhanced_code.x = 9; + + smtp_client_connection_transactions_fail_reply(conn, &reply); +} + +static void +smtp_client_connection_transactions_drop(struct smtp_client_connection *conn) +{ + struct smtp_client_transaction *trans; + + trans = conn->transactions_head; + while (trans != NULL) { + struct smtp_client_transaction *trans_next = trans->next; + smtp_client_transaction_connection_destroyed(trans); + trans = trans_next; + } +} + +static void +smtp_client_connection_login_callback(struct smtp_client_connection *conn, + const struct smtp_reply *reply) +{ + const struct smtp_client_login_callback *cb; + ARRAY(struct smtp_client_login_callback) login_cbs; + + if (conn->state_data.login_reply == NULL) { + conn->state_data.login_reply = + smtp_reply_clone(conn->state_pool, reply); + } + + if (!array_is_created(&conn->login_callbacks) || + array_count(&conn->login_callbacks) == 0) + return; + + t_array_init(&login_cbs, array_count(&conn->login_callbacks)); + array_copy(&login_cbs.arr, 0, &conn->login_callbacks.arr, 0, + array_count(&conn->login_callbacks)); + array_foreach(&login_cbs, cb) { + i_assert(cb->callback != NULL); + if (conn->closed) + break; + if (cb->callback != NULL) + cb->callback(reply, cb->context); + } + array_clear(&conn->login_callbacks); +} + +static void +smtp_client_connection_login_fail(struct smtp_client_connection *conn, + unsigned int status, const char *error) +{ + struct smtp_reply reply; + + smtp_reply_init(&reply, status, error); + reply.enhanced_code.x = 9; + + smtp_client_connection_login_callback(conn, &reply); +} + +static void +smtp_client_connection_set_state(struct smtp_client_connection *conn, + enum smtp_client_connection_state state) +{ + conn->state = state; +} + +void smtp_client_connection_cork(struct smtp_client_connection *conn) +{ + conn->corked = TRUE; + if (conn->conn.output != NULL) + o_stream_cork(conn->conn.output); +} + +void smtp_client_connection_uncork(struct smtp_client_connection *conn) +{ + conn->corked = FALSE; + if (conn->conn.output != NULL) { + if (o_stream_uncork_flush(conn->conn.output) < 0) { + smtp_client_connection_handle_output_error(conn); + return; + } + smtp_client_connection_trigger_output(conn); + } +} + +enum smtp_client_connection_state +smtp_client_connection_get_state(struct smtp_client_connection *conn) +{ + return conn->state; +} + +static void +smtp_client_command_timeout(struct smtp_client_connection *conn) +{ + smtp_client_connection_ref(conn); + + smtp_client_connection_fail(conn, SMTP_CLIENT_COMMAND_ERROR_TIMED_OUT, + "Command timed out, disconnecting", + "Command timed out"); + smtp_client_connection_unref(&conn); +} + +void smtp_client_connection_start_cmd_timeout( + struct smtp_client_connection *conn) +{ + unsigned int msecs = conn->set.command_timeout_msecs; + + if (conn->state < SMTP_CLIENT_CONNECTION_STATE_READY) { + /* pre-login uses connect timeout */ + return; + } + if (msecs == 0) { + /* no timeout configured */ + timeout_remove(&conn->to_commands); + return; + } + if (conn->cmd_wait_list_head == NULL && !conn->sending_command) { + /* no commands pending */ + timeout_remove(&conn->to_commands); + return; + } + + e_debug(conn->event, "Start timeout"); + if (conn->to_commands == NULL) { + conn->to_commands = timeout_add( + msecs, smtp_client_command_timeout, conn); + } +} + +void smtp_client_connection_update_cmd_timeout( + struct smtp_client_connection *conn) +{ + unsigned int msecs = conn->set.command_timeout_msecs; + + if (conn->state < SMTP_CLIENT_CONNECTION_STATE_READY) { + /* pre-login uses connect timeout */ + return; + } + if (msecs == 0) { + /* no timeout configured */ + timeout_remove(&conn->to_commands); + return; + } + + if (conn->cmd_wait_list_head == NULL && !conn->sending_command) { + if (conn->to_commands != NULL) { + e_debug(conn->event, + "No commands pending; stop timeout"); + } + timeout_remove(&conn->to_commands); + } else if (conn->to_commands != NULL) { + e_debug(conn->event, "Reset timeout"); + timeout_reset(conn->to_commands); + } else { + smtp_client_connection_start_cmd_timeout(conn); + } +} + +static void +smtp_client_connection_fail_reply(struct smtp_client_connection *conn, + const struct smtp_reply *reply) +{ + e_debug(conn->event, "Connection failed: %s", smtp_reply_log(reply)); + + smtp_client_connection_ref(conn); + conn->failing = TRUE; + + smtp_client_connection_disconnect(conn); + smtp_client_connection_login_callback(conn, reply); + + smtp_client_connection_transactions_fail_reply(conn, reply); + smtp_client_connection_commands_fail_reply(conn, reply); + + conn->failing = FALSE; + smtp_client_connection_unref(&conn); +} + +void smtp_client_connection_fail(struct smtp_client_connection *conn, + unsigned int status, const char *error, + const char *user_error) +{ + struct smtp_reply reply; + const char *text_lines[2]; + + if (error != NULL) + e_error(conn->event, "%s", error); + + i_zero(&text_lines); + i_assert(user_error != NULL); + if (conn->set.verbose_user_errors && error != NULL) + text_lines[0] = error; + else + text_lines[0] = user_error; + + timeout_remove(&conn->to_connect); + + if (status == SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED && + !smtp_client_connection_last_ip(conn)) { + conn->to_connect = timeout_add_short( + 0, smtp_client_connection_connect_next_ip, conn); + return; + } + + i_zero(&reply); + reply.status = status; + reply.text_lines = text_lines; + reply.enhanced_code.x = 9; + + smtp_client_connection_fail_reply(conn, &reply); +} + +static void +smtp_client_connection_fail_equal(struct smtp_client_connection *conn, + unsigned int status, const char *error) +{ + smtp_client_connection_fail(conn, status, error, error); +} + +static void +smtp_client_connection_lost(struct smtp_client_connection *conn, + const char *error, const char *user_error) +{ + if (error != NULL) + error = t_strdup_printf("Connection lost: %s", error); + + if (user_error == NULL) + user_error = "Lost connection to remote server"; + else { + user_error = t_strdup_printf( + "Lost connection to remote server: %s", + user_error); + } + + if (conn->ssl_iostream != NULL) { + const char *sslerr = + ssl_iostream_get_last_error(conn->ssl_iostream); + + if (error != NULL && sslerr != NULL) { + error = t_strdup_printf("%s (last SSL error: %s)", + error, sslerr); + } else if (sslerr != NULL) { + error = t_strdup_printf( + "Connection lost (last SSL error: %s)", sslerr); + } + if (ssl_iostream_has_handshake_failed(conn->ssl_iostream)) { + /* This isn't really a "connection lost", but that we + don't trust the remote's SSL certificate. */ + i_assert(error != NULL); + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED, + error, user_error); + return; + } + } + + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST, + error, user_error); +} + +void smtp_client_connection_handle_output_error( + struct smtp_client_connection *conn) +{ + struct ostream *output = conn->conn.output; + + if (output->stream_errno != EPIPE && + output->stream_errno != ECONNRESET) { + smtp_client_connection_lost( + conn, + t_strdup_printf("write(%s) failed: %s", + o_stream_get_name(conn->conn.output), + o_stream_get_error(conn->conn.output)), + "Write failure"); + } else { + smtp_client_connection_lost( + conn, "Remote disconnected while writing output", + "Remote closed connection unexpectedly"); + } +} + +static void +stmp_client_connection_ready(struct smtp_client_connection *conn, + const struct smtp_reply *reply) +{ + timeout_remove(&conn->to_connect); + + smtp_client_connection_set_state( + conn, SMTP_CLIENT_CONNECTION_STATE_READY); + conn->reset_needed = FALSE; + + e_debug(conn->event, "Connection ready"); + + smtp_client_connection_login_callback(conn, reply); + + smtp_client_connection_update_cmd_timeout(conn); + + smtp_client_connection_start_transaction(conn); +} + +static void +smtp_client_connection_xclient_cb(const struct smtp_reply *reply, + struct smtp_client_connection *conn) +{ + e_debug(conn->event, "Received XCLIENT handshake reply: %s", + smtp_reply_log(reply)); + + i_assert(conn->xclient_replies_expected > 0); + + if (reply->status == 421) { + smtp_client_connection_fail_reply(conn, reply); + return; + } + if (conn->state == SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED) + return; + + if (conn->to_connect != NULL) + timeout_reset(conn->to_connect); + if (--conn->xclient_replies_expected == 0) + smtp_client_connection_handshake(conn); +} + +static void +smtp_client_connection_xclient_submit(struct smtp_client_connection *conn, + const char *cmdstr) +{ + struct smtp_client_command *cmd; + enum smtp_client_command_flags flags; + + e_debug(conn->event, "Sending XCLIENT handshake"); + + flags = SMTP_CLIENT_COMMAND_FLAG_PRELOGIN | + SMTP_CLIENT_COMMAND_FLAG_PRIORITY; + + cmd = smtp_client_command_new(conn, flags, + smtp_client_connection_xclient_cb, conn); + smtp_client_command_write(cmd, cmdstr); + smtp_client_command_submit(cmd); + + conn->xclient_replies_expected++; +} + +static void +smtp_client_connection_xclient_add(struct smtp_client_connection *conn, + string_t *str, size_t offset, + const char *field, const char *value) +{ + size_t prev_offset = str_len(str); + const char *new_field; + + i_assert(prev_offset >= offset); + + str_append_c(str, ' '); + str_append(str, field); + str_append_c(str, '='); + smtp_xtext_encode_cstr(str, value); + + if (prev_offset == offset || + str_len(str) <= SMTP_BASE_LINE_LENGTH_LIMIT) + return; + + /* preserve field we just added */ + new_field = t_strdup(str_c(str) + prev_offset); + + /* revert to previous position */ + str_truncate(str, prev_offset); + + /* send XCLIENT command */ + smtp_client_connection_xclient_submit(conn, str_c(str)); + + /* start next XCLIENT command with new field */ + str_truncate(str, offset); + str_append(str, new_field); +} + +static void ATTR_FORMAT(5, 6) +smtp_client_connection_xclient_addf(struct smtp_client_connection *conn, + string_t *str, size_t offset, + const char *field, const char *format, ...) +{ + va_list args; + + va_start(args, format); + smtp_client_connection_xclient_add(conn, str, offset, field, + t_strdup_vprintf(format, args)); + va_end(args); +} + +void smtp_client_connection_send_xclient(struct smtp_client_connection *conn) +{ + const struct smtp_proxy_data *xclient = &conn->set.proxy_data; + const char **xclient_args = conn->caps.xclient_args; + size_t offset; + string_t *str; + + if (!conn->set.peer_trusted) + return; + if (conn->xclient_sent) + return; + if ((conn->caps.standard & SMTP_CAPABILITY_XCLIENT) == 0 || + conn->caps.xclient_args == NULL) + return; + + i_assert(conn->xclient_replies_expected == 0); + + /* http://www.postfix.org/XCLIENT_README.html: + + The client must not send XCLIENT commands that exceed the 512 + character limit for SMTP commands. To avoid exceeding the limit the + client should send the information in multiple XCLIENT commands; for + example, send NAME and ADDR last, after HELO and PROTO. Once ADDR is + sent, the client is usually no longer authorized to send XCLIENT + commands. + */ + + str = t_str_new(64); + str_append(str, "XCLIENT"); + offset = str_len(str); + + /* HELO */ + if (xclient->helo != NULL && + str_array_icase_find(xclient_args, "HELO")) { + smtp_client_connection_xclient_add(conn, str, offset, + "HELO", xclient->helo); + } + + /* PROTO */ + if (str_array_icase_find(xclient_args, "PROTO")) { + switch (xclient->proto) { + case SMTP_PROXY_PROTOCOL_SMTP: + smtp_client_connection_xclient_add(conn, str, offset, + "PROTO", "SMTP"); + break; + case SMTP_PROXY_PROTOCOL_ESMTP: + smtp_client_connection_xclient_add(conn, str, offset, + "PROTO", "ESMTP"); + break; + case SMTP_PROXY_PROTOCOL_LMTP: + smtp_client_connection_xclient_add(conn, str, offset, + "PROTO", "LMTP"); + break; + default: + break; + } + } + + /* LOGIN */ + if (xclient->login != NULL && + str_array_icase_find(xclient_args, "LOGIN")) { + smtp_client_connection_xclient_add(conn, str, offset, + "LOGIN", xclient->login); + } + + /* SESSION */ + if (xclient->session != NULL && + str_array_icase_find(xclient_args, "SESSION")) { + smtp_client_connection_xclient_add(conn, str, offset, + "SESSION", xclient->session); + } + + /* TTL */ + if (xclient->ttl_plus_1 > 0 && + str_array_icase_find(xclient_args, "TTL")) { + smtp_client_connection_xclient_addf(conn, str, offset, + "TTL", "%u", + xclient->ttl_plus_1-1); + } + + /* TIMEOUT */ + if (xclient->timeout_secs > 0 && + str_array_icase_find(xclient_args, "TIMEOUT")) { + smtp_client_connection_xclient_addf(conn, str, offset, + "TIMEOUT", "%u", + xclient->timeout_secs); + } + + /* PORT */ + if (xclient->source_port != 0 && + str_array_icase_find(xclient_args, "PORT")) { + smtp_client_connection_xclient_addf(conn, str, offset, + "PORT", "%u", + xclient->source_port); + } + + /* ADDR */ + if (xclient->source_ip.family != 0 && + str_array_icase_find(xclient_args, "ADDR")) { + const char *addr = net_ip2addr(&xclient->source_ip); + + /* Older versions of Dovecot LMTP don't quite follow Postfix' + specification of the XCLIENT command regarding IPv6 + addresses: the "IPV6:" prefix is omitted. For now, we + maintain this deviation for LMTP. Newer versions of Dovecot + LMTP can work with or without the prefix. */ + if (conn->protocol != SMTP_PROTOCOL_LMTP && + xclient->source_ip.family == AF_INET6) + addr = t_strconcat("IPV6:", addr, NULL); + smtp_client_connection_xclient_add(conn, str, offset, + "ADDR", addr); + } + + /* final XCLIENT command */ + if (str_len(str) > offset) + smtp_client_connection_xclient_submit(conn, str_c(str)); + + conn->xclient_sent = TRUE; +} + +static void +smtp_client_connection_clear_password(struct smtp_client_connection *conn) +{ + if (conn->set.remember_password) + return; + if (conn->password == NULL) + return; + safe_memset(conn->password, 0, strlen(conn->password)); + conn->set.password = NULL; + conn->password = NULL; +} + +static void +smtp_client_connection_auth_deinit(struct smtp_client_connection *conn) +{ + dsasl_client_free(&conn->sasl_client); + i_free(conn->sasl_ir); +} + +static void +smtp_client_connection_auth_cb(const struct smtp_reply *reply, + struct smtp_client_connection *conn) +{ + struct smtp_client_command *cmd, *cmd_auth = conn->cmd_auth; + const char *error; + + conn->cmd_auth = NULL; + i_assert(cmd_auth != NULL); + + if (reply->status == 334) { + const unsigned char *sasl_output; + size_t sasl_output_len, input_len; + buffer_t *buf; + + if (reply->text_lines[1] != NULL) { + error = t_strdup_printf( + "Authentication failed: " + "Server returned multi-line reply: %s", + smtp_reply_log(reply)); + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED, + error, "Authentication protocol error"); + return; + } + if (conn->sasl_ir != NULL) { + if (*reply->text_lines[0] == '\0') { + /* Send initial response */ + cmd = smtp_client_command_new( + conn, SMTP_CLIENT_COMMAND_FLAG_PRELOGIN, + smtp_client_connection_auth_cb, conn); + smtp_client_command_write(cmd, conn->sasl_ir); + smtp_client_command_submit_after(cmd, cmd_auth); + conn->cmd_auth = cmd; + i_free(conn->sasl_ir); + return; + } + error = t_strdup_printf( + "Authentication failed: " + "Server sent unexpected server-first challenge: %s", + smtp_reply_log(reply)); + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED, + error, "Authentication protocol error"); + return; + } + + input_len = strlen(reply->text_lines[0]); + buf = buffer_create_dynamic(pool_datastack_create(), + MAX_BASE64_DECODED_SIZE(input_len)); + if (base64_decode(reply->text_lines[0], input_len, + NULL, buf) < 0) { + error = t_strdup_printf( + "Authentication failed: " + "Server sent non-base64 input for AUTH: %s", + reply->text_lines[0]); + } else if (dsasl_client_input(conn->sasl_client, + buf->data, buf->used, + &error) < 0) { + error = t_strdup_printf("Authentication failed: %s", + error); + } else if (dsasl_client_output(conn->sasl_client, &sasl_output, + &sasl_output_len, &error) < 0) { + error = t_strdup_printf("Authentication failed: %s", + error); + } else { + string_t *smtp_output = t_str_new( + MAX_BASE64_ENCODED_SIZE(sasl_output_len) + 2); + base64_encode(sasl_output, sasl_output_len, + smtp_output); + cmd = smtp_client_command_new( + conn, SMTP_CLIENT_COMMAND_FLAG_PRELOGIN, + smtp_client_connection_auth_cb, conn); + smtp_client_command_write(cmd, conn->sasl_ir); + smtp_client_command_submit_after(cmd, cmd_auth); + conn->cmd_auth = cmd; + return; + } + + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED, + error, "Authentication failed"); + return; + } + + if ((reply->status / 100) != 2) { + e_error(conn->event, "Authentication failed: %s", + smtp_reply_log(reply)); + smtp_client_connection_fail_reply(conn, reply); + return; + } + + smtp_client_connection_clear_password(conn); + smtp_client_connection_auth_deinit(conn); + + e_debug(conn->event, "Authenticated successfully"); + + if (conn->to_connect != NULL) + timeout_reset(conn->to_connect); + conn->authenticated = TRUE; + smtp_client_connection_handshake(conn); +} + +static int +smtp_client_connection_get_sasl_mech(struct smtp_client_connection *conn, + const struct dsasl_client_mech **mech_r, + const char **error_r) +{ + const struct smtp_client_settings *set = &conn->set; + const char *const *mechanisms; + + if (set->sasl_mech != NULL) { + const char *mech = dsasl_client_mech_get_name(set->sasl_mech); + + if (!str_array_icase_find(conn->caps.auth_mechanisms, mech)) { + *error_r = t_strdup_printf( + "Server doesn't support `%s' SASL mechanism", + mech); + return -1; + } + *mech_r = set->sasl_mech; + return 0; + } + if (set->sasl_mechanisms == NULL || + set->sasl_mechanisms[0] == '\0') { + *mech_r = &dsasl_client_mech_plain; + return 0; + } + + /* find one of the specified SASL mechanisms */ + mechanisms = t_strsplit_spaces(set->sasl_mechanisms, ", "); + for (; *mechanisms != NULL; mechanisms++) { + if (str_array_icase_find(conn->caps.auth_mechanisms, + *mechanisms)) { + *mech_r = dsasl_client_mech_find(*mechanisms); + if (*mech_r != NULL) + return 0; + + *error_r = t_strdup_printf( + "Support for SASL mechanism `%s' is missing", + *mechanisms); + return -1; + } + } + *error_r = t_strdup_printf( + "Server doesn't support any of " + "the requested SASL mechanisms: %s", set->sasl_mechanisms); + return -1; +} + +static bool +smtp_client_connection_authenticate(struct smtp_client_connection *conn) +{ + const struct smtp_client_settings *set = &conn->set; + struct dsasl_client_settings sasl_set; + const struct dsasl_client_mech *sasl_mech = NULL; + struct smtp_client_command *cmd; + const unsigned char *sasl_output; + size_t sasl_output_len; + string_t *sasl_output_base64; + const char *error; + + if (set->username == NULL && set->sasl_mech == NULL) { + if (!conn->set.xclient_defer) + smtp_client_connection_send_xclient(conn); + return (conn->xclient_replies_expected == 0); + } + + smtp_client_connection_send_xclient(conn); + if (conn->xclient_replies_expected > 0) + return FALSE; + if (conn->authenticated) + return TRUE; + + if ((conn->caps.standard & SMTP_CAPABILITY_AUTH) == 0) { + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED, + NULL, "Authentication not supported"); + return FALSE; + } + + if (set->master_user != NULL) { + e_debug(conn->event, "Authenticating as %s for user %s", + set->master_user, set->username); + } else if (set->username == NULL) { + e_debug(conn->event, "Authenticating"); + } else { + e_debug(conn->event, "Authenticating as %s", set->username); + } + + if (smtp_client_connection_get_sasl_mech(conn, &sasl_mech, + &error) < 0) { + error = t_strdup_printf("Authentication failed: %s", error); + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED, + error, "Server authentication mechanisms incompatible"); + return FALSE; + } + + 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; + + conn->sasl_client = dsasl_client_new(sasl_mech, &sasl_set); + + if (dsasl_client_output(conn->sasl_client, &sasl_output, + &sasl_output_len, &error) < 0) { + error = t_strdup_printf( + "Failed to create initial %s SASL reply: %s", + dsasl_client_mech_get_name(sasl_mech), error); + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED, + error, "Internal authentication failure"); + return FALSE; + } + + sasl_output_base64 = t_str_new( + MAX_BASE64_ENCODED_SIZE(sasl_output_len)); + base64_encode(sasl_output, sasl_output_len, sasl_output_base64); + + /* RFC 4954, Section 4: + + Note that the AUTH command is still subject to the line length + limitations defined in [SMTP]. If use of the initial response + argument would cause the AUTH command to exceed this length, the + client MUST NOT use the initial response parameter (and instead + proceed as defined in Section 5.1 of [SASL]). + + If the client is transmitting an initial response of zero length, it + MUST instead transmit the response as a single equals sign ("="). + This indicates that the response is present, but contains no data. + */ + + const char *init_resp = ""; + const char *mech_name = dsasl_client_mech_get_name(sasl_mech); + + i_assert(conn->sasl_ir == NULL); + if (str_len(sasl_output_base64) == 0) + init_resp = "="; + else if ((5 + strlen(mech_name) + 1 + str_len(sasl_output_base64)) > + SMTP_BASE_LINE_LENGTH_LIMIT) + conn->sasl_ir = i_strdup(str_c(sasl_output_base64)); + else + init_resp = str_c(sasl_output_base64); + + cmd = smtp_client_command_new(conn, SMTP_CLIENT_COMMAND_FLAG_PRELOGIN, + smtp_client_connection_auth_cb, conn); + if (*init_resp == '\0') + smtp_client_command_printf(cmd, "AUTH %s", mech_name); + else { + smtp_client_command_printf(cmd, "AUTH %s %s", + mech_name, init_resp); + } + smtp_client_command_submit(cmd); + conn->cmd_auth = cmd; + + smtp_client_connection_set_state( + conn, SMTP_CLIENT_CONNECTION_STATE_AUTHENTICATING); + return FALSE; +} + +static void +smtp_client_connection_starttls_cb(const struct smtp_reply *reply, + struct smtp_client_connection *conn) +{ + const char *error; + + e_debug(conn->event, "Received STARTTLS reply: %s", + smtp_reply_log(reply)); + + if ((reply->status / 100) != 2) { + smtp_client_connection_fail_reply(conn, reply); + return; + } + + if (smtp_client_connection_ssl_init(conn, &error) < 0) { + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED, + error, SMTP_CLIENT_ERROR_TEXT_CONNECT_FAILED); + } else { + if (conn->to_connect != NULL) + timeout_reset(conn->to_connect); + smtp_client_connection_handshake(conn); + } +} + +static bool smtp_client_connection_starttls(struct smtp_client_connection *conn) +{ + struct smtp_client_command *cmd; + + if (conn->ssl_mode == SMTP_CLIENT_SSL_MODE_STARTTLS && + conn->ssl_iostream == NULL) { + if ((conn->caps.standard & SMTP_CAPABILITY_STARTTLS) == 0) { + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED, + "Requested STARTTLS, " + "but server doesn't support it", + "STARTTLS not supported"); + return FALSE; + } + + e_debug(conn->event, "Starting TLS"); + + cmd = smtp_client_command_new( + conn, SMTP_CLIENT_COMMAND_FLAG_PRELOGIN, + smtp_client_connection_starttls_cb, conn); + smtp_client_command_write(cmd, "STARTTLS"); + smtp_client_command_submit(cmd); + return FALSE; + } + + return smtp_client_connection_authenticate(conn); +} + +static void +smtp_client_connection_record_param_extensions( + struct smtp_client_connection *conn, ARRAY_TYPE(const_string) *arr, + const char *const *extensions) +{ + pool_t pool = conn->cap_pool; + + if (extensions == NULL || *extensions == NULL) + return; + + if (!array_is_created(arr)) + p_array_init(arr, pool, 4); + else { + const char *const *end; + + /* Drop end marker */ + i_assert(array_count(arr) > 0); + end = array_back(arr); + i_assert(*end == NULL); + array_pop_back(arr); + } + + const char *const *new_p; + for (new_p = extensions; *new_p != NULL; new_p++) { + /* Drop duplicates */ + if (array_lsearch(arr, new_p, i_strcasecmp_p) != NULL) + continue; + + array_push_back(arr, new_p); + } + + /* Add new end marker */ + array_append_zero(arr); +} + +static void +smtp_client_connection_record_extra_capability( + struct smtp_client_connection *conn, const char *cap_name, + const char *const *params) +{ + const struct smtp_client_capability_extra *ccap_extra; + struct smtp_capability_extra cap_extra; + pool_t pool = conn->cap_pool; + + ccap_extra = smtp_client_connection_find_extra_capability( + conn, cap_name); + if (ccap_extra == NULL) + return; + if (smtp_client_connection_get_extra_capability(conn, cap_name) != NULL) + return; + + if (!array_is_created(&conn->caps.extra)) + p_array_init(&conn->caps.extra, pool, 4); + + i_zero(&cap_extra); + cap_extra.name = p_strdup(pool, ccap_extra->name); + cap_extra.params = p_strarray_dup(pool, params); + + array_push_back(&conn->caps.extra, &cap_extra); + + smtp_client_connection_record_param_extensions( + conn, &conn->caps.mail_param_extensions, + ccap_extra->mail_param_extensions); + smtp_client_connection_record_param_extensions( + conn, &conn->caps.rcpt_param_extensions, + ccap_extra->rcpt_param_extensions); +} + +static void +smtp_client_connection_handshake_cb(const struct smtp_reply *reply, + struct smtp_client_connection *conn) +{ + const char *const *lines; + + e_debug(conn->event, "Received handshake reply"); + + /* check reply status */ + if ((reply->status / 100) != 2) { + /* RFC 5321, Section 3.2: + For a particular connection attempt, if the server returns a + "command not recognized" response to EHLO, the client SHOULD + be able to fall back and send HELO. */ + if (conn->protocol == SMTP_PROTOCOL_SMTP && !conn->old_smtp && + (reply->status == 500 || reply->status == 502)) { + /* try HELO */ + conn->old_smtp = TRUE; + smtp_client_connection_handshake(conn); + return; + } + /* failed */ + smtp_client_connection_fail_reply(conn, reply); + return; + } + + /* reset capabilities */ + p_clear(conn->cap_pool); + i_zero(&conn->caps); + conn->caps.standard = conn->set.forced_capabilities; + + lines = reply->text_lines; + if (*lines == NULL) { + smtp_client_connection_fail_equal( + conn, SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY, + "Invalid handshake reply"); + return; + } + + /* greeting line */ + lines++; + + /* capability lines */ + while (*lines != NULL) { + enum smtp_capability cap; + const char *const *params; + const char *cap_name, *error; + + if (smtp_ehlo_line_parse(*lines, &cap_name, ¶ms, + &error) <= 0) { + e_warning(conn->event, + "Received invalid EHLO response line: %s", + error); + lines++; + continue; + } + + cap = smtp_capability_find_by_name(cap_name); + switch (cap) { + case SMTP_CAPABILITY_AUTH: + conn->caps.auth_mechanisms = + p_strarray_dup(conn->cap_pool, params); + break; + case SMTP_CAPABILITY_SIZE: + if (params == NULL || *params == NULL) + break; + if (str_to_uoff(*params, &conn->caps.size) < 0) { + e_warning(conn->event, + "Received invalid SIZE capability " + "in EHLO response line"); + cap = SMTP_CAPABILITY_NONE; + } + break; + case SMTP_CAPABILITY_XCLIENT: + conn->caps.xclient_args = + p_strarray_dup(conn->cap_pool, params); + break; + case SMTP_CAPABILITY_NONE: + smtp_client_connection_record_extra_capability( + conn, cap_name, params); + break; + default: + break; + } + + conn->caps.standard |= cap; + lines++; + } + + e_debug(conn->event, "Received server capabilities"); + + if (conn->to_connect != NULL) + timeout_reset(conn->to_connect); + if (smtp_client_connection_starttls(conn)) { + stmp_client_connection_ready(conn, reply); + } +} + +static void +smtp_client_connection_handshake(struct smtp_client_connection *conn) +{ + struct smtp_client_command *cmd; + enum smtp_client_command_flags flags; + const char *command; + + flags = SMTP_CLIENT_COMMAND_FLAG_PRELOGIN | + SMTP_CLIENT_COMMAND_FLAG_PRIORITY; + + switch (conn->protocol) { + case SMTP_PROTOCOL_SMTP: + command = (conn->old_smtp ? "HELO" : "EHLO"); + break; + case SMTP_PROTOCOL_LMTP: + command = "LHLO"; + break; + default: + i_unreached(); + } + + e_debug(conn->event, "Sending %s handshake", command); + + cmd = smtp_client_command_new( + conn, flags, smtp_client_connection_handshake_cb, conn); + smtp_client_command_write(cmd, command); + smtp_client_command_write(cmd, " "); + smtp_client_command_write(cmd, conn->set.my_hostname); + smtp_client_command_submit(cmd); + smtp_client_connection_set_state( + conn, SMTP_CLIENT_CONNECTION_STATE_HANDSHAKING); +} + +static int +smtp_client_connection_input_reply(struct smtp_client_connection *conn, + const struct smtp_reply *reply) +{ + int ret; + + /* initial greeting? */ + if (conn->state == SMTP_CLIENT_CONNECTION_STATE_CONNECTING) { + e_debug(conn->event, "Received greeting from server: %s", + smtp_reply_log(reply)); + if (reply->status != 220) { + if (smtp_reply_is_success(reply)) { + smtp_client_connection_fail_equal( + conn, + SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY, + "Received inappropriate greeting"); + } else { + smtp_client_connection_fail_reply(conn, reply); + } + return -1; + } + smtp_client_connection_handshake(conn); + return 1; + } + + if (reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECTION_CLOSED) { + smtp_client_connection_fail_reply(conn, reply); + return -1; + } + + /* unexpected reply? */ + if (conn->cmd_wait_list_head == NULL) { + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY, + t_strdup_printf("Unexpected reply: %s", + smtp_reply_log(reply)), + "Got unexpected reply"); + return -1; + } + + /* replied early? */ + if (conn->cmd_wait_list_head == conn->cmd_streaming && + !conn->cmd_wait_list_head->stream_finished) { + if (!smtp_reply_is_success(reply)) { + e_debug(conn->event, "Early reply: %s", + smtp_reply_log(reply)); + } else { + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY, + t_strdup_printf( + "Got early success reply: %s", + smtp_reply_log(reply)), + "Got early success reply"); + return -1; + } + } + + /* command reply */ + ret = smtp_client_command_input_reply(conn->cmd_wait_list_head, reply); + + if (conn->state == SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED || + conn->conn.output == NULL) + return -1; + return ret; +} + +static void smtp_client_connection_input(struct connection *_conn) +{ + struct smtp_client_connection *conn = + (struct smtp_client_connection *)_conn; + bool enhanced_codes = ((conn->caps.standard & + SMTP_CAPABILITY_ENHANCEDSTATUSCODES) != 0); + struct smtp_reply *reply; + const char *error = NULL; + int ret; + + if (conn->ssl_iostream != NULL && + !ssl_iostream_is_handshaked(conn->ssl_iostream)) { + /* finish SSL negotiation by reading from input stream */ + while ((ret = i_stream_read(conn->conn.input)) > 0 || + ret == -2) { + if (ssl_iostream_is_handshaked(conn->ssl_iostream)) + break; + } + if (ret < 0) { + /* failed somehow */ + i_assert(ret != -2); + error = t_strdup_printf( + "SSL handshaking with %s failed: " + "read(%s) failed: %s", _conn->name, + i_stream_get_name(conn->conn.input), + i_stream_get_error(conn->conn.input)); + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED, + error, SMTP_CLIENT_ERROR_TEXT_CONNECT_FAILED); + return; + } + + if (!ssl_iostream_is_handshaked(conn->ssl_iostream)) { + /* not finished */ + i_assert(ret == 0); + return; + } + + if (conn->to_connect != NULL) + timeout_reset(conn->to_connect); + } + + if (!conn->connect_succeeded) { + /* just got ready for SMTP handshake */ + smtp_client_connection_established(conn); + } + + smtp_client_connection_ref(conn); + o_stream_cork(conn->conn.output); + for (;;) { + if (conn->cmd_wait_list_head != NULL && + conn->cmd_wait_list_head->ehlo) { + if ((ret = smtp_reply_parse_ehlo(conn->reply_parser, + &reply, &error)) <= 0) + break; + } else { + if ((ret = smtp_reply_parse_next(conn->reply_parser, + enhanced_codes, + &reply, &error)) <= 0) + break; + } + + T_BEGIN { + ret = smtp_client_connection_input_reply(conn, reply); + } T_END; + if (ret < 0) { + if (conn->conn.output != NULL && !conn->corked) + o_stream_uncork(conn->conn.output); + smtp_client_connection_unref(&conn); + return; + } + } + + if (ret < 0 || (ret == 0 && conn->conn.input->eof)) { + if (conn->conn.input->stream_errno == ENOBUFS) { + smtp_client_connection_fail_equal( + conn, SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY, + "Command reply line too long"); + } else if (conn->conn.input->stream_errno != 0) { + smtp_client_connection_lost( + conn, + t_strdup_printf( + "read(%s) failed: %s", + i_stream_get_name(conn->conn.input), + i_stream_get_error(conn->conn.input)), + "Read failure"); + } else if (!i_stream_have_bytes_left(conn->conn.input)) { + if (conn->sent_quit) { + smtp_client_connection_lost( + conn, NULL, + "Remote closed connection"); + } else { + smtp_client_connection_lost( + conn, NULL, + "Remote closed connection unexpectedly"); + } + } else { + i_assert(error != NULL); + error = t_strdup_printf("Invalid command reply: %s", + error); + smtp_client_connection_fail_equal( + conn, SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY, + error); + } + } + if (ret >= 0 && conn->conn.output != NULL && !conn->corked) { + if (o_stream_uncork_flush(conn->conn.output) < 0) + smtp_client_connection_handle_output_error(conn); + } + smtp_client_connection_unref(&conn); +} + +static int smtp_client_connection_output(struct smtp_client_connection *conn) +{ + int ret; + + if (conn->to_connect != NULL) + timeout_reset(conn->to_connect); + + ret = o_stream_flush(conn->conn.output); + if (ret <= 0) { + if (ret < 0) + smtp_client_connection_handle_output_error(conn); + return ret; + } + + smtp_client_connection_ref(conn); + o_stream_cork(conn->conn.output); + if (smtp_client_command_send_more(conn) < 0) + ret = -1; + if (ret >= 0 && conn->conn.output != NULL && !conn->corked) { + if (o_stream_uncork_flush(conn->conn.output) < 0) + smtp_client_connection_handle_output_error(conn); + } + smtp_client_connection_unref(&conn); + return ret; +} + +void smtp_client_connection_trigger_output(struct smtp_client_connection *conn) +{ + if (conn->conn.output != NULL) + o_stream_set_flush_pending(conn->conn.output, TRUE); +} + +static void smtp_client_connection_destroy(struct connection *_conn) +{ + struct smtp_client_connection *conn = + (struct smtp_client_connection *)_conn; + const char *error; + + switch (_conn->disconnect_reason) { + case CONNECTION_DISCONNECT_NOT: + break; + case CONNECTION_DISCONNECT_DEINIT: + e_debug(conn->event, "Connection deinit"); + smtp_client_connection_close(&conn); + break; + case CONNECTION_DISCONNECT_CONNECT_TIMEOUT: + error = t_strdup_printf( + "connect(%s) failed: Connection timed out", + _conn->name); + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED, + error, "Connect timed out"); + break; + default: + case CONNECTION_DISCONNECT_CONN_CLOSED: + if (conn->connect_failed) { + smtp_client_connection_fail(conn, + SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED, + NULL, SMTP_CLIENT_ERROR_TEXT_CONNECT_FAILED); + break; + } + if (_conn->input != NULL && _conn->input->stream_errno != 0) { + smtp_client_connection_lost( + conn, + t_strdup_printf("read(%s) failed: %s", + i_stream_get_name(conn->conn.input), + i_stream_get_error(conn->conn.input)), + "Read failure"); + break; + } + smtp_client_connection_lost( + conn, "Remote disconnected", + "Remote closed connection unexpectedly"); + break; + } +} + +static void +smtp_client_connection_established(struct smtp_client_connection *conn) +{ + i_assert(!conn->connect_succeeded); + conn->connect_succeeded = TRUE; + + if (conn->to_connect != NULL) + timeout_reset(conn->to_connect); + + /* set flush callback */ + o_stream_set_flush_callback(conn->conn.output, + smtp_client_connection_output, conn); +} + +static int +smtp_client_connection_ssl_handshaked(const char **error_r, void *context) +{ + struct smtp_client_connection *conn = context; + const char *error, *host = conn->host; + + if (ssl_iostream_check_cert_validity(conn->ssl_iostream, + host, &error) == 0) { + e_debug(conn->event, "SSL handshake successful"); + } else if (conn->set.ssl->allow_invalid_cert) { + e_debug(conn->event, "SSL handshake successful, " + "ignoring invalid certificate: %s", error); + } else { + *error_r = error; + return -1; + } + return 0; +} + +static void +smtp_client_connection_streams_changed(struct smtp_client_connection *conn) +{ + struct stat st; + + if (conn->set.rawlog_dir != NULL && + stat(conn->set.rawlog_dir, &st) == 0) { + iostream_rawlog_create(conn->set.rawlog_dir, + &conn->conn.input, &conn->conn.output); + } + + if (conn->reply_parser == NULL) { + conn->reply_parser = smtp_reply_parser_init( + conn->conn.input, conn->set.max_reply_size); + } else { + smtp_reply_parser_set_stream(conn->reply_parser, + conn->conn.input); + } + + connection_streams_changed(&conn->conn); +} + +static int +smtp_client_connection_init_ssl_ctx(struct smtp_client_connection *conn, + const char **error_r) +{ + struct smtp_client *client = conn->client; + const char *error; + + if (conn->ssl_ctx != NULL) + return 0; + + if (conn->set.ssl == client->set.ssl) { + if (smtp_client_init_ssl_ctx(client, error_r) < 0) + return -1; + conn->ssl_ctx = client->ssl_ctx; + ssl_iostream_context_ref(conn->ssl_ctx); + return 0; + } + + if (conn->set.ssl == NULL) { + *error_r = + "Requested SSL connection, but no SSL settings given"; + return -1; + } + if (ssl_iostream_client_context_cache_get(conn->set.ssl, &conn->ssl_ctx, + &error) < 0) { + *error_r = t_strdup_printf( + "Couldn't initialize SSL context: %s", error); + return -1; + } + return 0; +} + +static int +smtp_client_connection_ssl_init(struct smtp_client_connection *conn, + const char **error_r) +{ + const char *error; + + if (smtp_client_connection_init_ssl_ctx(conn, &error) < 0) { + *error_r = t_strdup_printf( + "Failed to initialize SSL: %s", error); + return -1; + } + + e_debug(conn->event, "Starting SSL handshake"); + + if (conn->raw_input != conn->conn.input) { + /* recreate rawlog after STARTTLS */ + i_stream_ref(conn->raw_input); + o_stream_ref(conn->raw_output); + i_stream_destroy(&conn->conn.input); + o_stream_destroy(&conn->conn.output); + conn->conn.input = conn->raw_input; + conn->conn.output = conn->raw_output; + } + + connection_input_halt(&conn->conn); + if (io_stream_create_ssl_client( + conn->ssl_ctx, conn->host, conn->set.ssl, + &conn->conn.input, &conn->conn.output, + &conn->ssl_iostream, &error) < 0) { + *error_r = t_strdup_printf( + "Couldn't initialize SSL client for %s: %s", + conn->conn.name, error); + return -1; + } + connection_input_resume(&conn->conn); + smtp_client_connection_streams_changed(conn); + + ssl_iostream_set_handshake_callback( + conn->ssl_iostream, smtp_client_connection_ssl_handshaked, + conn); + if (ssl_iostream_handshake(conn->ssl_iostream) < 0) { + *error_r = t_strdup_printf( + "SSL handshake to %s failed: %s", conn->conn.name, + ssl_iostream_get_last_error(conn->ssl_iostream)); + return -1; + } + + if (ssl_iostream_is_handshaked(conn->ssl_iostream) && + !conn->connect_succeeded) { + smtp_client_connection_established(conn); + } else { + /* wait for handshake to complete; connection input handler + does the rest by reading from the input stream */ + o_stream_set_flush_callback( + conn->conn.output, smtp_client_connection_output, conn); + } + return 0; +} + +static void +smtp_client_connection_connected(struct connection *_conn, bool success) +{ + struct smtp_client_connection *conn = + (struct smtp_client_connection *)_conn; + const struct smtp_client_settings *set = &conn->set; + const char *error; + + if (!success) { + e_error(conn->event, "connect(%s) failed: %m", _conn->name); + conn->connect_failed = TRUE; + return; + } + + if (conn->set.debug) { + struct ip_addr local_ip; + in_port_t local_port; + int ret; + + ret = net_getsockname(_conn->fd_in, &local_ip, &local_port); + i_assert(ret == 0); + e_debug(conn->event, "Connected to server (from %s:%u)", + net_ip2addr(&local_ip), local_port); + } + + (void)net_set_tcp_nodelay(_conn->fd_out, TRUE); + if (set->socket_send_buffer_size > 0 && + net_set_send_buffer_size(_conn->fd_out, + set->socket_send_buffer_size) < 0) { + e_error(conn->event, + "net_set_send_buffer_size(%zu) failed: %m", + set->socket_send_buffer_size); + } + if (set->socket_recv_buffer_size > 0 && + net_set_recv_buffer_size(_conn->fd_in, + set->socket_recv_buffer_size) < 0) { + e_error(conn->event, + "net_set_recv_buffer_size(%zu) failed: %m", + set->socket_recv_buffer_size); + } + + conn->raw_input = conn->conn.input; + conn->raw_output = conn->conn.output; + smtp_client_connection_streams_changed(conn); + + if (conn->ssl_mode == SMTP_CLIENT_SSL_MODE_IMMEDIATE) { + if (smtp_client_connection_ssl_init(conn, &error) < 0) { + error = t_strdup_printf("connect(%s) failed: %s", + _conn->name, error); + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED, + error, SMTP_CLIENT_ERROR_TEXT_CONNECT_FAILED); + } + } else { + smtp_client_connection_established(conn); + smtp_client_connection_input(_conn); + } +} + +static void +smtp_client_connection_connect_timeout(struct smtp_client_connection *conn) +{ + switch (conn->state) { + case SMTP_CLIENT_CONNECTION_STATE_CONNECTING: + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED, + t_strdup_printf( + "Connection timed out after %u seconds", + conn->set.connect_timeout_msecs/1000), + "Connect timed out"); + break; + case SMTP_CLIENT_CONNECTION_STATE_HANDSHAKING: + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED, + t_strdup_printf( + "SMTP handshake timed out after %u seconds", + conn->set.connect_timeout_msecs/1000), + "Handshake timed out"); + break; + case SMTP_CLIENT_CONNECTION_STATE_AUTHENTICATING: + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED, + t_strdup_printf( + "Authentication timed out after %u seconds", + conn->set.connect_timeout_msecs/1000), + "Authentication timed out"); + break; + default: + i_unreached(); + } +} + +static void +smtp_client_connection_delayed_connect_error( + struct smtp_client_connection *conn) +{ + e_debug(conn->event, "Delayed connect error"); + + timeout_remove(&conn->to_connect); + errno = conn->connect_errno; + smtp_client_connection_connected(&conn->conn, FALSE); + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED, + NULL, SMTP_CLIENT_ERROR_TEXT_CONNECT_FAILED); +} + +static void +smtp_client_connection_do_connect(struct smtp_client_connection *conn) +{ + unsigned int msecs; + + if (conn->closed || conn->failing) + return; + + /* Clear state data */ + i_zero(&conn->state_data); + p_clear(conn->state_pool); + + if (connection_client_connect(&conn->conn) < 0) { + conn->connect_errno = errno; + e_debug(conn->event, "Connect failed: %m"); + conn->to_connect = timeout_add_short( + 0, smtp_client_connection_delayed_connect_error, conn); + return; + } + + /* don't use connection.h timeout because we want this timeout + to include also the SSL handshake */ + msecs = conn->set.connect_timeout_msecs; + if (msecs == 0) + msecs = conn->set.command_timeout_msecs; + i_assert(conn->to_connect == NULL); + if (msecs > 0) { + conn->to_connect = timeout_add( + msecs, smtp_client_connection_connect_timeout, conn); + } +} + +static bool smtp_client_connection_last_ip(struct smtp_client_connection *conn) +{ + i_assert(conn->prev_connect_idx < conn->ips_count); + return (conn->prev_connect_idx + 1) % conn->ips_count == 0; +} + +static void +smtp_client_connection_connect_next_ip(struct smtp_client_connection *conn) +{ + const struct ip_addr *ip, *my_ip = &conn->set.my_ip; + + timeout_remove(&conn->to_connect); + + conn->prev_connect_idx = (conn->prev_connect_idx+1) % conn->ips_count; + ip = &conn->ips[conn->prev_connect_idx]; + + if (my_ip->family != 0) { + e_debug(conn->event, "Connecting to %s:%u (from %s)", + net_ip2addr(ip), conn->port, net_ip2addr(my_ip)); + } else { + e_debug(conn->event, "Connecting to %s:%u", + net_ip2addr(ip), conn->port); + } + + connection_init_client_ip_from(conn->client->conn_list, &conn->conn, + (conn->host_is_ip ? NULL : conn->host), + ip, conn->port, my_ip); + + smtp_client_connection_do_connect(conn); +} + +static void +smtp_client_connection_connect_unix(struct smtp_client_connection *conn) +{ + timeout_remove(&conn->to_connect); + + e_debug(conn->event, "Connecting to socket %s", conn->path); + + connection_init_client_unix(conn->client->conn_list, &conn->conn, + conn->path); + + smtp_client_connection_do_connect(conn); +} + +static void +smtp_client_connection_delayed_host_lookup_failure( + struct smtp_client_connection *conn) +{ + e_debug(conn->event, "Delayed host lookup failure"); + + i_assert(conn->to_connect != NULL); + timeout_remove(&conn->to_connect); + smtp_client_connection_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED, + NULL, "Failed to lookup remote server"); +} + +static void +smtp_client_connection_dns_callback(const struct dns_lookup_result *result, + struct smtp_client_connection *conn) +{ + conn->dns_lookup = NULL; + + if (result->ret != 0) { + e_error(conn->event, "dns_lookup(%s) failed: %s", + conn->host, result->error); + timeout_remove(&conn->to_connect); + conn->to_connect = timeout_add_short( + 0, smtp_client_connection_delayed_host_lookup_failure, + conn); + return; + } + + e_debug(conn->event, "DNS lookup successful; got %d IPs", + result->ips_count); + + 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; + + smtp_client_connection_connect_next_ip(conn); +} + +static void +smtp_client_connection_lookup_ip(struct smtp_client_connection *conn) +{ + struct dns_lookup_settings dns_set; + struct ip_addr ip, *ips; + unsigned int ips_count; + int ret; + + if (conn->ips_count != 0) + return; + + e_debug(conn->event, "Looking up IP address"); + + if (net_addr2ip(conn->host, &ip) == 0) { + /* IP address */ + conn->ips_count = 1; + conn->ips = i_new(struct ip_addr, conn->ips_count); + conn->ips[0] = ip; + conn->host_is_ip = TRUE; + } else if (conn->set.dns_client != NULL) { + e_debug(conn->event, "Performing asynchronous DNS lookup"); + (void)dns_client_lookup( + conn->set.dns_client, conn->host, + smtp_client_connection_dns_callback, conn, + &conn->dns_lookup); + } else if (conn->set.dns_client_socket_path != NULL) { + i_zero(&dns_set); + dns_set.dns_client_socket_path = + conn->set.dns_client_socket_path; + dns_set.timeout_msecs = conn->set.connect_timeout_msecs; + dns_set.event_parent = conn->event; + e_debug(conn->event, "Performing asynchronous DNS lookup"); + (void)dns_lookup(conn->host, &dns_set, + smtp_client_connection_dns_callback, conn, + &conn->dns_lookup); + } else { + /* no dns-conn, use blocking lookup */ + ret = net_gethostbyname(conn->host, &ips, &ips_count); + if (ret != 0) { + e_error(conn->event, "net_gethostbyname(%s) failed: %s", + conn->host, net_gethosterror(ret)); + timeout_remove(&conn->to_connect); + conn->to_connect = timeout_add_short( + 0, + smtp_client_connection_delayed_host_lookup_failure, + conn); + return; + } + + e_debug(conn->event, "DNS lookup successful; got %d IPs", + ips_count); + + conn->ips_count = ips_count; + conn->ips = i_new(struct ip_addr, ips_count); + memcpy(conn->ips, ips, ips_count * sizeof(*ips)); + } +} + +static void +smtp_client_connection_already_connected(struct smtp_client_connection *conn) +{ + i_assert(conn->state_data.login_reply != NULL); + + timeout_remove(&conn->to_connect); + + e_debug(conn->event, "Already connected"); + + smtp_client_connection_login_callback( + conn, conn->state_data.login_reply); +} + +static void +smtp_client_connection_connect_more(struct smtp_client_connection *conn) +{ + if (!array_is_created(&conn->login_callbacks) || + array_count(&conn->login_callbacks) == 0) { + /* No login callbacks required */ + return; + } + if (conn->state < SMTP_CLIENT_CONNECTION_STATE_READY) { + /* Login callbacks will be called once the connection succeeds + or fails. */ + return; + } + + if (array_count(&conn->login_callbacks) > 1) { + /* Another login callback is already pending */ + i_assert(conn->to_connect != NULL); + return; + } + + /* Schedule immediate login callback */ + i_assert(conn->to_connect == NULL); + conn->to_connect = timeout_add( + 0, smtp_client_connection_already_connected, conn); +} + +void smtp_client_connection_connect( + struct smtp_client_connection *conn, + smtp_client_command_callback_t login_callback, void *login_context) +{ + struct smtp_client_login_callback *login_cb; + + if (conn->closed) + return; + + if (login_callback != NULL) { + if (!array_is_created(&conn->login_callbacks)) + i_array_init(&conn->login_callbacks, 4); + + login_cb = array_append_space(&conn->login_callbacks); + login_cb->callback = login_callback; + login_cb->context = login_context; + } + + if (conn->state != SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED) { + /* Already connecting or connected */ + smtp_client_connection_connect_more(conn); + return; + } + if (conn->failing) + return; + + e_debug(conn->event, "Disconnected"); + + conn->xclient_replies_expected = 0; + conn->authenticated = FALSE; + conn->xclient_sent = FALSE; + conn->connect_failed = FALSE; + conn->connect_succeeded = FALSE; + conn->handshake_failed = FALSE; + conn->sent_quit = FALSE; + conn->reset_needed = FALSE; + + smtp_client_connection_set_state( + conn, SMTP_CLIENT_CONNECTION_STATE_CONNECTING); + + if (conn->path == NULL) { + smtp_client_connection_lookup_ip(conn); + if (conn->ips_count == 0) + return; + + /* always work asynchronously */ + timeout_remove(&conn->to_connect); + conn->to_connect = timeout_add( + 0, smtp_client_connection_connect_next_ip, conn); + } else { + /* always work asynchronously */ + timeout_remove(&conn->to_connect); + conn->to_connect = timeout_add( + 0, smtp_client_connection_connect_unix, conn); + } +} + +static const struct connection_settings smtp_client_connection_set = { + .input_max_size = SIZE_MAX, + .output_max_size = SIZE_MAX, + .client = TRUE, + .delayed_unix_client_connected_callback = TRUE, + .log_connection_id = TRUE, +}; + +static const struct connection_vfuncs smtp_client_connection_vfuncs = { + .destroy = smtp_client_connection_destroy, + .input = smtp_client_connection_input, + .client_connected = smtp_client_connection_connected +}; + +struct connection_list *smtp_client_connection_list_init(void) +{ + return connection_list_init(&smtp_client_connection_set, + &smtp_client_connection_vfuncs); +} + +void smtp_client_connection_disconnect(struct smtp_client_connection *conn) +{ + if (conn->state == SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED) + return; + + e_debug(conn->event, "Disconnected"); + + smtp_client_connection_clear_password(conn); + + if (conn->conn.output != NULL && !conn->sent_quit && + !conn->sending_command) { + /* Close the connection gracefully if possible */ + o_stream_nsend_str(conn->conn.output, "QUIT\r\n"); + o_stream_uncork(conn->conn.output); + } + + if (conn->dns_lookup != NULL) + dns_lookup_abort(&conn->dns_lookup); + io_remove(&conn->io_cmd_payload); + timeout_remove(&conn->to_connect); + timeout_remove(&conn->to_trans); + timeout_remove(&conn->to_commands); + timeout_remove(&conn->to_cmd_fail); + + ssl_iostream_destroy(&conn->ssl_iostream); + if (conn->ssl_ctx != NULL) + ssl_iostream_context_unref(&conn->ssl_ctx); + smtp_client_connection_auth_deinit(conn); + + o_stream_destroy(&conn->dot_output); + + connection_disconnect(&conn->conn); + + smtp_client_connection_set_state( + conn, SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED); + + if (!conn->failing) { + smtp_client_connection_login_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_ABORTED, + "Disconnected from server"); + smtp_client_connection_transactions_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_ABORTED, + "Disconnected from server"); + smtp_client_connection_commands_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_ABORTED, + "Disconnected from server"); + } + smtp_client_command_unref(&conn->cmd_streaming); +} + +static struct smtp_client_connection * +smtp_client_connection_do_create(struct smtp_client *client, const char *name, + enum smtp_protocol protocol, + const struct smtp_client_settings *set) +{ + struct smtp_client_connection *conn; + struct event *conn_event; + pool_t pool; + + pool = pool_alloconly_create("smtp client connection", 2048); + conn = p_new(pool, struct smtp_client_connection, 1); + conn->refcount = 1; + conn->pool = pool; + + conn->client = client; + conn->protocol = protocol; + + conn->set = client->set; + if (set != NULL) { + if (set->my_ip.family != 0) + conn->set.my_ip = set->my_ip; + if (set->my_hostname != NULL && *set->my_hostname != '\0') { + conn->set.my_hostname = + p_strdup(pool, set->my_hostname); + } + + conn->set.forced_capabilities |= set->forced_capabilities; + if (set->extra_capabilities != NULL) { + conn->set.extra_capabilities = + p_strarray_dup(pool, set->extra_capabilities); + } + + if (set->rawlog_dir != NULL && *set->rawlog_dir != '\0') { + conn->set.rawlog_dir = + p_strdup_empty(pool, set->rawlog_dir); + } + + if (set->ssl != NULL) { + conn->set.ssl = + ssl_iostream_settings_dup(pool, set->ssl); + } + + if (set->master_user != NULL && *set->master_user != '\0') { + conn->set.master_user = + p_strdup_empty(pool, set->master_user); + } + if (set->username != NULL && *set->username != '\0') { + conn->set.username = + p_strdup_empty(pool, set->username); + } + if (set->password != NULL && *set->password != '\0') { + conn->password = p_strdup(pool, set->password); + conn->set.password = conn->password; + } + if (set->sasl_mech != NULL) + conn->set.sasl_mech = set->sasl_mech; + else if (set->sasl_mechanisms != NULL && + *set->sasl_mechanisms != '\0') { + conn->set.sasl_mechanisms = + p_strdup(pool, set->sasl_mechanisms); + } + conn->set.remember_password = set->remember_password; + + if (set->command_timeout_msecs > 0) { + conn->set.command_timeout_msecs = + set->command_timeout_msecs; + } + if (set->connect_timeout_msecs > 0) { + conn->set.connect_timeout_msecs = + set->connect_timeout_msecs; + } + if (set->max_reply_size > 0) + conn->set.max_reply_size = set->max_reply_size; + if (set->max_data_chunk_size > 0) { + conn->set.max_data_chunk_size = + set->max_data_chunk_size; + } + if (set->max_data_chunk_pipeline > 0) { + conn->set.max_data_chunk_pipeline = + set->max_data_chunk_pipeline; + } + + if (set->socket_send_buffer_size > 0) { + conn->set.socket_send_buffer_size = + set->socket_send_buffer_size; + } + if (set->socket_recv_buffer_size > 0) { + conn->set.socket_recv_buffer_size = + set->socket_recv_buffer_size; + } + conn->set.debug = conn->set.debug || set->debug; + + smtp_proxy_data_merge(conn->pool, &conn->set.proxy_data, + &set->proxy_data); + conn->set.xclient_defer = set->xclient_defer; + conn->set.peer_trusted = set->peer_trusted; + + conn->set.mail_send_broken_path = set->mail_send_broken_path; + + conn->set.verbose_user_errors = + conn->set.verbose_user_errors || + set->verbose_user_errors; + } + + if (set != NULL && set->extra_capabilities != NULL) { + const char *const *extp; + + p_array_init(&conn->extra_capabilities, pool, + str_array_length(set->extra_capabilities) + 8); + for (extp = set->extra_capabilities; *extp != NULL; extp++) { + struct smtp_client_capability_extra cap = { + .name = p_strdup(pool, *extp), + }; + + array_push_back(&conn->extra_capabilities, &cap); + } + } + + i_assert(conn->set.my_hostname != NULL && + *conn->set.my_hostname != '\0'); + + conn->caps.standard = conn->set.forced_capabilities; + conn->cap_pool = pool_alloconly_create( + "smtp client connection capabilities", 128); + conn->state_pool = pool_alloconly_create( + "smtp client connection state", 256); + + if (set != NULL && set->event_parent != NULL) + conn_event = event_create(set->event_parent); + else + conn_event = event_create(client->event); + event_set_append_log_prefix( + conn_event, + t_strdup_printf("%s-client: ", + smtp_protocol_name(conn->protocol))); + event_add_str(conn_event, "protocol", + smtp_protocol_name(conn->protocol)); + event_set_forced_debug(conn_event, (set != NULL && set->debug)); + + conn->conn.event_parent = conn_event; + connection_init(conn->client->conn_list, &conn->conn, name); + conn->event = conn->conn.event; + event_unref(&conn_event); + + return conn; +} + +struct smtp_client_connection * +smtp_client_connection_create(struct smtp_client *client, + enum smtp_protocol protocol, + const char *host, in_port_t port, + enum smtp_client_connection_ssl_mode ssl_mode, + const struct smtp_client_settings *set) +{ + struct smtp_client_connection *conn; + const char *name = t_strdup_printf("%s:%u", host, port); + + conn = smtp_client_connection_do_create(client, name, protocol, set); + conn->host = p_strdup(conn->pool, host); + conn->port = port; + conn->ssl_mode = ssl_mode; + + event_add_str(conn->event, "host", host); + + e_debug(conn->event, "Connection created"); + + return conn; +} + +struct smtp_client_connection * +smtp_client_connection_create_ip(struct smtp_client *client, + enum smtp_protocol protocol, + const struct ip_addr *ip, in_port_t port, + const char *hostname, + enum smtp_client_connection_ssl_mode ssl_mode, + const struct smtp_client_settings *set) +{ + struct smtp_client_connection *conn; + bool host_is_ip = FALSE; + + if (hostname == NULL) { + hostname = net_ip2addr(ip); + host_is_ip = TRUE; + } + + conn = smtp_client_connection_create(client, protocol, hostname, port, + ssl_mode, set); + conn->ips_count = 1; + conn->ips = i_new(struct ip_addr, conn->ips_count); + conn->ips[0] = *ip; + conn->host_is_ip = host_is_ip; + return conn; +} + +struct smtp_client_connection * +smtp_client_connection_create_unix(struct smtp_client *client, + enum smtp_protocol protocol, + const char *path, + const struct smtp_client_settings *set) +{ + struct smtp_client_connection *conn; + const char *name = t_strconcat("unix:", path, NULL); + + conn = smtp_client_connection_do_create(client, name, protocol, set); + conn->path = p_strdup(conn->pool, path); + + e_debug(conn->event, "Connection created"); + + return conn; +} + +void smtp_client_connection_ref(struct smtp_client_connection *conn) +{ + i_assert(conn->refcount >= 0); + conn->refcount++; +} + +void smtp_client_connection_unref(struct smtp_client_connection **_conn) +{ + struct smtp_client_connection *conn = *_conn; + + *_conn = NULL; + + i_assert(conn->refcount > 0); + if (--conn->refcount > 0) + return; + if (conn->destroying) + return; + + conn->destroying = TRUE; + + smtp_client_connection_clear_password(conn); + smtp_client_connection_disconnect(conn); + + /* could have been created while already disconnected */ + timeout_remove(&conn->to_commands); + timeout_remove(&conn->to_cmd_fail); + + e_debug(conn->event, "Destroy"); + + if (conn->reply_parser != NULL) + smtp_reply_parser_deinit(&conn->reply_parser); + + smtp_client_connection_login_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_ABORTED, + "Connection destroy"); + smtp_client_connection_transactions_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_ABORTED, + "Connection destroy"); + smtp_client_connection_commands_fail( + conn, SMTP_CLIENT_COMMAND_ERROR_ABORTED, + "Connection destroy"); + smtp_client_connection_transactions_drop(conn); + + connection_deinit(&conn->conn); + + i_free(conn->ips); + array_free(&conn->login_callbacks); + pool_unref(&conn->cap_pool); + pool_unref(&conn->state_pool); + pool_unref(&conn->pool); +} + +void smtp_client_connection_close(struct smtp_client_connection **_conn) +{ + struct smtp_client_connection *conn = *_conn; + + *_conn = NULL; + + if (conn->closed) + return; + conn->closed = TRUE; + + smtp_client_connection_transactions_abort(conn); + smtp_client_connection_commands_abort(conn); + smtp_client_connection_disconnect(conn); + + /* could have been created while already disconnected */ + timeout_remove(&conn->to_commands); + timeout_remove(&conn->to_cmd_fail); + + smtp_client_connection_unref(&conn); +} + +void smtp_client_connection_update_proxy_data( + struct smtp_client_connection *conn, + const struct smtp_proxy_data *proxy_data) +{ + if (conn->xclient_sent) + return; + + smtp_proxy_data_merge(conn->pool, &conn->set.proxy_data, proxy_data); +} + +void smtp_client_connection_switch_ioloop(struct smtp_client_connection *conn) +{ + struct smtp_client_transaction *trans; + + if (conn->io_cmd_payload != NULL) + conn->io_cmd_payload = io_loop_move_io(&conn->io_cmd_payload); + if (conn->to_connect != NULL) + conn->to_connect = io_loop_move_timeout(&conn->to_connect); + if (conn->to_trans != NULL) + conn->to_trans = io_loop_move_timeout(&conn->to_trans); + if (conn->to_commands != NULL) + conn->to_commands = io_loop_move_timeout(&conn->to_commands); + if (conn->to_cmd_fail != NULL) + conn->to_cmd_fail = io_loop_move_timeout(&conn->to_cmd_fail); + connection_switch_ioloop(&conn->conn); + + trans = conn->transactions_head; + while (trans != NULL) { + smtp_client_transaction_switch_ioloop(trans); + trans = trans->next; + } +} + +static void +smtp_client_connection_rset_dummy_cb( + const struct smtp_reply *reply ATTR_UNUSED, + struct smtp_client_connection *conn ATTR_UNUSED) +{ + /* nothing */ +} + +static void +smtp_client_connection_reset(struct smtp_client_connection *conn) +{ + e_debug(conn->event, "Submitting RSET command"); + + conn->reset_needed = FALSE; + + (void)smtp_client_command_rset_submit( + conn, SMTP_CLIENT_COMMAND_FLAG_PRIORITY, + smtp_client_connection_rset_dummy_cb, conn); +} + +static void +smtp_client_connection_do_start_transaction(struct smtp_client_connection *conn) +{ + struct smtp_reply reply; + + timeout_remove(&conn->to_trans); + + if (conn->state != SMTP_CLIENT_CONNECTION_STATE_TRANSACTION) + return; + if (conn->transactions_head == NULL) { + smtp_client_connection_set_state( + conn, SMTP_CLIENT_CONNECTION_STATE_READY); + return; + } + + if (conn->reset_needed) + smtp_client_connection_reset(conn); + + e_debug(conn->event, "Start next transaction"); + + smtp_reply_init(&reply, 200, "Connection ready"); + smtp_client_transaction_connection_result( + conn->transactions_head, &reply); +} + +static void +smtp_client_connection_start_transaction(struct smtp_client_connection *conn) +{ + if (conn->state != SMTP_CLIENT_CONNECTION_STATE_READY) + return; + if (conn->transactions_head == NULL) + return; + if (conn->to_trans != NULL) + return; + + smtp_client_connection_set_state( + conn, SMTP_CLIENT_CONNECTION_STATE_TRANSACTION); + conn->to_trans = timeout_add_short( + 0, smtp_client_connection_do_start_transaction, conn); +} + +void smtp_client_connection_add_transaction( + struct smtp_client_connection *conn, + struct smtp_client_transaction *trans) +{ + e_debug(conn->event, "Add transaction"); + + DLLIST2_APPEND(&conn->transactions_head, &conn->transactions_tail, + trans); + + smtp_client_connection_connect(conn, NULL, NULL); + smtp_client_connection_start_transaction(conn); +} + +void smtp_client_connection_abort_transaction( + struct smtp_client_connection *conn, + struct smtp_client_transaction *trans) +{ + bool was_first = (trans == conn->transactions_head); + + e_debug(conn->event, "Abort transaction"); + + DLLIST2_REMOVE(&conn->transactions_head, &conn->transactions_tail, + trans); + + if (!was_first) + return; + i_assert(conn->state != SMTP_CLIENT_CONNECTION_STATE_READY); + if (conn->state != SMTP_CLIENT_CONNECTION_STATE_TRANSACTION) + return; + + /* transaction messed up; protocol state needs to be reset for + next transaction */ + conn->reset_needed = TRUE; + + smtp_client_connection_set_state( + conn, SMTP_CLIENT_CONNECTION_STATE_READY); + smtp_client_connection_start_transaction(conn); +} + +void smtp_client_connection_next_transaction( + struct smtp_client_connection *conn, + struct smtp_client_transaction *trans) +{ + e_debug(conn->event, "Initiate next transaction"); + + i_assert(trans == conn->transactions_head); + + DLLIST2_REMOVE(&conn->transactions_head, &conn->transactions_tail, + trans); + + i_assert(conn->state != SMTP_CLIENT_CONNECTION_STATE_READY); + if (conn->state != SMTP_CLIENT_CONNECTION_STATE_TRANSACTION) + return; + + smtp_client_connection_set_state( + conn, SMTP_CLIENT_CONNECTION_STATE_READY); + smtp_client_connection_start_transaction(conn); +} |