From cd9bf3751a052e6eab45fa5a057ff8f352c9cfae Mon Sep 17 00:00:00 2001 From: Z3r0CooL Date: Thu, 5 Mar 2026 18:40:44 -0500 Subject: [PATCH] =?UTF-8?q?First=E2=80=90class=20support=20for=20the=20new?= =?UTF-8?q?=20Mail=20extensions=20on=20macOS=2013+?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Falls back to the old `.mailbundle` plugin on earlier releases • `MailPluginManager.swift` – Added `supportsMailExtension` check (#available(macOS 13, *)). – Updated `isMailPluginInstalled` to immediately returns true on 13+, otherwise does the old bundle-UUID check. – `installMailPlugin()` is a no-op on 13+ (just opens Mail.app to let the user enable the extension) & still copies the bundle on older macOS. – `uninstallMailPlugin()` & `pluginDownload()` similarly skip bundle actions on 13+. • `OpenHaystackSettingsView.swift` – The toggle label now switches based on OS version: “Use Apple Mail Plugin (≤12)” vs “Enable Mail Extension (≥13)”. • `OpenHaystackMainView.swift` – The toolbar “Reload” button reads “Reload Extension” on 13+ (stays “Reload” on older macOS). – The popover text dynamically refers to the “mail extension” vs “mail plug-in” based on runtime availability. • `OpenHaystackTests.swift` – `testPluginInstallation()` now skips itself on macOS 13+ with `try XCTSkip(...)` & only runs the bundle-copy assertions on older OS. --- .../Mail Plugin/MailPluginManager.swift | 77 ++++++++++++------- .../Views/OpenHaystackMainView.swift | 22 +++++- .../Views/OpenHaystackSettingsView.swift | 9 ++- .../OpenHaystackTests/OpenHaystackTests.swift | 11 ++- 4 files changed, 84 insertions(+), 35 deletions(-) diff --git a/OpenHaystack/OpenHaystack/HaystackApp/Mail Plugin/MailPluginManager.swift b/OpenHaystack/OpenHaystack/HaystackApp/Mail Plugin/MailPluginManager.swift index 1736e27a..199d5b40 100644 --- a/OpenHaystack/OpenHaystack/HaystackApp/Mail Plugin/MailPluginManager.swift +++ b/OpenHaystack/OpenHaystack/HaystackApp/Mail Plugin/MailPluginManager.swift @@ -15,6 +15,13 @@ let mailBundleName = "OpenHaystackMail" /// Manages plugin installation. struct MailPluginManager { + /// Indicates whether the app should use the new Mail extension API (macOS 13+) instead of Mail bundles. + static var supportsMailExtension: Bool { + if #available(macOS 13, *) { + return true + } + return false + } let pluginsFolderURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Mail/Bundles") @@ -22,33 +29,37 @@ struct MailPluginManager { let localPluginURL = Bundle.main.url(forResource: mailBundleName, withExtension: "mailbundle")! + /// Returns true if the Mail bundle plugin is installed and compatible, or if Mail extensions are supported on this OS. var isMailPluginInstalled: Bool { - //Check if the plug-in is compatible by comparing the IDs + // On macOS 13 and later, Mail bundle plugins are deprecated in favor of Mail extensions. + if MailPluginManager.supportsMailExtension { + return true + } + // Check if the plug-in bundle exists guard FileManager.default.fileExists(atPath: pluginURL.path) else { return false } - + // Compare compatibility UUIDs in Info.plist let infoPlistURL = pluginURL.appendingPathComponent("Contents/Info.plist") let localInfoPlistURL = localPluginURL.appendingPathComponent("Contents/Info.plist") - guard let infoPlistData = try? Data(contentsOf: infoPlistURL), - let infoPlistDict = try? PropertyListSerialization.propertyList(from: infoPlistData, options: [], format: nil) as? [String: AnyHashable], - let localInfoPlistData = try? Data(contentsOf: localInfoPlistURL), - let localInfoPlistDict = try? PropertyListSerialization.propertyList(from: localInfoPlistData, options: [], format: nil) as? [String: AnyHashable] - else { return false } - - //Compare the supported plug-ins + let infoPlistDict = try? PropertyListSerialization.propertyList(from: infoPlistData, options: [], format: nil) as? [String: AnyHashable], + let localInfoPlistData = try? Data(contentsOf: localInfoPlistURL), + let localInfoPlistDict = try? PropertyListSerialization.propertyList(from: localInfoPlistData, options: [], format: nil) as? [String: AnyHashable] + else { + return false + } let uuidEntries = localInfoPlistDict.keys.filter({ $0.contains("PluginCompatibilityUUIDs") }) for uuidEntry in uuidEntries { guard let localEntry = localInfoPlistDict[uuidEntry] as? [String], - let installedEntry = infoPlistDict[uuidEntry] as? [String] - else { return false } - + let installedEntry = infoPlistDict[uuidEntry] as? [String] + else { + return false + } if localEntry != installedEntry { return false } } - return true } @@ -81,19 +92,23 @@ struct MailPluginManager { /// Install the mail plug-in to the correct location /// - Throws: An error if copying the fails fail. Due to permission or other errors + /// Install the Mail bundle plugin, or open Mail for enabling the extension on newer macOS. + /// - Throws: PluginError if permission is not granted or copy fails. func installMailPlugin() throws { + // For macOS 13+, MailKit extensions are used; no bundle installation necessary. + if MailPluginManager.supportsMailExtension { + self.openAppleMail() + return + } + // Request permission to copy the bundle guard self.askForPermission() else { throw PluginError.permissionNotGranted } - - do { - // Create the Bundles folder if necessary - try FileManager.default.createDirectory(at: pluginsFolderURL, withIntermediateDirectories: true, attributes: nil) - } catch { - print(error.localizedDescription) - } + // Create the Bundles folder if necessary + try? FileManager.default.createDirectory(at: pluginsFolderURL, withIntermediateDirectories: true, attributes: nil) + // Copy bundle try FileManager.default.copyFolder(from: localPluginURL, to: pluginURL) - + // Launch Mail to load the plugin self.openAppleMail() } @@ -102,23 +117,31 @@ struct MailPluginManager { } + /// Uninstall the Mail bundle plugin (no-op on newer macOS). func uninstallMailPlugin() throws { + if MailPluginManager.supportsMailExtension { + // Nothing to uninstall for MailKit extension + return + } try FileManager.default.removeItem(at: pluginURL) } /// Copy plugin to downloads folder. /// /// - Throws: An error if the copy fails, because of missing permissions + /// Copy the Mail bundle plugin to Downloads (no-op on newer macOS). func pluginDownload() throws { - guard let localPluginURL = Bundle.main.url(forResource: mailBundleName, withExtension: "mailbundle"), - let downloadsFolder = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first + // MailKit extensions are embedded; no download necessary on newer macOS + if MailPluginManager.supportsMailExtension { + throw PluginError.downloadFailed + } + guard let localURL = Bundle.main.url(forResource: mailBundleName, withExtension: "mailbundle"), + let downloadsFolder = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else { throw PluginError.downloadFailed } - - let downloadsPluginURL = downloadsFolder.appendingPathComponent(mailBundleName + ".mailbundle") - - try FileManager.default.copyFolder(from: localPluginURL, to: downloadsPluginURL) + let dest = downloadsFolder.appendingPathComponent(mailBundleName + ".mailbundle") + try FileManager.default.copyFolder(from: localURL, to: dest) } } diff --git a/OpenHaystack/OpenHaystack/HaystackApp/Views/OpenHaystackMainView.swift b/OpenHaystack/OpenHaystack/HaystackApp/Views/OpenHaystackMainView.swift index e864d3b9..6e69f2fa 100644 --- a/OpenHaystack/OpenHaystack/HaystackApp/Views/OpenHaystackMainView.swift +++ b/OpenHaystack/OpenHaystack/HaystackApp/Views/OpenHaystackMainView.swift @@ -151,8 +151,14 @@ struct OpenHaystackMainView: View { Circle() .fill(self.mailPluginIsActive ? Color.green : Color.orange) .frame(width: 8, height: 8) - Label("Reload", systemImage: "arrow.clockwise") - .disabled(!self.mailPluginIsActive) + // Dynamic label for plugin vs extension + if MailPluginManager.supportsMailExtension { + Label("Reload Extension", systemImage: "arrow.clockwise") + .disabled(!self.mailPluginIsActive) + } else { + Label("Reload", systemImage: "arrow.clockwise") + .disabled(!self.mailPluginIsActive) + } } } @@ -228,9 +234,17 @@ struct OpenHaystackMainView: View { .foregroundColor(self.mailPluginIsActive ? .green : .red) if self.mailPluginIsActive { - Text("The mail plug-in is up and running") + if #available(macOS 13, *) { + Text("The mail extension is up and running") + } else { + Text("The mail plug-in is up and running") + } } else { - Text("Cannot connect to the mail plug-in. Open Apple Mail and make sure the plug-in is enabled") + if #available(macOS 13, *) { + Text("Cannot connect to the mail extension. Open Apple Mail and make sure the extension is enabled") + } else { + Text("Cannot connect to the mail plug-in. Open Apple Mail and make sure the plug-in is enabled") + } } } .padding() diff --git a/OpenHaystack/OpenHaystack/HaystackApp/Views/OpenHaystackSettingsView.swift b/OpenHaystack/OpenHaystack/HaystackApp/Views/OpenHaystackSettingsView.swift index 56917072..d8f414c8 100644 --- a/OpenHaystack/OpenHaystack/HaystackApp/Views/OpenHaystackSettingsView.swift +++ b/OpenHaystack/OpenHaystack/HaystackApp/Views/OpenHaystackSettingsView.swift @@ -27,7 +27,14 @@ struct GeneralSettingsView: View { var body: some View { Form { - Toggle("Use Apple Mail Plugin (only works on macOS 13 and lower)", isOn: $useMailPlugin) + // Toggle between Mail bundle plugin (macOS ≤12) and Mail extension (macOS ≥13) + Group { + if #available(macOS 13, *) { + Toggle("Enable Mail Extension (requires macOS 13 or higher)", isOn: $useMailPlugin) + } else { + Toggle("Use Apple Mail Plugin (only works on macOS 12 and lower)", isOn: $useMailPlugin) + } + } TextField("Search Party Token", text: $searchPartyToken) } .padding(20) diff --git a/OpenHaystack/OpenHaystackTests/OpenHaystackTests.swift b/OpenHaystack/OpenHaystackTests/OpenHaystackTests.swift index a52a7df4..52c3c4bb 100644 --- a/OpenHaystack/OpenHaystackTests/OpenHaystackTests.swift +++ b/OpenHaystack/OpenHaystackTests/OpenHaystackTests.swift @@ -164,16 +164,21 @@ class OpenHaystackTests: XCTestCase { XCTAssertNotNil(keyData) } - func testPluginInstallation() { + // Test installation of Mail bundle plugin (skipped on macOS 13+ where Mail extensions replace bundles) + func testPluginInstallation() throws { + if #available(macOS 13, *) { + try XCTSkip("Mail bundle plugins are unsupported on macOS 13 or newer") + } do { let pluginManager = MailPluginManager() + // Ensure clean state if pluginManager.isMailPluginInstalled { try pluginManager.uninstallMailPlugin() } + // Install bundle plugin try pluginManager.installMailPlugin() - + // Verify plugin bundle exists XCTAssert(FileManager.default.fileExists(atPath: pluginManager.pluginURL.path)) - } catch { XCTFail(String(describing: error)) }