Files
MagnumOpus/Packages/MagnumOpusCore/Sources/MailStore/Queries.swift

115 lines
4.3 KiB
Swift

import Foundation
import GRDB
import Models
extension MailStore {
/// Full-text search across subject, sender, and body via FTS5
public func search(query: String) throws -> [MessageRecord] {
try dbWriter.read { db in
guard let pattern = FTS5Pattern(matchingAllPrefixesIn: query) else { return [] }
return try MessageRecord.fetchAll(db, sql: """
SELECT message.* FROM message
JOIN messageFts ON messageFts.rowid = message.rowid
WHERE messageFts MATCH ?
""", arguments: [pattern])
}
}
/// Thread summaries with unread count and latest sender info, ordered by lastDate DESC
public func threadSummaries(accountId: String) throws -> [ThreadSummary] {
try dbWriter.read { db in
try Self.threadSummariesFromDB(db, accountId: accountId)
}
}
/// Observe thread summaries reactively UI updates automatically on DB change.
public func observeThreadSummaries(accountId: String) -> AsyncValueObservation<[ThreadSummary]> {
ValueObservation.tracking { db -> [ThreadSummary] in
try Self.threadSummariesFromDB(db, accountId: accountId)
}.values(in: dbWriter)
}
/// Observe messages in a thread reactively
public func observeMessages(threadId: String) -> AsyncValueObservation<[MessageSummary]> {
ValueObservation.tracking { db -> [MessageSummary] in
let records = try MessageRecord.fetchAll(db, sql: """
SELECT m.* FROM message m
JOIN threadMessage tm ON tm.messageId = m.id
WHERE tm.threadId = ?
ORDER BY m.date ASC
""", arguments: [threadId])
return records.map(Self.toMessageSummary)
}.values(in: dbWriter)
}
// MARK: - Internal helpers
static func threadSummariesFromDB(_ db: Database, accountId: String) throws -> [ThreadSummary] {
let sql = """
SELECT
t.id, t.accountId, t.subject, t.lastDate, t.messageCount,
(SELECT COUNT(*) FROM threadMessage tm JOIN message m ON m.id = tm.messageId WHERE tm.threadId = t.id AND m.isRead = 0) as unreadCount,
(SELECT GROUP_CONCAT(DISTINCT m.fromName) FROM threadMessage tm JOIN message m ON m.id = tm.messageId WHERE tm.threadId = t.id AND m.fromName IS NOT NULL) as senders,
(SELECT m.snippet FROM threadMessage tm JOIN message m ON m.id = tm.messageId WHERE tm.threadId = t.id ORDER BY m.date DESC LIMIT 1) as snippet,
(SELECT COUNT(*) FROM attachment a JOIN threadMessage tm ON tm.messageId = a.messageId WHERE tm.threadId = t.id) as attachmentCount
FROM thread t
WHERE t.accountId = ?
ORDER BY t.lastDate DESC
"""
let rows = try Row.fetchAll(db, sql: sql, arguments: [accountId])
return rows.map { row in
ThreadSummary(
id: row["id"],
accountId: row["accountId"],
subject: row["subject"],
lastDate: parseDate(row["lastDate"] as String? ?? "") ?? Date.distantPast,
messageCount: row["messageCount"],
unreadCount: row["unreadCount"],
senders: row["senders"] ?? "",
snippet: row["snippet"],
attachmentCount: row["attachmentCount"]
)
}
}
nonisolated(unsafe) private static let isoFormatterWithFractional: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()
nonisolated(unsafe) private static let isoFormatter: ISO8601DateFormatter = {
ISO8601DateFormatter()
}()
static func parseDate(_ iso: String) -> Date? {
isoFormatterWithFractional.date(from: iso) ?? isoFormatter.date(from: iso)
}
public static func toMessageSummary(_ record: MessageRecord) -> MessageSummary {
MessageSummary(
id: record.id,
messageId: record.messageId,
threadId: nil,
from: record.fromAddress.map { EmailAddress.parse($0) },
to: parseAddressList(record.toAddresses),
cc: parseAddressList(record.ccAddresses),
subject: record.subject,
date: parseDate(record.date) ?? Date.distantPast,
snippet: record.snippet,
bodyText: record.bodyText,
bodyHtml: record.bodyHtml,
isRead: record.isRead,
isFlagged: record.isFlagged,
hasAttachments: record.hasAttachments
)
}
static func parseAddressList(_ json: String?) -> [EmailAddress] {
guard let json, let data = json.data(using: .utf8) else { return [] }
struct Addr: Codable { var name: String?; var address: String }
guard let addrs = try? JSONDecoder().decode([Addr].self, from: data) else { return [] }
return addrs.map { EmailAddress(name: $0.name, address: $0.address) }
}
}