Merge pull request #198 from lanedirt/188-add-email-anchor-tags-translation-to-open-in-new-tabs-instead-of-iframe

Fix email anchor tag target=blank conversion
This commit is contained in:
Leendert de Borst
2024-09-01 12:58:10 +02:00
committed by GitHub
10 changed files with 153 additions and 98 deletions

View File

@@ -77,7 +77,6 @@ public class EmailBoxController(IDbContextFactory<AliasServerDbContext> dbContex
{
Id = x.Id,
Subject = x.Subject,
FromDisplay = ConversionHelper.ConvertFromToFromDisplay(x.From),
FromDomain = x.FromDomain,
FromLocal = x.FromLocal,
ToDomain = x.ToDomain,
@@ -140,7 +139,7 @@ public class EmailBoxController(IDbContextFactory<AliasServerDbContext> dbContex
{
Id = x.Id,
Subject = x.Subject,
FromDisplay = ConversionHelper.ConvertFromToFromDisplay(x.From),
FromDisplay = x.From,
FromDomain = x.FromDomain,
FromLocal = x.FromLocal,
ToDomain = x.ToDomain,

View File

@@ -45,7 +45,6 @@ public class EmailController(ILogger<VaultController> logger, IDbContextFactory<
{
Id = email!.Id,
Subject = email.Subject,
FromDisplay = ConversionHelper.ConvertFromToFromDisplay(email.From),
FromDomain = email.FromDomain,
FromLocal = email.FromLocal,
ToDomain = email.ToDomain,
@@ -71,12 +70,6 @@ public class EmailController(ILogger<VaultController> logger, IDbContextFactory<
returnEmail.Attachments = attachments;
// Enrich HTML by changing all anchor tags to open in new tab
if (returnEmail.MessageHtml != null && !string.IsNullOrEmpty(email.MessageHtml))
{
returnEmail.MessageHtml = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(email.MessageHtml);
}
return Ok(returnEmail);
}

View File

@@ -1,60 +0,0 @@
//-----------------------------------------------------------------------
// <copyright file="ConversionHelper.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Api.Helpers;
using System.Text.RegularExpressions;
/// <summary>
/// Class which contains various helper methods for data conversion.
/// </summary>
public static class ConversionHelper
{
/// <summary>
/// Extract only displayname from full "From" string. E.g. "John Doe" [johndoe@john.com] becomes "John Doe".
/// </summary>
/// <param name="from">The full from string.</param>
/// <returns>Stripped displayname.</returns>
public static string ConvertFromToFromDisplay(string from)
{
// Get the display name from the From field, which is everything before the first < and after the first >
string fromDisplay = from;
if (!from.Contains('<'))
{
return fromDisplay;
}
// Remove everything after the last < until the last >
fromDisplay = from.Substring(0, from.LastIndexOf('<'));
// Remove any double quotes
fromDisplay = fromDisplay.Replace("\"", string.Empty);
// Trim any whitespace
fromDisplay = fromDisplay.Trim();
return fromDisplay;
}
/// <summary>
/// Convert all anchor tags to open in a new tab.
/// </summary>
/// <param name="html">HTML input.</param>
/// <returns>HTML with all anchor tags converted to open in a new tab when clicked on.</returns>
public static string ConvertAnchorTagsToOpenInNewTab(string html)
{
// Match any <a tag with href attribute, regardless of the position of href or other attributes
html = Regex.Replace(
html,
@"<a\s+(.*?)href=([""'])(.*?)\2(.*?)>",
m => $"<a {m.Groups[1].Value}href={m.Groups[2].Value}{m.Groups[3].Value}{m.Groups[2].Value} {m.Groups[4].Value} target=\"_blank\">",
RegexOptions.IgnoreCase | RegexOptions.Singleline,
TimeSpan.FromSeconds(1));
return html;
}
}

View File

@@ -1,4 +1,5 @@
@using AliasVault.Shared.Models.Spamok
@using AliasVault.Shared.Utilities
@inject JsInteropService JsInteropService
@inject GlobalNotificationService GlobalNotificationService
@inject IHttpClientFactory HttpClientFactory
@@ -31,7 +32,7 @@
</div>
<div class="mt-4 text-gray-700 dark:text-gray-300">
<div>
<iframe class="w-full overscroll-y-auto" style="height:500px;" srcdoc="@(EmailBody ?? "<div>This email has no HTML content.</div>")">
<iframe class="w-full overscroll-y-auto" style="height:500px;" srcdoc="@EmailBody">
</iframe>
</div>
</div>
@@ -176,17 +177,17 @@
// Check if there is HTML content, if not, then set default viewtype to plain
if (Email.MessageHtml is not null && !string.IsNullOrWhiteSpace(Email.MessageHtml))
{
// No HTML is available
EmailBody = Email.MessageHtml;
// HTML is available
EmailBody = ConversionUtility.ConvertAnchorTagsToOpenInNewTab(Email.MessageHtml);
}
else if (Email.MessagePlain is not null)
{
// HTML is available
// No HTML but plain text is available
EmailBody = Email.MessagePlain;
}
else
{
// No HTML is available
// No HTML and no plain text available
EmailBody = "[This email has no body.]";
}
}

View File

@@ -21,6 +21,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.65" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -0,0 +1,71 @@
//-----------------------------------------------------------------------
// <copyright file="ConversionUtility.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Shared.Utilities;
using HtmlAgilityPack;
/// <summary>
/// Class which contains various helper methods for data conversion.
/// </summary>
public static class ConversionUtility
{
/// <summary>
/// Convert all anchor tags to open in a new tab.
/// </summary>
/// <param name="html">HTML input.</param>
/// <returns>HTML with all anchor tags converted to open in a new tab when clicked on.</returns>
public static string ConvertAnchorTagsToOpenInNewTab(string html)
{
try
{
var doc = new HtmlDocument();
doc.LoadHtml(html);
var anchors = doc.DocumentNode.SelectNodes("//a[@href]");
if (anchors != null)
{
foreach (var anchor in anchors)
{
var targetAttr = anchor.Attributes["target"];
if (targetAttr == null)
{
anchor.SetAttributeValue("target", "_blank");
}
else if (targetAttr.Value != "_blank")
{
targetAttr.Value = "_blank";
}
// Add rel="noopener noreferrer" for security
var relAttr = anchor.Attributes["rel"];
if (relAttr == null)
{
anchor.SetAttributeValue("rel", "noopener noreferrer");
}
else if (!relAttr.Value.Contains("noopener") || !relAttr.Value.Contains("noreferrer"))
{
var relValues = new HashSet<string>(relAttr.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries));
relValues.Add("noopener");
relValues.Add("noreferrer");
anchor.SetAttributeValue("rel", string.Join(" ", relValues));
}
}
}
return doc.DocumentNode.OuterHtml;
}
catch (Exception ex)
{
// Log the exception
Console.WriteLine($"Error in ConvertAnchorTagsToOpenInNewTab: {ex.Message}");
// Return the original HTML if an error occurs
return html;
}
}
}

View File

@@ -78,8 +78,21 @@ public class EmailDecryptionTests : ClientPlaywrightTest
message.To.Add(new MailboxAddress("Test Recipient", email));
const string textSubject = "Encrypted Email Subject";
const string textBody = "This is a test email plain.";
const string htmlBody = @"
<html>
<body>
<h1>Test Email</h1>
<p>This is a test email with HTML content.</p>
<p>Sample anchor tag: <a href=""https://example.com"">Example Link</a></p>
</body>
</html>";
message.Subject = textSubject;
message.Body = new BodyBuilder { TextBody = textBody }.ToMessageBody();
var bodyBuilder = new BodyBuilder
{
TextBody = textBody,
HtmlBody = htmlBody,
};
message.Body = bodyBuilder.ToMessageBody();
await SendMessageToSmtpServer(message);
// Assert that email was received by the server.
@@ -109,6 +122,10 @@ public class EmailDecryptionTests : ClientPlaywrightTest
await Page.Locator("text=" + textSubject).First.ClickAsync();
await WaitForUrlAsync("emails**", "Delete");
// Assert that the anchor tag in the email iframe has target="_blank" attribute.
var anchorTag = await Page.Locator("iframe").First.GetAttributeAsync("srcdoc");
Assert.That(anchorTag, Does.Contain("target=\"_blank\""), "Anchor tag in email iframe does not have target=\"_blank\" attribute. Check email decryption logic.");
// Click the delete button to delete the email.
await Page.Locator("id=delete-email").First.ClickAsync();

View File

@@ -22,7 +22,7 @@ public class CodeLockoutTests : TwoFactorAuthBase
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task TwoFactorAuthLockoutTest()
public async Task TwoFactorAuthCodeLockoutTest()
{
await DisableTwoFactorIfEnabled();
await EnableTwoFactor();

View File

@@ -22,7 +22,7 @@ public class RecoveryLockoutTests : TwoFactorAuthBase
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task TwoFactorAuthLockoutTest()
public async Task TwoFactorAuthRecoveryLockoutTest()
{
await DisableTwoFactorIfEnabled();
await EnableTwoFactor();

View File

@@ -1,32 +1,20 @@
//-----------------------------------------------------------------------
// <copyright file="ConversionHelperTest.cs" company="lanedirt">
// <copyright file="ConversionUtilityTest.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Tests.Helpers;
namespace AliasVault.Tests.Utilities;
using AliasVault.Api.Helpers;
using System.Text.RegularExpressions;
using AliasVault.Shared.Utilities;
/// <summary>
/// Tests for the CsvImportExport class.
/// </summary>
public class ConversionHelperTest
public partial class ConversionUtilityTest
{
/// <summary>
/// Tests the conversion of an email address with a display name to just the display name.
/// </summary>
[Test]
public void TestFromConversion()
{
string from = "\"My full Name\" <myname@example.com>";
string convertedFrom = ConversionHelper.ConvertFromToFromDisplay(from);
// Check that conversion works as expected.
Assert.That(convertedFrom, Is.EqualTo("My full Name"));
}
/// <summary>
/// Tests the conversion of a simple anchor tag to open in a new tab.
/// </summary>
@@ -34,7 +22,7 @@ public class ConversionHelperTest
public void TestAnchorTabConversionSimple()
{
string anchorHtml = "<a href=\"https://dutchamzmasters.lt.acemlnb.com/Prod/link-tracker?redirectUrl=aHR0cHMlM0ElMkYlMkZ3d3cuZHV0Y2hhbXptYXN0ZXJzLmNvbSUyRnRoYW5rLXlvdTloN3poZ3Rp&amp;sig=CpED3rRPX48ddoWTUZURadAYPYgPppT312jUNnvUCPo5&amp;iat=1679512450&amp;a=%7C%7C25799960%7C%7C&amp;account=dutchamzmasters%2Eactivehosted%2Ecom&amp;email=DQeVbqE%2Fy2FD5V3I2cvSxXjJCI3Tg5qfUHKGneOhzjJYZ1kM3LVZcQ%3D%3D%3AvdAW7N7fs1pZlI1ib%2BNbsMYz5m4FssAR&amp;s=5241db963ffe25d6f4b762fc00038ee2&amp;i=163A299A10A816\"></a>";
string convertedAnchorTags = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
string convertedAnchorTags = ConversionUtility.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
// Check that conversion works as expected.
Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
@@ -47,7 +35,7 @@ public class ConversionHelperTest
public void TestAnchorTabConversionComplex1()
{
string anchorHtml = "<a\nhref=\"https://dutchamzmasters.lt.acemlnb.com/Prod/link-tracker?redirectUrl=aHR0cHMlM0ElMkYlMkZ3d3cuZHV0Y2hhbXptYXN0ZXJzLmNvbSUyRnRoYW5rLXlvdTloN3poZ3Rp&sig=CpED3rRPX48ddoWTUZURadAYPYgPppT312jUNnvUCPo5&iat=1679512450&a=%7C%7C25799960%7C%7C&account=dutchamzmasters%2Eactivehosted%2Ecom&email=DQeVbqE%2Fy2FD5V3I2cvSxXjJCI3Tg5qfUHKGneOhzjJYZ1kM3LVZcQ%3D%3D%3AvdAW7N7fs1pZlI1ib%2BNbsMYz5m4FssAR&s=5241db963ffe25d6f4b762fc00038ee2&i=163A299A10A816\" data-ac-default-color=\"1\" style=\"margin: 0; outline: none; padding: 0; color: #045FB4; text-decoration: underline; font-weight: bold;\"><span style=\"color: ; font-size: inherit; font-weight: inherit; line-height: inherit; text-decoration: inherit;\">Start hier met de training &gt;&gt;&gt;</span></a>";
string convertedAnchorTags = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
string convertedAnchorTags = ConversionUtility.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
// Check that conversion works as expected.
Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
@@ -60,7 +48,7 @@ public class ConversionHelperTest
public void TestAnchorTabConversionComplex2()
{
string anchorHtml = "<div class=\"btn btn--flat btn--large\" style=\"Margin-bottom: 20px;text-align: center;\">\n <!--[if !mso]><!--><a style=\"border-radius: 4px;display: inline-block;font-size: 14px;font-weight: bold;line-height: 24px;padding: 12px 24px;text-align: center;text-decoration: none !important;transition: opacity 0.1s ease-in;color: #212529 !important;background-color: #ffdd55;font-family: Open Sans, sans-serif;\" href=\"https://eazegamesbv.cmail19.com/t/j-l-sktidll-ddhdthjhkj-j/\">Haal je beloning op</a><!--<![endif]-->\n <!--[if mso]><p style=\"line-height:0;margin:0;\">&nbsp;</p><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"https://eazegamesbv.cmail19.com/t/j-l-sktidll-ddhdthjhkj-j/\" style=\"width:136.5pt\" arcsize=\"9%\" fillcolor=\"#FFDD55\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0pt,8.25pt,0pt,8.25pt\"><center style=\"font-size:14px;line-height:24px;color:#212529;font-family:Open Sans,sans-serif;font-weight:bold;mso-line-height-rule:exactly;mso-text-raise:1.5px\">Haal je beloning op</center></v:textbox></v:roundrect><![endif]--></div>";
string convertedAnchorTags = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
string convertedAnchorTags = ConversionUtility.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
// Check that conversion works as expected.
Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
@@ -73,9 +61,54 @@ public class ConversionHelperTest
public void TestAnchorTabConversionComplex3()
{
string anchorHtml = "<td style=\"word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; font-family: Helvetica, Arial, sans-serif; font-weight: normal; margin: 0; Margin: 0; font-size: 16px; line-height: 1.3; text-align: center; color: #fefefe; background: #f17130; border-radius: 5px; border: 0 solid #f17130; width: 400px; padding: 5px; border-collapse: collapse;\"><a href=\"https://click.info.wijkopenautos.nl/f/a/chSbfTJZeP5dGZaVjOIUlw~~/AABMyAA~/RgRnzW26P0SwaHR0cHM6Ly93d3cud2lqa29wZW5hdXRvcy5ubC9pbnNwZWN0aW9uL2UwZTg0M2Y4NGEzZDRjYjc4MDYwMGU2NDEzNzc1NmEyLz9NSUQ9TkxfQ1JNXzFfM18wXzE5NjY3MF8yNDE5NTgwMTEyNDdfMSZ0bXM9MTcwOTg5Mzc4MyZ1dG1fc291cmNlPUNSTSZ1dG1fbWVkaXVtPWVtYWlsJnV0bV9jYW1wYWlnbj0zXzdXBXNwY2V1Qgpl6bro6mX9yH0mUhJidWxhYmVlckBhc2Rhc2QubmxYBAAAAAw~\" style=\"margin: 0; Margin: 0; line-height: 1.3; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: bold; color: #fefefe; text-decoration: none; display: inline-block; background: #f17130; border: 0 solid #f17130; width: 400px; text-align: center; padding: 5px; border-radius: 5px;\">Ontvang nu jouw prijs&nbsp;&nbsp;<b>&gt;</b></a></td>";
string convertedAnchorTags = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
string convertedAnchorTags = ConversionUtility.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
// Check that conversion works as expected.
Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
}
/// <summary>
/// Tests the conversion of a complex anchor tag within a table cell to open in a new tab.
/// </summary>
[Test]
public void TestAnchorTabConversionComplex4()
{
string anchorHtml = "<a href=\"https://www.maxmind.com/en/account/set-password?token=FEA9D6D78B624D6BB048687F4D0A2DD9\">https://www<span>.</span>maxmind<span>.</span>com/en/account/set-password?token=FEA9D6D78B624D6BB048687F4D0A2DD9</a>";
string convertedAnchorTags = ConversionUtility.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
// Check that conversion works as expected.
Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
}
/// <summary>
/// Tests the conversion of a complex anchor tag with existing target="_blank" attribute to
/// not get a second attribute after conversion.
/// </summary>
[Test]
public void TestAnchorTabConversionComplex5()
{
string anchorHtml = "<a href=\"test.html\" target=\"_blank\">test anchor text</a>";
string convertedAnchorTags = ConversionUtility.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
int targetBlankCount = TargetBlankRegex().Matches(convertedAnchorTags).Count;
Assert.Multiple(() =>
{
// Check that only one target="_blank" appears.
Assert.That(targetBlankCount, Is.EqualTo(1), "There should be exactly one target=\"_blank\" attribute.");
// Ensure other attributes are preserved
Assert.That(convertedAnchorTags, Does.Contain("href=\"test.html\""), "The href attribute should be preserved.");
// Check that the anchor text is preserved
Assert.That(convertedAnchorTags, Does.Contain(">test anchor text</a>"), "The anchor text should be preserved.");
});
}
/// <summary>
/// Regex to match an anchor tag with an href attribute, generated at compile time.
/// </summary>
/// <returns></returns>
[GeneratedRegex("target=\"_blank\"")]
private static partial Regex TargetBlankRegex();
}