add mailstore schema: accounts, mailboxes, messages, threads, FTS5

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 18:16:23 +01:00
parent f37b287f5e
commit 0927d9827d
8 changed files with 251 additions and 1 deletions

View File

@@ -0,0 +1,101 @@
import GRDB
public enum DatabaseSetup {
public static func migrator() -> DatabaseMigrator {
var migrator = DatabaseMigrator()
migrator.registerMigration("v1_initial") { db in
try db.create(table: "account") { t in
t.primaryKey("id", .text)
t.column("name", .text).notNull()
t.column("email", .text).notNull()
t.column("imapHost", .text).notNull()
t.column("imapPort", .integer).notNull()
}
try db.create(table: "mailbox") { t in
t.primaryKey("id", .text)
t.belongsTo("account", onDelete: .cascade).notNull()
t.column("name", .text).notNull()
t.column("uidValidity", .integer).notNull()
t.column("uidNext", .integer).notNull()
}
try db.create(table: "message") { t in
t.primaryKey("id", .text)
t.belongsTo("account", onDelete: .cascade).notNull()
t.belongsTo("mailbox", onDelete: .cascade).notNull()
t.column("uid", .integer).notNull()
t.column("messageId", .text)
t.column("inReplyTo", .text)
t.column("refs", .text)
t.column("subject", .text)
t.column("fromAddress", .text)
t.column("fromName", .text)
t.column("toAddresses", .text)
t.column("ccAddresses", .text)
t.column("date", .text).notNull()
t.column("snippet", .text)
t.column("bodyText", .text)
t.column("bodyHtml", .text)
t.column("isRead", .boolean).notNull().defaults(to: false)
t.column("isFlagged", .boolean).notNull().defaults(to: false)
t.column("size", .integer).notNull().defaults(to: 0)
t.uniqueKey(["mailboxId", "uid"])
}
try db.create(table: "thread") { t in
t.primaryKey("id", .text)
t.belongsTo("account", onDelete: .cascade).notNull()
t.column("subject", .text)
t.column("lastDate", .text).notNull()
t.column("messageCount", .integer).notNull().defaults(to: 0)
}
try db.create(table: "threadMessage") { t in
t.belongsTo("thread", onDelete: .cascade).notNull()
t.belongsTo("message", onDelete: .cascade).notNull()
t.primaryKey(["threadId", "messageId"])
}
try db.create(table: "attachment") { t in
t.primaryKey("id", .text)
t.belongsTo("message", onDelete: .cascade).notNull()
t.column("filename", .text)
t.column("mimeType", .text).notNull()
t.column("size", .integer).notNull().defaults(to: 0)
t.column("contentId", .text)
t.column("cachePath", .text)
}
try db.create(index: "idx_message_mailbox_uid", on: "message", columns: ["mailboxId", "uid"])
try db.create(index: "idx_message_messageId", on: "message", columns: ["messageId"])
try db.create(index: "idx_thread_lastDate", on: "thread", columns: ["lastDate"])
}
migrator.registerMigration("v1_fts5") { db in
try db.create(virtualTable: "messageFts", using: FTS5()) { t in
t.synchronize(withTable: "message")
t.tokenizer = .porter(wrapping: .unicode61())
t.column("subject")
t.column("fromName")
t.column("fromAddress")
t.column("bodyText")
}
}
return migrator
}
public static func openDatabase(atPath path: String) throws -> DatabasePool {
let pool = try DatabasePool(path: path)
try migrator().migrate(pool)
return pool
}
public static func openInMemoryDatabase() throws -> DatabaseQueue {
let queue = try DatabaseQueue()
try migrator().migrate(queue)
return queue
}
}

View File

@@ -1 +0,0 @@
enum MailStorePlaceholder {}

View File

@@ -0,0 +1,19 @@
import GRDB
public struct AccountRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
public static let databaseTableName = "account"
public var id: String
public var name: String
public var email: String
public var imapHost: String
public var imapPort: Int
public init(id: String, name: String, email: String, imapHost: String, imapPort: Int) {
self.id = id
self.name = name
self.email = email
self.imapHost = imapHost
self.imapPort = imapPort
}
}

View File

@@ -0,0 +1,26 @@
import GRDB
public struct AttachmentRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
public static let databaseTableName = "attachment"
public var id: String
public var messageId: String
public var filename: String?
public var mimeType: String
public var size: Int
public var contentId: String?
public var cachePath: String?
public init(
id: String, messageId: String, filename: String?, mimeType: String,
size: Int, contentId: String?, cachePath: String?
) {
self.id = id
self.messageId = messageId
self.filename = filename
self.mimeType = mimeType
self.size = size
self.contentId = contentId
self.cachePath = cachePath
}
}

View File

@@ -0,0 +1,19 @@
import GRDB
public struct MailboxRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
public static let databaseTableName = "mailbox"
public var id: String
public var accountId: String
public var name: String
public var uidValidity: Int
public var uidNext: Int
public init(id: String, accountId: String, name: String, uidValidity: Int, uidNext: Int) {
self.id = id
self.accountId = accountId
self.name = name
self.uidValidity = uidValidity
self.uidNext = uidNext
}
}

View File

@@ -0,0 +1,54 @@
import GRDB
public struct MessageRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
public static let databaseTableName = "message"
public var id: String
public var accountId: String
public var mailboxId: String
public var uid: Int
public var messageId: String?
public var inReplyTo: String?
public var refs: String?
public var subject: String?
public var fromAddress: String?
public var fromName: String?
public var toAddresses: String?
public var ccAddresses: String?
public var date: String
public var snippet: String?
public var bodyText: String?
public var bodyHtml: String?
public var isRead: Bool
public var isFlagged: Bool
public var size: Int
public init(
id: String, accountId: String, mailboxId: String, uid: Int,
messageId: String?, inReplyTo: String?, refs: String?,
subject: String?, fromAddress: String?, fromName: String?,
toAddresses: String?, ccAddresses: String?,
date: String, snippet: String?, bodyText: String?, bodyHtml: String?,
isRead: Bool, isFlagged: Bool, size: Int
) {
self.id = id
self.accountId = accountId
self.mailboxId = mailboxId
self.uid = uid
self.messageId = messageId
self.inReplyTo = inReplyTo
self.refs = refs
self.subject = subject
self.fromAddress = fromAddress
self.fromName = fromName
self.toAddresses = toAddresses
self.ccAddresses = ccAddresses
self.date = date
self.snippet = snippet
self.bodyText = bodyText
self.bodyHtml = bodyHtml
self.isRead = isRead
self.isFlagged = isFlagged
self.size = size
}
}

View File

@@ -0,0 +1,13 @@
import GRDB
public struct ThreadMessageRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
public static let databaseTableName = "threadMessage"
public var threadId: String
public var messageId: String
public init(threadId: String, messageId: String) {
self.threadId = threadId
self.messageId = messageId
}
}

View File

@@ -0,0 +1,19 @@
import GRDB
public struct ThreadRecord: Codable, FetchableRecord, PersistableRecord, Sendable {
public static let databaseTableName = "thread"
public var id: String
public var accountId: String
public var subject: String?
public var lastDate: String
public var messageCount: Int
public init(id: String, accountId: String, subject: String?, lastDate: String, messageCount: Int) {
self.id = id
self.accountId = accountId
self.subject = subject
self.lastDate = lastDate
self.messageCount = messageCount
}
}