郵件發送需考慮很多因素,包括發送郵件客戶端(一般編碼實現),發送和接收郵件服務器設置等。如果使用第三方郵件服務器作為發送服務器,就需要考慮該服務器的發送限制,(如發送郵件時間間隔,單位時間內發送郵件數量,是否使用安全連接SSL),同時無論使用第三方還是自己的郵件服務器都還需要考慮接收郵件服務器的限制。為理清思路,下面我們簡單回顧電子郵件系統的基本網絡結構和郵件發送接收流程。
一、電子郵件系統的基本網絡結構
如下圖:
郵件發送接收一般經過以下幾個節點:
- 發送郵件客戶端(Mail User Agent, MUA) : Formail, Outlook, Webmail, C# Code, Java Code, etc.
- 發送郵件服務器(Mail Transfer Agent, MTA) : hMailServer, Exchange, TurboMail, etc.
- 接收郵件服務器(Mail Transfer Agent, MTA)
- 接收郵件客戶端(Mail User Agent, MUA)
發送過程中客戶端與服務器及服務器之間使用SMTP協議,在接收過程中客戶端與服務端之間使用POP3或IMAP(POP3的替代協議,支持郵件摘要顯示和脫機操作)。郵件發送可簡單認為是一種文件傳輸,但與FTP實時文件傳輸不同,各郵件服務器會保存郵件文件本身,直至被下一個郵件服務器或客戶端接收,類似異步與同步的差別。
由上可知,為順利發送和接受郵件,客戶端設置或編碼需要嚴格適應郵件服務器的要求。對於發送郵件需明確:SMTP服務器地址和端口(默認端口25),是否使用安全連接(SSL),驗證憑據(用戶和密碼),及更加細節的郵件格式,郵件編碼方式等;對於接收郵件需明確:POP3或IMAP服務器地址和端口(POP3默認端口110,IMAP默認端口143),是否使用安全連接(SSL),驗證憑據(用戶和密碼)
二、C#下發送郵件組件及測試
C#下發送郵件的組件使用較為普遍的有以下三個:System.Net.Mail, OpenSmtp, LumiSoft.Net。下面我們就分別對他們進行測試。
發送郵件至少需要發送郵件服務器信息和郵件信息,因此我們建立Host和Mail兩個配置類。
public class ConfigHost { public string Server { get; set; } public int Port { get; set; } public string Username { get; set; } public string Password { get; set; } public bool EnableSsl { get; set; } } public class ConfigMail { public string From { get; set; } public string[] To { get; set; } public string Subject { get; set; } public string Body { get; set; } public string[] Attachments { get; set; } public string[] Resources { get; set; } }
同時定義一個統一的接口ISendMail,以方便測試和比較。
public interface ISendMail { void CreateHost(ConfigHost host); void CreateMail(ConfigMail mail); void CreateMultiMail(ConfigMail mail); void SendMail(); }
1、使用System.Net.Mail
System.Net.Mail屬於.Net Framework 的一部分,.Net2.0以后可以使用這個組件。
using System.Net.Mail; public class UseNetMail : ISendMail { private MailMessage Mail { get; set; } private SmtpClient Host { get; set; } public void CreateHost(ConfigHost host) { Host = new SmtpClient(host.Server, host.Port); Host.Credentials = new System.Net.NetworkCredential(host.Username, host.Password); Host.EnableSsl = host.EnableSsl; } public void CreateMail(ConfigMail mail) { Mail = new MailMessage(); Mail.From = new MailAddress(mail.From); foreach (var t in mail.To) Mail.To.Add(t); Mail.Subject = mail.Subject; Mail.Body = mail.Body; Mail.IsBodyHtml = true; Mail.BodyEncoding = System.Text.Encoding.UTF8; } public void CreateMultiMail(ConfigMail mail) { CreateMail(mail); Mail.AlternateViews.Add(AlternateView.CreateAlternateViewFromString("If you see this message, it means that your mail client does not support html.", Encoding.UTF8, "text/plain")); var html = AlternateView.CreateAlternateViewFromString(mail.Body, Encoding.UTF8, "text/html"); foreach (string resource in mail.Resources) { var image = new LinkedResource(resource, "image/jpeg"); image.ContentId = Convert.ToBase64String(Encoding.Default.GetBytes(Path.GetFileName(resource))); html.LinkedResources.Add(image); } Mail.AlternateViews.Add(html); foreach (var attachment in mail.Attachments) { Mail.Attachments.Add(new Attachment(attachment)); } } public void SendMail() { if (Host != null && Mail != null) Host.Send(Mail); else throw new Exception("These is not a host to send mail or there is not a mail need to be sent."); } }
using OpenSmtp.Mail; public class UseOpenSmtp : ISendMail { private MailMessage Mail { get; set; } private Smtp Host { get; set; } public void CreateHost(ConfigHost host) { Host = new Smtp(host.Server, host.Username, host.Password, host.Port); } public void CreateMail(ConfigMail mail) { Mail = new MailMessage(); Mail.From = new EmailAddress(mail.From); foreach (var t in mail.To) Mail.AddRecipient(t, AddressType.To); Mail.HtmlBody = mail.Body; Mail.Subject = mail.Subject; Mail.Charset = "UTF-8"; } public void CreateMultiMail(ConfigMail mail) { CreateMail(mail); foreach (var attachment in mail.Attachments) { Mail.AddAttachment(attachment); } foreach (var resource in mail.Resources) { Mail.AddImage(resource, Convert.ToBase64String(Encoding.Default.GetBytes(Path.GetFileName(resource)))); } } public void SendMail() { if (Host != null && Mail != null) Host.SendMail(Mail); else throw new Exception("These is not a host to send mail or there is not a mail need to be sent."); }
3、使用LumiSoft.Net
LumiSoft.Net是非常強大的開源組件,不僅僅發送郵件,同樣也可用於接收郵件,是個人認為最好的開源組件了。在這里可以詳細了解LumiSoft.Net組件的命名空間,也可以在這里下載其源碼和樣例。
using LumiSoft.Net.SMTP.Client; using LumiSoft.Net.AUTH; using LumiSoft.Net.Mail; using LumiSoft.Net.MIME; public class UseLumiSoft : ISendMail { private SMTP_Client Host { get; set; } private Mail_Message Mail { get; set; } public void CreateHost(ConfigHost host) { Host = new SMTP_Client(); Host.Connect(host.Server, host.Port, host.EnableSsl); Host.EhloHelo(host.Server); Host.Auth(Host.AuthGetStrongestMethod(host.Username, host.Password)); } public void CreateMail(ConfigMail mail) { Mail = new Mail_Message(); Mail.Subject = mail.Subject; Mail.From = new Mail_t_MailboxList(); Mail.From.Add(new Mail_t_Mailbox(mail.From, mail.From)); Mail.To = new Mail_t_AddressList(); foreach (var to in mail.To) { Mail.To.Add(new Mail_t_Mailbox(to, to)); } var body = new MIME_b_Text(MIME_MediaTypes.Text.html); Mail.Body = body; //Need to be assigned first or will throw "Body must be bounded to some entity first" exception. body.SetText(MIME_TransferEncodings.Base64, Encoding.UTF8, mail.Body); } public void CreateMultiMail(ConfigMail mail) { CreateMail(mail); var contentTypeMixed = new MIME_h_ContentType(MIME_MediaTypes.Multipart.mixed); contentTypeMixed.Param_Boundary = Guid.NewGuid().ToString().Replace("-", "_"); var multipartMixed = new MIME_b_MultipartMixed(contentTypeMixed); Mail.Body = multipartMixed; //Create a entity to hold multipart/alternative body var entityAlternative = new MIME_Entity(); var contentTypeAlternative = new MIME_h_ContentType(MIME_MediaTypes.Multipart.alternative); contentTypeAlternative.Param_Boundary = Guid.NewGuid().ToString().Replace("-", "_"); var multipartAlternative = new MIME_b_MultipartAlternative(contentTypeAlternative); entityAlternative.Body = multipartAlternative; multipartMixed.BodyParts.Add(entityAlternative); var entityTextPlain = new MIME_Entity(); var plain = new MIME_b_Text(MIME_MediaTypes.Text.plain); entityTextPlain.Body = plain; plain.SetText(MIME_TransferEncodings.Base64, Encoding.UTF8, "If you see this message, it means that your mail client does not support html."); multipartAlternative.BodyParts.Add(entityTextPlain); var entityTextHtml = new MIME_Entity(); var html = new MIME_b_Text(MIME_MediaTypes.Text.html); entityTextHtml.Body = html; html.SetText(MIME_TransferEncodings.Base64, Encoding.UTF8, mail.Body); multipartAlternative.BodyParts.Add(entityTextHtml); foreach (string attachment in mail.Attachments) { multipartMixed.BodyParts.Add(Mail_Message.CreateAttachment(attachment)); } foreach (string resource in mail.Resources) { var entity = new MIME_Entity(); entity.ContentDisposition = new MIME_h_ContentDisposition(MIME_DispositionTypes.Inline); entity.ContentID = Convert.ToBase64String(Encoding.Default.GetBytes(Path.GetFileName(resource))); //eg.<img src="cid:ContentID"/> var image = new MIME_b_Image(MIME_MediaTypes.Image.jpeg); entity.Body = image; image.SetDataFromFile(resource, MIME_TransferEncodings.Base64); multipartMixed.BodyParts.Add(entity); } } public void SendMail() { if (Host != null && Mail != null) { foreach (Mail_t_Mailbox from in Mail.From.ToArray()) { Host.MailFrom(from.Address, -1); } foreach (Mail_t_Mailbox to in Mail.To) { Host.RcptTo(to.Address); } using (var stream = new MemoryStream()) { Mail.ToStream(stream, new MIME_Encoding_EncodedWord(MIME_EncodedWordEncoding.Q, Encoding.UTF8), Encoding.UTF8); stream.Position = 0;//Need to be reset to 0, otherwise nothing will be sent; Host.SendMessage(stream); Host.Disconnect(); } } else throw new Exception("These is not a host to send mail or there is not a mail need to be sent."); } }
閱讀LumiSoft.Net的源代碼,可以看到LumiSoft.Net編程嚴格遵循了RFC(Request For Comments)定義的協議規范。通過閱讀這些源碼對於了解RFC和其中關於郵件網絡協議規范也是非常有幫助的。如果想查閱RFC文檔可以通過這個鏈接。
在上面的代碼中MIME_MediaTypes類,MIME_TransferEncodings類和Encoding類(System.Text.Encoding)都是或類似於枚舉,設置了郵件內容的編碼方式或解析方式,這個幾個類從根本上決定了郵件的正常傳輸和顯示。MIME_TransferEncodings類設置了文件傳輸編碼,決定郵件頭中的Content-Transfer-Encoding字段的值及其他需要傳輸編碼字段的編碼方式(如標題中的多國語言)。MIME_MediaTypes類設置郵件各部分內容的類型,決定郵件中Content-Type字段的值。而Encoding類不用說,決定了charset的值。關於這些設置的具體作用下文還將提到,這里略過。
4、測試
下表是通過網絡搜集的各大SMTP服務器的配置情況,可以選擇使用這些配置進行測試:
服務商 | SMTP地址 | SMTP端口 | EnableSsl |
gmail | smtp.google.com | 25, 465 or 587 | true |
126 | smtp.126.com | 25 | false |
163 | smtp.126.com | 25 | false |
hotmail | smtp.live.com | 25 | true |
sina | smtp.sina.com | 25 | false |
sohu | smtp.sohu.com | 25 | false |
新建控制台應用程序,測試發送只包含正文的簡單郵件:
class Program { static void Main(string[] args) { var h1 = new ConfigHost() { Server = "smtp.gmail.com", Port = 465, Username = "******@gmail.com", Password = "******", EnableSsl = true };
var m1 = new ConfigMail() { Subject = "Test", Body = "Just a test.", From = "******@gmail.com", To = new string[] { "******@gmail.com" },
}; var agents = new List<ISendMail>() { new UseNetMail(), new UseOpenSmtp(), new UseLumiSoft() }; foreach (var agent in agents) { var output = "Send m1 via h1 " + agent.GetType().Name + " "; Console.WriteLine(output + "start"); try { agent.CreateHost(h1); m1.Subject = output; agent.CreateMail(m1); agent.SendMail(); Console.WriteLine(output + "success"); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.WriteLine(output + "end"); Console.WriteLine("-----------------------------------"); } Console.Read(); } }
通過gmail發送郵件時,OpenSmtp由於不支持SSL發送失敗,NetMail使用587端口能夠成功發送,LumiSoft使用465端口能夠成功發送。查閱Gmail相關文檔,描述說Gmail的465端口使用SSL協議,而587端口使用TLS協議,但587是需要STARTTLS命令支持才能提升為TLS。在命令提示符下測試發現的確需要在發送STARTTLS命令后才能使用TLS協議:
> telnet smtp.gmail.com 587 220 mx.google.com ESMTP o5sm40420786eeg.8 - gsmtp EHLO g1 250-mx.google.com at your service, [173.231.8.212] 250-SIZE 35882577 250-8BITMIME 250-STARTTLS 250-ENHANCEDSTATUSCODES 250 CHUNKING AUTH LOGIN 530 5.7.0 Must issue a STARTTLS command first. o5sm40420786eeg.8 – gsmtpSTARTTLS220
STARTTLS
2.0.0 Ready to start TLS
…
QUIT
對於TLS與STARTTLS人們經常搞混,這里找到一篇關於它們的解釋,請點擊這里。
因而LumiSoft如果連接gmail服務器時還需明確發送STARTTLS命令,已經發現LumiSoft有相關方法SMTP_Client.StartTLS(),連接gmail相較其他smtp服務器還是較為復雜些。另外一些服務器要求郵件配置中的Username必須與From相一致,需要特別注意。
測試發送帶附件和內嵌資源的郵件:
class Program { static void Main(string[] args) { var h2 = new ConfigHost() { Server = "smtp.163.com", Port = 25, Username = "******@163.com", Password = "******", EnableSsl = false }; var m2 = new ConfigMail() { Subject = "Test", Body = "Just a test. <br/><img src='cid:" + Convert.ToBase64String(Encoding.Default.GetBytes("Resource.jpg")) + "' alt=''/> ",
From = "******@163.com", To = new string[] { "******@163.com" },
Attachments = new string[] { @"E:\Test\SendMail\Attachment.pdf" },
Resources = new string[] { @"E:\Test\SendMail\Resource.jpg" } }; var agents = new List<ISendMail>() { new UseNetMail(), new UseOpenSmtp(), new UseLumiSoft() }; foreach (var agent in agents) { var output = "Send m2 via h2 " + agent.GetType().Name + " "; Console.WriteLine(output + "start"); try { agent.CreateHost(h2); m2.Subject = output; agent.CreateMultiMail(m2); agent.SendMail(); Console.WriteLine(output + "success"); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.WriteLine(output + "end"); Console.WriteLine("-----------------------------------"); } Console.Read(); } }