Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 228 additions & 0 deletions shesha-core/src/Shesha.Application/Email/MailKitEmailHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
using MimeKit.Utils;
using Shesha.Configuration;

namespace Shesha.Email
{
internal static class MailKitEmailHelper
{
public static MimeMessage ConvertToMimeMessage(MailMessage mail)
{
if (mail == null)
throw new ArgumentNullException(nameof(mail));

var message = new MimeMessage();

if (mail.From != null)
message.From.Add(CreateMailboxAddress(mail.From));

foreach (var address in mail.To.Cast<MailAddress>())
{
message.To.Add(CreateMailboxAddress(address));
}

foreach (var address in mail.CC.Cast<MailAddress>())
{
message.Cc.Add(CreateMailboxAddress(address));
}

foreach (var address in mail.Bcc.Cast<MailAddress>())
{
message.Bcc.Add(CreateMailboxAddress(address));
}

message.Subject = mail.Subject ?? string.Empty;

foreach (var headerKey in mail.Headers.AllKeys)
{
var value = mail.Headers[headerKey];
if (!string.IsNullOrEmpty(headerKey) && !string.IsNullOrEmpty(value))
{
message.Headers.Replace(headerKey, value);
}
}

var builder = new BodyBuilder();

var htmlView = mail.AlternateViews
.Cast<AlternateView>()
.FirstOrDefault(v => string.Equals(v.ContentType.MediaType, "text", StringComparison.OrdinalIgnoreCase)
&& string.Equals(v.ContentType.MediaSubtype, "html", StringComparison.OrdinalIgnoreCase));

if (htmlView != null)
{
builder.HtmlBody = ReadAlternateView(htmlView);
AddLinkedResources(builder, htmlView);
}
else if (mail.IsBodyHtml)
{
builder.HtmlBody = mail.Body;
}
else
{
builder.TextBody = mail.Body;
}

foreach (var alternateView in mail.AlternateViews.Cast<AlternateView>())
{
if (alternateView == htmlView)
continue;

if (string.Equals(alternateView.ContentType.MediaType, "text", StringComparison.OrdinalIgnoreCase)
&& string.Equals(alternateView.ContentType.MediaSubtype, "plain", StringComparison.OrdinalIgnoreCase))
{
builder.TextBody = ReadAlternateView(alternateView);
}
}

foreach (var attachment in mail.Attachments.Cast<Attachment>())
{
builder.Attachments.Add(CreateAttachmentPart(attachment));
}

message.Body = builder.ToMessageBody();

return message;
}

public static async Task SendAsync(MimeMessage message, SmtpSettings settings, CancellationToken cancellationToken = default)
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));

using var smtpClient = new SmtpClient();
await smtpClient.ConnectAsync(settings.Host, settings.Port, GetSecureSocketOption(settings), cancellationToken).ConfigureAwait(false);

var credential = CreateCredential(settings);
if (credential != null)
{
await smtpClient.AuthenticateAsync(credential, cancellationToken).ConfigureAwait(false);
}

await smtpClient.SendAsync(message, cancellationToken).ConfigureAwait(false);
await smtpClient.DisconnectAsync(true, cancellationToken).ConfigureAwait(false);
}
Comment on lines +98 to +114

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add timeout configuration for SMTP operations.

The SMTP client operations (Connect, Authenticate, Send, Disconnect) have no timeout configured, which can cause indefinite hangs if the SMTP server becomes unresponsive.

Consider adding a timeout parameter or using a reasonable default:

-public static async Task SendAsync(MimeMessage message, SmtpSettings settings, CancellationToken cancellationToken = default)
+public static async Task SendAsync(MimeMessage message, SmtpSettings settings, CancellationToken cancellationToken = default, int timeoutSeconds = 30)
 {
     if (settings == null)
         throw new ArgumentNullException(nameof(settings));

-    using var smtpClient = new SmtpClient();
+    using var smtpClient = new SmtpClient
+    {
+        Timeout = timeoutSeconds * 1000
+    };
     await smtpClient.ConnectAsync(settings.Host, settings.Port, GetSecureSocketOption(settings), cancellationToken).ConfigureAwait(false);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static async Task SendAsync(MimeMessage message, SmtpSettings settings, CancellationToken cancellationToken = default)
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));
using var smtpClient = new SmtpClient();
await smtpClient.ConnectAsync(settings.Host, settings.Port, GetSecureSocketOption(settings), cancellationToken).ConfigureAwait(false);
var credential = CreateCredential(settings);
if (credential != null)
{
await smtpClient.AuthenticateAsync(credential, cancellationToken).ConfigureAwait(false);
}
await smtpClient.SendAsync(message, cancellationToken).ConfigureAwait(false);
await smtpClient.DisconnectAsync(true, cancellationToken).ConfigureAwait(false);
}
public static async Task SendAsync(MimeMessage message, SmtpSettings settings, CancellationToken cancellationToken = default, int timeoutSeconds = 30)
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));
using var smtpClient = new SmtpClient
{
Timeout = timeoutSeconds * 1000
};
await smtpClient.ConnectAsync(settings.Host, settings.Port, GetSecureSocketOption(settings), cancellationToken).ConfigureAwait(false);
var credential = CreateCredential(settings);
if (credential != null)
{
await smtpClient.AuthenticateAsync(credential, cancellationToken).ConfigureAwait(false);
}
await smtpClient.SendAsync(message, cancellationToken).ConfigureAwait(false);
await smtpClient.DisconnectAsync(true, cancellationToken).ConfigureAwait(false);
}
🤖 Prompt for AI Agents
In shesha-core/src/Shesha.Application/Email/MailKitEmailHelper.cs around lines
98 to 114, the SMTP operations have no timeout configured and can hang
indefinitely; set an operation timeout on the SmtpClient before connecting
(e.g., smtpClient.Timeout) using a timeout value from SmtpSettings or a sensible
default, and use a CancellationTokenSource.CancelAfter to enforce per-operation
timeouts by linking it with the provided cancellationToken for
ConnectAsync/AuthenticateAsync/SendAsync/DisconnectAsync calls so each call
fails fast when the SMTP server is unresponsive.


public static void Send(MimeMessage message, SmtpSettings settings, CancellationToken cancellationToken = default)
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));

using var smtpClient = new SmtpClient();
smtpClient.Connect(settings.Host, settings.Port, GetSecureSocketOption(settings), cancellationToken);

var credential = CreateCredential(settings);
if (credential != null)
{
smtpClient.Authenticate(credential, cancellationToken);
}

smtpClient.Send(message, cancellationToken);
smtpClient.Disconnect(true, cancellationToken);
}
Comment on lines +116 to +132

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add timeout configuration for synchronous SMTP operations.

Similar to the async path, the synchronous SMTP operations lack timeout configuration.

-public static void Send(MimeMessage message, SmtpSettings settings, CancellationToken cancellationToken = default)
+public static void Send(MimeMessage message, SmtpSettings settings, CancellationToken cancellationToken = default, int timeoutSeconds = 30)
 {
     if (settings == null)
         throw new ArgumentNullException(nameof(settings));

-    using var smtpClient = new SmtpClient();
+    using var smtpClient = new SmtpClient
+    {
+        Timeout = timeoutSeconds * 1000
+    };
     smtpClient.Connect(settings.Host, settings.Port, GetSecureSocketOption(settings), cancellationToken);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static void Send(MimeMessage message, SmtpSettings settings, CancellationToken cancellationToken = default)
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));
using var smtpClient = new SmtpClient();
smtpClient.Connect(settings.Host, settings.Port, GetSecureSocketOption(settings), cancellationToken);
var credential = CreateCredential(settings);
if (credential != null)
{
smtpClient.Authenticate(credential, cancellationToken);
}
smtpClient.Send(message, cancellationToken);
smtpClient.Disconnect(true, cancellationToken);
}
public static void Send(MimeMessage message, SmtpSettings settings, CancellationToken cancellationToken = default, int timeoutSeconds = 30)
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));
using var smtpClient = new SmtpClient
{
Timeout = timeoutSeconds * 1000
};
smtpClient.Connect(settings.Host, settings.Port, GetSecureSocketOption(settings), cancellationToken);
var credential = CreateCredential(settings);
if (credential != null)
{
smtpClient.Authenticate(credential, cancellationToken);
}
smtpClient.Send(message, cancellationToken);
smtpClient.Disconnect(true, cancellationToken);
}
🤖 Prompt for AI Agents
In shesha-core/src/Shesha.Application/Email/MailKitEmailHelper.cs around lines
116 to 132, the synchronous Send method never sets a timeout on the MailKit
SmtpClient; set the client's Timeout property before calling Connect (same place
the async path configures timeouts) using the configured SMTP timeout value
(e.g. settings.Timeout.TotalMilliseconds cast to int or
settings.TimeoutInMilliseconds) with a sensible fallback, so
Connect/Authenticate/Send will honor the configured timeout for synchronous
operations.


private static void AddLinkedResources(BodyBuilder builder, AlternateView view)
{
foreach (var resource in view.LinkedResources.Cast<LinkedResource>())
{
builder.LinkedResources.Add(CreateLinkedResource(resource));
}
}

private static MimeEntity CreateAttachmentPart(Attachment attachment)
{
var mediaType = attachment.ContentType.MediaType ?? "application";
var mediaSubType = attachment.ContentType.MediaSubtype ?? "octet-stream";
var mimePart = new MimePart(mediaType, mediaSubType)
{
Content = new MimeContent(CopyToMemoryStream(attachment.ContentStream)),
ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),
ContentTransferEncoding = ContentEncoding.Base64,
FileName = attachment.Name
};

return mimePart;
}

private static MimeEntity CreateLinkedResource(LinkedResource resource)
{
var mediaType = resource.ContentType.MediaType ?? "application";
var mediaSubType = resource.ContentType.MediaSubtype ?? "octet-stream";
var mimePart = new MimePart(mediaType, mediaSubType)
{
Content = new MimeContent(CopyToMemoryStream(resource.ContentStream)),
ContentDisposition = new ContentDisposition(ContentDisposition.Inline),
ContentTransferEncoding = ContentEncoding.Base64,
ContentId = string.IsNullOrWhiteSpace(resource.ContentId) ? MimeUtils.GenerateMessageId() : resource.ContentId,
FileName = resource.ContentId
};

return mimePart;
}

private static MemoryStream CopyToMemoryStream(Stream stream)
{
var memoryStream = new MemoryStream();
if (stream.CanSeek)
stream.Position = 0;

stream.CopyTo(memoryStream);
memoryStream.Position = 0;
return memoryStream;
}

private static MailboxAddress CreateMailboxAddress(MailAddress address)
{
return string.IsNullOrEmpty(address.DisplayName)
? MailboxAddress.Parse(address.Address)
: new MailboxAddress(address.DisplayName, address.Address);
}

private static string ReadAlternateView(AlternateView view)
{
var encoding = !string.IsNullOrWhiteSpace(view.ContentType.CharSet)
? Encoding.GetEncoding(view.ContentType.CharSet)
: Encoding.UTF8;

if (view.ContentStream.CanSeek)
view.ContentStream.Position = 0;

using var reader = new StreamReader(view.ContentStream, encoding, detectEncodingFromByteOrderMarks: true, leaveOpen: true);
var content = reader.ReadToEnd();
if (view.ContentStream.CanSeek)
view.ContentStream.Position = 0;

return content;
}

private static NetworkCredential? CreateCredential(SmtpSettings settings)
{
if (string.IsNullOrWhiteSpace(settings.UserName))
return null;

return string.IsNullOrWhiteSpace(settings.Domain)
? new NetworkCredential(settings.UserName, settings.Password)
: new NetworkCredential(settings.UserName, settings.Password, settings.Domain);
}

private static SecureSocketOptions GetSecureSocketOption(SmtpSettings settings)
{
if (!settings.EnableSsl)
return SecureSocketOptions.None;

return settings.Port == 465
? SecureSocketOptions.SslOnConnect
: SecureSocketOptions.StartTls;
}
}
}
41 changes: 10 additions & 31 deletions shesha-core/src/Shesha.Application/Email/SheshaEmailSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Text;
using System.Threading.Tasks;
using System.Net.Mail;
using System.Text;
using System.Threading.Tasks;

namespace Shesha.Email
{
Expand Down Expand Up @@ -78,10 +77,8 @@ protected override async Task SendEmailAsync(MailMessage mail)
if (!PrepareAndCheckMail(mail, smtpSettings))
return;

using (var smtpClient = GetSmtpClient(smtpSettings))
{
await smtpClient.SendMailAsync(mail);
}
var mimeMessage = MailKitEmailHelper.ConvertToMimeMessage(mail);
await MailKitEmailHelper.SendAsync(mimeMessage, smtpSettings);
}

protected override void SendEmail(MailMessage mail)
Expand All @@ -95,10 +92,8 @@ protected override void SendEmail(MailMessage mail)
if (!PrepareAndCheckMail(mail, smtpSettings))
return;

using (var smtpClient = GetSmtpClient(smtpSettings))
{
smtpClient.Send(mail);
}
var mimeMessage = MailKitEmailHelper.ConvertToMimeMessage(mail);
MailKitEmailHelper.Send(mimeMessage, smtpSettings);
}

#region private methods
Expand Down Expand Up @@ -197,22 +192,6 @@ private void NormalizeMailSender(MailMessage mail, SmtpSettings smtpSettings)
}
}

/// <summary>
/// Returns SmtpClient configured according to the current application settings
/// </summary>
private SmtpClient GetSmtpClient(SmtpSettings smtpSettings)
{
var client = new SmtpClient(smtpSettings.Host, smtpSettings.Port)
{
EnableSsl = smtpSettings.EnableSsl,
Credentials = string.IsNullOrWhiteSpace(smtpSettings.Domain)
? new NetworkCredential(smtpSettings.UserName, smtpSettings.Password)
: new NetworkCredential(smtpSettings.UserName, smtpSettings.Password, smtpSettings.Domain)
};

return client;
}

#endregion
}
}
#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
using Shesha.Configuration;
using Shesha.Configuration.Email;
using Shesha.Domain;
using Shesha.Email;
using Shesha.Email.Dtos;
using Shesha.Notifications.Dto;
using Shesha.Notifications.MessageParticipants;
using Shesha.Utilities;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Mail;
using System.Threading.Tasks;

Expand Down Expand Up @@ -86,10 +86,9 @@ private async Task SendEmailAsync(MailMessage mail)
{
try
{
using (var smtpClient = await GetSmtpClientAsync())
{
smtpClient.Send(mail);
}
var mimeMessage = MailKitEmailHelper.ConvertToMimeMessage(mail);
var smtpSettings = await _emailSettings.SmtpSettings.GetValueAsync();
await MailKitEmailHelper.SendAsync(mimeMessage, smtpSettings);
}
catch (Exception ex)
{
Expand All @@ -98,28 +97,6 @@ private async Task SendEmailAsync(MailMessage mail)
}
}

private async Task<SmtpClient> GetSmtpClientAsync()
{
var smtpSettings = await _emailSettings.SmtpSettings.GetValueAsync();
return GetSmtpClient(smtpSettings);
}

/// <summary>
/// Returns SmtpClient configured according to the current application settings
/// </summary>
private SmtpClient GetSmtpClient(SmtpSettings smtpSettings)
{
var client = new SmtpClient(smtpSettings.Host, smtpSettings.Port)
{
EnableSsl = smtpSettings.EnableSsl,
Credentials = string.IsNullOrWhiteSpace(smtpSettings.Domain)
? new NetworkCredential(smtpSettings.UserName, smtpSettings.Password)
: new NetworkCredential(smtpSettings.UserName, smtpSettings.Password, smtpSettings.Domain)
};

return client;
}

/// <summary>
///
/// </summary>
Expand Down
13 changes: 7 additions & 6 deletions shesha-core/src/Shesha.Application/Shesha.Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,13 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="DocumentFormat.OpenXml" Version="3.0.0" />
<PackageReference Include="Hangfire.Core" Version="1.8.6" />
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MediaTypeMap.Core" Version="2.3.3" />
<PackageReference Include="Hangfire.Core" Version="1.8.6" />
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MailKit" Version="4.7.0" />
<PackageReference Include="MediaTypeMap.Core" Version="2.3.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.13.2">
<PrivateAssets>all</PrivateAssets>
Expand Down