記錄一次.Net框架Bug發現和提交過程:.Net Framework和.Net Core均受影響
SmtpClient一處程式碼編寫錯誤導致非同步傳送郵件時DeliveryFormat配置項無法正確工作,非同步操作已經完全不受我們設定屬性控制了,UTF-8內容(如中文)轉不轉碼完全看對方郵件伺服器心情!
.Net開發者社群富文字編輯器太難用了,還是簡書的編輯器好用,然後掘金的版面好看,最後還是喜歡cnblog裡面可以修改版面css。
盡瞎說大實話。
重新碼一份好看的。
出問題的地方
出現問題的函式:
//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引數 message.BeginSend(writer, DeliveryMethod != SmtpDeliveryMethod.Network, ServerSupportsEai, new AsyncCallback(SendMessageCallback), result.AsyncState); ... }
ServerSupportsEai
所在位置為allowUnicode
引數。SmtpClient
中所有涉及到allowUnicode
引數的地方,賦值都為IsUnicodeSupported()
返回值。但唯一這一處是例外。
我們看看IsUnicodeSupported
函式:
//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) { ... //如果伺服器支援SMTPUTF8,那麼ServerSupportsEai=true else if (String.Compare(extension, 0, "SMTPUTF8", 0, 8, StringComparison.OrdinalIgnoreCase) == 0) { ((SmtpPooledStream)pooledStream).serverSupportsEai = true; } ... }
產生的現象
SendMailCallback
是SmtpClient.SendAsync
(SendMailAsync
會呼叫SendAsync
)呼叫的,so,非同步操作已經完全不受我們設定的DeliveryFormat
引數控制了,UTF-8內容(如中文)轉不轉碼完全看對方郵件伺服器心情!!!
SmtpClient
物件DeliveryFormat
屬性賦值為SmtpDeliveryFormat.SevenBit
,要求郵件使用 7 位 ASCII 的傳遞格式,並且用非同步方法來發送;本來Subject
、附件檔名
等裡面的UTF-8內容(如中文)將會被轉碼;但如果郵件伺服器EHLO
返回了SMTPUTF8
,那麼SmtpClient物件
將會將UTF-8內容不轉碼直接傳送出去!導致傳送出去的資料內容和預期的資料內容不一致!!!
同步方法Send
不受此影響。
解決辦法
SendMailCallback
函式中的ServerSupportsEai
應該換成統一的IsUnicodeSupported
,Bug就解決。
受影響版本
- .Net Framework 4.5 - 4.7.2(最新版),估計是全系列
- .Net Core 看最新版也是受此影響
一次.Net框架Bug的發現記錄
DKIM簽名功能寫好後測試了很多個郵箱,都能通過驗證。但隔一天測試卻發現沒有一個郵箱通過驗證,並且下載下來的郵件原始碼body部分和本地額外儲存的一份有很大出入,表現在郵件主題、附件檔名,本地是Base64編碼,下載下來的是中文漢字。
首先發現問題的是outlook郵箱,他們家會告訴你DKIM簽名是否正確,本地直接傳送郵件沒有一個通過簽名驗證的,但通過郵箱伺服器傳送卻都是好的。對比直髮和伺服器發的郵件原始碼區別,發現郵箱伺服器的沒有中文,直髮的裡面中文的地方全是中文。
看樣子中文部分有問題,然後試著把郵件裡面的中文全部換成英文,傳送,又可以了!想了一下昨天測試好像全部是英文,因為郵件內容寫了一次基本上就不會改了。
到了這時候,感覺還以為是outlook伺服器進行了什麼處理,難道郵箱伺服器發郵件用的協議和我們用Smtp協議發郵件的協議有出入?但並沒有找到什麼相關的資料。然後測試了QQ郵箱、網易yeah.net,並且抓了一下包看了一下,發現切換SmtpClient.DeliveryFormat
引數,使用SevenBit
(此值為預設值)(中文會被編碼)QQ郵箱沒問題,網易有問題;使用International
(中文不編碼)QQ郵箱有問題,網易反倒沒問題。
抓包發現使用SevenBit
時,中文部分給QQ郵箱傳送的是Base64編碼,給網易傳送的是中文內容,本地儲存的是Base64編碼(和簽名時使用到的郵件內容一致);使用International
時正好相反。簽名的資料和傳送的資料不一致,導致了不管怎麼改這個引數,都有一個是錯的。
為什麼會這樣?查閱.Net原始碼,一路看編碼部分,發現基本上每個涉及到字元編碼、傳送的地方都會傳入allowUnicode
引數,所有allowUnicode
=
SmtpClient.IsUnicodeSupported()
,但有唯一的一處例外:
[此處忽略,見上文]
SendMailCallback
是SmtpClient.SendAsync
(SendMailAsync
會呼叫SendAsync
)呼叫的,so,非同步操作已經完全不受我們設定的DeliveryFormat
引數控制了,中文轉不轉碼完全看對方郵件伺服器心情!!!函式中的ServerSupportsEai
應該換成統一的IsUnicodeSupported
,Bug就解決。
但,我們沒法去改這個地方,那麼上Hook吧,把SmtpClient.ServerSupportsEai
Hook一下,如果是SendMailCallback
呼叫的就return IsUnicodeSupported()
。
但,DotNetDetour庫可以Hook String.Length
屬性,但沒法HookSmtpClient.ServerSupportsEai
屬性,不知道啥原因。最後除錯煩了放棄了。
結尾使用SmtpClient.Send
沒有這種問題,就把非同步操作全部換成了同步,程式碼還少了不少。Bug修理完畢,給outlook、QQ、網易發英文、中文郵件都能通過DKIM簽名驗證。