Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Mon livre « Cyberstructure »

Ève

Modifier un message entrant en Python

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.
  • Si l'expression rationnelle correspond au motif donné, on ajoute la version modifiée, sinon on ne touche à rien.
  • set_content() remplace l'ancien contenu par le nouveau.
  • Et enfin, 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 :

  • Plusieurs « crypter » sur une ligne.
  • Remplacer la concaténation + 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)