[couverture de Linux Magazine 60]

Envoi de courriels avec Perl (2)

Article publié dans Linux Magazine 60, avril 2004.

Copyright © 2004 - David Elbaz.

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

Chapeau

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.

Introduction

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...

Alors pourquoi MIME::Lite ?

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.

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 :

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

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.

Valeurs globales

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.

La méthode 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.

La méthode 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 :

    $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",
    );

La méthode 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'
    );

Les attributs

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.

Les méthodes 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");

La méthode 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.

La méthode 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"

La méthode 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" => '');

La méthode 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");

Le rendu

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.

Les méthodes 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.

L'envoi

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.

La méthode 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');

Le castel gascon

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.

Premièrement un peu d'ordre

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.

Pourquoi tout cela ?

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 ? :-)

Le code

    #!/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>>,

le rendu

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');

Explication de texte

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).

MIME::Lite ou Net::SMTP ?

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.

Conclusion

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.

Références de l'article:

Notes de l'article :

  1. Comprehensive Perl Archive Network. La somme de toute la logitèque de Perl. http://search.cpan.org

  2. Data Base Indepedant. L'interface Perl à (presque) toutes les bases de données existantes. http://search.cpan.org/dist/DBI/DBI.pm

  3. Simple Mail Transfer Protocol. Le protocole d'envoi et de transfert du courriel.

  4. Les opérateurs de test en Perl. http://www.mongueurs.net/perlfr/perlfunc.html#item_%2DX

  5. Simple Authentification and Secutity Layer. Une méthode pour rajouter une couche de sécurité aux protocoles basés sur une connexion.

Auteur

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.

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