feat: add TrustedSenderStore with case-insensitive lookup and persistence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
68
src/SpamGuard/State/TrustedSenderStore.cs
Normal file
68
src/SpamGuard/State/TrustedSenderStore.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// src/SpamGuard/State/TrustedSenderStore.cs
|
||||||
|
namespace SpamGuard.State;
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
public sealed class TrustedSenderStore
|
||||||
|
{
|
||||||
|
private readonly string _filePath;
|
||||||
|
private readonly HashSet<string> _senders;
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
public TrustedSenderStore(string dataDirectory)
|
||||||
|
{
|
||||||
|
_filePath = Path.Combine(dataDirectory, "trusted-senders.json");
|
||||||
|
_senders = Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get { lock (_lock) { return _senders.Count; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsTrusted(string email)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _senders.Contains(Normalize(email));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(string email)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_senders.Add(Normalize(email));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRange(IEnumerable<string> emails)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
foreach (var email in emails)
|
||||||
|
_senders.Add(Normalize(email));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(_senders);
|
||||||
|
File.WriteAllText(_filePath, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string email) => email.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
private HashSet<string> Load()
|
||||||
|
{
|
||||||
|
if (!File.Exists(_filePath))
|
||||||
|
return new HashSet<string>();
|
||||||
|
|
||||||
|
var json = File.ReadAllText(_filePath);
|
||||||
|
return JsonSerializer.Deserialize<HashSet<string>>(json)
|
||||||
|
?? new HashSet<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
79
tests/SpamGuard.Tests/State/TrustedSenderStoreTests.cs
Normal file
79
tests/SpamGuard.Tests/State/TrustedSenderStoreTests.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// tests/SpamGuard.Tests/State/TrustedSenderStoreTests.cs
|
||||||
|
namespace SpamGuard.Tests.State;
|
||||||
|
|
||||||
|
using SpamGuard.State;
|
||||||
|
|
||||||
|
public class TrustedSenderStoreTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _tempDir;
|
||||||
|
private readonly TrustedSenderStore _store;
|
||||||
|
|
||||||
|
public TrustedSenderStoreTests()
|
||||||
|
{
|
||||||
|
_tempDir = Path.Combine(Path.GetTempPath(), $"spamguard-test-{Guid.NewGuid()}");
|
||||||
|
Directory.CreateDirectory(_tempDir);
|
||||||
|
_store = new TrustedSenderStore(_tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_tempDir))
|
||||||
|
Directory.Delete(_tempDir, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsTrusted_ReturnsFalse_WhenEmpty()
|
||||||
|
{
|
||||||
|
Assert.False(_store.IsTrusted("someone@example.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsTrusted_ReturnsTrue_AfterAdd()
|
||||||
|
{
|
||||||
|
_store.Add("someone@example.com");
|
||||||
|
Assert.True(_store.IsTrusted("someone@example.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsTrusted_IsCaseInsensitive()
|
||||||
|
{
|
||||||
|
_store.Add("Someone@Example.COM");
|
||||||
|
Assert.True(_store.IsTrusted("someone@example.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsTrusted_TrimsWhitespace()
|
||||||
|
{
|
||||||
|
_store.Add(" someone@example.com ");
|
||||||
|
Assert.True(_store.IsTrusted("someone@example.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddRange_AddsMultiple()
|
||||||
|
{
|
||||||
|
_store.AddRange(["a@b.com", "c@d.com", "e@f.com"]);
|
||||||
|
Assert.True(_store.IsTrusted("a@b.com"));
|
||||||
|
Assert.True(_store.IsTrusted("c@d.com"));
|
||||||
|
Assert.True(_store.IsTrusted("e@f.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_And_Load_RoundTrips()
|
||||||
|
{
|
||||||
|
_store.Add("a@b.com");
|
||||||
|
_store.Add("c@d.com");
|
||||||
|
_store.Save();
|
||||||
|
|
||||||
|
var loaded = new TrustedSenderStore(_tempDir);
|
||||||
|
Assert.True(loaded.IsTrusted("a@b.com"));
|
||||||
|
Assert.True(loaded.IsTrusted("c@d.com"));
|
||||||
|
Assert.False(loaded.IsTrusted("x@y.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Count_ReturnsCorrectValue()
|
||||||
|
{
|
||||||
|
_store.AddRange(["a@b.com", "c@d.com"]);
|
||||||
|
Assert.Equal(2, _store.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user