C# で SMTP サーバーに接続したい( SSL 接続、SMTP 認証編)!!!

投稿者: | 2018年4月23日

前回の記事で C# を使用して SMTP サーバーに接続する方法をまとめました。今回は、SMTP 認証と SSL 接続に挑戦したいと思います。

目次

やりたいこと

SMTP サーバーに SSL 接続し、SMTP 認証を行うことを目指します。今回は勉強のために、SMTP コマンドを使用してサーバーと送受信を行うところから作成します。複数メールの送信、複数宛への送信、CC、BCC については今回も実装はしません。

事前準備

1.前回の記事で作成したコードを用意する
前回の記事で作成したコードを今回は使用します。導入がまだでしたら「C# で SMTP サーバーに接続したい(入門編)!!!」を参照してください。

2.SMTP サーバーの認証情報を用意する
SMTP 認証接続したい SMTP サーバーの下記情報を新たに用意してください。
・認証ユーザー名
・認証パスワード
・認証の種類(Plain, Login, CRAM-MD5)

手順概要

手順1.SMTP サーバーへ SMTP 認証をする
手順2.SMTP サーバーに SSL 接続をする
手順3.SMTP サーバーに SSL 接続と SMTP 認証を使ってメールを送信する

手順1.SMTP サーバーへ SMTP 認証をする

SMTP 認証をするクラス を実装する

新たに SMTP 認証をする Auth クラスを実装します。

public class Auth
{
    /// <summary>
    /// 認証方法
    /// </summary>
    public enum Type
    {
        /// <summary>
        /// 認証なし
        /// </summary>
        None,

        /// <summary>
        /// Plain 認証
        /// </summary>
        Plain,

        /// <summary>
        /// Login 認証
        /// </summary>
        Login,

        /// <summary>
        /// CRAM-MD5 認証
        /// </summary>
        CRAM_MD5,
    }

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

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

    /// <summary>
    /// 認証方法
    /// </summary>
    public Type type { get; set; }

    /// <summary>
    /// サーバー応答メッセージの開始位置
    /// </summary>
    const int _messageStartPos = 4;

    // 認証
    protected void Authenticate(Message message)
    {

        if (type != Type.None)
        {
            if (userName == "")
                throw new InvalidOperationException("UserName is empty.");
            if (password == "")
                throw new InvalidOperationException("Password is empty.");
        }

        string rstr = "";
        string sstr = "";

        switch (type)
        {
            case Type.Plain:

                sstr = "AUTH PLAIN";
                message.SendReceive(sstr, new List<UInt16> { 334, 502 }, false, out rstr, "[S]");

                // 502:Command not implemented
                if (rstr.StartsWith("502")) return;

                sstr = Common.GetBase64(userName + '\0' + userName + '\0' + password);
                message.SendReceive(sstr, new List<UInt16> { 235 }, false, out rstr, "[C]base64");


                break;

            case Type.Login:

                sstr = "AUTH LOGIN";
                message.SendReceive(sstr, new List<UInt16> { 334, 502 }, false, out rstr, "[C]");

                // 502:Command not implemented
                if (rstr.StartsWith("502")) return;

                sstr = Common.GetBase64(userName);
                message.SendReceive(sstr, new List<UInt16> { 334 }, false, out rstr, "[C]" + userName + "; base64: ");

                sstr = Common.GetBase64(password);
                message.SendReceive(sstr, new List<UInt16> { 235 }, false, out rstr, "[C]" + password + "; base64: ");

                break;

            case Type.CRAM_MD5:

                sstr = "AUTH CRAM-MD5";
                message.SendReceive(sstr, new List<UInt16> { 334, 502 }, false, out rstr, "[C]");

                // 502:Command not implemented
                if (rstr.StartsWith("502")) return;

                // AUTH CRAM-MD5
                sstr = Common.GetBase64(GetMD5(rstr.Substring(_messageStartPos), userName, password));
                message.SendReceive(sstr, new List<UInt16> { 235 }, false, out rstr, "[C]base64: ");

                break;

            case Type.None:
            default:
                break;
        }
    }

    // MD5 値取得
    private string GetMD5(string challenge, string userName, string password)
    {
        var key = (new System.Security.Cryptography.HMACMD5(Encoding.UTF8.GetBytes(password)))
                    .ComputeHash(System.Convert.FromBase64String(challenge));
        var digest = "";
        foreach (var octet in key)
        {
            digest += octet.ToString("x02");
        }

        return string.Format("{0} {1}", userName, digest);
    }
}

前回の記事で実装した Smtp クラスを変更し、Auth クラスを継承させます。

/// <summary>
/// SMTP サーバーとの交信を行う
/// </summary>
public class Smtp : Auth
{
    /// <summary>
    /// SMTP サーバーの名前
    /// </summary>
    public string serverName { get; set; }

    ・・・以下省略・・・
}

前回の記事で実装した SMTPConnect 関数を変更し、SMTP 認証を実装します。

// SMTP 接続
private void SMTPConnect(System.IO.Stream stream, Mail mail)
{
    string rstr = "";
    string sstr = "";

    Message message = new Message(stream, encode);

    // SMTP 認証
    Authenticate(message);

    try
    {
        sstr = "MAIL FROM:<" + mail.From.Address + ">";
        message.SendReceive(sstr, new List<UInt16> { 250 }, false, out rstr, "[C]");
    }
    catch (ApplicationException e)
    {
        sstr = "MAIL FROM:" + mail.From.Address;
        message.SendReceive(sstr, new List<UInt16> { 250 }, false, out rstr, "[C]");
    }

    try
    {
        sstr = "RCPT TO:<" + mail.To.Address + ">";
        message.SendReceive(sstr, new List<UInt16> { 250 }, false, out rstr, "[C]");
    }
    catch (ApplicationException e)
    {
        sstr = "RCPT TO:" + mail.To.Address;
        message.SendReceive(sstr, new List<UInt16> { 250 }, false, out rstr, "[C]");
    }
    
    sstr = "DATA";
    message.SendReceive(sstr, new List { 354 }, false, out rstr, "[C]");
    
    // メールデータ作成
    sstr = message.Create(mail).ToString();
    message.SendReceive(sstr, new List { 250 }, false, out rstr, "[C]mail DATA:\r\n");
    
    sstr = "QUIT";
    message.SendReceive(sstr, new List { 221 }, false, out rstr, "[C]");
}

手順2.SMTP サーバーに SSL 接続をする

SSL 接続をするクラスを実装する

手順1で実装した Smtp クラスを変更し SSL 接続を実装します。
SSLConnect 関数を新たに作成し enableSSL を追加します。Send 関数で enableSSL の値によって SMTPConnect と SSLConnect を切り替えるようにします。SMTPConnect 関数の変更はありません。

/// <summary>
/// SMTP サーバーとの交信を行う
/// </summary>
public class Smtp : Auth
{
    /// <summary>
    /// SMTP サーバーの名前
    /// </summary>
    public string serverName { get; set; }

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

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

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

    /// <summary>
    /// 受信タイムアウト
    /// </summary>
    public int readTimeout { get; set; }

    /// <summary>
    /// メールを送信する
    /// </summary>
    /// <exception cref="InvalidOperationException">
    /// Thrown when... プロパティ不足
    /// </exception>
    /// <exception cref="SocketException">
    /// Thrown when... SMTPサーバー接続失敗
    /// </exception>
    /// <exception cref="System.Security.Authentication.AuthenticationException">
    /// Thrown when... SSL 接続失敗
    /// </exception>
    /// <exception cref="ApplicationException">
    /// Thrown when... SMTPサーバー通信エラー
    /// </exception>
    /// <param name="mail">送信するメール</param>
    /// <returns></returns>
    public void Send(Mail mail)
    {
        if (serverName == "")
            throw new InvalidOperationException("ServerName is empty.");
        if (Encoding.GetEncodings().Where(c =>  c.Name.ToUpper() == encode.ToUpper()).Count() == 0)
            throw new InvalidOperationException("Encoding name not found(encoding: " + encode + ")");

        // サーバー接続
        Debug.WriteLine("Connecting...");
        using (var sock = new TcpClient())
        {
            try
            {
                sock.Connect(serverName, serverPort);
                Debug.WriteLine("Connected!");
                Debug.WriteLine("Socket open!");
                using (var stream = sock.GetStream())
                {
                    stream.ReadTimeout = readTimeout;

                    try
                    {
                        string rstr = "";
                        string sstr = "";

                        Message message = new Message(stream, encode);

                        if (!message.Receive(new List<UInt16> { 220 }, false, out rstr))
                            throw new ApplicationException("Server Not Ready");

                        sstr = "EHLO " + System.Net.Dns.GetHostName();
                        message.SendReceive(sstr, new List<UInt16> { 250 }, true, out rstr, "[S]"); //無応答となるまで受信し続ける

                        if (enableSSL)
                        {
                            sstr = "STARTTLS";
                            message.SendReceive(sstr, new List<UInt16> { 220 }, false, out rstr, "[S]");

                            // SMTP over SSL
                            SSLConnect(serverName, stream, mail);
                        }
                        else
                        {
                            // SMTP
                            SMTPConnect(stream, mail);
                        }
                    }
                    finally
                    {
                        Debug.WriteLine("Socket close!");
                        stream.Close();
                    }
                }
            }
            catch (System.Net.Sockets.SocketException e)
            {
                Debug.WriteLine("Socket error! code: " + e.ErrorCode.ToString() + "; " + e.Message);
                throw;
            }
            finally
            {
                // サーバー接続のクローズ
                Debug.WriteLine("Connection close!");
                sock.Close();
            }
        }
    }

    // SSL 接続を確立する
    private void SSLConnect(string serverName,
        System.IO.Stream sockStream, Mail mail)
    {
        using (var stream = new SslStream(sockStream))
        {
            try
            {
                Debug.WriteLine("SSL authenticating...");
                stream.AuthenticateAsClient(serverName);
                Debug.WriteLine("SSL authenticated!");
                SMTPConnect(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 close!");
                stream.Close();
            }
        }
    }

    // SMTP 接続
    private void SMTPConnect(System.IO.Stream stream, Mail mail)
    {
        string rstr = "";
        string sstr = "";

        Message message = new Message(stream, encode);

        // SMTP 認証
        Authenticate(message);

        try
        {
            sstr = "MAIL FROM:<" + mail.From.Address + ">";
            message.SendReceive(sstr, new List<UInt16> { 250 }, false, out rstr, "[C]");
        }
        catch (ApplicationException e)
        {
            sstr = "MAIL FROM:" + mail.From.Address;
            message.SendReceive(sstr, new List<UInt16> { 250 }, false, out rstr, "[C]");
        }

        try
        {
            sstr = "RCPT TO:<" + mail.To.Address + ">";
            message.SendReceive(sstr, new List<UInt16> { 250 }, false, out rstr, "[C]");
        }
        catch (ApplicationException e)
        {
            sstr = "RCPT TO:" + mail.To.Address;
            message.SendReceive(sstr, new List<UInt16> { 250 }, false, out rstr, "[C]");
        }
        
        sstr = "DATA";
        message.SendReceive(sstr, new List { 354 }, false, out rstr, "[C]");
        
        // メールデータ作成
        sstr = message.Create(mail).ToString();
        message.SendReceive(sstr, new List { 250 }, false, out rstr, "[C]mail DATA:\r\n");
        
        sstr = "QUIT";
        message.SendReceive(sstr, new List { 221 }, false, out rstr, "[C]");
    }
}

手順3.SMTP サーバーに SSL 接続と SMTP 認証を使ってメールを送信する

SMTP サーバーへメールを送信してみましょう

前回の記事の手順2で作成した smtp を変更しましょう。事前準備で用意した、認証ユーザー名、認証パスワード、認証の種類(Plain, Login, CRAM-MD5)を使用します。

var smtp = new Smtp
{
    serverName = "SMTP サーバー名",
    serverPort = 25,
    encode = "ISO-2022-JP",
    readTimeout = 500,
    
    /*----------ここから下を追加-----------*/
    enableSSL = true,
    userName = "認証ユーザー名",
    password = "認証パスワード",
    type = Auth.Type.Plain //認証の種類(Plain, Login, CRAM-MD5)
};

まとめ

C#で SMTP サーバーに対して、SSL 接続と SMTP 認証を使用して、メールを送信する関数とクラスを作成してみました。是非お試しください。

関連するページ

その他の備忘録も以下のページでまとめていますので、よろしければアクセスしてください。

以上!!

コメントを残す

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