SMTPの仕組みをC#で理解する
SMTPの概要
メールシステムが出来上がった頃2つのプロトコルがありました。SMTPとPOP3です。後にIMAPが追加されました。 SMTPはメールの送信をするためのプロトコルになります。POP3とIMAPはメールの受信をするためのプロトコルです。 SMTPの基本的な流れは以下のようになります。 このページではSMTPでメールをするための処理をC#での実装例とともに解説します。 実装例としてはHigLaboのソースを元に解説します。
接続を開く
メールサーバーと通信をするためにSocketオブジェクトを初期化する必要があります。HigLabo.NetプロジェクトのSocket.csファイル を見ると以下のようになっています。
protected Socket GetSocket()
{
    Socket tc = null;
    IPHostEntry hostEntry = null;
    hostEntry = this.GetHostEntry();
    if (hostEntry != null)
    {
        foreach (IPAddress address in hostEntry.AddressList)
        {
            tc = this.TryGetSocket(address);
            if (tc != null) { break; }
        }
    }
    return tc;
}
private Socket TryGetSocket(IPAddress address)
{
    IPEndPoint ipe = new IPEndPoint(address, this._Port);
    Socket tc = null;

    try
    {
        tc = new Socket(ipe.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
        tc.Connect(ipe);
        if (tc.Connected == true)
        {
            tc.ReceiveTimeout = this.ReceiveTimeout;
            tc.SendBufferSize = this.SendBufferSize;
            tc.ReceiveBufferSize = this.ReceiveBufferSize;
        }
    }
    catch
    {
        tc = null;
    }
    return tc;
}
private IPHostEntry GetHostEntry()
{
    try
    {
        return Dns.GetHostEntry(this.ServerName);
    }
    catch { }
    return null;
}
これでSocketオブジェクトを利用してデータの送受信が可能になります。 SMTPでのコマンドの送信とサーバーからのレスポンスを以下の図に示します。
接続確立後のサーバーからのレスポンスは以下のようになります。
220 mx.google.com ESMTP pp8sm11319893pbb.21
SMTPでのレスポンスは1行の場合と複数行の場合があります。1行の場合のレスポンスのフォーマットは以下になります。
[responseCode][whitespace][message]
220の場合はServiceReadyという意味になります。 次にHELOコマンドを送信します。
helo xxx@xxx.com
リクエストのコマンドのフォーマットは以下になります。
[commandName][whitespace][message]
サーバーからは以下のレスポンスが送られてきます。
250-mx.google.com at your service, [61.197.223.240]
mx.google.com at your service, [61.197.223.240]
SIZE 35882577
8BITMIME
AUTH LOGIN PLAIN XOAUTH
ENHANCEDSTATUSCODES
複数行のレスポンスは以下になります。
[responseCode]-[message]
[message]
...
[message]
レスポンスのメッセージにはサポートしている認証の種類が含まれています。このメールサーバーはAUTH, LOGIN, PLAIN, XOAUTHの認証方式を サポートしていることがわかります。
認証処理
Plain認証
Plain認証の流れは以下のようになります。
Plain認証を行うためにはbase64でエンコードした文字列をサーバーに送信する必要があります。
String text = MailParser.ToBase64String(String.Format("{0}\0{0}\0{1}", this.UserName, this.Password));
Login認証
Login認証の流れは以下のようになります。
Plain認証と同様にbase64でエンコードした文字列をサーバーに送信する必要があります。
CRAM-MD5認証
CRAM-MD5認証の流れは以下のようになります。
ユーザー名・パスワードとサーバーから受信したチャレンジテキストをエンコードしてサーバーに送信する必要があります。
public static String ToCramMd5String(String challenge, String userName, String password)
{
    StringBuilder sb = new StringBuilder(256);
    Byte[] bb = null;
    HMACMD5 md5 = new HMACMD5(Encoding.ASCII.GetBytes(password));
    bb = md5.ComputeHash(Convert.FromBase64String(challenge));
    for (int i = 0; i < bb.Length; i++)
    {
        sb.Append(bb[i].ToString("x02"));
    }
    bb = Encoding.ASCII.GetBytes(String.Format("{0} {1}", userName, sb.ToString()));
    return Convert.ToBase64String(bb);
}
メールのデータの送信
認証が完了した後はメールのデータの送信処理を行います。まずはメールの送信元のデータをMail Fromコマンドで送ります。
mail from:xxx@xxx.com
サーバーから以下のようなレスポンスが送信されてきます。
250 2.1.0 OK mt9sm7913789pbb.14
次にRcpt Toコマンドを使用して送信先のメールアドレスのデータをサーバーへ送信します。
rcpt to:yyy@yyy.com
3つ送信先がある場合は3回コマンドを送信する必要があります。 次にメールの本文を送ります。メールの本文を送るにはDataコマンドをサーバーへ送信します。
Data
サーバーからメール本文を受信する準備が整ったという354のレスポンスが送信されてきます。
354 Go ahead mt9sm7913789pbb.14
続いてメールの本文を送信します。メールの本文の最後はピリオドだけの行で示されます。
Date: Fri, 25 May 2012 14:43:46 +0900
From: 
Subject: TheTestMail
Content-Transfer-Encoding: Base64
Content-Disposition: inline
X-Priority: 3
To: yyy@xxx.com
Content-Type: text/plain; charset="iso-2022-jp"

GyRCS1xKOCVGJTklSBsoQg==

.
サーバーはこのピリオドを見つけるとメールの本文の最後と判断します。 最後にQuitコマンドを送信してメール送信の処理を終了します。
通信の暗号化
SSLで通信する
SSLでメールサーバーと通信するにはSslStreamクラスを使用してサーバーと通信する必要があります。 下記のコードでSslStreamオブジェクトを使用してサーバーと通信するためのコードになります。
this.TcpSocket = this.GetSocket();
SslStream ssl = new SslStream(new NetworkStream(this.Socket), true, this.RemoteCertificateValidationCallback);
ssl.AuthenticateAsClient("myserver.com");
TLSで通信する
TLSを使用して通信をする場合の流れは以下のようになります。
StartTlsコマンドをサーバーに送信後にSslStreamオブジェクトを作成して通信を行うようにします。
private Boolean StartTls()
{
    SmtpCommandResult rs = null;

    if (this.EnsureOpen() == SmtpConnectionState.Connected)
    {
        rs = this.Execute("STARTTLS");
        if (rs.StatusCode != SmtpCommandResultCode.ServiceReady)
        { return false; }

        this.Ssl = true;
        this._Tls = true;
        SslStream ssl = new SslStream(new NetworkSream(this.Socket)
            , true, this.RemoteCertificateValidationCallback, null);
        ssl.AuthenticateAsClient(this.ServerName);
        this.Stream = ssl;
        return true;
    }
    return false;
}
C#での実装と使用例
HigLaboのライブラリでメールを送信するためにはSmtpClientクラスとSmtpMessageクラスを使用します。 SmtpClientクラスはMailClientクラスを継承しています。SmtpMessageクラスはInternetTextMessageクラスを継承しています。 以下にクラス図を示します。
SmtpClientクラスは低レベルレイヤーの機能として各コマンドをサーバーに送信するためのExecuteXXXメソッドを持っています。 また高レベルレイヤーの機能としてSendMailメソッドを提供しています。 メールをSSLで送信するためには以下のようになります。
using (var cl = new SmtpClient("smtp.gmail.com"))
{
    cl.Port = 465;
    cl.Ssl = true;
    cl.AuthenticateMode = SmtpAuthenticateMode.Auto;
    cl.UserName = "xxx@xxx.com";
    cl.Password = "???????";

    SmtpMessage mg = new SmtpMessage();
    mg.ContentEncoding = Encoding.GetEncoding("iso-8859-1");
    mg.ContentTransferEncoding = TransferEncoding.QuotedPrintable;
    mg.HeaderEncoding = Encoding.GetEncoding("iso-8859-1");
    mg.HeaderTransferEncoding = TransferEncoding.QuotedPrintable;
    mg.Date = DateTime.Now.ToUniversalTime();
    mg["Mime-Version"] = "1.0";
    mg.From = "xxx@xxx.com";
    mg.ReplyTo = "xxx1@xxx.com";
    mg.To.Add(new MailAddress("yyy@yyy.com"));
    mg.Subject = "Sample mail";
    mg.BodyText = "This is a sample mail!";
    SendMailResult rs = cl.SendMail(mg);
    if (rs.SendSuccessful == true)
    {
        //Do something. ex)show a message
    }
}
TLSを使用する場合は以下のようになります。
using (var cl = new SmtpClient("smtp.gmail.com"))
{
    cl.Port = 587;
    cl.Tls = true;
    //以下上記のコードと同じ
}
添付ファイル有りのメールを送信する場合は以下のようにSmtpContentクラスを使用します。
using (var cl = new SmtpClient("smtp.gmail.com"))
{
    //SmtpClientのプロパティをセット
    SmtpMessage mg = new SmtpMessage();
    //SmtpMessageのプロパティをセット

    //添付ファイルをCドライブから読み込む場合
    SmtpContent ct = new SmtpContent();
    ct.LoadFileData("C:\\MyPicture.png");
    //Html形式のテキストファイル
    //ct.LoadHtml("<html>....</html>");
    //普通のテキストファイルを添付する
    //ct.LoadText("This is a text file.");
    //バイトデータを直接添付する
    //ct.LoadData(new Byte[0]);
    mg.Contents.Add(ct);

    SendMailResult rs = cl.SendMail(mg);
    if (rs.SendSuccessful == true)
    {
        //何か処理。例)完了メッセージを表示など
    }
}
指定したパスのファイル、テキスト、HTML形式のテキスト、バイトデータを添付することが可能です。
SendMailメソッドの内部処理
SendMailメソッドは以下のようになってます。
public SendMailResult SendMail(String from, String to, String cc, String bcc, String text)
{
    List<MailAddress> l = new List<MailAddress>();
    String[] ss = null;

    ss = to.Split(',');
    for (int i = 0; i < ss.Length; i++)
    {
        if (String.IsNullOrEmpty(ss[i]) == true)
        { continue; }
        l.Add(MailAddress.Create(ss[i]));
    }
    ss = cc.Split(',');
    for (int i = 0; i < ss.Length; i++)
    {
        if (String.IsNullOrEmpty(ss[i]) == true)
        { continue; }
        l.Add(MailAddress.Create(ss[i]));
    }
    ss = bcc.Split(',');
    for (int i = 0; i < ss.Length; i++)
    {
        if (String.IsNullOrEmpty(ss[i]) == true)
        { continue; }
        l.Add(MailAddress.Create(ss[i]));
    }
    return this.SendMail(new SendMailCommand(from, text, l));
}
public SendMailResult SendMail(String from, SmtpMessage message)
{
    return this.SendMail(new SendMailCommand(from, message));
}
public SendMailResult SendMail(SmtpMessage message)
{
    return this.SendMail(new SendMailCommand(message));
}
public SendMailListResult SendMailList(IEnumerable<SmtpMessage> messages)
{
    List<SendMailCommand> l = new List<SendMailCommand>();
    foreach (var mg in messages)
    {
        l.Add(new SendMailCommand(mg));
    }
    return this.SendMailList(l.ToArray());
}
public SendMailResult SendMail(SendMailCommand command)
{
    var l = this.SendMailList(new[] { command });
    if (l.Results.Count == 1)
    {
        return new SendMailResult(l.Results[0].State, command);
    }
    return new SendMailResult(l.State, command);
}
public SendMailListResult SendMailList(IEnumerable<SendMailCommand> commandList)
{
    SmtpCommandResult rs = null;
    Boolean HasRcpt = false;

    if (this.EnsureOpen() == SmtpConnectionState.Disconnected)
    { return new SendMailListResult(SendMailResultState.Connection); }

    if (this.State != SmtpConnectionState.Connected &&
        this.State != SmtpConnectionState.Authenticated)
    {
        return new SendMailListResult(SendMailResultState.InvalidState);
    }
    if (this.State != SmtpConnectionState.Authenticated)
    {
        rs = this.ExecuteEhloAndHelo();
        if (rs.StatusCode != SmtpCommandResultCode.RequestedMailActionOkay_Completed)
        { return new SendMailListResult(SendMailResultState.Helo); }
        if (this._Tls == true)
        {
            if (this.StartTls() == false)
            { return new SendMailListResult(SendMailResultState.Tls); }
            rs = this.ExecuteEhloAndHelo();
            if (rs.StatusCode != SmtpCommandResultCode.RequestedMailActionOkay_Completed)
            { return new SendMailListResult(SendMailResultState.Helo); }
        }
        if (SmtpClient.NeedAuthenticate(rs.Message) == true)
        {
            if (this.Authenticate() == false)
            { return new SendMailListResult(SendMailResultState.Authenticate); }
        }
    }

    List<SendMailResult> results = new List<SendMailResult>();

    foreach (var command in commandList)
    {
        rs = this.ExecuteMail(command.From);
        if (rs.StatusCode != SmtpCommandResultCode.RequestedMailActionOkay_Completed)
        {
            results.Add(new SendMailResult(SendMailResultState.MailFrom, command));
            continue;
        }
        List<MailAddress> mailAddressList = new List<MailAddress>();
        foreach (var m in command.RcptTo)
        {
            String mailAddress = m.ToString();
            if (mailAddress.StartsWith("<") == true && mailAddress.EndsWith(">") == true)
            {
                rs = this.ExecuteRcpt(mailAddress);
            }
            else
            {
                rs = this.ExecuteRcpt("<" + mailAddress + ">");
            }
            if (rs.StatusCode == SmtpCommandResultCode.RequestedMailActionOkay_Completed)
            {
                HasRcpt = true;
            }
            else
            {
                mailAddressList.Add(m);
            }
        }
        if (HasRcpt == false)
        {
            results.Add(new SendMailResult(SendMailResultState.Rcpt, command, mailAddressList));
            continue;
        }
        rs = this.ExecuteData();
        if (rs.StatusCode == SmtpCommandResultCode.StartMailInput)
        {
            this.SendCommand(command.Text + MailParser.NewLine + ".");
            rs = this.GetResponse();
            if (rs.StatusCode == SmtpCommandResultCode.RequestedMailActionOkay_Completed)
            {
                results.Add(new SendMailResult(SendMailResultState.Success, command, mailAddressList));
                this.ExecuteRset();
            }
            else
            {
                results.Add(new SendMailResult(SendMailResultState.Data, command, mailAddressList));
            }
        }
        else
        {
            results.Add(new SendMailResult(SendMailResultState.Data, command, mailAddressList));
        }
    }
    rs = this.ExecuteQuit();
    if (results.Exists(el => el.State != SendMailResultState.Success) == true)
    {
        return new SendMailListResult(SendMailResultState.SendMailData, results);
    }
    return new SendMailListResult(SendMailResultState.Success, results);
}
SendMailメソッドには2つの機能が必要だと設計時に考えました。 順番にSendMailメソッドを見ていきます。SendMailメソッドの内部ではSendMailCommandオブジェクトが作成されています。
Fromプロパティは送信元のメールアドレスを取得または設定します。 RcptToプロパティは送信先のメールアドレスの一覧を管理しています。 Textプロパティは実際に送信されるメールのテキストデータを取得または設定します。 Textプロパティの値は例えば以下のようになります。
Date: Fri, 25 May 2012 14:43:46 +0900
From: <xxx@xxx.com>
Subject: TheTestMail
Content-Transfer-Encoding: Base64
Content-Disposition: inline
X-Priority: 3
To: yyy@xxx.com
Content-Type: text/plain; charset="iso-2022-jp"

GyRCS1xKOCVGJTklSBsoQg==

.
SendMailメソッドの内部を見ると実際のSMTPプロトコルの流れがそのまま実装されているのがわかると思います。
接続を開く、Helo、Tls(オプション)、認証(オプション)、MailFrom、RcptTo(複数回実行)、Data、Quit という順番で処理が実行されています。フロー図と全く同じ順番です。
SendMailメソッドはSendMailListResultオブジェクトを戻り値として返します。
ResultsプロパティはSendMailResultオブジェクトのリストになっています。
SendMailResultオブジェクトにはメール送信の結果を格納されています。SendSuccessfulプロパティはメールの送信処理全てが 完全に正常に行われた場合にtrueを返します。InvalidMailAddressListはRcptToコマンドで送信に失敗したメールアドレスの リストが含まれています。Stateプロパティにはメール送信のどのプロセスでエラーが起きたかを示す値が入っています。 Commandプロパティからは実際にメールの送信に使用したSendMailCommandオブジェクトを取得可能です。
Create at 2012/12/10 LastUpdate 2012/12/10