[couverture de Linux Magazine 61]

Réception de courriels avec Perl

Article publié dans Linux Magazine 61, mai 2004.

Copyright © 2004 - David Elbaz.

[+ del.icio.us] [+ Developers Zone] [+ Bookmarks.fr] [Digg this] [+ My Yahoo!]

Chapeau

Nous poursuivons la série d'articles sur les outils et méthodes perliennes pour appréhender le service le plus utilisé d'Internet : le courriel. Nous avons déjà abordé l'envoi et ainsi, laissant le transit à des programmes qui ont fait leur preuves dans le monde Unix (comme Sendmail), il ne nous reste plus que la récupération à traiter.

Introduction

Réceptionner implique beaucoup de choses : aller chercher le courriel sur le serveur, éventuellement le redistribuer, le trier, le classer, le lire bien entendu, l'archiver et j'en passe. Il est évident que nous ne pouvons pas traiter tout cela. Cependant, à la fin de cet article, fort des connaissances accumulées avec les deux précédents, attendez-vous à avoir les doigts qui vous démangent de créer la nouvelle alternative révolutionnaire à Mutt, Sylpheed ou KMail.

Dans le goût de mes précédentes introductions, je pourrais trouver un exemple de code qui vous montrerait, par sa simplicité, l'aisance et la puissance qu'offre Perl dans ce domaine. Si Perl était capable, par exemple, en une seule ligne, de vous imprimer à l'écran le nombre de messages qui vous attendent sur le serveur du fournisseur fai.com pour le compte user,

    $ perl -MNet::POP3 -le'print Net::POP3->new("pop.fai.com")->login("user","pass")'

je tiendrais à coup sûr une belle façon de commencer cet article.

Mais je n'userai pas de ce stratagème cette fois-ci (ou si peu ;-), nous en sommes au troisième article et vous savez désormais que l'outil courriel, malgré sa facilité d'accès est une chose complexe. C'est pourquoi il faut prendre le temps de bien expliquer les choses et c'est ainsi que nous allons nous borner à parler de la récupération du courriel et de l'extraction des différentes éléments qui le composent.

De son arrivée dans votre boîte

Pour accomplir sa destinée de courriel, un message doit arriver dans votre boîte aux lettres. Ce réceptacle logiciel peut se matérialiser différemment suivant les formats et les protocoles : il peut être sur votre disque ou pas, intégralement ou pas, figurer dans un seul fichier ou dans plusieurs... Ces options peuvent être présentées comme suit, du plus simple, au plus compliqué...

Du plus simple

Lors du premier article de cette série, nous vous avions donné quelques éléments de compréhension sur les étapes de l'acheminement d'un courriel. Nous avions expliqué comment votre correspondant, pour vous expédier un message, avait bien formaté un message et l'avait laissé aux mains d'un serveur SMTP qui l'avait fait transiter de serveur en serveur jusqu'au serveur dépositaire de votre compte.

Si votre lecteur de courriel se trouve sur la même machine que ce dernier serveur SMTP[1] (mini-réseau domestique, petite officine...), le courriel a fini son voyage. Il ne lui reste plus qu'à être lu en faisant pointer votre MUA[2] sur l'arborescence dédiée du MTA[3] (typiquement dans /var/mail/).

Dans le cas (assez classique) où l'administrateur système préfère faire une distribution du courrier (de facto, déplacer ou copier les messages dans les répertoires dédiés aux courriels des différents utilisateurs), il y a des programmes pour prendre le relais et s'acquitter de cette tâche. Citons Procmail, ou les modules Mail::Audit (de Simon Cozens) et Mail::Procmail (de Johan Vromans) pour développer une solution « maison » avec Perl.

Cependant, il peut être des situations où les choses étant simples, elles ont toutes les raisons de le rester : un courriel est un fichier texte et notre système d'exploitation favori dispose de toute une batterie de petits utilitaires pour les manipuler (les déplacer, les concaténer...).

Voici un petit morceau de code avec lequel vous êtes familiers (les lecteurs assidus retrouveront avec plaisir la discussion de nos amis « toi » et « moi » au sujet de la soirée de ce premier) :

    #!/usr/bin/perl -w

    use MIME::Lite;

    $msg = MIME::Lite->new(
            From    => 'moi@monchezmoi.org',
            To      => 'toi@toncheztoi.org',
            Subject => "Re: Hier soir !",
            Data    => "Effectivement, ça avait l'air sympa ! Très belle photo..."
    );

    $msg->send;

Ces lignes de code, en utilisant un module avec une interface de haut niveau, forment et envoient très simplement un message. Si nos deux amis utilisent en fait la même machine (au travail ou à l'université par exemple), et que « moi » a les droits nécessaires, on peut remplacer la dernière ligne

    $msg->send;

qui envoie le message par le biais d'un serveur SMTP (sur la machine ou par le réseau suivant les options disponibles) par :

    # Création d'un identifiant pour le message en utilisant la fonction time()
    my $sauvegarde = time();

    # Création d'un fichier de sauvegarde du message envoyé
    open COPIE, '>/home/moi/courriel/envoyes/'.$sauvegarde
      or die "ne peut pas créer le fichier $sauvegarde $!\n";

    # Sortie du message formé en une seule chaîne
    print COPIE $msg->as_string;

    close COPIE;

    # Livraison du message à son destinataire
    system('cp',
           '/home/moi/courriel/envoyes/'.$sauvegarde,
           '/home/toi/courriel/Reception');

Cet exemple montre que le service postal le plus simple pour un courriel est le couple cp - mv (auquel il faut parfois adjoindre cat pour ajouter le message à la fin d'une suite d'autres comme dans le format mbox, ainsi que cron pour automatiser la tâche). Les premières applications de messagerie (comme MAILBOX dans les années soixantes ou SENDMSG en 1972 sur Arpanet) ne faisaient d'ailleurs rien de plus.

C'est cette simple opération que j'ai mise en pratique, en l'occurrence, lors des différents tests effectués dans le cadre de la rédaction de cette série d'articles. J'ai livré le résultat de ces tests via mv dans le répertoire de travail de mon MUA. J'ai ainsi vérifié la conformité des courriels créés via les exemples donnés dans les articles.

Faites-en autant lorsque vous développez une application qui envoie des courriels : livrez-vous les d'abord !

Au plus compliqué

Linux regorge de programmes qui se contentent de faire une seule chose mais bien. On peut ainsi trouver toutes sortes d'utilitaires qui vont accomplir une tâche, un fragment de l'opération de récupération-récupération et distribution.

On peut alors chaîner Fetchmail pour la récupération des messages, Procmail pour les filtrer, Mutt pour les lire, les classer et y répondre (du moins rédiger la réponse. Et même si la rédaction stricto sensu se fait par un éditeur externe, mais bref, je ne vais pas m'en sortir ;-), et Postfix pour les expédier.

Tout ceci demande tout de même une certaine somme d'expertise pour la mise en œuvre, la configuration et le contrôle de cet ensemble logiciel.

En passant par des solutions intermédiaires

Il est tout de même possible de simplifier cela : les lecteurs de courriels actuels intègrent pour la plupart les fonctionnalités des programmes susmentionnés. Un programme unique (Sylpheed, Evolution...) sera suffisant pour récupérer vos courriels sur votre disque, s'il respecte un des protocoles conçus à cet effet.

Principalement, il existe les protocoles IMAP et POP3. Tous les deux ne requièrent de vous que l'installation d'un lecteur de courriel et la création d'un compte.

IMAP a été créé pour allier le meilleur des deux mondes : un minimum de composants logiciels côté client avec néanmoins des possibilités accrues de gestion de son compte courriel sur le serveur. IMAP vous autorise à manipuler vos messages et par là-même l'arborescence du serveur SMTP comme s'il s'agissait de vos dossiers locaux. Cette manipulation est d'ailleurs assez fine puisque le protocole prévoit plus de vingt commandes. Il présente aussi l'avantage de pouvoir consulter son courrier à partir d'ordinateurs différents tout en conservant l'intégrité de sa boîte aux lettres puisque les messages restent sur le serveur. Mais ce protocole a plusieurs défaut majeurs. Tout d'abord, le défaut de ses qualités : son nombre important de fonctionnalités le rendent plus compliqué à appréhender, à programmer et à offrir en tant que service à des usagers. Il y a aussi la nécessité d'être en quasi-permanence en liaison avec le serveur lors de la consultation et de la manipulation (problème), et celle de laisser une grande marge de manœuvre à l'utilisateur (et par conséquent de devoir lui faire plus confiance : problème ;-).

Le protocole POP3 est devenu prédominant parce qu'avec lui ne se posaient pas ces deux problèmes. Il y a aussi le fait qu'il soit plus mûr (plus ancien). D'autres facteurs importants ont contribué à cet état de fait. Il y eut l'émergence de toute une catégorie d'utilisateurs d'Internet (les particuliers, qui ne disposaient pas, à l'ère du RTC[4], de connexion permanente). Avec eux est venue la nécessité de simplicité, tant du côté client que du côté serveur.

POP3

Initié par la société Qualcomm, le protocole POP a évolué aujourd'hui dans sa troisième version majeure pour devenir un standard d'Internet à part entière. Il y a eu bien entendu les protocoles POP (pour la première version) et POP2 mais ils sont obsolètes depuis un certain temps et lorsqu'on parle aujourd'hui du protocole POP, c'est de POP3 qu'il s'agit.

Le protocole POP (Post-Office Protocol) version 3 est un protocole assez simple. Comme tous (presque) les autres standards, il est couvert par une RFC, la 1939. Elle est assez digeste, si vous êtes intéressé par le sujet du courriel, je vous conseille de la lire.

Autrement, comme à chaque fois, je vais vous en livrer une synthèse.

La base du protocole est avant tout la simplicité. Même s'il est évident qu'au bout du compte ce sont les développeurs de MUA qui vont manipuler ce protocole, il n'en est pas moins conçu à l'attention d'un utilisateur moyen. Techniquement, c'est le principe des états qui est utilisé. La connexion TCP avec le serveur va passer par différents états qui vont autoriser l'accès à un certain nombre et type de commandes en rapport avec cet état.

Même si vous pouvez avoir besoin de certaines données avant de pouvoir en demander d'autres, il n'y a pas de séquence de commandes imposée, à contrario du protocole SMTP, qui envoie le courriel selon une séquence de commandes précise.

Il y a trois états, dont l'un est à usage interne du protocole (la mise à jour Update), restent deux qui sont l'autorisation et la transaction (Authorization et Transaction).

Le premier des deux est le stade initial de la connexion au serveur, stade où est attendue l'authentification de l'utilisateur. Ceci se fait classiquement par l'intermédiaire de la transmission d'un nom d'utilisateur et d'un mot de passe. Ici on touche au gros défaut du protocole : la sécurité. POP3 prévoit deux méthodes d'envoi, l'une est sécurisée (apop, native mais au final assez peu utilisée), l'autre (login) ne l'est pas et envoie les nom d'utilisateur et mot de passe en clair sur le réseau.

On ne compte plus les conférences qui ont comporté une présentation sur la sécurité réseau qui, à titre de démonstration, faisait défiler à l'écran des mots de passe interceptés récemment par les présentateurs. Il s'agit la plupart du temps d'utilisateurs ayant vérifié leur courrier, via le réseau de la conférence (espionné par lesdits présentateurs), avec le protocole POP3 qui envoie leur mot de passe en clair. Malheureusement, ça n'est pas le seul problème du point de vue de la sécurité : les messages aussi sont envoyés en clair et là il n'y a rien de prévu en natif au protocole pour pallier à ce manque.

Le second état, la transaction, est celui où on va accéder aux messages et aux données inhérentes à ces messages. À la différence du protocole IMAP, le nombre de commandes est restreint puisque le but est uniquement de préparer le téléchargement (par l'obtention préliminaire de certaines données) et de télécharger les messages depuis le serveur. Le but est aussi de les effacer : il est bien dit dans la RFC que POP n'est pas IMAP et que les messages ont vocation à être effacés du serveur. Cette disposition dépend de la bonne programmation des MUA et du bon comportement des utilisateurs. Néanmoins, il y a à la fin de la RFC plusieurs propositions pour aider l'administrateur à faire face à des utilisateurs qui ne respecteraient pas ces dispositions.

La conversation entre client et serveur

Voici un exemple de conversation entre un poste client et un poste serveur dans le cadre du protocole POP3.

    Serveur: <Attente de la connexion sur le port 110>
    Client:  <ouverture de la connexion>
    Serveur: +OK POP3 server ready <1896.697170952@pop.fai.com>
    Client:  APOP laurent.dupont c4c9334bac560ecc979e58001b3e22fb
    Serveur: +OK laurent.dupont's maildrop has 2 messages (320 octets)
    Client:  STAT
    Serveur: +OK 2 320
    Client:  RETR 1
    Serveur: +OK 120 octets
    Serveur: <envoi du message 1>
    Serveur:  .
    Client:  DELE 1
    Serveur: +OK message 1 deleted
    Client:  RETR 2
    Serveur: +OK 200 octets
    Serveur: <envoi du message 2>
    Serveur:  .
    Client:  DELE 2
    Serveur: +OK message 2 deleted
    Client:  QUIT
    Serveur: +OK dewey POP3 server signing off (maildrop empty)
    (client et serveur ferment leur connexion)

Dans cette session classique de récupération de courrier, on voit d'abord le client se connecter. Le serveur lui répond qu'il est opérationnel. Le client reprend la main en envoyant par la méthode apop (sécurisée) le doublon composé du nom d'utilisateur et de la clé d'identification. Le serveur renvoie, en cas de succès de l'authentification, le nombre de messages présents sur ce serveur pour ce compte. Ensuite, le client demande un peu plus d'information sur les messages et obtient du serveur le nombre de messages et leur taille cumulée (nous verrons un exemple pratique de ces informations). Par la suite et par deux fois, le client va télécharger les messages et les effacer, en prenant en argument le numéro des messages l'un après l'autre. Dans les trois dernières lignes, le client annonce qu'il souhaite cesser la transaction, le serveur lui répond qu'il se retire. Client et serveur ferment la connexion.

C'est à ce moment que le serveur entre dans l'état « mise à jour » (UPDATE) et c'est véritablement à ce moment que les changements demandés par l'utilisateur sont effectués. Par exemple, les messages donnés en arguments de la commande DELE (effacement du serveur) étaient juste marqués pour l'effacement. L'incidence immédiate de la commande DELE est que les messages ainsi marqués n'apparaîtraient plus si l'utilisateur demandait la liste des messages présents sur le serveur. L'effacement physique des messages ne se fait véritablement que dans l'état suivant (UPDATE) et tant que cet état n'est pas atteint, il est possible de revenir en arrière avec une autre commande POP (RSET).

Vous avez peut-être observé des lignes où le serveur ne fait qu'envoyer un point. En fait, il s'agit de la chaîne de cinq octets "CR LF . CR LF". Cette chaîne est en fait la chaîne qui ferme conventionnellement une suite de données multi-lignes (comme dans le protocole SMTP).

Pour le reste, et notamment le détail des commandes qui interviennent dans cette session, il n'est pas indispensable de les connaître par cœur : il y a un module Perl qui les interface parfaitement dans le respect des standards. Autant apprendre directement à manipuler ce module.

Net::POP3

Net::POP3 est, comme son nom l'indique, de la même famille que Net::SMTP présenté dans le premier volet de cette série. Cette famille, c'est la libnet Perl qui a vocation à fournir des API de bas niveau aux protocoles réseaux. Les API sont orientées objet et héritent de Net::Cmd.

Bien entendu, je parle ici de la série de modules développés initialement par Graham Barr aux mileu des années 90. Depuis, une quantité impressionnante de modules utilisant les briques élémentaires de la libnet a été publiée et il est sûr qu'un certain nombre n'est plus de bas niveau et n'hérite pas de Net::Cmd.

La méthode new()

Le constructeur est dans la droite ligne de ce qui se fait dans la famille libnet : en construisant l'objet, on passe en argument le nom ou l'adresse de la machine distante à laquelle le module va s'adresser. On peut ensuite donner des options sur le comportement général du module pendant l'exécution du script.

En dehors du nom du serveur POP, les arguments sont donnés sous forme de hachage avec des couples clés-valeurs.

    $pop = Net::POP3->new('pop.fai.com', Debug => 1);

On se connecte à la machine pop.fai.com et on demande au module de passer en mode débogage.

La méthode login()

Cette méthode interface deux commandes POP qui, de toutes façons, ne sont jamais utilisées l'une sans l'autre (du moins lorsqu'on cherche à faire un programme qui fonctionne ;-). Ces commandes USER et PASS, vous l'avez compris, servent à envoyer (attention ! envoi en clair) vos nom d'utilisateur et mot de passe.

En cas de succès (lorsque l'utilisateur a été reconnu par le serveur), elle renvoie le nombre de messages du compte. C'est le serveur qui prend l'initiative de vous fournir cette valeur de retour. Elle n'est pas spécifiée dans la RFC qui préconise l'envoi de la valeur de retour standard pour le succès de l'exécution d'une commande. Pas plus qu'on ne retrouve ceci dans les sources du module qui se contente d'envoyer la commande et de renvoyer ce qu'il a reçu.

    $num_msg = $pop->login('utilisateur', 'mot_de_passe');

Lorsque le nombre de messages sur le serveur pour le compte 'utilisateur' est de 0, nous nous retrouvons dans le célèbre cas de l'informatique : 0 but true. 0 est souvent utilisé en informatique pour représenter l'équivalent numérique à la valeur faux. Perl n'échappe pas à cet usage et si la valeur renvoyée en cas d'absence de message était 0, elle pourrait être interprétée comme un échec dans l'authentification de client.

Il faut donc ruser pour renvoyer le nombre de messages, possiblement 0, tout en renvoyant toujours une valeur vraie lors de la réussite de l'authentification (0 mais vrai). C'est donc la chaîne 0E0 qui est utilisée dans ce cas de figure. Elle signifie 0 multiplié par 10 exponentielle 0 qui a une valeur numérique nulle, mais qui est vraie en tant que booléen au sens de Perl.

La méthode apop()

Cette méthode interface la commande POP APOP. C'est la seule (et maigre) tentative de sécuriser un peu ce protocole dont ce n'est pas le souci premier (c'est le moins qu'on puisse dire, mais ceci s'explique par son histoire : ce protocole a été défini à une époque où le problème de la sécurité ne se posait pas de la même façon qu'aujourd'hui).

Cette méthode nécessite la présence du module Digest::MD5 et renverra la valeur undef en cas d'absence de ce dernier.

Elle est en fait similaire à login() en cela qu'elle va avoir les mêmes arguments, le même but (l'authentification auprès du serveur), et la même valeur de retour.

La seule différence est que le mot de passe est crypté via le module Digest::MD5 et ne transite donc pas sur le réseau en clair.

    $num_msg = $pop->apop('utilisateur', 'mot_de_passe');

La méthode list()

Interface à la commande LIST. Sans argument, cette méthode renvoie une référence à un hachage dont les clés sont les numéros des messages sur le serveur (non marqués comme effacés) et les valeurs leur taille. Avec un numéro en argument, c'est la taille du message référencé qui est renvoyée.

    $tailles_ref = $pop->list();

La méthode top()

Avec cette méthode nous est donnée la possibilité, avant même d'avoir le message en local sur notre disque, d'accéder au contenu d'un message. Les arguments sont obligatoirement un numéro de message et optionnellement un nombre n de lignes. La valeur renvoyée sera une référence à un tableau contenant les en-têtes du message et optionnellement les n premières lignes du message.

    $lignes_ref = $pop->top($index);

La méthode get()

Comme le nom l'indique, il s'agit ici de récupérer le message du serveur. Cette méthode, interfaçant la commande RETR (retrieve, récupérer en anglais), prend bien sûr en argument le numéro du message que l'on désire télécharger. Le contenu du message est renvoyé dans une référence vers un tableau contenant les lignes du message. Il est par ailleurs possible de préciser un descripteur de fichier ; à ce moment-là, le message est imprimé sur ce descripteur de fichier et donc lisible à partir de celui-ci.

    $lignes_ref = $pop->get($index);
    $pop->get($index, FH);

La méthode getfh()

Même finalité que get(), mais une procédure sensiblement différente. Un descripteur de fichier lié (Tie) est renvoyé et le message n'est véritablement téléchargé que lorsque que la valeur EOF (end of file, fin de fichier en anglais) est renvoyée.

    $fh = $pop->getfh($index);

La méthode uidl()

Interface à la commande UIDL, cette commande renvoie une référence à un hachage ayant comme clés les numéros des messages et comme valeurs un identifiant unique à chaque message. Si un numéro de message est donné en argument, c'est l'identifiant de ce message qui est renvoyé.

    $id_ref = $pop->uidl();

La méthode delete()

Cette méthode marque un message comme étant à effacer à la prochaine mise à jour. L'argument est évidemment un numéro de message.

    $pop->delete($index);

La méthode reset()

C'est la commande RSET, interfacée par cette méthode, qui remet le serveur dans son état initial. Ceci est notamment utile pour faire disparaître le marqueur qui préparait un message à l'effacement, et faire de nouveau apparaître ce dernier dans les listings obtenus avec la méthode list().

La méthode quit()

Dernière méthode de ce survol de l'API de Net::POP3, et dernière méthode de ce module utilisée lors d'une connexion avec un serveur POP. Elle ferme la connexion et provoque le passage du serveur à l'état « mise à jour » (UPDATE).

Encore quelques mots

Il faut toujours aller à la source lorsqu'on cherche l'instruction et je vous engage, d'évidence, à aller voir la documentation de ce module. Mais pour une fois, POP étant un protocole assez simple, nous pouvons être contents de l'aperçu donné ici. Profitons-en, une fois n'est pas coutume ;-)

Il me reste à vous dire que toutes les méthodes du module vous renverrons les valeurs 'vrai' ou 'faux' en cas de réussite ou d'échec. Il vous est ainsi possible comme avec Net::SMTP de contrôler pas à pas l'exécution de votre script. On doit remercier la RFC pour cela qui insiste sur la nécessité de renvoyer systématiquement une réponse à chaque commande POP (en l'occurrence +OK pour une exécution réussie et -ERR pour un échec).

Biff.pl

    "Alors que peut-on bien faire avec tout cela ? Récupérer des messages ?     - Ah ! Non ! C'est un peu court, jeune homme. On aurait pu dire bien des choses en somme."

Récupérer des messages, votre MUA le fait très bien. Alors, à moins d'avoir un besoin spécifique comme nous le verrons à la fin de l'article, je vous conseille de continuer à le laisser opérer. En revanche, on peut s'amuser à créer en quelques lignes des programmes bien utiles.

Vous connaissez peut-être biff, ce vieux programme qui allait vérifier si nous avions des messages dans notre (nos) boîte(s) aux lettres.

Avec Perl, moins d'une vingtaine de lignes suffisent (en prenant ses aises :-).

    #!/usr/bin/perl -w
    use strict;
    use Net::POP3;

    while (<DATA>) {
        chomp;
        my ( $server, $login, $pass ) = split /\s/;

        my $fail;
        my $pop = Net::POP3->new($server)
          or $fail++;

       if($fail) {
           warn "ne peut se connecter à $server\n";
           next;
       }

        my $num = $pop->login( $login, $pass );
        print "$num nouveau(x) message(s) pour $login sur le serveur $server\n"
          if $num > 0;

        $pop->quit();
    }

    __DATA__
    serveur1 user1 pass1
    serveur2 user2 pass2
    serveur3 user3 pass3

Libre à vous d'améliorer l'affichage avec une jolie fenêtre et quelques boutons construits avec une boîte à outils graphique comme GTK+ ou Tk (présenté dans un article que vous pourrez prochainement lire).

Perl sait faire cela aussi !

Biffplus.pl

Je ne résiste pas au plaisir de vous montrer un exemple encore plus amusant.

Ici, je vais aller récupérer des informations sur le serveur POP sur la base du script précédent. En plus d'informer sur la présence de messages sur le serveur, je vais vérifier leur taille : j'ai une connexion RTC, et je préfère télécharger les gros messages la nuit (encombrement du réseau, disponibilité de ma ligne de téléphone...). De même, certaines de mes adresses de courriel ne servant que pour des listes de diffusions techniques, un courriel de plus de 40 Ko est nécessairement un spam. Autant économiser son téléchargement.

Je vais aussi filtrer, sur le serveur, les expéditeurs de mes courriels pour ne lancer une récupération de messages que lorsque j'ai reçu un certain message.

    #!/usr/bin/perl -w
    use strict;
    use Getopt::Long;
    use Net::POP3;

    # définition et récupération des options :
    # * 'no-big' si c'est un compte qui ne doit pas recevoir de gros courriels
    # * 'from' pour spécifier une chaîne contenant (une partie de) l'adresse
    #   d'un expéditeur attendu
    my %opt;
    GetOptions(\%opt, "no-big", "from")
      or die "Options: --no-big --from <expéditeur>\n";

    # tant que nous n'avons pas lu toutes les lignes sous le marqueur __DATA__
    while(<DATA>) {
        chomp;
        # les variables sont séparées par une espace
        # elles parlent d'elles-mêmes sauf $big qui s'il est vrai indique
        # que nous ne souhaitons pas garder les gros courriels
        my ($server,$login,$pass,$big) = split/\s/;

        my $fail;
        my $pop = Net::POP3->new($server)
          or $fail++;

        if($fail) {
           warn "ne peut se connecter à $server\n";
           next;
        }

        # authentification et récupération du nombre de messages pour ce compte
        my $num = $pop->login($login, $pass);

        # s'il y a des messages,
        if($num > 0) {
            print "$num nouveau(x) message(s) pour $login sur le serveur $server\n";

            # et qu'au moins une des options est activée,
            unless($opt{'no-big'} or $opt{from}) {
                # on boucle sur les messages.
                for my $mess(1..$num) {
                    # Récupération de l'en-tête 'from' si elle contient la chaîne
                    # recherchée, à partir de la totalité des en-têtes stockées dans
                    # un tableau, référencé par la valeur renvoyée par la méthode top()
                    my($from) = grep {/^from: /i and /$opt{from}/} @{$pop->top($mess)}
                      if $opt{from};
                    print "expéditeur reconnu :\n$from\n" if $from;

                    # Si le message est trop gros,
                    if($pop->list($num) > 40000) {
                        # que l'option no-big est activée et que le compte est
                        # marqué comme étant un compte dédié à une liste de
                        # diffusion avec la variable '$big', on efface alors le 
                        # message, autrement on signale son existence.
                        if($opt{"no-big"} and $big) {
                            $pop->delete($num)
                        }
                        else {
                            print "attention, gros message\n"
                        }
                    }
                }
            }
        }

        $pop->quit();
    }

    __DATA__
    serveur1 user1 pass1 1
    serveur2 user2 pass2
    serveur3 user3 pass3

MIME::Tools

Nous continuons le tour des éléments logiciels dont nous avons besoin pour faire face aux diverses éventualités en matière de récupération et traitement de courriel reçu.

Un exemple typique est celui où l'on reçoit un message au format MIME, avec une pièce attachée binaire (à la base) et encodée avec le format Base64 (une image au format PNG par exemple).

Notre apprentissage de MIME::Lite (article précédent) ne nous est ici d'aucune utilité puisque nous nous souvenons que ce module est uniquement un constructeur de messages MIME et pas un analyseur.

Nous devons donc nous tourner vers les MIME::Tools.

Branche sœur de MIME::Lite, la branche des MIME::Tools comprend tous les outils nécessaires pour encoder correctement ce qui doit l'être, formater un courriel selon les spécifications MIME, ou au contraire l'analyser.

Rappel succinct

MIME (Multipurpose Internet Mail Extensions) est une norme régie par sept RFC visant à établir les standards du formatage du courriel moderne. Le respect de cette norme permet au courriel de remplir les tâches les plus diverses dans le domaine de la transmission de données, de simple missive à colis express.

Grâce à MIME, il est possible à votre MUA de séparer correctement la vidéo montrant les premiers pas du petit dernier du texte d'accompagnement. Pour cela, le message aura été construit grosso modo comme suit : des en-têtes qui fournissent des données précieuses sur le message tout entier (expéditeur, date, route empruntée...), puis plusieurs parties MIME comprenant elles aussi des en-têtes (spécifiques à cette partie), et les données elles-mêmes (texte, image, vidéo...). MIME prévoit aussi l'encodage de ces données selon certains besoins et certaines contraintes.

MIME::Base64

Un des encodages préconisés par MIME est le base64. Il est mis à contribution pour transformer les fichiers binaires (images, vidéos, texte formaté...) en longues chaînes de caractères. Le base64 encode les octets de 8 bits sur 6 bits, qui sont eux-mêmes codés en utilisant un sous-ensemble de l'US-ASCII : [A-Za-z+/=].

L'API de ce module est très simple : elle est constitué en tout et pour tout de deux fonctions exportées.

Boris

On expliquait dans l'article précédent sur MIME et MIME::Lite que même si la théorie voulait qu'on ait le choix de l'encodage d'une partie MIME, il en était autrement dans la pratique. Pour s'assurer de l'intégrité des données transmises via un courriel, il apparaissait assez vite qu'à un type de données correspond un encodage particulier.

Mais ceci ne s'applique pas à mon ami Boris (vous aurez remarqué au cours de ces trois articles que j'ai beaucoup d'amis ;-).

Mon ami Boris espionne la République Démocratique de Bordurie pour le compte de la Syldavie depuis bien longtemps maintenant. Comme tout bon espion, il sait que les méthodes de dissimulation les plus simples sont souvent les meilleures.

Alors Boris fait passer des documents confidentiels comme s'ils étaient des photographies en les encodant en base64.

    #!/usr/bin/perl -w
    use strict;
    use MIME::Lite;

    my $o = MIME::Lite->new();

    $o->field_order(qw(from to date subject));

    $o->build(From     => 'boris@boris.org',
              To       => 'igor@igor.org',
              Subject  => "mon beau petit chat",
              Data     => "Rendez-vous ce soir\nVenez seul",
              Encoding => 'base64',
              Type     => 'image/pjpeg'
    );

    $o->send();

Et voici ce que cela donne :

    From: boris@boris.org
    To: igor@igor.org
    Date: Fri, 19 Mar 2004 08:41:08 UT
    Subject: mon beau petit chat
    Content-Disposition: inline
    Content-Transfer-Encoding: base64
    Content-Type: image/pjpeg
    MIME-Version: 1.0
    X-Mailer: MIME::Lite 2.117  (F2.72; B2.21; Q2.21)

    UmVuZGV6LXZvdXMgY2Ugc29pcgpWZW5leiBzZXVs

Igor, le destinataire, n'a plus qu'à exécuter l'uniligne suivant :

    $ perl -MMIME::Base64 -nle 'chomp;print decode_base64($_) if /^$/..1' mess_boris.txt

Et il obtient bien :

    Rendez-vous ce soir
    Venez seul

Il ne s'agit là pas encore de stéganographie mais on s'en approche.

Le courriel de Boris est anormalement simple et il faudrait habituellement plus qu'un simple flip-flop pour déterminer qui fait quoi dans l'habillage d'un message MIME.

Evidemment étant donné la simplicité du rendu des courriels de Boris, Igor ne voit pas la nécessité d'employer une suite d'outils plus sophistiquée pour analyser son message. Nous si.

MIME::Parser

MIME::Parser est l'élément magique des MIME::Tools. C'est celui qui va transformer une source de données quelconque en objet Perl (un objet MIME::Entity), utilisable par la suite par les autres membres de la famille MIME::Tools.

Ce module propose un nombre certain de méthodes. On peut évoquer les multiples méthodes d'analyse qui sont autant d'appels à la même méthode parse() avec certaines automatisations. Il y a aussi toutes celles qui vont déterminer comment le module doit travailler (avec des fichiers temporaires sur disque, où les mettre, avec la RAM...). D'autres encore sont pensées pour gérer les erreurs.

Je ne prétend pas ici vous présenter MIME::Parser de la façon dont je l'ai fait pour d'autres, mais simplement vous décrire un peu les fonctionnalités que je vais utiliser plus bas, et essayer de vous donner l'eau à la bouche.

MIME::Entity

C'est de ce module plus précisément que MIME::Lite, présenté dans l'article précédent, est une version allégée. On y retrouve beaucoup des éléments de l'API de MIME:Lite, notamment toutes les méthodes de contruction du message (build(), attach() entre autres).

On y retrouve aussi (et surtout pour le propos de cet article) des méthodes pour analyser un message MIME représenté par un objet MIME::Entity.

Je ne suis plus président !

Il faut savoir passer la main et je l'ai fait. Je ne suis plus le président de l'association que j'ai fondée au premier article et qui s'est fixé comme tâche, au deuxième, la réhabilitation d'un castel gascon.

C'est le nouveau président qui se charge désormais de rédiger les lettres d'information mensuelles et de prendre la photographie attestant de l'avancée des travaux. C'est en revanche toujours moi qui envoie la lettre aux adhérents avec mon script élaboré au cours des deux articles précédents.

Il m'envoie ces informations par courriel et après avoir extrait plusieurs fois à la main le message et la photographie, j'ai décidé d'adapter mon script à la nouvelle donne.

Qu'est-ce qui change ?

La photographie et le texte ne sont plus sauvegardés sur disque par mes soins, ils arrivent par courriel. Je vais donc analyser le courriel entrant et fournir directement les données du courriel à mon programme.

Afin de faciliter la récupération du courriel en question, nous avons créé un compte dédié à cette tâche lettre_mensuelle@monassoce.org.

En outre, le nouveau président souhaite ajouter une partie HTML au message. Cela va à l'encontre de la netiquette mais nos messages sont relativement courts. Demi-mal...

Et qu'est-ce qui reste identique ?

moncourriel.pl v3.0-final

    #!/usr/bin/perl -w
    use strict;
    use Net::POP3;
    use Net::SMTP;
    use MIME::Parser;
    use MIME::Entity;
    use MIME::Lite;
    use POSIX 'strftime';

    my $pop = Net::POP3->new('pop.monfai.com')
      or die "ne peut me connecter au serveur POP pop.monfai.com\n";
    my $get = pop->get(1, FH) if $pop->login('lettre_mensuelle', 'secret') > 0;

    die "le message n'est pas encore arrivé\n" unless $get;

    $pop->delete(1) if $get;
    $pop->quit();

    my $parser = MIME::Parser->new();
    my $entity = $parser->parse(\*FH);

    my($body,$image_encoded);

    for($entity->parts()) {
        my $corps = $_->body();
        my $head = $_->head()->as_string;

        $body = join '', @$corps if $head =~ /content-type: text\/plain/is;
        $image_encoded = join '', @$corps if $head =~ /content-type: image\//is;
    }

    # obtention de la date sous la forme d'une chaîne aaaammjj 
    my $time = strftime("%Y%m%d", localtime);
    my $image_name = "castel_monassoce_$time.jpg";
    my $message_name = "message-$time.txt";

    open W, "/home/president/moncourriel/message/archives/$message_name"
      or die "ne peut créer le fichier $message_name\n";
    print W $body;
    close W;

    open W, "/home/president/moncourriel/image/archives/$image_name"
      or die "ne peut créer le fichier $image_name\n";
    print W decode_base64($image_encoded);
    close W;

    my $image_part = MIME::Lite->build(
        Type     => 'image/pjpeg',
        Encoding => 'base64',
        Data     => '/home/president/moncourriel/image/archives/$image_name',
        Filename => $image_name
    );

    # La partie variable du message concaténée en une seule chaîne
    my $head = join '', <DATA>;

    # Initialisation de la table contenant les champs du fichier 'liste'
    my @field = qw(GENRE NOM PRENOM ADRESSE);

    # Ouverture du fichier des adhérents
    open LIRE_LISTE, '/home/president/moncourriel/moncourriel/liste.csv'
      or die "ne peut ouvrir /home/president/moncourriel/moncourriel/liste.csv $!\n";

    while(<LIRE_LISTE>) {
        chomp;

        my @id = split/\t/;
        my %replace;
        @replace{@field} = @id;

        my $current_head = $head;
        $current_head =~ s/<<([A-Z]+)>>/$replace{$1}/sg;
        my $message = $current_head.$body;

        (my $html = $message) =~ s/\n/<br>/sg;
        $html = "<html>\n<body>\n<p>$html</p></body></html>";

        # Création d'un objet MIME::Lite avec les en-têtes du message
        my $mime = MIME::Lite->new(
            From       => 'La présidence <president@monassoce.org>',
            To         => "$replace{PRENOM} $replace{NOM} <$replace{ADRESSE}>",
            "Reply-To" => 'president@monassoce.org',
            Subject    => 'Avancement de notre projet',
            "X-Mailer" => 'moncourriel.pl v3.0-final',
            Type       => 'multipart/mixed'
        );

        # Attachement de la première partie MIME, le texte du message
        $mime->attach(
            Type       => 'TEXT',
            Encoding   => '8bit',
            Data       => $message
        );

        # Attachement de la partie MIME contenant le texte du message en
        # version HTML
        $mime->attach(
            Type       => 'text/html',
            Encoding   => '8bit',
            Data       => $html
        );

        # Attachement de l'image
        $mime->attach($image_part);

        # Envoi du courriel
        my $smtp = Net::SMTP->new("smtp.monfai.com")
          or die "pas de connexion smtp $!\n";
        $smtp->mail('president@monassoce.org') 
          or die "Problème avec la commande MAIL FROM\n";
        $smtp->to($replace{ADRESSE}) 
          or die "Problème avec la commande RCPT TO\n";
        $smtp->data() 
          or die "Problème avec la commande DATA\n";
        $smtp->datasend($mime->as_string())
          or die "Problème d'envoi de message\n";
        $smtp->dataend()
          or die "Problème avec la fin d'envoi de message\n";
        $smtp->quit()
          or die "Problème avec la commande QUIT\n";

        # pause de 20 secondes
        sleep 20;
    }

    close LIRE_LISTE;
    exit;

    __DATA__
    Bonjour <<GENRE>> <<NOM>>,

Comment ça marche ?

Note : Certaines portions non négligeables de ce code ont déjà été commentées, je vais néanmoins devoir revenir dessus, mais plus succinctement. Essayez de vous reporter aux deux articles précédents (et à la documentation de Perl) si tout ne vous est pas clair.

Le script commence donc comme presque tous ceux que nous vous présentons, par l'appel de l'interpréteur perl avec l'option (switch) -w, et le chargement de la pragma strict. Le but étant de nous aider à détecter rapidement les erreurs d'inattention.

On rentre dans le vif du sujet avec le chargement de tous les modules dont ce script va avoir besoin. C'est ici un peu la photo de famille de cette série d'articles : on y retrouve Net::SMTP pour l'envoi de courriel, Net::POP3 pour la récupération, MIME::Parser et MIME::Entity qui vont nous permettre d'analyser les courriels entrants, MIME::Lite pour formater des courriels dans le respect des spécifications MIME. Enfin, POSIX importe la fonction strftime() pour nous aider à formater aisément une date.

Les lignes suivantes sont dédiées à la récupération du message sur le serveur. On établit une connexion, initialise un marqueur (la variable $get) qui est incrémenté en cas de réussite du téléchargement du message. Téléchargement lui-même fonction de la valeur de retour de l'opération d'authentification auprès du serveur. (Rappelez-vous, le nombre de messages est renvoyé en cas de succès de l'authentification du client) Si le message n'est pas téléchargé ($get inchangée), on arrête le programme, autrement, on efface le message du serveur et on ferme la connexion.

Le message étant à notre disposition dans un descripteur de fichier, il ne nous reste plus qu'à le désosser pour en obtenir la substantifique moelle : le message de mon président qui est celui qui doit être repris pour tous les adhérents et l'image du mois.

Pour cela, nous construisons un analyseur (parser en anglais) sous la forme d'un objet MIME::Parser. Nous lui donnons une option de configuration avant de l'alimenter du message téléchargé via la méthode parse().

Une fois que MIME::Parser a fait son œuvre (transformer des données en un objet MIME::Entity utilisable par la suite des MIME::Tools), l'analyse est poursuivie par MIME::Entity et sa méthode parts(). Elle me renvoie une liste contenant les parties MIME composant le message.

Nous bouclons sur cette liste, et pour chaque partie MIME, nous isolons son corps (sous la forme d'une référence à un tableau des lignes le composant) et ses en-têtes. On force la sortie de ces dernières sous forme de chaîne (stringification) avec la méthode as_string().

En fonction du contenu des en-têtes, on détermine si on est en présence du texte du message (l'attribut MIME Content-Type sera égal à text/plain) ou d'une image (le même attribut serait par exemple égal à image/pjpeg). Les autres données, si elles existent, seront ignorées.

On passe ensuite à une phase préparatoire pour un certain nombre de données utiles à la suite du script. On va obtenir la date du jour qui va nous servir à nommer les fichier produits par le script (les noms du fichier image d'abord et du message ensuite). Ces fichiers sont archivés dans les répertoires prévus à cet effet (l'image est bien entendu décodée grâce au module MIME::Base64 et est sauvée sous forme d'image).

On va aussi d'ores et déjà préparer une des parties MIME, celle contenant l'image, qui se retrouvera à l'identique pour chaque adhérent (référez-vous à la présentation de MIME::Lite dans l'article précédent pour les arguments). Cette partie sera stockée dans la variable $image_part.

Pour en finir avec cette phase, la partie variable du message est récupérée, stockée et concaténée en une seule chaîne. De même, on initialise le tableau contenant les noms des champs des informations sur les adhérents contenues dans le fichier source.

Ce fichier est ouvert et on boucle dessus (sur ses lignes pour être précis).

Pour chaque ligne de ce fichier, on va récupérer les valeurs courantes avec un simple split(), alimenter le hachage courant regroupant ces valeurs avec comme clés les éléments du tableau de la phase préparatoire. Grâce à ce hachage, je peux simplement substituer les valeurs courantes là où il le faut dans la partie variable du message. Après concaténation de la partie fixe (message du président), j'obtiens le message complet et définitif.

À la suite de quoi, je crée une version HTML (très simple) de ce même message, en substituant la balise <br> au caractère \n pour respecter un tant soit peu la mise en page. Je complète cette version HTML par l'adjonction avant et après d'autres balises HTML.

Il ne reste plus qu'à créer le message MIME. Ceci est accompli par l'ajout les unes aux autres des parties MIME contenant le texte simple, le texte HTML et la photographie.

Le message ainsi formé est envoyé par la séquence des méthodes de Net::SMTP. Pour finir, une pause de 20 secondes est observée pour ne pas pas se faire marquer comme spammeur (et se faire bloquer) à cause de l'envoi massif de messages relativement gros.

On passe à la phase de clôture du script avec la fin de la boucle, la fermeture du descripteur du fichier source et la sortie du programme.

Quelques mots de plus

On peut, à la lecture de ce code, légitimement se demander pourquoi on décode (base64) l'image correctement isolée, on l'archive dans un fichier repris comme argument lors de la création d'une autre partie MIME (et donc du ré-encodage de l'image). Pourquoi ne pas utiliser la chaîne en base64 directement ?

Dans une partie MIME, en plus de fournir les données elles-mêmes, il faut aussi fournir certains attributs et notamment l'encodage afin que le MUA de votre destinataire sache comment prendre en compte ce message. Avec MIME::Lite, renseigner l'encodage dans le cas du base64 entraîne l'encodage des données fournies. Si c'est la chaîne, fruit de l'encodage en base64 qui est donnée en argument, l'image est en fait encodée une seconde fois.

Cependant, il y avait au moins une solution. J'ai choisi d'utiliser MIME::Lite comme formateur MIME parce que c'est celui que nous connaissons mais MIME::Entity qui est chargé au début du script peut s'en occuper aussi.

D'un autre côté, le résultat de l'analyse et de la segmentation du courriel reçu du président renvoie des objets MIME::Entity, on peut donc mettre de côté la partie MIME contenant l'image. Cette partie (un objet MIME::Entity) serait alors intégrée telle quelle dans le message MIME final à destination des adhérents.

Ce procédé est impossible avec MIME::Lite puisque, comme dit dans l'article précédent, il n'y a pas d'héritage possible entre les objets MIME::Lite et les objets MIME::Entity.

Une dernière chose sur l'emploi de Net::SMTP pour l'envoi du message. Il est expliqué dans l'article précédent pourquoi on peut préférer Net::SMTP à la méthode send() du module MIME::Lite pour envoyer son courrier. Je ne reviendrai pas dessus mais la chose me paraît suffisamment intéressante pour dire qu'elle n'est pas faite au hasard et qu'il y a quelque part une explication de ce choix.

Conclusion

Avec cet article qui se termine, vous savez à présent comment récupérer un courriel et l'analyser. Avec les apprentissages des deux articles précédents, qui ont traité de l'envoi et du formatage, vous êtes parés pour les cas de figure les plus classiques. La pratique se chargera de vous obliger à vous pencher un peu plus sur les documentations que je vous ai conseillé.

Mais surtout vous savez que toutes les fonctionnalités ayant trait au courriel sont implémentées de façon simple en Perl, langage polyvalent s'il en est. Il vous permettra avec la même aisance d'aller bien au-delà de la simple routine d'envoi de courriel. On peut penser à un webmail avec le module CGI, ou à un archiveur de courriels qui analyserait le contenu de ces derniers et le stockerait avec DBI[5], tout en proposant une interface utilisateur à ce programme avec Perl/Tk ou le module GTK2 (des articles sont en préparation sur ces sujets).

Références de l'article

La RFC 1939

La magie du flip-flop.

Les modules mentionnés

Annexes

Notes de l'article

  1. Simple Mail Transfer Protocol (serveur SMTP qui fait transiter le courrier d'un point à l'autre du réseau, régi par les RFC 2821 et 2822)

  2. Mail User Agent (votre lecteur de courriel)

  3. Mail Transfer Agent (serveur SMTP qui fait transiter le courrier d'un point à l'autre du réseau)

  4. Réseau Téléphonique Commuté (réseau téléphonique classique. Appellation aussi utilisée pour définir la connexion internet qui se fait sur ce réseau en utilisant les mêmes fréquences que la voix)

  5. Data Base Independent L'interface Perl à (presque) toutes les bases de données existantes.

    http://search.cpan.org/dist/DBI/DBI.pm

Auteur

David Elbaz, <lacravate@mongueurs.net>.

David Elbaz est membre de l'association des mongueurs de Perl ainsi que de Paris.pm. Il n'est l'auteur d'aucun module publié sur CPAN, ce qui tend à démontrer que ce n'est pas la peine d'être un gourou pour être un mongueur, participer à la vie de l'association ou simplement participer au groupe de travail "Articles".

Il remercie une troisième fois, mais pas moins chaudement pour autant, les membres du groupe de travail pour l'aide qu'ils lui ont prodiguée.

Vous pourrez retrouver les membres de ce groupe de travail, ainsi que de nombreux autres mongueurs et utilisateurs de Perl de tous horizons, à la prochaine conférence Perl en France et en français. Elle se déroulera à Paris à la Cité des Sciences, les 6 et 7 juin 2004.

http://conferences.mongueurs.net/2004/

[IE7, par Dean Edwards] [Validation du HTML] [Validation du CSS]