/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */ #include "lib.h" #include "str.h" #include "message-address.h" #include "test-common.h" enum test_message_address { TEST_MESSAGE_ADDRESS_FLAG_SKIP_LIST = BIT(0), }; static bool cmp_addr(const struct message_address *a1, const struct message_address *a2) { return null_strcmp(a1->name, a2->name) == 0 && null_strcmp(a1->route, a2->route) == 0 && null_strcmp(a1->mailbox, a2->mailbox) == 0 && null_strcmp(a1->domain, a2->domain) == 0 && a1->invalid_syntax == a2->invalid_syntax; } static void test_parse_address_full(const char *input, bool fill_missing, struct message_address_list *list_r) { const enum message_address_parse_flags flags = fill_missing ? MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING : 0; /* duplicate the input (without trailing NUL) so valgrind notices if there's any out-of-bounds access */ size_t input_len = strlen(input); unsigned char *input_dup = i_memdup(input, input_len); message_address_parse_full(pool_datastack_create(), input_dup, input_len, UINT_MAX, flags, list_r); i_free(input_dup); } static const struct message_address * test_parse_address(const char *input, bool fill_missing) { struct message_address_list list; test_parse_address_full(input, fill_missing, &list); return list.head; } static void test_message_address(void) { static const struct test { const char *input; const char *wanted_output; const char *wanted_filled_output; struct message_address addr; struct message_address filled_addr; enum test_message_address flags; } tests[] = { /* user@domain -> */ { "user@domain", "", NULL, { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, { "\"user\"@domain", "", NULL, { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, { "\"user name\"@domain", "<\"user name\"@domain>", NULL, { NULL, NULL, NULL, NULL, "user name", "domain", FALSE }, { NULL, NULL, NULL, NULL, "user name", "domain", FALSE }, 0 }, { "\"user@na\\\\me\"@domain", "<\"user@na\\\\me\"@domain>", NULL, { NULL, NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, { NULL, NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, 0 }, { "\"user\\\"name\"@domain", "<\"user\\\"name\"@domain>", NULL, { NULL, NULL, NULL, NULL, "user\"name", "domain", FALSE }, { NULL, NULL, NULL, NULL, "user\"name", "domain", FALSE }, 0 }, { "\"\"@domain", "<\"\"@domain>", NULL, { NULL, NULL, NULL, NULL, "", "domain", FALSE }, { NULL, NULL, NULL, NULL, "", "domain", FALSE }, 0 }, { "user", "", "", { NULL, NULL, NULL, NULL, "user", "", TRUE }, { NULL, NULL, NULL, NULL, "user", "MISSING_DOMAIN", TRUE }, 0 }, { "@domain", "<\"\"@domain>", "", { NULL, NULL, NULL, NULL, "", "domain", TRUE }, { NULL, NULL, NULL, NULL, "MISSING_MAILBOX", "domain", TRUE }, 0 }, /* Display Name -> Display Name */ { "Display Name", "\"Display Name\"", "\"Display Name\" ", { NULL, NULL, "Display Name", NULL, "", "", TRUE }, { NULL, NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, { "\"Display Name\"", "\"Display Name\"", "\"Display Name\" ", { NULL, NULL, "Display Name", NULL, "", "", TRUE }, { NULL, NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, { "Display \"Name\"", "\"Display Name\"", "\"Display Name\" ", { NULL, NULL, "Display Name", NULL, "", "", TRUE }, { NULL, NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, { "\"Display\" \"Name\"", "\"Display Name\"", "\"Display Name\" ", { NULL, NULL, "Display Name", NULL, "", "", TRUE }, { NULL, NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, { "\"\"", "", "", { NULL, NULL, "", NULL, "", "", TRUE }, { NULL, NULL, "", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, /* -> */ { "", NULL, NULL, { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, { "<\"user\"@domain>", "", NULL, { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, { "<\"user name\"@domain>", NULL, NULL, { NULL, NULL, NULL, NULL, "user name", "domain", FALSE }, { NULL, NULL, NULL, NULL, "user name", "domain", FALSE }, 0 }, { "<\"user@na\\\\me\"@domain>", NULL, NULL, { NULL, NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, { NULL, NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, 0 }, { "<\"user\\\"name\"@domain>", NULL, NULL, { NULL, NULL, NULL, NULL, "user\"name", "domain", FALSE }, { NULL, NULL, NULL, NULL, "user\"name", "domain", FALSE }, 0 }, { "<\"\"@domain>", NULL, NULL, { NULL, NULL, NULL, NULL, "", "domain", FALSE }, { NULL, NULL, NULL, NULL, "", "domain", FALSE }, 0 }, { "", NULL, "", { NULL, NULL, NULL, NULL, "user", "", TRUE }, { NULL, NULL, NULL, NULL, "user", "MISSING_DOMAIN", TRUE }, 0 }, { "<@route>", "<@route:\"\">", "", { NULL, NULL, NULL, "@route", "", "", TRUE }, { NULL, NULL, NULL, "INVALID_ROUTE", "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, /* user@domain (Display Name) -> "Display Name" */ { "user@domain (DisplayName)", "DisplayName ", NULL, { NULL, NULL, "DisplayName", NULL, "user", "domain", FALSE }, { NULL, NULL, "DisplayName", NULL, "user", "domain", FALSE }, 0 }, { "user@domain (Display Name)", "\"Display Name\" ", NULL, { NULL, NULL, "Display Name", NULL, "user", "domain", FALSE }, { NULL, NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 }, { "user@domain (Display\"Name)", "\"Display\\\"Name\" ", NULL, { NULL, NULL, "Display\"Name", NULL, "user", "domain", FALSE }, { NULL, NULL, "Display\"Name", NULL, "user", "domain", FALSE }, 0 }, { "user (Display Name)", "\"Display Name\" ", "\"Display Name\" ", { NULL, NULL, "Display Name", NULL, "user", "", TRUE }, { NULL, NULL, "Display Name", NULL, "user", "MISSING_DOMAIN", TRUE }, 0 }, { "@domain (Display Name)", "\"Display Name\" <\"\"@domain>", "\"Display Name\" ", { NULL, NULL, "Display Name", NULL, "", "domain", TRUE }, { NULL, NULL, "Display Name", NULL, "MISSING_MAILBOX", "domain", TRUE }, 0 }, { "user@domain ()", "", NULL, { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, /* Display Name -> "Display Name" */ { "DisplayName ", NULL, NULL, { NULL, NULL, "DisplayName", NULL, "user", "domain", FALSE }, { NULL, NULL, "DisplayName", NULL, "user", "domain", FALSE }, 0 }, { "Display Name ", "\"Display Name\" ", NULL, { NULL, NULL, "Display Name", NULL, "user", "domain", FALSE }, { NULL, NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 }, { "\"Display Name\" ", NULL, NULL, { NULL, NULL, "Display Name", NULL, "user", "domain", FALSE }, { NULL, NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 }, { "\"Display\\\"Name\" ", NULL, NULL, { NULL, NULL, "Display\"Name", NULL, "user", "domain", FALSE }, { NULL, NULL, "Display\"Name", NULL, "user", "domain", FALSE }, 0 }, { "Display Name ", "\"Display Name\" ", "\"Display Name\" ", { NULL, NULL, "Display Name", NULL, "user", "", TRUE }, { NULL, NULL, "Display Name", NULL, "user", "MISSING_DOMAIN", TRUE }, 0 }, { "\"\" ", "", NULL, { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, /* <@route:user@domain> -> <@route:user@domain> */ { "<@route:user@domain>", NULL, NULL, { NULL, NULL, NULL, "@route", "user", "domain", FALSE }, { NULL, NULL, NULL, "@route", "user", "domain", FALSE }, 0 }, { "<@route,@route2:user@domain>", NULL, NULL, { NULL, NULL, NULL, "@route,@route2", "user", "domain", FALSE }, { NULL, NULL, NULL, "@route,@route2", "user", "domain", FALSE }, 0 }, { "<@route@route2:user@domain>", "<@route,@route2:user@domain>", NULL, { NULL, NULL, NULL, "@route,@route2", "user", "domain", FALSE }, { NULL, NULL, NULL, "@route,@route2", "user", "domain", FALSE }, 0 }, { "<@route@route2:user>", "<@route,@route2:user>", "<@route,@route2:user@MISSING_DOMAIN>", { NULL, NULL, NULL, "@route,@route2", "user", "", TRUE }, { NULL, NULL, NULL, "@route,@route2", "user", "MISSING_DOMAIN", TRUE }, 0 }, { "<@route@route2:\"\"@domain>", "<@route,@route2:\"\"@domain>", NULL, { NULL, NULL, NULL, "@route,@route2", "", "domain", FALSE }, { NULL, NULL, NULL, "@route,@route2", "", "domain", FALSE }, 0 }, /* Display Name <@route:user@domain> -> "Display Name" <@route:user@domain> */ { "Display Name <@route:user@domain>", "\"Display Name\" <@route:user@domain>", NULL, { NULL, NULL, "Display Name", "@route", "user", "domain", FALSE }, { NULL, NULL, "Display Name", "@route", "user", "domain", FALSE }, 0 }, { "Display Name <@route,@route2:user@domain>", "\"Display Name\" <@route,@route2:user@domain>", NULL, { NULL, NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, { NULL, NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, 0 }, { "Display Name <@route@route2:user@domain>", "\"Display Name\" <@route,@route2:user@domain>", NULL, { NULL, NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, { NULL, NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, 0 }, { "Display Name <@route@route2:user>", "\"Display Name\" <@route,@route2:user>", "\"Display Name\" <@route,@route2:user@MISSING_DOMAIN>", { NULL, NULL, "Display Name", "@route,@route2", "user", "", TRUE }, { NULL, NULL, "Display Name", "@route,@route2", "user", "MISSING_DOMAIN", TRUE }, 0 }, { "Display Name <@route@route2:\"\"@domain>", "\"Display Name\" <@route,@route2:\"\"@domain>", NULL, { NULL, NULL, "Display Name", "@route,@route2", "", "domain", FALSE }, { NULL, NULL, "Display Name", "@route,@route2", "", "domain", FALSE }, 0 }, /* other tests: */ { "\"foo: ;,\" ", NULL, NULL, { NULL, NULL, "foo: ;,", NULL, "user", "domain", FALSE }, { NULL, NULL, "foo: ;,", NULL, "user", "domain", FALSE }, 0 }, { "<>", "", "", { NULL, NULL, NULL, NULL, "", "", TRUE }, { NULL, NULL, NULL, NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, { "<@>", "", "", { NULL, NULL, NULL, NULL, "", "", TRUE }, { NULL, NULL, NULL, "INVALID_ROUTE", "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, /* Test against a out-of-bounds read bug - keep these two tests together in this same order: */ { "aaaa@", "", "", { NULL, NULL, NULL, NULL, "aaaa", "", TRUE }, { NULL, NULL, NULL, NULL, "aaaa", "MISSING_DOMAIN", TRUE }, 0 }, { "a(aa", "", "", { NULL, NULL, NULL, NULL, "", "", TRUE }, { NULL, NULL, NULL, NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, TEST_MESSAGE_ADDRESS_FLAG_SKIP_LIST }, }; static struct message_address group_prefix = { NULL, NULL, NULL, NULL, "group", NULL, FALSE }; static struct message_address group_suffix = { NULL, NULL, NULL, NULL, NULL, NULL, FALSE }; const struct message_address *addr; string_t *str, *group; const char *wanted_string; unsigned int i; test_begin("message address parsing"); str = t_str_new(128); group = t_str_new(256); for (i = 0; i < N_ELEMENTS(tests)*2; i++) { const struct test *test = &tests[i/2]; const struct message_address *test_wanted_addr; bool fill_missing = i%2 != 0; test_wanted_addr = !fill_missing ? &test->addr : &test->filled_addr; addr = test_parse_address(test->input, fill_missing); test_assert_idx(addr != NULL && addr->next == NULL && cmp_addr(addr, test_wanted_addr), i); /* test the address alone */ str_truncate(str, 0); message_address_write(str, addr); if (fill_missing && test->wanted_filled_output != NULL) wanted_string = test->wanted_filled_output; else if (test->wanted_output != NULL) wanted_string = test->wanted_output; else wanted_string = test->input; test_assert_idx(strcmp(str_c(str), wanted_string) == 0, i); if ((test->flags & TEST_MESSAGE_ADDRESS_FLAG_SKIP_LIST) != 0) continue; /* test the address as a list of itself */ for (unsigned int list_length = 2; list_length <= 5; list_length++) { str_truncate(group, 0); str_append(group, test->input); for (unsigned int j = 1; j < list_length; j++) { if ((j % 2) == 0) str_append(group, ","); else str_append(group, " , \n "); str_append(group, test->input); } addr = test_parse_address(str_c(group), fill_missing); for (unsigned int j = 0; j < list_length; j++) { test_assert_idx(addr != NULL && cmp_addr(addr, test_wanted_addr), i); if (addr != NULL) addr = addr->next; } test_assert_idx(addr == NULL, i); } /* test the address as a group of itself */ for (unsigned int list_length = 1; list_length <= 5; list_length++) { str_truncate(group, 0); str_printfa(group, "group: %s", test->input); for (unsigned int j = 1; j < list_length; j++) { if ((j % 2) == 0) str_append(group, ","); else str_append(group, " , \n "); str_append(group, test->input); } str_append_c(group, ';'); addr = test_parse_address(str_c(group), fill_missing); test_assert(addr != NULL && cmp_addr(addr, &group_prefix)); addr = addr->next; for (unsigned int j = 0; j < list_length; j++) { test_assert_idx(addr != NULL && cmp_addr(addr, test_wanted_addr), i); if (addr != NULL) addr = addr->next; } test_assert_idx(addr != NULL && addr->next == NULL && cmp_addr(addr, &group_suffix), i); } } test_end(); test_begin("message address parsing with empty group"); str_truncate(group, 0); str_append(group, "group:;"); addr = test_parse_address(str_c(group), FALSE); str_truncate(str, 0); message_address_write(str, addr); test_assert(addr != NULL && cmp_addr(addr, &group_prefix)); addr = addr->next; test_assert(addr != NULL && addr->next == NULL && cmp_addr(addr, &group_suffix)); test_assert(strcmp(str_c(str), "group:;") == 0); test_end(); test_begin("message address parsing empty string"); test_assert(message_address_parse(unsafe_data_stack_pool, &uchar_nul, 0, 10, MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING) == NULL); str_truncate(str, 0); message_address_write(str, NULL); test_assert(str_len(str) == 0); test_end(); } static void test_message_address_list(void) { test_begin("message address list"); const char *test_input = "user1@example1.com, user2@example2.com, user3@example3.com"; const struct message_address wanted_addrs[] = { { NULL, NULL, NULL, NULL, "user1", "example1.com", FALSE }, { NULL, NULL, NULL, NULL, "user2", "example2.com", FALSE }, { NULL, NULL, NULL, NULL, "user3", "example3.com", FALSE }, }; struct message_address_list list; struct message_address *addr, *scanned_last_addr; test_parse_address_full(test_input, FALSE, &list); addr = list.head; for (unsigned int i = 0; i < N_ELEMENTS(wanted_addrs); i++) { test_assert_idx(cmp_addr(addr, &wanted_addrs[i]), i); scanned_last_addr = addr; addr = addr->next; } test_assert(list.tail == scanned_last_addr); test_assert(addr == NULL); test_end(); } static void test_message_address_nuls(void) { const unsigned char input[] = "\"user\0nuls\\\0-esc\"@[domain\0nuls\\\0-esc] (comment\0nuls\\\0-esc)"; const struct message_address output = { NULL, NULL, "comment\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", NULL, "user\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", "[domain\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc]", FALSE }; const struct message_address *addr; test_begin("message address parsing with NULs"); addr = message_address_parse(pool_datastack_create(), input, sizeof(input)-1, UINT_MAX, 0); test_assert(addr != NULL && cmp_addr(addr, &output)); test_end(); } static void test_message_address_nuls_display_name(void) { const unsigned char input[] = "\"displayname\0nuls\\\0-esc\" <\"user\0nuls\\\0-esc\"@[domain\0nuls\\\0-esc]>"; const struct message_address output = { NULL, NULL, "displayname\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", NULL, "user\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", "[domain\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc]", FALSE }; const struct message_address *addr; test_begin("message address parsing with NULs in display-name"); addr = message_address_parse(pool_datastack_create(), input, sizeof(input)-1, UINT_MAX, 0); test_assert(addr != NULL && cmp_addr(addr, &output)); test_end(); } static void test_message_address_non_strict_dots(void) { const char *const inputs[] = { ".@example.com", "..@example.com", "..foo@example.com", "..foo..@example.com", "..foo..bar..@example.com", }; const struct message_address *addr; struct message_address output = { NULL, NULL, NULL, NULL, "local-part", "example.com", FALSE }; test_begin("message address parsing with non-strict dots"); for (unsigned int i = 0; i < N_ELEMENTS(inputs); i++) { const unsigned char *addr_input = (const unsigned char *)inputs[i]; /* invalid with strict-dots flag */ addr = message_address_parse(pool_datastack_create(), addr_input, strlen(inputs[i]), UINT_MAX, MESSAGE_ADDRESS_PARSE_FLAG_STRICT_DOTS); test_assert_idx(addr != NULL && addr->invalid_syntax, i); /* valid without the strict-dots flag */ addr = message_address_parse(pool_datastack_create(), addr_input, strlen(inputs[i]), UINT_MAX, 0); output.mailbox = t_strcut(inputs[i], '@'); test_assert_idx(addr != NULL && cmp_addr(addr, &output), i); } test_end(); } static int test_parse_path(const char *input, const struct message_address **addr_r) { struct message_address *addr; char *input_dup; int ret; /* duplicate the input (without trailing NUL) so valgrind notices if there's any out-of-bounds access */ size_t input_len = strlen(input); if (input_len > 0) input = input_dup = i_memdup(input, input_len); ret = message_address_parse_path(pool_datastack_create(), (const unsigned char *)input, input_len, &addr); if (input_len > 0) i_free(input_dup); *addr_r = addr; return ret; } static void test_message_address_path(void) { static const struct test { const char *input; const char *wanted_output; struct message_address addr; } tests[] = { { "<>", NULL, { NULL, NULL, NULL, NULL, NULL, NULL, FALSE } }, { " < > ", "<>", { NULL, NULL, NULL, NULL, NULL, NULL, FALSE } }, { "", NULL, { NULL, NULL, NULL, NULL, "user", "domain", FALSE } }, { " ", "", { NULL, NULL, NULL, NULL, "user", "domain", FALSE } }, { "user@domain", "", { NULL, NULL, NULL, NULL, "user", "domain", FALSE } }, { " user@domain ", "", { NULL, NULL, NULL, NULL, "user", "domain", FALSE } }, { "<\"user\"@domain>", "", { NULL, NULL, NULL, NULL, "user", "domain", FALSE } }, { "<\"user name\"@domain>", NULL, { NULL, NULL, NULL, NULL, "user name", "domain", FALSE } }, { "<\"user@na\\\\me\"@domain>", NULL, { NULL, NULL, NULL, NULL, "user@na\\me", "domain", FALSE } }, { "<\"user\\\"name\"@domain>", NULL, { NULL, NULL, NULL, NULL, "user\"name", "domain", FALSE } }, { "<\"\"@domain>", NULL, { NULL, NULL, NULL, NULL, "", "domain", FALSE } }, { "<@source", "<>", { NULL, NULL, NULL, NULL, NULL, NULL, TRUE } }, }; const struct message_address *addr; string_t *str; const char *wanted_string; unsigned int i; test_begin("message address path parsing"); str = t_str_new(128); for (i = 0; i < N_ELEMENTS(tests); i++) { const struct test *test = &tests[i]; const struct message_address *test_wanted_addr; int ret; test_wanted_addr = &test->addr; ret = test_parse_path(test->input, &addr); if (addr->invalid_syntax) test_assert_idx(ret == -1, i); else test_assert_idx(ret == 0, i); test_assert_idx(addr != NULL && addr->next == NULL && cmp_addr(addr, test_wanted_addr), i); /* test the address alone */ str_truncate(str, 0); message_address_write(str, addr); if (test->wanted_output != NULL) wanted_string = test->wanted_output; else wanted_string = test->input; test_assert_idx(strcmp(str_c(str), wanted_string) == 0, i); } test_end(); } static void test_message_address_path_invalid(void) { static const char *tests[] = { "", "<", " < ", ">", " > ", "", " user@domain> ", "", "<@route@route2:user>", "<@domain>", "@domain", " @domain ", "", "user@", " user@ ", "bladiebla", "user@domain@" }; const struct message_address *addr; unsigned int i; test_begin("message address path invalid"); for (i = 0; i < N_ELEMENTS(tests); i++) { const char *test = tests[i]; int ret; ret = test_parse_path(test, &addr); test_assert_idx(ret < 0, i); } test_end(); } int main(void) { static void (*const test_functions[])(void) = { test_message_address, test_message_address_list, test_message_address_nuls, test_message_address_nuls_display_name, test_message_address_non_strict_dots, test_message_address_path, test_message_address_path_invalid, NULL }; return test_run(test_functions); }