ソースを参照

Implement Apple Mail MCP server with full AppleScript integration

- Clean up all Apple Notes references from forked codebase
- Implement 17 AppleMailManager methods with AppleScript:
  - Message ops: search, list, get, send, mark read/unread, flag, delete, move
  - Mailbox ops: list mailboxes, get unread count
  - Account ops: list accounts
  - Diagnostics: health check, mail statistics
- Update error mappings for Mail-specific errors
- Update all config files (.mcp.json, plugin.json, marketplace.json)
- Update documentation (CHANGELOG, CONTRIBUTING, SECURITY, bug template)
- Delete stale build/ and skills/apple-notes/ directories
Robert Sweet 5 ヶ月 前
コミット
182a20101f

+ 6 - 6
.claude-plugin/marketplace.json

@@ -1,17 +1,17 @@
 {
   "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
-  "name": "apple-notes-mcp",
-  "version": "1.0.0",
-  "description": "Apple Notes integration for Claude Code via MCP",
+  "name": "apple-mail-mcp",
+  "version": "0.1.0",
+  "description": "Apple Mail integration for Claude Code via MCP",
   "owner": {
     "name": "Rob Sweet",
     "email": "rob@superiortech.io"
   },
   "plugins": [
     {
-      "name": "apple-notes",
-      "description": "Manage Apple Notes through natural language - create, search, read, update, delete, and organize notes and folders",
-      "version": "1.1.1",
+      "name": "apple-mail",
+      "description": "Manage Apple Mail through natural language - read, search, send, and organize emails",
+      "version": "0.1.0",
       "author": {
         "name": "Rob Sweet",
         "email": "rob@superiortech.io"

+ 3 - 3
.claude-plugin/plugin.json

@@ -1,7 +1,7 @@
 {
-  "name": "apple-notes",
-  "version": "1.1.1",
-  "description": "Manage Apple Notes through natural language - create, search, read, update, delete, and organize notes and folders (macOS only)",
+  "name": "apple-mail",
+  "version": "0.1.0",
+  "description": "Manage Apple Mail through natural language - read, search, send, and organize emails (macOS only)",
   "author": {
     "name": "Rob Sweet",
     "email": "rob@superiortech.io"

+ 2 - 2
.github/ISSUE_TEMPLATE/bug_report.md

@@ -1,6 +1,6 @@
 ---
 name: Bug Report
-about: Report a problem with apple-notes-mcp
+about: Report a problem with apple-mail-mcp
 title: ''
 labels: bug
 assignees: ''
@@ -28,7 +28,7 @@ What actually happened.
 
 - macOS version:
 - Node.js version:
-- apple-notes-mcp version:
+- apple-mail-mcp version:
 - Claude Desktop version (if applicable):
 
 ## Error Messages

+ 2 - 2
.mcp.json

@@ -1,8 +1,8 @@
 {
   "mcpServers": {
-    "apple-notes": {
+    "apple-mail": {
       "command": "npx",
-      "args": ["apple-notes-mcp"]
+      "args": ["apple-mail-mcp"]
     }
   }
 }

+ 17 - 167
CHANGELOG.md

@@ -5,177 +5,27 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [1.2.17] - 2025-01-01
-
-### Security
-- **Fixed command injection vulnerability** in `moveNote()` - HTML content from notes was not properly escaped before embedding in AppleScript commands
-
-### Changed
-- **Improved sleep implementation** - Replaced CPU-spinning busy-wait with efficient system sleep command
-- **Added sync status caching** - Sync detection now caches results for 2 seconds to reduce database queries
-- **Extracted shared parsing logic** - Consolidated duplicated note property parsing into `parseNotePropertiesOutput()` helper
-
-### Added
-- **New helper functions** for cleaner code:
-  - `escapeHtmlForAppleScript()` - Safely escape already-HTML content for AppleScript
-  - `generateFallbackId()` - Consistent unique ID generation when AppleScript doesn't return one
-  - `parseNotePropertiesOutput()` - Shared parsing for AppleScript note property output
-  - `clearSyncStatusCache()` - Clear cached sync status for testing/forced refresh
-- **Export type definitions** - Added proper TypeScript interfaces for export operations (`NotesExport`, `ExportedNote`, etc.)
-- **Additional retry tests** - Coverage for all retryable error patterns (timed out, lost connection, busy)
-
-### Developer Experience
-- **ESLint flat config** - Migrated from deprecated `.eslintrc.cjs` to modern `eslint.config.js`
-- **Pre-commit hooks** - Added husky + lint-staged for automatic linting on commit
-- **Test coverage thresholds** - Enforced minimum coverage (services ≥80%, utils ≥90%)
-- **Dynamic version** - Server version now read from package.json instead of hardcoded
-
-## [1.2.16] - 2025-01-01
-
-### Added
-- **Collaboration Awareness**
-  - `list-shared-notes` tool to find all notes shared with collaborators
-  - Warnings on `update-note` when modifying shared notes
-  - Warnings on `delete-note` when removing shared notes
-  - `listSharedNotes()` method in AppleNotesManager
-
-## [1.2.15] - 2025-01-01
-
-### Added
-- **iCloud Sync Awareness**
-  - `get-sync-status` tool to check if iCloud sync is active
-  - Sync warnings integrated into `search-notes`, `list-notes`, `list-folders`
-  - Detection of pending uploads and recent database activity
-  - Follow-up verification to detect sync interference
-
-- **JXA Research** (utilities only, not primary executor)
-  - `src/utils/jxa.ts` - JavaScript for Automation execution utilities
-  - Research documented in `docs/JXA_RESEARCH.md`
-  - Finding: JXA is 7.6x slower than AppleScript, not recommended for primary use
-
-## [1.2.14] - 2024-12-31
-
-### Added
-- **Markdown Export**
-  - `get-note-markdown` tool to retrieve note content as Markdown
-  - Uses Turndown library for HTML to Markdown conversion
-
-## [1.2.13] - 2024-12-31
-
-### Added
-- **Database Export**
-  - `export-notes-json` tool for complete notes backup as JSON
-
-## [1.2.12] - 2024-12-31
-
-### Added
-- **Batch Operations**
-  - `batch-delete-notes` tool to delete multiple notes by ID
-  - `batch-move-notes` tool to move multiple notes to a folder
-
-## [1.2.11] - 2024-12-31
+## [Unreleased]
 
 ### Added
-- **Attachment Listing**
-  - `list-attachments` tool to see attachments in a note
-
-## [1.2.10] - 2024-12-31
-
-### Added
-- **Verbose Logging**
-  - DEBUG environment variable support for troubleshooting
-
-## [1.2.9] - 2024-12-31
-
-### Added
-- **Statistics**
-  - `get-notes-stats` tool for comprehensive notes statistics
-
-## [1.2.8] - 2024-12-31
+- Initial project structure forked from apple-notes-mcp
+- MCP server skeleton with tool definitions
+- TypeScript types for Mail data models (Message, Mailbox, Account)
+- AppleMailManager class with stub methods
+- Working `list-accounts` tool via AppleScript
+- Working `health-check` tool for Mail.app connectivity
+- AppleScript utilities with error handling, retries, and timeouts
 
 ### Changed
-- Validate note existence before destructive operations
-- Better error messages for missing notes
-
-## [1.2.7] - 2024-12-31
-
-### Added
-- Retry logic for transient failures (Notes.app not responding)
-- Improved error message mapping
-
-## [1.2.6] - 2024-12-31
-
-### Added
-- `health-check` tool to verify Notes.app connectivity and permissions
-
-## [1.2.5] - 2024-12-31
-
-### Added
-- `folder` parameter to `search-notes` for filtering by folder
-
-## [1.2.4] - 2024-12-31
-
-### Added
-- Timeout handling for AppleScript operations (30 second default)
-- Password-protected note detection with clear error messages
-
-## [1.1.2] - 2024-12-31
-
-### Fixed
-
-- Search functionality crash when notes have inaccessible containers (orphaned/corrupted notes)
-  - Added error handling in AppleScript loop to skip problematic notes instead of failing entirely
-  - Search now returns all accessible matching notes even if some cannot be processed
-
-## [1.1.0] - 2025-12-27
-
-### Added
-
-- **Folder Operations**
-  - `list-folders` - List all folders in an account
-  - `create-folder` - Create a new folder
-  - `delete-folder` - Delete a folder
-
-- **Multiple Account Support**
-  - `list-accounts` - List all available accounts
-  - All tools now accept optional `account` parameter
-
-- **Enhanced Search**
-  - `searchContent` option to search note bodies instead of just titles
-
-- **Note Management**
-  - `get-note-by-id` - Retrieve note by unique ID
-  - `get-note-details` - Get full note metadata (dates, shared status)
-  - `update-note` - Update existing note title and content
-  - `delete-note` - Delete notes by title
-  - `move-note` - Move notes between folders (copy-then-delete)
-
-- **Developer Experience**
-  - Comprehensive JSDoc documentation
-  - Unit tests with Vitest (121 tests)
-  - Integration tests for all MCP tool handlers
-  - ESLint and Prettier configuration
-  - TypeScript strict mode
-
-### Fixed
-
-- AppleScript escaping for apostrophes (shell quoting issue)
-- Newline handling in note content (now converts to HTML breaks)
-- Date parsing in getNoteById (handles commas in AppleScript date format)
-
-### Changed
-
-- Complete rewrite of all source code with new architecture
-- Updated to Node.js 20+ requirement
-- Improved error messages throughout
-
-## [1.0.0] - 2025-01-01
+- Updated all references from Apple Notes to Apple Mail
+- Updated error mappings for Mail-specific errors
 
-Initial release.
+## [0.1.0] - 2026-01-06
 
-### Features
+Initial release - work in progress.
 
-- Create notes with title and content
-- Search notes by title
-- Retrieve note content by title
-- iCloud account support
+### Features (Stubbed)
+- Message operations: search, list, get, send, mark read/unread, flag, delete, move
+- Mailbox operations: list mailboxes, get unread count
+- Account operations: list accounts
+- Diagnostics: health check, mail statistics

+ 7 - 7
CONTRIBUTING.md

@@ -1,4 +1,4 @@
-# Contributing to Apple Notes MCP Server
+# Contributing to Apple Mail MCP Server
 
 Thank you for your interest in contributing! This document provides guidelines for contributing to the project.
 
@@ -6,8 +6,8 @@ Thank you for your interest in contributing! This document provides guidelines f
 
 1. **Clone the repository**
    ```bash
-   git clone https://github.com/sweetrb/mcp-apple-notes.git
-   cd mcp-apple-notes
+   git clone https://github.com/sweetrb/apple-mail-mcp.git
+   cd apple-mail-mcp
    ```
 
 2. **Install dependencies**
@@ -57,7 +57,7 @@ npm run test:watch
 
 ### Testing Guidelines
 
-- Tests mock the `runAppleScript` function since AppleScript only works on macOS
+- Tests mock the `executeAppleScript` function since AppleScript only works on macOS
 - Test both success and failure paths
 - Test edge cases (empty strings, special characters, etc.)
 
@@ -94,14 +94,14 @@ npm run test:watch
 When adding a new MCP tool:
 
 1. **Add the schema** in `src/index.ts`
-2. **Implement the method** in `src/services/appleNotesManager.ts`
+2. **Implement the method** in `src/services/appleMailManager.ts`
 3. **Add type definitions** in `src/types.ts`
-4. **Write tests** in `src/services/appleNotesManager.test.ts`
+4. **Write tests** in `src/services/appleMailManager.test.ts`
 5. **Update documentation** in README.md and CHANGELOG.md
 
 ## AppleScript Guidelines
 
-- Always escape user input using `escapeForAppleScript()` for plain text or `escapeHtmlForAppleScript()` for HTML content
+- Always escape user input using `escapeForAppleScript()`
 - Handle errors gracefully (return null/false instead of throwing)
 - Log errors with `console.error()` for debugging
 - Test on actual macOS when possible

+ 11 - 3
SECURITY.md

@@ -4,7 +4,7 @@
 
 | Version | Supported          |
 | ------- | ------------------ |
-| 1.x.x   | :white_check_mark: |
+| 0.x.x   | :white_check_mark: |
 
 ## Reporting a Vulnerability
 
@@ -24,9 +24,17 @@ You will receive a response within 48 hours acknowledging receipt. Security issu
 
 This MCP server:
 - Runs locally on your machine
-- Uses AppleScript to interact with Notes.app
+- Uses AppleScript to interact with Mail.app
 - Does not transmit data to external servers
 - Does not store credentials or passwords
-- Cannot access password-protected notes
+- Requires explicit user confirmation before sending emails (recommended)
 
 The server requires macOS automation permissions to function. These permissions are managed by macOS and can be revoked at any time in System Preferences > Privacy & Security > Automation.
+
+## Email Security Best Practices
+
+When using this server with AI assistants:
+- Always review email content before sending
+- Be cautious with auto-send functionality
+- Monitor sent emails periodically
+- Report any unexpected behavior immediately

+ 0 - 155
skills/apple-notes/SKILL.md

@@ -1,155 +0,0 @@
----
-name: apple-notes
-description: Use this skill when the user wants to interact with Apple Notes on macOS - creating, searching, reading, updating, deleting, or organizing notes and folders. This skill provides access to the full Apple Notes app through MCP tools.
----
-
-# Apple Notes Skill
-
-This skill enables you to manage Apple Notes on macOS through natural language. Use it whenever the user mentions notes, wants to save information to Notes, or needs to retrieve, update, or organize their notes.
-
-## When to Use This Skill
-
-Use this skill when the user:
-- Wants to create a new note or save information
-- Asks to find, search, or look up notes
-- Wants to read the contents of a note
-- Needs to update or edit an existing note
-- Wants to delete or remove a note
-- Asks to move or organize notes into folders
-- Wants to list their notes or folders
-- Mentions Apple Notes, Notes app, or "my notes"
-
-## Available Tools
-
-### Note Operations
-
-| Tool | Purpose |
-|------|---------|
-| `create-note` | Create a new note with title and content |
-| `search-notes` | Find notes by title or content |
-| `get-note-content` | Read the full content of a note |
-| `get-note-details` | Get metadata (created, modified, account) |
-| `update-note` | Modify a note's title or content |
-| `delete-note` | Remove a note (moves to Recently Deleted) |
-| `move-note` | Move a note to a different folder |
-| `list-notes` | List all notes or notes in a folder |
-
-### Folder Operations
-
-| Tool | Purpose |
-|------|---------|
-| `list-folders` | List all folders in an account |
-| `create-folder` | Create a new folder |
-| `delete-folder` | Delete an empty folder |
-
-### Account Operations
-
-| Tool | Purpose |
-|------|---------|
-| `list-accounts` | List configured accounts (iCloud, Gmail, etc.) |
-
-## Usage Patterns
-
-### Creating Notes
-
-When the user wants to save information:
-
-```
-User: "Save this meeting summary as a note"
-Action: Use create-note with appropriate title and the content
-```
-
-```
-User: "Create a shopping list note"
-Action: Use create-note with title="Shopping List" and formatted content
-```
-
-### Finding Notes
-
-When the user wants to find notes:
-
-```
-User: "Find my notes about the project"
-Action: Use search-notes with query="project"
-```
-
-```
-User: "Search for notes containing budget information"
-Action: Use search-notes with query="budget" and searchContent=true
-```
-
-### Reading Notes
-
-When the user wants to see note contents:
-
-```
-User: "Show me my shopping list"
-Action: Use get-note-content with title="Shopping List"
-```
-
-### Updating Notes
-
-When the user wants to modify a note:
-
-```
-User: "Add milk to my shopping list"
-Action:
-1. Use get-note-content to read current content
-2. Use update-note with the updated content including "milk"
-```
-
-### Organizing Notes
-
-When the user wants to organize:
-
-```
-User: "Move my old notes to Archive"
-Action: Use move-note with the note title and folder="Archive"
-```
-
-```
-User: "Create a Work folder"
-Action: Use create-folder with name="Work"
-```
-
-## Important Guidelines
-
-1. **Title Matching**: Note operations require exact title matches. If a note isn't found, suggest using `search-notes` first.
-
-2. **Default Account**: Operations default to iCloud. Use the `account` parameter for other accounts (Gmail, Exchange).
-
-3. **Content Format**: Notes store content as HTML. Plain text works fine for creation, but retrieved content may include HTML tags.
-
-4. **Backslash Escaping**: When content contains backslashes, escape them as `\\` in the JSON.
-
-5. **Password-Protected Notes**: Cannot be accessed via this skill. Inform the user if they try.
-
-6. **macOS Only**: This skill only works on macOS systems.
-
-## Error Handling
-
-- **"Note not found"**: Use search-notes to find similar titles
-- **"Permission denied"**: User needs to grant automation permission in System Preferences
-- **"Folder not empty"**: Cannot delete folders with notes; move notes first
-
-## Examples
-
-### Save conversation to notes
-```
-User: "Save our conversation about the API design to my notes"
-→ create-note with title="API Design Discussion" and summarized content
-```
-
-### Daily workflow
-```
-User: "What's on my todo list?"
-→ search-notes with query="todo" or get-note-content with title="Todo"
-```
-
-### Multi-step organization
-```
-User: "Archive all my completed project notes"
-→ 1. list-notes to find notes
-→ 2. create-folder name="Archive" if needed
-→ 3. move-note for each relevant note
-```

+ 690 - 96
src/services/appleMailManager.ts

@@ -4,6 +4,12 @@
  * Handles all interactions with Apple Mail via AppleScript.
  * This is the core service layer for the MCP server.
  *
+ * Architecture:
+ * - Text escaping is handled by dedicated helper functions
+ * - AppleScript generation uses template builders for consistency
+ * - All public methods return typed results (no raw strings)
+ * - Error handling is consistent across all operations
+ *
  * @module services/appleMailManager
  */
 
@@ -16,8 +22,72 @@ import type {
   Attachment,
   HealthCheckResult,
   MailStats,
+  AccountStats,
 } from "@/types.js";
 
+// =============================================================================
+// Text Processing Utilities
+// =============================================================================
+
+/**
+ * Escapes text for safe embedding in AppleScript string literals.
+ *
+ * AppleScript strings use double quotes, so we need to escape:
+ * 1. Backslashes (\) - escaped as \\
+ * 2. Double quotes (") - escaped as \"
+ *
+ * @param text - Raw text to escape
+ * @returns Text safe for AppleScript string embedding
+ */
+function escapeForAppleScript(text: string): string {
+  if (!text) return "";
+  return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
+}
+
+/**
+ * Parses AppleScript date representation to JavaScript Date.
+ *
+ * AppleScript returns dates in a verbose format like:
+ * "date Saturday, December 27, 2025 at 3:44:02 PM"
+ *
+ * @param appleScriptDate - Date string from AppleScript
+ * @returns Parsed Date, or current date if parsing fails
+ */
+function parseAppleScriptDate(appleScriptDate: string): Date {
+  const withoutPrefix = appleScriptDate.replace(/^date\s+/, "");
+  const normalized = withoutPrefix.replace(" at ", " ");
+  const parsed = new Date(normalized);
+  return isNaN(parsed.getTime()) ? new Date() : parsed;
+}
+
+/**
+ * Builds an AppleScript command scoped to a specific account.
+ */
+function buildAccountScopedScript(account: string, command: string): string {
+  return `
+    tell application "Mail"
+      tell account "${escapeForAppleScript(account)}"
+        ${command}
+      end tell
+    end tell
+  `;
+}
+
+/**
+ * Builds an AppleScript command at the application level.
+ */
+function buildAppLevelScript(command: string): string {
+  return `
+    tell application "Mail"
+      ${command}
+    end tell
+  `;
+}
+
+// =============================================================================
+// Apple Mail Manager Class
+// =============================================================================
+
 /**
  * Manager class for Apple Mail operations.
  *
@@ -26,55 +96,260 @@ import type {
  * - Sending emails
  * - Managing mailboxes
  * - Listing accounts
+ *
+ * All operations are synchronous since they rely on AppleScript
+ * execution via osascript. Error handling is consistent: methods
+ * return null/false/empty-array on failure rather than throwing.
  */
 export class AppleMailManager {
+  /**
+   * Default account used when no account is specified.
+   */
+  private defaultAccount: string | null = null;
+
+  /**
+   * Resolves the account to use for an operation.
+   * Falls back to first available account if not specified.
+   */
+  private resolveAccount(account?: string): string {
+    if (account) return account;
+    if (this.defaultAccount) return this.defaultAccount;
+
+    // Get first account as default
+    const accounts = this.listAccounts();
+    if (accounts.length > 0) {
+      this.defaultAccount = accounts[0].name;
+      return this.defaultAccount;
+    }
+
+    return "iCloud"; // Last resort fallback
+  }
+
   // ===========================================================================
   // Message Operations
   // ===========================================================================
 
   /**
    * Search for messages matching criteria.
+   *
+   * @param query - Text to search for in subject or sender
+   * @param mailbox - Mailbox to search in (e.g., "INBOX")
+   * @param account - Account to search in
+   * @param limit - Maximum number of results
+   * @returns Array of matching messages
    */
   searchMessages(query?: string, mailbox?: string, account?: string, limit = 50): Message[] {
-    // TODO: Implement message search via AppleScript
-    void query;
-    void mailbox;
-    void account;
-    void limit;
-    return [];
+    const targetAccount = this.resolveAccount(account);
+    const targetMailbox = mailbox || "INBOX";
+
+    // Build the search condition
+    let searchCondition = "";
+    if (query) {
+      const safeQuery = escapeForAppleScript(query);
+      searchCondition = `whose subject contains "${safeQuery}" or sender contains "${safeQuery}"`;
+    }
+
+    const searchCommand = `
+      set msgList to {}
+      set theMailbox to mailbox "${escapeForAppleScript(targetMailbox)}"
+      set allMessages to messages of theMailbox ${searchCondition}
+      set msgCount to 0
+      repeat with msg in allMessages
+        if msgCount >= ${limit} then exit repeat
+        try
+          set msgId to id of msg
+          set msgSubject to subject of msg
+          set msgSender to sender of msg
+          set msgDate to date received of msg
+          set msgRead to read status of msg
+          set msgFlagged to flagged status of msg
+          set end of msgList to msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & (msgDate as string) & "|||" & msgRead & "|||" & msgFlagged
+          set msgCount to msgCount + 1
+        end try
+      end repeat
+      set AppleScript's text item delimiters to "|||ITEM|||"
+      return msgList as text
+    `;
+
+    const script = buildAccountScopedScript(targetAccount, searchCommand);
+    const result = executeAppleScript(script);
+
+    if (!result.success) {
+      console.error(`Failed to search messages: ${result.error}`);
+      return [];
+    }
+
+    if (!result.output.trim()) return [];
+
+    return this.parseMessageList(result.output, targetMailbox, targetAccount);
   }
 
   /**
    * Get a message by ID.
    */
   getMessageById(id: string): Message | null {
-    // TODO: Implement get message by ID
-    void id;
-    return null;
+    const script = buildAppLevelScript(`
+      try
+        set msg to message id ${id}
+        set msgSubject to subject of msg
+        set msgSender to sender of msg
+        set msgDate to date received of msg
+        set msgRead to read status of msg
+        set msgFlagged to flagged status of msg
+        set msgJunk to junk mail status of msg
+        set msgDeleted to deleted status of msg
+        set msgMailbox to name of mailbox of msg
+        set msgAccount to name of account of mailbox of msg
+        return msgSubject & "|||" & msgSender & "|||" & (msgDate as string) & "|||" & msgRead & "|||" & msgFlagged & "|||" & msgJunk & "|||" & msgDeleted & "|||" & msgMailbox & "|||" & msgAccount
+      on error
+        return ""
+      end try
+    `);
+
+    const result = executeAppleScript(script);
+
+    if (!result.success || !result.output.trim()) {
+      console.error(`Failed to get message ${id}: ${result.error}`);
+      return null;
+    }
+
+    const parts = result.output.split("|||");
+    if (parts.length < 9) return null;
+
+    return {
+      id: id.toString(),
+      subject: parts[0],
+      sender: parts[1],
+      recipients: [],
+      dateReceived: parseAppleScriptDate(parts[2]),
+      isRead: parts[3] === "true",
+      isFlagged: parts[4] === "true",
+      isJunk: parts[5] === "true",
+      isDeleted: parts[6] === "true",
+      mailbox: parts[7],
+      account: parts[8],
+      hasAttachments: false, // Would require separate query
+    };
   }
 
   /**
    * Get the content of a message.
    */
   getMessageContent(id: string): MessageContent | null {
-    // TODO: Implement get message content
-    void id;
-    return null;
+    const script = buildAppLevelScript(`
+      try
+        set msg to message id ${id}
+        set msgSubject to subject of msg
+        set msgContent to content of msg
+        return msgSubject & "|||CONTENT|||" & msgContent
+      on error errMsg
+        return "ERROR:" & errMsg
+      end try
+    `);
+
+    const result = executeAppleScript(script);
+
+    if (!result.success || result.output.startsWith("ERROR:")) {
+      console.error(`Failed to get message content: ${result.error || result.output}`);
+      return null;
+    }
+
+    const parts = result.output.split("|||CONTENT|||");
+    if (parts.length < 2) return null;
+
+    return {
+      id: id.toString(),
+      subject: parts[0],
+      plainText: parts[1],
+    };
   }
 
   /**
    * List messages in a mailbox.
+   *
+   * @param mailbox - Mailbox to list from (default: INBOX)
+   * @param account - Account to list from
+   * @param limit - Maximum number of messages
+   * @returns Array of messages
    */
   listMessages(mailbox?: string, account?: string, limit = 50): Message[] {
-    // TODO: Implement list messages
-    void mailbox;
-    void account;
-    void limit;
-    return [];
+    const targetAccount = this.resolveAccount(account);
+    const targetMailbox = mailbox || "INBOX";
+
+    const listCommand = `
+      set msgList to {}
+      set theMailbox to mailbox "${escapeForAppleScript(targetMailbox)}"
+      set msgCount to 0
+      repeat with msg in messages of theMailbox
+        if msgCount >= ${limit} then exit repeat
+        try
+          set msgId to id of msg
+          set msgSubject to subject of msg
+          set msgSender to sender of msg
+          set msgDate to date received of msg
+          set msgRead to read status of msg
+          set msgFlagged to flagged status of msg
+          set end of msgList to msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & (msgDate as string) & "|||" & msgRead & "|||" & msgFlagged
+          set msgCount to msgCount + 1
+        end try
+      end repeat
+      set AppleScript's text item delimiters to "|||ITEM|||"
+      return msgList as text
+    `;
+
+    const script = buildAccountScopedScript(targetAccount, listCommand);
+    const result = executeAppleScript(script);
+
+    if (!result.success) {
+      console.error(`Failed to list messages: ${result.error}`);
+      return [];
+    }
+
+    if (!result.output.trim()) return [];
+
+    return this.parseMessageList(result.output, targetMailbox, targetAccount);
+  }
+
+  /**
+   * Parse message list output from AppleScript.
+   */
+  private parseMessageList(output: string, mailbox: string, account: string): Message[] {
+    const items = output.split("|||ITEM|||");
+    const messages: Message[] = [];
+
+    for (const item of items) {
+      const parts = item.split("|||");
+      if (parts.length < 6) continue;
+
+      messages.push({
+        id: parts[0].trim(),
+        subject: parts[1],
+        sender: parts[2],
+        recipients: [],
+        dateReceived: parseAppleScriptDate(parts[3]),
+        isRead: parts[4] === "true",
+        isFlagged: parts[5] === "true",
+        isJunk: false,
+        isDeleted: false,
+        mailbox,
+        account,
+        hasAttachments: false,
+      });
+    }
+
+    return messages;
   }
 
   /**
    * Send an email.
+   *
+   * @param to - Recipient email addresses
+   * @param subject - Email subject
+   * @param body - Email body (plain text)
+   * @param cc - CC recipients
+   * @param bcc - BCC recipients
+   * @param account - Account to send from
+   * @returns true if sent successfully
    */
   sendEmail(
     to: string[],
@@ -84,79 +359,246 @@ export class AppleMailManager {
     bcc?: string[],
     account?: string
   ): boolean {
-    // TODO: Implement send email via AppleScript
-    void to;
-    void subject;
-    void body;
-    void cc;
-    void bcc;
-    void account;
-    return false;
+    const safeSubject = escapeForAppleScript(subject);
+    const safeBody = escapeForAppleScript(body);
+
+    // Build recipient additions
+    let recipientCommands = "";
+    for (const addr of to) {
+      recipientCommands += `make new to recipient at end of to recipients with properties {address:"${escapeForAppleScript(addr)}"}\n`;
+    }
+    if (cc) {
+      for (const addr of cc) {
+        recipientCommands += `make new cc recipient at end of cc recipients with properties {address:"${escapeForAppleScript(addr)}"}\n`;
+      }
+    }
+    if (bcc) {
+      for (const addr of bcc) {
+        recipientCommands += `make new bcc recipient at end of bcc recipients with properties {address:"${escapeForAppleScript(addr)}"}\n`;
+      }
+    }
+
+    let sendCommand: string;
+    if (account) {
+      const safeAccount = escapeForAppleScript(account);
+      sendCommand = `
+        set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:true}
+        tell newMessage
+          ${recipientCommands}
+          set sender to "${safeAccount}"
+        end tell
+        send newMessage
+        return "sent"
+      `;
+    } else {
+      sendCommand = `
+        set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:true}
+        tell newMessage
+          ${recipientCommands}
+        end tell
+        send newMessage
+        return "sent"
+      `;
+    }
+
+    const script = buildAppLevelScript(sendCommand);
+    const result = executeAppleScript(script);
+
+    if (!result.success) {
+      console.error(`Failed to send email: ${result.error}`);
+      return false;
+    }
+
+    return result.output.includes("sent");
   }
 
   /**
    * Mark a message as read.
    */
   markAsRead(id: string): boolean {
-    // TODO: Implement mark as read
-    void id;
-    return false;
+    const script = buildAppLevelScript(`
+      try
+        set read status of message id ${id} to true
+        return "ok"
+      on error errMsg
+        return "error:" & errMsg
+      end try
+    `);
+
+    const result = executeAppleScript(script);
+
+    if (!result.success || result.output.startsWith("error:")) {
+      console.error(`Failed to mark message as read: ${result.error || result.output}`);
+      return false;
+    }
+
+    return true;
   }
 
   /**
    * Mark a message as unread.
    */
   markAsUnread(id: string): boolean {
-    // TODO: Implement mark as unread
-    void id;
-    return false;
+    const script = buildAppLevelScript(`
+      try
+        set read status of message id ${id} to false
+        return "ok"
+      on error errMsg
+        return "error:" & errMsg
+      end try
+    `);
+
+    const result = executeAppleScript(script);
+
+    if (!result.success || result.output.startsWith("error:")) {
+      console.error(`Failed to mark message as unread: ${result.error || result.output}`);
+      return false;
+    }
+
+    return true;
   }
 
   /**
    * Flag a message.
    */
   flagMessage(id: string): boolean {
-    // TODO: Implement flag message
-    void id;
-    return false;
+    const script = buildAppLevelScript(`
+      try
+        set flagged status of message id ${id} to true
+        return "ok"
+      on error errMsg
+        return "error:" & errMsg
+      end try
+    `);
+
+    const result = executeAppleScript(script);
+
+    if (!result.success || result.output.startsWith("error:")) {
+      console.error(`Failed to flag message: ${result.error || result.output}`);
+      return false;
+    }
+
+    return true;
   }
 
   /**
    * Unflag a message.
    */
   unflagMessage(id: string): boolean {
-    // TODO: Implement unflag message
-    void id;
-    return false;
+    const script = buildAppLevelScript(`
+      try
+        set flagged status of message id ${id} to false
+        return "ok"
+      on error errMsg
+        return "error:" & errMsg
+      end try
+    `);
+
+    const result = executeAppleScript(script);
+
+    if (!result.success || result.output.startsWith("error:")) {
+      console.error(`Failed to unflag message: ${result.error || result.output}`);
+      return false;
+    }
+
+    return true;
   }
 
   /**
    * Delete a message.
    */
   deleteMessage(id: string): boolean {
-    // TODO: Implement delete message
-    void id;
-    return false;
+    const script = buildAppLevelScript(`
+      try
+        delete message id ${id}
+        return "ok"
+      on error errMsg
+        return "error:" & errMsg
+      end try
+    `);
+
+    const result = executeAppleScript(script);
+
+    if (!result.success || result.output.startsWith("error:")) {
+      console.error(`Failed to delete message: ${result.error || result.output}`);
+      return false;
+    }
+
+    return true;
   }
 
   /**
    * Move a message to a different mailbox.
    */
   moveMessage(id: string, mailbox: string, account?: string): boolean {
-    // TODO: Implement move message
-    void id;
-    void mailbox;
-    void account;
-    return false;
+    const targetAccount = this.resolveAccount(account);
+    const safeMailbox = escapeForAppleScript(mailbox);
+    const safeAccount = escapeForAppleScript(targetAccount);
+
+    const script = buildAppLevelScript(`
+      try
+        set msg to message id ${id}
+        set destMailbox to mailbox "${safeMailbox}" of account "${safeAccount}"
+        move msg to destMailbox
+        return "ok"
+      on error errMsg
+        return "error:" & errMsg
+      end try
+    `);
+
+    const result = executeAppleScript(script);
+
+    if (!result.success || result.output.startsWith("error:")) {
+      console.error(`Failed to move message: ${result.error || result.output}`);
+      return false;
+    }
+
+    return true;
   }
 
   /**
    * List attachments for a message.
    */
   listAttachments(id: string): Attachment[] {
-    // TODO: Implement list attachments
-    void id;
-    return [];
+    const script = buildAppLevelScript(`
+      try
+        set msg to message id ${id}
+        set attachList to {}
+        repeat with att in mail attachments of msg
+          set attName to name of att
+          set attType to MIME type of att
+          set attSize to file size of att
+          set end of attachList to attName & "|||" & attType & "|||" & attSize
+        end repeat
+        set AppleScript's text item delimiters to "|||ITEM|||"
+        return attachList as text
+      on error errMsg
+        return ""
+      end try
+    `);
+
+    const result = executeAppleScript(script);
+
+    if (!result.success || !result.output.trim()) {
+      return [];
+    }
+
+    const items = result.output.split("|||ITEM|||");
+    const attachments: Attachment[] = [];
+
+    for (const item of items) {
+      const parts = item.split("|||");
+      if (parts.length < 3) continue;
+
+      attachments.push({
+        id: `${id}-${parts[0]}`, // Composite ID
+        name: parts[0],
+        mimeType: parts[1],
+        size: parseInt(parts[2]) || 0,
+      });
+    }
+
+    return attachments;
   }
 
   // ===========================================================================
@@ -164,22 +606,81 @@ export class AppleMailManager {
   // ===========================================================================
 
   /**
-   * List all mailboxes.
+   * List all mailboxes for an account.
    */
   listMailboxes(account?: string): Mailbox[] {
-    // TODO: Implement list mailboxes
-    void account;
-    return [];
+    const targetAccount = this.resolveAccount(account);
+
+    const listCommand = `
+      set mailboxList to {}
+      repeat with mb in mailboxes
+        set mbName to name of mb
+        set mbUnread to unread count of mb
+        set mbCount to count of messages of mb
+        set end of mailboxList to mbName & "|||" & mbUnread & "|||" & mbCount
+      end repeat
+      set AppleScript's text item delimiters to "|||ITEM|||"
+      return mailboxList as text
+    `;
+
+    const script = buildAccountScopedScript(targetAccount, listCommand);
+    const result = executeAppleScript(script);
+
+    if (!result.success) {
+      console.error(`Failed to list mailboxes: ${result.error}`);
+      return [];
+    }
+
+    if (!result.output.trim()) return [];
+
+    const items = result.output.split("|||ITEM|||");
+    const mailboxes: Mailbox[] = [];
+
+    for (const item of items) {
+      const parts = item.split("|||");
+      if (parts.length < 3) continue;
+
+      mailboxes.push({
+        name: parts[0],
+        account: targetAccount,
+        unreadCount: parseInt(parts[1]) || 0,
+        messageCount: parseInt(parts[2]) || 0,
+      });
+    }
+
+    return mailboxes;
   }
 
   /**
    * Get unread count for a mailbox.
    */
   getUnreadCount(mailbox?: string, account?: string): number {
-    // TODO: Implement get unread count
-    void mailbox;
-    void account;
-    return 0;
+    const targetAccount = this.resolveAccount(account);
+
+    let command: string;
+    if (mailbox) {
+      const safeMailbox = escapeForAppleScript(mailbox);
+      command = `return unread count of mailbox "${safeMailbox}"`;
+    } else {
+      // Get total unread across all mailboxes
+      command = `
+        set total to 0
+        repeat with mb in mailboxes
+          set total to total + (unread count of mb)
+        end repeat
+        return total
+      `;
+    }
+
+    const script = buildAccountScopedScript(targetAccount, command);
+    const result = executeAppleScript(script);
+
+    if (!result.success) {
+      console.error(`Failed to get unread count: ${result.error}`);
+      return 0;
+    }
+
+    return parseInt(result.output) || 0;
   }
 
   // ===========================================================================
@@ -190,27 +691,46 @@ export class AppleMailManager {
    * List all mail accounts.
    */
   listAccounts(): Account[] {
-    const result = executeAppleScript(`
-      tell application "Mail"
-        set accountList to {}
-        repeat with acct in accounts
-          set end of accountList to name of acct
-        end repeat
-        return accountList
-      end tell
+    const script = buildAppLevelScript(`
+      set accountList to {}
+      repeat with acct in accounts
+        set acctName to name of acct
+        set acctEmail to email addresses of acct
+        set acctEnabled to enabled of acct
+        set emailStr to ""
+        if (count of acctEmail) > 0 then
+          set emailStr to item 1 of acctEmail
+        end if
+        set end of accountList to acctName & "|||" & emailStr & "|||" & acctEnabled
+      end repeat
+      set AppleScript's text item delimiters to "|||ITEM|||"
+      return accountList as text
     `);
 
+    const result = executeAppleScript(script);
+
     if (!result.success) {
+      console.error(`Failed to list accounts: ${result.error}`);
       return [];
     }
 
-    // Parse the output (comma-separated list)
-    const names = result.output.split(", ").filter((n) => n.trim());
-    return names.map((name) => ({
-      name: name.trim(),
-      email: "", // Would need additional query
-      enabled: true,
-    }));
+    if (!result.output.trim()) return [];
+
+    const items = result.output.split("|||ITEM|||");
+    const accounts: Account[] = [];
+
+    for (const item of items) {
+      const parts = item.split("|||");
+      if (parts.length < 3) continue;
+
+      accounts.push({
+        name: parts[0],
+        email: parts[1],
+        enabled: parts[2] === "true",
+      });
+    }
+
+    return accounts;
   }
 
   // ===========================================================================
@@ -224,29 +744,72 @@ export class AppleMailManager {
     const checks: HealthCheckResult["checks"] = [];
 
     // Check 1: Mail.app is accessible
-    const mailCheck = executeAppleScript(`
-      tell application "Mail"
-        return name
-      end tell
-    `);
-    checks.push({
-      name: "mail_app",
-      passed: mailCheck.success,
-      message: mailCheck.success ? "Mail.app is accessible" : `Mail.app error: ${mailCheck.error}`,
-    });
+    const mailCheck = executeAppleScript('tell application "Mail" to return "ok"');
+    if (mailCheck.success && mailCheck.output === "ok") {
+      checks.push({
+        name: "mail_app",
+        passed: true,
+        message: "Mail.app is accessible",
+      });
+    } else {
+      const errorHint = mailCheck.error?.includes("not authorized")
+        ? " (check Automation permissions in System Preferences)"
+        : "";
+      checks.push({
+        name: "mail_app",
+        passed: false,
+        message: `Mail.app is not accessible${errorHint}`,
+      });
+      return { healthy: false, checks };
+    }
 
-    // Check 2: Can list accounts
-    const accountsCheck = executeAppleScript(`
-      tell application "Mail"
-        return count of accounts
-      end tell
-    `);
+    // Check 2: AppleScript permissions
+    const permCheck = executeAppleScript('tell application "Mail" to get name of account 1');
+    if (permCheck.success) {
+      checks.push({
+        name: "permissions",
+        passed: true,
+        message: "AppleScript automation permissions granted",
+      });
+    } else {
+      const isPermError =
+        permCheck.error?.includes("not authorized") || permCheck.error?.includes("not permitted");
+      checks.push({
+        name: "permissions",
+        passed: !isPermError,
+        message: isPermError
+          ? "AppleScript permissions denied. Grant access in System Preferences > Privacy & Security > Automation"
+          : `Permission check returned: ${permCheck.error}`,
+      });
+      if (isPermError) {
+        return { healthy: false, checks };
+      }
+    }
+
+    // Check 3: At least one account accessible
+    const accounts = this.listAccounts();
+    if (accounts.length > 0) {
+      const accountNames = accounts.map((a) => a.name).join(", ");
+      checks.push({
+        name: "accounts",
+        passed: true,
+        message: `Found ${accounts.length} account(s): ${accountNames}`,
+      });
+    } else {
+      checks.push({
+        name: "accounts",
+        passed: false,
+        message: "No Mail accounts found. Set up an account in Mail.app first.",
+      });
+      return { healthy: false, checks };
+    }
+
+    // Check 4: Basic operations work
+    const mailboxes = this.listMailboxes(accounts[0].name);
     checks.push({
-      name: "accounts",
-      passed: accountsCheck.success && parseInt(accountsCheck.output) > 0,
-      message: accountsCheck.success
-        ? `Found ${accountsCheck.output} account(s)`
-        : `Cannot access accounts: ${accountsCheck.error}`,
+      name: "operations",
+      passed: true,
+      message: `Basic operations working (${mailboxes.length} mailbox(es) in ${accounts[0].name})`,
     });
 
     return {
@@ -259,11 +822,42 @@ export class AppleMailManager {
    * Get mail statistics.
    */
   getMailStats(): MailStats {
-    // TODO: Implement mail statistics
+    const accounts = this.listAccounts();
+    const accountStats: AccountStats[] = [];
+    let totalMessages = 0;
+    let totalUnread = 0;
+
+    for (const account of accounts) {
+      const mailboxes = this.listMailboxes(account.name);
+      let accountMessages = 0;
+      let accountUnread = 0;
+
+      const mailboxStats = mailboxes.map((mb) => {
+        accountMessages += mb.messageCount;
+        accountUnread += mb.unreadCount;
+        return {
+          name: mb.name,
+          messageCount: mb.messageCount,
+          unreadCount: mb.unreadCount,
+        };
+      });
+
+      totalMessages += accountMessages;
+      totalUnread += accountUnread;
+
+      accountStats.push({
+        name: account.name,
+        totalMessages: accountMessages,
+        unreadMessages: accountUnread,
+        mailboxCount: mailboxes.length,
+        mailboxes: mailboxStats,
+      });
+    }
+
     return {
-      totalMessages: 0,
-      totalUnread: 0,
-      accounts: [],
+      totalMessages,
+      totalUnread,
+      accounts: accountStats,
     };
   }
 }

+ 28 - 30
src/utils/applescript.test.ts

@@ -25,14 +25,14 @@ describe("executeAppleScript", () => {
   describe("successful execution", () => {
     it("returns success result with trimmed output", () => {
       // Arrange: Mock a successful AppleScript execution
-      mockExecSync.mockReturnValue("  Note Title  \n");
+      mockExecSync.mockReturnValue("  Message Subject  \n");
 
       // Act: Execute a simple script
-      const result = executeAppleScript('tell app "Notes" to get name of note 1');
+      const result = executeAppleScript('tell app "Mail" to get subject of message 1');
 
       // Assert: Output should be trimmed
       expect(result.success).toBe(true);
-      expect(result.output).toBe("Note Title");
+      expect(result.output).toBe("Message Subject");
       expect(result.error).toBeUndefined();
     });
 
@@ -41,9 +41,9 @@ describe("executeAppleScript", () => {
 
       // Multi-line AppleScript with tell blocks
       const script = `
-        tell application "Notes"
+        tell application "Mail"
           tell account "iCloud"
-            get notes
+            get messages of mailbox "INBOX"
           end tell
         end tell
       `;
@@ -59,8 +59,8 @@ describe("executeAppleScript", () => {
     it("escapes single quotes in the script for shell safety", () => {
       mockExecSync.mockReturnValue("content");
 
-      // Script containing a single quote (e.g., in a note title)
-      executeAppleScript('get note "Rob\'s Notes"');
+      // Script containing a single quote (e.g., in a search query)
+      executeAppleScript('search messages for "Rob\'s Messages"');
 
       // Verify the quote was escaped for shell
       const calledCommand = mockExecSync.mock.calls[0][0] as string;
@@ -72,11 +72,11 @@ describe("executeAppleScript", () => {
     it("returns error result when execution fails", () => {
       // Arrange: Mock an AppleScript execution failure
       mockExecSync.mockImplementation(() => {
-        throw new Error("execution error: Can't get note. (-1728)");
+        throw new Error("execution error: Can't get message. (-1728)");
       });
 
       // Act: Try to execute a script that will fail
-      const result = executeAppleScript('get note "Nonexistent"');
+      const result = executeAppleScript('get message "Nonexistent"');
 
       // Assert: Should return structured error
       expect(result.success).toBe(false);
@@ -86,25 +86,23 @@ describe("executeAppleScript", () => {
 
     it("parses execution error messages cleanly", () => {
       mockExecSync.mockImplementation(() => {
-        throw new Error("execution error: Note not found (-1728)");
+        throw new Error("execution error: Message not found (-1728)");
       });
 
-      const result = executeAppleScript("get note 1");
+      const result = executeAppleScript("get message 1");
 
       // Should extract the meaningful part of the error
-      expect(result.error).toBe("Note not found");
+      expect(result.error).toBe("Message not found");
     });
 
-    it("handles 'not found' error patterns with user-friendly message", () => {
+    it("handles 'message not found' error patterns with user-friendly message", () => {
       mockExecSync.mockImplementation(() => {
-        throw new Error('Can\'t get note "Missing".');
+        throw new Error("Can't get message.");
       });
 
-      const result = executeAppleScript('get note "Missing"');
+      const result = executeAppleScript('get message "Missing"');
 
-      expect(result.error).toContain("not found");
-      expect(result.error).toContain("Missing");
-      expect(result.error).toContain("case-sensitive"); // Includes helpful hint
+      expect(result.error).toContain("Message not found");
     });
 
     it("provides helpful message for permission errors", () => {
@@ -118,16 +116,16 @@ describe("executeAppleScript", () => {
       expect(result.error).toContain("System Preferences");
     });
 
-    it("provides helpful message for folder not found", () => {
+    it("provides helpful message for mailbox not found", () => {
       mockExecSync.mockImplementation(() => {
-        throw new Error('Can\'t get folder "Work".');
+        throw new Error('Can\'t get mailbox "Work".');
       });
 
       const result = executeAppleScript("test");
 
       expect(result.error).toContain("Work");
       expect(result.error).toContain("not found");
-      expect(result.error).toContain("list-folders");
+      expect(result.error).toContain("list-mailboxes");
     });
 
     it("provides helpful message for account not found", () => {
@@ -231,7 +229,7 @@ describe("executeAppleScript", () => {
 
       expect(result.success).toBe(false);
       expect(result.error).toContain("timed out after 30 seconds");
-      expect(result.error).toContain("Notes.app may be unresponsive");
+      expect(result.error).toContain("Mail.app may be unresponsive");
     });
 
     it("includes custom timeout value in error message", () => {
@@ -255,7 +253,7 @@ describe("executeAppleScript", () => {
   describe("retry logic", () => {
     it("does not retry by default (maxRetries=1)", () => {
       mockExecSync.mockImplementation(() => {
-        throw new Error("Notes.app is not responding");
+        throw new Error("Mail.app is not responding");
       });
 
       executeAppleScript("test");
@@ -269,7 +267,7 @@ describe("executeAppleScript", () => {
       mockExecSync.mockImplementation(() => {
         callCount++;
         if (callCount < 3) {
-          throw new Error("Notes.app is not responding");
+          throw new Error("Mail.app is not responding");
         }
         return "success";
       });
@@ -283,12 +281,12 @@ describe("executeAppleScript", () => {
 
     it("does not retry on non-transient errors", () => {
       mockExecSync.mockImplementation(() => {
-        throw new Error('Can\'t get note "Missing"');
+        throw new Error("syntax error");
       });
 
       executeAppleScript("test", { maxRetries: 3, retryDelayMs: 1 });
 
-      // Should not retry for "note not found" errors
+      // Should not retry for syntax errors
       expect(mockExecSync).toHaveBeenCalledTimes(1);
     });
 
@@ -333,7 +331,7 @@ describe("executeAppleScript", () => {
 
     it("returns last error after all retries exhausted", () => {
       mockExecSync.mockImplementation(() => {
-        throw new Error("Notes.app is not responding");
+        throw new Error("Mail.app is not responding");
       });
 
       const result = executeAppleScript("test", { maxRetries: 3, retryDelayMs: 1 });
@@ -364,7 +362,7 @@ describe("executeAppleScript", () => {
       mockExecSync.mockImplementation(() => {
         callCount++;
         if (callCount < 2) {
-          throw new Error("lost connection to Notes.app");
+          throw new Error("lost connection to Mail.app");
         }
         return "recovered";
       });
@@ -380,7 +378,7 @@ describe("executeAppleScript", () => {
       mockExecSync.mockImplementation(() => {
         callCount++;
         if (callCount < 2) {
-          throw new Error("Notes.app is busy");
+          throw new Error("Mail.app is busy");
         }
         return "recovered";
       });
@@ -398,7 +396,7 @@ describe("executeAppleScript", () => {
       mockExecSync.mockImplementation(() => {
         execCallCount++;
         if (execCallCount <= 3) {
-          throw new Error("Notes.app is not responding");
+          throw new Error("Mail.app is not responding");
         }
         return "success";
       });

+ 20 - 25
src/utils/applescript.ts

@@ -13,7 +13,7 @@ import type { AppleScriptResult, AppleScriptOptions } from "@/types.js";
 /**
  * Default execution timeout for AppleScript commands in milliseconds.
  * 30 seconds is sufficient for most operations, including complex
- * searches on large note collections. Can be overridden per-call.
+ * searches on large mailboxes. Can be overridden per-call.
  */
 const DEFAULT_TIMEOUT_MS = 30000;
 
@@ -92,7 +92,7 @@ function isTimeoutError(error: unknown): boolean {
 
 /**
  * Error patterns that indicate transient failures worth retrying.
- * These typically occur when Notes.app is syncing or temporarily busy.
+ * These typically occur when Mail.app is busy or temporarily unresponsive.
  */
 const RETRYABLE_ERROR_PATTERNS = [
   /timed? out/i,
@@ -152,47 +152,42 @@ const ERROR_MAPPINGS: Array<{ pattern: RegExp; message: string }> = [
   // Application not running
   {
     pattern: /application isn't running|not running/i,
-    message: "Notes.app is not responding. Try opening Notes.app manually.",
+    message: "Mail.app is not responding. Try opening Mail.app manually.",
   },
   // Connection errors
   {
     pattern: /connection is invalid|lost connection/i,
-    message: "Lost connection to Notes.app. The app may have crashed or been restarted.",
+    message: "Lost connection to Mail.app. The app may have crashed or been restarted.",
   },
-  // Note not found (general)
+  // Message not found
   {
-    pattern: /can't get note "([^"]+)"/i,
-    message: 'Note "$1" not found. Verify the title is exact (case-sensitive).',
+    pattern: /can't get message/i,
+    message: "Message not found. The message may have been deleted or moved.",
   },
-  // Note not found by ID
+  // Mailbox not found
   {
-    pattern: /can't get note id/i,
-    message: "Note not found. The note may have been deleted or the ID is invalid.",
-  },
-  // Folder not found
-  {
-    pattern: /can't get folder "([^"]+)"/i,
-    message: 'Folder "$1" not found. Use list-folders to see available folders.',
+    pattern: /can't get mailbox "([^"]+)"/i,
+    message: 'Mailbox "$1" not found. Use list-mailboxes to see available mailboxes.',
   },
   // Account not found
   {
     pattern: /can't get account "([^"]+)"/i,
     message: 'Account "$1" not found. Use list-accounts to see available accounts.',
   },
-  // Folder already exists
+  // Send failed
   {
-    pattern: /folder.*already exists/i,
-    message: "A folder with that name already exists.",
+    pattern: /couldn't send|send failed|cannot send/i,
+    message: "Failed to send email. Check your network connection and Mail.app settings.",
   },
-  // Cannot delete (various reasons)
+  // Offline
   {
-    pattern: /can't delete|cannot delete/i,
-    message: "Cannot delete. The item may be locked or in use.",
+    pattern: /offline|no connection/i,
+    message: "Mail.app is offline. Check your network connection.",
   },
-  // Password protected notes
+  // Cannot delete (various reasons)
   {
-    pattern: /password protected|locked note/i,
-    message: "Note is password-protected. Unlock it in Notes.app first.",
+    pattern: /can't delete|cannot delete/i,
+    message: "Cannot delete. The message may be locked or in use.",
   },
   // Syntax/script errors (usually programming bugs)
   {
@@ -353,7 +348,7 @@ export function executeAppleScript(
       if (isTimeoutError(error)) {
         isTimeout = true;
         const timeoutSecs = Math.round(timeoutMs / 1000);
-        errorMessage = `Operation timed out after ${timeoutSecs} seconds. Notes.app may be unresponsive or the operation involves too many notes.`;
+        errorMessage = `Operation timed out after ${timeoutSecs} seconds. Mail.app may be unresponsive or the operation involves too many messages.`;
       } else if (error instanceof Error) {
         rawError = error.message;
         // Node's ExecException includes stderr in the message