C#實現.Net對郵件進行DKIM簽名和驗證,支持附件,發送郵件簽名後直接投遞到對方服務器(無需己方郵件服務器)

項目地址

github.com/xiangyuecn/…html

主要支持

  • 對郵件進行DKIM簽名,支持帶附件
  • 對整個郵件內容(.eml文件)的DKIM簽名進行驗證
  • MailMessageSmtpClient進行了一次封裝,發送郵件簡單易用,進行DKIM簽名後直接投遞到對方服務器(無需己方郵件服務器)

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的MailMessageSmtpClient簡陋到一份郵件發送到一個Stream的接口都不捨得暴露(任性寫入到文件夾不給文件名卻支持),直接就沒有支持簽名的頭緒。那自行實現。服務器

研究了一下RFC 6376長篇大論看不懂(主要沒給一個簡單的實現步驟),而後QQ給的簡單易懂多了(流式.清晰)。簽名和驗證算法就清楚了。app

發現DKIM.Net

要簽名先搞定bodyhash計算body怎麼獲取?一堆附件、一堆轉碼...... MailMessageSmtpClient沒給獲取body的支持。而後找到一個庫 DKIM.Net,他裏面實現了獲取整個郵件內容的方法,簡單調用一下MailMessage的私有方法搞定。框架

而後遇到了DKIM.Net也沒有搞定的問題,對於帶附件AttachmentsAlternateViews的郵件,因爲每次獲取的郵件內容由於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相同的狀況下)。

發現DotNetDetour

而後就是尋找控制MimeMultiPart.GetNextBoundary函數的方法。研究了半天反射,沒有找到頭緒,反射能替換一個類實例的方法爲另外一個方法?而後順着查找C# hook,找到多篇同樣的內容,仍是看原創吧《本身寫的一個能夠hook .net方法的庫》,內容自己並不感冒(沒看懂),但結尾一句話但hook通常都須要dll注入來配合,由於hook自身進程沒什麼意義,hook別人的進程纔有意義,咦,搞本身,有意思,而後仔細看了一下代碼,沒錯!這就是我要的功能,修改類的一個方法爲另一個方法!DotNetDetour庫。

Date隱患

由於簽名和發送是在不一樣時間內,就有可能致使簽名時是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的修改

測試DotNetDetour過程當中發現他不能 hook 非public的方法,而後魔改了一下Monitor.cs,主要在反射獲取類的方法的時候添加了flags參數,用來獲取類的全部方法。

使用中給IMethodMonitor接口加了一個void SetMethod(MethodInfo method)方法,用來把原始方法信息傳遞給咱們本身的方法,簡化咱們本身函數內的反射操做(獲取.Net框架內的類型敲的字符串比較複雜,有了MethodInfo就是一個屬性調用的事)。

準備好了

有了DKIM.Net提供的思路來獲取郵件內容,搬來DotNetDetour hook修改 .Net系統類的方法,郵件DKIM簽名唾手可得~

參考文章:

C#發送DKIM簽名的郵件》:發現DKIM.Net

RFC 6376》:DKIM簽名規則

DKIM指引》:QQ提供的DKIM簽名、驗證規則文檔

本身寫的一個能夠hook .net方法的庫》:發現DotNetDetour

DKIM 測試》:測試簽名,測試前提:須要有一個本身的域名,並配置郵箱域名的DKIM公鑰

方法文檔

EMail_DKIM.cs

郵件進行DKIM簽名和驗證的全部代碼都在裏面。

EMail_DKIM類:提供簽名Sign和驗證Verify

EMail_DKIM_RAW_EML類:提供ParseOrNull用來解析一封.eml文件內容。

EMail_DKIM_MailMessageText類:提供ToRAW用來獲取MailMessage的所有內容,並轉成EMail_DKIM_RAW_EML格式。

EMail.cs

封裝的一個發送郵件的功能。

主要提供TimeoutMillisecond,ClientName設置,一堆添加附件的方法AddAttachment(x,x,x),最後Send發送郵件。

EMail_Unit.cs

封裝的一些通用方法,如:base64。都是比較周邊的功能。

/Lib/RSA-csharp目錄

這個目錄裏面是個人RSA-csharp倉庫代碼,用來解析PEM祕鑰對的。

/Lib/DotNetDetour目錄

這個目錄裏面是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就是收件地址,隨便挑一個給他發垃圾郵件。

郵箱域名DKIM公鑰查詢

要驗證一份郵件的簽名,須要先獲取公鑰(有私鑰用私鑰驗證也能夠)。給個郵箱而後查詢公鑰的方法(好比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驗證。

線上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

相關截圖

測試線上結果:

測試結果

開始線上測試:

開始測試

控制檯運行:

控制檯運行
相關文章
相關標籤/搜索