IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vérification et envoi d'e-mail en C++ avec boost

Ce tutoriel vous montre comment vérifier puis envoyer des e-mails en C++ avec boost mais vous pouvez très bien reprendre et adapter l'algorithme pour envoyer des e-mails avec d'autres langages ou bibliothèques. 12 commentaires Donner une note à l´article (4.5)

Article lu   fois.

Les trois auteurs

Profil ProSite personnel

Profil ProSite personnel

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Vous développez un logiciel dans lequel l'utilisateur doit avoir un compte pour l'utiliser ?

Vous souhaiteriez intégrer le processus d'inscription à votre logiciel afin que l'utilisateur puisse directement s'enregistrer à partir de celui-ci ?

Vous souhaitez en plus demander et valider l'adresse e-mail de l'utilisateur et pouvoir par la suite lui envoyer des e-mails pour, par exemple, confirmer son inscription ?

Ne cherchez plus. Ce tutoriel vous montrera comment gérer correctement les adresses e-mail de vos membres.

I-A. Pourquoi vérifier l'adresse e-mail ?

I-A-1. Réinitialiser un mot de passe

On demande souvent une adresse e-mail pour la création d'un compte. Celui-ci permet, en cas de perte du mot de passe par un utilisateur, de réinitialiser le mot de passe et de lui envoyer un e-mail pour confirmer la réinitialisation.

Ne jamais stocker un mot de passe non-hashé ! Ne jamais envoyer un mot de passe dans un message électronique.

Envoyer un e-mail permet alors d'avertir l'utilisateur qu'une personne a tenté de réinitialiser son mot de passe mais aussi de lui envoyer les informations nécessaires pour réinitialiser son mot de passe.

Les boîtes de messagerie électroniques ne sont pas toujours sécurisées, vous devez donc n'envoyer que des données temporaires qui ne serviront qu'à la réinitialisation du mot de passe.
De plus, avant d'envoyer un e-mail de réinitialisation, faites en sorte qu'un usurpateur ou un pirate ne puisse pas réinitialiser le mot de passe grâce au système de récupération mis en place. Pour cela, vous pouvez demander quelques infos supplémentaires comme son identifiant ou une réponse à une « question secrète ».

Or, si l'adresse e-mail est invalide, il ne sera pas possible d'effectuer cette procédure.

I-A-2. Combattre le spam

Une autre raison de la mise en place de ce système est la lutte contre le spam. Certaines personnes peu scrupuleuses créent des robots capables de remplir des formulaires d'inscriptions pour ensuite envoyer automatiquement des messages publicitaires.

Demander une adresse e-mail valide et ne permettre la création que d'un seul compte par adresse e-mail permet de complexifier l'inscription de robots.

Comme ces robots parcourent les sites internet « au hasard », le fait d'effectuer l'inscription sur votre client permet de se protéger de ces derniers. En effet, il faudrait alors qu'une personne vise particulièrement votre jeu, analyse votre système d'inscription et réécrive un client pour créer automatiquement des comptes.

I-B. Comment vérifier l'adresse e-mail ?

La vérification d'une adresse e-mail est assez simple, on vérifie d'abord que son format est valide puis on envoie un e-mail contenant un code à renseigner lors de la première tentative de connexion.

I-C. Autres utilisations de l'envoi d'e-mail

De plus, avoir l'adresse e-mail de l'utilisateur peut vous permettre :

  • d'envoyer de petites newsletters ;
  • d'avertir automatiquement un groupe de personnes quand une action particulière survient, par exemple avertir les administrateurs lors d'un bug du serveur ou envoyer des statistiques régulières ;
  • et bien d'autres…

Nous déclinons toutes responsabilités quant à l'utilisation qui pourrait être faite de l'envoi d'e-mail automatique.
De plus, pour faire les choses proprement, le mieux est de demander l'accord à l'utilisateur et de le prévenir des différents usages que vous allez avoir de son adresse e-mail.

II. Obtenir et vérifier le format de l'adresse e-mail

II-A. Obtenir l'adresse e-mail

Pour obtenir l'adresse e-mail de l'utilisateur, il suffit de lui demander de remplir un champ « adresse e-mail » et ne valider la saisie que lorsque le format de l'adresse e-mail est correct.

Pour ce faire, il vous suffit d'utiliser votre bibliothèque d'interface graphique préférée ou d'utiliser la console.

II-B. Vérifier le format de l'adresse e-mail

II-B-1. Format de l'adresse e-mail

Avant de valider la saisie de l'adresse e-mail, nous allons nous assurer qu'elle soit au bon format.

Mais quel est le format d'une adresse e-mail ?

Une adresse mail est composée de trois parties :

  • partie locale : selon le serveur, le format peut changer donc on va dire qu'il peut tout contenir sauf un '@' avec au minimum un caractère ;
  • @ : caractère séparateur ;
  • nom de domaine : on va faire simple, on va dire que c'est au moins un caractère sans un '@' suivit d'un '.' et d'au moins deux caractères qui ne sont pas un '@'.

Vous pouvez retrouver le format exact dans la RFC 3696 mais n'oubliez pas que la partie locale peut être très exotique et que le format pour le nom de domaine peut changer comme en 2012 par l'ajout des accents ou l'ajout de nouveaux Top Level Domain (TLD) qui correspond à ce que vous trouvez derrière le dernier « . ».

II-B-2. Fonction de vérification du format

Pour vérifier le format de notre adresse e-mail, nous allons utiliser boost::regex, il vous suffit d'inclure l'en-tête <boost/regex.hpp> et de rajouter l'option de compilation -lboost_regex.

Il faut d'abord définir notre format avec une chaîne de caractères :

  • partie locale :
  • tout sauf '@' : [^@]
  • avec au minimum un caractère : [^@]+
  • caractère séparateur : '@'
  • nom de domaine :
  • tout sauf '@' : [^@]
  • au moins un caractère : [^@]+
  • un '.' : \.
  • au moins deux caractères qui ne sont pas des '@' : [^@]{2,}
    Notre chaîne de caractères est donc : [^@]+@[@+]\.[^@]{2,}

Bien évidemment, dans un but de simplification, nous utilisons un format plus permissif que le vrai format des adresses e-mail.
Pour information, ce format serait :
(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])

Il ne nous reste plus qu'à créer notre fonction :

 
Sélectionnez
bool isValidMail(const std::string & mail)
{
    // On crée le format sans oublier d'échapper le caractère « \ ».
    boost::regex format("[^@]+@[@+]\\.[^@]{2,}");

    // On vérifie que l'e-mail a bien le bon format.
    return boost::regex_match(mail, format);
}

III. Enregistrer le compte dans la base de données

Une fois que la saisie de l'adresse e-mail est validée et l'inscription terminée, on envoie et enregistre toutes ces informations dans la base de données du serveur sans oublier de hasher le mot de passe avant de l'envoyer.

L'état initial du compte sera « non-activé », lors de la première connexion, si le mot de passe et l'identifiant sont corrects, on demandera le code que l'utilisateur aura reçu par e-mail et on activera le compte si ce dernier est correct.

On peut créer une colonne « état du compte » (banni, non-activé, activé…) ainsi qu'une colonne « raison » qui renseignera la raison d'un bannissement ou le code d'activation si le compte n'est pas activé. Cela évite d'avoir une colonne juste pour l'activation du compte qui sera inutile par la suite.

Vous pouvez régulièrement supprimer les comptes créés depuis X jours et qui n'ont pas encore été activés.

Pour générer le code d'activation, un simple nombre aléatoire peut suffire.

IV. Envoyer l'e-mail

Il vous faut désormais envoyer le code d'activation par e-mail.

Pour cela il va falloir dialoguer avec le serveur SMTP (message exchange) du domaine de l'adresse e-mail et pour trouver ce serveur, il va falloir interroger un serveur DNS (Domain Name System) pour obtenir l'enregistrement MX (Mail eXchange) de la zone concernée.

Vous pouvez aussi vous connecter au serveur SMTP du domaine de l'expéditeur mais dans ce cas-là, il faudra vous identifier.
La connexion au serveur SMTP du FAI est également possible et peut parfois nécessiter une identification.

Dans les échanges d'e-mails, deux types de serveurs interviennent :

  • les serveurs Mail Transfert Address (MTA) aussi appelés serveurs SMTP ;
  • les serveurs Mail Delivery Agent (MDA).

Les serveurs MTA ont plusieurs rôles :

  • recevoir les e-mails en provenance d'autres serveurs MTA ;
  • recevoir les e-mails que les utilisateurs souhaitent envoyer (nécessite souvent l'identification de l'utilisateur) ;
  • relayer les e-mails vers le MTA de destination ;
  • relayer les e-mails au MDA lorsqu'ils sont arrivés à destination.

Les serveurs MDA quant à eux servent à :

  • authentifier les utilisateurs (login et mot de passe) ;
  • gérer le remplissage de la boîte mail de l'utilisateur (quotas par exemple) ;
  • recevoir les e-mails en provenance du serveur MTA local et les stocke dans la boîte aux lettres ;
  • effacer les e-mails à la demande de l'utilisateur.

IV-A. Trouver un serveur DNS

IV-A-1. Général

Pour le serveur DNS, vous pouvez utiliser ceux de Google qui sont gratuits, ils ont pour adresse IP : 8.8.8.8 et 8.8.4.4.

IV-A-2. Unix-like

Sous une majorité de système Unix-like, il vous suffit de lire le fichier /etc/resolv.conf, rien de bien compliqué pour vous.

IV-A-3. Windows

Sous Windows, on va devoir utiliser l'API Windows.

 
Sélectionnez
// Structure qui contiendra les informations que l'on recherche.
FIXED_INFO * info = (FIXED_INFO *)malloc( sizeof(FIXED_INFO) );

ULONG bufferSize = sizeof(info); // Taille de la structure.
DWORD retour = GetNetworkParams( info, &bufferSize );

if( retour == ERROR_BUFFER_OVERFLOW ) // Si la structure n'est pas assez grande.
{
    info = (FIXED_INFO *)realloc(info, bufferSize);
    retour = GetNetworkParams( info, &bufferSize );
}

// Si on a réussi à obtenir nos informations.
if( retour == ERROR_SUCCESS)
{
    // On récupère la liste des serveurs DNS.
    struct _IP_ADDR_STRING * next = &info->DnsServerList;

    // On parcourt la liste des serveurs DNS.
    while( next )
    {
        dns_names.push_back( next->IpAddress.String  );
        next = next->Next;
    }
}
free(info);

IV-A-4. Code final

Bien que l'utilisation des serveurs DNS de Google (8.8.8.8 et 8.8.4.4) soit plus aisée, ces serveurs ne sont pas toujours accessibles si on se trouve dans un réseau d'entreprise par exemple. Il faut donc utiliser en priorité les serveurs DNS indiqués par la configuration locale.

 
Sélectionnez
#include <vector>
#include <string>

#if defined WIN32 || defined WIN64 || defined _WIN32_ || defined _WIN64_ || defined __WIN32 || defined __WIN64
    #define WINDOWS
#endif

#ifdef WINDOWS
    #include <windows.h>
    #include <iphlpapi.h>
    #pragma comment(lib, "IPHLPAPI.lib")
#endif


std::vector<std::string> getDNS(void)
{
    std::vector<std::string> dns_names;
  #ifdef WINDOWS
     // Structure qui contiendra les informations que l'on recherche.
    FIXED_INFO * info = (FIXED_INFO *)malloc( sizeof(FIXED_INFO) );

    ULONG bufferSize = sizeof(info); // Taille de la structure.
    DWORD retour = GetNetworkParams( info, &bufferSize );

    if( retour == ERROR_BUFFER_OVERFLOW ) // Si la structure n'est pas assez grande.
    {
        info = (FIXED_INFO *)realloc(info, bufferSize);
        retour = GetNetworkParams( info, &bufferSize );
    }

    // Si on a réussi à obtenir nos informations.
    if( retour == ERROR_SUCCESS)
    {
        // On récupère la liste des serveurs DNS.
        struct _IP_ADDR_STRING * next = &info->DnsServerList;

        // On parcourt la liste des serveurs DNS.
        while( next )
        {
            dns_names.push_back( next->IpAddress.String  );
            next = next->Next;
        }
     }
     free(info);
  #else
      // pour les Unix-like, lire le fichier /etc/resolv.conf
  #endif
    // au cas où, on ajoute les serveurs DNS de Google.
    dns_names.push_back("8.8.8.8");
    dns_names.push_back("8.8.4.4");

    return dns_names;
}

IV-B. Dialoguer avec le serveur DNS

Il va falloir maintenant discuter avec le serveur DNS en UDP sur le port 53.

Vous trouverez plus de détails en suivant ce lien : http://www.frameip.com/dns/

IV-B-1. Requête

On va devoir envoyer notre requête :

 
Sélectionnez
#include <string>

std::string buildDNSQuery(const std::string & mail)
{
    // On récupère le nom de domaine.
    int i = mail.find('@');
    std::string host = mail.substr(i + 1);
    // on met un identifiant quelconque pour notre demande
    std::string dnsQueryText = "AA"; //id

    char octet = 0x00 ; // On envoie une requête.
    octet |= 0x00 ; // C'est une requête standard.
    octet |= 0x00 ; // Le message n'est pas tronqué.
    octet |= 0x01 ; // C'est une requête récursive.

    dnsQueryText += octet;
    dnsQueryText += '\x00'; // Réservé au serveur.

    dnsQueryText += '\x00';
    dnsQueryText += '\x01'; // On n'a qu'une question.

    dnsQueryText += '\x00';
    dnsQueryText += '\x00'; // On n'a pas de réponses.

    dnsQueryText += '\x00';
    dnsQueryText += '\x00'; // Pas de réponses d’autorité, on s'en moque ici.

    dnsQueryText += '\x00';
    dnsQueryText += '\x00'; // Pas de réponses additionnelles, on s'en moque aussi ici.
    // Début de la requête.
    // On va devoir renseigner le nom de domaine, avant chaque partie du nom de domaine
    // séparé par des « . », on va devoir mettre la taille de cette partie et ne plus mettre les « . »
    size_t pos = 0;
    size_t oldPos = 0;

    // On recherche tous les « . » du nom de domaine.
    while( (pos = host.find('.', pos) ) != std::string::npos )
    {
        // On met la taille de la partie.
        dnsQueryText += (char)pos - oldPos;

        // On ajoute la partie.
        dnsQueryText += host.substr(oldPos, pos - oldPos);

        pos++;
        oldPos = pos;
    }
    // On ajoute la dernière partie du nom de domaine.
    dnsQueryText += (char)(host.size() - oldPos);
    dnsQueryText += host.substr(oldPos, host.size() - oldPos );
    dnsQueryText += '\x00'; // Fin du nom de domaine.

    dnsQueryText += '\x00';
    dnsQueryText += '\x0f'; // On recherche un serveur SMTP.

    dnsQueryText += '\x00';
    dnsQueryText += '\x01'; // On utilise la classe INET.

    return dnsQueryText;
}

IV-B-2. Envoi de la requête et réception de la réponse

On va utiliser boost pour envoyer la requête au serveur DNS et recevoir sa réponse. Il vous faudra indiquer à l'éditeur de lien d'ajouter les bibliothèques : -lpthread -lboost_thread -lboost_system.

 
Sélectionnez
#include <vector>
#include <string>
#include <boost/asio.hpp>
#include <boost/array.hpp>

std::string askDNS( const std::string & queryText,
                    const std::vector<std::string> & DNSnames,
                    boost::asio::io_service & io_service )
{
    // Socket UDP pour dialoguer avec le serveur DNS.
    boost::asio::ip::udp::socket dnsSocket(io_service);

    // Erreur par défaut
    boost::system::error_code dnsError = boost::asio::error::host_not_found;

    // Tampon pour contenir la réponse du serveur DNS.
    boost::array<char, 1024> dnsBuffer;

    // Trouve le serveur à partir de son nom de domaine ou de son adresse IPv4 ou IPv6 et de son port.
    boost::asio::ip::udp::resolver dns_resolver(io_service);

    // On va parcourir la liste des serveurs DNS tant qu'on n'a pas réussi à avoir une réponse.      
    std::vector<std::string>::const_iterator it = DNSnames.begin();
    while( dnsError && it != DNSnames.end() )
    {
        // On recherche le serveur.
        boost::asio::ip::udp::resolver::query dns_query(*it, "53");
        auto dns_endpoint_iterator = dns_resolver.resolve(dns_query);
        boost::asio::ip::udp::resolver::iterator dns_end;

        // Pour chaque serveur trouvé.
        while(dnsError && dns_endpoint_iterator != dns_end )
        {
            // On essaye de connecter le socket UDP.
            dnsSocket.connect( *dns_endpoint_iterator++ , dnsError);

            // Si on a réussi, on lui envoie notre requête.
            if( ! dnsError)
            {
                dnsSocket.send( boost::asio::buffer(queryText) ,
                                   boost::asio::socket_base::message_flags(), dnsError);

                // Si pas d'erreur, on attend une réponse
                if( ! dnsError )
                {
                    // Si le serveur ne répond pas, on attendra indéfiniment, il faut donc penser à rajouter un timeout.
                    dnsSocket.receive( boost::asio::buffer(dnsBuffer) );
                }
            }
        }
        it++;
    }

    // Si on n'a pas eu de réponse, on lance une exception.
    if(dnsError)
        throw boost::system::system_error(dnsError);

    // Si on a eu une réponse, on la retourne
    return std::string (dnsBuffer.c_array(), dnsBuffer.size() );
}

IV-B-3. Traitement de la réponse

Le format de la réponse du serveur DNS n'est vraiment pas pratique à traiter, c'est sûrement la partie la plus dure de ce tutoriel.

Le serveur DNS va nous donner le ou les serveurs SMTP du domaine avec une « préférence » qui sert de priorité, c'est-à-dire qu'on va essayer de contacter d'abord le serveur ayant la préférence la plus haute et s'il est indisponible, on essayera avec le serveur ayant la seconde priorité la plus élevée.

La commande nslookup peut nous permettre d'envoyer et de voir la réponse du DNS :

 
Sélectionnez
Serveur par défaut :   dns1.proxad.net
Address:  212.27.40.240

> set type=mx
> cnes.fr
Serveur :   dns1.proxad.net
Address:  212.27.40.240

Réponse ne faisant pas autorité :
cnes.fr MX preference = 40, mail exchanger = courrier.cnes.fr
cnes.fr MX preference = 45, mail exchanger = chappe.cnes.fr
cnes.fr MX preference = 45, mail exchanger = baudot.cnes.fr
cnes.fr MX preference = 10, mail exchanger = edge1.cnes.fr
cnes.fr MX preference = 10, mail exchanger = edge2.cnes.fr
cnes.fr MX preference = 10, mail exchanger = edge3.cnes.fr
cnes.fr MX preference = 10, mail exchanger = edge4.cnes.fr

cnes.fr nameserver = tu-dnsext-p02.cnes.fr
cnes.fr nameserver = hal.cerfacs.fr
cnes.fr nameserver = tu-dnsext-p01.cnes.fr
cnes.fr nameserver = dns.univ-lyon1.fr
courrier.cnes.fr        internet address = 132.149.22.11
courrier.cnes.fr        internet address = 132.149.22.2
chappe.cnes.fr  internet address = 132.149.22.2
baudot.cnes.fr  internet address = 132.149.22.11
edge1.cnes.fr   internet address = 132.149.49.1
edge2.cnes.fr   internet address = 132.149.49.3
edge3.cnes.fr   internet address = 132.149.49.2
edge4.cnes.fr   internet address = 132.149.49.4
tu-dnsext-p01.cnes.fr   internet address = 132.149.107.225
tu-dnsext-p02.cnes.fr   internet address = 132.149.107.226

Vous remarquerez ces lignes :

 
Sélectionnez
cnes.fr MX preference = 40, mail exchanger = courrier.cnes.fr
cnes.fr MX preference = 45, mail exchanger = chappe.cnes.fr
cnes.fr MX preference = 45, mail exchanger = baudot.cnes.fr
cnes.fr MX preference = 10, mail exchanger = edge1.cnes.fr
cnes.fr MX preference = 10, mail exchanger = edge2.cnes.fr
cnes.fr MX preference = 10, mail exchanger = edge3.cnes.fr
cnes.fr MX preference = 10, mail exchanger = edge4.cnes.fr

, qui sont les réponses du DNS à notre requête.

Nous avons ici :

  • le domaine : cnes.fr ;
  • le type de serveur : MX pour Mail eXchange ;
  • la préférence : 40, 45 ou 10.
  • et le nom des serveurs : courrier.cnes.fr.

Vous aurez plus d'informations en suivant ce lien : http://www.frameip.com/dns/#5.2_-_Les_Requêtes

 
Sélectionnez
#include <vector>
#include <string>

std::string hostnameToString(std::string hostname);

std::vector<std::string> getXMServerFromAnswer( const std::string & answer, const std::string & query)
{
    // On obtient le nombre de réponses :
    int nbAnswer = (answer[7] << 8) + answer[8];

    // On alloue le vecteur pour stocker les réponses
    std::vector<std::string> MXserver(nbAnswer);

    // On calcule la position du début de la première réponse
    int offset = query.size() + 1;


    // Pour chaque réponse.
    for( ; nbAnswer ; nbAnswer--)
    {
        // On se place à la fin du nom de domaine
        offset = answer.find('\0', offset);

        // On se place après le type, la classe et le "time to live" 
        offset += 8;

        // On récupère la taille des informations supplémentaires
        size_t size = ( (unsigned int)answer[offset] << 8) + (unsigned int)answer[offset + 1];

        // On se place après la longueur des informations supplémentaires et la préférence.
        // Vous pouvez néanmoins récupérer la préférence pour établir une priorité entre les serveurs SMTP à utiliser.
        offset += 4;

        // On récupère le nom de domaine du serveur SMTP.
        std::string hostname = answer.substr(offset, size - 2);

        // Mais ce n'est pas fini, ce nom de domaine à un format un peu particulier...
        MXserver.push_back(  hostnameToString(hostname, answer) );
    }

    return MXserver;
}



std::string hostnameToString(std::string hostname)
{
    size_t i = 0;
    // On va parcourir notre chaîne de caractères.
    while( i < hostname.size() )
    {
        // Si le caractère vaut 0xc0, cela signifie que la suite du nom de domaine est à rechercher à l'emplacement indiqué par le caractère suivant.
        if( hostname[i] == '\xc0' )
        {
            // On récupère la suite du nom de domaine.
            size_t fin = result.find('\0', (unsigned char)hostname[i+1]) - (unsigned char)hostname[i+1];
            hostname += result.substr( (unsigned char)hostname[i+1],
                                             fin );

            // On supprime le caractère 0xc0 et celui qui suivait.
            hostname.erase(i,2);
        }
        else
        {
            // Sinon, le caractère sur lequel nous tombons indique la longueur de la partie de nom de domaine qui suit.
            size_t size = hostname[i];
            if(size)
            {
                // On le remplace par un « . ».
                hostname[i] = '.';

                // On se place après la fin de cette partie de nom de domaine.
                i += size + 1;
            }
            else
                break; // La taille est nulle, on est arrivé à la fin.
        }
    }

     // Les noms de domaine ne commencent pas par un « . », on le supprime donc.
    hostname.erase(0,1);

    return hostname;
}

IV-B-4. Obtenir la liste des serveurs SMTP

 
Sélectionnez
#include <vector>
#include <string>
#include <boost/asio.hpp>

std::vector<std::string> getMXServer( const std::string & mail, boost::asio::io_service & io_service )
{
    // On obtient la liste des serveurs DNS.
    std::vector<std::string> DNSnames = getDNS();

    // On obtient la requête à envoyer.
    std::string queryText = buildDNSQuery(mail);

    // On obtient la réponse du serveur DNS.
    std::string response = askDNS( queryText, DNSnames, io_service);

    // On obtient la liste des serveurs MX.
    return getXMServerFromAnswer( response, queryText);
}

IV-C. Dialoguer avec le serveur SMTP

IV-C-1. Obtenir une socket

Maintenant on va tenter d'établir une connexion TCP sur un serveur SMTP sur le port 25.

 
Sélectionnez
#include <vector>
#include <string>
#include <boost/asio.hpp>

boost::asio::ip::tcp::socket getMXSocket( std::vector<std::string> Mxnames,
                                          boost::asio::io_service & io_service )
{
    boost::asio::ip::tcp::socket socket(io_service);

    boost::asio::ip::tcp::resolver resolver(io_service);

    // Erreur par défaut.
    boost::system::error_code error = boost::asio::error::host_not_found;

    auto it = Mxnames.begin();
    auto end = Mxnames.end();

    // Tant qu'on n'a pas réussi à se connecter, on parcourt la liste des serveurs MX.
    while( error && it != end)
    {

        boost::asio::ip::tcp::resolver::query query(*it, "25");
        boost::asio::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
        boost::asio::ip::tcp::resolver::iterator end;

        // Pour chaque serveur trouvé.
        while(error && endpoint_iterator != end )
        {
            // On essaye de se connecter au serveur.
            socket.close();
            socket.connect(*endpoint_iterator++, error);
        }
        ++it;
    }

    // Si aucun serveur n'a été trouvé.
    if( error )
        throw boost::system::system_error(error);

    return socket;
}

IV-C-2. Envoyer l'e-mail

Maintenant que nous avons notre socket, nous sommes enfin prêts à envoyer notre e-mail !

 
Sélectionnez
bool verifyResponse(const std::string & responseWaited, boost::asio::ip::tcp::socket & socket)
{
    bool retour = true;

    boost::asio::streambuf response;
    std::string code;

    boost::asio::read_until(socket, response, "\r\n");
    {
        std::istream is(&response);
        is >> code;
        if( code != responseWaited)
        {
            std::cerr << "Réponse du serveur SMTP inattendu" << std::endl;
            retour = false;
        }

        response.consume( response.size() );

        boost::asio::socket_base::bytes_readable command(true);
        socket.io_control(command);
        while( command.get() )
        {
            boost::asio::read_until(socket, response, "\r\n");
            socket.io_control(command);
        }
        response.consume( response.size() );

    }

    return retour;
}
        
void sendMail( const std::string & destinataire,
               const std::string & expediteur,
               const std::string & message,
               boost::asio::ip::tcp::socket & socket )
{
    std::string hello = "HELO Client\r\n";
    std::string exp = "MAIL FROM: <" + expediteur + ">\r\n";
    std::string dest = "RCPT TO: <" + destinataire + ">\r\n";
    std::string beginData = "DATA\r\n";
    std::string endData = "\r\n.\r\n";
    std::string quit = "QUIT\r\n";

    verifyResponse("220", socket);

    boost::asio::write(socket, boost::asio::buffer(hello) );
    verifyResponse("250", socket);

    boost::asio::write(socket, boost::asio::buffer(exp) );
    verifyResponse("250", socket);

    // Vous pouvez répéter cette action si vous souhaitez envoyer l'e-mail à plusieurs destinataires
    // dont l'adresse e-mail a le même nom de domaine.
    boost::asio::write(socket, boost::asio::buffer(dest) );
    verifyResponse("250", socket);

    boost::asio::write(socket, boost::asio::buffer(beginData) );
    verifyResponse("354", socket);

    boost::asio::write(socket, boost::asio::buffer(message) );
    boost::asio::write(socket, boost::asio::buffer(endData) );
    verifyResponse("250", socket);

    boost::asio::write(socket, boost::asio::buffer(quit) );
    verifyResponse("221", socket);

    socket.close();
}

Exemple de discussions SMTP, en bleu ce qui a été tapé dans la console, en noir des explications :

telnet mx4.hotmail.com 25
Trying 65.55.37.104...
Connected to mx4.hotmail.com.
Escape character is '^]'.
220 COL0-MC3-F6.Col0.hotmail.com Sending unsolicited commercial or bulk e-mail to Microsoft's computer network is prohibited. Other restrictions are found at http://privacy.microsoft.com/en-us/anti-spam.mspx. Sun, 19 May 2013 11:36:35 -0700
HELO Client // On dit bonjour au serveur.
250 COL0-MC3-F6.Col0.hotmail.com (3.18.0.71) Hello [81.56.196.139]
MAIL FROM: <neckara@fake.fr> // On donne l'adresse e-mail de l'expéditeur.
250 neckara@fake.fr....Sender OK
RCPT TO: <testXM@outlook.fr> // On donne le(s) adresse(s) e-mail des destinataires.
250 testXM@outlook.fr
DATA // On dit qu'on va donner le contenu du message.
354 Start mail input; end with <CRLF>.<CRLF>
Mon message
Encore mon message // On donne le message.
. // On finit le message avec un point seul sur une ligne suivi d'un retour à la ligne.
250 <COL0-MC3-F6D8LEHlA900304fa4@COL0-MC3-F6.Col0.hotmail.com> Queued mail for delivery
QUIT // On ferme la connexion avec le serveur.
221 COL0-MC3-F6.Col0.hotmail.com Service closing transmission channel
Connection closed by foreign host.

Certains FAI filtrent les accès sortant SMTP, vous aurez alors des problèmes de timeout lorsque vous essayerez de joindre ce serveur sur le port 25.
Il vous faut alors aller dans le panneau d'administration de votre box pour autoriser explicitement les accès sortant SMTP (fonctionnalité n'étant pas présente chez tous les FAI).
Il est aussi possible que le réseau d'entreprise où vous vous trouvez interdise la connexion aux serveurs SMTP distants, il vous faudra alors vous connecter au serveur SMTP de l'entreprise.

Pour tester les échanges SMTP, vous pouvez utiliser la commande « telnet » sous Windows et Linux ainsi que la commande « nslookup -type=MX hotmail.com » pour trouver les serveurs SMTP associés à un nom de domaine.

IV-C-3. Rédaction du message

Nous utiliserons le MIME (Multipurpose Internet Mail Extensions ou Extensions multifonctions du courrier Internet) qui permet d'envoyer des messages avec des formats autres que le simple format ASCII.

Vous trouverez plus d'information sur le MIME en suivant ce lien : https://en.wikipedia.org/wiki/MIME

 
Sélectionnez
#include <vector>
#include <string>

std::string buildMessage( const std::string & expediteur,
                          const std::vector<std::string> & listDestinataires,
                          const std::string & subject,
                          const std::string & htmlMessage,
                          const std::string & rawMessage,
                          const std::string & sectionWord = "08zs01293eraf47a7804dcd17b1e")
{
    std::string message = "Mime-Version: 1.0\r\n";

    // On renseigne l'expéditeur.
    message += "from: automatique<" + expediteur + ">\r\n";

    // On renseigne tous les destinataires.
    // Ils doivent être au format alias<e-mail>, alias étant facultatif.
    // Exemple : toto<toto@foo.fr>
    for( const auto & destinataire : listDestinataires)
        message += "to: "+ destinataire + "\r\n";

    // Ajout du sujet du message.
    message += "subject: " + subject + "\r\n";

    // On a un message contenant plusieurs formats.
    // Les différents formats seront séparés par le mot sectionWord.
    message += "Content-Type: multipart/alternative; boundary=" + sectionWord  + "\r\n";
    message += "\r\n";

    // Début du premier format.
    message += "\r\n--" + sectionWord  + "\r\n";

    // Texte brut.
    message += "Content-type: text/plain; charset=ISO-8859-1\r\n";
    message += "\r\n";

    // Texte brut ASCII.
    // Pensez toujours à intégrer ce format à vos messages car tous les clients de messagerie ne supportent pas le HTML.
    message += rawMessage;

    message += "\r\n";

    // Fin de ce format et début de la prochaine.
    message += "--" + sectionWord  + "\r\n";

    // Texte HTML.           
    message += "Content-type: text/html; charset=ISO-8859-1\r\n";
    message += "\r\n";

    // Texte HTML.
    message += htmlMessage;

    message += "\r\n";

    // Fin de l'e-mail.
    message += "\r\n--" + sectionWord  + "--\r\n";

    return message;
}

Exemple d'e-mail complet sans mise en forme ni interprétation, en bleu des commentaires rajoutés :

// Rajouté par le serveur SMTP
Delivered-To: XXXXX@XXXX.XX
Received: by 10.194.85.135 with SMTP id h7csp40544wjz;
Sun, 19 May 2013 11:53:29 -0700 (PDT)
X-Received: by 10.68.185.162 with SMTP id fd2mr57142367pbc.176.1368989608596;
Sun, 19 May 2013 11:53:28 -0700 (PDT)
Return-Path: <XXXX@XXXXX.XX>
Received: from Client (lns-bzn-50f-81-56-196-139.adsl.proxad.net. [XX.XX.XX.XX])
by mx.google.com with SMTP id tw9si14607938pac.158.2013.05.19.11.53.26
for <XXXXX@XXXX.XXX>;
Sun, 19 May 2013 11:53:28 -0700 (PDT)
Received-SPF: neutral (google.com: XX.XX.XX.XX is neither permitted nor denied by best guess record for domain of XXXXX@XXXXXX.XX) client-ip= XX.XX.XX.XX;
Authentication-Results: mx.google.com;
spf=neutral (google.com: XX.XX.XX.XX is neither permitted nor denied by best guess record for domain of XXXXXXXX@XXXXXXX.XX) smtp.mail=XXXXXXXXX@XXXXXXX.XX
Date: Sun, 19 May 2013 11:53:28 -0700 (PDT)
Message-Id: <51991fa8.89e9420a.2fc5.ffffca51SMTPIN_ADDED_MISSING@mx.google.com>
Mime-Version: 1.0 // Version du MIME utilisé.
from: automatique<XXXXXX@XXXX.XX> // Expéditeur.
to: <XXXXXXX@XXXXXX.XXX> // Destinataire(s).
subject: test // Sujet du message.
// On a un message contenant plusieurs formats alternatifs.
Content-Type: multipart/alternative; boundary=08zs01293eraf47a7804dcd17b1e

// Premier format.
--08zs01293eraf47a7804dcd17b1e
// Format texte brut.
Content-type: text/plain; charset=ISO-8859-
// Message en texte brut.
Votre client de messagerie ne supporte pas le HTML, vous pouvez voir le contenu de cet e-mail à  cette adresse : .....

// Second format.
--08zs01293eraf47a7804dcd17b1e
// Format HTML.
Content-type: text/html; charset=ISO-8859-1
// Message en HTML.
<html><body><h1>Test</h1><p>Ceci est un test</p></body></html>

// Fin des formats.
--08zs01293eraf47a7804dcd17b1e--

IV-C-4. Code final

Et voilà enfin le code qui va appeler toutes nos fonctions pour envoyer notre e-mail !

 
Sélectionnez
#include <string>
#include <vector>
#include <boost/asio.hpp>

void sendMail( const std::string & destinataire,
               const std::string & expediteur,
               const std::vector<std::string> & listDestinataires,
               const std::string & subject,
               const std::string & htmlMessage,
               const std::string & rawMessage,
               const std::string & sectionWord,
               boost::asio::io_service & io_service )
{
    // On construit le message
    std::string message = buildMessage( expediteur, listDestinataires, subject, htmlMessage, rawMessage, sectionWord);

    // On recherche les serveurs SMTP.
    std::vector<std::string> MXnames = getMXServer( destinataire, io_service );

    // On obtient un socket sur un serveur SMTP.
    boost::asio::ip::tcp::socket socket = getMXSocket( MXnames, io_service );


    // On envoie l'e-mail.
    sendMail( destinataire, expediteur, message, socket );
}

V. Résumé

Voici un court résumé de ce que nous avons vu dans ce tutoriel.

V-A. Inscription

  • On demande à l'utilisateur de fournir son adresse e-mail en remplissant un formulaire.
  • On n'oublie pas de prévenir l'utilisateur de l'utilisation qui sera faite de son adresse e-mail.
  • On enregistre le compte dans la base de données mais nous spécifions que ce dernier n'est pas activé.
  • On envoie à l'utilisateur un e-mail contenant un code nécessaire à l'activation de son compte.
  • L'utilisateur peut alors activer son compte soit en cliquant sur un lien éventuellement fourni dans l'e-mail envoyé soit en renseignant le code à sa première tentative de connexion.
  • Le compte passe alors de l'état non-activé à l'état activé, il est désormais possible à l'utilisateur d'accéder à son nouveau compte.

V-B. Réinitialisation du mot de passe

  • On vérifie que l'utilisateur est bien le propriétaire du compte en lui demandant par exemple son login ou une réponse à une question secrète.
  • On envoie à l'utilisateur un e-mail permettant à l'utilisateur de réinitialiser son mot de passe.

V-C. Envois d'e-mail

  • On recherche la liste des serveurs DNS :

    • sous Windows, on utilise l'API Windows,
    • sous Linux, on lit le contenu du fichier /etc/resolv.conf,
    • dans tous les cas, on peut aussi utiliser les serveurs DNS de Google.
  • On interroge ensuite les serveurs DNS pour trouver les entrées MX correspondant au domaine de l'adresse e-mail du destinataire.
  • On récupère et on lit la réponse du serveur DNS.
  • On tente ensuite de se connecter aux serveurs SMTP renseignés par les entrées MX du serveur DNS.
  • Dès qu'on arrive à se connecter à l'un des serveurs, on dialogue avec ce dernier pour envoyer l'e-mail.

VI. Conclusion

Voilà, vous pouvez désormais envoyer un e-mail en C++ avec boost.

Il ne tient qu'à vous d'améliorer ce code en permettant d'envoyer en une seule fois un e-mail à une liste de personnes dont le serveur SMTP à utiliser est le même, vous pouvez aussi permettre l'ajout de pièces jointes ou pourquoi pas créer votre propre modèle de message.

VII. Remerciements

Je tiens à remercier ram-0000 et sevyc64 pour leurs conseils, ram-0000, LittleWhite et germinolegrand pour leur relecture technique ainsi que f-leb pour sa relecture orthographique.

VIII. Sources

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Neckara. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.