This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
OutlookAddin/src/OutlookAddin.Core/Services/DiskRetryQueue.cs
T
PointStar e527aa80af feat(filing): scaffold Task #3 Feature 1 — file mail to Case/Contact
Core layer:
- FilingService: orchestrates batch filing with 401-aborts-batch, 409-treats-as-success, transient errors fall to DiskRetryQueue
- DiskRetryQueue: atomic write (tmp + rename), Hebrew UTF-8 round-trip, idempotent remove
- Models: MailEnvelope (STA-extracted primitives), FilingTarget, FilingOutcome enum, FilingItemResult, FilingBatchSummary, RetryQueueItem
- EspoCrmClient.CreateContactAsync for "create contact from sender" flow

UI layer:
- FileToCaseDialog (WPF, RTL, Hebrew labels) with grouped search results + recent sidebar
- FileToCaseViewModel: 250ms debounced GlobalSearch, create-contact-from-sender command, DialogResult signaling

Tests (38 passing):
- FilingServiceTests: happy path, 409, 401 aborts batch, transient enqueues, queue failure → Failed, target propagation, null guards
- DiskRetryQueueTests: id assignment, Hebrew round-trip, corrupt-file skip, idempotent remove, atomic overwrite

VSTO host project (OutlookAddin.csproj) regenerated; ThisAddIn wiring in follow-up commits.
gitignore: exclude .claude/settings.local.json (per-machine).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:04:10 +03:00

142 lines
4.8 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using MarcusLaw.OutlookAddin.Core.Models;
using Serilog;
namespace MarcusLaw.OutlookAddin.Core.Services
{
public sealed class DiskRetryQueue : IRetryQueue
{
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNameCaseInsensitive = true
};
private static readonly UTF8Encoding Utf8NoBom = new UTF8Encoding(false);
private readonly string _directory;
private readonly ILogger _logger;
public DiskRetryQueue(ILogger logger)
: this(DefaultDirectory(), logger)
{
}
public DiskRetryQueue(string directory, ILogger logger)
{
if (string.IsNullOrWhiteSpace(directory)) throw new ArgumentException("directory required", nameof(directory));
_directory = directory;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public string Directory => _directory;
public Task<string> EnqueueAsync(RetryQueueItem item, CancellationToken cancellationToken = default)
{
if (item == null) throw new ArgumentNullException(nameof(item));
EnsureDirectory();
if (string.IsNullOrEmpty(item.Id))
{
item.Id = Guid.NewGuid().ToString("N");
}
if (item.EnqueuedAt == default)
{
item.EnqueuedAt = DateTimeOffset.UtcNow;
}
var finalPath = Path.Combine(_directory, item.Id + ".json");
var tempPath = finalPath + ".tmp";
var json = JsonSerializer.Serialize(item, JsonOptions);
File.WriteAllText(tempPath, json, Utf8NoBom);
if (File.Exists(finalPath))
{
File.Delete(finalPath);
}
File.Move(tempPath, finalPath);
_logger.Information("Retry queue enqueued {Id} (messageId={MessageId})", item.Id, item.Request?.MessageId);
return Task.FromResult(item.Id);
}
public Task<IReadOnlyList<RetryQueueItem>> ListAllAsync(CancellationToken cancellationToken = default)
{
var result = new List<RetryQueueItem>();
if (!System.IO.Directory.Exists(_directory))
{
return Task.FromResult<IReadOnlyList<RetryQueueItem>>(result);
}
foreach (var path in System.IO.Directory.EnumerateFiles(_directory, "*.json"))
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var json = File.ReadAllText(path, Encoding.UTF8);
var item = JsonSerializer.Deserialize<RetryQueueItem>(json, JsonOptions);
if (item != null)
{
if (string.IsNullOrEmpty(item.Id))
{
item.Id = Path.GetFileNameWithoutExtension(path);
}
result.Add(item);
}
}
catch (JsonException ex)
{
_logger.Warning(ex, "Retry queue: skipping corrupt file {Path}", path);
}
catch (IOException ex)
{
_logger.Warning(ex, "Retry queue: cannot read {Path}", path);
}
}
return Task.FromResult<IReadOnlyList<RetryQueueItem>>(result);
}
public Task RemoveAsync(string id, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("id required", nameof(id));
var path = Path.Combine(_directory, id + ".json");
if (File.Exists(path))
{
File.Delete(path);
_logger.Information("Retry queue removed {Id}", id);
}
return Task.CompletedTask;
}
private void EnsureDirectory()
{
if (!System.IO.Directory.Exists(_directory))
{
System.IO.Directory.CreateDirectory(_directory);
}
}
private static string DefaultDirectory()
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MarcusLaw",
"OutlookAddin",
"retry-queue");
}
}
}