Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,5 @@ Wino/obj/x86/Debug/XamlSaveStateFile.xml
.vs/Wino/v16/.suo
/.claude/settings.local.json
scripts/translate_resources.local.bat
Wino-Safe-handoff.zip
claude-code-wino-mail-security-mode-full-prompt.md
5 changes: 5 additions & 0 deletions Wino.Core.Domain/Interfaces/IPreferencesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ public interface IPreferencesService : INotifyPropertyChanged
/// </summary>
bool IsShowPreviewEnabled { get; set; }

/// <summary>
/// Setting: Whether Mail Security Mode is enabled to block external content by default.
/// </summary>
bool IsSecurityModeEnabled { get; set; }

/// <summary>
/// Setting: Set whether 'img' tags in rendered HTMLs should be removed.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions Wino.Core.Domain/Translations/en_US/resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,9 @@
"KeyboardShortcuts_ActionToggleFlag": "Toggle flag",
"KeyboardShortcuts_ActionToggleArchive": "Toggle archive/unarchive",
"ImageRenderingDisabled": "Image rendering is disabled for this message.",
"SecurityMode_ImageRenderingDisabled": "Security Mode is enabled. External content has been blocked.",
"SettingsSecurityMode_Title": "Security Mode",
"SettingsSecurityMode_Description": "Block external images and remote content by default when reading emails.",
"ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method",
"ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security",
"IMAPAdvancedSetupDialog_ValidationAuthMethodRequired": "Authentication method is required",
Expand Down
149 changes: 149 additions & 0 deletions Wino.Core.Tests/Services/MailSecurityModeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using System.Reflection;
using FluentAssertions;
using HtmlAgilityPack;
using Wino.Core.Domain.Interfaces;
using Wino.Services.Extensions;
using Xunit;

namespace Wino.Core.Tests.Services;

public class MailSecurityModeTests
{
[Fact]
public void IsSecurityModeEnabled_Should_Exist_On_IPreferencesService()
{
var property = typeof(IPreferencesService).GetProperty(nameof(IPreferencesService.IsSecurityModeEnabled));

property.Should().NotBeNull();
property!.CanRead.Should().BeTrue();
property.CanWrite.Should().BeTrue();
property.PropertyType.Should().Be(typeof(bool));
}

[Fact]
public void IsSecurityModeEnabled_Should_Be_Excluded_From_Syncable_Properties()
{
var syncableProperties = typeof(IPreferencesService)
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0)
.Where(p => p.Name != nameof(IPreferencesService.DiagnosticId)
&& p.Name != nameof(IPreferencesService.IsSecurityModeEnabled))
.ToList();

syncableProperties.Should().NotContain(p => p.Name == nameof(IPreferencesService.IsSecurityModeEnabled),
"Security Mode should not be exported/imported to prevent silent security downgrades");
}

[Fact]
public void ClearImages_Should_Remove_CSS_Url_Tracking_In_Inline_Styles()
{
var document = new HtmlDocument();
document.LoadHtml("""
<html><body>
<div style="background-image:url('https://tracker.example/pixel.gif');color:blue;">content</div>
<p style="list-style-image:url(https://tracker.example/list.png);font-size:14px;">item</p>
</body></html>
""");

document.ClearImages();
var output = document.DocumentNode.OuterHtml;

output.Should().NotContain("tracker.example", "CSS url() references should be sanitized by ClearImages");
output.Should().Contain("color:blue", "safe CSS properties should be preserved");
output.Should().Contain("font-size:14px", "safe CSS properties should be preserved");
}

[Fact]
public void ClearImages_Should_Remove_CSS_Import_In_Style_Tags()
{
var document = new HtmlDocument();
document.LoadHtml("""
<html><head>
<style>
@import url('https://tracker.example/remote.css');
body { color: red; }
</style>
</head><body><p>content</p></body></html>
""");

document.ClearImages();
var output = document.DocumentNode.OuterHtml;

output.Should().NotContain("tracker.example", "@import directives should be removed");
output.Should().Contain("color: red", "safe CSS rules should remain");
}

[Fact]
public void ClearImages_Should_Remove_CSS_ImageSet_Tracking()
{
var document = new HtmlDocument();
document.LoadHtml("""
<html><body>
<div style="background:image-set(url('https://tracker.example/1x.png') 1x);color:green;">content</div>
</body></html>
""");

document.ClearImages();
var output = document.DocumentNode.OuterHtml;

output.Should().NotContain("tracker.example", "image-set() references should be sanitized");
output.Should().Contain("color:green", "safe CSS properties should be preserved");
}

[Fact]
public void ClearImages_Should_Handle_Audio_Video_Source_Tags()
{
var document = new HtmlDocument();
document.LoadHtml("""
<html><body>
<video src="https://tracker.example/video.mp4"></video>
<audio src="https://tracker.example/audio.mp3"></audio>
<video><source src="https://tracker.example/source.mp4" /></video>
</body></html>
""");

document.ClearImages();
var output = document.DocumentNode.OuterHtml;

output.Should().NotContain("tracker.example", "media element remote sources should be removed by ClearImages");
}

[Fact]
public void ClearImages_Should_Preserve_CID_And_Data_Images()
{
var document = new HtmlDocument();
document.LoadHtml("""
<html><body>
<img id="data-img" src="data:image/png;base64,iVBOR" />
<img id="cid-img" src="cid:image001@example" />
<img id="fragment" src="#local-ref" />
<img id="remote" src="https://example.com/track.png" />
</body></html>
""");

document.ClearImages();
var output = document.DocumentNode.OuterHtml;

output.Should().Contain("id=\"data-img\" src=\"data:image/png;base64,iVBOR\"", "data: images should be preserved");
output.Should().Contain("id=\"cid-img\" src=\"cid:image001@example\"", "cid: references should be preserved");
output.Should().Contain("id=\"fragment\" src=\"#local-ref\"", "fragment references should be preserved");
output.Should().NotContain("id=\"remote\" src=", "remote images should be removed");
}

[Fact]
public void ClearImages_Should_Not_Crash_On_Empty_Or_Malformed_Html()
{
var empty = new HtmlDocument();
empty.LoadHtml("");
var act1 = () => empty.ClearImages();
act1.Should().NotThrow();

var malformed = new HtmlDocument();
malformed.LoadHtml("<div><img src='https://x.com/a.png'><p>unclosed");
var act2 = () => malformed.ClearImages();
act2.Should().NotThrow();

var output = malformed.DocumentNode.OuterHtml;
output.Should().NotContain("x.com/a.png");
}
}
30 changes: 21 additions & 9 deletions Wino.Mail.ViewModels/MailRenderingPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,23 @@ public bool IsImageRenderingDisabled
{
get
{
if (forceImageLoading)
return false;

if (IsJunkMail)
{
return !forceImageLoading;
}
else
{
return !CurrentRenderModel?.MailRenderingOptions?.LoadImages ?? false;
}
return true;

if (PreferencesService.IsSecurityModeEnabled)
return true;

return !CurrentRenderModel?.MailRenderingOptions?.LoadImages ?? false;
}
}

public string ImageBlockedInfoBarMessage => PreferencesService.IsSecurityModeEnabled && !IsJunkMail
? Translator.SecurityMode_ImageRenderingDisabled
: Translator.ImageRenderingDisabled;

private bool isDarkWebviewRenderer;
public bool IsDarkWebviewRenderer
{
Expand Down Expand Up @@ -508,14 +514,19 @@ await ExecuteUIThread(() =>
CreationDate = initializedMailItemViewModel?.MailCopy.CreationDate ?? message.Date.DateTime;

// Automatically block remote image loading for Junk folder to reduce pixel tracking.
// This can only work for selected mail item rendering, not for EML file rendering.
if (initializedMailItemViewModel != null &&
initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk)
{
renderingOptions.LoadImages = false;
}

// Load images if forced.
// Block remote images when Security Mode is enabled.
if (PreferencesService.IsSecurityModeEnabled)
{
renderingOptions.LoadImages = false;
}

// Manual override: user explicitly chose to load images for this message.
if (ignoreJunkFilter)
{
renderingOptions.LoadImages = true;
Expand All @@ -529,6 +540,7 @@ await ExecuteUIThread(() =>
}

OnPropertyChanged(nameof(IsImageRenderingDisabled));
OnPropertyChanged(nameof(ImageBlockedInfoBarMessage));

StatePersistenceService.IsReadingMail = true;
});
Expand Down
9 changes: 8 additions & 1 deletion Wino.Mail.WinUI/Services/PreferencesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ public bool RenderPlaintextLinks
set => SetPropertyAndSave(nameof(RenderPlaintextLinks), value);
}

public bool IsSecurityModeEnabled
{
get => _configurationService.Get(nameof(IsSecurityModeEnabled), false);
set => SetPropertyAndSave(nameof(IsSecurityModeEnabled), value);
}

public bool RenderImages
{
get => _configurationService.Get(nameof(RenderImages), true);
Expand Down Expand Up @@ -653,7 +659,8 @@ private static IEnumerable<PropertyInfo> GetSyncablePreferenceProperties()
continue;
}

if (property.Name == nameof(IPreferencesService.DiagnosticId))
if (property.Name == nameof(IPreferencesService.DiagnosticId)
|| property.Name == nameof(IPreferencesService.IsSecurityModeEnabled))
{
continue;
}
Expand Down
2 changes: 1 addition & 1 deletion Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@
HorizontalContentAlignment="Stretch"
x:Load="{x:Bind ViewModel.IsImageRenderingDisabled, Mode=OneWay}"
IsOpen="True"
Message="{x:Bind domain:Translator.ImageRenderingDisabled}"
Message="{x:Bind ViewModel.ImageBlockedInfoBarMessage, Mode=OneWay}"
Severity="Warning">
<InfoBar.ActionButton>
<Button
Expand Down
7 changes: 7 additions & 0 deletions Wino.Mail.WinUI/Views/Settings/ReadComposePanePage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.PreferencesService.IsShowActionLabelsEnabled, Mode=TwoWay}" />
</controls:SettingsCard>

<controls:SettingsCard Description="{x:Bind domain:Translator.SettingsSecurityMode_Description}" Header="{x:Bind domain:Translator.SettingsSecurityMode_Title}">
<controls:SettingsCard.HeaderIcon>
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="&#xE72E;" />
</controls:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{x:Bind ViewModel.PreferencesService.IsSecurityModeEnabled, Mode=TwoWay}" />
</controls:SettingsCard>

<controls:SettingsCard Header="{x:Bind domain:Translator.SettingsLoadImages_Title}">
<controls:SettingsCard.HeaderIcon>
<PathIcon Data="F1 M 4.921875 16.25 C 4.433594 16.25 3.966471 16.150717 3.520508 15.952148 C 3.074544 15.753581 2.683919 15.486654 2.348633 15.151367 C 2.013346 14.816081 1.746419 14.425456 1.547852 13.979492 C 1.349284 13.533529 1.25 13.066406 1.25 12.578125 L 1.25 4.921875 C 1.25 4.433594 1.349284 3.966473 1.547852 3.520508 C 1.746419 3.074545 2.013346 2.68392 2.348633 2.348633 C 2.683919 2.013348 3.074544 1.74642 3.520508 1.547852 C 3.966471 1.349285 4.433594 1.25 4.921875 1.25 L 12.578125 1.25 C 13.066406 1.25 13.533528 1.349285 13.979492 1.547852 C 14.425455 1.74642 14.81608 2.013348 15.151367 2.348633 C 15.486653 2.68392 15.75358 3.074545 15.952148 3.520508 C 16.150715 3.966473 16.25 4.433594 16.25 4.921875 L 16.25 12.578125 C 16.25 13.066406 16.150715 13.533529 15.952148 13.979492 C 15.75358 14.425456 15.486653 14.816081 15.151367 15.151367 C 14.81608 15.486654 14.425455 15.753581 13.979492 15.952148 C 13.533528 16.150717 13.066406 16.25 12.578125 16.25 Z M 8.75 8.90625 C 9.082031 8.90625 9.401041 8.969727 9.707031 9.09668 C 10.013021 9.223633 10.283203 9.404297 10.517578 9.638672 L 14.658203 13.769531 C 14.768879 13.580729 14.853515 13.377279 14.912109 13.15918 C 14.970702 12.941081 14.999999 12.721354 15 12.5 L 15 4.951172 C 14.999999 4.625651 14.933268 4.314779 14.799805 4.018555 C 14.666341 3.722332 14.487305 3.461914 14.262695 3.237305 C 14.038086 3.012695 13.777669 2.83366 13.481445 2.700195 C 13.185221 2.566732 12.874348 2.5 12.548828 2.5 L 4.951172 2.5 C 4.625651 2.5 4.314778 2.566732 4.018555 2.700195 C 3.722331 2.83366 3.461914 3.012695 3.237305 3.237305 C 3.012695 3.461914 2.833659 3.722332 2.700195 4.018555 C 2.566732 4.314779 2.5 4.625651 2.5 4.951172 L 2.5 12.5 C 2.5 12.721354 2.529297 12.941081 2.587891 13.15918 C 2.646484 13.377279 2.73112 13.580729 2.841797 13.769531 L 6.982422 9.638672 C 7.216797 9.404297 7.486979 9.223633 7.792969 9.09668 C 8.098958 8.969727 8.417969 8.90625 8.75 8.90625 Z M 10.625 5.625 C 10.625 5.449219 10.657552 5.286459 10.722656 5.136719 C 10.78776 4.98698 10.877278 4.855145 10.991211 4.741211 C 11.105143 4.627279 11.236979 4.537761 11.386719 4.472656 C 11.536458 4.407553 11.699219 4.375 11.875 4.375 C 12.04427 4.375 12.205402 4.407553 12.358398 4.472656 C 12.511393 4.537761 12.644856 4.627279 12.758789 4.741211 C 12.872721 4.855145 12.962239 4.988607 13.027344 5.141602 C 13.092447 5.294597 13.124999 5.455729 13.125 5.625 C 13.124999 5.800781 13.092447 5.963542 13.027344 6.113281 C 12.962239 6.263021 12.872721 6.394857 12.758789 6.508789 C 12.644856 6.622722 12.513021 6.71224 12.363281 6.777344 C 12.213541 6.842449 12.050781 6.875001 11.875 6.875 C 11.699219 6.875001 11.53483 6.842449 11.381836 6.777344 C 11.228841 6.71224 11.097005 6.62435 10.986328 6.513672 C 10.87565 6.402996 10.78776 6.27116 10.722656 6.118164 C 10.657552 5.96517 10.625 5.800781 10.625 5.625 Z M 7.5 18.75 C 6.966146 18.75 6.455078 18.642578 5.966797 18.427734 C 5.478516 18.212891 5.058594 17.903646 4.707031 17.5 L 13.193359 17.5 C 13.785807 17.5 14.344075 17.382812 14.868164 17.148438 C 15.392252 16.914062 15.849609 16.59668 16.240234 16.196289 C 16.630859 15.795898 16.938477 15.332031 17.163086 14.804688 C 17.387695 14.277344 17.5 13.717448 17.5 13.125 L 17.5 4.707031 C 17.903645 5.058595 18.212891 5.478517 18.427734 5.966797 C 18.642578 6.455079 18.75 6.966146 18.75 7.5 L 18.75 13.125 C 18.75 13.89974 18.601887 14.628906 18.305664 15.3125 C 18.009439 15.996094 17.607422 16.591797 17.099609 17.099609 C 16.591797 17.607422 15.996094 18.009439 15.3125 18.305664 C 14.628906 18.601889 13.899739 18.75 13.125 18.75 Z M 12.5 15 C 12.721354 15 12.94108 14.970703 13.15918 14.912109 C 13.377278 14.853516 13.580729 14.768881 13.769531 14.658203 L 9.638672 10.517578 C 9.391275 10.270183 9.095052 10.146484 8.75 10.146484 C 8.404947 10.146484 8.108724 10.270183 7.861328 10.517578 L 3.730469 14.658203 C 3.919271 14.768881 4.122721 14.853516 4.34082 14.912109 C 4.558919 14.970703 4.778646 15 5 15 Z " />
Expand Down