Un faux serveur d'emails pour les tests (FR)
lun. 31 janvier 2022 Dan LousquiUn faux serveur d'emails pour les tests
Quand on développe une application complexe, il peut arriver que l'email s'intègre dans plusieurs workflows :
- Notification de génération de compte ;
- Envoi de lien de confirmation ;
- Réinitialisation de mot de passe ;
- ect.
Je ne reviendrai ni sur l'importance de tester son application, ni sur l'importance d'automatiser ces tests afin de les lancer à la fois à chaque amélioration de l'application, et également de façon périodique afin de valider du bon fonctionnement du projet.
Cependant, quand on teste une fonctionnalité nécessitant les emails, plusieurs solutions sont possibles :
- Ne pas envoyer d'email pendant les tests (mais du coup, on ne teste pas tout) ;
- Faire un envoi réel d'émail (mais on a un besoin d'un compte SMTP, on se limitera uniquement à cette adresse, il faut attendre la réception de l'email, avec un risque de SPAM) ;
- Utiliser un système d'email jetable, type TEMPMail ou YopMail (mais cela rajoute une dépendance à un service, qui plus est souvent indisponible) ;
- Créer son service d'email jetable et faire des tests comme un pro !
C'est cette dernière proposition que détaillera ce billet.
Tl;dr
Si ce qui vous intéresse est uniquement le projet final et de savoir comment le lancer, toutes les informations sont disponibles sur le repository TheBlusky/fakesmtpserver.
La suite de l'article traitera des différentes étapes de la création de ce projet.
Fake SMTP Server
Notre objectif est donc de faire un projet avec les caractéristiques suivantes :
- Un serveur email SMTP :
- Réceptionnant les emails, quel que soit le destinataire ;
- Stock les emails afin de pouvoir les afficher plus tard.
- Un serveur Web :
- Avec une API REST ;
- Permettant de lire les emails réceptionnés ;
- Dans un format permettant de tout vérifier (en-tête, pièce jointe, destinataire, ...).
Pour faire tout ça, nous utiliserons Python et tenterons d'utiliser au maximum les capacités asynchrones de ce langage.
Un serveur SMTP
Historiquement, il est préconisé d'utiliser le paquet smtpd de la bibliothèque standard de Python. Cependant, ce paquet n'est pas asynchrone.
Afin de profiter au maximum des capacités asynchrones du Python, nous utiliserons aiosmtpd.
Ensuite, pour lancer un serveur SMTP, il suffit de créer un Handler et d'instancier un Controller.
Dans le Handler
, il suffira de surcharger handle_DATA
, qui sera appelé lors de la reception d'un email, afin de
le stocker et de répondre la bonne réception de l'email.
Le bout de code suivant illustre comment implémenter cela :
from collections import defaultdict
from aiosmtpd.controller import Controller
emails = defaultdict(list)
class SMTPHandler:
async def handle_DATA(self, server, session, envelope):
message = envelope.content.decode("utf8", errors="replace")
for email_to in envelope.rcpt_tos:
emails[email_to].append(message)
return "250 Message accepted for delivery"
Controller(SMTPHandler(), hostname="", port=25).start()
Il faudra également, afin de recevoir les emails, configurer le champ MX d'un DNS vers l'adresse IP ou le hostname
du serveur lançant ce script. On peut même imaginer un sous domaine. Par exemple, si l'on possède le domaine
entreprise.com
, on peut créer un champ MX pour fake-mail.entreprise.com
, afin de recevoir tous les messages
envoyés à XXXXX@fake-mail.entreprise.com
.
Lire ses emails avec une API REST
Généralement, ma bibliothèque de prédilection pour faire une API REST est le combo Django / Django Rest Framework.
Néanmoins, nous souhaitons ici faire un projet asynchrone, je me suis penché vers la bibliothèque FastAPI (utilisant
Starlette). L'utilisation de cette bibliothèque est assez simple, et il suffit juste de faire un point d'entrée
mettant à disposition les éléments de la variable emails
précédemment déclarée.
L'utilisation d'Uvicorn est également la façon la plus simple d'exposer un service HTTP avec des méthodes asynchrones en Python.
Le bout de code suivant illustre comment implémenter cela :
from fastapi import FastAPI, Path
import uvicorn
app = FastAPI()
@app.get("/emails/{email}/")
async def get_emails(email=Path(...)):
return emails[email]
uvicorn.run("web:app", host="0.0.0.0", port=5000)
Quelques mesures de précaution et projet final
Les deux bouts de fonctionnalités précédentes sont la base de ce projet. Néanmoins, tel quel, il n'est pas envisageable d'ouvrir ce service sur Internet. Il faut encore :
- rajouter une couche d'authentification sur l'API afin de ne pas exposer des informations librement sur Internet ;
- limiter les enregistrements d'emails afin de ne pas avoir une consommation mémoire sans limite ;
- permettre de stopper / relancer le serveur SMTP afin de ne pas constamment écouter les messages reçus.
Toutes ces mesures sont prises sur le projet final, disponible sur le repository TheBlusky/fakesmtpserver.