From 5145e99adbf8e9af56e8264e5564daa7bd38ae29 Mon Sep 17 00:00:00 2001 From: Rolf Eike Beer Date: Sat, 1 Aug 2020 11:12:36 +0200 Subject: [PATCH] qmail-remote: add infrastructure for EHLO parsing Implementing extensions for qmail-remote often requires the usage of EHLO and checking if the server supports the extension. Every patch therefore usually comes with it's own version of EHLO parsing. Add yet another one that the patches can easily hook into. For the moment it will just send mails using ESMTP instead of SMTP for basically all cases, but not use any of the new information. --- Makefile | 8 +- TARGETS | 1 + ehlo_parse.c | 47 +++++++++++ ehlo_parse.h | 44 ++++++++++ qmail-remote.c | 52 ++++++++++-- tests/Makefile | 14 +++- tests/unittest_ehlo_parse.c | 161 ++++++++++++++++++++++++++++++++++++ 7 files changed, 319 insertions(+), 8 deletions(-) create mode 100644 ehlo_parse.c create mode 100644 ehlo_parse.h create mode 100644 tests/unittest_ehlo_parse.c diff --git a/Makefile b/Makefile index 701c8685..bcf266dc 100644 --- a/Makefile +++ b/Makefile @@ -490,6 +490,10 @@ dot-qmail.9 conf-qmail conf-break conf-spawn | sed s}SPAWN}"`head -n 1 conf-spawn`"}g \ > dot-qmail.5 +ehlo_parse.o: \ +compile ehlo_parse.c ehlo_parse.h + ./compile ehlo_parse.c + env.a: \ makelib env.o envread.o ./makelib env.a env.o envread.o @@ -1363,10 +1367,10 @@ uidgid.h auto_qmail.h auto_uids.h auto_users.h date822fmt.h fmtqfn.h qmail-remote: \ load qmail-remote.o control.o constmap.o timeoutread.o timeoutwrite.o \ timeoutconn.o tcpto.o dns.o ip.o ipalloc.o ipme.o quote.o \ -ndelay.a case.a sig.a open.a lock.a getln.a stralloc.a \ +ndelay.a case.a sig.a open.a lock.a getln.a stralloc.a ehlo_parse.o \ substdio.a error.a str.a fs.a auto_qmail.o dns.lib socket.lib ./load qmail-remote control.o constmap.o timeoutread.o \ - timeoutwrite.o timeoutconn.o tcpto.o dns.o ip.o \ + timeoutwrite.o timeoutconn.o tcpto.o dns.o ip.o ehlo_parse.o \ ipalloc.o ipme.o quote.o ndelay.a case.a sig.a open.a \ lock.a getln.a stralloc.a substdio.a error.a \ str.a fs.a auto_qmail.o `cat dns.lib` `cat socket.lib` diff --git a/TARGETS b/TARGETS index 00416864..09468a66 100644 --- a/TARGETS +++ b/TARGETS @@ -17,6 +17,7 @@ case_lowerb.o case_lowers.o case_starts.o case.a +ehlo_parse.o getln.o getln2.o getln.a diff --git a/ehlo_parse.c b/ehlo_parse.c new file mode 100644 index 00000000..ff251e54 --- /dev/null +++ b/ehlo_parse.c @@ -0,0 +1,47 @@ +#include "ehlo_parse.h" + +#include "case.h" +#include "str.h" + +unsigned int ehlo_parse(const stralloc *smtptext, const struct smtpext *callbacks, unsigned int count) +{ + /* if this is a one line answer there will be no extensions */ + if (smtptext->s[4] == ' ') + return 0; + + size_t search = 0; + unsigned int extensions = 0; + const unsigned int maxmask = (1 << count) - 1; + + /* go through all lines of the multi line answer until we found all + known extensions or we reach the last line */ + do { + unsigned int i; + + /* set search to the index of the next extension in the answer: + it's always 5 characters after the '\n' (the other 4 are + normally "250-") */ + search += 5 + str_chr(smtptext->s + search, '\n'); + + for (unsigned int i = 0; i < count; i++) { + const size_t elen = strlen(callbacks[i].name); + if (!case_diffb(smtptext->s + search, elen, callbacks[i].name)) { + if (smtptext->s[search + elen] == '\n' || + smtptext->s[search + elen] == ' ') { + if (callbacks[i].callback) { + if (callbacks[i].callback(smtptext->s + search, str_chr(smtptext->s + search, '\n'))) + extensions |= (1 << i); + } else { + extensions |= (1 << i); + } + break; + } + } + } + /* all known extensions found, no need to search any longer */ + if (extensions == maxmask) + break; + } while (smtptext->s[search - 1] == '-'); + + return extensions; +} diff --git a/ehlo_parse.h b/ehlo_parse.h new file mode 100644 index 00000000..659a0078 --- /dev/null +++ b/ehlo_parse.h @@ -0,0 +1,44 @@ +#ifndef EHLO_PARSE_H +#define EHLO_PARSE_H + +#include "stralloc.h" + +#include + +/** + * Callbacks for EHLO response parsing + */ +struct smtpext { + const char *name; /**< name of the EHLO string */ + /** + * callback in case a space character follows name in the EHLO response + * The function is given the current extension line without the leading 250- + * and the length of the remainder, not including the trailing newline. This + * includes the name part of the line so one could reuse the same callback + * for multiple extensions. + * + * The callback shall return 0 if the line was ignored, or 1 if it was + * accepted. + * + * In case the callback is NULL the extension is automatically accepted if + * the name is matched and either followed by a space or newline. + */ + int (*callback)(const char *ext, size_t extlen); +}; + +/** + * @brief parse the EHLO replies + * @param smtptext the reply to parse + * @param callbacks the list of callbacks + * @param count number of entries in callbacks + * @return mask of the matched callbacks + * + * Will parse all callbacks until either all lines are processed or all + * callbacks have been matched. + * + * The return value will have the bit positions set according to the + * entries in the callbacks param. + */ +unsigned int ehlo_parse(const stralloc *smtptext, const struct smtpext *callbacks, unsigned int count); + +#endif diff --git a/qmail-remote.c b/qmail-remote.c index 10296fac..ce4b677c 100644 --- a/qmail-remote.c +++ b/qmail-remote.c @@ -16,6 +16,7 @@ #include "ip.h" #include "ipalloc.h" #include "ipme.h" +#include "ehlo_parse.h" #include "gen_alloc.h" #include "gen_allocdefs.h" #include "str.h" @@ -223,6 +224,50 @@ void blast() substdio_flush(&smtpto); } +void +do_helo(const char *cmd) +{ + substdio_puts(&smtpto,cmd); + substdio_put(&smtpto,helohost.s,helohost.len); + substdio_put(&smtpto,"\r\n",2); + substdio_flush(&smtpto); +} + +/* array of EHLO callbacks + * When you add callbacks: make your live easy and add entries like this + * #define EXTENSION_FOO (1 << 0) + * { "FOO", ehlo_foo }, + * Whe the "0" shall be the index in that array. If you later want to check + * if that extension was found simply compare the return value of esmtp_ehlo() + * with it: + * if (ehlo_flags & EXTENSION_FOO) + * In case of multiple colliding patches one can easily renumber the entries + * without breaking any code. + */ +static const struct smtpext ehlo_callbacks[] = { +}; + +/** + * @brief find out which ESMTP extensions the remote server supports + */ +unsigned int +esmtp_ehlo(void) +{ + /* Ok, remote host will talk to us. Let's look if we can use ESMTP */ + do_helo("EHLO "); + + if (smtpcode() == 250) + return ehlo_parse(&smtptext, ehlo_callbacks, sizeof(ehlo_callbacks) / sizeof(ehlo_callbacks[0])); + + /* remote host does not like our EHLO. Maybe HELO is better? */ + do_helo("HELO "); + + if (smtpcode() != 250) + quit("ZConnected to "," but my name was rejected"); + + return 0; +} + stralloc recip = {0}; void smtp() @@ -230,14 +275,11 @@ void smtp() unsigned long code; int flagbother; int i; + unsigned int extensions; if (smtpcode() != 220) quit("ZConnected to "," but greeting failed"); - substdio_puts(&smtpto,"HELO "); - substdio_put(&smtpto,helohost.s,helohost.len); - substdio_puts(&smtpto,"\r\n"); - substdio_flush(&smtpto); - if (smtpcode() != 250) quit("ZConnected to "," but my name was rejected"); + extensions = esmtp_ehlo(); substdio_puts(&smtpto,"MAIL FROM:<"); substdio_put(&smtpto,sender.s,sender.len); diff --git a/tests/Makefile b/tests/Makefile index 8237de0d..bf82333a 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -9,7 +9,7 @@ default: it .PHONY: clean default it test -TESTBINS = unittest_stralloc unittest_blast unittest_prioq +TESTBINS = unittest_ehlo_parse unittest_stralloc unittest_blast unittest_prioq CHECK_INCLUDES := `pkg-config --cflags check` CHECK_LIBS := `pkg-config --libs check` @@ -24,6 +24,18 @@ test: it ./$$tbin || exit 1 ; \ done +unittest_ehlo_parse: \ +../load unittest_ehlo_parse.o ../ehlo_parse.o \ +../stralloc.a ../case.a ../str.a + ../load unittest_ehlo_parse ../ehlo_parse.o \ + ../stralloc.a ../case.a ../str.a \ + $(CHECK_LIBS) + +unittest_ehlo_parse.o: \ +unittest_ehlo_parse.c ../compile ../ehlo_parse.h ../stralloc.h + ../compile $< -I.. \ + $(CHECK_INCLUDES) + unittest_stralloc: \ ../load unittest_stralloc.o ../stralloc.a ../str.a ../error.a ../load unittest_stralloc ../stralloc.a ../str.a ../error.a \ diff --git a/tests/unittest_ehlo_parse.c b/tests/unittest_ehlo_parse.c new file mode 100644 index 00000000..49f2d1de --- /dev/null +++ b/tests/unittest_ehlo_parse.c @@ -0,0 +1,161 @@ +#include + +#include "ehlo_parse.h" +#include "stralloc.h" + +static int bad_call(const char *ext, size_t extlen) +{ + (void)ext; + (void)extlen; + ck_abort(); + return 0; +} + +static const struct smtpext bad_call_entry = { + "server.example.org", bad_call +}; + +START_TEST(test_ehlo_noext) +{ + stralloc thingy = { 0 }; + unsigned int exts; + thingy.s = (char*) "250 server.example.org\n"; + thingy.len = strlen(thingy.s); + + exts = ehlo_parse(&thingy, &bad_call_entry, 1); + + ck_assert_uint_eq(exts, 0); +} +END_TEST + +START_TEST(test_ehlo_noparams) +{ + stralloc thingy = { 0 }; + unsigned int exts; + thingy.s = (char*) "250-server.example.org\n" + "250-THEGOOD 1\n" + "250-THEBAD1\n" + "250 THEUGLY1\n"; + thingy.len = strlen(thingy.s); + + const struct smtpext callbacks[] = { + bad_call_entry, + { "THEGOOD", NULL }, + { "THEBAD", NULL }, + { "THEUGLY1", NULL } + }; + + exts = ehlo_parse(&thingy, callbacks, 4); + + ck_assert_uint_eq(exts, 8 | 2); +} +END_TEST + +static int only_called_once(const char *ext, size_t extlen) +{ + static int guard; + (void)ext; + ck_assert_uint_eq(extlen, strlen("THEGOOD")); + (void)extlen; + if (guard++) + ck_abort(); + return 1; +} + +START_TEST(test_ehlo_nodupes) +{ + stralloc thingy = { 0 }; + unsigned int exts; + thingy.s = (char*) "250-server.example.org\n" + "250-THEGOOD\n" + "250-THEGOOD\n" + "250 THEGOOD\n"; + thingy.len = strlen(thingy.s); + + const struct smtpext callback = { + "THEGOOD", only_called_once + }; + + exts = ehlo_parse(&thingy, &callback, 1); + + ck_assert_uint_eq(exts, 1); +} +END_TEST + +static int param_verifier(const char *ext, size_t extlen) +{ + static int guard; + const char *params[] = { + "THEGOOD a", + "THEGOOD", + "THEGOOD a b", + "FINAL 1" + }; + char buf[32]; + + ck_assert_uint_eq(extlen, strlen(params[guard])); + strncpy(buf, ext, extlen); + buf[extlen] = '\0'; + ck_assert_str_eq(buf, params[guard]); + guard++; + + return guard == 4 ? 1 : 0; +} + +START_TEST(test_ehlo_params) +{ + stralloc thingy = { 0 }; + unsigned int exts; + thingy.s = (char*) "250-server.example.org\n" + "250-THEGOOD a\n" + "250-THEGOOD\n" + "250-THEGOOD a b\n" + "250 FINAL 1"; + thingy.len = strlen(thingy.s); + + const struct smtpext callbacks[] = { + { "THEGOOD", param_verifier }, + { "FINAL", param_verifier } + }; + + exts = ehlo_parse(&thingy, callbacks, 2); + + ck_assert_uint_eq(exts, 2); +} +END_TEST + +TCase +*ehlo_parse_checks(void) +{ + TCase *tc = tcase_create("basic operations"); + + tcase_add_test(tc, test_ehlo_noext); + tcase_add_test(tc, test_ehlo_noparams); + tcase_add_test(tc, test_ehlo_nodupes); + tcase_add_test(tc, test_ehlo_params); + + return tc; +} + +Suite +*stralloc_suite(void) +{ + Suite *s = suite_create("notqmail ehlo_parse"); + + suite_add_tcase(s, ehlo_parse_checks()); + + return s; +} + +int +main(void) +{ + int number_failed; + + SRunner *sr = srunner_create(stralloc_suite()); + srunner_run_all(sr, CK_NORMAL); + number_failed = srunner_ntests_failed(sr); + srunner_free(sr); + + return number_failed; +}