SMTP Over SSL 接続で配送依頼を行う DLL

最近 ASP.NET MVC を弄っているんですけど、System.Net.Mail.SmtpClient クラスの実装が、SSL を有効にすると SMTP AUTH Plain の認証ができないんですよね。メインで使っている SMTP サーバーが SMTP Over SSL 接続を要求しているので、ちょっと不便(もう一つ使える SMTP サーバーは SMTP Over SSL  を要求しないからテストはできるんだけど)。

それで、ちょっと作ってみました。機能は次のようなもので。

  • SMTP AUTH をサポート。対応する認証方式は次のとおり。
    • Plain
    • Login
    • CRAM-MD5
  • SSL 接続のサポート(SSL を利用しない接続もOK)。
  • From, To の表示名及び Subject に非 ASCII 文字がある場合には Base64 エンコーディングを行う。
  • 指定できる送信先は1件のみ(ASP.NET MVC などサーバーシステムを弄るときの利用を想定しているため)。
  • ファイルの添付はサポートしない(理由は同上)。

DIGEST-MD5 については、使える SMTP サーバーに対応しているものがないので、実装していません 😛

Debug.WriteLine() でサーバーとの通信内容を出しているので、デバッグ実行させると普段見えないサーバーとの会話が見れて面白いかも 🙂

DLL  とソリューションファイル等一式は MAKCRAFT のほうに置いてみました。

ここではプログラムソースだけ見たいという方のためにプログラムを掲載しておきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Text;
using System.Net.Sockets;
using System.Net.Security;
using System.Diagnostics;

namespace MakCraft.SmtpOverSsl
{
    /// <summary>
    /// 送信用のメールメッセージ
    /// </summary>
    public class SmtpMailMessage
    {
        /// <summary>
        /// 送信者のメールアドレス
        /// </summary>
        public System.Net.Mail.MailAddress From { get; set; }

        /// <summary>
        /// 送信先のメールアドレス
        /// </summary>
        public System.Net.Mail.MailAddress To { get; set; }

        /// <summary>
        /// メールの件名
        /// </summary>
        public string Subject { get; set; }

        /// <summary>
        /// メールの本文
        /// </summary>
        public string Body { get; set; }
    }

    public class Smtp
    {
        /// <summary>
        /// SMTP サーバーの名前
        /// </summary>
        public string ServerName { get; set; }

        /// <summary>
        /// SMTP サーバーの受付ポート番号
        /// </summary>
        public int ServerPort { get; set; }

        /// <summary>
        /// 本文のエンコーディングに使用する IANA に登録されている名前
        /// </summary>
        public string encoding { get; set; }

        /// <summary>
        /// 認証で用いるユーザー名
        /// </summary>
        public string AuthUserName { get; set; }

        /// <summary>
        /// 認証で用いるパスワード
        /// </summary>
        public string AuthPassword { get; set; }

        /// <summary>
        /// SMTP の認証方法
        /// </summary>
        public enum SmtpAuthMethod
        {
            None,
            Plain,
            Login,
            CRAM_MD5,
        }

        /// <summary>
        /// 認証方法
        /// </summary>
        public SmtpAuthMethod AuthMethod { get; set; }

        /// <summary>
        /// SMTP サーバーとの接続に SSL を使用するかどうかを指定
        /// </summary>
        public bool EnableSsl { get; set; }

        /// <summary>
        /// メール送信時のエラー情報
        /// </summary>
        public string ErrorMessage { get; private set; }

        // バッファ長
        private const int _bufferLen = 1024;
        // サーバー応答のメッセージ部分の開始位置
        private const int _messageStartPos = 4;

        /// <summary>
        /// メールを送信する
        /// </summary>
        /// <exception cref="InvalidOperationException">
        /// Thrown when... プロパティに必須項目がセットされていないとき
        /// </exception>
        /// <exception cref="SocketException">
        /// Thrown when... サーバーとのコネクションが確立できなかったとき
        /// </exception>
        /// <exception cref="AuthenticationException">
        /// Thrown when... SSL 接続が確立できなかったとき
        /// </exception>
        /// <param name="mail">送信するメール</param>
        public bool Send(SmtpMailMessage mail)
        {
            ErrorMessage = "";

            if (ServerName == "")
                throw new InvalidOperationException("ServerName is empty.");
            if (AuthUserName == "")
                throw new InvalidOperationException("UserName is empty.");
            if (AuthPassword == "")
                throw new InvalidOperationException("Password is empty.");
            if (Encoding.GetEncodings().Where(c =>  c.Name.ToUpper() == encoding.ToUpper()).Count() == 0)
                throw new InvalidOperationException("Encoding name not found(encoding: " + encoding + ")");

            // サーバー接続
            Debug.WriteLine("Connecting...");
            using (var sock = new TcpClient())
            {
                try
                {
                    sock.Connect(ServerName, ServerPort);
                    Debug.WriteLine("Connected!");
                    Debug.WriteLine("Socket stream open!");
                    using (var stream = sock.GetStream())
                    {
                        try
                        {
                            if (EnableSsl)
                                // SMTP over SSL
                                sslConnect(ServerName, stream, mail);
                            else
                                // SMTP
                                smtpConnection(stream, mail);
                        }
                        finally
                        {
                            Debug.WriteLine("Socket stream close!");
                            stream.Close();
                        }
                    }
                }
                catch (System.Net.Sockets.SocketException e)
                {
                    Debug.WriteLine("Socket error! code: " + e.ErrorCode.ToString() + "; " + e.Message);
                    throw;
                }
                catch (ApplicationException e)
                {
                    ErrorMessage = e.Message;
                    return false;
                }
                finally
                {
                    // サーバー接続のクローズ
                    Debug.WriteLine("Connection close!");
                    sock.Close();
                }

                return true;
            }
        }

        /// <summary>
        /// SSL 接続を確立する
        /// </summary>
        /// <param name="serverName"></param>
        /// <param name="sock"></param>
        private void sslConnect(string serverName,
            System.IO.Stream sockStream, SmtpMailMessage mail)
        {
            using (var stream = new SslStream(sockStream))
            {
                try
                {
                    Debug.WriteLine("SSL authenticating...");
                    stream.AuthenticateAsClient(serverName);
                    Debug.WriteLine("SSL authenticated!");
                    smtpConnection(stream, mail);
                }
                catch (System.Security.Authentication.AuthenticationException e)
                {
                    Debug.WriteLine("Authentication error: " + e.Message);
                    if (e.InnerException != null)
                    {
                        Debug.WriteLine("Inner exception: " + e.InnerException.Message);
                    }
                    throw;
                }
                finally
                {
                    Debug.WriteLine("SSL stream close!");
                    stream.Close();
                }
            }
        }

        // SMTP 接続
        private void smtpConnection(System.IO.Stream stream, SmtpMailMessage mail)
        {
            string rstr;
            rstr = receiveData(stream);
            Debug.WriteLine(rstr);
            if (!rstr.StartsWith("220"))
                throw new ApplicationException("Server Not Ready");

            sendData(stream, "EHLO " + System.Net.Dns.GetHostName() + "\r\n");
            Debug.WriteLine("[S]EHLO " + System.Net.Dns.GetHostName());
            rstr = receiveData(stream);
            Debug.WriteLine(rstr);
            if (!rstr.StartsWith("250"))
                throw new ApplicationException(rstr);

            // 認証
            if (AuthMethod != SmtpAuthMethod.None)
                authenticate(stream);

            sendData(stream, "MAIL FROM:" + mail.From.Address + "\r\n");
            Debug.WriteLine("[C]MAIL FROM:" + mail.From.Address);
            rstr = receiveData(stream);
            Debug.WriteLine(rstr);
            if (!rstr.StartsWith("250"))
                throw new ApplicationException(rstr);

            sendData(stream, "RCPT TO:" + mail.To.Address + "\r\n");
            Debug.WriteLine("[C]RCPT TO:" + mail.To.Address);
            rstr = receiveData(stream);
            Debug.WriteLine(rstr);
            if (!rstr.StartsWith("250"))
                throw new ApplicationException(rstr);

            sendData(stream, "DATA\r\n");
            Debug.WriteLine("[C]DATA");
            rstr = receiveData(stream);
            Debug.WriteLine(rstr);
            if (!rstr.StartsWith("354"))
                throw new ApplicationException(rstr);

            // メールデータ
            string data = "";
            data += "From: " + encodeDisplayAddress(mail.From) + "\r\n";
            data += "To: " + encodeDisplayAddress(mail.To) + "\r\n";
            if (mail.Subject != "" && !Regex.IsMatch(mail.Subject, @"^\p{IsBasicLatin}+$"))
            {   // Subject に非 ASCII 文字が含まれていたらエンコードする
                data += "Subject: " + string.Format("=?{0}?B?{1}?=\r\n",
                    encoding, getBase64String(mail.Subject, encoding));
            }
            else
                data += "Subject: " + mail.Subject + "\r\n";
            var date = DateTime.Now.ToString(@"ddd, dd MMM yyyy HH\:mm\:ss",
                System.Globalization.CultureInfo.CreateSpecificCulture("en-US"));
            var timeZone = DateTimeOffset.Now.ToString("%K").Replace(":", "");
            data += string.Format("Date: {0} {1}\r\n", date, timeZone);
            data += string.Format("Message-ID: <{0}@{1}>\r\n", Guid.NewGuid(), mail.From.Host);
            data += "MIME-Version: 1.0\r\n";
            data += "Content-Transfer-Encoding: 7bit\r\n";
            data += "Content-Type: text/plain; charset=" + encoding + "\r\n";
            data += "\r\n" +
                // see RFC 5321 4.5.2 Transparency
                Regex.Replace(mail.Body, @"^\.", "..", RegexOptions.Multiline) + "\r\n";
            data += ".\r\n";
            sendData(stream, data);
            Debug.WriteLine("[C]mail DATA:\r\n" + data);
            rstr = receiveData(stream);
            Debug.WriteLine(rstr);
            if (!rstr.StartsWith("250"))
                throw new ApplicationException(rstr);

            sendData(stream, "QUIT\r\n");
            Debug.WriteLine("[C]QUIT");
            rstr = receiveData(stream);
            Debug.WriteLine(rstr);
            if (!rstr.StartsWith("221"))
                throw new ApplicationException(rstr);
        }

        // データを受信
        private string receiveData(System.IO.Stream stream)
        {
            var enc = Encoding.GetEncoding(encoding);
            var data = new byte[_bufferLen];
            int len;
            var str = "";
            using (var memStream = new System.IO.MemoryStream())
            {
                try
                {
                    // すべて受信する
                    do
                    {
                        // 届いているものを受信
                        len = stream.Read(data, 0, data.Length);
                        memStream.Write(data, 0, len);
                        // デコード
                        str = enc.GetString(memStream.ToArray());
                    }
                    while (!str.EndsWith("\r\n"));
                }
                finally
                {
                    memStream.Close();
                }
            }

            return str;
        }

        // データを送信
        private void sendData(System.IO.Stream stream, string str)
        {
            var enc = Encoding.GetEncoding(encoding);

            // byte 型配列に変換
            var data = enc.GetBytes(str);
            stream.Write(data, 0, data.Length);
        }

        // 認証
        private void authenticate(System.IO.Stream stream)
        {
            string rstr;

            switch (AuthMethod)
            {
                case SmtpAuthMethod.Plain:
                    sendData(stream, "AUTH PLAIN\r\n");
                    Debug.WriteLine("[S]AUTH PLAIN");
                    rstr = receiveData(stream);
                    Debug.WriteLine(rstr);
                    if (!rstr.StartsWith("334"))
                    {
                        if (rstr.StartsWith("502"))
                        {   // 502:Command not implemented; SMTP AUTH をサポートしていない
                            Debug.WriteLine(rstr);
                            return;
                        }
                        // 504:Command parameter not implemented; AUTH PLAIN をサポートしていない
                        // サーバーからそれなりのエラーメッセージが期待できるので、そのままメッセージとして活用する
                        throw new ApplicationException(rstr);
                    }

                    string str = AuthUserName + '' + AuthUserName + '' + AuthPassword;
                    sendData(stream, getBase64String(str) + "\r\n");
                    Debug.WriteLine("[C]base64: " + getBase64String(str));
                    rstr = receiveData(stream);
                    Debug.WriteLine(rstr);
                    if (!rstr.StartsWith("235"))
                        throw new ApplicationException(rstr);
                    break;

                case SmtpAuthMethod.Login:
                    sendData(stream, "AUTH LOGIN\r\n");
                    Debug.WriteLine("[C]AUTH LOGIN");
                    rstr = receiveData(stream);
                    Debug.WriteLine(rstr);
                    if (!rstr.StartsWith("334"))
                    {
                        if (rstr.StartsWith("502"))
                        {   // 502:Command not implemented; SMTP AUTH をサポートしていない
                            Debug.WriteLine(rstr);
                            return;
                        }
                        // 504:Command parameter not implemented; AUTH LOGIN をサポートしていない
                        throw new ApplicationException(rstr);
                    }

                    sendData(stream, getBase64String(AuthUserName) + "\r\n");
                    Debug.WriteLine("[C]" + AuthUserName + "; base64: " + getBase64String(AuthUserName));
                    rstr = receiveData(stream);
                    Debug.WriteLine(rstr);
                    if (!rstr.StartsWith("334"))
                        throw new ApplicationException(rstr);

                    sendData(stream, getBase64String(AuthPassword) + "\r\n");
                    Debug.WriteLine("[C]" + AuthPassword + "; base64: " + getBase64String(AuthPassword));
                    rstr = receiveData(stream);
                    Debug.WriteLine(rstr);
                    if (!rstr.StartsWith("235"))
                        throw new ApplicationException(rstr);
                    break;

                case SmtpAuthMethod.CRAM_MD5:
                    sendData(stream, "AUTH CRAM-MD5\r\n");
                    Debug.WriteLine("[C]AUTH CRAM-MD5");
                    rstr = receiveData(stream);
                    Debug.WriteLine(rstr);
                    if (!rstr.StartsWith("334"))
                    {
                        if (rstr.StartsWith("502"))
                        {   // 502:Command not implemented; SMTP AUTH をサポートしていない
                            Debug.WriteLine(rstr);
                            return;
                        }
                        // 504:Command parameter not implemented; AUTH CRAM-MD5 をサポートしていない
                        throw new ApplicationException(rstr);
                    }
                    var key = (new System.Security.Cryptography.HMACMD5(Encoding.UTF8.GetBytes(AuthPassword)))
                        .ComputeHash(System.Convert.FromBase64String(rstr.Substring(_messageStartPos)));
                    var digest = "";
                    foreach (var octet in key)
                    {
                        digest += octet.ToString("x02");
                    }
                    var response = getBase64String(string.Format("{0} {1}", AuthUserName, digest));
                    sendData(stream, response + "\r\n");
                    Debug.WriteLine("[C]base64: " + response);
                    rstr = receiveData(stream);
                    Debug.WriteLine(rstr);
                    if (!rstr.StartsWith("235"))
                        throw new ApplicationException(rstr);
                    break;
            }
        }

        // 指定された文字コードでエンコードし、さらに Base64 でエンコード
        private string getBase64String(string str, string charSetName = "utf-8")
        {
            var enc = Encoding.GetEncoding(charSetName);
            return Convert.ToBase64String(enc.GetBytes(str));
        }

        // メールアドレスの表記名部分が非 ASCII なときにはエンコード
        private System.Net.Mail.MailAddress encodeDisplayAddress(System.Net.Mail.MailAddress email)
        {
            if (email.DisplayName != "" && !Regex.IsMatch(email.DisplayName, @"^\p{IsBasicLatin}+$"))
            {
                var enc = Encoding.GetEncoding(encoding);
                var dispName = string.Format("=?{0}?B?{1}?=", encoding,
                    getBase64String(email.DisplayName, encoding));
                var mail = new System.Net.Mail.MailAddress(email.Address, dispName);
                return mail;
            }
            return email;
        }
    }
}

このプログラムの作成に、次のサイトの情報を活用させていただきました。

このプログラムは「SMTP Over SSL 接続で配送依頼を行う DLL の更新」にて更新しています。


1 thought on “SMTP Over SSL 接続で配送依頼を行う DLL

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です