From: Stephan Bosch Date: Sun, 6 Nov 2016 20:15:41 +0000 (+0100) Subject: lib-smtp: Implemented parsing, manipulation and encoding of SMTP addresses. X-Git-Tag: 2.3.0.rc1~377 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=03f08e4abef2afc782069bdfef305f84e98f0922;p=thirdparty%2Fdovecot%2Fcore.git lib-smtp: Implemented parsing, manipulation and encoding of SMTP addresses. --- diff --git a/src/lib-smtp/Makefile.am b/src/lib-smtp/Makefile.am index c7af84038c..6c99e3f87d 100644 --- a/src/lib-smtp/Makefile.am +++ b/src/lib-smtp/Makefile.am @@ -12,12 +12,14 @@ AM_CPPFLAGS = \ libsmtp_la_SOURCES = \ smtp-parser.c \ + smtp-address.c \ lmtp-client.c \ smtp-submit-settings.c \ smtp-submit.c headers = \ smtp-parser.h \ + smtp-address.h \ lmtp-client.h \ smtp-submit-settings.h \ smtp-submit.h @@ -25,7 +27,8 @@ headers = \ pkginc_libdir=$(pkgincludedir) pkginc_lib_HEADERS = $(headers) -test_programs = +test_programs = \ + test-smtp-address test_nocheck_programs = \ test-smtp-submit @@ -33,7 +36,7 @@ test_nocheck_programs = \ noinst_PROGRAMS = $(test_programs) $(test_nocheck_programs) test_libs = \ - libsmtp.la \ + $(noinst_LTLIBRARIES) \ ../lib-program-client/libprogram_client.la \ ../lib-dns/libdns.la \ ../lib-mail/libmail.la \ @@ -46,7 +49,7 @@ test_libs = \ $(MODULE_LIBS) test_deps = \ - libsmtp.la \ + $(noinst_LTLIBRARIES) \ ../lib-program-client/libprogram_client.la \ ../lib-dns/libdns.la \ ../lib-mail/libmail.la \ @@ -57,6 +60,11 @@ test_deps = \ ../lib-test/libtest.la \ ../lib/liblib.la +test_smtp_address_SOURCES = test-smtp-address.c +test_smtp_address_LDFLAGS = -export-dynamic +test_smtp_address_LDADD = $(test_libs) +test_smtp_address_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-address.c b/src/lib-smtp/smtp-address.c new file mode 100644 index 0000000000..d29eb2649f --- /dev/null +++ b/src/lib-smtp/smtp-address.c @@ -0,0 +1,674 @@ +/* Copyright (c) 2013-2017 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "message-address.h" +#include "smtp-parser.h" +#include "smtp-address.h" + +/* From RFC 5321: + + Reverse-path = Path / "<>" + Forward-path = Path + + Path = "<" [ A-d-l ":" ] Mailbox ">" + A-d-l = At-domain *( "," At-domain ) + ; Note that this form, the so-called "source + ; route", MUST BE accepted, SHOULD NOT be + ; generated, and SHOULD be ignored. + At-domain = "@" Domain + + Domain = sub-domain *("." sub-domain) + + sub-domain = Let-dig [Ldh-str] + Let-dig = ALPHA / DIGIT + Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig + + address-literal = "[" ( IPv4-address-literal / + IPv6-address-literal / + General-address-literal ) "]" + ; See Section 4.1.3 + + Mailbox = Local-part "@" ( Domain / address-literal ) + + Local-part = Dot-string / Quoted-string + ; MAY be case-sensitive + Dot-string = Atom *("." Atom) + Atom = 1*atext + */ + +/* + * SMTP address parsing + */ + +struct smtp_address_parser { + struct smtp_parser parser; + + struct smtp_address address; + + bool parse:1; + bool path:1; +}; + +static int +smtp_parser_parse_dot_string(struct smtp_parser *parser, + const char **value_r) +{ + const unsigned char *pbegin = parser->cur; + + /* Dot-string = Atom *("." Atom) + */ + + /* NOTE: this deviates from Dot-String syntax to allow some Japanese + mail addresses with dots at non-standard places to be accepted. */ + + if (parser->cur >= parser->end || + (!smtp_char_is_atext(*parser->cur) && *parser->cur != '.')) + return 0; + parser->cur++; + + while (parser->cur < parser->end && + (smtp_char_is_atext(*parser->cur) || *parser->cur == '.')) + parser->cur++; + + if (value_r != NULL) + *value_r = t_strndup(pbegin, parser->cur - pbegin); + return 1; +} + +static int +smtp_parse_localpart(struct smtp_parser *parser, + const char **localpart_r) +{ + int ret; + + if ((ret=smtp_parser_parse_quoted_string(parser, localpart_r)) != 0) + return ret; + + return smtp_parser_parse_dot_string(parser, localpart_r); +} + +static int +smtp_parse_mailbox(struct smtp_address_parser *aparser, + enum smtp_address_parse_flags flags) +{ + struct smtp_parser *parser = &aparser->parser; + const char **value = NULL; + int ret; + + /* Mailbox = Local-part "@" ( Domain / address-literal ) + */ + + value = (aparser->parse ? &aparser->address.localpart : NULL); + if ((ret=smtp_parse_localpart(parser, value)) <= 0) + return ret; + + if ((parser->cur >= parser->end || *parser->cur != '@') && + (flags & SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART) == 0) { + if (parser->cur >= parser->end || + (aparser->path && *parser->cur == '>')) + parser->error = "Missing domain"; + else + parser->error = "Invalid character in localpart"; + return -1; + } + + if (parser->cur >= parser->end || *parser->cur != '@') + return 1; + parser->cur++; + + value = (aparser->parse ? &aparser->address.domain : NULL); + if ((ret=smtp_parser_parse_domain(parser, value)) == 0 && + (ret=smtp_parser_parse_address_literal(parser, + value, NULL)) == 0) { + if (parser->cur >= parser->end || + (aparser->path && *parser->cur == '>')) { + parser->error = "Missing domain after '@'"; + return -1; + } else { + parser->error = "Invalid domain"; + return -1; + } + } + return ret; +} + +static int +smtp_parse_source_route(struct smtp_parser *parser) +{ + /* Source-route = [ A-d-l ":" ] + A-d-l = At-domain *( "," At-domain ) + At-domain = "@" Domain + */ + + /* "@" Domain */ + if (parser->cur >= parser->end || *parser->cur != '@') + return 0; + parser->cur++; + + for (;;) { + /* Domain */ + if (smtp_parser_parse_domain(parser, NULL) <= 0) { + parser->error = + "Missing domain after '@' in source route"; + return -1; + } + + /* *( "," At-domain ) */ + if (parser->cur >= parser->end || *parser->cur != ',') + break; + parser->cur++; + + /* "@" Domain */ + if (parser->cur >= parser->end || *parser->cur != '@') { + parser->error = "Missing '@' after ',' in source route"; + return -1; + } + parser->cur++; + } + + /* ":" */ + if (parser->cur >= parser->end || *parser->cur != ':') { + parser->error = "Missing ':' at end of source route"; + return -1; + } + parser->cur++; + return 1; +} + +static int +smtp_parse_path(struct smtp_address_parser *aparser, + enum smtp_address_parse_flags flags) +{ + struct smtp_parser *parser = &aparser->parser; + int ret, sret; + + /* Path = "<" [ A-d-l ":" ] Mailbox ">" + */ + + /* "<" */ + if (parser->cur < parser->end && *parser->cur == '<') { + aparser->path = TRUE; + parser->cur++; + } else if ((flags & SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL) == 0) { + return 0; + } + + /* [ A-d-l ":" ] */ + if ((sret=smtp_parse_source_route(parser)) < 0) + return -1; + + /* Mailbox */ + if ((ret=smtp_parse_mailbox(aparser, flags)) < 0) + return -1; + if (ret == 0) { + if (parser->cur < parser->end && *parser->cur == '>') { + if (sret > 0) { + parser->error = + "Path only consists of source route"; + return -1; + } + if ((flags & SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY) + == 0) { + parser->error = "Null path not allowed"; + return -1; + } + } else { + parser->error = "Invalid character in localpart"; + return -1; + } + } + + /* ">" */ + if (aparser->path) { + if (parser->cur >= parser->end || *parser->cur != '>') { + parser->error = "Missing '>' at end of path"; + return -1; + } + parser->cur++; + } else if (parser->cur < parser->end && *parser->cur == '>') { + parser->error = "Unmatched '>' at end of path"; + return -1; + } + return 1; +} + +static int +smtp_parse_username(struct smtp_address_parser *aparser) +{ + struct smtp_parser *parser = &aparser->parser; + const char **value = NULL; + const unsigned char *p, *dp; + int ret; + + /* Best-effort extraction of SMTP address from a user name. + */ + + value = (aparser->parse ? &aparser->address.localpart : NULL); + if (*parser->cur == '\"') { + /* if the local part is a quoted string, parse it as any other + SMTP address */ + if ((ret=smtp_parse_localpart(parser, value)) <= 0) + return ret; + } else { + /* use the right-most '@' as separator */ + dp = parser->end - 1; + while (dp > parser->cur && *dp != '@') + dp--; + if (dp == parser->cur) + dp = parser->end; + /* check whether the resulting localpart could be encoded as + quoted string */ + for (p = parser->cur; p < dp; p++) { + if (!smtp_char_is_qtext(*p) || *p == ' ') { + parser->error = + "Invalid character in user name"; + return -1; + } + } + if (aparser->parse) { + aparser->address.localpart = + p_strdup_until(parser->pool, parser->cur, dp); + } + parser->cur = dp; + } + + if (parser->cur < parser->end && *parser->cur != '@') { + parser->error = "Invalid character in user name"; + return -1; + } + + if (parser->cur >= parser->end || *parser->cur != '@') + return 1; + parser->cur++; + + value = (aparser->parse ? &aparser->address.domain : NULL); + if ((ret=smtp_parser_parse_domain(parser, value)) == 0 && + (ret=smtp_parser_parse_address_literal(parser, + value, NULL)) == 0) { + if (parser->cur >= parser->end) { + parser->error = "Missing domain after '@'"; + return -1; + } else { + parser->error = "Invalid domain"; + return -1; + } + } + return ret; +} + +int smtp_address_parse_mailbox(pool_t pool, + const char *mailbox, enum smtp_address_parse_flags flags, + struct smtp_address **address_r, const char **error_r) +{ + struct smtp_address_parser aparser; + int ret; + + if (address_r != NULL) + *address_r = NULL; + if (error_r != NULL) + *error_r = NULL; + + if ((mailbox == NULL || *mailbox == '\0')) { + if ((flags & SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY) == 0) { + if (error_r != NULL) + *error_r = "Mailbox is empty string"; + return -1; + } + + if (address_r != NULL) + *address_r = p_new(pool, struct smtp_address, 1); + return 1; + } + + i_zero(&aparser); + smtp_parser_init(&aparser.parser, pool_datastack_create(), mailbox); + aparser.parse = (address_r != NULL); + + if ((ret=smtp_parse_mailbox(&aparser, flags)) <= 0) { + if (error_r != NULL) + *error_r = (ret < 0 ? aparser.parser.error : + "Invalid character in localpart"); + return -1; + } + if (aparser.parser.cur != aparser.parser.end) { + if (error_r != NULL) + *error_r = "Invalid character in mailbox"; + return -1; + } + + if (address_r != NULL) + *address_r = smtp_address_clone(pool, &aparser.address); + return 1; +} + +int smtp_address_parse_path_full(pool_t pool, const char *path, + enum smtp_address_parse_flags flags, + struct smtp_address **address_r, const char **error_r, + const char **endp_r) +{ + struct smtp_address_parser aparser; + int ret; + + if (address_r != NULL) + *address_r = NULL; + if (error_r != NULL) + *error_r = NULL; + + if (path == NULL || *path == '\0') { + if ((flags & SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY) == 0 || + (flags & SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL) == 0) { + if (error_r != NULL) + *error_r = "Path is empty string"; + return -1; + } + if (address_r != NULL) + *address_r = p_new(pool, struct smtp_address, 1); + return 1; + } + + i_zero(&aparser); + smtp_parser_init(&aparser.parser, pool_datastack_create(), path); + aparser.parse = (address_r != NULL); + + if ((ret=smtp_parse_path(&aparser, flags)) <= 0) { + if (error_r != NULL) { + *error_r = (ret < 0 ? aparser.parser.error : + "Missing '<' at beginning of path"); + } + return -1; + } + if (endp_r != NULL) + *endp_r = (const char *)aparser.parser.cur; + else if (aparser.parser.cur != aparser.parser.end) { + if (error_r != NULL) + *error_r = "Invalid character in path"; + return -1; + } + + if (address_r != NULL) + *address_r = smtp_address_clone(pool, &aparser.address); + return 1; +} + +int smtp_address_parse_path(pool_t pool, const char *path, + enum smtp_address_parse_flags flags, + struct smtp_address **address_r, const char **error_r) +{ + return smtp_address_parse_path_full(pool, path, flags, + address_r, error_r, NULL); +} + +int smtp_address_parse_username(pool_t pool, const char *username, + struct smtp_address **address_r, const char **error_r) +{ + struct smtp_address_parser aparser; + int ret; + + if (address_r != NULL) + *address_r = NULL; + if (error_r != NULL) + *error_r = NULL; + + if ((username == NULL || *username == '\0')) { + if (error_r != NULL) + *error_r = "Username is empty string"; + return -1; + } + + i_zero(&aparser); + smtp_parser_init(&aparser.parser, pool_datastack_create(), username); + aparser.parse = (address_r != NULL); + + if ((ret=smtp_parse_username(&aparser)) <= 0) { + if (error_r != NULL) { + *error_r = (ret < 0 ? aparser.parser.error : + "Invalid character in user name"); + } + return -1; + } + if (aparser.parser.cur != aparser.parser.end) { + if (error_r != NULL) + *error_r = "Invalid character in user name"; + return -1; + } + + if (address_r != NULL) + *address_r = smtp_address_clone(pool, &aparser.address); + return 1; +} + +/* + * SMTP address construction + */ + +void smtp_address_write(string_t *out, + const struct smtp_address *address) ATTR_NULL(2) +{ + bool quoted = FALSE; + const unsigned char *p, *pend, *pblock; + size_t begin; + + if (address == NULL || address->localpart == NULL) + return; + begin = str_len(out); + + /* encode localpart */ + if (address->localpart == NULL) + return; + p = (const unsigned char *)address->localpart; + pend = p + strlen(address->localpart); + pblock = p; + while (p < pend) { + while (p < pend && smtp_char_is_atext(*p)) + p++; + + if (!quoted && p < pend && (*p != '.' || p == pblock)) { + quoted = TRUE; + str_insert(out, begin, "\""); + } + + str_append_n(out, pblock, p - pblock); + if (p >= pend) + break; + + if (!quoted) { + str_append_c(out, '.'); + } else { + i_assert(smtp_char_is_qpair(*p)); + if (!smtp_char_is_qtext(*p)) + str_append_c(out, '\\'); + str_append_c(out, *p); + } + + p++; + pblock = p; + } + + if (p == pblock) { + quoted = TRUE; + str_insert(out, begin, "\""); + } + + if (quoted) + str_append_c(out, '\"'); + + if (address->domain == NULL) + return; + + str_append_c(out, '@'); + str_append(out, address->domain); +} + +void smtp_address_write_path(string_t *out, + const struct smtp_address *address) +{ + str_append_c(out, '<'); + smtp_address_write(out, address); + str_append_c(out, '>'); +} + +const char * +smtp_address_encode(const struct smtp_address *address) +{ + string_t *str = t_str_new(256); + smtp_address_write(str, address); + return str_c(str); +} + +const char * +smtp_address_encode_path(const struct smtp_address *address) +{ + string_t *str = t_str_new(256); + smtp_address_write_path(str, address); + return str_c(str); +} + +/* + * SMTP address manipulation + */ + +void smtp_address_init(struct smtp_address *address, + const char *localpart, const char *domain) +{ + i_zero(address); + address->localpart = localpart; + address->domain = (localpart == NULL ? NULL : domain); +} + +void smtp_address_init_from_msg(struct smtp_address *address, + const struct message_address *msg_addr) +{ + i_zero(address); + address->localpart = msg_addr->mailbox; + address->domain = (msg_addr->mailbox == NULL ? NULL : msg_addr->domain); +} + +struct smtp_address * +smtp_address_clone(pool_t pool, const struct smtp_address *src) +{ + struct smtp_address *new; + size_t size, lpsize, dsize; + char *data, *localpart, *domain; + + if (src == NULL) + return NULL; + + /* @UNSAFE */ + + lpsize = dsize = 0; + size = sizeof(struct smtp_address); + if (src->localpart != NULL) { + lpsize = strlen(src->localpart) + 1; + size = MALLOC_ADD(size, lpsize); + } + if (src->domain != NULL) { + dsize = strlen(src->domain) + 1; + size = MALLOC_ADD(size, dsize); + } + + localpart = domain = NULL; + data = p_malloc(pool, size); + new = (struct smtp_address *)data; + if (lpsize > 0) { + localpart = PTR_OFFSET(data, sizeof(*new)); + memcpy(localpart, src->localpart, lpsize); + } + if (dsize > 0) { + domain = PTR_OFFSET(data, sizeof(*new) + lpsize); + memcpy(domain, src->domain, dsize); + } + new->localpart = localpart; + new->domain = domain; + + return new; +} + +struct smtp_address * +smtp_address_create(pool_t pool, + const char *localpart, const char *domain) +{ + struct smtp_address addr; + + smtp_address_init(&addr, localpart, domain); + return smtp_address_clone(pool, &addr); +} + +struct smtp_address * +smtp_address_create_from_msg(pool_t pool, + const struct message_address *msg_addr) +{ + struct smtp_address addr; + + smtp_address_init_from_msg(&addr, msg_addr); + return smtp_address_clone(pool, &addr); +} + +struct smtp_address * +smtp_address_clone_temp(const struct smtp_address *src) +{ + struct smtp_address *new; + + new = t_new(struct smtp_address, 1); + new->localpart = t_strdup(src->localpart); + new->domain = t_strdup(src->domain); + return new; +} + +struct smtp_address * +smtp_address_create_temp(const char *localpart, const char *domain) +{ + struct smtp_address addr; + + smtp_address_init(&addr, localpart, domain); + return smtp_address_clone_temp(&addr); +} + +struct smtp_address * +smtp_address_create_from_msg_temp(const struct message_address *msg_addr) +{ + struct smtp_address addr; + + smtp_address_init_from_msg(&addr, msg_addr); + return smtp_address_clone_temp(&addr); +} + +struct smtp_address * +smtp_address_add_detail(pool_t pool, const struct smtp_address *address, + const char *detail, char delim_c) +{ + struct smtp_address *new_addr; + const char delim[] = {delim_c, '\0'}; + + new_addr = p_new(pool, struct smtp_address, 1); + new_addr->localpart = p_strconcat(pool, + address->localpart, delim, detail, NULL); + new_addr->domain = p_strdup(pool, address->domain); + + return new_addr; +} + +struct smtp_address * +smtp_address_add_detail_temp(const struct smtp_address *address, + const char *detail, char delim_c) +{ + struct smtp_address *new_addr; + const char delim[] = {delim_c, '\0'}; + + new_addr = t_new(struct smtp_address, 1); + new_addr->localpart = t_strconcat( + address->localpart, delim, detail, NULL); + new_addr->domain = t_strdup(address->domain); + + return new_addr; +} + +int smtp_address_cmp(const struct smtp_address *address1, + const struct smtp_address *address2) +{ + int ret; + + if ((ret=strcasecmp(address1->localpart, address2->localpart)) != 0) + return ret; + return strcasecmp(address1->domain, address2->domain); +} diff --git a/src/lib-smtp/smtp-address.h b/src/lib-smtp/smtp-address.h new file mode 100644 index 0000000000..01b499ad06 --- /dev/null +++ b/src/lib-smtp/smtp-address.h @@ -0,0 +1,115 @@ +#ifndef SMTP_ADDRESS_H +#define SMTP_ADDRESS_H + +#include "array-decl.h" + +struct message_address; + +enum smtp_address_parse_flags { + /* Strictly enforce the RFC 5321 syntax */ + SMTP_ADDRESS_PARSE_FLAG_STRICT = BIT(0), + /* Allow an empty/NULL address */ + SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY = BIT(1), + /* Allow an address without a domain part */ + SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART = BIT(2), + /* Allow omission of the <...> brackets in a path */ + SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL = BIT(3) +}; + +struct smtp_address { + const char *localpart; + const char *domain; +}; + +ARRAY_DEFINE_TYPE(smtp_address, struct smtp_address *); +ARRAY_DEFINE_TYPE(smtp_address_const, const struct smtp_address *); + +/* Not const! Never return this as a result directly! */ +#define SMTP_ADDRESS_LITERAL(localpart, domain) \ + &((struct smtp_address){ (localpart), (domain) }) + +/* + * SMTP address parsing + */ + +int smtp_address_parse_mailbox(pool_t pool, + const char *mailbox, enum smtp_address_parse_flags flags, + struct smtp_address **address_r, const char **error_r) + ATTR_NULL(4, 5); +int smtp_address_parse_path_full(pool_t pool, const char *path, + enum smtp_address_parse_flags flags, + struct smtp_address **address_r, const char **error_r, + const char **endp_r) ATTR_NULL(4, 5, 6); +int smtp_address_parse_path(pool_t pool, const char *path, + enum smtp_address_parse_flags flags, + struct smtp_address **address_r, const char **error_r) + ATTR_NULL(4, 5); +int smtp_address_parse_username(pool_t pool, const char *username, + struct smtp_address **address_r, const char **error_r) + ATTR_NULL(3, 4); + +/* + * SMTP address construction + */ + +void smtp_address_write(string_t *out, + const struct smtp_address *address) ATTR_NULL(2); +void smtp_address_write_path(string_t *out, + const struct smtp_address *address) ATTR_NULL(2); + +const char * +smtp_address_encode(const struct smtp_address *address) + ATTR_NULL(1); +const char * +smtp_address_encode_path(const struct smtp_address *address) + ATTR_NULL(1); + +/* + * SMTP address manipulation + */ + +void smtp_address_init(struct smtp_address *address, + const char *localpart, const char *domain); +void smtp_address_init_from_msg(struct smtp_address *address, + const struct message_address *msg_addr); + +struct smtp_address * +smtp_address_clone(pool_t pool, const struct smtp_address *address); +struct smtp_address * +smtp_address_create(pool_t pool, const char *localpart, const char *domain); +struct smtp_address * +smtp_address_create_from_msg(pool_t pool, + const struct message_address *msg_addr); + +struct smtp_address * +smtp_address_clone_temp(const struct smtp_address *address); +struct smtp_address * +smtp_address_create_temp(const char *localpart, const char *domain); +struct smtp_address * +smtp_address_create_from_msg_temp(const struct message_address *msg_addr); + +struct smtp_address * +smtp_address_add_detail(pool_t pool, const struct smtp_address *address, + const char *detail, char delim_c); +struct smtp_address * +smtp_address_add_detail_temp(const struct smtp_address *address, + const char *detail, char delim_c); + +int smtp_address_cmp(const struct smtp_address *address1, + const struct smtp_address *address2); + +static inline bool +smtp_address_equals(const struct smtp_address *address1, + const struct smtp_address *address2) +{ + return (smtp_address_cmp(address1, address2) == 0); +} + +static inline bool ATTR_NULL(1) ATTR_PURE +smtp_address_isnull(const struct smtp_address *address) +{ + return (address == NULL || address->localpart == NULL || + *address->localpart == '\0'); +} + +#endif diff --git a/src/lib-smtp/test-smtp-address.c b/src/lib-smtp/test-smtp-address.c new file mode 100644 index 0000000000..c0a22bd596 --- /dev/null +++ b/src/lib-smtp/test-smtp-address.c @@ -0,0 +1,751 @@ +/* Copyright (c) 2013-2017 Dovecot authors, see the included COPYING file */ + +#include "test-lib.h" +#include "str.h" +#include "test-common.h" +#include "smtp-address.h" + +/* + * Valid mailbox parse tests + */ + +struct valid_mailbox_parse_test { + const char *input, *output; + enum smtp_address_parse_flags flags; + + struct smtp_address address; +}; + +static const struct valid_mailbox_parse_test +valid_mailbox_parse_tests[] = { + { + .input = "", + .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY, + .address = { .localpart = NULL, .domain = NULL }, + },{ + .input = "user", + .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART, + .address = { .localpart = "user", .domain = NULL }, + },{ + .input = "user@domain.tld", + .address = { .localpart = "user", .domain = "domain.tld" }, + },{ + .input = "1234567890@domain.tld", + .address = { + .localpart = "1234567890", + .domain = "domain.tld" }, + },{ + .input = "_______@domain.tld", + .address = { + .localpart = "_______", + .domain = "domain.tld" }, + },{ + .input = "firstname.lastname@domain.tld", + .address = { + .localpart = "firstname.lastname", + .domain = "domain.tld" }, + },{ + .input = "firstname+lastname@domain.tld", + .address = { + .localpart = "firstname+lastname", + .domain = "domain.tld" }, + },{ + .input = "firstname-lastname@domain.tld", + .address = { + .localpart = "firstname-lastname", + .domain = "domain.tld" }, + },{ + .input = "\"user\"@domain.tld", + .address = { .localpart = "user", .domain = "domain.tld" }, + .output = "user@domain.tld" + },{ + .input = "\"user@frop\"@domain.tld", + .address = { .localpart = "user@frop", .domain = "domain.tld" }, + .output = "\"user@frop\"@domain.tld" + },{ + .input = "user@127.0.0.1", + .address = { .localpart = "user", .domain = "127.0.0.1" }, + },{ + .input = "user@[127.0.0.1]", + .address = { .localpart = "user", .domain = "[127.0.0.1]" }, + },{ + .input = "user@[IPv6:::1]", + .address = { .localpart = "user", .domain = "[IPv6:::1]" }, + },{ + .input = "user@[IPv6:::127.0.0.1]", + .address = { .localpart = "user", .domain = "[IPv6:::127.0.0.1]" }, + /* Japanese deviations */ + }, { + .input = "email@-example.com", + .address = { .localpart = "email", .domain = "-example.com" }, + }, { + .input = ".email@example.com", + .output = "\".email\"@example.com", + .address = { .localpart = ".email", .domain = "example.com" }, + }, { + .input = "email.@example.com", + .output = "\"email.\"@example.com", + .address = { .localpart = "email.", .domain = "example.com" }, + }, { + .input = "email..email@example.com", + .output = "\"email..email\"@example.com", + .address = { .localpart = "email..email", .domain = "example.com" }, + }, { + .input = "Abc..123@example.com", + .output = "\"Abc..123\"@example.com", + .address = { .localpart = "Abc..123", .domain = "example.com" }, + } +}; + +unsigned int valid_mailbox_parse_test_count = + N_ELEMENTS(valid_mailbox_parse_tests); + +static void test_smtp_mailbox_parse_valid(void) +{ + unsigned int i; + + for (i = 0; i < valid_mailbox_parse_test_count; i++) T_BEGIN { + const struct valid_mailbox_parse_test *test; + struct smtp_address *address; + const char *error = NULL, *output, *encoded; + int ret; + + test = &valid_mailbox_parse_tests[i]; + ret = smtp_address_parse_mailbox(pool_datastack_create(), + test->input, test->flags, &address, &error); + + test_begin(t_strdup_printf("smtp mailbox valid [%d]", i)); + test_out_reason(t_strdup_printf("parse(\"%s\")", + test->input), ret > 0, error); + + if (ret > 0) { + if (address->localpart == NULL || + test->address.localpart == NULL) { + test_out(t_strdup_printf + ("address->localpart = %s", address->localpart), + (address->localpart == test->address.localpart)); + } else { + test_out(t_strdup_printf + ("address->localpart = \"%s\"", address->localpart), + strcmp(address->localpart, test->address.localpart) == 0); + } + if (address->domain == NULL || + test->address.domain == NULL) { + test_out(t_strdup_printf + ("address->domain = %s", address->domain), + (address->domain == test->address.domain)); + } else { + test_out(t_strdup_printf + ("address->domain = \"%s\"", address->domain), + strcmp(address->domain, test->address.domain) == 0); + } + + encoded = smtp_address_encode(address); + output = (test->output == NULL ? test->input : test->output); + test_out( + t_strdup_printf("encode() = \"%s\"", encoded), + strcmp(encoded, output) == 0); + } + test_end(); + } T_END; +} + +/* + * Valid path parse tests + */ + +struct valid_path_parse_test { + const char *input, *output; + enum smtp_address_parse_flags flags; + + struct smtp_address address; +}; + +static const struct valid_path_parse_test +valid_path_parse_tests[] = { + { + .input = "<>", + .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY, + .address = { .localpart = NULL, .domain = NULL } + },{ + .input = "", + .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART, + .address = { .localpart = "user", .domain = NULL } + },{ + .input = "", + .address = { .localpart = "user", .domain = "domain.tld" } + },{ + .input = "<@otherdomain.tld,@yetanotherdomain.tld:user@domain.tld>", + .address = { .localpart = "user", .domain = "domain.tld" }, + .output = "" + } +}; + +unsigned int valid_path_parse_test_count = + N_ELEMENTS(valid_path_parse_tests); + +static void test_smtp_path_parse_valid(void) +{ + unsigned int i; + + for (i = 0; i < valid_path_parse_test_count; i++) T_BEGIN { + const struct valid_path_parse_test *test; + struct smtp_address *address; + const char *error = NULL, *output, *encoded; + int ret; + + test = &valid_path_parse_tests[i]; + ret = smtp_address_parse_path(pool_datastack_create(), + test->input, test->flags, &address, &error); + + test_begin(t_strdup_printf("smtp path valid [%d]", i)); + test_out_reason(t_strdup_printf("parse(\"%s\")", + test->input), ret > 0, error); + + if (ret > 0) { + if (address->localpart == NULL || + test->address.localpart == NULL) { + test_out(t_strdup_printf + ("address->localpart = %s", address->localpart), + (address->localpart == test->address.localpart)); + } else { + test_out(t_strdup_printf + ("address->localpart = \"%s\"", address->localpart), + strcmp(address->localpart, test->address.localpart) == 0); + } + if (address->domain == NULL || + test->address.domain == NULL) { + test_out(t_strdup_printf + ("address->domain = %s", address->domain), + (address->domain == test->address.domain)); + } else { + test_out(t_strdup_printf + ("address->domain = \"%s\"", address->domain), + strcmp(address->domain, test->address.domain) == 0); + } + + encoded = smtp_address_encode_path(address); + output = (test->output == NULL ? test->input : test->output); + test_out( + t_strdup_printf("encode() = \"%s\"", encoded), + strcmp(encoded, output) == 0); + } + test_end(); + } T_END; +} + +/* + * Valid username parse tests + */ + +struct valid_username_parse_test { + const char *input, *output; + + struct smtp_address address; +}; + +static const struct valid_username_parse_test +valid_username_parse_tests[] = { + { + .input = "user", + .address = { + .localpart = "user", + .domain = NULL }, + },{ + .input = "user@domain.tld", + .address = { + .localpart = "user", + .domain = "domain.tld" }, + },{ + .input = "user@domain.tld", + .address = { + .localpart = "user", + .domain = "domain.tld" }, + },{ + .input = "1234567890@domain.tld", + .address = { + .localpart = "1234567890", + .domain = "domain.tld" }, + },{ + .input = "_______@domain.tld", + .address = { + .localpart = "_______", + .domain = "domain.tld" }, + },{ + .input = "firstname.lastname@domain.tld", + .address = { + .localpart = "firstname.lastname", + .domain = "domain.tld" }, + },{ + .input = "firstname+lastname@domain.tld", + .address = { + .localpart = "firstname+lastname", + .domain = "domain.tld" }, + },{ + .input = "firstname-lastname@domain.tld", + .address = { + .localpart = "firstname-lastname", + .domain = "domain.tld" }, + },{ + .input = "\"user\"@domain.tld", + .address = { .localpart = "user", .domain = "domain.tld" }, + .output = "user@domain.tld" + },{ + .input = "\"user@frop\"@domain.tld", + .address = { .localpart = "user@frop", .domain = "domain.tld" }, + .output = "\"user@frop\"@domain.tld" + },{ + .input = "user@frop@domain.tld", + .address = { .localpart = "user@frop", .domain = "domain.tld" }, + .output = "\"user@frop\"@domain.tld" + },{ + .input = "user@127.0.0.1", + .address = { .localpart = "user", .domain = "127.0.0.1" }, + },{ + .input = "user@[127.0.0.1]", + .address = { .localpart = "user", .domain = "[127.0.0.1]" }, + },{ + .input = "user@[IPv6:::1]", + .address = { .localpart = "user", .domain = "[IPv6:::1]" }, + },{ + .input = "user@[IPv6:::127.0.0.1]", + .address = { .localpart = "user", .domain = "[IPv6:::127.0.0.1]" }, + } +}; + +unsigned int valid_username_parse_test_count = + N_ELEMENTS(valid_username_parse_tests); + +static void test_smtp_username_parse_valid(void) +{ + unsigned int i; + + for (i = 0; i < valid_username_parse_test_count; i++) T_BEGIN { + const struct valid_username_parse_test *test; + struct smtp_address *address; + const char *error = NULL, *output, *encoded; + int ret; + + test = &valid_username_parse_tests[i]; + ret = smtp_address_parse_username(pool_datastack_create(), + test->input, &address, &error); + + test_begin(t_strdup_printf("smtp username valid [%d]", i)); + test_out_reason(t_strdup_printf("parse(\"%s\")", + test->input), ret > 0, error); + + if (ret > 0) { + if (address->localpart == NULL || + test->address.localpart == NULL) { + test_out(t_strdup_printf + ("address->localpart = %s", address->localpart), + (address->localpart == test->address.localpart)); + } else { + test_out(t_strdup_printf + ("address->localpart = \"%s\"", address->localpart), + strcmp(address->localpart, test->address.localpart) == 0); + } + if (address->domain == NULL || + test->address.domain == NULL) { + test_out(t_strdup_printf + ("address->domain = %s", address->domain), + (address->domain == test->address.domain)); + } else { + test_out(t_strdup_printf + ("address->domain = \"%s\"", address->domain), + strcmp(address->domain, test->address.domain) == 0); + } + + encoded = smtp_address_encode(address); + output = (test->output == NULL ? test->input : test->output); + test_out( + t_strdup_printf("encode() = \"%s\"", encoded), + strcmp(encoded, output) == 0); + } + test_end(); + } T_END; +} + +/* + * Invalid mailbox parse tests + */ + +struct invalid_mailbox_parse_test { + const char *input; + enum smtp_address_parse_flags flags; +}; + +static const struct invalid_mailbox_parse_test +invalid_mailbox_parse_tests[] = { + { + .input = "", + }, { + .input = "user", + }, { + .input = "\"user@domain.tld", + }, { + .input = "us\"er@domain.tld", + }, { + .input = "user@frop@domain.tld", + }, { + .input = "user@.tld", + }, { + .input = "user@a$.tld", + }, { + .input = "user@a..tld", + }, { + .input = "user@[]", + }, { + .input = "user@[", + }, { + .input = "user@[AA]", + }, { + .input = "user@[AA", + }, { + .input = "user@[127.0.0]", + }, { + .input = "user@[256.256.256.256]", + }, { + .input = "user@[127.0.0.1", + }, { + .input = "user@[::1]", + }, { + .input = "user@[IPv6:flierp]", + }, { + .input = "user@[IPv6:aa:bb::cc::dd]", + }, { + .input = "user@[IPv6::1]", + }, { + .input = "user@[IPv6:::1", + }, { + .input = "user@[Gen:]", + }, { + .input = "user@[Gen:Hopsa", + }, { + .input = "user@[Gen-:Hopsa]", + }, { + .input = "#@%^%#$@#$@#.com", + }, { + .input = "@example.com", + }, { + .input = "Eric Mail ", + }, { + .input = "email.example.com", + }, { + .input = "email@example@example.com", + }, { + .input = "あいうえお@example.com", + }, { + .input = "email@example.com (Eric Mail)", + }, { + .input = "email@example..com", +#if 0 /* These deviations are allowed (maybe implement strict mode) */ + }, { + .input = "email@-example.com", + }, { + .input = ".email@example.com", + }, { + .input = "email.@example.com", + }, { + .input = "email..email@example.com", + }, { + .input = "Abc..123@example.com" +#endif + } +}; + +unsigned int invalid_mailbox_parse_test_count = + N_ELEMENTS(invalid_mailbox_parse_tests); + +static void test_smtp_mailbox_parse_invalid(void) +{ + unsigned int i; + + for (i = 0; i < invalid_mailbox_parse_test_count; i++) T_BEGIN { + const struct invalid_mailbox_parse_test *test; + struct smtp_address *address; + const char *error = NULL; + int ret; + + test = &invalid_mailbox_parse_tests[i]; + ret = smtp_address_parse_mailbox(pool_datastack_create(), + test->input, test->flags, &address, &error); + + test_begin(t_strdup_printf("smtp mailbox invalid [%d]", i)); + test_out_reason(t_strdup_printf + ("parse(\"%s\")", test->input), ret < 0, error); + test_end(); + } T_END; +} + +/* + * Invalid path parse tests + */ + +struct invalid_path_parse_test { + const char *input; + enum smtp_address_parse_flags flags; +}; + +static const struct invalid_path_parse_test +invalid_path_parse_tests[] = { + { + .input = "", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "\"user@domain.tld", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "us\"er@domain.tld", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@frop@domain.tld", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@.tld", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@a$.tld", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@a..tld", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[]", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[AA]", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[AA", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[127.0.0]", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[256.256.256.256]", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[127.0.0.1", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[::1]", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[IPv6:flierp]", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[IPv6:aa:bb::cc::dd]", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[IPv6::1]", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[IPv6:::1", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[Gen:]", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[Gen:Hopsa", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "user@[Gen-:Hopsa]", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "#@%^%#$@#$@#.com", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "@example.com", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "Eric Mail ", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "email.example.com", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "email@example@example.com", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "あいうえお@example.com", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "email@example.com (Eric Mail)", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "email@example..com", + .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL + }, { + .input = "<>", + }, { + .input = "", + }, { + .input = "<\"user@domain.tld>", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "<#@%^%#$@#$@#.com>", + }, { + .input = "<@example.com>", + }, { + .input = "Eric Mail ", + }, { + .input = "", + }, { + .input = "", + }, { + .input = "<あいうえお@example.com>", + }, { + .input = " (Eric Mail)", + }, { + .input = "", + }, { + .input = "input, test->flags, &address, &error); + + test_begin(t_strdup_printf("smtp path invalid [%d]", i)); + test_out_reason(t_strdup_printf + ("parse(\"%s\")", test->input), ret < 0, error); + test_end(); + } T_END; +} + +/* + * Invalid username parse tests + */ + +struct invalid_username_parse_test { + const char *input; + enum smtp_address_parse_flags flags; +}; + +static const struct invalid_username_parse_test +invalid_username_parse_tests[] = { + { + .input = "frop@$%^$%^.tld", + }, { + .input = "fr op@domain.tld", + }, { + .input = "f r o p@domain.tld", + } +}; + +unsigned int invalid_username_parse_test_count = + N_ELEMENTS(invalid_username_parse_tests); + +static void test_smtp_username_parse_invalid(void) +{ + unsigned int i; + + for (i = 0; i < invalid_username_parse_test_count; i++) T_BEGIN { + const struct invalid_username_parse_test *test; + struct smtp_address *address; + const char *error = NULL; + int ret; + + test = &invalid_username_parse_tests[i]; + ret = smtp_address_parse_username(pool_datastack_create(), + test->input, &address, &error); + + test_begin(t_strdup_printf("smtp username invalid [%d]", i)); + test_out_reason(t_strdup_printf + ("parse(\"%s\")", test->input), ret < 0, error); + test_end(); + } T_END; +} + +/* + * Tests + */ + +int main(void) +{ + static void (*test_functions[])(void) = { + test_smtp_mailbox_parse_valid, + test_smtp_path_parse_valid, + test_smtp_username_parse_valid, + test_smtp_mailbox_parse_invalid, + test_smtp_path_parse_invalid, + test_smtp_username_parse_invalid, + NULL + }; + return test_run(test_functions); +}