SMTP mail server connection with Python
- Deploy your local SMTP Server with aiosmtpd library
- Deploy your local SMTP Client with smtplib library
On condition that you want to deploy the SMTP Server:
- Go to /server folder and:
docker compose up --buildYour docker-compose.ymlbuilds the server image from yourDockerfile:
python-server:
build: . (which means "Dockerfile is in the same directory as docker-compose.yml")-
Your
Dockerfiledefines how the image is created and what python to run when that image is created (i.e., the main code of your server). -
The
/src/index.pystarts running thanks toDockerfile. To deploy a SMTP Programmatic Server withaiosmtpdyou need to:3.1. Create your own Handler class (see
/src/Handler/HandlerClass.py) with its constructor. In this HandlerClass.py you can specify which domains your server rely on, or how to handle and print messages.3.2. Create a Controller object from that HandlerClass:
handler = HandlerConstructor() controller = Controller(handler, hostname="0.0.0.0") #Please, specify explicitly localhost because otherwise It might use IPv6 default addresses
3.3.
controller.start()-> However, It is compulsory to run this line inside an async function (a.k.a. asynchronous event loop). Whenever you define an async function in your code and invoke it, you will have to return a promise and so on up to main function. E.g.:async my_function0(){ #1. You define your async function inside which run controller.start() ... controller.start() } async my_function1(){ await my_function0() #2. You invoke your funcion my_function0, but It is async, so invoke it with await. As my_function0 is async, my_function1 too, so we are forced to define my_function1 as async in the same way. } if __name__="__main__": await my_function1() #3. At the end, your main function will also be asynchronous, but this is not possible in Python, as main is not asynchronous!
Hence: the solution is
asynciolibrary, which endows Python with the ability to run, from main, async functions, so check out this reference on how to create an event loop. To work with asynchronous processes in Python, you will needasynciolibrary. Luckily, the documentation says...Application developers should typically use the high-level asyncio functions, such as asyncio.run(), and should rarely need to reference the loop object or call its methods. This section is intended mostly for authors of lower-level code, libraries, and frameworks, who need finer control over the event loop behavior.
Therefore, It's enough with controlling asyncio Runners, which are coroutines that are executed as event loops (Watch out! In this context, event loops don't mean processes which are repeatedly such as for or while, but asynchronous tasks!).
async def main(): ... controller.start() asyncio.run(main()) #This works!
In short, the complete code looks like this:
import Handler.HandlerClass as handler_package from aiosmtpd.controller import Controller import asyncio async def main(): controller = Controller(handler_package.ExampleHandler(), hostname="0.0.0.0") #Specify custom port if you want (by default = 8025) as port = X, but If you change it, you will need to change it too in docker-compose.yml print("Listening...") controller.start() while True: pass if __name__ == "__main__": asyncio.run(main(), debug=False)
-
As you are running your SMTP Python server inside a docker container which is running inside your system (host), you need to bind the port of your container to your system port and then you just need to use the system port regardless of the container port. By default, the Python SMTP server runs in 8025 port (you can change it in Controller() and docker-compose.yml). This means that your container is using the 8025 port. Now you have to bind that fixed container port=8025 to the host system port you want. For this purpose, refer to
docker-compose.ymland bind them:
ports:
- ${SMTP_PORT}:8025Where SMTP_PORT is an environment variable you define in .env file. To sum up: to connect to the server, just use the SMTP_PORT you have set in .env file.
On condition that you want to deploy the SMTP Client:
- Go to /client folder and:
docker compose up --buildOnce you have your server running, you can try to connect to it from a Client Python code just specifying the IP destination and destination port. Since you might be running the server on another host machine and maybe your system hasn't Python installed, the client runs inside another container with its own Dockerfile and docker-compose.yml files. However, executing a client inside a container would imply you to bind ports and make some complex configurations and can difficult the task of connecting to remote server by just specifying IP:port to connect to.
For this reason, the client container docker-compose.yml specifies to use the same network as your host machine:
network_mode: hostThanks to this configuration, running the python client code container is exactly the same in terms of networking as running in your host machine! So you don't have to specify port binding and, besides, don't need to have installed python because it runs inside a python-based container. All advantages!
[1] Server aiosmtpd library reference [2] Client smtplib library reference