@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,666 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation threading functionality
|
|
3
|
+
*
|
|
4
|
+
* Manages email conversation threads - listing, retrieving, and exporting.
|
|
5
|
+
*/
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const {
|
|
9
|
+
callGraphAPI,
|
|
10
|
+
callGraphAPIRaw,
|
|
11
|
+
callGraphAPIPaginated: _callGraphAPIPaginated,
|
|
12
|
+
} = require('../utils/graph-api');
|
|
13
|
+
const { ensureAuthenticated } = require('../auth');
|
|
14
|
+
const { getEmailFields } = require('../utils/field-presets');
|
|
15
|
+
const {
|
|
16
|
+
formatEmailContent,
|
|
17
|
+
VERBOSITY,
|
|
18
|
+
} = require('../utils/response-formatter');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format a date for filenames
|
|
22
|
+
* @param {string} isoDate - ISO date string
|
|
23
|
+
* @returns {string} - Formatted date string
|
|
24
|
+
*/
|
|
25
|
+
function formatDateForFilename(isoDate) {
|
|
26
|
+
if (!isoDate) return 'unknown-date';
|
|
27
|
+
const date = new Date(isoDate);
|
|
28
|
+
return date.toISOString().split('T')[0];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sanitize string for filename
|
|
33
|
+
* @param {string} str - String to sanitize
|
|
34
|
+
* @param {number} maxLength - Max length
|
|
35
|
+
* @returns {string} - Sanitized string
|
|
36
|
+
*/
|
|
37
|
+
function sanitizeForFilename(str, maxLength = 50) {
|
|
38
|
+
if (!str) return 'untitled';
|
|
39
|
+
return str
|
|
40
|
+
.replace(/[<>:"/\\|?*]/g, '')
|
|
41
|
+
.replace(/\s+/g, '-')
|
|
42
|
+
.substring(0, maxLength)
|
|
43
|
+
.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* List conversations handler - groups emails by conversationId
|
|
48
|
+
* @param {object} args - Tool arguments
|
|
49
|
+
* @param {string} [args.folder] - Folder to search (default: inbox)
|
|
50
|
+
* @param {number} [args.count] - Number of conversations to return (default: 20, max: 50)
|
|
51
|
+
* @param {string} [args.outputVerbosity] - Output verbosity level
|
|
52
|
+
* @returns {object} - MCP response with conversation list
|
|
53
|
+
*/
|
|
54
|
+
async function handleListConversations(args) {
|
|
55
|
+
const folder = args.folder || 'inbox';
|
|
56
|
+
const count = Math.min(args.count || 20, 50);
|
|
57
|
+
const verbosity = args.outputVerbosity || VERBOSITY.STANDARD;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const accessToken = await ensureAuthenticated();
|
|
61
|
+
|
|
62
|
+
// Get recent emails with conversation fields
|
|
63
|
+
const selectFields = [
|
|
64
|
+
'id',
|
|
65
|
+
'subject',
|
|
66
|
+
'from',
|
|
67
|
+
'receivedDateTime',
|
|
68
|
+
'conversationId',
|
|
69
|
+
'conversationIndex',
|
|
70
|
+
'isRead',
|
|
71
|
+
'bodyPreview',
|
|
72
|
+
].join(',');
|
|
73
|
+
|
|
74
|
+
const endpoint = `me/mailFolders/${folder}/messages`;
|
|
75
|
+
const queryParams = {
|
|
76
|
+
$select: selectFields,
|
|
77
|
+
$orderby: 'receivedDateTime desc',
|
|
78
|
+
$top: 200, // Get more to group
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const response = await callGraphAPI(
|
|
82
|
+
accessToken,
|
|
83
|
+
'GET',
|
|
84
|
+
endpoint,
|
|
85
|
+
null,
|
|
86
|
+
queryParams
|
|
87
|
+
);
|
|
88
|
+
const messages = response.value || [];
|
|
89
|
+
|
|
90
|
+
// Group by conversationId
|
|
91
|
+
const conversations = new Map();
|
|
92
|
+
|
|
93
|
+
messages.forEach((msg) => {
|
|
94
|
+
const convId = msg.conversationId;
|
|
95
|
+
if (!conversations.has(convId)) {
|
|
96
|
+
conversations.set(convId, {
|
|
97
|
+
conversationId: convId,
|
|
98
|
+
subject: msg.subject,
|
|
99
|
+
messages: [],
|
|
100
|
+
participants: new Set(),
|
|
101
|
+
firstDate: msg.receivedDateTime,
|
|
102
|
+
lastDate: msg.receivedDateTime,
|
|
103
|
+
unreadCount: 0,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const conv = conversations.get(convId);
|
|
108
|
+
conv.messages.push(msg);
|
|
109
|
+
conv.lastDate = msg.receivedDateTime;
|
|
110
|
+
if (msg.from?.emailAddress?.address) {
|
|
111
|
+
conv.participants.add(msg.from.emailAddress.address);
|
|
112
|
+
}
|
|
113
|
+
if (!msg.isRead) {
|
|
114
|
+
conv.unreadCount++;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Convert to array and sort by most recent
|
|
119
|
+
let conversationList = Array.from(conversations.values())
|
|
120
|
+
.sort((a, b) => new Date(b.lastDate) - new Date(a.lastDate))
|
|
121
|
+
.slice(0, count);
|
|
122
|
+
|
|
123
|
+
// Format output
|
|
124
|
+
let output = [];
|
|
125
|
+
output.push(`# Email Conversations\n`);
|
|
126
|
+
output.push(`**Folder**: ${folder}`);
|
|
127
|
+
output.push(`**Conversations**: ${conversationList.length}`);
|
|
128
|
+
output.push(`**Total Messages Scanned**: ${messages.length}\n`);
|
|
129
|
+
output.push('---\n');
|
|
130
|
+
|
|
131
|
+
conversationList.forEach((conv, index) => {
|
|
132
|
+
output.push(`## ${index + 1}. ${conv.subject || '(no subject)'}`);
|
|
133
|
+
output.push(`- **Messages**: ${conv.messages.length}`);
|
|
134
|
+
output.push(`- **Unread**: ${conv.unreadCount}`);
|
|
135
|
+
output.push(
|
|
136
|
+
`- **Participants**: ${Array.from(conv.participants).slice(0, 5).join(', ')}${conv.participants.size > 5 ? '...' : ''}`
|
|
137
|
+
);
|
|
138
|
+
output.push(
|
|
139
|
+
`- **Date Range**: ${formatDateForFilename(conv.firstDate)} → ${formatDateForFilename(conv.lastDate)}`
|
|
140
|
+
);
|
|
141
|
+
output.push(`- **Conversation ID**: \`${conv.conversationId}\``);
|
|
142
|
+
|
|
143
|
+
if (verbosity === VERBOSITY.FULL && conv.messages.length > 0) {
|
|
144
|
+
output.push(
|
|
145
|
+
`\n**Latest Preview**: ${conv.messages[0].bodyPreview?.substring(0, 100) || ''}...`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
output.push('');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: 'text',
|
|
155
|
+
text: output.join('\n'),
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
_meta: {
|
|
159
|
+
folder,
|
|
160
|
+
conversationCount: conversationList.length,
|
|
161
|
+
totalMessagesScanned: messages.length,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error.message === 'Authentication required') {
|
|
166
|
+
return {
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: 'text',
|
|
170
|
+
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
content: [
|
|
177
|
+
{ type: 'text', text: `Error listing conversations: ${error.message}` },
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get conversation handler - retrieves all messages in a thread
|
|
185
|
+
* @param {object} args - Tool arguments
|
|
186
|
+
* @param {string} args.conversationId - Conversation ID (required)
|
|
187
|
+
* @param {boolean} [args.includeHeaders] - Include email headers
|
|
188
|
+
* @param {string} [args.outputVerbosity] - Output verbosity level
|
|
189
|
+
* @returns {object} - MCP response with conversation messages
|
|
190
|
+
*/
|
|
191
|
+
async function handleGetConversation(args) {
|
|
192
|
+
const conversationId = args.conversationId;
|
|
193
|
+
const includeHeaders = args.includeHeaders || false;
|
|
194
|
+
const verbosity = args.outputVerbosity || VERBOSITY.STANDARD;
|
|
195
|
+
|
|
196
|
+
if (!conversationId) {
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: 'text', text: 'Conversation ID is required.' }],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const accessToken = await ensureAuthenticated();
|
|
204
|
+
|
|
205
|
+
// Select appropriate fields
|
|
206
|
+
const fieldPreset = includeHeaders ? 'forensic' : 'conversation';
|
|
207
|
+
const selectFields = getEmailFields(fieldPreset);
|
|
208
|
+
|
|
209
|
+
// Search all folders for messages with this conversation ID
|
|
210
|
+
const endpoint = 'me/messages';
|
|
211
|
+
const queryParams = {
|
|
212
|
+
$select: selectFields,
|
|
213
|
+
$filter: `conversationId eq '${conversationId}'`,
|
|
214
|
+
$orderby: 'receivedDateTime asc',
|
|
215
|
+
$top: 100,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
let response;
|
|
219
|
+
try {
|
|
220
|
+
response = await callGraphAPI(
|
|
221
|
+
accessToken,
|
|
222
|
+
'GET',
|
|
223
|
+
endpoint,
|
|
224
|
+
null,
|
|
225
|
+
queryParams
|
|
226
|
+
);
|
|
227
|
+
} catch (apiError) {
|
|
228
|
+
if (
|
|
229
|
+
apiError.message.includes('ErrorInvalidUrlQueryFilter') ||
|
|
230
|
+
apiError.message.includes('InefficientFilter') ||
|
|
231
|
+
apiError.message.includes('filter')
|
|
232
|
+
) {
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: `Conversation retrieval by conversationId is not supported on personal Microsoft accounts. Use read-email with individual message IDs instead.`,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
throw apiError;
|
|
243
|
+
}
|
|
244
|
+
const messages = response.value || [];
|
|
245
|
+
|
|
246
|
+
if (messages.length === 0) {
|
|
247
|
+
return {
|
|
248
|
+
content: [
|
|
249
|
+
{
|
|
250
|
+
type: 'text',
|
|
251
|
+
text: `No messages found for conversation ID: ${conversationId}`,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Format output
|
|
258
|
+
let output = [];
|
|
259
|
+
output.push(`# Email Conversation\n`);
|
|
260
|
+
output.push(`**Subject**: ${messages[0].subject || '(no subject)'}`);
|
|
261
|
+
output.push(`**Messages**: ${messages.length}`);
|
|
262
|
+
output.push(`**Conversation ID**: \`${conversationId}\`\n`);
|
|
263
|
+
output.push('---\n');
|
|
264
|
+
|
|
265
|
+
messages.forEach((msg, index) => {
|
|
266
|
+
output.push(`## Message ${index + 1} of ${messages.length}`);
|
|
267
|
+
output.push(formatEmailContent(msg, verbosity, { includeHeaders }));
|
|
268
|
+
output.push('\n---\n');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
content: [
|
|
273
|
+
{
|
|
274
|
+
type: 'text',
|
|
275
|
+
text: output.join('\n'),
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
_meta: {
|
|
279
|
+
conversationId,
|
|
280
|
+
messageCount: messages.length,
|
|
281
|
+
subject: messages[0]?.subject,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (error.message === 'Authentication required') {
|
|
286
|
+
return {
|
|
287
|
+
content: [
|
|
288
|
+
{
|
|
289
|
+
type: 'text',
|
|
290
|
+
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
content: [
|
|
297
|
+
{ type: 'text', text: `Error getting conversation: ${error.message}` },
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Export conversation handler - exports entire thread to various formats
|
|
305
|
+
* @param {object} args - Tool arguments
|
|
306
|
+
* @param {string} args.conversationId - Conversation ID (required)
|
|
307
|
+
* @param {string} [args.format] - Export format (eml, mbox, markdown, json, html)
|
|
308
|
+
* @param {string} [args.outputDir] - Output directory (required)
|
|
309
|
+
* @param {boolean} [args.includeAttachments] - Include attachments (default: true)
|
|
310
|
+
* @param {string} [args.order] - Message order: 'chronological' or 'reverse' (default: chronological)
|
|
311
|
+
* @returns {object} - MCP response with export status
|
|
312
|
+
*/
|
|
313
|
+
async function handleExportConversation(args) {
|
|
314
|
+
const conversationId = args.conversationId;
|
|
315
|
+
const format = (args.format || 'markdown').toLowerCase();
|
|
316
|
+
const outputDir = args.outputDir;
|
|
317
|
+
const _includeAttachments = args.includeAttachments !== false;
|
|
318
|
+
const order = args.order || 'chronological';
|
|
319
|
+
|
|
320
|
+
if (!conversationId) {
|
|
321
|
+
return {
|
|
322
|
+
content: [{ type: 'text', text: 'Conversation ID is required.' }],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!outputDir) {
|
|
327
|
+
return {
|
|
328
|
+
content: [{ type: 'text', text: 'Output directory is required.' }],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const validFormats = ['eml', 'mbox', 'markdown', 'json', 'html'];
|
|
333
|
+
if (!validFormats.includes(format)) {
|
|
334
|
+
return {
|
|
335
|
+
content: [
|
|
336
|
+
{
|
|
337
|
+
type: 'text',
|
|
338
|
+
text: `Invalid format. Use: ${validFormats.join(', ')}`,
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const accessToken = await ensureAuthenticated();
|
|
346
|
+
|
|
347
|
+
// Get all messages in conversation
|
|
348
|
+
const selectFields = getEmailFields('export');
|
|
349
|
+
const endpoint = 'me/messages';
|
|
350
|
+
const queryParams = {
|
|
351
|
+
$select: selectFields,
|
|
352
|
+
$filter: `conversationId eq '${conversationId}'`,
|
|
353
|
+
$orderby: `receivedDateTime ${order === 'reverse' ? 'desc' : 'asc'}`,
|
|
354
|
+
$top: 100,
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
let response;
|
|
358
|
+
try {
|
|
359
|
+
response = await callGraphAPI(
|
|
360
|
+
accessToken,
|
|
361
|
+
'GET',
|
|
362
|
+
endpoint,
|
|
363
|
+
null,
|
|
364
|
+
queryParams
|
|
365
|
+
);
|
|
366
|
+
} catch (apiError) {
|
|
367
|
+
if (
|
|
368
|
+
apiError.message.includes('ErrorInvalidUrlQueryFilter') ||
|
|
369
|
+
apiError.message.includes('InefficientFilter') ||
|
|
370
|
+
apiError.message.includes('filter')
|
|
371
|
+
) {
|
|
372
|
+
return {
|
|
373
|
+
content: [
|
|
374
|
+
{
|
|
375
|
+
type: 'text',
|
|
376
|
+
text: `Conversation export is not supported on personal Microsoft accounts. Use export with target=message and individual message IDs instead.`,
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
throw apiError;
|
|
382
|
+
}
|
|
383
|
+
const messages = response.value || [];
|
|
384
|
+
|
|
385
|
+
if (messages.length === 0) {
|
|
386
|
+
return {
|
|
387
|
+
content: [
|
|
388
|
+
{
|
|
389
|
+
type: 'text',
|
|
390
|
+
text: `No messages found for conversation ID: ${conversationId}`,
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Create output directory
|
|
397
|
+
const resolvedDir = path.resolve(outputDir);
|
|
398
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
399
|
+
fs.mkdirSync(resolvedDir, { recursive: true });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Generate filename base
|
|
403
|
+
const subject = sanitizeForFilename(messages[0].subject);
|
|
404
|
+
const date = formatDateForFilename(messages[0].receivedDateTime);
|
|
405
|
+
const filenameBase = `${date}_${subject}_conversation`;
|
|
406
|
+
|
|
407
|
+
let exportedFiles = [];
|
|
408
|
+
let exportStats = { messages: messages.length, attachments: 0, bytes: 0 };
|
|
409
|
+
|
|
410
|
+
switch (format) {
|
|
411
|
+
case 'eml': {
|
|
412
|
+
// Export each message as individual .eml file
|
|
413
|
+
const emlDir = path.join(resolvedDir, filenameBase);
|
|
414
|
+
if (!fs.existsSync(emlDir)) {
|
|
415
|
+
fs.mkdirSync(emlDir, { recursive: true });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
for (let i = 0; i < messages.length; i++) {
|
|
419
|
+
const msg = messages[i];
|
|
420
|
+
const mimeContent = await callGraphAPIRaw(accessToken, msg.id);
|
|
421
|
+
const msgDate = formatDateForFilename(msg.receivedDateTime);
|
|
422
|
+
const emlPath = path.join(
|
|
423
|
+
emlDir,
|
|
424
|
+
`${i + 1}_${msgDate}_${sanitizeForFilename(msg.from?.emailAddress?.name || 'unknown', 20)}.eml`
|
|
425
|
+
);
|
|
426
|
+
fs.writeFileSync(emlPath, mimeContent, 'utf8');
|
|
427
|
+
exportStats.bytes += Buffer.byteLength(mimeContent, 'utf8');
|
|
428
|
+
exportedFiles.push(emlPath);
|
|
429
|
+
}
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
case 'mbox': {
|
|
434
|
+
// Export all messages to single MBOX file
|
|
435
|
+
const mboxPath = path.join(resolvedDir, `${filenameBase}.mbox`);
|
|
436
|
+
let mboxContent = '';
|
|
437
|
+
|
|
438
|
+
for (const msg of messages) {
|
|
439
|
+
const mimeContent = await callGraphAPIRaw(accessToken, msg.id);
|
|
440
|
+
const from = msg.from?.emailAddress?.address || 'unknown@unknown.com';
|
|
441
|
+
const date = new Date(msg.receivedDateTime);
|
|
442
|
+
const mboxDate = date.toUTCString().replace('GMT', '+0000');
|
|
443
|
+
|
|
444
|
+
// MBOX format: From line + MIME content + blank line
|
|
445
|
+
mboxContent += `From ${from} ${mboxDate}\n`;
|
|
446
|
+
mboxContent += mimeContent.replace(/^From /gm, '>From '); // Escape From lines
|
|
447
|
+
mboxContent += '\n\n';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
fs.writeFileSync(mboxPath, mboxContent, 'utf8');
|
|
451
|
+
exportStats.bytes = Buffer.byteLength(mboxContent, 'utf8');
|
|
452
|
+
exportedFiles.push(mboxPath);
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
case 'markdown': {
|
|
457
|
+
// Export as threaded Markdown document
|
|
458
|
+
const mdPath = path.join(resolvedDir, `${filenameBase}.md`);
|
|
459
|
+
let mdContent = [];
|
|
460
|
+
|
|
461
|
+
mdContent.push(
|
|
462
|
+
`# Email Conversation: ${messages[0].subject || '(no subject)'}\n`
|
|
463
|
+
);
|
|
464
|
+
mdContent.push(`**Exported**: ${new Date().toISOString()}`);
|
|
465
|
+
mdContent.push(`**Messages**: ${messages.length}`);
|
|
466
|
+
mdContent.push(`**Conversation ID**: \`${conversationId}\`\n`);
|
|
467
|
+
mdContent.push('---\n');
|
|
468
|
+
|
|
469
|
+
for (let i = 0; i < messages.length; i++) {
|
|
470
|
+
const msg = messages[i];
|
|
471
|
+
mdContent.push(`## Message ${i + 1}/${messages.length}\n`);
|
|
472
|
+
mdContent.push(
|
|
473
|
+
`**From**: ${msg.from?.emailAddress?.name || ''} <${msg.from?.emailAddress?.address || ''}>`
|
|
474
|
+
);
|
|
475
|
+
mdContent.push(
|
|
476
|
+
`**To**: ${msg.toRecipients?.map((r) => r.emailAddress?.address).join(', ') || ''}`
|
|
477
|
+
);
|
|
478
|
+
if (msg.ccRecipients?.length) {
|
|
479
|
+
mdContent.push(
|
|
480
|
+
`**CC**: ${msg.ccRecipients.map((r) => r.emailAddress?.address).join(', ')}`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
mdContent.push(`**Date**: ${msg.receivedDateTime}`);
|
|
484
|
+
mdContent.push(`**Subject**: ${msg.subject || '(no subject)'}\n`);
|
|
485
|
+
|
|
486
|
+
// Body content
|
|
487
|
+
if (msg.body?.content) {
|
|
488
|
+
if (msg.body.contentType === 'html') {
|
|
489
|
+
// Simple HTML to text conversion
|
|
490
|
+
const text = msg.body.content
|
|
491
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
492
|
+
.replace(/<\/p>/gi, '\n\n')
|
|
493
|
+
.replace(/<[^>]+>/g, '')
|
|
494
|
+
.replace(/ /g, ' ')
|
|
495
|
+
.replace(/</g, '<')
|
|
496
|
+
.replace(/>/g, '>')
|
|
497
|
+
.replace(/&/g, '&');
|
|
498
|
+
mdContent.push(text.trim());
|
|
499
|
+
} else {
|
|
500
|
+
mdContent.push(msg.body.content);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (msg.hasAttachments) {
|
|
505
|
+
mdContent.push(
|
|
506
|
+
`\n*[${msg.hasAttachments ? 'Has attachments' : 'No attachments'}]*`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
mdContent.push('\n---\n');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const content = mdContent.join('\n');
|
|
514
|
+
fs.writeFileSync(mdPath, content, 'utf8');
|
|
515
|
+
exportStats.bytes = Buffer.byteLength(content, 'utf8');
|
|
516
|
+
exportedFiles.push(mdPath);
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
case 'json': {
|
|
521
|
+
// Export as JSON
|
|
522
|
+
const jsonPath = path.join(resolvedDir, `${filenameBase}.json`);
|
|
523
|
+
const jsonContent = JSON.stringify(
|
|
524
|
+
{
|
|
525
|
+
conversationId,
|
|
526
|
+
subject: messages[0].subject,
|
|
527
|
+
exportedAt: new Date().toISOString(),
|
|
528
|
+
messageCount: messages.length,
|
|
529
|
+
messages: messages,
|
|
530
|
+
},
|
|
531
|
+
null,
|
|
532
|
+
2
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
fs.writeFileSync(jsonPath, jsonContent, 'utf8');
|
|
536
|
+
exportStats.bytes = Buffer.byteLength(jsonContent, 'utf8');
|
|
537
|
+
exportedFiles.push(jsonPath);
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
case 'html': {
|
|
542
|
+
// Export as HTML document
|
|
543
|
+
const htmlPath = path.join(resolvedDir, `${filenameBase}.html`);
|
|
544
|
+
let htmlContent = [];
|
|
545
|
+
|
|
546
|
+
htmlContent.push('<!DOCTYPE html>');
|
|
547
|
+
htmlContent.push('<html><head>');
|
|
548
|
+
htmlContent.push(
|
|
549
|
+
`<title>${messages[0].subject || 'Email Conversation'}</title>`
|
|
550
|
+
);
|
|
551
|
+
htmlContent.push('<meta charset="utf-8">');
|
|
552
|
+
htmlContent.push('<style>');
|
|
553
|
+
htmlContent.push(
|
|
554
|
+
'body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }'
|
|
555
|
+
);
|
|
556
|
+
htmlContent.push(
|
|
557
|
+
'.message { border: 1px solid #ddd; margin: 20px 0; padding: 15px; border-radius: 5px; }'
|
|
558
|
+
);
|
|
559
|
+
htmlContent.push(
|
|
560
|
+
'.header { color: #666; font-size: 0.9em; margin-bottom: 10px; }'
|
|
561
|
+
);
|
|
562
|
+
htmlContent.push('.body { white-space: pre-wrap; }');
|
|
563
|
+
htmlContent.push('</style>');
|
|
564
|
+
htmlContent.push('</head><body>');
|
|
565
|
+
htmlContent.push(
|
|
566
|
+
`<h1>${messages[0].subject || 'Email Conversation'}</h1>`
|
|
567
|
+
);
|
|
568
|
+
htmlContent.push(
|
|
569
|
+
`<p><strong>Messages:</strong> ${messages.length} | <strong>Exported:</strong> ${new Date().toISOString()}</p>`
|
|
570
|
+
);
|
|
571
|
+
htmlContent.push('<hr>');
|
|
572
|
+
|
|
573
|
+
for (let i = 0; i < messages.length; i++) {
|
|
574
|
+
const msg = messages[i];
|
|
575
|
+
htmlContent.push('<div class="message">');
|
|
576
|
+
htmlContent.push('<div class="header">');
|
|
577
|
+
htmlContent.push(
|
|
578
|
+
`<strong>From:</strong> ${msg.from?.emailAddress?.name || ''} <${msg.from?.emailAddress?.address || ''}><br>`
|
|
579
|
+
);
|
|
580
|
+
htmlContent.push(
|
|
581
|
+
`<strong>Date:</strong> ${msg.receivedDateTime}<br>`
|
|
582
|
+
);
|
|
583
|
+
htmlContent.push(
|
|
584
|
+
`<strong>Subject:</strong> ${msg.subject || '(no subject)'}`
|
|
585
|
+
);
|
|
586
|
+
htmlContent.push('</div>');
|
|
587
|
+
htmlContent.push('<div class="body">');
|
|
588
|
+
|
|
589
|
+
if (msg.body?.contentType === 'html') {
|
|
590
|
+
htmlContent.push(msg.body.content);
|
|
591
|
+
} else {
|
|
592
|
+
htmlContent.push(`<pre>${msg.body?.content || ''}</pre>`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
htmlContent.push('</div></div>');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
htmlContent.push('</body></html>');
|
|
599
|
+
const content = htmlContent.join('\n');
|
|
600
|
+
fs.writeFileSync(htmlPath, content, 'utf8');
|
|
601
|
+
exportStats.bytes = Buffer.byteLength(content, 'utf8');
|
|
602
|
+
exportedFiles.push(htmlPath);
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Format result
|
|
608
|
+
const sizeFormatted =
|
|
609
|
+
exportStats.bytes < 1024
|
|
610
|
+
? `${exportStats.bytes} B`
|
|
611
|
+
: exportStats.bytes < 1024 * 1024
|
|
612
|
+
? `${(exportStats.bytes / 1024).toFixed(1)} KB`
|
|
613
|
+
: `${(exportStats.bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
614
|
+
|
|
615
|
+
let output = [];
|
|
616
|
+
output.push(`# Conversation Exported\n`);
|
|
617
|
+
output.push(`**Subject**: ${messages[0].subject || '(no subject)'}`);
|
|
618
|
+
output.push(`**Format**: ${format.toUpperCase()}`);
|
|
619
|
+
output.push(`**Messages**: ${exportStats.messages}`);
|
|
620
|
+
output.push(`**Total Size**: ${sizeFormatted}`);
|
|
621
|
+
output.push(`**Output Directory**: ${resolvedDir}\n`);
|
|
622
|
+
output.push('## Exported Files\n');
|
|
623
|
+
exportedFiles.forEach((f) => output.push(`- \`${path.basename(f)}\``));
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
content: [
|
|
627
|
+
{
|
|
628
|
+
type: 'text',
|
|
629
|
+
text: output.join('\n'),
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
_meta: {
|
|
633
|
+
conversationId,
|
|
634
|
+
format,
|
|
635
|
+
messageCount: exportStats.messages,
|
|
636
|
+
bytes: exportStats.bytes,
|
|
637
|
+
files: exportedFiles,
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
} catch (error) {
|
|
641
|
+
if (error.message === 'Authentication required') {
|
|
642
|
+
return {
|
|
643
|
+
content: [
|
|
644
|
+
{
|
|
645
|
+
type: 'text',
|
|
646
|
+
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
content: [
|
|
653
|
+
{
|
|
654
|
+
type: 'text',
|
|
655
|
+
text: `Error exporting conversation: ${error.message}`,
|
|
656
|
+
},
|
|
657
|
+
],
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
module.exports = {
|
|
663
|
+
handleListConversations,
|
|
664
|
+
handleGetConversation,
|
|
665
|
+
handleExportConversation,
|
|
666
|
+
};
|