涉及到的Github倉庫:
https://github.com/xiangyuecn/DKIM-Smtp-csharp
.Net開發(fā)者社區(qū)富文本編輯器太難用了道逗,還是簡書的編輯器好用摊鸡,然后掘金的版面好看孤里,最后還是喜歡cnblog里面可以修改版面css累舷。
盡瞎說大實(shí)話知纷。
重新碼一份好看的壤圃。
出問題的地方
出現(xiàn)問題的函數(shù):
//https://source.dot.net/#System.Net.Mail/System/Net/Mail/SmtpClient.cs,966
//https://referencesource.microsoft.com/#System/net/System/Net/mail/SmtpClient.cs,892
void SendMailCallback(IAsyncResult result) {
...
//注意這個ServerSupportsEai,這個位置是allowUnicode參數(shù)
message.BeginSend(writer, DeliveryMethod != SmtpDeliveryMethod.Network,
ServerSupportsEai, new AsyncCallback(SendMessageCallback), result.AsyncState);
...
}
ServerSupportsEai
所在位置為allowUnicode
參數(shù)琅轧。SmtpClient
中所有涉及到allowUnicode
參數(shù)的地方伍绳,賦值都為IsUnicodeSupported()
返回值。但唯一這一處是例外乍桂。
我們看看IsUnicodeSupported
函數(shù):
//https://referencesource.microsoft.com/#System/net/System/Net/mail/SmtpClient.cs,382
private bool IsUnicodeSupported() {
if (DeliveryMethod == SmtpDeliveryMethod.Network) {
//注意看這里的ServerSupportsEai和SmtpDeliveryFormat
return (ServerSupportsEai && (DeliveryFormat == SmtpDeliveryFormat.International));
}
else {
return (DeliveryFormat == SmtpDeliveryFormat.International);
}
}
DeliveryFormat
我們可以賦值冲杀,我們來找找ServerSupportsEai
是在哪里取值的:
//https://referencesource.microsoft.com/#System/net/System/Net/mail/smtpconnection.cs,280
internal void ParseExtensions(string[] extensions) {
...
//如果服務(wù)器支持SMTPUTF8,那么ServerSupportsEai=true
else if (String.Compare(extension, 0, "SMTPUTF8", 0, 8, StringComparison.OrdinalIgnoreCase) == 0) {
((SmtpPooledStream)pooledStream).serverSupportsEai = true;
}
...
}
產(chǎn)生的現(xiàn)象
SendMailCallback
是SmtpClient.SendAsync
(SendMailAsync
會調(diào)用SendAsync
)調(diào)用的睹酌,so权谁,異步操作已經(jīng)完全不受我們設(shè)置的DeliveryFormat
參數(shù)控制了,UTF-8內(nèi)容(如中文)轉(zhuǎn)不轉(zhuǎn)碼完全看對方郵件服務(wù)器心情H碳病4炒!
SmtpClient
對象DeliveryFormat
屬性賦值為SmtpDeliveryFormat.SevenBit
卤妒,要求郵件使用 7 位 ASCII 的傳遞格式甥绿,并且用異步方法來發(fā)送;本來Subject
则披、附件文件名
等里面的UTF-8內(nèi)容(如中文)將會被轉(zhuǎn)碼共缕;但如果郵件服務(wù)器EHLO
返回了SMTPUTF8
,那么SmtpClient對象
將會將UTF-8內(nèi)容不轉(zhuǎn)碼直接發(fā)送出去士复!導(dǎo)致發(fā)送出去的數(shù)據(jù)內(nèi)容和預(yù)期的數(shù)據(jù)內(nèi)容不一致M脊取!阱洪!
同步方法Send
不受此影響便贵。
解決辦法
SendMailCallback
函數(shù)中的ServerSupportsEai
應(yīng)該換成統(tǒng)一的IsUnicodeSupported
,Bug就解決冗荸。
受影響版本
- .Net Framework 4.5 - 4.7.2(最新版)承璃,估計(jì)是全系列
- .Net Core 看最新版也是受此影響
一次.Net框架Bug的發(fā)現(xiàn)記錄
DKIM簽名功能寫好后測試了很多個郵箱,都能通過驗(yàn)證蚌本。但隔一天測試卻發(fā)現(xiàn)沒有一個郵箱通過驗(yàn)證盔粹,并且下載下來的郵件源碼body部分和本地額外保存的一份有很大出入隘梨,表現(xiàn)在郵件主題、附件文件名舷嗡,本地是Base64編碼轴猎,下載下來的是中文漢字。
首先發(fā)現(xiàn)問題的是outlook郵箱进萄,他們家會告訴你DKIM簽名是否正確捻脖,本地直接發(fā)送郵件沒有一個通過簽名驗(yàn)證的,但通過郵箱服務(wù)器發(fā)送卻都是好的垮斯。對比直發(fā)和服務(wù)器發(fā)的郵件源碼區(qū)別郎仆,發(fā)現(xiàn)郵箱服務(wù)器的沒有中文,直發(fā)的里面中文的地方全是中文兜蠕。
看樣子中文部分有問題扰肌,然后試著把郵件里面的中文全部換成英文,發(fā)送熊杨,又可以了曙旭!想了一下昨天測試好像全部是英文,因?yàn)猷]件內(nèi)容寫了一次基本上就不會改了晶府。
到了這時候桂躏,感覺還以為是outlook服務(wù)器進(jìn)行了什么處理,難道郵箱服務(wù)器發(fā)郵件用的協(xié)議和我們用Smtp協(xié)議發(fā)郵件的協(xié)議有出入川陆?但并沒有找到什么相關(guān)的資料剂习。然后測試了QQ郵箱、網(wǎng)易yeah.net较沪,并且抓了一下包看了一下鳞绕,發(fā)現(xiàn)切換SmtpClient.DeliveryFormat
參數(shù),使用SevenBit
(此值為默認(rèn)值)(中文會被編碼)QQ郵箱沒問題尸曼,網(wǎng)易有問題们何;使用International
(中文不編碼)QQ郵箱有問題,網(wǎng)易反倒沒問題控轿。
抓包發(fā)現(xiàn)使用SevenBit
時冤竹,中文部分給QQ郵箱發(fā)送的是Base64編碼,給網(wǎng)易發(fā)送的是中文內(nèi)容茬射,本地保存的是Base64編碼(和簽名時使用到的郵件內(nèi)容一致)鹦蠕;使用International
時正好相反。簽名的數(shù)據(jù)和發(fā)送的數(shù)據(jù)不一致在抛,導(dǎo)致了不管怎么改這個參數(shù)片部,都有一個是錯的。
為什么會這樣霜定?查閱.Net源碼档悠,一路看編碼部分,發(fā)現(xiàn)基本上每個涉及到字符編碼望浩、發(fā)送的地方都會傳入allowUnicode
參數(shù)辖所,所有allowUnicode
=
SmtpClient.IsUnicodeSupported()
,但有唯一的一處例外:
[此處忽略磨德,見上文]
SendMailCallback
是SmtpClient.SendAsync
(SendMailAsync
會調(diào)用SendAsync
)調(diào)用的缘回,so,異步操作已經(jīng)完全不受我們設(shè)置的DeliveryFormat
參數(shù)控制了典挑,中文轉(zhuǎn)不轉(zhuǎn)碼完全看對方郵件服務(wù)器心情K盅纭!您觉!函數(shù)中的ServerSupportsEai
應(yīng)該換成統(tǒng)一的IsUnicodeSupported
拙寡,Bug就解決。
但琳水,我們沒法去改這個地方肆糕,那么上Hook吧,把SmtpClient.ServerSupportsEai
Hook一下在孝,如果是SendMailCallback
調(diào)用的就return IsUnicodeSupported()
诚啃。
但,DotNetDetour庫可以Hook String.Length
屬性私沮,但沒法HookSmtpClient.ServerSupportsEai
屬性始赎,不知道啥原因。最后調(diào)試煩了放棄了仔燕。
結(jié)尾使用SmtpClient.Send
沒有這種問題造垛,就把異步操作全部換成了同步,代碼還少了不少涨享。Bug修理完畢筋搏,給outlook、QQ厕隧、網(wǎng)易發(fā)英文奔脐、中文郵件都能通過DKIM簽名驗(yàn)證。