Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HtmlHelper attributes and embedded images content type #98

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 51 additions & 49 deletions src/Postal/EmailViewResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,107 +18,109 @@ public class EmailViewResult : ViewResult
IEmailViewRenderer Renderer { get; set; }
IEmailParser Parser { get; set; }
Email Email { get; set; }
string Format { get; set; }

/// <summary>
/// Creates a new <see cref="EmailViewResult"/>.
/// </summary>
public EmailViewResult(Email email, IEmailViewRenderer renderer, IEmailParser parser)
public EmailViewResult( Email email, IEmailViewRenderer renderer, IEmailParser parser, string format = null )
{
Email = email;
Renderer = renderer ?? new EmailViewRenderer(ViewEngineCollection);
Parser = parser ?? new EmailParser(Renderer);
Renderer = renderer ?? new EmailViewRenderer( ViewEngineCollection );
Parser = parser ?? new EmailParser( Renderer );
Format = format;
}

/// <summary>
/// Creates a new <see cref="EmailViewResult"/>.
/// </summary>
public EmailViewResult(Email email)
: this(email, null, null)
public EmailViewResult( Email email, string format = null )
: this( email, null, null, format )
{
}

/// <summary>
/// When called by the action invoker, renders the view to the response.
/// </summary>
public override void ExecuteResult(ControllerContext context)
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);
var format = Format ?? query["format"];
var contentType = ExecuteResult( context.HttpContext.Response.Output, format );
httpContext.Response.ContentType = contentType;
}

/// <summary>
/// Writes the email preview in the given format.
/// </summary>
/// <returns>The content type for the HTTP response.</returns>
public string ExecuteResult(TextWriter writer, string format = null)
public string ExecuteResult( TextWriter writer, string format = null )
{
var result = Renderer.Render(Email);
var mailMessage = Parser.Parse(result, Email);
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 (string.IsNullOrEmpty( format ))
{
if (!mailMessage.IsBodyHtml)
{
writer.Write(result);
writer.Write( result );
return TextContentType;
}

var template = Extract(result);
template.Write(writer);
var template = Extract( result );
template.Write( writer );
return HtmlContentType;
}

// Check if alternative
var alternativeContentType = CheckAlternativeViews(writer, mailMessage, format);
var alternativeContentType = CheckAlternativeViews( writer, mailMessage, format );

if (!string.IsNullOrEmpty(alternativeContentType))
if (!string.IsNullOrEmpty( alternativeContentType ))
return alternativeContentType;

if (format == "text")
{
if(mailMessage.IsBodyHtml)
throw new NotSupportedException("No text view available for this email");
if (mailMessage.IsBodyHtml)
throw new NotSupportedException( "No text view available for this email" );

writer.Write(result);
writer.Write( result );
return TextContentType;
}

if (format == "html")
{
if (!mailMessage.IsBodyHtml)
throw new NotSupportedException("No html view available for this email");
throw new NotSupportedException( "No html view available for this email" );

var template = Extract(result);
template.Write(writer);
var template = Extract( result );
template.Write( writer );
return HtmlContentType;
}

throw new NotSupportedException(string.Format("Unsupported format {0}", format));
throw new NotSupportedException( string.Format( "Unsupported format {0}", format ) );
}

static string CheckAlternativeViews(TextWriter writer, MailMessage mailMessage, string 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);
var view = mailMessage.AlternateViews.FirstOrDefault( v => v.ContentType.MediaType == contentType );

if (view == null)
return null;

string content;
using (var reader = new StreamReader(view.ContentStream))
using (var reader = new StreamReader( view.ContentStream ))
content = reader.ReadToEnd();

content = ReplaceLinkedImagesWithEmbeddedImages(view, content);
content = ReplaceLinkedImagesWithEmbeddedImages( view, content );

writer.Write(content);
writer.Write( content );
return contentType;
}

Expand All @@ -127,46 +129,46 @@ class TemplateParts
readonly string header;
readonly string body;

public TemplateParts(string header, string body)
public TemplateParts( string header, string body )
{
this.header = header;
this.body = body;
}

public void Write(TextWriter writer)
public void Write( TextWriter writer )
{
writer.WriteLine("<!--");
writer.WriteLine(header);
writer.WriteLine("-->");
writer.WriteLine(body);
writer.WriteLine( "<!--" );
writer.WriteLine( header );
writer.WriteLine( "-->" );
writer.WriteLine( body );
}
}

static TemplateParts Extract(string template)
static TemplateParts Extract( string template )
{
var headerBuilder = new StringBuilder();

using (var reader = new StringReader(template))
using (var reader = new StringReader( template ))
{
// try to read until we passed headers
var line = reader.ReadLine();

while (line != null)
{
if (string.IsNullOrEmpty(line))
if (string.IsNullOrEmpty( line ))
{
return new TemplateParts(headerBuilder.ToString(), reader.ReadToEnd());
return new TemplateParts( headerBuilder.ToString(), reader.ReadToEnd() );
}

headerBuilder.AppendLine(line);
headerBuilder.AppendLine( line );
line = reader.ReadLine();
}
}

return null;
}

internal static string ReplaceLinkedImagesWithEmbeddedImages(AlternateView view, string content)
internal static string ReplaceLinkedImagesWithEmbeddedImages( AlternateView view, string content )
{
var resources = view.LinkedResources;

Expand All @@ -176,27 +178,27 @@ internal static string ReplaceLinkedImagesWithEmbeddedImages(AlternateView view,
foreach (var resource in resources)
{
var find = "src=\"cid:" + resource.ContentId + "\"";
var imageData = ComposeImageData(resource);
content = content.Replace(find, "src=\"" + imageData + "\"");
var imageData = ComposeImageData( resource );
content = content.Replace( find, "src=\"" + imageData + "\"" );
}

return content;
}

static string ComposeImageData(LinkedResource resource)
static string ComposeImageData( LinkedResource resource )
{
var contentType = resource.ContentType.MediaType;
var bytes = ReadFully(resource.ContentStream);
return string.Format("data:{0};base64,{1}",
var bytes = ReadFully( resource.ContentStream );
return string.Format( "data:{0};base64,{1}",
contentType,
Convert.ToBase64String(bytes));
Convert.ToBase64String( bytes ) );
}

static byte[] ReadFully(Stream input)
static byte[] ReadFully( Stream input )
{
using (var ms = new MemoryStream())
{
input.CopyTo(ms);
input.CopyTo( ms );
return ms.ToArray();
}
}
Expand Down
40 changes: 31 additions & 9 deletions src/Postal/HtmlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,45 @@ public static class HtmlExtensions
/// <param name="imagePathOrUrl">An image file path or URL. A file path can be relative to the web application root directory.</param>
/// <param name="alt">The content for the &lt;img alt&gt; attribute.</param>
/// <returns>An HTML &lt;img&gt; tag.</returns>
public static IHtmlString EmbedImage(this HtmlHelper html, string imagePathOrUrl, string alt = "")
[Obsolete( "Use second method overload" )]
public static IHtmlString EmbedImage( this HtmlHelper html, string imagePathOrUrl, string alt = "" )
{
if (string.IsNullOrWhiteSpace(imagePathOrUrl)) throw new ArgumentException("Path or URL required", "imagePathOrUrl");
return EmbedImage( html, imagePathOrUrl, new { alt = alt } );
}

/// <summary>
/// Embeds the given image into the email and returns an HTML &lt;img&gt; tag referencing the image.
/// </summary>
/// <param name="html">The <see cref="HtmlHelper"/>.</param>
/// <param name="imagePathOrUrl">An image file path or URL. A file path can be relative to the web application root directory.</param>
/// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
/// <returns>An HTML &lt;img&gt; tag.</returns>
public static IHtmlString EmbedImage( this HtmlHelper html, string imagePathOrUrl, object htmlAttributes = null )
{
if (string.IsNullOrWhiteSpace( imagePathOrUrl )) throw new ArgumentException( "Path or URL required", "imagePathOrUrl" );

if (IsFileName(imagePathOrUrl))
if (IsFileName( imagePathOrUrl ))
{
imagePathOrUrl = html.ViewContext.HttpContext.Server.MapPath(imagePathOrUrl);
imagePathOrUrl = html.ViewContext.HttpContext.Server.MapPath( imagePathOrUrl );
}
var imageEmbedder = (ImageEmbedder)html.ViewData[ImageEmbedder.ViewDataKey];
var resource = imageEmbedder.ReferenceImage(imagePathOrUrl);
return new HtmlString(string.Format("<img src=\"cid:{0}\" alt=\"{1}\"/>", resource.ContentId, html.AttributeEncode(alt)));
var resource = imageEmbedder.ReferenceImage( imagePathOrUrl );

var img = new TagBuilder( "img" );

if (htmlAttributes is string) // method overload back compatibility
img.MergeAttribute( "alt", (string)htmlAttributes );
else
img.MergeAttributes( HtmlHelper.AnonymousObjectToHtmlAttributes( htmlAttributes ) );

img.MergeAttribute( "src", String.Format( "cid:{0}", resource.ContentId ), true );
return new MvcHtmlString( img.ToString( TagRenderMode.SelfClosing ) );
}

static bool IsFileName(string pathOrUrl)
static bool IsFileName( string pathOrUrl )
{
return !(pathOrUrl.StartsWith("http:", StringComparison.OrdinalIgnoreCase)
|| pathOrUrl.StartsWith("https:", StringComparison.OrdinalIgnoreCase));
return !(pathOrUrl.StartsWith( "http:", StringComparison.OrdinalIgnoreCase )
|| pathOrUrl.StartsWith( "https:", StringComparison.OrdinalIgnoreCase ));
}
}
}
12 changes: 11 additions & 1 deletion src/Postal/ImageEmbedder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,17 @@ public static LinkedResource CreateLinkedResource(string imagePathOrUrl)
{
var client = new WebClient();
var bytes = client.DownloadData(imagePathOrUrl);
return new LinkedResource(new MemoryStream(bytes));

// get content type from response
string contentType = client.ResponseHeaders[HttpResponseHeader.ContentType];
if (!String.IsNullOrWhiteSpace( contentType ))
{
return new LinkedResource( new MemoryStream( bytes ), new ContentType( contentType ) );
}
else
{
return new LinkedResource( new MemoryStream( bytes ));
}
}
else
{
Expand Down