Erste Alpha-Version
This commit is contained in:
committed by
Dr. Michael Kübler
parent
93feb97739
commit
223bf746ef
@@ -0,0 +1,65 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import MikeMail
|
||||
|
||||
@MainActor
|
||||
struct MailArchiveTests {
|
||||
@Test func diskPersistenceIncrementalReconciliationAndRestore() throws {
|
||||
let base = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: base) }
|
||||
let root = base.appending(path: "Archive")
|
||||
let account = MailAccount(name: "Fixture", email: "fixture@example.com", username: "fixture", host: "example.com")
|
||||
let folder = MailFolder(accountID: account.id, path: "INBOX", name: "Posteingang")
|
||||
let bytes = Data((0...255).map(UInt8.init))
|
||||
let raw = try MIME.compose(from: account.email, to: ["recipient@example.com"], subject: "Offline Büro",
|
||||
body: "Dauerhaft verfügbar", attachments: [MailAttachment(name: "Prüfung.bin", mimeType: "application/octet-stream", data: bytes)])
|
||||
var archive: MailArchive? = try MailArchive(root: root)
|
||||
try archive?.saveConfiguration(ArchiveConfiguration(accounts: [account], folders: [account.id: [folder]], recipients: ["recipient@example.com"]))
|
||||
try archive?.store(raw: raw, folderID: folder.id, validity: "10", uid: "1", flags: [])
|
||||
try archive?.store(raw: raw, folderID: folder.id, validity: "10", uid: "1", flags: ["\\Seen"])
|
||||
#expect(try archive?.messages(folderID: folder.id).count == 1)
|
||||
#expect(try archive?.knownUIDs(folderID: folder.id, validity: "10") == [1])
|
||||
#expect(try archive?.knownUIDs(folderID: folder.id, validity: "11") == [])
|
||||
archive = nil
|
||||
|
||||
let reopened = try MailArchive(root: root)
|
||||
#expect(try reopened.configuration()?.accounts == [account])
|
||||
#expect(try reopened.messages(folderID: folder.id).first?.attachments.first?.data == bytes)
|
||||
#expect(try reopened.messages(folderID: folder.id).first?.isRead == true)
|
||||
try reopened.reconcile(folderID: folder.id, inventory: MailboxInventory(validity: "10", flags: [1: []]))
|
||||
#expect(try reopened.messages(folderID: folder.id).first?.isRead == false)
|
||||
// Partial download of a new UID generation is durable but not mixed into the visible old generation.
|
||||
try reopened.store(raw: raw, folderID: folder.id, validity: "11", uid: "2", flags: [], visible: false)
|
||||
#expect(try reopened.messages(folderID: folder.id).count == 1)
|
||||
#expect(try reopened.knownUIDs(folderID: folder.id, validity: "11") == [2])
|
||||
let backup = base.appending(path: "Backup")
|
||||
try reopened.exportSnapshot(to: backup)
|
||||
let restored = try MailArchive.restoreSnapshot(from: backup, to: base.appending(path: "Restored"))
|
||||
#expect(try restored.messages(folderID: folder.id).first?.body == "Dauerhaft verfügbar")
|
||||
#expect(try restored.messages(folderID: folder.id).first?.attachments.first?.data == bytes)
|
||||
#expect(try restored.configuration()?.recipients == ["recipient@example.com"])
|
||||
|
||||
// A server UIDVALIDITY reset must not attach old data to the reused UID.
|
||||
try reopened.reconcile(folderID: folder.id, inventory: MailboxInventory(validity: "11", flags: [1: []]))
|
||||
#expect(try reopened.messages(folderID: folder.id).isEmpty)
|
||||
let retained = base.appending(path: "RetainedBackup")
|
||||
try reopened.exportSnapshot(to: retained)
|
||||
let manifest = try JSONDecoder().decode(MailArchive.Snapshot.self, from: Data(contentsOf: retained.appending(path: "manifest.json")))
|
||||
#expect(manifest.messages.count == 2)
|
||||
#expect(manifest.messages.allSatisfy { !$0.present })
|
||||
let object = retained.appending(path: "Objects/\(MailArchive.digest(raw)).eml")
|
||||
try Data("corrupt".utf8).write(to: object)
|
||||
#expect(throws: (any Error).self) {
|
||||
try MailArchive.restoreSnapshot(from: retained, to: base.appending(path: "Rejected"))
|
||||
}
|
||||
#expect(!FileManager.default.fileExists(atPath: base.appending(path: "Rejected").path))
|
||||
}
|
||||
|
||||
@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"]) }
|
||||
let flags = try IMAPClient.parseFlags(["* 1 FETCH (UID 7 FLAGS (\\Seen \\Flagged))", "* 2 FETCH (FLAGS () UID 9)"])
|
||||
#expect(flags[7] == ["\\Seen", "\\Flagged"])
|
||||
#expect(flags[9] == [])
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,91 @@
|
||||
//
|
||||
// MikeMailTests.swift
|
||||
// MikeMailTests
|
||||
//
|
||||
// Created by Dr. med. Michael Kübler on 06.11.26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import MikeMail
|
||||
|
||||
@MainActor
|
||||
struct MikeMailTests {
|
||||
|
||||
@Test func example() async throws {
|
||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||
// Swift Testing Documentation
|
||||
// https://developer.apple.com/documentation/testing
|
||||
@Test func recipientSuggestionsAndInsertion() {
|
||||
let history = ["anna@example.com", "bert@example.com", "anna@work.de"]
|
||||
#expect(RecipientCompletion.matches(in: history, input: "ANN") == ["anna@example.com", "anna@work.de"])
|
||||
#expect(RecipientCompletion.matches(in: history, input: "anna@example.com, ann") == ["anna@work.de"])
|
||||
#expect(RecipientCompletion.matches(in: history, input: "") == [])
|
||||
#expect(RecipientCompletion.inserting("bert@example.com", into: "anna@example.com, be") == "anna@example.com, bert@example.com, ")
|
||||
#expect(RecipientCompletion.inserting("anna@example.com", into: "anna@example.com, an") == "anna@example.com, ")
|
||||
}
|
||||
|
||||
@Test func imapEscapingAndMailboxNames() throws {
|
||||
#expect(try IMAPClient.quote("Ordner \"Büro\"") == "\"Ordner \\\"Büro\\\"\"")
|
||||
#expect(throws: (any Error).self) { try IMAPClient.quote("INBOX\r\nDELETE INBOX") }
|
||||
#expect(IMAPClient.tokens("\"/\" \"Ordner mit Leerzeichen\"") == ["/", "Ordner mit Leerzeichen"])
|
||||
#expect(IMAPClient.decodeMailbox("B&APw-ro") == "Büro")
|
||||
#expect(IMAPClient.decodeMailbox("A &- B") == "A & B")
|
||||
}
|
||||
|
||||
@Test func binaryAttachmentRoundTrip() throws {
|
||||
let bytes = Data((0...255).map(UInt8.init))
|
||||
let attachment = MailAttachment(name: "Büroprüfung.bin", mimeType: "application/octet-stream", data: bytes)
|
||||
let raw = try MIME.compose(from: "sender@example.com", to: ["recipient@example.com"],
|
||||
subject: "Grüße aus dem Büro", body: "Zeile 1\n.\nÄÖÜ ß", attachments: [attachment])
|
||||
let decoded = MIME.parse(raw, id: "1")
|
||||
#expect(decoded.subject == "Grüße aus dem Büro")
|
||||
#expect(decoded.body == "Zeile 1\n.\nÄÖÜ ß")
|
||||
#expect(decoded.attachments.count == 1)
|
||||
#expect(decoded.attachments.first?.name == "Büroprüfung.bin")
|
||||
#expect(decoded.attachments.first?.data == bytes)
|
||||
}
|
||||
|
||||
@Test func incomingNestedMIME() {
|
||||
let raw = """
|
||||
From: =?UTF-8?Q?J=C3=B6rg?= <joerg@example.com>
|
||||
Subject: =?UTF-8?B?R3LDvMOfZQ==?=
|
||||
Date: Fri, 06 Nov 2026 10:30:00 +0100
|
||||
Content-Type: multipart/mixed; boundary="outer"
|
||||
|
||||
--outer
|
||||
Content-Type: multipart/alternative; boundary="inner"
|
||||
|
||||
--inner
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Gr=C3=BC=C3=9Fe aus dem B=C3=BCro.
|
||||
--inner
|
||||
Content-Type: text/html; charset=utf-8
|
||||
|
||||
<p>Alternative</p>
|
||||
--inner--
|
||||
--outer
|
||||
Content-Type: application/pdf
|
||||
Content-Disposition: attachment; filename="rechnung.pdf"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
JVBERi0xLjc=
|
||||
--outer--
|
||||
"""
|
||||
let message = MIME.parse(Data(raw.replacingOccurrences(of: "\n", with: "\r\n").utf8), id: "2")
|
||||
#expect(message.sender == "Jörg <joerg@example.com>")
|
||||
#expect(message.subject == "Grüße")
|
||||
#expect(message.body == "Grüße aus dem Büro.")
|
||||
#expect(message.attachments.first?.name == "rechnung.pdf")
|
||||
#expect(message.attachments.first?.data == Data("%PDF-1.7".utf8))
|
||||
#expect(message.date != .distantPast)
|
||||
}
|
||||
|
||||
@Test func foldedHeaderAndQuotedPrintable() {
|
||||
#expect(MIME.decodeHeader("=?UTF-8?Q?Gr=C3=BC=C3=9Fe?= =?UTF-8?Q?_Welt?=") == "Grüße Welt")
|
||||
#expect(MIME.headers("Subject: Erste\r\n zweite\r\nFrom: a@example.com")["subject"] == "Erste zweite")
|
||||
#expect(MIME.quotedPrintable(Data("a=\r\nb=3Dc".utf8)) == Data("ab=c".utf8))
|
||||
}
|
||||
|
||||
@Test func preventsHeaderInjection() {
|
||||
#expect(throws: (any Error).self) {
|
||||
try MIME.compose(from: "a@example.com", to: ["b@example.com\r\nBcc: x@example.com"],
|
||||
subject: "Hallo", body: "", attachments: [])
|
||||
}
|
||||
#expect(throws: (any Error).self) {
|
||||
try MIME.compose(from: "a@example.com", to: ["b@example.com"],
|
||||
subject: "Hallo\r\nBcc: x@example.com", body: "", attachments: [])
|
||||
}
|
||||
#expect(!MIME.validAddress("not an address"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user