65.9K
CodeProject 正在变化。 阅读更多。
Home

了解 SMTP 邮件协议内部原理:第一部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (50投票s)

2012 年 6 月 6 日

MIT

4分钟阅读

viewsIcon

149360

downloadIcon

3612

本文描述了使用 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` 方法有两个要求:

  1. 使用一次打开的连接发送多封邮件
  2. 发送邮件命令的底层功能

`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: 修改了源代码和文章
© . All rights reserved.