Files
aliasvault/apps/server/Tests/AliasVault.IntegrationTests/SmtpServer/SmtpServerTests.cs
2025-09-03 14:59:14 +02:00

352 lines
16 KiB
C#

//-----------------------------------------------------------------------
// <copyright file="SmtpServerTests.cs" company="aliasvault">
// Copyright (c) aliasvault. All rights reserved.
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.IntegrationTests.SmtpServer;
using System.Text;
using AliasServerDb;
using AliasVault.Cryptography.Server;
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using MimeKit;
/// <summary>
/// SmtpServerTests class.
/// </summary>
[TestFixture]
public class SmtpServerTests
{
/// <summary>
/// Example public key for RSA encryption tests. This is a public key generated by the JSInterop on the client.
/// We use this here to also test the server-side decryption implementation, even though this isn't a real-world scenario.
/// </summary>
public const string PublicKey = "{\"alg\":\"RSA-OAEP-256\",\"e\":\"AQAB\",\"ext\":true,\"key_ops\":[\"encrypt\"],\"kty\":\"RSA\",\"n\":\"lW8fRfSvLQiK9uZgm_kFjHMY1SedAZlVvZ_8d_d5oqWezQhan8-Y10Qvx0NMe57sQB3ePnShJFNE33w83kgRNkOyxKJ2FOVKtRptd7CgwIt_l9TPjdrB0J0hFn9b1eit2vpQlOdP_Wa8WvW2eVdXYEMWuBU4-aj8vY2qzcmBc-HhJX-Me9oXhUscJxeqMP4_sNiN7D4I0enrmYicB3JQMhUIwMmNt-0srHTdSvHh_6vFZMqB9ohfh2D9Q0BzYcI8rGEy1RTYsmF1zYyoOOzeRGOcKCVNeLO9LZxfAdm1Eq5zv47uw543cxCZXIZPlXOVriMEtTRwaGzE_3RZmpGJqw\"}";
/// <summary>
/// Example private key for RSA encryption tests. This is a private key generated by the JSInterop on the client.
/// We use this here to also test the server-side decryption implementation, even though this isn't a real-world scenario.
/// </summary>
public const string PrivateKey = "{\"alg\":\"RSA-OAEP-256\",\"d\":\"KLByToUaseNym1oNkkrTRPQOHfREXywWWaTXhP8AwtXgEKomqv9G-c6aR-K-T6btY2P-oPj268I0rbnRhSEQdrsmUT5_cp8goYGJrx6MFwGlA32x6klXnus6GDsjkXJi7I5eJL17XV99CDOBtTagFxkNdaBpvClUcHTDvncQ5bGAIrNqS7KADoi-E19BxiW_GcSJiVT4H8kDHCkcgTjZx4rKJjTPqqJOLg_poDrvnTJbsjcXP80kQ1AAENRAvDGhSWzP0IYtP1DM_2FzM1s1b_SrUsS3KiO8drR2Kv-PSOvncpaNVnZGElGCraJ3B2Mm-dr3vFjkyWeWPceqyhtYoQ\",\"dp\":\"ttxRg6uB2YLWfkPKUkzAaBWniZDHM4silJX3IgexA5GJBd9GIhUiVEolc_MgmieQbZ10CC65wqcHVv82lgCeqxYHxHWLxxJCrOpvkFlYE8wr_WqOPQEzYKv3KsL6s6Fj7Pbv9WehWpXdlbJUm4Cy5cgUkdH6PXiwBSvfhCQGrYk\",\"dq\":\"YFqlDAVTfvTR2bMJulvWzd_at81CsEmR-lPo91h-3cLpxcLDOlrTP-d3Ass2I4r1PtBT1bKuuHeQ6fZmHH55a6m8XxPEs2BuIxlh9RiFfWbd66969UOnItuawf0rfGneKt1zl4st60T3KXd8-ECrLxdsvOYpOEuNzvIY_b3qitE\",\"e\":\"AQAB\",\"ext\":true,\"key_ops\":[\"decrypt\"],\"kty\":\"RSA\",\"n\":\"lW8fRfSvLQiK9uZgm_kFjHMY1SedAZlVvZ_8d_d5oqWezQhan8-Y10Qvx0NMe57sQB3ePnShJFNE33w83kgRNkOyxKJ2FOVKtRptd7CgwIt_l9TPjdrB0J0hFn9b1eit2vpQlOdP_Wa8WvW2eVdXYEMWuBU4-aj8vY2qzcmBc-HhJX-Me9oXhUscJxeqMP4_sNiN7D4I0enrmYicB3JQMhUIwMmNt-0srHTdSvHh_6vFZMqB9ohfh2D9Q0BzYcI8rGEy1RTYsmF1zYyoOOzeRGOcKCVNeLO9LZxfAdm1Eq5zv47uw543cxCZXIZPlXOVriMEtTRwaGzE_3RZmpGJqw\",\"p\":\"yUdbuDwmVwKhou5xXUxJfi1eOjN-5F88wtyR4LpgU2OvZ7m-er4hpXx5I2E-KTVX_iIp0Q9VDXhHH-WkN3qg20RXjRoxwgrggYbfdIYdrB-2kbMamq5cOf2XbXGEO8PoDXYoZprIB0EhrD4qVVykPUYg5El0hIKPdfs9LNoOEzs\",\"q\":\"vg93lGTurG0EY179tPr6Qe3ttKEN9zvQ97dZ9034DOWDoWLe-iMKG1-yKmkG4uwC8QqNnm1mPz7EqOuHPPGVTTib9NA4JdM27PUHSPKDUvp0cV4LhF6e-W7tMFk8WbJ2ACqkqhZHYgm-FDkZBCpnehNegTxipLluKa79G__ZHFE\",\"qi\":\"fnI3Wh5aYuxI0R18NTeFKjo1P7_Ck65Gc9O3CmeqiIe58EJaXQEcdwdSOG8aVmn03szXLHEnp7anNIH63f0ericbRYdCQVhcQpvsXzEM_sp4aYmwz45palrjlY4Jc6G6XQn3FwiqqRDvpnXdsunnQ62HHhxmslaEMYHQyLng2ss\"}";
/// <summary>
/// The test host instance.
/// </summary>
private IHost _testHost;
/// <summary>
/// The test host builder instance.
/// </summary>
private TestHostBuilder _testHostBuilder;
/// <summary>
/// Setup logic for every test.
/// </summary>
/// <returns>Task.</returns>
[SetUp]
public async Task Setup()
{
_testHostBuilder = new TestHostBuilder();
_testHost = _testHostBuilder.Build();
await _testHost.StartAsync();
// Create an AliasVault user, public key and an email claim.
var dbContext = _testHostBuilder.GetDbContext();
var user = new AliasVaultUser
{
UserName = "testuser",
Email = "testuser@example.tld",
};
dbContext.AliasVaultUsers.Add(user);
await dbContext.SaveChangesAsync();
// Create email claims.
var emailClaim = new UserEmailClaim
{
UserId = user.Id,
Address = "claimed@example.tld",
AddressLocal = "claimed",
AddressDomain = "example.tld",
};
dbContext.UserEmailClaims.Add(emailClaim);
var emailClaim2 = new UserEmailClaim
{
UserId = user.Id,
Address = "claimed.cc@example.tld",
AddressLocal = "claimed.cc",
AddressDomain = "example.tld",
};
dbContext.UserEmailClaims.Add(emailClaim2);
// Create disabled email claim.
var emailClaimDisabled = new UserEmailClaim
{
UserId = user.Id,
Address = "disabled@example.tld",
AddressLocal = "disabled",
AddressDomain = "example.tld",
Disabled = true,
};
dbContext.UserEmailClaims.Add(emailClaimDisabled);
// Create public key.
var encryptionKey = new UserEncryptionKey
{
UserId = user.Id,
PublicKey = PublicKey,
IsPrimary = true,
};
dbContext.UserEncryptionKeys.Add(encryptionKey);
await dbContext.SaveChangesAsync();
}
/// <summary>
/// Tear down logic for every test.
/// </summary>
/// <returns>Task.</returns>
[TearDown]
public async Task TearDown()
{
await _testHost.StopAsync();
_testHost.Dispose();
await _testHostBuilder.DisposeAsync();
}
/// <summary>
/// Tests sending a single email in plain format to the SMTP server with valid claim to check if it is processed correctly.
/// </summary>
/// <returns>Task.</returns>
[Test]
public async Task SingleEmailPlain()
{
// Email the SMTP server.
var message = new MimeMessage();
message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
message.To.Add(new MailboxAddress("Test Recipient", "claimed@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();
// Test non-encrypted field.
Assert.That(processedEmail.To, Is.EqualTo("claimed@example.tld"));
// Decrypt the email and then check all individual fields.
processedEmail = EmailEncryption.DecryptEmail(processedEmail, PrivateKey);
Assert.Multiple(() =>
{
Assert.That(processedEmail, Is.Not.Null);
Assert.That(processedEmail.From, Is.EqualTo("\"Test Sender\" <sender@example.com>"));
Assert.That(processedEmail.FromLocal, Is.EqualTo("sender"));
Assert.That(processedEmail.FromDomain, Is.EqualTo("example.com"));
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);
});
}
/// <summary>
/// Tests sending a single email in html format to the SMTP server to check if it is processed correctly.
/// </summary>
/// <returns>Task.</returns>
[Test]
public async Task SingleEmailHtml()
{
// Arrange
var message = new MimeMessage();
message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
message.To.Add(new MailboxAddress("Test Recipient", "claimed@example.tld"));
message.Subject = "Test Email with HTML body.";
const string htmlBody = "<html><body><h1>This is a test email html.</h1></body></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();
// Test non-encrypted field.
Assert.That(processedEmail.To, Is.EqualTo("claimed@example.tld"));
// Decrypt the email and then check all individual fields.
processedEmail = EmailEncryption.DecryptEmail(processedEmail, PrivateKey);
Assert.Multiple(() =>
{
Assert.That(processedEmail, Is.Not.Null);
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));
});
}
/// <summary>
/// Tests sending a single email in multipart format to the SMTP server to check if it is processed correctly.
/// </summary>
/// <returns>Task.</returns>
[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", "claimed@example.tld"));
message.Subject = "Test Email with multipart body.";
const string textBody = "This is a test email multipart.";
const string htmlBody = "<html><body><h1>This is a test email multipart.</h1></body></html>";
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();
// Test non-encrypted field.
Assert.That(processedEmail.To, Is.EqualTo("claimed@example.tld"));
// Decrypt the email and then check all individual fields.
processedEmail = EmailEncryption.DecryptEmail(processedEmail, PrivateKey);
Assert.Multiple(() =>
{
Assert.That(processedEmail, Is.Not.Null);
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));
});
}
/// <summary>
/// Tests sending a single email in plain format to the SMTP server to check if it is processed correctly.
/// </summary>
/// <returns>Task.</returns>
[Test]
public async Task MultipleRecipientsEmail()
{
// Send 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", "claimed@example.tld"));
message.Cc.Add(new MailboxAddress("Test Recipient 2", "claimed.cc@example.tld"));
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));
}
/// <summary>
/// Tests sending an email to an unknown recipient domain, we expect to get an error from the SMTP server.
/// </summary>
[Test]
public void SingleEmailUnknownRecipientDomain()
{
// Send 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<SmtpCommandException>(async () => await SendMessageToSmtpServer(message));
}
/// <summary>
/// Tests sending an email to an existing but disabled email claim, we expect to get an error from the SMTP server.
/// </summary>
[Test]
public void SingleEmailDisabledUserClaim()
{
// Send 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", "disabled@example.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<SmtpCommandException>(async () => await SendMessageToSmtpServer(message));
}
/// <summary>
/// Tests sending a single email to a known recipient domain but with no valid user claim. We expect
/// to get an error from the SMTP server.
/// </summary>
[Test]
public void SingleEmailNoUserClaim()
{
// Send 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", "not-claimed@example.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<SmtpCommandException>(async () => await SendMessageToSmtpServer(message));
}
/// <summary>
/// Tests sending a single email in plain format to the SMTP server to check if it is processed correctly.
/// </summary>
/// <returns>Task.</returns>
[Test]
public async Task AttachmentEmail()
{
var message = new MimeMessage();
message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
message.To.Add(new MailboxAddress("Test Recipient", "claimed@example.tld"));
message.Subject = "Test Email with attachment";
var bodyBuilder = new BodyBuilder();
bodyBuilder.TextBody = "This is a test email with attachment.";
// Add attachment using BodyBuilder
byte[] attachmentData = Encoding.UTF8.GetBytes("This is an attachment.");
bodyBuilder.Attachments.Add("attachment.txt", attachmentData, ContentType.Parse("text/plain"));
message.Body = bodyBuilder.ToMessageBody();
await SendMessageToSmtpServer(message);
// Check that attachment is in the database and the bytes are encrypted.
Assert.That(await _testHostBuilder.GetDbContext().EmailAttachments.CountAsync(), Is.EqualTo(1));
var attachment = await _testHostBuilder.GetDbContext().EmailAttachments.FirstAsync();
Assert.That(attachment.Bytes, Is.Not.EqualTo(attachmentData), "Email attachment bytes are not encrypted. Check email encryption logic.");
}
/// <summary>
/// Sends a message to the SMTP server.
/// </summary>
/// <param name="message">MimeMessage to send.</param>
private static async Task SendMessageToSmtpServer(MimeMessage message)
{
using var client = new SmtpClient();
await client.ConnectAsync("localhost", 2525, SecureSocketOptions.None);
try
{
await client.SendAsync(message);
}
finally
{
await client.DisconnectAsync(true);
}
}
}