Catching Email With Rnwood.SmtpServer

Not too long ago, Llewellyn Falco posted Using ApprovalTests in .Net 19 Email, where he describes a really easy way to test email messages using ApprovalTests.  The video describes a testing seam that separates message creation from message sending, and this makes testing email straightforward.  If you are currently working with .NET source, then you really should follow the simple instructions in that video and stop reading this post, it’s not for you.

This post is for you if:

  • You don’t control the source of the email you want to test.
  • You control the source, but it’s not .NET.
  • You control the source in theory, but you can’t change it (eg. boss says no)

Context

In my case, I’m moving a little reporting script from Perl to .NET.  Lets call the script “report.pl” for the sake of discussion.  We want to move from Perl to .NET using TDD and without any noticeable changes to the users (which happen to be the Executive Committee where I work).  I don’t want to create a system that works more or less the same, I want one that works exactly the same.  I need to lock down the current system, and use it as the gold standard for the new system.

Here’s what the current script looks like with the responsibilities color-coded:

image

The blue blocks are responsibilities that are implementation specific, when the script wakes up the first thing it does is read its config and validate its environment.  Its not likely that these tasks will transfer to a new system.

The script creates an Excel Workbook, this responsibility is identified by green blocks.

The script reads its data from a database, this responsibility is identified by red blocks.

The script manipulates the data before writing it to the Workbook, these blocks are purple.

The script sends the Workbook to its recipients over email, this block is yellow.  This is the first block I’ll work on, since it’s the only one that isn’t interwoven with other responsibilities.

Strategy

1. Identify a responsibility

image

2. Let’s assume that this responsibility was encapsulated in a subroutine.  Copy that subroutine to a new Perl script “sendReport.pl”.  If the responsibility was not encapsulated in a subroutine, make sure to wrap the lines of code you copy with a sub{} in sendReport.pl.

image

3. Use Perl’s “require” mechanism to include “SendReport.pl” in “Report.pl” and remove the local definition of the “sendReport” subroutine.  The script will call the imported definition instead.

image

4. Create a third Perl script “SendReportRunner.pl” which is just a thin shell around the extracted responsibility, which will let me execute the responsibility with any parameters I like.

image

5. Create a unit test in C# that uses a Process object to invoke SendReportRunner.pl.  Notice that Report.pl is no longer in the picture.

image

6. Capture the output in an ApprovalTest.  Because SendReport.pl actually wants to send email over SMTP, this is where the plan starts to go off the rails, but we can work through it.

image

7. Build C# implementation (Report.dll) that passes same ApprovalTest.  Once we have a C# implementation, we can create a seam that separates the message creation from the message sending, and use EmailApprovals, just like Llewellyn described, but getting past step 6 will be tricky.

image

8. Now we need to get Report.dll working with Report.pl.  I’ll create C# shell that invokes Report.dll from the command line.

image

9. Replace sendReport() call in Report.pl with a system() call to to ReportRunner.exe

image

10. Repeat until Report.pl is just a bunch of blue blocks containing system() calls to C# code runners.

11. Replace Report.pl with a C# executable, PowerShell script or whatever.

Moving the responsibility down to Step 4 was relatively easy.  I spent some tedious hours tracing variables to determine their scope and effect.  Then I spent half a day wresting with the COM object the Perl script uses to send the mail.  At the end of the day I could use SendReportRunner.pl to send emails and catch them with smtp4dev.  But to get past Step 6 I needed to answer the question: How do I get those messages into a .NET unit test so I can capture and approve them?

Catching Email

Smtp4dev is a nice little application that fills a similar niche to CassiniDev a webserver I wrote about previously.  Smtp4dev sits in your system tray and listens on the SMTP port for incoming mail.  When it gets a message, it logs the message arrival in it’s window and you you can double click the message to see it in your default email program:

image

Since smtp4dev lives on CodePlex, I figured there was a good chance that it was written in .NET and sure enough it was written in C#.  Thinking back to CassiniDev, I wondered if there was a way I could host smtp4dev in my unit test, catch the messages from the Perl process, and then hand them over to ApprovalTests.  I grabbed the source for the project and found an example named “SimpleServer’’ that looked like it could be used to create a test fixture similar to the CassiniDev fixture I used when testing MVC views.

I created an empty class library and a test project to go with it.  The test project will need ApprovalTests and a reference to Rnwood.SmtpServer, which is the server that powers smtp4dev.  The server wasn’t on NuGet yet, so I put it there and used NuGet to add both references.  The pattern for creating the test fixture was nearly the same as creating a fixture for CassiniDev:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Rnwood.SmtpServer;

namespace Report.Tests
{
  [TestClass]
  public class SmtpFixture : DefaultServer
  {
    public SmtpFixture(Ports port)
      : base(port)
    {
    }

    [TestInitialize]
    public void StartServer()
    {
      this.Start();
    }

    [TestCleanup]
    public void StopServer()
    {
      this.Stop();
    }
  }
}

To implement a test, I’ll extend this fixture.  In the broad strokes, we want something like this:

public SendReportTest()
  : base(Ports.SMTP)
{
}

[TestMethod]
public void SendReportOverEmail()
{
  try
  {
    this.MessageReceived += CatchMessage;
    GenerateMessage();
    ApprovalTests.Email.EmailApprovals.Verify("??");
  }
  finally
  {
    this.MessageReceived -= CatchMessage;
  }
}

I specify the default SMTP port in the constructor, I could have used “Ports.AssignAutomatically” and the server would pick an empty port.  That’s nice functionality, but the Perl script wants to use the default port.  I’ve declared some methods but not implemented them, and I’m still not sure what I’m going to give to ApprovalTests.

When we get a MessageReceivedEvent, it will come with a MessageEventArgs and we need to figure out if we can somehow get a MailMessage from that, which is what EmailApprovals is expecting from us.  CatchMessage needs to do that for us.

We also need to generate a message. I’ll do that first, since once I can generate and catch messages I’ll be able to look at a live instance of MessageEventArgs and see what its guts look like.  Our goal is for the message to come from Perl, but for the moment, we’ll just use an SmtpClient to stand in for the script.  Notice that we pass the fixture’s port number to the SmtpClient, if we were using a random port, this would ensure that we actually send it to the right place.

private void GenerateMessage()
{
  using (var client = new SmtpClient("localhost", this.PortNumber))
  {
    using (var message = new MailMessage(
      "noreply@localhost", 
      "jim@localhost", 
      "Hello World", 
      "Well, you caught me."))
    {
      client.Send(message);
    }
  }
}

Implementing CatchMessage gave me some pause.  If I use a lambda, I can’t easily unsubscribe from the event.  Maybe that doesn’t matter in the context of a test, but it’s a bad idea to leave events attached, and I don’t want to be in the habit.  I could unsubscribe safely I had a regular method, but then I need some plumbing to get the data back to the test method. I thought about it for a minute or two and decided to create a class to handle the event.  Later on this turned out to be a pretty good decision, because I was able to substitute some special logic to handle the Perl message in the catcher class without obscuring the test intention.

The basic MessageCatcher just needs to handle the event and store the message data.  Then we can create one of these in our test and use it there.

public class MessageCatcher
{
  public IMessage Message { get; private set; }
  public void CatchMessage(object sender, MessageEventArgs e)
  {
    this.Message = e.Message;
  }
}
[TestMethod]
public void SendReportOverEmail()
{
  var catcher = new MessageCatcher();
  try
  {
    this.MessageReceived += catcher.CatchMessage;
    GenerateMessage();
    ApprovalTests.Email.EmailApprovals.Verify(catcher.Message);
  }
  finally
  {
    this.MessageReceived -= catcher.CatchMessage;
  }
}

But it turns out that the IMessage interface is not what we want, because it’s not what EmailApprovals wants, and its not convertible into a MailMessage.  At the moment it doesn’t look like we can use EmailApprovals, but that doesn’t mean we can’t use ApprovalTests.  The SimpleServer example code shows how to dump the IMessage to a eml file:

// If you wanted to write the message out to a file, then could do this...
File.WriteAllBytes("myfile.eml", e.Message.GetData());

It turns out that *.eml is just a fancy name for “text file”.  I don’t want to dump it to the file system if I can avoid it.  Since GetData() returns a Stream, I should be able to read it directly.

public class MessageCatcher
{
  public string Message { get; private set; }

  public void CatchMessage(object sender, MessageEventArgs e)
  {
    using (var reader = new StreamReader(e.Message.GetData()))
    {
      this.Message = reader.ReadToEnd();
    }
  }
}

Then I can update my test to be an ordinary Approval instead of an EmailApproval.  Since I think I’m about ready to run this, I add a FileLauncherReporter.

[TestMethod]
[UseReporter(typeof(FileLauncherReporter))]
public void SendReportOverEmail()
{
  var catcher = new MessageCatcher();
  try
  {
    this.MessageReceived += catcher.CatchMessage;
    GenerateMessage();
    ApprovalTests.Approvals.Verify(catcher.Message);
  }
  finally
  {
    this.MessageReceived -= catcher.CatchMessage;
  }
}

The test run completes and notepad launches:

image

This is both really cool, and kind of a bummer.  Its really cool because I can (in theory) catch the Perl script’s messages and use them as a baseline for developing my C# implementation.  Although, before moving on, I see one thing I need to take care of, and that is the timestamp in the middle of the message.  A little regex should take care of that:

public void CatchMessage(object sender, MessageEventArgs e)
{
  using (var reader = new StreamReader(e.Message.GetData()))
  {
    this.Message = Regex.Replace(
      reader.ReadToEnd(),
      @"Date:\s[\d\s\w,:-]+\d+\r\n",
      string.Empty);
  }
}

The bigger disappointment is that notepad launched at all.  When Llewellyn used a FileLauncherReporter in his video, Thunderbird launched.  That was cool.  I’m jealous.  Luckily ApprovalTests is open source so I can go see how de did that.  Turns out to be pretty simple, we just need to make sure that when ApprovalTests saves the received file, it uses the .eml extension.  To do this, I make a small change to the way I call Verify().

[TestMethod]
[UseReporter(typeof(FileLauncherReporter))]
public void SendReportOverEmail()
{
  var catcher = new MessageCatcher();
  try
  {
    this.MessageReceived += catcher.CatchMessage;
    GenerateMessage();
    Approvals.Verify(new ApprovalTextWriter(catcher.Message, "eml"));
  }
  finally
  {
    this.MessageReceived -= catcher.CatchMessage;
  }
}

And now the file launches in my default mail client, which happens to be Outlook.

image

Catching Perl

Now that I understand how to catch mail using Rnwood.Smtpserver, and my childish need to see my message in an email client is satisfied, I can get this working with Perl.  I’m going to create a PerlMessageGenerator class for that.

public class PerlMessageGenerator : IMessageGenerator
{
  private const string MissingPerlMessage = "You must have a 32-bit perl at [{0}]. Please visit http://http://www.activestate.com/ to acquire Perl.";
  private const string PerlPath = @"C:\Perl\bin\perl.exe";

  public PerlMessageGenerator()
  {
    if (!File.Exists(PerlPath))
    {
      throw new InvalidOperationException(MissingPerlMessage.FormatWith(PerlPath));
    }
  }

  public void GenerateMessage(string host, string to, string attachementPath)
  {
    var binPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    var arguments = "sendReportRunner.pl {0} {1} {2}".FormatWith(host, to, attachementPath);
    var pi = new ProcessStartInfo(PerlPath, arguments)
    {
      UseShellExecute = false,
      WorkingDirectory = binPath,
      CreateNoWindow = true
    };
    using (var p = new Process { StartInfo = pi })
    {
      p.Start();
      p.WaitForExit();
    }
  }
}

Now I just need to get my scripts into place by adding them as linked files to my test project, with “Copy To Output Directory” set to “Copy Always.”  This actually works, the test catches the Perl message, but as I mentioned the Perl output needs some additional scrubbing over and above the simple message.  SendReport.pl adds another timestamp in the subject line, a Message-ID field that varies on each run, and because it has an attachment, there are MIME boundaries that need to be ditched.  I’ll spare you the gory details.

The important part is that we caught the message.  After creating a separate PerlMessageCatcher to handle all the special cases, my test passes consistently in Visual Studio.  Just for kicks, and because this will eventually be production code, I turn on NCrunch.  And I’m very happy to see that the test passes under NCrunch as well.

Here’s the final test class:

[TestClass]
public class SendReportTest : SmtpFixture
{
  public SendReportTest()
    : base(Ports.SMTP)
  {
  }

  [TestMethod]
  public void SendReportOverEmail()
  {
    var catcher = new PerlMessageCatcher();
    try
    {
      this.MessageReceived += catcher.CatchMessage;
      new PerlMessageGenerator().GenerateMessage("localhost", "jim@contoso.com", "sendreport.pl");
      Approvals.Verify(new ApprovalTextWriter(catcher.Message, "eml"));
    }
    finally
    {
      this.MessageReceived -= catcher.CatchMessage;
    }
  }
}

That’s probably enough for one day.  I’ve made it past Step 6 in my porting list.  PerlMessageCatcher is pretty twisted code and could use some refactoring.  On the other hand, once I make it to Step 9, the (as yet non-existent) .NET implementation will be the canonical implementation, and I can simply use EmailApprovals directly.  The need for the PerlMessageCatcher will go away, so perhaps getting to Step 9 is a more worthy goal than refactoring the catcher.  We’ll see.