Skip to content

Commit

Permalink
Add support for email attachments
Browse files Browse the repository at this point in the history
  • Loading branch information
sfmskywalker committed Jun 16, 2021
1 parent 618f4e8 commit 97a9619
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// ReSharper disable once CheckNamespace
namespace Elsa.Activities.Email
{
public record EmailAttachment(byte[] Content, string? FileName, string? ContentType);
}
110 changes: 95 additions & 15 deletions src/activities/Elsa.Activities.Email/Activities/SendEmail/SendEmail.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Email.Options;
using Elsa.Activities.Email.Services;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Design;
using Elsa.Expressions;
using Elsa.Serialization;
using Elsa.Services;
using Elsa.Services.Models;
using Microsoft.Extensions.Options;
using MimeKit;
using MimeKit.Text;

// ReSharper disable once CheckNamespace
namespace Elsa.Activities.Email
Expand All @@ -20,11 +26,15 @@ namespace Elsa.Activities.Email
public class SendEmail : Activity
{
private readonly ISmtpService _smtpService;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IContentSerializer _contentSerializer;
private readonly SmtpOptions _options;

public SendEmail(ISmtpService smtpService, IOptions<SmtpOptions> options)
public SendEmail(ISmtpService smtpService, IOptions<SmtpOptions> options, IHttpClientFactory httpClientFactory, IContentSerializer contentSerializer)
{
_smtpService = smtpService;
_httpClientFactory = httpClientFactory;
_contentSerializer = contentSerializer;
_options = options.Value;
}

Expand All @@ -35,39 +45,47 @@ public SendEmail(ISmtpService smtpService, IOptions<SmtpOptions> options)
public ICollection<string> To { get; set; } = new List<string>();

[ActivityInput(
Hint = "The cc recipients email addresses.",
UIHint = ActivityInputUIHints.MultiText,
DefaultSyntax = SyntaxNames.Json,
Hint = "The cc recipients email addresses.",
UIHint = ActivityInputUIHints.MultiText,
DefaultSyntax = SyntaxNames.Json,
SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript },
Category = "More")]
public ICollection<string> Cc { get; set; } = new List<string>();

[ActivityInput(
Hint = "The Bcc recipients email addresses.",
UIHint = ActivityInputUIHints.MultiText,
DefaultSyntax = SyntaxNames.Json,
Hint = "The Bcc recipients email addresses.",
UIHint = ActivityInputUIHints.MultiText,
DefaultSyntax = SyntaxNames.Json,
SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript },
Category = "More")]
public ICollection<string> Bcc { get; set; } = new List<string>();

[ActivityInput(Hint = "The subject of the email message.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? Subject { get; set; }

[ActivityInput(
Hint = "The attachments to send with the email message. Can be (an array of) a fully-qualified file path, URL, stream, byte array or instances of EmailAttachment.",
UIHint = ActivityInputUIHints.MultiLine,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public object? Attachments { get; set; }

[ActivityInput(Hint = "The body of the email message.", UIHint = ActivityInputUIHints.MultiLine, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? Body { get; set; }

protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
var cancellationToken = context.CancellationToken;
var message = new MimeMessage();
var from = From is null or "" ? _options.DefaultSender : From;
var from = string.IsNullOrWhiteSpace(From) ? _options.DefaultSender : From;

message.From.Add(MailboxAddress.Parse(from));
message.Subject = Subject;

message.Body = new TextPart(TextFormat.Html)
{
Text = Body
};
var bodyBuilder = new BodyBuilder { HtmlBody = Body };
await AddAttachmentsAsync(bodyBuilder, cancellationToken);

message.Body = bodyBuilder.ToMessageBody();

SetRecipientsEmailAddresses(message.To, To);
SetRecipientsEmailAddresses(message.Cc, Cc);
Expand All @@ -78,12 +96,74 @@ protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(Acti
return Done();
}

private async Task AddAttachmentsAsync(BodyBuilder bodyBuilder, CancellationToken cancellationToken)
{
var attachments = Attachments;

if (attachments != null)
{
var index = 0;
var attachmentObjects = InterpretAttachmentsModel(attachments);

foreach (var attachmentObject in attachmentObjects)
{
switch (attachmentObject)
{
case Uri url:
await AttachOnlineFileAsync(bodyBuilder, url, cancellationToken);
break;
case string path when path.Contains("://"):
await AttachOnlineFileAsync(bodyBuilder, new Uri(path), cancellationToken);
break;
case string path:
await AttachLocalFileAsync(bodyBuilder, path, cancellationToken);
break;
case EmailAttachment emailAttachment:
{
var fileName = emailAttachment.FileName ?? $"Attachment-{++index}";
var contentType = emailAttachment.ContentType ?? "application/binary";
bodyBuilder.Attachments.Add(fileName, emailAttachment.Content, ContentType.Parse(contentType));
break;
}
default:
{
var json = _contentSerializer.Serialize(attachmentObject);
var fileName = $"Attachment-{++index}";
var contentType = "application/json";
bodyBuilder.Attachments.Add(fileName, Encoding.UTF8.GetBytes(json), ContentType.Parse(contentType));
break;
}
}
}
}
}

private async Task AttachLocalFileAsync(BodyBuilder bodyBuilder, string path, CancellationToken cancellationToken) => await bodyBuilder.Attachments.AddAsync(path, cancellationToken);

private async Task AttachOnlineFileAsync(BodyBuilder bodyBuilder, Uri url, CancellationToken cancellationToken)
{
var fileName = Path.GetFileName(url.LocalPath);
var response = await DownloadUrlAsync(url);
var contentStream = await response.Content.ReadAsStreamAsync();
var contentType = response.Content.Headers.ContentType.MediaType;
await bodyBuilder.Attachments.AddAsync(fileName, contentStream, ContentType.Parse(contentType), cancellationToken);
}

private IEnumerable InterpretAttachmentsModel(object attachments) => attachments is string text ? new[] { text } : attachments is IEnumerable enumerable ? enumerable : new[] { attachments };

private void SetRecipientsEmailAddresses(InternetAddressList list, IEnumerable<string>? addresses)
{
if(addresses == null)
if (addresses == null)
return;

list.AddRange(addresses.Select(MailboxAddress.Parse));
}

private async Task<HttpResponseMessage> DownloadUrlAsync(Uri url)
{
using var httpClient = _httpClientFactory.CreateClient();
var response = await httpClient.GetAsync(url);
return response;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
<ProjectReference Include="..\..\scripting\Elsa.Scripting.JavaScript\Elsa.Scripting.JavaScript.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="MailKit" Version="2.12.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using Elsa;
using Elsa.Activities.Email;
using Elsa.Activities.Email.Handlers;
using Elsa.Activities.Email.Options;
using Elsa.Activities.Email.Services;

Expand All @@ -12,6 +13,9 @@ public static class ServiceCollectionExtensions
public static ElsaOptionsBuilder AddEmailActivities(this ElsaOptionsBuilder options, Action<SmtpOptions>? configureOptions = null)
{
options.Services.AddEmailServices(configureOptions);
options.Services.AddNotificationHandlersFrom<ConfigureJavaScriptEngine>();
options.Services.AddJavaScriptTypeDefinitionProvider<EmailTypeDefinitionProvider>();
options.Services.AddHttpClient();
options.AddEmailActivitiesInternal();
return options;
}
Expand All @@ -21,7 +25,7 @@ public static IServiceCollection AddEmailServices(this IServiceCollection servic
if (configureOptions != null)
services.Configure(configureOptions);

return services.AddSingleton<ISmtpService, SmtpService>();
return services.AddSingleton<ISmtpService, MailKitSmtpService>();
}

private static ElsaOptionsBuilder AddEmailActivitiesInternal(this ElsaOptionsBuilder services) => services.AddActivity<SendEmail>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Scripting.JavaScript.Extensions;
using Elsa.Scripting.JavaScript.Messages;
using Elsa.Services;
using Elsa.Services.WorkflowStorage;
using MediatR;
using Microsoft.Extensions.Configuration;
using NodaTime;

namespace Elsa.Activities.Email.Handlers
{
public class ConfigureJavaScriptEngine : INotificationHandler<EvaluatingJavaScriptExpression>
{
private readonly IConfiguration _configuration;
private readonly IActivityTypeService _activityTypeService;
private readonly IWorkflowStorageService _workflowStorageService;

public ConfigureJavaScriptEngine(IConfiguration configuration, IActivityTypeService activityTypeService, IWorkflowStorageService workflowStorageService)
{
_configuration = configuration;
_activityTypeService = activityTypeService;
_workflowStorageService = workflowStorageService;
}

public Task Handle(EvaluatingJavaScriptExpression notification, CancellationToken cancellationToken)
{
var engine = notification.Engine;

engine.RegisterType<EmailAttachment>();
return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using Elsa.Scripting.JavaScript.Services;

namespace Elsa.Activities.Email.Handlers
{
public class EmailTypeDefinitionProvider : TypeDefinitionProvider
{
public override IEnumerable<Type> CollectTypes(TypeDefinitionContext context)
{
return new[] { typeof(EmailAttachment) };
}
}
}
17 changes: 17 additions & 0 deletions src/activities/Elsa.Activities.Email/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;

// ReSharper disable once CheckNamespace
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@

namespace Elsa.Activities.Email.Services
{
public class SmtpService : ISmtpService
public class MailKitSmtpService : ISmtpService
{
private readonly SmtpOptions _options;
private readonly ILogger<SmtpService> _logger;
private readonly ILogger<MailKitSmtpService> _logger;
private const string EmailExtension = ".eml";

public SmtpService(
public MailKitSmtpService(
IOptions<SmtpOptions> options,
ILogger<SmtpService> logger
ILogger<MailKitSmtpService> logger
)
{
_options = options.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public static class MessageHandlerServiceCollectionExtensions
{
return services.AddTransient(typeof(INotificationHandler<T>), typeof(THandler));
}

public static IServiceCollection AddNotificationHandlersFrom<TMarker>(this IServiceCollection services) => services.AddNotificationHandlers(typeof(TMarker));

public static IServiceCollection AddNotificationHandlers(this IServiceCollection services, params Type[] markerTypes)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public void Build(IWorkflowBuilder builder)
.WithReadContent(true))
.WithName("TestHttpRequest")
.WriteHttpResponse(setup => setup.WithStatusCode(System.Net.HttpStatusCode.OK)
.WithContent(async context => JsonSerializer.Serialize((await context.GetNamedActivityPropertyAsync<SendHttpRequest, HttpResponseModel>("TestHttpRequest", x => x.Output))!.Content)));
.WithContent(async context => JsonSerializer.Serialize(await context.GetNamedActivityPropertyAsync<SendHttpRequest, object>("TestHttpRequest", x => x.ResponseContent))));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Jint;
using Jint.Runtime.Interop;

namespace Elsa.Scripting.JavaScript.Extensions
{
public static class EngineExtensions
{
public static void RegisterType<T>(this Engine engine) => engine.SetValue(typeof(T).Name, TypeReference.CreateTypeReference(engine, typeof(T)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Providers.WorkflowStorage;
using Elsa.Scripting.JavaScript.Extensions;
using Elsa.Scripting.JavaScript.Messages;
using Elsa.Services;
using Elsa.Services.Models;
Expand Down Expand Up @@ -65,15 +66,15 @@ public async Task Handle(EvaluatingJavaScriptExpression notification, Cancellati
engine.SetValue("workflowContext", activityExecutionContext.GetWorkflowContext());

// Types.
RegisterType<Instant>(engine);
RegisterType<Duration>(engine);
RegisterType<Period>(engine);
RegisterType<LocalDate>(engine);
RegisterType<LocalTime>(engine);
RegisterType<LocalDateTime>(engine);
RegisterType<Guid>(engine);
RegisterType<WorkflowExecutionContext>(engine);
RegisterType<ActivityExecutionContext>(engine);
engine.RegisterType<Instant>();
engine.RegisterType<Duration>();
engine.RegisterType<Period>();
engine.RegisterType<LocalDate>();
engine.RegisterType<LocalTime>();
engine.RegisterType<LocalDateTime>();
engine.RegisterType<Guid>();
engine.RegisterType<WorkflowExecutionContext>();
engine.RegisterType<ActivityExecutionContext>();

// Workflow variables.
var variables = workflowExecutionContext.GetMergedVariables();
Expand Down Expand Up @@ -157,7 +158,5 @@ private void AddActivityOutputOld(Engine engine, ActivityExecutionContext activi
var storageContext = new WorkflowStorageContext(context.WorkflowInstance, activityBlueprint.Id);
return await storageService.LoadAsync(providerName, storageContext, propertyName, context.CancellationToken);
}

private void RegisterType<T>(Engine engine) => engine.SetValue(typeof(T).Name, TypeReference.CreateTypeReference(engine, typeof(T)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ public class TypeConverterResultConverter : IConvertsJintEvaluationResult

public object? ConvertToDesiredType(object? evaluationResult, Type desiredType)
{
if (desiredType == typeof(object))
return evaluationResult;

var converter = TypeDescriptor.GetConverter(evaluationResult!);

if (converter.CanConvertTo(desiredType))
Expand Down

0 comments on commit 97a9619

Please sign in to comment.