namespace WpywMail.Native; /// /// 邮件存储的统一接口。 /// /// 之所以要抽出接口:v2.0.x 只有 一种实现,任何一次改动 /// (哪怕只是把一封邮件标记为已读)都要把**全部邮件**重新序列化并整文件重写, /// 且 UID 分配、搜索、统计全是 O(N) 全表扫描。引入 后 /// 上层(API / IMAP / SMTP / 投递队列)不需要知道底下是 JSON 还是 SQLite。 /// public interface IMailStore : IDisposable { /// 每次写入自增,供长轮询判断「有没有新变化」。 long Version { get; } // ---------------------------------------------------------------- 用户 MailUser? FindUser(string email); /// /// 不看过滤 active 的查号。用途:① 投递(信箱先存在、访问才受控); /// ② 登录时区分「密码错」与「账号未激活/已停用」(真实原因只进审计)。 /// MailUser? FindUserAnyState(string email); MailUser? Authenticate(string email, string password); bool IsLocalAddress(string email); IReadOnlyList ListUsers(); MailUser CreateUser(string email, string password, string displayName); /// 用已经算好的哈希建号(注册验证通过时用,避免明文密码再走一遍内存)。 MailUser CreateUserWithHash(string email, string passwordHash, string passwordSalt, string displayName, bool active = true); void ChangePassword(string email, string password); void SetUserActive(string email, bool active); /// /// 彻底删除一个账号:用户行 + 它的会话 + 验证码 + 它名下的出站队列。 /// **邮件不在这里删** —— 调用方要先 ListMessages + DeleteMessage(permanent:true) 逐封删, /// 那样才会顺带回收无引用的大对象。审计保留(删除动作本身也会写一条审计)。 /// bool DeleteUser(string email); // ---------------------------------------------------------------- 会话 SessionRecord CreateSession(string email, int days); SessionRecord? GetSession(string? token); void RemoveSession(string token); /// 列出某个账号的全部活跃会话(用于「在哪登录了 / 退出其他设备」)。 IReadOnlyList ListSessions(string email); /// 吊销该账号的会话;keepToken 非空时保留它(即「退出其他设备」)。返回吊销数量。 int RemoveSessions(string email, string? keepToken); // ---------------------------------------------------------------- 账号体系(注册 / 验证码 / 审计) /// 把用户资料写回存储(目前只有显示名)。 void UpdateProfile(string email, string displayName); /// 记录一次成功登录时间。 void SetLastLogin(string email); /// 保存验证码(同一 email+purpose 覆盖旧的)。只存哈希。 void SaveVerificationCode(VerificationCode code); VerificationCode? FindVerificationCode(string email, string purpose); /// 验证码试错次数 +1,返回自增后的次数。 int IncrementVerificationAttempts(string email, string purpose); void RemoveVerificationCode(string email, string purpose); /// 写一条认证审计。 void RecordAuthEvent(AuthEvent entry); /// 按条件查审计(都为 null 表示不限制);limit 为 0 时用实现自己的默认上限。 IReadOnlyList ListAuthEvents(string? email, string? ip, string? reason, int limit); /// 统计窗口期内的认证事件数量(登录锁定、注册与重发限流都用它)。 int CountAuthEvents(string? email, string? ip, string? reason, bool? success, int minutes); // ---------------------------------------------------------------- 原始报文与附件 string SaveRaw(byte[] raw); byte[] ReadRaw(string relativePath); string SaveAttachment(byte[] data, string suggestedName); byte[] ReadAttachment(string relativePath); // ---------------------------------------------------------------- 邮件 MailMessage SaveMessage(MailMessage message, byte[]? raw = null); MailMessage? DeliverLocal(string recipient, byte[] raw, string sender); MailMessage QueueOutbound(string owner, string[] recipients, string subject, string text, byte[] raw, string cc = "", string html = "", string inReplyTo = "", IReadOnlyList? attachments = null); MailMessage CreateBounce(string owner, string originalSubject, string[] recipients, string error, string originalRawPath); IReadOnlyList ListMessages(string owner, string folder, string query); /// 分页查询:只取需要的一页(SQLite 直接下推到 SQL,不再把整个邮箱读进内存)。 (int Total, IReadOnlyList Messages) ListMessagesPage( string owner, string folder, string query, bool unreadOnly, bool starredOnly, int limit, int offset); MailMessage? GetMessage(string owner, string id); MailMessage? GetById(string id); bool MarkRead(string owner, string id, bool read); bool SetStar(string owner, string id, bool starred); bool MoveMessage(string owner, string id, string folder); bool DeleteMessage(string owner, string id, bool permanent); object Stats(string owner); // ---------------------------------------------------------------- IMAP IReadOnlyList ListForImap(string owner, string folder); MailMessage? GetByUid(string owner, string folder, int uid); bool StoreFlags(string owner, string id, bool? seen, bool? flagged); bool Expunge(string owner, string id); MailMessage? Append(string owner, string folder, byte[] raw, bool seen); int CountUnseen(string owner, string folder); int NextUidFor(string owner, string folder); // ---------------------------------------------------------------- 出站队列 IReadOnlyList TakeDueQueue(int limit); void CompleteQueue(QueueItem item); void FailQueue(QueueItem item, Exception error, RetryConfig retry, out bool gaveUp); void RetryQueueItem(string owner, string queueId); IReadOnlyList ListQueue(string owner); // ---------------------------------------------------------------- 维护 IReadOnlyList AllMessages(); IReadOnlyList AllUsers(); /// 把内存中的改动落盘。JSON 实现会整文件重写;SQLite 实现是空操作(写入即提交)。 void Persist(); } /// 存储占用统计。 public sealed class StoreUsage { public string Provider { get; set; } = ""; public string Database { get; set; } = ""; public long DbBytes { get; set; } public long WalBytes { get; set; } public long Blobs { get; set; } public long BlobRawBytes { get; set; } public long BlobStoredBytes { get; set; } public long BlobGzipped { get; set; } public long Messages { get; set; } public long Orphans { get; set; } /// 正文文本占用的字节数(text_body + html_body),用于判断「重复存储」的成本。 public long TextBytes { get; set; } /// 数据库空闲页字节数(未 VACUUM 时会被计入文件大小)。 public long FreeBytes { get; set; } /// 数据库实际使用的页字节数(page_count × page_size)。 public long TotalBytes { get; set; } /// 文件高水位(含已回收但未归还操作系统的空间)。 public long PageCount { get; set; } public long PageSize { get; set; } /// 最大一封邮件的正文长度与其主题(排查「空间被谁吃了」)。 public long LargestTextBytes { get; set; } public string LargestTextSubject { get; set; } = ""; } /// 文件夹常量与名称归一化(IMAP 与 API 共用)。 public static class MailFolders { /// IMAP 用的文件夹列表(固定集合,未使用也返回,便于客户端订阅)。 public static readonly string[] ImapFolders = ["inbox", "sent", "drafts", "archive", "trash", "spam"]; /// 把客户端给的各种写法(含中文别名)归一化成内部文件夹名;无法识别返回 null。 public static string? Normalize(string name) { var key = (name ?? "").Trim().Trim('"').ToLowerInvariant(); if (key is "inbox" or "收件箱") return "inbox"; if (key is "sent" or "sent items" or "sent messages" or "已发送") return "sent"; if (key is "drafts" or "草稿") return "drafts"; if (key is "archive" or "archives" or "归档") return "archive"; if (key is "trash" or "deleted" or "deleted items" or "已删除" or "垃圾箱") return "trash"; if (key is "junk" or "spam" or "垃圾邮件") return "spam"; return null; } }