为 System.Net.Mail.MailMessage 添加 Save() 功能






4.92/5 (40投票s)
使用 Reflector、Reflection 和 C# 3.0 扩展方法组合,为 System.Net.Mail 中的 MailMessage 添加 Save(string FileName) 功能。
引言
本文将使用一些简单的技术来分析现有的 .NET 框架类,并以原始开发人员未曾预料到的方式对其进行扩展。
背景
我最近一直在做一个关于批量生成大量电子邮件以便稍后发送的项目。MailMessage
类似乎提供了我需要的功能。它允许我同时添加纯文本和 HTML 内容,并使用 SmtpClient
的 DeliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory
,我可以将电子邮件生成到文件文件夹中。然而,在使用此方法时,我遇到了一个问题……
我的项目其中一项要求是需要控制输出电子邮件时使用的文件名,以便多个作业可以并发运行,并且文件名可以以某种方式将电子邮件与运行它的作业关联起来。我还想追加一个序列号,这样我就可以用它作为作业在失败时从中断处继续的依据。
SmtpClient
类有一个 Send(MailMessage Message)
方法,当指定了 PickupDirectoryLocation
且 DeliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory
时,该方法会在指定目录中生成电子邮件。然而,SmtpClient
使用的文件名似乎是一个随机的 Guid
。标准的 SmtpClient
或 MailMessage
并没有提供任何方式让我选择输出时使用的文件名,甚至连已使用的文件名也无法获取。
使用 Reflector 分析现有类
我没有选择放弃使用 MailMessage
,因此也不选择 .NET 框架之外的第三方组件,而是着手寻找一种扩展 MailMessage
和 SmtpClient
以提供我所需功能的方法。
首先,我使用了优秀的 Reflector 工具来分析 MailMessage
和 SmtpClient
类,以了解它们“幕后”到底在做什么。您可以从 RedGate 免费下载。
将 Reflector 指向 SmtpClient.Send()
方法,它告诉我两件事。当指定拾取目录时,SmtpClient.Send()
使用 GetFileMailWriter(string Path)
方法创建一个名为 fileMailWriter
的 MailWriter
对象。
为了实际生成电子邮件,它接着调用 MailMessage
对象上的 Send()
方法,并将新创建的 MailWriter
对象作为参数传递。
为了进一步调查,我查看了 SmtpClient.GetFileMailWriter()
方法中发生的情况。
正如预期的那样,反汇编的代码显示,该方法使用 Guid.NewGuid() + “.eml”
创建了一个随机文件名。然后,它创建一个 MailWriter
对象,并将一个标准的 FileStream
传递给它。
从 Reflector 到 Reflection
因此,此时,我知道当我调用 SmtpClient.Send(message)
时会发生什么。但是,我如何才能改变这种行为,以便我能够生成一个具有自己指定文件名的电子邮件呢?
我基本想要创建一个具有我自己的 FileStream
的 MailWriter
对象,然后将其传递给 MailMessage.Send()
。然而,微软并没有将此功能暴露给我。MailWriter
类、GetFileMailWriter()
和 MailMessage.Send()
方法都被标记为 internal
,因此我无法以标准方式构造或调用这些方法。
这时 Reflection 就派上用场了。我可以使用 Reflection 来构造一个内部的 MailWriter
,然后再用它来调用 MailMessage
上的内部 Send()
方法。
Assembly _assembly = typeof(SmtpClient).Assembly;
Type _mailWriterType = _assembly.GetType("System.Net.Mail.MailWriter");
// Create FileStream object
FileStream _myFileStream = new FileStream(_myFileName, FileMode.Create);
// Get reflection info for MailWriter contructor
ConstructorInfo _mailWriterContructor =
_mailWriterType.GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new Type[] { typeof(Stream) },
null);
// Construct MailWriter object with our FileStream
object _mailWriter =
_mailWriterContructor.Invoke(new object[] { _myFileStream });
// Get reflection info for Send() method on MailMessage
MethodInfo _sendMethod =
typeof(MailMessage).GetMethod(
"Send",
BindingFlags.Instance | BindingFlags.NonPublic);
// Call method passing in MailWriter
_sendMethod.Invoke(
Message,
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new object[] { _mailWriter, true },
null);
- 首先,我需要获取
MailWriter
对象的类型,以便能够构造它。我们可以通过暴露的SmtpClient
类来间接实现这一点。首先,我获取SmtpClient
所属的程序集,然后使用GetType()
来获取对MailWriter
类型的引用。 - 然后,我需要调用
MailWriter
类型的内部构造函数来创建一个MailWriter
对象。我创建了自己的FileStream
对象并将其作为参数传入。 - 接下来,再次使用 Reflection,我调用
MailMessage
对象上的Send()
方法,并将我的MailWriter
对象作为参数传递。
就是这样——生成的电子邮件已按照构造我的 FileStream
类时指定的 文件名创建。通过使用 Reflection,我重用了 System.Net.Mail
命名空间中的内部类和方法,将邮件保存到文件系统并使用我自己的文件名——正是我所需要的。
画龙点睛——扩展方法
为了最终有个漂亮的收尾,我现在可以将它封装起来,使用 C# 3.0 的一项新功能——扩展方法。通过使用扩展方法,您可以向 MailMessage
类添加一个 Save(string FileName)
方法,就像它一开始就存在一样。
下面是完整的扩展方法
public static class MailMessageExt
{
public static void Save(this MailMessage Message, string FileName)
{
Assembly assembly = typeof(SmtpClient).Assembly;
Type _mailWriterType =
assembly.GetType("System.Net.Mail.MailWriter");
using (FileStream _fileStream =
new FileStream(FileName, FileMode.Create))
{
// Get reflection info for MailWriter contructor
ConstructorInfo _mailWriterContructor =
_mailWriterType.GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new Type[] { typeof(Stream) },
null);
// Construct MailWriter object with our FileStream
object _mailWriter =
_mailWriterContructor.Invoke(new object[] { _fileStream });
// Get reflection info for Send() method on MailMessage
MethodInfo _sendMethod =
typeof(MailMessage).GetMethod(
"Send",
BindingFlags.Instance | BindingFlags.NonPublic);
// Call method passing in MailWriter
_sendMethod.Invoke(
Message,
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new object[] { _mailWriter, true },
null);
// Finally get reflection info for Close() method on our MailWriter
MethodInfo _closeMethod =
_mailWriter.GetType().GetMethod(
"Close",
BindingFlags.Instance | BindingFlags.NonPublic);
// Call close method
_closeMethod.Invoke(
_mailWriter,
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new object[] { },
null);
}
}
}
基本上,我们在一个静态类中创建了一个静态方法。标记该方法为扩展方法的关键是第一个参数 this MailMessage Message
。第一个参数被标记为 this <ObjectType>
时,表示此方法将扩展 <ObjectType>
类,在本例中是 MailMessage
类。
现在,我们可以这样调用我们额外添加的功能
MailMessage _testMail = new MailMessage();
_testMail.Body = "This is a test email";
_testMail.To.Add(new MailAddress("email@domain.com"));
_testMail.From = new MailAddress("sender@domain.com");
_testMail.Subject = "Test email";
_testMail.Save(@"c:\testemail.eml");
最后说明
我们在这里实现的是
- 能够使用 Reflector 确定内部工作原理,包括识别内部类和方法。
- 使用 Reflection 来利用原本未向外部开发人员公开的类和方法。
- 使用扩展方法来封装额外功能。
当然,就像大多数事情一样,我所采取的方法也有其弊端。
其中一个主要问题是,因为我们使用 Reflection 来绕过微软在此案例中设置的原始限制,所以他们可以在未来的框架版本中自由更改 System.Net.Mail
的“内部”实现,这可能会破坏我们的实现。例如,既然 MailWriter
类最初没有向我们公开,那么什么能阻止他们将其重命名为其他名称呢?
注意:本文撰写以来,微软已在 .NET 4.5 中更改了 MailMessage.Send() 方法的参数,这会导致现有代码失败。各位读者已在文章评论区提供了解决方案。