Article publié dans Linux Magazine 61, mai 2004.
Copyright © 2004 - David Elbaz.
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.
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.
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é...
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 !
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.
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.
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.
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
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
.
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.
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.
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');
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();
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);
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);
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);
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();
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);
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()
.
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
).
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).
"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 !
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
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.
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.
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.
encode_base64()
Cette fonction encode la chaîne passée en argument.
decode_base64()
Cette autre décode la chaîne passée en argument.
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
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.
new()
C'est évidemment la méthode qui constuit et initialise un objet analyseur.
$parser = MIME::Parser->new();
parse()
C'est la formule magique :)
. Elle prend comme argument n'importe quel
descripteur de fichier, ou n'importe quel objet dont la classe implémente les
read()
et getline()
de l'interface IO::
.
Elle renvoie un objet MIME::Entity
en cas de succès et undef dans le cas
contraire.
$entity = $parser->parse(\*FH);
output_dir()
C'est une des nombreuses méthodes pour modifier la façon qu'aura MIME::Parser
de travailler. En l'occurrence, on lui spécifie ici de stocker les résultats de
son analyse au moyen de fichiers temporaires, dans un certain répertoire.
L'argument de cette méthode est un chemin.
$parser->output_dir("/tmp");
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
.
new()
Le constructeur, incontournable lors d'un module orienté objet.
$entity = MIME::Entity->new();
parts()
Il s'agit ici de décomposer le message MIME et d'isoler ses différents composants (parties MIME).
Avec un numéro d'index n
en argument, cette méthode renvoie le n-ième
composant du message MIME. Autrement, une liste de toutes les parties MIME est
renvoyée.
Cette méthode peut aussi servir dans le cadre de la construction d'un message
MIME. Référez-vous à la documentation pour savoir comment.
@parts = $entity->parts();
body()
Cette méthode renvoie le corps d'une partie MIME, encodé, prêt à l'emploi. Il s'agit d'une structure en lecture seule et elle est sous la forme d'une référence à un tableau de lignes composant le corps du message.
$body_ref = $entity->body();
head()
Ici ce sont les en-têtes du message qui sont renvoyées sous la forme d'un objet
MIME::Head
.
$head = $entity->head();
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.
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...
l'arborescence dédiée :
/home/president/moncourriel/ /home/president/moncourriel/message/ /home/president/moncourriel/message/archives/ /home/president/moncourriel/image/ /home/president/moncourriel/image/archives/
Même si je ne suis plus président :)
.
Je laisse en l'état l'arborescence, je peux un jour m'occuper de nouveau de prendre les photographies et rédiger les messages (et ainsi reprendre la version 2 du script). Mais aujourd'hui, seuls les sous-répertoires d'archives vont me servir.
Le fichier source des adhérents est un simple fichier CSV (avec la tabulation comme séparateur) et attend qu'on ait besoin de lui dans le répertoire /home/president/moncourriel/.
La partie variable du message (avec les nom et prénom de l'adhérent) est à la
fin du script, accessible via le descripteur de fichier spécial DATA
.
#!/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>>,
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.
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.
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).
La RFC 1939
La magie du flip-flop
.
Le module Net::POP3
.
La suite des MIME::Tools
.
Le module Digest::MD5
.
La documentation de MIME::Lite
.
Mail::Procmail
de Johan Vromans.
Mail::Audit
de Simon Cozens.
La distribution IO
.
Pour ceux qui n'ont pas répéré l'allusion dans l'article, et pour tous ceux qui souhaitent s'offrir quelques instants de jubilation littéraire.
Si vous voulez en savoir un peu plus sur Boris et son pays.
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)
Mail User Agent (votre lecteur de courriel)
Mail Transfer Agent (serveur SMTP qui fait transiter le courrier d'un point à l'autre du réseau)
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)
Data Base Independent L'interface Perl à (presque) toutes les bases de données existantes.
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.
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.