Alpha 7
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Xcode user-specific interface state
|
||||
*.xcuserstate
|
||||
|
||||
MikeMail.xcodeproj/project.xcworkspace/xcuserdata/michael.xcuserdatad/UserInterfaceState.xcuserstate
Generated
BIN
Binary file not shown.
@@ -169,15 +169,41 @@ struct MailSidebarList: NSViewRepresentable {
|
||||
texts.spacing = 2
|
||||
let title = NSTextField(labelWithString: entry.title)
|
||||
title.lineBreakMode = .byTruncatingTail
|
||||
title.font = .systemFont(ofSize: 13)
|
||||
texts.addArrangedSubview(title)
|
||||
if let account = entry.account {
|
||||
title.font = .systemFont(ofSize: 13, weight: .semibold)
|
||||
let nameRow = NSStackView()
|
||||
nameRow.orientation = .horizontal
|
||||
nameRow.alignment = .centerY
|
||||
nameRow.spacing = 6
|
||||
nameRow.addArrangedSubview(title)
|
||||
let colorBadge = NSView()
|
||||
colorBadge.wantsLayer = true
|
||||
colorBadge.layer?.backgroundColor = NSColor(Color(accountHex: account.colorHex)).cgColor
|
||||
colorBadge.layer?.cornerRadius = 6
|
||||
colorBadge.translatesAutoresizingMaskIntoConstraints = false
|
||||
colorBadge.setAccessibilityLabel("Kontofarbe")
|
||||
let badgeContainer = NSView()
|
||||
badgeContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
badgeContainer.addSubview(colorBadge)
|
||||
NSLayoutConstraint.activate([
|
||||
badgeContainer.widthAnchor.constraint(equalToConstant: 12),
|
||||
badgeContainer.heightAnchor.constraint(equalToConstant: 18),
|
||||
colorBadge.widthAnchor.constraint(equalToConstant: 12),
|
||||
colorBadge.heightAnchor.constraint(equalToConstant: 12),
|
||||
colorBadge.centerXAnchor.constraint(equalTo: badgeContainer.centerXAnchor),
|
||||
colorBadge.centerYAnchor.constraint(equalTo: badgeContainer.centerYAnchor, constant: 1)
|
||||
])
|
||||
nameRow.addArrangedSubview(badgeContainer)
|
||||
texts.addArrangedSubview(nameRow)
|
||||
let email = NSTextField(labelWithString: account.email)
|
||||
email.font = .systemFont(ofSize: 11)
|
||||
email.textColor = .secondaryLabelColor
|
||||
email.lineBreakMode = .byTruncatingTail
|
||||
texts.addArrangedSubview(email)
|
||||
cell.setAccessibilityIdentifier("accountRow-" + account.email)
|
||||
} else {
|
||||
title.font = .systemFont(ofSize: 13)
|
||||
texts.addArrangedSubview(title)
|
||||
}
|
||||
texts.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
stack.addArrangedSubview(texts)
|
||||
|
||||
+75
-74
@@ -13,12 +13,7 @@ struct AccountsView: View {
|
||||
@State private var confirmingReset = false
|
||||
@State private var resetResult: String?
|
||||
@State private var testing = false
|
||||
@State private var testedAccount: MailAccount?
|
||||
@State private var testedPassword = ""
|
||||
@State private var testMailAccount: MailAccount?
|
||||
@State private var askingTestMail = false
|
||||
@State private var accountsNeedingRefresh: Set<UUID> = []
|
||||
@AppStorage("offerTestMail") private var offerTestMail = true
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@@ -67,12 +62,8 @@ struct AccountsView: View {
|
||||
HStack {
|
||||
Spacer()
|
||||
if testing { ProgressView().controlSize(.small) }
|
||||
Button("Verbindung testen") { testConnection() }
|
||||
.disabled(selection == nil || testing)
|
||||
Button("Speichern") { save() }
|
||||
.disabled(selection == nil || testing || store.isBusy ||
|
||||
(newAccount?.id == selection && (testedAccount != selection.flatMap { drafts[$0] } ||
|
||||
testedPassword != (selection.flatMap { passwords[$0] } ?? ""))))
|
||||
.disabled(selection == nil || testing || store.isBusy)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
}
|
||||
@@ -100,22 +91,9 @@ struct AccountsView: View {
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 22)
|
||||
}
|
||||
.frame(width: 850, height: 600)
|
||||
.frame(width: 1_000, height: 820)
|
||||
.interactiveDismissDisabled(testing)
|
||||
.disabled(testing)
|
||||
.alert("Testmail senden?", isPresented: $askingTestMail) {
|
||||
Button("Nein", role: .cancel) {}
|
||||
Button("Testmail senden") {
|
||||
if let account = testMailAccount {
|
||||
Task {
|
||||
do {
|
||||
try await store.send(accountID: account.id, recipients: account.email,
|
||||
subject: "MikeMail – Testnachricht", body: "Versand und Empfang mit MikeMail sind eingerichtet.", attachments: [])
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
}
|
||||
}
|
||||
} message: { Text("Eine Testnachricht an \(testMailAccount?.email ?? "") senden?") }
|
||||
.onAppear {
|
||||
drafts = Dictionary(uniqueKeysWithValues: store.accounts.map { ($0.id, $0) })
|
||||
selection = store.accounts.first?.id
|
||||
@@ -148,22 +126,32 @@ struct AccountsView: View {
|
||||
|
||||
private func save() {
|
||||
guard let id = selection, let account = drafts[id] else { return }
|
||||
do {
|
||||
let isNew = !store.accounts.contains { $0.id == id }
|
||||
try store.save(account: account, password: passwords[id] ?? "")
|
||||
passwords[id] = nil
|
||||
if newAccount?.id == id { newAccount = nil }
|
||||
if isNew {
|
||||
Task {
|
||||
while store.isBusy { try? await Task.sleep(for: .milliseconds(150)) }
|
||||
await store.refresh(folderID: nil, accountID: id)
|
||||
if offerTestMail { testMailAccount = account; askingTestMail = true }
|
||||
let password = passwords[id] ?? ""
|
||||
testing = true
|
||||
error = nil
|
||||
resetResult = nil
|
||||
Task {
|
||||
do {
|
||||
try await store.testConnection(account: account, password: password)
|
||||
let isNew = !store.accounts.contains { $0.id == id }
|
||||
try store.save(account: account, password: password)
|
||||
passwords[id] = nil
|
||||
if newAccount?.id == id { newAccount = nil }
|
||||
if isNew {
|
||||
Task {
|
||||
while store.isBusy { try? await Task.sleep(for: .milliseconds(150)) }
|
||||
await store.refresh(folderID: nil, accountID: id)
|
||||
}
|
||||
} else {
|
||||
accountsNeedingRefresh.insert(id)
|
||||
}
|
||||
} else {
|
||||
accountsNeedingRefresh.insert(id)
|
||||
testing = false
|
||||
dismiss()
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
testing = false
|
||||
}
|
||||
error = nil
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshChangedAccounts() {
|
||||
@@ -178,23 +166,6 @@ struct AccountsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func testConnection() {
|
||||
guard let id = selection, let account = drafts[id] else { return }
|
||||
let password = passwords[id] ?? ""
|
||||
testing = true
|
||||
error = nil
|
||||
testedAccount = nil
|
||||
Task {
|
||||
defer { testing = false }
|
||||
do {
|
||||
try await store.testConnection(account: account, password: password)
|
||||
testedAccount = account
|
||||
testedPassword = password
|
||||
resetResult = "IMAP- und SMTP-Verbindung erfolgreich geprüft."
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
}
|
||||
|
||||
private func delete() {
|
||||
guard let id = selection else { return }
|
||||
do {
|
||||
@@ -246,31 +217,20 @@ struct AccountSettingsForm: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.accessibilityIdentifier("accountEmail")
|
||||
}
|
||||
Divider()
|
||||
.gridCellColumns(2)
|
||||
.padding(.vertical, 4)
|
||||
GridRow {
|
||||
Text("Login-Daten")
|
||||
.font(.headline)
|
||||
.gridCellColumns(2)
|
||||
}
|
||||
GridRow(alignment: .center) {
|
||||
Text("Benutzername")
|
||||
TextField("Benutzername", text: $account.username)
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
GridRow(alignment: .center) {
|
||||
Text("Server")
|
||||
TextField("Server", text: $account.host)
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
GridRow(alignment: .center) {
|
||||
Text("IMAP-Port (TLS)")
|
||||
TextField("IMAP-Port (TLS)", value: $account.imapPort, format: .number.grouping(.never))
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
GridRow(alignment: .center) {
|
||||
Text("SMTP-Port (587 STARTTLS / 465 TLS)")
|
||||
TextField("SMTP-Port (587 STARTTLS / 465 TLS)", value: $account.smtpPort,
|
||||
format: .number.grouping(.never))
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
GridRow(alignment: .center) {
|
||||
Text("Passwort")
|
||||
HStack(spacing: 8) {
|
||||
@@ -290,6 +250,47 @@ struct AccountSettingsForm: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
Divider()
|
||||
.gridCellColumns(2)
|
||||
.padding(.vertical, 4)
|
||||
GridRow {
|
||||
Text("Posteingang (IMAP)")
|
||||
.font(.headline)
|
||||
.gridCellColumns(2)
|
||||
}
|
||||
GridRow(alignment: .center) {
|
||||
Text("IMAP-Server")
|
||||
TextField("IMAP-Server", text: $account.host)
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
GridRow(alignment: .center) {
|
||||
Text("IMAP-Port (TLS)")
|
||||
TextField("IMAP-Port (TLS)", value: $account.imapPort, format: .number.grouping(.never))
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
Divider()
|
||||
.gridCellColumns(2)
|
||||
.padding(.vertical, 4)
|
||||
GridRow {
|
||||
Text("Postausgang (SMTP)")
|
||||
.font(.headline)
|
||||
.gridCellColumns(2)
|
||||
}
|
||||
GridRow(alignment: .center) {
|
||||
Text("SMTP-Server")
|
||||
TextField("SMTP-Server", text: $account.smtpServer)
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
GridRow(alignment: .center) {
|
||||
Text("SMTP-Port (587 STARTTLS / 465 TLS)")
|
||||
TextField("SMTP-Port (587 STARTTLS / 465 TLS)", value: $account.smtpPort,
|
||||
format: .number.grouping(.never))
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ struct ContentView: View {
|
||||
selection: $selectedMessages,
|
||||
busy: store.isBusy,
|
||||
accountIndicators: accountIndicators,
|
||||
canRestoreToInbox: { store.selectionCanRestoreToInbox(Array($0)) },
|
||||
action: perform
|
||||
)
|
||||
.onDeleteCommand { deleteSelection() }
|
||||
@@ -277,6 +278,8 @@ struct ContentView: View {
|
||||
}
|
||||
case .delete:
|
||||
requestDeletion(Array(ids))
|
||||
case .restoreToInbox:
|
||||
Task { await store.restoreToInbox(ids: Array(ids)) }
|
||||
case .read, .unread:
|
||||
Task { await store.setRead(ids: Array(ids), read: command == .read) }
|
||||
}
|
||||
@@ -450,6 +453,7 @@ struct MessageTable: View {
|
||||
@Binding var selection: Set<String>
|
||||
var busy = false
|
||||
var accountIndicators: [UUID: AccountIndicator] = [:]
|
||||
var canRestoreToInbox: (Set<String>) -> Bool = { _ in false }
|
||||
var action: (MessageCommand, Set<String>) -> Void = { _, _ in }
|
||||
@AppStorage("mailSortColumn") private var savedColumn = "date"
|
||||
@AppStorage("mailSortAscending") private var savedAscending = false
|
||||
@@ -507,6 +511,13 @@ struct MessageTable: View {
|
||||
transaction.animation = nil
|
||||
}
|
||||
.contextMenu(forSelectionType: String.self) { ids in
|
||||
if canRestoreToInbox(ids) {
|
||||
Button("In Posteingang verschieben", systemImage: "tray.and.arrow.down") {
|
||||
action(.restoreToInbox, ids)
|
||||
}
|
||||
.disabled(busy)
|
||||
Divider()
|
||||
}
|
||||
if ids.count == 1 {
|
||||
Button("In neuem Fenster öffnen") { action(.open, ids) }
|
||||
MessageActionButtons { action($0, ids) }
|
||||
@@ -568,15 +579,8 @@ struct MessageReader: View {
|
||||
ScrollView {
|
||||
if let message {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(message.subject).font(.title2).bold().textSelection(.enabled)
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("Von: \(message.sender)")
|
||||
Text("An: \(message.recipient)").foregroundStyle(.secondary)
|
||||
if !message.cc.isEmpty { Text("CC: \(message.cc)").foregroundStyle(.secondary) }
|
||||
if message.date != .distantPast {
|
||||
Text(message.date, format: .dateTime.day().month(.wide).year().hour().minute()).foregroundStyle(.secondary)
|
||||
}
|
||||
}.textSelection(.enabled)
|
||||
MessageHeader(subject: message.subject, sender: message.sender,
|
||||
recipient: message.recipient, cc: message.cc, date: message.date)
|
||||
Divider()
|
||||
if let html = message.htmlBody {
|
||||
HTMLMessageBody(html: html, attachments: message.attachments)
|
||||
@@ -602,6 +606,48 @@ struct MessageReader: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct MessageHeader: View {
|
||||
let subject: String
|
||||
let sender: String
|
||||
let recipient: String
|
||||
let cc: String
|
||||
let date: Date
|
||||
|
||||
var body: some View {
|
||||
Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 7) {
|
||||
GridRow(alignment: .firstTextBaseline) {
|
||||
Text("Von:").bold().gridColumnAlignment(.leading)
|
||||
Text(sender).gridColumnAlignment(.leading)
|
||||
}
|
||||
GridRow(alignment: .firstTextBaseline) {
|
||||
Text("An:").bold()
|
||||
Text(recipient)
|
||||
}
|
||||
if !cc.isEmpty {
|
||||
GridRow(alignment: .firstTextBaseline) {
|
||||
Text("CC:").bold()
|
||||
Text(cc)
|
||||
}
|
||||
}
|
||||
if date != .distantPast {
|
||||
GridRow(alignment: .firstTextBaseline) {
|
||||
Text("Datum:").bold()
|
||||
Text(date, format: .dateTime.day().month(.wide).year().hour().minute())
|
||||
}
|
||||
}
|
||||
GridRow(alignment: .firstTextBaseline) {
|
||||
Text("Betreff:").bold()
|
||||
Text(subject).bold()
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.primary)
|
||||
.textSelection(.enabled)
|
||||
.padding(16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(.secondary.opacity(0.08), in: RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
}
|
||||
|
||||
enum AttachmentMentionDetector {
|
||||
static let settingKey = "warnMissingAttachment"
|
||||
|
||||
|
||||
@@ -17,7 +17,13 @@ final class IMAPClient {
|
||||
let greeting = try await connection.readLine()
|
||||
if greeting.uppercased().hasPrefix("* PREAUTH") { return }
|
||||
guard greeting.uppercased().hasPrefix("* OK") else { throw MailError.message("IMAP: \(greeting)") }
|
||||
_ = try await command("LOGIN \(Self.quote(account.username)) \(Self.quote(password))")
|
||||
let capabilities = greeting.uppercased()
|
||||
if capabilities.contains("AUTH=PLAIN"), capabilities.contains("SASL-IR") {
|
||||
let credentials = Data(("\0" + account.username + "\0" + password).utf8).base64EncodedString()
|
||||
_ = try await command("AUTHENTICATE PLAIN \(credentials)")
|
||||
} else {
|
||||
_ = try await command("LOGIN \(Self.quote(account.username)) \(Self.quote(password))")
|
||||
}
|
||||
}
|
||||
|
||||
static func quote(_ value: String) throws -> String {
|
||||
|
||||
@@ -361,8 +361,12 @@
|
||||
},
|
||||
"An (mehrere mit Semikolon trennen)" : {
|
||||
|
||||
},
|
||||
"An:" : {
|
||||
|
||||
},
|
||||
"An: %@" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
@@ -514,6 +518,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Betreff:" : {
|
||||
|
||||
},
|
||||
"CC" : {
|
||||
"extractionState" : "stale",
|
||||
@@ -528,8 +535,12 @@
|
||||
},
|
||||
"CC (mehrere mit Semikolon trennen)" : {
|
||||
|
||||
},
|
||||
"CC:" : {
|
||||
|
||||
},
|
||||
"CC: %@" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
@@ -580,6 +591,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Datum:" : {
|
||||
|
||||
},
|
||||
"Der Inhalt und die Anhänge erscheinen hier." : {
|
||||
"localizations" : {
|
||||
@@ -714,6 +728,7 @@
|
||||
}
|
||||
},
|
||||
"Eine Testnachricht an %@ senden?" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
@@ -874,6 +889,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"IMAP-Server" : {
|
||||
|
||||
},
|
||||
"Immer" : {
|
||||
"localizations" : {
|
||||
@@ -894,6 +912,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"In Posteingang verschieben" : {
|
||||
|
||||
},
|
||||
"Inhalt" : {
|
||||
"extractionState" : "stale",
|
||||
@@ -1015,6 +1036,9 @@
|
||||
},
|
||||
"Lesestatus" : {
|
||||
|
||||
},
|
||||
"Login-Daten" : {
|
||||
|
||||
},
|
||||
"Lokale Mails endgültig löschen" : {
|
||||
"localizations" : {
|
||||
@@ -1224,6 +1248,7 @@
|
||||
}
|
||||
},
|
||||
"Nein" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
@@ -1443,6 +1468,12 @@
|
||||
},
|
||||
"Passwörter in das Backup aufnehmen" : {
|
||||
|
||||
},
|
||||
"Postausgang (SMTP)" : {
|
||||
|
||||
},
|
||||
"Posteingang (IMAP)" : {
|
||||
|
||||
},
|
||||
"Prüfen und speichern" : {
|
||||
"localizations" : {
|
||||
@@ -1537,6 +1568,7 @@
|
||||
}
|
||||
},
|
||||
"Server" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
@@ -1558,6 +1590,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SMTP-Server" : {
|
||||
|
||||
},
|
||||
"Sofort bei Auswahl" : {
|
||||
"localizations" : {
|
||||
@@ -1612,6 +1647,7 @@
|
||||
}
|
||||
},
|
||||
"Testmail senden" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
@@ -1622,6 +1658,7 @@
|
||||
}
|
||||
},
|
||||
"Testmail senden?" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
@@ -1661,6 +1698,7 @@
|
||||
}
|
||||
},
|
||||
"Verbindung testen" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
@@ -1685,8 +1723,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Von:" : {
|
||||
|
||||
},
|
||||
"Von: %@" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
|
||||
@@ -9,9 +9,20 @@ struct MailAccount: Identifiable, Codable, Hashable, Sendable {
|
||||
var email = ""
|
||||
var username = ""
|
||||
var host = ""
|
||||
var smtpHost: String?
|
||||
var smtpPort: UInt16 = 587
|
||||
var imapPort: UInt16 = 993
|
||||
|
||||
/// Older account archives only contain `host`. Falling back keeps those
|
||||
/// accounts usable until a separate SMTP server is entered.
|
||||
var smtpServer: String {
|
||||
get {
|
||||
let value = smtpHost?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return value.isEmpty ? host : value
|
||||
}
|
||||
set { smtpHost = newValue }
|
||||
}
|
||||
|
||||
var senderDisplay: String {
|
||||
let value = displayName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return value.isEmpty ? email : "\(value) <\(email)>"
|
||||
@@ -41,6 +52,11 @@ struct MailFolder: Identifiable, Hashable, Codable, Sendable {
|
||||
|| ["drafts", "entwürfe", "entwuerfe"].contains(name.lowercased())
|
||||
}
|
||||
|
||||
var isSpam: Bool {
|
||||
specialUse?.contains("\\junk") == true
|
||||
|| ["junk", "spam", "junk-e-mail", "unerwünscht"].contains(name.lowercased())
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
if isInbox { return "tray" }
|
||||
if isTrash { return "trash" }
|
||||
@@ -54,7 +70,7 @@ struct MailFolder: Identifiable, Hashable, Codable, Sendable {
|
||||
if uses.contains("\\drafts") || value.contains("draft") || value.contains("entwurf") {
|
||||
return "doc.text"
|
||||
}
|
||||
if uses.contains("\\junk") || value.contains("junk") || value.contains("spam") {
|
||||
if isSpam {
|
||||
return "exclamationmark.octagon"
|
||||
}
|
||||
if uses.contains("\\archive") || value.contains("archive") || value.contains("archiv") {
|
||||
|
||||
@@ -56,8 +56,8 @@ enum MailPrinter {
|
||||
let headerHTML = """
|
||||
<!doctype html>
|
||||
<html><head><meta charset="utf-8"></head>
|
||||
<body style="font-family: -apple-system; font-size: 9pt; color: #000; margin: 0;">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" style="font-size: 9pt; line-height: 1.25;">
|
||||
<body style="font-family: -apple-system; font-size: 7pt; color: #000; margin: 0;">
|
||||
<table width="100%" cellspacing="0" cellpadding="0" style="font-size: 7pt; line-height: 1.25;">
|
||||
<tr><td nowrap style="font-weight: bold; width: 105px; padding-bottom: 3pt;">Betreff:</td><td nowrap style="padding-bottom: 3pt;">\(escaped(message.subject))</td></tr>
|
||||
<tr><td nowrap style="font-weight: bold; width: 105px; padding-bottom: 3pt;">Von:</td><td nowrap style="padding-bottom: 3pt;">\(escaped(message.sender))</td></tr>
|
||||
<tr><td nowrap style="font-weight: bold; width: 105px; padding-bottom: 3pt;">An:</td><td nowrap style="padding-bottom: 3pt;">\(escaped(message.recipient))</td></tr>
|
||||
@@ -69,7 +69,7 @@ enum MailPrinter {
|
||||
let bodyHTML = """
|
||||
<!doctype html>
|
||||
<html><head><meta charset="utf-8"></head>
|
||||
<body style="font-family: -apple-system; font-size: 9.5pt; color: #000; margin: 0; line-height: 1.35;">\(body)</body></html>
|
||||
<body style="font-family: -apple-system; font-size: 7.5pt; color: #000; margin: 0; line-height: 1.35;">\(body)</body></html>
|
||||
"""
|
||||
|
||||
guard let header = try? NSAttributedString(
|
||||
|
||||
@@ -65,6 +65,8 @@ final class MailStore {
|
||||
guard !isBusy else { throw MailError.message("Bitte den laufenden Abruf abwarten.") }
|
||||
guard MIME.validAddress(account.email), !account.username.isEmpty,
|
||||
!account.host.isEmpty, !account.host.contains(where: { $0.isWhitespace || $0 == "/" }),
|
||||
!account.smtpServer.isEmpty,
|
||||
!account.smtpServer.contains(where: { $0.isWhitespace || $0 == "/" }),
|
||||
account.imapPort > 0, account.smtpPort > 0 else {
|
||||
throw MailError.message("Bitte E-Mail-Adresse, Benutzername, Server und Ports prüfen.")
|
||||
}
|
||||
@@ -538,6 +540,29 @@ final class MailStore {
|
||||
}
|
||||
}
|
||||
|
||||
func selectionCanRestoreToInbox(_ ids: [String]) -> Bool {
|
||||
!ids.isEmpty && ids.allSatisfy { id in
|
||||
guard let record = try? storage().record(id: id),
|
||||
let source = folder(id: record.folderID) else { return false }
|
||||
return source.isTrash || source.isSpam
|
||||
}
|
||||
}
|
||||
|
||||
func restoreToInbox(ids: [String]) async {
|
||||
await mutate(refreshAfter: false) {
|
||||
for id in Set(ids) {
|
||||
let record = try self.storage().record(id: id)
|
||||
guard let source = self.folder(id: record.folderID), source.isTrash || source.isSpam else {
|
||||
throw MailError.message("Die Nachricht befindet sich weder im Papierkorb noch im Spam-Ordner.")
|
||||
}
|
||||
guard let inbox = self.allFolders(self.folders[source.accountID] ?? []).first(where: \.isInbox) else {
|
||||
throw MailError.message("Für das ursprüngliche Konto wurde kein Posteingang gefunden.")
|
||||
}
|
||||
try await self.moveOne(id: id, to: inbox)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createFolder(accountID: UUID, parent: MailFolder?, name: String) async {
|
||||
await mutate {
|
||||
let name = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -587,6 +612,8 @@ final class MailStore {
|
||||
func testConnection(account: MailAccount, password: String) async throws {
|
||||
guard MIME.validAddress(account.email), !account.host.isEmpty,
|
||||
!account.host.contains(where: { $0.isWhitespace || "/:@".contains($0) }),
|
||||
!account.smtpServer.isEmpty,
|
||||
!account.smtpServer.contains(where: { $0.isWhitespace || "/:@".contains($0) }),
|
||||
!account.username.isEmpty else { throw MailError.message("Bitte gültige Kontodaten eingeben.") }
|
||||
let secret = password.isEmpty ? try PasswordStore.password(for: account.id) : password
|
||||
let client = try IMAPClient(account: account)
|
||||
|
||||
@@ -109,9 +109,12 @@ final class TLSConnection {
|
||||
|
||||
enum SMTPClient {
|
||||
static func check(account: MailAccount, password: String) async throws {
|
||||
let host = account.smtpServer
|
||||
let port = account.smtpPort
|
||||
let username = account.username
|
||||
let result = await Task.detached(priority: .userInitiated) {
|
||||
var error = [CChar](repeating: 0, count: 1024)
|
||||
let code = mail_smtp_check(account.host, account.smtpPort, account.username, password, &error, error.count)
|
||||
let code = mail_smtp_check(host, port, username, password, &error, error.count)
|
||||
return (code, String(cString: error))
|
||||
}.value
|
||||
guard result.0 == 0 else { throw MailError.message("SMTP-Verbindungstest: \(result.1)") }
|
||||
@@ -121,7 +124,7 @@ enum SMTPClient {
|
||||
guard MIME.validAddress(account.email), !recipients.isEmpty, recipients.allSatisfy(MIME.validAddress) else {
|
||||
throw MailError.message("Ungültige E-Mail-Adresse.")
|
||||
}
|
||||
let host = account.host
|
||||
let host = account.smtpServer
|
||||
let port = account.smtpPort
|
||||
let username = account.username
|
||||
let sender = account.email
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import SwiftUI
|
||||
|
||||
enum MessageCommand {
|
||||
case open, reply, forward, delete, read, unread
|
||||
case open, reply, forward, delete, restoreToInbox, read, unread
|
||||
}
|
||||
|
||||
struct ComposeDraft: Identifiable {
|
||||
|
||||
Reference in New Issue
Block a user