Skip to main content
Glama

Outlook MCP Server

by cqyefeng119
outlook-manager.ts•16.7 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'; } }

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