24

I am using Postal to render MVC Razor views and send them via email. I have a custom CSS that I have defined specifically for the email views. Currently I am including them as follows:

@Styles.Render("~/Content/EmailStyles.css")

However, this only includes the relative link to the stylesheet, which will not work in an email:

<link href="/Content/EmailStyles.css" rel="stylesheet"/>

I want to include the stylesheet inline so that it functions properly in the email. What is the best way to render the contents of a file-based resource within an MVC view?

luksan
  • 7,661
  • 3
  • 36
  • 38

4 Answers4

37

I had the same question myself, and came across Premailer.Net. It looks like the library you need. Here's what you'd have to do:

  1. Create an extension method to help you embed your CSS into your page; there's an answer on a question on how to embed HTML in a Razor view that should help you. I've modified it for embedding CSS:

    public static class HtmlHelpers
    {
        public static MvcHtmlString EmbedCss(this HtmlHelper htmlHelper, string path)
        {
            // take a path that starts with "~" and map it to the filesystem.
            var cssFilePath = HttpContext.Current.Server.MapPath(path);
            // load the contents of that file
            try
            {
                var cssText = System.IO.File.ReadAllText(cssFilePath);
                var styleElement = new TagBuilder("style");
                styleElement.InnerHtml = cssText;
                return MvcHtmlString.Create(styleElement.ToString());
            }
            catch (Exception ex)
            {
                // return nothing if we can't read the file for any reason
                return null;
            }
        }
    }
    
  2. Then in your Razor template, just go:

    @Html.EmbedCss("~/Content/EmailStyles.css")
    

    to embed your CSS text.

  3. Install the Premailer.Net package in your project; you can get it through NuGet.

  4. Render your Razor view into a string (I guess that's what Postal is for in your process? I believe RazorEngine can also do that).

  5. Run the string through Premailer.Net:

    PreMailer pm = new PreMailer();
    string premailedOutput = pm.MoveCssInline(htmlSource, false);
    
  6. Send as an e-mail!

I've been using this technique in production for a while now, and it seems to be working quite well.

Edit: Remember that styles on pseudo-elements can't be inlined because they don't exist in the markup. I've also noticed the odd little bug in Premailer.Net -- I think their specificity and cascade rules aren't perfectly conformant. Still, it's pretty good and it's one more piece of code I didn't have to write!

pmccloghrylaing
  • 1,110
  • 8
  • 12
Paul d'Aoust
  • 3,019
  • 1
  • 25
  • 36
  • This is a good start.... 1.) How would this be modified to work with bundled styles? 2.) What about bundled .less files using the 'dotless' nuget package which serves up .less files transformed as .css. – sheamus Jun 21 '15 at 05:26
  • Hm, interesting question. Asset compiling and bundling was always the domain of my coworker. I bet Cassette or System.Web.Optimization have some way of outputting a bundle as a string or memory stream; that'd be the place to start. Then you could address both of your questions with one solution. – Paul d'Aoust Jun 23 '15 at 04:01
  • gmail client can not read inline stylesheets like the style-tag inside the header-tag. The only approach is to design with a .css file and parse that id/ class definitions and render them into the html-tags as inline-style="background:blue" that will work on all email clients! – Pascal May 06 '16 at 12:16
  • Can you share an example of how you use postal to get the rendered html back? I'm trying to follow this example but am having trouble implementing the IEmailService http://aboutcode.net/postal/create-mail-message.html – TWilly May 17 '16 at 01:23
  • @TWilly Sorry, unfortunately I've got no experience with the Postal part, and it's outside the scope of my answer. Maybe ask the question asker? But I'm guessing that Postal accepts a message body as a string? If so, you'd just pass the value of `premailedOutput` in my step 5 as the body. Make sure to tell it that you're using a `text/html` mimetype if there's a place for that. – Paul d'Aoust May 18 '16 at 17:22
  • hi Paul, this is great, could you please show a modified version to directly embed CSS block rather than read from a file `.myIconDiv { color : @getColrVariableFromMyAction}` and how to consume/use it inside the view, and would it need additional lids, when I tried it to embed native css blocks it would not compile – Transformer Jan 23 '17 at 00:01
14

Upvoted Paul d'Aoust's answer, but I found this version of his helper method to work a little better for me (wouldn't try to encode things like quotes in the CSS):

public static class CssHelper
{
  public static IHtmlString EmbedCss(this HtmlHelper htmlHelper, string path)
  {
    // take a path that starts with "~" and map it to the filesystem.
    var cssFilePath = HttpContext.Current.Server.MapPath(path);
    // load the contents of that file
    try
    {
      var cssText = File.ReadAllText(cssFilePath);
      return htmlHelper.Raw("<style>\n" + cssText + "\n</style>");
    }
    catch
    {
      // return nothing if we can't read the file for any reason
      return null;
    }
  }
}
dprothero
  • 2,683
  • 2
  • 21
  • 28
  • Thanks for pointing that out; I don't think I noticed that my code was escaping things like quotes. – Paul d'Aoust May 18 '16 at 17:27
  • hi, this is great, could you please show a modified version to directly embed CSS block rather than read from a file `.p { color : @getColrVariableFromMyAction}` and how to consume/use it inside the view. what additional libs do i need? – Transformer Jan 23 '17 at 00:00
  • 1
    @transformer Directly embedding a CSS block would be simple; you just would accept the string as the CSS itself rather than loading a file's contents into a string; I'll leave it as an exercise for the reader :) As for using it in a view, all you'd do is `@Html.EmbedCss(path)`, where `path` is the path to your CSS file. (Or, if you modify the function to use a CSS block, it would be actual CSS.) – Paul d'Aoust Jan 23 '17 at 18:18
  • Got it! so it emits the CSS blocks, I should wrap with inside a section? `body {background-color: powderblue;} h1 {color: blue;} p {color: red;} ` – Transformer Jan 23 '17 at 22:37
  • **can I put the static extension** `public class NotStaticClass{ public static IHtmlString EmbedCssStaticFunc(this HtmlHelper htmlHelper, string path)` **inside a non-static class?** I am trying to group my utils, and would like to throw this in there – Transformer Jan 23 '17 at 23:03
1

I guess you will need to have a custom helper for that. On top of my head, there is no such a method to render the css path including the absolute path of the website.

e.g. http:www.example.com/css/EmailStyles.css

Stay Foolish
  • 3,636
  • 3
  • 26
  • 30
  • 3
    Yeah, but I don't even want to include the absolute path to the stylesheet, since the email client won't retrieve it for security reasons. I want to include the *contents* of the css file. – luksan Jan 12 '13 at 22:36
  • So write a html helper which will render content of your css file as inline style. – mipe34 Jan 12 '13 at 23:12
  • A lot of email clients won't even use styles defined in the header. You are better off with inline styles for emails. See http://www.campaignmonitor.com/css/ – viperguynaz Jan 12 '13 at 23:33
  • no, not a pointless question; just a tricky one. You need to go one step beyond just embedding the styles in the header. See [my answer](http://stackoverflow.com/a/15443654/183350). – Paul d'Aoust Mar 15 '13 at 23:55
0

I realize this is an old question, but here is a modified version of dprothero's answer that will embed bundles. Create a static C# class and put this method in it:

public static IHtmlString EmbedCss(this HtmlHelper htmlHelper, string path)
{
  try
  {
      // Get files from bundle
      StyleBundle b = (StyleBundle)BundleTable.Bundles.GetBundleFor("~/Content/css");
      BundleContext bc = new BundleContext(new HttpContextWrapper(HttpContext.Current), BundleTable.Bundles, "~/Content/css");
      List<BundleFile> files = b.EnumerateFiles(bc).ToList();
      // Create string to return
      string stylestring = "";
      // Iterate files in bundle
      foreach(BundleFile file in files)
      {
          // Get full path to file
          string filepath = HttpContext.Current.Server.MapPath(file.IncludedVirtualPath);
          // Read file text and append to style string
          string filetext = File.ReadAllText(filepath);
          stylestring += $"<!-- Style for {file.IncludedVirtualPath} -->\n<style>\n{filetext}\n</style>\n";
      }
      return htmlHelper.Raw(stylestring);
  }
  catch
  {
      // return nothing if we can't read the file for any reason
      return null;
  }

Then go to whichever view you want to use it in. Be sure to add a using statement so your view can see the CSS helper. I also use TempData to decide whether or not to render it inline:

<!-- Using statement -->
@using Namespace.Helpers;

<!-- Check tempdata flag for whether or not to render inline -->
@if (TempData["inlinecss"] != null)
{
    <!-- Embed CSS with custom code -->
    @Html.EmbedCss("~/Content/css")
}
else
{
    <!-- Use links to reference CSS -->
    @Styles.Render("~/Content/css")
}
brandonstrong
  • 628
  • 7
  • 21