使用C# 創建郵件發送組件(SMTP協議)

轉自: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: [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
  
  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:水生月<[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();//發送郵件
  
  
  在此篇文章中我並沒有給出完整的代碼,而只是給出了代碼片段,但是這已經足夠整理出整個程序了。這樣做的目的是不希望大家看見了就直接拷貝過去使用,希望能夠看清楚了,瞭解了其中的內容再去用,這樣對於自己水平的提高才是有幫助的。郵件發送一直是一個比較困擾大家的問題,特別是加上認證程序後,速度又慢,所以我想現在很多郵件羣發軟件都支持免SMTP郵件發送,等什麼時候有空了,也許會做一個免SMTP的郵件發送組件拿出來和大家分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章