Projet Mail

L'objectif de ce projet est de programmer pas à pas une application mail en se basant sur les socket en Python.

Dans ce projet, nous allons utiliser le protocole SMTP pour l'envoi de mails et POP3 pour la réception des mails, dont voici les différentes spécifications :

Ces RFC ne sont pas très faciles à lire, mais elles sont néanmoins la spécification officielle de ces protocoles. Vous trouverez facilement des documentations complémentaires sur le web pour vous aider, comme Wikipedia :

Nota Bene : Le protocole IMAP est une alternative plus moderne à POP3, que nous n'aborderons pas dans ce projet !

Avertissement : Pour réaliser ce projet, on se limitera à l'utilisation des socket en Python. Ainsi, vous ne devez pas utiliser des modules Python, clés en main, qui implémentent déjà ces protocoles, car c'est précisément votre travail ! Vous pouvez néanmoins consulter la documentation de smtplib ou poplib, qui illustre bien ce que nous souhaitons faire dans ce projet.

De manière générale, une application mail est une infrastructure complexe qui utilise plusieurs composants :

  • un MUA (Mail User Agent), qui est le composant qui permet à un utilisateur d'envoyer des emails (client SMTP) et de les recevoir (client POP3) ;
  • un MTA (Mail Transfert Agent), qui est le composant en charge d'acheminer un email du MUA au MDA en se basant sur l'adresse mail du destinataire ;
  • un MDA (Mail Delivery Agent), qui est le composant en charge de recevoir les emails envoyés par le MTA et de les stocker en attendant que le MUA du destinataire consulte ces mails.

overview

Ainsi, le MUA joue le rôle d'un double client SMTP & POP3, tandis que le MTA joue le rôle d'un serveur SMTP et que le MDA celui d'un serveur POP3. Sur Internet, il faut évidemment que le MTA du domaine de l'expéditeur transfère le mail vers le MDA du destinataire.

Pour simplifier un peu notre projet, nous allons nous limiter à un seul domaine mail pouet.com, de telle sorte que l'envoi et la réception des mails restent local à ce domaine. Par exemple, toto@pouet.com envoie un email à tutu@pouet.com. On parlera dans ce contexte d'un LMA (Local Mail Agent), qui joue à la fois le rôle du MTA et du MDA, en fournissant à la fois un serveur SMTP et un serveur POP3 pour les utilisateurs du domaine local.

Exercice 0 : prise en main (TP)

L'objectif de cet exercice préliminaire est de prendre en main le projet mail et l'environnement technique du CREMI qui vous sera utile pour réaliser ce projet.

  • Commencez par consulter le projet https://github.com/orel33/local-mail-agent, qui met à votre disposition l'image Docker d'un Local Mail Agent (LMA). Lisez attentivement le fichier README, avant de répondre aux questions suivantes.
  • Le projet LMA est déjà installé au CREMI dans le répertoire /net/ens/lma/ et positionné sur la branche cremi. Vous disposez dans cette branche du script run.sh qui permet de démarrer une instance du docker en vous donnant un accès root à ce conteneur (machine virtuelle légère). Lors du premier lancement, l'image docker va être téléchargé depuis le docker hub puis sauvegardé dans le répertoire $TMPDIR de la machine locale.
  • Lancez une instance du LMA sur votre machine au CREMI.

Par exemple sur quirrell :

# lancement du docker
quirrell$ /net/ens/lma/run.sh
# accès root dans le conteneur
root@pouet:/home/docker# ...

Astuce : Pour utiliser le LMA sur votre machine perso, il faudra préalablement installer Docker et utiliser la branche main du projet LMA sur GitHub.

  • Dans ce conteneur (en tant que root), vérifiez avec la commande netstat que les serveur SMTP et POP3 sont à l'écoute.
  • Toujours dans le conteneur, connectez-vous au compte de toto, puis utilisez la commande mail pour envoyer un mail à tutu@pouet.com. A l'inverse, connectez-vous au compte de tutu et utilisez la commande mail pour vérifier que le mail a bien été reçu.

Regardons maintenant de plus près le fonctionnement des protocoles SMTP et POP3.

  • Vérifiez avec telnet que vous arrivez à envoyer un email au serveur SMTP (port 25 à l'intérieur du conteneur, sinon 10025 à l'extérieur).
  • De même, vérifiez avec telnet que vous arrivez à recevoir un email depuis le serveur POP3 (port 110 à l'intérieur du conteneur, sinon port 10110).
  • Faire de même en version sécurisée SSL/TLS en utilisant la commande openssl. On utilisera le port 10465 pour SMTPS à l'extérieur du conteneur (sinon 465), et le port 10995 pour POP3S à l'extérieur du conteneur (sinon 995). Pourquoi le certificat n'est-il pas reconnu ?

Par exemple :

# connexion sécurisée au serveur SMTP (port 465)
$ openssl s_client -quiet -crlf -connect localhost:465

Nota Bene : Pour forcer l'arrêt d'une session telnet, si Ctrl + c ne marche pas, faites Ctrl + ] puis tapez quit dans la console telnet>.

  • N'oubliez pas en fin de séance de stopper le conteneur docker en sortant du shell avec la commande exit. Puis faites du ménage en appelant le script cleanall.sh afin de supprimer l'image docker préalablement téléchargée.
# sortie du docker
root@pouet:/home/docker# exit
# libération des ressources
quirrell$ /net/ens/lma/cleanall.sh

Exercice 1 : envoyer un email

L'objectif de cet exercice est d'implémenter la commande sendmail.py afin d'envoyer un mail en utilisant le protocole SMTP, avec ou sans authentification, en version sécurisée ou non.

Pour représenter les messages mail, nous utiliserons dans ce projet le type EmailMessage défini dans le module Python email.message.

Voici un exemple :

import email
import email.message

msg = email.message.EmailMessage()
msg['From'] = "toto@pouet.com"
msg['To'] = "tutu@pouet.com"
msg['Subject'] = "Test"
msg['Date'] = "Wed, 21 Jul 2021 14:00:00 +0200"
msg.set_payload("Hello World!")
# print email message as string
print(msg)
# convert email message into bytes
data = msg.as_bytes()
# convert message bytes into email
msg2 = email.message_from_bytes(data)

Pour réaliser cet exercice, vous disposez des fichiers suivants :

Pour simplifier un peu ce projet, nous nous limiterons aux commandes SMTP suivantes : EHLO, NOOP, MAIL FROM, RCPT TO, DATA, QUIT. Concernant l'authentification, nous utiliserons uniquement la méthode d'authentification AUTH PLAIN basé sur le login/password Unix d'un utilisateur. Concernant la version sécurisée de SMTP, nous implémenterons SMTPS qui utilise du SMTP classique dans une socket sécurisée avec SSL/TLS. En particulier, il n'est pas demandé d'implémenter la commande STARTTLS, qui est une autre façon de sécuriser la connexion SMTP. De plus, nous nous limiterons à un seul destinataire (ou recipient en anglais) par email.

Voici la description des arguments de la commande sendmail.py, dont le code est fourni.

$ ./sendmail.py  -h
usage: sendmail.py [-h] [-H HOST] [-P PORT] [-S] [-A] [-l LOGIN] [-p PASSWORD]
                   [-f SENDER] [-t RECIPIENT] [-s SUBJECT] [-b BODY] [-v]
options:
  -h, --help                          help
  -H HOST, --host HOST                server host
  -P PORT, --port PORT                server port
  -S, --secure                        secure mode
  -A, --auth                          auth mode
  -l LOGIN, --login LOGIN             user login
  -p PASSWORD, --password PASSWORD    user password
  -f SENDER, --from SENDER            mail sender
  -t RECIPIENT, --to RECIPIENT        mail recipient
  -s SUBJECT, --subject SUBJECT       mail subject
  -b BODY, --body BODY                mail body
  -v, --verbose                       verbose

Voici un premier exemple d'utilisation de cette commande, qui envoie un mail de test en utilisant tous les paramètres par défaut. Dans cet exemple, l'envoi de mail se fait sans authentification, ni connexion sécurisée. Par défaut, la commande sendmail.py va cibler le serveur localhost:10025 (serveur SMTP du LMA).

$ ./sendmail.py
----------------------------------------
From: toto@pouet.com
To: tutu@pouet.com
Subject: Test
Date: Tue, 28 Nov 2023 09:37:14 +0100

Hello World!
----------------------------------------
[Success]

Voici un deuxième exemple avec l'option -A mettant en oeuvre l'authentification par login. Notez que c'est l'expéditeur du mail (toto@pouet.com dans cet exemple) qui doit s'authentifier auprès du serveur SMTP avec son login toto et son password toto.

$ ./sendmail.py -A -l toto -p toto

On utilisera l'option -S pour sécuriser la connexion au dessus de SSL/TLS. Par défaut, la commande sendmail.py va cibler le serveur localhost:10465 (serveur SMTPS du LMA). Comme le serveur LMA utilise un certificat self-signed, il faut ignorer sa vérification au risque de voir sa connexion rejetée. Ajoutez pour cela au contexte SSL les options suivantes :

context.check_hostname = False      # no hostname verification
context.verify_mode = ssl.CERT_NONE # no certificate verification

Si tout fonctionne correctement, vous pouvez en principe utiliser votre commande pour envoyer un mail au serveur smtpauth.u-bordeaux.fr en clair sans authentification depuis le CREMI, ou en connexion sécurisée depuis l'extérieur avec vos identifiants académiques.

Par exemple :

$ ./sendmail.py -H smtpauth.u-bordeaux.fr -P 465 -S -A -l auesnard -p xxxxxxxx \
                -f aurelien.esnard@u-bordeaux.fr -t aurelien.esnard@free.fr

Rendu : Pour réaliser cet exercice, vous devez implémenter les fonctions du fichier sendlib.py qui sont utilisés par sendmail.py. Rendre ensuite votre code sur Moodle. Vous ne devez en aucun cas modifier le fichier sendmail.py.

Nota Bene : On supposera que les commandes et les réponses à ces commandes sont des lignes d'au plus MAXLINE=1024 caractères.

Exercice 2 : recevoir un email

L'objectif de cet exercice est d'implémenter la commande recvmail.py afin de recevoir un mail en utilisant le protocole POP3, en version sécurisée ou non.

Pour réaliser cet exercice, vous disposez des fichiers suivants :

Pour simplifier un peu ce projet, nous nous limiterons aux commandes POP3 suivantes : USER, PASS, NOOP, STAT, LIST, RETR et QUIT. Concernant l'authentification, celle-ci est obligatoire pour le protocole POP3, contrairement à SMTP. Nous nous limiterons à l'authentification simple par login/password en clair. Concernant la version sécurisée de POP3, nous implémenterons POP3S qui utilise du POP3 classique dans une socket sécurisée avec SSL/TLS. En particulier, il n'est pas demandé d'implémenter la commande STARTTLS, qui est une autre façon de sécuriser la connexion POP3.

Voici la description des arguments de la commande recvmail.py, dont le code est fourni.

$ ./recvmail.py  -h
usage: recvmail.py [-h] [-H HOST] [-P PORT] [-S] [-l LOGIN] [-p PASSWORD]
                   [-v] {noop,stat,list,retr,dele} [rank]
arguments:
  {noop,stat,list,retr,dele}          command [required]
  rank                                mail rank [optional]
options:
  -h, --help                          help
  -H HOST, --host HOST                server host
  -P PORT, --port PORT                server port
  -S, --secure                        secure mode
  -l LOGIN, --login LOGIN             user login
  -p PASSWORD, --password PASSWORD    user password
  -v, --verbose                       verbose

Voici un premier exemple d'utilisation de cette commande, qui permet de se connecter au serveur POP3 du LMA avec tous les paramètres par défaut (login tutu et password tutu). Par défaut, la connexion n'est pas sécurisée.

# envoi d'un mail de test de toto à tutu (SMTP)
$ ./sendmail.py
# liste des mails sur le serveur (POP3)
$ ./recvmail.py list
+OK 1 messages:
1 417
.
[Success]

# récupération du mail de rang 1 (POP3)
$ ./recvmail.py retr 1
+OK 417 octets
From: toto@pouet.com
To: tutu@pouet.com
Subject: Test
Date: Tue, 28 Nov 2023 13:43:31 +0100

Hello World!
.
[Success]

Rendu : Pour réaliser cet exercice, vous devez implémenter les fonctions du fichier recvlib.py qui sont utilisés par recvmail.py. Rendre ensuite votre code sur Moodle. Vous ne devez en aucun cas modifier le fichier recvmail.py.

Nota Bene : On supposera que les commandes et les réponses à ces commandes sont des lignes d'au plus MAXLINE=1024 caractères. Seul les commandes RETR et LIST peuvent renvoyer des données de longueur arbitraire.

Exercice 3 : Extensions

Finalement, pas d'exercice 3 prévu pour cette année.