Building a Python SMTP relay host for old wildlife cameras with aiosmtpd

Some devices are not able to encrypt their connections. For instance some wildlife cameras send email unencrypted but authenticated. A buddy of mine owns many such devices and no longer found any online SMTP service to use. So I decided to hack a simple python based service for this task.

The first Python module that came to mind is smtpd but it is deprecated. So I tried its replacement aiosmtpd.

As a bonus one example in the aiosmtpd github repo is an authenticated replayer really close to my target task.

Playing around with my Thunderbird as client and the example server I noticed that no authentication took place. The emails I send were simply accepted.

async def main():
handler = RelayHandler()
cont = Controller(
handler,
hostname=LISTEN_HOST,
port=LISTEN_PORT,
authenticator=Authenticator(DB_AUTH),
)
cont.start()

So I dug into the code. But the code of aiosmtpd was not really a help. The Controller instance which acts as the top level API pushes many of its parameters as **kwargs dict simply down the framework. Even the authenticator parameter goes this way. So the API is virtually not documented. Sure - use the force read the source - but this cannot be an excuse for everything.

At first I found "authentication_required" as a good candidate.

async def main():
handler = RelayHandler()
cont = Controller(
handler,
hostname=LISTEN_HOST,
port=LISTEN_PORT,
authenticator=Authenticator(DB_AUTH),
authentication_required=True,
)
cont.start()

But this leads to Problems with Thunderbird ("Host requires Authentication" .. but I do not know how) forcing me to use a more low level client smtplib.

With smtplib I  got also an error but with more context "SMTP AUTH extension not supported by server". OK, with this I can work, but it took a while till I stumbled on a thread

https://github.com/aio-libs/aiosmtpd/issues/283 leading to a thread https://github.com/aio-libs/aiosmtpd/blob/master/aiosmtpd/smtp.py#L833. Then back into the source where I found the relevant parameter:

auth_require_tls=False,

Without this parameter aiosmtpd does not offer AUTH as capability if not a STARTTLS command is issued beforehand.

And then I noticed that this parameter is indeed described in length in the documentation of aiosmtpd.

So I was wrong blaming aiosmtpd I should have read the documentation better.

But after getting authentication, it does not work at all. 

The problem was broken code in the example

        password = auth_data.password
        hashpass = self.ph.hash(password)
        conn = sqlite3.connect(self.auth_db)
        curs = conn.execute(
            "SELECT hashpass FROM userauth WHERE username=?", (username,)
        )
        hash_db = curs.fetchone()
        conn.close()
        if not hash_db:
            return fail_nothandled
        if hashpass != hash_db[0]:
            return fail_nothandled

which should read

        username = auth_data.login.decode()
        password = auth_data.password.decode()
        conn = sqlite3.connect(self.auth_db)
        curs = conn.execute(
            "SELECT hashpass FROM userauth WHERE username=?", (username,)
        )
        hash_db = curs.fetchone()
        conn.close()
        if not hash_db:
            return fail_nothandled
        if not self.ph.verify(hash_db[0], password):
            return fail_nothandled

The UTF-8 decoding was missing as well as the hash verification was crap.

With these changes Emails from Thunderbird as well as smtplib can be run against aiosmtpd with authentication but without encryption.

Cheers,

Volker