.NET Emails with Html Body via C#
Sending Html emails from an ASP.NET application need not be an elaborate fancy-footwork dance of AlternateView implementations. It can and should be straightforward one liners to literally dispatch an email message to recipients.
The .NET namespace provides, enough rope for developers to hang themselves with and hard-code static HTML string literals into the codebase for no apparent reason other than the fact that they don't know how to mix in dynamic runtime parameters with HTML output.
First and foremost the difficulty is the HTML itself which is reduced for email clients and the MS Outlook capacity to render it (as the lowest common denominator of email clients). Faced with the gigantic task, the novice is tempted by inertia to dive right in and start dealing with those HTML compatibility issues in the same code set dealing with the MailMessage constructs.
So before taking the first step in dispatching the email, step zero would be to separate out the HTML from the construct and deal only with runtime values while leaving the HTML in a static file somewhere to be edited and re-edited as the feedback about various email clients rolls in. Perhaps even create multiple static files for each client.
I typically utilize the following set of abstractions by compromising the design requirements in a back-and-forth with designers/information architects/UX specialists 'till we hit the golden ratio of broadest target of clients with a legible layout and branding mix in one HTML template. But that work is separate from the logic of MailMessage constructs and there is always if-all-else-fails room to create multiple layout templates using the approach below i.e. if your advice about less code to maintain and fail falls on deaf client/management ears.
The first thing we need is to set a convention for dyanmic runtime values embeded in the HTML layout template. I use $[identifier] with Hungarian camel case notation so that the sample HTML might be:
<img src="cid:logo_img_jpg_embeded_mime_b64_data"/><p>Hi $[strUser],</p><p>Your statement for $[strDateStamp] is below:</p><table><tr><th>Item</th><th>Qty.</th><th>Price</th></tr>$[strStatementTableRows]<tr><th></th><th>Total:</th><th>$[strTotal]</th></tr></table><p>Regards,</p><p>BrandX Team</p><p><a href="$[strSiteURL]">Visit us on our website</a></p>
This is a simple enough template so that it could be replaced relatively easily with a String.Format method statement but for demo purposes we know that there will be style attributes and many other HTML paraphernalia attached going forward which, if hard-coded in a string literal, would not be able to be previewed in any way. There is still less-than-ideal question of hard-coding '<tr><td>' in string literals and not being able to preview them but having done a few of these for various employers targeting an array of email clients I found that this is the best pragmatic compromise for the kind of coding I prefer.
After creating this HTML gem, we can store it as a file in a directory relative to the application. I'm not sure how this relates to mobile and desktop development, but for a web app we put this file in the ~/App_Data/emailers folder, and thus hard-code this relative path in the base class dealing with MailMessage constructs as follows:
public static class ServerFileSystem { public static string MapPath(params string[] paths) { return System.Web.Hosting.HostingEnvironment.MapPath(Path.Combine(paths)); } } public class SmtpEmail { private static readonly string emailerRootPath = ServerFileSystem.MapPath("~", "App_Data", "emailers"); }
So far so good. We'll add some fields and overloaded constructors and move onto the send method.
public class SmtpEmail { private static readonly string emailerRootPath = ServerFileSystem.MapPath("~", "App_Data", "emailers"); private readonly string _smtpFromAddress; private readonly string _smtpFromName; private readonly string _smtpPword; private readonly string _hostDNS; private readonly int _port; public SmtpEmail(string smtpFromAddress, string smtpFromName, string hostDNS, int port) { _smtpFromAddress = smtpFromAddress; _smtpFromName = smtpFromName; _hostDNS = hostDNS; _port = port; } public SmtpEmail(string smtpFromAddress, string smtpFromName, string smtpPword, string hostDNS, int port) { _smtpFromAddress = smtpFromAddress; _smtpFromName = smtpFromName; _smtpPword = smtpPword; _hostDNS = hostDNS; _port = port; } }
Nothing fancy here just some logic to deal with SMTP connection settings to dispatch a message:
public void Send(MailMessage message, bool isSsl) { SmtpClient client = string.IsNullOrEmpty(_smtpPword) ? new SmtpClient { Host = _hostDNS, Port = _port, EnableSsl = isSsl, DeliveryMethod = SmtpDeliveryMethod.Network, UseDefaultCredentials = false, } : new SmtpClient { Host = _hostDNS, Port = _port, EnableSsl = isSsl, DeliveryMethod = SmtpDeliveryMethod.Network, UseDefaultCredentials = false, Credentials = new NetworkCredential(_smtpFromAddress, _smtpPword), }; using (client) client.Send(message); }
Implementation of MailMessage and AlternateView
Let's get into the code and justify the rationale after the fact:
public MailMessage Mail(AlternateView htmlBody, string address, string fullName, string subj) { return new MailMessage(new MailAddress(_smtpFromAddress, _smtpFromName), new MailAddress(address, fullName)) { Sender = new MailAddress(_smtpFromAddress, _smtpFromName), Body = "", Subject = subj, AlternateViews = { htmlBody }, IsBodyHtml = true }; }
A more-or-less a trivial abstraction to ensure the sender and the parameters go hand-in-hand.
This leaves the main issue of AlternateView, and embedding of images. To deal with those we'll need to understand the AlternateView class inner workings and embedding of images in the email. AlternateView requires that image data be mapped as resources with identifiers, not entirely unlike the src attribute of the img tag specifies where to get the image data.
We need to get the email body HTML string representation for which I set up a file system helper class to load the data from the file:
public static class IO { public static string LoadFromStream(Stream stream) { using (stream) using (var sr = new StreamReader(stream)) { return sr.ReadToEnd(); } } }
Now we can use it in a method of the SmtpEmail class to deal with dynamic runtime values string injection into identifier placeholders:
public static string EmailBody(string which, IEnumerable<KeyValuePair<string, string>> placeHolders) { StreamReader stream = File.OpenText(Path.Combine(emailerRootPath, which)); var copy = IO.LoadFromStream(stream.BaseStream); copy = placeHolders.Aggregate(copy, (current, placeHolder) => current.Replace(string.Format("$[{0}]", placeHolder.Key), string.Format("{0}", placeHolder.Value))); return copy; }
This is hunky-dory for text only emails but AlternateView image resources now need to be added on the implementation layer.
We could try and internalize to the SmtpEmail instance but it is for my own personal preference intended as an implementation helper not the implementation itself so that logic comes in the form of overloading Html method as follows:
public static AlternateView Html(string body, IEnumerable<KeyValuePair<string, string>> images) { AlternateView html = AlternateView.CreateAlternateViewFromString(body, null, System.Net.Mime.MediaTypeNames.Text.Html); foreach (var image in images) { html.LinkedResources.Add(new LinkedResource(Path.Combine(emailerRootPath, image.Value), System.Net.Mime.MediaTypeNames.Image.Jpeg) { ContentId = image.Key }); } return html; } public static AlternateView Html(string fileName, DictionaryNullable<string, string> placeHolders, IEnumerable<KeyValuePair<string, string>> images) { return Html(EmailBody(fileName, images), placeHolders); }
OK we have constricted (constrained?) the image types to jpeg only and that is a Good EnoughTM for this purpose.
A more general approach would be to handle each image type individually but that would create overkill bloat for me so I left it out.
Also, we made the EmailBody method public for the explicit purpose of those who prefer to invoke the dynamic placeholder injection manually and do some post processing if need be and overloaded method for those who see no need for such things.
That makes the class public interface more verbose and, hence, confusing for first-time use but also more flexible for all those little just-in-caseies.
public class SmtpEmail { private readonly string _smtpFromAddress; private readonly string _smtpFromName; private readonly string _smtpPword; private readonly string _hostDNS; private readonly int _port; private static readonly string emailerRootPath = ServerFileSystem.MapPath("~", "App_Data", "emailers"); public SmtpEmail(string smtpFromAddress, string smtpFromName, string hostDNS, int port) { _smtpFromAddress = smtpFromAddress; _smtpFromName = smtpFromName; _hostDNS = hostDNS; _port = port; } public SmtpEmail(string smtpFromAddress, string smtpFromName, string smtpPword, string hostDNS, int port) { _smtpFromAddress = smtpFromAddress; _smtpFromName = smtpFromName; _smtpPword = smtpPword; _hostDNS = hostDNS; _port = port; } public void Send(MailMessage message, bool isSsl) { SmtpClient client = string.IsNullOrEmpty(_smtpPword) ? new SmtpClient { Host = _hostDNS, Port = _port, EnableSsl = isSsl, DeliveryMethod = SmtpDeliveryMethod.Network, UseDefaultCredentials = false, } : new SmtpClient { Host = _hostDNS, Port = _port, EnableSsl = isSsl, DeliveryMethod = SmtpDeliveryMethod.Network, UseDefaultCredentials = false, Credentials = new NetworkCredential(_smtpFromAddress, _smtpPword), }; using (client) client.Send(message); } public static string EmailBody(string which, IEnumerable<KeyValuePair<string, string>> placeHolders) { StreamReader stream = File.OpenText(Path.Combine(emailerRootPath, which)); var copy = IO.LoadFromStream(stream.BaseStream); copy = placeHolders.Aggregate(copy, (current, placeHolder) => current.Replace(string.Format("$[{0}]", placeHolder.Key), string.Format("{0}", placeHolder.Value))); return copy; } public static AlternateView Html(string body, IEnumerable<KeyValuePair<string, string>> images) { AlternateView html = AlternateView.CreateAlternateViewFromString(body, null, System.Net.Mime.MediaTypeNames.Text.Html); foreach (var image in images) { html.LinkedResources.Add(new LinkedResource(Path.Combine(emailerRootPath, image.Value), System.Net.Mime.MediaTypeNames.Image.Jpeg) { ContentId = image.Key }); } return html; } public static AlternateView Html(string fileName, DictionaryNullable<string, string> placeHolders, IEnumerable<KeyValuePair<string, string>> images) { return Html(EmailBody(fileName, images), placeHolders); } public MailMessage Mail(AlternateView htmlBody, string address, string fullName, string subj) { return new MailMessage(new MailAddress(_smtpFromAddress, _smtpFromName), new MailAddress(address, fullName)) { Sender = new MailAddress(_smtpFromAddress, _smtpFromName), Body = "", Subject = subj, AlternateViews = { htmlBody }, IsBodyHtml = true }; } }
Before we move onto implementing a class instance, we'll create the emailers sub-directory in the App_Data sub-directory of the root directory of the web application. In it we will place a file called logo.jpg (any jpeg will do) as well as statement.tmpl.html and put the wireframe HTML layout template text from above:
<img src="cid:logo_img_jpg_embeded_mime_b64_data"/><p>Hi $[strUser],</p><p>Your statement for $[strDateStamp] is below:</p><table><tr><th>Item</th><th>Qty.</th><th>Price</th></tr>$[strStatementTableRows]<tr><th></th><th>Total:</th><th>$[strTotal]</th></tr></table><p>Regards,</p><p>BrandX Team</p><p><a href="$[strSiteURL]">Visit us on our website</a></p>
Next we'll write a static class Emailers in the web app and create an instance of the SmptEmail client:
public static class Emailers { private const bool IS_SSL = true; private static readonly SmtpEmail client = new SmtpEmail("[email protected]", "[email protected]", "mypassword", "smtp.gmail.com", 587); }
Hopefully by now it is clear that this SMTP account needs to be first created with the fine people of Google Inc over at gmail.com.
We'll implement a method to send the invoice statement as well as embedded image key-value data mappings:
public static class Emailers { private const bool IS_SSL = true; private static readonly SmtpEmail client = new SmtpEmail("[email protected]", "[email protected]", "mypassword", "smtp.gmail.com", 587); private static readonly DictionaryNullable<string, string> statementImages = new Dictionary<string, string>{ {"logo_img_jpg_embeded_mime_b64_data", "logo.jpg"} }; public static bool Statement(string userAddress, string userName, string subject, Uri site) { DictionaryNullable<string, string> dynamicVals = new DictionaryNullable<string, string> { {"strDateStamp", DateTime.Now.ToString("yyyy/MM/dd"), {"strSiteURL", site.AbsoluteUri }, {"strUser", userName} }; AlternateView body = SmtpEmail.Html("statement.tmpl.html", dynamicVals, statementImages); try { client.Send(client.Mail(body, userAddress, userName, "BrandX: " + subject), IS_SSL); return true; } catch (Exception ex) { //log or rethrow or both... } return false; } }
What is missing is statement data which will now needs to be loaded and rendered and injected as string in between <table> elements as a string representation of HTML row and cell tags.
The way I approach this is using IEnumerable<T> parameter where T is a custom object representing a statement item. Suppose we have a class Order which is the type of each item in a statement:
public class Order { public decimal Price; public string Display; public int Quantity; }
Create a method to process the orders and return corresponding HTML row string representation:
private const string TR_INVOICE_DATA = "<tr style=\"text-align: center; font-size: 10pt; height: 65px; color: #364fa2;\">"; private static string ordersHtml(string current, Order item) { return current + appendRow(item.Quantity, item.Price, item.Display); } private static string appendRow(int qnty, decimal price, string display) { return TR_STATEMENT + formatTableCell(display) + formatTableCell(qnty.ToString()) + formatTableCell((price * qnty).ToString()), "background-color: #e7e7e8;") + "</tr>"; } private static string formatTableCell(string input, string style) { return "<td style=\"" + style + "\">" + input + "</td>"; } private static string formatTableCell(string input) { return "<td>" + input + "</td>"; }
Amend the Statement emailer method to take the parameter of type IEnumerable<Order> in order to render out the individual table rows:
public static bool Statement(string userAddress, string userName, string subject, Uri site, IEnuemrable<Order> orders) { string statementHtmlRows = orders.Aggregate("", ordersHtml); DictionaryNullable<string, string> dynamicVals = new DictionaryNullable<string, string> { {"strDateStamp", DateTime.Now.ToString("yyyy/MM/dd"), {"strSiteURL", site.AbsoluteUri }, {"strUser", userName}, {"strStatementTableRows", statementHtmlRows}, {"strTotal", orders.Sum(itm => itm.Price * itm.Quantity).ToString("c")} }; AlternateView body = SmtpEmail.Html("statement.tmpl.html", dynamicVals, statementImages); try { client.Send(client.Mail(body, userAddress, userName, "BrandX: " + subject), IS_SSL); return true; } catch (Exception ex) { //log or rethrow or both... } return false; }
And finally implement this thing from elsewhere in our code as:
if(Emailers.Statement("[email protected]", "Joe Soap", "Order Statement", new Uri("http://www.brandx.com"), new List<Order>())){ //proceed }else{ //notify it didn't go through }
For multiple templates we can always create additional html templates (e.g. statement.tmpl.clientY.html) and overload the Statement method if we know the destination address client.
Be aware this is probably a bad usability idea in the era of Exchange servers and multiple email clients for a single address accessed from home, web browser, mobile device and work depending on the user's circumstance.
public static bool Statement(string userAddress, string userName, string subject, Uri site, IEnuemrable<Order> orders, bool isGmail) { string statementHtmlRows = orders.Aggregate("", ordersHtml); DictionaryNullable<string, string> dynamicVals = new DictionaryNullable<string, string> { {"strDateStamp", DateTime.Now.ToString("yyyy/MM/dd"), {"strSiteURL", site.AbsoluteUri }, {"strUser", userName}, {"strStatementTableRows", statementHtmlRows}, {"strTotal", orders.Sum(itm => itm.Price * itm.Quantity).ToString("c")} }; AlternateView body = SmtpEmail.Html(isGmail ? "statement.tmpl.gmail.html" : "statement.tmpl.html", dynamicVals, statementImages); try { client.Send(client.Mail(body, userAddress, userName, "BrandX: " + subject), IS_SSL); return true; } catch (Exception ex) { //log or rethrow or both... } return false; } //... var customerAddr = string.Trim(getEmailAddressSomehow().ToLowerInvariant()); if(Emailers.Statement(customerAddr, "Joe Soap", "Order Statement", new Uri("http://www.brandx.com"), new List<Order>(), customerAddr.EndsWith("@gmail.com"))){ //proceed }else{ //notify someone it didn't go through }
I found this to be a straight forward way of approaching dispatiching of stylized HTML emails with a reusable class to embed images than any inline coding of HTML strings per implementation.