Browse Source

feat: Add Email Activities Module

pull/1568/head
colin 3 weeks ago
parent
commit
c70fc6bda0
  1. 3
      aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/FodyWeavers.xml
  2. 30
      aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/FodyWeavers.xsd
  3. 29
      aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN.Abp.ElsaNext.Email.csproj
  4. 38
      aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/AbpElsaNextEmailModule.cs
  5. 274
      aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Activities/SendEmail.cs
  6. 16
      aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Contracts/IDownloader.cs
  7. 14
      aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Extensions/ModuleExtensions.cs
  8. 30
      aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Features/EmailFeature.cs
  9. 12
      aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Localization/Resources/en.json
  10. 12
      aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Localization/Resources/zh-Hans.json
  11. 28
      aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Services/DefaultDownloader.cs

3
aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

29
aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN.Abp.ElsaNext.Email.csproj

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\configureawait.props" />
<Import Project="..\..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>LINGYUN.Abp.ElsaNext.Email</AssemblyName>
<PackageId>LINGYUN.Abp.ElsaNext.Email</PackageId>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<Content Remove="LINGYUN\Abp\ElsaNext\Email\Localization\Resources\**\*.json" />
<EmbeddedResource Include="LINGYUN\Abp\ElsaNext\Email\Localization\Resources\**\*.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Emailing" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext\LINGYUN.Abp.ElsaNext.csproj" />
</ItemGroup>
</Project>

38
aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/AbpElsaNextEmailModule.cs

@ -0,0 +1,38 @@
using Elsa.Features.Services;
using LINGYUN.Abp.ElsaNext.Email.Extensions;
using LINGYUN.Abp.ElsaNext.Localization;
using Volo.Abp.Emailing;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace LINGYUN.Abp.ElsaNext.Email;
[DependsOn(
typeof(AbpEmailingModule),
typeof(AbpElsaNextModule))]
public class AbpElsaNextEmailModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<IModule>(elsa =>
{
elsa.UseAbpEmail();
});
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpElsaNextEmailModule>();
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Get<ElsaNextResource>()
.AddVirtualJson("/LINGYUN/Abp/ElsaNext/Email/Localization/Resources");
});
}
}

274
aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Activities/SendEmail.cs

@ -0,0 +1,274 @@
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Elsa.Workflows.UIHints;
using LINGYUN.Abp.ElsaNext.Email.Contracts;
using Microsoft.Extensions.Logging;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Net.Mail;
using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Emailing;
using Volo.Abp.Json;
namespace LINGYUN.Abp.ElsaNext.Email.Activities;
[Activity("Elsa", "Email", "Send an email message.", Kind = ActivityKind.Task)]
public class SendEmail : Activity
{
/// <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 MailMessage();
var from = From.GetOrDefault(context);
if (MailAddress.TryCreate(from, out var fromAddress))
{
message.Sender = fromAddress;
message.From = fromAddress;
}
message.Subject = Subject.GetOrDefault(context) ?? "";
await AddAttachmentsAsync(context, message, cancellationToken);
message.Body = Body.GetOrDefault(context);
SetRecipientsEmailAddresses(message.To, GetAddresses(context, To));
SetRecipientsEmailAddresses(message.CC, GetAddresses(context, Cc));
SetRecipientsEmailAddresses(message.Bcc, GetAddresses(context, Bcc));
var emailSender = context.GetRequiredService<IEmailSender>();
var logger = context.GetRequiredService<ILogger<SendEmail>>();
try
{
await emailSender.SendAsync(message);
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, MailMessage mailMessage, 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, mailMessage, url, cancellationToken);
break;
case string path when path?.Contains("://") == true:
await AttachOnlineFileAsync(context, mailMessage, new Uri(path), cancellationToken);
break;
case string path when !string.IsNullOrWhiteSpace(path):
AttachLocalFile(mailMessage, path);
break;
case byte[] bytes:
{
var fileName = $"Attachment-{++index}";
var byteStream = new MemoryStream(bytes.Length);
await byteStream.WriteAsync(bytes, cancellationToken);
mailMessage.Attachments.Add(new Attachment(byteStream, new ContentType { Name = fileName }));
break;
}
case Stream stream:
{
var fileName = $"Attachment-{++index}";
mailMessage.Attachments.Add(new Attachment(stream, new ContentType { Name = fileName }));
break;
}
case Attachment attachment:
{
attachment.Name ??= $"Attachment-{++index}";
mailMessage.Attachments.Add(attachment);
break;
}
case EmailAttachment emailAttachment when emailAttachment.File != null:
{
var fileName = emailAttachment.Name ?? $"Attachment-{++index}";
var emailAttachmentStream = new MemoryStream(emailAttachment.File.Length);
await emailAttachmentStream.WriteAsync(emailAttachment.File, cancellationToken);
mailMessage.Attachments.Add(new Attachment(emailAttachmentStream, new ContentType { Name = fileName }));
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/octet-stream";
var parsedContentType = new ContentType(contentType);
var content = dictionary.GetValue<object>("Content");
if (content is byte[] bytes)
{
var byteStream = new MemoryStream(bytes.Length);
await byteStream.WriteAsync(bytes, cancellationToken);
mailMessage.Attachments.Add(new Attachment(byteStream, new ContentType { Name = fileName }));
}
else if (content is Stream stream)
{
mailMessage.Attachments.Add(new Attachment(stream, new ContentType { Name = fileName }));
}
break;
}
default:
{
var jsonSerializer = context.GetRequiredService<IJsonSerializer>();
var json = jsonSerializer.Serialize(attachmentObject);
var fileName = $"Attachment-{++index}";
var jsonBytes = Encoding.UTF8.GetBytes(json);
var byteStream = new MemoryStream(jsonBytes.Length);
await byteStream.WriteAsync(jsonBytes, cancellationToken);
mailMessage.Attachments.Add(new Attachment(byteStream, new ContentType { Name = fileName }));
break;
}
}
}
}
private static void AttachLocalFile(MailMessage mailMessage, string path)
{
var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
mailMessage.Attachments.Add(new Attachment(stream, new ContentType{ Name = Path.GetFileName(path) }));
}
private async static Task AttachOnlineFileAsync(ActivityExecutionContext context, MailMessage mailMessage, 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";
mailMessage.Attachments.Add(new Attachment(contentStream, new ContentType(contentType) { Name = fileName }));
}
private static 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 static void SetRecipientsEmailAddresses(MailAddressCollection list, IEnumerable<string>? addresses)
{
if (addresses == null)
{
return;
}
foreach (var addresse in addresses)
{
if (MailAddress.TryCreate(addresse, out var mailAddress))
{
list.Add(mailAddress);
}
}
}
}

16
aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Contracts/IDownloader.cs

@ -0,0 +1,16 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace LINGYUN.Abp.ElsaNext.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);
}

14
aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Extensions/ModuleExtensions.cs

@ -0,0 +1,14 @@
using Elsa.Features.Services;
using LINGYUN.Abp.ElsaNext.Email.Features;
using System;
namespace LINGYUN.Abp.ElsaNext.Email.Extensions;
public static class ModuleExtensions
{
public static IModule UseAbpEmail(this IModule configuration, Action<EmailFeature>? configure = null)
{
configuration.Configure(configure);
return configuration;
}
}

30
aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Features/EmailFeature.cs

@ -0,0 +1,30 @@
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using LINGYUN.Abp.ElsaNext.Email.Contracts;
using LINGYUN.Abp.ElsaNext.Email.Services;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
namespace LINGYUN.Abp.ElsaNext.Email.Features;
public class EmailFeature : FeatureBase
{
public Action<IServiceProvider, HttpClient> ConfigureDownloaderHttpClient { get; set; } = (_, _) => { };
public EmailFeature(IModule module) : base(module)
{
}
public override void Configure()
{
Module.AddActivitiesFrom<EmailFeature>();
}
public override void Apply()
{
Services
.AddHttpClient<IDownloader, DefaultDownloader>(ConfigureDownloaderHttpClient);
}
}

12
aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Localization/Resources/en.json

@ -0,0 +1,12 @@
{
"culture": "en",
"texts": {
"The sender's email address.": "The sender's email address.",
"The recipients email addresses.": "The recipients email addresses.",
"The CC recipient email addresses.": "The CC recipient email addresses.",
"The BCC recipients email addresses.": "The BCC recipients email addresses.",
"The subject of the email message.": "The subject of the email message.",
"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.": "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.",
"The body of the email message.": "The body of the email message."
}
}

12
aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Localization/Resources/zh-Hans.json

@ -0,0 +1,12 @@
{
"culture": "zh-Hans",
"texts": {
"The sender's email address.": "发件人的电子邮件地址。",
"The recipients email addresses.": "收件人的电子邮件地址。",
"The CC recipient email addresses.": "CC收件人电子邮件地址。",
"The BCC recipients email addresses.": "BCC收件人邮箱地址。",
"The subject of the email message.": "邮件主题。",
"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.": "随邮件发送的附件。可以是(一个数组)完全限定的文件路径、URL、流、字节数组,或 EmailAttachment 类的实例。",
"The body of the email message.": "邮件正文。"
}
}

28
aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Services/DefaultDownloader.cs

@ -0,0 +1,28 @@
using LINGYUN.Abp.ElsaNext.Email.Contracts;
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace LINGYUN.Abp.ElsaNext.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);
}
Loading…
Cancel
Save