Envoyer un email depuis une application Windows 8.1 : Utiliser StreamSocket pour créer un client Smtp

Bonjour à tous;

Aujourd’hui nous allons voir comment envoyer un email depuis une application Modern UI Windows 8.1

image

Il faut savoir qu’il n’existe pas encore dans WinRT d’API dédiée à la messagerie, l’espace de nommage System.Net.Mail contenant des classes comme SmtpClient ou encore MailMessage, n’existant que pour le Framework .NET

Voici le code source de cet article : SendMail.zip

Pour résoudre ce problème, il va falloir :

  1. Comprendre comment communiquer avec un serveur Smtp,
  2. Envoyer des instructions et recevoir des messages provenant du serveur smtp, en utilisant l’API StreamSocket.

Communiquer avec un serveur Smtp

Tout d’abord, un peu de lecture. Afin de comprendre les messages à échanger entre vous et un serveur smtp, voici quelques pointeurs intéressants :

Dans cet article, nous allons juste envoyer un mail, court, en utilisant une authentification simple. Le principe va être d’envoyer des commandes au serveur smtp que celui-ci comprenne, à l’aide de mots clés tel que :

  • Ehlo : Permet de connaître les types d’authentification nécessaires ou le mode de sécurité SSL, TSL.
  • Auth Login : Authentification par login / mot de passe.
  • Starttls : Upgrade de la connexion en SSL.
  • Mail From : Initiateur du mail.
  • Rcpt To : Destinataire du mail.
  • Data : Corps du mail.
  • Quit : Fin de connexion.

Le serveur smtp quant à lui, va nous répondre à l’aide de codes (entiers), ainsi que d’un ou plusieurs messages. Les messages sont assez différents suivant les serveurs smtp, les codes eux sont fixes et permettent de coordonner nos actions.

Les principaux codes utilisés dans cet exemple seront :

  1. 220 : Service Ready
  2. 250 : Request completed
  3. 334 : Waiting for Authentication
  4. 235 : Authentication successful
  5. 354 : Start mail input
  6. 221 : Closing connection

Une communication va s’initier entre le client et le serveur smtp, une fois la connexion établie.

Voici un exemple de communication :

image

Dans les faits, voici un exemple de communication complète avec un serveur SMTP.GMAIL.COM :

Connect smtp.google.com 465
S : 220 mx.google.com ESMTP o47sm20731478eem.21 - gsmtp
C : EHLO www.contoso.com
S : 250-mx.google.com at your service, [94.245.87.37]
    250-SIZE 35882577
    250-8BITMIME
    250-AUTH LOGIN PLAIN XOAUTH XOAUTH2 PLAIN-CLIENTTOKEN
    250-ENHANCEDSTATUSCODES
    250 CHUNKING
C : LOGIN AUTH
S : 334 VXNlcm5hbWU6
C : john.doe@gmail.com
S : 334 UGFzc3dvcmQ6
C : MyPassword@5o5tr0ng
S : 235 2.7.0 Accepted
C : MAIL FROM:<john.doe@gmail.com>
S : 250 2.1.0 OK o47sm20731478eem.21 - gsmtp
C : RCPT TO:<spertus@microsoft.com>
S : 250 2.1.5 OK o47sm20731478eem.21 - gsmtp
C : DATA
S : 354  Go ahead o47sm20731478eem.21 - gsmtp
C : Date: Wed, 27 Nov 2013 16:47:26 +0000
    X-Priority: 0
    To: spertus@microsoft.com
    MIME-Version: 1.0
    Content-Transfer-Encoding: 7bit
    Content-Disposition: inline
    Subject: Hi Guy !
    Content-Type: text/plain; charset="utf-8"

    Hi Sebastien, how are you ??
    John D.
    .
S : 250 2.0.0 OK 1385567306 o47sm20731478eem.21 - gsmtp
C : QUIT
S : 221 2.0.0 closing connection o47sm20731478eem.21 – gsmtp

La même version depuis un serveur SMTP-MAIL.OUTLOOK.COM :

Connect smtp-mail.outlook.com 587
S : 220 BLU0-SMTP180.phx.gbl Microsoft ESMTP MAIL Service, Version: 6.0.3790.4675 ready at  Wed, 27 Nov 2013 08:28:59 -0800
C : EHLO www.contoso.com
S : 250-BLU0-SMTP180.phx.gbl Hello [94.245.87.37]
    250-TURN
    250-SIZE 41943040
    250-ETRN
    250-PIPELINING     
    250-DSN
    250-ENHANCEDSTATUSCODES
    250-8bitmime
    250-BINARYMIME
    250-CHUNKING
    250-VRFY
    250-TLS
    250-STARTTLS
    250 OK
C : STARTTLS
S : 220 2.0.0 SMTP server ready
C : EHLO www.contoso.com
S : 250-BLU0-SMTP180.phx.gbl Hello [94.245.87.37]
    250-TURN
    250-SIZE 41943040
    250-ETRN
    250-PIPELINING
    250-DSN
    250-ENHANCEDSTATUSCODES
    250-8bitmime
    250-BINARYMIME
    250-CHUNKING
    250-VRFY
    250-AUTH LOGIN PLAIN XOAUTH2
    250 OK
C : AUTH LOGIN
S : 334 VXNlcm5hbWU6
C : john.doe@outlook.com
S : 334 UGFzc3dvcmQ6
C : MyF4bulousP@zzw0rd
S : 235 2.7.0 Authentication succeeded
C : MAIL FROM:<john.doe@outlook.com>
S : 250 2.1.0 john.doe@outlook.com....Sender OK
C : RCPT TO:<spertus@microsoft.com>
S : 250 2.1.5 spertus@microsoft.com
C : DATA
S : 354 Start mail input; end with <CRLF>.<CRLF>
C : Date: Wed, 27 Nov 2013 17:28:56 +0000
    X-Priority: 0
    To: sebastien.pertus@gmail.com, spertus@microsoft.com
    MIME-Version: 1.0
    Content-Transfer-Encoding: 7bit
    Content-Disposition: inline
    Subject: Hi Guy.
    Content-Type: text/plain; charset="utf-8"

    Hi Sebastien, how are you ??
     John D.
    .
S : 250 2.6.0 <BLU0-SMTP180Np7vXee0000a899@BLU0-SMTP180.phx.gbl> Queued mail for delivery
C : QUIT
S : 221 2.0.0 BLU0-SMTP180.phx.gbl Service closing transmission channel

Note : Le nom d’utilisateur et le mot de passe doivent être transmis encodés en Base 64, mais pour des raisons de lisibilité, dans les 2 exemples précédents, je les ai laissés en clair dans le texte :)

StreamSocket

Pour communiquer avec un serveur smtp depuis une application WinRT, il suffit d’utiliser StreamSocket.

Connexion

Se connecter à un serveur smtp est la partie la plus simple :

 if (this.isSsl)
    await socket.ConnectAsync(this.hostName, this.port.ToString(), SocketProtectionLevel.Ssl);
else
    await socket.ConnectAsync(this.hostName, this.port.ToString(), SocketProtectionLevel.PlainSocket);

Une fois la connexion établie, il est possible d’upgrader la connexion si celle ci n’est pas SSL par défaut :

 await socket.UpgradeToSslAsync(SocketProtectionLevel.Ssl, this.hostName);

Lecture / Ecriture

l’objet socket va nous permettre de nous brancher sur le flux d’entrée et de sortie, via les deux propriétés InputStream et OutputStream de l'objet StreamSocket

Ces deux flux peuvent être contrôler respectivement via un DataReader et un DataWriter  (namespace Windows.Storage.Streams) :

 this.reader = new DataReader(socket.InputStream);
this.reader.InputStreamOptions = InputStreamOptions.Partial;

this.writer = new DataWriter(socket.OutputStream);

Ecrire dans le flux est relativement simple :

 public async Task Send(String command)
{
    Debug.WriteLine(command);
    return await this.Send(Encoding.UTF8.GetBytes(command + System.Environment.NewLine), command);
}

public async Task Send(Byte[] bytes, string command)
{
    try
    {
        writer.WriteBytes(bytes);
        await writer.StoreAsync();
    }
    catch (Exception ex)
    {
        Debug.WriteLine(command + ":" + ex.Message);
        return null;
    }

}

Lire un flux est plus complexe (mais pas tant que ça :) )
Ne connaissant pas la taille du flux entrant, il faut donc découper le flux et lire tant qu’il reste un buffer à lire.
La propriété UnconsummedBufferLength permet de vérifier combien de bytes il reste à lire dans le buffer en cours.
Il suffit donc de lire des blocs de 1024 bytes et boucler tant qu’il reste de quoi lire :

private async Task<MemoryStream> GetResponseStream() { MemoryStream ms = new MemoryStream(); while (true) { await reader.LoadAsync(bufferLength); if (reader.UnconsumedBufferLength == 0) { break; } Int32 index = 0; while (reader.UnconsumedBufferLength > 0) { ms.WriteByte(reader.ReadByte()); index = index + 1; } if (index == 0 || index < bufferLength) break; } ms.Seek(0, SeekOrigin.Begin); return ms; }

La lecture du flux pouvant contenir plusieurs lignes, voici la méthode GetResponse() utilisant un StreamReader :

 private async Task<List<String>> GetResponse()
{
    List<String> lines = new List<String>();
    using (MemoryStream ms = await GetResponseStream())
    {
        using (StreamReader sr = new StreamReader(ms))
        {
            while (!sr.EndOfStream)
            {
                var line = sr.ReadLine();

                if (String.IsNullOrEmpty(line))
                    break;

                lines.Add(line);
            }
        }
    }
    return lines;
}

Note : Dans l’exemple fournit avec cet article vous trouverez une méthode plus complexe mais plus efficace pour la lecture du flux, évitant de parcourir deux fois le Stream en lecture.

Communication Smtp

A chaque envoi d’un message, il est nécessaire de récupérer la réponse du serveur smtp avant de renvoyer un autre message.

Voici l’exemple d’envoi de la commande “EHLO”, utilisant la méthode Send() et la méthode GetResponse() que nous venons d’implémenter:

 await this.smtpSocket.Send("EHLO " + this.Server);
var r = this.smtpSocket.GetResponse();

En intégrant la méthode GetResponse() dans la méthode Send(), il est alors possible d’effectuer l’opération en 1 seule ligne :

 var r  = this.smtpSocket.Send("EHLO " + this.Server);

Envoyer un mail

Envoyer un mail est relativement simple. Il suffit d’empiler les ordres correctement, suivant les réponses serveur :

 public async Task<Boolean> SendMail(SmtpMessage message)
{

    if (!this.IsConnected)
        await this.Connect();

    if (!this.IsConnected)
        throw new Exception("Can't connect");

    if (!this.IsAuthenticated)
        await this.Authenticate();

    var rs = await this.smtpSocket.Send(String.Format("Mail From:<{0}>", message.From));

    if (!rs.ContainsStatus(SmtpCode.RequestedMailActionCompleted))
        return false;

    foreach (var to in message.To)
    {
        var toRs = await this.smtpSocket.Send(String.Format("Rcpt To:<{0}>", to));

        if (!toRs.ContainsStatus(SmtpCode.RequestedMailActionCompleted))
            break;
    }

    var rsD = await this.smtpSocket.Send(String.Format("Data"));

    if (!rsD.ContainsStatus(SmtpCode.StartMailInput))
        return false;

    var rsM = await this.smtpSocket.Send(message.GetBody());

    if (!rsM.ContainsStatus(SmtpCode.RequestedMailActionCompleted))
        return false;

    var rsQ = await this.smtpSocket.Send("Quit");

    if (!rsQ.ContainsStatus(SmtpCode.ServiceClosingTransmissionChannel))
        return false;

    return true;
}

Vous trouverez dans l'exemple fournit avec l’article le détail des méthodes Authenticate() ou encore Connect() qui suivent le même principe d’ordonnancement des messages / réponses.

Le corps du mail contient des informations nécessaires comme la priorité, le type Mime, l’encoding, le sujet, le corps du mail et la fin du message terminé par <CRLF>.<CRLF> :

 public String GetBody()
{
    StringBuilder sb = new StringBuilder();

    var dateFormat = "ddd, dd MMM yyyy HH:mm:ss +0000";
    sb.AppendFormat("Date: {0}{1}", DateTime.Now.ToString(dateFormat), System.Environment.NewLine);

    if (String.IsNullOrEmpty(this.From))
        throw new Exception("From is mandatory");

    sb.AppendFormat("X-Priority: {0}{1}", ((byte)this.Priority).ToString(), System.Environment.NewLine);

    if (this.to.Count == 0)
        throw new Exception("To is mandatory");

    sb.Append("To: ");
    for (int i = 0; i < this.to.Count; i++)
    {
        var to = this.to[i];
        if (i == this.to.Count - 1)
            sb.AppendFormat("{0}{1}", to, System.Environment.NewLine);
        else 
            sb.AppendFormat("{0}{1}", to, ", ");

    }
    foreach (var to in this.To)
  
    if (this.cc.Count != 0)
    {
        sb.Append("Cc: ");
        for (int i = 0; i < this.cc.Count; i++)
        {
            var cc = this.cc[i];
            if (i == this.cc.Count - 1)
                sb.AppendFormat("{0}{1}", cc, System.Environment.NewLine);
            else
                sb.AppendFormat("{0}{1}", cc, ", ");

        }
    }

    sb.AppendFormat("MIME-Version: 1.0{0}", System.Environment.NewLine);
    sb.AppendFormat("Content-Transfer-Encoding: {0}{1}", this.TransferEncoding, System.Environment.NewLine);
    sb.AppendFormat("Content-Disposition: inline{0}", System.Environment.NewLine);
    sb.AppendFormat("Subject: {0}{1}" , this.Subject, System.Environment.NewLine);

    if (this.IsHtml)
        sb.AppendFormat("Content-Type: text/html; {0}", System.Environment.NewLine);
    else
        sb.AppendFormat("Content-Type: text/plain; charset=\"{0}\"{1}", this.Encoding.WebName, System.Environment.NewLine);

    sb.Append(System.Environment.NewLine);
    sb.Append(this.Body);
    sb.Append(System.Environment.NewLine);
    sb.Append(".");

    return sb.ToString();

}

Utilser notre custom API

Au final, l’envoi du mail via la classe SmtpClient et SmtpMessage que nous venons de créer et fournis dans le zip fournit avec l’article, peut s’effectuer de la manière suivante (exemple avec Outlook.com et Gmail.com) :

 // Outlook.com
// HostName : smtp-mail.outlook.com
// Port : 587
// SSL : No (upgarde ssl after STARTTLS)

// Gmail.com
// HostName : smtp.gmail.com
// Port : 465
// SSL : Yes

// Gmail
//SmtpClient client = new SmtpClient("smtp.gmail.com", 465, 
// "your_mail@gmail.com", "password", true);

// Outlook
SmtpClient client = new SmtpClient("smtp-mail.outlook.com", 587, 
                                    "your_mail@outlook.com", "password", false);
         
SmtpMessage message = new SmtpMessage("your_mail@outlook.com", 
                                        "john.doe@contoso.com", null, "sujet", "corps du mail");
message.To.Add("spertus@microsoft.com");

await client.SendMail(message);

Conclusion

Il existe beaucoup d’amélioration à apporter sur ce genre de composant, comme le support des pièces jointes ou encore le découpage (CHUNKING) de mail lourd.
Cependant c’est un bon début pour comprendre comment fonctionne un client Smtp et l’utilisation avantageuse de l’objet StreamSocket avec WinRT !

Bon mail !

//seb

SendMail.zip