我们都知道在平常的桌面应用中, 如果要发邮件的话, 肯定首选SmtpClient 很简单就能发送邮件了, 但是在UWP中, 是无法使用System.Net.Email 这个namespace的, 所以我们要实现全自动化发送Email的话, 就得手动去模拟了.
注1:本文来源: https://blogs.msdn.microsoft.com/mim/2013/11/29/sending-an-email-within-a-windows-8-1-application-using-streamsocket-to-emulate-a-smtpclient/
注2: 在非特殊情况下, 不建议使用此方法, 可使用官方推荐的通过调用系统自带邮件APP来发邮件, 详情戳这里>>> 或者看这里: http://lindexi.oschina.io/lindexi/post/win10-UWP-%E5%8F%91%E9%82%AE%E4%BB%B6/
在此之前, 我们需要解决以下几个问题:
- Smtp协议是个什么样的, Smtp服务器又是怎么工作的?
- 怎么发送和接收来自服务器端的消息和数据?
Smtp协议
可以参考以下几个关于SMTP协议的链接:
- 维基百科: https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol
- 使用Telnet发邮件: http://thedaneshproject.com/posts/send-mail-through-smtp-using-telnet/
- SMTP命令参考: http://www.samlogic.net/articles/smtp-commands-reference.htm
接下来, 我们得了解客户端和服务器端是怎么交流的了

注1: 在Client发送完密码, Server通过验证后, 返回的代码应该是235 表示验证成功. 上图有误(实际中, 也不完全是这样的)
注2: 在编程的时候, 需要对用户名和密码必须通过base 64加密后再发送给Server, 不能使用明文发送!
在此次连接中用到的命令解释如下:
- Ehlo : Send a command to the smtp server to know some properties like authentication mode, SSL, TSL support etc …
- Auth Login : Send a command requesting authentication on the smtp server.
- StartTls : Send a command requesting a TSL communication.
- Mail From : Send a command with the mail author.
- Rcpt To : Send a command with one or more receivers.
- Data : Send a command for the body mail.
- Quit : Send a command ending the connection with the smtp server.
此次连接中用到的Code解释如下:
- 220 : Service Ready.
- 250 : Request completed.
- 334 : Waiting for Authentication.
- 235 : Authentication successful.
- 354 : Start mail input.
- 221 : Closing connection.
以下是一个Client和Server交流的实例(smtp-mail.outlook.com)
Connect smtp-mail.outlook.com 587
S : 220 BLU0-SMTP180.phx.gbl Microsoft ESMTP MAIL Service, Version: 6.0.3790.4675 ready at Wed, 27 Nov 2013 08:28:59 -0800
C : EHLO www.contoso.com
S : 250-BLU0-SMTP180.phx.gbl Hello [94.245.87.37]
250-TURN
250-SIZE 41943040
250-ETRN
250-PIPELINING
250-DSN
250-ENHANCEDSTATUSCODES
250-8bitmime
250-BINARYMIME
250-CHUNKING
250-VRFY
250-TLS
250-STARTTLS
250 OK
C : STARTTLS
S : 220 2.0.0 SMTP server ready
C : EHLO www.contoso.com
S : 250-BLU0-SMTP180.phx.gbl Hello [94.245.87.37]
250-TURN
250-SIZE 41943040
250-ETRN
250-PIPELINING
250-DSN
250-ENHANCEDSTATUSCODES
250-8bitmime
250-BINARYMIME
250-CHUNKING
250-VRFY
250-AUTH LOGIN PLAIN XOAUTH2
250 OK
C : AUTH LOGIN
S : 334 VXNlcm5hbWU6
C : john.doe@outlook.com
S : 334 UGFzc3dvcmQ6
C : MyF4bulousP@zzw0rd
S : 235 2.7.0 Authentication succeeded
C : MAIL FROM:<john.doe@outlook.com>
S : 250 2.1.0 john.doe@outlook.com….Sender OK
C : RCPT TO:<spertus@microsoft.com>
S : 250 2.1.5 spertus@microsoft.com
C : DATA
S : 354 Start mail input; end with <CRLF>.<CRLF>
C : Date: Wed, 27 Nov 2013 17:28:56 +0000
X-Priority: 0
To: sebastien.pertus@gmail.com, spertus@microsoft.com
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
Subject: Hi Guy.
Content-Type: text/plain; charset="utf-8"
Hi Sebastien, how are you ??
John D.
.
S : 250 2.6.0 <BLU0-SMTP180Np7vXee0000a899@BLU0-SMTP180.phx.gbl> Queued mail for delivery
C : QUIT
S : 221 2.0.0 BLU0-SMTP180.phx.gbl Service closing transmission channel
Client发送邮件
首先上代码:
public async Task<bool> SendEmail(SmtpMessage message)
{
if (!IsConnected)
{
await this.Connect();
}
if (!IsConnected)
{
throw new Exception("Can't connect server!");
}
if (!IsAuthentication)
{
await this.Authenticate();
}
var rs = await this.SmtpSocket.Send(string.Format("Mail From:<{0}>", message.MailFrom));
if (!rs.IsContainsStatus(SmtpCode.RequestedMailActionCompleted))
{
return false;
}
foreach (var to in message.RcptTo)
{
var rsTo = await this.SmtpSocket.Send(string.Format("Rcpt To:<{0}>", to));
if (!rsTo.IsContainsStatus(SmtpCode.RequestedMailActionCompleted))
{
break;
}
}
var rsD = await this.SmtpSocket.Send("Data");
if (!rsD.IsContainsStatus(SmtpCode.StartMailInput))
{
return false;
}
var rsM = await this.SmtpSocket.Send(message.GetBody());
if (!rsM.IsContainsStatus(SmtpCode.RequestedMailActionCompleted))
{
return false;
}
var rsQ = await this.SmtpSocket.Send("Quit");
if (!rsQ.IsContainsStatus(SmtpCode.ServiceClosingTransmissionChannel))
{
return false;
}
return true;
}
通过代码可以很清晰的看出, 完全是按照 Client 发送, 然后等待回应, 然后判断回应是否正确来判断是否发送成功.
通过SendEmail() 可以看出, 整个过程主要可以分为以下几个部分:
- 连接 Connect();
- 认证Authenticate();
- 向Server发送消息或命令: Send();
- 获取Server的回应: GetResponse();
连接Server的话, 这里使用StreamSocket中的ConnectAsync()方法来实现:
if (this.isssll)
{
await this.socket.ConnectAsync(this.hostName, this.port.ToString(), SocketProtectionLevel.Ssl);
}
else
{
await this.socket.ConnectAsync(this.hostName, this.port.ToString(), SocketProtectionLevel.PlainSocket);
}
进行连接之后, 我们得看看Server返回的数据, 以此来判断我们的连接是否真正的成功.也就是我们的GetResponse();
首先需要提到的是, 我们使用DataReader 和 DataWriter 来对流进行读写(namepace 是 Windows.Storage.Streams)
this.reader = new DataReader(this.socket.InputStream);
this.reader.InputStreamOptions = InputStreamOptions.Partial;
this.writer = new DataWriter(this.socket.OutputStream);
其次, Server返回的数据都是含有多条消息的(这里将一个Code和一个string 称为一条消息), 在上面的例子中我们也能看到, 所以, 这里选用 List<KeyValuePair<SmtpCode, string>> 类型的来存储响应数据.
do
{
char chr = (char)buffer[charpos];
if (!isStringLine)
{
// code部分
codeStr += chr;
if (codeStr.Length == 3)
{
if (int.TryParse(codeStr, out int codeInit))
{
code = (SmtpCode)codeInit;
}
isStringLine = true;
}
}
else if (chr == '\r' || chr == '\n')
{
// string 结束
if (chr == '\r' && charpos < (charlen - 1))
{
// 再判断下一个字符
charpos++;
chr = (char)buffer[charpos];
}
if (chr == '\n')
{
// 回车换行符 string结束
KeyValuePair<SmtpCode, string> kvp = new KeyValuePair<SmtpCode, string>(
code, stringBuilder.ToString());
response.Values.Add(kvp);
Debug.WriteLine("{0}:{1}", ((int)code).ToString(), stringBuilder.ToString());
isStringLine = false;
stringBuilder = null;
codeStr = string.Empty;
code = SmtpCode.None;
stringBuilder = new StringBuilder();
}
}
else
{
// 表示是string部分
stringBuilder.Append(chr);
}
charpos++;
} while (charpos < charlen);
接下来便是认证的过程了, 认证就开始需要向Server发送数据了, 所以这里向实现Send(); 这里利用DataWriter 的WriteBytes()和StoreAsync();
writer.WriteBytes(buffer);
await writer.StoreAsync();
好啦, 总算来到认证了, 认证分为Login 和Plain 两种方式, 不过我们一般都选择Login, 具体是哪种方式, 需要根据Server返回的数据来判断
Login:
private async Task<bool> AuthenticateByLogin()
{
if (!IsConnected)
{
return false;
}
var rs = await this.SmtpSocket.Send("Auth Login");
if (!rs.IsContainsStatus(SmtpCode.WaitingForAuthentication))
{
return false;
}
var rsUserName = await this.SmtpSocket.Send(Convert.ToBase64String(Encoding.UTF8.GetBytes(this.UserName)));
if (!rsUserName.IsContainsStatus(SmtpCode.WaitingForAuthentication))
{
return false;
}
var rsPasswd = await this.SmtpSocket.Send(Convert.ToBase64String(Encoding.UTF8.GetBytes(this.Password)));
if (!rsPasswd.IsContainsStatus(SmtpCode.AuthenticationSuccessful))
{
return false;
}
return true;
}
Plain:
private async Task<bool> AuthenticateByPlain()
{
if (!this.IsConnected)
{
return false;
}
var rs = await this.SmtpSocket.Send("Auth Plain");
if (!rs.IsContainsStatus(SmtpCode.WaitingForAuthentication))
{
return false;
}
var lineAuthentication = string.Format("{0}\0{0}\0{1}", this.UserName, this.Password);
var rsAuth = await this.SmtpSocket.Send(Convert.ToBase64String(Encoding.UTF8.GetBytes(lineAuthentication)));
if (!rsAuth.IsContainsStatus(SmtpCode.AuthenticationSuccessful))
{
return false;
}
return true;
}
到这里为止, 发送的流程基本上就完了, 是不是很简单的啊, 但是其中会有很多小细节需要注意的
这里, 再放一个邮件Body格式的代码, 仅供参考
public string GetBody()
{
StringBuilder sb = new StringBuilder();
var dateFormat = "ddd, dd MMM yyyy HH:mm:ss +0000";
sb.AppendFormat("Date:{0}{1}", DateTime.Now.ToString(dateFormat), System.Environment.NewLine);
if (string.IsNullOrEmpty(this.MailFrom))
{
throw new Exception("MailFrom is mandatory!");
}
sb.AppendFormat("X-Priority: {0}{1}", ((byte)this.SmtpPriority).ToString(), System.Environment.NewLine);
if (rcptTo.Count == 0)
{
throw new Exception("RCPT is mandatory!");
}
sb.Append("TO: ");
for (int i = 0; i < this.rcptTo.Count; i++)
{
var to = rcptTo[i];
if (i == rcptTo.Count - 1)
{
sb.AppendFormat("{0}{1}", to, System.Environment.NewLine);
}
else
{
sb.AppendFormat("{0}{1}", to, ",");
}
}
if (this.cc.Count != 0)
{
sb.Append("CC: ");
for (int i = 0; i < this.cc.Count; i++)
{
var c = cc[i];
if (i == cc.Count - 1)
{
sb.AppendFormat("{0}{1}", c, System.Environment.NewLine);
}
else
{
sb.AppendFormat("{0}{1}", c, ",");
}
}
}
sb.AppendFormat("MIME-Version: 1.0{0}", System.Environment.NewLine);
sb.AppendFormat("Content-Transfer-Encoding: {0}{1}", this.TransferEncoding, System.Environment.NewLine);
sb.AppendFormat("Content-Disposition: inline{0}", System.Environment.NewLine);
sb.AppendFormat("Subject: {0}{1}", this.Subject, System.Environment.NewLine);
if (this.IsHtml)
{
sb.AppendFormat("Content-Type: text/html; {0}", System.Environment.NewLine);
}
else
{
sb.AppendFormat("Content-Type: text/plain; charset=\"{0}\"{1}", this.Encoding.WebName, System.Environment.NewLine);
}
sb.Append(System.Environment.NewLine);
sb.Append(this.Body);
sb.Append(System.Environment.NewLine);
sb.Append(".");
return sb.ToString();
}
好啦, 到这里就正式结束了~
Happy Coding!o(* ̄▽ ̄*)o