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; +}