SMTP Client on C#

ライブラリのSMTP送信機能を使わないでオリジナルでSMTPでメールを送るクラス。細かい設定が可能となる。


Cram-MD5にも対応しました。コードがぐちゃぐちゃだったので、一から書き直しました。ただ相変わらずDigestMD5の仕様が見つからない…マイナーらしいから良いか。

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;

using AuthEx = SMTP.AuthentificationException;
using AuthType = SMTP.AuthenticationType;
using MesEx = SMTP.MessageException;


namespace SMTP
{
    public class MessageException : Exception
    {
        public MessageException(string Message)
            : base(Message) { }
    }
    public class AuthentificationException : Exception
    {
        public AuthentificationException(string Type)
            : base(string.Format("{0} is invalid.", Type)) { }
    }

    public enum AuthenticationType { Plain, Login, CramMD5 }

    public class Settings
    {
        public class Server
        {
            public string Address;
            public int Port;
            public Server(string Address, int Port)
            {
                this.Address = Address;
                this.Port = Port;
            }
            public Server(string Address)
            {
                this.Address = Address;
                this.Port = 25;
            }
            public Server() { this.Port = 25; }
        }
        public Server server;

        public class Authentification
        {
            public string AuthType;
            public string User;
            public string Password;
            public Authentification(string AuthType, string User, string Password)
            {
                this.AuthType = AuthType;
                this.User = User;
                this.Password = Password;
            }
            public Authentification() { this.AuthType = null; }
        }
        public Authentification authentification;

        public class Message
        {
            public string From;
            public List<string> Recipients;
            string _Body = null;
            public string Body {
                get
                {
                    return this._Body;
                }
                set
                {
                    this._Body = value;
                    this._EML = null;
                }
            }
            string _EML = null;
            public string EML
            {
                get
                {
                    return this._EML;
                }
                set
                {
                    this._EML = value;
                    this._Body = null;
                }
            }
            public Message(string From, string Recipient, string Body,string EML)
            {
                this.From = From;
                this.Recipients = new List<string>();
                this.Recipients.Add(Recipient);
                this.Body = Body;
                this.EML = EML;
            }
            public Message() { }
        }
        public Message message;

        public Settings()
        {
            this.server = new Server();
            this.authentification = new Authentification();
            this.message = new Message();
        }
        public Settings(Server server, Authentification authentification, Message message)
        {
            this.server = server;
            this.authentification = authentification;
            this.message = message;
        }

        
    }
    

    public class Client : IDisposable
    {

        #region 変数&プロパティー
        TcpClient tcpClient;
        NetworkStream networkStream;
        StreamReader streamReader;

        string _FQDN = null;
        public string FQDN
        {
            get
            {
                if (_FQDN == null)
                    return Dns.GetHostName();
                else
                    return _FQDN;
            }
            set
            {
                _FQDN = value;
            }
        }

        public int ReadTimeout
        {
            get
            {
                return this.networkStream.ReadTimeout;
            }
            set
            {
                this.networkStream.ReadTimeout = value;
            }
        }

        string[] authenticationTypes;

        #endregion
        #region コンストラクタ
        public Client(string Server, int Port)
        {
            this.tcpClient = new TcpClient(Server, Port);
            this.networkStream = tcpClient.GetStream();
            this.streamReader = new StreamReader(networkStream);
            //tcpClient.Close();
            ReadToEnd("220");

            WriteLine("EHLO ", this.FQDN);
            foreach (string read in ReadToEnd("250"))
            {
                if (read.StartsWith("250-AUTH "))
                {
                    authenticationTypes = read.Substring(9).Split(' ', '\0');
                }
            }
        }
        public Client(string Server) : this(Server, 25) { }
        public static void SendBySettings(Settings settings)
        {
            Client client = new Client(settings.server.Address,settings.server.Port);
            if (settings.authentification != null)
                if (settings.authentification.AuthType != null)
                    client.Auth(settings.authentification.AuthType, settings.authentification.User, settings.authentification.Password);
            client.From(settings.message.From);
            foreach (string recipent in settings.message.Recipients)
            {
                client.Recipient(recipent);
            }
            if (settings.message.EML == null)
            {
                client.Send(settings.message.Body);
            }
            if (settings.message.Body == null)
            {
                client.SendFile(settings.message.EML);
            }
            else
            {
                throw new Exception("neither EML nor Body set...");
            }
            client.Quit();
        }
        #endregion

        #region 認証


        
        public void AuthPlain(string User, string Password)
        {
            AuthCheck("PLAIN");
            WriteLine("AUTH PLAIN ", EncodeBase64(User + '\0' + User + '\0' + Password));
            ReadToEnd("235");
        }
        public void AuthLogin(string User, string Password){

            AuthCheck("LOGIN");
            WriteLine("AUTH LOGIN");
            ReadToEnd("334");
            WriteLine(EncodeBase64(User));
            ReadToEnd("334");
            WriteLine(EncodeBase64(Password));
            ReadToEnd("235");
        }

        public void AuthCramMD5(string User, string Password)
        {
            AuthCheck("CRAM-MD5");
            WriteLine("AUTH CRAM-MD5");
            byte[] data = System.Convert.FromBase64String(ReadLine().Substring(4));
            byte[] key = Encoding.ASCII.GetBytes(Password);
            WriteLine(EncodeBase64(User + " " + HMAC_MD5(data, key)));
            ReadToEnd("235");
        }

        public void Auth(AuthType Type, string User, string Password)
        {
            switch (Type)
            {
                default:
                case AuthenticationType.Plain:
                    AuthPlain(User, Password);
                    break;
                case AuthenticationType.Login:
                    AuthLogin(User, Password);
                    break;
                case AuthenticationType.CramMD5:
                    AuthCramMD5(User, Password);
                    break;
            }
        }
        public void Auth(string Type, string User, string Password)
        {
            Type = Type.ToUpper();
            switch (Type)
            {
                case "PLAIN":
                    AuthPlain(User, Password);
                    break;
                case "LOGIN":
                    AuthLogin(User, Password);
                    break;
                case "CRAM-MD5":
                    AuthCramMD5(User, Password);
                    break;
            }
        }

        void AuthCheck(string Type)
        {
            foreach (string type in authenticationTypes)
            {
                if (type == Type)
                    return;
            } throw new AuthEx(Type);
        }

        #endregion
        #region 各種コマンド
        public void From(string from)
        {
            WriteLine("MAIL FROM:<", from, ">");
            ReadLine("250");
        }
        public void Recipient(string rcpt)
        {
            WriteLine("RCPT TO:<", rcpt, ">");
            ReadLine("250", "251");
        }
        #region Send
        public void Send(byte[] data, int offset, int size)
        {
            WriteLine("DATA");
            ReadToEnd("250", "354");
            Write(data, offset, size);
            WriteLine();
            WriteLine(".");
            ReadToEnd("250");
        }
        public void Send(byte[] data)
        {
            WriteLine("DATA");
            ReadToEnd("250", "354");
            WriteLine(data);
            WriteLine(".");
            ReadToEnd("250");
        }
        public void Send(string value, Encoding encoding)
        {
            Send(encoding.GetBytes(value));
        }
        public void Send(string value)
        {
            Send(Encoding.ASCII.GetBytes(value));
        }
        public void Send(Stream stream, int offset, int size)
        {
            WriteLine("DATA");
            ReadToEnd("250", "354");

            byte[] data = new byte[size];
            stream.Read(data, offset, size);
            WriteLine(data);

            WriteLine(".");
            ReadToEnd("250");
        }
        public void Send(Stream stream)
        {
            WriteLine("DATA");
            ReadToEnd("250", "354");

            while (true)
            {
                int read = stream.ReadByte();
                if (read == -1)
                    break;
                else
                    Write((byte)read);
            }
            WriteLine();
            WriteLine(".");
            ReadToEnd("250");
        }
        public void SendFile(string FileName)
        {
            FileStream fileStream = new FileStream(FileName, FileMode.Open);
            Send(fileStream);
        }
        #endregion
        public void Quit()
        {
            WriteLine("QUIT");
            ReadLine("221");
        }

        #endregion
        #region 応答の読み取り
        //使わないMethods
        //int ReadByte() { return streamReader.Read(); }
        //int Read(char[] buffer, int offset, int size) { return streamReader.Read(buffer, offset, size); }
        string ReadLine() { return streamReader.ReadLine(); }
        string ReadLine(params string[] codes)
        {
            string read = streamReader.ReadLine();
            foreach (string code in codes)
            {
                if (read.StartsWith(code))
                    return read;
            }
            throw new MesEx(read);
        }
        List<string> ReadToEnd(params string[] codes)
        {
            string read;
            List<string> messages = new List<string>();
            do
            {
                read = ReadLine(codes);
                messages.Add(read);

            } while (read[3] == '-');
            return messages;
        }
        #endregion
        #region コマンドの送信
        void Write(byte value) { networkStream.WriteByte(value); }
        void Write(byte[] buffer, int offset, int size) { networkStream.Write(buffer, offset, size); }
        void Write(params byte[] buffer) { networkStream.Write(buffer, 0, buffer.Length); }
        void Write(params byte[][] buffers)
        {
            foreach (byte[] buffer in buffers)
            {
                Write(buffer);
            }
        }
        void Write(string value)
        {
            byte[] bytearray = Encoding.ASCII.GetBytes(value);
            Write(bytearray);
        }
        void Write(params string[] values)
        {
            foreach (string value in values)
            {
                Write(value);
            }
        }
        void WriteLine(string value)
        {
            Write(value);
            Write(13, 10);
        }
        void WriteLine() { Write(13, 10); }
        void WriteLine(params string[] values)
        {
            foreach (string value in values)
            {
                Write(value);
            }
            WriteLine();
        }
        void WriteLine(params byte[] data)
        {
            Write(data);
            WriteLine();
        }
        void WriteLines(params string[] values)
        {
            foreach (string value in values)
            {
                WriteLine(value);
            }
        }

        #endregion
        #region 文字列の符号化

        static string EncodeBase64(string value)
        {
            return Convert.ToBase64String(Encoding.ASCII.GetBytes(value), Base64FormattingOptions.None);
        }

        static string HMAC_MD5(byte[] data, byte[] key)
        {
            HMACMD5 hmacmd5 = new HMACMD5(key);
            byte[] hmacData = hmacmd5.ComputeHash(data);

            StringBuilder result = new StringBuilder();
            foreach (byte b in hmacData)
            {
                result.Append(b.ToString("x2"));
            }

            return result.ToString();
        }
        #endregion

        #region IDisposable メンバ

        void IDisposable.Dispose()
        {

            this.tcpClient.Close();
            this.networkStream.Dispose();
        }

        #endregion
    }
}


使い方。いろいろ実装したので、かくのがめんどくさいw 一例だけ。この方法だと、インテリセンスが補完してくれてやりやすい。

            Client.SendBySettings(new Settings(
                new Settings.Server("smtpserver.com"),
                new Settings.Authentification("Plain", "user", "pass"),
                new Settings.Message("from@msn.com", "to@gmail.com", null, @"f:\test.bin")
                )
            );

関連