有時我們需要在網站中加入發送郵件的功能,例如一個網上投稿系統,當稿件被採用的時候發送郵件通知作者。下面就以這個功能為例說明如何?自動發送郵件。
實現發送郵件功能 首先說一下在.Net下如何發送郵件。.Net已經為我們準備好了與發送郵件相關的類,只要直接調用即可,非常方便。下面是我自己寫的一個郵件通知類: /// <summary> /// 郵件通知服務類。 /// </summary> public class EmailNotificationService { /// <summary> /// 構造一個郵件通知服務類的執行個體。 /// </summary> /// <param name="smtpService">SMTP伺服器的IP地址</param> /// <param name="enableSSL">是否使用SSL串連SMTP伺服器器</param> /// <param name="port">SMTP伺服器連接埠</param> /// <param name="loginName">用於登入SMTP伺服器的使用者名稱</param> /// <param name="password">登入密碼</param> public EmailNotificationService( string smtpService, bool enableSSL, int port, string loginName, string password) { this.m_smtpService = smtpService; this.m_loginName = loginName; this.m_password = password; this.m_enableSSL = enableSSL; this.m_port = port; } private readonly string m_smtpService; private readonly string m_loginName; private readonly string m_password; private readonly bool m_enableSSL; private readonly int m_port; /// <summary> /// 發送郵件通知到指定的EMAIL地址。 /// </summary> /// <param name="senderName">顯示在“寄件者”一欄上的名稱</param> /// <param name="address">目的EMAIL地址</param> /// <param name="title">郵件標題</param> /// <param name="content">郵件內容</param> public void SendTo(string senderName, string address, string title, string content) { MailMessage mail = new MailMessage(); mail.To.Add(address); mail.From = new MailAddress(this.m_loginName, senderName, Encoding.UTF8); mail.Subject = title; mail.Body = content; mail.BodyEncoding = Encoding.UTF8; mail.IsBodyHtml = false; mail.Priority = MailPriority.Normal; SmtpClient smtp = new SmtpClient(); smtp.Credentials = new NetworkCredential(this.m_loginName, this.m_password); smtp.Host = this.m_smtpService; smtp.EnableSsl = this.m_enableSSL; smtp.Port = this.m_port; smtp.Send(mail); } }在使用時,首先構造一個EmailNotificationService類,再調用SendTo方法即可。例如: EmailNotificationService mailNotificationService = new EmailNotificationService("smtp.gmail.com", true, 587, "LoginName@gmail.com", "LoginPassword"); mailNotificationService.SendTo("SenderName", "TargetAddress@qq.com", "Title", "Content"); 發送郵件實現方案 上面建立好了一個負責發送郵件的類,接下來的問題是應該在什麼時候調用這個類。寄送電子郵件需要進行網路通訊,耗時比較多,而且SmtpClient的Send方法是會阻塞調用線程的,一旦調用了該方法,就要等到郵件發送完畢或出錯才能結束方法調用,所以不能將對EmailNotificationService的調用放在ASP.NET頁面的代碼中。如果這麼做,用戶端就要等待很長時間才能獲得響應,使用者體驗是比較差的。 SmtpClient還有一個SendAsync方法,該方法與Send方法的區別是,SendAsync是非同步,調用該方法之後會產生一個新的線程來負責發送郵件,之後調用線程立即返回,不會再等待郵件發送結束。那麼我們是不是可以用SendAsync代替Send,並在頁面代碼中調用呢?答案是否定的,雖然用戶端可以很快獲得相應,但郵件根本沒有發送出去。這是由ASP.NET頁面生命週期的特性決定的,用戶端向伺服器的每一次請求,頁面都會經曆一個由產生到銷毀的過程,當頁面銷毀的時候,負責發送郵件的線程還沒有完成發送郵件的工作就被強制結束了。 由於ASP.NET頁面生命週期的特性,我們不能將調用代碼放在頁面的代碼中。我們需要一個與頁面無關的線程,一個在網站運行時始終存在的線程。我的方案是使用一個全域對象來管理一個髮送郵件線程,同時維護一個待發送郵件鏈表。當全域對象建立的時候,鏈表中沒有任何內容,發送郵件線程處於掛起狀態。當某個頁面中的處理需要寄送電子郵件時,就將與發送郵件相關的資訊添加到待發送郵件鏈表中。此時鏈表不為空白,發送郵件線程開始工作,逐個取出鏈表中的郵件資訊並發送,一直到鏈表為空白,再次進入掛起狀態。如此迴圈反覆。 實現發送郵件功能 基本的構思已經確定好了,接下來就是寫代碼實現了。首先定義一個類來封裝待發送郵件的相關資訊,本文開頭已經說過要以一個網上投稿系統作為例子,所以這裡所用的資訊與該應用有關。 /// <summary> /// 封裝發送郵件時所需資訊的類。 /// </summary> public class MailNotifyInfo { /// <summary> /// 擷取或設定稿件的標題。 /// </summary> public string Title { get; set; } /// <summary> /// 擷取或設定稿件的作者名稱。 /// </summary> public string Author { get; set; } /// <summary> /// 擷取或設定作者的電子郵件地址。 /// </summary> public string EmailAddress { get; set; } /// <summary> /// 擷取或設定稿件的狀態。 /// </summary> public ArticleStatus ArticleStatus { get; set; } } 然後是全域對象類的定義,我使用了單件模式來實現其全域性。 /// <summary> /// 處理郵件發送功能的類。 /// </summary> public class NotificationHandler { /// <summary> /// 該類的靜態執行個體。 /// </summary> private static readonly NotificationHandler g_instance = new NotificationHandler(); /// <summary> /// 擷取該類的唯一執行個體。 /// </summary> public static NotificationHandler Instance { get { return g_instance; } } /// <summary> /// 預設構造方法。 /// </summary> private NotificationHandler() { this.m_lockObject = new object(); this.m_mailNotifyInfos = new LinkedList<MailNotifyInfo>(); this.m_threadEvent = new ManualResetEvent(false); this.m_workThread = new Thread(this.ThreadStart); this.m_workThread.Start(); } private readonly LinkedList<MailNotifyInfo> m_mailNotifyInfos; private readonly Thread m_workThread; private readonly ManualResetEvent m_threadEvent; private readonly Object m_lockObject; /// <summary> /// 添加待發送郵件的相關資訊。 /// </summary> public void AppendNotification(MailNotifyInfo mailNotifyInfo) { lock (this.m_lockObject) { this.m_mailNotifyInfos.AddLast(mailNotifyInfo); if (this.m_mailNotifyInfos.Count != 0) { this.m_threadEvent.Set(); } } } /// <summary> /// 發送郵件線程的執行方法。 /// </summary> private void ThreadStart() { while (true) { this.m_threadEvent.WaitOne(); MailNotifyInfo mailNotifyInfo = this.m_mailNotifyInfos.First.Value; EmailNotificationService mailNotificationService = new EmailNotificationService("smtp.gmail.com", true, 587, "LoginName@gmail.com", "LoginPassword"); mailNotificationService.SendTo("稿件中心", mailNotifyInfo.EmailAddress, "稿件狀態變更通知", String.Format("{0}你的稿件{1}狀態已變更為{2}", mailNotifyInfo.Author, mailNotifyInfo.Title, mailNotifyInfo.ArticleStatus)); lock (this.m_lockObject) { this.m_mailNotifyInfos.Remove(mailNotifyInfo); if (this.m_mailNotifyInfos.Count == 0) { this.m_threadEvent.Reset(); } } } } 該類比較簡單,首先在建構函式中初始化成員變數,然後啟動發送郵件線程,此時該線程是掛起的。 當外部調用AppendNotification方法時,會在鏈表中添加一個MailNotifyInfo對象,然後喚醒發送郵件線程。由於在生產環境下可能會出現同時調用AppendNotification方法的情形,所以這裡要進行同步。 發送郵件線程喚醒後進入一個死迴圈,等待事件對象觸發。當事件對象出發之後就開始發送郵件了。郵件發送完畢後從鏈表中刪除已發送的郵件,然後檢查鏈表是否為空白,如果是則重設事件對象,重新進入掛起狀態。同樣地,在對鏈表進行操作時也要進行同步。 至此,發送郵件的功能實現完畢。需要發送郵件的時候只要像這樣調用即可: MailNotifyInfo mailNotifyInfo = new MailNotifyInfo(); ..... NotificationHandler.Instance.AppendNotification(mailNotifyInfo); 這隻是一個很粗陋的架構,而且還不完善。例如,這裡假設網站是不間斷啟動並執行系統,沒有考慮當網站關閉時發送郵件線程的處理。大家可以在這個基礎上添磚加瓦,使其更加完善。另外,自動發送郵件也是常見的功能,例如定時檢查某個條件,如果成立則發送郵件。要實現自動發送郵件的話,只要對本文的方案稍加修改即可:在NotificationHandler中添加一個Timer,定時執行某個方法,在這個方法中進行條件檢查並觸發事件即可。