引言:
在我们的平常工作中,邮件的发送和接收应该是我们经常要使用到的功能的。因此知道电子邮件的应用程序的原理也是非常有必要的,在这一个专题中将介绍电子邮件应用程序的原理、电子邮件应用程序中涉及的协议和实现一个简答的电子邮件收发器程序。
一、邮件应用程序基本知识
1.1 电子邮件原理及相关协议
说到电子邮件的原理,其实和我们现实生活中寄邮件和寄包裹是一样的原理的。就让我们先回顾下现实生活中寄邮件的流程吧——首先,我们先写好信,信封上面写好收信人的地址,写信人的地址,然后把信放到寄信箱中,然后邮局的人会某个时候去这个信箱中的信取出来,然后邮局的人根据信封上写的收信人地址进行转发到当地的邮局,当地邮局然后把信寄到收信人的信箱中(寄包裹的话可能会电话联系,像我们在淘宝,京东买的东西的,收货人就是通过电话联系一样),最后收信人会到自己的信箱中收取信件。上面大致是我们平时生活中寄信的一个流程的。前面已经讲过电子邮件的原理和这个差不多的,下面就介绍了本专题中电子邮件的原理,大家可以和现实生活中的寄信过程进行对比下的,这样可以更加容易理解和掌握:
我们通过电子邮件应用(例如 基于客户端的Outlook电子邮件软件 和一些基于Web的电子邮件系统——新浪邮箱、谷歌邮箱、QQ邮箱等都属于电子邮件应用)将一封写好的邮件(相当于现实生活中的信,当然邮件也要写明收件人地址,邮件内容等信息的)通过电子邮件协议(SMTP,在后面的电子邮件相关协议中会介绍)发送到SMTP服务器(就是存储邮件的地方,相当于生活中的邮局一样),然后SMTP服务器根据收件人的地址通过SMTP协议转发到相应SMTP接收服务器上,(SMTP服务器进行转发相当于现实生活中邮局的人配送信的过程,配送到收件人当地的邮局,然而现实生活中邮局都是一家,所以可以相互识别——意思就是发送到当地邮局,当地邮局会接收,并且帮助你发送到指定人的信箱中,在网上上就是通过SMTP协议来规定这样的一个过程的,发送到别人的SMTP服务器上别人的服务器必须要认识发送来的邮件并接收)结束,接收端邮件服务器(POP3服务器)把邮件存放到接受者的电子信箱内(相当于当地邮局的人把信放到收信人的邮箱中),最后收件人可以登录自己的电子信箱,再与POP3服务器进行连接,从POP3服务器上下载发送来的邮件,这样在收件人的电子信箱中就可以看到发送来的电子邮件了(这就是现实生活中收信人从自己的信箱中取信的一个过程)。
注:括号中都是个人的理解,如果有什么不对的地方还望大家指出来,我好及时更正。
上面已经把电子邮件的原理和现实生活中寄信的过程进行对比,相信大家可以更加清楚电子邮件的原理和发送接收过程的,其实网络上的很多应用都可以以现实生活的例子去理解,这样的话我认为可以加深对知识的理解。下面就介绍下电子邮件中的相关协议的内容:
网络上的应用的核心就是协议,因为协议让网络上的客户端相互认识发生来的数据,所以电子邮件应用也不例外,也有相关的电子邮件协议来完成发送电子邮件和接收电子邮件的过程,这些协议主要是:SMTP(简单邮件传输协议,Simple Mail Transfer Protocol)、POP3(邮局协议,Post Office Protocol)和IMAP(网络邮件访问协议,Internet Message Access Protocol)。
1.2 邮件系统的分类
邮件系统主要分为两类的——基于客户端的邮件系统和基于Web浏览器的邮件系统。Office OutLook就是基于客户端的邮件客户端系统,而像我们经常使用的QQ邮箱、新浪、网易邮箱等都是属于基于Web浏览器的邮件系统,基于客户端的邮件系统的收发过程,通过下面的图片来描述(图片从网上摘下的):
图 1.1 基于客户端的邮件收发过程
发送方通过邮件客户端,将编辑好的邮件向邮件服务(SMTP服务器,在发送过程中也叫发送端邮件服务器)发送,发送端邮件服务器根据收件人的地址来识别接收端邮件服务器(POP3服务器),然后向POP3服务器发送邮件信息,接收端邮件服务器将邮件存放在接收者的电子信箱中,并告知接收者有新邮件,接收者通过邮件客户端与POP3服务器连接后,就可以查看新邮件。
然而,基于Web浏览器的邮件系统与基于客户端的邮件系统不同的地方有:
1.3 目前主要的电子邮件服务系统
电子邮件服务系统——就是向大家提供邮箱服务的服务系统,这样的系统当然是由专门的公司进行研发的,我们一般叫这样的公司为邮件服务商,我们平常使用的网易邮箱,新、Gmail邮箱等都是建立在电子邮件服务系统(这里我的理解是——我们使用的新浪,网易等邮箱相当于现实生活中每个人的信箱,通过信箱可以获得邮局来的信,同样道理通过邮箱可以获得邮件服务系统的邮件,这样电子邮件系统相当于邮局) 。现在主要电子邮件服务系统主要有下面几种:
二、.Net 平台对邮件发送功能的支持
在.NET类库中,在System.Net.Mail命名空间下定义了对邮件处理的类,这样使邮件的发送更加方便(这些类也就是对SMTP协议的封装,使我们更好地区编程,只需要使用类中的方法和属性等去完成邮件的发送,避免写复杂的SMTP协议的命令),下面是一张在System.Net.Mail命名空间下对邮件发送的支持的类截图:
从图片中类的名字中也可以看出每个类的作用的,在这里我就不一个介绍的, 大家可以参考MSDN去看每个类的使用,并且我在后面程序的实现部分也会有详细的注释去介绍程序中使用到类的使用。从图中还可以i看出一点——就是只有SMTP的字样,却没有POP3这样的字样的,这说明.Net类库本身中并没有提供对POP3协议的封装类,但是我们可以使用Jmail组件来完成从POP3服务器中收取邮件的功能,具体的使用将在后面的邮件收发器程序中邮件的接收部分介绍的。
三、邮件收发器程序的实现
3.1 邮件发送功能的实现
3.1.1 SMTP协议
SMTP 协议是用于电子邮件的传输的协议,电子邮件是通过SMTP服务器进行发送的,SMTP服务器的默认端口为25,通常发送邮件有两种方式——一种是不使用客户端认证,即客户端可以使用匿名发送邮件(这种方式叫做SMTP);另一种是客户端必须提供用户名和密码认证(这种方式叫做ESMTP,Extended SMTP)目前大部分邮件服务器采用用户名和密码认证的方式。
客户端发送邮件过程为——先通过客户端软件(本程序中的邮件收发器)将邮件发送到SMTP服务器,然后再由SMTP服务器发送到目标SMTP服务器。下面介绍SMTP协议的内容:
SMTP协议总共定义了14个命令,命令由命令码和气候的参数域组成, 不区别大小写的(通过前面专题的讲述可以得出各个协议的命令组成都差不多的),下面就简单介绍下5个常用的命令码
名称 |
解释 |
HELO或EHLO |
发送连接到服务器的命令,EHLO主要用于与ESMTP服务器建立连接时发送的命令 |
MAIL FROM |
指定发件人的邮件地址 |
Rcpt to |
指定收件人的邮件地址 |
Data |
指定邮件正文内容,邮件内容以单独一行 ”.” 表示接触 |
Quit |
关闭与服务器的连接,然后退出 |
电子邮件由信封、首部、正文和结束符号4部分组成,下面就具体介绍下这4个部分的内容:
1. 信封
信封包括发信人的邮件地址和接收人的邮件地址,具体对应两条SMTP命令——Mail from: [email protected](发信人的地址)和Rcpt to: [email protected]
2. 首部
首部中常用的命令有:
3. 正文
正文当然指的就是邮件的内容了, 用Data命令指定,首部以一个空行结束,下面就是正文部分
4. 结束符号
邮件以“."结束,
接收方收到SMTP命令之后,会给出一个响应码,每个命令都只有一个响应码,SMTP响应码也是由3位数字组成,后面附加一些文本信息,响应信息的格式为:
响应码<空格>文本信息<回车换行>
客户端发出一条命令后,服务器端返回一个响应,发送者在发送下一条命令前必须等待服务器的响应,成功接收到响应码后才继续发送命令。
附:SMTP常用的响应码:
响应码 |
解释 |
响应码 |
解释 |
211 |
系统状态或系统帮助响应 |
421 |
服务未就绪,关闭了传输通道 |
214 |
帮助信息 |
501 |
参数格式错误 |
220 |
服务就绪 |
502 |
命令不可实现 |
221 |
服务关闭传输通道 |
535 |
用户验证失败 |
235 |
用户验证成功 |
553 |
邮箱名不可用,要求的操作未执行 |
334 |
等待用户输入验证 |
554 |
操作失败 |
354 |
开始邮件输入 |
|
|
3.1.2 邮件的发送过程
第一步:客户端与服务器建立连接(该步中客户端首先发送EHLO local 连接命令,服务器如果返回“220”响应码表示服务器准备就绪了,客户端再继续发送“Auto login”命令,请求登录,服务器收到命令后返回“334”响应码,表示要输入用户名,之后客户端发送用户名命令,等到响应后再发送密码命令,具体在程序的实现中也会有注释。)
第二步:客户端发送邮件的信封
第三步:开始发送邮件数据,(包括邮件首部,正文和结束符号,注:结束符号要单独占一行,表示邮件发送结束)
第四步: 客户端与服务器断开连接。
3.1.3 发送功能的实现代码
相信有了上面的理论解释邮件发送的过程后,实现邮件发送的功能并不难的,并且.net类库中SMTPClient类帮我们封装了SMTP协议,使得我们实现邮件发送功能就不要记住那些具体的命令了, 只需要使用该类中提供的方法来完成邮件的发送(当然你也可以通过发送命令的方式实现,SMTPClient类的方法也是帮我们完成发送命令功能而已的),下面是邮件发送功能的核心代码:
1 #region 邮件发送功能代码 2 // 添加附件 3 private void btnAddFile_Click(object sender, EventArgs e) 4 { 5 OpenFileDialog openFileDialog = new OpenFileDialog(); 6 openFileDialog.CheckFileExists = true; 7 // 只接受有效的文件名 8 openFileDialog.ValidateNames = true; 9 // 允许一次选择多个文件作为附件 10 openFileDialog.Multiselect = true; 11 openFileDialog.Filter = "所有文件(*.*)|*.*"; 12 if (openFileDialog.ShowDialog() != DialogResult.OK) 13 { 14 return; 15 } 16 if (openFileDialog.FileNames.Length > 0) 17 { 18 // 因为这里允许选择多个文件,所以这里用AddRange而没有用Add方法 19 cmbAttachment.Items.AddRange(openFileDialog.FileNames); 20 } 21 } 22 23 // 删除附件 24 private void btnDeleteFile_Click(object sender, EventArgs e) 25 { 26 int index = cmbAttachment.SelectedIndex; 27 if (index == -1) 28 { 29 MessageBox.Show("请选择要删除的附件!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); 30 return; 31 } 32 else 33 { 34 cmbAttachment.Items.RemoveAt(index); 35 } 36 } 37 38 // 发送邮件 39 private void btnSend_Click(object sender, EventArgs e) 40 { 41 this.Cursor = Cursors.WaitCursor; 42 // 实例化一个发送的邮件 43 // 相当于与现实生活中先写信,程序中把信(邮件)抽象为邮件类了 44 MailMessage mailMessage = new MailMessage(); 45 // 指明邮件发送的地址,主题,内容等信息 46 // 发信人的地址为登录收发器的地址,这个收发器相当于我们平时Web版的邮箱或者是OutLook中配置的邮箱 47 mailMessage.From = new MailAddress(tbxUserMail.Text); 48 mailMessage.To.Add(txbSendTo.Text); 49 mailMessage.Subject = txbSubject.Text; 50 mailMessage.SubjectEncoding = Encoding.Default; 51 mailMessage.Body = richtbxBody.Text; 52 mailMessage.BodyEncoding = Encoding.Default; 53 // 设置邮件正文不是Html格式的内容 54 mailMessage.IsBodyHtml = false; 55 // 设置邮件的优先级为普通优先级 56 mailMessage.Priority = MailPriority.Normal; 57 //mailMessage.ReplyTo = new MailAddress(tbxUserMail.Text); 58 59 // 封装发送的附件 60 System.Net.Mail.Attachment attachment = null; 61 if (cmbAttachment.Items.Count > 0) 62 { 63 for (int i = 0; i < cmbAttachment.Items.Count; i++) 64 { 65 string fileNamePath = cmbAttachment.Items[i].ToString(); 66 string extName = Path.GetExtension(fileNamePath).ToLower(); 67 if (extName == ".rar" || extName == ".zip") 68 { 69 attachment = new System.Net.Mail.Attachment(fileNamePath, MediaTypeNames.Application.Zip); 70 } 71 else 72 { 73 attachment = new System.Net.Mail.Attachment(fileNamePath,MediaTypeNames.Application.Octet); 74 } 75 76 // 表示MIMEContent-Disposition标头信息 77 // 对于ContentDisposition具体类的解释大家可以参考MSDN 78 // 这里我就不重复贴出来了,给个地址: http://msdn.microsoft.com/zh-cn/library/System.Net.Mime.ContentDisposition.aspx (着重看备注部分) 79 ContentDisposition cd = attachment.ContentDisposition; 80 cd.CreationDate = File.GetCreationTime(fileNamePath); 81 cd.ModificationDate = File.GetLastWriteTime(fileNamePath); 82 cd.ReadDate = File.GetLastAccessTime(fileNamePath); 83 // 把附件对象加入到邮件附件集合中 84 mailMessage.Attachments.Add(attachment); 85 } 86 } 87 88 // 发送写好的邮件 89 try 90 { 91 // SmtpClient类用于将邮件发送到SMTP服务器 92 // 该类封装了SMTP协议的实现, 93 // 通过该类可以简化发送邮件的过程,只需要调用该类的Send方法就可以发送邮件到SMTP服务器了。 94 smtpClient.Send(mailMessage); 95 MessageBox.Show("邮件发送成功!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); 96 97 } 98 catch(SmtpException smtpError) 99 { 100 MessageBox.Show("邮件发送失败:[" + smtpError.StatusCode + "];[" 101 + smtpError.Message+"];\r\n["+smtpError.StackTrace+"]." 102 ,"错误",MessageBoxButtons.RetryCancel,MessageBoxIcon.Error); 103 } 104 finally 105 { 106 mailMessage.Dispose(); 107 this.Cursor = Cursors.Default; 108 } 109 } 110 111 #endregion
3.2 邮件接收功能的实现
3.2.1 POP3协议
前面介绍了邮件的发送,当然接收者需要登录邮箱来查看收到的邮件了,此时就必有有一个协议去读取服务器上邮件,POP3就是这样的一个协议。还有两外一种协议也是用来接收邮件的——IMAP协议,它与POP3协议区别有:1. IMAP使用的端口号是143而POP3邮件服务器通过监听110端口来提供POP3服务;
2 . IMAP 允许客户端在邮件服务器上建立文件夹来保持邮件,而不用把邮件下载到本地。而POP3需要把邮件下载到本地。
和SMTP协议一样,客户端要通过POP3协议从POP3服务器上获取邮件,也需要先与POP3服务器建立TCP连接,等待服务器向客户端发送确认信息表明连接成功时,客户端才可以继续发送命令给服务器来获取邮件,在POP3协议中,规定的命令也是几十条的,每条命令由命令和参数两部分组成,都是以回车换行结束,并且命令和参数之间由空格分隔,命令通常也是由3-4个字母组成,参数最多可以为40个字符的长度,而服务器的响应信息是由一个状态码和可能附加信息的字符组成,所有的响应信息也是以回车换行结束的。状态码和其他协议定义的状态码有点不一样,POP3服务器响应的状态码有两种——“+OK”(确定)和"-ERR"(失败)。这样客户端可以通过检查响应的状态码所包含的字符来判断服务器是否响应客户端发送的命令,即响应信息中包含“+OK”表示成功响应,包含“-ERR”表示服务器未响应。同时在程序的实现中大家可以通过Debug来查看响应消息的组成,这样可以加深理解。
3.2.2 邮件接收的过程
客户端从服务器接收邮件的过程主要经历3个状态:授权状态、操作状态和更新状态
(1)授权状态——客户端发送与POP3服务器的TCP连接请求,服务器接收后发送一个响应确认信息之后,此时客户端需要发送正确的用户名和密码进行确认,因为在邮件服务器上有很多用户邮箱,只有提供正确的用户名和密码才有权限访问自己的邮箱,就像现实生活中我们邮箱的钥匙一样的。
发送用户名命令: USER [email protected]
发送密码命令: PASS ******(这两个命令都在代码中有给出的,大家可以参考代码来理解邮件的接收过程)
(2) 操作状态——如果客户端提供了正确的用户名和密码,则授权状态也就通过了,就相当于打开了在服务器上自己的邮箱,现在用户就有权限进去下载,查看和删除邮件等操作的,然后在现实生活中的取邮件和删除邮件都很简单(只要打开了邮箱门,用手去拿就可以了),然后在网络应用上,这些操作都需要发送POP3命令给服务器,服务器接收到命令后再给出响应。操作中常用的命令有:
如:
客户端发送POP3命令: STAT
服务器响应命令: +OK 2 1340<BR>服务器响应命令:
|
如:
客户端发送POP3命令:LIST
服务器响应命令: +OK 2 message(1430 octect)
服务器响应命令:1 700
服务器响应命令:2 730
服务器响应命令:<一个空行>
|
如:
客户端发送POP3命令:RETR 1
服务器响应命令: 700 octets
服务器响应命令:<邮件头和内容>
服务器响应命令: <空行>
|
(3)更新状态——客户端发送QUIT命令后,此时就进入更新状态,POP3服务器释放在操作状态中取得的资源,并将逻辑删除的邮件进行物理删除,然后关闭与客户端的TCP连接。这样整个邮件处理的过程就结束了。
3.2.3 接收功能的实现代码
有了前面接收邮件过程的介绍,再参考代码的实现,相信大家可以更好的理解客户端从POP3服务器中获取邮件的过程的,由于.net类库并没有帮我们封装POP3协议的实现类,要实现邮件的获取可以采用发送命令的方式,也可以使用Jmail组件,这个组件其实就是POP3协议的封装类,既然微软没有帮我们做,其他公司帮我们做好后来帮助我们简单的实现邮件的接收的一个类库罢了。然后在使用这个组件的过程中出现了好几个问题的,在源码中我都解释,大家可以下载源代码后查看的。
实现邮件接收的核心代码如下:
// 登录邮箱(这里是本程序——邮件收发器) private void btnLogin_Click_1(object sender, EventArgs e) { // 与POP3服务器建立TCP连接 // 建立连接后把服务器上的邮件下载到本地 // 设置当前界面的光标为等待光标(就是我们看到的一个动的圆形) Cursor.Current = Cursors.WaitCursor; lsttbxStatus.Items.Clear(); try { // POP3服务器通过监听TCP110端口来提供POP3服务的 // 向POP3服务器发出tcp请求 tcpClient = new TcpClient(tbxPOP3Server.Text, 110); lsttbxStatus.Items.Add("正在连接..."); } catch { MessageBox.Show("连接失败", "错误", MessageBoxButtons.RetryCancel, MessageBoxIcon.Error); lsttbxStatus.Items.Add("连接失败!"); return; } // 连接成功的情况 networkStream = tcpClient.GetStream(); streamReader = new StreamReader(networkStream, Encoding.Default); streamWriter = new StreamWriter(networkStream, Encoding.Default); streamWriter.AutoFlush = true; string str; // 读取服务器返回的响应连接信息 str = GetResponse(); if (CheckResponse(str) == false) { lsttbxStatus.Items.Add("服务器拒接了连接请求"); return; } // 如果服务器接收请求 // 向服务器发送凭证——用户名和密码 // 向服务器发送用户名,请求确认 lsttbxStatus.Items.Add("核实用户名阶段..."); SendToServer("USER " + tbxUserMail.Text); str = GetResponse(); if (CheckResponse(str) == false) { lsttbxStatus.Items.Add("用户名错误."); return; } // 用户名审核通过后再发送密码等待确认 // 向服务器发送密码,请求确认 SendToServer("PASS "+txbPassword.Text); str = GetResponse(); if (CheckResponse(str) == false) { lsttbxStatus.Items.Add("密码错误!"); return; } lsttbxStatus.Items.Add("身份验证成功,可以开始会话"); // 向服务器发送LIST 命令,请求获得邮件列表和大小 lsttbxStatus.Items.Add("获取邮件...."); SendToServer("LIST"); str = GetResponse(); if (CheckResponse(str) == false) { lsttbxStatus.Items.Add("获取邮件列表失败"); return; } lsttbxStatus.Items.Add("邮件获取成功"); // 窗口控件控制 tabControlMyMailbox.Enabled = true; btnReadMail.Enabled = false; btnDownLoad.Enabled = false; btnDeleteMail.Enabled = false; // 登陆成功后实例化邮件发送对象,以便后面完成发送邮件的操作 // 实例化邮件发送类(SmtpClient)对象 if (smtpClient == null) { smtpClient = new SmtpClient(); smtpClient.Host = tbxSmtpServer.Text; smtpClient.Port = 25; // 不使用默认凭证,即需要认证登陆 smtpClient.UseDefaultCredentials = false; smtpClient.Credentials = new NetworkCredential(tbxUserMail.Text, txbPassword.Text); smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network; } // 登陆成功后,自动接收新邮件 // 开始接收邮件 try { btnRefreshMailList.PerformClick(); } catch { MessageBox.Show("读取邮件列表失败!", "错误", MessageBoxButtons.RetryCancel, MessageBoxIcon.Error); } lsttbxStatus.Items.Add("登陆成功!"); lsttbxStatus.TopIndex = lsttbxStatus.Items.Count - 1; Cursor.Current = Cursors.Default; // 窗口控件控制 richtbxMailContentReview.Enabled = true; tbxUserMail.Enabled = false; txbPassword.Enabled = false; btnLogin.Enabled = false; btnLogout.Enabled = true; tbxSmtpServer.Enabled = false; tbxPOP3Server.Enabled = false; btnReadMail.Enabled = true; btnDownLoad.Enabled = true; btnDeleteMail.Enabled = true; tabControlMyMailbox.Focus(); } #region 处理与POP3服务器交互事件 // 获取服务器响应的信息 private string GetResponse() { string str = null; try { str = streamReader.ReadLine(); if (str == null) { lsttbxStatus.Items.Add("连接失败——服务器没有响应"); } else { lsttbxStatus.Items.Add("收到:[" + str + "]"); if (str.StartsWith("-ERR")) { str = null; } } } catch(Exception err) { lsttbxStatus.Items.Add("连接失败:[" + err.Message + "]"); } return str; } // 检查响应信息 private bool CheckResponse(string responseString) { if (responseString == null) { return false; } else { if (responseString.StartsWith("+OK")) { return true; } else { return false; } } } // 向服务器发送命令 private bool SendToServer(string str) { try { // 这里必须使用WriteLine方法的,因为POP3协议中定义的命令是以回车换行结束的 // 如果客户端发送的命令没有以回车换行结束,POP3服务器就不能识别,也就不能响应客户端的请求了 // 如果想用Write方法,则str输入的参数字符中必须包含“\r\n”,也就是回车换行字符串。 streamWriter.WriteLine(str); streamWriter.Flush(); lsttbxStatus.Items.Add("发送:[" + str + "]"); return true; } catch(Exception ex) { lsttbxStatus.Items.Add("发送失败:[" + ex.Message + "]"); return false; } } #endregion
3.3 程序运行结果演示
首先输入邮箱名和密码登录到POP3服务器来获取邮件列表的演示:
然后在邮件列表中选中一个邮件进行阅读,然后进行回复邮件的操作演示(邮件的发送都可以附加附件发送出去):
阅读邮件的界面:
回复邮件的界面:
同时点击发送按钮后,就可以把邮件发送到sina的SMTP服务器上,再由新浪的SMTP服务器转发到QQ的SMTP服务器,QQ的POP3服务器中QQ的SMTP服务器获取收到的邮件,当QQ用户输入正确的邮箱名和密码后就可以从QQ的POP3服务器上获取收到的邮件。
点击发送按钮后成功发送邮件的图片:
四、总结
介绍到这里,本专题的内容就已经介绍完了,希望通过本专题可以让大家明白邮件发送和接收的原理,并且可以自定义一个简单邮件收发器的功能的,在后面一专题将介绍FTP协议(文件传输协议),并实现一个简单的文件上传和下载的程序。