Article publié dans Linux Magazine 60, avril 2004.
Copyright © 2004 - David Elbaz.
Dans l'article précédent, vous a été présentée la façon d'inclure dans vos programmes la fonctionnalité d'envoi de courriel grâce à l'utilisation d'une boîte à outils simple, efficace et de bas niveau. Vous avez vu cependant qu'elle ne permettait pas de tout faire dans ce domaine, qu'elle se contentait de faire bien une seule chose.
Mais bien entendu, la solution existe sur CPAN[1], la bibliothèque logicielle de
Perl (et même en plusieurs exemplaires). Je vous propose donc de poursuivre
notre survol des belles qualités de ce langage dans le domaine du courriel par
la présentation d'une autre boîte à outils : MIME::Lite. Ce module est un de
ceux qui vous permettront d'inclure autre chose que du texte dans votre courriel
et d'user (sans en abuser ;-)
, du plus rapide, efficace et économique, des
services postaux du monde.
La première opération délicate lorsqu'on veut utiliser un de ces modules "à tout faire" dans le domaine du courriel n'est pas l'installation ou la compréhension de la documentation, c'est de faire son marché.
Il n'y a pas moins de trois lignées de modules dont c'est le domaine premier :
on peut trouver sur CPAN les Mail::*
, les MIME::*
et les Email::*
. La
première a été initiée par Graham Barr (l'auteur de la libnet
, dont Net::SMTP
présenté dans le premier article est un composant), et la troisième avait le but
(présompteux ?) de mettre tout le monde d'accord en simplifiant et en
unifiant les procédures. Elle n'a fait que se surajouter aux deux autres.
Mon choix s'est donc porté sur la deuxième famille. Voici un exemple de code du module que j'ai choisi de vous présenter (les lecteurs de l'article précédent reconnaîtront la réponse faite à notre ami "moi" par notre autre ami "toi" au sujet de la soirée de ce dernier) :
#!/usr/bin/perl -w use MIME::Lite; $msg = MIME::Lite->new( From => 'toi@toncheztoi.org', To => 'moi@monchezmoi.org', Cc => 'lautre@sonchezlui.org', Subject => "Re: Hier soir !", Data => "Je te raconterai...\n\nToi." ); $msg->send;
Je vous propose de prendre le reste de cet article pour vous montrer la face cachée de cet exemple, en apparence si simple...
Il y a une certaine part de subjectivité. Lorsque j'ai survolé les
documentations des trois familles de modules et en particulier des modules dont
j'aurais besoin, c'est l'approche de MIME::Lite qui m'a le plus parlé. (Un lien
en fin d'article vous montrera aussi pourquoi :)
Mais à moins d'avoir besoin d'un morceau de code opérationnel dans les cinq
minutes, ne vous fiez pas à moi et allez voir par vous-mêmes.
Ne serait-ce que pour la qualité des contributeurs de la lignée Mail::*
.
Par exemple le module Mail::Mailer a été écrit initialement par Tim Bunce
(l'auteur de DBI
[2]) et Graham Barr, corrigé et documenté par Nat
Torkington (co-auteur du Cookbook - Perl en Action) et maintenu par Mark
Overmeer.
Le choix de MIME::Lite s'est aussi commandé à moi par quelques critères
tangibles (quand même ;-)
. MIME::Lite a exactement les fonctionnalités
dont j'ai besoin et rien de plus (c'est un encodeur, pas un décodeur), mais rien
de moins non plus (il connaît les cinq types d'encodage MIME et respecte
parfaitement les spécifications). Il est suffisamment modulaire pour me
permettre d'accéder à certaines fonctionnalités, tout en gardant la possibilité
de choisir de le mettre à contribution, ou pas, pour certaines autres.
De plus, il sait prendre des décisions techniques mieux que moi qui ne maîtrise
pas toutes les subtilités des spécifications MIME.
Ne pas connaître MIME sur le bout des doigts n'est pas une honte, loin de là ! MIME, c'est la bagatelle de sept RFC !
Les deux premières pour le courriel en général (déjà mentionnées dans l'article précédent, les RFC 2821 et 2822), que respectent les cinq autres qui donnent les spécifications de MIME à proprement parler. Y sont détaillés le format du corps d'un courriel (2045), les familles de formats (de fichiers) prévues et ce qui est préconisé pour chaque (2046), les en-têtes pour les parties du message en caractères non ASCII (2047), différentes procédures (2048) et les critères de conformité (2049).
L'idée de base est d'étendre le cadre d'utilisation du courriel, de lui rajouter des fonctionnalités, en quelque sorte de transformer l'enveloppe contenant une lettre en un paquet contenant un colis. Pour arriver à ce résultat sans rien modifier de ce qui avait cours précédemment en matière de courriel, on a utilisé la seule partie du courriel qui n'avait pas encore fait l'objet de spécification : le corps du message.
C'est donc dans cette partie du courriel que, grâce aux spécifications des Multipurpose Internet Mail Extensions (MIME), on va découper le corps du message de façon conventionnelle. De cette manière, de chaque côté de la chaîne du courriel, on saura retrouver, en plus de ce qu'on retrouvait déjà (des en-têtes fournissant des données supplémentaires, du texte), des fichiers vidéo, des fichiers musicaux, un texte formaté. Le tout empaqueté dans un seul et unique flux de données.
Pour cela on va couper le corps du message en plusieurs parties à l'aide de marqueurs (boundary en anglais) qui seront spécifiés dans les en-têtes (il peut y avoir plusieurs marqueurs pour un seul et même corps de message). Ensuite, on va un peu procéder comme si chaque partie du corps ainsi délimitée était un message à part entière : il va y avoir une série d'en-têtes qui auront la même forme que celle détaillée dans l'article précédent. Un saut de ligne marquera la fin de ces en-têtes et sera suivie des données (texte, code HTML, encodage d'une pièce jointe principalement).
Voici un exemple de courriel au format MIME présentant une version texte et une version HTML
Return-Path: <president@monassoce.org> Received: from smtp.monfai.com (81.228.148.17) by mail.autrefai.com; Fri, 30 Jan 2004 09:59:33 +0100 Subject: notre site web From: "la présidence" <president@monassoce.org> To: un_adherent@autrefai.com Date: Fri, 30 Jan 2004 08:55:10 +0000 Message-ID: <401A1BEE.7F98485B@toile.monfai.com> MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="----=_NextPart_000_041C_C40163C4.34BCFEC3" This is a multi-part message in MIME format. ------=_NextPart_000_041C_C40163C4.34BCFEC3 Content-Type: text/plain; charset="iso-8859-1" Content-Transfer-Encoding: 8bit Cher adhérent, voici notre site web enfin terminé. Il est consultable à l'adresse : http://www.monassoce.org la présidence. -- president@monassoce.org www.monassoce.org ------=_NextPart_000_041C_C40163C4.34BCFEC3 Content-Type: text/html; charset="iso-8859-1" Content-Transfer-Encoding: 8bit <html><body> <p>Cher adhérent,<br><br> voici notre site web enfin terminé. Il est consultable à l'adresse :<br> <a href="http://www.monassoce.org">http://www.monassoce.org</a><br> la présidence.<br><br> --<br> president@monassoce.org<br> www.monassoce.org<br> </p> </body></html> ------=_NextPart_000_041C_C40163C4.34BCFEC3--
Les données d'une partie MIME d'un courriel sont transmises selon un formatage plus ou moins élaboré qu'on appelle ici encodage. Mais par encodage, on entend aussi bien les encodages classiques de caractères sur 7 ou 8 bits (US-ASCII, ISO-8859-1 pour le français, etc...) que des modélisations plus avancées, ou au contraire un non-encodage qui restitue telles quelles les données.
Il y a cinq types d'encodages préconisés par MIME :
binary
Ce format est un non-encodage. Les données ne subissent aucune transfomation.
base64
Encode sous forme d'une longue chaîne de caractères. Spécialement adapté pour des fichiers binaires (images, film...)
7bit
Seulement des caractères encodés sur 7 bits (pas de caractères accentués donc) et une limitation dans la taille des lignes (moins de 1000 caractères)
8bit
Caractères encodés sur 8 bits mais toujours la limitation sur la longueur des lignes.
quoted-printable
N'importe quel type de caractères sans limitation aucune.
Théoriquement il est possible de choisir parmi ces cinq encodages pour un même type de données. Dans la pratique, ces encodages correspondent à des tâches et des données assez précises et si vous voulez être sûr que votre correspondant reçoive, par exemple, le PNG que vous lui envoyez, vous n'avez pas tant le choix que cela.
Seuls deux de ces encodages vont au-delà des classiques encodages de
caractères : le quoted-printable
et le base64
.
Ce sont aussi ces deux-là qui assureront, en particulier à nous français, de
l'intégrité des données transmises en toutes situations (même si le 8bit
pour le texte peut être souvent suffisant).
Le quoted-printable
assurera la transmission correcte, par exemple, de nos
caractères accentués et ne se souciera pas de la longueur des lignes (pratique
donc si vous développez un programme à destination d'utilisateurs lambda, sans
savoir donc quelle va être la langue de l'écrit et si l'utilisateur ne va pas
écrire un long courriel sans revenir à la ligne).
Le base64
sera la seule vraie alternative lorsqu'il s'agit d'acheminer un
fichier binaire (un exécutable, mais aussi une image ou un film). Il y a bien
l'encodage binary
mais il est paradoxalement trop libre car de fait aucun
encodage n'est réalisé et les données sont envoyées brutes.
Inutile de vous dire que ce (non-)encodage n'est pas, notamment, le plus à
même de faire passer votre courriel sans encombre au travers des différents
filtres, ô combien jusfifiés, placés sur sa route.
Le base64
en revanche va transformer votre fichier binaire en une longue
chaîne de caractères qui sera plus conforme à certaines règles de sécurité et
lisible par n'importe quelle machine sur la planète (le base64
encode les
caractères 8 bits sur 6 bits et les 6 bits sont codés selon un sous-ensemble de
l'US-ASCII : [A-Za-z+/=]
).
Voici ce en quoi est transformée une image JPEG blanche en 8x8 encodée en
base64
:
/9j/4AAQSkZJRgABAQEASABIAAD//gAXQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q /9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAi LCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIy MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy MjIy/8AAEQgACAAIAwEiAAIRAQMRAf/EABUAAQEAAAAAAAAAAAAAAAAAAAAH /8QAFBABAAAAAAAAAAAAAAAAAAAAAP/EABQBAQAAAAAAAAAAAAAAAAAAAAD/ xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwC/gA//2Q==
Bien entendu les spécifications MIME vont autrement plus loin et très au-delà de ce que j'ai synthétisé ici. Mais ceci devrait être suffisant pour nous y retrouver dans l'envoi de courriels avec pièces jointes, a fortiori avec l'aide de MIME::Lite.
MIME::Lite a été initié en 1997 par Eryq, l'auteur des MIME::Tools. Le code est désormais maintenu par Yves Orton (demerphq).
Il faut savoir que MIME::Lite est une sous-branche de la branche MIME::*
mais
aussi une branche sœur (et non pas fille) des MIME::Tools. Par conséquent, elle
n'en hérite pas et les objets MIME::Lite n'héritent donc pas des objets
MIME::Entity. C'est assurément un manque mais, vous allez le voir, MIME::Lite
sait faire plein de choses tout seul et se suffit bien souvent à lui-même.
Le principe général du module est simple : chaque objet MIME::Lite symbolise une partie MIME (section du courriel limitée par un boundary, cf. l'exemple de message MIME plus haut). Ainsi un bon nombre des méthodes de l'API tournent autour de l'initialisation, l'alimentation, la modification, l'accès aux attributs d'un objet MIME::Lite, attributs qui figureront pour la plupart les en-têtes, le texte, les fichiers joints du futur message.
Le comportement global du module au cours d'un script est configurable à travers quelques éléments de l'interface publique du module.
Il y a pour cela plusieurs méthodes de classe mais j'y reviendrai lors de
l'évocation de ces méthodes. Il y a aussi des variables d'environnement mais
leur positionnement par défaut est satisfaisant pour la majorité des emplois,
à part pour une.
Cependant cette variable est positionnée de façon à maintenir une
compatibilité ascendante et la documentation vous suggère de la laisser en
l'état. De toute façon, la fonctionnalité qui en découlait est accessible d'une
autre façon au prix de quelques lignes de code supplémentaires deci-delà.
(il s'agit de $MIME::Lite::AUTO_CONTENT_TYPE
qui, lorsqu'elle est vraie
,
demande au module de déterminer le type de fichier, l'attribut content-type
,
à partir du nom du fichier joint)
Du reste de l'API, on peut retenir en particulier les méthodes qui suivent.
new()
Bien sûr, dans le cas d'un module orienté objet, il y a besoin d'un constructeur.
$mime = MIME::Lite->new();
Cette méthode ne prend pas nécessairement d'argument. Dans ce cas, elle crée
simplement un objet qui va nous permettre d'appeler des méthodes par la suite.
Lorsqu'on fournit des arguments à cette méthode, ils ne lui sont en fait pas
destinés. Le module suppose que, lorsque des arguments sont envoyés à la méthode
new()
, on lui demande en fait de construire un objet et que cet objet appelle
la méthode build()
avec les dits arguments.
build()
Les données passées en argument à cette méthode déterminent de quoi sera fait le message MIME (en-têtes, texte, pièces jointes).
Ces données sont parfois pré-traitées, pour appliquer notamment des comportements par défaut (vous permettant de ne pas avoir à tout savoir des spécifications MIME), avant de produire le message MIME dans son intégralité.
build()
peut être appelée en tant que méthode d'instance ou méthode de
classe. Dans le premier cas, le message est formé à partir des arguments, dans
le second, un objet est créé préalablement avec new()
, le message est ensuite
formé avec build()
et l'objet est finalement retourné.
Le nombre et le type des arguments qu'accepte cette méthode est assez conséquent. En voici une synthèse :
Toutes les en-têtes classiques sont acceptées comme argument, néanmoins l'auteur
du module a prévu de considérer comme en-tête de courriel tout argument à cette
méthode qui se terminerait par les deux points :
.
"X-Bizarre-Header:" => "valeur" # sera accepté
Il vous suffit donc de renseigner ces en-têtes à l'appel (direct ou implicite) de
la méthode build()
pour qu'ils figurent dans le courriel.
A noter que les valeurs données aux en-têtes par le biais de cette méthode le
sont après que le module a positionné les en-têtes MIME. Autrement dit, vous
avez la possibilité de maîtriser ce qui est produit dans ce domaine (ou de faire
des erreurs. A manipuler avec précaution donc...)
Data
, FH
et Path
Ce sont les trois options pour fournir les données à proprement parler (message,
fichiers).
Data
est utilisé pour un scalaire ou une référence à une liste de scalaires
(qui seront simplement concaténés).
FH
est (étonnamment ! ;-)
utilisé avec un descripteur de fichier
(FileHandle en anglais).
Enfin Path
convient à tout ce qui est ouvrable au sens de la fonction
open()
, c'est à dire un grand nombre de choses, mais principalement un fichier
ou une commande.
Dans ce dernier cas, il est bon de connaître un autre argument,
ReadNow
, qui va lire une fois pour toutes le fichier ou la sortie de la
commande, lui évitant d'être lu et lu et lu... à chaque appel de la méthode
build()
.
Type
Cet argument va vous permettre de spécifier à quel type MIME la pièce jointe correspond. La chose est généralement assez triviale à déterminer mais vous pouvez vous référer à un lien, que j'ai fait figurer à la fin de cet article, vers une page qui les recense.
Il y a trois valeurs spéciales qui sont prévues par l'auteur du module.
TEXT
qui est la valeur par défaut si rien n'est spécifié (et si la valeur
globale $MIME::Lite::AUTO_CONTENT_TYPE
n'est pas modifiée). Dans le courriel,
elle figurera en tant que text/plain
.
AUTO
cherchera à établir le type en fonction du nom du fichier joint avec
application/octet-stream
comme défaut. Reste BINARY
qui est une autre
manière de renseigner application/octet-stream
.
Encoding
Ici on va dire comment sont encodées les données (ou comment on souhaite les voir encodées). Ce paramètre est censé, à la différence des précédents, être optionnel parce que le module a prévu de deviner quel doit être l'encodage si rien n'est précisé. Mais il semble clair que l'on ne puisse complètement se fier à ses choix, particulièrement pour le texte (et particulièrement pour nous français encore une fois). Donc, apprenez à savoir quoi faire de qui, à quel fichier appliquer quel encodage, le module vous demande (relativement) peu d'efforts, mais faites-en ici, c'est nécessaire (et plutôt simple de surcroît).
Filename
Cet argument sert à donner un nom au fichier envoyé, ainsi point de .
ou \
ou encore de /
ici, juste un nom (et pas un chemin). Cet argument est
notamment utile dans le cas où vous voulez faire apparaître à votre destinataire
un nom de fichier différent de celui du fichier sur votre disque.
$mime = MIME::Lite->build( From => 'toi@toncheztoi.org', To => 'moi@monchezmoi.org', Subject => "Re: Hier soir !", Type => 'TEXT', Encoding => 'quoted-printable', Data => "C'était très bien, je te raconterai..." ); $mime->build( From => 'president@monassoce.org', To => 'tresorier@monassoce.org', Subject => "nouvelle acquisition pour l'asociation", X-Mailer => 'moncourriel.pl v1.1', Type => 'image/jpeg', Encoding => 'base64', Path => "/home/president/bureaux_hors_de_prix.jpg", Filename => "nos_futurs_locaux.jpg", );
attach()
La méthode attach()
est celle qui va être utilisée après build()
ou
new()
pour ajouter une partie de message MIME, sous la forme d'un objet
MIME::Lite, à une autre partie MIME (ou la concaténation d'autres parties de
message).
Cette méthode prend un objet MIME::Lite en argument et l'ajoute à l'objet qui
invoque attach()
. Là aussi, il y a moyen de cumuler plusieurs actions en une
seule invocation. Si on fournit les mêmes types d'arguments que ceux donnés à
build()
(toujours sous forme d'un hachage), attach()
va automatiquement
créer un objet MIME::Lite (avec la méthode new()
), construire la partie de
message MIME (en passant les arguments à build()
), et l'attacher à l'objet
qui a invoqué attach()
.
La partie MIME (un objet MIME::Lite) créée au long de ce processus est retournée
par attach()
.
(cette méthode permet d'automatiser d'autres choses mais je vous engage à
apprendre à faire sans au début, simplement pour apprendre à faire les choses
bien. Prendre des raccourcis est bon lorsque l'on a conscience des étapes que
l'on a sautées...)
$mime = MIME::Lite->build( From => 'toi@toncheztoi.org', To => 'moi@monchezmoi.org', Subject => "Re: Hier soir !", Type => 'TEXT', Encoding => 'quoted-printable', Data => "C'était très bien, je te raconterai...\nRegarde en attendant !" ); $mime->attach( Type => 'image/pjpeg', Encoding => 'base64', Path => '/home/toi/photo/dream_team_soiree.jpg', Filename => 'soiree_hier.jpg' );
MIME::Lite possède toute une batterie de méthodes pour renseigner, lire, modifier les atributs des objets MIME::Lite après leur création. Il est inapproprié (autant qu'inutile) de les lister toutes. En voici un rapide survol.
add()
et attr()
.Ces deux méthodes servent à la même chose : positionner la valeur d'une
en-tête. L'une (attr()
) est destinée à traiter les en-têtes (attributs) MIME,
l'autre (add()
) est destinée à s'occuper des autres.
$mime->add("X-Mailer" => "moncourriel.pl v1.1"); $mime->attr("content-type" => "text/plain");
add()
a une syntaxe spéciale lorsqu'on a plusieurs occurrences de la même
en-tête :
$mime->add("X-Mailer" => ["moncourriel.pl v1.1", "MIME::Lite 2.117"]);
attr()
prévoit comment préciser un sous-champ MIME :
$mime->attr("content-type.charset" => "iso-8859-1");
field_order()
Cette méthode permet de préciser dans quel ordre vont figurer les en-têtes du courriel.
$mime->field_order("from", "to", "subject", "X-Mailer");
Cette méthode est aussi une méthode de classe. Utilisée comme telle, elle modifie le comportement du module pendant toute l'exécution du script.
get()
get()
est utilisée pour les en-têtes non-MIME. Elle permet de retrouver la
valeur d'un champ. Lorsqu'un champ a plusieurs valeurs, il est possible de les
avoir toutes ou de les récupérer individuellement grâce à leur indice.
$field = $mime->get("from"); # retourne la valeur du champ 'from' $field = $mime->get("X-Mailer"); # retourne "moncourriel.pl v1.1" $field = $mime->get("X-Mailer, 1"); # retourne "MIME::Lite 2.117" @field = $mime->get("X-Mailer"); # une liste contenant "moncourriel.pl v1.1" et "MIME::Lite 2.117"
replace()
Cette méthode remplace toutes les occurrences d'un champ par la valeur fournie. Elle est notamment utile pour faire disparaître certains champs MIME du rendu final.
$mime->replace("content-length" => $length);
Pour faire disparaître cet attribut MIME du courriel que va recevoir votre correspondant.
$mime->replace("content-length" => '');
sign()
D'autres méthodes (nombreuses) existent aussi pour manipuler le message
lui-même. J'ai retenu la méthode sign()
.
Elle permet d'ajouter une signature sans que vous ayez vous-même à le faire
(en la concaténant au reste du message comme vu dans l'article précédent).
Avec une chaine comme paramètre :
$mime->sign(Data => "--\npresident\@monassoce.org\nwww.monassoce.org");
Avec un fichier comme paramètre :
$mime->sign(Path => "/home/president/.sig");
Il y a aussi les méthodes qui s'occupent du rendu, il en existe deux familles : celles qui retournent une valeur et celles qui l'impriment.
as_string()
, body_as_string()
et header_as_string()
Ces méthodes retournent, respectivement, le rendu (la sortie) complet du message, son corps et l'ensemble de ses en-têtes.
$mess = $mime->as_string(); $body = $mime->body_as_string(); $headers = $mime->header_as_string();
Des équivalences existent pour chacune de ces méthodes dans l'autre famille de méthodes.
La dernière famille de méthodes est celle qui (enfin, après tout ce
travail ;-)
envoie le message.
Vous n'avez guère que deux choix en la matière : passer par votre propre serveur SMTP[3] (Sendmail, Postfix, Qmail...), ou vous adresser au travers du réseau à un autre qui acceptera de le faire.
send()
Cette méthode est une couche d'indirection pour toutes les méthodes d'envoi prévues dans le module.
Elle peut être invoquée sans argument et envoie à ce moment le message à
sendmail avec les arguments -t -oi -oem
.
Autrement on peut utiliser l'argument sendmail
avec les options désirées
$mime->send('sendmail', '/usr/lib/sendmail -t -oi -oem');
ou l'argument smtp
avec le nom du serveur
$mime->send('smtp', 'smtp.monfai.com');
ou enfin l'argument sub
avec une référence à une subroutine ainsi que les
arguments
$mime->send('sub', \&SUBROUTINE, @arg);
Enfin, il est possible d'utiliser cette méthode comme méthode de classe et (vous
me voyez venir ;-)
modifier les options de l'envoi de message pour tous
les messages, durant toute l'exécution du script.
MIME::Lite->send('smtp', 'smtp.monfai.com');
Vous vous souvenez de notre association favorite. Après moultes recherches, nous lui avons trouvé un projet digne d'elle : la réhabilitation d'un castel gascon. Afin de garder les troupes mobilisées, il est bon d'envoyer régulièrement une jolie photographie du castel avec sa lettre d'information (pour attester de son évolution).
Voici comment il est possible, en reprenant le travail déjà effectué (le script présenté dans l'article précédent) et en le structurant un peu, de tenir informés tous les adhérents au prix modique d'un peu d'ordre sur le disque dur et de l'exécution du script d'exemple.
Mise en place d'une arborescence dédiée avec la création de
/home/president/moncourriel/ /home/president/moncourriel/message/ /home/president/moncourriel/message/archives/ /home/president/moncourriel/image/ /home/president/moncourriel/image/archives/
Le fichier source des adhérents n'a pas changé, il s'agit toujours d'un simple fichier CSV liste.csv (avec la tabulation comme séparateur), et est placé dans le répertoire moncourriel.
La nouveauté de ce script (l'envoi d'une image) étant un facteur de complication, il devient nécessaire de structurer un peu plus les choses pour se simplifier la vie.
Le message de la prochaine lettre d'information sera donc sauvé dans le répertoire ad hoc et ainsi il sera simple de le retrouver. Ce sera d'autant plus facile que le fichier s'y trouvera seul puisqu'il est prévu qu'à la fin du script, le dit-message soit archivé dans le répertoire correspondant.
La même procédure et le même traitement seront bien entendu appliqués aux images.
Simple, non ? :-)
#!/usr/bin/perl -w use strict; use POSIX 'strftime'; use MIME::Lite; # Décommenter la ligne suivante si vous prenez l'option bas niveau # pour l'envoi #use Net::SMTP; # Précision de paramètres d'envoi pour tout le script MIME::Lite->send('smtp', 'smtp.monfai.com'); # Ouverture et lecture du répertoire des messages, détermination du # nom du fichier contenant le message courant opendir MESS, "/home/president/moncourriel/moncourriel/message" or die "ne peut ouvrir /home/president/moncourriel/moncourriel/message $!\n"; my ($body_file) = grep{/[a-z]/ and !-d "/home/president/moncourriel/moncourriel/message/$_"} readdir(MESS); closedir MESS; # ouverture du fichier contenant le message # et stockage du message tout entier dans une chaîne open LIRE_CORPS, "/home/president/moncourriel/moncourriel/message/$body_file" or die "ne peut ouvrir corps $!\n"; my $body = join "", <LIRE_CORPS>; close LIRE_CORPS; # Ouverture et lecture du répertoire des images, détermination du # nom du fichier contenant l'image courante opendir IMAGE, "/home/president/moncourriel/moncourriel/image" or die "ne peut ouvrir /home/president/moncourriel/moncourriel/image $!\n"; my ($image) = grep{/[a-z]/ and !-d "/home/president/moncourriel/moncourriel/image/$_"} readdir(IMAGE); closedir IMAGE; # 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"; # Les en-têtes concaténées 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; # 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 v2.0', Type => 'multipart/mixed' ); # Attachement de la première partie MIME, le texte du message $mime->attach( Type => 'TEXT', Encoding => '8bit', Data => $message ); # Attachement de l'image $mime->attach( Type => 'image/pjpeg', Encoding => 'base64', Path => "/home/president/moncourriel/moncourriel/image/$image", Filename => $image_name ); #envoi du message $mime->send(); # Méthode d'envoi alternative, ne pas oublier de décommenter la ligne avec 'use' # # 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; # Archivage du texte du message system("mv", "/home/president/moncourriel/moncourriel/message/$body_file", "/home/president/moncourriel/moncourriel/message/archives/$body_file"); # Archivage de l'image system("mv", "/home/president/moncourriel/moncourriel/image/$image", "/home/president/moncourriel/moncourriel/image/archives/$image"); exit; __DATA__ Bonjour <<GENRE>> <<NOM>>,
Voici le contenu d'un des messages envoyé par le script à l'un des adhérents, monsieur Laurent Dupont.
From: La présidence <president@monassoce.org> To: Laurent Dupont <dlaurent@yahoo.fr> Reply-To: president@monassoce.org Date: Fri, 27 Feb 2004 03:31:23 UT Subject: Avancement de notre projet Content-Transfer-Encoding: binary Content-Type: multipart/mixed; boundary="_----------=_10778526837130" MIME-Version: 1.0 X-Mailer: MIME::Lite 2.117 (F2.71; B2.12; Q2.03) X-Mailer: moncourriel.pl v2.0 This is a multi-part message in MIME format. --_----------=_10778526837130 Content-Disposition: inline Content-Transfer-Encoding: 8bit Content-Type: text/plain Bonjour Mr Dupont, je ne vous transmets pas ce mois-ci une photographie de l'avancement de notre projet, mais un écantillon de la couleur choisie pour le carrelage des cuisines, afin d'avoir votre avis. J'attends votre réponse (si vous souhaitez vous prononcer) par retour de courriel. Merci. -- president@monassoce.org www.monassoce.org --_----------=_10778526837130 Content-Disposition: inline; filename="castel_monassoce_20040227.jpg" Content-Transfer-Encoding: base64 Content-Type: image/pjpeg; name="castel_monassoce_20040227.jpg" /9j/4AAQSkZJRgABAQEASABIAAD//gAXQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q /9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAi LCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIy MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy MjIy/8AAEQgAPAA8AwEiAAIRAQMRAf/EABUAAQEAAAAAAAAAAAAAAAAAAAAH /8QAFBABAAAAAAAAAAAAAAAAAAAAAP/EABUBAQEAAAAAAAAAAAAAAAAAAAAF /8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8Aq4CEsgAAAAAA AAAAAAAAAAAAAAAAAAAAAAAP/9k= --_----------=_10778526837130--
A noter que ce rendu, précisément, est un petit peu mieux "ordonné" que celui qui serait obtenu par le script d'envoi. Afin d'avoir cette sortie, il faudrait rajouter après la ligne :
MIME::Lite->send('smtp', 'smtp.monfai.com');
la ligne :
MIME::Lite->field_order('from','to','reply-to','date','subject');
Le script commence donc pareillement à l'autre (et que beaucoup d'autres). On
appelle Perl avec l'option -w
et la pragma strict
(toujours dans
un souci de détecter rapidement les erreurs d'inattention). Parallèlement, on a
aussi chargé le module MIME::Lite et le module POSIX dont je parlerai un peu
plus bas.
Laissons de côté le code mis en commentaire, nous reviendrons dessus plus loin aussi.
La première utilisation du module sera pour fixer des valeurs globales et en
l'occurrence dire quelles sont les options par défaut de la méthode send()
(le courriel sera adressé à un serveur SMTP qui a pour nom smtp.monfai.com
).
Ensuite, on ouvre le répertoire où est sauvé le texte du message à envoyer. Nous
savons qu'il n'y aura qu'un seul fichier qui sera trouvé par le grep
(puisque
nous n'avons laissé qu'un seul fichier dans le répertoire), ainsi il est
possible de cumuler l'initialisation de la variable avec son assignation.
Le grep
va aller chercher dans la liste des fichiers présents dans le
répertoire (readdir()
utilisé dans un contexte de liste) celui qui contient
des lettres (le motif du block grep
) et qui n'est pas un répertoire (le
!-d
du block grep
). En effet, lors du listage des fichiers d'un
répertoire, Perl liste tous les fichiers y compris les sous-répertoires, de
même, il ne faut pas oublier que sont inclus les fichiers spéciaux "."
et
".."
.
-d
fait partie de la batterie des opérateurs de tests[4]. Ils prennent en
argument le chemin du fichier (relatif ou absolu) et retourne au moins les
valeurs vrai
ou faux
, ou une valeur plus précise selon l'opérateur (-s
par exemple va retourner la taille du fichier, ce qui permettra en contexte
booléen de determiner si le fichier à une taille nulle ou pas).
En l'absence d'argument, c'est comme souvent $_
qui est évalué.
A la suite de ce test et de cette assignation, on ouvre le fichier ainsi obtenu et on stocke le message dans un scalaire.
L'image courante est ensuite récupérée de la même façon que le message a été
récupéré. C'est à la ligne suivante, celle où l'on fait appel à la fonction
strftime()
que se justifie le chargement plus haut du module POSIX.
Ce module est livré de base dans la distribution Perl et il vous donne accès à
un grand nombre des fonctions POSIX. Elles ne sont pas toutes accessibles de la
même façon (certaines sont accessibles automatiquement, d'autres pas) et je vous
recommande vivement la documentation de ce module très intéressant.
En l'occurrence donc, après chargement du module, il suffit d'appeler la
fonction strftime()
en indiquant un patron et les valeurs qu'il faut formater
(valeurs retournées ici par la fonction localtime()
), pour que cette fonction
retourne la date sous forme d'une chaîne se conformant au patron.
Celui qui est utilisé, %Y%m%d
, retourne une chaîne formée de la concaténation
de l'année, du numéro du mois, et du jour.
On obtient, dans la ligne qui suit dans le script, le nom de l'image tel qu'il apparaîtra aux membres de l'association. Cette manipulation à pour but de fournir aux adhérents un nom de fichier différent à chaque envoi.
Le patron de la partie variable du message est à son tour stockée dans une variable comme dans la première version du script (elle est de taille beaucoup plus réduite que précédemment puisque MIME::Lite fait une bonne partie de travail que nous effectuions alors).
Le fichier source des adhérents est ouvert et on boucle dessus.
Passons toutes les lignes jusqu'à la construction de l'objet MIME::Lite qui vous sont familières.
Est donc construit l'objet MIME::Lite (avec la méthode new()
) qui lui-même
construit une partie de message MIME (avec l'appel implicite de la méthode
build()
), en l'occurrence toute la partie des en-têtes du message.
Avec la méthode attach()
à laquelle est adjointe les paramètres, dans le
premier appel du texte, dans le deuxième appel de la pièce jointe, on construit
un objet MIME::Lite. Il construit à son tour la partie MIME avec le texte ou
l'image encodée, pour finalement l'ajouter aux parties précédentes dans le
respect des spécifications MIME.
L'opération de la construction du message MIME aurait pu être faite en moins de lignes que cela mais je vous laisse cela au compte des joies de la découverte. Ce n'est d'ailleurs pas ce qu'il y de plus intéressant à dire sur ce passage.
Ce qu'on peut voir ici, c'est qu'un message qui envoie du texte et une image n'est pas (comme beaucoup le croient) un courriel classique auquel on rajoute une partie MIME contenant une image. Il est montré clairement que ce type de message est bien la concaténation de plusieurs parties MIME, en l'occurrence, une partie MIME contenant du texte et une partie MIME contenant une image.
Les dernières lignes de la boucle envoient le message, selon les règles édictées
au début du script par l'appel à la méthode de classe send()
, et marque une
pause de 20 secondes.
Cette pause est pratiquement indispensable. Pas du point de vue de l'exécution
du script mais plutôt de ses contingences techniques. L'envoi en masse de
courriels, ayant une taille qui dépasse les quelques octets d'un simple message
texte, vous fait vite ressembler à un spammeur aux yeux du (ou des)
serveur(s) SMTP qui vont vous offrir leur service de relai (cf. l'article
précédent). En conséquence, vous allez vous faire interdire ce service au bout
de quelques messages. Une parade simple, comme nous n'avons que quelques
dizaines de courriels à envoyer, est d'espacer les envois.
A la suite de la boucle, les dernières lignes, tour à tour, ferment le fichier des adhérents, archivent dans les répertoires prévus à cet effet le texte puis l'image (avec l'aide d'un appel système de la commande mv).
Venons-en à présent au code qui se trouve derrière les commentaires. La ligne du début charge Net::SMTP et la séquence en fin de boucle envoie le courriel à partir de données dont nous sommes sûrs (puisque nous maîtrisons tout, de la population de la base de données à la récupération de ces dernières). La rédaction et l'éventuelle utilisation de ces lignes de code supplémentaires ne sont pas là juste pour rentabiliser notre précédent apprentissage de Net::SMTP. Elle viennent de notre connaissance du protocole SMTP et d'une constatation à propos de MIME::Lite.
Souvenez-vous, un serveur SMTP ne connaît les expéditeur et destinataire
d'un message que si ces derniers sont renseignés précisement et explicitement
par des commandes SMTP (interfacées par les méthodes mail()
et to()
du
module Net::SMTP). Ces méthodes spécifiques n'existent manifestement pas dans
MIME::Lite.
La lecture de la documentation et des sources surtout nous montre comment
MIME::Lite pallie à ce manque. En fait, le module détermine quelles données il
doit envoyer au serveur SMTP à partir de celles que nous avons renseignées par
les arguments from
et to
des méthodes new()
, build()
ou attach()
(principalement).
Autrement dit, alors même que nous connaissons la plupart du temps ces données
nécessaires à l'envoi, que nous les formattons pour qu'elles apparaissent d'une
certaine façon à notre destinataire (par exemple de la forme
"From: nom prénom <adresse>"
), le module va accomplir l'opération
inverse pour extraire ce dont il a besoin (et que nous possédions déjà, donc).
C'est la version moderne et informatique de la tapisserie de Pénélope.
L'utilisation de Net::SMTP nous évite ainsi une perte de temps (de l'assemblage et désassemblage des données) et ne nous oblige pas à faire confiance à MIME::Lite pour extraire correctement des données qui lui sont nécessaires à partir de celles que nous lui fournissons.
Cette logique peut être poussée plus loin. C'est même le point de vue d'Yves Orton qui maintient le module. Il s'est exprimé récemment à ce sujet sur une liste de diffusion des auteurs de modules Perl.
Car enfin, lorsque le courriel n'est pas envoyé sur le réseau mais au service
SMTP qui se trouve sur votre machine, le problème est le même : MIME::Lite
n'a toujours pas pour autant les données nécessaires au protocole SMTP. Il
demande au serveur de se débrouiller avec ce qu'il lui envoie (la signification
des options -t
, recherche dans le message des en-têtes comme To:
ou
Cc:
, -oi
, ne pas traiter une ligne composée seulement du point .
comme
la fin d'un message, et -oem
, renvoyer le message en cas d'erreur. Options
passées par défaut à sendmail si rien de particulier n'est spécifié).
On retombe donc bien sur le cas de figure de la perte de temps et du risque dus au désassemblage de vos données par un programme externe, la différence étant qu'il ne se passe plus au niveau de MIME::Lite mais à celui de sendmail.
Bonnet blanc, blanc bonnet.
Le conseil de procédure est donc, à des fins de performances, d'efficacité et de meilleur contrôle de votre travail, de ne prendre MIME::Lite que pour ce qu'il sait bien faire, et utiliser le bon outil, Net::SMTP, pour l'expédition quel que soit le cas de figure.
Dans le script d'exemple, pour peu qu'il y ait un serveur SMTP qui tourne sur ma
machine, cela reviendrait à supprimer la ligne où on appelle la méthode de
classe send()
MIME::Lite->send('smtp', 'smtp.monfai.com');
et à changer la ligne :
$mime->send();
par les lignes :
my $smtp = Net::SMTP->new('localhost') 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";
Plutôt que d'appeler l'executable sendmail, on s'adresse au service
sendmail
par le biais de la boucle locale.
Nous sommes arrivés au bout du survol des fonctionnalités d'envoi de courriels
avec Perl. Ce que je vais dire est évident, mais j'insiste, il ne s'agit que
d'un survol, l'API de Net::SMTP présente bien d'autres caractéristiques comme
par exemple l'authentification via SASL[5]. De même, Net::SMTP est la brique
élémentaire d'un certain nombre de modules, comme Net::SMTP::Multipart (qui va
allier les capacités de Net::SMTP et celles de MIME::Base64 qui est la
bibliothèque qui interface l'encodage base64
). On va aussi regarder du côté
de MIME::Lite::HTML, un module qui va permettre d'envoyer, avec la simplicité
coutumière de la série MIME::*
, une page HTML dans un courriel. Pour finir,
ne manquez pas d'aller approfondir vos connaissances avec les MIME::Tools,
frères aînés de MIME::Lite, ou d'aller jeter un œil, comme je le suggère au
début, du côté des deux autres lignées de modules ayant trait aux courriels.
Dans le prochain article, nous nous efforcerons d'apprendre, après avoir déployé tant d'efforts pour envoyer des courriels de façon appropriée, à les recevoir de la même manière.
Pour retrouver facilement les RFC dont nous parlons à partir de leur numéro.
La documentation en ligne de MIME::Lite.
La partie de la documentation qui m'a confirmé que MIME::Lite était bien le module que je recherchais ;-).
La fonction open()
(sur le miroir des mongueurs).
Les types MIME (pour l'affectation de l'attribut content-type
).
A vous de trouver lequel est gascon !
La documentation du module POSIX sur CPAN.
Pénélope.
Comprehensive Perl Archive Network. La somme de toute la logitèque de Perl. http://search.cpan.org
Data Base Indepedant. L'interface Perl à (presque) toutes les bases de données existantes. http://search.cpan.org/dist/DBI/DBI.pm
Simple Mail Transfer Protocol. Le protocole d'envoi et de transfert du courriel.
Les opérateurs de test en Perl. http://www.mongueurs.net/perlfr/perlfunc.html#item_%2DX
Simple Authentification and Secutity Layer. Une méthode pour rajouter une couche de sécurité aux protocoles basés sur une connexion.
David Elbaz, <lacravate@mongueurs.net>.
David Elbaz est membre de l'association des mongueurs de Perl ainsi que de paris.pm. Il a découvert Perl dans Linux Magazine et l'utilise depuis pour à peu près tout (web, interfaces utilisateurs, sérialisation en tous genres) avec toujours autant de bonheur et de plaisir intellectuel.
Il remercie très chaudement les membres du groupe de relecture pour l'aide massive et productive qu'ils ont continué à lui prodiguer.
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.