Sending an email within a Windows 8.1 application : using StreamSocket to emulate a SmtpClient

Hi,

Today we will see how to send a simple email from a Windows 8.1 Modern UI application.

image

For now, there is no API in the WinRT stack dedicated to mail. The System.Net.Mail namespace, containing classes like SmtpClient or MailMessage, is not present in WinRT, like it is in the .NET Framework.

Here is the source code of this articl : SendMail.zip

To Solve this problem, we need to know :

  1. How work a smtp server and how communicate within it.
  2. How send and receive messages to and from a smtp Server, with the StreamSocket API.

Smtp Communication

First of all, some interesting reading. Here are some interesting articles on the smtp protocol :

In this article, we’ll just send a simple mail, using our own SmtpClient, communicating with an existing smtp server (Outlook.com or Gmail.com) using some key words like :

  • Ehlo : Send a command to the smtp server to know some properties like authentication mode, SSL, TSL support etc …
  • Auth Login : Send a command requesting authentication on the smtp server.
  • StartTls : Send a command requesting a TSL communication.
  • Mail From : Send a command with the mail author.
  • Rcpt To : Send a command with one or more receivers.
  • Data : Send a command for the body mail.
  • Quit : Send a command ending the connection with the smtp server.

The smtp server in turn, will respond to us, using some status string (int code) with one or more string rows. Each message are specific for each smtp server meanwhile each status string is shared by all smtp server (come from the smtp specification)

Here are some codes that we will use in our sample :

  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.

All the communication process between a client and a server can be described like this :

image6

For example, here is a complete communication between a client (C) and the gmail.com smtp server (S) :

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

and here the same communication between a client (C) and the Outlook.com smtp server (S) :

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

For your information : In these two examples, the User name and the password are written in clear text. In the smtp Protocol you must send them in base 64.

StreamSocket

Communicating with a smtp server within a Windows 8.1 application can be done with the StreamSocket API.

Connection

Connecting a smtp server with StreamSocket is straightforward:

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

Once the connections established, you can easily upgrade to SSL, like this :

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

Reading / Writing

The StreamSocket object is exposing two properties that can be used to read the input Stream (InputStream) and write in the output Stream (OutputStream)

Those two properties can be managed by two dedicated objects, respectfully the DataReader object and the DataWriter object (namespace Windows.Storage.Streams)

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

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

First of all, and by the way the most simple operation : Writing in the output Stream :

 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;
    }

}

Reading the Input Stream is a little bit more complicated (but, not so complicated :) )

Initially, because we are in a streaming mode, we don’t know the stream length. We need to read the steam until we reach the end of the stream. The property UnconsummedBufferLength will help us to check how many bytes we don’t have read in the actual buffer.

We choose a buffer size (1024) and we read while … we have something to read !

 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;
}

In my particular case, I can read multiple line, i m using a 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;
}

For your information : In the sample provided within this article, you will find a more complex method, which will parse the stream in a single pass.

Smtp Communication

Each time we are sending a command message, we have to get the response from the server, before sending an other command.

Pretty Straightforward now that we have implemented the Send() method and the GetResponse() method:

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

After a quick refactoring, integrating the GetResponse() in the Send() method, we are able to send and get the response within one line of code, like this :

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

Sending an email

Once you know how to send and get a response, and what kind of messages you can send to the smtp server, creating an email and sending is pretty … straightforward :

 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;
}

You will find in the sample code attached with this article, the full code of the Authenticate() and Connect() methods.

About the mail body, you need to send some metadatas like priority, encoding, subject and body (of course) and terminating with a <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();

}

Using our own SmptClient API

Finally, using our implementation is straightforward (yes again). Here is the final code using two smtp servers (Gmail.com and Outlook.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

This sample is a quick sample. There is still a lot of work to be done. For example, supporting attached file or large email body (CHUNKING)

Meanwhile, it’s a good starting point to understand how work a smtp server and how work StreamSocket within WinRT !

Happy mailing :)

//seb

SendMail.zip

Comments

  • Anonymous
    April 29, 2014
    Thank you I searhed for this I will try now Thank you so much

  • Anonymous
    July 01, 2014
    Thanks a lot buddy. Finally I've something which works! Can you help me getting inbox messages and notifications on new email in windows 8.1 app using C# & XAML?

  • Anonymous
    August 07, 2014
    Thanks you so much. Finally got something that works.. :) awesome work.

  • Anonymous
    September 01, 2014
    The comment has been removed

  • Anonymous
    February 19, 2015
    Thank You so much. Can you please help me that how to attached photo ?

  • Anonymous
    May 23, 2015
    Thank you so much for this great article.. Its really a great job. But can we add a feature of sending attachment with this email? if possible then how?

  • Anonymous
    June 04, 2015
    Can you please post the example how to attach file ?

  • Anonymous
    September 03, 2015
    Hey Sebastien, thank you for the awesome work. I found a small bug you and the users visiting the site might be interested in. The problem: The date of the message does not take timezones into account. For example if I live in UTC +0200 timezone and send a message at 21:00 local time, the receiver will read that the date of the message is 23:00 +0000. Some email clients (such as outlook.com) are smart enough to correct this in the visualization, but some (like gmail or the default iOS mail app) are not, and will show a wrong date. The easy fix: In SmtpMessage.cs, in the definition of GetBody() change var dateFormat = "ddd, dd MMM yyyy HH:mm:ss +0000"; sb.AppendFormat("Date: {0}{1}", DateTime.Now.ToString(dateFormat), System.Environment.NewLine); to var dateFormat = "ddd, dd MMM yyyy HH:mm:ss +0000"; sb.AppendFormat("Date: {0}{1}", DateTime.UtcNow.ToString(dateFormat), System.Environment.NewLine); In my example, if I use UtcNow, the receiver will see 19:00 +0000 which is equivalent to 21:00 +0200. This means that all visualizers in his client will show the correct date. Anyway the message source should show 21:00 +0200, and this can only be achieved by changing dateFormat using the active timezone.

  • Anonymous
    February 01, 2016
    Can You Please Describe How To add Html <br /> and href Tag in Body Content Send To User.

  • Anonymous
    February 07, 2016
    while sending the mail with outlook server the mail is Queued  and I get this message ""250 2.6.0 <BLU437-SMTP42cM3ory00004cff@BLU437-SMTP42.smtp.hotmail.com> Queued mail for delivery". And while sending it with gmail I receive and email from Gmail saying that someone is traying to connect to my account from unsecured app. What should I do in these cases?

  • Anonymous
    March 13, 2016
    Hi, It is working fine. I want to know, how to add bcc?

  • Anonymous
    June 20, 2016
    The comment has been removed