From cb7dcc7cf77380f39fa25f7cd26711c9fa75a706 Mon Sep 17 00:00:00 2001 From: CodeAnimal Date: Wed, 27 Jan 2016 13:45:10 +0000 Subject: [PATCH] Change EmailViewResult to use the final Parser output, rather than the result output. EmailViewResult now gives an accurate output if a custom parser is used (e.g. when inlining the CSS), as it now uses the MailMessage body as the output, rather than the Renderer's result. Now also checks for alternative views if no format query is given, to render emails with embedded images. --- src/Postal/EmailViewResult.cs | 450 +++++++++++++++++++--------------- 1 file changed, 246 insertions(+), 204 deletions(-) diff --git a/src/Postal/EmailViewResult.cs b/src/Postal/EmailViewResult.cs index 48f62af..68339cb 100644 --- a/src/Postal/EmailViewResult.cs +++ b/src/Postal/EmailViewResult.cs @@ -1,204 +1,246 @@ -using System; -using System.IO; -using System.Linq; -using System.Net.Mail; -using System.Text; -using System.Web.Mvc; - -namespace Postal -{ - /// - /// Renders a preview of an email to display in the browser. - /// - public class EmailViewResult : ViewResult - { - const string TextContentType = "text/plain"; - const string HtmlContentType = "text/html"; - - IEmailViewRenderer Renderer { get; set; } - IEmailParser Parser { get; set; } - Email Email { get; set; } - - /// - /// Creates a new . - /// - public EmailViewResult(Email email, IEmailViewRenderer renderer, IEmailParser parser) - { - Email = email; - Renderer = renderer ?? new EmailViewRenderer(ViewEngineCollection); - Parser = parser ?? new EmailParser(Renderer); - } - - /// - /// Creates a new . - /// - public EmailViewResult(Email email) - : this(email, null, null) - { - } - - /// - /// When called by the action invoker, renders the view to the response. - /// - public override void ExecuteResult(ControllerContext context) - { - var httpContext = context.RequestContext.HttpContext; - var query = httpContext.Request.QueryString; - var format = query["format"]; - var contentType = ExecuteResult(context.HttpContext.Response.Output, format); - httpContext.Response.ContentType = contentType; - } - - /// - /// Writes the email preview in the given format. - /// - /// The content type for the HTTP response. - public string ExecuteResult(TextWriter writer, string format = null) - { - var result = Renderer.Render(Email); - var mailMessage = Parser.Parse(result, Email); - - // no special requests; render what's in the template - if (string.IsNullOrEmpty(format)) - { - if (!mailMessage.IsBodyHtml) - { - writer.Write(result); - return TextContentType; - } - - var template = Extract(result); - template.Write(writer); - return HtmlContentType; - } - - // Check if alternative - var alternativeContentType = CheckAlternativeViews(writer, mailMessage, format); - - if (!string.IsNullOrEmpty(alternativeContentType)) - return alternativeContentType; - - if (format == "text") - { - if(mailMessage.IsBodyHtml) - throw new NotSupportedException("No text view available for this email"); - - writer.Write(result); - return TextContentType; - } - - if (format == "html") - { - if (!mailMessage.IsBodyHtml) - throw new NotSupportedException("No html view available for this email"); - - var template = Extract(result); - template.Write(writer); - return HtmlContentType; - } - - throw new NotSupportedException(string.Format("Unsupported format {0}", format)); - } - - static string CheckAlternativeViews(TextWriter writer, MailMessage mailMessage, string format) - { - var contentType = format == "html" - ? HtmlContentType - : TextContentType; - - // check for alternative view - var view = mailMessage.AlternateViews.FirstOrDefault(v => v.ContentType.MediaType == contentType); - - if (view == null) - return null; - - string content; - using (var reader = new StreamReader(view.ContentStream)) - content = reader.ReadToEnd(); - - content = ReplaceLinkedImagesWithEmbeddedImages(view, content); - - writer.Write(content); - return contentType; - } - - class TemplateParts - { - readonly string header; - readonly string body; - - public TemplateParts(string header, string body) - { - this.header = header; - this.body = body; - } - - public void Write(TextWriter writer) - { - writer.WriteLine(""); - writer.WriteLine(body); - } - } - - static TemplateParts Extract(string template) - { - var headerBuilder = new StringBuilder(); - - using (var reader = new StringReader(template)) - { - // try to read until we passed headers - var line = reader.ReadLine(); - - while (line != null) - { - if (string.IsNullOrEmpty(line)) - { - return new TemplateParts(headerBuilder.ToString(), reader.ReadToEnd()); - } - - headerBuilder.AppendLine(line); - line = reader.ReadLine(); - } - } - - return null; - } - - internal static string ReplaceLinkedImagesWithEmbeddedImages(AlternateView view, string content) - { - var resources = view.LinkedResources; - - if (!resources.Any()) - return content; - - foreach (var resource in resources) - { - var find = "src=\"cid:" + resource.ContentId + "\""; - var imageData = ComposeImageData(resource); - content = content.Replace(find, "src=\"" + imageData + "\""); - } - - return content; - } - - static string ComposeImageData(LinkedResource resource) - { - var contentType = resource.ContentType.MediaType; - var bytes = ReadFully(resource.ContentStream); - return string.Format("data:{0};base64,{1}", - contentType, - Convert.ToBase64String(bytes)); - } - - static byte[] ReadFully(Stream input) - { - using (var ms = new MemoryStream()) - { - input.CopyTo(ms); - return ms.ToArray(); - } - } - } -} +using System; +using System.IO; +using System.Linq; +using System.Net.Mail; +using System.Text; +using System.Web.Mvc; + +namespace Postal +{ + /// + /// Renders a preview of an email to display in the browser. + /// + public class EmailViewResult : ViewResult + { + const string TextContentType = "text/plain"; + const string HtmlContentType = "text/html"; + + IEmailViewRenderer Renderer { get; set; } + IEmailParser Parser { get; set; } + Email Email { get; set; } + + /// + /// Creates a new . + /// + public EmailViewResult(Email email, IEmailViewRenderer renderer, IEmailParser parser) + { + Email = email; + Renderer = renderer ?? new EmailViewRenderer(ViewEngineCollection); + Parser = parser ?? new EmailParser(Renderer); + } + + /// + /// Creates a new . + /// + public EmailViewResult(Email email) + : this(email, null, null) + { + } + + /// + /// When called by the action invoker, renders the view to the response. + /// + public override void ExecuteResult(ControllerContext context) + { + var httpContext = context.RequestContext.HttpContext; + var query = httpContext.Request.QueryString; + var format = query["format"]; + var contentType = ExecuteResult(context.HttpContext.Response.Output, format); + httpContext.Response.ContentType = contentType; + } + + /// + /// Writes the email preview in the given format. + /// + /// The content type for the HTTP response. + public string ExecuteResult(TextWriter writer, string format = null) + { + var result = Renderer.Render(Email); + var mailMessage = Parser.Parse(result, Email); + + // no special requests; render what's in the template + if (string.IsNullOrEmpty(format)) + { + // Check if HTML alternative + if (mailMessage.AlternateViews.Any(v => v.ContentType.MediaType == HtmlContentType)) + { + var alternativeHtmlContentType = CheckAlternativeViews(writer, mailMessage, "html"); + + if (!string.IsNullOrEmpty(alternativeHtmlContentType)) + return HtmlContentType; + } + + // Check if Text alternative + if (mailMessage.AlternateViews.Any(v => v.ContentType.MediaType == TextContentType)) + { + var alternativeTextContentType = CheckAlternativeViews(writer, mailMessage, "text"); + + if (!string.IsNullOrEmpty(alternativeTextContentType)) + return TextContentType; + } + + var template = Extract(result, mailMessage); + template.Write(writer); + + if (mailMessage.IsBodyHtml) + return HtmlContentType; + + return TextContentType; + } + + // Check if alternative + var alternativeContentType = CheckAlternativeViews(writer, mailMessage, format); + + if (!string.IsNullOrEmpty(alternativeContentType)) + return alternativeContentType; + + + if (format == "text") + { + if(mailMessage.IsBodyHtml) + throw new NotSupportedException("No text view available for this email"); + + var template = Extract(result, mailMessage); + template.Write(writer); + return TextContentType; + } + + if (format == "html") + { + if (!mailMessage.IsBodyHtml) + throw new NotSupportedException("No html view available for this email"); + + var template = Extract(result, mailMessage); + template.Write(writer); + return HtmlContentType; + } + + throw new NotSupportedException(string.Format("Unsupported format {0}", format)); + } + + static string CheckAlternativeViews(TextWriter writer, MailMessage mailMessage, string format) + { + var contentType = format == "html" + ? HtmlContentType + : TextContentType; + + // check for alternative view + var view = mailMessage.AlternateViews.FirstOrDefault(v => v.ContentType.MediaType == contentType); + + if (view == null) + return null; + + string content; + using (var reader = new StreamReader(view.ContentStream)) + content = reader.ReadToEnd(); + + content = ReplaceLinkedImagesWithEmbeddedImages(view, content); + + writer.Write(content); + return contentType; + } + + class TemplateParts + { + readonly string header; + readonly string body; + + public TemplateParts(string header, string body) + { + this.header = header; + this.body = body; + } + + public void Write(TextWriter writer) + { + writer.WriteLine(""); + writer.WriteLine(body); + } + } + + static TemplateParts Extract(string template) + { + var headerBuilder = new StringBuilder(); + + using (var reader = new StringReader(template)) + { + // try to read until we passed headers + var line = reader.ReadLine(); + + while (line != null) + { + if (string.IsNullOrEmpty(line)) + { + return new TemplateParts(headerBuilder.ToString(), reader.ReadToEnd()); + } + + headerBuilder.AppendLine(line); + line = reader.ReadLine(); + } + } + + return null; + } + + static TemplateParts Extract(string template, MailMessage mailMessage) + { + var headerBuilder = new StringBuilder(); + + using (var reader = new StringReader(template)) + { + // try to read until we passed headers + var line = reader.ReadLine(); + + while (line != null) + { + if (string.IsNullOrEmpty(line)) + { + return new TemplateParts(headerBuilder.ToString(), mailMessage.Body); + } + + headerBuilder.AppendLine(line); + line = reader.ReadLine(); + } + } + + return null; + } + + internal static string ReplaceLinkedImagesWithEmbeddedImages(AlternateView view, string content) + { + var resources = view.LinkedResources; + + if (!resources.Any()) + return content; + + foreach (var resource in resources) + { + var find = "src=\"cid:" + resource.ContentId + "\""; + var imageData = ComposeImageData(resource); + content = content.Replace(find, "src=\"" + imageData + "\""); + } + + return content; + } + + static string ComposeImageData(LinkedResource resource) + { + var contentType = resource.ContentType.MediaType; + var bytes = ReadFully(resource.ContentStream); + return string.Format("data:{0};base64,{1}", + contentType, + Convert.ToBase64String(bytes)); + } + + static byte[] ReadFully(Stream input) + { + using (var ms = new MemoryStream()) + { + input.CopyTo(ms); + return ms.ToArray(); + } + } + } +}