@littlebearapps/outlook-assistant 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +22 -0
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/advanced/index.js +652 -0
- package/auth/index.js +32 -0
- package/auth/oauth-server.js +233 -0
- package/auth/token-manager.js +105 -0
- package/auth/token-storage.js +359 -0
- package/auth/tools.js +159 -0
- package/calendar/accept.js +72 -0
- package/calendar/cancel.js +72 -0
- package/calendar/create.js +115 -0
- package/calendar/decline.js +72 -0
- package/calendar/delete.js +67 -0
- package/calendar/index.js +130 -0
- package/calendar/list.js +108 -0
- package/categories/index.js +955 -0
- package/config.js +95 -0
- package/contacts/index.js +754 -0
- package/email/attachments.js +365 -0
- package/email/conversations.js +666 -0
- package/email/delta.js +210 -0
- package/email/export.js +572 -0
- package/email/folder-utils.js +192 -0
- package/email/headers.js +344 -0
- package/email/index.js +537 -0
- package/email/list.js +136 -0
- package/email/mark-as-read.js +114 -0
- package/email/mime.js +286 -0
- package/email/read.js +161 -0
- package/email/search.js +628 -0
- package/email/send.js +169 -0
- package/folder/create.js +137 -0
- package/folder/delete.js +108 -0
- package/folder/index.js +112 -0
- package/folder/list.js +289 -0
- package/folder/move.js +186 -0
- package/folder/stats.js +322 -0
- package/index.js +162 -0
- package/llms.txt +76 -0
- package/outlook-auth-server.js +384 -0
- package/package.json +97 -0
- package/rules/create.js +273 -0
- package/rules/index.js +276 -0
- package/rules/list.js +216 -0
- package/settings/index.js +678 -0
- package/utils/field-presets.js +311 -0
- package/utils/graph-api.js +268 -0
- package/utils/mock-data.js +154 -0
- package/utils/odata-helpers.js +33 -0
- package/utils/response-formatter.js +457 -0
- package/utils/safety.js +123 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock data functions for test mode
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Simulates Microsoft Graph API responses for testing
|
|
7
|
+
* @param {string} method - HTTP method
|
|
8
|
+
* @param {string} path - API path
|
|
9
|
+
* @param {object} data - Request data
|
|
10
|
+
* @param {object} queryParams - Query parameters
|
|
11
|
+
* @returns {object} - Simulated API response
|
|
12
|
+
*/
|
|
13
|
+
function simulateGraphAPIResponse(method, path, _data, _queryParams) {
|
|
14
|
+
console.error(`Simulating response for: ${method} ${path}`);
|
|
15
|
+
|
|
16
|
+
if (method === 'GET') {
|
|
17
|
+
if (path.includes('messages') && !path.includes('sendMail')) {
|
|
18
|
+
// Simulate a successful email list/search response
|
|
19
|
+
if (path.includes('/messages/')) {
|
|
20
|
+
// Single email response
|
|
21
|
+
return {
|
|
22
|
+
id: 'simulated-email-id',
|
|
23
|
+
subject: 'Simulated Email Subject',
|
|
24
|
+
from: {
|
|
25
|
+
emailAddress: {
|
|
26
|
+
name: 'Simulated Sender',
|
|
27
|
+
address: 'sender@example.com',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
toRecipients: [
|
|
31
|
+
{
|
|
32
|
+
emailAddress: {
|
|
33
|
+
name: 'Recipient Name',
|
|
34
|
+
address: 'recipient@example.com',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
ccRecipients: [],
|
|
39
|
+
bccRecipients: [],
|
|
40
|
+
receivedDateTime: new Date().toISOString(),
|
|
41
|
+
bodyPreview: 'This is a simulated email preview...',
|
|
42
|
+
body: {
|
|
43
|
+
contentType: 'text',
|
|
44
|
+
content:
|
|
45
|
+
"This is the full content of the simulated email. Since we can't connect to the real Microsoft Graph API, we're returning this placeholder content instead.",
|
|
46
|
+
},
|
|
47
|
+
hasAttachments: false,
|
|
48
|
+
importance: 'normal',
|
|
49
|
+
isRead: false,
|
|
50
|
+
internetMessageHeaders: [],
|
|
51
|
+
};
|
|
52
|
+
} else {
|
|
53
|
+
// Email list response
|
|
54
|
+
return {
|
|
55
|
+
value: [
|
|
56
|
+
{
|
|
57
|
+
id: 'simulated-email-1',
|
|
58
|
+
subject: 'Important Meeting Tomorrow',
|
|
59
|
+
from: {
|
|
60
|
+
emailAddress: {
|
|
61
|
+
name: 'John Doe',
|
|
62
|
+
address: 'john@example.com',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
toRecipients: [
|
|
66
|
+
{
|
|
67
|
+
emailAddress: {
|
|
68
|
+
name: 'You',
|
|
69
|
+
address: 'you@example.com',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
ccRecipients: [],
|
|
74
|
+
receivedDateTime: new Date().toISOString(),
|
|
75
|
+
bodyPreview: "Let's discuss the project status...",
|
|
76
|
+
hasAttachments: false,
|
|
77
|
+
importance: 'high',
|
|
78
|
+
isRead: false,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'simulated-email-2',
|
|
82
|
+
subject: 'Weekly Report',
|
|
83
|
+
from: {
|
|
84
|
+
emailAddress: {
|
|
85
|
+
name: 'Jane Smith',
|
|
86
|
+
address: 'jane@example.com',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
toRecipients: [
|
|
90
|
+
{
|
|
91
|
+
emailAddress: {
|
|
92
|
+
name: 'You',
|
|
93
|
+
address: 'you@example.com',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
ccRecipients: [],
|
|
98
|
+
receivedDateTime: new Date(Date.now() - 86400000).toISOString(), // Yesterday
|
|
99
|
+
bodyPreview: 'Please find attached the weekly report...',
|
|
100
|
+
hasAttachments: true,
|
|
101
|
+
importance: 'normal',
|
|
102
|
+
isRead: true,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'simulated-email-3',
|
|
106
|
+
subject: 'Question about the project',
|
|
107
|
+
from: {
|
|
108
|
+
emailAddress: {
|
|
109
|
+
name: 'Bob Johnson',
|
|
110
|
+
address: 'bob@example.com',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
toRecipients: [
|
|
114
|
+
{
|
|
115
|
+
emailAddress: {
|
|
116
|
+
name: 'You',
|
|
117
|
+
address: 'you@example.com',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
ccRecipients: [],
|
|
122
|
+
receivedDateTime: new Date(Date.now() - 172800000).toISOString(), // 2 days ago
|
|
123
|
+
bodyPreview: 'I had a question about the timeline...',
|
|
124
|
+
hasAttachments: false,
|
|
125
|
+
importance: 'normal',
|
|
126
|
+
isRead: false,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
} else if (path.includes('mailFolders')) {
|
|
132
|
+
// Simulate a mail folders response
|
|
133
|
+
return {
|
|
134
|
+
value: [
|
|
135
|
+
{ id: 'inbox', displayName: 'Inbox' },
|
|
136
|
+
{ id: 'drafts', displayName: 'Drafts' },
|
|
137
|
+
{ id: 'sentItems', displayName: 'Sent Items' },
|
|
138
|
+
{ id: 'deleteditems', displayName: 'Deleted Items' },
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
} else if (method === 'POST' && path.includes('sendMail')) {
|
|
143
|
+
// Simulate a successful email send
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// If we get here, we don't have a simulation for this endpoint
|
|
148
|
+
console.error(`No simulation available for: ${method} ${path}`);
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
simulateGraphAPIResponse,
|
|
154
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OData helper functions for Microsoft Graph API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Escapes a string for use in OData queries
|
|
7
|
+
* @param {string} str - The string to escape
|
|
8
|
+
* @returns {string} - The escaped string
|
|
9
|
+
*/
|
|
10
|
+
function escapeODataString(str) {
|
|
11
|
+
if (!str) return str;
|
|
12
|
+
|
|
13
|
+
// Replace single quotes with double single quotes (OData escaping)
|
|
14
|
+
return str.replace(/'/g, "''");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Builds an OData filter from filter conditions
|
|
19
|
+
* @param {Array<string>} conditions - Array of filter conditions
|
|
20
|
+
* @returns {string} - Combined OData filter expression
|
|
21
|
+
*/
|
|
22
|
+
function buildODataFilter(conditions) {
|
|
23
|
+
if (!conditions || conditions.length === 0) {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return conditions.join(' and ');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
escapeODataString,
|
|
32
|
+
buildODataFilter,
|
|
33
|
+
};
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response formatting utilities for token-efficient MCP responses
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Markdown formatting (15% more token-efficient than JSON)
|
|
6
|
+
* - Verbosity levels (minimal/standard/full)
|
|
7
|
+
* - Truncation with metadata
|
|
8
|
+
* - Response metadata (_meta blocks)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Verbosity levels for response formatting
|
|
13
|
+
*/
|
|
14
|
+
const VERBOSITY = {
|
|
15
|
+
MINIMAL: 'minimal', // IDs + subject only (for batch operations)
|
|
16
|
+
STANDARD: 'standard', // Key fields (default)
|
|
17
|
+
FULL: 'full', // All available fields
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default limits for token efficiency
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_LIMITS = {
|
|
24
|
+
listEmails: 25,
|
|
25
|
+
searchEmails: 10,
|
|
26
|
+
bodyPreviewLength: 100,
|
|
27
|
+
batchExport: 25,
|
|
28
|
+
maxBodyTruncation: 2000,
|
|
29
|
+
maxTableRows: 50,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Truncates text with metadata for long content
|
|
34
|
+
* @param {string} text - Text to truncate
|
|
35
|
+
* @param {number} maxChars - Maximum characters (default 2000)
|
|
36
|
+
* @returns {object|string} - Truncated content with metadata or original text
|
|
37
|
+
*/
|
|
38
|
+
function truncateWithMeta(text, maxChars = DEFAULT_LIMITS.maxBodyTruncation) {
|
|
39
|
+
if (!text || text.length <= maxChars) {
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
content: text.substring(0, maxChars),
|
|
45
|
+
_truncated: true,
|
|
46
|
+
_fullLength: text.length,
|
|
47
|
+
_hint: 'Use read-email with includeFullBody=true for complete content',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Formats a single email for list display (minimal verbosity)
|
|
53
|
+
* @param {object} email - Email object from Graph API
|
|
54
|
+
* @param {number} index - Index in list (1-based)
|
|
55
|
+
* @returns {string} - Minimal format: ID, subject, from
|
|
56
|
+
*/
|
|
57
|
+
function formatEmailMinimal(email, index) {
|
|
58
|
+
const from = email.from?.emailAddress?.address || 'unknown';
|
|
59
|
+
return `${index}. ${email.id} | ${email.subject || '(no subject)'} | ${from}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Formats a single email for list display (standard verbosity)
|
|
64
|
+
* @param {object} email - Email object from Graph API
|
|
65
|
+
* @param {number} index - Index in list (1-based)
|
|
66
|
+
* @returns {string} - Standard markdown format
|
|
67
|
+
*/
|
|
68
|
+
function formatEmailStandard(email, index) {
|
|
69
|
+
const from = email.from?.emailAddress || {
|
|
70
|
+
name: 'Unknown',
|
|
71
|
+
address: 'unknown',
|
|
72
|
+
};
|
|
73
|
+
const date = formatDate(email.receivedDateTime);
|
|
74
|
+
const readStatus = email.isRead ? '' : '**[UNREAD]** ';
|
|
75
|
+
const attachIcon = email.hasAttachments ? ' 📎' : '';
|
|
76
|
+
|
|
77
|
+
return `${index}. ${readStatus}**${email.subject || '(no subject)'}**${attachIcon}
|
|
78
|
+
From: ${from.name} <${from.address}>
|
|
79
|
+
Date: ${date}
|
|
80
|
+
ID: \`${email.id}\``;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Formats a single email for list display (full verbosity)
|
|
85
|
+
* @param {object} email - Email object from Graph API
|
|
86
|
+
* @param {number} index - Index in list (1-based)
|
|
87
|
+
* @returns {string} - Full markdown format with all fields
|
|
88
|
+
*/
|
|
89
|
+
function formatEmailFull(email, index) {
|
|
90
|
+
const from = email.from?.emailAddress || {
|
|
91
|
+
name: 'Unknown',
|
|
92
|
+
address: 'unknown',
|
|
93
|
+
};
|
|
94
|
+
const to = formatRecipients(email.toRecipients);
|
|
95
|
+
const cc = formatRecipients(email.ccRecipients);
|
|
96
|
+
const date = formatDate(email.receivedDateTime);
|
|
97
|
+
const readStatus = email.isRead ? 'Read' : '**UNREAD**';
|
|
98
|
+
const importance = email.importance || 'normal';
|
|
99
|
+
const preview = email.bodyPreview
|
|
100
|
+
? truncateText(email.bodyPreview, DEFAULT_LIMITS.bodyPreviewLength)
|
|
101
|
+
: '';
|
|
102
|
+
|
|
103
|
+
let output = `### ${index}. ${email.subject || '(no subject)'}
|
|
104
|
+
|
|
105
|
+
| Field | Value |
|
|
106
|
+
|-------|-------|
|
|
107
|
+
| From | ${from.name} <${from.address}> |
|
|
108
|
+
| To | ${to} |
|
|
109
|
+
| Date | ${date} |
|
|
110
|
+
| Status | ${readStatus} |
|
|
111
|
+
| Importance | ${importance} |
|
|
112
|
+
| Attachments | ${email.hasAttachments ? 'Yes' : 'No'} |
|
|
113
|
+
| ID | \`${email.id}\` |`;
|
|
114
|
+
|
|
115
|
+
if (cc) {
|
|
116
|
+
output += `\n| CC | ${cc} |`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (preview) {
|
|
120
|
+
output += `\n\n> ${preview}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return output;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Formats an email list as Markdown
|
|
128
|
+
* @param {Array} emails - Array of email objects from Graph API
|
|
129
|
+
* @param {string} folder - Folder name
|
|
130
|
+
* @param {string} verbosity - Verbosity level (minimal/standard/full)
|
|
131
|
+
* @param {object} meta - Metadata (totalAvailable, hasMore, nextPageToken)
|
|
132
|
+
* @returns {string} - Formatted Markdown string
|
|
133
|
+
*/
|
|
134
|
+
function formatEmailList(
|
|
135
|
+
emails,
|
|
136
|
+
folder,
|
|
137
|
+
verbosity = VERBOSITY.STANDARD,
|
|
138
|
+
meta = {}
|
|
139
|
+
) {
|
|
140
|
+
if (!emails || emails.length === 0) {
|
|
141
|
+
return `No emails found in ${folder}.`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const count = emails.length;
|
|
145
|
+
let output = `## Emails in ${folder} (${count}${meta.totalAvailable ? `/${meta.totalAvailable}` : ''})\n\n`;
|
|
146
|
+
|
|
147
|
+
// Format based on verbosity
|
|
148
|
+
const formatFn =
|
|
149
|
+
{
|
|
150
|
+
[VERBOSITY.MINIMAL]: formatEmailMinimal,
|
|
151
|
+
[VERBOSITY.STANDARD]: formatEmailStandard,
|
|
152
|
+
[VERBOSITY.FULL]: formatEmailFull,
|
|
153
|
+
}[verbosity] || formatEmailStandard;
|
|
154
|
+
|
|
155
|
+
output += emails.map((email, i) => formatFn(email, i + 1)).join('\n\n');
|
|
156
|
+
|
|
157
|
+
// Add metadata footer
|
|
158
|
+
if (meta.hasMore || meta.nextPageToken) {
|
|
159
|
+
output += `\n\n---\n_More emails available. ${meta.nextPageToken ? 'Use nextPageToken to continue.' : ''}_`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return output;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Formats an email list as Markdown table (compact format)
|
|
167
|
+
* @param {Array} emails - Array of email objects
|
|
168
|
+
* @param {string} folder - Folder name
|
|
169
|
+
* @param {object} meta - Metadata
|
|
170
|
+
* @returns {string} - Markdown table
|
|
171
|
+
*/
|
|
172
|
+
function formatEmailListAsTable(emails, folder, meta = {}) {
|
|
173
|
+
if (!emails || emails.length === 0) {
|
|
174
|
+
return `No emails found in ${folder}.`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let output = `## Emails in ${folder} (${emails.length}${meta.totalAvailable ? `/${meta.totalAvailable}` : ''})\n\n`;
|
|
178
|
+
output += '| # | Status | Date | From | Subject | ID |\n';
|
|
179
|
+
output += '|---|--------|------|------|---------|----|\n';
|
|
180
|
+
|
|
181
|
+
const rows = emails.slice(0, DEFAULT_LIMITS.maxTableRows);
|
|
182
|
+
rows.forEach((email, i) => {
|
|
183
|
+
const from =
|
|
184
|
+
email.from?.emailAddress?.name ||
|
|
185
|
+
email.from?.emailAddress?.address ||
|
|
186
|
+
'Unknown';
|
|
187
|
+
const date = formatDateShort(email.receivedDateTime);
|
|
188
|
+
const status = email.isRead ? '📖' : '📬';
|
|
189
|
+
const subject = truncateText(email.subject || '(no subject)', 40);
|
|
190
|
+
const shortId = email.id.substring(0, 20) + '...';
|
|
191
|
+
|
|
192
|
+
output += `| ${i + 1} | ${status} | ${date} | ${truncateText(from, 20)} | ${subject} | \`${shortId}\` |\n`;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (emails.length > DEFAULT_LIMITS.maxTableRows) {
|
|
196
|
+
output += `\n_Showing ${DEFAULT_LIMITS.maxTableRows} of ${emails.length} emails._`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (meta.hasMore) {
|
|
200
|
+
output += `\n\n---\n_More emails available._`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return output;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Formats a single email for reading (full content)
|
|
208
|
+
* @param {object} email - Email object from Graph API
|
|
209
|
+
* @param {string} verbosity - Verbosity level
|
|
210
|
+
* @param {object} options - Additional options (includeHeaders, includeRaw)
|
|
211
|
+
* @returns {string} - Formatted Markdown string
|
|
212
|
+
*/
|
|
213
|
+
function formatEmailContent(
|
|
214
|
+
email,
|
|
215
|
+
verbosity = VERBOSITY.STANDARD,
|
|
216
|
+
options = {}
|
|
217
|
+
) {
|
|
218
|
+
const from = formatEmailAddress(email.from?.emailAddress);
|
|
219
|
+
const to = formatRecipients(email.toRecipients);
|
|
220
|
+
const cc = formatRecipients(email.ccRecipients);
|
|
221
|
+
const bcc = formatRecipients(email.bccRecipients);
|
|
222
|
+
const date = formatDate(email.receivedDateTime);
|
|
223
|
+
|
|
224
|
+
let output = `# ${email.subject || '(no subject)'}
|
|
225
|
+
|
|
226
|
+
**From:** ${from}`;
|
|
227
|
+
|
|
228
|
+
if (to) output += `\n**To:** ${to}`;
|
|
229
|
+
|
|
230
|
+
output += `\n**Date:** ${date}`;
|
|
231
|
+
|
|
232
|
+
if (cc) output += `\n**CC:** ${cc}`;
|
|
233
|
+
if (bcc) output += `\n**BCC:** ${bcc}`;
|
|
234
|
+
|
|
235
|
+
output += `\n**Importance:** ${email.importance || 'normal'}`;
|
|
236
|
+
output += `\n**Attachments:** ${email.hasAttachments ? 'Yes' : 'No'}`;
|
|
237
|
+
|
|
238
|
+
if (verbosity === VERBOSITY.FULL) {
|
|
239
|
+
output += `\n**ID:** \`${email.id}\``;
|
|
240
|
+
if (email.conversationId)
|
|
241
|
+
output += `\n**Conversation ID:** \`${email.conversationId}\``;
|
|
242
|
+
if (email.internetMessageId)
|
|
243
|
+
output += `\n**Message-ID:** \`${email.internetMessageId}\``;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
output += '\n\n---\n\n';
|
|
247
|
+
|
|
248
|
+
// Body content
|
|
249
|
+
let body;
|
|
250
|
+
if (verbosity === VERBOSITY.MINIMAL) {
|
|
251
|
+
// At minimal verbosity, show bodyPreview only
|
|
252
|
+
body = email.bodyPreview || '_(Body omitted at minimal verbosity)_';
|
|
253
|
+
} else if (email.body) {
|
|
254
|
+
body =
|
|
255
|
+
email.body.contentType === 'html'
|
|
256
|
+
? stripHtml(email.body.content)
|
|
257
|
+
: email.body.content;
|
|
258
|
+
} else {
|
|
259
|
+
body = email.bodyPreview || 'No content';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Truncate if needed (unless full verbosity requested)
|
|
263
|
+
if (verbosity !== VERBOSITY.FULL) {
|
|
264
|
+
const truncated = truncateWithMeta(body, DEFAULT_LIMITS.maxBodyTruncation);
|
|
265
|
+
if (typeof truncated === 'object') {
|
|
266
|
+
output += truncated.content;
|
|
267
|
+
output += `\n\n---\n_Content truncated (${truncated._fullLength} chars). ${truncated._hint}_`;
|
|
268
|
+
} else {
|
|
269
|
+
output += body;
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
output += body;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Headers if requested
|
|
276
|
+
if (options.includeHeaders && email.internetMessageHeaders) {
|
|
277
|
+
output += formatEmailHeaders(
|
|
278
|
+
email.internetMessageHeaders,
|
|
279
|
+
options.includeAllHeaders
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return output;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Formats email headers for legal/forensic use
|
|
288
|
+
* @param {Array} headers - Array of header objects
|
|
289
|
+
* @param {boolean} includeAll - Include all headers (not just important ones)
|
|
290
|
+
* @returns {string} - Formatted headers section
|
|
291
|
+
*/
|
|
292
|
+
function formatEmailHeaders(headers, includeAll = false) {
|
|
293
|
+
if (!headers || headers.length === 0) return '';
|
|
294
|
+
|
|
295
|
+
const importantHeaders = [
|
|
296
|
+
'Message-ID',
|
|
297
|
+
'Date',
|
|
298
|
+
'Received',
|
|
299
|
+
'DKIM-Signature',
|
|
300
|
+
'Authentication-Results',
|
|
301
|
+
'X-MS-Exchange-Organization-AuthSource',
|
|
302
|
+
'X-MS-Exchange-Organization-AuthAs',
|
|
303
|
+
'Return-Path',
|
|
304
|
+
'X-Originating-IP',
|
|
305
|
+
'X-MS-Has-Attach',
|
|
306
|
+
'SPF',
|
|
307
|
+
'DMARC',
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
const filteredHeaders = includeAll
|
|
311
|
+
? headers
|
|
312
|
+
: headers.filter((h) =>
|
|
313
|
+
importantHeaders.some((ih) =>
|
|
314
|
+
h.name.toLowerCase().startsWith(ih.toLowerCase())
|
|
315
|
+
)
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (filteredHeaders.length === 0) return '';
|
|
319
|
+
|
|
320
|
+
let output = '\n\n---\n\n## Email Headers (Legal/Forensic)\n\n';
|
|
321
|
+
output += '| Header | Value |\n|--------|-------|\n';
|
|
322
|
+
|
|
323
|
+
filteredHeaders.forEach((h) => {
|
|
324
|
+
const value = truncateText(h.value, 60);
|
|
325
|
+
output += `| ${h.name} | \`${value}\` |\n`;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return output;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Creates a response metadata block
|
|
333
|
+
* @param {object} data - Response data
|
|
334
|
+
* @returns {object} - Metadata object
|
|
335
|
+
*/
|
|
336
|
+
function createResponseMeta(data) {
|
|
337
|
+
return {
|
|
338
|
+
returned: data.returned || 0,
|
|
339
|
+
totalAvailable: data.totalAvailable || null,
|
|
340
|
+
hasMore: data.hasMore || false,
|
|
341
|
+
nextPageToken: data.nextPageToken || null,
|
|
342
|
+
verbosity: data.verbosity || VERBOSITY.STANDARD,
|
|
343
|
+
truncated: data.truncated || false,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Wraps response content with MCP format
|
|
349
|
+
* @param {string} text - Response text
|
|
350
|
+
* @param {object} meta - Optional metadata
|
|
351
|
+
* @returns {object} - MCP response object
|
|
352
|
+
*/
|
|
353
|
+
function wrapMcpResponse(text, meta = null) {
|
|
354
|
+
const response = {
|
|
355
|
+
content: [{ type: 'text', text }],
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
if (meta) {
|
|
359
|
+
response._meta = meta;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return response;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ============ Helper Functions ============
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Formats a date string
|
|
369
|
+
*/
|
|
370
|
+
function formatDate(dateStr) {
|
|
371
|
+
if (!dateStr) return 'Unknown';
|
|
372
|
+
const date = new Date(dateStr);
|
|
373
|
+
return date.toLocaleString('en-AU', {
|
|
374
|
+
day: '2-digit',
|
|
375
|
+
month: 'short',
|
|
376
|
+
year: 'numeric',
|
|
377
|
+
hour: '2-digit',
|
|
378
|
+
minute: '2-digit',
|
|
379
|
+
hour12: true,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Formats a date string (short version for tables)
|
|
385
|
+
*/
|
|
386
|
+
function formatDateShort(dateStr) {
|
|
387
|
+
if (!dateStr) return 'Unknown';
|
|
388
|
+
const date = new Date(dateStr);
|
|
389
|
+
return date.toLocaleDateString('en-AU', {
|
|
390
|
+
day: '2-digit',
|
|
391
|
+
month: 'short',
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Formats an email address object
|
|
397
|
+
*/
|
|
398
|
+
function formatEmailAddress(addr) {
|
|
399
|
+
if (!addr) return 'Unknown';
|
|
400
|
+
return `${addr.name || addr.address} <${addr.address}>`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Formats recipient array to string
|
|
405
|
+
*/
|
|
406
|
+
function formatRecipients(recipients) {
|
|
407
|
+
if (!recipients || recipients.length === 0) return '';
|
|
408
|
+
return recipients.map((r) => formatEmailAddress(r.emailAddress)).join(', ');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Truncates text to specified length
|
|
413
|
+
*/
|
|
414
|
+
function truncateText(text, maxLength) {
|
|
415
|
+
if (!text) return '';
|
|
416
|
+
if (text.length <= maxLength) return text;
|
|
417
|
+
return text.substring(0, maxLength - 3) + '...';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Strips HTML tags (simple implementation)
|
|
422
|
+
*/
|
|
423
|
+
function stripHtml(html) {
|
|
424
|
+
if (!html) return '';
|
|
425
|
+
return html
|
|
426
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
427
|
+
.replace(/<\/p>/gi, '\n\n')
|
|
428
|
+
.replace(/<[^>]*>/g, '')
|
|
429
|
+
.replace(/ /g, ' ')
|
|
430
|
+
.replace(/&/g, '&')
|
|
431
|
+
.replace(/</g, '<')
|
|
432
|
+
.replace(/>/g, '>')
|
|
433
|
+
.replace(/"/g, '"')
|
|
434
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
435
|
+
.trim();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
module.exports = {
|
|
439
|
+
VERBOSITY,
|
|
440
|
+
DEFAULT_LIMITS,
|
|
441
|
+
truncateWithMeta,
|
|
442
|
+
formatEmailMinimal,
|
|
443
|
+
formatEmailStandard,
|
|
444
|
+
formatEmailFull,
|
|
445
|
+
formatEmailList,
|
|
446
|
+
formatEmailListAsTable,
|
|
447
|
+
formatEmailContent,
|
|
448
|
+
formatEmailHeaders,
|
|
449
|
+
createResponseMeta,
|
|
450
|
+
wrapMcpResponse,
|
|
451
|
+
formatDate,
|
|
452
|
+
formatDateShort,
|
|
453
|
+
formatEmailAddress,
|
|
454
|
+
formatRecipients,
|
|
455
|
+
truncateText,
|
|
456
|
+
stripHtml,
|
|
457
|
+
};
|