Alpha 2 Update
This commit is contained in:
committed by
Dr. Michael Kübler
parent
223bf746ef
commit
8e95cc3acd
@@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import MikeMail
|
||||
|
||||
@MainActor
|
||||
struct MailActionTests {
|
||||
@Test func ccAndBccStaySeparateFromEnvelope() throws {
|
||||
let raw = try MIME.compose(from: "sender@example.com", to: ["to@example.com"],
|
||||
cc: ["copy@example.com"], bcc: ["hidden@example.com"], subject: "CC BCC", body: "Hello", attachments: [])
|
||||
let headers = MIME.split(raw).0
|
||||
#expect(headers["cc"] == "copy@example.com")
|
||||
#expect(headers["bcc"] == nil)
|
||||
#expect(!String(decoding: raw, as: UTF8.self).contains("hidden@example.com"))
|
||||
#expect(MIME.parse(raw, id: "1").cc == "copy@example.com")
|
||||
#expect(throws: (any Error).self) {
|
||||
try MIME.compose(from: "sender@example.com", to: [], bcc: ["bad\r\nBcc: evil@example.com"],
|
||||
subject: "No injection", body: "", attachments: [])
|
||||
}
|
||||
let blindOnly = try MIME.compose(from: "sender@example.com", to: [], bcc: ["hidden@example.com"],
|
||||
subject: "Blind only", body: "", attachments: [])
|
||||
#expect(MIME.split(blindOnly).0["bcc"] == nil)
|
||||
}
|
||||
|
||||
@Test func replyUsesReplyToAndForwardIncludesAttachments() {
|
||||
let attachment = MailAttachment(name: "sample.txt", mimeType: "text/plain", data: Data("abc".utf8))
|
||||
let mail = MailMessage(id: "id", sender: "Sender <from@example.com>", recipient: "to@example.com",
|
||||
subject: "Question", date: Date(), body: "Original", attachments: [attachment], replyTo: "Reply <reply@example.com>")
|
||||
let account = UUID()
|
||||
let reply = ComposeDraft.from(mail, accountID: account, forwarding: false)
|
||||
#expect(reply.recipient == "reply@example.com")
|
||||
#expect(reply.accountID == account)
|
||||
#expect(reply.body.contains("> Original"))
|
||||
#expect(reply.attachments.isEmpty)
|
||||
let forward = ComposeDraft.from(mail, accountID: account, forwarding: true)
|
||||
#expect(forward.recipient.isEmpty)
|
||||
#expect(forward.attachments.first?.data == attachment.data)
|
||||
}
|
||||
|
||||
@Test func newAccountRequiresValidationAndFlagsDriveCounts() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
let archive = try MailArchive(root: root)
|
||||
try archive.saveConfiguration(ArchiveConfiguration(accounts: [], folders: [:], recipients: []))
|
||||
let store = MailStore(archive: archive)
|
||||
let account = MailAccount(name: "New", email: "new@example.com", username: "new", host: "example.com")
|
||||
#expect(throws: (any Error).self) { try store.save(account: account, password: "unused") }
|
||||
#expect(store.accounts.isEmpty)
|
||||
let folder = MailFolder(accountID: account.id, path: "INBOX", name: "Inbox")
|
||||
let raw = Data("Subject: Flags\r\nMessage-ID: <fixture@example.com>\r\n\r\nBody".utf8)
|
||||
try archive.store(raw: raw, folderID: folder.id, validity: "1", uid: "1", flags: [])
|
||||
#expect(try archive.unreadCounts()[folder.id] == 1)
|
||||
try archive.reconcile(folderID: folder.id, inventory: MailboxInventory(validity: "1", flags: [1: ["\\Seen"]]))
|
||||
#expect(try archive.unreadCounts()[folder.id] == nil)
|
||||
try archive.store(raw: raw, folderID: account.id.uuidString + ":__local_outgoing__",
|
||||
validity: "local", uid: "outgoing", flags: ["\\Seen", "SentCopyPending"])
|
||||
#expect(try archive.pendingSent(accountID: account.id).count == 1)
|
||||
try archive.store(raw: raw, folderID: account.id.uuidString + ":__local_outgoing__",
|
||||
validity: "local", uid: "outgoing", flags: ["\\Seen", "SentCopied"], visible: false)
|
||||
#expect(try archive.pendingSent(accountID: account.id).isEmpty)
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,46 @@ struct MailArchiveTests {
|
||||
#expect(!FileManager.default.fileExists(atPath: base.appending(path: "Rejected").path))
|
||||
}
|
||||
|
||||
@Test func localResetIsAccountScopedAndForcesRedownload() async throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
let archive = try MailArchive(root: root)
|
||||
let a = MailAccount(name: "A", email: "a@example.com", username: "a", host: "example.com")
|
||||
let b = MailAccount(name: "B", email: "b@example.com", username: "b", host: "example.com")
|
||||
let inboxA = MailFolder(accountID: a.id, path: "INBOX", name: "Posteingang")
|
||||
let inboxB = MailFolder(accountID: b.id, path: "INBOX", name: "Posteingang")
|
||||
let hiddenA = MailFolder(accountID: a.id, path: "OldFolder", name: "Alt")
|
||||
let outgoingA = MailFolder(accountID: a.id, path: "__local_outgoing__", name: "Versand")
|
||||
let shared = Data("Subject: Shared\r\n\r\nShared body".utf8)
|
||||
let unique = Data("Subject: Local\r\n\r\nOnly A".utf8)
|
||||
try archive.saveConfiguration(ArchiveConfiguration(accounts: [a, b], folders: [a.id: [inboxA], b.id: [inboxB]], recipients: ["x@example.com"]))
|
||||
try archive.store(raw: shared, folderID: inboxA.id, validity: "1", uid: "1", flags: [])
|
||||
try archive.store(raw: shared, folderID: inboxB.id, validity: "1", uid: "1", flags: [])
|
||||
try archive.store(raw: unique, folderID: hiddenA.id, validity: "old", uid: "2", flags: [], visible: false)
|
||||
try archive.store(raw: unique, folderID: outgoingA.id, validity: "local", uid: "pending", flags: ["Pending"])
|
||||
let store = MailStore(archive: archive)
|
||||
await store.load(folderID: inboxA.id)
|
||||
#expect(store.messages.count == 1)
|
||||
store.isBusy = true
|
||||
#expect(throws: (any Error).self) { try store.clearLocalMessages(accountID: a.id) }
|
||||
#expect(try archive.knownUIDs(folderID: inboxA.id, validity: "1") == [1])
|
||||
store.isBusy = false
|
||||
#expect(try store.clearLocalMessages(accountID: a.id) == 0)
|
||||
#expect(store.messages.isEmpty)
|
||||
#expect(store.accounts == [a, b])
|
||||
#expect(try archive.configuration()?.folders[a.id] == [inboxA])
|
||||
#expect(try archive.knownUIDs(folderID: inboxA.id, validity: "1").isEmpty)
|
||||
#expect(try archive.knownUIDs(folderID: hiddenA.id, validity: "old").isEmpty)
|
||||
#expect(try archive.messages(folderID: outgoingA.id).isEmpty)
|
||||
#expect(try archive.messages(folderID: inboxB.id).count == 1)
|
||||
#expect(FileManager.default.fileExists(atPath: root.appending(path: "Objects/\(MailArchive.digest(shared)).eml").path))
|
||||
#expect(!FileManager.default.fileExists(atPath: root.appending(path: "Objects/\(MailArchive.digest(unique)).eml").path))
|
||||
let reopened = try MailArchive(root: root)
|
||||
#expect(try reopened.knownUIDs(folderID: inboxA.id, validity: "1").isEmpty)
|
||||
try reopened.store(raw: shared, folderID: inboxA.id, validity: "1", uid: "1", flags: [])
|
||||
#expect(try reopened.messages(folderID: inboxA.id).count == 1)
|
||||
}
|
||||
|
||||
@Test func imapInventoryParsing() throws {
|
||||
#expect(try IMAPClient.uidValidity(from: ["* OK [UIDVALIDITY 123] UIDs valid"]) == "123")
|
||||
#expect(throws: (any Error).self) { try IMAPClient.uidValidity(from: ["* OK hello"]) }
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import MikeMail
|
||||
|
||||
@MainActor
|
||||
struct MailIdleTests {
|
||||
@Test func classifiesServerResponses() {
|
||||
#expect(IMAPClient.isMailboxChange("* 12 EXISTS"))
|
||||
#expect(IMAPClient.isMailboxChange("* 2 EXPUNGE"))
|
||||
#expect(IMAPClient.isMailboxChange("* 2 FETCH (FLAGS (\\Seen))"))
|
||||
#expect(!IMAPClient.isMailboxChange("* OK Still here"))
|
||||
#expect(!IMAPClient.isMailboxChange("M4 OK IDLE completed"))
|
||||
#expect(IMAPClient.isAuthenticationFailure("M1 NO [AUTHENTICATIONFAILED] Authentication failed."))
|
||||
#expect(IMAPClient.isAuthenticationFailure("M1 NO LOGIN failed"))
|
||||
#expect(!IMAPClient.isAuthenticationFailure("M1 NO [UNAVAILABLE] Server temporarily unavailable"))
|
||||
#expect(!IMAPClient.isAuthenticationFailure("* BYE Connection closed"))
|
||||
}
|
||||
|
||||
@Test func queuesPasswordPromptsPerAccount() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
let archive = try MailArchive(root: root)
|
||||
let a = MailAccount(name: "A"), b = MailAccount(name: "B")
|
||||
try archive.saveConfiguration(ArchiveConfiguration(accounts: [a, b], folders: [:], recipients: []))
|
||||
let store = MailStore(archive: archive)
|
||||
store.requestPassword(accountID: a.id)
|
||||
store.requestPassword(accountID: a.id)
|
||||
store.requestPassword(accountID: b.id)
|
||||
#expect(store.authenticationAccount?.id == a.id)
|
||||
store.dismissPasswordRequest()
|
||||
#expect(store.authenticationAccount?.id == b.id)
|
||||
store.dismissPasswordRequest()
|
||||
#expect(store.authenticationAccount == nil)
|
||||
store.requestPassword(accountID: UUID())
|
||||
#expect(store.authenticationAccount == nil)
|
||||
store.configureIdle(enabled: false)
|
||||
#expect(store.idleStatuses.isEmpty)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import MikeMail
|
||||
|
||||
@MainActor
|
||||
struct MailWorkflowTests {
|
||||
@Test func searchScopesAndExpressions() {
|
||||
let mail = MailMessage(id: "1", sender: "anna@example.com", recipient: "", subject: "Rechnung 2026",
|
||||
date: Date(), body: "Betrag: 123,45 EUR", attachments: [])
|
||||
#expect(MailSearch(text: "123,45", subject: false, sender: false, body: true, regularExpression: false).matches(mail))
|
||||
#expect(!MailSearch(text: "123,45", subject: true, sender: true, body: false, regularExpression: false).matches(mail))
|
||||
#expect(MailSearch(text: #"Rechnung \d{4}$"#, subject: true, sender: false, body: false, regularExpression: true).matches(mail))
|
||||
#expect(MailSearch(text: "^ANNA@", subject: false, sender: true, body: false, regularExpression: true).matches(mail))
|
||||
let invalid = MailSearch(text: "[", subject: true, sender: true, body: true, regularExpression: true)
|
||||
#expect(invalid.error != nil)
|
||||
#expect(!invalid.matches(mail))
|
||||
}
|
||||
|
||||
@Test func dragSelectionAndMailboxNames() {
|
||||
let ids: Set<String> = ["account:folder:1:2", "account:folder:1:9"]
|
||||
#expect(Set(MailDrag.decode(MailDrag.encode(ids))) == ids)
|
||||
#expect(MailDrag.decode("untrusted input").isEmpty)
|
||||
for name in ["Büro & Reisen", "日本語", "ASCII", "📨"] {
|
||||
#expect(IMAPClient.decodeMailbox(IMAPClient.encodeMailbox(name)) == name)
|
||||
}
|
||||
#expect(AttachmentList.safeName("../../file.txt") == "file.txt")
|
||||
#expect(AttachmentList.safeName(#"C:\temp\file.pdf"#) == "file.pdf")
|
||||
#expect(AttachmentList.safeName("..") == "Anhang")
|
||||
}
|
||||
|
||||
@Test func unifiedFoldersAndAccountOrderSurviveReopen() async throws {
|
||||
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
let archive = try MailArchive(root: root)
|
||||
let a = MailAccount(name: "A"), b = MailAccount(name: "B")
|
||||
let inboxA = MailFolder(accountID: a.id, path: "INBOX", name: "Posteingang")
|
||||
let inboxB = MailFolder(accountID: b.id, path: "INBOX", name: "Posteingang")
|
||||
let trash = MailFolder(accountID: a.id, path: "Deleted", name: "Gelöscht", specialUse: ["\\trash"])
|
||||
try archive.saveConfiguration(ArchiveConfiguration(accounts: [a, b],
|
||||
folders: [a.id: [inboxA, trash], b.id: [inboxB]], recipients: []))
|
||||
let raw = Data("Subject: Same\r\n\r\nBody".utf8)
|
||||
for folder in [inboxA, inboxB, trash] {
|
||||
try archive.store(raw: raw, folderID: folder.id, validity: "1", uid: "1", flags: [])
|
||||
}
|
||||
let store = MailStore(archive: archive)
|
||||
await store.load(folderID: "unified:inbox")
|
||||
#expect(store.messages.count == 2)
|
||||
#expect(Set(store.messages.map(\.id)).count == 2)
|
||||
await store.load(folderID: "unified:trash")
|
||||
#expect(store.messages.count == 1)
|
||||
#expect(store.selectionContainsTrash(store.messages.map(\.id)))
|
||||
store.reorderAccount(b.id, before: a.id)
|
||||
let reopened = try MailArchive(root: root)
|
||||
#expect(try reopened.configuration()?.accounts.map(\.id) == [b.id, a.id])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user