diff --git a/src/AliasVault.Client/Main/Components/Email/RecentEmails.razor b/src/AliasVault.Client/Main/Components/Email/RecentEmails.razor
index 71c1b72a0..f55bc74e5 100644
--- a/src/AliasVault.Client/Main/Components/Email/RecentEmails.razor
+++ b/src/AliasVault.Client/Main/Components/Email/RecentEmails.razor
@@ -1,6 +1,10 @@
-@using AliasVault.Client.Main.Models.Spamok
+@using AliasVault.Shared.Models.Spamok
@inherits ComponentBase
@inject IHttpClientFactory HttpClientFactory
+@inject HttpClient HttpClient
+@inject JsInteropService JsInteropService
+@inject DbService DbService
+@inject Config Config
@if (EmailModalVisible)
{
@@ -21,6 +25,10 @@
{
}
+ else if (!string.IsNullOrEmpty(Error))
+ {
+
+ }
else if (MailboxEmails.Count == 0)
{
No emails found.
@@ -76,6 +84,7 @@
private bool ShowComponent { get; set; } = false;
private EmailApiModel Email { get; set; } = new();
private bool EmailModalVisible { get; set; }
+ private string Error { get; set; } = string.Empty;
///
protected override async Task OnInitializedAsync()
@@ -83,12 +92,46 @@
await base.OnInitializedAsync();
// Check if email has a known SpamOK domain, if not, don't show this component.
- if (EmailAddress.EndsWith("@landmail.nl"))
+ if (IsSpamOkDomain(EmailAddress) || IsAliasVaultDomain(EmailAddress))
{
ShowComponent = true;
}
}
+ ///
+ /// Returns true if the email address is from a known SpamOK domain.
+ ///
+ protected bool IsSpamOkDomain(string email)
+ {
+ return email.EndsWith("@spamok.nl") ||
+ email.EndsWith("@spamok.de") ||
+ email.EndsWith("@spamok.es") ||
+ email.EndsWith("@spamok.fr") ||
+ email.EndsWith("@spamok.com") ||
+ email.EndsWith("@spamok.com.ua") ||
+ email.EndsWith("@landmail.nl") ||
+ email.EndsWith("@landmeel.nl") ||
+ email.EndsWith("@asdasd.nl") ||
+ email.EndsWith("@sdfsdf.nl") ||
+ email.EndsWith("@solarflarecorp.com");
+ }
+
+ ///
+ /// Returns true if the email address is from a known AliasVault domain.
+ ///
+ protected bool IsAliasVaultDomain(string email)
+ {
+ foreach (var domain in Config.SmtpAllowedDomains)
+ {
+ if (email.EndsWith(domain))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
///
protected override async Task OnAfterRenderAsync(bool firstRender)
{
@@ -112,19 +155,66 @@
return;
}
+ Error = string.Empty;
+
IsLoading = true;
StateHasChanged();
// Get email prefix, which is the part before the @ symbol.
string emailPrefix = EmailAddress.Split('@')[0];
- var client = HttpClientFactory.CreateClient("EmailClient");
- MailboxApiModel? mailbox = await client.GetFromJsonAsync
($"https://api.spamok.com/v2/EmailBox/{emailPrefix}");
+ MailboxApiModel? mailbox = new();
- if (mailbox?.Mails != null)
+ if (IsSpamOkDomain(EmailAddress))
{
- // Show maximum of 10 recent emails.
- MailboxEmails = mailbox.Mails.Take(10).ToList();
+ // We construct a new HttpClient to avoid using the default one, which is used for the API and sends
+ // the Authorization header. We don't want to send the Authorization header to the external email API.
+ var client = HttpClientFactory.CreateClient("EmailClient");
+ mailbox = await client.GetFromJsonAsync($"https://api.spamok.com/v2/EmailBox/{emailPrefix}");
+
+ if (mailbox?.Mails != null)
+ {
+ // Show maximum of 10 recent emails.
+ MailboxEmails = mailbox.Mails.Take(10).ToList();
+ }
+ }
+ else if (IsAliasVaultDomain(EmailAddress))
+ {
+ try
+ {
+ mailbox = await HttpClient.GetFromJsonAsync($"api/v1/EmailBox/{EmailAddress}");
+ if (mailbox?.Mails != null)
+ {
+ // Show maximum of 10 recent emails.
+ MailboxEmails = mailbox.Mails.Take(10).ToList();
+ }
+
+ // Loop through emails and decrypt the subject locally.
+ var context = await DbService.GetDbContextAsync();
+ var privateKeys = await context.EncryptionKeys.ToListAsync();
+ foreach (var mail in MailboxEmails)
+ {
+ var privateKey = privateKeys.FirstOrDefault(x => x.PublicKey == mail.EncryptionKey);
+ if (privateKey is not null)
+ {
+ try
+ {
+ var decryptedSymmetricKey = await JsInteropService.DecryptWithPrivateKey(mail.EncryptedSymmetricKey, privateKey.PrivateKey);
+ mail.Subject = await JsInteropService.SymmetricDecrypt(mail.Subject, Convert.ToBase64String(decryptedSymmetricKey));
+ }
+ catch (Exception ex)
+ {
+ Error = ex.Message;
+ Console.WriteLine(ex);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Error = ex.Message;
+ Console.WriteLine(ex);
+ }
}
IsLoading = false;
@@ -139,16 +229,59 @@
// Get email prefix, which is the part before the @ symbol.
string emailPrefix = EmailAddress.Split('@')[0];
- // Load email from API
- var client = HttpClientFactory.CreateClient("EmailClient");
- EmailApiModel? mail = await client.GetFromJsonAsync($"https://api.spamok.com/v2/Email/{emailPrefix}/{emailId}");
- if (mail != null)
+ if (IsSpamOkDomain(EmailAddress))
{
- Email = mail;
- EmailModalVisible = true;
- StateHasChanged();
+ var client = HttpClientFactory.CreateClient("EmailClient");
+ EmailApiModel? mail = await client.GetFromJsonAsync($"https://api.spamok.com/v2/Email/{emailPrefix}/{emailId}");
+ if (mail != null)
+ {
+ Email = mail;
+ EmailModalVisible = true;
+ StateHasChanged();
+ }
}
+ else if (IsAliasVaultDomain(EmailAddress))
+ {
+ EmailApiModel? mail = await HttpClient.GetFromJsonAsync($"api/v1/Email/{emailId}");
+ if (mail != null)
+ {
+ // Decrypt the email content locally.
+ var context = await DbService.GetDbContextAsync();
+ var privateKey = await context.EncryptionKeys.FirstOrDefaultAsync(x => x.PublicKey == mail.EncryptionKey);
+ if (privateKey is not null)
+ {
+ try
+ {
+ var decryptedSymmetricKey = await JsInteropService.DecryptWithPrivateKey(mail.EncryptedSymmetricKey, privateKey.PrivateKey);
+ mail.Subject = await JsInteropService.SymmetricDecrypt(mail.Subject, Convert.ToBase64String(decryptedSymmetricKey));
+ if (mail.MessageHtml is not null)
+ {
+ mail.MessageHtml = await JsInteropService.SymmetricDecrypt(mail.MessageHtml, Convert.ToBase64String(decryptedSymmetricKey));
+ }
+
+ if (mail.MessagePlain is not null)
+ {
+ mail.MessagePlain = await JsInteropService.SymmetricDecrypt(mail.MessagePlain, Convert.ToBase64String(decryptedSymmetricKey));
+ }
+
+ mail.FromDisplay = await JsInteropService.SymmetricDecrypt(mail.FromDisplay, Convert.ToBase64String(decryptedSymmetricKey));
+ mail.FromLocal = await JsInteropService.SymmetricDecrypt(mail.FromLocal, Convert.ToBase64String(decryptedSymmetricKey));
+ mail.FromDomain = await JsInteropService.SymmetricDecrypt(mail.FromDomain, Convert.ToBase64String(decryptedSymmetricKey));
+ }
+ catch (Exception ex)
+ {
+ Error = ex.Message;
+ }
+ }
+
+ Email = mail;
+ EmailModalVisible = true;
+ StateHasChanged();
+ }
+
+ }
+
}
///
diff --git a/src/AliasVault.Client/Main/Layout/TopMenu.razor b/src/AliasVault.Client/Main/Layout/TopMenu.razor
index 961c11dd1..67b8db4b1 100644
--- a/src/AliasVault.Client/Main/Layout/TopMenu.razor
+++ b/src/AliasVault.Client/Main/Layout/TopMenu.razor
@@ -1,5 +1,4 @@
-@inherits MainBase
-@using AliasVault.Client.Main.Pages;
+@inherits AliasVault.Client.Main.Pages.MainBase
@implements IDisposable
diff --git a/src/AliasVault.Client/Main/Models/CredentialEdit.cs b/src/AliasVault.Client/Main/Models/CredentialEdit.cs
index 35bac8257..ddf99afcb 100644
--- a/src/AliasVault.Client/Main/Models/CredentialEdit.cs
+++ b/src/AliasVault.Client/Main/Models/CredentialEdit.cs
@@ -11,7 +11,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using AliasClientDb;
-using AliasVault.Client.Models.FormValidation;
+using AliasVault.Client.Main.Models.FormValidation;
///
/// Credential edit model.
diff --git a/src/AliasVault.Client/Main/Models/FormValidation/StringDateFormatAttribute.cs b/src/AliasVault.Client/Main/Models/FormValidation/StringDateFormatAttribute.cs
index 7ebc1b877..e8611b92a 100644
--- a/src/AliasVault.Client/Main/Models/FormValidation/StringDateFormatAttribute.cs
+++ b/src/AliasVault.Client/Main/Models/FormValidation/StringDateFormatAttribute.cs
@@ -5,7 +5,7 @@
//
//-----------------------------------------------------------------------
-namespace AliasVault.Client.Models.FormValidation;
+namespace AliasVault.Client.Main.Models.FormValidation;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
diff --git a/src/AliasVault.Client/Services/CredentialService.cs b/src/AliasVault.Client/Services/CredentialService.cs
index c067d1367..48f57d393 100644
--- a/src/AliasVault.Client/Services/CredentialService.cs
+++ b/src/AliasVault.Client/Services/CredentialService.cs
@@ -14,7 +14,6 @@ using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using AliasClientDb;
-using AliasVault.Client.Models;
using AliasVault.Shared.Models;
using Microsoft.EntityFrameworkCore;
using Identity = AliasGenerators.Identity.Models.Identity;
@@ -279,7 +278,7 @@ public class CredentialService(HttpClient httpClient, DbService dbService)
try
{
var apiReturn =
- await httpClient.GetFromJsonAsync("api/v1/Favicon/Extract?url=" + url);
+ await httpClient.GetFromJsonAsync($"api/v1/Favicon/Extract?url={url}");
if (apiReturn != null && apiReturn.Image != null)
{
credentialObject.Service.Logo = apiReturn.Image;
diff --git a/src/AliasVault.Client/Services/JsInteropService.cs b/src/AliasVault.Client/Services/JsInteropService.cs
index f04c6fbf9..677c077cf 100644
--- a/src/AliasVault.Client/Services/JsInteropService.cs
+++ b/src/AliasVault.Client/Services/JsInteropService.cs
@@ -7,7 +7,7 @@
namespace AliasVault.Client.Services;
-using System.ComponentModel;
+using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.JSInterop;
@@ -93,9 +93,21 @@ public class JsInteropService(IJSRuntime jsRuntime)
///
/// Decrypts a ciphertext with a private key.
///
- /// Ciphertext to decrypt.
+ /// Ciphertext to decrypt.
/// Private key to use for decryption.
/// Decrypted string.
- public async Task DecryptWithPrivateKey(string ciphertext, string privateKey) =>
- await jsRuntime.InvokeAsync("rsaInterop.decryptWithPrivateKey", ciphertext, privateKey);
+ public async Task DecryptWithPrivateKey(string base64Ciphertext, string privateKey)
+ {
+ try
+ {
+ // Invoke the JavaScript function and get the result as a byte array
+ byte[] result = await jsRuntime.InvokeAsync("rsaInterop.decryptWithPrivateKey", base64Ciphertext, privateKey);
+ return result;
+ }
+ catch (JSException ex)
+ {
+ Console.Error.WriteLine($"JavaScript decryption error: {ex.Message}");
+ throw new CryptographicException("Decryption failed", ex);
+ }
+ }
}
diff --git a/src/AliasVault.Client/wwwroot/js/cryptoInterop.js b/src/AliasVault.Client/wwwroot/js/cryptoInterop.js
index 4d5e1803e..11712a5aa 100644
--- a/src/AliasVault.Client/wwwroot/js/cryptoInterop.js
+++ b/src/AliasVault.Client/wwwroot/js/cryptoInterop.js
@@ -118,29 +118,42 @@ window.rsaInterop = {
* Decrypts a ciphertext string using an RSA private key.
* @param {string} ciphertext - The base64-encoded ciphertext to decrypt.
* @param {string} privateKey - The private key in JWK format.
- * @returns {Promise} A promise that resolves to the decrypted plaintext.
+ * @returns {Promise} A promise that resolves to the decrypted data as a Uint8Array.
*/
- decryptWithPrivateKey : async function(ciphertext, privateKey) {
- const privateKeyObj = await window.crypto.subtle.importKey(
- "jwk",
- JSON.parse(privateKey),
- {
- name: "RSA-OAEP",
- hash: "SHA-256",
- },
- false,
- ["decrypt"]
- );
+ decryptWithPrivateKey: async function(ciphertext, privateKey) {
+ try {
+ // Parse the private key
+ let parsedPrivateKey = JSON.parse(privateKey);
- const cipherBuffer = Uint8Array.from(atob(ciphertext), c => c.charCodeAt(0));
- const plaintextBuffer = await window.crypto.subtle.decrypt(
- {
- name: "RSA-OAEP"
- },
- privateKeyObj,
- cipherBuffer
- );
+ // Import the private key
+ let privateKeyObj = await window.crypto.subtle.importKey(
+ "jwk",
+ parsedPrivateKey,
+ {
+ name: "RSA-OAEP",
+ hash: "SHA-256",
+ },
+ true,
+ ["decrypt"]
+ );
- return new TextDecoder().decode(plaintextBuffer);
+ // Decode the base64 ciphertext
+ let cipherBuffer = Uint8Array.from(atob(ciphertext), c => c.charCodeAt(0));
+
+ // Decrypt the ciphertext
+ let plaintextBuffer = await window.crypto.subtle.decrypt(
+ {
+ name: "RSA-OAEP",
+ hash: "SHA-256",
+ },
+ privateKeyObj,
+ cipherBuffer
+ );
+
+ // Return the decrypted data as a Uint8Array
+ return new Uint8Array(plaintextBuffer);
+ } catch (error) {
+ throw new Error(`Failed to decrypt: ${error.message}`);
+ }
}
};
diff --git a/src/AliasVault.Client/Main/Models/Spamok/AttachmentApiModel.cs b/src/AliasVault.Shared/Models/Spamok/AttachmentApiModel.cs
similarity index 96%
rename from src/AliasVault.Client/Main/Models/Spamok/AttachmentApiModel.cs
rename to src/AliasVault.Shared/Models/Spamok/AttachmentApiModel.cs
index 6f5951c8f..f8f3cb512 100644
--- a/src/AliasVault.Client/Main/Models/Spamok/AttachmentApiModel.cs
+++ b/src/AliasVault.Shared/Models/Spamok/AttachmentApiModel.cs
@@ -5,7 +5,7 @@
//
//-----------------------------------------------------------------------
-namespace AliasVault.Client.Main.Models.Spamok;
+namespace AliasVault.Shared.Models.Spamok;
///
/// Represents an attachment for an email.
diff --git a/src/AliasVault.Client/Main/Models/Spamok/Base/EmailApiModelBase.cs b/src/AliasVault.Shared/Models/Spamok/Base/EmailApiModelBase.cs
similarity index 79%
rename from src/AliasVault.Client/Main/Models/Spamok/Base/EmailApiModelBase.cs
rename to src/AliasVault.Shared/Models/Spamok/Base/EmailApiModelBase.cs
index 2c96fb79b..599007fe1 100644
--- a/src/AliasVault.Client/Main/Models/Spamok/Base/EmailApiModelBase.cs
+++ b/src/AliasVault.Shared/Models/Spamok/Base/EmailApiModelBase.cs
@@ -5,7 +5,7 @@
//
//-----------------------------------------------------------------------
-namespace AliasVault.Client.Main.Models.Spamok.Base;
+namespace AliasVault.Shared.Models.Spamok.Base;
///
/// Represents a mailbox email API model base.
@@ -61,4 +61,15 @@ public abstract class EmailApiModelBase
/// Gets or sets the number of seconds ago the email was received.
///
public double SecondsAgo { get; set; }
+
+ ///
+ /// Gets or sets the encrypted symmetric key which was used to encrypt the email message.
+ /// This key is encrypted with the public key of the user.
+ ///
+ public string EncryptedSymmetricKey { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the public key of the user used to encrypt the symmetric key.
+ ///
+ public string EncryptionKey { get; set; } = string.Empty;
}
diff --git a/src/AliasVault.Client/Main/Models/Spamok/EmailApiModel.cs b/src/AliasVault.Shared/Models/Spamok/EmailApiModel.cs
similarity index 90%
rename from src/AliasVault.Client/Main/Models/Spamok/EmailApiModel.cs
rename to src/AliasVault.Shared/Models/Spamok/EmailApiModel.cs
index aae8f03cd..875126147 100644
--- a/src/AliasVault.Client/Main/Models/Spamok/EmailApiModel.cs
+++ b/src/AliasVault.Shared/Models/Spamok/EmailApiModel.cs
@@ -5,9 +5,9 @@
//
//-----------------------------------------------------------------------
-namespace AliasVault.Client.Main.Models.Spamok;
+namespace AliasVault.Shared.Models.Spamok;
-using AliasVault.Client.Main.Models.Spamok.Base;
+using AliasVault.Shared.Models.Spamok.Base;
///
/// Represents an email API model.
diff --git a/src/AliasVault.Client/Main/Models/Spamok/MailboxApiModel.cs b/src/AliasVault.Shared/Models/Spamok/MailboxApiModel.cs
similarity index 95%
rename from src/AliasVault.Client/Main/Models/Spamok/MailboxApiModel.cs
rename to src/AliasVault.Shared/Models/Spamok/MailboxApiModel.cs
index c042f25fd..d6a272abc 100644
--- a/src/AliasVault.Client/Main/Models/Spamok/MailboxApiModel.cs
+++ b/src/AliasVault.Shared/Models/Spamok/MailboxApiModel.cs
@@ -5,7 +5,7 @@
//
//-----------------------------------------------------------------------
-namespace AliasVault.Client.Main.Models.Spamok;
+namespace AliasVault.Shared.Models.Spamok;
///
/// Represents a mailbox API model.
diff --git a/src/AliasVault.Client/Main/Models/Spamok/MailboxEmailApiModel.cs b/src/AliasVault.Shared/Models/Spamok/MailboxEmailApiModel.cs
similarity index 87%
rename from src/AliasVault.Client/Main/Models/Spamok/MailboxEmailApiModel.cs
rename to src/AliasVault.Shared/Models/Spamok/MailboxEmailApiModel.cs
index 0e2741690..d76a50069 100644
--- a/src/AliasVault.Client/Main/Models/Spamok/MailboxEmailApiModel.cs
+++ b/src/AliasVault.Shared/Models/Spamok/MailboxEmailApiModel.cs
@@ -5,9 +5,9 @@
//
//-----------------------------------------------------------------------
-namespace AliasVault.Client.Main.Models.Spamok;
+namespace AliasVault.Shared.Models.Spamok;
-using AliasVault.Client.Main.Models.Spamok.Base;
+using AliasVault.Shared.Models.Spamok.Base;
///
/// Represents a mailbox email API model.
diff --git a/src/Databases/AliasServerDb/Email.cs b/src/Databases/AliasServerDb/Email.cs
index 4c7643525..fdb420aec 100644
--- a/src/Databases/AliasServerDb/Email.cs
+++ b/src/Databases/AliasServerDb/Email.cs
@@ -123,5 +123,5 @@ public class Email
///
/// Gets or sets the collection of email attachments.
///
- public virtual ICollection Attachments { get; set; } = [];
+ public virtual List Attachments { get; set; } = [];
}
diff --git a/src/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs b/src/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs
index 9f390f210..e0807742e 100644
--- a/src/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs
+++ b/src/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs
@@ -6,6 +6,7 @@
//-----------------------------------------------------------------------
using Cryptography;
+using SmtpServer.Mail;
namespace AliasVault.SmtpService.Handlers;
@@ -63,13 +64,15 @@ public class DatabaseMessageStore(ILogger logger, Config c
stream.Position = 0;
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()
.ToList();
- // Limit list to 15 addresses max. (to prevent mailbomb spam abuse)
+
+ // Limit list to 15 addresses maximum (to prevent mailbomb spam abuse).
var toAddresses = allAddresses.Take(15).ToList();
- // For every toAddress
+
foreach (var toAddress in toAddresses)
{
// Check if toAddress domain is allowed.
@@ -131,7 +134,8 @@ public class DatabaseMessageStore(ILogger logger, Config c
return SmtpResponse.NoValidRecipientsGiven;
}
- var insertedId = await InsertEmailIntoDatabase(message, userPublicKey);
+ // Set the "to" for the email to the actual one we are looping through now.
+ var insertedId = await InsertEmailIntoDatabase(message, new MailAddress(toAddress.AsAddress()), userPublicKey);
logger.LogInformation("Email for {ToAddress} successfully saved into database with ID {insertedId}.",
toAddress.User + "@" + toAddress.Host, insertedId);
}
@@ -149,12 +153,13 @@ public class DatabaseMessageStore(ILogger logger, Config c
/// Insert email into database.
///
/// MimeMessage to save into database.
+ /// The recipient for this mail.
/// The public key of the user to encrypt the mail contents with.
- private async Task InsertEmailIntoDatabase(MimeMessage message, UserEncryptionKey userEncryptionKey)
+ private async Task InsertEmailIntoDatabase(MimeMessage message, MailAddress toAddress, UserEncryptionKey userEncryptionKey)
{
var dbContext = await dbContextFactory.CreateDbContextAsync();
- var newEmail = ConvertMimeMessageToEmail(message);
+ var newEmail = ConvertMimeMessageToEmail(message, toAddress);
newEmail = EmailEncryption.EncryptEmail(newEmail, userEncryptionKey);
// Insert the email into the database.
@@ -168,9 +173,10 @@ public class DatabaseMessageStore(ILogger logger, Config c
/// Convert MimeMessage to Email database object.
///
/// MimeMessage object.
+ /// The recipient for this mail.
/// Email object.
///
- private static Email ConvertMimeMessageToEmail(MimeMessage message)
+ private static Email ConvertMimeMessageToEmail(MimeMessage message, MailAddress toAddress)
{
string from = "";
@@ -195,57 +201,23 @@ public class DatabaseMessageStore(ILogger logger, Config c
}
catch
{
- // If the above fails, try to find the x-sender in the mail
- try
- {
- MailAddress fromAddress = new MailAddress(message.Headers.First(x => x.Field == "x-sender").Value.ToString());
- fromLocal = fromAddress.User;
- fromDomain = fromAddress.Host;
- }
- catch
- {
- // If this fails as well, then simply use a blank value
- fromLocal = "";
- fromDomain = "";
- }
- }
-
- MailAddress toAddress;
- string to;
-
- // Try to extract to address firstly from x-receiver address..
- try
- {
- to = message.Headers.First(x => x.Field == "x-receiver").Value.ToString();
- toAddress = new MailAddress(to);
- }
- catch
- {
- // If the above fails, try to find the "to" in the mail
- try
- {
- to = message.To.FirstOrDefault()?.ToString() ?? "";
- toAddress = new MailAddress(to);
- }
- catch
- {
- // If this fails as well, then simply let it throw an error to the caller.
- throw new EmailParseMissingToException("Could not find x-receiver or to address in email.");
- }
+ // If this fails, then simply use a blank value
+ fromLocal = "";
+ fromDomain = "";
}
// Create email object
var email = new Email();
email.From = from;
- email.FromLocal = fromLocal;
- email.FromDomain = fromDomain;
+ email.FromLocal = fromLocal.ToLower();
+ email.FromDomain = fromDomain.ToLower();
- email.To = to;
// Local part to lowercase, as mailboxes are always lowercase
+ email.To = toAddress.Address.ToLower();
email.ToLocal = toAddress.User.ToLower();
- email.ToDomain = toAddress.Host;
+ email.ToDomain = toAddress.Host.ToLower();
- email.Subject = message.Subject ?? "";
+ email.Subject = message.Subject ?? string.Empty;
email.MessageHtml = message.HtmlBody;
email.MessagePlain = message.TextBody;
email.MessageSource = message.ToString();
diff --git a/src/Services/AliasVault.SmtpService/Scripts/sendEmailAllowed.sh b/src/Services/AliasVault.SmtpService/Scripts/sendEmailAllowed.sh
index c45f3a511..56720cd5e 100755
--- a/src/Services/AliasVault.SmtpService/Scripts/sendEmailAllowed.sh
+++ b/src/Services/AliasVault.SmtpService/Scripts/sendEmailAllowed.sh
@@ -1 +1 @@
-curl --url "smtp://localhost:25" --mail-from "sender@example.com" --mail-rcpt "yourname@example.tld" --upload-file testEmail1.txt
+curl --url "smtp://localhost:25" --mail-from "sender@example.com" --mail-rcpt "test@example.tld" --upload-file testEmail1.txt
diff --git a/src/Tests/AliasVault.IntegrationTests/SmtpServer/SmtpServerTests.cs b/src/Tests/AliasVault.IntegrationTests/SmtpServer/SmtpServerTests.cs
index 25084bf0f..871d5f46b 100644
--- a/src/Tests/AliasVault.IntegrationTests/SmtpServer/SmtpServerTests.cs
+++ b/src/Tests/AliasVault.IntegrationTests/SmtpServer/SmtpServerTests.cs
@@ -124,7 +124,7 @@ public class SmtpServerTests
var processedEmail = await _testHostBuilder.GetDbContext().Emails.FirstAsync();
// Test non-encrypted field.
- Assert.That(processedEmail.To, Is.EqualTo("\"Test Recipient\" "));
+ Assert.That(processedEmail.To, Is.EqualTo("claimed@example.tld"));
// Decrypt the email and then check all individual fields.
processedEmail = EmailEncryption.DecryptEmail(processedEmail, PrivateKey);
@@ -159,7 +159,7 @@ public class SmtpServerTests
var processedEmail = await _testHostBuilder.GetDbContext().Emails.FirstAsync();
// Test non-encrypted field.
- Assert.That(processedEmail.To, Is.EqualTo("\"Test Recipient\" "));
+ Assert.That(processedEmail.To, Is.EqualTo("claimed@example.tld"));
// Decrypt the email and then check all individual fields.
processedEmail = EmailEncryption.DecryptEmail(processedEmail, PrivateKey);
@@ -192,7 +192,7 @@ public class SmtpServerTests
var processedEmail = await _testHostBuilder.GetDbContext().Emails.FirstAsync();
// Test non-encrypted field.
- Assert.That(processedEmail.To, Is.EqualTo("\"Test Recipient\" "));
+ Assert.That(processedEmail.To, Is.EqualTo("claimed@example.tld"));
// Decrypt the email and then check all individual fields.
processedEmail = EmailEncryption.DecryptEmail(processedEmail, PrivateKey);
diff --git a/src/Tests/AliasVault.UnitTests/Helpers/ConversionHelperTest.cs b/src/Tests/AliasVault.UnitTests/Helpers/ConversionHelperTest.cs
new file mode 100644
index 000000000..fb5a4a205
--- /dev/null
+++ b/src/Tests/AliasVault.UnitTests/Helpers/ConversionHelperTest.cs
@@ -0,0 +1,81 @@
+//-----------------------------------------------------------------------
+//
+// 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.Tests.Helpers;
+
+using AliasVault.Api.Helpers;
+
+///
+/// Tests for the CsvImportExport class.
+///
+public class ConversionHelperTest
+{
+ ///
+ /// Tests the conversion of an email address with a display name to just the display name.
+ ///
+ [Test]
+ public void TestFromConversion()
+ {
+ string from = "\"My full Name\" ";
+ string convertedFrom = ConversionHelper.ConvertFromToFromDisplay(from);
+
+ // Check that conversion works as expected.
+ Assert.That(convertedFrom, Is.EqualTo("My full Name"));
+ }
+
+ ///
+ /// Tests the conversion of a simple anchor tag to open in a new tab.
+ ///
+ [Test]
+ public void TestAnchorTabConversionSimple()
+ {
+ string anchorHtml = "";
+ string convertedAnchorTags = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
+
+ // Check that conversion works as expected.
+ Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
+ }
+
+ ///
+ /// Tests the conversion of a complex anchor tag with multiple attributes to open in a new tab.
+ ///
+ [Test]
+ public void TestAnchorTabConversionComplex1()
+ {
+ string anchorHtml = "Start hier met de training >>>";
+ string convertedAnchorTags = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
+
+ // Check that conversion works as expected.
+ Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
+ }
+
+ ///
+ /// Tests the conversion of a complex anchor tag with nested elements to open in a new tab.
+ ///
+ [Test]
+ public void TestAnchorTabConversionComplex2()
+ {
+ string anchorHtml = "";
+ string convertedAnchorTags = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
+
+ // Check that conversion works as expected.
+ Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
+ }
+
+ ///
+ /// Tests the conversion of a complex anchor tag within a table cell to open in a new tab.
+ ///
+ [Test]
+ public void TestAnchorTabConversionComplex3()
+ {
+ string anchorHtml = "Ontvang nu jouw prijs > | ";
+ string convertedAnchorTags = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
+
+ // Check that conversion works as expected.
+ Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
+ }
+}
diff --git a/src/Utilities/Cryptography/Encryption.cs b/src/Utilities/Cryptography/Encryption.cs
index 7f6318e63..5dec4775d 100644
--- a/src/Utilities/Cryptography/Encryption.cs
+++ b/src/Utilities/Cryptography/Encryption.cs
@@ -38,10 +38,13 @@ public static class Encryption
/// The encrypted symmetric key as a base64-encoded string.
public static string EncryptSymmetricKeyWithRsa(byte[] symmetricKey, string publicKey)
{
- using (var rsa = new RSACryptoServiceProvider())
+ using (var rsa = RSA.Create())
{
ImportPublicKey(rsa, publicKey);
- byte[] encryptedKey = rsa.Encrypt(symmetricKey, true);
+ rsa.KeySize = 2048;
+ var rsaParams = RSAEncryptionPadding.OaepSHA256;
+
+ byte[] encryptedKey = rsa.Encrypt(symmetricKey, rsaParams);
return Convert.ToBase64String(encryptedKey);
}
}
@@ -54,11 +57,14 @@ public static class Encryption
/// The encrypted symmetric key as a base64-encoded string.
public static byte[] DecryptSymmetricKeyWithRsa(string ciphertext, string privateKey)
{
- using (var rsa = new RSACryptoServiceProvider())
+ using (var rsa = RSA.Create())
{
ImportPrivateKey(rsa, privateKey);
+ rsa.KeySize = 2048;
+ var rsaParams = RSAEncryptionPadding.OaepSHA256;
+
byte[] cipherBytes = Convert.FromBase64String(ciphertext);
- return rsa.Decrypt(cipherBytes, true);
+ return rsa.Decrypt(cipherBytes, rsaParams);
}
}
@@ -178,7 +184,7 @@ public static class Encryption
///
/// The RSA provider to import the key into.
/// The public key in JWK format.
- private static void ImportPublicKey(RSACryptoServiceProvider rsa, string jwk)
+ private static void ImportPublicKey(RSA rsa, string jwk)
{
var jwkObj = JsonSerializer.Deserialize(jwk);
var n = Base64UrlDecode(jwkObj.GetProperty("n").GetString()!);
@@ -198,7 +204,7 @@ public static class Encryption
///
/// The RSA provider to import the key into.
/// The private key in JWK format.
- private static void ImportPrivateKey(RSACryptoServiceProvider rsa, string jwk)
+ private static void ImportPrivateKey(RSA rsa, string jwk)
{
var jwkObj = JsonSerializer.Deserialize(jwk);
var n = Base64UrlDecode(jwkObj.GetProperty("n").GetString()!);