了解 SMTP 邮件协议内部原理:第一部分
本文描述了使用 SMTP 邮件协议发送邮件的过程。
引言
任何拥有电脑或移动设备的人都使用过邮件。邮件系统是一个古老、传统且简单的协议。本文(第一部分)的目的是探索 SMTP 协议的内部原理,并向您展示如何在 C# 中实现它。
SMTP
最初,邮件有两个协议:SMTP 和 POP3。之后添加了 IMAP。本文描述了 SMTP。SMTP 是一个发送邮件的协议。基本流程如下:
- 打开连接
- Authenticate
- Hello
- Authenticate
- 从
- RcptTo
- Data
- 退出
打开连接
要从邮件客户端与邮件服务器通信,您必须初始化 `Socket` 对象。`SocketClient.cs` 位于 `HigLabo.Net` 项目中。
SocketClient.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
此处显示了单行响应格式。
[responseCode][whitespace][message]
220 表示 `ServiceReady`。
发送 hello 命令
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]
`Response` 消息包含支持的认证类型。此邮件服务器支持 `AUTH`、`LOGIN`、`PLAIN` 和 `XOAUTH`。
`SmtpCommandResult` 表示单行和多行响应文本。
Plain 认证
这是 plain 认证命令流程:
您必须发送 base64 编码的文本。
String text = MailParser.ToBase64String
(String.Format("{0}\0{0}\0{1}", this.UserName, this.Password));
Login 认证
这是 login 认证命令流程:
您必须发送 base64 编码的用户名和密码。
CRAM-MD5 认证
这是 CRAM-MD5 认证命令流程:
您必须发送一个使用用户名、密码以及来自服务器的挑战文本作为响应创建的文本。
MailParser.cs
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);
}
From、RcptTo、Data、Quit
认证后,您将像这样发送邮件命令:
mail from:xxx@xxx.com
您将收到这样的响应:
250 2.1.0 OK mt9sm7913789pbb.14
接下来,您将使用要发送邮件的邮件地址发送 `rcpt` 命令。
rcpt to:yyy@yyy.com
要发送给三个用户,您必须三次发送 `rcpt` 命令。为了准备发送邮件实体正文,请发送 data 命令。
Data
您将收到这样的响应:
354 Go ahead mt9sm7913789pbb.14
然后发送邮件实体数据。仅包含句点的行表示邮件实体的结束。
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==
.
作为最后一个过程,您将发送 `quit` 命令以完成发送邮件过程。然后关闭连接。
SSL
当您使用 SSL 时,必须使用 `SslStream` 类创建一个 `stream` 对象。以下是创建 `SslStream` 对象的代码:
this.TcpSocket = this.GetSocket();
SslStream ssl = new SslStream(new NetworkStream(this.Socket),
true, this.RemoteCertificateValidationCallback);
ssl.AuthenticateAsClient("myserver.com");
TLS
这是 SMTP over TLS 流程的图示。
在收到 `starttls` 命令的响应后,您必须创建一个 `SslStream` 对象并与之通信。
SmtpClient.cs
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;
...same as above code
}
发送带附件的邮件
using (var cl = new SmtpClient("smtp.gmail.com"))
{
//Set property of SmtpClient...
SmtpMessage mg = new SmtpMessage();
//Set property of SmtpMessage...
//Attachment file
SmtpContent ct = new SmtpContent();
ct.LoadFileData("C:\\MyPicture.png");
//Load Html format text as file
//ct.LoadHtml("<html>....</html>");
//Load text data as file
//ct.LoadText("This is a text file.");
//Load from byte data
//ct.LoadData(new Byte[0]);
mg.Contents.Add(ct);
SendMailResult rs = cl.SendMail(mg);
if (rs.SendSuccessful == true)
{
//Do something. ex)show a complete message
}
}
您可以通过从文件路径、文本、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);
}
`SmtpClient` 类和 `SendMail` 方法有两个要求:
- 使用一次打开的连接发送多封邮件
- 发送邮件命令的底层功能
`SendMailCommand` 对象在 `SendMail` 方法内部创建。
`From` 是一个包含发件人邮件地址的属性。`(xxx@xxx.com).RcptTo` 属性包含目标地址列表。`Text` 属性将返回由 `Data` 命令发送的原始文本数据。
`SendMailCommand` 对象的 `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==
.
您可以在 `SendMailList(IEnumerable<SendMailCommand> commandList)` 方法重载版本中看到代表 SMTP 协议的实际代码。
打开 `connection,Helo,Tls(optional),Authenticate(optional),MailFrom,RcptTo(multiple),Data,Quit`。与图示完全相同。
该方法将返回 `SendMailListResult` 对象。
`Results` 属性是 `SendMailResult` 对象列表。
`SendMailResult` 对象表示发送邮件过程的结果。`SendSuccessful` 属性显示该过程是否完全成功。`InvalidMailAddressList` 是 `RcptTo` 命令失败的地址列表。您可以使用 `State` 属性找到哪个过程失败了。您可以使用 `Command` 属性获取在 `SendMailList` 方法中执行的 `SendMailCommand` 对象。
历史
- 2012/05/31: 首次发布
- 2012/06/07: 修改了链接
- 2012/07/06: 修改了源代码和文章