11 changed files with 486 additions and 0 deletions
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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"); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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." |
||||
|
} |
||||
|
} |
||||
@ -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.": "邮件正文。" |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue