using Microsoft.Data.Sqlite; namespace MaksIT.PostClient.Shared.Archive; public sealed class MailArchiveStore : IDisposable { private readonly SqliteConnection _db; private readonly Lock _gate = new(); public bool KeywordIndexEnabled { get; set; } = true; public MailArchiveStore(string? path = null) { var file = string.IsNullOrWhiteSpace(path) ? AppPaths.ArchiveDatabase() : path; var dir = Path.GetDirectoryName(file); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); _db = new SqliteConnection("Data Source=" + file + ";Pooling=False"); _db.Open(); using var pragma = _db.CreateCommand(); pragma.CommandText = "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;"; pragma.ExecuteNonQuery(); EnsureSchema(); } public void UpsertHeaders(string mailboxId, IEnumerable headers) { ArgumentNullException.ThrowIfNull(headers); lock (_gate) { using var tx = _db.BeginTransaction(); foreach (var header in headers) UpsertHeader(tx, mailboxId, header, updateFlags: true); tx.Commit(); } } public void UpdateFlags( string mailboxId, string folder, IEnumerable<(uint Uid, bool IsSeen, bool IsFlagged)> flags) { ArgumentNullException.ThrowIfNull(flags); lock (_gate) { using var tx = _db.BeginTransaction(); foreach (var row in flags) { using var cmd = _db.CreateCommand(); cmd.Transaction = tx; cmd.CommandText = """ UPDATE messages SET is_seen = $seen, is_flagged = $flag WHERE mailbox_id = $m AND folder = $f AND uid = $u; """; cmd.Parameters.AddWithValue("$seen", row.IsSeen ? 1 : 0); cmd.Parameters.AddWithValue("$flag", row.IsFlagged ? 1 : 0); cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); cmd.Parameters.AddWithValue("$u", row.Uid); cmd.ExecuteNonQuery(); } tx.Commit(); } } public void UpsertBody( string mailboxId, MailArchiveHeader header, string emlPath, string bodyText, string attachmentText) { lock (_gate) { using var tx = _db.BeginTransaction(); var id = UpsertHeader(tx, mailboxId, header, updateFlags: false); using (var cmd = _db.CreateCommand()) { cmd.Transaction = tx; cmd.CommandText = """ UPDATE messages SET eml_path = $eml, body_text = $body, attachment_text = $att, size = CASE WHEN $size > 0 THEN $size ELSE size END WHERE id = $id; """; cmd.Parameters.AddWithValue("$eml", emlPath); cmd.Parameters.AddWithValue("$size", BodySize(header.Size, emlPath)); cmd.Parameters.AddWithValue("$body", bodyText); cmd.Parameters.AddWithValue("$att", attachmentText); cmd.Parameters.AddWithValue("$id", id); cmd.ExecuteNonQuery(); } IndexFts(tx, id, header, bodyText, attachmentText); tx.Commit(); } } public IReadOnlyList PendingEmbeddings(string modelId, int limit) { var take = Math.Clamp(limit, 1, 64); lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT m.mailbox_id, m.id, m.subject, m.from_addr, m.body_text, m.attachment_text FROM messages m LEFT JOIN message_embeddings e ON e.message_id = m.id WHERE (e.message_id IS NULL OR e.model_id <> $model) AND (m.body_text <> '' OR m.subject <> '') ORDER BY m.date_utc DESC, m.id DESC LIMIT $n; """; cmd.Parameters.AddWithValue("$model", modelId); cmd.Parameters.AddWithValue("$n", take); var rows = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) { rows.Add(new EmbeddingWorkItem( reader.GetString(0), reader.GetInt64(1), reader.IsDBNull(2) ? "" : reader.GetString(2), reader.IsDBNull(3) ? "" : reader.GetString(3), reader.IsDBNull(4) ? "" : reader.GetString(4), reader.IsDBNull(5) ? "" : reader.GetString(5))); } return rows; } } public int EmbeddingCount(string modelId) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM message_embeddings WHERE model_id = $model;"; cmd.Parameters.AddWithValue("$model", modelId); return (int)(long)(cmd.ExecuteScalar() ?? 0L); } } public int EmbeddingPendingCount(string modelId) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT COUNT(*) FROM messages m LEFT JOIN message_embeddings e ON e.message_id = m.id WHERE (e.message_id IS NULL OR e.model_id <> $model) AND (m.body_text <> '' OR m.subject <> ''); """; cmd.Parameters.AddWithValue("$model", modelId); return (int)(long)(cmd.ExecuteScalar() ?? 0L); } } public void UpsertEmbedding(long messageId, string modelId, float[] vector) { ArgumentNullException.ThrowIfNull(vector); var stored = EmbeddingVector.Truncate(vector, EmbeddingModelSpec.StoredDimensions); lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ INSERT INTO message_embeddings (message_id, model_id, dims, vector) VALUES ($id, $model, $dims, $vec) ON CONFLICT(message_id) DO UPDATE SET model_id = excluded.model_id, dims = excluded.dims, vector = excluded.vector; """; cmd.Parameters.AddWithValue("$id", messageId); cmd.Parameters.AddWithValue("$model", modelId); cmd.Parameters.AddWithValue("$dims", stored.Length); cmd.Parameters.AddWithValue("$vec", EmbeddingVector.ToBlob(stored)); cmd.ExecuteNonQuery(); } } public void DropForeignEmbeddings(string modelId) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = "DELETE FROM message_embeddings WHERE model_id <> $model;"; cmd.Parameters.AddWithValue("$model", modelId); cmd.ExecuteNonQuery(); } } public int ClearEmbeddings() { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = "DELETE FROM message_embeddings;"; return cmd.ExecuteNonQuery(); } } public ArchiveIndexStats IndexStats(string modelId) { lock (_gate) { return ReadIndexStats(modelId); } } public int RebuildKeywordIndex() { KeywordIndexEnabled = true; lock (_gate) { using var tx = _db.BeginTransaction(); using (var del = _db.CreateCommand()) { del.Transaction = tx; del.CommandText = "DELETE FROM messages_fts;"; del.ExecuteNonQuery(); } using var list = _db.CreateCommand(); list.Transaction = tx; list.CommandText = """ SELECT id, subject, from_addr, body_text, attachment_text, envelope_badge FROM messages; """; var rows = new List<(long Id, MailArchiveHeader Header, string Body, string Att)>(); using (var reader = list.ExecuteReader()) { while (reader.Read()) { rows.Add(( reader.GetInt64(0), new MailArchiveHeader { Subject = reader.IsDBNull(1) ? "" : reader.GetString(1), From = reader.IsDBNull(2) ? "" : reader.GetString(2), EnvelopeBadge = reader.IsDBNull(5) ? "" : reader.GetString(5) }, reader.IsDBNull(3) ? "" : reader.GetString(3), reader.IsDBNull(4) ? "" : reader.GetString(4))); } } foreach (var row in rows) IndexFts(tx, row.Id, row.Header, row.Body, row.Att); tx.Commit(); return rows.Count; } } public ArchiveIndexStats SanitizeIndices(string modelId) { lock (_gate) { using var tx = _db.BeginTransaction(); using (var fts = _db.CreateCommand()) { fts.Transaction = tx; fts.CommandText = "DELETE FROM messages_fts WHERE rowid NOT IN (SELECT id FROM messages);"; fts.ExecuteNonQuery(); } using (var orphans = _db.CreateCommand()) { orphans.Transaction = tx; orphans.CommandText = """ DELETE FROM message_embeddings WHERE message_id NOT IN (SELECT id FROM messages); """; orphans.ExecuteNonQuery(); } using (var foreign = _db.CreateCommand()) { foreign.Transaction = tx; foreign.CommandText = "DELETE FROM message_embeddings WHERE model_id <> $model;"; foreign.Parameters.AddWithValue("$model", modelId); foreign.ExecuteNonQuery(); } tx.Commit(); return ReadIndexStats(modelId); } } public IReadOnlyList ListFolders(string mailboxId) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = "SELECT DISTINCT folder FROM messages WHERE mailbox_id = $m ORDER BY folder COLLATE NOCASE;"; cmd.Parameters.AddWithValue("$m", mailboxId); var rows = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) { var folder = reader.GetString(0); if (!string.IsNullOrWhiteSpace(folder)) rows.Add(folder); } return rows; } } public IReadOnlyList ListFolder(string mailboxId, string folder) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT uid, folder, subject, from_addr, date_utc, is_seen, is_flagged, has_attachments, priority, envelope_kind, envelope_badge, envelope_tipo, message_id, in_reply_to, eml_path, (SELECT GROUP_CONCAT(name, ', ') FROM message_labels l WHERE l.mailbox_id = messages.mailbox_id AND l.folder = messages.folder AND l.uid = messages.uid), size FROM messages WHERE mailbox_id = $m AND folder = $f ORDER BY date_utc DESC, uid DESC; """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); return ReadHeaders(cmd); } } public (int Total, int Unread) FolderCounts(string mailboxId, string folder) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT COUNT(*), COALESCE(SUM(CASE WHEN is_seen = 0 THEN 1 ELSE 0 END), 0) FROM messages WHERE mailbox_id = $m AND folder = $f; """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); using var reader = cmd.ExecuteReader(); if (!reader.Read()) return (0, 0); return ((int)reader.GetInt64(0), (int)reader.GetInt64(1)); } } public IReadOnlySet Uids(string mailboxId, string folder) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = "SELECT uid FROM messages WHERE mailbox_id = $m AND folder = $f;"; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); var set = new HashSet(); using var reader = cmd.ExecuteReader(); while (reader.Read()) set.Add((uint)reader.GetInt64(0)); return set; } } public IReadOnlyList MissingBodies(string mailboxId, string folder) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT uid FROM messages WHERE mailbox_id = $m AND folder = $f AND (eml_path IS NULL OR eml_path = '') ORDER BY date_utc DESC; """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); var ids = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) ids.Add((uint)reader.GetInt64(0)); return ids; } } public IReadOnlyList MissingBodies(string mailboxId) => MissingBodies([mailboxId]); public IReadOnlyList MissingBodies( IReadOnlyCollection? mailboxIds = null, int limit = 0) { lock (_gate) { using var cmd = _db.CreateCommand(); var sql = """ SELECT mailbox_id, folder, uid, size FROM messages WHERE (eml_path IS NULL OR eml_path = '') """; sql += MailboxInSql(cmd, mailboxIds); sql += """ ORDER BY CASE WHEN size <= 0 THEN 1 ELSE 0 END, size ASC, date_utc DESC, uid DESC """; if (limit > 0) { sql += " LIMIT $n"; cmd.Parameters.AddWithValue("$n", Math.Clamp(limit, 1, 4096)); } cmd.CommandText = sql; var rows = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) { rows.Add(new MailArchiveUid( reader.GetString(0), reader.GetString(1), (uint)reader.GetInt64(2), reader.FieldCount > 3 && !reader.IsDBNull(3) ? reader.GetInt64(3) : 0)); } return rows; } } public int MissingBodyCount(IReadOnlyCollection? mailboxIds = null) { lock (_gate) { using var cmd = _db.CreateCommand(); var sql = """ SELECT COUNT(*) FROM messages WHERE (eml_path IS NULL OR eml_path = '') """; sql += MailboxInSql(cmd, mailboxIds); cmd.CommandText = sql; return (int)(long)(cmd.ExecuteScalar() ?? 0L); } } private static string MailboxInSql(SqliteCommand cmd, IReadOnlyCollection? mailboxIds) { if (mailboxIds is not { Count: > 0 }) return ""; var names = new List(mailboxIds.Count); var i = 0; foreach (var id in mailboxIds) { var name = "$mb" + i; names.Add(name); cmd.Parameters.AddWithValue(name, id); i++; } return " AND mailbox_id IN (" + string.Join(",", names) + ")"; } public void SetSeen(string mailboxId, string folder, bool seen) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ UPDATE messages SET is_seen = $seen WHERE mailbox_id = $m AND folder = $f; """; cmd.Parameters.AddWithValue("$seen", seen ? 1 : 0); cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); cmd.ExecuteNonQuery(); } } public IReadOnlyList RemoveFolder(string mailboxId, string folder) => RemoveUids(mailboxId, folder, uids: null); public int RewriteFolderPrefix(string mailboxId, string from, string to) { if (string.IsNullOrWhiteSpace(mailboxId) || string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to)) return 0; var folders = ListFolders(mailboxId); var count = 0; lock (_gate) { using var tx = _db.BeginTransaction(); foreach (var folder in folders) { var next = MailFolderPath.Rewrite(folder, from, to); if (string.IsNullOrWhiteSpace(next) || next.Equals(folder, StringComparison.Ordinal)) continue; using (var labels = _db.CreateCommand()) { labels.Transaction = tx; labels.CommandText = """ UPDATE message_labels SET folder = $to WHERE mailbox_id = $m AND folder = $from; """; labels.Parameters.AddWithValue("$to", next); labels.Parameters.AddWithValue("$m", mailboxId); labels.Parameters.AddWithValue("$from", folder); labels.ExecuteNonQuery(); } using (var messages = _db.CreateCommand()) { messages.Transaction = tx; messages.CommandText = """ UPDATE messages SET folder = $to WHERE mailbox_id = $m AND folder = $from; """; messages.Parameters.AddWithValue("$to", next); messages.Parameters.AddWithValue("$m", mailboxId); messages.Parameters.AddWithValue("$from", folder); messages.ExecuteNonQuery(); } count++; } tx.Commit(); } return count; } public IReadOnlyList RemoveUids( string mailboxId, string folder, IReadOnlyCollection? uids) { if (uids is { Count: 0 }) return []; lock (_gate) { var paths = EmlPaths(mailboxId, folder, uids); using var tx = _db.BeginTransaction(); DeleteFts(tx, mailboxId, folder, uids); DeleteEmbeddings(tx, mailboxId, folder, uids); using (var labels = _db.CreateCommand()) { labels.Transaction = tx; labels.CommandText = "DELETE FROM message_labels WHERE mailbox_id = $m AND folder = $f" + UidFilter(labels, uids) + ";"; labels.Parameters.AddWithValue("$m", mailboxId); labels.Parameters.AddWithValue("$f", folder); labels.ExecuteNonQuery(); } using (var messages = _db.CreateCommand()) { messages.Transaction = tx; messages.CommandText = "DELETE FROM messages WHERE mailbox_id = $m AND folder = $f" + UidFilter(messages, uids) + ";"; messages.Parameters.AddWithValue("$m", mailboxId); messages.Parameters.AddWithValue("$f", folder); messages.ExecuteNonQuery(); } tx.Commit(); return paths; } } public string? EmlPath(string mailboxId, string folder, uint uid) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT eml_path FROM messages WHERE mailbox_id = $m AND folder = $f AND uid = $u; """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); cmd.Parameters.AddWithValue("$u", uid); var value = cmd.ExecuteScalar() as string; return string.IsNullOrWhiteSpace(value) ? null : value; } } public IReadOnlyList Search(string mailboxId, string folder, string query) => Search(mailboxId, folder, query, queryVector: null); public IReadOnlyList Search( string mailboxId, string folder, string query, float[]? queryVector) { var text = query.Trim(); if (text.Length == 0) return ListFolder(mailboxId, folder); IReadOnlyList lexical; lock (_gate) { try { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT m.uid, m.folder, m.subject, m.from_addr, m.date_utc, m.is_seen, m.is_flagged, m.has_attachments, m.priority, m.envelope_kind, m.envelope_badge, m.envelope_tipo, m.message_id, m.in_reply_to, m.eml_path, (SELECT GROUP_CONCAT(name, ', ') FROM message_labels l WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid), m.size FROM messages_fts JOIN messages m ON m.id = messages_fts.rowid WHERE messages_fts MATCH $q AND m.mailbox_id = $m AND m.folder = $f UNION SELECT m.uid, m.folder, m.subject, m.from_addr, m.date_utc, m.is_seen, m.is_flagged, m.has_attachments, m.priority, m.envelope_kind, m.envelope_badge, m.envelope_tipo, m.message_id, m.in_reply_to, m.eml_path, (SELECT GROUP_CONCAT(name, ', ') FROM message_labels l WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid), m.size FROM messages m WHERE m.mailbox_id = $m AND m.folder = $f AND EXISTS ( SELECT 1 FROM message_labels l WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid AND l.name LIKE $like) ORDER BY 5 DESC, 1 DESC; """; cmd.Parameters.AddWithValue("$q", FtsQuery(text)); cmd.Parameters.AddWithValue("$like", "%" + text.Replace("%", "\\%").Replace("_", "\\_") + "%"); cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); lexical = ReadHeaders(cmd); } catch (SqliteException) { lexical = SearchLike(mailboxId, folder, text); } } if (queryVector is null || queryVector.Length == 0) return lexical; return MergeSemantic(mailboxId, folder, lexical, queryVector); } public uint NextUid(string mailboxId, string folder) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = "SELECT COALESCE(MAX(uid), 0) FROM messages WHERE mailbox_id = $m AND folder = $f;"; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); var max = (long)(cmd.ExecuteScalar() ?? 0L); return (uint)max + 1; } } public IReadOnlyList LabelNames(string mailboxId) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT DISTINCT name FROM message_labels WHERE mailbox_id = $m ORDER BY name COLLATE NOCASE; """; cmd.Parameters.AddWithValue("$m", mailboxId); var names = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) names.Add(reader.GetString(0)); return names; } } public void AddLabel(string mailboxId, string folder, uint uid, string name) { var label = name.Trim(); if (label.Length == 0) return; lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ INSERT OR IGNORE INTO message_labels (mailbox_id, folder, uid, name) VALUES ($m, $f, $u, $n); """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); cmd.Parameters.AddWithValue("$u", uid); cmd.Parameters.AddWithValue("$n", label); cmd.ExecuteNonQuery(); } } public void Checkpoint() { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; cmd.ExecuteNonQuery(); } } public bool HasMessageId(string mailboxId, string messageId) { if (string.IsNullOrWhiteSpace(messageId)) return false; lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT 1 FROM messages WHERE mailbox_id = $m AND message_id = $mid LIMIT 1; """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$mid", messageId.Trim()); return cmd.ExecuteScalar() is not null; } } private bool HasMessageElsewhere( SqliteTransaction tx, string mailboxId, string folder, string? messageId) { if (string.IsNullOrWhiteSpace(messageId)) return false; using var cmd = _db.CreateCommand(); cmd.Transaction = tx; cmd.CommandText = """ SELECT 1 FROM messages WHERE mailbox_id = $m AND message_id = $mid AND folder <> $f LIMIT 1; """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$mid", messageId.Trim()); cmd.Parameters.AddWithValue("$f", folder); return cmd.ExecuteScalar() is not null; } public MailArchiveCopy? ReadCopy(string mailboxId, string folder, uint uid) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT m.uid, m.folder, m.subject, m.from_addr, m.date_utc, m.is_seen, m.is_flagged, m.has_attachments, m.priority, m.envelope_kind, m.envelope_badge, m.envelope_tipo, m.message_id, m.in_reply_to, m.eml_path, (SELECT GROUP_CONCAT(name, ', ') FROM message_labels l WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid), m.size, m.body_text, m.attachment_text, e.model_id, e.vector FROM messages m LEFT JOIN message_embeddings e ON e.message_id = m.id WHERE m.mailbox_id = $m AND m.folder = $f AND m.uid = $u; """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); cmd.Parameters.AddWithValue("$u", uid); using var reader = cmd.ExecuteReader(); if (!reader.Read()) return null; var header = ReadHeader(reader); var body = reader.IsDBNull(17) ? "" : reader.GetString(17); var att = reader.IsDBNull(18) ? "" : reader.GetString(18); var model = reader.IsDBNull(19) ? "" : reader.GetString(19); float[]? vector = null; if (!reader.IsDBNull(20)) vector = EmbeddingVector.FromBlob(reader.GetFieldValue(20)); var labels = string.IsNullOrWhiteSpace(header.Labels) ? Array.Empty() : header.Labels.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); return new MailArchiveCopy { Header = header, BodyText = body, AttachmentText = att, Labels = labels, ModelId = model, Vector = vector }; } } public uint InsertCopy(string mailboxId, string folder, MailArchiveCopy copy) { ArgumentNullException.ThrowIfNull(copy); lock (_gate) { using var tx = _db.BeginTransaction(); var uid = copy.Header.Uid; if (uid == 0) uid = NextUidUnlocked(mailboxId, folder); var header = new MailArchiveHeader { Uid = uid, Folder = folder, Subject = copy.Header.Subject, From = copy.Header.From, Date = copy.Header.Date, IsSeen = copy.Header.IsSeen, IsFlagged = copy.Header.IsFlagged, HasAttachments = copy.Header.HasAttachments, Priority = copy.Header.Priority, EnvelopeKind = copy.Header.EnvelopeKind, EnvelopeBadge = copy.Header.EnvelopeBadge, EnvelopeTipo = copy.Header.EnvelopeTipo, MessageId = copy.Header.MessageId, InReplyTo = copy.Header.InReplyTo, EmlPath = copy.Header.EmlPath, Labels = copy.Header.Labels, Size = copy.Header.Size }; var id = UpsertHeader( tx, mailboxId, header, updateFlags: true, stampTrashTime: MailRetention.IsTrash(folder)); using (var body = _db.CreateCommand()) { body.Transaction = tx; body.CommandText = """ UPDATE messages SET eml_path = $eml, body_text = $body, attachment_text = $att, size = CASE WHEN $size > 0 THEN $size ELSE size END WHERE id = $id; """; body.Parameters.AddWithValue("$eml", header.EmlPath ?? ""); body.Parameters.AddWithValue("$size", BodySize(header.Size, header.EmlPath)); body.Parameters.AddWithValue("$body", copy.BodyText ?? ""); body.Parameters.AddWithValue("$att", copy.AttachmentText ?? ""); body.Parameters.AddWithValue("$id", id); body.ExecuteNonQuery(); } IndexFts(tx, id, header, copy.BodyText, copy.AttachmentText); if (copy.Vector is { Length: > 0 }) { using var emb = _db.CreateCommand(); emb.Transaction = tx; emb.CommandText = """ INSERT INTO message_embeddings (message_id, model_id, dims, vector) VALUES ($id, $model, $dims, $vec) ON CONFLICT(message_id) DO UPDATE SET model_id = excluded.model_id, dims = excluded.dims, vector = excluded.vector; """; var stored = EmbeddingVector.Truncate(copy.Vector, EmbeddingModelSpec.StoredDimensions); emb.Parameters.AddWithValue("$id", id); emb.Parameters.AddWithValue("$model", string.IsNullOrWhiteSpace(copy.ModelId) ? EmbeddingModelSpec.Id : copy.ModelId); emb.Parameters.AddWithValue("$dims", stored.Length); emb.Parameters.AddWithValue("$vec", EmbeddingVector.ToBlob(stored)); emb.ExecuteNonQuery(); } foreach (var label in copy.Labels) { if (string.IsNullOrWhiteSpace(label)) continue; using var lab = _db.CreateCommand(); lab.Transaction = tx; lab.CommandText = """ INSERT OR IGNORE INTO message_labels (mailbox_id, folder, uid, name) VALUES ($m, $f, $u, $n); """; lab.Parameters.AddWithValue("$m", mailboxId); lab.Parameters.AddWithValue("$f", folder); lab.Parameters.AddWithValue("$u", uid); lab.Parameters.AddWithValue("$n", label.Trim()); lab.ExecuteNonQuery(); } tx.Commit(); return uid; } } public IReadOnlyList UidsOlderThan( string mailboxId, string folder, DateTimeOffset cutoff, bool useReceivedDate = false) { var trashStamp = MailRetention.IsTrash(folder) && !useReceivedDate; lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = trashStamp ? """ SELECT uid FROM messages WHERE mailbox_id = $m AND folder = $f AND COALESCE(NULLIF(trashed_utc, ''), date_utc) <> '' AND COALESCE(NULLIF(trashed_utc, ''), date_utc) < $cut ORDER BY COALESCE(NULLIF(trashed_utc, ''), date_utc); """ : """ SELECT uid FROM messages WHERE mailbox_id = $m AND folder = $f AND date_utc <> '' AND date_utc < $cut ORDER BY date_utc; """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); cmd.Parameters.AddWithValue("$cut", cutoff.ToString("O")); var ids = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) ids.Add((uint)reader.GetInt64(0)); return ids; } } public int MessageCount(string mailboxId) { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM messages WHERE mailbox_id = $m;"; cmd.Parameters.AddWithValue("$m", mailboxId); return (int)(long)(cmd.ExecuteScalar() ?? 0L); } } public IReadOnlyList DistinctMailboxIds() { lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = "SELECT DISTINCT mailbox_id FROM messages ORDER BY mailbox_id;"; var ids = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) ids.Add(reader.GetString(0)); return ids; } } public void CopyMailboxInto(string mailboxId, MailArchiveStore dest) { ArgumentNullException.ThrowIfNull(dest); foreach (var folder in ListFolders(mailboxId)) { foreach (var header in ListFolder(mailboxId, folder)) { var copy = ReadCopy(mailboxId, folder, header.Uid); if (copy is null) continue; dest.InsertCopy(mailboxId, folder, copy); } } } public void Dispose() { lock (_gate) { try { using var cmd = _db.CreateCommand(); cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; cmd.ExecuteNonQuery(); } catch { } _db.Dispose(); } } private uint NextUidUnlocked(string mailboxId, string folder) { using var cmd = _db.CreateCommand(); cmd.CommandText = "SELECT COALESCE(MAX(uid), 0) FROM messages WHERE mailbox_id = $m AND folder = $f;"; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); var max = (long)(cmd.ExecuteScalar() ?? 0L); return (uint)max + 1; } private IReadOnlyList SearchLike(string mailboxId, string folder, string text) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT uid, folder, subject, from_addr, date_utc, is_seen, is_flagged, has_attachments, priority, envelope_kind, envelope_badge, envelope_tipo, message_id, in_reply_to, eml_path, (SELECT GROUP_CONCAT(name, ', ') FROM message_labels l WHERE l.mailbox_id = messages.mailbox_id AND l.folder = messages.folder AND l.uid = messages.uid), size FROM messages WHERE mailbox_id = $m AND folder = $f AND (subject LIKE $q OR from_addr LIKE $q OR body_text LIKE $q OR attachment_text LIKE $q OR envelope_badge LIKE $q OR EXISTS (SELECT 1 FROM message_labels l WHERE l.mailbox_id = messages.mailbox_id AND l.folder = messages.folder AND l.uid = messages.uid AND l.name LIKE $q)) ORDER BY date_utc DESC, uid DESC; """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); cmd.Parameters.AddWithValue("$q", "%" + text.Replace("%", "\\%").Replace("_", "\\_") + "%"); return ReadHeaders(cmd); } private long UpsertHeader( SqliteTransaction tx, string mailboxId, MailArchiveHeader header, bool updateFlags, bool stampTrashTime = false) { using var cmd = _db.CreateCommand(); cmd.Transaction = tx; var flagAssign = updateFlags ? """ is_seen = excluded.is_seen, is_flagged = excluded.is_flagged, """ : ""; cmd.CommandText = $""" INSERT INTO messages ( mailbox_id, folder, uid, message_id, subject, from_addr, date_utc, envelope_kind, envelope_badge, envelope_tipo, in_reply_to, is_seen, is_flagged, has_attachments, priority, trashed_utc, size) VALUES ($m, $f, $u, $mid, $sub, $from, $date, $kind, $badge, $tipo, $reply, $seen, $flag, $att, $pri, $trashed, $size) ON CONFLICT(mailbox_id, folder, uid) DO UPDATE SET message_id = excluded.message_id, subject = excluded.subject, from_addr = excluded.from_addr, date_utc = excluded.date_utc, envelope_kind = excluded.envelope_kind, envelope_badge = excluded.envelope_badge, envelope_tipo = excluded.envelope_tipo, in_reply_to = excluded.in_reply_to, {flagAssign} has_attachments = CASE WHEN excluded.has_attachments != 0 THEN 1 ELSE messages.has_attachments END, priority = excluded.priority, size = CASE WHEN excluded.size > 0 THEN excluded.size ELSE messages.size END; """; var trash = MailRetention.IsTrash(header.Folder); var stampTrash = trash && (stampTrashTime || HasMessageElsewhere(tx, mailboxId, header.Folder, header.MessageId)); var trashed = stampTrash ? DateTimeOffset.UtcNow.ToString("O") : ""; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", header.Folder); cmd.Parameters.AddWithValue("$u", header.Uid); cmd.Parameters.AddWithValue("$mid", header.MessageId); cmd.Parameters.AddWithValue("$sub", header.Subject); cmd.Parameters.AddWithValue("$from", header.From); cmd.Parameters.AddWithValue("$date", header.Date.ToString("O")); cmd.Parameters.AddWithValue("$kind", header.EnvelopeKind); cmd.Parameters.AddWithValue("$badge", header.EnvelopeBadge); cmd.Parameters.AddWithValue("$tipo", header.EnvelopeTipo); cmd.Parameters.AddWithValue("$reply", header.InReplyTo); cmd.Parameters.AddWithValue("$seen", header.IsSeen ? 1 : 0); cmd.Parameters.AddWithValue("$flag", header.IsFlagged ? 1 : 0); cmd.Parameters.AddWithValue("$att", header.HasAttachments ? 1 : 0); cmd.Parameters.AddWithValue("$pri", header.Priority); cmd.Parameters.AddWithValue("$trashed", trashed); cmd.Parameters.AddWithValue("$size", header.Size); cmd.ExecuteNonQuery(); using var idCmd = _db.CreateCommand(); idCmd.Transaction = tx; idCmd.CommandText = "SELECT id FROM messages WHERE mailbox_id = $m AND folder = $f AND uid = $u;"; idCmd.Parameters.AddWithValue("$m", mailboxId); idCmd.Parameters.AddWithValue("$f", header.Folder); idCmd.Parameters.AddWithValue("$u", header.Uid); var id = (long)(idCmd.ExecuteScalar() ?? 0L); IndexFts(tx, id, header, null, null); return id; } private void IndexFts( SqliteTransaction tx, long id, MailArchiveHeader header, string? bodyText, string? attachmentText) { if (!KeywordIndexEnabled) return; using (var del = _db.CreateCommand()) { del.Transaction = tx; del.CommandText = "DELETE FROM messages_fts WHERE rowid = $id;"; del.Parameters.AddWithValue("$id", id); del.ExecuteNonQuery(); } var body = bodyText; var att = attachmentText; if (body is null || att is null) { using var read = _db.CreateCommand(); read.Transaction = tx; read.CommandText = "SELECT body_text, attachment_text FROM messages WHERE id = $id;"; read.Parameters.AddWithValue("$id", id); using var reader = read.ExecuteReader(); if (reader.Read()) { body ??= reader.IsDBNull(0) ? "" : reader.GetString(0); att ??= reader.IsDBNull(1) ? "" : reader.GetString(1); } } using var ins = _db.CreateCommand(); ins.Transaction = tx; ins.CommandText = """ INSERT INTO messages_fts(rowid, subject, from_addr, body_text, attachment_text, envelope_badge) VALUES ($id, $sub, $from, $body, $att, $badge); """; ins.Parameters.AddWithValue("$id", id); ins.Parameters.AddWithValue("$sub", header.Subject); ins.Parameters.AddWithValue("$from", header.From); ins.Parameters.AddWithValue("$body", body ?? ""); ins.Parameters.AddWithValue("$att", att ?? ""); ins.Parameters.AddWithValue("$badge", header.EnvelopeBadge); ins.ExecuteNonQuery(); } private static List ReadHeaders(SqliteCommand cmd) { var rows = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) rows.Add(ReadHeader(reader)); return rows; } private static MailArchiveHeader ReadHeader(SqliteDataReader reader) { DateTimeOffset.TryParse(reader.GetString(4), out var date); var emlPath = reader.IsDBNull(14) ? "" : reader.GetString(14); var stored = reader.FieldCount > 16 && !reader.IsDBNull(16) ? reader.GetInt64(16) : 0; return new MailArchiveHeader { Uid = (uint)reader.GetInt64(0), Folder = reader.GetString(1), Subject = reader.GetString(2), From = reader.GetString(3), Date = date, IsSeen = reader.GetInt64(5) != 0, IsFlagged = reader.GetInt64(6) != 0, HasAttachments = reader.GetInt64(7) != 0, Priority = reader.GetString(8), EnvelopeKind = reader.GetString(9), EnvelopeBadge = reader.GetString(10), EnvelopeTipo = reader.GetString(11), MessageId = reader.GetString(12), InReplyTo = reader.GetString(13), EmlPath = emlPath, Labels = reader.FieldCount > 15 && !reader.IsDBNull(15) ? reader.GetString(15) : "", Size = stored }; } private static long BodySize(long size, string? emlPath) => size > 0 ? size : FileSize(emlPath); private static long FileSize(string? path) => !string.IsNullOrWhiteSpace(path) && File.Exists(path) ? new FileInfo(path).Length : 0; internal static string FtsQuery(string text) { var parts = text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length == 0) return "\"\""; return string.Join(" AND ", parts.Select(p => "\"" + p.Replace("\"", " ") + "\"")); } private List EmlPaths(string mailboxId, string folder, IReadOnlyCollection? uids = null) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT eml_path FROM messages WHERE mailbox_id = $m AND folder = $f AND eml_path IS NOT NULL AND eml_path <> '' """ + UidFilter(cmd, uids) + ";"; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); var paths = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) paths.Add(reader.GetString(0)); return paths; } private IReadOnlyList MergeSemantic( string mailboxId, string folder, IReadOnlyList lexical, float[] queryVector) { var query = EmbeddingVector.Truncate(queryVector, EmbeddingModelSpec.StoredDimensions); List<(float Score, MailArchiveHeader Header)> extra; lock (_gate) { using var cmd = _db.CreateCommand(); cmd.CommandText = """ SELECT m.uid, m.folder, m.subject, m.from_addr, m.date_utc, m.is_seen, m.is_flagged, m.has_attachments, m.priority, m.envelope_kind, m.envelope_badge, m.envelope_tipo, m.message_id, m.in_reply_to, m.eml_path, (SELECT GROUP_CONCAT(name, ', ') FROM message_labels l WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid), m.size, e.vector FROM message_embeddings e JOIN messages m ON m.id = e.message_id WHERE m.mailbox_id = $m AND m.folder = $f AND e.model_id = $model; """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); cmd.Parameters.AddWithValue("$model", EmbeddingModelSpec.Id); extra = []; using var reader = cmd.ExecuteReader(); while (reader.Read()) { var blob = reader.IsDBNull(17) ? null : reader.GetFieldValue(17); if (blob is null || blob.Length == 0) continue; var vector = EmbeddingVector.FromBlob(blob); var score = EmbeddingVector.Cosine(query, vector); if (score < EmbeddingModelSpec.MinScore) continue; extra.Add((score, ReadHeader(reader))); } } if (extra.Count == 0) return lexical; var seen = new HashSet(lexical.Select(h => h.Uid)); extra.Sort((a, b) => b.Score.CompareTo(a.Score)); var merged = new List(lexical); var added = 0; foreach (var row in extra) { if (!seen.Add(row.Header.Uid)) continue; merged.Add(row.Header); added++; if (added >= EmbeddingModelSpec.VectorTopK) break; } return merged; } private void DeleteEmbeddings( SqliteTransaction tx, string mailboxId, string folder, IReadOnlyCollection? uids = null) { using var cmd = _db.CreateCommand(); cmd.Transaction = tx; cmd.CommandText = """ DELETE FROM message_embeddings WHERE message_id IN ( SELECT id FROM messages WHERE mailbox_id = $m AND folder = $f """ + UidFilter(cmd, uids) + """ ); """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); cmd.ExecuteNonQuery(); } private void DeleteFts( SqliteTransaction tx, string mailboxId, string folder, IReadOnlyCollection? uids = null) { using var cmd = _db.CreateCommand(); cmd.Transaction = tx; cmd.CommandText = """ DELETE FROM messages_fts WHERE rowid IN ( SELECT id FROM messages WHERE mailbox_id = $m AND folder = $f """ + UidFilter(cmd, uids) + """ ); """; cmd.Parameters.AddWithValue("$m", mailboxId); cmd.Parameters.AddWithValue("$f", folder); cmd.ExecuteNonQuery(); } private ArchiveIndexStats ReadIndexStats(string modelId) { var messages = Scalar("SELECT COUNT(*) FROM messages;"); var keyword = Scalar("SELECT COUNT(*) FROM messages_fts;"); int meaning; using (var cmd = _db.CreateCommand()) { cmd.CommandText = "SELECT COUNT(*) FROM message_embeddings WHERE model_id = $model;"; cmd.Parameters.AddWithValue("$model", modelId); meaning = (int)(long)(cmd.ExecuteScalar() ?? 0L); } int pending; using (var cmd = _db.CreateCommand()) { cmd.CommandText = """ SELECT COUNT(*) FROM messages m LEFT JOIN message_embeddings e ON e.message_id = m.id WHERE (e.message_id IS NULL OR e.model_id <> $model) AND (m.body_text <> '' OR m.subject <> ''); """; cmd.Parameters.AddWithValue("$model", modelId); pending = (int)(long)(cmd.ExecuteScalar() ?? 0L); } var ftsOrphans = Scalar("SELECT COUNT(*) FROM messages_fts WHERE rowid NOT IN (SELECT id FROM messages);"); var embeddingOrphans = Scalar(""" SELECT COUNT(*) FROM message_embeddings WHERE message_id NOT IN (SELECT id FROM messages); """); return new ArchiveIndexStats(messages, keyword, meaning, pending, ftsOrphans + embeddingOrphans); } private int Scalar(string sql) { using var cmd = _db.CreateCommand(); cmd.CommandText = sql; return (int)(long)(cmd.ExecuteScalar() ?? 0L); } private static string UidFilter(SqliteCommand cmd, IReadOnlyCollection? uids) { if (uids is null) return ""; var names = new List(); var i = 0; foreach (var uid in uids) { var name = "$uid" + i++; names.Add(name); cmd.Parameters.AddWithValue(name, uid); } return names.Count == 0 ? " AND 0" : " AND uid IN (" + string.Join(", ", names) + ")"; } private void EnsureSchema() { using var cmd = _db.CreateCommand(); cmd.CommandText = """ CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, mailbox_id TEXT NOT NULL, folder TEXT NOT NULL, uid INTEGER NOT NULL, message_id TEXT NOT NULL DEFAULT '', subject TEXT NOT NULL DEFAULT '', from_addr TEXT NOT NULL DEFAULT '', date_utc TEXT NOT NULL DEFAULT '', envelope_kind TEXT NOT NULL DEFAULT '', envelope_badge TEXT NOT NULL DEFAULT '', envelope_tipo TEXT NOT NULL DEFAULT '', in_reply_to TEXT NOT NULL DEFAULT '', is_seen INTEGER NOT NULL DEFAULT 0, is_flagged INTEGER NOT NULL DEFAULT 0, has_attachments INTEGER NOT NULL DEFAULT 0, priority TEXT NOT NULL DEFAULT '', eml_path TEXT NOT NULL DEFAULT '', body_text TEXT NOT NULL DEFAULT '', attachment_text TEXT NOT NULL DEFAULT '', trashed_utc TEXT NOT NULL DEFAULT '', size INTEGER NOT NULL DEFAULT 0, UNIQUE(mailbox_id, folder, uid) ); CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5( subject, from_addr, body_text, attachment_text, envelope_badge ); CREATE TABLE IF NOT EXISTS message_labels ( mailbox_id TEXT NOT NULL, folder TEXT NOT NULL, uid INTEGER NOT NULL, name TEXT NOT NULL, PRIMARY KEY (mailbox_id, folder, uid, name) ); CREATE TABLE IF NOT EXISTS message_embeddings ( message_id INTEGER PRIMARY KEY, model_id TEXT NOT NULL, dims INTEGER NOT NULL, vector BLOB NOT NULL ); """; cmd.ExecuteNonQuery(); EnsureColumn("messages", "trashed_utc", "TEXT NOT NULL DEFAULT ''"); EnsureColumn("messages", "size", "INTEGER NOT NULL DEFAULT 0"); } private void EnsureColumn(string table, string column, string definition) { using var info = _db.CreateCommand(); info.CommandText = "PRAGMA table_info(" + table + ");"; using var reader = info.ExecuteReader(); while (reader.Read()) { if (reader.GetString(1).Equals(column, StringComparison.OrdinalIgnoreCase)) return; } reader.Dispose(); using var alter = _db.CreateCommand(); alter.CommandText = "ALTER TABLE " + table + " ADD COLUMN " + column + " " + definition + ";"; alter.ExecuteNonQuery(); } } public readonly record struct MailArchiveUid(string MailboxId, string Folder, uint Uid, long Size = 0); public readonly record struct EmbeddingWorkItem( string MailboxId, long MessageId, string Subject, string From, string Body, string Attachments); public readonly record struct ArchiveIndexStats( int Messages, int KeywordRows, int MeaningRows, int MeaningPending, int Orphans); public sealed class MailArchiveHeader { public string MailboxId { get; set; } = ""; public uint Uid { get; init; } public string Folder { get; init; } = ""; public string Subject { get; init; } = ""; public string From { get; init; } = ""; public DateTimeOffset Date { get; init; } public bool IsSeen { get; init; } public bool IsFlagged { get; init; } public bool HasAttachments { get; init; } public string Priority { get; init; } = ""; public string EnvelopeKind { get; init; } = ""; public string EnvelopeBadge { get; init; } = ""; public string EnvelopeTipo { get; init; } = ""; public string MessageId { get; init; } = ""; public string InReplyTo { get; init; } = ""; public string EmlPath { get; init; } = ""; public string Labels { get; init; } = ""; public long Size { get; init; } }