diff --git a/.github/workflows/dotnet-e2e-tests.yml b/.github/workflows/dotnet-e2e-tests.yml
new file mode 100644
index 000000000..0bf12ce37
--- /dev/null
+++ b/.github/workflows/dotnet-e2e-tests.yml
@@ -0,0 +1,29 @@
+# This workflow will build a .NET project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
+
+name: .NET E2E Tests (Playwright)
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ test:
+ timeout-minutes: 60
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+ - name: Install dependencies
+ run: dotnet workload install wasm-tools
+ - name: Build
+ run: dotnet build
+ - name: Ensure browsers are installed
+ run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net8.0/playwright.ps1 install --with-deps
+ - name: Run E2E tests
+ run: dotnet test src/Tests/AliasVault.E2ETests --no-build --verbosity normal
diff --git a/.github/workflows/dotnet-integration-tests.yml b/.github/workflows/dotnet-integration-tests.yml
index 221052ab5..31330a2e3 100644
--- a/.github/workflows/dotnet-integration-tests.yml
+++ b/.github/workflows/dotnet-integration-tests.yml
@@ -1,7 +1,7 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
-name: Playwright integration tests
+name: .NET Integration Tests
on:
push:
@@ -23,7 +23,5 @@ jobs:
run: dotnet workload install wasm-tools
- name: Build
run: dotnet build
- - name: Ensure browsers are installed
- run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net8.0/playwright.ps1 install --with-deps
- - name: Run your tests
- run: dotnet test src/Tests/AliasVault.E2ETests --no-build --verbosity normal
+ - name: Run integration tests
+ run: dotnet test src/Tests/AliasVault.IntegrationTests --no-build --verbosity normal
diff --git a/.github/workflows/dotnet-build-run-tests.yml b/.github/workflows/dotnet-unit-tests.yml
similarity index 92%
rename from .github/workflows/dotnet-build-run-tests.yml
rename to .github/workflows/dotnet-unit-tests.yml
index 79f2a4f6e..75314608b 100644
--- a/.github/workflows/dotnet-build-run-tests.yml
+++ b/.github/workflows/dotnet-unit-tests.yml
@@ -1,7 +1,7 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
-name: .NET build and run tests
+name: .NET Unit Tests
on:
push:
@@ -10,10 +10,8 @@ on:
branches: [ "main" ]
jobs:
- build:
-
+ test:
runs-on: ubuntu-latest
-
steps:
- uses: actions/checkout@v4
- name: Setup .NET
@@ -26,5 +24,5 @@ jobs:
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- - name: Test
+ - name: Run unittests
run: dotnet test src/Tests/AliasVault.UnitTests --no-build --verbosity normal
diff --git a/src/Services/AliasVault.SmtpService/AllowedDomainsFilter.cs b/src/Services/AliasVault.SmtpService/AllowedDomainsFilter.cs
deleted file mode 100644
index 50a55b854..000000000
--- a/src/Services/AliasVault.SmtpService/AllowedDomainsFilter.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-//-----------------------------------------------------------------------
-//
-// Copyright (c) lanedirt. All rights reserved.
-// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
-//
-//-----------------------------------------------------------------------
-
-namespace AliasVault.SmtpService;
-
-using SmtpServer;
-using SmtpServer.Mail;
-using SmtpServer.Storage;
-
-///
-/// Filter to allow only emails from configured domains.
-///
-public class AllowedDomainsFilter(Config config, ILogger logger) : IMailboxFilter, IMailboxFilterFactory
-{
- private readonly TimeSpan _delay = TimeSpan.Zero;
-
- public async Task CanAcceptFromAsync(ISessionContext context, IMailbox from, int size, CancellationToken cancellationToken)
- {
- await Task.Delay(_delay, cancellationToken);
- return true;
- }
-
- public async Task CanDeliverToAsync(ISessionContext context, IMailbox to, IMailbox from, CancellationToken cancellationToken)
- {
- await Task.Delay(_delay, cancellationToken);
-
- if (!config.AllowedToDomains.Contains(to.Host.ToLowerInvariant()))
- {
- // ToAddress host is not allowed, return error to sender.
- logger.LogWarning("Email to {ToAddress} is not allowed", to);
- return false;
- }
-
- return true;
- }
-
- public IMailboxFilter CreateInstance(ISessionContext context)
- {
- return new AllowedDomainsFilter(context.ServiceProvider.GetRequiredService(), context.ServiceProvider.GetRequiredService>());
- }
-}
diff --git a/src/Services/AliasVault.SmtpService/DatabaseMessageStore.cs b/src/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs
similarity index 94%
rename from src/Services/AliasVault.SmtpService/DatabaseMessageStore.cs
rename to src/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs
index 6bbb12a93..3839cb375 100644
--- a/src/Services/AliasVault.SmtpService/DatabaseMessageStore.cs
+++ b/src/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs
@@ -5,6 +5,8 @@
//
//-----------------------------------------------------------------------
+namespace AliasVault.SmtpService.Handlers;
+
using System.Buffers;
using System.Net.Mail;
using System.Text.RegularExpressions;
@@ -16,8 +18,6 @@ using SmtpServer;
using SmtpServer.Protocol;
using SmtpServer.Storage;
-namespace AliasVault.SmtpService;
-
///
/// Custom exception for when the email parsing fails to find the "to" address in the email.
///
@@ -58,7 +58,7 @@ public class DatabaseMessageStore(ILogger logger, Config c
}
stream.Position = 0;
- var message = await MimeKit.MimeMessage.LoadAsync(stream, cancellationToken);
+ var message = await MimeMessage.LoadAsync(stream, cancellationToken);
// Retrieve all addresses from the SMTP transaction which should contain all recipients for this mail instance.
var allAddresses = transaction.To
.Distinct()
@@ -76,19 +76,18 @@ public class DatabaseMessageStore(ILogger logger, Config c
}
if (!config.AllowedToDomains.Contains(toAddress.Host.ToLowerInvariant()))
{
- // ToAddress domain is not allowed, return error to sender.
+ // ToAddress domain is not allowed.
+ if (toAddresses.Count > 1)
+ {
+ // If more recipients, silently skip this one.
+ continue;
+ }
+
+ // If only one recipient, return error.
logger.LogWarning("Email to {ToAddress} is not allowed", toAddress.User + "@" + toAddress.Host);
return SmtpResponse.NoValidRecipientsGiven;
}
- // Remove existing x-receiver and x-sender headers to avoid duplication.
- message.Headers.RemoveAll("x-receiver");
- message.Headers.RemoveAll("x-sender");
-
- // Add new x-receiver and x-sender headers.
- message.Headers.Add("x-receiver", toAddress.User + "@" + toAddress.Host);
- message.Headers.Add("x-sender", transaction.From.User + "@" + transaction.From.Host);
-
var insertedId = await InsertEmailIntoDatabase(message);
logger.LogInformation("Email saved into database with ID {insertedId}.", insertedId);
}
@@ -115,8 +114,8 @@ public class DatabaseMessageStore(ILogger logger, Config c
///
/// Convert MimeMessage to Email database object.
///
- ///
- ///
+ /// MimeMessage object.
+ /// Email object.
///
private static Email ConvertMimeMessageToEmail(MimeMessage message)
{
diff --git a/src/Services/AliasVault.SmtpService/Program.cs b/src/Services/AliasVault.SmtpService/Program.cs
index 1198f726f..4aea2c4ab 100644
--- a/src/Services/AliasVault.SmtpService/Program.cs
+++ b/src/Services/AliasVault.SmtpService/Program.cs
@@ -6,12 +6,13 @@
//-----------------------------------------------------------------------
using System.Data.Common;
-using AliasVault.SmtpService;
-using SmtpServer;
using System.Security.Cryptography.X509Certificates;
using AliasServerDb;
+using AliasVault.SmtpService;
+using AliasVault.SmtpService.Handlers;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
+using SmtpServer;
using SmtpServer.Storage;
var builder = Host.CreateApplicationBuilder(args);
@@ -47,7 +48,6 @@ builder.Services.AddDbContextFactory((container, options)
});
builder.Services.AddTransient();
-builder.Services.AddTransient();
builder.Services.AddSingleton(
provider =>
diff --git a/src/Tests/AliasVault.IntegrationTests/SmtpServer/SmtpServerTests.cs b/src/Tests/AliasVault.IntegrationTests/SmtpServer/SmtpServerTests.cs
index 1e343a709..1113d75e7 100644
--- a/src/Tests/AliasVault.IntegrationTests/SmtpServer/SmtpServerTests.cs
+++ b/src/Tests/AliasVault.IntegrationTests/SmtpServer/SmtpServerTests.cs
@@ -19,10 +19,19 @@ using MimeKit;
[TestFixture]
public class SmtpServerTests
{
- private IHost _testHost;
+ ///
+ /// The test host instance.
+ ///
+ private IHost _testHost = null!;
- private TestHostBuilder _testHostBuilder;
+ ///
+ /// The test host builder instance.
+ ///
+ private TestHostBuilder _testHostBuilder = null!;
+ ///
+ /// Setup logic for every test.
+ ///
[SetUp]
public async Task Setup()
{
@@ -32,6 +41,9 @@ public class SmtpServerTests
await _testHost.StartAsync();
}
+ ///
+ /// Tear down logic for every test.
+ ///
[TearDown]
public async Task TearDown()
{
@@ -42,46 +54,138 @@ public class SmtpServerTests
}
}
+ ///
+ /// Tests sending a single email in plain format to the SMTP server to check if it is processed correctly.
+ ///
[Test]
- public async Task TestWorkerProcessesEmails()
+ public async Task SingleEmailPlain()
+ {
+ // Send an email to the SMTP server.
+ var message = new MimeMessage();
+ message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
+ message.To.Add(new MailboxAddress("Test Recipient", "recipient@example.tld"));
+ message.Subject = "Test Email";
+ const string textBody = "This is a test email plain.";
+ message.Body = new BodyBuilder { TextBody = textBody}.ToMessageBody();
+ await SendMessageToSmtpServer(message);
+
+ // Check if the email is in the database.
+ var processedEmail = await _testHostBuilder.GetDbContext().Emails.FirstAsync();
+ Assert.That(processedEmail, Is.Not.Null);
+ Assert.That(processedEmail.From, Is.EqualTo("\"Test Sender\" "));
+ Assert.That(processedEmail.FromLocal, Is.EqualTo("sender"));
+ Assert.That(processedEmail.FromDomain, Is.EqualTo("example.com"));
+ Assert.That(processedEmail.To, Is.EqualTo("\"Test Recipient\" "));
+ Assert.That(processedEmail.MessagePreview, Is.EqualTo("This is a test email plain."));
+ Assert.That(processedEmail.MessagePlain, Is.EqualTo("This is a test email plain."));
+ Assert.That(processedEmail.MessageHtml, Is.Null);
+ }
+
+ ///
+ /// Tests sending a single email in html format to the SMTP server to check if it is processed correctly.
+ ///
+ [Test]
+ public async Task SingleEmailHtml()
{
// Arrange
- // Simulate sending an email to your SMTP server
- // You might need to implement a method to do this in your test SMTP server
+ var message = new MimeMessage();
+ message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
+ message.To.Add(new MailboxAddress("Test Recipient", "recipient@example.tld"));
+ message.Subject = "Test Email with HTML body.";
+ const string htmlBody = "This is a test email html.
";
+ message.Body = new BodyBuilder { HtmlBody = htmlBody }.ToMessageBody();
+ await SendMessageToSmtpServer(message);
+
+ // Check if the email is in the database.
+ var processedEmail = await _testHostBuilder.GetDbContext().Emails.FirstAsync();
+ Assert.That(processedEmail, Is.Not.Null);
+ Assert.That(processedEmail.To, Is.EqualTo("\"Test Recipient\" "));
+ Assert.That(processedEmail.MessagePreview, Is.EqualTo("This is a test email html."));
+ Assert.That(processedEmail.MessagePlain, Is.Null);
+ Assert.That(processedEmail.MessageHtml, Is.EqualTo(htmlBody));
+ }
+
+ ///
+ /// Tests sending a single email in multipart format to the SMTP server to check if it is processed correctly.
+ ///
+ [Test]
+ public async Task SingleEmailMultipart()
+ {
// Arrange
var message = new MimeMessage();
message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
+ message.To.Add(new MailboxAddress("Test Recipient", "recipient@example.tld"));
+ message.Subject = "Test Email with multipart body.";
+ const string textBody = "This is a test email multipart.";
+ const string htmlBody = "This is a test email multipart.
";
+ message.Body = new BodyBuilder { TextBody = textBody, HtmlBody = htmlBody}.ToMessageBody();
+ await SendMessageToSmtpServer(message);
+
+ // Check if the email is in the database.
+ var processedEmail = await _testHostBuilder.GetDbContext().Emails.FirstAsync();
+ Assert.That(processedEmail, Is.Not.Null);
+ Assert.That(processedEmail.To, Is.EqualTo("\"Test Recipient\" "));
+ Assert.That(processedEmail.MessagePreview, Is.EqualTo("This is a test email multipart."));
+ Assert.That(processedEmail.MessagePlain, Is.EqualTo("This is a test email multipart."));
+ Assert.That(processedEmail.MessageHtml, Is.EqualTo(htmlBody));
+ }
+
+ ///
+ /// Tests sending a single email in plain format to the SMTP server to check if it is processed correctly.
+ ///
+ [Test]
+ public async Task MultipleRecipientsEmail()
+ {
+ // Send an email to the SMTP server.
+ var message = new MimeMessage();
+ message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
message.To.Add(new MailboxAddress("Test Recipient", "recipient.to@example.tld"));
message.Cc.Add(new MailboxAddress("Test Recipient 2", "recipient.cc@example.tld"));
- message.Subject = "Test Email";
- message.Body = new TextPart("plain")
- {
- Text = "This is a test email."
- };
+ message.Cc.Add(new MailboxAddress("Test Recipient 3 unknown domain", "recipient@unknowndomain.tld"));
+ message.Subject = "Test Email";
+ const string textBody = "This is a test email plain.";
+ message.Body = new BodyBuilder { TextBody = textBody}.ToMessageBody();
+ await SendMessageToSmtpServer(message);
+
+ // Check that two emails are in the database, one for each allowed recipient.
+ Assert.That(await _testHostBuilder.GetDbContext().Emails.CountAsync(), Is.EqualTo(2));
+ }
+
+ ///
+ /// Tests sending a single email in plain format to the SMTP server to check if it is processed correctly.
+ ///
+ [Test]
+ public void SingleEmailUnknownRecipient()
+ {
+ // Send an email to the SMTP server.
+ var message = new MimeMessage();
+ message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
+ message.To.Add(new MailboxAddress("Test Recipient", "recipient@unknowndomain.tld"));
+ message.Subject = "Test Email";
+ const string textBody = "This is a test email plain.";
+ message.Body = new BodyBuilder { TextBody = textBody}.ToMessageBody();
+
+ // Expect error from SmtpClient when sending email to unknown domain.
+ Assert.ThrowsAsync(async () => await SendMessageToSmtpServer(message));
+ }
+
+ ///
+ /// Sends a message to the SMTP server.
+ ///
+ ///
+ private async Task SendMessageToSmtpServer(MimeMessage message)
+ {
using var client = new SmtpClient();
- // Send message to SMTP server
await client.ConnectAsync("localhost", 25, SecureSocketOptions.None);
try
{
await client.SendAsync(message);
}
- catch (Exception ex)
- {
- throw;
- // Show failure message indicating that message was not sent: SMTP server issue?
- Assert.Fail($"Failed to send email, check SMTP server receive logs: {ex}");
- }
finally
{
await client.DisconnectAsync(true);
}
-
- var dbContext = _testHostBuilder.GetDbContext();
-
- var processedEmail = await dbContext.Emails.FirstOrDefaultAsync(e => e.Subject == "Test Email");
- Assert.That(processedEmail, Is.Not.Null);
- Assert.That(processedEmail.To, Is.EqualTo("recipient.to@example.tld"));
}
}
diff --git a/src/Tests/AliasVault.IntegrationTests/SmtpServer/TestHostBuilder.cs b/src/Tests/AliasVault.IntegrationTests/SmtpServer/TestHostBuilder.cs
index e5a97c353..97ca5fcae 100644
--- a/src/Tests/AliasVault.IntegrationTests/SmtpServer/TestHostBuilder.cs
+++ b/src/Tests/AliasVault.IntegrationTests/SmtpServer/TestHostBuilder.cs
@@ -5,11 +5,13 @@
//
// -----------------------------------------------------------------------
+using AliasVault.SmtpService.Handlers;
+
namespace AliasVault.IntegrationTests.SmtpServer;
using System.Data.Common;
using AliasServerDb;
-using AliasVault.SmtpService;
+using SmtpService;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Data.Sqlite;
@@ -75,8 +77,6 @@ public class TestHostBuilder
});
services.AddTransient();
- services.AddTransient();
-
services.AddSingleton(
provider =>
{