c#socket發送郵件詳解

郵件發送在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:   [email protected]  
   
  這是告訴服務器發件人的郵件地址  
   
  RCPT   TO:<收件人地址>   例如:RCPT   TO:   [email protected]  
   
  這是告訴服務器收件人的郵件地址  
   
  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<length)  
   
  page=length/size+1;  
   
  else  
   
  page=length/size;  
   
  for   (int   i=0;i<page;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   用戶不在本地,轉寄到<   P   a   t   h   >  
   
  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的連接,否則我

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<filelist.Rows.Count;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:管理員<[email protected]>  
   
  TO:<姓名><郵件地址>   格式:TO:水生月<[email protected]>  
   
  SUBJECT:<標題>   格式:SUBJECT:今天的天氣很不錯!  
   
  DATE:<時間>   格式:DATE:   Thu,   29   Aug   2002   09:52:47   +0800   (CST)  
   
  REPLY-TO:<郵件地址>   格式:REPLY-TO:[email protected]  
   
  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();//發送郵件  
   
  在此篇文章中我並沒有給出完整的代碼,而只是給出了代碼片段,但是這已經足夠整理出整個程序了。    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章