MailMessage
、SmtpClient
進行了一次封裝,發送郵件簡單易用,進行DKIM簽名後直接投遞到對方服務器(無需己方郵件服務器)對於DKIM的簽名和驗證規則,QQ郵箱的《DKIM指引》這個文章已經寫的足夠詳細,就不搬運了。git
還不行還能夠去參考DKIM.Net庫對簽名的實現。github
//建立DKIM簽名對象
var dkim = new EMail_DKIM("domain.com", "dkimSelector", new RSA.RSA(/*"-----BEGIN RSA PRIVATE KEY-----....", true*/ 1024));
//經過EMail類來操做發郵件
using (var email = new EMail("mx1.qq.com", 25)) {
//使用簽名
email.TryUseDKIM(dkim);
email.FromEmail = "test@test.test";
email.ToEmail("11111111@qq.com");//改爲有效的郵箱地址
//發送郵件出去,去垃圾箱找,若是私鑰是域名設置的話正常點
var res = email.Send("標題", "內容");
Console.WriteLine(res.IsError ? "發送失敗:" + res.ErrorMessage : "發送成功");
}
//直接給MailMessage簽名
var msg = new MailMessage("test@test.test", "11111111@qq.com");
msg.SubjectEncoding = msg.BodyEncoding = msg.HeadersEncoding = Encoding.UTF8;
msg.Subject = "標題";
msg.Body = "內容";
msg.Attachments.Add(new Attachment(new MemoryStream(Encoding.UTF8.GetBytes("abc文本內容123")), "文本.txt"));
//簽名
Console.WriteLine(dkim.Sign(msg).IsError ? "簽名失敗" : "簽名完成");
//獲取郵件內容
var eml = EMail_DKIM_MailMessageText.ToRAW(msg).Value;
//驗證eml文件簽名
Console.WriteLine(dkim.Verify(eml) ? "驗證經過" : "驗證未經過");
//郵件總體內容
Console.WriteLine(eml.Raw);
複製代碼
輸出:算法
發送失敗:郵件發送出錯:郵箱不可用。 服務器響應爲:Mailbox not found. http://servi
ce.mail.qq.com/cgi-bin/help?subtype=1&&id=20022&&no=1000728
簽名完成
驗證經過
MIME-Version: 1.0
From: test@test.test
To: 11111111@qq.com
Date: Sun, 11 Nov 2018 05:31:55 +000
Subject: =?utf-8?B?5qCH6aKY?=
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=domain.com;
s=dkimSelector; q=dns/txt; t=1541914315; h=Date:From:Subject:To;
bh=iKgtfjx6cvO8YCUPyjjnbHU9jziQ+q1c/Hrz0aRDb98=;
b=CidpxecyNHkZGsIQGnUD8eQwrEGS+Nx09RUOff6hU/7H1DV50m/h0xqRLFlgskiqm1r0exDTPf/zS
CKui1WWNO5iKXSZt9/3s0YN9fhliP72c0GRIJ8DM3tQilVYgFnayK61jmvCW0gtrPd3biDdMp/s+Arq8
lWD6CbQfBMIPmQ=
Content-Type: multipart/mixed; boundary=--boundaryhRN0aXVHKzDLi76qUZTq
----boundaryhRN0aXVHKzDLi76qUZTq
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64
5YaF5a65
----boundaryhRN0aXVHKzDLi76qUZTq
Content-Type: application/octet-stream; name="=?utf-8?B?5paH5pysLnR4dA==?="
Content-Transfer-Encoding: base64
Content-Disposition: attachment
YWJj5paH5pys5YaF5a65MTIz
----boundaryhRN0aXVHKzDLi76qUZTq--
複製代碼
clone下來用vs應該可以直接打開,經目測看起來沒什麼卵用的文件都svn:ignore掉了(svn滑稽。c#
在實現郵件發送時發現就算不把郵件投遞到本身的郵件服務器(由郵件服務服務器進行發送給對方),有些郵箱(QQ郵箱
)不會拒絕,但有郵箱直接就拒絕了(網易郵箱
)。對比由郵件服務器發送的和直接發送的郵件內容的區別,發現直接發送缺乏了DKIM-Signature
郵件頭。bash
好了,缺乏那就加上。但.Net的MailMessage
、SmtpClient
簡陋到一份郵件發送到一個Stream
的接口都不捨得暴露(任性寫入到文件夾不給文件名卻支持),直接就沒有支持簽名的頭緒。那自行實現。服務器
研究了一下RFC 6376長篇大論看不懂(主要沒給一個簡單的實現步驟),而後QQ給的簡單易懂多了(流式.清晰)。簽名和驗證算法就清楚了。app
要簽名先搞定body
的hash計算
,body
怎麼獲取?一堆附件、一堆轉碼...... MailMessage
、SmtpClient
沒給獲取body
的支持。而後找到一個庫 DKIM.Net,他裏面實現了獲取整個郵件內容的方法,簡單調用一下MailMessage
的私有方法搞定。框架
而後遇到了DKIM.Net
也沒有搞定的問題,對於帶附件Attachments
或AlternateViews
的郵件,因爲每次獲取的郵件內容由於boundary
分隔符(邊界)不一致致使簽名無效,DKIM.Net
是直接粗暴的拒絕multipart
格式郵件的簽名的。而後翻閱.NET Framework MimeMultiPart源碼找到了如下代碼:dom
internal string GetNextBoundary() {
int b = Interlocked.Increment(ref boundary) - 1;
string boundaryString = "--boundary_" + b.ToString(CultureInfo.InvariantCulture)+"_"+Guid.NewGuid().ToString(null, CultureInfo.InvariantCulture);
return boundaryString;
}
複製代碼
這個方法只會在MimeMultiPart
初始化時調用一次,MimeMultiPart
的初始化時機在MailMessage.Send
調用時,經過MailMessage.SetContent
來初始化。而私有方法MailMessage.Send
是發送郵件時纔會調用到的,咱們獲取郵件內容也是經過這個方法。若是咱們經過手段使MimeMultiPart.GetNextBoundary
返回的boundary
相同,那麼每次獲取的郵件內容也會相同了(Date相同的狀況下)。
而後就是尋找控制MimeMultiPart.GetNextBoundary
函數的方法。研究了半天反射,沒有找到頭緒,反射能替換一個類實例的方法爲另外一個方法?而後順着查找C# hook
,找到多篇同樣的內容,仍是看原創吧《本身寫的一個能夠hook .net方法的庫》,內容自己並不感冒(沒看懂),但結尾一句話但hook通常都須要dll注入來配合,由於hook自身進程沒什麼意義,hook別人的進程纔有意義
,咦,搞本身,有意思,而後仔細看了一下代碼,沒錯!這就是我要的功能,修改類的一個方法爲另一個方法!DotNetDetour庫。
由於簽名和發送是在不一樣時間內,就有可能致使簽名時是8:05.999,而發送時是8:06.001,從而致使帶Date header的簽名失敗,但簽名時建議攜帶Date header一塊兒簽名。
so 這個問題hook System.Net.Mail.Message.PrepareHeaders
能夠解決,每次原始函數處理完成後咱們獲取System.Net.Mail.Message.Headers
,而後把Date header刪掉,而後寫入咱們能夠控制的值。
測試DotNetDetour
過程當中發現他不能 hook 非public的方法,而後魔改了一下Monitor.cs
,主要在反射獲取類的方法的時候添加了flags參數,用來獲取類的全部方法。
使用中給IMethodMonitor
接口加了一個void SetMethod(MethodInfo method)
方法,用來把原始方法信息傳遞給咱們本身的方法,簡化咱們本身函數內的反射操做(獲取.Net框架內的類型敲的字符串比較複雜,有了MethodInfo就是一個屬性調用的事)。
有了DKIM.Net
提供的思路來獲取郵件內容,搬來DotNetDetour
hook修改 .Net系統類的方法,郵件DKIM簽名唾手可得~
參考文章:
《RFC 6376》:DKIM簽名規則
《DKIM指引》:QQ提供的DKIM簽名、驗證規則文檔
《本身寫的一個能夠hook .net方法的庫》:發現DotNetDetour
《DKIM 測試》:測試簽名,測試前提:須要有一個本身的域名,並配置郵箱域名的DKIM公鑰
郵件進行DKIM簽名和驗證的全部代碼都在裏面。
EMail_DKIM
類:提供簽名Sign
和驗證Verify
。
EMail_DKIM_RAW_EML
類:提供ParseOrNull
用來解析一封.eml文件內容。
EMail_DKIM_MailMessageText
類:提供ToRAW
用來獲取MailMessage
的所有內容,並轉成EMail_DKIM_RAW_EML
格式。
封裝的一個發送郵件的功能。
主要提供TimeoutMillisecond
,ClientName
設置,一堆添加附件的方法AddAttachment(x,x,x)
,最後Send
發送郵件。
封裝的一些通用方法,如:base64。都是比較周邊的功能。
這個目錄裏面是個人RSA-csharp倉庫代碼,用來解析PEM祕鑰對的。
這個目錄裏面是DotNetDetour庫,使用的這個版本代碼。已經修改過,用來支持私有方法的hook。
好比qq郵箱,smtp.qq.com
這種是發件用的地址,不是收件地址,接收郵件的地址須要進行mx查詢。有了收件地址就能夠發送任意郵件給他,他收不收是另一回事,好比僞造發件人。
mx查詢方法:
好比查詢qq郵箱的收件地址
> nslookup
> set type=mx
> qq.com
非權威應答:
qq.com MX preference = 30, mail exchanger = mx1.qq.com
qq.com MX preference = 20, mail exchanger = mx2.qq.com
qq.com MX preference = 10, mail exchanger = mx3.qq.com
複製代碼
而後響應的mail exchanger就是收件地址,隨便挑一個給他發垃圾郵件。
要驗證一份郵件的簽名,須要先獲取公鑰(有私鑰用私鑰驗證也能夠)。給個郵箱而後查詢公鑰的方法(好比QQ郵箱):
步驟1:打開郵件源碼獲取到DKIM-Signature
中的s參數(selector
),QQ爲s201512
步驟2:和QQ郵箱拼接出ns txt記錄名稱:s201512._domainkey.qq.com
> nslookup
> set type=txt
> s201512._domainkey.qq.com
非權威應答:
s201512._domainkey.qq.com text =
"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDPsFIOSteMStsN6
15gUWK2RpNJ/B/ekmm4jVlu2fNzXADFkjF8mCMgh0uYe8w46FVqxUS97habZq6P5jmCj/WvtPGZAX49j
mdaB38hzZ5cUmwYZkdue6dM17sWocPZO8e7HVdq7bQwfGuUjVuMKfeTB3iNeo6/hFhb9TmUgnwjpQIDA
QAB"
複製代碼
而後響應的text內的p參數就是公鑰了,copy出來拼成PEM格式就能夠拿來進行DKIM驗證。
測試須要有一個域名而且配置好相應ns DKIM的 txt記錄。
本次測試實例代碼:
var rsa = new RSA.RSA(@"-----BEGIN RSA PRIVATE KEY-----
私鑰內容
-----END RSA PRIVATE KEY-----
", true);
var mail = new EMail("mail.appmaildev.com", 25);
mail.TryUseDKIM(new EMail_DKIM("email.jiebian.life", "email", rsa));
mail.FromEmail = "test-7ea72484@email.jiebian.life";
mail.ToEmail("test-7ea72484@appmaildev.com");
var res=mail.Send("測試", "測試內容");
Console.WriteLine(res.IsError?"發送失敗:"+res.ErrorMessage:"發送成功");
複製代碼
本次測試報告:見images/report-7ea72484.txt
測試線上結果:
開始線上測試:
控制檯運行: