Skip to main content
Glama
outlook-manager.ts39.5 kB
import { spawn } from 'child_process'; export interface EmailMessage { id: string; storeId?: string; subject: string; sender: string; recipients: string[]; body: string; receivedTime: Date; isRead: boolean; hasAttachments: boolean; } export interface EmailDraft { to: string[]; cc?: string[]; bcc?: string[]; subject: string; body: string; isHtml?: boolean; } export class OutlookManager { private powershellPath: string; constructor() { this.powershellPath = 'powershell.exe'; } private async executePowerShell(script: string): Promise<string> { return new Promise((resolve, reject) => { // Prepare UTF-8 encoded script const utf8Script = ` chcp 65001 > $null [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::InputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8 ${script} `; const ps = spawn(this.powershellPath, [ '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', utf8Script ], { env: { ...process.env, 'PYTHONIOENCODING': 'utf-8' } }); let stdout = ''; let stderr = ''; ps.stdout.setEncoding('utf8'); ps.stderr.setEncoding('utf8'); ps.stdout.on('data', (data) => { stdout += data; }); ps.stderr.on('data', (data) => { stderr += data; }); ps.on('close', (code) => { if (code === 0) { // Clean the output let cleanOutput = stdout .replace(/^\uFEFF/, '') // Remove BOM .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control characters .trim(); resolve(cleanOutput); } else { reject(new Error(`PowerShell failed (code ${code}): ${stderr}`)); } }); }); } /** * Common email retrieval function */ private async getEmailsFromFolder(folderType: number, count: number = 10, sortBy: string = "[ReceivedTime]"): Promise<EmailMessage[]> { const script = ` try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $namespace = $outlook.GetNamespace("MAPI") $folder = $namespace.GetDefaultFolder(${folderType}) if ($folder.Items.Count -eq 0) { Write-Output "[]" exit 0 } $items = $folder.Items $items.Sort("${sortBy}", $true) $emails = @() $counter = 0 foreach ($item in $items) { if ($counter -ge ${count}) { break } try { $subject = if ($item.Subject) { $item.Subject.ToString() -replace '[\\x00-\\x1F\\x7F]', '' } else { "No Subject" } $sender = if ($item.SenderEmailAddress) { $item.SenderEmailAddress.ToString() -replace '[\\x00-\\x1F\\x7F]', '' } else { "Unknown" } $body = if ($item.Body) { $bodyStr = $item.Body.ToString() -replace '[\\x00-\\x1F\\x7F]', '' if ($bodyStr.Length -gt 150) { $bodyStr.Substring(0, 150) + "..." } else { $bodyStr } } else { "" } $timeStamp = if ($item.SentOn -and ${folderType} -eq 5) { $item.SentOn.ToString("yyyy-MM-dd HH:mm:ss") } elseif ($item.ReceivedTime) { $item.ReceivedTime.ToString("yyyy-MM-dd HH:mm:ss") } else { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") } $emails += [PSCustomObject]@{ Id = if ($item.EntryID) { $item.EntryID.ToString() } else { "no-id-$counter" } StoreID = if ($item.Session -and $item.Session.DefaultStore -and $item.Session.DefaultStore.StoreID) { $item.Session.DefaultStore.StoreID.ToString() } elseif ($item.Parent -and $item.Parent.StoreID) { $item.Parent.StoreID.ToString() } else { try { $namespace.DefaultStore.StoreID.ToString() } catch { "" } } Subject = $subject Sender = $sender Recipients = @() Body = $body ReceivedTime = $timeStamp IsRead = if (${folderType} -eq 5) { $true } else { -not $item.UnRead } HasAttachments = $item.Attachments.Count -gt 0 } $counter++ } catch { $counter++; continue } } if ($emails.Count -eq 0) { Write-Output "[]" } else { Write-Output ($emails | ConvertTo-Json -Depth 2 -Compress) } } catch { Write-Output ([PSCustomObject]@{ error = $_.Exception.Message; type = "OutlookConnectionError" } | ConvertTo-Json -Compress) } `; try { const result = await this.executePowerShell(script); if (!result || result.trim() === '') return []; const cleanResult = result.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '').trim(); const parsed = JSON.parse(cleanResult); if (parsed.error) throw new Error(`Outlook Error: ${parsed.error}`); const emailArray = Array.isArray(parsed) ? parsed : [parsed]; return emailArray.map((item: any) => ({ id: this.cleanText(item.Id || ''), storeId: this.cleanText(item.StoreID || ''), subject: this.cleanText(item.Subject || 'No Subject'), sender: this.cleanText(item.Sender || 'Unknown'), recipients: [], body: this.cleanText(item.Body || ''), receivedTime: new Date(item.ReceivedTime), isRead: Boolean(item.IsRead), hasAttachments: Boolean(item.HasAttachments) })); } catch (error) { console.error('Email fetch failed:', error); return [{ id: 'fallback-1', storeId: '', subject: 'Email content unavailable', sender: 'system@outlook.com', recipients: [], body: 'Unable to retrieve email content.', receivedTime: new Date(), isRead: true, hasAttachments: false }]; } } private cleanText(text: string): string { if (!text) return ''; return text .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control chars .replace(/\r\n/g, ' ') // Replace CRLF with space .replace(/[\r\n]/g, ' ') // Replace any remaining line breaks .replace(/\s+/g, ' ') // Collapse multiple spaces .trim(); } private formatBodyForOutlook(body: string): string { if (!body) return ''; // Normalize line breaks to Windows format (CRLF) for Outlook return body .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control chars except CR and LF .replace(/\r\n/g, '\n') // Normalize to LF first .replace(/\r/g, '\n') // Convert any remaining CR to LF .replace(/\n/g, '\r\n') // Convert all LF to CRLF for Windows .trim(); } async getInboxEmails(count: number = 10): Promise<EmailMessage[]> { return this.getEmailsFromFolder(6, count, "[ReceivedTime]"); // 6 = Inbox } async getSentEmails(count: number = 10): Promise<EmailMessage[]> { return this.getEmailsFromFolder(5, count, "[SentOn]"); // 5 = Sent Items } async getDraftEmails(count: number = 10): Promise<EmailMessage[]> { return this.getEmailsFromFolder(16, count, "[LastModificationTime]"); // 16 = Drafts } async getEmailById(id: string): Promise<EmailMessage> { const script = ` try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $namespace = $outlook.GetNamespace("MAPI") # Try GetItemFromID first (fastest method) $item = $null try { $item = $namespace.GetItemFromID("${id.replace(/"/g, '""')}") } catch { # Fallback: search through folders foreach ($folderNum in @(6, 5, 16)) { $folder = $namespace.GetDefaultFolder($folderNum) foreach ($email in $folder.Items) { if ($email.EntryID -eq "${id.replace(/"/g, '""')}") { $item = $email break } } if ($item) { break } } } if (-not $item) { throw "Email not found" } # Extract data $subject = if ($item.Subject) { $item.Subject } else { "No Subject" } $sender = if ($item.SenderEmailAddress) { $item.SenderEmailAddress } else { "Unknown" } $body = if ($item.Body) { $item.Body } else { "" } $recipients = @() if ($item.Recipients) { foreach ($r in $item.Recipients) { $addr = if ($r.Address) { $r.Address } else { $r.Name } if ($addr) { $recipients += $addr } } } $timestamp = if ($item.SentOn) { $item.SentOn.ToString("yyyy-MM-dd HH:mm:ss") } elseif ($item.ReceivedTime) { $item.ReceivedTime.ToString("yyyy-MM-dd HH:mm:ss") } else { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") } Write-Output ([PSCustomObject]@{ Id = "${id.replace(/"/g, '""')}" Subject = $subject Sender = $sender Recipients = $recipients Body = $body ReceivedTime = $timestamp IsRead = -not $item.UnRead HasAttachments = $item.Attachments.Count -gt 0 Success = $true } | ConvertTo-Json -Depth 3 -Compress) } catch { Write-Output ([PSCustomObject]@{ Id = "${id.replace(/"/g, '""')}" Subject = "Email not found" Sender = "system" Recipients = @() Body = "Error: $($_.Exception.Message)" ReceivedTime = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") IsRead = $true HasAttachments = $false Success = $false } | ConvertTo-Json -Depth 3 -Compress) } `; try { const result = await this.executePowerShell(script); const cleanResult = result.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '').trim(); const emailData = JSON.parse(cleanResult); return { id: emailData.Id || id, subject: this.cleanText(emailData.Subject || 'No Subject'), sender: this.cleanText(emailData.Sender || 'Unknown Sender'), recipients: Array.isArray(emailData.Recipients) ? emailData.Recipients.map((r: any) => this.cleanText(r)) : [], body: emailData.Body || '', receivedTime: new Date(emailData.ReceivedTime || new Date()), isRead: Boolean(emailData.IsRead), hasAttachments: Boolean(emailData.HasAttachments) }; } catch (error) { return { id: id, subject: 'Email parsing failed', sender: 'system', recipients: [], body: `Failed to parse email: ${error instanceof Error ? error.message : String(error)}`, receivedTime: new Date(), isRead: true, hasAttachments: false }; } } async createDraft(draft: EmailDraft): Promise<string> { const cleanSubject = this.cleanText(draft.subject); // Don't clean the body for drafts - preserve line breaks const formattedBody = this.formatBodyForOutlook(draft.body); const script = ` try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" $outlook = New-Object -ComObject Outlook.Application $mail = $outlook.CreateItem(0) $mail.Subject = "${cleanSubject.replace(/"/g, '""')}" $mail.Body = "${formattedBody.replace(/"/g, '""')}" foreach ($recipient in @("${draft.to.join('","')}")) { if ($recipient.Trim()) { $mail.Recipients.Add($recipient.Trim()) | Out-Null } } $mail.Recipients.ResolveAll() | Out-Null $mail.Save() Write-Output "success" } catch { Write-Output "error: $($_.Exception.Message)" } `; const result = await this.executePowerShell(script); if (result.startsWith('error:')) { throw new Error(result.substring(7)); } return 'Draft created successfully'; } async markAsRead(id: string): Promise<void> { return Promise.resolve(); } async searchInboxEmails(query: string, count: number = 10): Promise<EmailMessage[]> { const emails = await this.getInboxEmails(Math.min(count * 2, 50)); const { EmailSummarizer } = await import('./email-summarizer.js'); const searchResults = EmailSummarizer.searchEmails(emails, query); return searchResults.slice(0, count); } async searchSentEmails(query: string, count: number = 10): Promise<EmailMessage[]> { const emails = await this.getSentEmails(Math.min(count * 2, 50)); const { EmailSummarizer } = await import('./email-summarizer.js'); const searchResults = EmailSummarizer.searchEmails(emails, query); return searchResults.slice(0, count); } async searchDraftEmails(query: string, count: number = 10): Promise<EmailMessage[]> { const emails = await this.getDraftEmails(Math.min(count * 2, 50)); const { EmailSummarizer } = await import('./email-summarizer.js'); const searchResults = EmailSummarizer.searchEmails(emails, query); return searchResults.slice(0, count); } /** * Duplicate an existing email to create a new draft * Uses ReplyAll method to preserve complete formatting */ async duplicateEmailAsDraft(sourceEmailId: string, newSubject?: string, newRecipients?: string[], storeId?: string): Promise<string> { const script = ` try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $namespace = $outlook.GetNamespace("MAPI") # Find the original email using EntryID and StoreID $item = $null $sourceEntryID = "${sourceEmailId.replace(/"/g, '""')}" $sourceStoreID = "${(storeId || '').replace(/"/g, '""')}" # Try GetItemFromID with StoreID first, then fallback methods try { if ($sourceStoreID -and $sourceStoreID.Length -gt 0) { $item = $namespace.GetItemFromID($sourceEntryID, $sourceStoreID) } else { $item = $namespace.GetItemFromID($sourceEntryID) } } catch { # Fallback: search through folders $folders = @( $namespace.GetDefaultFolder(6), # Inbox $namespace.GetDefaultFolder(5), # Sent Items $namespace.GetDefaultFolder(16) # Drafts ) foreach ($folder in $folders) { try { foreach ($email in $folder.Items) { if ($email.EntryID -eq $sourceEntryID) { $item = $email break } } if ($item) { break } } catch { continue } } } if (-not $item) { throw "Original email not found with EntryID: $sourceEntryID" } # Use ReplyAll to preserve all formatting, then modify $draft = $item.ReplyAll() # Update subject if provided $subjectToUse = "${(newSubject || '').replace(/"/g, '""')}" if ($subjectToUse.Length -gt 0) { $draft.Subject = $subjectToUse } # Clear and set recipients if provided if ("${(newRecipients || []).join(',')}" -ne "") { $draft.Recipients.RemoveAll() $newRecipientsList = @("${(newRecipients || []).join('","')}") foreach ($recipient in $newRecipientsList) { if ($recipient.Trim().Length -gt 0) { $draft.Recipients.Add($recipient.Trim()) | Out-Null } } } # Resolve recipients and save try { $draft.Recipients.ResolveAll() | Out-Null } catch { } $draft.Save() Write-Output "success" } catch { Write-Output "error: $($_.Exception.Message)" } `; const result = await this.executePowerShell(script); if (result.startsWith('error:')) { throw new Error(result.substring(7)); } return 'Draft created successfully using ReplyAll method'; } /** * Set Show As (Free/Busy status) for calendar events */ async setShowAs(options: { eventId?: string; subject?: string; startDate?: Date; showAs: 'Free' | 'Tentative' | 'Busy' | 'OutOfOffice' | 'WorkingElsewhere'; }): Promise<{ success: boolean; message: string }> { try { // Map ShowAs values to Outlook constants const showAsMap: Record<string, number> = { 'Free': 0, // olFree 'Tentative': 1, // olTentative 'Busy': 2, // olBusy 'OutOfOffice': 3, // olOutOfOffice 'WorkingElsewhere': 4 // olWorkingElsewhere }; const busyStatus = showAsMap[options.showAs]; const eventId = options.eventId ? `"${options.eventId.replace(/"/g, '""')}"` : 'null'; const subject = options.subject ? `"${options.subject.replace(/"/g, '""')}"` : 'null'; const startDate = options.startDate ? `"${options.startDate.toISOString()}"` : 'null'; const script = ` try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $namespace = $outlook.GetNamespace("MAPI") $calendar = $namespace.GetDefaultFolder(9) $appointmentItem = $null # Find by ID if provided if (${eventId} -ne $null) { try { $appointmentItem = $namespace.GetItemFromID(${eventId}) } catch { } } # Search by subject or date if not found if (-not $appointmentItem) { $items = $calendar.Items $items.Sort("[Start]") foreach ($item in $items) { $matchSubject = (${subject} -eq $null) -or ($item.Subject -like "*$(${subject})*") $matchDate = (${startDate} -eq $null) -or ([Math]::Abs(([DateTime]$item.Start - [DateTime]${startDate}).TotalMinutes) -lt 1) if ($matchSubject -and $matchDate) { $appointmentItem = $item break } } } if (-not $appointmentItem) { throw "Appointment not found. Please provide eventId, subject, or startDate." } $appointmentItem.BusyStatus = ${busyStatus} $appointmentItem.Save() Write-Output ([PSCustomObject]@{ Success = $true Subject = $appointmentItem.Subject ShowAs = "${options.showAs}" } | ConvertTo-Json -Compress) } catch { Write-Output ([PSCustomObject]@{ Success = $false Error = $_.Exception.Message } | ConvertTo-Json -Compress) } `; const result = await this.executePowerShell(script); const cleanResult = result.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '').trim(); const data = JSON.parse(cleanResult); if (!data.Success) { throw new Error(data.Error || 'Failed to set Show As'); } return { success: true, message: `Show As set to ${options.showAs} for: ${data.Subject}` }; } catch (error) { throw new Error(`Failed to set Show As: ${error instanceof Error ? error.message : String(error)}`); } } /** * Create calendar event with Show As setting */ async createEventWithShowAs(options: { subject: string; start: Date; end: Date; location?: string; body?: string; showAs?: 'Free' | 'Tentative' | 'Busy' | 'OutOfOffice' | 'WorkingElsewhere'; reminderMinutes?: number; }): Promise<{ success: boolean; eventId: string; message: string }> { try { // Map ShowAs values to Outlook constants const showAsMap: Record<string, number> = { 'Free': 0, 'Tentative': 1, 'Busy': 2, 'OutOfOffice': 3, 'WorkingElsewhere': 4 }; const busyStatus = showAsMap[options.showAs || 'Busy']; const cleanSubject = this.cleanText(options.subject); const cleanLocation = options.location ? this.cleanText(options.location) : ''; const cleanBody = options.body ? this.cleanText(options.body) : ''; const script = ` try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $appointmentItem = $outlook.CreateItem(1) $appointmentItem.Subject = "${cleanSubject.replace(/"/g, '""')}" $appointmentItem.Start = [DateTime]"${options.start.toISOString()}" $appointmentItem.End = [DateTime]"${options.end.toISOString()}" ${options.location ? `$appointmentItem.Location = "${cleanLocation.replace(/"/g, '""')}"` : ''} ${options.body ? `$appointmentItem.Body = "${cleanBody.replace(/"/g, '""')}"` : ''} $appointmentItem.BusyStatus = ${busyStatus} ${options.reminderMinutes !== undefined ? ` $appointmentItem.ReminderSet = $true $appointmentItem.ReminderMinutesBeforeStart = ${options.reminderMinutes} ` : ''} $appointmentItem.Save() Write-Output ([PSCustomObject]@{ Success = $true EventId = $appointmentItem.EntryID Subject = $appointmentItem.Subject ShowAs = "${options.showAs || 'Busy'}" } | ConvertTo-Json -Compress) } catch { Write-Output ([PSCustomObject]@{ Success = $false Error = $_.Exception.Message } | ConvertTo-Json -Compress) } `; const result = await this.executePowerShell(script); const cleanResult = result.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '').trim(); const data = JSON.parse(cleanResult); if (!data.Success) { throw new Error(data.Error || 'Failed to create event'); } return { success: true, eventId: data.EventId, message: `Event created: ${data.Subject} with Show As: ${data.ShowAs}` }; } catch (error) { throw new Error(`Failed to create event with Show As: ${error instanceof Error ? error.message : String(error)}`); } } /** * List calendar events within a date range */ async listEvents(options: { startDate: Date; endDate?: Date; calendar?: string; }): Promise<any[]> { try { const endDate = options.endDate || options.startDate; const calendarName = options.calendar || ''; const script = ` try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $namespace = $outlook.GetNamespace("MAPI") # Get calendar ${calendarName ? ` $calendar = $null foreach ($folder in $namespace.Folders) { if ($folder.Name -eq "${calendarName.replace(/"/g, '""')}") { $calendar = $folder.Folders("Calendar") break } } if (-not $calendar) { throw "Calendar not found: ${calendarName.replace(/"/g, '""')}" } ` : ` $calendar = $namespace.GetDefaultFolder(9) `} # Create filter for date range $startDate = [DateTime]"${options.startDate.toISOString()}" $endDate = [DateTime]"${endDate.toISOString()}".AddDays(1) $filter = "[Start] >= '$($startDate.ToString('g'))' AND [End] <= '$($endDate.ToString('g'))'" # Get events $items = $calendar.Items.Restrict($filter) $items.Sort("[Start]") # Build JSON array $events = @() foreach ($item in $items) { $events += [PSCustomObject]@{ Id = $item.EntryID Subject = $item.Subject Start = $item.Start.ToString("yyyy-MM-ddTHH:mm:ss") End = $item.End.ToString("yyyy-MM-ddTHH:mm:ss") Location = if ($item.Location) { $item.Location } else { "" } Body = if ($item.Body) { $item.Body } else { "" } BusyStatus = $item.BusyStatus IsAllDayEvent = $item.AllDayEvent Organizer = if ($item.Organizer) { $item.Organizer } else { "" } RequiredAttendees = if ($item.RequiredAttendees) { $item.RequiredAttendees } else { "" } } } Write-Output ($events | ConvertTo-Json -Compress) } catch { Write-Output ([PSCustomObject]@{ Error = $_.Exception.Message } | ConvertTo-Json -Compress) } `; const result = await this.executePowerShell(script); const cleanResult = result.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '').trim(); if (!cleanResult || cleanResult === '') { return []; } const data = JSON.parse(cleanResult); if (data.Error) { throw new Error(data.Error); } return Array.isArray(data) ? data : [data]; } catch (error) { throw new Error(`Failed to list events: ${error instanceof Error ? error.message : String(error)}`); } } /** * Update an existing calendar event */ async updateEvent(options: { eventId: string; subject?: string; startDate?: string; startTime?: string; endDate?: string; endTime?: string; location?: string; body?: string; calendar?: string; }): Promise<{ success: boolean; message: string }> { try { const calendarName = options.calendar || ''; let startDateTime = ''; let endDateTime = ''; if (options.startDate && options.startTime) { startDateTime = `${options.startDate} ${options.startTime}`; } if (options.endDate && options.endTime) { endDateTime = `${options.endDate} ${options.endTime}`; } else if (startDateTime) { // Default to 30 minutes after start endDateTime = 'ADD30MIN'; } const script = ` try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $namespace = $outlook.GetNamespace("MAPI") # Get appointment $appointment = $namespace.GetItemFromID("${options.eventId.replace(/"/g, '""')}") if (-not $appointment) { throw "Event not found with ID: ${options.eventId.replace(/"/g, '""')}" } # Update properties ${options.subject ? `$appointment.Subject = "${options.subject.replace(/"/g, '""')}"` : ''} ${startDateTime ? `$appointment.Start = [DateTime]"${startDateTime.replace(/"/g, '""')}"` : ''} ${endDateTime === 'ADD30MIN' ? `$appointment.End = $appointment.Start.AddMinutes(30)` : endDateTime ? `$appointment.End = [DateTime]"${endDateTime.replace(/"/g, '""')}"` : ''} ${options.location ? `$appointment.Location = "${options.location.replace(/"/g, '""')}"` : ''} ${options.body ? `$appointment.Body = "${options.body.replace(/"/g, '""')}"` : ''} $appointment.Save() Write-Output ([PSCustomObject]@{ Success = $true } | ConvertTo-Json -Compress) } catch { Write-Output ([PSCustomObject]@{ Success = $false Error = $_.Exception.Message } | ConvertTo-Json -Compress) } `; const result = await this.executePowerShell(script); const cleanResult = result.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '').trim(); const data = JSON.parse(cleanResult); if (!data.Success) { throw new Error(data.Error || 'Failed to update event'); } return { success: true, message: 'Event updated successfully' }; } catch (error) { throw new Error(`Failed to update event: ${error instanceof Error ? error.message : String(error)}`); } } /** * Delete a calendar event */ async deleteEvent(options: { eventId: string; calendar?: string; }): Promise<{ success: boolean; message: string }> { try { const script = ` try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $namespace = $outlook.GetNamespace("MAPI") # Get appointment $appointment = $namespace.GetItemFromID("${options.eventId.replace(/"/g, '""')}") if (-not $appointment) { throw "Event not found with ID: ${options.eventId.replace(/"/g, '""')}" } # Delete the appointment $appointment.Delete() Write-Output ([PSCustomObject]@{ Success = $true } | ConvertTo-Json -Compress) } catch { Write-Output ([PSCustomObject]@{ Success = $false Error = $_.Exception.Message } | ConvertTo-Json -Compress) } `; const result = await this.executePowerShell(script); const cleanResult = result.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '').trim(); const data = JSON.parse(cleanResult); if (!data.Success) { throw new Error(data.Error || 'Failed to delete event'); } return { success: true, message: 'Event deleted successfully' }; } catch (error) { throw new Error(`Failed to delete event: ${error instanceof Error ? error.message : String(error)}`); } } /** * Find free time slots in calendar */ async findFreeSlots(options: { startDate: Date; endDate?: Date; duration?: number; workDayStart?: number; workDayEnd?: number; calendar?: string; }): Promise<any[]> { try { const endDate = options.endDate || new Date(options.startDate.getTime() + 7 * 24 * 60 * 60 * 1000); const duration = options.duration || 30; const workDayStart = options.workDayStart || 9; const workDayEnd = options.workDayEnd || 17; const calendarName = options.calendar || ''; const script = ` try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $namespace = $outlook.GetNamespace("MAPI") # Get calendar ${calendarName ? ` $calendar = $null foreach ($folder in $namespace.Folders) { if ($folder.Name -eq "${calendarName.replace(/"/g, '""')}") { $calendar = $folder.Folders("Calendar") break } } if (-not $calendar) { throw "Calendar not found: ${calendarName.replace(/"/g, '""')}" } ` : ` $calendar = $namespace.GetDefaultFolder(9) `} # Get all events in date range $startDate = [DateTime]"${options.startDate.toISOString()}" $endDate = [DateTime]"${endDate.toISOString()}" $filter = "[Start] >= '$($startDate.ToString('g'))' AND [End] <= '$($endDate.AddDays(1).ToString('g'))'" $items = $calendar.Items.Restrict($filter) # Build busy slots array $busySlots = @() foreach ($item in $items) { if ($item.BusyStatus -eq 2 -or $item.BusyStatus -eq 3) { # Busy or OutOfOffice $busySlots += [PSCustomObject]@{ Start = $item.Start End = $item.End } } } # Find free slots $freeSlots = @() $currentDate = $startDate.Date while ($currentDate -le $endDate.Date) { # Skip weekends $dayOfWeek = $currentDate.DayOfWeek if ($dayOfWeek -ne [DayOfWeek]::Saturday -and $dayOfWeek -ne [DayOfWeek]::Sunday) { $slotStart = $currentDate.AddHours(${workDayStart}) $workDayEndTime = $currentDate.AddHours(${workDayEnd}) while ($slotStart.AddMinutes(${duration}) -le $workDayEndTime) { $slotEnd = $slotStart.AddMinutes(${duration}) # Check if slot is free $isFree = $true foreach ($busy in $busySlots) { if ($slotStart -lt $busy.End -and $slotEnd -gt $busy.Start) { $isFree = $false break } } if ($isFree) { $freeSlots += [PSCustomObject]@{ Start = $slotStart.ToString("yyyy-MM-ddTHH:mm:ss") End = $slotEnd.ToString("yyyy-MM-ddTHH:mm:ss") } } $slotStart = $slotStart.AddMinutes(30) } } $currentDate = $currentDate.AddDays(1) } Write-Output ($freeSlots | ConvertTo-Json -Compress) } catch { Write-Output ([PSCustomObject]@{ Error = $_.Exception.Message } | ConvertTo-Json -Compress) } `; const result = await this.executePowerShell(script); const cleanResult = result.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '').trim(); if (!cleanResult || cleanResult === '' || cleanResult === '[]') { return []; } const data = JSON.parse(cleanResult); if (data.Error) { throw new Error(data.Error); } return Array.isArray(data) ? data : [data]; } catch (error) { throw new Error(`Failed to find free slots: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get attendee status for a meeting */ async getAttendeeStatus(options: { eventId: string; calendar?: string; }): Promise<any> { try { const script = ` try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $namespace = $outlook.GetNamespace("MAPI") # Get appointment $appointment = $namespace.GetItemFromID("${options.eventId.replace(/"/g, '""')}") if (-not $appointment) { throw "Event not found with ID: ${options.eventId.replace(/"/g, '""')}" } # Check if it's a meeting if ($appointment.MeetingStatus -eq 0) { # olNonMeeting throw "The specified event is not a meeting" } # Get attendees $attendees = @() foreach ($recipient in $appointment.Recipients) { $responseStatus = switch ($recipient.MeetingResponseStatus) { 1 { "Organizer" } 2 { "Tentative" } 3 { "Accepted" } 4 { "Declined" } 0 { "Not Responded" } default { "Unknown" } } $attendees += [PSCustomObject]@{ Name = $recipient.Name Email = if ($recipient.Address) { $recipient.Address } else { $recipient.Name } ResponseStatus = $responseStatus } } # Build result $result = [PSCustomObject]@{ Subject = $appointment.Subject Start = $appointment.Start.ToString("yyyy-MM-ddTHH:mm:ss") End = $appointment.End.ToString("yyyy-MM-ddTHH:mm:ss") Location = if ($appointment.Location) { $appointment.Location } else { "" } Organizer = if ($appointment.Organizer) { $appointment.Organizer } else { "" } Attendees = $attendees } Write-Output ($result | ConvertTo-Json -Compress -Depth 3) } catch { Write-Output ([PSCustomObject]@{ Error = $_.Exception.Message } | ConvertTo-Json -Compress) } `; const result = await this.executePowerShell(script); const cleanResult = result.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '').trim(); const data = JSON.parse(cleanResult); if (data.Error) { throw new Error(data.Error); } return data; } catch (error) { throw new Error(`Failed to get attendee status: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get available calendars */ async getCalendars(): Promise<any[]> { try { const script = ` try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $namespace = $outlook.GetNamespace("MAPI") # Build calendars array $calendars = @() # Add default calendar $calendars += [PSCustomObject]@{ Name = "Default" Owner = $namespace.CurrentUser.Name IsDefault = $true } # Add other calendars foreach ($folder in $namespace.Folders) { try { $calendarFolder = $folder.Folders("Calendar") if ($calendarFolder) { $calendars += [PSCustomObject]@{ Name = "$($folder.Name) - Calendar" Owner = $folder.Name IsDefault = $false } } } catch { } } Write-Output ($calendars | ConvertTo-Json -Compress) } catch { Write-Output ([PSCustomObject]@{ Error = $_.Exception.Message } | ConvertTo-Json -Compress) } `; const result = await this.executePowerShell(script); const cleanResult = result.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '').trim(); const data = JSON.parse(cleanResult); if (data.Error) { throw new Error(data.Error); } return Array.isArray(data) ? data : [data]; } catch (error) { throw new Error(`Failed to get calendars: ${error instanceof Error ? error.message : String(error)}`); } } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cqyefeng119/windows-outlook-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server