Skip to content

Commit

Permalink
qmail-remote: add infrastructure for EHLO parsing
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
DerDakon committed Nov 8, 2020
1 parent 56ffd3b commit 5145e99
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 8 deletions.
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down
1 change: 1 addition & 0 deletions TARGETS
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ case_lowerb.o
case_lowers.o
case_starts.o
case.a
ehlo_parse.o
getln.o
getln2.o
getln.a
Expand Down
47 changes: 47 additions & 0 deletions ehlo_parse.c
Original file line number Diff line number Diff line change
@@ -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;
}
44 changes: 44 additions & 0 deletions ehlo_parse.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#ifndef EHLO_PARSE_H
#define EHLO_PARSE_H

#include "stralloc.h"

#include <sys/types.h>

/**
* 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
52 changes: 47 additions & 5 deletions qmail-remote.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -223,21 +224,62 @@ 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()
{
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);
Expand Down
14 changes: 13 additions & 1 deletion tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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 \
Expand Down
161 changes: 161 additions & 0 deletions tests/unittest_ehlo_parse.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#include <check.h>

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

0 comments on commit 5145e99

Please sign in to comment.