Tuesday, August 25, 2009

Sending HTML-based email from .NET applications

Summary: How to use XSL templates to design, build, and test HTML-based email, and avoid common pitfalls in the process.

If you're writing a .NET application that sends HTML-based emails, you have a number of options: you can use .NET Framework's own MailDefinition class, build a custom solution just like Dave did, or use a third party library similar to TemplateEngine by Ader Software. In this post I'll explain how to design, test, and generate complex HTML-based email messages using XSL transformations (XSLT). I'll also explain how to avoid common problems related to HTML-based emails and describe the tools that will help you in doing so.

Before I get to the nitty-gritty, let me briefly summarize the idea. You, a designer (or developer), create an XSLT file that defines an email template. To convert the XSLT template to an HTML message body, you application will load the XSLT file at run time (you will need to make sure that the application has access to the file location) and merge it with the data (data must be formatted as an XML document). The XSLT engine will substitute the placeholders with data and use conditional formatting to generate the HTML markup.

The advantages of using XSLT files for email templates include:
  • No dependency on custom libraries: Everything is built into the .NET Framework.
  • Flexible transformations: XSLT allows rather complex transformations and substitutions. For example, you may not know at design time how many items the message must display, in which case, word-by-word substitution will not work; XSLT handles cases like this nicely.
  • Ease of design and testing: You can build a number of data XML files and use them to test your template without running or debugging the application.
Here are the steps you need to follow:
  1. Define XML structure to hold your data inputs.
    I found it helpful to create static XML files to be used for testing XSLT templates for various combination of inputs. Your sample XML data file may look like this one:

    Newsletter.xml
    <?xml version="1.0" encoding="utf-8" ?>
    <Root>
    <UserName>Mary Sweet</UserName>
    <Message>Lorem ipsum dolor sit amet, consectetur adipiscing elit...</Message>
    <Offers>
    <Offer>Aenean sed nunc nec felis interdum rutrum...</Offer>
    <Offer>Nullam facilisis erat nec dolor tempor sed interdum neque consectetur...</Offer>
    </Offers>
    <UnsubscribeUrl>http://somesite.com/unsubscribe </UnsubscribeUrl>
    </Root>

    You can add sample XML data files to your project so you have them at hand for quick testing once you need to make changes to XSLT files, which you'll create in the next step.
  2. Create XSLT files for HTML-based email templates.
    If your email templates are totally different (say, you use one template for password expiration notices, and another for a weekly newsletter), then you will need to define multiple XSLT files (one for each template), but slight variations in a single template can be handled with the help of XSLT language constructs within the same file. If you end up with more then one XSLT template, I recommend implementing common sections of the message (e.g. header, footer, styles) in separate XSLT files, which you can then include in the final templates. For example here are three shared templates that define common header, footer, and CSS styles:

    Header.xslt
    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template name="Header">
    <!-- If you need common header elements (logo, etc), include them here. -->
    </xsl:template>
    </xsl:stylesheet>

    Footer.xslt
    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template name="Footer">
    <xsl:param name="UnsubscribeUrl"/>
    <p>Best regards,</p>
    
    <p>John Doe, Editor</p>
    
    <!-- Only display message if URL is available -->
    <xsl:if test="string($UnsubscribeUrl) != ''">
    <p>
    <hr/>
    <span class="Footer">
    If you wish to stop receiving this newsletter, please
    <a>
    <xsl:attribute name="href">
    <xsl:value-of select="$UnsubscribeUrl"/>
    </xsl:attribute>unsubscribe</a>.
    </span>
    </p>
    </xsl:if>
    </xsl:template>
    </xsl:stylesheet>

    Style.xslt
    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template name="Style">
    <style type="text/css">
    body { 
    font-family:Verdana; 
    font-size: 10pt; 
    background-color: white; 
    color: black; }
    td {
    font-family:Verdana;
    font-size: 10pt;
    background-color: white; 
    color: black; }
    p {
    margin-top: 8pt; 
    margin-bottom: 8pt; }
    p + p { 
    margin-top: 8pt; 
    margin-bottom: 8pt; 
    }
    <!-- More definitions -->
    </style>
    </xsl:template>
    </xsl:stylesheet>

    Assuming that you place the shared files in the Common subfolder under the main XSLT templates, you can reference them from templates as illustrated in the following example (note: when referencing an external template, use relative path in relation to the caller template):

    Newsletter.xslt
    <?xml version="1.0"?>
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:include href="Common/Style.xslt"/>
    <xsl:include href="Common/Header.xslt"/>
    <xsl:include href="Common/Footer.xslt"/>
    <xsl:output method="html"/>
    <xsl:template match="/">
    <html>
    <head>
    <xsl:call-template name="Style"/>
    </head>
    <body>
    <table>
    <tr>
    <td>
    <xsl:call-template name="Header"/>
    
    <p>Dear <xsl:value-of select="/Root/UserName"/>,</p>
    
    <p><xsl:value-of select="/Root/Message"/></p>
    
    <xsl:if test="/Root/Offers/Offer">
    <p>Here are this week's offers:</p>
    <xsl:for-each select="/Root/Offers/Offer">
    <blockquote>
    <xsl:value-of disable-output-escaping="yes" select="."/>
    </blockquote>
    </xsl:for-each>
    </xsl:if>
    
    <xsl:call-template name="Footer">
    <xsl:with-param name="UnsubscribeUrl" 
    select="/Root/UnsubscribeUrl"/>
    </xsl:call-template>
    </td>
    </tr>
    </table>
    </body>
    </html>
    </xsl:template>
    </xsl:stylesheet>
    Notice how the XSLT markup references data values from the XML document using XPath.
  3. Test XSL transformations.
    Once you got your XSLT templates and sample XML files ready, you can test transformation using Visual Studio (activate XSLT file and select Show XSLT output from the XML menu; on the first attempt you will be prompted to specify the XML file) or other tools. Use different versions of the data XML files to test various permutations of data.
  4. Test HTML-based email in the intended email client programs (web sites).
    Although, your XSLT template may produce perfectly good HTML, there is no guarantee that it will look good in a particular email client. For example, Microsoft Outlook 2007, which uses the Word 2007 HTML rendering engine, does not recognize many of the standard CSS constructs. I learned the hard way that I could not use DIV tags to limit the page width (had to switch to TABLE tags). In the references section at the bottom of the post I included some articles discussing inconsistencies between HTML rendering in various email clients, but they did not help me much (I figured out how to fix formatting issues by trial and error), so the best thing you can do is to check how the mesages appear in various client application (Outlook 2003, Outlook 2007, Thunderbird, Gmail, etc).

    UPDATE: Recommendations given in this step don't seem to work any more, so I now recommend testing using a helper utility I wrote in VBScript. You can ignore the rest of this step.
    For testing email messages, I recommend using the free Mozilla Thunderbird email client. Unlike other email clients, Thunderbird allows pasting unaltered HTML source in the message body, so you can send the message in the exact same format as your program. To send your HTML-based email messages via Thunderbird, do the following:

    • In the program you use to test your template transformations, select the option to view source of the resulting HTML and copy it to the clipboard.
    • Switch to Thunderbird and create a new message.
    • In the message Compose form, click in the body field and select Insert - HTML from the main menu.

    • Insert the HTML markup from the clipboard into the Insert HTML dialog box, and click the Insert button.

  5. Implement code to handle XSL transformations.
    The following example illustrates how a console program loads an XSLT template, merges it with XML document holding data, and uses the resulting HTML as the email body (note: the example does not contain any error handling):

    Program.cs
    using System;
    using System.Xml;
    using System.Xml.Xsl;
    using System.IO;
    using System.Net.Mail;
    
    namespace EmailXsltDemo
    {
    class Program
    {
    static void Main(string[] args)
    {
    // Input data will be defined in this XML document.
    XmlDocument xmlDoc = new XmlDocument();
    
    // We will use XML nodes to define data.
    XmlElement xmlRoot;
    XmlNode xmlNode;
    XmlNode xmlChild;
    
    // XML structure for data inputs (we'll add <Offer> elements later).
    xmlDoc.LoadXml(
    "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
    "<Root>" +
    "<UserName/>" +
    "<Message/>" +
    "<Offers/>" +
    "<UnsubscribeUrl/>" +
    "</Root>");
    
    // Set the values of the XML nodes that will be used by XSLT.
    xmlRoot = xmlDoc.DocumentElement;
    
    xmlNode = xmlRoot.SelectSingleNode("/Root/UserName");
    xmlNode.InnerText = "Mary Sweet";
    
    xmlNode = xmlRoot.SelectSingleNode("/Root/Message");
    xmlNode.InnerText = 
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
    
    xmlNode = xmlRoot.SelectSingleNode("/Root/Offers");
    
    // Insert two <Offer> elements and set their values.
    xmlChild = xmlDoc.CreateNode(XmlNodeType.Element, "Offer", null);
    xmlChild.InnerText = 
    "Aenean sed nunc nec felis interdum rutrum vivamus tempor.";
    xmlNode.AppendChild(xmlChild);
    
    xmlChild = xmlDoc.CreateNode(XmlNodeType.Element, "Offer", null);
    xmlChild.InnerText = 
    "Nullam facilisis erat nec dolor tempor sed interdum neque consectetur.";
    xmlNode.AppendChild(xmlChild);
    
    xmlNode = xmlRoot.SelectSingleNode("/Root/UnsubscribeUrl");
    xmlNode.InnerText = 
    "http://somesite.com/newsletter/unsubscribe/?user=1234567890";
    
    // This is our XSL template.
    XslCompiledTransform xslDoc = new XslCompiledTransform();
    xslDoc.Load(@"..\..\Xslt\Newsletter.xslt");
    
    XsltArgumentList xslArgs = new XsltArgumentList();
    StringWriter writer = new StringWriter();
    
    // Merge XSLT document with data XML document 
    // (writer will hold resulted transformation).
    xslDoc.Transform(xmlDoc, xslArgs, writer);
    
    MailMessage email = new MailMessage();
    
    email.From = new MailAddress(<YOUR_FROM_ADDRESS>);
    email.To.Add(<YOUR_TO_ADDRESS>);
    email.Subject = "Demo message";
    email.IsBodyHtml = true;
    email.Body  = writer.ToString();
    
    // Specify appropriate SMTP server, such as "localhost".
    SmtpClient smtp = new SmtpClient(<YOUR_MAIL_SERVER>);
    
    smtp.Send(email); 
    }
    }
    }
For your convenience, I created a sample Visual Studio 2008 project which illustrates the functionality:

Download sample project

Before running the project, make sure that you substitute the YOUR_FROM_ADDRESS, YOUR_TO_ADDRESS, and YOUR_MAIL_SERVER placeholders with the actual string values.

See also:
Can Email Be Responsive?
Some Tips for Email Layout and Responsiveness
HTML Forms in HTML Emails
What kind of language is XSLT?
How to Code HTML Email Newsletters
The Dark Heart of HTML Email
Guide to CSS support in email clients
2007 Office System Tool: Outlook HTML and CSS Validator
Template Messages Using XSL Transformations and XML Serialization
XSL Transformations using ASP.NET
Getting Started with HTML Emails
HTML Email Boilerplate
Rock Solid HTML Emails
Render Email Templates Using Razor Engine
HTML Email and Accessibility
Everything You Need To Know About Transactional Email But Didn’t Know To Ask
Free online tool to build, test HTML emails
How to Send HTML Emails with Gmail

12 comments:

  1. Hello Alek,
    Very nice and informative article, particaularly for a learner like me. I'm trying samples given by you, and working fine. But I want to add images to header area like logo. but the images are not getting rendered, I tried both options viz. stylesheet as well as path in xml file, but of use.
    Please guide in this
    Regards,
    Manish

    ReplyDelete
  2. I did not discuss handling images in the article for two reasons. First, it's a bit tricky. Second, and most important, it's not an XSLT issue. XSLT just handles transformation of the template and data into an HTML document, so if you were just use an HTML email (without XSLT), how would you handle images? One option would be to reference them from an existing URL (you would need to make sure that the URL is valid and also specify full URL in the SRC element of the IMG tag). In this case, you may need to add some logic to the application to generate the base part of the URL to point to the active environment (dev, test, production, etc). Or you can embed images as part of multi-part email attachment. I haven't tried this because it's a bit tricky to make it work for all email clients, but I've seen some articles on how to approach this option.

    ReplyDelete
  3. Alek,
    Thanks for the prompt reply,
    Can you please suggest few good articles on the same

    Thanks,
    Manish

    ReplyDelete
  4. Can't vouch for accuracy or correctness, but here is a couple of references: CodeProject: Send Inline Image Email (VB.NET 2.0) and System.Net.Mail and embedded images. I'm sure Google can help you find more articles on this.

    ReplyDelete
  5. Thank you for this wonderful tutorial. This is exactly what i need.

    However, when i download the sample project, it doesnt extract. I have tried different systems and different browsers. Help?

    ReplyDelete
  6. I just tried it and it extracted all project files just fine (using WinZip v14). Wat do you mean by saying that it does not extract? Are you getting an error (which one)? Or something else?

    ReplyDelete
  7. I just tried downloading it and I also got the following errors:

    ! C:\Users\Greg\Downloads\Desktop\EmailXsltDemo.zip: Unknown method in EmailXsltDemo.csproj
    ! C:\Users\Greg\Downloads\Desktop\EmailXsltDemo.zip: Unknown method in EmailXsltDemo.sln
    ! C:\Users\Greg\Downloads\Desktop\EmailXsltDemo.zip: Unknown method in Program.cs
    ! C:\Users\Greg\Downloads\Desktop\EmailXsltDemo.zip: Unknown method in Properties\AssemblyInfo.cs
    ! C:\Users\Greg\Downloads\Desktop\EmailXsltDemo.zip: Unknown method in Xslt\Common\Footer.xslt
    ! C:\Users\Greg\Downloads\Desktop\EmailXsltDemo.zip: Unknown method in Xslt\Common\Header.xslt
    ! C:\Users\Greg\Downloads\Desktop\EmailXsltDemo.zip: Unknown method in Xslt\Common\Style.xslt
    ! C:\Users\Greg\Downloads\Desktop\EmailXsltDemo.zip: Unknown method in Xslt\Newsletter.xml
    ! C:\Users\Greg\Downloads\Desktop\EmailXsltDemo.zip: Unknown method in Xslt\Newsletter.xslt

    ReplyDelete
  8. Just to add some more detail to above comment: When trying to extract the zip , I got the errors.

    ReplyDelete
  9. Which program do you use to extract files? I just tried it in WinZip 14 and it works fine. Are you using WinRAR by any chance? Most likely, this is an issue with your compression program. Do a Google search for: "unknown method in" zip (with quotes).

    ReplyDelete
  10. I also tried in in just installed 7-zip and directly in Windows Explorer (on 64-bit Windows 7), and both worked as well.

    ReplyDelete
  11. Any plans to update this for .Net 4.0+ ?

    ReplyDelete
  12. No, but I have a brand new tutorial coming up (in 1-2 weeks) which will address the same problem using more modern technologies (Razor, .NET 4+). For this (XSLT) implementation, I'm not sure upgrade to .NET 4 would require any specific changes (I'm almost certain it can just be recompiled).

    ReplyDelete