an ASP.NET, C# technical blog, by Gianni Tropiano

Helper Mailer class for template-based e-mails with embedded images

by CodeGolem 25. November 2009 12:38

Article of the day at ASP.NET (january 13th, 2010)

This is a natural evolution of a Previous Post about using UserControls as mail templates.

Here I will show a way to code a reusable helper class to:

  • create a MailMessage
  • set its body by eventually using a paremeterized template control
  • automatically embed images as LinkedResources
  • send the MailMessage using the desired SMTP server

Full source control is available at the bottom of this article.

Let's give a look to the main parts of the code.

In the previous post, I already showed how to use a UserControl as a mail template.

The Mailer class uses the same technique, but I've added something new about template's parameters:

the previous article showed how to use an interface to set parameters on the template control. Here we will use reflection instead.

The Mailer class has, among others, two Properties (and corresponding private fields) to hold a reference to the template control and a generic dictionary of key-value pairs for template's parameters:


/// <summary>
/// Gets or sets a reference to a Control to be used as template
/// </summary>
public Control TemplateControl
{
    get { return _templateControl; }
    set { _templateControl = value; }
}

/// <summary>
/// Gets or sets a Dictionary to hold parameters for the template control
/// </summary>
public Dictionary<string, object> MailTemplateParameters
{
    get { return _mailTemplateParameters; }
    set { _mailTemplateParameters = value; }
}

Here is the Mailer's only method, which does all the magic:


/// <summary>
/// Sends the e-mail
/// </summary>
public void Send()
{
    // checks if a template control is set
    if (_templateControl != null)
    {
        // gets the control's Type
        Type templateType = _templateControl.GetType();
        // sets properties from the dictionary using reflection
        foreach (KeyValuePair<string, object> parameter in MailTemplateParameters)
            templateType.GetProperty(parameter.Key).SetValue(_templateControl, parameter.Value, null);

        // renders the template control as a string
        StringBuilder stringBuilder = new StringBuilder();
        TextWriter textWriter = new StringWriter(stringBuilder);
        HtmlTextWriter htmlWriter = new HtmlTextWriter(textWriter);
        _templateControl.RenderControl(htmlWriter);
        _mailMessage.Body = stringBuilder.ToString();

        // release resources
        htmlWriter.Close();
        htmlWriter.Dispose();
        textWriter.Dispose();
    }

    // embeds images for html messages
    if (_mailMessage.IsBodyHtml)
    {
        ImagesEmbedder embedder = new ImagesEmbedder();
        embedder.EmbedImages(_mailMessage);
    }

    // sends the message
    SmtpClient client = new SmtpClient(_mailServer);
    client.Send(_mailMessage);

    // release resources
    _mailMessage.Dispose();
    _mailTemplateParameters.Clear();
}

It starts checking whether a template control was set. If so, it enumerates all the key-value pairs within the dictionary and sets corresponding values to the control's properies.

This means, we could use the following sample UserControl as template:


<%@ Control Language="C#" AutoEventWireup="true" CodeFile="Mail.ascx.cs" Inherits="Mail" %>
<html>
<head>
    <title></title>
</head>
<body>
    <img src="Images/MailBanner.jpg" alt="" /><br />

    Hello <%= Username %>, this is a templated e-mail for you!
</body>
</html>

With its corresponding codebehind:


using System.Web;
using System.Web.UI;

public partial class Mail : System.Web.UI.UserControl
{
    #region Public properties

    public string Username { get; set; }

    #endregion
}

This template uses a single property: "Username". We would use this template with the Mailer class this way:


// load the template control
Control templateControl = LoadControl("Mail.ascx");

// create mailer object
Mailer mailer = new Mailer();

// set the template control
mailer.TemplateControl = templateControl;

// add parameter to the dictionary
mailer.MailTemplateParameters.Add("Username", "Code Golem");

// set smtp server
mailer.MailServer = "mysmtpserver";

// specify it's an html message
mailer.MailMessage.IsBodyHtml = true;

// set from, to, subject
mailer.MailMessage.Subject = "Test template message";
mailer.MailMessage.From = "codegolem@codegolem.com";
mailer.MailMessage.To.Add(new MailAddress("user@hisdomain.com"));

// send the message
mailer.Send();

This is one step forward to using specific interfaces for each template!

Now, the other interesting part: image embedding.

This is done through another helper class: the ImagesEmbedder.

It has a single private field to store the messages linked resources:


/// <summary>
/// Holds a list of linked resources
/// </summary>
List<LinkedResource> _linkedResources;

And a single public method to perform embedding:


/// <summary>
/// Embeds images in the MailMessage
/// </summary>
/// <param name="mailMessage">MailMessage instance</param>
public void EmbedImages(MailMessage mailMessage)
{
    // creates a new list of linked resources
    _linkedResources = new List<LinkedResource>();

    // regular expression to find <img> tags within the message's body
    Regex regex = new Regex("src=(?:\"|\')?(?<imgSrc>[^>]*[^/].(?:jpeg|jpg|bmp|gif|png|tif))(?:\"|\')?");

    // replaces urls in <img> tag with corresponding unique ids
    string body = regex.Replace(mailMessage.Body, imageHandler);

    // creates an alternate view with linked resources
    AlternateView htmlView = AlternateView.CreateAlternateViewFromString(body, null, "text/html");
    foreach (LinkedResource linkedResource in _linkedResources)
        htmlView.LinkedResources.Add(linkedResource);

    mailMessage.AlternateViews.Add(htmlView);

    // release resources
    _linkedResources.Clear();
    _linkedResources = null;
}

This goes through the message's body looking for <img> tags using regular expressions. Each match is handled by the following delegate:


/// <summary>
/// Updates imgs' src and adds linkedResources to the list
/// </summary>
/// <param name="match"></param>
/// <returns></returns>
private string imageHandler(Match match)
{
    // gets the imgs src attribute from the Regular Expression's match
    string src = match.Groups[1].Value;
   
    Uri uri = null;

    // try to create a valid Uri by the img's src
    try
    {
        uri = new Uri(src);
    }
    catch
    {
    }

    LinkedResource linkedResource = null;

    // if it's a valid, absolute Uri, download the image from remote source using a WebClient request
    if (uri != null && uri.IsAbsoluteUri)
    {
        WebClient webClient = new WebClient();

        try
        {
            byte[] buf = webClient.DownloadData(uri);
            MemoryStream memoryStream = new MemoryStream(buf);
            linkedResource = new LinkedResource(memoryStream);
        }
        catch
        {
        }
        finally
        {
            webClient.Dispose();
        }
    }
    else
        // otherwise, get the image from local server
        linkedResource = new LinkedResource(HttpContext.Current.Server.MapPath(src));
    
    if (linkedResource != null)
    {
        // add the linked resource to the list
        linkedResource.ContentId = Guid.NewGuid().ToString();
        linkedResource.TransferEncoding = System.Net.Mime.TransferEncoding.Base64;
        _linkedResources.Add(linkedResource);

        // update the img's src attribute with the linked resource's unique id
        return string.Format("src='cid:{0}'", linkedResource.ContentId);
    }
    else
        // leave src unchanged if we can't get the image
        return match.Value;
}

This delegate tries to understand if the img src attribute point to an absolute Uri (I didn't find any better way than trying to create an Uri object and catching exceptions when it's not a valid Uri string).

If it's a remote image, it's downloaded using a WebClient request.
The downloaded (or local) image is then used to create a LinkedResource, which is added to the list.

The ImageEmbedder finally creates an alternate view and stores all LinkedResources from the list.

The embedder is invoked from the Mailer class before sending the e-mail.

So we can reuse the Mailer class to send templated e-mails with few lines of code in the calling application.

Here is the full source code for the Mailer and ImagesEmbedder classes.
CodeGolem_Mailer.zip (4.19 kb)

Happy image-embedding-template-mailing! Wink

Tags: , ,

ASP.NET