Add Mail integration: search, read, and save attachments via AppleScript
Lets the AI search Apple Mail, read a message, and save an attachment to disk (e.g. to hand off to paperless_upload_document) — e.g. "find the receipt from Elkjøp and add it to Paperless." Talks to Mail.app via AppleScript/Apple Events rather than parsing its private on-disk store, so no Full Disk Access or MIME parsing is needed; Mail's own attachment save handles all decoding. Every mail_* tool call requires approval (Deny/Allow Once/Allow for Session), mirroring the bash_execute and Personal Data gate pattern. Also fixes a pre-existing bug found while touching the adjacent tool-activation condition: paperlessEnabled was missing from it, so Paperless tools could fail to activate unless another integration was also active.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// MailApprovalSheet.swift
|
||||
// Confab
|
||||
//
|
||||
// Approval UI for AI-requested Mail actions (search/read/save attachment)
|
||||
//
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
// Copyright (C) 2026 Rune Olsen
|
||||
//
|
||||
// This file is part of Confab.
|
||||
//
|
||||
// Confab is licensed under the PolyForm Noncommercial License 1.0.0.
|
||||
// You may use, study, modify, and share it for any noncommercial
|
||||
// purpose. Commercial use — including selling Confab or any part of
|
||||
// it, standalone or bundled into another product or service —
|
||||
// requires a separate commercial license from the copyright holder.
|
||||
//
|
||||
// See the LICENSE file or
|
||||
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
|
||||
// the full license text. For commercial licensing, contact Rune
|
||||
// Olsen via <https://confab.no>.
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MailApprovalSheet: View {
|
||||
let pending: MCPService.PendingMailAction
|
||||
let onApprove: (_ forSession: Bool) -> Void
|
||||
let onDeny: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
// Header
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "envelope.badge.shield.half.filled")
|
||||
.font(.title2)
|
||||
.foregroundStyle(.orange)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Allow This Action?")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
Text("The AI wants to access your Mail")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// Action description
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("ACTION")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
Text(pending.summary)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.primary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
.padding(12)
|
||||
.background(Color.secondary.opacity(0.08))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.secondary.opacity(0.2), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
// Buttons
|
||||
HStack(spacing: 8) {
|
||||
Button("Deny") {
|
||||
onDeny()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.red)
|
||||
.keyboardShortcut(.escape, modifiers: [])
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Allow Once") {
|
||||
onApprove(false)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.orange)
|
||||
|
||||
Button("Allow for Session") {
|
||||
onApprove(true)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.orange)
|
||||
.keyboardShortcut(.return, modifiers: [])
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.frame(width: 480)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user