Search code examples
pythonsmtpaiosmtpd

Cannot connect to aiosmtpd server from non-localhost


I have an SMTP server set up using aiosmtpd, while I can use telnet to send SMTP emails on my own local machine; if I try to telnet to it from anywhere else, I get Could not open connection to the host, on port 8025: Connect failed (note: I have tried it with port 80, 25 and 8025). All those ports are forwarded, along with Windows Firewall being completely disabled. With logging.basicConfig(level=logging.DEBUG), I get the following log:

DEBUG:asyncio:Using proactor: IocpProactor
DEBUG:asyncio:Using proactor: IocpProactor
INFO:mail.log:Available AUTH mechanisms: LOGIN(builtin) PLAIN(builtin)
INFO:mail.log:Peer: ('::1', 50067, 0, 0)
INFO:mail.log:('::1', 50067, 0, 0) handling connection
SMTP server started...
DEBUG:mail.log:('::1', 50067, 0, 0) << b'220 EC2AMAZ-QCHL34E.eu-north-1.compute.internal Python SMTP 1.4.6'
INFO:mail.log:('::1', 50067, 0, 0) EOF received
INFO:mail.log:('::1', 50067, 0, 0) Connection lost during _handle_client()
INFO:mail.log:('::1', 50067, 0, 0) connection lost

./smtp_server.py:

import asyncio
from aiosmtpd.controller import Controller
import os
import logging

logging.basicConfig(level=logging.DEBUG)

class CustomSMTPHandler:
    def __init__(self, save_path='emails.txt'):
        self.save_path = save_path

    async def handle_DATA(self, server, session, envelope):
        print('Incoming message:')
        print(f'From: {envelope.mail_from}')
        print(f'To: {envelope.rcpt_tos}')
        print(f'Content: {envelope.content.decode("utf8", errors="replace")}')

        with open(self.save_path, 'a') as f:
            f.write(f'From: {envelope.mail_from}\n')
            f.write(f'To: {", ".join(envelope.rcpt_tos)}\n')
            f.write(f'Content: {envelope.content.decode("utf8", errors="replace")}\n')
            f.write(f'-----------------------\n')
        
        return '250 Message accepted for delivery'

async def main():
    if not os.path.exists('emails.txt'):
        open('emails.txt', 'w').close()

    handler = CustomSMTPHandler()
    controller = Controller(handler)
    controller.start()

    print('SMTP server started...')
    try:
        await asyncio.Event().wait()
    except KeyboardInterrupt:
        pass
    finally:
        controller.stop()

if __name__ == '__main__':
    asyncio.run(main())

I've triple-checked all port forwarding settings on both computers, and to see if its a port issue; I wrote a simple socket server in python to see if I could send data from another computer.

./test.py

import socket

HOST = '0.0.0.0'
PORT = 8025

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.bind((HOST, PORT))
    server_socket.listen()
    print(f"Server listening on {PORT}...")

    conn, addr = server_socket.accept()
    with conn:
        print(f"Connection est w/ {addr}")
        while True:
            data = conn.recv(1024)
            if not data:
                break
            print(f"Incoming data: {data.decode()}")
            conn.sendall(data)

I could send data from the other computer to the computer that the aiosmtpd SMTP server is running on; and interestingly, I could start the test socket server even if the aiosmtpd SMTP server was still running.


Solution

  • You should specify a different hostname for the Controller. The default hostname is "::1", which means it only listens for connections from localhost. You can also specify a port other than the default (8025).

    controller = Controller(handler, hostname="0.0.0.0", port=8025)
    

    The reason aiosmtpd listens on localhost only by default is that in production environments you're expected to run another SMTP server such as postfix in front of aiosmtpd. It's fine to expose it to the internet for testing, but it's not advised to rely on such a raw setup for reliable email handling; see Python aiosmtpd - what is missing for an Mail-Transfer-Agent (MTA).