最近 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 の更新」にて更新しています。
「SMTP Over SSL 接続で配送依頼を行う DLL」への1件のフィードバック