Commit pre-existing working changes (multi-account, blocked domains, overrides)
This commit is contained in:
@@ -4,13 +4,14 @@ public sealed class SpamGuardOptions
|
||||
{
|
||||
public const string SectionName = "SpamGuard";
|
||||
|
||||
public ImapOptions Imap { get; set; } = new();
|
||||
public List<ImapOptions> Accounts { get; set; } = new();
|
||||
public ClaudeOptions Claude { get; set; } = new();
|
||||
public MonitoringOptions Monitoring { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ImapOptions
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string Host { get; set; } = "";
|
||||
public int Port { get; set; } = 993;
|
||||
public bool UseSsl { get; set; } = true;
|
||||
@@ -21,6 +22,7 @@ public sealed class ImapOptions
|
||||
public sealed class ClaudeOptions
|
||||
{
|
||||
public string ApiKey { get; set; } = "";
|
||||
public string BaseUrl { get; set; } = "https://api.anthropic.com/";
|
||||
public string Model { get; set; } = "claude-sonnet-4-6";
|
||||
public int MaxBodyLength { get; set; } = 2000;
|
||||
}
|
||||
|
||||
@@ -8,12 +8,21 @@ public sealed class SpamGuardOptionsValidator : IValidateOptions<SpamGuardOption
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.Imap.Host))
|
||||
errors.Add("SpamGuard:Imap:Host is required.");
|
||||
if (string.IsNullOrWhiteSpace(options.Imap.Username))
|
||||
errors.Add("SpamGuard:Imap:Username is required.");
|
||||
if (string.IsNullOrWhiteSpace(options.Imap.Password))
|
||||
errors.Add("SpamGuard:Imap:Password is required.");
|
||||
if (options.Accounts.Count == 0)
|
||||
errors.Add("SpamGuard:Accounts must contain at least one account.");
|
||||
|
||||
for (int i = 0; i < options.Accounts.Count; i++)
|
||||
{
|
||||
var a = options.Accounts[i];
|
||||
var prefix = $"SpamGuard:Accounts[{i}]";
|
||||
if (string.IsNullOrWhiteSpace(a.Host))
|
||||
errors.Add($"{prefix}:Host is required.");
|
||||
if (string.IsNullOrWhiteSpace(a.Username))
|
||||
errors.Add($"{prefix}:Username is required.");
|
||||
if (string.IsNullOrWhiteSpace(a.Password))
|
||||
errors.Add($"{prefix}:Password is required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.Claude.ApiKey))
|
||||
errors.Add("SpamGuard:Claude:ApiKey is required.");
|
||||
|
||||
|
||||
@@ -10,11 +10,22 @@ public enum Verdict
|
||||
Error
|
||||
}
|
||||
|
||||
public sealed record ActivityEntry(
|
||||
DateTime Timestamp,
|
||||
string Sender,
|
||||
string Subject,
|
||||
Verdict Verdict,
|
||||
double? Confidence,
|
||||
string? Reason
|
||||
);
|
||||
public enum UserOverride
|
||||
{
|
||||
MarkedSpam,
|
||||
MarkedLegitimate
|
||||
}
|
||||
|
||||
public sealed class ActivityEntry
|
||||
{
|
||||
public DateTime Timestamp { get; init; }
|
||||
public string Sender { get; init; } = "";
|
||||
public string Subject { get; init; } = "";
|
||||
public Verdict Verdict { get; set; }
|
||||
public double? Confidence { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
public uint Uid { get; init; }
|
||||
public string AccountName { get; init; } = "";
|
||||
public string? BodySnippet { get; init; }
|
||||
public UserOverride? Override { get; set; }
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ using SpamGuard.Services;
|
||||
using SpamGuard.State;
|
||||
using SpamGuard.Tray;
|
||||
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
|
||||
namespace SpamGuard;
|
||||
|
||||
static class Program
|
||||
@@ -52,32 +54,69 @@ static class Program
|
||||
services.AddSingleton<IValidateOptions<SpamGuardOptions>, SpamGuardOptionsValidator>();
|
||||
services.AddOptionsWithValidateOnStart<SpamGuardOptions>();
|
||||
|
||||
// State stores
|
||||
services.AddSingleton(new ProcessedUidStore(dataDir));
|
||||
services.AddSingleton(new TrustedSenderStore(dataDir));
|
||||
var opts = context.Configuration
|
||||
.GetSection(SpamGuardOptions.SectionName)
|
||||
.Get<SpamGuardOptions>()!;
|
||||
|
||||
// Services
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
var opts = sp.GetRequiredService<IOptions<SpamGuardOptions>>().Value;
|
||||
return new ActivityLog(opts.Monitoring.MaxActivityLogEntries);
|
||||
});
|
||||
// Shared state / services
|
||||
services.AddSingleton(new TrustedSenderStore(dataDir));
|
||||
services.AddSingleton(new BlockedDomainStore(dataDir));
|
||||
services.AddSingleton(new OverrideStore(dataDir));
|
||||
services.AddSingleton(new ActivityLog(opts.Monitoring.MaxActivityLogEntries, dataDir));
|
||||
services.AddSingleton<ImapClientFactory>();
|
||||
|
||||
// EmailClassifier with managed HttpClient (timeout + base address)
|
||||
// EmailClassifier with managed HttpClient
|
||||
services.AddHttpClient<EmailClassifier>(client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
client.BaseAddress = new Uri("https://api.anthropic.com/");
|
||||
client.BaseAddress = new Uri(opts.Claude.BaseUrl);
|
||||
});
|
||||
|
||||
// Background services
|
||||
services.AddSingleton<InboxMonitorService>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<InboxMonitorService>());
|
||||
services.AddHostedService<TrustedSenderService>();
|
||||
// Per-account monitor + trusted sender service
|
||||
var monitors = new List<InboxMonitorService>();
|
||||
foreach (var account in opts.Accounts)
|
||||
{
|
||||
var accountName = account.Name.Length > 0 ? account.Name : account.Username;
|
||||
var uidStore = new ProcessedUidStore(dataDir, accountName);
|
||||
|
||||
services.AddHostedService(sp => new TrustedSenderService(
|
||||
sp.GetRequiredService<ImapClientFactory>(),
|
||||
sp.GetRequiredService<TrustedSenderStore>(),
|
||||
account,
|
||||
opts.Monitoring,
|
||||
sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<TrustedSenderService>>()));
|
||||
|
||||
var capturedAccount = account;
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
var monitor = new InboxMonitorService(
|
||||
sp.GetRequiredService<ImapClientFactory>(),
|
||||
sp.GetRequiredService<TrustedSenderStore>(),
|
||||
sp.GetRequiredService<BlockedDomainStore>(),
|
||||
uidStore,
|
||||
sp.GetRequiredService<EmailClassifier>(),
|
||||
sp.GetRequiredService<ActivityLog>(),
|
||||
capturedAccount,
|
||||
opts.Monitoring,
|
||||
sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<InboxMonitorService>>());
|
||||
monitors.Add(monitor);
|
||||
return monitor;
|
||||
});
|
||||
}
|
||||
|
||||
// Force singleton creation so monitors list is populated before host starts
|
||||
services.AddHostedService(sp =>
|
||||
{
|
||||
// Resolve all InboxMonitorService singletons
|
||||
sp.GetServices<InboxMonitorService>();
|
||||
return new MonitorHostedServiceRunner(monitors);
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
|
||||
// Trigger DI resolution of InboxMonitorService singletons
|
||||
host.Services.GetServices<InboxMonitorService>();
|
||||
|
||||
host.Start();
|
||||
|
||||
Application.Run(new TrayApplicationContext(host));
|
||||
@@ -94,3 +133,18 @@ static class Program
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Bridges per-account InboxMonitorService instances into the hosted service pipeline.</summary>
|
||||
sealed class MonitorHostedServiceRunner : IHostedService
|
||||
{
|
||||
private readonly List<InboxMonitorService> _monitors;
|
||||
|
||||
public MonitorHostedServiceRunner(List<InboxMonitorService> monitors)
|
||||
=> _monitors = monitors;
|
||||
|
||||
public Task StartAsync(CancellationToken ct)
|
||||
=> Task.WhenAll(_monitors.Select(m => m.StartAsync(ct)));
|
||||
|
||||
public Task StopAsync(CancellationToken ct)
|
||||
=> Task.WhenAll(_monitors.Select(m => m.StopAsync(ct)));
|
||||
}
|
||||
|
||||
@@ -3,24 +3,20 @@ namespace SpamGuard.Services;
|
||||
using MailKit.Net.Imap;
|
||||
using MailKit.Security;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SpamGuard.Configuration;
|
||||
|
||||
public sealed class ImapClientFactory
|
||||
{
|
||||
private readonly SpamGuardOptions _options;
|
||||
private readonly ILogger<ImapClientFactory> _logger;
|
||||
|
||||
public ImapClientFactory(IOptions<SpamGuardOptions> options, ILogger<ImapClientFactory> logger)
|
||||
public ImapClientFactory(ILogger<ImapClientFactory> logger)
|
||||
{
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ImapClient> CreateConnectedClientAsync(CancellationToken ct = default)
|
||||
public async Task<ImapClient> CreateConnectedClientAsync(ImapOptions imap, CancellationToken ct = default)
|
||||
{
|
||||
var client = new ImapClient();
|
||||
var imap = _options.Imap;
|
||||
|
||||
_logger.LogDebug("Connecting to {Host}:{Port} (SSL={UseSsl})", imap.Host, imap.Port, imap.UseSsl);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ using MailKit;
|
||||
using MailKit.Search;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MimeKit;
|
||||
using SpamGuard.Configuration;
|
||||
using SpamGuard.State;
|
||||
@@ -14,25 +13,28 @@ public sealed class TrustedSenderService : BackgroundService
|
||||
{
|
||||
private readonly ImapClientFactory _imapFactory;
|
||||
private readonly TrustedSenderStore _store;
|
||||
private readonly SpamGuardOptions _options;
|
||||
private readonly ImapOptions _imap;
|
||||
private readonly MonitoringOptions _monitoring;
|
||||
private readonly ILogger<TrustedSenderService> _logger;
|
||||
private bool _initialScanDone;
|
||||
|
||||
public TrustedSenderService(
|
||||
ImapClientFactory imapFactory,
|
||||
TrustedSenderStore store,
|
||||
IOptions<SpamGuardOptions> options,
|
||||
ImapOptions imap,
|
||||
MonitoringOptions monitoring,
|
||||
ILogger<TrustedSenderService> logger)
|
||||
{
|
||||
_imapFactory = imapFactory;
|
||||
_store = store;
|
||||
_options = options.Value;
|
||||
_imap = imap;
|
||||
_monitoring = monitoring;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("TrustedSenderService started");
|
||||
_logger.LogInformation("TrustedSenderService started for {Account}", _imap.Username);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -51,22 +53,22 @@ public sealed class TrustedSenderService : BackgroundService
|
||||
_logger.LogError(ex, "Error scanning sent folder");
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMinutes(_options.Monitoring.TrustedSenderRefreshMinutes), stoppingToken);
|
||||
await Task.Delay(TimeSpan.FromMinutes(_monitoring.TrustedSenderRefreshMinutes), stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ScanSentFolderAsync(CancellationToken ct)
|
||||
{
|
||||
using var client = await _imapFactory.CreateConnectedClientAsync(ct);
|
||||
using var client = await _imapFactory.CreateConnectedClientAsync(_imap, ct);
|
||||
|
||||
var sentFolder = client.GetFolder(MailKit.SpecialFolder.Sent)
|
||||
var sentFolder = await FindSentFolderAsync(client, ct)
|
||||
?? throw new InvalidOperationException("Could not find Sent folder");
|
||||
|
||||
await sentFolder.OpenAsync(FolderAccess.ReadOnly, ct);
|
||||
|
||||
// After initial full scan, only check messages from the last refresh period
|
||||
var query = _initialScanDone
|
||||
? SearchQuery.DeliveredAfter(DateTime.UtcNow.AddMinutes(-_options.Monitoring.TrustedSenderRefreshMinutes))
|
||||
? SearchQuery.DeliveredAfter(DateTime.UtcNow.AddMinutes(-_monitoring.TrustedSenderRefreshMinutes))
|
||||
: SearchQuery.All;
|
||||
|
||||
var uids = await sentFolder.SearchAsync(query, ct);
|
||||
@@ -84,6 +86,37 @@ public sealed class TrustedSenderService : BackgroundService
|
||||
await client.DisconnectAsync(true, ct);
|
||||
}
|
||||
|
||||
private static readonly string[] SentFolderNames = ["Sent", "Sent Items", "Sent Messages", "INBOX.Sent"];
|
||||
|
||||
private async Task<IMailFolder?> FindSentFolderAsync(MailKit.Net.Imap.ImapClient client, CancellationToken ct)
|
||||
{
|
||||
// Try special folder first (requires SPECIAL-USE or XLIST extension)
|
||||
try
|
||||
{
|
||||
var sent = client.GetFolder(MailKit.SpecialFolder.Sent);
|
||||
if (sent != null) return sent;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "SPECIAL-USE not supported, falling back to folder name lookup");
|
||||
}
|
||||
|
||||
// Fall back to well-known folder names
|
||||
try
|
||||
{
|
||||
var personal = client.GetFolder(client.PersonalNamespaces[0]);
|
||||
var folders = await personal.GetSubfoldersAsync(false, ct);
|
||||
return folders.FirstOrDefault(f =>
|
||||
SentFolderNames.Any(name => f.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not enumerate folders to find Sent folder");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void ExtractAddresses(HeaderList headers, List<string> addresses)
|
||||
{
|
||||
foreach (var headerName in new[] { "To", "Cc" })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
@@ -20,4 +20,10 @@
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
77
src/SpamGuard/State/BlockedDomainStore.cs
Normal file
77
src/SpamGuard/State/BlockedDomainStore.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
namespace SpamGuard.State;
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
public sealed class BlockedDomainStore
|
||||
{
|
||||
private readonly string _filePath;
|
||||
private readonly HashSet<string> _domains;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public BlockedDomainStore(string dataDirectory)
|
||||
{
|
||||
_filePath = Path.Combine(dataDirectory, "blocked-domains.json");
|
||||
_domains = Load();
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get { lock (_lock) { return _domains.Count; } }
|
||||
}
|
||||
|
||||
public bool IsBlocked(string email)
|
||||
{
|
||||
var domain = ExtractDomain(email);
|
||||
if (string.IsNullOrEmpty(domain)) return false;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
return _domains.Contains(domain);
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(string domain)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_domains.Add(Normalize(domain));
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(string domain)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_domains.Remove(Normalize(domain));
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(_domains);
|
||||
var tempPath = _filePath + ".tmp";
|
||||
File.WriteAllText(tempPath, json);
|
||||
File.Move(tempPath, _filePath, overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ExtractDomain(string email)
|
||||
{
|
||||
var at = email.IndexOf('@');
|
||||
return at >= 0 ? Normalize(email[(at + 1)..]) : "";
|
||||
}
|
||||
|
||||
private static string Normalize(string domain) => domain.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>();
|
||||
}
|
||||
}
|
||||
85
src/SpamGuard/State/OverrideStore.cs
Normal file
85
src/SpamGuard/State/OverrideStore.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
namespace SpamGuard.State;
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
public sealed record OverrideRecord(
|
||||
DateTime Timestamp,
|
||||
string Sender,
|
||||
string Subject,
|
||||
string? BodySnippet,
|
||||
string UserVerdict,
|
||||
string OriginalVerdict
|
||||
);
|
||||
|
||||
public sealed class OverrideStore
|
||||
{
|
||||
private readonly string _filePath;
|
||||
private readonly List<OverrideRecord> _overrides;
|
||||
private readonly object _lock = new();
|
||||
private const int MaxOverrides = 50;
|
||||
|
||||
public OverrideStore(string dataDirectory)
|
||||
{
|
||||
_filePath = Path.Combine(dataDirectory, "overrides.json");
|
||||
_overrides = Load();
|
||||
}
|
||||
|
||||
public void Add(OverrideRecord record)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_overrides.Add(record);
|
||||
while (_overrides.Count > MaxOverrides)
|
||||
_overrides.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
public List<OverrideRecord> GetRecent(int count = 10)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _overrides
|
||||
.OrderByDescending(o => o.Timestamp)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public string BuildFewShotText(int count = 10)
|
||||
{
|
||||
var recent = GetRecent(count);
|
||||
if (recent.Count == 0)
|
||||
return "";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("The user has previously corrected these classifications:");
|
||||
foreach (var o in recent)
|
||||
{
|
||||
sb.AppendLine($"- [From: {o.Sender}, Subject: {o.Subject}] was originally classified as {o.OriginalVerdict} but the user marked it as {o.UserVerdict}.");
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(_overrides, new JsonSerializerOptions { WriteIndented = true });
|
||||
var tempPath = _filePath + ".tmp";
|
||||
File.WriteAllText(tempPath, json);
|
||||
File.Move(tempPath, _filePath, overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
private List<OverrideRecord> Load()
|
||||
{
|
||||
if (!File.Exists(_filePath))
|
||||
return new List<OverrideRecord>();
|
||||
|
||||
var json = File.ReadAllText(_filePath);
|
||||
return JsonSerializer.Deserialize<List<OverrideRecord>>(json)
|
||||
?? new List<OverrideRecord>();
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,10 @@ public sealed class ProcessedUidStore
|
||||
private readonly Dictionary<uint, DateTime> _uids;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public ProcessedUidStore(string dataDirectory)
|
||||
public ProcessedUidStore(string dataDirectory, string accountName)
|
||||
{
|
||||
_filePath = Path.Combine(dataDirectory, "processed-uids.json");
|
||||
var safeName = string.Concat(accountName.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
|
||||
_filePath = Path.Combine(dataDirectory, $"processed-uids-{safeName}.json");
|
||||
_uids = Load();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
{
|
||||
"SpamGuard": {
|
||||
"Imap": {
|
||||
"Host": "imap.example.com",
|
||||
"Port": 993,
|
||||
"UseSsl": true,
|
||||
"Username": "user@example.com",
|
||||
"Password": ""
|
||||
},
|
||||
"Accounts": [
|
||||
{
|
||||
"Name": "Work",
|
||||
"Host": "imap.dynu.com",
|
||||
"Port": 993,
|
||||
"UseSsl": true,
|
||||
"Username": "peter.foster@ukdataservices.co.uk",
|
||||
"Password": "Piglet69!"
|
||||
},
|
||||
{
|
||||
"Name": "Personal",
|
||||
"Host": "mail.hover.com",
|
||||
"Port": 993,
|
||||
"UseSsl": true,
|
||||
"Username": "peter@foster.net",
|
||||
"Password": "Piglet1969!!"
|
||||
}
|
||||
],
|
||||
"Claude": {
|
||||
"ApiKey": "",
|
||||
"Model": "claude-sonnet-4-6",
|
||||
"ApiKey": "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",
|
||||
"BaseUrl": "https://openrouter.ai/api/",
|
||||
"Model": "anthropic/claude-3.5-haiku",
|
||||
"MaxBodyLength": 2000
|
||||
},
|
||||
"Monitoring": {
|
||||
|
||||
@@ -12,7 +12,7 @@ public class ProcessedUidStoreTests : IDisposable
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"spamguard-test-{Guid.NewGuid()}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
_store = new ProcessedUidStore(_tempDir);
|
||||
_store = new ProcessedUidStore(_tempDir, "TestAccount");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -50,7 +50,7 @@ public class ProcessedUidStoreTests : IDisposable
|
||||
_store.Add(3);
|
||||
_store.Save();
|
||||
|
||||
var loaded = new ProcessedUidStore(_tempDir);
|
||||
var loaded = new ProcessedUidStore(_tempDir, "TestAccount");
|
||||
Assert.True(loaded.Contains(1));
|
||||
Assert.True(loaded.Contains(2));
|
||||
Assert.True(loaded.Contains(3));
|
||||
|
||||
Reference in New Issue
Block a user