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