From 97a9619fad4fd3e8859334de59da2f99f219ec65 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 16 Jun 2021 16:57:23 +0200 Subject: [PATCH] Add support for email attachments --- .../Activities/SendEmail/EmailAttachment.cs | 5 + .../Activities/SendEmail/SendEmail.cs | 110 +++++++++++++++--- .../Elsa.Activities.Email.csproj | 2 + .../Extensions/ServiceCollectionExtensions.cs | 6 +- .../Handlers/ConfigureJavaScriptEngine.cs | 34 ++++++ .../Handlers/EmailTypeDefinitionProvider.cs | 14 +++ .../Elsa.Activities.Email/IsExternalInit.cs | 17 +++ .../{SmtpService.cs => MailKitSmtpService.cs} | 8 +- ...ssageHandlerServiceCollectionExtensions.cs | 2 + .../Elsa.Samples.SendHttp/SendHttpWorkflow.cs | 2 +- .../Extensions/EngineExtensions.cs | 10 ++ .../Handlers/ConfigureJavaScriptEngine.cs | 21 ++-- .../Services/TypeConverterResultConverter.cs | 3 + 13 files changed, 202 insertions(+), 32 deletions(-) create mode 100644 src/activities/Elsa.Activities.Email/Activities/SendEmail/EmailAttachment.cs create mode 100644 src/activities/Elsa.Activities.Email/Handlers/ConfigureJavaScriptEngine.cs create mode 100644 src/activities/Elsa.Activities.Email/Handlers/EmailTypeDefinitionProvider.cs create mode 100644 src/activities/Elsa.Activities.Email/IsExternalInit.cs rename src/activities/Elsa.Activities.Email/Services/{SmtpService.cs => MailKitSmtpService.cs} (95%) create mode 100644 src/scripting/Elsa.Scripting.JavaScript/Extensions/EngineExtensions.cs diff --git a/src/activities/Elsa.Activities.Email/Activities/SendEmail/EmailAttachment.cs b/src/activities/Elsa.Activities.Email/Activities/SendEmail/EmailAttachment.cs new file mode 100644 index 000000000..591b826e5 --- /dev/null +++ b/src/activities/Elsa.Activities.Email/Activities/SendEmail/EmailAttachment.cs @@ -0,0 +1,5 @@ +// ReSharper disable once CheckNamespace +namespace Elsa.Activities.Email +{ + public record EmailAttachment(byte[] Content, string? FileName, string? ContentType); +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Email/Activities/SendEmail/SendEmail.cs b/src/activities/Elsa.Activities.Email/Activities/SendEmail/SendEmail.cs index d70fa5648..818f0b2d0 100644 --- a/src/activities/Elsa.Activities.Email/Activities/SendEmail/SendEmail.cs +++ b/src/activities/Elsa.Activities.Email/Activities/SendEmail/SendEmail.cs @@ -1,5 +1,11 @@ +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; @@ -7,11 +13,11 @@ 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 @@ -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 options) + public SendEmail(ISmtpService smtpService, IOptions options, IHttpClientFactory httpClientFactory, IContentSerializer contentSerializer) { _smtpService = smtpService; + _httpClientFactory = httpClientFactory; + _contentSerializer = contentSerializer; _options = options.Value; } @@ -35,17 +45,17 @@ namespace Elsa.Activities.Email public ICollection To { get; set; } = new List(); [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 Cc { get; set; } = new List(); [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 Bcc { get; set; } = new List(); @@ -53,21 +63,29 @@ namespace Elsa.Activities.Email [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 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); @@ -78,12 +96,74 @@ namespace Elsa.Activities.Email 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? addresses) { - if(addresses == null) + if (addresses == null) return; - + list.AddRange(addresses.Select(MailboxAddress.Parse)); } + + private async Task DownloadUrlAsync(Uri url) + { + using var httpClient = _httpClientFactory.CreateClient(); + var response = await httpClient.GetAsync(url); + return response; + } } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Email/Elsa.Activities.Email.csproj b/src/activities/Elsa.Activities.Email/Elsa.Activities.Email.csproj index 74ca560d6..28ac0be65 100644 --- a/src/activities/Elsa.Activities.Email/Elsa.Activities.Email.csproj +++ b/src/activities/Elsa.Activities.Email/Elsa.Activities.Email.csproj @@ -16,10 +16,12 @@ + + diff --git a/src/activities/Elsa.Activities.Email/Extensions/ServiceCollectionExtensions.cs b/src/activities/Elsa.Activities.Email/Extensions/ServiceCollectionExtensions.cs index f41bbf42b..5fba4ec4a 100644 --- a/src/activities/Elsa.Activities.Email/Extensions/ServiceCollectionExtensions.cs +++ b/src/activities/Elsa.Activities.Email/Extensions/ServiceCollectionExtensions.cs @@ -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; @@ -12,6 +13,9 @@ namespace Microsoft.Extensions.DependencyInjection public static ElsaOptionsBuilder AddEmailActivities(this ElsaOptionsBuilder options, Action? configureOptions = null) { options.Services.AddEmailServices(configureOptions); + options.Services.AddNotificationHandlersFrom(); + options.Services.AddJavaScriptTypeDefinitionProvider(); + options.Services.AddHttpClient(); options.AddEmailActivitiesInternal(); return options; } @@ -21,7 +25,7 @@ namespace Microsoft.Extensions.DependencyInjection if (configureOptions != null) services.Configure(configureOptions); - return services.AddSingleton(); + return services.AddSingleton(); } private static ElsaOptionsBuilder AddEmailActivitiesInternal(this ElsaOptionsBuilder services) => services.AddActivity(); diff --git a/src/activities/Elsa.Activities.Email/Handlers/ConfigureJavaScriptEngine.cs b/src/activities/Elsa.Activities.Email/Handlers/ConfigureJavaScriptEngine.cs new file mode 100644 index 000000000..3d70687e3 --- /dev/null +++ b/src/activities/Elsa.Activities.Email/Handlers/ConfigureJavaScriptEngine.cs @@ -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 + { + 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(); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Email/Handlers/EmailTypeDefinitionProvider.cs b/src/activities/Elsa.Activities.Email/Handlers/EmailTypeDefinitionProvider.cs new file mode 100644 index 000000000..2ea1335b4 --- /dev/null +++ b/src/activities/Elsa.Activities.Email/Handlers/EmailTypeDefinitionProvider.cs @@ -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 CollectTypes(TypeDefinitionContext context) + { + return new[] { typeof(EmailAttachment) }; + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Email/IsExternalInit.cs b/src/activities/Elsa.Activities.Email/IsExternalInit.cs new file mode 100644 index 000000000..475dd51b6 --- /dev/null +++ b/src/activities/Elsa.Activities.Email/IsExternalInit.cs @@ -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 +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Email/Services/SmtpService.cs b/src/activities/Elsa.Activities.Email/Services/MailKitSmtpService.cs similarity index 95% rename from src/activities/Elsa.Activities.Email/Services/SmtpService.cs rename to src/activities/Elsa.Activities.Email/Services/MailKitSmtpService.cs index 4f984da8d..4a645bd2a 100644 --- a/src/activities/Elsa.Activities.Email/Services/SmtpService.cs +++ b/src/activities/Elsa.Activities.Email/Services/MailKitSmtpService.cs @@ -12,15 +12,15 @@ using MimeKit; namespace Elsa.Activities.Email.Services { - public class SmtpService : ISmtpService + public class MailKitSmtpService : ISmtpService { private readonly SmtpOptions _options; - private readonly ILogger _logger; + private readonly ILogger _logger; private const string EmailExtension = ".eml"; - public SmtpService( + public MailKitSmtpService( IOptions options, - ILogger logger + ILogger logger ) { _options = options.Value; diff --git a/src/core/Elsa.Core/Extensions/MessageHandlerServiceCollectionExtensions.cs b/src/core/Elsa.Core/Extensions/MessageHandlerServiceCollectionExtensions.cs index a41af2dca..c5b60c021 100644 --- a/src/core/Elsa.Core/Extensions/MessageHandlerServiceCollectionExtensions.cs +++ b/src/core/Elsa.Core/Extensions/MessageHandlerServiceCollectionExtensions.cs @@ -15,6 +15,8 @@ namespace Elsa { return services.AddTransient(typeof(INotificationHandler), typeof(THandler)); } + + public static IServiceCollection AddNotificationHandlersFrom(this IServiceCollection services) => services.AddNotificationHandlers(typeof(TMarker)); public static IServiceCollection AddNotificationHandlers(this IServiceCollection services, params Type[] markerTypes) { diff --git a/src/samples/aspnet/Elsa.Samples.SendHttp/SendHttpWorkflow.cs b/src/samples/aspnet/Elsa.Samples.SendHttp/SendHttpWorkflow.cs index 029fb791a..67d5337b5 100644 --- a/src/samples/aspnet/Elsa.Samples.SendHttp/SendHttpWorkflow.cs +++ b/src/samples/aspnet/Elsa.Samples.SendHttp/SendHttpWorkflow.cs @@ -20,7 +20,7 @@ namespace Elsa.Samples.SendHttp .WithReadContent(true)) .WithName("TestHttpRequest") .WriteHttpResponse(setup => setup.WithStatusCode(System.Net.HttpStatusCode.OK) - .WithContent(async context => JsonSerializer.Serialize((await context.GetNamedActivityPropertyAsync("TestHttpRequest", x => x.Output))!.Content))); + .WithContent(async context => JsonSerializer.Serialize(await context.GetNamedActivityPropertyAsync("TestHttpRequest", x => x.ResponseContent)))); } } } diff --git a/src/scripting/Elsa.Scripting.JavaScript/Extensions/EngineExtensions.cs b/src/scripting/Elsa.Scripting.JavaScript/Extensions/EngineExtensions.cs new file mode 100644 index 000000000..3fb8d7be6 --- /dev/null +++ b/src/scripting/Elsa.Scripting.JavaScript/Extensions/EngineExtensions.cs @@ -0,0 +1,10 @@ +using Jint; +using Jint.Runtime.Interop; + +namespace Elsa.Scripting.JavaScript.Extensions +{ + public static class EngineExtensions + { + public static void RegisterType(this Engine engine) => engine.SetValue(typeof(T).Name, TypeReference.CreateTypeReference(engine, typeof(T))); + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Handlers/ConfigureJavaScriptEngine.cs b/src/scripting/Elsa.Scripting.JavaScript/Handlers/ConfigureJavaScriptEngine.cs index 0fce35ee7..92373246f 100644 --- a/src/scripting/Elsa.Scripting.JavaScript/Handlers/ConfigureJavaScriptEngine.cs +++ b/src/scripting/Elsa.Scripting.JavaScript/Handlers/ConfigureJavaScriptEngine.cs @@ -5,6 +5,7 @@ using System.Linq; 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; @@ -65,15 +66,15 @@ namespace Elsa.Scripting.JavaScript.Handlers engine.SetValue("workflowContext", activityExecutionContext.GetWorkflowContext()); // Types. - RegisterType(engine); - RegisterType(engine); - RegisterType(engine); - RegisterType(engine); - RegisterType(engine); - RegisterType(engine); - RegisterType(engine); - RegisterType(engine); - RegisterType(engine); + engine.RegisterType(); + engine.RegisterType(); + engine.RegisterType(); + engine.RegisterType(); + engine.RegisterType(); + engine.RegisterType(); + engine.RegisterType(); + engine.RegisterType(); + engine.RegisterType(); // Workflow variables. var variables = workflowExecutionContext.GetMergedVariables(); @@ -157,7 +158,5 @@ namespace Elsa.Scripting.JavaScript.Handlers var storageContext = new WorkflowStorageContext(context.WorkflowInstance, activityBlueprint.Id); return await storageService.LoadAsync(providerName, storageContext, propertyName, context.CancellationToken); } - - private void RegisterType(Engine engine) => engine.SetValue(typeof(T).Name, TypeReference.CreateTypeReference(engine, typeof(T))); } } \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Services/TypeConverterResultConverter.cs b/src/scripting/Elsa.Scripting.JavaScript/Services/TypeConverterResultConverter.cs index cb84096a6..5ec394155 100644 --- a/src/scripting/Elsa.Scripting.JavaScript/Services/TypeConverterResultConverter.cs +++ b/src/scripting/Elsa.Scripting.JavaScript/Services/TypeConverterResultConverter.cs @@ -9,6 +9,9 @@ namespace Elsa.Scripting.JavaScript.Services public object? ConvertToDesiredType(object? evaluationResult, Type desiredType) { + if (desiredType == typeof(object)) + return evaluationResult; + var converter = TypeDescriptor.GetConverter(evaluationResult!); if (converter.CanConvertTo(desiredType))