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)
This commit is contained in:
parent
ba45f57b39
commit
68a64461eb
11 changed files with 71 additions and 18 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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` : '';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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}` : '';
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`));
|
||||
|
|
|
|||
|
|
@ -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 || '';
|
||||
|
|
|
|||
42
utils/time-formatter.js
Normal file
42
utils/time-formatter.js
Normal file
|
|
@ -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
|
||||
};
|
||||
Loading…
Reference in a new issue