diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:51:24 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:51:24 +0000 |
commit | f7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch) | |
tree | a3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /src/lib-smtp/smtp-server-cmd-data.c | |
parent | Initial commit. (diff) | |
download | dovecot-upstream.tar.xz dovecot-upstream.zip |
Adding upstream version 1:2.3.19.1+dfsg1.upstream/1%2.3.19.1+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/lib-smtp/smtp-server-cmd-data.c')
-rw-r--r-- | src/lib-smtp/smtp-server-cmd-data.c | 679 |
1 files changed, 679 insertions, 0 deletions
diff --git a/src/lib-smtp/smtp-server-cmd-data.c b/src/lib-smtp/smtp-server-cmd-data.c new file mode 100644 index 0000000..7f33fdf --- /dev/null +++ b/src/lib-smtp/smtp-server-cmd-data.c @@ -0,0 +1,679 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "array.h" +#include "istream.h" +#include "istream-chain.h" + +#include "smtp-command-parser.h" +#include "smtp-server-private.h" + +/* DATA/BDAT/B... commands */ + +struct cmd_data_context { + struct istream *main_input; + struct istream *chunk_input; + uoff_t chunk_size; + + bool chunking:1; + bool client_input:1; + bool chunk_first:1; + bool chunk_last:1; +}; + +static void +smtp_server_cmd_data_size_limit_exceeded(struct smtp_server_cmd_ctx *cmd) +{ + struct smtp_server_command *command = cmd->cmd; + + smtp_server_command_fail(command, 552, "5.2.3", + "Message size exceeds administrative limit"); +} + +bool smtp_server_cmd_data_check_size(struct smtp_server_cmd_ctx *cmd) +{ + struct smtp_server_connection *conn = cmd->conn; + const struct smtp_server_settings *set = &conn->set; + + i_assert(conn->state.state == SMTP_SERVER_STATE_DATA); + + if (conn->state.data_input == NULL) + return TRUE; + if (set->max_message_size == 0) + return TRUE; + if (conn->state.data_input->v_offset <= set->max_message_size) + return TRUE; + + smtp_server_cmd_data_size_limit_exceeded(cmd); + return FALSE; +} + +bool smtp_server_connection_data_check_state(struct smtp_server_cmd_ctx *cmd) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_command *command = cmd->cmd; + struct cmd_data_context *data_cmd = command->data; + + if (conn->state.data_chunks > 0 && conn->state.data_failed) { + // FIXME: should it even reply anything? RFC is unclear. + smtp_server_command_fail(command, 503, "5.5.0", + "Previous data chunk failed, issue RSET first"); + return FALSE; + } + + /* check valid MAIL */ + if (conn->state.trans == NULL + && conn->state.pending_mail_cmds == 0) { + smtp_server_command_fail(command, + 503, "5.5.0", "MAIL needed first"); + return FALSE; + } + if (conn->state.trans != NULL && + (conn->state.trans->params.body.type == + SMTP_PARAM_MAIL_BODY_TYPE_BINARYMIME) && + !data_cmd->chunking) { + /* RFC 3030, Section 3: + BINARYMIME cannot be used with the DATA command. If a DATA + command is issued after a MAIL command containing the + body-value of "BINARYMIME", a 503 "Bad sequence of commands" + response MUST be sent. The resulting state from this error + condition is indeterminate and the transaction MUST be reset + with the RSET command. */ + smtp_server_command_fail(command, + 503, "5.5.0", "DATA cannot be used with BINARYMIME"); + return FALSE; + } + + /* Can only decide whether we have valid recipients once there are no + pending RCPT commands */ + if (conn->state.pending_rcpt_cmds > 0) + return TRUE; + + /* special handling for LMTP */ + if (conn->set.protocol == SMTP_PROTOCOL_LMTP) { + /* check valid RCPT (at least one) */ + if (conn->state.trans == NULL || + !smtp_server_transaction_has_rcpt(conn->state.trans)) { + if (data_cmd->chunk_size > 0 && data_cmd->chunk_last) { + /* RFC 2033, Section 4.3: + If there were no previously successful RCPT + commands in the mail transaction, then the + BDAT LAST command returns zero replies. + */ + smtp_server_command_abort(&command); + } else { + /* RFC 2033, Section 4.2: + The additional restriction is that when there + have been no successful RCPT commands in the + mail transaction, the DATA command MUST fail + with a 503 reply code. + */ + smtp_server_command_fail(command, + 503, "5.5.0", "No valid recipients"); + } + return FALSE; + } + + } else { + /* check valid RCPT (at least one) */ + if (conn->state.trans == NULL || + !smtp_server_transaction_has_rcpt(conn->state.trans)) { + smtp_server_command_fail(command, + 554, "5.5.0", "No valid recipients"); + return FALSE; + } + } + return TRUE; +} + +static void +cmd_data_destroy(struct smtp_server_cmd_ctx *cmd, + struct cmd_data_context *data_cmd) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_command *command = cmd->cmd; + + i_assert(data_cmd != NULL); + + if (data_cmd->main_input == conn->state.data_input && + (data_cmd->chunk_last || + !smtp_server_command_replied_success(command))) { + /* clean up */ + i_stream_destroy(&conn->state.data_input); + i_stream_destroy(&conn->state.data_chain_input); + conn->state.data_chain = NULL; + } + + i_stream_unref(&data_cmd->chunk_input); +} + +static void +cmd_data_replied_one(struct smtp_server_cmd_ctx *cmd, + struct cmd_data_context *data_cmd ATTR_UNUSED) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_transaction *trans = conn->state.trans; + struct smtp_server_recipient *rcpt; + + if (trans == NULL || !array_is_created(&trans->rcpt_to)) + return; + + array_foreach_elem(&trans->rcpt_to, rcpt) + smtp_server_recipient_data_replied(rcpt); +} + +static void +cmd_data_replied(struct smtp_server_cmd_ctx *cmd, + struct cmd_data_context *data_cmd ATTR_UNUSED) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_command *command = cmd->cmd; + + i_assert(conn->state.pending_data_cmds > 0); + conn->state.pending_data_cmds--; + + smtp_server_command_input_lock(cmd); + if (!smtp_server_command_replied_success(command)) + smtp_server_command_input_unlock(cmd); +} + +static void +cmd_data_completed(struct smtp_server_cmd_ctx *cmd, + struct cmd_data_context *data_cmd) +{ + struct smtp_server_connection *conn = cmd->conn; + + i_assert(data_cmd != NULL); + i_stream_unref(&data_cmd->chunk_input); + + i_assert(conn->state.trans != NULL); + smtp_server_transaction_finished(conn->state.trans, cmd); + + /* reset state */ + smtp_server_connection_reset_state(conn); +} + +static void +cmd_data_chunk_replied(struct smtp_server_cmd_ctx *cmd, + struct cmd_data_context *data_cmd) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_command *command = cmd->cmd; + + i_assert(data_cmd != NULL); + + i_assert(conn->state.pending_data_cmds > 0); + conn->state.pending_data_cmds--; + + i_assert(smtp_server_command_is_replied(command)); + if (!smtp_server_command_replied_success(command) && + conn->state.pending_data_cmds == 0) + conn->state.data_failed = TRUE; +} + +static void +cmd_data_chunk_completed(struct smtp_server_cmd_ctx *cmd, + struct cmd_data_context *data_cmd ATTR_UNUSED) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_command *command = cmd->cmd; + + if (!smtp_server_command_replied_success(command)) + conn->state.data_failed = TRUE; +} + +static void +cmd_data_chunk_finish(struct smtp_server_cmd_ctx *cmd) +{ + struct smtp_server_command *command = cmd->cmd; + struct cmd_data_context *data_cmd = command->data; + + smtp_server_command_input_lock(cmd); + i_stream_unref(&data_cmd->chunk_input); + + /* re-check transaction state (for BDAT/B... command) */ + if (!smtp_server_connection_data_check_state(cmd)) + return; + + smtp_server_reply(cmd, 250, "2.0.0", + "Added %"PRIuUOFF_T" octets", data_cmd->chunk_size); +} + +static void cmd_data_input_error(struct smtp_server_cmd_ctx *cmd) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_command *command = cmd->cmd; + struct cmd_data_context *data_cmd = command->data; + struct istream *data_input = conn->state.data_input; + const char *error; + + conn->state.data_failed = TRUE; + + if (!data_cmd->client_input) { + if (!smtp_server_command_is_replied(command)) { + smtp_server_command_fail(command, + 400, "4.0.0", "Failed to add data"); + } + return; + } + + error = i_stream_get_disconnect_reason(data_input); + e_debug(conn->event, "Connection lost during data transfer: %s", error); + smtp_server_connection_close(&conn, error); +} + +static int cmd_data_do_handle_input(struct smtp_server_cmd_ctx *cmd) +{ + struct smtp_server_connection *conn = cmd->conn; + const struct smtp_server_callbacks *callbacks = conn->callbacks; + struct smtp_server_command *command = cmd->cmd; + struct cmd_data_context *data_cmd = command->data; + int ret; + + i_assert(data_cmd != NULL); + + i_assert(callbacks != NULL && + callbacks->conn_cmd_data_continue != NULL); + struct event_reason *reason = + smtp_server_connection_reason_begin(conn, "cmd_data"); + ret = callbacks->conn_cmd_data_continue(conn->context, + cmd, conn->state.trans); + event_reason_end(&reason); + if (ret >= 0) { + if (!smtp_server_cmd_data_check_size(cmd)) { + return -1; + } else if (!i_stream_have_bytes_left(conn->state.data_input)) { + e_debug(cmd->event, "End of data"); + smtp_server_transaction_received( + conn->state.trans, + conn->state.data_input->v_offset); + smtp_server_command_input_lock(cmd); + smtp_server_connection_timeout_stop(conn); + } else if (!data_cmd->chunk_last && + !i_stream_have_bytes_left(data_cmd->chunk_input)) { + e_debug(cmd->event, "End of chunk"); + cmd_data_chunk_finish(cmd); + } else if (i_stream_get_data_size( + conn->state.data_input) > 0) { + e_debug(cmd->event, "Not all client data read"); + smtp_server_connection_timeout_stop(cmd->conn); + } else { + smtp_server_connection_timeout_start(cmd->conn); + } + } else { + if (conn->state.data_input->stream_errno != 0) { + cmd_data_input_error(cmd); + return -1; + } + /* command is waiting for external event or it failed */ + i_assert(smtp_server_command_is_replied(command)); + } + + return 1; +} + +static int cmd_data_handle_input(struct smtp_server_cmd_ctx *cmd) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_command *command = cmd->cmd; + int ret; + + if (!smtp_server_cmd_data_check_size(cmd)) + return -1; + + smtp_server_connection_ref(conn); + smtp_server_command_ref(command); + + /* continue reading from client */ + ret = cmd_data_do_handle_input(cmd); + + smtp_server_command_unref(&command); + smtp_server_connection_unref(&conn); + + return ret; +} + +static void cmd_data_input(struct smtp_server_cmd_ctx *cmd) +{ + smtp_server_connection_timeout_reset(cmd->conn); + (void)cmd_data_handle_input(cmd); +} + +static void +cmd_data_next(struct smtp_server_cmd_ctx *cmd, + struct cmd_data_context *data_cmd) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_transaction *trans = conn->state.trans; + const struct smtp_server_callbacks *callbacks = conn->callbacks; + struct smtp_server_command *command = cmd->cmd; + + /* this command is next to send a reply */ + + i_assert(data_cmd != NULL); + i_assert(trans != NULL); + + /* DATA command stops the pipeline, so if it is next to reply, nothing + else can be pending. */ + i_assert(conn->state.pending_mail_cmds == 0 && + conn->state.pending_rcpt_cmds == 0); + + e_debug(cmd->event, "Command is next to be replied"); + + smtp_server_transaction_data_command(trans, cmd); + + /* check whether we have had successful mail and rcpt commands */ + if (!smtp_server_connection_data_check_state(cmd)) + return; + + if (data_cmd->chunk_last) { + /* LMTP 'DATA' and 'BDAT LAST' commands need to send more than + one reply per recipient */ + if (HAS_ALL_BITS(trans->flags, + SMTP_SERVER_TRANSACTION_FLAG_REPLY_PER_RCPT)) { + smtp_server_command_set_reply_count(command, + array_count(&trans->rcpt_to)); + } + } + + smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_DATA, NULL); + + /* chain data streams in the correct order */ + if (conn->state.data_chain != NULL) { + i_assert(data_cmd->chunk_input != NULL); + i_stream_chain_append(conn->state.data_chain, + data_cmd->chunk_input); + if (data_cmd->chunk_last) { + e_debug(cmd->event, "Seen the last chunk"); + i_stream_chain_append_eof(conn->state.data_chain); + } + } + + if (data_cmd->chunk_first) { + struct smtp_server_command *cmd_temp = command; + + e_debug(cmd->event, "First chunk"); + + smtp_server_command_ref(cmd_temp); + i_assert(callbacks != NULL && + callbacks->conn_cmd_data_begin != NULL); + i_assert(conn->state.data_input != NULL); + struct event_reason *reason = + smtp_server_connection_reason_begin(conn, "cmd_data"); + int ret = callbacks->conn_cmd_data_begin(conn->context, + cmd, conn->state.trans, conn->state.data_input); + event_reason_end(&reason); + if (ret < 0) { + i_assert(smtp_server_command_is_replied(cmd_temp)); + /* command failed */ + smtp_server_command_unref(&cmd_temp); + return; + } + if (!smtp_server_command_unref(&cmd_temp)) + return; + } + + if (smtp_server_command_is_replied(command)) { + smtp_server_command_input_unlock(cmd); + } else { + if (data_cmd->client_input) { + /* using input from client connection; + capture I/O event */ + smtp_server_connection_timeout_start(conn); + smtp_server_command_input_capture(cmd, cmd_data_input); + } + + (void)cmd_data_handle_input(cmd); + } +} + +static void +cmd_data_start_input(struct smtp_server_cmd_ctx *cmd, + struct cmd_data_context *data_cmd, struct istream *input) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_command *command = cmd->cmd; + + i_assert(data_cmd != NULL); + + if (input != NULL) { + i_assert(conn->state.data_input == NULL); + conn->state.data_input = input; + i_stream_ref(input); + } + data_cmd->main_input = conn->state.data_input; + + if (data_cmd->client_input) + smtp_server_command_input_lock(cmd); + + if (data_cmd->chunk_last) { + smtp_server_command_add_hook( + command, SMTP_SERVER_COMMAND_HOOK_COMPLETED, + cmd_data_completed, data_cmd); + } else { + smtp_server_command_add_hook( + command, SMTP_SERVER_COMMAND_HOOK_COMPLETED, + cmd_data_chunk_completed, data_cmd); + } + + if (conn->state.pending_mail_cmds == 0 && + conn->state.pending_rcpt_cmds == 0) { + cmd_data_next(cmd, data_cmd); + } else { + smtp_server_command_add_hook( + command, SMTP_SERVER_COMMAND_HOOK_NEXT, + cmd_data_next, data_cmd); + } +} + +/* DATA command */ + +static void +cmd_data_start(struct smtp_server_cmd_ctx *cmd, + struct cmd_data_context *data_cmd) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_transaction *trans = conn->state.trans; + struct istream *dot_input; + + /* called when all previous commands were finished */ + i_assert(conn->state.pending_mail_cmds == 0 && + conn->state.pending_rcpt_cmds == 0); + + if (trans != NULL) + smtp_server_transaction_data_command(trans, cmd); + + /* check whether we have had successful mail and rcpt commands */ + if (!smtp_server_connection_data_check_state(cmd)) + return; + + /* don't allow classic DATA when CHUNKING sequence was started before */ + if (conn->state.data_chunks > 0) { + smtp_server_command_fail(cmd->cmd, + 503, "5.5.0", "Bad sequence of commands"); + return; + } + + smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_DATA, NULL); + + /* confirm initial success to client */ + smtp_server_connection_reply_immediate(conn, 354, "OK"); + + /* start reading message data from client */ + dot_input = smtp_command_parse_data_with_dot(conn->smtp_parser); + cmd_data_start_input(cmd, data_cmd, dot_input); + i_stream_unref(&dot_input); +} + +void smtp_server_cmd_data(struct smtp_server_cmd_ctx *cmd, + const char *params) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_command *command = cmd->cmd; + struct cmd_data_context *data_cmd; + + /* data = "DATA" CRLF */ + if (*params != '\0') { + smtp_server_reply(cmd, + 501, "5.5.4", "Invalid parameters"); + return; + } + + smtp_server_command_input_lock(cmd); + + data_cmd = p_new(cmd->pool, struct cmd_data_context, 1); + data_cmd->chunk_first = TRUE; + data_cmd->chunk_last = TRUE; + data_cmd->client_input = TRUE; + command->data = data_cmd; + + smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_NEXT, + cmd_data_start, data_cmd); + smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_REPLIED_ONE, + cmd_data_replied_one, data_cmd); + smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_REPLIED, + cmd_data_replied, data_cmd); + smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_DESTROY, + cmd_data_destroy, data_cmd); + + conn->state.pending_data_cmds++; +} + +/* BDAT/B... commands */ + +void smtp_server_connection_data_chunk_init(struct smtp_server_cmd_ctx *cmd) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_command *command = cmd->cmd; + struct cmd_data_context *data_cmd; + + data_cmd = p_new(cmd->pool, struct cmd_data_context, 1); + data_cmd->chunking = TRUE; + data_cmd->chunk_first = (conn->state.data_chunks++ == 0); + command->data = data_cmd; + + smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_REPLIED, + cmd_data_chunk_replied, data_cmd); + smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_DESTROY, + cmd_data_destroy, data_cmd); + + conn->state.pending_data_cmds++; + + if (!conn->state.data_failed && conn->state.data_chain == NULL) { + i_assert(data_cmd->chunk_first); + i_assert(conn->state.data_chain_input == NULL); + conn->state.data_chain_input = + i_stream_create_chain(&conn->state.data_chain, + IO_BLOCK_SIZE); + } +} + +int smtp_server_connection_data_chunk_add(struct smtp_server_cmd_ctx *cmd, + struct istream *chunk, uoff_t chunk_size, bool chunk_last, + bool client_input) +{ + struct smtp_server_connection *conn = cmd->conn; + struct smtp_server_transaction *trans = conn->state.trans; + const struct smtp_server_settings *set = &conn->set; + struct smtp_server_command *command = cmd->cmd; + struct cmd_data_context *data_cmd = command->data; + uoff_t new_size; + + i_assert(data_cmd != NULL); + + if (trans != NULL) + smtp_server_transaction_data_command(trans, cmd); + + if (!smtp_server_connection_data_check_state(cmd)) + return -1; + + /* check message size increase early */ + new_size = conn->state.data_size + chunk_size; + if (new_size < conn->state.data_size || + (set->max_message_size > 0 && new_size > set->max_message_size)) { + smtp_server_cmd_data_size_limit_exceeded(cmd); + return -1; + } + conn->state.data_size = new_size; + + if (chunk_last) { + smtp_server_command_remove_hook( + command, SMTP_SERVER_COMMAND_HOOK_REPLIED, + cmd_data_chunk_replied); + smtp_server_command_add_hook( + command, SMTP_SERVER_COMMAND_HOOK_REPLIED, + cmd_data_replied, data_cmd); + } + + data_cmd->chunk_input = chunk; + data_cmd->chunk_size = chunk_size; + data_cmd->chunk_last = chunk_last; + data_cmd->client_input = client_input; + i_stream_ref(chunk); + + cmd_data_start_input(cmd, data_cmd, conn->state.data_chain_input); + i_stream_unref(&conn->state.data_chain_input); + return 0; +} + +/* BDAT command */ + +void smtp_server_cmd_bdat(struct smtp_server_cmd_ctx *cmd, + const char *params) +{ + struct smtp_server_connection *conn = cmd->conn; + struct istream *input = NULL; + uoff_t size = 0; + const char *const *argv; + bool chunk_last = FALSE; + int ret = 1; + + if ((conn->set.capabilities & SMTP_CAPABILITY_CHUNKING) == 0) { + smtp_server_reply(cmd, + 502, "5.5.1", "Unsupported command"); + return; + } + + smtp_server_connection_data_chunk_init(cmd); + + /* bdat-cmd = "BDAT" SP chunk-size [ SP end-marker ] CR LF + chunk-size = 1*DIGIT + end-marker = "LAST" + */ + argv = t_strsplit(params, " "); + if (argv[0] == NULL || str_to_uoff(argv[0], &size) < 0) { + smtp_server_reply(cmd, + 501, "5.5.4", "Invalid chunk size parameter"); + size = 0; + ret = -1; + } else if (argv[1] != NULL) { + if (argv[2] != NULL) { + smtp_server_reply(cmd, + 501, "5.5.4", "Invalid parameters"); + ret = -1; + } else if (strcasecmp(argv[1], "LAST") != 0) { + smtp_server_reply(cmd, + 501, "5.5.4", "Invalid end marker parameter"); + ret = -1; + } else { + chunk_last = TRUE; + } + } + + if (ret > 0 || (size > 0 && !conn->disconnected)) { + /* Read/skip data even in case of error, as long as size is + known and connection is still usable. */ + input = smtp_command_parse_data_with_size(conn->smtp_parser, + size); + } + + if (ret < 0) { + i_stream_unref(&input); + return; + } + + (void)smtp_server_connection_data_chunk_add(cmd, + input, size, chunk_last, TRUE); + i_stream_unref(&input); +} |