Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
5 changes: 5 additions & 0 deletions src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ internal const string
/// </summary>
public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host);

/// <summary>
/// Gets a value indicating whether SMTP expiry is configured.
/// </summary>
public bool IsSmtpExpiryConfigured => Smtp?.EmailExpiration != null && Smtp?.EmailExpiration.HasValue == true;

/// <summary>
/// Gets a value indicating whether there is a physical pickup directory configured.
/// </summary>
Expand Down
15 changes: 15 additions & 0 deletions src/Umbraco.Core/Configuration/Models/SecuritySettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public class SecuritySettings
internal const string StaticAuthorizeCallbackLogoutPathName = "/umbraco/logout";
internal const string StaticAuthorizeCallbackErrorPathName = "/umbraco/error";

internal const string StaticPasswordResetEmailExpiry = "01:00:00";
internal const string StaticUserInviteEmailExpiry = "72:00:00";

/// <summary>
/// Gets or sets a value indicating whether to keep the user logged in.
/// </summary>
Expand Down Expand Up @@ -159,4 +162,16 @@ public class SecuritySettings
/// </summary>
[DefaultValue(StaticAuthorizeCallbackErrorPathName)]
public string AuthorizeCallbackErrorPathName { get; set; } = StaticAuthorizeCallbackErrorPathName;

/// <summary>
/// Gets or sets the expiry time for password reset emails.
/// </summary>
[DefaultValue(StaticPasswordResetEmailExpiry)]
public TimeSpan PasswordResetEmailExpiry { get; set; } = TimeSpan.Parse(StaticPasswordResetEmailExpiry);

/// <summary>
/// Gets or sets the expiry time for user invite emails.
/// </summary>
[DefaultValue(StaticUserInviteEmailExpiry)]
public TimeSpan UserInviteEmailExpiry { get; set; } = TimeSpan.Parse(StaticUserInviteEmailExpiry);
}
5 changes: 5 additions & 0 deletions src/Umbraco.Core/Configuration/Models/SmtpSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,9 @@ public class SmtpSettings : ValidatableEntryBase
/// Gets or sets a value for the SMTP password.
/// </summary>
public string? Password { get; set; }

/// <summary>
/// Gets or sets a value for the time until an email expires.
/// </summary>
public TimeSpan? EmailExpiration { get; set; }
}
12 changes: 12 additions & 0 deletions src/Umbraco.Core/Mail/IEmailSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,21 @@ namespace Umbraco.Cms.Core.Mail;
/// </summary>
public interface IEmailSender
{
[Obsolete("Use the overload with expires parameter.")]
Task SendAsync(EmailMessage message, string emailType);

Task SendAsync(EmailMessage message, string emailType, TimeSpan? expires)
#pragma warning disable CS0618 // Type or member is obsolete
=> SendAsync(message, emailType);
#pragma warning restore CS0618 // Type or member is obsolete

[Obsolete("Use the overload with expires parameter.")]
Task SendAsync(EmailMessage message, string emailType, bool enableNotification);

Task SendAsync(EmailMessage message, string emailType, bool enableNotification, TimeSpan? expires)
#pragma warning disable CS0618 // Type or member is obsolete
=> SendAsync(message, emailType, enableNotification);
#pragma warning restore CS0618 // Type or member is obsolete

bool CanSendRequiredEmail();
}
8 changes: 8 additions & 0 deletions src/Umbraco.Core/Mail/NotImplementedEmailSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@ public Task SendAsync(EmailMessage message, string emailType)
=> throw new NotImplementedException(
"To send an Email ensure IEmailSender is implemented with a custom implementation");

public Task SendAsync(EmailMessage message, string emailType, TimeSpan? expires) =>
throw new NotImplementedException(
"To send an Email ensure IEmailSender is implemented with a custom implementation");

public Task SendAsync(EmailMessage message, string emailType, bool enableNotification) =>
throw new NotImplementedException(
"To send an Email ensure IEmailSender is implemented with a custom implementation");

public Task SendAsync(EmailMessage message, string emailType, bool enableNotification, TimeSpan? expires) =>
throw new NotImplementedException(
"To send an Email ensure IEmailSender is implemented with a custom implementation");

public bool CanSendRequiredEmail()
=> throw new NotImplementedException(
"To send an Email ensure IEmailSender is implemented with a custom implementation");
Expand Down
26 changes: 21 additions & 5 deletions src/Umbraco.Infrastructure/Mail/BasicSmtpEmailSenderClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,44 @@ namespace Umbraco.Cms.Infrastructure.Mail
public class BasicSmtpEmailSenderClient : IEmailSenderClient
{
private readonly GlobalSettings _globalSettings;

/// <inheritdoc />
public BasicSmtpEmailSenderClient(IOptionsMonitor<GlobalSettings> globalSettings)
{
_globalSettings = globalSettings.CurrentValue;
}
=> _globalSettings = globalSettings.CurrentValue;

/// <inheritdoc />
public async Task SendAsync(EmailMessage message)
=> await SendAsync(message, null);

/// <inheritdoc />
public async Task SendAsync(EmailMessage message, TimeSpan? expires)
{
using var client = new SmtpClient();

await client.ConnectAsync(
_globalSettings.Smtp!.Host,
_globalSettings.Smtp.Port,
_globalSettings.Smtp.Port,
(SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions);

if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) &&
!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password))
!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password))
{
await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password);
}

var mimeMessage = message.ToMimeMessage(_globalSettings.Smtp!.From);

if (_globalSettings.IsSmtpExpiryConfigured)
{
expires ??= _globalSettings.Smtp.EmailExpiration;
}

if (expires.HasValue)
{
// `Expires` header needs to be in RFC 1123/2822 compatible format
mimeMessage.Headers.Add("Expires", DateTimeOffset.UtcNow.Add(expires.GetValueOrDefault()).ToString("R"));
}

if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network)
{
await client.SendAsync(mimeMessage);
Expand Down
38 changes: 34 additions & 4 deletions src/Umbraco.Infrastructure/Mail/EmailSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,42 @@ public EmailSender(
/// <summary>
/// Sends the message async
/// </summary>
/// <param name="message"></param>
/// <param name="emailType"></param>
/// <returns></returns>
public async Task SendAsync(EmailMessage message, string emailType) =>
await SendAsyncInternal(message, emailType, false);
await SendAsyncInternal(message, emailType, false, null);

/// <summary>
/// Sends the message async
/// </summary>
/// <param name="message"></param>
/// <param name="emailType"></param>
/// <param name="expires"></param>
/// <returns></returns>
public async Task SendAsync(EmailMessage message, string emailType, TimeSpan? expires) =>
await SendAsyncInternal(message, emailType, false, expires);

/// <summary>
/// Sends the message async
/// </summary>
/// <param name="message"></param>
/// <param name="emailType"></param>
/// <param name="enableNotification"></param>
/// <returns></returns>
public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification) =>
await SendAsyncInternal(message, emailType, enableNotification);
await SendAsyncInternal(message, emailType, enableNotification, null);

/// <summary>
/// Sends the message async
/// </summary>
/// <param name="message"></param>
/// <param name="emailType"></param>
/// <param name="enableNotification"></param>
/// <param name="expires"></param>
/// <returns></returns>
public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification, TimeSpan? expires) =>
await SendAsyncInternal(message, emailType, enableNotification, expires);

/// <summary>
/// Returns true if the application should be able to send a required application email
Expand All @@ -93,7 +123,7 @@ public bool CanSendRequiredEmail() => _globalSettings.IsSmtpServerConfigured
|| _globalSettings.IsPickupDirectoryLocationConfigured
|| _notificationHandlerRegistered;

private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification)
private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification, TimeSpan? expires)
{
if (enableNotification)
{
Expand Down Expand Up @@ -173,7 +203,7 @@ private async Task SendAsyncInternal(EmailMessage message, string emailType, boo
while (true);
}

await _emailSenderClient.SendAsync(message);
await _emailSenderClient.SendAsync(message, expires);
}

}
12 changes: 10 additions & 2 deletions src/Umbraco.Infrastructure/Mail/Interfaces/IEmailSenderClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@
namespace Umbraco.Cms.Infrastructure.Mail.Interfaces
{
/// <summary>
/// Client for sending an email from a MimeMessage
/// Client for sending an email from a MimeMessage.
/// </summary>
public interface IEmailSenderClient
{
/// <summary>
/// Sends the email message
/// Sends the email message.
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public Task SendAsync(EmailMessage message);

/// <summary>
/// Sends the email message with an expiration date.
/// </summary>
/// <param name="message"></param>
/// <param name="expires"></param>
/// <returns></returns>
public Task SendAsync(EmailMessage message, TimeSpan? expires);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public async Task SendForgotPassword(UserForgotPasswordMessage messageModel)

var message = new EmailMessage(senderEmail, address.ToString(), emailSubject, emailBody, true);

await _emailSender.SendAsync(message, Constants.Web.EmailTypes.PasswordReset, true);
await _emailSender.SendAsync(message, Constants.Web.EmailTypes.PasswordReset, true, _securitySettings.PasswordResetEmailExpiry);
}

public bool CanSend() => _securitySettings.AllowPasswordReset && _emailSender.CanSendRequiredEmail();
Expand Down
10 changes: 7 additions & 3 deletions src/Umbraco.Infrastructure/Security/EmailUserInviteSender.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.Net;
using Microsoft.Extensions.Options;
using MimeKit;
Expand All @@ -18,15 +18,19 @@ public class EmailUserInviteSender : IUserInviteSender
private readonly IEmailSender _emailSender;
private readonly ILocalizedTextService _localizedTextService;
private readonly GlobalSettings _globalSettings;
private readonly SecuritySettings _securitySettings;

public EmailUserInviteSender(
IEmailSender emailSender,
ILocalizedTextService localizedTextService,
IOptions<GlobalSettings> globalSettings)
IOptions<GlobalSettings> globalSettings,
IOptions<SecuritySettings> securitySettings
)
{
_emailSender = emailSender;
_localizedTextService = localizedTextService;
_globalSettings = globalSettings.Value;
_securitySettings = securitySettings.Value;
}

public async Task InviteUser(UserInvitationMessage invite)
Expand Down Expand Up @@ -67,7 +71,7 @@ public async Task InviteUser(UserInvitationMessage invite)

var message = new EmailMessage(senderEmail, address.ToString(), emailSubject, emailBody, true);

await _emailSender.SendAsync(message, Constants.Web.EmailTypes.UserInvite, true);
await _emailSender.SendAsync(message, Constants.Web.EmailTypes.UserInvite, true, _securitySettings.UserInviteEmailExpiry);
}

public bool CanSendInvites() => _emailSender.CanSendRequiredEmail();
Expand Down
Loading