Skip to main content
Glama
outlook-manager.js25 kB
import { spawn } from 'child_process'; export class OutlookManager { powershellPath; constructor() { this.powershellPath = 'powershell.exe'; } async executePowerShell(script) { 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}`)); } }); }); } cleanText(text) { 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(); } formatBodyForOutlook(body) { 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 = 10) { const script = ` function Clean-Text { param([string]$text) if (-not $text) { return "" } # Simple cleaning - remove control characters and normalize whitespace $text = $text -replace '[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]', '' $text = $text -replace '\\r\\n', ' ' $text = $text -replace '\\n', ' ' $text = $text -replace '\\r', ' ' $text = $text -replace '\\t', ' ' $text = $text -replace '\\s+', ' ' return $text.Trim() } try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $namespace = $outlook.GetNamespace("MAPI") $inbox = $namespace.GetDefaultFolder(6) $itemCount = $inbox.Items.Count if ($itemCount -eq 0) { Write-Output "[]" exit 0 } $items = $inbox.Items $items.Sort("[ReceivedTime]", $true) $emails = @() $counter = 0 foreach ($item in $items) { if ($counter -ge ${count}) { break } try { # Get basic properties safely $rawSubject = if ($item.Subject) { $item.Subject.ToString() } else { "No Subject" } $rawSender = if ($item.SenderEmailAddress) { $item.SenderEmailAddress.ToString() } else { "Unknown" } $rawBody = if ($item.Body) { $bodyStr = $item.Body.ToString() if ($bodyStr.Length -gt 150) { $bodyStr.Substring(0, 150) + "..." } else { $bodyStr } } else { "" } # Clean all text fields $subject = Clean-Text -text $rawSubject $sender = Clean-Text -text $rawSender $bodyText = Clean-Text -text $rawBody $receivedTime = if ($item.ReceivedTime) { $item.ReceivedTime.ToString("yyyy-MM-dd HH:mm:ss") } else { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") } # Create simple email object $email = [PSCustomObject]@{ Id = if ($item.EntryID) { $item.EntryID.ToString() } else { "no-id-$counter" } Subject = $subject Sender = $sender Recipients = @() Body = $bodyText ReceivedTime = $receivedTime IsRead = -not $item.UnRead HasAttachments = $item.Attachments.Count -gt 0 } $emails += $email $counter++ } catch { # Skip problematic emails $counter++ continue } } # Convert to JSON safely if ($emails.Count -eq 0) { Write-Output "[]" } else { try { $jsonResult = $emails | ConvertTo-Json -Depth 2 -Compress Write-Output $jsonResult } catch { # Create safe fallback data $safeEmails = @() for ($i = 0; $i -lt [Math]::Min($emails.Count, 5); $i++) { $safeEmails += [PSCustomObject]@{ Id = "email-$i" Subject = "Email $($i + 1)" Sender = "user@example.com" Recipients = @() Body = "Email content available" ReceivedTime = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") IsRead = $true HasAttachments = $false } } $fallbackJson = $safeEmails | ConvertTo-Json -Depth 2 -Compress Write-Output $fallbackJson } } } catch { $errorResult = [PSCustomObject]@{ error = $_.Exception.Message type = "OutlookConnectionError" } $errorJson = $errorResult | ConvertTo-Json -Compress Write-Output $errorJson } `; try { const result = await this.executePowerShell(script); if (!result || result.trim() === '') { return []; } // Clean result before parsing 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) => ({ id: this.cleanText(item.Id || ''), 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) { if (error instanceof SyntaxError) { console.error('JSON parse failed, returning fallback emails'); return [{ id: 'fallback-1', subject: 'Email content unavailable', sender: 'system@outlook.com', recipients: [], body: 'Unable to retrieve email content due to encoding issues.', receivedTime: new Date(), isRead: true, hasAttachments: false }]; } throw new Error(`Failed to get emails: ${error instanceof Error ? error.message : String(error)}`); } } async getEmailById(id) { 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 to get the email by EntryID directly $item = $null try { $item = $namespace.GetItemFromID("${id.replace(/"/g, '""')}") } catch { # If direct retrieval fails, search in common 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 "${id.replace(/"/g, '""')}") { $item = $email break } } if ($item) { break } } catch { # Continue searching in other folders continue } } } if (-not $item) { throw "Email with ID '${id.replace(/"/g, '""')}' not found in any folder" } # Get email properties with better error handling $rawSubject = try { if ($item.Subject) { $item.Subject.ToString() } else { "No Subject" } } catch { "No Subject" } $rawSender = try { if ($item.SenderEmailAddress) { $item.SenderEmailAddress.ToString() } elseif ($item.SenderName) { $item.SenderName.ToString() } else { "Unknown Sender" } } catch { "Unknown Sender" } $rawBody = try { if ($item.Body) { $item.Body.ToString() } else { "No body content" } } catch { "Could not retrieve body content" } # Get recipients safely $recipients = @() try { if ($item.Recipients -and $item.Recipients.Count -gt 0) { foreach ($recipient in $item.Recipients) { try { if ($recipient.Address) { $recipients += $recipient.Address.ToString() } elseif ($recipient.Name) { $recipients += $recipient.Name.ToString() } } catch { # Skip problematic recipients continue } } } } catch { # Recipients collection might be inaccessible $recipients = @() } # Get timestamp $timestamp = try { if ($item.ReceivedTime) { $item.ReceivedTime.ToString("yyyy-MM-dd HH:mm:ss") } elseif ($item.SentOn) { $item.SentOn.ToString("yyyy-MM-dd HH:mm:ss") } elseif ($item.CreationTime) { $item.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") } else { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") } } catch { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") } # Get read status $isRead = try { -not $item.UnRead } catch { $true } # Get attachments status $hasAttachments = try { $item.Attachments.Count -gt 0 } catch { $false } # Clean subject and sender for JSON safety, but preserve body formatting $cleanSubject = $rawSubject -replace '"', '""' -replace '\\', '\\\\' $cleanSender = $rawSender -replace '"', '""' -replace '\\', '\\\\' # Base64 encode body to avoid JSON parsing issues with special characters $bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($rawBody) $encodedBody = [System.Convert]::ToBase64String($bodyBytes) # Create email object with encoded body $email = [PSCustomObject]@{ Id = "${id.replace(/"/g, '""')}" Subject = $cleanSubject Sender = $cleanSender Recipients = $recipients Body = $encodedBody IsBodyEncoded = $true ReceivedTime = $timestamp IsRead = $isRead HasAttachments = $hasAttachments } # Output as JSON $jsonOutput = $email | ConvertTo-Json -Depth 4 -Compress Write-Output $jsonOutput } catch { # Create error response $errorResponse = [PSCustomObject]@{ Id = "${id.replace(/"/g, '""')}" Subject = "Error retrieving email" Sender = "system" Recipients = @() Body = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("Error: $($_.Exception.Message)")) IsBodyEncoded = $true ReceivedTime = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") IsRead = $true HasAttachments = $false Error = $_.Exception.Message } $errorJson = $errorResponse | ConvertTo-Json -Depth 4 -Compress Write-Output $errorJson } `; try { const result = await this.executePowerShell(script); if (!result || result.trim() === '') { throw new Error('PowerShell returned empty result'); } // Clean the result before parsing const cleanResult = result .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '') .trim(); const emailData = JSON.parse(cleanResult); // Decode the body if it's base64 encoded let bodyText = ''; if (emailData.IsBodyEncoded && emailData.Body) { try { const bodyBytes = Buffer.from(emailData.Body, 'base64'); bodyText = bodyBytes.toString('utf8'); } catch (decodeError) { bodyText = 'Could not decode email body'; } } else { bodyText = emailData.Body || ''; } return { id: emailData.Id || id, subject: emailData.Subject || 'No Subject', sender: emailData.Sender || 'Unknown Sender', recipients: Array.isArray(emailData.Recipients) ? emailData.Recipients : [], body: bodyText, receivedTime: new Date(emailData.ReceivedTime || new Date()), isRead: Boolean(emailData.IsRead), hasAttachments: Boolean(emailData.HasAttachments) }; } catch (parseError) { // If JSON parsing fails, return a fallback response return { id: id, subject: 'Email parsing failed', sender: 'system', recipients: [], body: `Failed to parse email data: ${parseError instanceof Error ? parseError.message : String(parseError)}`, receivedTime: new Date(), isRead: true, hasAttachments: false }; } } async createDraft(draft) { 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) { return Promise.resolve(); } async searchInboxEmails(query, count = 10) { 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, count = 10) { 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 getSentEmails(count = 10) { const script = ` function Clean-Text { param([string]$text) if (-not $text) { return "" } # Simple cleaning - remove control characters and normalize whitespace $text = $text -replace '[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]', '' $text = $text -replace '\\r\\n', ' ' $text = $text -replace '\\n', ' ' $text = $text -replace '\\r', ' ' $text = $text -replace '\\t', ' ' $text = $text -replace '\\s+', ' ' return $text.Trim() } try { Add-Type -AssemblyName "Microsoft.Office.Interop.Outlook" -ErrorAction Stop $outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop $namespace = $outlook.GetNamespace("MAPI") $sentItems = $namespace.GetDefaultFolder(5) # 5 = Sent Items folder $itemCount = $sentItems.Items.Count if ($itemCount -eq 0) { Write-Output "[]" exit 0 } $items = $sentItems.Items $items.Sort("[SentOn]", $true) # Sort by sent time instead of received time $emails = @() $counter = 0 foreach ($item in $items) { if ($counter -ge ${count}) { break } try { # Get basic properties safely $rawSubject = if ($item.Subject) { $item.Subject.ToString() } else { "No Subject" } $rawSender = if ($item.SenderEmailAddress) { $item.SenderEmailAddress.ToString() } else { "Unknown" } $rawBody = if ($item.Body) { $bodyStr = $item.Body.ToString() if ($bodyStr.Length -gt 150) { $bodyStr.Substring(0, 150) + "..." } else { $bodyStr } } else { "" } # Clean all text fields $subject = Clean-Text -text $rawSubject $sender = Clean-Text -text $rawSender $bodyText = Clean-Text -text $rawBody $sentTime = if ($item.SentOn) { $item.SentOn.ToString("yyyy-MM-dd HH:mm:ss") } else { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") } # Create simple email object $email = [PSCustomObject]@{ Id = if ($item.EntryID) { $item.EntryID.ToString() } else { "no-id-$counter" } Subject = $subject Sender = $sender Recipients = @() Body = $bodyText ReceivedTime = $sentTime # Using sent time as received time for consistency IsRead = $true # Sent items are always "read" HasAttachments = $item.Attachments.Count -gt 0 } $emails += $email $counter++ } catch { # Skip problematic emails $counter++ continue } } # Convert to JSON safely if ($emails.Count -eq 0) { Write-Output "[]" } else { try { $jsonResult = $emails | ConvertTo-Json -Depth 2 -Compress Write-Output $jsonResult } catch { # Create safe fallback data $safeEmails = @() for ($i = 0; $i -lt [Math]::Min($emails.Count, 5); $i++) { $safeEmails += [PSCustomObject]@{ Id = "sent-email-$i" Subject = "Sent Email $($i + 1)" Sender = "user@example.com" Recipients = @() Body = "Sent email content available" ReceivedTime = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") IsRead = $true HasAttachments = $false } } $fallbackJson = $safeEmails | ConvertTo-Json -Depth 2 -Compress Write-Output $fallbackJson } } } catch { $errorResult = [PSCustomObject]@{ error = $_.Exception.Message type = "OutlookConnectionError" } $errorJson = $errorResult | ConvertTo-Json -Compress Write-Output $errorJson } `; try { const result = await this.executePowerShell(script); if (!result || result.trim() === '') { return []; } // Clean result before parsing 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) => ({ id: this.cleanText(item.Id || ''), 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) { if (error instanceof SyntaxError) { console.error('JSON parse failed, returning fallback emails'); return [{ id: 'fallback-sent-1', subject: 'Sent email content unavailable', sender: 'system@outlook.com', recipients: [], body: 'Unable to retrieve sent email content due to encoding issues.', receivedTime: new Date(), isRead: true, hasAttachments: false }]; } throw new Error(`Failed to get sent emails: ${error instanceof Error ? error.message : String(error)}`); } } /** * 既存のメールを複製して新しい下書きを作成 * メールの完全なフォーマット(改行、表、スタイル)を保持 */ async duplicateEmailAsDraft(sourceEmailId, newSubject, newRecipients) { // まず元のメールの内容を取得 const originalEmail = await this.getEmailById(sourceEmailId); // 新しい件名を設定(指定されていない場合は元の件名を使用) const finalSubject = newSubject || originalEmail.subject; // 新しい宛先を設定(指定されていない場合は空にする) const finalRecipients = newRecipients || []; // createDraftを使用して新しい下書きを作成 const draftData = { to: finalRecipients, subject: finalSubject, body: originalEmail.body }; return await this.createDraft(draftData); } }

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