使用C#製作《郵件特快專遞》

作者:蔡曉暉
漳州師範學院082信箱

下載源代碼

paragraph.gif一、前言

  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均爲真實的。

paragraph.gif二、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 這樣的形式。

paragraph.gif三、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截包如圖:

catch.gif

  當前的圖示顯示出的包是第二個報文,即從DNS回覆過來的報文。第1個報文是查詢報文,比回覆報文簡單多了。因此如果分析得懂回覆報文,查詢報文相信聰明如你一樣可以輕鬆搞定。
  圖中顯示的紅色部分的數據爲DNS報文包,上邊的數據內容分別爲MAC協義包、PPP協議包,IPv4協議包及UDP協議包。這些包不在本文範圍內,不關注。
  (插說一些廢話:點選Iris左側樹中顯示的條目,在右邊的數據包中不一定正確反映,這點我一開始不知道,點中一個資源後按其錯誤的指示分析,分析了半天,也不知所言。我對包的分析是在此次寫這個程序中學會的,以前大二時網絡課沒有好好去聽我們學院那個少有的工程師兼教授的網絡課,也怪我們系,咋不叫一個漂亮的MM過來教,那我會非常專心地聽的:)。言歸正傳)
  爲了說明的方便,我把包中經色的DNS協議部分處理一下,如下圖:

data.gif

  其中紅色部分爲包頭,藍色爲查詢部分,綠色爲回覆的第一條資源記錄,金色爲回覆的第二條資源記錄。其他的沒有划起來的就權威記錄、額外記錄,不要分析考慮。

紅色部分:
第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記錄查詢的編程要點分析。

paragraph.gif四、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類中,我做了詳細的代碼註釋,如果您有一些開發經驗的話,應該很容易看得懂的,如果有疑問的話,歡迎聯繫我。我在精力及能力許可的範圍內幫您解答。 

paragraph.gif五、分析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

paragraph.gif六、郵件發送程序編程要點分析
  我們先定義一個郵件結構,以描述郵件的各個屬性。
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();

  結束。

  程序的界面截圖:

NoSmtpMail.gif

paragraph.gif七、最後的話

  非常感謝您很有耐心地看完我的囉嗦。我花了四個通宵(其實是早上睡覺)時間編寫軟件及教程的辛苦值得了。這份軟件算我暑假學習半個月C#後的一份比較好的成品吧。我是從大二開始學習真正的編程,用了九個月的VB及寫了一年又三個月的C++/MFC程序。現在在學C#。感覺有C++的基礎入門C#挺容易的。
  本文件的郵件附件的發送方法參照羅前輩(http://www.luocong.com)的C++源碼,不然可能又要多發上一天去思考了。在此感謝羅老師的無私。
  寫完這個東東後,再過四天,我的暑假就結束了,下個學期是大四了。很迷惘的一個學年,我想在畢業後能到上海找到一份好工作。我想請業內的前輩指引我一下,以我現在的水平要跨過上海大軟件公司的門檻還要爬多高?我在這剩下一年內會更加努力去超越的 。

您可以任意地傳播本軟件、源碼及相關文檔,但請保留完整性。
如若用於商業用途需經我的同意。

paragraph.gif我的聯繫方式:
  通訊地址:漳州師範學院082信箱(363000)
  姓名:蔡曉暉
  電郵:mail.gif
  QQ:22415

希望我們能交個朋友。

  如果一年內有問題需要我解答的話,請到http://bbs.zzsy.com上的【信息技術】版塊發貼詢問。
我會在精力及能力許可範圍內幫您解答的。

Programmed by 蔡曉暉
2004.9.7 - 2004.9.9

發佈了73 篇原創文章 · 獲贊 6 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章