Saturday, March 10, 2012

NServiceBus 3.0 - SMTP Transport

NServiceBus 3.0 is fresh out of GitHub, and I'm wading through the code and testing out different concepts. You might learn something from this code (the code in this blog post ;), but its far from production stable.

SmtpClient

With NServiceBus 3.0 there is a FtpTransport that enable us to send/receive messages over FTP instead of MSMQ. RFC 2549 is not supported, but before we start with that, lets try something simpler: Send messages using SMTP.

Looking at the code for FtpMessageQueue, ISendMessages and IReceiveMessages are the interfaces that NServiceBus uses for sending and receiving TransportMessages.

.NET includes a SmtpClient, but no POP3 support, so we will now limit ourself to sending messages. Let's start.

The preferred way I like to host NServiceBus applications, are using the NServiceBus.Host.exe. This host has the concept of one input queue and one error queue. At the moment we will not process any input messages (in lack of an pop3 client), but we will send out messages.

Message

It could be a message that would launch a nuclear bomb, brew coffee or buy a movie ticket, but for the example let's use:

public class MyMessage : IMessage
{
  public string Foo;
  public string Bar;
}

IWantToRunAtStartup

There are various ways to interact with the Host, IWantToRunAtStartup is an interface you can decoracte a class with and it will be called at startup. We'll combine the marker interface IConfigureThisEndpoint and IWantCustomInitialization that will setup the smtptransport.

public class Runner :
             IConfigureThisEndpoint,
             IWantToRunAtStartup,
             IWantCustomInitialization
{
  public IBus Bus { get; set; }

  public void Run()
  {
    string line;

    while ((line = Console.ReadLine()) != null)
    {
      Bus.Send(new MyMessage { Bar = "bar", Foo = "foo" });
    }
  }

  public void Stop()
  {
  }

  public void Init()
  {
    Configure
     .With()
     .DefaultBuilder()
     .SmtpTransport()
     .UnicastBus()
     .SendOnly();
    }
}

App.config

The config file is the preferred place to put any configuration that will affect the application; connectionstrings, paths, ....

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="SmtpQueueConfig"
              type="SmtpTransport.SmtpQueueConfig, SmtpTransport" />
    <section name="UnicastBusConfig"
              type="NServiceBus.Config.UnicastBusConfig, NServiceBus.Core" />
    <section name="MessageForwardingInCaseOfFaultConfig"
              type="NServiceBus.Config.MessageForwardingInCaseOfFaultConfig, NServiceBus.Core" />
  </configSections>

  <MessageForwardingInCaseOfFaultConfig ErrorQueue="errors"/>

  <SmtpQueueConfig
      SmtpServer = "smtp.gmail.com"
      Port       = "587"
      EnableSsl  = "true"
      From       = "username@gmail.com"
      UserName   = "username@gmail.com"
      Password   = ""/>

  <UnicastBusConfig>
    <MessageEndpointMappings>
      <add Messages="SmtpTransport" Endpoint="username@gmail.com" />
    </MessageEndpointMappings>
  </UnicastBusConfig>

</configuration>

UnicastBusConfig and MessageForwardingInCaseOfFaultConfig is part of NServiceBus, SmtpQueueConfig is created by me and looks like:

public class SmtpQueueConfig : ConfigurationSection
{
  [ConfigurationProperty("SmtpServer", IsRequired = true)]
  public string SmtpServer
  {
    get { return this["SmtpServer"].ToString(); }
    set { this["SmtpServer"] = value; }
  }

  [ConfigurationProperty("Port", IsRequired = true)]
  public int Port
  {
    get { return (int)this["Port"]; }
    set { this["Port"] = value; }
  }

  [ConfigurationProperty("EnableSsl", IsRequired = true)]
  public bool EnableSsl
  {
    get { return (bool)this["EnableSsl"]; }
    set { this["EnableSsl"] = value; }
  }

  [ConfigurationProperty("From", IsRequired = true)]
  public string From
  {
    get { return this["From"].ToString(); }
    set { this["From"] = value; }
  }

  [ConfigurationProperty("UserName", IsRequired = true)]
  public string UserName
  {
    get { return this["UserName"].ToString(); }
    set { this["UserName"] = value; }
  }

  [ConfigurationProperty("Password", IsRequired = true)]
  public string Password
  {
    get { return this["Password"].ToString(); }
    set { this["Password"] = value; }
  }
}

No big surprises here. If you now goes back to the Init method of the Runner class you will see that we call .SmtpTransport() on the Configure class. This is an extension method:

public static class ConfigureSmtpQueue
{
  public static Configure SmtpTransport(this Configure config)
  {
    var smtpQueue = config.Configurer.ConfigureComponent<SmtpMessageQueue>(
                                 DependencyLifecycle.SingleInstance);
    var cfg = Configure.GetConfigSection<SmtpQueueConfig>();

    if (cfg != null)
    {
      smtpQueue.ConfigureProperty(t => t.SmtpServer, cfg.SmtpServer);
      smtpQueue.ConfigureProperty(t => t.Port, cfg.Port);
      smtpQueue.ConfigureProperty(t => t.EnableSsl, cfg.EnableSsl);
      smtpQueue.ConfigureProperty(t => t.From, cfg.From);
      smtpQueue.ConfigureProperty(t => t.UserName, cfg.UserName);
      smtpQueue.ConfigureProperty(t => t.Password, cfg.Password);
    }

    return config;
  }
}

It's the binding between the configuration and our transport class. Then finally:

SmtpMessageQueue

public class SmtpMessageQueue : ISendMessages, IReceiveMessages
{
  public string SmtpServer { get; set; }
  public int Port { get; set; }
  public bool EnableSsl { get; set; }
  public string From { get; set; }
  public string UserName { get; set; }
  public string Password { get; set; }

  public void Send(TransportMessage message, Address address)
  {
    using (var client = new SmtpClient(SmtpServer, Port))
    {
      client.EnableSsl = EnableSsl;
      client.Credentials = new NetworkCredential(UserName, Password);

      var serializer = new JavaScriptSerializer();

      var mailMessage = new MailMessage(From, address.ToString())
                        {
                          Subject = "NServiceBus TransportMessage",
                          Body = serializer.Serialize(CreateMailMetaData(message));
                        };

      mailMessage.Attachments.Add(
        new Attachment(
          new MemoryStream(message.Body),
          "body.bin",
          "application/octet-stream"));

          client.Send(mailMessage);
        }
    }

  private MailMetaData CreateMailMetaData(TransportMessage message)
  {
    return new MailMetaData
    {
      CorrelationId = message.CorrelationId,
      Id = message.Id,
      IdForCorrelation = message.IdForCorrelation,
      ReplyToAddress = message.ReplyToAddress.ToString(),
      TimeToBeReceived = message.TimeToBeReceived,
      Header = message.Headers.Select(
        x => new HeaderPair
             {
               Name = x.Key,
               Value = x.Value }).ToList()
      };
  }

  public void Init(Address address, bool transactional)
  {
    // TODO: add a pop3 client and poll for new messages
  }

  public bool HasMessage()
  {
    return false;
  }

  public TransportMessage Receive()
  {
    return null;
  }
}

public class MailMetaData
{
  public string CorrelationId;
  public string Id;
  public string IdForCorrelation;
  public string ReplyToAddress;
  public TimeSpan TimeToBeReceived;
  public List<HeaderPair> Header;
}

public class HeaderPair
{
  public string Name;
  public string Value;
}

Does it work? This is the email in gmail:

Yes! We'll fix the pop3 / receiver next week.

No comments:

Post a Comment