1 Star 0 Fork 2

scsme / DKIM-Smtp-csharp

forked from xiangyuecn / DKIM-Smtp-csharp 
Create your Gitee Account
Explore and code with more than 12 million developers,Free private repositories !:)
Sign up
This repository doesn't specify license. Please pay attention to the specific project description and its upstream code dependency when using it.
Clone or Download
contribute
Sync branch
Cancel
Notice: Creating folder will generate an empty file .keep, because not support in Git
Loading...
README

DKIM-Smtp-csharp 帮助文档

跑起来

clone下来用vs应该能够直接打开,经目测看起来没什么卵用的文件都svn:ignore掉了(svn滑稽。

主要支持

  • 对邮件进行DKIM签名,支持带附件
  • 对整个邮件内容(.eml文件)的DKIM签名进行验证
  • MailMessageSmtpClient进行了一次封装,发送邮件简单易用,进行DKIM签名后直接投递到对方服务器(无需己方邮件服务器)

DKIM签名、验证规则

对于DKIM的签名和验证规则,QQ邮箱的《DKIM指引》这个文章已经写的足够详细,就不搬运了。

还不行还可以去参考DKIM.Net库对签名的实现。

举个例子

//创建DKIM签名对象
var dkim = new EMail_DKIM("domain.com", "dkimSelector", new RSA.RSA(/*"-----BEGIN RSA PRIVATE KEY-----....", true*/ 1024));

//通过EMail类来操作发邮件
using (var email = new EMail()) {//new EMail("mx1.qq.com", 25),默认会自动解析收件箱的mx记录,得到smtp服务器地址,如果是要通过发件服务器来发送,则需要手动填写为发件服务器地址
	//使用签名
	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--

方法文档

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发送邮件。

【注】.Net框架的SmtpClient中异步方法有Bug,我们默认设置中文要进行编码,但如果服务器支持SMTPUTF8,那么就会发原始的中文过去,详情见下面的发现记录。

EMail_Unit.cs

封装的一些通用方法,如:base64。都是比较周边的功能。

/Lib/DNS-csharp目录

这个目录里面是我的DNS-csharp仓库代码,用来解析邮箱域名的MX记录。

/Lib/RSA-csharp目录

这个目录里面是我的RSA-csharp仓库代码,用来解析PEM秘钥对的。

/Lib/DotNetDetour目录

这个目录里面是DotNetDetour库,使用的这个版本代码。已经修改过,用来支持私有方法的hook。

前言、自述、还有啥

在实现邮件发送时发现就算不把邮件投递到自己的邮件服务器(由邮件服务服务器进行发送给对方),有些邮箱(QQ邮箱)不会拒绝,但有邮箱直接就拒绝了(网易邮箱)。对比由邮件服务器发送的和直接发送的邮件内容的区别,发现直接发送缺少了DKIM-Signature邮件头。

好了,缺少那就加上。但.Net的MailMessageSmtpClient简陋到一份邮件发送到一个Stream的接口都不舍得暴露(任性写入到文件夹不给文件名却支持),直接就没有支持签名的头绪。那自行实现。

研究了一下RFC 6376长篇大论看不懂(主要没给一个简单的实现步骤),然后QQ给的简单易懂多了(流式.清晰)。签名和验证算法就清楚了。

发现DKIM.Net

要签名先搞定bodyhash计算body怎么获取?一堆附件、一堆转码...... MailMessageSmtpClient没给获取body的支持。然后找到一个库 DKIM.Net,他里面实现了获取整个邮件内容的方法,简单调用一下MailMessage的私有方法搞定。

然后遇到了DKIM.Net也没有搞定的问题,对于带附件AttachmentsAlternateViews的邮件,由于每次获取的邮件内容因为boundary分隔符(边界)不一致导致签名无效,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;
}

这个方法只会在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公钥

一次.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(),但有唯一的一处例外:

我们先看看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;
	}
	...
}

最后看看这处唯一的例外:

//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);
	...
}

SendMailCallbackSmtpClient.SendAsyncSendMailAsync会调用SendAsync)调用的,so,异步操作已经完全不受我们设置的DeliveryFormat参数控制了,中文转不转码完全看对方邮件服务器心情!!!函数中的ServerSupportsEai应该换成统一的IsUnicodeSupported,Bug就解决。

但,我们没法去改这个地方,那么上Hook吧,把SmtpClient.ServerSupportsEaiHook一下,如果是SendMailCallback调用的就return IsUnicodeSupported()

但,DotNetDetour库可以Hook String.Length属性,但没法HookSmtpClient.ServerSupportsEai属性,不知道啥原因。最后调试烦了放弃了。

后面发现是Hook错了地方,换到最深层次调用地方,一抓一个准。找出SmtpClient.ServerSupportsEai最结果最终是从SmtpConnection.ServerSupportsEai得来的,也许是C#编译后把整个调用过程都优化掉了,变成了取值的地方直接调用的SmtpConnection中的方法,导致Hook前面的方法都是不会被执行,Hook SmtpConnection.ServerSupportsEai就行了。

结尾使用SmtpClient.Send没有这种问题,就把异步操作全部换成了同步,代码还少了不少。Bug修理完毕,给outlook、QQ、网易发英文、中文邮件都能通过DKIM签名验证。

注:此Bug已提交给.NET Core Libraries (CoreFX)并合并到了主线上,应该在.Net Core 3.0上能够得到修复。

任意邮箱收件地址查询

比如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

相关截图

控制台运行:

控制台运行

开始线上测试:

开始测试

测试线上结果:

测试结果

Empty file

About

C# .NET Framework下发送DKIM签名email,支持附件,支持验证一封邮件的DKIM签名 expand collapse
Cancel

Releases

No release

Contributors

All

Activities

Load More
can not load any more
C#
1
https://gitee.com/Jakey870/DKIM-Smtp-csharp.git
git@gitee.com:Jakey870/DKIM-Smtp-csharp.git
Jakey870
DKIM-Smtp-csharp
DKIM-Smtp-csharp
master

Search