diff --git a/Frends.SMTP.SendEmail/CHANGELOG.md b/Frends.SMTP.SendEmail/CHANGELOG.md index d3eb860..276afc3 100644 --- a/Frends.SMTP.SendEmail/CHANGELOG.md +++ b/Frends.SMTP.SendEmail/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [1.2.0] - 2023-12-21 +### Changed +- Changed Task to use MailKit library instead of deprecated System.Net.Mail. +- Changed the Task to create a temp file from the AttachmentFromString. The temp file will be removed afterwards. + +### Added +- Added OAuth2 support. +- New parameters: + - SecureSocket + - UseOAuth2 + - Token + +### Removed +- Removed UseWindowsAuthentication because it's not supported with the new library. + ## [1.1.0] - 2023-11-29 ### Fixed - [Breaking] Fixed issue with the attachments can't be given as expression by adding AttachmentOptions class. diff --git a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail.Tests/SendEmailTests.cs b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail.Tests/SendEmailTests.cs index a452b6e..ff381f4 100644 --- a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail.Tests/SendEmailTests.cs +++ b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail.Tests/SendEmailTests.cs @@ -1,6 +1,7 @@ using NUnit.Framework; using System; using System.IO; +using System.Threading.Tasks; using Frends.SMTP.SendEmail.Definitions; namespace Frends.SMTP.SendEmail.Tests; @@ -15,8 +16,6 @@ public class SendEmailTests private static readonly string TOEMAILADDRESS = Environment.GetEnvironmentVariable("Frends_SMTP_Email"); private static readonly string FROMEMAILADDRESS = Environment.GetEnvironmentVariable("Frends_SMTP_Email"); private const int PORT = 587; - private const bool USESSL = true; - private const bool USEWINDOWSAUTHENTICATION = false; // ************************************************************************************************************ @@ -75,11 +74,12 @@ public void EmailTestSetup() Password = PASSWORD, SMTPServer = SMTPADDRESS, Port = PORT, - UseSsl = USESSL, - UseWindowsAuthentication = USEWINDOWSAUTHENTICATION, + UseOAuth2 = false, + SecureSocket = SecureSocketOption.None }; } + [TearDown] public void EmailTestTearDown() { @@ -88,17 +88,17 @@ public void EmailTestTearDown() } [Test] - public void SendEmailWithPlainText() + public async Task SendEmailWithPlainText() { var input = _input; input.Subject = "Email test - PlainText"; - var result = SMTP.SendEmail(input, null, _options, default); + var result = await SMTP.SendEmail(input, null, _options, default); Assert.IsTrue(result.EmailSent); } [Test] - public void SendEmailWithFileAttachment() + public async Task SendEmailWithFileAttachment() { var input = _input; input.Subject = "Email test - FileAttachment"; @@ -114,12 +114,12 @@ public void SendEmailWithFileAttachment() var Attachments = new AttachmentOptions { Attachments = new Attachment[] { attachment } }; - var result = SMTP.SendEmail(input, Attachments, _options, default); + var result = await SMTP.SendEmail(input, Attachments, _options, default); Assert.IsTrue(result.EmailSent); } [Test] - public void SendEmailWithStringAttachment() + public async Task SendEmailWithStringAttachment() { var input = _input; input.Subject = "Email test - AttachmentFromString"; @@ -131,12 +131,12 @@ public void SendEmailWithStringAttachment() }; var Attachments = new AttachmentOptions { Attachments = new Attachment[] { attachment } }; - var result = SMTP.SendEmail(input, Attachments, _options, default); + var result = await SMTP.SendEmail(input, Attachments, _options, default); Assert.IsTrue(result.EmailSent); } [Test] - public void TrySendingEmailWithNoFileAttachmentFound() + public async Task TrySendingEmailWithNoFileAttachmentFound() { var input = _input; input.Subject = "Email test"; @@ -151,12 +151,12 @@ public void TrySendingEmailWithNoFileAttachmentFound() var Attachments = new AttachmentOptions { Attachments = new Attachment[] { attachment } }; - var result = SMTP.SendEmail(input, Attachments, _options, default); + var result = await SMTP.SendEmail(input, Attachments, _options, default); Assert.IsFalse(result.EmailSent); } [Test] - public void TrySendingEmailWithNoCcAndBcc() + public async Task TrySendingEmailWithNoCcAndBcc() { var input = _input2; input.Subject = "Email test"; @@ -171,7 +171,7 @@ public void TrySendingEmailWithNoCcAndBcc() var Attachments = new AttachmentOptions { Attachments = new Attachment[] { attachment } }; - var result = SMTP.SendEmail(input, Attachments, _options, default); + var result = await SMTP.SendEmail(input, Attachments, _options, default); Assert.IsFalse(result.EmailSent); } @@ -191,7 +191,7 @@ public void TrySendingEmailWithNoFileAttachmentFoundException() var Attachments = new AttachmentOptions { Attachments = new Attachment[] { attachment } }; - Assert.Throws(() => SMTP.SendEmail(input, Attachments, _options, default)); - + var ex = Assert.ThrowsAsync(async () => await SMTP.SendEmail(input, Attachments, _options, default)); + Assert.AreEqual(@$"The given filepath '{attachment.FilePath}' had no matching files", ex.Message); } } diff --git a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/Attachment.cs b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/Attachment.cs index 8133139..b58bd65 100644 --- a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/Attachment.cs +++ b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/Attachment.cs @@ -11,11 +11,13 @@ public class Attachment /// /// Chooses if the attachment file is created from a string or copied from disk. /// + /// AttachmentType.FileAttachment public AttachmentType AttachmentType { get; set; } /// /// Attachment from string. /// + /// AttachmentFromString { "Test file content", "testFile.txt" } [UIHint(nameof(AttachmentType), "", AttachmentType.AttachmentFromString)] public AttachmentFromString StringAttachment { get; set; } @@ -23,6 +25,7 @@ public class Attachment /// Attachment file's path. Uses Directory.GetFiles(string, string) as a pattern matching technique. See https://msdn.microsoft.com/en-us/library/wz42302f(v=vs.110).aspx. /// Exception: If the path ends in a directory, all files in that folder are added as attachments. /// + /// C:\path\to\file.txt [DefaultValue("")] [UIHint(nameof(AttachmentType), "", AttachmentType.FileAttachment)] public string FilePath { get; set; } @@ -30,12 +33,14 @@ public class Attachment /// /// If set true and no files match the given path, an exception is thrown. /// + /// true [UIHint(nameof(AttachmentType), "", AttachmentType.FileAttachment)] public bool ThrowExceptionIfAttachmentNotFound { get; set; } /// /// If set true and no files match the given path, email will be sent nevertheless. /// + /// true [UIHint(nameof(AttachmentType), "", AttachmentType.FileAttachment)] public bool SendIfNoAttachmentsFound { get; set; } } diff --git a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/Input.cs b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/Input.cs index 21cce39..9b16b18 100644 --- a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/Input.cs +++ b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/Input.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.ComponentModel.DataAnnotations; namespace Frends.SMTP.SendEmail.Definitions; @@ -11,6 +12,7 @@ public class Input /// Recipient addresses separated by ',' or ';' /// /// jane.doe@somedomain.com + [DisplayFormat(DataFormatString = "Text")] [DefaultValue("jane.doe@somedomain.com")] public string To { get; set; } @@ -18,6 +20,7 @@ public class Input /// Cc recipient addresses separated by ',' or ';' /// /// jane.doe@somedomain.com + [DisplayFormat(DataFormatString = "Text")] [DefaultValue("jane.doe@somedomain.com")] public string Cc { get; set; } @@ -25,6 +28,7 @@ public class Input /// Bcc recipient addresses separated by ',' or ';' /// /// jane.doe@somedomain.com + [DisplayFormat(DataFormatString = "Text")] [DefaultValue("jane.doe@somedomain.com")] public string Bcc { get; set; } @@ -32,6 +36,7 @@ public class Input /// Sender address. /// /// jane.doe@somedomain.com + [DisplayFormat(DataFormatString = "Text")] [DefaultValue("john.doe@somedomain.com")] public string From { get; set; } @@ -39,6 +44,7 @@ public class Input /// Name of the sender. /// /// Jane Doe + [DisplayFormat(DataFormatString = "Text")] [DefaultValue("")] public string SenderName { get; set; } @@ -46,13 +52,15 @@ public class Input /// Email message's subject. /// /// Hello Jane + [DisplayFormat(DataFormatString = "Text")] [DefaultValue("Hello Jane")] public string Subject { get; set; } /// /// Body of the message. /// - /// You've got mail! + /// You've got mail! + [DisplayFormat(DataFormatString = "Text")] [DefaultValue("You've got mail!")] public string Message { get; set; } @@ -67,6 +75,7 @@ public class Input /// Encoding of message body and subject. Use following table's name column for other options. https://msdn.microsoft.com/en-us/library/system.text.encoding(v=vs.110).aspx#Anchor_5 /// /// utf-8 + [DisplayFormat(DataFormatString = "Text")] [DefaultValue("utf-8")] public string MessageEncoding { get; set; } diff --git a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/Options.cs b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/Options.cs index aae9903..ed4f48d 100644 --- a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/Options.cs +++ b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/Options.cs @@ -12,6 +12,7 @@ public class Options /// SMTP server address. /// /// smtp.somedomain.com + [DisplayFormat(DataFormatString = "Text")] [DefaultValue("smtp.somedomain.com")] public string SMTPServer { get; set; } @@ -23,25 +24,34 @@ public class Options public int Port { get; set; } /// - /// Set this true if SMTP expects to be connected using SSL. + /// Choose the SecureSocketOption to use, default is Auto /// - /// true - [DefaultValue("false")] - public bool UseSsl { get; set; } + /// SecureSocketOption.None + [DefaultValue(SecureSocketOption.Auto)] + public SecureSocketOption SecureSocket { get; set; } /// - /// Set this true if you want to use windows authentication to authenticate to SMTP server. + /// Set this true if SMTP server expectes OAuth token. /// - /// false - [DefaultValue("true")] - public bool UseWindowsAuthentication { get; set; } + /// true + [DefaultValue(false)] + public bool UseOAuth2 { get; set; } + + /// + /// Token to be used when using OAuth2. + /// + /// cec4ce4f98e4f68e4vc89v1489v4987s4erv8794... + [DisplayFormat(DataFormatString = "Text")] + [UIHint(nameof(UseOAuth2), "", true)] + [PasswordPropertyText] + public string Token { get; set; } /// /// Use this username to log in to the SMTP server /// /// testuser + [DisplayFormat(DataFormatString = "Text")] [DefaultValue("")] - [UIHint(nameof(UseWindowsAuthentication), "", false)] public string UserName { get; set; } /// @@ -50,6 +60,6 @@ public class Options /// Password123 [PasswordPropertyText(true)] [DefaultValue("")] - [UIHint(nameof(UseWindowsAuthentication), "", false)] + [UIHint(nameof(UseOAuth2), "", false)] public string Password { get; set; } } diff --git a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/SecureSocketOption.cs b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/SecureSocketOption.cs new file mode 100644 index 0000000..cc58324 --- /dev/null +++ b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Definitions/SecureSocketOption.cs @@ -0,0 +1,32 @@ +namespace Frends.SMTP.SendEmail.Definitions; + +/// +/// Secure Socket Options. +/// +public enum SecureSocketOption +{ + /// + /// No SSL or TLS encryption should be used. + /// + None, + /// + /// Allow the IMailService to decide which SSL or TLS options to use (default). + /// If the server does not support SSL or TLS, then the connection will continue without any encryption. + /// + Auto, + /// + /// The connection should use SSL or TLS encryption immediately. + /// + SslOnConnect, + /// + /// Elevates the connection to use TLS encryption immediately after reading the greeting and capabilities of + /// the server. If the server does not support the STARTTLS extension, then the connection will fail and a + /// NotSupportedException will be thrown. + /// + StartTls, + /// + /// Elevates the connection to use TLS encryption immediately after reading the greeting and capabilities of + /// the server, but only if the server supports the STARTTLS extension. + /// + StartTlsWhenAvailable +} diff --git a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Frends.Smtp.SendEmail.csproj b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Frends.Smtp.SendEmail.csproj index 1572356..0ea3711 100644 --- a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Frends.Smtp.SendEmail.csproj +++ b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/Frends.Smtp.SendEmail.csproj @@ -1,9 +1,8 @@ - - + net6.0 - 1.1.0 + 1.2.0 latest Frends Frends @@ -24,6 +23,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/SendEmailTask.cs b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/SendEmailTask.cs index a05ca87..e3bb243 100644 --- a/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/SendEmailTask.cs +++ b/Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/SendEmailTask.cs @@ -2,10 +2,15 @@ using System.Collections.Generic; using System.ComponentModel; using System.IO; +using System.Linq; using System.Net; -using System.Net.Mail; +using MailKit.Net.Smtp; +using MailKit.Security; +using MimeKit; +using MimeKit.Text; using System.Text; using System.Threading; +using System.Threading.Tasks; using Frends.SMTP.SendEmail.Definitions; namespace Frends.SMTP.SendEmail; @@ -18,16 +23,24 @@ public static class SMTP /// Sends email message with optional attachments. /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.SMTP.SendEmail) /// - /// Message parameters. + /// Message parameters. /// Parameters for adding attachments. /// Connection parameters. /// Token given by Frends to terminate the Task. /// Object { bool EmailSent, string StatusString } - public static Result SendEmail([PropertyTab] Input message, [PropertyTab] AttachmentOptions attachments, [PropertyTab] Options SMTPSettings, CancellationToken cancellationToken) + public static async Task SendEmail([PropertyTab] Input input, [PropertyTab] AttachmentOptions attachments, [PropertyTab] Options SMTPSettings, CancellationToken cancellationToken) { - using var client = InitializeSmtpClient(SMTPSettings); - using var mail = InitializeMailMessage(message, cancellationToken); - if (attachments != null) + using var mail = InitializeMimeMessage(input); + + if (attachments != null && attachments.Attachments.Length > 0) + { + var builder = new BodyBuilder(); + + if (input.IsMessageHtml) + builder.HtmlBody = input.Message; + else + builder.TextBody = input.Message; + foreach (var attachment in attachments.Attachments) { cancellationToken.ThrowIfCancellationRequested(); @@ -37,25 +50,47 @@ public static Result SendEmail([PropertyTab] Input message, [PropertyTab] Attach ICollection allAttachmentFilePaths = GetAttachmentFiles(attachment.FilePath); if (attachment.ThrowExceptionIfAttachmentNotFound && allAttachmentFilePaths.Count == 0) - throw new FileNotFoundException($@"The given filepath ""attachment.FilePath"" had no matching files", attachment.FilePath); + throw new FileNotFoundException($@"The given filepath '{attachment.FilePath}' had no matching files", attachment.FilePath); if (allAttachmentFilePaths.Count == 0 && !attachment.SendIfNoAttachmentsFound) - return new Result(false, $@"No attachments found matching path ""{attachment.FilePath}"". No email sent."); + return new Result(false, $@"No attachments found matching path '{attachment.FilePath}'. No email sent."); foreach (var fp in allAttachmentFilePaths) { cancellationToken.ThrowIfCancellationRequested(); - mail.Attachments.Add(new System.Net.Mail.Attachment(fp)); + builder.Attachments.Add(fp, cancellationToken); } } - if (attachment.AttachmentType == AttachmentType.AttachmentFromString - && !string.IsNullOrEmpty(attachment.StringAttachment.FileContent)) - mail.Attachments.Add(System.Net.Mail.Attachment.CreateAttachmentFromString - (attachment.StringAttachment.FileContent, attachment.StringAttachment.FileName)); + // Create attachment only if content is not empty. + if (attachment.AttachmentType == AttachmentType.AttachmentFromString && !string.IsNullOrEmpty(attachment.StringAttachment.FileContent)) + { + var path = CreateTemporaryFile(attachment); + builder.Attachments.Add(path, cancellationToken); + CleanUpTempWorkDir(path); + } } - client.Send(mail); + mail.Body = builder.ToMessageBody(); + } + else + { + //Set message encoding + Encoding encoding = Encoding.GetEncoding(input.MessageEncoding); + + var body = (input.IsMessageHtml) + ? new TextPart(TextFormat.Html) { Text = input.Message } + : new TextPart(TextFormat.Plain) { Text = input.Message }; + body.SetText(encoding, input.Message); + mail.Body = body; + } + + using var client = new SmtpClient(); + + await ConnectAndAuthenticate(client, SMTPSettings, cancellationToken); + + await client.SendAsync(mail, cancellationToken); + await client.DisconnectAsync(true, cancellationToken); return new Result(true, $"Email sent to: {mail.To}"); } @@ -63,72 +98,56 @@ public static Result SendEmail([PropertyTab] Input message, [PropertyTab] Attach /// /// Initializes new SmtpClient with given parameters. /// - private static SmtpClient InitializeSmtpClient(Options settings) + private static async Task ConnectAndAuthenticate(SmtpClient client, Options settings, CancellationToken cancellationToken) { - var smtpClient = new SmtpClient + var secureSocketOption = settings.SecureSocket switch { - Port = settings.Port, - DeliveryMethod = SmtpDeliveryMethod.Network, - UseDefaultCredentials = settings.UseWindowsAuthentication, - EnableSsl = settings.UseSsl, - Host = settings.SMTPServer + SecureSocketOption.None => SecureSocketOptions.None, + SecureSocketOption.SslOnConnect => SecureSocketOptions.SslOnConnect, + SecureSocketOption.StartTls => SecureSocketOptions.StartTls, + SecureSocketOption.StartTlsWhenAvailable => SecureSocketOptions.StartTlsWhenAvailable, + _ => SecureSocketOptions.Auto, }; + await client.ConnectAsync(settings.SMTPServer, settings.Port, secureSocketOption, cancellationToken); + + SaslMechanism mechanism; - if (!settings.UseWindowsAuthentication && !string.IsNullOrEmpty(settings.UserName)) - smtpClient.Credentials = new NetworkCredential(settings.UserName, settings.Password); + if (settings.UseOAuth2) + mechanism = new SaslMechanismOAuth2(settings.UserName, settings.Token); + else + mechanism = new SaslMechanismLogin(new NetworkCredential(settings.UserName, settings.Password)); - return smtpClient; + await client.AuthenticateAsync(mechanism, cancellationToken); + + return client.IsConnected && client.IsAuthenticated; } /// /// Initializes new MailMessage with given parameters. Uses default value 'true' for IsBodyHtml /// - private static MailMessage InitializeMailMessage(Input input, CancellationToken cancellationToken) + private static MimeMessage InitializeMimeMessage(Input input) { //split recipients, either by comma or semicolon var separators = new[] { ',', ';' }; - string[] recipients = string.IsNullOrEmpty(input.To) - ? Array.Empty() - : input.To.Split(separators, StringSplitOptions.RemoveEmptyEntries); - string[] ccRecipients = string.IsNullOrEmpty(input.Cc) - ? Array.Empty() - : input.Cc.Split(separators, StringSplitOptions.RemoveEmptyEntries); - string[] bccRecipients = string.IsNullOrEmpty(input.Bcc) - ? Array.Empty() - : input.Bcc.Split(separators, StringSplitOptions.RemoveEmptyEntries); + MailboxAddress[] recipients = string.IsNullOrEmpty(input.To) + ? Array.Empty() + : input.To.Split(separators, StringSplitOptions.RemoveEmptyEntries).Select(x => MailboxAddress.Parse(x)).ToArray(); + MailboxAddress[] ccRecipients = string.IsNullOrEmpty(input.Cc) + ? Array.Empty() + : input.Cc.Split(separators, StringSplitOptions.RemoveEmptyEntries).Select(x => MailboxAddress.Parse(x)).ToArray(); + MailboxAddress[] bccRecipients = string.IsNullOrEmpty(input.Bcc) + ? Array.Empty() + : input.Bcc.Split(separators, StringSplitOptions.RemoveEmptyEntries).Select(x => MailboxAddress.Parse(x)).ToArray(); //Create mail object - var mail = new MailMessage() - { - From = new MailAddress(input.From, input.SenderName), - Subject = input.Subject, - Body = input.Message, - IsBodyHtml = input.IsMessageHtml - }; - //Add recipients - foreach (var recipientAddress in recipients) - { - cancellationToken.ThrowIfCancellationRequested(); - mail.To.Add(recipientAddress); - } - //Add CC recipients - foreach (var ccRecipient in ccRecipients) - { - cancellationToken.ThrowIfCancellationRequested(); - mail.CC.Add(ccRecipient); - } - //Add BCC recipients - foreach (var bccRecipient in bccRecipients) - { - cancellationToken.ThrowIfCancellationRequested(); - mail.Bcc.Add(bccRecipient); - } - //Set message encoding - Encoding encoding = Encoding.GetEncoding(input.MessageEncoding); - - mail.BodyEncoding = encoding; - mail.SubjectEncoding = encoding; + var mail = new MimeMessage(); + mail.From.Add(new MailboxAddress(input.SenderName, input.From)); + mail.Sender = new MailboxAddress(input.SenderName, input.From); + mail.To.AddRange(recipients); + mail.Cc.AddRange(ccRecipients); + mail.Bcc.AddRange(bccRecipients); + mail.Subject = input.Subject; return mail; } @@ -145,4 +164,39 @@ private static ICollection GetAttachmentFiles(string filePath) return filePaths; } + + /// + /// Create temp file of attachment from string. + /// + /// + private static string CreateTemporaryFile(Attachment attachment) + { + var TempWorkDirBase = InitializeTemporaryWorkPath(); + var filePath = Path.Combine(TempWorkDirBase, attachment.StringAttachment.FileName); + var content = attachment.StringAttachment.FileContent; + + using var sw = File.CreateText(filePath); + sw.Write(content); + + return filePath; + } + + /// + /// Remove the temporary workdir. + /// + /// + private static void CleanUpTempWorkDir(string tempWorkDir) + { + if (!string.IsNullOrEmpty(tempWorkDir) && Directory.Exists(tempWorkDir)) Directory.Delete(tempWorkDir, true); + } + + /// + /// Create temperary directory for temp file. + /// + private static string InitializeTemporaryWorkPath() + { + var tempWorkDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempWorkDir); + return tempWorkDir; + } } diff --git a/Frends.SMTP.SendEmail/README.md b/Frends.SMTP.SendEmail/README.md index 6f1ffff..2777a75 100644 --- a/Frends.SMTP.SendEmail/README.md +++ b/Frends.SMTP.SendEmail/README.md @@ -1,7 +1,6 @@ # Frends.SMTP.SendEmail Frends task for sending emails with SMTP. Task sends emails via SMTP protocol and can handle attachments either from file or as raw string input. - [![Frends.Smtp.SendEmail Main](https://github.com/FrendsPlatform/Frends.SMTP/actions/workflows/SendEmail_build_and_test_on_main.yml/badge.svg)](https://github.com/FrendsPlatform/Frends.SMTP/actions) ![GitHub](https://img.shields.io/github/license/FrendsPlatform/Frends.SMTP?label=License) ![Coverage](https://app-github-custom-badges.azurewebsites.net/Badge?key=FrendsPlatform/Frends.SMTP/Frends.SMTP.SendEmail|main)