Première rédaction de cet article le 24 novembre 2023
Un peu de programmation aujourd'hui. Supposons qu'on reçoive des messages qui ont été modifiés en cours de route et qu'on veut remettre dans leur état initial. Comment faire cela en Python ?
Comme exemple, on va supposer que les messages contenant le mot « chiffrer » ont été modifiés pour mettre « crypter » et qu'on veut remettre le terme correct. On va écrire un programme qui reçoit le message sur son entrée standard et met la version corrigée sur la sortie standard. D'abord, la mauvaise méthode, qui ne tient pas compte de la complexité du courrier électronique :
import re import sys botched_line = re.compile("^(.*?)crypter(.*?)$") for line in sys.stdin: match = botched_line.search(line[:-1]) if match: # re.sub() serait peut-être meilleur ? newcontent = match.group(1) + "chiffrer" + match.group(2) + "\n" else: newcontent = line print(newcontent, end="")
On lit l'entrée standard, on se sert d'une expression rationnelle (avec le module re) pour trouver les lignes pertinentes, et on les modifie (au passage, le point d'interrogation à la fin des groupes entre parenthèses est pour rendre l'expression non gourmande). Cette méthode n'est pas bonne car elle oublie :
Il faut donc faire mieux.
Il va falloir passer au module email. Il fournit tout ce qu'il faut pour analyser proprement un message, même complexe :
import sys import re import email import email.message import email.policy import email.contentmanager botched_line = re.compile("^(.*?)crypter(.*?)$") msg = email.message_from_file(sys.stdin, _class=email.message.EmailMessage, policy=email.policy.default) for part in msg.walk(): if part.get_content_type() == "text/plain": newcontent = "" for line in part.get_content().splitlines(): match = botched_line.search(line) if match: # re.sub() serait peut-être meilleur ? newcontent += match.group(1) + "chiffrer" + match.group(2) + "\n" else: newcontent += line + "\n" email.contentmanager.raw_data_manager.set_content(part, newcontent) print(msg.as_string(unixfrom=True))
Ce code mérite quelques explications :
email.message_from_file
lit un fichier
(ici, l'entrée standard) et rend un objet Python de type
message. Attention, par défaut, c'est un ancien type, et les
opérations suivantes donneront des messages d'erreur
incompréhensibles (comme « AttributeError: 'Compat32'
object has no attribute 'content_manager' » ou
« AttributeError: 'Message' object has no attribute
'get_content'. Did you mean: 'get_content_type'? »). Les
paramètres _class
et
policy
sont là pour produire des messages
suivant les types Python modernes.walk()
va parcourir les différentes
parties MIME du message, récursivement.get_content_type()
renvoie le
type MIME de la partie et nous ne nous
intéressons qu'aux textes
bruts, les autres parties sont laissées telles
quelles.get_content()
donne accès aux données
(des lignes de texte) que splitlines()
va
découper.set_content()
remplace l'ancien contenu
par le nouveau.as_string
transforme l'objet
Python en texte. Notre message a été transformé.Ce code peut s'utiliser, par exemple, depuis procmail, avec cette configuration :
:0fw | $HOME/bin/repair-email.py
Évidemment, il peut être prudent de sauvegarder le message avant cette manipulation, au cas où. En procmail :
:0c: Mail/unmodified
Vous voulez tester (sage précaution) ? Voici un exemple de message, fait à partir d'un spam reçu :
From spammer@example Fri Nov 24 15:08:06 2023 To: <stephane@bortzmeyer.org> MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="----=_Part_9748132_1635010878.1700827045631" Date: Fri, 24 Nov 2023 13:42:17 +0100 (CET) Subject: 🍷 Catalogues vins From: Ornella FEDI <ornella.fedi@vinodiff.com> Content-Length: 65540 Lines: 2041 ------=_Part_9748132_1635010878.1700827045631 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Cet E-mail dit "crypter". Pour pouvoir afficher cet E-mail, le clie= nt de messagerie du destinataire doit supporter ce format. ------=_Part_9748132_1635010878.1700827045631 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable <?xml version=3D"1.0" ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns=3D"http://www.w3.org/1999/xhtml"> <head> <title>E-Mail</title> <style type=3D"text/css" media=3D"screen"> </style> </head> <body>Deleted to save room</body></html> ------=_Part_9748132_1635010878.1700827045631--
Mettez-le dans un fichier, mettons spam.email
et passez le au programme Python :
% cat /tmp/spam.email | ./repair.py From spammer@example Fri Nov 24 15:08:06 2023 To: <stephane@bortzmeyer.org> MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="----=_Part_9748132_1635010878.1700827045631" Date: Fri, 24 Nov 2023 13:42:17 +0100 (CET) ... ------=_Part_9748132_1635010878.1700827045631 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Cet E-mail dit "chiffrer". Pour pouvoir afficher cet E-mail, le client de mes= sagerie du destinataire doit supporter ce format. ------=_Part_9748132_1635010878.1700827045631 ...
Si vous voulez améliorer ce programme, vous pouvez gérer les cas :
+
par
quelque chose de plus rapide (je cite Bertrand Petit : concaténer des chaines en Python est
couteux. Chaque application de l'opérateur + crée une nouvelle
chaîne qui n'aura qu'une existence brève, et cela cause
aussi, à chaque fois, la copie de l'intéralité du contenu de la
chaine en croissance. À la place, il vaudrait mieux stocker chaque
bout de chaine dans une liste, pour finir par tout assembler en
une fois, par exemple avec join
).Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)
Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)