# Directory Structure
```
├── .gitignore
├── bun.lock
├── index.ts
├── install.sh
├── package.json
└── README.md
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Claude Outlook MCP Tool
2 |
3 | This is a Model Context Protocol (MCP) tool that allows Claude to interact with Microsoft Outlook for macOS.
4 |
5 | <a href="https://glama.ai/mcp/servers/0j71n92wnh">
6 | <img width="380" height="200" src="https://glama.ai/mcp/servers/0j71n92wnh/badge" alt="Claude Outlook Tool MCP server" />
7 | </a>
8 |
9 | ## Features
10 |
11 | - Mail:
12 | - Read unread and regular emails
13 | - Search emails by keywords
14 | - Send emails with to, cc, and bcc recipients
15 | - **Send HTML-formatted emails**
16 | - **Attach files to emails**
17 | - List mail folders
18 | - Calendar:
19 | - View today's events
20 | - View upcoming events
21 | - Search for events
22 | - Create new calendar events
23 | - Contacts:
24 | - List contacts
25 | - Search contacts by name
26 |
27 | ## Prerequisites
28 |
29 | - macOS with Apple Silicon (M1/M2/M3) or Intel chip
30 | - [Microsoft Outlook for Mac](https://apps.apple.com/us/app/microsoft-outlook/id985367838) installed and configured
31 | - [Bun](https://bun.sh/) installed
32 | - [Claude desktop app](https://claude.ai/desktop) installed
33 |
34 | ## Installation
35 |
36 | 1. Clone this repository:
37 |
38 | ```bash
39 | git clone https://github.com/syedazharmbnr1/claude-outlook-mcp.git
40 | cd claude-outlook-mcp
41 | ```
42 |
43 | 2. Install dependencies:
44 |
45 | ```bash
46 | bun install
47 | ```
48 |
49 | 3. Make sure the script is executable:
50 |
51 | ```bash
52 | chmod +x index.ts
53 | ```
54 |
55 | 4. Update your Claude Desktop configuration:
56 |
57 | Edit your `claude_desktop_config.json` file (located at `~/Library/Application Support/Claude/claude_desktop_config.json`) to include this tool:
58 |
59 | ```json
60 | {
61 | "mcpServers": {
62 | "outlook-mcp": {
63 | "command": "/Users/YOURUSERNAME/.bun/bin/bun",
64 | "args": ["run", "/path/to/claude-outlook-mcp/index.ts"]
65 | }
66 | }
67 | }
68 | ```
69 |
70 | Make sure to replace `YOURUSERNAME` with your actual macOS username and adjust the path to where you cloned this repository.
71 |
72 | 5. Restart Claude Desktop app
73 |
74 | 6. Grant permissions:
75 | - Go to System Preferences > Privacy & Security > Privacy
76 | - Give Terminal (or your preferred terminal app) access to Accessibility features
77 | - You may see permission prompts when the tool is first used
78 |
79 | ## Usage
80 |
81 | Once installed, you can use the Outlook tool directly from Claude by asking questions like:
82 |
83 | - "Can you check my unread emails in Outlook?"
84 | - "Search my Outlook emails for the quarterly report"
85 | - "Send an email to [email protected] with the subject 'Meeting Tomorrow'"
86 | - "What's on my calendar today?"
87 | - "Create a meeting for tomorrow at 2pm"
88 | - "Find the contact information for Jane Smith"
89 |
90 | ## Examples
91 |
92 | ### Email Operations
93 |
94 | ```
95 | Check my unread emails in Outlook
96 | ```
97 |
98 | ```
99 | Send an email to [email protected] with subject "Project Update" and the following body: Here's the latest update on our project. We've completed phase 1 and are moving on to phase 2.
100 | ```
101 |
102 | ```
103 | Send an HTML email to [email protected] with subject "Weekly Report" and attach the quarterly_results.pdf file
104 | ```
105 |
106 | ```
107 | Search my emails for "budget meeting"
108 | ```
109 |
110 | ### Calendar Operations
111 |
112 | ```
113 | What events do I have today?
114 | ```
115 |
116 | ```
117 | Create a calendar event for a team meeting tomorrow from 2pm to 3pm
118 | ```
119 |
120 | ```
121 | Show me my upcoming events for the next 2 weeks
122 | ```
123 |
124 | ### Contact Operations
125 |
126 | ```
127 | List all my Outlook contacts
128 | ```
129 |
130 | ```
131 | Search for contact information for Jane Smith
132 | ```
133 |
134 | ## Advanced Features
135 |
136 | ### HTML Email Support
137 |
138 | You can send rich HTML-formatted emails by setting the `isHtml` parameter to true:
139 |
140 | ```
141 | Send an HTML email to [email protected] with the subject "Project Update" and body "<h1>Project Update</h1><p>We've made <b>significant progress</b> on the project.</p>"
142 | ```
143 |
144 | ### File Attachments
145 |
146 | You can attach files to your emails by providing the file paths in the `attachments` parameter:
147 |
148 | ```
149 | Send an email to [email protected] with subject "Monthly Report" and attach the reports/march_2025.pdf file
150 | ```
151 |
152 | For best results with attachments:
153 | - Use absolute file paths when possible
154 | - Make sure the files are accessible to the process running the MCP tool
155 | - Attachments will automatically be handled with robust error detection
156 |
157 | ## Troubleshooting
158 |
159 | If you encounter issues with attachments:
160 | - Check if the file exists and is readable
161 | - Use absolute file paths instead of relative paths
162 | - Make sure the user running the process has permission to read the file
163 |
164 | If you encounter the error `Cannot find module '@modelcontextprotocol/sdk/server/index.js'`:
165 |
166 | 1. Make sure you've run `bun install` to install all dependencies
167 | 2. Try installing the MCP SDK explicitly:
168 | ```bash
169 | bun add @modelcontextprotocol/sdk@^1.5.0
170 | ```
171 | 3. Check if the module exists in your node_modules directory:
172 | ```bash
173 | ls -la node_modules/@modelcontextprotocol/sdk/server/
174 | ```
175 |
176 | If the error persists, try creating a new project with Bun:
177 |
178 | ```bash
179 | mkdir -p ~/yourpath/claude-outlook-mcp
180 | cd ~/yourpath/claude-outlook-mcp
181 | bun init -y
182 | ```
183 |
184 | Then copy the package.json and index.ts files to the new directory and run:
185 |
186 | ```bash
187 | bun install
188 | bun run index.ts
189 | ```
190 |
191 | Update your claude_desktop_config.json to point to the new location.
192 |
193 | ## License
194 |
195 | MIT
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "claude-outlook-mcp",
3 | "version": "1.0.0",
4 | "module": "index.ts",
5 | "type": "module",
6 | "description": "A Claude MCP tool to interact with Microsoft Outlook for macOS",
7 | "author": "Syed Azhar",
8 | "license": "MIT",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/syedazharmbnr1/claude-outlook-mcp.git"
12 | },
13 | "keywords": [
14 | "mcp",
15 | "claude",
16 | "outlook",
17 | "microsoft365"
18 | ],
19 | "scripts": {
20 | "dev": "bun run index.ts",
21 | "start": "bun run index.ts"
22 | },
23 | "dependencies": {
24 | "@jxa/global-type": "^1.3.6",
25 | "@jxa/run": "^1.3.6",
26 | "@modelcontextprotocol/sdk": "^1.5.0",
27 | "run-applescript": "^7.0.0"
28 | },
29 | "devDependencies": {
30 | "@types/bun": "latest",
31 | "@types/node": "^22.13.4"
32 | }
33 | }
```
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | # Colors for output
4 | GREEN='\033[0;32m'
5 | YELLOW='\033[1;33m'
6 | RED='\033[0;31m'
7 | NC='\033[0m' # No Color
8 |
9 | echo -e "${GREEN}Installing Claude Outlook MCP Tool...${NC}"
10 |
11 | # Check if Bun is installed
12 | if ! command -v bun &> /dev/null; then
13 | echo -e "${RED}Bun is not installed. Please install Bun first:${NC}"
14 | echo -e "${YELLOW}curl -fsSL https://bun.sh/install | bash${NC}"
15 | exit 1
16 | fi
17 |
18 | # Install dependencies
19 | echo -e "${GREEN}Installing dependencies...${NC}"
20 | bun install
21 |
22 | if [ $? -ne 0 ]; then
23 | echo -e "${RED}Failed to install dependencies. Trying with explicit MCP SDK...${NC}"
24 | bun add @modelcontextprotocol/sdk@^1.5.0
25 | bun install
26 | fi
27 |
28 | # Make script executable
29 | chmod +x index.ts
30 |
31 | # Get current username
32 | USERNAME=$(whoami)
33 | INSTALL_PATH=$(pwd)
34 |
35 | # Create claude_desktop_config.json snippet
36 | CONFIG_SNIPPET=$(cat << EOF
37 | {
38 | "mcpServers": {
39 | "outlook-mcp": {
40 | "command": "/Users/$USERNAME/.bun/bin/bun",
41 | "args": ["run", "$INSTALL_PATH/index.ts"]
42 | }
43 | }
44 | }
45 | EOF
46 | )
47 |
48 | echo -e "${GREEN}Installation complete!${NC}"
49 | echo -e "${YELLOW}Please add the following to your Claude Desktop config file at:${NC}"
50 | echo -e "${YELLOW}~/Library/Application Support/Claude/claude_desktop_config.json${NC}"
51 | echo ""
52 | echo -e "${GREEN}$CONFIG_SNIPPET${NC}"
53 | echo ""
54 | echo -e "${YELLOW}Don't forget to restart Claude Desktop app after making these changes.${NC}"
55 | echo -e "${YELLOW}You may need to grant Terminal access to Accessibility features in System Preferences.${NC}"
```
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env bun
2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4 | import {
5 | CallToolRequestSchema,
6 | ListToolsRequestSchema,
7 | type Tool,
8 | } from "@modelcontextprotocol/sdk/types.js";
9 | import { runAppleScript } from 'run-applescript';
10 |
11 | // ====================================================
12 | // 1. Tool Definitions
13 | // ====================================================
14 |
15 | // Define Outlook Mail tool
16 | const OUTLOOK_MAIL_TOOL: Tool = {
17 | name: "outlook_mail",
18 | description: "Interact with Microsoft Outlook for macOS - read, search, send, and manage emails",
19 | inputSchema: {
20 | type: "object",
21 | properties: {
22 | operation: {
23 | type: "string",
24 | description: "Operation to perform: 'unread', 'search', 'send', 'folders', or 'read'",
25 | enum: ["unread", "search", "send", "folders", "read"]
26 | },
27 | folder: {
28 | type: "string",
29 | description: "Email folder to use (optional - if not provided, uses inbox or searches across all folders)"
30 | },
31 | limit: {
32 | type: "number",
33 | description: "Number of emails to retrieve (optional, for unread, read, and search operations)"
34 | },
35 | searchTerm: {
36 | type: "string",
37 | description: "Text to search for in emails (required for search operation)"
38 | },
39 | to: {
40 | type: "string",
41 | description: "Recipient email address (required for send operation)"
42 | },
43 | subject: {
44 | type: "string",
45 | description: "Email subject (required for send operation)"
46 | },
47 | body: {
48 | type: "string",
49 | description: "Email body content (required for send operation)"
50 | },
51 | isHtml: {
52 | type: "boolean",
53 | description: "Whether the body content is HTML (optional for send operation, default: false)"
54 | },
55 | cc: {
56 | type: "string",
57 | description: "CC email address (optional for send operation)"
58 | },
59 | bcc: {
60 | type: "string",
61 | description: "BCC email address (optional for send operation)"
62 | },
63 | attachments: {
64 | type: "array",
65 | description: "File paths to attach to the email (optional for send operation)",
66 | items: {
67 | type: "string"
68 | }
69 | }
70 | },
71 | required: ["operation"]
72 | }
73 | };
74 |
75 | // Define Outlook Calendar tool
76 | const OUTLOOK_CALENDAR_TOOL: Tool = {
77 | name: "outlook_calendar",
78 | description: "Interact with Microsoft Outlook for macOS calendar - view, create, and manage events",
79 | inputSchema: {
80 | type: "object",
81 | properties: {
82 | operation: {
83 | type: "string",
84 | description: "Operation to perform: 'today', 'upcoming', 'search', or 'create'",
85 | enum: ["today", "upcoming", "search", "create"]
86 | },
87 | searchTerm: {
88 | type: "string",
89 | description: "Text to search for in events (required for search operation)"
90 | },
91 | limit: {
92 | type: "number",
93 | description: "Number of events to retrieve (optional, for today and upcoming operations)"
94 | },
95 | days: {
96 | type: "number",
97 | description: "Number of days to look ahead (optional, for upcoming operation, default: 7)"
98 | },
99 | subject: {
100 | type: "string",
101 | description: "Event subject/title (required for create operation)"
102 | },
103 | start: {
104 | type: "string",
105 | description: "Start time in ISO format (required for create operation)"
106 | },
107 | end: {
108 | type: "string",
109 | description: "End time in ISO format (required for create operation)"
110 | },
111 | location: {
112 | type: "string",
113 | description: "Event location (optional for create operation)"
114 | },
115 | body: {
116 | type: "string",
117 | description: "Event description/body (optional for create operation)"
118 | },
119 | attendees: {
120 | type: "string",
121 | description: "Comma-separated list of attendee email addresses (optional for create operation)"
122 | }
123 | },
124 | required: ["operation"]
125 | }
126 | };
127 |
128 | // Define Outlook Contacts tool
129 | const OUTLOOK_CONTACTS_TOOL: Tool = {
130 | name: "outlook_contacts",
131 | description: "Search and retrieve contacts from Microsoft Outlook for macOS",
132 | inputSchema: {
133 | type: "object",
134 | properties: {
135 | operation: {
136 | type: "string",
137 | description: "Operation to perform: 'list' or 'search'",
138 | enum: ["list", "search"]
139 | },
140 | searchTerm: {
141 | type: "string",
142 | description: "Text to search for in contacts (required for search operation)"
143 | },
144 | limit: {
145 | type: "number",
146 | description: "Number of contacts to retrieve (optional)"
147 | }
148 | },
149 | required: ["operation"]
150 | }
151 | };
152 |
153 | // ====================================================
154 | // 2. Server Setup
155 | // ====================================================
156 |
157 | console.error("Starting Outlook MCP server...");
158 |
159 | const server = new Server(
160 | {
161 | name: "Outlook MCP Tool",
162 | version: "1.0.0",
163 | },
164 | {
165 | capabilities: {
166 | tools: {},
167 | },
168 | }
169 | );
170 |
171 | // ====================================================
172 | // 3. Core Functions
173 | // ====================================================
174 |
175 | // Check if Outlook is installed and running
176 | async function checkOutlookAccess(): Promise<boolean> {
177 | console.error("[checkOutlookAccess] Checking if Outlook is accessible...");
178 | try {
179 | const isInstalled = await runAppleScript(`
180 | tell application "System Events"
181 | set outlookExists to exists application process "Microsoft Outlook"
182 | return outlookExists
183 | end tell
184 | `);
185 |
186 | if (isInstalled !== "true") {
187 | console.error("[checkOutlookAccess] Microsoft Outlook is not installed or running");
188 | throw new Error("Microsoft Outlook is not installed or running on this system");
189 | }
190 |
191 | const isRunning = await runAppleScript(`
192 | tell application "System Events"
193 | set outlookRunning to application process "Microsoft Outlook" exists
194 | return outlookRunning
195 | end tell
196 | `);
197 |
198 | if (isRunning !== "true") {
199 | console.error("[checkOutlookAccess] Microsoft Outlook is not running, attempting to launch...");
200 | try {
201 | await runAppleScript(`
202 | tell application "Microsoft Outlook" to activate
203 | delay 2
204 | `);
205 | console.error("[checkOutlookAccess] Launched Outlook successfully");
206 | } catch (activateError) {
207 | console.error("[checkOutlookAccess] Error activating Microsoft Outlook:", activateError);
208 | throw new Error("Could not activate Microsoft Outlook. Please start it manually.");
209 | }
210 | } else {
211 | console.error("[checkOutlookAccess] Microsoft Outlook is already running");
212 | }
213 |
214 | return true;
215 | } catch (error) {
216 | console.error("[checkOutlookAccess] Outlook access check failed:", error);
217 | throw new Error(
218 | `Cannot access Microsoft Outlook. Please make sure Outlook is installed and properly configured. Error: ${error instanceof Error ? error.message : String(error)}`
219 | );
220 | }
221 | }
222 |
223 | // ====================================================
224 | // 4. EMAIL FUNCTIONS
225 | // ====================================================
226 |
227 | // Function to get unread emails
228 | async function getUnreadEmails(folder: string = "Inbox", limit: number = 10): Promise<any[]> {
229 | console.error(`[getUnreadEmails] Getting unread emails from folder: ${folder}, limit: ${limit}`);
230 | await checkOutlookAccess();
231 |
232 | const folderPath = folder === "Inbox" ? "inbox" : folder;
233 | const script = `
234 | tell application "Microsoft Outlook"
235 | try
236 | set theFolder to ${folderPath} -- Use the specified folder or default to inbox
237 | set unreadMessages to {}
238 | set allMessages to messages of theFolder
239 | set i to 0
240 |
241 | repeat with theMessage in allMessages
242 | if read status of theMessage is false then
243 | set i to i + 1
244 | set msgData to {subject:subject of theMessage, sender:sender of theMessage, ¬
245 | date:time sent of theMessage, id:id of theMessage}
246 |
247 | -- Try to get content
248 | try
249 | set msgContent to content of theMessage
250 | if length of msgContent > 500 then
251 | set msgContent to (text 1 thru 500 of msgContent) & "..."
252 | end if
253 | set msgData to msgData & {content:msgContent}
254 | on error
255 | set msgData to msgData & {content:"[Content not available]"}
256 | end try
257 |
258 | set end of unreadMessages to msgData
259 |
260 | -- Stop if we've reached the limit
261 | if i >= ${limit} then
262 | exit repeat
263 | end if
264 | end if
265 | end repeat
266 |
267 | return unreadMessages
268 | on error errMsg
269 | return "Error: " & errMsg
270 | end try
271 | end tell
272 | `;
273 |
274 | try {
275 | const result = await runAppleScript(script);
276 | console.error(`[getUnreadEmails] Raw result length: ${result.length}`);
277 |
278 | // Parse the results (AppleScript returns records as text)
279 | if (result.startsWith("Error:")) {
280 | throw new Error(result);
281 | }
282 |
283 | // Simple parsing for demonstration
284 | // In a production environment, you'd want more robust parsing
285 | const emails = [];
286 | const matches = result.match(/\{([^}]+)\}/g);
287 |
288 | if (matches && matches.length > 0) {
289 | for (const match of matches) {
290 | try {
291 | const props = match.substring(1, match.length - 1).split(',');
292 | const email: any = {};
293 |
294 | props.forEach(prop => {
295 | const parts = prop.split(':');
296 | if (parts.length >= 2) {
297 | const key = parts[0].trim();
298 | const value = parts.slice(1).join(':').trim();
299 | email[key] = value;
300 | }
301 | });
302 |
303 | if (email.subject || email.sender) {
304 | emails.push({
305 | subject: email.subject || "No subject",
306 | sender: email.sender || "Unknown sender",
307 | dateSent: email.date || new Date().toString(),
308 | content: email.content || "[Content not available]",
309 | id: email.id || ""
310 | });
311 | }
312 | } catch (parseError) {
313 | console.error('[getUnreadEmails] Error parsing email match:', parseError);
314 | }
315 | }
316 | }
317 |
318 | console.error(`[getUnreadEmails] Found ${emails.length} unread emails`);
319 | return emails;
320 | } catch (error) {
321 | console.error("[getUnreadEmails] Error getting unread emails:", error);
322 | throw error;
323 | }
324 | }
325 |
326 | // Function to search emails
327 | async function searchEmails(searchTerm: string, folder: string = "Inbox", limit: number = 10): Promise<any[]> {
328 | console.error(`[searchEmails] Searching for "${searchTerm}" in folder: ${folder}, limit: ${limit}`);
329 | await checkOutlookAccess();
330 |
331 | const folderPath = folder === "Inbox" ? "inbox" : folder;
332 | const script = `
333 | tell application "Microsoft Outlook"
334 | try
335 | set theFolder to ${folderPath}
336 | set searchResults to {}
337 | set allMessages to messages of theFolder
338 | set i to 0
339 | set searchString to "${searchTerm.replace(/"/g, '\\"')}"
340 |
341 | repeat with theMessage in allMessages
342 | if (subject of theMessage contains searchString) or (content of theMessage contains searchString) then
343 | set i to i + 1
344 | set msgData to {subject:subject of theMessage, sender:sender of theMessage, ¬
345 | date:time sent of theMessage, id:id of theMessage}
346 |
347 | -- Try to get content
348 | try
349 | set msgContent to content of theMessage
350 | if length of msgContent > 500 then
351 | set msgContent to (text 1 thru 500 of msgContent) & "..."
352 | end if
353 | set msgData to msgData & {content:msgContent}
354 | on error
355 | set msgData to msgData & {content:"[Content not available]"}
356 | end try
357 |
358 | set end of searchResults to msgData
359 |
360 | -- Stop if we've reached the limit
361 | if i >= ${limit} then
362 | exit repeat
363 | end if
364 | end if
365 | end repeat
366 |
367 | return searchResults
368 | on error errMsg
369 | return "Error: " & errMsg
370 | end try
371 | end tell
372 | `;
373 |
374 | try {
375 | const result = await runAppleScript(script);
376 | console.error(`[searchEmails] Raw result length: ${result.length}`);
377 |
378 | // Parse the results
379 | if (result.startsWith("Error:")) {
380 | throw new Error(result);
381 | }
382 |
383 | // Parse the emails similar to unread emails
384 | const emails = [];
385 | const matches = result.match(/\{([^}]+)\}/g);
386 |
387 | if (matches && matches.length > 0) {
388 | for (const match of matches) {
389 | try {
390 | const props = match.substring(1, match.length - 1).split(',');
391 | const email: any = {};
392 |
393 | props.forEach(prop => {
394 | const parts = prop.split(':');
395 | if (parts.length >= 2) {
396 | const key = parts[0].trim();
397 | const value = parts.slice(1).join(':').trim();
398 | email[key] = value;
399 | }
400 | });
401 |
402 | if (email.subject || email.sender) {
403 | emails.push({
404 | subject: email.subject || "No subject",
405 | sender: email.sender || "Unknown sender",
406 | dateSent: email.date || new Date().toString(),
407 | content: email.content || "[Content not available]",
408 | id: email.id || ""
409 | });
410 | }
411 | } catch (parseError) {
412 | console.error('[searchEmails] Error parsing email match:', parseError);
413 | }
414 | }
415 | }
416 |
417 | console.error(`[searchEmails] Found ${emails.length} matching emails`);
418 | return emails;
419 | } catch (error) {
420 | console.error("[searchEmails] Error searching emails:", error);
421 | throw error;
422 | }
423 | }
424 |
425 | async function checkAttachmentPath(filePath: string): Promise<string> {
426 | try {
427 | // Convert to absolute path if relative
428 | let fullPath = filePath;
429 | if (!filePath.startsWith('/')) {
430 | const cwd = process.cwd();
431 | fullPath = `${cwd}/${filePath}`;
432 | }
433 |
434 | // Check if the file exists and is readable
435 | const fs = require('fs');
436 | const { promisify } = require('util');
437 | const access = promisify(fs.access);
438 | const stat = promisify(fs.stat);
439 |
440 | try {
441 | await access(fullPath, fs.constants.R_OK);
442 | const stats = await stat(fullPath);
443 |
444 | return `File exists and is readable: ${fullPath}\nSize: ${stats.size} bytes\nPermissions: ${stats.mode.toString(8)}\nLast modified: ${stats.mtime}`;
445 | } catch (err) {
446 | return `ERROR: Cannot access file: ${fullPath}\nError details: ${err.message}`;
447 | }
448 | } catch (error) {
449 | return `Failed to check attachment path: ${error.message}`;
450 | }
451 | }
452 |
453 | // Add a debug version of sending email with attachment to test if files are accessible
454 | async function debugSendEmailWithAttachment(
455 | to: string,
456 | subject: string,
457 | body: string,
458 | attachmentPath: string
459 | ): Promise<string> {
460 | // First check if the file exists and is readable
461 | const fileStatus = await checkAttachmentPath(attachmentPath);
462 | console.error(`[debugSendEmail] Attachment status: ${fileStatus}`);
463 |
464 | // Create a simple AppleScript that just attempts to open the file
465 | const script = `
466 | set theFile to POSIX file "${attachmentPath.replace(/"/g, '\\"')}"
467 | try
468 | tell application "Finder"
469 | set fileExists to exists file theFile
470 | set fileInfo to info for file theFile
471 | return "File exists: " & fileExists & ", size: " & (size of fileInfo)
472 | end tell
473 | on error errMsg
474 | return "Error accessing file: " & errMsg
475 | end try
476 | `;
477 |
478 | try {
479 | const result = await runAppleScript(script);
480 | console.error(`[debugSendEmail] AppleScript file check: ${result}`);
481 |
482 | // Now try to actually create a draft with the attachment
483 | const emailScript = `
484 | tell application "Microsoft Outlook"
485 | try
486 | set newMessage to make new outgoing message with properties {subject:"DEBUG: ${subject.replace(/"/g, '\\"')}", visible:true}
487 | set content of newMessage to "${body.replace(/"/g, '\\"')}"
488 | set to recipients of newMessage to {"${to}"}
489 |
490 | try
491 | set attachmentFile to POSIX file "${attachmentPath.replace(/"/g, '\\"')}"
492 | make new attachment at newMessage with properties {file:attachmentFile}
493 | set attachResult to "Successfully attached file"
494 | on error attachErrMsg
495 | set attachResult to "Failed to attach file: " & attachErrMsg
496 | end try
497 |
498 | return attachResult
499 | on error errMsg
500 | return "Error creating email: " & errMsg
501 | end try
502 | end tell
503 | `;
504 |
505 | const attachResult = await runAppleScript(emailScript);
506 | console.error(`[debugSendEmail] Attachment result: ${attachResult}`);
507 |
508 | return `File check: ${fileStatus}\n\nAttachment test: ${attachResult}`;
509 | } catch (error) {
510 | console.error("[debugSendEmail] Error during debug:", error);
511 | return `Debugging error: ${error.message}\n\nFile check: ${fileStatus}`;
512 | }
513 | }
514 | // Update the sendEmail function to handle attachments and HTML content
515 | async function sendEmail(
516 | to: string,
517 | subject: string,
518 | body: string,
519 | cc?: string,
520 | bcc?: string,
521 | isHtml: boolean = false,
522 | attachments?: string[]
523 | ): Promise<string> {
524 | console.error(`[sendEmail] Sending email to: ${to}, subject: "${subject}"`);
525 | console.error(`[sendEmail] Attachments: ${attachments ? JSON.stringify(attachments) : 'none'}`);
526 |
527 | await checkOutlookAccess();
528 |
529 | // Extract name from email if possible (for display name)
530 | const extractNameFromEmail = (email: string): string => {
531 | const namePart = email.split('@')[0];
532 | return namePart
533 | .split('.')
534 | .map(part => part.charAt(0).toUpperCase() + part.slice(1))
535 | .join(' ');
536 | };
537 |
538 | // Get name for display
539 | const toName = extractNameFromEmail(to);
540 | const ccName = cc ? extractNameFromEmail(cc) : "";
541 | const bccName = bcc ? extractNameFromEmail(bcc) : "";
542 |
543 | // Escape special characters
544 | const escapedSubject = subject.replace(/"/g, '\\"');
545 | const escapedBody = body.replace(/"/g, '\\"').replace(/\n/g, '\\n');
546 |
547 | // Process attachments: Convert to absolute paths if they are relative
548 | let processedAttachments: string[] = [];
549 | if (attachments && attachments.length > 0) {
550 | processedAttachments = attachments.map(path => {
551 | // Check if path is absolute (starts with /)
552 | if (path.startsWith('/')) {
553 | return path;
554 | }
555 | // Get current working directory and join with relative path
556 | const cwd = process.cwd();
557 | return `${cwd}/${path}`;
558 | });
559 | console.error(`[sendEmail] Processed attachments: ${JSON.stringify(processedAttachments)}`);
560 | }
561 |
562 | // Create attachment script part with better error handling
563 | const attachmentScript = processedAttachments.length > 0
564 | ? processedAttachments.map(filePath => {
565 | const escapedPath = filePath.replace(/"/g, '\\"');
566 | return `
567 | try
568 | set attachmentFile to POSIX file "${escapedPath}"
569 | make new attachment at msg with properties {file:attachmentFile}
570 | log "Successfully attached file: ${escapedPath}"
571 | on error errMsg
572 | log "Failed to attach file: ${escapedPath} - Error: " & errMsg
573 | end try
574 | `;
575 | }).join('\n')
576 | : '';
577 |
578 | // Try approach 1: Using specific syntax for creating a message with attachments
579 | try {
580 | const script1 = `
581 | tell application "Microsoft Outlook"
582 | try
583 | set msg to make new outgoing message with properties {subject:"${escapedSubject}"}
584 |
585 | ${isHtml ?
586 | `set content type of msg to HTML
587 | set content of msg to "${escapedBody}"`
588 | :
589 | `set content of msg to "${escapedBody}"`
590 | }
591 |
592 | tell msg
593 | set recipTo to make new to recipient with properties {email address:{name:"${toName}", address:"${to}"}}
594 | ${cc ? `set recipCc to make new cc recipient with properties {email address:{name:"${ccName}", address:"${cc}"}}` : ''}
595 | ${bcc ? `set recipBcc to make new bcc recipient with properties {email address:{name:"${bccName}", address:"${bcc}"}}` : ''}
596 |
597 | ${attachmentScript}
598 | end tell
599 |
600 | -- Delay to allow attachments to be processed
601 | delay 1
602 |
603 | send msg
604 | return "Email sent successfully with attachments"
605 | on error errMsg
606 | return "Error: " & errMsg
607 | end try
608 | end tell
609 | `;
610 |
611 | console.error("[sendEmail] Executing AppleScript method 1");
612 | const result = await runAppleScript(script1);
613 | console.error(`[sendEmail] Result (method 1): ${result}`);
614 |
615 | if (result.startsWith("Error:")) {
616 | throw new Error(result);
617 | }
618 |
619 | return result;
620 | } catch (error1) {
621 | console.error("[sendEmail] Method 1 failed:", error1);
622 |
623 | // Try approach 2: Using AppleScript's draft window method
624 | try {
625 | const script2 = `
626 | tell application "Microsoft Outlook"
627 | try
628 | set newDraft to make new draft window
629 | set theMessage to item 1 of mail items of newDraft
630 | set subject of theMessage to "${escapedSubject}"
631 |
632 | ${isHtml ?
633 | `set content type of theMessage to HTML
634 | set content of theMessage to "${escapedBody}"`
635 | :
636 | `set content of theMessage to "${escapedBody}"`
637 | }
638 |
639 | set to recipients of theMessage to {"${to}"}
640 | ${cc ? `set cc recipients of theMessage to {"${cc}"}` : ''}
641 | ${bcc ? `set bcc recipients of theMessage to {"${bcc}"}` : ''}
642 |
643 | ${processedAttachments.map(filePath => {
644 | const escapedPath = filePath.replace(/"/g, '\\"');
645 | return `
646 | try
647 | set attachmentFile to POSIX file "${escapedPath}"
648 | make new attachment at theMessage with properties {file:attachmentFile}
649 | log "Successfully attached file: ${escapedPath}"
650 | on error attachErrMsg
651 | log "Failed to attach file: ${escapedPath} - Error: " & attachErrMsg
652 | end try
653 | `;
654 | }).join('\n')}
655 |
656 | -- Delay to allow attachments to be processed
657 | delay 1
658 |
659 | send theMessage
660 | return "Email sent successfully with method 2"
661 | on error errMsg
662 | return "Error: " & errMsg
663 | end try
664 | end tell
665 | `;
666 |
667 | console.error("[sendEmail] Executing AppleScript method 2");
668 | const result = await runAppleScript(script2);
669 | console.error(`[sendEmail] Result (method 2): ${result}`);
670 |
671 | if (result.startsWith("Error:")) {
672 | throw new Error(result);
673 | }
674 |
675 | return result;
676 | } catch (error2) {
677 | console.error("[sendEmail] Method 2 failed:", error2);
678 |
679 | // Try approach 3: Create a draft for the user to manually send
680 | try {
681 | const script3 = `
682 | tell application "Microsoft Outlook"
683 | try
684 | set newMessage to make new outgoing message with properties {subject:"${escapedSubject}", visible:true}
685 |
686 | ${isHtml ?
687 | `set content type of newMessage to HTML
688 | set content of newMessage to "${escapedBody}"`
689 | :
690 | `set content of newMessage to "${escapedBody}"`
691 | }
692 |
693 | set to recipients of newMessage to {"${to}"}
694 | ${cc ? `set cc recipients of newMessage to {"${cc}"}` : ''}
695 | ${bcc ? `set bcc recipients of newMessage to {"${bcc}"}` : ''}
696 |
697 | ${processedAttachments.map(filePath => {
698 | const escapedPath = filePath.replace(/"/g, '\\"');
699 | return `
700 | try
701 | set attachmentFile to POSIX file "${escapedPath}"
702 | make new attachment at newMessage with properties {file:attachmentFile}
703 | log "Successfully attached file: ${escapedPath}"
704 | on error attachErrMsg
705 | log "Failed to attach file: ${escapedPath} - Error: " & attachErrMsg
706 | end try
707 | `;
708 | }).join('\n')}
709 |
710 | -- Display the message
711 | activate
712 | return "Email draft created with attachments. Please review and send manually."
713 | on error errMsg
714 | return "Error: " & errMsg
715 | end try
716 | end tell
717 | `;
718 |
719 | console.error("[sendEmail] Executing AppleScript method 3");
720 | const result = await runAppleScript(script3);
721 | console.error(`[sendEmail] Result (method 3): ${result}`);
722 |
723 | if (result.startsWith("Error:")) {
724 | throw new Error(result);
725 | }
726 |
727 | return "A draft has been created in Outlook with the content and attachments. Please review and send it manually.";
728 | } catch (error3) {
729 | console.error("[sendEmail] All methods failed:", error3);
730 | throw new Error(`Could not send or create email. Please check if Outlook is properly configured and that you have granted necessary permissions. Error details: ${error3}`);
731 | }
732 | }
733 | }
734 | }
735 | // Function to get mail folders - this works based on your logs
736 | async function getMailFolders(): Promise<string[]> {
737 | console.error("[getMailFolders] Getting mail folders");
738 | await checkOutlookAccess();
739 |
740 | const script = `
741 | tell application "Microsoft Outlook"
742 | set folderNames to {}
743 | set allFolders to mail folders
744 |
745 | repeat with theFolder in allFolders
746 | set end of folderNames to name of theFolder
747 | end repeat
748 |
749 | return folderNames
750 | end tell
751 | `;
752 |
753 | try {
754 | const result = await runAppleScript(script);
755 | console.error(`[getMailFolders] Result: ${result}`);
756 | return result.split(", ");
757 | } catch (error) {
758 | console.error("[getMailFolders] Error getting mail folders:", error);
759 | throw error;
760 | }
761 | }
762 |
763 | // Function to read emails in a folder that uses simple AppleScript
764 | async function readEmails(folder: string = "Inbox", limit: number = 10): Promise<any[]> {
765 | console.error(`[readEmails] Reading emails from folder: ${folder}, limit: ${limit}`);
766 | await checkOutlookAccess();
767 |
768 | // Use a simplified approach that should be more compatible
769 | const script = `
770 | tell application "Microsoft Outlook"
771 | try
772 | -- Get the folder by name safely
773 | set targetFolder to null
774 | set allFolders to mail folders
775 | repeat with mailFolder in allFolders
776 | if name of mailFolder is "${folder}" then
777 | set targetFolder to mailFolder
778 | exit repeat
779 | end if
780 | end repeat
781 |
782 | if targetFolder is null then set targetFolder to inbox
783 |
784 | -- Get messages
785 | set messageList to {}
786 | set msgCount to 0
787 | set allMsgs to messages of targetFolder
788 |
789 | repeat with i from 1 to (count of allMsgs)
790 | if msgCount >= ${limit} then exit repeat
791 |
792 | try
793 | set theMsg to item i of allMsgs
794 | set msgSubject to subject of theMsg
795 | set msgSender to sender of theMsg
796 | set msgDate to time sent of theMsg
797 |
798 | -- Create a simple text representation for the message
799 | set msgInfo to msgSubject & " | " & msgSender & " | " & msgDate
800 | set end of messageList to msgInfo
801 | set msgCount to msgCount + 1
802 | on error
803 | -- Skip problematic messages
804 | end try
805 | end repeat
806 |
807 | return messageList
808 | on error errMsg
809 | return "Error: " & errMsg
810 | end try
811 | end tell
812 | `;
813 |
814 | try {
815 | const result = await runAppleScript(script);
816 |
817 | if (result.startsWith("Error:")) {
818 | throw new Error(result);
819 | }
820 |
821 | // Parse the results in a simple format
822 | const emails = result.split(", ").map(msgInfo => {
823 | const parts = msgInfo.split(" | ");
824 | return {
825 | subject: parts[0] || "No subject",
826 | sender: parts[1] || "Unknown sender",
827 | dateSent: parts[2] || new Date().toString(),
828 | content: "Content not retrieved in simple mode"
829 | };
830 | });
831 |
832 | console.error(`[readEmails] Found ${emails.length} emails using simplified approach`);
833 | return emails;
834 | } catch (error) {
835 | console.error("[readEmails] Error reading emails:", error);
836 | throw error;
837 | }
838 | }
839 |
840 | // ====================================================
841 | // 5. CALENDAR FUNCTIONS
842 | // ====================================================
843 |
844 | // Function to get today's calendar events
845 | async function getTodayEvents(limit: number = 10): Promise<any[]> {
846 | console.error(`[getTodayEvents] Getting today's events, limit: ${limit}`);
847 | await checkOutlookAccess();
848 |
849 | const script = `
850 | tell application "Microsoft Outlook"
851 | set todayEvents to {}
852 | set theCalendar to default calendar
853 | set todayDate to current date
854 | set startOfDay to todayDate - (time of todayDate)
855 | set endOfDay to startOfDay + 1 * days
856 |
857 | set eventList to events of theCalendar whose start time is greater than or equal to startOfDay and start time is less than endOfDay
858 |
859 | set eventCount to count of eventList
860 | set limitCount to ${limit}
861 |
862 | if eventCount < limitCount then
863 | set limitCount to eventCount
864 | end if
865 |
866 | repeat with i from 1 to limitCount
867 | set theEvent to item i of eventList
868 | set eventData to {subject:subject of theEvent, ¬
869 | start:start time of theEvent, ¬
870 | end:end time of theEvent, ¬
871 | location:location of theEvent, ¬
872 | id:id of theEvent}
873 |
874 | set end of todayEvents to eventData
875 | end repeat
876 |
877 | return todayEvents
878 | end tell
879 | `;
880 |
881 | try {
882 | const result = await runAppleScript(script);
883 | console.error(`[getTodayEvents] Raw result length: ${result.length}`);
884 |
885 | // Parse the results
886 | const events = [];
887 | const matches = result.match(/\{([^}]+)\}/g);
888 |
889 | if (matches && matches.length > 0) {
890 | for (const match of matches) {
891 | try {
892 | const props = match.substring(1, match.length - 1).split(',');
893 | const event: any = {};
894 |
895 | props.forEach(prop => {
896 | const parts = prop.split(':');
897 | if (parts.length >= 2) {
898 | const key = parts[0].trim();
899 | const value = parts.slice(1).join(':').trim();
900 | event[key] = value;
901 | }
902 | });
903 |
904 | if (event.subject) {
905 | events.push({
906 | subject: event.subject,
907 | start: event.start,
908 | end: event.end,
909 | location: event.location || "No location",
910 | id: event.id
911 | });
912 | }
913 | } catch (parseError) {
914 | console.error('[getTodayEvents] Error parsing event match:', parseError);
915 | }
916 | }
917 | }
918 |
919 | console.error(`[getTodayEvents] Found ${events.length} events for today`);
920 | return events;
921 | } catch (error) {
922 | console.error("[getTodayEvents] Error getting today's events:", error);
923 | throw error;
924 | }
925 | }
926 |
927 | // Function to get upcoming calendar events
928 | async function getUpcomingEvents(days: number = 7, limit: number = 10): Promise<any[]> {
929 | console.error(`[getUpcomingEvents] Getting upcoming events for next ${days} days, limit: ${limit}`);
930 | await checkOutlookAccess();
931 |
932 | const script = `
933 | tell application "Microsoft Outlook"
934 | set upcomingEvents to {}
935 | set theCalendar to default calendar
936 | set todayDate to current date
937 | set startOfToday to todayDate - (time of todayDate)
938 | set endDate to startOfToday + ${days} * days
939 |
940 | set eventList to events of theCalendar whose start time is greater than or equal to todayDate and start time is less than endDate
941 |
942 | set eventCount to count of eventList
943 | set limitCount to ${limit}
944 |
945 | if eventCount < limitCount then
946 | set limitCount to eventCount
947 | end if
948 |
949 | repeat with i from 1 to limitCount
950 | set theEvent to item i of eventList
951 | set eventData to {subject:subject of theEvent, ¬
952 | start:start time of theEvent, ¬
953 | end:end time of theEvent, ¬
954 | location:location of theEvent, ¬
955 | id:id of theEvent}
956 |
957 | set end of upcomingEvents to eventData
958 | end repeat
959 |
960 | return upcomingEvents
961 | end tell
962 | `;
963 |
964 | try {
965 | const result = await runAppleScript(script);
966 | console.error(`[getUpcomingEvents] Raw result length: ${result.length}`);
967 |
968 | // Parse the results
969 | const events = [];
970 | const matches = result.match(/\{([^}]+)\}/g);
971 |
972 | if (matches && matches.length > 0) {
973 | for (const match of matches) {
974 | try {
975 | const props = match.substring(1, match.length - 1).split(',');
976 | const event: any = {};
977 |
978 | props.forEach(prop => {
979 | const parts = prop.split(':');
980 | if (parts.length >= 2) {
981 | const key = parts[0].trim();
982 | const value = parts.slice(1).join(':').trim();
983 | event[key] = value;
984 | }
985 | });
986 |
987 | if (event.subject) {
988 | events.push({
989 | subject: event.subject,
990 | start: event.start,
991 | end: event.end,
992 | location: event.location || "No location",
993 | id: event.id
994 | });
995 | }
996 | } catch (parseError) {
997 | console.error('[getUpcomingEvents] Error parsing event match:', parseError);
998 | }
999 | }
1000 | }
1001 |
1002 | console.error(`[getUpcomingEvents] Found ${events.length} upcoming events`);
1003 | return events;
1004 | } catch (error) {
1005 | console.error("[getUpcomingEvents] Error getting upcoming events:", error);
1006 | throw error;
1007 | }
1008 | }
1009 |
1010 | // Function to search calendar events
1011 | async function searchEvents(searchTerm: string, limit: number = 10): Promise<any[]> {
1012 | console.error(`[searchEvents] Searching for events with term: "${searchTerm}", limit: ${limit}`);
1013 | await checkOutlookAccess();
1014 |
1015 | const script = `
1016 | tell application "Microsoft Outlook"
1017 | set searchResults to {}
1018 | set theCalendar to default calendar
1019 | set allEvents to events of theCalendar
1020 | set i to 0
1021 | set searchString to "${searchTerm.replace(/"/g, '\\"')}"
1022 |
1023 | repeat with theEvent in allEvents
1024 | if (subject of theEvent contains searchString) or (location of theEvent contains searchString) then
1025 | set i to i + 1
1026 | set eventData to {subject:subject of theEvent, ¬
1027 | start:start time of theEvent, ¬
1028 | end:end time of theEvent, ¬
1029 | location:location of theEvent, ¬
1030 | id:id of theEvent}
1031 |
1032 | set end of searchResults to eventData
1033 |
1034 | -- Stop if we've reached the limit
1035 | if i >= ${limit} then
1036 | exit repeat
1037 | end if
1038 | end if
1039 | end repeat
1040 |
1041 | return searchResults
1042 | end tell
1043 | `;
1044 |
1045 | try {
1046 | const result = await runAppleScript(script);
1047 | console.error(`[searchEvents] Raw result length: ${result.length}`);
1048 |
1049 | // Parse the results
1050 | const events = [];
1051 | const matches = result.match(/\{([^}]+)\}/g);
1052 |
1053 | if (matches && matches.length > 0) {
1054 | for (const match of matches) {
1055 | try {
1056 | const props = match.substring(1, match.length - 1).split(',');
1057 | const event: any = {};
1058 |
1059 | props.forEach(prop => {
1060 | const parts = prop.split(':');
1061 | if (parts.length >= 2) {
1062 | const key = parts[0].trim();
1063 | const value = parts.slice(1).join(':').trim();
1064 | event[key] = value;
1065 | }
1066 | });
1067 |
1068 | if (event.subject) {
1069 | events.push({
1070 | subject: event.subject,
1071 | start: event.start,
1072 | end: event.end,
1073 | location: event.location || "No location",
1074 | id: event.id
1075 | });
1076 | }
1077 | } catch (parseError) {
1078 | console.error('[searchEvents] Error parsing event match:', parseError);
1079 | }
1080 | }
1081 | }
1082 |
1083 | console.error(`[searchEvents] Found ${events.length} matching events`);
1084 | return events;
1085 | } catch (error) {
1086 | console.error("[searchEvents] Error searching events:", error);
1087 | throw error;
1088 | }
1089 | }
1090 |
1091 | // Function to create a calendar event
1092 | async function createEvent(subject: string, start: string, end: string, location?: string, body?: string, attendees?: string): Promise<string> {
1093 | console.error(`[createEvent] Creating event: "${subject}", start: ${start}, end: ${end}`);
1094 | await checkOutlookAccess();
1095 |
1096 | // Parse the ISO date strings to a format AppleScript can understand
1097 | const startDate = new Date(start);
1098 | const endDate = new Date(end);
1099 |
1100 | // Format for AppleScript (month/day/year hour:minute:second)
1101 | const formattedStart = `date "${startDate.getMonth() + 1}/${startDate.getDate()}/${startDate.getFullYear()} ${startDate.getHours()}:${startDate.getMinutes()}:${startDate.getSeconds()}"`;
1102 | const formattedEnd = `date "${endDate.getMonth() + 1}/${endDate.getDate()}/${endDate.getFullYear()} ${endDate.getHours()}:${endDate.getMinutes()}:${endDate.getSeconds()}"`;
1103 |
1104 | // Escape strings for AppleScript
1105 | const escapedSubject = subject.replace(/"/g, '\\"');
1106 | const escapedLocation = location ? location.replace(/"/g, '\\"') : "";
1107 | const escapedBody = body ? body.replace(/"/g, '\\"') : "";
1108 |
1109 | let script = `
1110 | tell application "Microsoft Outlook"
1111 | set theCalendar to default calendar
1112 | set newEvent to make new calendar event at theCalendar with properties {subject:"${escapedSubject}", start time:${formattedStart}, end time:${formattedEnd}
1113 | `;
1114 |
1115 | if (location) {
1116 | script += `, location:"${escapedLocation}"`;
1117 | }
1118 |
1119 | if (body) {
1120 | script += `, content:"${escapedBody}"`;
1121 | }
1122 |
1123 | script += `}
1124 | `;
1125 |
1126 | // Add attendees if provided
1127 | if (attendees) {
1128 | const attendeeList = attendees.split(',').map(email => email.trim());
1129 |
1130 | for (const attendee of attendeeList) {
1131 | const escapedAttendee = attendee.replace(/"/g, '\\"');
1132 | script += `
1133 | make new attendee at newEvent with properties {email address:"${escapedAttendee}"}
1134 | `;
1135 | }
1136 | }
1137 |
1138 | script += `
1139 | save newEvent
1140 | return "Event created successfully"
1141 | end tell
1142 | `;
1143 |
1144 | try {
1145 | const result = await runAppleScript(script);
1146 | console.error(`[createEvent] Result: ${result}`);
1147 | return result;
1148 | } catch (error) {
1149 | console.error("[createEvent] Error creating event:", error);
1150 | throw error;
1151 | }
1152 | }
1153 |
1154 | // ====================================================
1155 | // 6. CONTACTS FUNCTIONS
1156 | // ====================================================
1157 |
1158 | // Function to list contacts with improved AppleScript syntax
1159 | async function listContacts(limit: number = 20): Promise<any[]> {
1160 | console.error(`[listContacts] Listing contacts, limit: ${limit}`);
1161 | await checkOutlookAccess();
1162 |
1163 | const script = `
1164 | tell application "Microsoft Outlook"
1165 | set contactList to {}
1166 | set allContactsList to contacts
1167 | set contactCount to count of allContactsList
1168 | set limitCount to ${limit}
1169 |
1170 | if contactCount < limitCount then
1171 | set limitCount to contactCount
1172 | end if
1173 |
1174 | repeat with i from 1 to limitCount
1175 | try
1176 | set theContact to item i of allContactsList
1177 | set contactName to full name of theContact
1178 |
1179 | -- Create a basic object with name
1180 | set contactData to {name:contactName}
1181 |
1182 | -- Try to get email
1183 | try
1184 | set emailList to email addresses of theContact
1185 | if (count of emailList) > 0 then
1186 | set emailAddr to address of item 1 of emailList
1187 | set contactData to contactData & {email:emailAddr}
1188 | else
1189 | set contactData to contactData & {email:"No email"}
1190 | end if
1191 | on error
1192 | set contactData to contactData & {email:"No email"}
1193 | end try
1194 |
1195 | -- Try to get phone
1196 | try
1197 | set phoneList to phones of theContact
1198 | if (count of phoneList) > 0 then
1199 | set phoneNum to formatted dial string of item 1 of phoneList
1200 | set contactData to contactData & {phone:phoneNum}
1201 | else
1202 | set contactData to contactData & {phone:"No phone"}
1203 | end if
1204 | on error
1205 | set contactData to contactData & {phone:"No phone"}
1206 | end try
1207 |
1208 | set end of contactList to contactData
1209 | on error
1210 | -- Skip contacts that can't be processed
1211 | end try
1212 | end repeat
1213 |
1214 | return contactList
1215 | end tell
1216 | `;
1217 |
1218 | try {
1219 | const result = await runAppleScript(script);
1220 | console.error(`[listContacts] Raw result length: ${result.length}`);
1221 |
1222 | // Parse the results
1223 | const contacts = [];
1224 | const matches = result.match(/\{([^}]+)\}/g);
1225 |
1226 | if (matches && matches.length > 0) {
1227 | for (const match of matches) {
1228 | try {
1229 | const props = match.substring(1, match.length - 1).split(',');
1230 | const contact: any = {};
1231 |
1232 | props.forEach(prop => {
1233 | const parts = prop.split(':');
1234 | if (parts.length >= 2) {
1235 | const key = parts[0].trim();
1236 | const value = parts.slice(1).join(':').trim();
1237 | contact[key] = value;
1238 | }
1239 | });
1240 |
1241 | if (contact.name) {
1242 | contacts.push({
1243 | name: contact.name,
1244 | email: contact.email || "No email",
1245 | phone: contact.phone || "No phone"
1246 | });
1247 | }
1248 | } catch (parseError) {
1249 | console.error('[listContacts] Error parsing contact match:', parseError);
1250 | }
1251 | }
1252 | }
1253 |
1254 | console.error(`[listContacts] Found ${contacts.length} contacts`);
1255 | return contacts;
1256 | } catch (error) {
1257 | console.error("[listContacts] Error listing contacts:", error);
1258 |
1259 | // Try an alternative approach using a simpler script
1260 | try {
1261 | const alternativeScript = `
1262 | tell application "Microsoft Outlook"
1263 | set contactList to {}
1264 | set contactCount to count of contacts
1265 | set limitCount to ${limit}
1266 |
1267 | if contactCount < limitCount then
1268 | set limitCount to contactCount
1269 | end if
1270 |
1271 | repeat with i from 1 to limitCount
1272 | try
1273 | set theContact to item i of contacts
1274 | set contactName to full name of theContact
1275 | set end of contactList to contactName
1276 | end try
1277 | end repeat
1278 |
1279 | return contactList
1280 | end tell
1281 | `;
1282 |
1283 | const result = await runAppleScript(alternativeScript);
1284 |
1285 | // Parse the simpler result format (just names)
1286 | const simplifiedContacts = result.split(", ").map(name => ({
1287 | name: name,
1288 | email: "Not available with simplified method",
1289 | phone: "Not available with simplified method"
1290 | }));
1291 |
1292 | console.error(`[listContacts] Found ${simplifiedContacts.length} contacts using alternative method`);
1293 | return simplifiedContacts;
1294 | } catch (altError) {
1295 | console.error("[listContacts] Alternative method also failed:", altError);
1296 | throw new Error(`Error accessing contacts. The error might be related to Outlook permissions or configuration: ${error instanceof Error ? error.message : String(error)}`);
1297 | }
1298 | }
1299 | }
1300 |
1301 | // Function to search contacts
1302 | // Function to search contacts with improved AppleScript syntax
1303 | async function searchContacts(searchTerm: string, limit: number = 10): Promise<any[]> {
1304 | console.error(`[searchContacts] Searching for contacts with term: "${searchTerm}", limit: ${limit}`);
1305 | await checkOutlookAccess();
1306 |
1307 | const script = `
1308 | tell application "Microsoft Outlook"
1309 | set searchResults to {}
1310 | set allContacts to contacts
1311 | set i to 0
1312 | set searchString to "${searchTerm.replace(/"/g, '\\"')}"
1313 |
1314 | repeat with theContact in allContacts
1315 | try
1316 | set contactName to full name of theContact
1317 |
1318 | if contactName contains searchString then
1319 | set i to i + 1
1320 |
1321 | -- Create basic contact info
1322 | set contactData to {name:contactName}
1323 |
1324 | -- Try to get email
1325 | try
1326 | set emailList to email addresses of theContact
1327 | if (count of emailList) > 0 then
1328 | set emailAddr to address of item 1 of emailList
1329 | set contactData to contactData & {email:emailAddr}
1330 | else
1331 | set contactData to contactData & {email:"No email"}
1332 | end if
1333 | on error
1334 | set contactData to contactData & {email:"No email"}
1335 | end try
1336 |
1337 | -- Try to get phone
1338 | try
1339 | set phoneList to phones of theContact
1340 | if (count of phoneList) > 0 then
1341 | set phoneNum to formatted dial string of item 1 of phoneList
1342 | set contactData to contactData & {phone:phoneNum}
1343 | else
1344 | set contactData to contactData & {phone:"No phone"}
1345 | end if
1346 | on error
1347 | set contactData to contactData & {phone:"No phone"}
1348 | end try
1349 |
1350 | set end of searchResults to contactData
1351 |
1352 | -- Stop if we've reached the limit
1353 | if i >= ${limit} then
1354 | exit repeat
1355 | end if
1356 | end if
1357 | on error
1358 | -- Skip contacts that can't be processed
1359 | end try
1360 | end repeat
1361 |
1362 | return searchResults
1363 | end tell
1364 | `;
1365 |
1366 | try {
1367 | const result = await runAppleScript(script);
1368 | console.error(`[searchContacts] Raw result length: ${result.length}`);
1369 |
1370 | // Parse the results
1371 | const contacts = [];
1372 | const matches = result.match(/\{([^}]+)\}/g);
1373 |
1374 | if (matches && matches.length > 0) {
1375 | for (const match of matches) {
1376 | try {
1377 | const props = match.substring(1, match.length - 1).split(',');
1378 | const contact: any = {};
1379 |
1380 | props.forEach(prop => {
1381 | const parts = prop.split(':');
1382 | if (parts.length >= 2) {
1383 | const key = parts[0].trim();
1384 | const value = parts.slice(1).join(':').trim();
1385 | contact[key] = value;
1386 | }
1387 | });
1388 |
1389 | if (contact.name) {
1390 | contacts.push({
1391 | name: contact.name,
1392 | email: contact.email || "No email",
1393 | phone: contact.phone || "No phone"
1394 | });
1395 | }
1396 | } catch (parseError) {
1397 | console.error('[searchContacts] Error parsing contact match:', parseError);
1398 | }
1399 | }
1400 | }
1401 |
1402 | console.error(`[searchContacts] Found ${contacts.length} matching contacts`);
1403 | return contacts;
1404 | } catch (error) {
1405 | console.error("[searchContacts] Error searching contacts:", error);
1406 |
1407 | // Try an alternative approach with a simpler script that just returns names
1408 | try {
1409 | const alternativeScript = `
1410 | tell application "Microsoft Outlook"
1411 | set matchingContacts to {}
1412 | set searchString to "${searchTerm.replace(/"/g, '\\"')}"
1413 | set i to 0
1414 |
1415 | repeat with theContact in contacts
1416 | try
1417 | set contactName to full name of theContact
1418 | if contactName contains searchString then
1419 | set i to i + 1
1420 | set end of matchingContacts to contactName
1421 | if i >= ${limit} then exit repeat
1422 | end if
1423 | end try
1424 | end repeat
1425 |
1426 | return matchingContacts
1427 | end tell
1428 | `;
1429 |
1430 | const result = await runAppleScript(alternativeScript);
1431 |
1432 | // Parse the simpler result format (just names)
1433 | const simplifiedContacts = result.split(", ").map(name => ({
1434 | name: name,
1435 | email: "Not available with simplified method",
1436 | phone: "Not available with simplified method"
1437 | }));
1438 |
1439 | console.error(`[searchContacts] Found ${simplifiedContacts.length} contacts using alternative method`);
1440 | return simplifiedContacts;
1441 | } catch (altError) {
1442 | console.error("[searchContacts] Alternative method also failed:", altError);
1443 | throw new Error(`Error searching contacts. The error might be related to Outlook permissions or configuration: ${error instanceof Error ? error.message : String(error)}`);
1444 | }
1445 | }
1446 | }
1447 |
1448 | // ====================================================
1449 | // 7. TYPE GUARDS
1450 | // ====================================================
1451 |
1452 | // Type guards for arguments
1453 | function isMailArgs(args: unknown): args is {
1454 | operation: "unread" | "search" | "send" | "folders" | "read";
1455 | folder?: string;
1456 | limit?: number;
1457 | searchTerm?: string;
1458 | to?: string;
1459 | subject?: string;
1460 | body?: string;
1461 | isHtml?: boolean;
1462 | cc?: string;
1463 | bcc?: string;
1464 | attachments?: string[];
1465 | } {
1466 | if (typeof args !== "object" || args === null) return false;
1467 |
1468 | const { operation } = args as any;
1469 |
1470 | if (!operation || !["unread", "search", "send", "folders", "read"].includes(operation)) {
1471 | return false;
1472 | }
1473 |
1474 | // Check required fields based on operation
1475 | switch (operation) {
1476 | case "search":
1477 | if (!(args as any).searchTerm) return false;
1478 | break;
1479 | case "send":
1480 | if (!(args as any).to || !(args as any).subject || !(args as any).body) return false;
1481 | break;
1482 | }
1483 |
1484 | return true;
1485 | }
1486 |
1487 | function isCalendarArgs(args: unknown): args is {
1488 | operation: "today" | "upcoming" | "search" | "create";
1489 | searchTerm?: string;
1490 | limit?: number;
1491 | days?: number;
1492 | subject?: string;
1493 | start?: string;
1494 | end?: string;
1495 | location?: string;
1496 | body?: string;
1497 | attendees?: string;
1498 | } {
1499 | if (typeof args !== "object" || args === null) return false;
1500 |
1501 | const { operation } = args as any;
1502 |
1503 | if (!operation || !["today", "upcoming", "search", "create"].includes(operation)) {
1504 | return false;
1505 | }
1506 |
1507 | // Check required fields based on operation
1508 | switch (operation) {
1509 | case "search":
1510 | if (!(args as any).searchTerm) return false;
1511 | break;
1512 | case "create":
1513 | if (!(args as any).subject || !(args as any).start || !(args as any).end) return false;
1514 | break;
1515 | }
1516 |
1517 | return true;
1518 | }
1519 |
1520 | function isContactsArgs(args: unknown): args is {
1521 | operation: "list" | "search";
1522 | searchTerm?: string;
1523 | limit?: number;
1524 | } {
1525 | if (typeof args !== "object" || args === null) return false;
1526 |
1527 | const { operation } = args as any;
1528 |
1529 | if (!operation || !["list", "search"].includes(operation)) {
1530 | return false;
1531 | }
1532 |
1533 | // Check required fields based on operation
1534 | if (operation === "search" && !(args as any).searchTerm) {
1535 | return false;
1536 | }
1537 |
1538 | return true;
1539 | }
1540 |
1541 | // ====================================================
1542 | // 8. MCP REQUEST HANDLERS
1543 | // ====================================================
1544 |
1545 | // Set up request handlers
1546 | server.setRequestHandler(ListToolsRequestSchema, async () => {
1547 | console.error("[ListToolsRequest] Returning available tools");
1548 | return {
1549 | tools: [OUTLOOK_MAIL_TOOL, OUTLOOK_CALENDAR_TOOL, OUTLOOK_CONTACTS_TOOL],
1550 | };
1551 | });
1552 |
1553 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
1554 | try {
1555 | const { name, arguments: args } = request.params;
1556 | console.error(`[CallToolRequest] Received request for tool: ${name}`);
1557 |
1558 | if (!args) {
1559 | throw new Error("No arguments provided");
1560 | }
1561 |
1562 | switch (name) {
1563 | case "outlook_mail": {
1564 | if (!isMailArgs(args)) {
1565 | throw new Error("Invalid arguments for outlook_mail tool");
1566 | }
1567 |
1568 | const { operation } = args;
1569 | console.error(`[CallToolRequest] Mail operation: ${operation}`);
1570 |
1571 | switch (operation) {
1572 | case "unread": {
1573 | const emails = await getUnreadEmails(args.folder, args.limit);
1574 | return {
1575 | content: [{
1576 | type: "text",
1577 | text: emails.length > 0 ?
1578 | `Found ${emails.length} unread email(s)${args.folder ? ` in folder "${args.folder}"` : ''}\n\n` +
1579 | emails.map(email =>
1580 | `[${email.dateSent}] From: ${email.sender}\nSubject: ${email.subject}\n${email.content.substring(0, 200)}${email.content.length > 200 ? '...' : ''}`
1581 | ).join("\n\n") :
1582 | `No unread emails found${args.folder ? ` in folder "${args.folder}"` : ''}`
1583 | }],
1584 | isError: false
1585 | };
1586 | }
1587 |
1588 | case "search": {
1589 | if (!args.searchTerm) {
1590 | throw new Error("Search term is required for search operation");
1591 | }
1592 | const emails = await searchEmails(args.searchTerm, args.folder, args.limit);
1593 | return {
1594 | content: [{
1595 | type: "text",
1596 | text: emails.length > 0 ?
1597 | `Found ${emails.length} email(s) for "${args.searchTerm}"${args.folder ? ` in folder "${args.folder}"` : ''}\n\n` +
1598 | emails.map(email =>
1599 | `[${email.dateSent}] From: ${email.sender}\nSubject: ${email.subject}\n${email.content.substring(0, 200)}${email.content.length > 200 ? '...' : ''}`
1600 | ).join("\n\n") :
1601 | `No emails found for "${args.searchTerm}"${args.folder ? ` in folder "${args.folder}"` : ''}`
1602 | }],
1603 | isError: false
1604 | };
1605 | }
1606 |
1607 | // Update the handler in CallToolRequestSchema
1608 | case "send": {
1609 | if (!args.to || !args.subject || !args.body) {
1610 | throw new Error("Recipient (to), subject, and body are required for send operation");
1611 | }
1612 |
1613 | // Validate attachments if provided
1614 | if (args.attachments && !Array.isArray(args.attachments)) {
1615 | throw new Error("Attachments must be an array of file paths");
1616 | }
1617 |
1618 | // Log attachment information for debugging
1619 | console.error(`[CallTool] Send email with attachments: ${args.attachments ? JSON.stringify(args.attachments) : 'none'}`);
1620 |
1621 | const result = await sendEmail(
1622 | args.to,
1623 | args.subject,
1624 | args.body,
1625 | args.cc,
1626 | args.bcc,
1627 | args.isHtml || false,
1628 | args.attachments
1629 | );
1630 |
1631 | return {
1632 | content: [{ type: "text", text: result }],
1633 | isError: false
1634 | };
1635 | }
1636 |
1637 | case "folders": {
1638 | const folders = await getMailFolders();
1639 | return {
1640 | content: [{
1641 | type: "text",
1642 | text: folders.length > 0 ?
1643 | `Found ${folders.length} mail folders:\n\n${folders.join("\n")}` :
1644 | "No mail folders found. Make sure Outlook is running and properly configured."
1645 | }],
1646 | isError: false
1647 | };
1648 | }
1649 |
1650 | case "read": {
1651 | const emails = await readEmails(args.folder, args.limit);
1652 | return {
1653 | content: [{
1654 | type: "text",
1655 | text: emails.length > 0 ?
1656 | `Found ${emails.length} email(s)${args.folder ? ` in folder "${args.folder}"` : ''}\n\n` +
1657 | emails.map(email =>
1658 | `[${email.dateSent}] From: ${email.sender}\nSubject: ${email.subject}\n${email.content.substring(0, 200)}${email.content.length > 200 ? '...' : ''}`
1659 | ).join("\n\n") :
1660 | `No emails found${args.folder ? ` in folder "${args.folder}"` : ''}`
1661 | }],
1662 | isError: false
1663 | };
1664 | }
1665 |
1666 | default:
1667 | throw new Error(`Unknown mail operation: ${operation}`);
1668 | }
1669 | }
1670 |
1671 | case "outlook_calendar": {
1672 | if (!isCalendarArgs(args)) {
1673 | throw new Error("Invalid arguments for outlook_calendar tool");
1674 | }
1675 |
1676 | const { operation } = args;
1677 | console.error(`[CallToolRequest] Calendar operation: ${operation}`);
1678 |
1679 | switch (operation) {
1680 | case "today": {
1681 | const events = await getTodayEvents(args.limit);
1682 | return {
1683 | content: [{
1684 | type: "text",
1685 | text: events.length > 0 ?
1686 | `Found ${events.length} event(s) for today:\n\n` +
1687 | events.map(event =>
1688 | `${event.subject}\nTime: ${event.start} - ${event.end}\nLocation: ${event.location}`
1689 | ).join("\n\n") :
1690 | "No events found for today"
1691 | }],
1692 | isError: false
1693 | };
1694 | }
1695 |
1696 | case "upcoming": {
1697 | const days = args.days || 7;
1698 | const events = await getUpcomingEvents(days, args.limit);
1699 | return {
1700 | content: [{
1701 | type: "text",
1702 | text: events.length > 0 ?
1703 | `Found ${events.length} upcoming event(s) for the next ${days} days:\n\n` +
1704 | events.map(event =>
1705 | `${event.subject}\nTime: ${event.start} - ${event.end}\nLocation: ${event.location}`
1706 | ).join("\n\n") :
1707 | `No upcoming events found for the next ${days} days`
1708 | }],
1709 | isError: false
1710 | };
1711 | }
1712 |
1713 | case "search": {
1714 | if (!args.searchTerm) {
1715 | throw new Error("Search term is required for search operation");
1716 | }
1717 | const events = await searchEvents(args.searchTerm, args.limit);
1718 | return {
1719 | content: [{
1720 | type: "text",
1721 | text: events.length > 0 ?
1722 | `Found ${events.length} event(s) matching "${args.searchTerm}":\n\n` +
1723 | events.map(event =>
1724 | `${event.subject}\nTime: ${event.start} - ${event.end}\nLocation: ${event.location}`
1725 | ).join("\n\n") :
1726 | `No events found matching "${args.searchTerm}"`
1727 | }],
1728 | isError: false
1729 | };
1730 | }
1731 |
1732 | case "create": {
1733 | if (!args.subject || !args.start || !args.end) {
1734 | throw new Error("Subject, start time, and end time are required for create operation");
1735 | }
1736 | const result = await createEvent(args.subject, args.start, args.end, args.location, args.body, args.attendees);
1737 | return {
1738 | content: [{ type: "text", text: result }],
1739 | isError: false
1740 | };
1741 | }
1742 |
1743 | default:
1744 | throw new Error(`Unknown calendar operation: ${operation}`);
1745 | }
1746 | }
1747 |
1748 | case "outlook_contacts": {
1749 | if (!isContactsArgs(args)) {
1750 | throw new Error("Invalid arguments for outlook_contacts tool");
1751 | }
1752 |
1753 | const { operation } = args;
1754 | console.error(`[CallToolRequest] Contacts operation: ${operation}`);
1755 |
1756 | switch (operation) {
1757 | case "list": {
1758 | const contacts = await listContacts(args.limit);
1759 | return {
1760 | content: [{
1761 | type: "text",
1762 | text: contacts.length > 0 ?
1763 | `Found ${contacts.length} contact(s):\n\n` +
1764 | contacts.map(contact =>
1765 | `Name: ${contact.name}\nEmail: ${contact.email}\nPhone: ${contact.phone}`
1766 | ).join("\n\n") :
1767 | "No contacts found"
1768 | }],
1769 | isError: false
1770 | };
1771 | }
1772 |
1773 | case "search": {
1774 | if (!args.searchTerm) {
1775 | throw new Error("Search term is required for search operation");
1776 | }
1777 | const contacts = await searchContacts(args.searchTerm, args.limit);
1778 | return {
1779 | content: [{
1780 | type: "text",
1781 | text: contacts.length > 0 ?
1782 | `Found ${contacts.length} contact(s) matching "${args.searchTerm}":\n\n` +
1783 | contacts.map(contact =>
1784 | `Name: ${contact.name}\nEmail: ${contact.email}\nPhone: ${contact.phone}`
1785 | ).join("\n\n") :
1786 | `No contacts found matching "${args.searchTerm}"`
1787 | }],
1788 | isError: false
1789 | };
1790 | }
1791 |
1792 | default:
1793 | throw new Error(`Unknown contacts operation: ${operation}`);
1794 | }
1795 | }
1796 |
1797 | default:
1798 | return {
1799 | content: [{ type: "text", text: `Unknown tool: ${name}` }],
1800 | isError: true,
1801 | };
1802 | }
1803 | } catch (error) {
1804 | console.error("[CallToolRequest] Error:", error);
1805 | return {
1806 | content: [
1807 | {
1808 | type: "text",
1809 | text: `Error: ${error instanceof Error ? error.message : String(error)}`,
1810 | },
1811 | ],
1812 | isError: true,
1813 | };
1814 | }
1815 | });
1816 |
1817 | // ====================================================
1818 | // 9. START SERVER
1819 | // ====================================================
1820 |
1821 | // Start the MCP server
1822 | console.error("Initializing Outlook MCP server transport...");
1823 | const transport = new StdioServerTransport();
1824 |
1825 | (async () => {
1826 | try {
1827 | console.error("Connecting to transport...");
1828 | await server.connect(transport);
1829 | console.error("Outlook MCP Server running on stdio");
1830 | } catch (error) {
1831 | console.error("Failed to initialize MCP server:", error);
1832 | process.exit(1);
1833 | }
1834 | })();
```