Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,51 @@ 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")

let pluginURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Mail/Bundles").appendingPathComponent(mailBundleName + ".mailbundle")

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
}

Expand Down Expand Up @@ -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()
}

Expand All @@ -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)
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

}
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 8 additions & 3 deletions OpenHaystack/OpenHaystackTests/OpenHaystackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down