From 68a64461ebe7bb94340f8466a5a34ab9e665789a Mon Sep 17 00:00:00 2001 From: Seton Carmichael Date: Sun, 21 Jun 2026 20:28:11 -0400 Subject: [PATCH] fix(outlook-mcp): P0 audit fixes - Handle empty 2xx Graph API responses without JSON parse errors - Fix list-rules handler import (handleListRules named export) - Add timezone-aware display formatting using MS_TIMEZONE / DEFAULT_TIMEZONE - Return created event ID in create-event success message - Change default timezone from Windows name to IANA (America/New_York) --- calendar/create.js | 2 +- calendar/list.js | 6 ++++-- config.js | 4 ++-- email/list.js | 4 +++- email/read-multiple.js | 3 ++- email/read.js | 3 ++- email/search.js | 3 ++- rules/index.js | 7 +++---- utils/graph-api.js | 8 +++++++- utils/threadBuilder.js | 7 +++---- utils/time-formatter.js | 42 +++++++++++++++++++++++++++++++++++++++++ 11 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 utils/time-formatter.js diff --git a/calendar/create.js b/calendar/create.js index 1244bf5..fdb731f 100644 --- a/calendar/create.js +++ b/calendar/create.js @@ -44,7 +44,7 @@ async function handleCreateEvent(args) { return { content: [{ type: "text", - text: `Event '${subject}' has been successfully created.` + text: `Event '${subject}' has been successfully created. ID: ${response.id || '(not returned)'}` }] }; } catch (error) { diff --git a/calendar/list.js b/calendar/list.js index c8733c4..eac3058 100644 --- a/calendar/list.js +++ b/calendar/list.js @@ -1,3 +1,5 @@ +const { formatDateTime } = require('../utils/time-formatter'); + /** * List events functionality */ @@ -59,8 +61,8 @@ async function handleListEvents(args) { // Format results const eventList = response.value.map((event, index) => { - const startDate = new Date(event.start.dateTime).toLocaleString(event.start.timeZone); - const endDate = new Date(event.end.dateTime).toLocaleString(event.end.timeZone); + const startDate = formatDateTime(event.start.dateTime, event.start.timeZone); + const endDate = formatDateTime(event.end.dateTime, event.end.timeZone); const location = event.location.displayName || 'No location'; return `${index + 1}. ${event.subject} - Location: ${location}\nStart: ${startDate}\nEnd: ${endDate}\nSubject: ${event.subject}\nSummary: ${event.bodyPreview}\nID: ${event.id}\n`; diff --git a/config.js b/config.js index 0dd712b..1cdef92 100644 --- a/config.js +++ b/config.js @@ -43,9 +43,9 @@ module.exports = { // Calendar constants CALENDAR_SELECT_FIELDS: 'id,subject,bodyPreview,start,end,location,organizer,attendees,isAllDay,isCancelled', - // Default timezone for calendar event creation (IANA tz name, e.g. 'America/New_York'). + // Default timezone for calendar event creation and display (IANA tz name, e.g. 'America/New_York'). // Override via MS_TIMEZONE env var. Graph API accepts IANA timezone identifiers. - DEFAULT_TIMEZONE: process.env.MS_TIMEZONE || 'Eastern Standard Time', + DEFAULT_TIMEZONE: process.env.MS_TIMEZONE || 'America/New_York', DEFAULT_PAGE_SIZE: 25, MAX_RESULT_COUNT: 500 }; diff --git a/email/list.js b/email/list.js index da06703..7c129a6 100644 --- a/email/list.js +++ b/email/list.js @@ -1,3 +1,5 @@ +const { formatDateTime } = require('../utils/time-formatter'); + /** * List emails functionality */ @@ -51,7 +53,7 @@ async function handleListEmails(args) { // Format results const emailList = response.value.map((email, index) => { const sender = email.from ? email.from.emailAddress : { name: 'Unknown', address: 'unknown' }; - const date = new Date(email.receivedDateTime).toLocaleString(); + const date = formatDateTime(email.receivedDateTime); const readStatus = email.isRead ? '' : '[UNREAD] '; const convLine = email.conversationId ? `ConversationID: ${email.conversationId}\n` : ''; diff --git a/email/read-multiple.js b/email/read-multiple.js index 4f7feaf..2cfcd4a 100644 --- a/email/read-multiple.js +++ b/email/read-multiple.js @@ -5,6 +5,7 @@ const config = require('../config'); const { callGraphAPI } = require('../utils/graph-api'); const { ensureAuthenticated } = require('../auth'); const { cleanBody } = require('../utils/bodyParser'); +const { formatDateTime } = require('../utils/time-formatter'); /** * Format a single email for display @@ -23,7 +24,7 @@ function formatEmail(email, emailId) { const to = email.toRecipients ? email.toRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None'; const cc = email.ccRecipients && email.ccRecipients.length > 0 ? email.ccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None'; const bcc = email.bccRecipients && email.bccRecipients.length > 0 ? email.bccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None'; - const date = new Date(email.receivedDateTime).toLocaleString(); + const date = formatDateTime(email.receivedDateTime); // Extract and clean body content (cleanBody handles both HTML and plain text) let body = ''; diff --git a/email/read.js b/email/read.js index 423b533..38d9959 100644 --- a/email/read.js +++ b/email/read.js @@ -5,6 +5,7 @@ const config = require('../config'); const { callGraphAPI } = require('../utils/graph-api'); const { ensureAuthenticated } = require('../auth'); const { cleanBody } = require('../utils/bodyParser'); +const { formatDateTime } = require('../utils/time-formatter'); /** * Read email handler @@ -52,7 +53,7 @@ async function handleReadEmail(args) { const to = email.toRecipients ? email.toRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None'; const cc = email.ccRecipients && email.ccRecipients.length > 0 ? email.ccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None'; const bcc = email.bccRecipients && email.bccRecipients.length > 0 ? email.bccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None'; - const date = new Date(email.receivedDateTime).toLocaleString(); + const date = formatDateTime(email.receivedDateTime); // Extract and clean body content (cleanBody handles both HTML and plain text) let body = ''; diff --git a/email/search.js b/email/search.js index 046247c..1272fe2 100644 --- a/email/search.js +++ b/email/search.js @@ -5,6 +5,7 @@ const config = require('../config'); const { callGraphAPI } = require('../utils/graph-api'); const { ensureAuthenticated } = require('../auth'); const { resolveFolderPath } = require('./folder-utils'); +const { formatDateTime } = require('../utils/time-formatter'); /** * Search emails handler @@ -233,7 +234,7 @@ function formatSearchResults(response) { // Format results const emailList = response.value.map((email, index) => { const sender = email.from?.emailAddress || { name: 'Unknown', address: 'unknown' }; - const date = new Date(email.receivedDateTime).toLocaleString(); + const date = formatDateTime(email.receivedDateTime); const readStatus = email.isRead ? '' : '[UNREAD] '; const threadNote = email.conversationId ? `\nConversationID: ${email.conversationId}` : ''; diff --git a/rules/index.js b/rules/index.js index 459f791..7e52e73 100644 --- a/rules/index.js +++ b/rules/index.js @@ -1,13 +1,12 @@ /** * Email rules management module for Outlook MCP server */ -const handleListRules = require('./list'); -const handleCreateRule = require('./create'); const { callGraphAPI } = require('../utils/graph-api'); const { ensureAuthenticated } = require('../auth'); -// Import getInboxRules for the edit sequence tool -const { getInboxRules } = require('./list'); +// Import rule handlers +const { handleListRules, getInboxRules } = require('./list'); +const handleCreateRule = require('./create'); /** * Edit rule sequence handler diff --git a/utils/graph-api.js b/utils/graph-api.js index f69ecab..e1774c5 100644 --- a/utils/graph-api.js +++ b/utils/graph-api.js @@ -83,8 +83,14 @@ async function callGraphAPI(accessToken, method, path, data = null, queryParams res.on('end', () => { if (res.statusCode >= 200 && res.statusCode < 300) { + const trimmed = responseData.trim(); + if (!trimmed) { + // Graph returns empty bodies for some successful operations + // (e.g. sendMail 202, DELETE 204). Treat this as success. + return resolve({}); + } try { - const jsonResponse = JSON.parse(responseData); + const jsonResponse = JSON.parse(trimmed); resolve(jsonResponse); } catch (error) { reject(new Error(`Error parsing API response: ${error.message}`)); diff --git a/utils/threadBuilder.js b/utils/threadBuilder.js index 1002c5c..a817404 100644 --- a/utils/threadBuilder.js +++ b/utils/threadBuilder.js @@ -2,6 +2,8 @@ const { cleanBody } = require('./bodyParser'); +const { formatDateTime } = require('../utils/time-formatter'); + /** * threadBuilder.js * Reconstructs a clean, deduplicated email thread from a set of Graph API @@ -77,10 +79,7 @@ function formatThreadEntry(msg, index) { .join(', '); const date = msg.receivedDateTime - ? new Date(msg.receivedDateTime).toLocaleString('en-US', { - month: 'numeric', day: 'numeric', year: 'numeric', - hour: 'numeric', minute: '2-digit', hour12: true - }) + ? formatDateTime(msg.receivedDateTime) : 'Unknown date'; const bodyText = msg.body?.content || msg.bodyPreview || ''; diff --git a/utils/time-formatter.js b/utils/time-formatter.js new file mode 100644 index 0000000..a8904e5 --- /dev/null +++ b/utils/time-formatter.js @@ -0,0 +1,42 @@ +/** + * Timezone-aware date formatting utilities for Outlook MCP Server. + * + * All user-visible timestamps are formatted in the configured timezone + * (MS_TIMEZONE env var, default Eastern Standard Time) so that dates + * rendered inside the UTC container still match the user's local time. + */ +const config = require('../config'); + +/** + * Format an ISO 8601 or Graph dateTime string for display. + * @param {string} isoString - ISO 8601 / UTC timestamp from Graph API + * @param {string} [timeZone] - IANA or Windows timezone name (defaults to config.DEFAULT_TIMEZONE) + * @returns {string} Formatted date/time string with timezone abbreviation + */ +function formatDateTime(isoString, timeZone = config.DEFAULT_TIMEZONE) { + if (!isoString) return 'Unknown'; + + const d = new Date(isoString); + if (isNaN(d.getTime())) return String(isoString); + + try { + return d.toLocaleString('en-US', { + timeZone, + month: 'numeric', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true, + timeZoneName: 'short' + }); + } catch (err) { + // If the configured timezone identifier is invalid, fall back to ISO. + console.error(`Invalid timezone "${timeZone}", falling back to ISO: ${err.message}`); + return d.toISOString(); + } +} + +module.exports = { + formatDateTime +};