Portail / Sujets autour de la programmation / Programmation C/C++ sous Linux / Utilisation des courriels(Sommaire)

Envoi de courriels.

Mise à jour du 09/05/2024.

L'envoi de courriels depuis une application est une opération de base de tout système informatique. Du point de vue de l'architecture logicielle, la transmission synchrone est à proscrire car cela peut être un processus long et risqué avec pour conséquence la perte d'informations contenues dans le courriel.

Idéalement, il faut disposer d'une file d'attente où déposer les courriels à émettre tandis qu'un processus inter-systèmes se charge de les envoyer. Il nous faut donc un programme spécialisé dans l'envoi des courriels. En outre, comme toute application devra pouvoir produire des courriels, ce prototype intégrera le code nécessaire à la création de courriels selon 3 modes :

  1. mode texte seul (le mode d'origine de l'Internet) ;
  2. mode "alternatif" où coexistent au sein d'un même courriel un corps textuel et un message en langage HTML ;
  3. mode "agrégé" où coexistent au sein d'un même courriel des fragments divers comme des pièces jointes en plus des corps de message texte ou HTML.

Cet article propose un prototype réalisant la fonction de création de courriels ainsi que celle d'envoi. Les "parties" constituant le courriel (objet, adresses courriels, corps de message texte, corps de message HTML, pièces jointes et paramètres du serveur SMTP) sont fournies de façon externe par le biais de la ligne de commande.

Décomposition de la réalisation.

Nous avons 2 réalisations au sein de ce prototype : la construction d'un courriel conforme à la RFC2822 et l'envoi de ce dernier vers un serveur SMTP. Or l'envoi des courriels a suffisamment d'existence pour que l'on dispose de librairies sur étagère pour réaliser chacune de ces fonctions (il en existe même plusieurs). Pour des raisons de popularité, de disposnibilité dans le référentiel Debian et de documentation nous avons choisi la librairie C++ vmime.

Notez pour ceux qui doivent se limiter au langage C, que la librairie libcurl permet aussi l'envoi de courriels (et de bien d'autres choses).

La création de courriels.

Il ne faut pas perdre de vue qu'un courriel (format dit EML) n'est ni plus ni moins qu'un fichier texte. Même les élements binaires comme les images sont transformés en texte par codage hexadécimal de chaque octet les constituant. Notez au passage que grâce à cette forme, un courriel ne peut pas transporter de virus "activé". La seule forme possible pour leur transport est le script ou la sérialisation binaire au sein d'une pièce jointe (image par exemple) ou encore l'emploi des deux (le script servant à activer le virus sous forme binaire). C'est l'interprétation du texte des scripts ou des pièces jointes par un logiciel quelconque qui permet l'activation d'un virus.

La librairie vmime dispose d'une documentationassez riche. Son API C++ (doxygen) est également disponible.

Nous avons créé une librairie C++ nommée email qui utilise l'API de la librairie vmime pour simplifier la création et l'envoi de courriels. Cette librairie est mise en oeuvre dans le prototype p39 qui montre comment créer un courriel et l'envoyer.

Création d'un message.

Le prototype admet un fichier de configuration qui fixe les paramètres de création du courriel et ceux qui permettent ensuite son envoi :

subject=Message de test p39
from=admin@plumo.com
reply-to=admin@plumo.com
to=toto@plumo.com
cc=titi@plumo.com
bcc=tutu@plumo.com
text=Ceci est un message de test envoyé par le prototype p39.
html=~/p39/resources/p39.html
attachment.1=~/p39/resources/p39.html
attachment.2=~/p39/resources/3.png
certCA.1=~/p39/resources/DigiCert_Global_Root_G2.pem
certCA.2=~/p39/resources/RapidSSLTLSRSACAG1.pem
trustedCert.1=~/p39/resources/smtp.plumo.com.pem
smtp.host=smtp.plumo.com
smtp.port=465
smtp.account=admplumo
smtp.password=xxxxxxxxxxx
smtp.transport=tls

Une fois ce fichier de configuration chargé et exploité, la création d'un courriel est assez simple :

Email email;
email.addRecipients(recipients);
email.setSubject(subject);
email.setText(bodyText);
email.setHtml(htmlBody);
email.updateAttachments(attachments);
// Affichage du message au format RFC 2822
cout << email.toString() << endl;

L'invocation de la méthode toString() affiche ce courriel sous forme textuelle avec respect du format de la RF2822 :

Subject: Message de test p39
    From: admin@plumo.com
    To: toto@plumo.com
    Cc: titi@plumo.com
    Bcc: tutu@plumo.com
Date: Mon, 22 Apr 2024 11:59:11 +0200
Mime-Version: 1.0
Content-Type: multipart/mixed; 
  boundary="=_Ljz9T3tdywwaK0+u2MlRftUy9e5ovt43VTq8Mpb9gcnhNZhx"
Reply-To: admin@plumo.com

This is a multi-part message in MIME format. Your mail reader does not
understand MIME message format.
--=_Ljz9T3tdywwaK0+u2MlRftUy9e5ovt43VTq8Mpb9gcnhNZhx
Content-Type: multipart/alternative; 
  boundary="=_Ljz99k7Ww7WmxA7U7m6ci8om-KCqOrUNuD7riTdJ+nMjpxG4"

--=_Ljz99k7Ww7WmxA7U7m6ci8om-KCqOrUNuD7riTdJ+nMjpxG4
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

Ceci est un message de test envoy=C3=A9 par le prototype p39.
--=_Ljz99k7Ww7WmxA7U7m6ci8om-KCqOrUNuD7riTdJ+nMjpxG4
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 7bit

<!doctype html>
<html lang="fr">
<head>
<meta description="Prototype p39" />
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<meta name="author" content="Jean-Marie Piatte" />
<meta name="publication" content="2024-04-22" />
<title>Prototype p39</title>
</head>
<body>
<h1>Message de test du prototype p39.</h1>
<p style="color: blueviolet;">
Ceci est le corps d'un message de test du prototype p39.
</p>
<p style="text-align: right;font-weight: bold;">
p39 prototype.
</p>
</body>
</html>
--=_Ljz99k7Ww7WmxA7U7m6ci8om-KCqOrUNuD7riTdJ+nMjpxG4--

--=_Ljz9T3tdywwaK0+u2MlRftUy9e5ovt43VTq8Mpb9gcnhNZhx
Content-Type: text/html
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=p39.html; size=611

PCFkb2N0eXBlIGh0bWw+CjxodG1sIGxhbmc9ImZyIj4KICAgIDxoZWFkPgogICAgPG1ldGEg
ZGVzY3JpcHRpb249IlByb3RvdHlwZSBwMzkiIC8+CiAgICA8bWV0YSBjb250ZW50PSJ0ZXh0
L2h0bWw7IGNoYXJzZXQ9dXRmLTgiIGh0dHAtZXF1aXY9IkNvbnRlbnQtVHlwZSIgLz4KICAg
IDxtZXRhIG5hbWU9ImF1dGhvciIgY29udGVudD0iSmVhbi1NYXJpZSBQaWF0dGUiIC8+CiAg
ICA8bWV0YSBuYW1lPSJwdWJsaWNhdGlvbiIgY29udGVudD0iMjAyNC0wNC0yMiIgLz4KICAg
IDx0aXRsZT5Qcm90b3R5cGUgcDM5PC90aXRsZT4KICAgIDwvaGVhZD4KICAgIDxib2R5Pgog
ICAgICAgIDxoMT5NZXNzYWdlIGRlIHRlc3QgZHUgcHJvdG90eXBlIHAzOS48L2gxPgogICAg
ICAgIDxwIHN0eWxlPSJjb2xvcjogYmx1ZXZpb2xldDsiPgogICAgICAgICAgICBDZWNpIGVz
dCBsZSBjb3JwcyBkJ3VuIG1lc3NhZ2UgZGUgdGVzdCBkdSBwcm90b3R5cGUgcDM5LgogICAg
ICAgIDwvcD4KICAgICAgICA8cCBzdHlsZT0idGV4dC1hbGlnbjogcmlnaHQ7Zm9udC13ZWln
aHQ6IGJvbGQ7Ij4KICAgICAgICAgICAgcDM5IHByb3RvdHlwZS4KICAgICAgICA8L3A+CiAg
ICA8L2JvZHk+CjwvaHRtbD4=
--=_Ljz9T3tdywwaK0+u2MlRftUy9e5ovt43VTq8Mpb9gcnhNZhx
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=3.png; size=1163

iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAA7EAAAOxAGVKw4b
AAAEPUlEQVRIx7WWW4hVVRjHf9/eZ+8z55zOOBcc9YyOWjPWmGiWNWNhRElQUZGSFUmK1kOo
hA+BRE9dtJConoOgHooeKpJAuogSNUnShSLNC45lyplG5nTOzDn77LUvXw97j06E1RSul7VY
61v7//3//2+tvURVuZQt8183/rhbupsNBuoel1dqFDUAK8TL2/xS9fg6Uo6ve0tjmS6DoWcL
9oy890ZPr95fyOJaPuABjaRXA55PNHScoY9OOmumDfDmpmzx7nvm/NbWNZKViaaoh+KBNEjG
AYjY4ucW6IOvDN85bYnqdYXDZVjwDPT2w8QpaFQgiCEUCEKon4WjH1I3Kn8LkFu1s6jNSrN5
aHcwOad+ACFI5bTQ9whSWCHsexx+PYDERgi9JNAg6D+YXCwte3Hs8J6jwKvnJ1NFNTIqpiYK
iqkipiqqyaoIMhl5UYDlW/YvWzkwuHlPWKpK/2Pv6JHXyn8KiH0IxtNxOP0yDSX/0tzZnS7t
HTPJz3palj+1Tb/dqVHKQCIjmBoCMgmQZj7Z5KIACzd+smbtHTff9v3ZtMT6tzzKoR2vA9/8
7meAEI38CxJpiMC/k2j2hi9yYWbeC6uuyvLed2m6uTlZZg3sal266a71ne3AKEQGzHjynelI
VG80n7j91r7eqgfl6pSFKzasNkMH7h2JSx/DKMQGTG16HhTWfTY3uuzKHQMLbRkeBS+Yomqm
YPl9m3cND2//MvHATzwQBAKwSPy4UGl/9cBo5rnrFnW3Rqp64pwImdQDFSFWZc7qvhMnF2/D
OoLiq8Q1UUHJhkgh9UBBYgRFkSkA9tp9N9Bx/folPSKVAH72gKIkGcVAJEJgEy7Zui1y3nfE
NQIpg5YQopSBpvE6hYE8fNAm1/Hywp5WW1pUxxTOxiLMSBnEIgSqGBH1u/NxNoM6vorUREUT
BpBUVJwyCKcwsKOJh6LStSsXdEMFcGwI89DuJuyiCPwm+OmNiQu4PthVsIBcCPaUkx4BJnEh
4278vGh1zH++b74ri0qQz4vYLizOQiypyRFEgYjxoOoikgXJGrBriIWQC8AFUSSRE2imEkWm
/mRbb++8QlH1aENEY1WTgeNGBDuVKBLBqOKLODGqLqjrI05T1CkozixE86IaKuEEEtYFXxUL
MnFjpKtiRbiBjeMDNngWnHMBZ/LeSI2zoUWAXA5aS5CfCbaAtgEtnC+IsAmmjFIhQ/XEdv1g
60/lpbetl2tuWpopznQoONguRI6IJeAEYOVEPMsnika96phvd8XHssRnEBvBqoJlEonSc9MM
iGOL6vk/miy/zyGsd5Bp6be6Sj35aLxzRntn3o4DHa/WGhOhdS4cK5/q1JFjg61n5q7ormwa
vFEG+65mXr5d8mJnCYz4p0+b8lcHgx/2fsrbrW3slf/zqnjgFnGqTbJ1IxlEsES0vajGRs27
++MIQC71s+UP9H8E4et3Zq8AAAAASUVORK5CYII=
--=_Ljz9T3tdywwaK0+u2MlRftUy9e5ovt43VTq8Mpb9gcnhNZhx--

Si vous testez le prototype en omettant certains ajout comme les PJ ou le corps HTML, vous obtiendrez à chaque fois un courriel de forme différente. La plus simple est celle du courriel sans document HTML ni PJ car un tel courriel n'a pas besoin d'être structuré par l'ajout de plusieurs fragments (multipart).

Il existe ainsi de nombreuses variantes de création de courriels rendues possibles par la librairie vmime mais nous nous contenterons des suivantes :

  1. Message textuel (pas de PJ ni de corps HTML) ;
  2. Message textuel avec PJ mais san corps HTML ;
  3. Message avec corps HTML sans corps texte ni PJ ;
  4. Message avec corps HTML avec PJ sans corps texte ;
  5. Message avec corps HTML et corps texte sans PJ ;
  6. Message avec corps HTML, corps texte et PJ (le plus courant).

Ces formes couvrent la plupart des besoin courants lorsque le courriel répond à un usage traditionnnel. Notez qu'il est possible dans l'absolu d'envoyer un courriel sans corps texte. Cela n'est cependant pas recommandé car il existe des programmes console incapable de restituer le rendu HTML. Ausii, si vous n'ajoutez pas de copss texte, la classe Email en ajoutera un par défaut précisant cette absence de corps texte. Les client de messagerie console en seront ainsi informés.

A l'inverse, la classe Email dispose d'une méthode statique de transformation d'un corps texte en coprs HTML ce qui permet d'envoyer un même corps au format texte et au format HTML sans aucun effort. Bien entendu, le rendu HTML sera sommaire : objet au sein d'une balise <h1> et texte au sein d'une balise <pre>.

L'envoi de courriels.

L'envoi de courriels peut également se faire avec la librairie email. Les concepts sous-jacents utilisent ceux de la librairie vmime.

La librairie email propose la classe Mta pour effectuer l'envoi de courriels. Dans la plupart des cas, le déclaaration du constructeur suffit à paramétrer cette classe pour lui permettre de se connecter au serveur SMTP distant.

Si on doit utiliser le mode "submission" (TLS de bout en bout) ou STARTTLS (TLS après demande de mise en chiffrement), on doit au préalable préparare un objet de vérification de la chaîne de certification. Si on en déclare pas, la librairie vmime utilisera celui par défaut (classe security::cert::defaultCertificateVerifier). Vous pouvez créer le votre et le transmettre en tant que 1er paramètre du constructeur d'un objet de classe Mta. La classe de votre vérificateur devra hériter de la classe security::cert::defaultCertificateVerifier. Noter que la librairie email vous propose le vérificateur de classe BypassVerifier. Cetet classe dispose de 3 modes de vérfication :

  1. None  : aucune vérfication n'est effectuée, le vérfificateur accepte de facto la chaîne de certification émise par le serveur ;
  2. Dates : le vérificateur se limite à vérfier que la date du certificat du serveru est valide ;
  3. Strict : le vérificateur vérfie la validité de chaque certificat présenté par la chaîne de certification.

Que vous utilisiez comme vérificateur la classe security::cert::defaultCertificateVerifier ou la classe BypassVerifier, vous devez l'initialiser en lui transmettant les fichiers des certificats X509 du CA et des éventuelles autorités intermédiaires ayant contribués à la signature du certificat du serveur SMTP. Sans cette initialisation, la vérification échouera.

Pour l'emploi du mode "submission" (SMTPS) ou STARTTLS, il est donc impératif si on souhaite une vérification "stricte" de disposer des certificats du CA et des éventuelles autorités de certification intermédiares (RA). La commande openssl le permet : openssl s_client -showcerts -connect {hôte}:{port d'écoute}. Par exemple pour connaître la chaîne de certification du serveur SMTP de gmail, tapez la commande openssl s_client -showcerts -connect smtp.gmail.com:465. Le 1er certificat de la chaîne est celui du serveur, le second celui du RA ayant produit le certificat et le 3ème celui du CA ayant signé le certificat du RA. Les certificats RA et CA peuvent donc être récupérés par ce moyen et sauvegardés dans des fichiers au format PEM. Si vous devez travailler avec le serveur smtp.gmail.com, ce sont ces deux certificats que vous devrez transmettre au vérficateur.

L'envoi en lui-même est également simple :

  1. On construit le client SMTP (objet de classe Mta) ;
  2. On transmet au client SMTP le courriel à émettre ;
  3. A l'issue, on récupère la liste des adresses courriel auxquelles le courriel n'a pas été distibué ainsi que la raison de cette non distribution.
// Création du vérificateur de certificats "bypass"
shared_ptr<BypassVerifier> spBypass = make_shared<BypassVerifier>();
spBypass->setVerificationMode(certVerification::Strict);
vector<string> certs = certificatesFileNames(arguments);
// Ajout des certificats du CA et des RA
for (auto pI = certs.begin(); pI != certs.end(); ++pI)
    spBypass->addCaCertificate(*pI);
shared_ptr<security::cert::defaultCertificateVerifier> spVerifier =
    dynamicCast<security::cert::defaultCertificateVerifier>(spBypass);
// Envoi du message
try {
    // -- propriétés de la session (ici pour SMTPS)
    string account = arguments.at("smtp.account");
    string password = arguments.at("smtp.password");
    string host = arguments.at("smtp.host");
    Mta mta = Mta(spVerifier, SmtpTransport::Submission, account, password, host);
    vector<string> errors;
    if (!mta.sendEmail(email, errors)) {
        cout << "Courriel non distribué aux adresses :" << endl << endl;
        for (auto pI = errors.begin(); pI != errors.end(); ++pI)
            cout << "    - " << *pI << endl;
    }
    else
        cout << "Courriel distribué à toutes les adresses courriel" << endl;
    mta.releaseTransport();
}
catch(const std::exception& error) {
    cout << "***** Echec d'envoi du message : " << error.what() << endl;
    return 5;
}

Présentation du prototype.

Voici le lien sur la documentation doxygen de réalisation. Les sources sont visualisables sous l'onglet "Fichiers".

Rédaction par Jean-Marie Piatte (1983-2021)