邮件发送在web应用中是屡见不鲜的,在asp时代大家多是利用一些第三方提供的组件如JMAIL、ASPMAIL等进行邮件发送。自从微软推出Asp.net后,很多程序员开始转向采用C#作为主要的开发语言。asp.net提供了更加强大的功能,同时也提供给了大家一个SMTP类作为邮件发送之用。但是,随着垃圾邮件的广泛传播,很多邮件服务提供商纷纷增加了SMTP 的认证手续,也就是ESMTP,而微软提供的SMTP类居然不支持认证发送。当然现在网上也出现了一些解决方案,利用其他的一些手段来发出认证信息。但我想,是不是还有更好的呢?为了解决这个问题,笔者两日茶饭不思,日以继夜,终于找到了一个方法:)。下面,我们将利用TCPCLIENT这个类直接与SMTP服务器通讯进行邮件的发送。
实际上原理也就是利用套接字(Socket)和服务器进行对话通讯,按照SMTP协议的规范,和服务器建立联系。我们以往用的一些邮件组件都是这么做的。
在开始之前,我们要对SMTP协议及其扩展ESMTP有个初步的了解。
SMTP和ESMTP的一些主要命令格式有以下一些:
HELO <信息发送端的名称> 例如&#xff1a;HELO Localhost
这相当于和服务器打个招呼&#xff0c;你好&#xff0c;我是某某
EHLO <信息发送端的名称> 例如&#xff1a;EHLO Localhost
这是针对ESMTP服务器的接触方式&#xff0c;必须输入这个命令&#xff0c;系统才会开始认证程序
AUTH LOGIN
输入这个命令&#xff0c;系统的认证程序将会启动&#xff0c;同时系统会返回一个经过Base64处理过的字符串&#xff0c;意思是"请输入用户名"。接着必须发送用户名给服务器&#xff0c;用户名也必须经过Base64编码转换&#xff0c;服务器在通过用户名的认证之后会要求输入密码&#xff0c;此时输入经过Base64编码转换后的密码。成功后&#xff0c;即可运行下面的命令了。
MAIL FROM:<发件人地址> 例如&#xff1a;MAIL FROM: webmaster&#64;sina.com
这是告诉服务器发件人的邮件地址
RCPT TO:<收件人地址> 例如&#xff1a;RCPT TO: webmaster&#64;sina.com
这是告诉服务器收件人的邮件地址
DATA
输入这个命令后&#xff0c;服务器正式开始接受数据
.
数据输入完成后&#xff0c;必须输入命令"."&#xff0c;服务器就会停止数据的接受.
QUIT 退出系统
上面是一些基本命令的描述&#xff0c;如果大家还有什么不懂的地方&#xff0c;可以参考TCP/IP有关的书籍&#xff0c;也可以到这个网站看看RFC文档&#xff1a;http://210.25.132.18/rfc/index.html
现在我们正式开始&#xff0c;看看在C# 中如何来进行工作。
第一步&#xff1a;创建一个类&#xff0c;命名为MailSend&#xff0c;这个类继承System.Net.Sockets.TcpClient
using System;
using System.Net.Sockets;//用于处理网络连接
using System.IO; //用于处理附件的包
using System.Text;//用于处理文本编码
using System.Data;
using System.Net;
public class MailSend:TcpClient
{
public MailSend()
{
}
}
在这里我要讲讲TcpClient这个类&#xff0c;它的主要作用就是为TCP网络服务提供客户端的连接&#xff0c;大家可以看到&#xff0c;他来源于Sockets这个包&#xff0c;实际上是基于 Socket 类构建。不过他以更高的抽象程度提供 TCP 服务&#xff0c;操作起来也更简单。
第二步&#xff1a;建立一些基本的变量及连接方法
1、基本变量
private String server;//SMTP服务器域名
private int port;//端口
private String username;//用户名
private String password;//密码
private String subject;//主题
private String body;//文本内容
private String htmlbody;//超文本内容
private String from;//发件人地址
private String to;//收件人地址
private String fromname;//发件人姓名
private String toname;//收件人姓名
private String content_type;//邮件类型
private String encode;//邮件编码
private String charset;//语言编码
private DataTable filelist;//附件列表
private int priority;//邮件优先级
以上定义的都是邮件发送所需的一些基本信息&#xff0c;可以将上述变量做为属性来传递。
如&#xff1a;
public String SMTPServer
{
set{this.server&#61;value;}
}
其余的也可如此.
2、向服务器写入命令的方法
变量strCmd为需要输入的命令或数据的字符串
变量charset为数据的字符语言编码&#xff0c;一般可以设置为GB2312
private void WriteStream(String strCmd,String charset)
{
Stream TcpStream;//定义操作对象
strCmd &#61; strCmd &#43; "\r\n"; //加入换行符
TcpStream &#61;this.GetStream();//获取数据流
//将命令行转化为byte[]
byte[] bWrite &#61; Encoding.GetEncoding(charset).GetBytes(strCmd.ToCharArray());
//由于每次写入的数据大小是有限制的&#xff0c;那么我们将每次写入的数据长度定在&#xff17;&#xff15;个字节&#xff0c;一旦命令长度超过了&#xff17;&#xff15;&#xff0c;就分步写入。
int start&#61;0;
int length&#61;bWrite.Length;
int page&#61;0;
int size&#61;75;
int count&#61;size;
if (length>75)
{
//数据分页
if ((length/size)*size
page&#61;length/size&#43;1;
else
page&#61;length/size;
for (int i&#61;0;i
{
start&#61;i*size;
if (i&#61;&#61;page-1)
count&#61;length-(i*size);
TcpStream.Write(bWrite,start,count);//将数据写入到服务器上
}
}
else
TcpStream.Write(bWrite,0,bWrite.Length);
}
catch(Exception)
{}
}
本方法中&#xff0c;我们最后用到的也就最重要的就是TcpStream.Write()这句话&#xff0c;前面所做的只是将数据分页&#xff0c;可以分步写入。另外在写入数据时&#xff0c;必须把字符串转化为byte[]类型。在这里我用的是Stream这个对象&#xff0c;同时你也可以使用NetworkStream这个对象来进行操作&#xff0c;实际效果是一致的。在下面的返回信息获取中&#xff0c;我就用到了NetworkStream&#xff0c;实际上这也是帮助大家熟悉流操作对象的一个过程。
3、获取服务器的返回信息
private string ReceiveStream()
{
String sp&#61;null;
byte[] by&#61;new byte[1024];
NetworkStream ns &#61; this.GetStream();//此处即可获取服务器的返回数据流
int size&#61;ns.Read(by,0,by.Length);//读取数据流
if (size>0)
{
sp&#61;Encoding.Default.GetString(by);//转化为String
}
return sp;
}
除了输入DATA命令之后&#xff0c;其余的时间向服务器发送命令&#xff0c;服务器都会返回一些信息&#xff0c;并同时有一个状态码返回&#xff0c;告诉你操作是否成功完成了。一旦输入DATA命令&#xff0c;也就是数据开始传递的这段时间中&#xff0c;服务器不会返回任何信息&#xff0c;直到输入"."结束传递&#xff0c;服务器才会返回信息。
4、发出命令并判断返回信息是否正确&#xff0c;也就是看发出的命令服务器是否接受并通过了。
本方法实际上将上面的两个方法结合来用&#xff0c;一个写&#xff0c;一个收&#xff0c;然后进行判断&#xff0c;看是否正确。这样我们就能够监控每步操作是否正常进行了。
参数strCmd也就是需要输入的命令或者数据
参数state为返回的表明操作成功的状态码
private bool OperaStream(string strCmd,string state)
{ string sp&#61;null;
bool success&#61;false;
try
{
WriteStream(strCmd);//写入命令
sp &#61; ReceiveStream();//接受返回信息
if (sp.IndexOf(state)!&#61;-1)//判断状态码是否正确
success&#61;true;
}
catch(Exception ex)
{Console.Write(ex.ToString());}
return success;
}
我们进行每一步操作时&#xff0c;都是通过状态码来确定是否成功的&#xff0c;那么如果操作成功&#xff0c;就会返回正确的状态码&#xff0c;根据这个原理&#xff0c;我们在这个方法中&#xff0c;同时输入命令和表明操作成功的状态码&#xff0c;通过获取的数据判断返回的是不是正确的状态码&#xff0c;以此来决定是否继续进行下一步操作。
在这里我要告诉大家一些基本的状态码表示的含义。
211 帮助返回系统状态
214 帮助信息
220 服务准备就绪
221 关闭连接
250 请求操作就绪
251 用户不在本地&#xff0c;转寄到
354 开始邮件输入
421 服务不可用
450 操作未执行&#xff0c;邮箱忙
451 操作中止&#xff0c;本地错误
452 操作未执行&#xff0c;存储空间不足
500 命令不可识别或语法错
501 参数语法错
502 命令不支持
503 命令顺序错
504 命令参数不支持
550 操作未执行&#xff0c;邮箱不可用
551 非本地用户
552 中止&#xff0c;存储空间不足
553 操作未执行&#xff0c;邮箱名不正确
554 传输失败
写完以上的基本方法&#xff0c;我们可以开始和服务器进行连接了。由于现在的服务器有SMTP和ESMTP两种&#xff0c;不同的服务器连接的命令格式不一样&#xff0c;那么我们需要完成一个方法来取得服务器的连接。
public bool getMailServer()
{
try
{
//域名解析
System.Net.IPAddress ipaddress&#61;(IPAddress)System.Net.Dns.Resolve(this.server).AddressList.GetValue(0);
System.Net.IPEndPoint endpoint&#61;new IPEndPoint(ipaddress,25);
Connect(endpoint);//连接Smtp服务器
ReceiveStream();//获取连接信息
if (this.username!&#61;null)
{
//开始进行服务器认证
//如果状态码是250则表示操作成功
if (!OperaStream("EHLO Localhost","250"))
{
this.Close();
return false;
}
if (!OperaStream("AUTH LOGIN","334"))
{
this.Close();
return false;
}
username&#61;AuthStream(username);//此处将username转换为Base64码
if (!OperaStream(this.username,"334"))
{
this.Close();
return false;
}
password&#61;AuthStream(password);//此处将password转换为Base64码
if (!OperaStream(this.password,"235"))
{
this.Close();
return false;
}
return true;
}
else
{ //如果服务器不需要认证
if (OperaStream("HELO Localhost","250"))
{
return true;
}
else
{
return false;
}
}
}
catch(Exception ex)
{ return false;}
}
上面这个方法主要是用于和服务器取得联系&#xff0c;其中包含了针对两种不同服务器的连接方法&#xff0c;如果用户名不为空&#xff0c;那么我们首先进行ESMTP的连接&#xff0c;否则我们和服务器直接获取联系。在ESMTP连接时&#xff0c;用户名和密码必须为Base64编码&#xff0c;否则服务器不会识别。
private string AuthStream(String strCmd)
{
try
{
byte[] by&#61;Encoding.Default.GetBytes(strCmd.ToCharArray());
strCmd&#61;Convert.ToBase64String(by);
}
catch(Exception ex)
{return ex.ToString();}
return strCmd;
}
上面的方法将数据转化为Base64编码字符串&#xff0c;大家如果觉得太抽象了&#xff0c;可以这样试一试&#xff0c;在CMD模式输入telnet smtp.sohu.com 25 然后回车&#xff0c;就可以连接sohu的SMTP服务器&#xff0c;sohu的SMTP服务器采用ESMTP协议&#xff0c;必须认证&#xff0c;大家可以试着操作一下。
第三步&#xff1a;关于邮件的附件传递
大家有发送邮件时&#xff0c;有时候会包含一些附件&#xff0c;那么本组件也考虑到了这一点。下面我们将会详细讲述如何对附件进行处理
filelist&#61;new DataTable();//已定义变量&#xff0c;初始化操作
filelist.Columns.Add(new DataColumn("filename",typeof(string)));//文件名
filelist.Columns.Add(new DataColumn("filecontent",typeof(string)));//文件内容
public void LoadAttFile(String path)
{
//根据路径读出文件流
FileStream fstr&#61;new FileStream(path,FileMode.Open);//建立文件流对象
byte[] by&#61;new byte[Convert.ToInt32(fstr.Length)];
fstr.Read(by,0,by.Length);//读取文件内容
fstr.Close();//关闭
//格式转换
String fileinfo&#61;Convert.ToBase64String(by);//转化为base64编码
//增加到文件表中
DataRow dr&#61;filelist.NewRow();
dr[0]&#61;Path.GetFileName(path);//获取文件名
dr[1]&#61;fileinfo;//文件内容
filelist.Rows.Add(dr);//增加
}
通过这个方法将直接读取出文件的内容信息&#xff0c;然后存储在DataTable对象中&#xff0c;理论上可以读取无数个文件&#xff0c;当然&#xff0c;文件越大&#xff0c;发送时间也就越长。这个方法只是针对本地的附件加入&#xff0c;如果大家有兴趣&#xff0c;可以自己利用HttpRequest做一个网上文件抓取的程序&#xff0c;直接抓取网上的文件&#xff0c;不过一般来说&#xff0c;这种方法很少用得到。好了&#xff0c;闲话不谈&#xff0c;我们已经将文件读入&#xff0c;那么之后如何处理呢&#xff1f;请看下面的一个方法。
&#xff11;&#xff1a;private void Attachment()
&#xff12;&#xff1a;{ //对文件列表做循环
&#xff13;&#xff1a; for (int i&#61;0;i
&#xff14;&#xff1a; {
&#xff15;&#xff1a; DataRow dr&#61;filelist.Rows;
&#xff16;&#xff1a; WriteStream("--unique-boundary-1");//邮件内容分隔符
&#xff17;&#xff1a; WriteStream("Content-Type: application/octet-stream;name&#61;\""&#43;dr[0].ToString()&#43;"\"");//文件格式
&#xff18;&#xff1a; WriteStream("Content-Transfer-Encoding: base64");//内容的编码
9: WriteStream("Content-Disposition:attachment;filename&#61;\""&#43;dr[0].ToString()&#43;"\"");//文件名
10: WriteStream("");
11: String fileinfo&#61;dr[1].ToString();
12: WriteStream(fileinfo);//写入文件的内容
13: WriteStream("");
14: }
15:}
这个方法中我们就用到了WriteStream()方法&#xff0c;大家可能看的有些迷糊&#xff0c;好象无头无尾的&#xff0c;实际上这一段代码&#xff0c;将会在写完邮件的头部信息和文本内容之后再写入到服务器上&#xff0c;在下面的程序中大家可以看见前面的部分。那么在代码的第七行&#xff0c;表示了文件的类型&#xff0c;我这里用了一个偷懒的方式&#xff0c;采用application/octet-stream来代替所有的文件类型&#xff0c;实际上针对大部分的常用文件都有自己的一个格式&#xff0c;大家可以根据其文件名的扩展名进行判断&#xff0c;这里我给出其他的一些格式。
扩展名 格式
".gif" --->"image/gif"
".gz" --->"application/x-gzip"
".htm" --->"text/html"
".html" --->"text/html"
".jpg" --->"image/jpeg"
".tar" --->"application/x-tar"
".txt" --->"text/plain"
".zip" --->"application/zip"
我比较偷懒&#xff0c;如果有需要的朋友&#xff0c;可以补上一些判断&#xff0c;获取文件的原本格式。
第四步&#xff1a;关于邮件的头信息
前面讲了这么多&#xff0c;就像是吃大餐之前的甜点&#xff0c;现在我们要进入最重要的部份--邮件的头信息&#xff0c;实际上&#xff0c;这个东西我们见得非常的多&#xff0c;大家在收发邮件的时候&#xff0c;查看邮件的属性就会看见一大串代码&#xff0c;里面有一些邮件地址&#xff0c;IP地址什么的&#xff0c;这就是邮件的头信息。
那么头信息的基本内容现在开讲&#xff1a;
FROM:<姓名><邮件地址> 格式&#xff1a;FROM:管理员
TO:<姓名><邮件地址> 格式&#xff1a;TO:水生月<1234&#64;sina.com>
SUBJECT:<标题> 格式&#xff1a;SUBJECT:今天的天气很不错&#xff01;
DATE:<时间> 格式&#xff1a;DATE: Thu, 29 Aug 2002 09:52:47 &#43;0800 (CST)
REPLY-TO:<邮件地址> 格式&#xff1a;REPLY-TO:webmaster&#64;sina.com
Content-Type:<邮件类型> 格式&#xff1a;Content-Type: multipart/mixed; boundary&#61;unique-boundary-1
X-Priority:<邮件优先级> 格式&#xff1a;X-Priority:3
MIME-Version&#xff1a;<版本> 格式&#xff1a;MIME-Version:1.0
Content-Transfer-Encoding:<内容传输编码> 格式&#xff1a;Content-Transfer-Encoding:Base64
X-Mailer:<邮件发送者> 格式&#xff1a;X-Mailer:FoxMail 4.0 beta 1 [cn]
如果大家安装了OutLook&#xff08;一般都装了&#xff1a;&#xff09;&#xff09;&#xff0c;自己给自己发一封信&#xff0c;收下来后&#xff0c;查看邮件的属性&#xff0c;然后会看到包含上面一些信息的数据&#xff0c;大家可以根据Outlook的头信息为参照。在这里&#xff0c;我重点要讲的是Content-Type这个头信息&#xff0c;实际上我们在邮件发送时常常包含了文本内容&#xff0c;Html超文本内容以及附件内容&#xff0c;那么此时邮件的格式也就是multipart/mixed&#xff0c;但是这么多内容你要是全放在一块&#xff0c;服务器是不会认识的&#xff0c;那么需要在不同的内容之间加入分隔符&#xff0c;
一部分内容完了之后再加入一个结束分隔符&#xff0c;有点像Html。在Content-Type的例子中有一句话boundary&#61;unique-boundary-1&#xff0c;这里就告诉系统我的分隔符叫什么名字。那么在一个邮件中&#xff0c;可以有多个分隔符&#xff0c;其余的分隔符实际上是在你给出的第一个分隔符下扩展的。说了这么多&#xff0c;看看程序&#xff1a;
WriteStream("Date: "&#43;DateTime.Now);//时间
WriteStream("From: "&#43;this.fromname&#43;"<"&#43;this.from&#43;">");//发件人
WriteStream("Subject: "&#43;this.subject);//主题
WriteStream("To:"&#43;this.to);//收件人
//邮件格式
WriteStream("Content-Type: multipart/mixed; boundary&#61;\"unique-boundary-1\"");
WriteStream("Reply-To:"&#43;this.from);//回复地址
WriteStream("X-Priority:"&#43;priority);//优先级
WriteStream("MIME-Version:1.0");//MIME版本
//数据ID,随意
WriteStream("Message-Id: "&#43;DateTime.Now.ToFileTime()&#43;"&#64;security.com");
WriteStream("Content-Transfer-Encoding:"&#43;this.encode);//内容编码
WriteStream("X-Mailer:DS Mail Sender V1.0");//邮件发送者
WriteStream("");
看看这段头信息&#xff0c;里面的变量是事先定义好的&#xff0c;在头信息结束的时候&#xff0c;在写入一段空信息&#xff0c;这样Smtp服务器才会认为你已经写完了。
WriteStream(AuthStream("This is a multi-part message in MIME format."));
WriteStream("");
这里只是一端描述性内容。
//从此处开始进行分隔输入
WriteStream("--unique-boundary-1");
//在此处定义第二个分隔符
WriteStream("Content-Type: multipart/alternative;Boundary&#61;\"unique-boundary-2\"");
WriteStream("");
//文本信息
WriteStream("--unique-boundary-2");
WriteStream("Content-Type: text/plain;charset&#61;"&#43;this.charset);
WriteStream("Content-Transfer-Encoding:"&#43;this.encode);
WriteStream("");
WriteStream(body);
WriteStream("");//一个部分写完之后就写如空信息&#xff0c;分段
//html信息
WriteStream("--unique-boundary-2");
WriteStream("Content-Type: text/html;charset&#61;"&#43;this.charset);
WriteStream("Content-Transfer-Encoding:"&#43;this.encode);
WriteStream("");
WriteStream(htmlbody);
WriteStream("");
WriteStream("--unique-boundary-2--");//分隔符的结束符号&#xff0c;尾巴后面多了--
WriteStream("");
//增加附件
Attachment();//这个方法是我们在上面讲过的&#xff0c;实际上他放在这
WriteStream("");
WriteStream("--unique-boundary-1--")
if (!OperaStream(".","250"))//最后写完了&#xff0c;输入"."
{
this.Close(); //关闭连接
}
这就是一封邮件的核心部分&#xff0c;上面的变量都是已定义好的全局变量&#xff0c;由用户传递给对象。整个邮件组件的主要内容到此告一段落。手指都敲酸了&#xff0c;由于本人水平有限&#xff0c;可能有些地方不太让人满意&#xff0c;在此表示歉意。在研究邮件发送之前&#xff0c;在网上四处搜索资料&#xff0c;却没有收获&#xff0c;似乎大家都愿意把经验烂在肚子里&#xff0c;由于我肠胃不够强壮&#xff0c;所以希望能够和大家共同分享这顿美餐。最后我们看看如何应用。
在aspx文件或者其他cs文件中引用&#xff1a;
MailSend Ms&#61;new MailSend();//构造对象
Ms.SMTPServer&#61;"smtp.sohu.com";//传递参数
……
Ms.send();//发送邮件
在此篇文章中我并没有给出完整的代码&#xff0c;而只是给出了代码片段&#xff0c;但是这已经足够整理出整个程序了。这样做的目的是不希望大家看见了就直接拷贝过去使用&#xff0c;希望能够看清楚了&#xff0c;了解了其中的内容再去用&#xff0c;这样对于自己水平的提高才是有帮助的。邮件发送一直是一个比较困扰大家的问题&#xff0c;特别是加上认证程序后&#xff0c;速度又慢&#xff0c;所以我想现在很多邮件群发软件都支持免SMTP邮件发送&#xff0c;等什么时候有空了&#xff0c;也许会做一个免SMTP的邮件发送组件拿出来和大家分享。