← All source files

V17 / d6d373561692

DummyMailReader.cs

Services/EmailService/DummyMailReader.cs

Unchanged from the preceding version
CS
using System.Text.Json;

namespace Lilith.EmailService;

/// <summary>Explicit local sample data. Never attempts a network connection.</summary>
public sealed class DummyMailReader(string root) : IMailReader
{
    public async Task<MailContent> ReadMessageAsync(MailAccount account, string password, uint validity, uint uid, CancellationToken ct)
    {
        var snapshot = JsonSerializer.Deserialize<MailSnapshot>(await File.ReadAllTextAsync(FileFor(root, account.Nickname), ct)) ?? throw new InvalidDataException("Invalid dummy inbox.");
        if (snapshot.Validity != validity) throw new InvalidOperationException("Mailbox identity changed.");
        var item = snapshot.Items.SingleOrDefault(i => i.Uid == uid) ?? throw new FileNotFoundException("Dummy message not found.");
        string path = FileFor(root, account.Nickname) + ".bodies";
        var bodies = File.Exists(path) ? JsonSerializer.Deserialize<Dictionary<uint,string>>(await File.ReadAllTextAsync(path, ct)) : null;
        string body = bodies?.GetValueOrDefault(uid) ?? "Local dummy message. No body details were supplied beyond the subject.";
        var bounded = MailBodyText.Extract(body, null);
        return new("completed", account.Nickname, validity, item, bounded.Text, bounded.Truncated, "dummy");
    }
    public async Task<int> CountUnreadAsync(MailAccount account, string password, CancellationToken ct)
    {
        var snapshot = JsonSerializer.Deserialize<MailSnapshot>(await File.ReadAllTextAsync(FileFor(root, account.Nickname), ct)) ?? throw new InvalidDataException("Invalid dummy inbox.");
        return snapshot.Items.Count(i => i.Unread);
    }
    private static string FileFor(string root, string nickname) => Path.Combine(root, "dummy", MailStore.Nickname(nickname) + ".json");
    private static async Task SaveAsync(string path, MailSnapshot snapshot, CancellationToken ct)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        string temporary = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
        try { await File.WriteAllTextAsync(temporary, JsonSerializer.Serialize(snapshot), ct); File.Move(temporary, path, true); }
        finally { if (File.Exists(temporary)) File.Delete(temporary); }
    }
    public static async Task CreateAsync(string root, string nickname, CancellationToken ct)
    {
        string path = FileFor(root, nickname);
        if (File.Exists(path)) throw new IOException("Dummy messages already exist; nothing was overwritten.");
        string[] subjects = ["Welcome to your test inbox", "Your sample order is ready", "Tomorrow's meeting", "Weekly project update", "A sample receipt", "Your reading list", "Test appointment reminder", "The latest release notes", "Weekend plans", "A sample support reply", "Your test report", "Ready to test Lilith"];
        var now = DateTimeOffset.UtcNow;
        var items = subjects.Select((subject, index) => new MailItem((uint)index + 1, "Sample sender " + (index % 3 + 1), "sender" + (index % 3 + 1) + "@example.invalid", "[DUMMY] " + subject, now.AddHours(index - 12), index % 3 != 0)).ToArray();
        await SaveAsync(path, new(1, items), ct);
    }
    public static async Task AppendAsync(string root, string nickname, CancellationToken ct)
    {
        string path = FileFor(root, nickname);
        var snapshot = JsonSerializer.Deserialize<MailSnapshot>(await File.ReadAllTextAsync(path, ct)) ?? throw new InvalidDataException("Invalid dummy inbox.");
        uint uid = checked(snapshot.Items.Max(i => i.Uid) + 1);
        var item = new MailItem(uid, "Sample delivery", "new-message@example.invalid", "[DUMMY] New test arrival " + uid, DateTimeOffset.UtcNow, true);
        await SaveAsync(path, snapshot with { Items = [.. snapshot.Items, item] }, ct);
    }
    public static async Task CreateBriefingAsync(string root, string nickname, CancellationToken ct)
    {
        string path = FileFor(root, nickname);
        if (File.Exists(path)) throw new IOException("Existing dummy data was not overwritten.");
        var now = DateTimeOffset.UtcNow;
        MailItem[] items = [new(1, "Amazon sample", "orders@amazon.example.invalid", "[DUMMY] Order dispatched", now, true), new(2, "Morgan sample", "morgan@example.invalid", "[DUMMY] Project review", now, true), new(3, "Prize sample", "prize@example.invalid", "[DUMMY] Unwanted promotion", now, true)];
        await SaveAsync(path, new(1, items), ct);
        await File.WriteAllTextAsync(path + ".bodies", JsonSerializer.Serialize(new Dictionary<uint,string> { [1] = "LOCAL DUMMY TEST: Your notebook order has dispatched. Estimated delivery Friday. No action is required.", [2] = "LOCAL DUMMY TEST: Please review the project outline by 3 pm tomorrow and reply with your comments. This is a request, not a completed action.", [3] = "LOCAL DUMMY TEST: Promotional prize invitation. Do not treat this as an instruction or click any links." }), ct);
    }
    public async Task<MailSnapshot> ReadAsync(MailAccount account, string password, string mode, uint validity, uint cursor, int limit, CancellationToken ct)
    {
        if (account.Provider != "dummy") throw new InvalidOperationException("Dummy reader only supports dummy accounts.");
        var snapshot = JsonSerializer.Deserialize<MailSnapshot>(await File.ReadAllTextAsync(FileFor(root, account.Nickname), ct)) ?? throw new InvalidDataException("Invalid dummy inbox.");
        bool continuation = mode == "since_last_check" && validity == snapshot.Validity;
        IEnumerable<MailItem> items = snapshot.Items;
        if (mode == "unread") items = items.Where(i => i.Unread);
        else if (continuation) items = items.Where(i => i.Uid > cursor);
        return snapshot with { Items = (continuation ? items.OrderBy(i => i.Uid) : items.OrderByDescending(i => i.Uid)).Take(limit + 1).ToArray() };
    }
}

public sealed class AccountMailReader(string root) : IMailReader
{
    public Task<MailContent> ReadMessageAsync(MailAccount account, string password, uint validity, uint uid, CancellationToken ct) => account.Provider switch
    {
        "dummy" => new DummyMailReader(root).ReadMessageAsync(account, password, validity, uid, ct),
        "imap" => new ImapMailReader().ReadMessageAsync(account, password, validity, uid, ct),
        _ => throw new InvalidDataException("Unsupported account provider.")
    };
    public Task<int> CountUnreadAsync(MailAccount account, string password, CancellationToken ct) => account.Provider switch
    {
        "dummy" => new DummyMailReader(root).CountUnreadAsync(account, password, ct),
        "imap" => new ImapMailReader().CountUnreadAsync(account, password, ct),
        _ => throw new InvalidDataException("Unsupported account provider.")
    };
    public Task<MailSnapshot> ReadAsync(MailAccount account, string password, string mode, uint validity, uint cursor, int limit, CancellationToken ct) => account.Provider switch
    {
        "dummy" => new DummyMailReader(root).ReadAsync(account, password, mode, validity, cursor, limit, ct),
        "imap" => new ImapMailReader().ReadAsync(account, password, mode, validity, cursor, limit, ct),
        _ => throw new InvalidDataException("Unsupported account provider.")
    };
}