using System.Collections; using System.Text; using System.Text.Json; using Elsa.Email.Models; using Elsa.Email.Options; using Elsa.Email.Services; using Elsa.Workflows.Core; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; using Elsa.Workflows.Core.Services; using Elsa.Workflows.Management.Models; using Microsoft.Extensions.Options; using MimeKit; namespace Elsa.Email.Activities; /// /// Send an email message. /// [Activity("Email", "Send an email message.", Kind = ActivityKind.Task)] public class SendEmail : ActivityBase { /// /// The sender's email address. /// [Input(Description = "The sender's email address.")] public Input From { get; set; } [Input(Description = "The recipients email addresses.", UIHint = InputUIHints.MultiText)] public Input> To { get; set; } = default!; [Input( Description = "The cc recipient email addresses.", UIHint = InputUIHints.MultiText, Category = "More")] public Input> Cc { get; set; } = default!; [Input( Description = "The Bcc recipients email addresses.", UIHint = InputUIHints.MultiText, Category = "More")] public Input> Bcc { get; set; } = default!; [Input(Description = "The subject of the email message.")] public Input Subject { get; set; } = default!; [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 Attachments { get; set; } = default!; [Input( Description = "The body of the email message.", UIHint = InputUIHints.MultiLine )] public Input Body { get; set; } = default!; /// /// The activity to execute when an error occurs while trying to send the email. /// [Port] public IActivity? Error { get; set; } protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { var cancellationToken = context.CancellationToken; var message = new MimeMessage(); var options = context.GetRequiredService>().Value; var from = string.IsNullOrWhiteSpace(From.TryGet(context)) ? options.DefaultSender : From.Get(context)!; message.Sender = MailboxAddress.Parse(from); message.From.Add(MailboxAddress.Parse(from)); message.Subject = Subject.TryGet(context); var bodyBuilder = new BodyBuilder { HtmlBody = Body.TryGet(context) }; await AddAttachmentsAsync(context, bodyBuilder, cancellationToken); message.Body = bodyBuilder.ToMessageBody(); SetRecipientsEmailAddresses(message.To, To.Get(context)); SetRecipientsEmailAddresses(message.Cc, Cc.TryGet(context)); SetRecipientsEmailAddresses(message.Bcc, Bcc.TryGet(context)); var smtpService = context.GetRequiredService(); try { await smtpService.SendAsync(message, context.CancellationToken); await context.CompleteActivityAsync(); } catch (Exception) { await context.ScheduleActivityAsync(Error, OnErrorCompletedAsync); } } private async ValueTask OnErrorCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync(); private async Task AddAttachmentsAsync(ActivityExecutionContext context, BodyBuilder bodyBuilder, CancellationToken cancellationToken) { var attachments = Attachments.TryGet(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; } 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(); 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) => attachments is string text ? new[] { text } : attachments is IEnumerable enumerable ? enumerable : new[] { attachments }; private void SetRecipientsEmailAddresses(InternetAddressList list, IEnumerable? addresses) { if (addresses == null) return; list.AddRange(addresses.Select(MailboxAddress.Parse)); } }