]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-smtp: Implemented SMTP command reply parser.
authorStephan Bosch <stephan.bosch@dovecot.fi>
Sun, 6 Nov 2016 21:00:08 +0000 (22:00 +0100)
committerTimo Sirainen <timo.sirainen@dovecot.fi>
Sun, 26 Nov 2017 20:30:01 +0000 (22:30 +0200)
src/lib-smtp/Makefile.am
src/lib-smtp/smtp-reply-parser.c [new file with mode: 0644]
src/lib-smtp/smtp-reply-parser.h [new file with mode: 0644]
src/lib-smtp/smtp-reply.c [new file with mode: 0644]
src/lib-smtp/smtp-reply.h [new file with mode: 0644]
src/lib-smtp/test-smtp-reply-parser.c [new file with mode: 0644]

index 257315125da48f55a5742d7aa0e2bd6b5ac6de78..e0edd757415c0370dd2b93fcb0854307b4ce6733 100644 (file)
@@ -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 (file)
index 0000000..ecf55f7
--- /dev/null
@@ -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 <ctype.h>
+
+/* 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 <SP> 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 <SP> 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 (file)
index 0000000..bbafd75
--- /dev/null
@@ -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 (file)
index 0000000..7b6ff4b
--- /dev/null
@@ -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 (file)
index 0000000..a9b7236
--- /dev/null
@@ -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 (file)
index 0000000..96da8b2
--- /dev/null
@@ -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 <time.h>
+
+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 <frop@example.com> ok\r\n",
+               .status = 250,
+               .enhanced_code = { 2, 1, 0 },
+               .text_lines = (const char *[]){
+                       "Originator <frop@example.com> 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);
+}