add models module: shared types for accounts, messages, threads, sync
fix Package.swift: remove NIOIMAPCore product reference (only NIOIMAP is exported by swift-nio-imap) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,6 @@ let package = Package(
|
||||
name: "IMAPClient",
|
||||
dependencies: [
|
||||
"Models",
|
||||
.product(name: "NIOIMAPCore", package: "swift-nio-imap"),
|
||||
.product(name: "NIOIMAP", package: "swift-nio-imap"),
|
||||
.product(name: "NIOSSL", package: "swift-nio-ssl"),
|
||||
]
|
||||
@@ -47,7 +46,6 @@ let package = Package(
|
||||
name: "IMAPClientTests",
|
||||
dependencies: [
|
||||
"IMAPClient",
|
||||
.product(name: "NIOIMAPCore", package: "swift-nio-imap"),
|
||||
]
|
||||
),
|
||||
.testTarget(name: "SyncEngineTests", dependencies: ["SyncEngine", "IMAPClient", "MailStore"]),
|
||||
|
||||
15
Packages/MagnumOpusCore/Sources/Models/AccountConfig.swift
Normal file
15
Packages/MagnumOpusCore/Sources/Models/AccountConfig.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
public struct AccountConfig: Sendable, Codable, Equatable {
|
||||
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
|
||||
}
|
||||
}
|
||||
9
Packages/MagnumOpusCore/Sources/Models/Credentials.swift
Normal file
9
Packages/MagnumOpusCore/Sources/Models/Credentials.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
public struct Credentials: Sendable {
|
||||
public var username: String
|
||||
public var password: String
|
||||
|
||||
public init(username: String, password: String) {
|
||||
self.username = username
|
||||
self.password = password
|
||||
}
|
||||
}
|
||||
32
Packages/MagnumOpusCore/Sources/Models/EmailAddress.swift
Normal file
32
Packages/MagnumOpusCore/Sources/Models/EmailAddress.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
import Foundation
|
||||
|
||||
public struct EmailAddress: Sendable, Codable, Equatable, Hashable {
|
||||
public var name: String?
|
||||
public var address: String
|
||||
|
||||
public init(name: String? = nil, address: String) {
|
||||
self.name = name
|
||||
self.address = address
|
||||
}
|
||||
|
||||
public var displayName: String {
|
||||
name ?? address
|
||||
}
|
||||
|
||||
/// Parses "Alice <alice@example.com>" or bare "alice@example.com"
|
||||
public static func parse(_ raw: String) -> EmailAddress {
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespaces)
|
||||
guard let openAngle = trimmed.lastIndex(of: "<"),
|
||||
let closeAngle = trimmed.lastIndex(of: ">"),
|
||||
openAngle < closeAngle
|
||||
else {
|
||||
return EmailAddress(address: trimmed)
|
||||
}
|
||||
let addr = String(trimmed[trimmed.index(after: openAngle)..<closeAngle])
|
||||
let namepart = String(trimmed[..<openAngle]).trimmingCharacters(in: .whitespaces)
|
||||
return EmailAddress(
|
||||
name: namepart.isEmpty ? nil : namepart.trimmingCharacters(in: CharacterSet(charactersIn: "\"")),
|
||||
address: addr
|
||||
)
|
||||
}
|
||||
}
|
||||
27
Packages/MagnumOpusCore/Sources/Models/MailboxInfo.swift
Normal file
27
Packages/MagnumOpusCore/Sources/Models/MailboxInfo.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
public struct MailboxInfo: Sendable, Identifiable, Equatable {
|
||||
public var id: String
|
||||
public var accountId: String
|
||||
public var name: String
|
||||
public var unreadCount: Int
|
||||
public var totalCount: Int
|
||||
|
||||
public init(id: String, accountId: String, name: String, unreadCount: Int, totalCount: Int) {
|
||||
self.id = id
|
||||
self.accountId = accountId
|
||||
self.name = name
|
||||
self.unreadCount = unreadCount
|
||||
self.totalCount = totalCount
|
||||
}
|
||||
|
||||
public var systemImage: String {
|
||||
switch name.lowercased() {
|
||||
case "inbox": "tray"
|
||||
case "sent", "sent messages": "paperplane"
|
||||
case "drafts": "doc"
|
||||
case "trash", "deleted messages": "trash"
|
||||
case "archive", "all mail": "archivebox"
|
||||
case "junk", "spam": "xmark.bin"
|
||||
default: "folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Packages/MagnumOpusCore/Sources/Models/MessageSummary.swift
Normal file
41
Packages/MagnumOpusCore/Sources/Models/MessageSummary.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
|
||||
public struct MessageSummary: Sendable, Identifiable, Equatable {
|
||||
public var id: String
|
||||
public var messageId: String?
|
||||
public var threadId: String?
|
||||
public var from: EmailAddress?
|
||||
public var to: [EmailAddress]
|
||||
public var cc: [EmailAddress]
|
||||
public var subject: String?
|
||||
public var date: Date
|
||||
public var snippet: String?
|
||||
public var bodyText: String?
|
||||
public var bodyHtml: String?
|
||||
public var isRead: Bool
|
||||
public var isFlagged: Bool
|
||||
public var hasAttachments: Bool
|
||||
|
||||
public init(
|
||||
id: String, messageId: String?, threadId: String?,
|
||||
from: EmailAddress?, to: [EmailAddress], cc: [EmailAddress],
|
||||
subject: String?, date: Date, snippet: String?,
|
||||
bodyText: String?, bodyHtml: String?,
|
||||
isRead: Bool, isFlagged: Bool, hasAttachments: Bool
|
||||
) {
|
||||
self.id = id
|
||||
self.messageId = messageId
|
||||
self.threadId = threadId
|
||||
self.from = from
|
||||
self.to = to
|
||||
self.cc = cc
|
||||
self.subject = subject
|
||||
self.date = date
|
||||
self.snippet = snippet
|
||||
self.bodyText = bodyText
|
||||
self.bodyHtml = bodyHtml
|
||||
self.isRead = isRead
|
||||
self.isFlagged = isFlagged
|
||||
self.hasAttachments = hasAttachments
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
enum ModelsPlaceholder {}
|
||||
9
Packages/MagnumOpusCore/Sources/Models/SyncEvent.swift
Normal file
9
Packages/MagnumOpusCore/Sources/Models/SyncEvent.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
/// Note: spec defines syncFailed(Error), but Error is not Sendable.
|
||||
/// Using String instead for Swift 6 strict concurrency compliance.
|
||||
public enum SyncEvent: Sendable {
|
||||
case syncStarted
|
||||
case newMessages(count: Int, mailbox: String)
|
||||
case flagsChanged(messageIds: [String])
|
||||
case syncCompleted
|
||||
case syncFailed(String)
|
||||
}
|
||||
5
Packages/MagnumOpusCore/Sources/Models/SyncState.swift
Normal file
5
Packages/MagnumOpusCore/Sources/Models/SyncState.swift
Normal file
@@ -0,0 +1,5 @@
|
||||
public enum SyncState: Sendable, Equatable {
|
||||
case idle
|
||||
case syncing(mailbox: String?)
|
||||
case error(String)
|
||||
}
|
||||
26
Packages/MagnumOpusCore/Sources/Models/ThreadSummary.swift
Normal file
26
Packages/MagnumOpusCore/Sources/Models/ThreadSummary.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
|
||||
public struct ThreadSummary: Sendable, Identifiable, Equatable {
|
||||
public var id: String
|
||||
public var accountId: String
|
||||
public var subject: String?
|
||||
public var lastDate: Date
|
||||
public var messageCount: Int
|
||||
public var unreadCount: Int
|
||||
public var senders: String
|
||||
public var snippet: String?
|
||||
|
||||
public init(
|
||||
id: String, accountId: String, subject: String?, lastDate: Date,
|
||||
messageCount: Int, unreadCount: Int, senders: String, snippet: String?
|
||||
) {
|
||||
self.id = id
|
||||
self.accountId = accountId
|
||||
self.subject = subject
|
||||
self.lastDate = lastDate
|
||||
self.messageCount = messageCount
|
||||
self.unreadCount = unreadCount
|
||||
self.senders = senders
|
||||
self.snippet = snippet
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import Testing
|
||||
@testable import Models
|
||||
|
||||
@Suite("EmailAddress")
|
||||
struct EmailAddressTests {
|
||||
@Test("parses name and address from angle-bracket format")
|
||||
func parsesNameAndAddress() {
|
||||
let addr = EmailAddress.parse("Alice <alice@example.com>")
|
||||
#expect(addr.name == "Alice")
|
||||
#expect(addr.address == "alice@example.com")
|
||||
#expect(addr.displayName == "Alice")
|
||||
}
|
||||
|
||||
@Test("parses bare email address")
|
||||
func parsesBareAddress() {
|
||||
let addr = EmailAddress.parse("bob@example.com")
|
||||
#expect(addr.name == nil)
|
||||
#expect(addr.address == "bob@example.com")
|
||||
#expect(addr.displayName == "bob@example.com")
|
||||
}
|
||||
|
||||
@Test("parses quoted name with angle brackets")
|
||||
func parsesQuotedName() {
|
||||
let addr = EmailAddress.parse("\"Bob Smith\" <bob@example.com>")
|
||||
#expect(addr.name == "Bob Smith")
|
||||
#expect(addr.address == "bob@example.com")
|
||||
}
|
||||
|
||||
@Test("handles empty string gracefully")
|
||||
func handlesEmpty() {
|
||||
let addr = EmailAddress.parse("")
|
||||
#expect(addr.address == "")
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
import Testing
|
||||
Reference in New Issue
Block a user