diff --git a/Crypter.Core/Exceptions/HangfireJobException.cs b/Crypter.Core/Exceptions/HangfireJobException.cs
new file mode 100644
index 000000000..11611d089
--- /dev/null
+++ b/Crypter.Core/Exceptions/HangfireJobException.cs
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 Crypter File Transfer
+ *
+ * This file is part of the Crypter file transfer project.
+ *
+ * Crypter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Crypter source code is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * You can be released from the requirements of the aforementioned license
+ * by purchasing a commercial license. Buying such a license is mandatory
+ * as soon as you develop commercial activities involving the Crypter source
+ * code without disclosing the source code of your own applications.
+ *
+ * Contact the current copyright holder to discuss commercial license options.
+ */
+
+using System;
+
+namespace Crypter.Core.Exceptions;
+
+public class HangfireJobException : Exception
+{
+ public HangfireJobException(string message) : base(message)
+ { }
+}
diff --git a/Crypter.Core/Features/UserEmailVerification/Commands/SendVerificationEmailCommand.cs b/Crypter.Core/Features/UserEmailVerification/Commands/SendVerificationEmailCommand.cs
new file mode 100644
index 000000000..e20f9c4c7
--- /dev/null
+++ b/Crypter.Core/Features/UserEmailVerification/Commands/SendVerificationEmailCommand.cs
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 Crypter File Transfer
+ *
+ * This file is part of the Crypter file transfer project.
+ *
+ * Crypter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Crypter source code is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * You can be released from the requirements of the aforementioned license
+ * by purchasing a commercial license. Buying such a license is mandatory
+ * as soon as you develop commercial activities involving the Crypter source
+ * code without disclosing the source code of your own applications.
+ *
+ * Contact the current copyright holder to discuss commercial license options.
+ */
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Crypter.Core.Models;
+using Crypter.Core.Services;
+using Crypter.Crypto.Common;
+using Crypter.DataAccess;
+using Crypter.DataAccess.Entities;
+using EasyMonads;
+using MediatR;
+
+namespace Crypter.Core.Features.UserEmailVerification.Commands;
+
+///
+/// A command that will attempt to send a verification email to the provided
+/// user.
+///
+/// Returns 'true' if the email was successfully sent or if the user's current
+/// state is such that they should not receive a verification email.
+///
+/// Returns 'false' if the email should be sent, but failed to send.
+///
+///
+public sealed record SendVerificationEmailCommand(Guid UserId) : IRequest;
+
+internal sealed class SendVerificationEmailCommandHandler
+ : IRequestHandler
+{
+ private readonly ICryptoProvider _cryptoProvider;
+ private readonly DataContext _dataContext;
+ private readonly IEmailService _emailService;
+
+ public SendVerificationEmailCommandHandler(
+ ICryptoProvider cryptoProvider,
+ DataContext dataContext,
+ IEmailService emailService)
+ {
+ _cryptoProvider = cryptoProvider;
+ _dataContext = dataContext;
+ _emailService = emailService;
+ }
+
+ public async Task Handle(SendVerificationEmailCommand request, CancellationToken cancellationToken)
+ {
+ return await UserEmailVerificationQueries
+ .GenerateVerificationParametersAsync(_dataContext, _cryptoProvider, request.UserId)
+ .BindAsync(async parameters =>
+ {
+ bool deliverySuccess = await _emailService.SendEmailVerificationAsync(parameters);
+ if (deliverySuccess)
+ {
+ UserEmailVerificationEntity newEntity = new UserEmailVerificationEntity(parameters.UserId,
+ parameters.VerificationCode, parameters.VerificationKey, DateTime.UtcNow);
+ _dataContext.UserEmailVerifications.Add(newEntity);
+ await _dataContext.SaveChangesAsync(CancellationToken.None);
+ }
+
+ return deliverySuccess;
+ }).SomeOrDefaultAsync(true);
+ }
+}
diff --git a/Crypter.Core/Features/UserEmailVerification/UserEmailVerificationCommands.cs b/Crypter.Core/Features/UserEmailVerification/UserEmailVerificationCommands.cs
index 981ae1236..4909c3dfc 100644
--- a/Crypter.Core/Features/UserEmailVerification/UserEmailVerificationCommands.cs
+++ b/Crypter.Core/Features/UserEmailVerification/UserEmailVerificationCommands.cs
@@ -26,7 +26,6 @@
using System;
using System.Threading.Tasks;
-using Crypter.Core.Models;
using Crypter.DataAccess;
using Crypter.DataAccess.Entities;
using Microsoft.EntityFrameworkCore;
@@ -35,15 +34,6 @@ namespace Crypter.Core.Features.UserEmailVerification;
internal static class UserEmailVerificationCommands
{
- internal static Task SaveVerificationParametersAsync(DataContext dataContext,
- UserEmailAddressVerificationParameters parameters)
- {
- UserEmailVerificationEntity newEntity = new UserEmailVerificationEntity(parameters.UserId,
- parameters.VerificationCode, parameters.VerificationKey, DateTime.UtcNow);
- dataContext.UserEmailVerifications.Add(newEntity);
- return dataContext.SaveChangesAsync();
- }
-
internal static async Task DeleteUserEmailVerificationEntity(DataContext dataContext, Guid userId, bool saveChanges)
{
UserEmailVerificationEntity foundEntity = await dataContext.UserEmailVerifications
diff --git a/Crypter.Core/Services/EmailService.cs b/Crypter.Core/Services/EmailService.cs
index dbbef6eea..e5a296d7d 100644
--- a/Crypter.Core/Services/EmailService.cs
+++ b/Crypter.Core/Services/EmailService.cs
@@ -34,6 +34,7 @@
using MailKit.Security;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MimeKit;
@@ -63,11 +64,13 @@ public static void AddEmailService(this IServiceCollection services, Action _settings;
+ private readonly ILogger _logger;
- public EmailService(IOptions emailSettings)
+ public EmailService(IOptions emailSettings, ILogger logger)
{
- Settings = emailSettings.Value;
+ _settings = emailSettings;
+ _logger = logger;
}
///
@@ -80,13 +83,13 @@ public EmailService(IOptions emailSettings)
///
public virtual async Task SendAsync(string subject, string message, EmailAddress recipient)
{
- if (!Settings.Enabled)
+ if (!_settings.Value.Enabled)
{
return true;
}
var mailMessage = new MimeMessage();
- mailMessage.From.Add(MailboxAddress.Parse(Settings.From));
+ mailMessage.From.Add(MailboxAddress.Parse(_settings.Value.From));
mailMessage.To.Add(MailboxAddress.Parse(recipient.Value));
mailMessage.Subject = subject;
mailMessage.Body = new TextPart("plain")
@@ -98,13 +101,13 @@ public virtual async Task SendAsync(string subject, string message, EmailA
try
{
- await smtpClient.ConnectAsync(Settings.Host, Settings.Port, SecureSocketOptions.StartTls);
- await smtpClient.AuthenticateAsync(Settings.Username, Settings.Password);
+ await smtpClient.ConnectAsync(_settings.Value.Host, _settings.Value.Port, SecureSocketOptions.StartTls);
+ await smtpClient.AuthenticateAsync(_settings.Value.Username, _settings.Value.Password);
await smtpClient.SendAsync(mailMessage);
}
catch (Exception ex)
{
- Console.WriteLine(ex.Message);
+ _logger.LogError("An error occured while sending an email: {error}", ex.Message);
return false;
}
finally
@@ -130,9 +133,7 @@ public async Task SendAccountRecoveryLinkAsync(UserRecoveryParameters para
///
/// Send a verification email to the provided recipient.
///
- ///
- ///
- ///
+ ///
/// This method is 'virtual' to enable some unit tests.
///
public virtual async Task SendEmailVerificationAsync(UserEmailAddressVerificationParameters parameters)
diff --git a/Crypter.Core/Services/HangfireBackgroundService.cs b/Crypter.Core/Services/HangfireBackgroundService.cs
index a13346cac..83e8e988e 100644
--- a/Crypter.Core/Services/HangfireBackgroundService.cs
+++ b/Crypter.Core/Services/HangfireBackgroundService.cs
@@ -30,11 +30,12 @@
using System.Threading.Tasks;
using Crypter.Common.Enums;
using Crypter.Common.Primitives;
+using Crypter.Core.Exceptions;
using Crypter.Core.Features.AccountRecovery;
using Crypter.Core.Features.Keys.Commands;
using Crypter.Core.Features.Transfer;
using Crypter.Core.Features.UserAuthentication;
-using Crypter.Core.Features.UserEmailVerification;
+using Crypter.Core.Features.UserEmailVerification.Commands;
using Crypter.Core.Features.UserToken;
using Crypter.Core.LinqExpressions;
using Crypter.Core.Repositories;
@@ -43,13 +44,16 @@
using Crypter.DataAccess.Entities;
using EasyMonads;
using Hangfire;
+using MediatR;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Unit = EasyMonads.Unit;
namespace Crypter.Core.Services;
public interface IHangfireBackgroundService
{
- Task SendEmailVerificationAsync(Guid userId);
+ Task SendEmailVerificationAsync(Guid userId);
Task SendTransferNotificationAsync(Guid itemId, TransferItemType itemType);
Task SendRecoveryEmailAsync(string emailAddress);
@@ -85,35 +89,45 @@ Task DeleteTransferAsync(Guid itemId, TransferItemType itemType, TransferUserTyp
public class HangfireBackgroundService : IHangfireBackgroundService
{
private readonly DataContext _dataContext;
+ private readonly ISender _sender;
private readonly IBackgroundJobClient _backgroundJobClient;
private readonly ICryptoProvider _cryptoProvider;
private readonly IEmailService _emailService;
private readonly ITransferRepository _transferRepository;
+ private readonly ILogger _logger;
- private const int _accountRecoveryEmailExpirationMinutes = 30;
+ private const int AccountRecoveryEmailExpirationMinutes = 30;
public HangfireBackgroundService(
- DataContext dataContext, IBackgroundJobClient backgroundJobClient, ICryptoProvider cryptoProvider,
- IEmailService emailService, ITransferRepository transferRepository)
+ DataContext dataContext,
+ ISender sender,
+ IBackgroundJobClient backgroundJobClient,
+ ICryptoProvider cryptoProvider,
+ IEmailService emailService,
+ ITransferRepository transferRepository,
+ ILogger logger)
{
_dataContext = dataContext;
+ _sender = sender;
_backgroundJobClient = backgroundJobClient;
_cryptoProvider = cryptoProvider;
_emailService = emailService;
_transferRepository = transferRepository;
+ _logger = logger;
}
- public Task SendEmailVerificationAsync(Guid userId)
+ public async Task SendEmailVerificationAsync(Guid userId)
{
- return UserEmailVerificationQueries.GenerateVerificationParametersAsync(_dataContext, _cryptoProvider, userId)
- .IfSomeAsync(async x =>
- {
- bool deliverySuccess = await _emailService.SendEmailVerificationAsync(x);
- if (deliverySuccess)
- {
- await UserEmailVerificationCommands.SaveVerificationParametersAsync(_dataContext, x);
- }
- });
+ var request = new SendVerificationEmailCommand(userId);
+ var result = await _sender.Send(request);
+
+ if (!result)
+ {
+ _logger.LogError("Failed to send verification email for user: {userId}", userId);
+ throw new HangfireJobException($"{nameof(SendEmailVerificationAsync)} failed");
+ }
+
+ return Unit.Default;
}
public async Task SendTransferNotificationAsync(Guid itemId, TransferItemType itemType)
@@ -145,13 +159,13 @@ public Task SendRecoveryEmailAsync(string emailAddress)
{
bool deliverySuccess =
await _emailService.SendAccountRecoveryLinkAsync(parameters,
- _accountRecoveryEmailExpirationMinutes);
+ AccountRecoveryEmailExpirationMinutes);
if (deliverySuccess)
{
DateTime utcNow = DateTime.UtcNow;
await UserRecoveryCommands.SaveRecoveryParametersAsync(_dataContext, parameters, utcNow);
- DateTime recoveryExpiration = utcNow.AddMinutes(_accountRecoveryEmailExpirationMinutes);
+ DateTime recoveryExpiration = utcNow.AddMinutes(AccountRecoveryEmailExpirationMinutes);
_backgroundJobClient.Schedule(() => DeleteRecoveryParametersAsync(parameters.UserId),
recoveryExpiration);
}
@@ -180,9 +194,10 @@ public Task DeleteRecoveryParametersAsync(Guid userId)
return UserRecoveryCommands.DeleteRecoveryParametersAsync(_dataContext, userId);
}
- public Task DeleteUserKeysAsync(Guid userId)
+ public async Task DeleteUserKeysAsync(Guid userId)
{
- return DeleteUserKeysCommandHandler.HandleAsync(_dataContext, userId);
+ var request = new DeleteUserKeysCommand(userId);
+ return await _sender.Send(request);
}
public async Task DeleteReceivedTransfersAsync(Guid userId)
diff --git a/Crypter.Test/Core_Tests/Services_Tests/EmailService_Tests.cs b/Crypter.Test/Core_Tests/Services_Tests/EmailService_Tests.cs
index 319815c46..fc9e18c24 100644
--- a/Crypter.Test/Core_Tests/Services_Tests/EmailService_Tests.cs
+++ b/Crypter.Test/Core_Tests/Services_Tests/EmailService_Tests.cs
@@ -28,6 +28,8 @@
using Crypter.Common.Primitives;
using Crypter.Core.Services;
using Crypter.Core.Settings;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NUnit.Framework;
@@ -37,18 +39,25 @@ namespace Crypter.Test.Core_Tests.Services_Tests;
public class EmailService_Tests
{
private IOptions _defaultEmailSettings;
+ private ILogger _logger;
- [OneTimeSetUp]
- public void OneTimeSetup()
+ [SetUp]
+ public void Setup()
{
EmailSettings settings = new EmailSettings();
_defaultEmailSettings = Options.Create(settings);
+
+ var serviceProvider = new ServiceCollection()
+ .AddLogging()
+ .BuildServiceProvider();
+ var factory = serviceProvider.GetService();
+ _logger = factory.CreateLogger();
}
[Test]
public async Task ServiceDisabled_SendAsync_ReturnsTrue()
{
- var sut = new EmailService(_defaultEmailSettings);
+ var sut = new EmailService(_defaultEmailSettings, _logger);
var emailAddress = EmailAddress.From("jack@crypter.dev");
var result = await sut.SendAsync("foo", "bar", emailAddress);
Assert.IsTrue(result);
diff --git a/Crypter.Test/Core_Tests/Services_Tests/HangfireBackgroundService_Tests.cs b/Crypter.Test/Core_Tests/Services_Tests/HangfireBackgroundService_Tests.cs
index 05fa99662..b2c50ed53 100644
--- a/Crypter.Test/Core_Tests/Services_Tests/HangfireBackgroundService_Tests.cs
+++ b/Crypter.Test/Core_Tests/Services_Tests/HangfireBackgroundService_Tests.cs
@@ -34,8 +34,10 @@
using Crypter.Crypto.Providers.Default;
using Crypter.DataAccess;
using Hangfire;
+using MediatR;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
@@ -47,8 +49,10 @@ public class HangfireBackgroundService_Tests
private WebApplicationFactory _factory;
private IServiceScope _scope;
private DataContext _dataContext;
-
+ private ISender _sender;
private ICryptoProvider _cryptoProvider;
+ private ILogger _logger;
+
private Mock _backgroundJobClientMock;
private Mock _emailServiceMock;
private Mock _transferStorageMock;
@@ -66,6 +70,10 @@ public async Task SetupTestAsync()
_scope = _factory.Services.CreateScope();
_dataContext = _scope.ServiceProvider.GetRequiredService();
+ _sender = _scope.ServiceProvider.GetRequiredService();
+
+ var factory = _scope.ServiceProvider.GetRequiredService();
+ _logger = factory.CreateLogger();
}
[TearDown]
@@ -84,8 +92,8 @@ public async Task Verification_Email_Not_Sent_Without_Verification_Parameters()
It.IsAny()))
.ReturnsAsync((UserEmailAddressVerificationParameters parameters) => true);
- HangfireBackgroundService sut = new HangfireBackgroundService(_dataContext, _backgroundJobClientMock.Object,
- _cryptoProvider, _emailServiceMock.Object, _transferStorageMock.Object);
+ HangfireBackgroundService sut = new HangfireBackgroundService(_dataContext, _sender, _backgroundJobClientMock.Object,
+ _cryptoProvider, _emailServiceMock.Object, _transferStorageMock.Object, _logger);
await sut.SendEmailVerificationAsync(Guid.NewGuid());
_emailServiceMock.Verify(x => x.SendEmailVerificationAsync(It.IsAny()),
@@ -101,8 +109,8 @@ public async Task Recovery_Email_Not_Sent_Without_Recovery_Parameters()
It.IsAny()))
.ReturnsAsync((UserRecoveryParameters parameters, int expirationMinutes) => true);
- HangfireBackgroundService sut = new HangfireBackgroundService(_dataContext, _backgroundJobClientMock.Object,
- _cryptoProvider, _emailServiceMock.Object, _transferStorageMock.Object);
+ HangfireBackgroundService sut = new HangfireBackgroundService(_dataContext, _sender, _backgroundJobClientMock.Object,
+ _cryptoProvider, _emailServiceMock.Object, _transferStorageMock.Object, _logger);
await sut.SendRecoveryEmailAsync("foo@test.com");
_emailServiceMock.Verify(