From: Stephan Bosch Date: Sun, 6 Nov 2016 21:00:08 +0000 (+0100) Subject: lib-smtp: Implemented SMTP command reply parser. X-Git-Tag: 2.3.0.rc1~362 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a8433392e8a5ddfe8a125716f62b2a4dc8f1f01e;p=thirdparty%2Fdovecot%2Fcore.git lib-smtp: Implemented SMTP command reply parser. --- diff --git a/src/lib-smtp/Makefile.am b/src/lib-smtp/Makefile.am index 257315125d..e0edd75741 100644 --- a/src/lib-smtp/Makefile.am +++ b/src/lib-smtp/Makefile.am @@ -16,6 +16,8 @@ libsmtp_la_SOURCES = \ smtp-syntax.c \ smtp-common.c \ smtp-params.c \ + smtp-reply.c \ + smtp-reply-parser.c \ lmtp-client.c \ smtp-submit-settings.c \ smtp-submit.c @@ -26,6 +28,8 @@ headers = \ smtp-syntax.h \ smtp-common.h \ smtp-params.h \ + smtp-reply.h \ + smtp-reply-parser.h \ lmtp-client.h \ smtp-submit-settings.h \ smtp-submit.h @@ -35,7 +39,8 @@ pkginc_lib_HEADERS = $(headers) test_programs = \ test-smtp-address \ - test-smtp-params + test-smtp-params \ + test-smtp-reply-parser test_nocheck_programs = \ test-smtp-submit @@ -77,6 +82,11 @@ test_smtp_params_LDFLAGS = -export-dynamic test_smtp_params_LDADD = $(test_libs) test_smtp_params_DEPENDENCIES = $(test_deps) +test_smtp_reply_parser_SOURCES = test-smtp-reply-parser.c +test_smtp_reply_parser_LDFLAGS = -export-dynamic +test_smtp_reply_parser_LDADD = $(test_libs) +test_smtp_reply_parser_DEPENDENCIES = $(test_deps) + test_smtp_submit_SOURCES = test-smtp-submit.c test_smtp_submit_LDFLAGS = -export-dynamic test_smtp_submit_LDADD = $(test_libs) diff --git a/src/lib-smtp/smtp-reply-parser.c b/src/lib-smtp/smtp-reply-parser.c new file mode 100644 index 0000000000..ecf55f7793 --- /dev/null +++ b/src/lib-smtp/smtp-reply-parser.c @@ -0,0 +1,631 @@ +/* Copyright (c) 2013-2017 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "str.h" +#include "strfuncs.h" +#include "istream.h" +#include "smtp-parser.h" + +#include "smtp-reply-parser.h" + +#include + +/* From RFC 5321: + + Reply-line = *( Reply-code "-" [ textstring ] CRLF ) + Reply-code [ SP textstring ] CRLF + Reply-code = %x32-35 %x30-35 %x30-39 + textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII + + Greeting = ( "220 " (Domain / address-literal) + [ SP textstring ] CRLF ) / + ( "220-" (Domain / address-literal) + [ SP textstring ] CRLF + *( "220-" [ textstring ] CRLF ) + "220" [ SP textstring ] CRLF ) + + ehlo-ok-rsp = ( "250" SP Domain [ SP ehlo-greet ] CRLF ) + / ( "250-" Domain [ SP ehlo-greet ] CRLF + *( "250-" ehlo-line CRLF ) + "250" SP ehlo-line CRLF ) + ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127) + ; string of any characters other than CR or LF + ehlo-line = ehlo-keyword *( SP ehlo-param ) + ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-") + ; additional syntax of ehlo-params depends on + ; ehlo-keyword + ehlo-param = 1*(%d33-126) + ; any CHAR excluding and all + ; control characters (US-ASCII 0-31 and 127 + ; inclusive) + + From RFC 2034: + + status-code ::= class "." subject "." detail + class ::= "2" / "4" / "5" + subject ::= 1*3digit + detail ::= 1*3digit + */ + +enum smtp_reply_parser_state { + SMTP_REPLY_PARSE_STATE_INIT = 0, + SMTP_REPLY_PARSE_STATE_CODE, + SMTP_REPLY_PARSE_STATE_SEP, + SMTP_REPLY_PARSE_STATE_TEXT, + SMTP_REPLY_PARSE_STATE_EHLO_SPACE, + SMTP_REPLY_PARSE_STATE_EHLO_GREET, + SMTP_REPLY_PARSE_STATE_CR, + SMTP_REPLY_PARSE_STATE_CRLF, + SMTP_REPLY_PARSE_STATE_LF +}; + +struct smtp_reply_parser_state_data { + enum smtp_reply_parser_state state; + unsigned int line; + + struct smtp_reply *reply; + ARRAY_TYPE(const_string) reply_lines; + size_t reply_size; + + bool last_line:1; +}; + +struct smtp_reply_parser { + struct istream *input; + + size_t max_reply_size; + + const unsigned char *begin, *cur, *end; + + string_t *strbuf; + + struct smtp_reply_parser_state_data state; + pool_t reply_pool; + + char *error; + + bool enhanced_codes:1; + bool ehlo:1; +}; + +static inline void ATTR_FORMAT(2, 3) +smtp_reply_parser_error(struct smtp_reply_parser *parser, + const char *format, ...) +{ + va_list args; + + i_free(parser->error); + + va_start(args, format); + parser->error = i_strdup_vprintf(format, args); + va_end(args); +} + +struct smtp_reply_parser * +smtp_reply_parser_init(struct istream *input, size_t max_reply_size) +{ + struct smtp_reply_parser *parser; + + parser = i_new(struct smtp_reply_parser, 1); + parser->max_reply_size = + (max_reply_size > 0 ? max_reply_size : (size_t)-1); + parser->input = input; + i_stream_ref(input); + parser->strbuf = str_new(default_pool, 128); + return parser; +} + +void smtp_reply_parser_deinit(struct smtp_reply_parser **_parser) +{ + struct smtp_reply_parser *parser = *_parser; + + *_parser = NULL; + + str_free(&parser->strbuf); + pool_unref(&parser->reply_pool); + i_stream_unref(&parser->input); + i_free(parser->error); + i_free(parser); +} + +void smtp_reply_parser_set_stream(struct smtp_reply_parser *parser, + struct istream *input) +{ + i_stream_unref(&parser->input); + if (input != NULL) { + parser->input = input; + i_stream_ref(parser->input); + } +} + +static void +smtp_reply_parser_restart(struct smtp_reply_parser *parser) +{ + str_truncate(parser->strbuf, 0); + pool_unref(&parser->reply_pool); + i_zero(&parser->state); + + parser->reply_pool = pool_alloconly_create("smtp_reply", 256); + parser->state.reply = p_new(parser->reply_pool, struct smtp_reply, 1); + p_array_init(&parser->state.reply_lines, parser->reply_pool, 8); + +} + +static int smtp_reply_parse_code +(struct smtp_reply_parser *parser, unsigned int *code_r) +{ + const unsigned char *first = parser->cur; + const unsigned char *p; + + /* Reply-code = %x32-35 %x30-35 %x30-39 + */ + while (parser->cur < parser->end && i_isdigit(*parser->cur)) + parser->cur++; + + if (str_len(parser->strbuf) + (parser->cur-first) > 3) + return -1; + + str_append_n(parser->strbuf, first, parser->cur - first); + if (parser->cur == parser->end) + return 0; + if (str_len(parser->strbuf) != 3) + return -1; + p = str_data(parser->strbuf); + if (p[0] < '2' || p[0] > '5' || p[1] > '5') + return -1; + *code_r = (p[0] - '0')*100 + (p[1] - '0')*10 + (p[2] - '0'); + str_truncate(parser->strbuf, 0); + return 1; +} + +static int smtp_reply_parse_textstring(struct smtp_reply_parser *parser) +{ + const unsigned char *first = parser->cur; + + /* textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII + */ + while (parser->cur < parser->end && smtp_char_is_textstr(*parser->cur)) + parser->cur++; + + if (((parser->cur-first) + parser->state.reply_size + + str_len(parser->strbuf)) > parser->max_reply_size) { + smtp_reply_parser_error(parser, + "Reply exceeds size limit"); + return -1; + } + + str_append_n(parser->strbuf, first, parser->cur - first); + if (parser->cur == parser->end) + return 0; + return 1; +} + +static int smtp_reply_parse_ehlo_domain(struct smtp_reply_parser *parser) +{ + const unsigned char *first = parser->cur; + + /* Domain [ SP ... + */ + while (parser->cur < parser->end && *parser->cur != ' ' && + smtp_char_is_textstr(*parser->cur)) + parser->cur++; + + if (((parser->cur-first) + parser->state.reply_size + + str_len(parser->strbuf)) > parser->max_reply_size) { + smtp_reply_parser_error(parser, + "Reply exceeds size limit"); + return -1; + } + str_append_n(parser->strbuf, first, parser->cur - first); + if (parser->cur == parser->end) + return 0; + return 1; +} + +static int smtp_reply_parse_ehlo_greet(struct smtp_reply_parser *parser) +{ + const unsigned char *first = parser->cur; + + /* ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127) + * + * The greet is not supposed to be empty, but we don't really care + */ + + if (parser->cur == parser->end) + return 0; + if (smtp_char_is_ehlo_greet(*parser->cur)) { + for (;;) { + while (parser->cur < parser->end && + smtp_char_is_textstr(*parser->cur)) + parser->cur++; + + if (((parser->cur-first) + parser->state.reply_size + + str_len(parser->strbuf)) > + parser->max_reply_size) { + smtp_reply_parser_error(parser, + "Reply exceeds size limit"); + return -1; + } + + /* sanitize bad characters */ + str_append_n(parser->strbuf, + first, parser->cur - first); + + if (parser->cur == parser->end) + return 0; + if (!smtp_char_is_ehlo_greet(*parser->cur)) + break; + str_append_c(parser->strbuf, ' '); + parser->cur++; + first = parser->cur; + } + } + return 1; +} + +static inline const char *_chr_sanitize(unsigned char c) +{ + if (c >= 0x20 && c < 0x7F) + return t_strdup_printf("'%c'", c); + return t_strdup_printf("0x%02x", c); +} + +static void +smtp_reply_parse_enhanced_code(struct smtp_reply_parser *parser, + const char **pos) +{ + const char *p = *pos; + unsigned int digits, x, y, z; + unsigned int prevx = parser->state.reply->enhanced_code.x, + prevy = parser->state.reply->enhanced_code.y, + prevz = parser->state.reply->enhanced_code.z; + + if (prevx == 9) + return; /* failed on earlier line */ + + parser->state.reply->enhanced_code.x = 9; + parser->state.reply->enhanced_code.y = 0; + parser->state.reply->enhanced_code.z = 0; + + /* status-code ::= class "." subject "." detail + class ::= "2" / "4" / "5" + subject ::= 1*3digit + detail ::= 1*3digit + */ + + /* class */ + if (p[1] != '.' || (p[0] != '2' && p[0] != '4' && p[0] != '5')) + return; + x = p[0] - '0'; + p += 2; + + /* subject */ + digits = 0; + y = 0; + while (*p != '\0' && i_isdigit(*p) && digits++ < 3) { + y = y*10 + (*p - '0'); + p++; + } + if (digits == 0 || *p != '.') + return; + p++; + + /* detail */ + digits = 0; + z = 0; + while (*p != '\0' && i_isdigit(*p) && digits++ < 3) { + z = z*10 + (*p - '0'); + p++; + } + if (digits == 0 || (*p != ' ' && *p != '\r' && *p != '\n')) + return; + p++; + + /* code is syntactically valid; strip code from textstring */ + *pos = p; + + /* check for match with status */ + if (x != parser->state.reply->status / 100) { + /* ignore code */ + return; + } + + /* check for code consistency */ + if (parser->state.line > 0 && + (prevx != x || prevy != y || prevz != z)) { + /* ignore code */ + return; + } + + parser->state.reply->enhanced_code.x = x; + parser->state.reply->enhanced_code.y = y; + parser->state.reply->enhanced_code.z = z; +} + +static void smtp_reply_parser_finish_line(struct smtp_reply_parser *parser) +{ + const char *text = str_c(parser->strbuf); + + if (parser->enhanced_codes && str_len(parser->strbuf) > 5) { + smtp_reply_parse_enhanced_code(parser, &text); + } + + parser->state.line++; + parser->state.reply_size += str_len(parser->strbuf); + text = p_strdup(parser->reply_pool, text); + array_append(&parser->state.reply_lines, &text, 1); + str_truncate(parser->strbuf, 0); +} + +static int smtp_reply_parse_more(struct smtp_reply_parser *parser) +{ + unsigned int status; + int ret; + + /* + Reply-line = *( Reply-code "-" [ textstring ] CRLF ) + Reply-code [ SP textstring ] CRLF + Reply-code = %x32-35 %x30-35 %x30-39 + + ehlo-ok-rsp = ( "250" SP Domain [ SP ehlo-greet ] CRLF ) + / ( "250-" Domain [ SP ehlo-greet ] CRLF + *( "250-" ehlo-line CRLF ) + "250" SP ehlo-line CRLF ) + */ + + for (;;) { + switch (parser->state.state) { + case SMTP_REPLY_PARSE_STATE_INIT: + smtp_reply_parser_restart(parser); + parser->state.state = SMTP_REPLY_PARSE_STATE_CODE; + /* fall through */ + /* Reply-code */ + case SMTP_REPLY_PARSE_STATE_CODE: + if ((ret=smtp_reply_parse_code(parser, &status)) <= 0) { + if (ret < 0) { + smtp_reply_parser_error(parser, + "Invalid status code in reply"); + } + return ret; + } + if (parser->state.line == 0) { + parser->state.reply->status = status; + } else if (status != parser->state.reply->status) { + smtp_reply_parser_error(parser, + "Inconsistent status codes in reply"); + return -1; + } + parser->state.state = SMTP_REPLY_PARSE_STATE_SEP; + if (parser->cur == parser->end) + return 0; + /* fall through */ + /* "-" / SP / CRLF */ + case SMTP_REPLY_PARSE_STATE_SEP: + switch (*parser->cur) { + /* "-" [ textstring ] CRLF */ + case '-': + parser->cur++; + parser->state.last_line = FALSE; + parser->state.state = + SMTP_REPLY_PARSE_STATE_TEXT; + break; + /* SP [ textstring ] CRLF ; allow missing text */ + case ' ': + parser->cur++; + parser->state.state = + SMTP_REPLY_PARSE_STATE_TEXT; + parser->state.last_line = TRUE; + break; + /* CRLF */ + case '\r': + case '\n': + parser->state.last_line = TRUE; + parser->state.state = SMTP_REPLY_PARSE_STATE_CR; + break; + default: + smtp_reply_parser_error(parser, + "Encountered unexpected %s after reply status code", + _chr_sanitize(*parser->cur)); + return -1; + } + if (parser->state.state != SMTP_REPLY_PARSE_STATE_TEXT) + break; + /* fall through */ + /* textstring / (Domain [ SP ehlo-greet ]) */ + case SMTP_REPLY_PARSE_STATE_TEXT: + if (parser->ehlo && + parser->state.reply->status == 250 && + parser->state.line == 0) { + /* handle first line of EHLO success response + differently because it can contain control + characters (WHY??!) */ + if ((ret=smtp_reply_parse_ehlo_domain(parser)) <= 0) + return ret; + parser->state.state = + SMTP_REPLY_PARSE_STATE_EHLO_SPACE; + if (parser->cur == parser->end) + return 0; + break; + } + if ((ret=smtp_reply_parse_textstring(parser)) <= 0) + return ret; + parser->state.state = SMTP_REPLY_PARSE_STATE_CR; + if (parser->cur == parser->end) + return 0; + /* fall through */ + /* CR */ + case SMTP_REPLY_PARSE_STATE_CR: + if (*parser->cur == '\r') { + parser->cur++; + parser->state.state = + SMTP_REPLY_PARSE_STATE_CRLF; + } else { + parser->state.state = + SMTP_REPLY_PARSE_STATE_LF; + } + if (parser->cur == parser->end) + return 0; + /* fall through */ + /* CRLF / LF */ + case SMTP_REPLY_PARSE_STATE_CRLF: + case SMTP_REPLY_PARSE_STATE_LF: + if (*parser->cur != '\n') { + if (parser->state.state == + SMTP_REPLY_PARSE_STATE_CRLF) { + smtp_reply_parser_error(parser, + "Encountered stray CR in reply text"); + } else { + smtp_reply_parser_error(parser, + "Encountered stray %s in reply text", + _chr_sanitize(*parser->cur)); + } + return -1; + } + parser->cur++; + smtp_reply_parser_finish_line(parser); + if (parser->state.last_line) { + parser->state.state = + SMTP_REPLY_PARSE_STATE_INIT; + return 1; + } + parser->state.state = SMTP_REPLY_PARSE_STATE_CODE; + break; + /* SP ehlo-greet */ + case SMTP_REPLY_PARSE_STATE_EHLO_SPACE: + if (*parser->cur != ' ') { + parser->state.state = SMTP_REPLY_PARSE_STATE_CR; + break; + } + parser->cur++; + str_append_c(parser->strbuf, ' '); + parser->state.state = SMTP_REPLY_PARSE_STATE_EHLO_GREET; + if (parser->cur == parser->end) + return 0; + /* fall through */ + /* ehlo-greet */ + case SMTP_REPLY_PARSE_STATE_EHLO_GREET: + if ((ret=smtp_reply_parse_ehlo_greet(parser)) <= 0) + return ret; + parser->state.state = SMTP_REPLY_PARSE_STATE_CR; + if (parser->cur == parser->end) + return 0; + break; + default: + i_unreached(); + } + } + + i_unreached(); + return -1; +} + +static int smtp_reply_parse(struct smtp_reply_parser *parser) +{ + size_t size; + int ret; + + while ((ret = i_stream_read_more(parser->input, + &parser->begin, &size)) > 0) { + parser->cur = parser->begin; + parser->end = parser->cur + size; + + if ((ret = smtp_reply_parse_more(parser)) < 0) + return -1; + + i_stream_skip(parser->input, parser->cur - parser->begin); + if (ret > 0) + return 1; + } + + i_assert(ret != -2); + if (ret < 0) { + i_assert(parser->input->eof); + if (parser->input->stream_errno == 0) { + if (parser->state.state == SMTP_REPLY_PARSE_STATE_INIT) + return 0; + smtp_reply_parser_error(parser, + "Premature end of input"); + } else { + smtp_reply_parser_error(parser, + "Stream error: %s", + i_stream_get_error(parser->input)); + } + } + return ret; +} + +int smtp_reply_parse_next(struct smtp_reply_parser *parser, + bool enhanced_codes, struct smtp_reply **reply_r, + const char **error_r) +{ + int ret; + + i_assert(parser->state.state == SMTP_REPLY_PARSE_STATE_INIT || + (parser->enhanced_codes == enhanced_codes && !parser->ehlo)); + + parser->enhanced_codes = enhanced_codes; + parser->ehlo = FALSE; + + i_free_and_null(parser->error); + + /* + Reply-line = *( Reply-code "-" [ textstring ] CRLF ) + Reply-code [ SP textstring ] CRLF + Reply-code = %x32-35 %x30-35 %x30-39 + textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII + + Greeting is not handled specially here. + */ + if ((ret=smtp_reply_parse(parser)) <= 0) { + *error_r = parser->error; + return ret; + } + + parser->state.state = SMTP_REPLY_PARSE_STATE_INIT; + parser->state.reply->text_lines = + array_idx(&parser->state.reply_lines, 0); + *reply_r = parser->state.reply; + return 1; +} + +int smtp_reply_parse_ehlo(struct smtp_reply_parser *parser, + struct smtp_reply **reply_r, const char **error_r) +{ + int ret; + + i_assert(parser->state.state == SMTP_REPLY_PARSE_STATE_INIT || + (!parser->enhanced_codes && parser->ehlo)); + + parser->enhanced_codes = FALSE; + parser->ehlo = TRUE; + + i_free_and_null(parser->error); + + /* + ehlo-ok-rsp = ( "250" SP Domain [ SP ehlo-greet ] CRLF ) + / ( "250-" Domain [ SP ehlo-greet ] CRLF + *( "250-" ehlo-line CRLF ) + "250" SP ehlo-line CRLF ) + ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127) + ; string of any characters other than CR or LF + ehlo-line = ehlo-keyword *( SP ehlo-param ) + ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-") + ; additional syntax of ehlo-params depends on + ; ehlo-keyword + ehlo-param = 1*(%d33-126) + ; any CHAR excluding and all + ; control characters (US-ASCII 0-31 and 127 + ; inclusive) + */ + if ((ret=smtp_reply_parse(parser)) <= 0) { + *error_r = parser->error; + return ret; + } + + parser->state.state = SMTP_REPLY_PARSE_STATE_INIT; + parser->state.reply->text_lines = + array_idx(&parser->state.reply_lines, 0); + *reply_r = parser->state.reply; + return 1; +} diff --git a/src/lib-smtp/smtp-reply-parser.h b/src/lib-smtp/smtp-reply-parser.h new file mode 100644 index 0000000000..bbafd7542b --- /dev/null +++ b/src/lib-smtp/smtp-reply-parser.h @@ -0,0 +1,21 @@ +#ifndef SMTP_REPLY_PARSER_H +#define SMTP_REPLY_PARSER_H + +#include "smtp-reply.h" + +struct smtp_reply_parser; + +struct smtp_reply_parser * +smtp_reply_parser_init(struct istream *input, size_t max_reply_size); +void smtp_reply_parser_deinit(struct smtp_reply_parser **_parser); + +void smtp_reply_parser_set_stream(struct smtp_reply_parser *parser, + struct istream *input); + +int smtp_reply_parse_next(struct smtp_reply_parser *parser, + bool enhanced_codes, struct smtp_reply **reply_r, + const char **error_r); +int smtp_reply_parse_ehlo(struct smtp_reply_parser *parser, + struct smtp_reply **reply_r, const char **error_r); + +#endif diff --git a/src/lib-smtp/smtp-reply.c b/src/lib-smtp/smtp-reply.c new file mode 100644 index 0000000000..7b6ff4bc10 --- /dev/null +++ b/src/lib-smtp/smtp-reply.c @@ -0,0 +1,124 @@ +/* Copyright (c) 2013-2017 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "str.h" +#include "strfuncs.h" +#include "smtp-reply.h" + +void smtp_reply_init(struct smtp_reply *reply, unsigned int status, + const char *text) +{ + const char **text_lines = t_new(const char *, 2); + + text_lines[0] = text; + text_lines[1] = NULL; + + i_zero(reply); + reply->status = status; + reply->text_lines = text_lines; +} + +void smtp_reply_printf(struct smtp_reply *reply, unsigned int status, + const char *format, ...) +{ + va_list args; + + va_start(args, format); + smtp_reply_init(reply, status, t_strdup_vprintf(format, args)); + va_end(args); +} + +const char * +smtp_reply_get_enh_code(const struct smtp_reply *reply) +{ + if (reply->enhanced_code.x < 2) + return NULL; + if (reply->enhanced_code.x >= 6) + return NULL; + + return t_strdup_printf("%u.%u.%u", + reply->enhanced_code.x, reply->enhanced_code.y, reply->enhanced_code.z); +} + +void +smtp_reply_write(string_t *out, const struct smtp_reply *reply) +{ + const char *prefix, *enh_code; + const char *const *lines; + + i_assert(reply->status < 560); + i_assert(reply->enhanced_code.x < 6); + + prefix = t_strdup_printf("%03u", reply->status); + enh_code = smtp_reply_get_enh_code(reply); + + if (reply->text_lines == NULL || *reply->text_lines == NULL) { + str_append(out, prefix); + if (enh_code != NULL) { + str_append_c(out, ' '); + str_append(out, enh_code); + } + str_append(out, " \r\n"); + return; + } + + lines = reply->text_lines; + while (*lines != NULL) { + str_append(out, prefix); + if (*(lines+1) == NULL) + str_append_c(out, ' '); + else + str_append_c(out, '-'); + if (enh_code != NULL) { + str_append(out, enh_code); + str_append_c(out, ' '); + } + str_append(out, *lines); + str_append(out, "\r\n"); + lines++; + } +} + +const char *smtp_reply_log(const struct smtp_reply *reply) +{ + const char *const *lines; + string_t *msg = t_str_new(256); + + if (smtp_reply_is_remote(reply)) { + const char *enh_code = smtp_reply_get_enh_code(reply); + + str_printfa(msg, "%03u", reply->status); + if (enh_code != NULL) { + str_append_c(msg, ' '); + str_append(msg, enh_code); + } + } + + lines = reply->text_lines; + while (*lines != NULL) { + if (str_len(msg) > 0) + str_append_c(msg, ' '); + str_append(msg, *lines); + lines++; + } + return str_c(msg); +} + +void smtp_reply_copy(pool_t pool, struct smtp_reply *dst, + const struct smtp_reply *src) +{ + *dst = *src; + dst->text_lines = p_strarray_dup(pool, src->text_lines); +} + +struct smtp_reply *smtp_reply_clone(pool_t pool, + const struct smtp_reply *src) +{ + struct smtp_reply *dst; + + dst = p_new(pool, struct smtp_reply, 1); + smtp_reply_copy(pool, dst, src); + + return dst; +} diff --git a/src/lib-smtp/smtp-reply.h b/src/lib-smtp/smtp-reply.h new file mode 100644 index 0000000000..a9b72361d3 --- /dev/null +++ b/src/lib-smtp/smtp-reply.h @@ -0,0 +1,63 @@ +#ifndef SMTP_REPLY_H +#define SMTP_REPLY_H + +struct smtp_reply_enhanced_code { + /* x:class, y:subject, z:detail; + x==0 means no enhanced code present + x==9 means invalid/missing enhanced code in reply + */ + unsigned int x, y, z; +}; + +struct smtp_reply { + unsigned int status; + + struct smtp_reply_enhanced_code enhanced_code; + + const char *const *text_lines; +}; + +#define SMTP_REPLY_ENH_CODE(x, y, z) \ + (const struct smtp_reply_enhanced_code){(x), (y), (z)} +#define SMTP_REPLY_ENH_CODE_NONE SMTP_REPLY_ENH_CODE(0, 0, 0) + +static inline bool +smtp_reply_has_enhanced_code(const struct smtp_reply *reply) +{ + return reply->enhanced_code.x > 1 && reply->enhanced_code.x < 6; +} + +static inline bool +smtp_reply_is_success(const struct smtp_reply *reply) +{ + return ((reply->status / 100) == 2); +} + +static inline bool +smtp_reply_is_remote(const struct smtp_reply *reply) +{ + return (reply->status >= 200 && reply->status < 560); +} + +static inline bool +smtp_reply_is_temp_fail(const struct smtp_reply *reply) +{ + return ((reply->status / 100) == 4); +} + +void smtp_reply_init(struct smtp_reply *reply, unsigned int status, + const char *text); +void smtp_reply_printf(struct smtp_reply *reply, unsigned int status, + const char *format, ...) ATTR_FORMAT(3, 4); + +const char * +smtp_reply_get_enh_code(const struct smtp_reply *reply); +void smtp_reply_write(string_t *out, const struct smtp_reply *reply); +const char *smtp_reply_log(const struct smtp_reply *reply); + +void smtp_reply_copy(pool_t pool, struct smtp_reply *dst, + const struct smtp_reply *src); +struct smtp_reply *smtp_reply_clone(pool_t pool, + const struct smtp_reply *src); + +#endif diff --git a/src/lib-smtp/test-smtp-reply-parser.c b/src/lib-smtp/test-smtp-reply-parser.c new file mode 100644 index 0000000000..96da8b24e3 --- /dev/null +++ b/src/lib-smtp/test-smtp-reply-parser.c @@ -0,0 +1,262 @@ +/* Copyright (c) 2013-2017 Dovecot authors, see the included COPYING file */ + +#include "test-lib.h" +#include "array.h" +#include "buffer.h" +#include "str.h" +#include "str-sanitize.h" +#include "istream.h" +#include "ostream.h" +#include "test-common.h" +#include "smtp-reply-parser.h" + +#include + +struct smtp_reply_parse_valid_test { + const char *reply; + unsigned int status; + bool ehlo; + size_t max_size; + struct { + unsigned int x, y, z; + } enhanced_code; + const char *const *text_lines; +}; + +/* Valid reply tests */ + +static const struct smtp_reply_parse_valid_test +valid_reply_parse_tests[] = { + { + .reply = "220\r\n", + .status = 220, + .text_lines = (const char *[]){ "", NULL } + },{ + .reply = "220 \r\n", + .status = 220, + .text_lines = (const char *[]){ "", NULL } + },{ + .reply = "220 OK\r\n", + .status = 220, + .text_lines = (const char *[]){ "OK", NULL } + },{ + .reply = "550 Requested action not taken: mailbox unavailable\r\n", + .status = 550, + .text_lines = (const char *[]) + { "Requested action not taken: mailbox unavailable", NULL } + },{ + .reply = + "250-smtp.example.com Hello client.example.org [10.0.0.1]\r\n" + "250-SIZE 52428800\r\n" + "250-PIPELINING\r\n" + "250-STARTTLS\r\n" + "250 HELP\r\n", + .ehlo = TRUE, + .status = 250, + .text_lines = (const char *[]) { + "smtp.example.com Hello client.example.org [10.0.0.1]", + "SIZE 52428800", + "PIPELINING", + "STARTTLS", + "HELP", + NULL + } + },{ + .reply = + "250-smtp.example.com We got some nice '\x03' and '\x04'\r\n" + "250 HELP\r\n", + .ehlo = TRUE, + .status = 250, + .text_lines = (const char *[]) { + "smtp.example.com We got some nice ' ' and ' '", + "HELP", + NULL + } + },{ + .reply = + "250 smtp.example.com We got some nice '\x08'\r\n", + .ehlo = TRUE, + .status = 250, + .text_lines = (const char *[]) { + "smtp.example.com We got some nice ' '", + NULL + } + },{ + .reply = "250 2.1.0 Originator ok\r\n", + .status = 250, + .enhanced_code = { 2, 1, 0 }, + .text_lines = (const char *[]){ + "Originator ok", NULL + } + },{ + .reply = + "551-5.7.1 Forwarding to remote hosts disabled\r\n" + "551 5.7.1 Select another host to act as your forwarder\r\n", + .status = 551, + .enhanced_code = { 5, 7, 1 }, + .text_lines = (const char *[]) { + "Forwarding to remote hosts disabled", + "Select another host to act as your forwarder", + NULL + } + } +}; + +unsigned int valid_reply_parse_test_count = + N_ELEMENTS(valid_reply_parse_tests); + +static void test_smtp_reply_parse_valid(void) +{ + unsigned int i; + + for (i = 0; i < valid_reply_parse_test_count; i++) T_BEGIN { + struct istream *input; + const struct smtp_reply_parse_valid_test *test; + struct smtp_reply_parser *parser; + struct smtp_reply *reply; + const char *error; + int ret; + + test = &valid_reply_parse_tests[i]; + input = i_stream_create_from_data(test->reply, strlen(test->reply)); + parser = smtp_reply_parser_init(input, test->max_size); + i_stream_unref(&input); + + test_begin(t_strdup_printf("smtp reply valid [%d]", i)); + + if (test->ehlo) { + while ((ret=smtp_reply_parse_ehlo + (parser, &reply, &error)) > 0) { + } + } else { + while ((ret=smtp_reply_parse_next + (parser, test->enhanced_code.x > 0, &reply, &error)) > 0) { + } + } + + test_out_reason("parse success", ret == 0, error); + + if (ret == 0) { + /* verify last response only */ + test_out(t_strdup_printf("reply->status = %d", test->status), + reply->status == test->status); + if (test->enhanced_code.x > 0) { + test_out(t_strdup_printf("reply->enhanced_code = %d.%d.%d", + test->enhanced_code.x, test->enhanced_code.y, test->enhanced_code.z), + (reply->enhanced_code.x == test->enhanced_code.x && + reply->enhanced_code.y == test->enhanced_code.y && + reply->enhanced_code.z == test->enhanced_code.z)); + } + if (test->text_lines != NULL) { + const char *const *line = test->text_lines; + const char *const *reply_line = reply->text_lines; + unsigned int index = 0; + + while (*line != NULL) { + if (*reply_line == NULL) { + test_out( + t_strdup_printf("reply->text_lines[%d] = NULL", index), + FALSE); + break; + } + test_out(t_strdup_printf( + "reply->text_lines[%d] = \"%s\"", index, *reply_line), + strcmp(*line, *reply_line) == 0); + line++; + reply_line++; + index++; + } + } else { + test_out("reply->text_lines = NULL", reply->text_lines == NULL); + } + } + test_end(); + smtp_reply_parser_deinit(&parser); + } T_END; +} + +struct smtp_reply_parse_invalid_test { + const char *reply; + bool ehlo; + size_t max_size; +}; + +static const struct smtp_reply_parse_invalid_test + invalid_reply_parse_tests[] = { + { + .reply = "22X OK\r\n" + },{ + .reply = "220OK\r\n" + },{ + .reply = + "200-This is\r\n" + "250 inconsistent.\r\n" + },{ + .reply = "400 This \r is wrong\r\n" + },{ + .reply = "500 This is \x03 worse\r\n" + },{ + .reply = "699 Obscure\r\n" + },{ + .reply = "100 Invalid\r\n" + },{ + .reply = "400 Interrupted\r" + },{ + .reply = "251 example.com We got '\x04'\r\n", + .ehlo = TRUE + },{ + .reply = + "250-example.com Hello\r\n" + "250 We got some '\x08' for you\r\n", + .ehlo = TRUE + },{ + .reply = + "556-This is a very long reply\r\n" + "556 that exceeds the very low limit.\r\n", + .max_size = 50 + } +}; + +unsigned int invalid_reply_parse_test_count = + N_ELEMENTS(invalid_reply_parse_tests); + +static void test_smtp_reply_parse_invalid(void) +{ + unsigned int i; + + for (i = 0; i < invalid_reply_parse_test_count; i++) T_BEGIN { + const struct smtp_reply_parse_invalid_test *test; + struct istream *input; + struct smtp_reply_parser *parser; + struct smtp_reply *reply; + const char *reply_text, *error; + int ret; + + test = &invalid_reply_parse_tests[i]; + reply_text = test->reply; + input = i_stream_create_from_data(reply_text, strlen(reply_text)); + parser = smtp_reply_parser_init(input, test->max_size); + i_stream_unref(&input); + + test_begin(t_strdup_printf("smtp reply invalid [%d]", i)); + + if (test->ehlo) + while ((ret=smtp_reply_parse_ehlo(parser, &reply, &error)) > 0); + else + while ((ret=smtp_reply_parse_next(parser, FALSE, &reply, &error)) > 0); + test_out_reason(t_strdup_printf("parse(\"%s\")", + str_sanitize(reply_text, 80)), ret < 0, error); + test_end(); + smtp_reply_parser_deinit(&parser); + } T_END; +} + +int main(void) +{ + static void (*test_functions[])(void) = { + test_smtp_reply_parse_valid, + test_smtp_reply_parse_invalid, + NULL + }; + return test_run(test_functions); +}