[原创]使用C#制作《邮件特快专递》

类别:.NET开发 点击:0 评论:0 推荐:
源代码的下载地址(到下边的页面去下:
http://itboy.cn/product/index.asp
一、前言
  foxmail新版中有一个《邮件特快专递》的功能。起先搞不懂如何用,后来知道要在 工具->系统选项 那边设置 本地DNS服务器的IP地址。
  觉得这个新功能蛮好用的。不需要通过SMTP代理,可以直接通过本地往邮箱所在的邮件交换器发送邮件。在暑假一开始想在VC++中实现这个功能。用IRIS截包后,发现程序后mx8.263.net发送邮箱,不知道这个是什么东西所以作罢。   后来才想到这个就是263.net的MX记录主机,原来特快专递的原理就是往这个主机上发送数据就行。
  运行nslookup程序:
  set type=mx
  263.net

  有了,有了,得到结果:
  Non-authoritative answer:
  263.net MX preference = 10, mail exchanger = mx06.263.net
  263.net MX preference = 10, mail exchanger =
mx08.263.net
  263.net MX preference = 10, mail exchanger = mx09.263.net
  263.net MX preference = 10, mail exchanger = mx11.263.net
  263.net MX preference = 10, mail exchanger = mx12.263.net
  263.net MX preference = 40, mail exchanger = mx03.263.net
  263.net MX preference = 10, mail exchanger = mx01.263.net

  没有错了。就是这个了。后来因为不知道怎么实现nslookup的功能,就放弃了,学了半个多月的C#。后来偶然在网上查找到了一些相关的文档。几次实验。把我的开发过程拿过来分享,我第一次写教程性文档。所以不规范之处,请大家包涵。本文涉及的域名、邮箱及IP均为真实的。

二、DNS协议原理
  我认为,要想成为一个好的网络软件程序员,必须得读懂RFC文档。因为本文是面向大多广泛程序爱好者,所以我尽量从细节上写,如果高手的话,可以跳过此部分。
  DNS协议的相关RFC文档:
   RFC1034-《DOMAIN NAMES - CONCEPTS AND FACILITIES
  RFC1035-《DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION
  网上的计算机用形如 220.162.75.1 这样称为IP地址的数字串来标识一台计算机。而如果每次访问一台计算机都是通过输入这样的东东来访问,那不就太可怕了?以是出了DNS这样的好东东,用要指示其绑定的IP地址,当我们在浏览器内输入 http://zzsy.com 时,浏览器不知道网页该到哪里取,于是就向设定好的DNS服务器查询zzsy.com这个域名。DNS服务器会先寻找自己的记录库,如果没有发现就转向上一级DNS服务器进行查询(转发请求)。把找到后的IP告知你的浏览器。这里边浏览器查询的记录类型是A记录。RFC1035文档第11页中定义有16种记录类型,而常见的有A(地址)记录、CNAME(别名)记录、MX(邮件交换)记录。我们本篇要关心的是MX记录。
  查询的过程一般是:客户向DNS服务器的53端口发送UDP报文,DNS服务器收到后进行处理,并把结果记录仍以UDP报文的形式返回过来。
  此UDP报文的一般格式:
 
    +---------------------+ 
    |        报文头       | 
    +---------------------+ 
    |         问题       | 向服务器提出的查询部分 
    +---------------------+ 
    |         回答       | 服务器回复的资源记录 
    +---------------------+ 
    |         授权        | 权威的资源记录 
    +---------------------+ 
    |        格外的       | 格外的资源记录 
    +---------------------+ 
  除了报文头是固定的12字节外,其他每一部分的长度均为不定字节数。
  我们在这边关心的是报文头、问题、回答这三个部分。

  其中报文头的格式:
 
                                    1  1  1  1  1  1 
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                      ID                       | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                    QDCOUNT                    | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                    ANCOUNT                    | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                    NSCOUNT                    | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                    ARCOUNT                    | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 

  好家伙,是什么鬼画符!
  其中最上边是位的数字标识,0-15(注意,后边的10-15写成上下的形式了,一开始我楞没看懂)。
  接下来是:
  ID:占16位,2个字节。此报文的编号,由客户端指定。DNS回复时带上此标识,以指示处理的对应请应请求。
  QR:占1位,1/8字节。0代表查询,1代表DNS回复
  Opcode:占4位,1/2字节。指示查询种类:0:标准查询;1:反向查询;2:服务器状态查询;3-15:未使用。
  AA:占1位,1/8字节。是否权威回复。
  TC:占1位,1/8字节。因为一个UDP报文为512字节,所以该位指示是否截掉超过的部分。
  RD:占1位,1/8字节。此位在查询中指定,回复时相同。设置为1指示服务器进行递归查询。
  RA:占1位,1/8字节。由DNS回复返回指定,说明DNS服务器是否支持递归查询。
  Z:占3位,3/8字节。保留字段,必须设置为0。
  RCODE:占4位,1/2字节。由回复时指定的返回码:0:无差错;1:格式错;2:DNS出错;3:域名不存在;4:DNS不支持这类查询;5:DNS拒绝查询;6-15:保留字段。 
  QDCOUNT:占16位,2字节。一个无符号数指示查询记录的个数。
  ANCOUNT:占16位,2字节。一个无符号数指明回复记录的个数。
  NSCOUNT:占16位,2字节。一个无符号数指明权威记录的个数。
  ARCOUNT:占16位,2字节。一个无符号数指明格外记录的个数。

  其中每个查询的资源记录格式:

 
                                    1  1  1  1  1  1 
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                                               | 
    /                     QNAME                     / 
    /                                               / 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                     QTYPE                     | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                     QCLASS                    | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

  QNAME:不定长,表示要查询的域名。(两边的方框用 / 来表示不定长)
  QTYPE:2字节,根据RFC1035及nslookup的帮助文档,我定义以下枚举类型:
enum QueryType //查询的资源记录类型。
{
A=0x01, //指定计算机 IP 地址。
NS=0x02, //指定用于命名区域的 DNS 名称服务器。
MD=0x03, //指定邮件接收站(此类型已经过时了,使用MX代替)
MF=0x04, //指定邮件中转站(此类型已经过时了,使用MX代替)
CNAME=0x05, //指定用于别名的规范名称。
SOA=0x06, //指定用于 DNS 区域的“起始授权机构”。
MB=0x07, //指定邮箱域名。
MG=0x08, //指定邮件组成员。
MR=0x09, //指定邮件重命名域名。
NULL=0x0A, //指定空的资源记录
WKS=0x0B, //描述已知服务。
PTR=0x0C, //如果查询是 IP 地址,则指定计算机名;否则指定指向其它信息的指针。
HINFO=0x0D, //指定计算机 CPU 以及操作系统类型。
MINFO=0x0E, //指定邮箱或邮件列表信息。
MX=0x0F, //指定邮件交换器。
TXT=0x10, //指定文本信息。
UINFO=0x64, //指定用户信息。
UID=0x65, //指定用户标识符。
GID=0x66, //指定组名的组标识符。
ANY=0xFF //指定所有数据类型。
};

QTYPE:2字节。 根据RFC1035及nslookup的帮助文档,我定义以下枚举类型:
enum QueryClass //指定信息的协议组。
{
IN=0x01, //指定 Internet 类别。
CSNET=0x02, //指定 CSNET 类别。(已过时)
CHAOS=0x03, //指定 Chaos 类别。
HESIOD=0x04,//指定 MIT Athena Hesiod 类别。
ANY=0xFF //指定任何以前列出的通配符。
};

  QTYPE中的A,MX,CNAME为常用,QCLASS中的IN为常用。

  其中每个回复的记录格式:

 
                                    1  1  1  1  1  1 
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                                               | 
    /                                               / 
    /                      NAME                     / 
    |                                               | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                      TYPE                     | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                     CLASS                     | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                      TTL                      | 
    |                                               | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
    |                   RDLENGTH                    | 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| 
    /                     RDATA                     / 
    /                                               / 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 
 
  NAME:回复查询的域名,不定长。
  TYPE:回复的类型。2字节,与查询同义。指示RDATA中的资源记录类型。
  CLASS:回复的类。2字节,与查询同义。指示RDATA中的资源记录类。
  TTL:生存时间。4字节,指示RDATA中的资源记录在缓存的生存时间。
  RDLENGTH:长度。2字节,指示RDATA块的长度。
  RDATA:资源记录。不定义,依TYPE的不同,此记录的格示不同,通常一个MX记录是由一个2字节的指示该邮件交换器的优先级值及不定长的邮件交换器名组成的。

  这边述说一下名称的组合形式。名称由多个标识序列组成,每一个标识序列的首字节说明该标识符的长度,接着用是ASCII码表示字符,多个序列之后由字节0表示名字结束。其中某一个标识序列的首字符的长度若是0xC0的话,表示下一字节指示不是标识符序列,而是指示接下部分在本接收包内的偏移位置。
  比如 bbs.zzsy.com 以.分开bbs、zzsy、com三个部分。每个部分的长度为3、4、3
  在DNS报文中的形式就如 3 b b s 4 z z s y 3 c o m 0
  假如在包内的第12个字节位置存在有 4 z z s y 3 c o m 0 这样的名称了。
  那此时有可能为:3 b b s 4 z z s y 0xC0 0x0C 这样的形式。

三、DNS协议实例讲解
  说了这么多理论屁话,可能头都有两个大了吧。还是用一个实例的方法来说明吧。
  我选用著名的网络截包及协议分析工具IRIS 4.05,您可以从我的站点上下载:
  http://itboy.cn/data/Iris405Full.rar
  运行Iris,点击菜单的Filters 选 Port标签页 运用 53 端口后点确定。
  点击Iris工具栏上的绿色运行图标进行监听。

  在windows中运行nslookup程序。
  输入以下命令:
   set type=mx
  然后返回nslookup程序。
  再输入命令:
   yahoo.com.cn
  会得到
   yahoo.com.cn MX preference = 20, mail exchanger = mx5.mail.yahoo.com
   yahoo.com.cn MX preference = 10, mail exchanger = mta-v1.mail.vip.cnb.yahoo.com
  这样的两个MX资源记录。此时的Iris截包如图:


  当前的图示显示出的包是第二个报文,即从DNS回复过来的报文。第1个报文是查询报文,比回复报文简单多了。因此如果分析得懂回复报文,查询报文相信聪明如你一样可以轻松搞定。
  图中显示的红色部分的数据为DNS报文包,上边的数据内容分别为MAC协义包、PPP协议包,IPv4协议包及UDP协议包。这些包不在本文范围内,不关注。
  (插说一些废话:点选Iris左侧树中显示的条目,在右边的数据包中不一定正确反映,这点我一开始不知道,点中一个资源后按其错误的指示分析,分析了半天,也不知所言。我对包的分析是在此次写这个程序中学会的,以前大二时网络课没有好好去听我们学院那个少有的工程师兼教授的网络课,也怪我们系,咋不叫一个漂亮的MM过来教,那我会非常专心地听的:)。言归正传)
  为了说明的方便,我把包中经色的DNS协议部分处理一下,如下图:

  其中红色部分为包头,蓝色为查询部分,绿色为回复的第一条资源记录,金色为回复的第二条资源记录。其他的没有划起来的就权威记录、额外记录,不要分析考虑。

红色部分:
第0字节,第1字节 00 03 标识一个ID。
第2字节,第3字字 81 80 化成二进制形式 1000 0001 1000 0000
QR(0)为1表示是回复报文。
Opcode(2-5)为0表示标准查询。
AA(6)为0表示非权威查询。
TC(7)为0表示不超过512字节的包不截断。
RD(8)为1表示nslookup程序指示DNS进行递归查询。
RA(9)为1表示DNS支持递归查询。
Z(10-12)保留字段
RCODE(13-16)为0表示查询无查错
第4字节,第5字节 00 01 表示查询的资源记录数为1
第6字节,第7字节 00 02 表示回复的资源记录数为2
第8字节,第9字节 00 06 表示权威的资源记录数为6
第10字节,第11字节 00 04 表示额外的资源记录数为4

接着到达了蓝色部分,查询的资源记录部分:
一开始是查询的域名。
开始 05 表示后边的五个字节是序列字符,把 79 61 68 6F 6F 转为ASCII码为yahoo
到达 03 表示后边的三个字节是序列字符,把 63 6F 6D 转为ASCII码为com
到达 02 表示后边的二个字节是序列字符,把 63 6E 转为ASCII码为cn
到达 00 表示结束。
整个串起来,用.连接,即为:yahoo.com.cn
接下来的两个字节 00 0F 表示查询类型为15,即为MX记录查询。
再接下来两个字节 00 01 表示查询类为1,即为Internet连接。
Iris包中的第一个包中的DNS报文只包含红线及蓝线,分析方法相同。不再赘述。

绿色部分,回复的第一个资源记录部分:
一开始为查询的域名,
首字节C0表示压缩,接下来的位为偏移字节位。
接下来是0C表示跳到整个包中的第12个字节上,即为蓝色的第一个字节。
然后接着与上边的分析相同,得到域名为yahoo.com.cn
然后返回,到下一个字节
绿色包中的3-4字节 00 0F 表示查询类型为15,即为MX记录查询。
再接下来的5-6字节 00 01 表示查询类为1,即为Internet连接。
再接下来的7-10字节 00 00 05 95 化为十进制等于1429秒。即缓存时间为23分49秒。
接着11-12字节 00 16 指示本资源记录的数据部分为22字节。即剩下来的字节数。
从13字节开始的22字节为MX记录的数据部分。此部分的格式为两字节的邮件交换器优先级值,不定长的邮件交换器名。
第13-14字节 00 14 表示优先级值为20
接着从15开始是邮件交换器名部分,依蓝色的域名分析方法,得到mx5.mail.yahoo.com
(此段所说字节均是绿色块中的相对位置,而非整个DNS包的绝对位置)

金色那块的方析方法与绿色的大同小异,但是有一点不同的是,看金色包的邮件交换器名部分。
从包的绝对位置,第78位(图中标紫色78的那里)开始是邮件交换器名。我们进行分析,得到mta-v1.mail.vip.cnb此时接下来是0XC0表示压缩,再接下来的偏移是0x0C又跳到包的12字节上,得到yahoo.com.cn。整个合起来就是第二资源记录的邮件交换器名:mta-v1.mail.vip.cnb.yahoo.com

  这样的结果与nslookup运行结果:
   yahoo.com.cn MX preference = 20, mail exchanger = mx5.mail.yahoo.com
   yahoo.com.cn MX preference = 10, mail exchanger = mta-v1.mail.vip.cnb.yahoo.com
  相同。

  希望此节能帮助网络爱好者学会包的分析方法。下边我们现行DNS中MX记录查询的编程要点分析。

四、DNS查询MX记录编程要点分析
  foxmail 5.0 的邮件特快专递有一个很垃圾的地方是无法自动获得本地ISP的DNS服务器,还需要用户的手工输入,我想是因为API的局限或是foxmail开发组没有想到方法或是其他的不为我知的缘故吧。不管他,在C#中利用WMI,很容易的:

 
string[] dnses; 
ManagementClass mc = new ManagementClass("Win32_NetworkAdapterConfiguration"); 
ManagementObjectCollection moc = mc.GetInstances(); 

//枚举当前机子上的所有网卡 
foreach(ManagementObject mo in moc) 
{ 
    if((bool)mo["ipEnabled"]) 
    { 
        dnses = (string[]) mo["DNSServerSearchOrder"]; 
        if (dnses!=null) 
        { 
            dnsServer=dnses[0];    //使用第一个找到的DNS服务器。 
        } 
    } 
}
  当然,您不会忘记在 项目->添加引用 中加入对System.Management的引用吧?
  把这个放在DnsQuery类的静态构造函数中。

  还有一个自定义的MxRecord结构,用于存放一个MX资源记录的信息。
 
    struct MxRecord 
    { 
        public string domain;    //查询的域名 
        public QueryType queryType;    //查询类型 
        public QueryClass queryClass;    //查询类 
        public TimeSpan liveTime;    //生存时间 
        public int dataLength;    //资源部分的长度,即指示邮件服务器的优先级及名称的那部分资源的字节数。 
        public int preference;    //优先级值,其值越小越优先. 
        public string name;    //邮件交换器名 
    } 
  把两个字节组成一个INT16型的整数无法用到BitConverter类,因为与DNS服务器的端位法不同。所以来是用 << 的方法进行位移。
  其他的就不多说了,在程序的DnsQuery类中,我做了详细的代码注释,如果您有一些开发经验的话,应该很容易看得懂的,如果有疑问的话,欢迎联系我。我在精力及能力许可的范围内帮您解答。 

五、分析Foxmail的特快专递发送数据。

  1.运行ipconfig/all
  把得到的Dns Servers的第一个IP地址记录下来。

  2.还是运行Iris.设置Filters为SMTP,25端口。运行以便监听。

  3.运行Foxmail5.0,在 工具 -> 系统设置 内的“邮件特快专递”标签页设定域名服务器1为刚刚的IP地址。
  点“撰写”,用文本文件的格式编写。
  收件人:[email protected]
  抄送:
  主题:你好dreamchild,冒昧打饶
  内容:
  尊敬的dreamchild先生:
    这是一封邮件。
  附件:(添加 mm.gif 及 说明.txt 两个文件。为了保证此教程的完整性,把这两个额外的东东也放进包内了。)

  4.完成后停止Iris,点其菜单栏的Decode 得到以下内容:
 
220 Welcome to coremail System(With Anti-Spam) 2.1 for 263(040326) 
HELO dreamchild 
250 mta6.x263.net 
MAIL FROM: <> 
250 Ok 
RCPT TO:  
250 Ok 
Data 
354 End data with . 
Date: Thu, 9 Sep 2004 01:00:35 +0800 
From: "=?gb2312?B?w87Qobqi?=" <> 
To: "dreamchild"  
Subject: =?gb2312?B?xOO6w2RyZWFtY2hpbGSjrMOww8G08sjE?= 
X-mailer: Foxmail 5.0 [cn] 
Mime-Version: 1.0 
Content-Type: multipart/mixed; 
    boundary="=====001_Dragon788446150325_=====" 

This is a multi-part message in MIME format. 

--=====001_Dragon788446150325_===== 
Content-Type: text/plain; 
    charset="gb2312" 
Content-Transfer-Encoding: base64 

1/C+tLXEZHJlYW1jaGlsZM/Iyfqjug0KoaGhodXiysfSu7fi08q8/qGjDQo= 
--=====001_Dragon788446150325_===== 
Content-Type: image/gif; 
    name="MM.GIF" 
Content-Transfer-Encoding: base64 
Content-Disposition: attachment; 
    filename="MM.GIF" 

R0lGODlheAB4AIddABwNCixIjJtjkLO8iZux0NK2xpVJT9XayJWNuDA0Vt5Xh+Sw7dLc7p6Fh3mD 
(这边省略去关于一陀 MM.gif 文件内容的Base64编码) 
w75UTdHFHwD/1Ex6cBSs0jHpKaySFXI4XCwAEhPkRsUSBQQAOw== 

--=====001_Dragon788446150325_===== 
Content-Type: application/octet-stream; 
    name="=?gb2312?B?y7XD9y50eHQ=?=" 
Content-Transfer-Encoding: base64 
Content-Disposition: attachment; 
    filename="=?gb2312?B?y7XD9y50eHQ=?=" 

1eK49k1Nv8mwrrK7v8mwrqGjDQo= 

--=====001_Dragon788446150325_=====-- 

. 
250 Ok: queued as 7CDA613A26E 
QUIT 
221 Bye 

  这边涉及到的就是SMTP协议了,其中文版的RFC821文档 http://itboy.cn/data/rfc821.doc
  因为是中文版的,所以大家花些时间看,这边就不再赘述原理了。只列出状态标识
   211 系统状态或系统帮助响应
   214 帮助信息
   220 服务就绪
   221 服务关闭传输信道
   250 要求的邮件操作完成
   251 用户非本地,将转发向
   354 开始邮件输入,以.结束
   421 服务未就绪,关闭传输信道(当必须关闭时,此应答可以作为对任何命令的响应)
   450 要求的邮件操作未完成,邮箱不可用(例如,邮箱忙)
   451 放弃要求的操作;处理过程中出错
   452 系统存储不足,要求的操作未执行
   500 格式错误,命令不可识别(此错误也包括命令行过长)
   501 参数格式错误
   502 命令不可实现
   503 错误的命令序列
   504 命令参数不可实现
   550 要求的邮件操作未完成,邮箱不可用(例如,邮箱未找到,或不可访问)
   551 用户非本地,请尝试
   552 过量的存储分配,要求的操作未执行
   553 邮箱名不可用,要求的操作未执行(例如邮箱格式错误)
   554 操作失败

  可以从此看出,与一般的通过SMTP代理不同的是少了SMTP服务器的指定及其验证的用户名跟密码。
  描述一下整个过程:
  首先通过前述的方法得到263.net的一个邮件交换器,然后连到这个交换器上。然后连到此服务器的25端口上,
  服务器返回220。
  然后依次指示用户名,发送邮箱(人),接收邮箱(人)。接收写入邮件的数据。
  数据分为邮件头及邮件的正文两部分。
  邮件头包含:时间,发送邮箱(人),接收邮箱(人),主题,发信程序,MIME版本号,邮件内容的类型及分割符。
  当中有一些用BASE64编码的字符串就是原来的中文汉字,其实,我们在制作无SMTP代理邮件发送程序时可以直接写成中文的。
  这边就讲一下邮件内容的类型及分割符,其他的很容易理解的。
  这边的邮件内容类型是 multipart/mixed; 说明是由多种格式混合成的。
  分隔符,是用于分隔邮件内容部分与各个附件。用boundary关键字及键值来定义。
  比如本例用=====001_Dragon788446150325_=====来表示,这边有一个细节问题,键值最好要用"引起来,并不要出现空格。举个例子,如果你用boundary======001_Dragon788446150325_=====来表示的话,那FOXMAIl5.0将无法正确对邮件进行处理,邮件的内容部分被当成整个BASE64乱码文本,然而我登陆到263.net的网站去收信可以看到邮件被正常转化。
  而邮件的内容部分是通过两个减号--再连上分隔符来分隔各部分的。

  邮件主体从第一个--=====001_Dragon788446150325_=====开始,到第二个--=====001_Dragon788446150325_=====为内容的第一部分
  Content-Type: text/plain;
  charset="gb2312" Content-Transfer-Encoding: base64

  这两句说明了其类型及内容的字符集和编码。
  在这边是指定的是base64,然后一个空行,再加上“尊敬的dreamchild先生:\r\n    这是一封邮件。”这个字符串的BASE64编码构成邮件的正文部分。
  实际上,我们可以指定 Content-Transfer-Encoding:8bit然后就可以在正文部分用上原本表示了。

  接下来是隔开的附件1部分,
  多了一个Content-Disposition: attachment;以说明这部分是附件,以及相关的文件名filename="MM.GIF"。
  附件内容部分是把文件读成一个字节数组,然后把字节数组转为base64编码的字符串。这边的是mm.gif这个文件内容。

  第三部分是附件2 测试.txt 文件,测试.txt 又被foxmail处理成base64格式了,可以用原文表示的。
  最后完了之后,用“回车换行加一个.号再一个回车换行”表示Data部分的结束。

  如若正确过发送到达服务器,那就返回一个250状态。
  然后用Quit命令跟服务器3166

六、邮件发送程序编程要点分析
  我们先定义一个邮件结构,以描述邮件的各个属性。
 
    public struct MailContent    //邮件的内容 
    { 
        public string to;//收件人地址  
        public string toname;//收件人姓名  
        public string from;//发件人地址  
        public string fromname;//发件人姓名 

        public string title;//主题  
        public string body;//文本内容  

        public bool useAttachment; //是否使用附件 
        public string [] attachmentList; //附件列表  
    } 
      
  接着从各控件中取值,赋给这个结构的实例。
  其中取值过程中包括判断邮箱格式是否正确,我们用正则表达式来判断。如下:
  Regex.IsMatch(邮箱字符串, @"^([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$")
  如果返回的值为假的话,就用错误提示器提示。
  赋值完后,就开创一个工作线程进行邮件的发送。
  线程一开始先通过当地DNS取得邮箱域名部分的邮件交换器列表。然后取得其最小优先级值的交换器用于以后的发送。 比如取得[email protected]这个邮箱的域名263.net的一个优先级值为10的MX记录mx12.263.net
  然后就是连到服务器发送命令及接收返回信息。以下列出一些代码:

  关于创建Sock套接字:
 
    TcpClient sock = new TcpClient(); 
    NetworkStream netStream;         
     
    sock.NoDelay = true;    //不使用延时算法,以加快小数据包的发送。 
    sock.ReceiveTimeout = 10000; //接收超时为10秒。 
    sock.Connect(server,port); 
    netStream = sock.GetStream(); 
  Socket的Nagle算法将降低小数据报的发送速度,而系统默认是使用Nagle算法。所以设置NoDelay为true关掉它,以加快小数据的发送。

  关于Send方法:
 
    byte [] sendArray = Encoding.Default.GetBytes(sendString);         
    netStream.Write(sendArray,0,sendArray.Length); 
  先把字符串转为字节数组再发送出去

  关于Receive方法:
 
    const int MaxReceiveSize = 1460; 
    .... 
    byte [] buffer=new byte[MaxReceiveSize]; 
         
    length = netStream.Read(buffer, 0, MaxReceiveSize); 
    if (length == 0) 
        return null; 
    ....     
    receiveString = Encoding.Default.GetString(buffer, 0, length); 
  根据“在SOCK_STREAM方式下,如果单次发送数据超过1460,系统将分成多个数据报传送,在对方接受到的将是一个数据流,应用程序需要增加断帧的判断。当然可以采用修改注册表的方式改变1460的大小,但MicrcoSoft认为1460是最佳效率的参数,不建议修改。”
  这边设置一次接收1460字节,但其实返回码用不了这么多字节,这个是以前我在写基于HTTP网络程序时写的接收函数一部分,保留了这个方法。
  接收到后就再转回字符串返回。
 
    send = "Date: {$time}\r\n" 
    + "From: {$fromname} <{$from}>\r\n" 
    + "To: {$toname} <{$to}>\r\n" 
    + "Subject: {$title}\r\n" 
    + "X-mailer: NoSmtpSender [蔡晓晖 制作]\r\n" 
    + "MIME_Version:1.0\r\n" 
    + "Content-type:multipart/mixed;Boundary=\"{$splitline}\"\r\n" 
    + "\r\n" //头部结束,开始正文部分。  
    + "--{$splitline}\r\n" //内容部分。 
    + "Content-type:text/plain;Charset=gb2312\r\n" 
    + "Content-Transfer-Encoding:8bit\r\n" 
    + "\r\n"  
    + "{$body}\r\n" 
    + "\r\n"; 
     
    attachment = ""; 
    attachment += "--{$splitline}\r\n"; 
    attachment += "Content-Type:application/octet-stream;Name={$filename}\r\n"; 
    attachment += "Content-Disposition:attachment;FileName={$filename}\r\n"; 
    attachment += "Content-Transfer-Encoding:Base64\r\n"; 
    attachment += "\r\n"; 
    attachment = attachment.Replace("{$splitline}",splitLine); //先进行替换,以防加上附件内容后替换耗时。 
    attachment = attachment.Replace("{$filename}",file.Name); 
    attachment += Convert.ToBase64String(fileBytes,0,length); 
    attachment += "\r\n\r\n"; 
    send += attachment;  
  按上一节的分析,把格式给列出来,然后依次替换{$变量名}就OK了,其中time变量名的获得方法:
   DateTime.Now.ToString("R").Replace("GMT","中国标准时间")
  我不知道是不是VS2003的BUG,用"R"参数输出当前时间的RFC格式的格林威治时间,没有把本土化时间自动转换,比如现在的中国时间9点,它没有减去8小时。而只是直接在当前时间后边加上“GMT”。于是我们就把“GMT”改“中国标准时间”了以校时。

  按一问一答的方式与邮件交换器“交流”,如果其间有错的话,就把错误的信息保存下来,然后return false;
  如果整个发送过程没有错的话就可以
   netStream.Close();
   sock.Close();
  结束。

  程序的界面截图:



七、最后的话
  非常感谢您很有耐心地看完我的啰嗦。我花了四个通宵(其实是早上睡觉)时间编写软件及教程的辛苦值得了。这份软件算我暑假学习半个月C#后的一份比较好的成品吧。我是从大二开始学习真正的编程,用了九个月的VB及写了一年又三个月的C++/MFC程序。现在在学C#。感觉有C++的基础入门C#挺容易的。
  本文件的邮件附件的发送方法参照罗前辈(http://www.luocong.com)的C++源码,不然可能又要多发上一天去思考了。在此感谢罗老师的无私。
  写完这个东东后,再过四天,我的暑假就结束了,下个学期是大四了。很迷惘的一个学年,我想在毕业后能到上海找到一份好工作。我想请业内的前辈指引我一下,以我现在的水平要跨过上海大软件公司的门槛还要爬多高?我在这剩下一年内会更加努力去超越的的。

您可以任意地传播本软件、源码及相关文档,但请保留完整性。
如若用于商业用途需经我的同意。
我的联系方式:
通讯地址:漳州师范学院082信箱(363000)
姓名:蔡晓晖
电邮:
QQ:22415
希望我们能交个朋友。

如果一年内有问题需要我解答的话,请到http://bbs.zzsy.com上的【信息技术】版块发贴询问。
我会在精力及能力许可范围内帮您解答的。
Programmed by 蔡晓晖
2004.9.7 - 2004.9.9

本文地址:http://com.8s8s.com/it/it44172.htm