Skip to content

Commit

Permalink
feat: compatible to Hangfire and Json.Net, fix #28
Browse files Browse the repository at this point in the history
  • Loading branch information
hermanho committed Mar 21, 2024
1 parent fedf6b9 commit b99849f
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 131 deletions.
32 changes: 15 additions & 17 deletions src/Postal.AspNetCore/Email.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
using System.Collections.Generic;
using System.Dynamic;
using System.Net.Mail;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using Postal.AspNetCore;

namespace Postal
Expand All @@ -17,6 +19,7 @@ namespace Postal
/// ViewBag property of a Controller. Any dynamic property access is mapped to the
/// view data dictionary.
/// </summary>
[DataContract]
public class Email : DynamicObject, IViewData
{
/// <summary>Create an Email where the ViewName is derived from the name of the class.</summary>
Expand All @@ -25,9 +28,8 @@ protected Email()
{
Attachments = new List<Attachment>();
ViewName = DeriveViewNameFromClassName();
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
ViewData.Model = this;
ImageEmbedder = new ImageEmbedder();
ViewData = new Dictionary<string, object>();
RequestPath = new RequestPath();
}

/// <summary>
Expand All @@ -38,16 +40,10 @@ public Email(string viewName) : this(viewName, new EmptyModelMetadataProvider())
{
}

public Email(string viewName, IModelMetadataProvider modelMetadataProvider)
public Email(string viewName, IModelMetadataProvider modelMetadataProvider): this()
{
if (viewName == null) throw new ArgumentNullException(nameof(viewName));
if (string.IsNullOrWhiteSpace(viewName)) throw new ArgumentException("View name cannot be empty.", "viewName");

Attachments = new List<Attachment>();
ViewName = viewName;
ViewData = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary());
ViewData.Model = this;
ImageEmbedder = new ImageEmbedder();
}

/// <summary>
Expand All @@ -63,24 +59,29 @@ public Email(string viewName, string areaName, IModelMetadataProvider modelMetad
/// <summary>
/// The name of the view containing the email template.
/// </summary>
[DataMember]
public string ViewName { get; set; }

/// <summary>
/// The name of the area containing the email template.
/// </summary>
public string AreaName { get; set; }
[DataMember]
public string? AreaName { get; set; }

/// <summary>
/// The view data to pass to the view.
/// </summary>
public ViewDataDictionary ViewData { get; set; }
[DataMember]
public Dictionary<string, object> ViewData { get; set; }

/// <summary>
/// The attachments to send with the email.
/// </summary>
[DataMember]
public List<Attachment> Attachments { get; set; }

internal ImageEmbedder ImageEmbedder { get; private set; }
[DataMember]
public RequestPath RequestPath { get; set; }

/// <summary>
/// Adds an attachment to the email.
Expand Down Expand Up @@ -132,16 +133,13 @@ string DeriveViewNameFromClassName()
return viewName;
}

public RequestPath RequestPath { get; set; }
internal HttpContextData HttpContextData { get; private set; }

public void CaptureHttpContext(HttpContext httpContext)
{
var endpoint = httpContext.GetEndpoint();
var routeValues = httpContext.Features.Get<IRouteValuesFeature>()?.RouteValues;
HttpContextData = new HttpContextData { Endpoint = endpoint, RouteValues = routeValues };

RequestPath = new RequestPath();
RequestPath.Path = httpContext.Request.Path.ToString();
RequestPath.PathBase = httpContext.Request.PathBase.ToString();
RequestPath.Host = httpContext.Request.Host.ToString();
RequestPath.IsHttps = httpContext.Request.IsHttps;
Expand Down
51 changes: 32 additions & 19 deletions src/Postal.AspNetCore/EmailParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,30 +32,30 @@ public EmailParser(IEmailViewRender alternativeViewRenderer)
/// <param name="emailViewOutput">The email view output.</param>
/// <param name="email">The <see cref="Email"/> used to generate the output.</param>
/// <returns>A <see cref="MailMessage"/> containing the email headers and content.</returns>
public async Task<MailMessage> ParseAsync(string emailViewOutput, Email email)
public async Task<MailMessage> ParseAsync(string emailViewOutput, Email email, ImageEmbedder? imageEmbedder = null)
{
var message = new MailMessage();
await InitializeMailMessageAsync(message, emailViewOutput, email);
await InitializeMailMessageAsync(message, emailViewOutput, email, imageEmbedder);
return message;
}

private async Task InitializeMailMessageAsync(MailMessage message, string emailViewOutput, Email email)
private async Task InitializeMailMessageAsync(MailMessage message, string emailViewOutput, Email email, ImageEmbedder? imageEmbedder = null)
{
if (string.IsNullOrWhiteSpace(emailViewOutput))
{
throw new ArgumentNullException(nameof(emailViewOutput));
}
using (var reader = new StringReader(emailViewOutput))
{
await ParserUtils.ParseHeadersAsync(reader, (key, value) => ProcessHeaderAsync(key, value, message, email));
await ParserUtils.ParseHeadersAsync(reader, (key, value) => ProcessHeaderAsync(key, value, message, email, imageEmbedder));
AssignCommonHeaders(message, email);
if (message.AlternateViews.Count == 0)
{
var messageBody = reader.ReadToEnd().Trim();
if (email.ImageEmbedder.HasImages)
if (imageEmbedder != null && imageEmbedder.HasImages)
{
var view = AlternateView.CreateAlternateViewFromString(messageBody, new ContentType("text/html"));
email.ImageEmbedder.AddImagesToView(view);
imageEmbedder.AddImagesToView(view);
message.AlternateViews.Add(view);
message.Body = "Plain text not available.";
message.IsBodyHtml = false;
Expand Down Expand Up @@ -112,19 +112,29 @@ private void AssignCommonHeaders(MailMessage message, Email email)
private void AssignCommonHeader<T>(Email email, string header, Action<T> assign)
where T : class
{
object value;
object? value;
if (email.ViewData.TryGetValue(header, out value))
{
var typedValue = value as T;
if (typedValue != null) assign(typedValue);
if (value is T typedValue)
{
assign(typedValue);
return;
}
}
var foundKV = email.ViewData.Where(x => String.Equals(x.Key, header, StringComparison.OrdinalIgnoreCase) && x.Value is T typedValue);
if (foundKV.Any())
{
var val = foundKV.First();
assign((T)val.Value);
return;
}
}

private async Task ProcessHeaderAsync(string key, string value, MailMessage message, Email email)
private async Task ProcessHeaderAsync(string key, string value, MailMessage message, Email email, ImageEmbedder? imageEmbedder)
{
if (IsAlternativeViewsHeader(key))
{
foreach (var view in CreateAlternativeViews(value, email))
foreach (var view in CreateAlternativeViews(value, email, imageEmbedder))
{
message.AlternateViews.Add(await view);
}
Expand All @@ -135,17 +145,17 @@ private async Task ProcessHeaderAsync(string key, string value, MailMessage mess
}
}

private IEnumerable<Task<AlternateView>> CreateAlternativeViews(string deliminatedViewNames, Email email)
private IEnumerable<Task<AlternateView>> CreateAlternativeViews(string deliminatedViewNames, Email email, ImageEmbedder? imageEmbedder)
{
var viewNames = deliminatedViewNames.Split(new[] { ',', ' ', ';' }, StringSplitOptions.RemoveEmptyEntries);
return viewNames.Select(v => CreateAlternativeView(email, v)).ToList();
return viewNames.Select(v => CreateAlternativeView(email, v, imageEmbedder)).ToList();
}

private async Task<AlternateView> CreateAlternativeView(Email email, string alternativeViewName)
private async Task<AlternateView> CreateAlternativeView(Email email, string alternativeViewName, ImageEmbedder? imageEmbedder)
{
var fullViewName = GetAlternativeViewName(email, alternativeViewName);
var output = await alternativeViewRenderer.RenderAsync(email, fullViewName);
string contentType;
var output = await alternativeViewRenderer.RenderAsync(email, fullViewName, imageEmbedder);
string? contentType;
string body;
using (var reader = new StringReader(output))
{
Expand Down Expand Up @@ -179,7 +189,10 @@ private async Task<AlternateView> CreateAlternativeView(Email email, string alte
// A different charset can be specified in the Content-Type header.
// e.g. Content-Type: text/html; charset=utf-8
}
email.ImageEmbedder.AddImagesToView(alternativeView);
if (imageEmbedder != null)
{
imageEmbedder.AddImagesToView(alternativeView);
}
return alternativeView;
}

Expand All @@ -206,9 +219,9 @@ private MemoryStream CreateStreamOfBody(string body)
return stream;
}

private string ParseHeadersForContentType(StringReader reader)
private string? ParseHeadersForContentType(StringReader reader)
{
string contentType = null;
string? contentType = null;
ParserUtils.ParseHeaders(reader, (key, value) =>
{
if (key.Equals("content-type", StringComparison.OrdinalIgnoreCase))
Expand Down
22 changes: 8 additions & 14 deletions src/Postal.AspNetCore/EmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,6 @@ namespace Postal
/// </summary>
public class EmailService : IEmailService
{
/// <summary>Creates a new <see cref="EmailService"/>, using the given view engines.</summary>
[Obsolete]
public static EmailService Create(IServiceProvider serviceProvider, Func<SmtpClient> createSmtpClient = null)
{
var emailViewRender = serviceProvider.GetRequiredService<IEmailViewRender>();
var emailParser = serviceProvider.GetRequiredService<IEmailParser>();
var options = Options.Create(new EmailServiceOptions() { CreateSmtpClient = createSmtpClient });
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<EmailService>();
return new EmailService(emailViewRender, emailParser, options, logger);
}

/// <summary>
/// Creates a new <see cref="EmailService"/>.
/// </summary>
Expand Down Expand Up @@ -101,13 +89,19 @@ public async Task SendAsync(MailMessage mailMessage)
/// <returns>A <see cref="MailMessage"/> containing the rendered email.</returns>
public async Task<MailMessage> CreateMailMessageAsync(Email email)
{
var rawEmailString = await emailViewRenderer.RenderAsync(email);
var imageEmbedder = new ImageEmbedder();
var rawEmailString = await emailViewRenderer.RenderAsync(email, null, imageEmbedder);
emailParser = new EmailParser(emailViewRenderer);
var mailMessage = await emailParser.ParseAsync(rawEmailString, email);
var mailMessage = await emailParser.ParseAsync(rawEmailString, email, imageEmbedder);
if ((mailMessage.From == null || mailMessage.From.Address == null) && this.options.FromAddress != null)
{
mailMessage.From = new MailAddress(this.options.FromAddress);
}
mailMessage.BodyTransferEncoding = System.Net.Mime.TransferEncoding.Base64;
if (options.BodyTransferEncoding != null)
{
mailMessage.BodyTransferEncoding = (System.Net.Mime.TransferEncoding)options.BodyTransferEncoding;
}
return mailMessage;
}
}
Expand Down
38 changes: 28 additions & 10 deletions src/Postal.AspNetCore/EmailViewRender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.Extensions.Options;
using Postal.AspNetCore;

namespace Postal
Expand All @@ -17,10 +18,13 @@ public class EmailViewRender : IEmailViewRender
/// Creates a new <see cref="EmailViewRender"/> that uses the given view engines.
/// </summary>
/// <param name="viewEngines">The view engines to use when rendering email views.</param>
public EmailViewRender(ITemplateService templateService)
public EmailViewRender(
ITemplateService templateService,
IOptions<EmailServiceOptions> options
)
{
_templateService = templateService;
EmailViewDirectoryName = "Emails";
_emailViewDirectoryName = options?.Value?.EmailViewsDirectory ?? "Emails";
}

readonly ITemplateService _templateService;
Expand All @@ -29,7 +33,7 @@ public EmailViewRender(ITemplateService templateService)
/// The name of the directory in "Views" that contains the email views.
/// By default, this is "Emails".
/// </summary>
public string EmailViewDirectoryName { get; set; }
private string _emailViewDirectoryName;

/// <summary>
/// Renders an email view.
Expand All @@ -38,7 +42,8 @@ public EmailViewRender(ITemplateService templateService)
/// <returns>The rendered email view output.</returns>
public virtual Task<string> RenderAsync(Email email)
{
return RenderAsync(email, null);
var imageEmbedder = new ImageEmbedder();
return RenderAsync(email, null, imageEmbedder);
}

/// <summary>
Expand All @@ -47,22 +52,35 @@ public virtual Task<string> RenderAsync(Email email)
/// <param name="email">The email to render.</param>
/// <param name="viewName">Optional email view name override. If null then the email's ViewName property is used instead.</param>
/// <returns>The rendered email view output.</returns>
public virtual async Task<string> RenderAsync(Email email, string viewName = null)
public virtual Task<string> RenderAsync(Email email, string? viewName)
{
var imageEmbedder = new ImageEmbedder();
return RenderAsync(email, viewName, imageEmbedder);
}

/// <summary>
/// Renders an email view.
/// </summary>
/// <param name="email">The email to render.</param>
/// <param name="viewName">Optional email view name override. If null then the email's ViewName property is used instead.</param>
/// <param name="imageEmbedder">Optional ImageEmbedder. If null then the email cannot be generated with Image.</param>
/// <returns>The rendered email view output.</returns>
public virtual async Task<string> RenderAsync(Email email, string? viewName = null, ImageEmbedder? imageEmbedder = null)
{
viewName = viewName ?? email.ViewName;

var routeData = new Microsoft.AspNetCore.Routing.RouteData();
routeData.Values["controller"] = EmailViewDirectoryName;
routeData.Values["page"] = EmailViewDirectoryName;
routeData.Values["controller"] = _emailViewDirectoryName;
routeData.Values["page"] = _emailViewDirectoryName;
if (!string.IsNullOrWhiteSpace(email.AreaName))
{
routeData.Values["area"] = email.AreaName;
routeData.DataTokens["area"] = email.AreaName;
}

Dictionary<string, object> viewData = new Dictionary<string, object>();
viewData[ImageEmbedder.ViewDataKey] = email.ImageEmbedder;
var viewOutput = await _templateService.RenderTemplateAsync(email.HttpContextData, routeData, viewName, email, viewData, true);
Dictionary<string, object?> viewData = new Dictionary<string, object?>();
viewData[ImageEmbedder.ViewDataKey] = imageEmbedder;
var viewOutput = await _templateService.RenderTemplateAsync(routeData, viewName, email, viewData, true);
viewData.Remove(ImageEmbedder.ViewDataKey);
return viewOutput;
}
Expand Down
16 changes: 0 additions & 16 deletions src/Postal.AspNetCore/HttpContextData.cs

This file was deleted.

20 changes: 0 additions & 20 deletions src/Postal.AspNetCore/InternalClass/EndpointFeature.cs

This file was deleted.

Loading

0 comments on commit b99849f

Please sign in to comment.