116 lines
3.9 KiB
Swift
116 lines
3.9 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
struct MailAccount: Identifiable, Codable, Hashable, Sendable {
|
|
var id = UUID()
|
|
var name = ""
|
|
var email = ""
|
|
var username = ""
|
|
var host = ""
|
|
var smtpPort: UInt16 = 587
|
|
var imapPort: UInt16 = 993
|
|
|
|
static let testAccount = MailAccount(name: "Testkonto", email: "test@kuebler-sulzbach.de",
|
|
username: "test@kuebler-sulzbach.de", host: "w0099985.kasserver.com")
|
|
}
|
|
|
|
struct MailFolder: Identifiable, Hashable, Codable, Sendable {
|
|
var id: String { accountID.uuidString + ":" + path }
|
|
let accountID: UUID
|
|
let path: String
|
|
let name: String
|
|
var selectable = true
|
|
var children: [MailFolder]?
|
|
var delimiter: String?
|
|
var specialUse: [String]?
|
|
|
|
var isInbox: Bool { path.uppercased() == "INBOX" }
|
|
var isTrash: Bool {
|
|
specialUse?.contains("\\trash") == true || ["trash", "papierkorb", "deleted messages", "deleted items"].contains(name.lowercased())
|
|
}
|
|
|
|
var icon: String {
|
|
switch path.lowercased() {
|
|
case "inbox": "tray"
|
|
case let value where value.contains("sent"): "paperplane"
|
|
case let value where value.contains("trash"): "trash"
|
|
case let value where value.contains("draft"): "doc"
|
|
default: "folder"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MailAttachment: Identifiable, Sendable {
|
|
var id = UUID()
|
|
let name: String
|
|
let mimeType: String
|
|
let data: Data
|
|
var contentID: String? = nil
|
|
}
|
|
|
|
struct MailMessage: Identifiable, Sendable {
|
|
let id: String
|
|
let sender: String
|
|
let recipient: String
|
|
let subject: String
|
|
let date: Date
|
|
let body: String
|
|
let attachments: [MailAttachment]
|
|
var isRead = false
|
|
var cc = ""
|
|
var replyTo = ""
|
|
var htmlBody: String? = nil
|
|
var displaySender: String { MailDisplay.sender(sender) }
|
|
var attachmentCount: Int { attachments.count }
|
|
}
|
|
|
|
enum MailError: LocalizedError {
|
|
case message(String)
|
|
case authentication(String)
|
|
var errorDescription: String? {
|
|
switch self { case .message(let text), .authentication(let text): text }
|
|
}
|
|
}
|
|
|
|
enum PasswordStore {
|
|
private static func query(_ id: UUID) -> [String: Any] {
|
|
[kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: "de.mikemail.account",
|
|
kSecAttrAccount as String: id.uuidString]
|
|
}
|
|
|
|
static func save(_ password: String, for id: UUID) throws {
|
|
let attributes = [kSecValueData as String: Data(password.utf8)]
|
|
let status = SecItemUpdate(query(id) as CFDictionary, attributes as CFDictionary)
|
|
if status == errSecItemNotFound {
|
|
var entry = query(id)
|
|
entry.merge(attributes) { _, new in new }
|
|
entry[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlocked
|
|
let result = SecItemAdd(entry as CFDictionary, nil)
|
|
guard result == errSecSuccess else { throw MailError.message("Passwort konnte nicht im Schlüsselbund gespeichert werden (\(result)).") }
|
|
} else if status != errSecSuccess {
|
|
throw MailError.message("Schlüsselbundfehler (\(status)).")
|
|
}
|
|
}
|
|
|
|
static func delete(for id: UUID) throws {
|
|
let status = SecItemDelete(query(id) as CFDictionary)
|
|
guard status == errSecSuccess || status == errSecItemNotFound else {
|
|
throw MailError.message("Passwort konnte nicht entfernt werden (\(status)).")
|
|
}
|
|
}
|
|
|
|
static func password(for id: UUID) throws -> String {
|
|
var entry = query(id)
|
|
entry[kSecReturnData as String] = true
|
|
entry[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
var result: CFTypeRef?
|
|
let status = SecItemCopyMatching(entry as CFDictionary, &result)
|
|
guard status == errSecSuccess, let data = result as? Data,
|
|
let value = String(data: data, encoding: .utf8) else {
|
|
throw MailError.message("Bitte unter „Konten“ das Passwort für dieses Konto speichern.")
|
|
}
|
|
return value
|
|
}
|
|
}
|