Click here to Skip to main content
15,867,141 members
Articles / Programming Languages / C#

Adding Save() functionality to System.Net.Mail.MailMessage

Rate me:
Please Sign up or sign in to vote.
4.92/5 (40 votes)
19 Nov 2014CPOL5 min read 304K   3.7K   74   80
Using a combination of Reflector, Reflection, and C# 3.0 extension methods to add Save(string FileName) functionality to MailMessage in System.Net.Mail.

Introduction

This article is going to use some simple techniques to analyse an existing .NET framework class and to extend it in a way the original developers did not intend.

Background

I’ve been recently working on a project related to batch generating large numbers of emails for sending at a later stage. The MailMessage class appeared to provide the functionality I needed. It allowed me to add both plain text and HTML content, and using the DeliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory of SmtpClient, I could generate the emails to a file folder. However, when using this approach, I ran into a problem ...

One of the requirements of my project specified that I needed to control the filename used to output the email so that multiple jobs could be run concurrently and the filename could in some way relate the email to the job that run it. I also wanted to append a sequence number so that I could use this as a way for jobs to continue from where they left off if they failed.

The SmtpClient class has a Send(MailMessage Message) method which, when the PickupDirectoryLocation is specified and DeliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory, generates the email in that directory. However, the file name used by SmtpClient appeared to be a random Guid. No where does the standard SmtpClient or MailMessage expose a way for me to choose the filename used for output or even feedback the filename that was used.

Using Reflector to analyse existing classes

Not choosing to give up on using MailMessage and therefore opt for a third party component outside of the .NET framework, I embarked on finding a way of extending MailMessage and SmtpClient to provide the functionality I needed.

First, I used the excellent Reflector tool to analyse the MailMessage and SmtpClient classes to find out what they were really doing ‘under the hood’. You can download this free from RedGate.

Pointing Reflector at the SmtpClient.Send() method told me two things. When a pickup directory is specified, SmtpClient.Send() creates a MailWriter object called fileMailWriter using the GetFileMailWriter(string Path) method.

To actually generate the email, it then calls a Send() method on the MailMessage object, passing the newly created MailWriter object.

smtpclientext/reflect1.jpg

To investigate further, I looked at what was happening in the SmtpClient.GetFileMailWriter() method.

smtpclientext/reflect2.jpg

As expected, the disassembled code shows that this method creates a random filename with Guid.NewGuid() + “.eml”. It then creates a MailWriter object, passing it a standard FileStream.

From Reflector to Reflection

So, at this stage, I knew what was happening when I called SmtpClient.Send(message). But, how could I change this behaviour so that I could generate an email with my own specified filename?

I basically wanted to create a MailWriter object with my own FileStream and then pass it to MailMessage.Send(). However, Microsoft has not exposed this functionality to me. The MailWriter class, GetFileMailWriter(), and MailMessage.Send() methods are all marked as internal, so I couldn’t construct or invoke these methods in the standard way.

This is where Reflection stepped in. I could use Reflection to construct an internal MailWriter and then again to invoke the internal Send() method on MailMessage.

C#
Assembly _assembly = typeof(SmtpClient).Assembly;
Type _mailWriterType = _assembly.GetType("System.Net.Mail.MailWriter");
C#
// 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 });
C#
// 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);
  • First, I needed to get the type of the MailWriter object to be able to construct it. We can use the exposed SmtpClient class to do this indirectly. First, I obtained the assembly that SmtpClient is contained in, and then used GetType() to get a reference to the MailWriter type.
  • Then, I needed to invoke the internal constructor of the MailWriter type to create a MailWriter object. I created my own FileStream object and passed this in.
  • Next, again using Reflection, I invoked the Send() method of a MailMessage object passing in my MailWriter object.

And that was it – the generated email was created to the filename specified when constructing my FileStream class. By using Reflection, I had reused internal classes and methods in the System.Net.Mail namespace to save a mail to the file system with my own filename – exactly what I needed.

Finishing touches - extension method

For a final nice touch, I could now wrap this up by using a new feature of C# 3.0 called extension methods. By using an extension method, you can add a Save(string FileName) method to the MailMessage class as if it was put there in the first place.

Shown below is the complete extension method:

C#
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);
        }
    }
}

Basically, we have created a static method in a static class. The thing that marks this method out to be an extension method is the first parameter, this MailMessage Message. The first parameter when marked with this <ObjectType> says that this method is to extend the <ObjectType> class, in this case the MailMessage class.

Now, we can call our extra functionality like so:

C#
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");

Final note

What we have achieved here is:

  • The ability to determine how something works internally using Reflector, including identifying internal classes and methods.
  • Use Reflection to make use of classes and methods which were not originally exposed to outside developers.
  • Wrap up extra functionality using extension methods.

Of course, as with most things, there is a downside to the approach I have taken.

One of the major things is that because we are using Reflection to get around the original restrictions put in place by Microsoft in this case, they are free to change their ‘internal’ implementation of System.Net.Mail in future framework releases, which could break our implementation. For example, what is to stop them changing the name of the MailWriter class to something else as it wasn’t exposed to us in the first place.

NOTE: Since this article was written, Microsoft have changed the parameters of the MailMessage.Send() method in .NET 4.5 which causes the existing code to fail. Various members have provided solutions to this in the comments on the article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Globe Business Publishing Ltd
United Kingdom United Kingdom
Started tinkering on an old BBC Microcomputer using BBC BASIC and progressed up to dabbling with C and ARMCode on an Acorn RiscPC. Moving to the PC platform I progressed from C++ to now a lot of C# and ASP.NET.

Comments and Discussions

 
QuestionThanks! (And a suggestion) Pin
Member 132243208-Oct-18 6:38
Member 132243208-Oct-18 6:38 
QuestionHere is the VB.NET code, just in case if any one needs. Pin
goldsun18-Jul-18 4:04
goldsun18-Jul-18 4:04 
PraiseGreat Code!! Pin
MrCodeIt27-Jul-17 7:39
MrCodeIt27-Jul-17 7:39 
PraiseThanks Pin
Karen Payne28-Dec-16 8:58
Karen Payne28-Dec-16 8:58 
QuestionPotential project Pin
Member 126687763-Aug-16 15:12
Member 126687763-Aug-16 15:12 
AnswerRe: Potential project Pin
axuno9-Sep-16 6:12
axuno9-Sep-16 6:12 
QuestionParameter count does not match Pin
Member 122998271-Feb-16 17:50
Member 122998271-Feb-16 17:50 
AnswerRe: Parameter count does not match Pin
MoQuamar16-Mar-16 6:24
MoQuamar16-Mar-16 6:24 
AnswerRe: Parameter count does not match Pin
MoQuamar16-Mar-16 6:57
MoQuamar16-Mar-16 6:57 
PraiseNice post. Pin
cuteangel493916-Nov-15 9:40
cuteangel493916-Nov-15 9:40 
QuestionRename email message name Pin
ThierryAh19-Nov-14 23:02
ThierryAh19-Nov-14 23:02 
QuestionHow to get it work with Framework 4.5 and later? Pin
NightWizzard17-Nov-14 0:31
NightWizzard17-Nov-14 0:31 
AnswerRe: How to get it work with Framework 4.5 and later? Pin
Allan Eagle17-Nov-14 0:47
Allan Eagle17-Nov-14 0:47 
GeneralRe: How to get it work with Framework 4.5 and later? Pin
NightWizzard17-Nov-14 7:24
NightWizzard17-Nov-14 7:24 
QuestionFantastic Pin
User 109715105-Nov-14 0:22
User 109715105-Nov-14 0:22 
GeneralMy vote of 1 Pin
Member 1117112721-Oct-14 18:48
Member 1117112721-Oct-14 18:48 
GeneralRe: My vote of 1 Pin
Allan Eagle17-Nov-14 0:49
Allan Eagle17-Nov-14 0:49 
GeneralRe: My vote of 1 Pin
Member 1117112718-Nov-14 15:09
Member 1117112718-Nov-14 15:09 
GeneralRe: My vote of 1 Pin
Allan Eagle18-Nov-14 23:01
Allan Eagle18-Nov-14 23:01 
GeneralRe: My vote of 1 Pin
Nelek19-Nov-14 1:09
protectorNelek19-Nov-14 1:09 
Allan Eagle wrote:
The article clearly states what might happen and actually did happen after it was written. For an article written 5 years ago intending to explain the process of using certain techniques and not to give a solution, I think you've missed the point of the article. However, I am glad you have managed to bring the code up to date and I have now added a note to the end of the article.

And that is exactly the spirit of the site and the goal of the message boards, to rise up possible bugs and let the author get ride of them. Technology evolve, why don't let the articles evolve with it?
My 5 to you
M.D.V. Wink | ;)

If something has a solution... Why do we have to worry about?. If it has no solution... For what reason do we have to worry about?
Help me to understand what I'm saying, and I'll explain it better to you
Rating helpful answers is nice, but saying thanks can be even nicer.

GeneralRe: My vote of 1 Pin
Nelek19-Nov-14 1:11
protectorNelek19-Nov-14 1:11 
GeneralRe: My vote of 1 Pin
Member 1117112723-Nov-14 7:25
Member 1117112723-Nov-14 7:25 
QuestionSave Unsent Pin
Gennady Oster16-Sep-14 23:44
Gennady Oster16-Sep-14 23:44 
Questioncan we load a .msg file and then save it to .eml? Pin
ravikhoda11-Sep-14 20:09
professionalravikhoda11-Sep-14 20:09 
GeneralParameter mismatch occured Pin
rakeshgali7-Mar-14 3:08
rakeshgali7-Mar-14 3:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.