C#實(shí)現(xiàn).Net對(duì)郵件進(jìn)行DKIM簽名和驗(yàn)證,支持附件,發(fā)送郵件簽名后直接投遞到對(duì)方服務(wù)器(無需己方郵件服務(wù)器)

項(xiàng)目地址

https://github.com/xiangyuecn/DKIM-Smtp-csharp

主要支持

  • 對(duì)郵件進(jìn)行DKIM簽名,支持帶附件
  • 對(duì)整個(gè)郵件內(nèi)容(.eml文件)的DKIM簽名進(jìn)行驗(yàn)證
  • 對(duì)MailMessage、SmtpClient進(jìn)行了一次封裝,發(fā)送郵件簡單易用,進(jìn)行DKIM簽名后直接投遞到對(duì)方服務(wù)器(無需己方郵件服務(wù)器)

DKIM簽名、驗(yàn)證規(guī)則

對(duì)于DKIM的簽名和驗(yàn)證規(guī)則,QQ郵箱的《DKIM指引》這個(gè)文章已經(jīng)寫的足夠詳細(xì),就不搬運(yùn)了。

還不行還可以去參考DKIM.Net庫對(duì)簽名的實(shí)現(xiàn)。

舉個(gè)例子

//創(chuàng)建DKIM簽名對(duì)象
var dkim = new EMail_DKIM("domain.com", "dkimSelector", new RSA.RSA(/*"-----BEGIN RSA PRIVATE KEY-----....", true*/ 1024));

//通過EMail類來操作發(fā)郵件
using (var email = new EMail("mx1.qq.com", 25)) {
    //使用簽名
    email.TryUseDKIM(dkim);

    email.FromEmail = "test@test.test";
    email.ToEmail("11111111@qq.com");//改成有效的郵箱地址

    //發(fā)送郵件出去,去垃圾箱找,如果私鑰是域名設(shè)置的話正常點(diǎn)
    var res = email.Send("標(biāo)題", "內(nèi)容");
    Console.WriteLine(res.IsError ? "發(fā)送失敗:" + res.ErrorMessage : "發(fā)送成功");
}

//直接給MailMessage簽名
var msg = new MailMessage("test@test.test", "11111111@qq.com");
msg.SubjectEncoding = msg.BodyEncoding = msg.HeadersEncoding = Encoding.UTF8;
msg.Subject = "標(biāo)題";
msg.Body = "內(nèi)容";
msg.Attachments.Add(new Attachment(new MemoryStream(Encoding.UTF8.GetBytes("abc文本內(nèi)容123")), "文本.txt"));

//簽名
Console.WriteLine(dkim.Sign(msg).IsError ? "簽名失敗" : "簽名完成");

//獲取郵件內(nèi)容
var eml = EMail_DKIM_MailMessageText.ToRAW(msg).Value;

//驗(yàn)證eml文件簽名
Console.WriteLine(dkim.Verify(eml) ? "驗(yàn)證通過" : "驗(yàn)證未通過");

//郵件整體內(nèi)容
Console.WriteLine(eml.Raw);

輸出:

發(fā)送失敗:郵件發(fā)送出錯(cuò):郵箱不可用。 服務(wù)器響應(yīng)為:Mailbox not found. http://servi
ce.mail.qq.com/cgi-bin/help?subtype=1&&id=20022&&no=1000728
簽名完成
驗(yàn)證通過
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應(yīng)該能夠直接打開,經(jīng)目測看起來沒什么卵用的文件都svn:ignore掉了(svn滑稽。

前言、自述、還有啥

在實(shí)現(xiàn)郵件發(fā)送時(shí)發(fā)現(xiàn)就算不把郵件投遞到自己的郵件服務(wù)器(由郵件服務(wù)服務(wù)器進(jìn)行發(fā)送給對(duì)方),有些郵箱(QQ郵箱)不會(huì)拒絕,但有郵箱直接就拒絕了(網(wǎng)易郵箱)。對(duì)比由郵件服務(wù)器發(fā)送的和直接發(fā)送的郵件內(nèi)容的區(qū)別,發(fā)現(xiàn)直接發(fā)送缺少了DKIM-Signature郵件頭。

好了,缺少那就加上。但.Net的MailMessage、SmtpClient簡陋到一份郵件發(fā)送到一個(gè)Stream的接口都不舍得暴露(任性寫入到文件夾不給文件名卻支持),直接就沒有支持簽名的頭緒。那自行實(shí)現(xiàn)。

研究了一下RFC 6376長篇大論看不懂(主要沒給一個(gè)簡單的實(shí)現(xiàn)步驟),然后QQ給的簡單易懂多了(流式.清晰)。簽名和驗(yàn)證算法就清楚了。

發(fā)現(xiàn)DKIM.Net

要簽名先搞定bodyhash計(jì)算,body怎么獲取?一堆附件、一堆轉(zhuǎn)碼...... MailMessageSmtpClient沒給獲取body的支持。然后找到一個(gè)庫 DKIM.Net,他里面實(shí)現(xiàn)了獲取整個(gè)郵件內(nèi)容的方法,簡單調(diào)用一下MailMessage的私有方法搞定。

然后遇到了DKIM.Net也沒有搞定的問題,對(duì)于帶附件AttachmentsAlternateViews的郵件,由于每次獲取的郵件內(nèi)容因?yàn)?code>boundary分隔符(邊界)不一致導(dǎo)致簽名無效,DKIM.Net是直接粗暴的拒絕multipart格式郵件的簽名的。然后翻閱.NET Framework MimeMultiPart源碼找到了以下代碼:

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;
}

這個(gè)方法只會(huì)在MimeMultiPart初始化時(shí)調(diào)用一次,MimeMultiPart的初始化時(shí)機(jī)在MailMessage.Send調(diào)用時(shí),通過MailMessage.SetContent來初始化。而私有方法MailMessage.Send是發(fā)送郵件時(shí)才會(huì)調(diào)用到的,我們獲取郵件內(nèi)容也是通過這個(gè)方法。如果我們通過手段使MimeMultiPart.GetNextBoundary返回的boundary相同,那么每次獲取的郵件內(nèi)容也會(huì)相同了(Date相同的情況下)。

發(fā)現(xiàn)DotNetDetour

然后就是尋找控制MimeMultiPart.GetNextBoundary函數(shù)的方法。研究了半天反射,沒有找到頭緒,反射能替換一個(gè)類實(shí)例的方法為另一個(gè)方法?然后順著查找C# hook,找到多篇一樣的內(nèi)容,還是看原創(chuàng)吧《自己寫的一個(gè)可以hook .net方法的庫》,內(nèi)容本身并不感冒(沒看懂),但結(jié)尾一句話但hook一般都需要dll注入來配合,因?yàn)閔ook自身進(jìn)程沒什么意義,hook別人的進(jìn)程才有意義,咦,搞自己,有意思,然后仔細(xì)看了一下代碼,沒錯(cuò)!這就是我要的功能,修改類的一個(gè)方法為另外一個(gè)方法!DotNetDetour庫。

Date隱患

因?yàn)楹灻桶l(fā)送是在不同時(shí)間內(nèi),就有可能導(dǎo)致簽名時(shí)是8:05.999,而發(fā)送時(shí)是8:06.001,從而導(dǎo)致帶Date header的簽名失敗,但簽名時(shí)建議攜帶Date header一起簽名。

so 這個(gè)問題hook System.Net.Mail.Message.PrepareHeaders 可以解決,每次原始函數(shù)處理完成后我們獲取System.Net.Mail.Message.Headers,然后把Date header刪掉,然后寫入我們可以控制的值。

對(duì)DotNetDetour的修改

測試DotNetDetour過程中發(fā)現(xiàn)他不能 hook 非public的方法,然后魔改了一下Monitor.cs,主要在反射獲取類的方法的時(shí)候添加了flags參數(shù),用來獲取類的所有方法。

使用中給IMethodMonitor接口加了一個(gè)void SetMethod(MethodInfo method)方法,用來把原始方法信息傳遞給我們自己的方法,簡化我們自己函數(shù)內(nèi)的反射操作(獲取.Net框架內(nèi)的類型敲的字符串比較復(fù)雜,有了MethodInfo就是一個(gè)屬性調(diào)用的事)。

準(zhǔn)備好了

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

參考文章:

C#發(fā)送DKIM簽名的郵件》:發(fā)現(xiàn)DKIM.Net

RFC 6376》:DKIM簽名規(guī)則

DKIM指引》:QQ提供的DKIM簽名、驗(yàn)證規(guī)則文檔

自己寫的一個(gè)可以hook .net方法的庫》:發(fā)現(xiàn)DotNetDetour

DKIM 測試》:測試簽名,測試前提:需要有一個(gè)自己的域名,并配置郵箱域名的DKIM公鑰

方法文檔

EMail_DKIM.cs

郵件進(jìn)行DKIM簽名和驗(yàn)證的所有代碼都在里面。

EMail_DKIM類:提供簽名Sign和驗(yàn)證Verify

EMail_DKIM_RAW_EML類:提供ParseOrNull用來解析一封.eml文件內(nèi)容。

EMail_DKIM_MailMessageText類:提供ToRAW用來獲取MailMessage的全部內(nèi)容,并轉(zhuǎn)成EMail_DKIM_RAW_EML格式。

EMail.cs

封裝的一個(gè)發(fā)送郵件的功能。

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

EMail_Unit.cs

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

/Lib/RSA-csharp目錄

這個(gè)目錄里面是我的RSA-csharp倉庫代碼,用來解析PEM秘鑰對(duì)的。

/Lib/DotNetDetour目錄

這個(gè)目錄里面是DotNetDetour庫,使用的這個(gè)版本代碼。已經(jīng)修改過,用來支持私有方法的hook。

任意郵箱收件地址查詢

比如qq郵箱,smtp.qq.com這種是發(fā)件用的地址,不是收件地址,接收郵件的地址需要進(jìn)行mx查詢。有了收件地址就可以發(fā)送任意郵件給他,他收不收是另外一回事,比如偽造發(fā)件人。

mx查詢方法:

比如查詢qq郵箱的收件地址

> nslookup
> set type=mx
> qq.com

非權(quán)威應(yīng)答:
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

然后響應(yīng)的mail exchanger就是收件地址,隨便挑一個(gè)給他發(fā)垃圾郵件。

郵箱域名DKIM公鑰查詢

要驗(yàn)證一份郵件的簽名,需要先獲取公鑰(有私鑰用私鑰驗(yàn)證也可以)。給個(gè)郵箱然后查詢公鑰的方法(比如QQ郵箱):

步驟1:打開郵件源碼獲取到DKIM-Signature中的s參數(shù)(selector),QQ為s201512
步驟2:和QQ郵箱拼接出ns txt記錄名稱:s201512._domainkey.qq.com

> nslookup
> set type=txt
> s201512._domainkey.qq.com

非權(quán)威應(yīng)答:
s201512._domainkey.qq.com       text =

        "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDPsFIOSteMStsN6
15gUWK2RpNJ/B/ekmm4jVlu2fNzXADFkjF8mCMgh0uYe8w46FVqxUS97habZq6P5jmCj/WvtPGZAX49j
mdaB38hzZ5cUmwYZkdue6dM17sWocPZO8e7HVdq7bQwfGuUjVuMKfeTB3iNeo6/hFhb9TmUgnwjpQIDA
QAB"

然后響應(yīng)的text內(nèi)的p參數(shù)就是公鑰了,copy出來拼成PEM格式就可以拿來進(jìn)行DKIM驗(yàn)證。

線上DKIM簽名測試

測試需要有一個(gè)域名并且配置好相應(yīng)ns DKIM的 txt記錄。

本次測試實(shí)例代碼:

var rsa = new RSA.RSA(@"-----BEGIN RSA PRIVATE KEY-----
私鑰內(nèi)容
-----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("測試", "測試內(nèi)容");
Console.WriteLine(res.IsError?"發(fā)送失敗:"+res.ErrorMessage:"發(fā)送成功");

本次測試報(bào)告:見images/report-7ea72484.txt

相關(guān)截圖

測試線上結(jié)果:

測試結(jié)果

開始線上測試:

開始測試

控制臺(tái)運(yùn)行:

控制臺(tái)運(yùn)行
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容