Add project files.

This commit is contained in:
2025-08-18 17:11:43 +03:30
parent 807dc263be
commit e5eb10d604
3943 changed files with 223719 additions and 0 deletions
@@ -0,0 +1,254 @@
using System.Collections;
using System.Dynamic;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using Elsa.Email.Contracts;
using Elsa.Email.Models;
using Elsa.Email.Options;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MimeKit;
namespace Elsa.Email.Activities;
/// <summary>
/// Send an email message.
/// </summary>
[Activity("Elsa", "Email", "Send an email message.", Kind = ActivityKind.Task)]
public class SendEmail : Activity
{
/// <inheritdoc />
public SendEmail([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
}
/// <summary>
/// The sender's email address.
/// </summary>
[Input(Description = "The sender's email address.")]
public Input<string?> From { get; set; } = null!;
/// <summary>
/// The recipients email addresses.
/// </summary>
[Input(Description = "The recipients email addresses.", UIHint = InputUIHints.MultiText)]
public Input<ICollection<string>> To { get; set; } = null!;
/// <summary>
/// The CC recipient email addresses.
/// </summary>
[Input(
Description = "The CC recipient email addresses.",
UIHint = InputUIHints.MultiText,
Category = "More")]
public Input<ICollection<string>> Cc { get; set; } = null!;
/// <summary>
/// The BCC recipients email addresses.
/// </summary>
[Input(
Description = "The BCC recipients email addresses.",
UIHint = InputUIHints.MultiText,
Category = "More")]
public Input<ICollection<string>> Bcc { get; set; } = null!;
/// <summary>
/// The subject of the email message.
/// </summary>
[Input(Description = "The subject of the email message.")]
public Input<string?> Subject { get; set; } = null!;
/// <summary>
/// The attachments to send with the email message.
/// </summary>
[Input(
Description = "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 = InputUIHints.MultiLine
)]
public Input<object?> Attachments { get; set; } = null!;
/// <summary>
/// The body of the email message.
/// </summary>
[Input(
Description = "The body of the email message.",
UIHint = InputUIHints.MultiLine
)]
public Input<string> Body { get; set; } = null!;
/// <summary>
/// The activity to execute when an error occurs while trying to send the email.
/// </summary>
[Port] public IActivity? Error { get; set; }
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var cancellationToken = context.CancellationToken;
var message = new MimeMessage();
var options = context.GetRequiredService<IOptions<SmtpOptions>>().Value;
var from = string.IsNullOrWhiteSpace(From.GetOrDefault(context)) ? options.DefaultSender : From.Get(context)!;
message.Sender = MailboxAddress.Parse(from);
message.From.Add(MailboxAddress.Parse(from));
message.Subject = Subject.GetOrDefault(context) ?? "";
var bodyBuilder = new BodyBuilder
{
HtmlBody = Body.GetOrDefault(context)
};
await AddAttachmentsAsync(context, bodyBuilder, cancellationToken);
message.Body = bodyBuilder.ToMessageBody();
SetRecipientsEmailAddresses(message.To, GetAddresses(context, To));
SetRecipientsEmailAddresses(message.Cc, GetAddresses(context, Cc));
SetRecipientsEmailAddresses(message.Bcc, GetAddresses(context, Bcc));
var smtpService = context.GetRequiredService<ISmtpService>();
var logger = context.GetRequiredService<ILogger<SendEmail>>();
try
{
await smtpService.SendAsync(message, context.CancellationToken);
await context.CompleteActivityAsync();
}
catch (Exception e)
{
logger.LogWarning(e, "Error while sending email message");
context.AddExecutionLogEntry("Error", e.Message, payload: new
{
e.StackTrace
});
await context.ScheduleActivityAsync(Error, OnErrorCompletedAsync);
}
}
private async ValueTask OnErrorCompletedAsync(ActivityCompletedContext context) => await context.TargetContext.CompleteActivityAsync();
private static ICollection<string> GetAddresses(ActivityExecutionContext context, Input<ICollection<string>> input)
{
return input.GetOrDefault(context) ?? new List<string>(0);
}
private async Task AddAttachmentsAsync(ActivityExecutionContext context, BodyBuilder bodyBuilder, CancellationToken cancellationToken)
{
var attachments = Attachments.GetOrDefault(context);
if (attachments == null || attachments is string s && string.IsNullOrWhiteSpace(s))
return;
var index = 0;
var attachmentObjects = InterpretAttachmentsModel(attachments);
foreach (var attachmentObject in attachmentObjects)
{
switch (attachmentObject)
{
case Uri url:
await AttachOnlineFileAsync(context, bodyBuilder, url, cancellationToken);
break;
case string path when path?.Contains("://") == true:
await AttachOnlineFileAsync(context, bodyBuilder, new Uri(path), cancellationToken);
break;
case string path when !string.IsNullOrWhiteSpace(path):
await AttachLocalFileAsync(bodyBuilder, path, cancellationToken);
break;
case byte[] bytes:
{
var fileName = $"Attachment-{++index}";
bodyBuilder.Attachments.Add(fileName, bytes, ContentType.Parse("application/binary"));
break;
}
case Stream stream:
{
var fileName = $"Attachment-{++index}";
await bodyBuilder.Attachments.AddAsync(fileName, stream, ContentType.Parse("application/binary"), cancellationToken);
break;
}
case EmailAttachment emailAttachment:
{
var fileName = emailAttachment.FileName ?? $"Attachment-{++index}";
var contentType = emailAttachment.ContentType ?? "application/binary";
var parsedContentType = ContentType.Parse(contentType);
if (emailAttachment.Content is byte[] bytes)
bodyBuilder.Attachments.Add(fileName, bytes, parsedContentType);
else if (emailAttachment.Content is Stream stream)
await bodyBuilder.Attachments.AddAsync(fileName, stream, parsedContentType, cancellationToken);
break;
}
case ExpandoObject expandoObject:
{
var dictionary = new Dictionary<string, object>(expandoObject, StringComparer.OrdinalIgnoreCase);
var fileName = dictionary.GetValue<string>("FileName") ?? $"Attachment-{++index}";
var contentType = dictionary.GetValue<string>("ContentType") ?? "application/binary";
var parsedContentType = ContentType.Parse(contentType);
var content = dictionary.GetValue<object>("Content");
if (content is byte[] bytes)
bodyBuilder.Attachments.Add(fileName, bytes, parsedContentType);
else if (content is Stream stream)
await bodyBuilder.Attachments.AddAsync(fileName, stream, parsedContentType, cancellationToken);
break;
}
default:
{
var json = JsonSerializer.Serialize(attachmentObject);
var fileName = $"Attachment-{++index}";
bodyBuilder.Attachments.Add(fileName, Encoding.UTF8.GetBytes(json), ContentType.Parse("application/json"));
break;
}
}
}
}
private async Task AttachLocalFileAsync(BodyBuilder bodyBuilder, string path, CancellationToken cancellationToken) => await bodyBuilder.Attachments.AddAsync(path, cancellationToken);
private async Task AttachOnlineFileAsync(ActivityExecutionContext context, BodyBuilder bodyBuilder, Uri url, CancellationToken cancellationToken)
{
var fileName = Path.GetFileName(url.LocalPath);
var downloader = context.GetRequiredService<IDownloader>();
var response = await downloader.DownloadAsync(url, cancellationToken);
var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
var contentType = response.Content.Headers.ContentType?.MediaType ?? "application/binary";
await bodyBuilder.Attachments.AddAsync(fileName, contentStream, ContentType.Parse(contentType), cancellationToken);
}
private IEnumerable InterpretAttachmentsModel(object attachments)
{
if (attachments is byte[] bytes)
return new[]
{
bytes
};
return attachments is string text
? new[]
{
text
}
: attachments as IEnumerable ?? new[]
{
attachments
};
}
private void SetRecipientsEmailAddresses(InternetAddressList list, IEnumerable<string>? addresses)
{
if (addresses == null)
return;
list.AddRange(addresses.Select(MailboxAddress.Parse));
}
}
@@ -0,0 +1,12 @@
namespace Elsa.Email.Contracts;
/// <summary>
/// Download files from the Internet.
/// </summary>
public interface IDownloader
{
/// <summary>
/// Download whatever is returned at the specified URL.
/// </summary>
Task<HttpResponseMessage> DownloadAsync(Uri url, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,14 @@
using MimeKit;
namespace Elsa.Email.Contracts;
/// <summary>
/// Use this service to send emails.
/// </summary>
public interface ISmtpService
{
/// <summary>
/// Send the specified <see cref="MimeMessage"/>.
/// </summary>
Task SendAsync(MimeMessage message, CancellationToken cancellationToken);
}
+20
View File
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
Provides a SendEmail activity that lets you send emails from a workflow.
</Description>
<PackageTags>elsa module activities email</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MailKit" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,20 @@
using Elsa.Email.Features;
using Elsa.Features.Services;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Provides methods to install and configure email related features.
/// </summary>
public static class ModuleExtensions
{
/// <summary>
/// Adds the <see cref="EmailFeature"/> feature to the system.
/// </summary>
public static IModule UseEmail(this IModule configuration, Action<EmailFeature>? configure = default)
{
configuration.Configure(configure);
return configuration;
}
}
@@ -0,0 +1,51 @@
using Elsa.Email.Contracts;
using Elsa.Email.Options;
using Elsa.Email.Services;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Email.Features;
/// <summary>
/// Setup email features.
/// </summary>
public class EmailFeature : FeatureBase
{
/// <inheritdoc />
public EmailFeature(IModule module) : base(module)
{
}
/// <summary>
/// Set a callback to configure <see cref="SmtpOptions"/>.
/// </summary>
public Action<SmtpOptions> ConfigureOptions { get; set; } = _ => { };
/// <summary>
/// Gets or sets a callback for configuring the <see cref="ISmtpService"/> implementation.
/// </summary>
public Func<IServiceProvider, ISmtpService> SmtpService { get; set; } = sp => sp.GetRequiredService<MailKitSmtpService>();
/// <summary>
/// Gets or sets a callback for configuring the HTTP client use by the default implementation of <see cref="IDownloader"/>.
/// </summary>
public Action<IServiceProvider, HttpClient> ConfigureDownloaderHttpClient { get; set; } = (_, _) => { };
/// <inheritdoc />
public override void Configure()
{
Module.AddActivitiesFrom<EmailFeature>();
}
/// <inheritdoc />
public override void Apply()
{
Services
.Configure(ConfigureOptions)
.AddScoped<MailKitSmtpService>()
.AddScoped(SmtpService)
.AddHttpClient<IDownloader, DefaultDownloader>(ConfigureDownloaderHttpClient);
}
}
+3
View File
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
@@ -0,0 +1,9 @@
namespace Elsa.Email.Models;
/// <summary>
/// Represents an email attachment.
/// </summary>
/// <param name="Content">The content of the attachment.</param>
/// <param name="FileName">The name of the attachment.</param>
/// <param name="ContentType">The content type of the attachment.</param>
public record EmailAttachment(object Content, string? FileName, string? ContentType);
@@ -0,0 +1,22 @@
namespace Elsa.Email.Options;
/// <summary>
/// Represents the encryption method to use when connecting to an SMTP server.
/// </summary>
public enum SmtpEncryptionMethod
{
/// <summary>
/// No encryption.
/// </summary>
None = 0,
/// <summary>
/// SSL/TLS encryption.
/// </summary>
SslTlS = 1,
/// <summary>
/// STARTTLS encryption.
/// </summary>
StartTls = 2
}
@@ -0,0 +1,44 @@
using MailKit.Security;
namespace Elsa.Email.Options;
/// <summary>
/// Options to configure the SMTP client with.
/// </summary>
public class SmtpOptions
{
/// <summary>
/// The default sender address when no sender is specified while sending emails.
/// </summary>
public string DefaultSender { get; set; } = default!;
/// <summary>
/// The SMTP server IP address or hostname.
/// </summary>
public string? Host { get; set; }
/// <summary>
/// The SMTP server port
/// </summary>
public int Port { get; set; } = 25;
/// <summary>
/// Secure socket options.
/// </summary>
public SecureSocketOptions SecureSocketOptions { get; set; } = SecureSocketOptions.Auto;
/// <summary>
/// True if the SMTP host requires credentials.
/// </summary>
public bool RequireCredentials { get; set; }
/// <summary>
/// The username to authenticate with.
/// </summary>
public string? UserName { get; set; }
/// <summary>
/// The password to authenticate with.
/// </summary>
public string? Password { get; set; }
}
@@ -0,0 +1,24 @@
using Elsa.Email.Contracts;
namespace Elsa.Email.Services;
/// <summary>
/// Download files from the Internet.
/// </summary>
public class DefaultDownloader : IDownloader
{
private readonly HttpClient _httpClient;
/// <summary>
/// Constructor.
/// </summary>
public DefaultDownloader(HttpClient httpClient)
{
_httpClient = httpClient;
}
/// <summary>
/// Download whatever is returned at the specified URL.
/// </summary>
public async Task<HttpResponseMessage> DownloadAsync(Uri url, CancellationToken cancellationToken = default) => await _httpClient.GetAsync(url, cancellationToken);
}
@@ -0,0 +1,73 @@
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using Elsa.Email.Contracts;
using Elsa.Email.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MimeKit;
using SmtpClient = MailKit.Net.Smtp.SmtpClient;
namespace Elsa.Email.Services;
/// <summary>
/// A MailKit implementation of <see cref="ISmtpService"/>.
/// </summary>
public class MailKitSmtpService : ISmtpService
{
private readonly SmtpOptions _options;
private readonly ILogger<MailKitSmtpService> _logger;
/// <summary>
/// Constructor.
/// </summary>
public MailKitSmtpService(
IOptions<SmtpOptions> options,
ILogger<MailKitSmtpService> logger
)
{
_options = options.Value;
_logger = logger;
}
/// <summary>
/// Sends the specified message.
/// </summary>
public async Task SendAsync(MimeMessage message, CancellationToken cancellationToken) => await SendMessage(message, cancellationToken);
private async Task SendMessage(MimeMessage message, CancellationToken cancellationToken)
{
using var client = new SmtpClient();
client.ServerCertificateValidationCallback = CertificateValidationCallback;
await client.ConnectAsync(_options.Host, _options.Port, _options.SecureSocketOptions, cancellationToken);
if (_options.RequireCredentials)
await client.AuthenticateAsync(_options.UserName ?? "", _options.Password ?? "", cancellationToken);
await client.SendAsync(message, cancellationToken);
await client.DisconnectAsync(true, cancellationToken);
}
private bool CertificateValidationCallback(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors)
{
if (sslPolicyErrors == SslPolicyErrors.None)
return true;
_logger.LogError(
"SMTP Server's certificate {CertificateSubject} issued by {CertificateIssuer} with thumbprint {CertificateThumbprint} and expiration date {CertificateExpirationDate} is considered invalid with {SslPolicyErrors} policy errors",
certificate?.Subject,
certificate?.Issuer,
certificate?.GetCertHashString(),
certificate?.GetExpirationDateString(),
sslPolicyErrors);
if (!sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors) || chain?.ChainStatus == null)
return false;
foreach (var chainStatus in chain.ChainStatus)
_logger.LogError("Status: {Status} - {StatusInformation}", chainStatus.Status, chainStatus.StatusInformation);
return false;
}
}