@littlebearapps/outlook-assistant 3.5.1 → 3.6.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/README.md +13 -9
- package/auth/index.js +2 -1
- package/auth/tools.js +8 -1
- package/categories/index.js +26 -5
- package/email/conversations.js +59 -1
- package/email/draft.js +484 -0
- package/email/index.js +80 -0
- package/email/mail-tips.js +36 -3
- package/email/search.js +132 -59
- package/email/send.js +32 -43
- package/index.js +4 -1
- package/llms.txt +6 -6
- package/package.json +2 -2
- package/utils/field-presets.js +20 -0
- package/utils/graph-api.js +8 -0
package/email/search.js
CHANGED
|
@@ -152,33 +152,47 @@ async function progressiveSearch(
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
155
|
+
// Check if we have any actual search terms (not just boolean filters)
|
|
156
|
+
const hasSearchTerms =
|
|
157
|
+
searchTerms.query ||
|
|
158
|
+
searchTerms.from ||
|
|
159
|
+
searchTerms.to ||
|
|
160
|
+
searchTerms.subject;
|
|
161
|
+
|
|
162
|
+
// 1. Try combined search (most specific) — skip if only boolean filters
|
|
163
|
+
if (
|
|
164
|
+
!hasSearchTerms &&
|
|
165
|
+
(filterTerms.hasAttachments === true || filterTerms.unreadOnly === true)
|
|
166
|
+
) {
|
|
167
|
+
// Skip directly to boolean-only filter (step 3) — combined search is redundant
|
|
168
|
+
console.error('Only boolean filters provided, skipping combined search');
|
|
169
|
+
} else
|
|
170
|
+
try {
|
|
171
|
+
const params = buildSearchParams(
|
|
172
|
+
searchTerms,
|
|
173
|
+
filterTerms,
|
|
174
|
+
Math.min(50, maxCount),
|
|
175
|
+
selectFields
|
|
176
|
+
);
|
|
177
|
+
console.error('Attempting combined search with params:', params);
|
|
178
|
+
searchAttempts.push('combined-search');
|
|
165
179
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
);
|
|
173
|
-
if (response.value && response.value.length > 0) {
|
|
174
|
-
console.error(
|
|
175
|
-
`Combined search successful: found ${response.value.length} results`
|
|
180
|
+
const response = await callGraphAPIPaginated(
|
|
181
|
+
accessToken,
|
|
182
|
+
'GET',
|
|
183
|
+
endpoint,
|
|
184
|
+
params,
|
|
185
|
+
maxCount
|
|
176
186
|
);
|
|
177
|
-
|
|
187
|
+
if (response.value && response.value.length > 0) {
|
|
188
|
+
console.error(
|
|
189
|
+
`Combined search successful: found ${response.value.length} results`
|
|
190
|
+
);
|
|
191
|
+
return response;
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(`Combined search failed: ${error.message}`);
|
|
178
195
|
}
|
|
179
|
-
} catch (error) {
|
|
180
|
-
console.error(`Combined search failed: ${error.message}`);
|
|
181
|
-
}
|
|
182
196
|
|
|
183
197
|
// 2. Try each search term individually, starting with most specific
|
|
184
198
|
const searchPriority = ['from', 'to', 'subject', 'query'];
|
|
@@ -200,23 +214,16 @@ async function progressiveSearch(
|
|
|
200
214
|
// $search for free-text query only.
|
|
201
215
|
// NOTE: $filter and $orderby cannot be used together on mailbox - Graph API limitation
|
|
202
216
|
if (term === 'from') {
|
|
203
|
-
|
|
204
|
-
simplifiedParams.$filter = `from/emailAddress/address eq '${searchTerms[term]}'`;
|
|
205
|
-
} else {
|
|
206
|
-
simplifiedParams.$filter = `contains(from/emailAddress/name, '${searchTerms[term]}')`;
|
|
207
|
-
}
|
|
217
|
+
simplifiedParams.$filter = buildFromFilter(searchTerms[term]);
|
|
208
218
|
} else if (term === 'to') {
|
|
209
|
-
|
|
210
|
-
simplifiedParams.$filter = `toRecipients/any(r: r/emailAddress/address eq '${searchTerms[term]}')`;
|
|
211
|
-
} else {
|
|
212
|
-
simplifiedParams.$filter = `toRecipients/any(r: contains(r/emailAddress/name, '${searchTerms[term]}'))`;
|
|
213
|
-
}
|
|
219
|
+
simplifiedParams.$filter = buildToFilter(searchTerms[term]);
|
|
214
220
|
} else if (term === 'subject') {
|
|
215
221
|
// Use $filter with contains() — $search silently fails on personal MS accounts
|
|
216
222
|
simplifiedParams.$filter = `contains(subject, '${searchTerms[term].replace(/'/g, "''")}')`;
|
|
217
223
|
} else if (term === 'query') {
|
|
218
|
-
|
|
219
|
-
|
|
224
|
+
// On personal accounts, $search fails with 503. Use $filter with
|
|
225
|
+
// contains(subject) as a best-effort fallback for free-text queries.
|
|
226
|
+
simplifiedParams.$filter = `contains(subject, '${searchTerms[term].replace(/'/g, "''")}')`;
|
|
220
227
|
}
|
|
221
228
|
|
|
222
229
|
// Add boolean filters if applicable
|
|
@@ -241,21 +248,29 @@ async function progressiveSearch(
|
|
|
241
248
|
}
|
|
242
249
|
}
|
|
243
250
|
|
|
244
|
-
// 3. Try with only boolean filters
|
|
245
|
-
|
|
251
|
+
// 3. Try with only boolean filters (also date range filters)
|
|
252
|
+
const hasBooleanFilters =
|
|
253
|
+
filterTerms.hasAttachments === true || filterTerms.unreadOnly === true;
|
|
254
|
+
const hasDateFilters =
|
|
255
|
+
filterTerms.receivedAfter || filterTerms.receivedBefore;
|
|
256
|
+
if (hasBooleanFilters || hasDateFilters) {
|
|
246
257
|
try {
|
|
247
|
-
console.error('Attempting search with only boolean filters');
|
|
258
|
+
console.error('Attempting search with only boolean/date filters');
|
|
248
259
|
searchAttempts.push('boolean-filters-only');
|
|
249
260
|
|
|
250
261
|
const filterOnlyParams = {
|
|
251
262
|
$top: Math.min(50, maxCount),
|
|
252
263
|
$select: selectFields,
|
|
253
|
-
$orderby: 'receivedDateTime desc',
|
|
254
264
|
};
|
|
255
265
|
|
|
256
|
-
// Add the boolean filters
|
|
266
|
+
// Add the boolean + date filters
|
|
257
267
|
addBooleanFilters(filterOnlyParams, filterTerms);
|
|
258
268
|
|
|
269
|
+
// Only add $orderby if no $filter (they can conflict on personal accounts)
|
|
270
|
+
if (!filterOnlyParams.$filter) {
|
|
271
|
+
filterOnlyParams.$orderby = 'receivedDateTime desc';
|
|
272
|
+
}
|
|
273
|
+
|
|
259
274
|
const response = await callGraphAPIPaginated(
|
|
260
275
|
accessToken,
|
|
261
276
|
'GET',
|
|
@@ -269,6 +284,29 @@ async function progressiveSearch(
|
|
|
269
284
|
return response;
|
|
270
285
|
} catch (error) {
|
|
271
286
|
console.error(`Boolean filter search failed: ${error.message}`);
|
|
287
|
+
// Retry without $orderby if it was the issue
|
|
288
|
+
if (error.message && error.message.includes('InefficientFilter')) {
|
|
289
|
+
try {
|
|
290
|
+
console.error('Retrying boolean filters without $orderby');
|
|
291
|
+
const retryParams = {
|
|
292
|
+
$top: Math.min(50, maxCount),
|
|
293
|
+
$select: selectFields,
|
|
294
|
+
};
|
|
295
|
+
addBooleanFilters(retryParams, filterTerms);
|
|
296
|
+
const response = await callGraphAPIPaginated(
|
|
297
|
+
accessToken,
|
|
298
|
+
'GET',
|
|
299
|
+
endpoint,
|
|
300
|
+
retryParams,
|
|
301
|
+
maxCount
|
|
302
|
+
);
|
|
303
|
+
return response;
|
|
304
|
+
} catch (retryError) {
|
|
305
|
+
console.error(
|
|
306
|
+
`Boolean filter retry also failed: ${retryError.message}`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
272
310
|
}
|
|
273
311
|
}
|
|
274
312
|
|
|
@@ -304,6 +342,54 @@ async function progressiveSearch(
|
|
|
304
342
|
return response;
|
|
305
343
|
}
|
|
306
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Detect if a value is a domain-only filter (e.g. "@souliv.com.au" or "souliv.com.au")
|
|
347
|
+
* vs a full email address (e.g. "user@souliv.com.au") vs a display name (e.g. "Billie")
|
|
348
|
+
* @param {string} val - The from/to filter value
|
|
349
|
+
* @returns {'domain'|'email'|'name'} - The type of filter
|
|
350
|
+
*/
|
|
351
|
+
function classifyEmailFilter(val) {
|
|
352
|
+
if (val.startsWith('@')) return 'domain';
|
|
353
|
+
// Has dots but no @ — likely a domain like "souliv.com.au"
|
|
354
|
+
if (!val.includes('@') && val.includes('.')) return 'domain';
|
|
355
|
+
if (val.includes('@')) return 'email';
|
|
356
|
+
return 'name';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Build a $filter condition for a from field value
|
|
361
|
+
* @param {string} val - The from filter value
|
|
362
|
+
* @returns {string} - OData $filter condition
|
|
363
|
+
*/
|
|
364
|
+
function buildFromFilter(val) {
|
|
365
|
+
const type = classifyEmailFilter(val);
|
|
366
|
+
if (type === 'domain') {
|
|
367
|
+
// Use contains() — endswith() not supported on personal accounts
|
|
368
|
+
const domain = val.startsWith('@') ? val : `@${val}`;
|
|
369
|
+
return `contains(from/emailAddress/address, '${domain.substring(1)}')`;
|
|
370
|
+
} else if (type === 'email') {
|
|
371
|
+
return `from/emailAddress/address eq '${val}'`;
|
|
372
|
+
}
|
|
373
|
+
return `contains(from/emailAddress/name, '${val}')`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Build a $filter condition for a to field value
|
|
378
|
+
* @param {string} val - The to filter value
|
|
379
|
+
* @returns {string} - OData $filter condition
|
|
380
|
+
*/
|
|
381
|
+
function buildToFilter(val) {
|
|
382
|
+
const type = classifyEmailFilter(val);
|
|
383
|
+
if (type === 'domain') {
|
|
384
|
+
const domain = val.startsWith('@') ? val : `@${val}`;
|
|
385
|
+
// Use contains() — endswith() not supported on personal accounts
|
|
386
|
+
return `toRecipients/any(r: contains(r/emailAddress/address, '${domain.substring(1)}'))`;
|
|
387
|
+
} else if (type === 'email') {
|
|
388
|
+
return `toRecipients/any(r: r/emailAddress/address eq '${val}')`;
|
|
389
|
+
}
|
|
390
|
+
return `toRecipients/any(r: contains(r/emailAddress/name, '${val}'))`;
|
|
391
|
+
}
|
|
392
|
+
|
|
307
393
|
/**
|
|
308
394
|
* Build search parameters from search terms and filter terms
|
|
309
395
|
* Uses $filter for email addresses (more reliable than $search)
|
|
@@ -342,28 +428,12 @@ function buildSearchParams(searchTerms, filterTerms, count, selectFields) {
|
|
|
342
428
|
// NOTE: $filter on email addresses is incompatible with $orderby - Graph API limitation
|
|
343
429
|
if (searchTerms.from) {
|
|
344
430
|
usesEmailFilter = true;
|
|
345
|
-
|
|
346
|
-
filterConditions.push(
|
|
347
|
-
`from/emailAddress/address eq '${searchTerms.from}'`
|
|
348
|
-
);
|
|
349
|
-
} else {
|
|
350
|
-
filterConditions.push(
|
|
351
|
-
`contains(from/emailAddress/name, '${searchTerms.from}')`
|
|
352
|
-
);
|
|
353
|
-
}
|
|
431
|
+
filterConditions.push(buildFromFilter(searchTerms.from));
|
|
354
432
|
}
|
|
355
433
|
|
|
356
434
|
if (searchTerms.to) {
|
|
357
435
|
usesEmailFilter = true;
|
|
358
|
-
|
|
359
|
-
filterConditions.push(
|
|
360
|
-
`toRecipients/any(r: r/emailAddress/address eq '${searchTerms.to}')`
|
|
361
|
-
);
|
|
362
|
-
} else {
|
|
363
|
-
filterConditions.push(
|
|
364
|
-
`toRecipients/any(r: contains(r/emailAddress/name, '${searchTerms.to}'))`
|
|
365
|
-
);
|
|
366
|
-
}
|
|
436
|
+
filterConditions.push(buildToFilter(searchTerms.to));
|
|
367
437
|
}
|
|
368
438
|
|
|
369
439
|
// Add boolean filters (these ARE compatible with $orderby)
|
|
@@ -625,4 +695,7 @@ async function handleSearchByMessageId(args) {
|
|
|
625
695
|
module.exports = {
|
|
626
696
|
handleSearchEmails,
|
|
627
697
|
handleSearchByMessageId,
|
|
698
|
+
buildFromFilter,
|
|
699
|
+
buildToFilter,
|
|
700
|
+
classifyEmailFilter,
|
|
628
701
|
};
|
package/email/send.js
CHANGED
|
@@ -101,49 +101,7 @@ async function handleSendEmail(args) {
|
|
|
101
101
|
const allowlistError = checkRecipientAllowlist(allRecipients);
|
|
102
102
|
if (allowlistError) return allowlistError;
|
|
103
103
|
|
|
104
|
-
//
|
|
105
|
-
if (checkRecipients) {
|
|
106
|
-
const allAddresses = allRecipients.map((r) => r.emailAddress.address);
|
|
107
|
-
const tipsResult = await handleGetMailTips({
|
|
108
|
-
recipients: allAddresses,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// If there are warnings, prepend them to the response
|
|
112
|
-
if (tipsResult._meta?.warningCount > 0 && dryRun) {
|
|
113
|
-
// In dry-run mode, include mail tips in the preview
|
|
114
|
-
const tipsText = tipsResult.content[0]?.text || '';
|
|
115
|
-
const preview = formatDryRunPreview({
|
|
116
|
-
message: {
|
|
117
|
-
subject,
|
|
118
|
-
body: {
|
|
119
|
-
contentType:
|
|
120
|
-
/<(html|div|p|h[1-6]|br|table|ul|ol|li|span|a\s|img|strong|em|b|i)\b/i.test(
|
|
121
|
-
body
|
|
122
|
-
)
|
|
123
|
-
? 'html'
|
|
124
|
-
: 'text',
|
|
125
|
-
content: body,
|
|
126
|
-
},
|
|
127
|
-
toRecipients,
|
|
128
|
-
ccRecipients: ccRecipients.length > 0 ? ccRecipients : undefined,
|
|
129
|
-
bccRecipients: bccRecipients.length > 0 ? bccRecipients : undefined,
|
|
130
|
-
importance,
|
|
131
|
-
},
|
|
132
|
-
saveToSentItems,
|
|
133
|
-
});
|
|
134
|
-
return {
|
|
135
|
-
content: [
|
|
136
|
-
{
|
|
137
|
-
type: 'text',
|
|
138
|
-
text: tipsText + '\n\n---\n\n' + preview.content[0].text,
|
|
139
|
-
},
|
|
140
|
-
],
|
|
141
|
-
_meta: { mailTips: tipsResult._meta },
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Prepare email object
|
|
104
|
+
// Prepare email object (needed by both dryRun and actual send)
|
|
147
105
|
const emailObject = {
|
|
148
106
|
message: {
|
|
149
107
|
subject,
|
|
@@ -164,6 +122,37 @@ async function handleSendEmail(args) {
|
|
|
164
122
|
saveToSentItems,
|
|
165
123
|
};
|
|
166
124
|
|
|
125
|
+
// Pre-send mail tips check
|
|
126
|
+
if (checkRecipients) {
|
|
127
|
+
const allAddresses = allRecipients.map((r) => r.emailAddress.address);
|
|
128
|
+
const tipsResult = await handleGetMailTips({
|
|
129
|
+
recipients: allAddresses,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const tipsText = tipsResult.content[0]?.text || '';
|
|
133
|
+
|
|
134
|
+
// In dry-run mode, always include mail tips in the preview
|
|
135
|
+
if (dryRun) {
|
|
136
|
+
const preview = formatDryRunPreview(emailObject);
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: 'text',
|
|
141
|
+
text: tipsText + '\n\n---\n\n' + preview.content[0].text,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
_meta: { mailTips: tipsResult._meta },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// In send mode, warn if there are issues but proceed
|
|
149
|
+
if (tipsResult._meta?.warningCount > 0) {
|
|
150
|
+
// Store tips to prepend to send response
|
|
151
|
+
emailObject._mailTipsText = tipsText;
|
|
152
|
+
emailObject._mailTipsMeta = tipsResult._meta;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
167
156
|
// Dry-run mode: return preview without sending
|
|
168
157
|
if (dryRun) {
|
|
169
158
|
return formatDryRunPreview(emailObject);
|
package/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const {
|
|
|
12
12
|
const config = require('./config');
|
|
13
13
|
|
|
14
14
|
// Import module tools
|
|
15
|
-
const { authTools } = require('./auth');
|
|
15
|
+
const { authTools, setToolCount } = require('./auth');
|
|
16
16
|
const { calendarTools } = require('./calendar');
|
|
17
17
|
const { emailTools } = require('./email');
|
|
18
18
|
const { folderTools } = require('./folder');
|
|
@@ -39,6 +39,9 @@ const TOOLS = [
|
|
|
39
39
|
...advancedTools,
|
|
40
40
|
];
|
|
41
41
|
|
|
42
|
+
// Set dynamic tool count for auth about handler
|
|
43
|
+
setToolCount(TOOLS.length);
|
|
44
|
+
|
|
42
45
|
// Create server with tools capabilities
|
|
43
46
|
const server = new Server(
|
|
44
47
|
{ name: config.SERVER_NAME, version: config.SERVER_VERSION },
|
package/llms.txt
CHANGED
|
@@ -11,12 +11,12 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
11
11
|
- **License**: MIT
|
|
12
12
|
- **Node.js**: >= 18.0.0
|
|
13
13
|
- **Authentication**: OAuth 2.0 with Microsoft Graph API (requires Azure app registration)
|
|
14
|
-
- **Tools**:
|
|
14
|
+
- **Tools**: 22 tools across 9 modules (reduced from 55 for optimal AI performance)
|
|
15
15
|
|
|
16
16
|
## Why Outlook Assistant?
|
|
17
17
|
|
|
18
18
|
- Read, search, send, and export emails directly from Claude instead of switching apps
|
|
19
|
-
-
|
|
19
|
+
- 8 email tools covering search, conversations, attachments, bulk export, pre-send mail tips, and draft management
|
|
20
20
|
- Manage calendar events, contacts, rules, categories, and mailbox settings in one place
|
|
21
21
|
- Export to multiple formats: MIME/EML, MBOX, Markdown, JSON, HTML
|
|
22
22
|
|
|
@@ -31,9 +31,9 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
31
31
|
|
|
32
32
|
## Safety & Token Efficiency
|
|
33
33
|
|
|
34
|
-
- **MCP safety annotations** on all
|
|
34
|
+
- **MCP safety annotations** on all 22 tools — AI clients auto-approve reads and prompt for destructive operations
|
|
35
35
|
- **Send-email protections**: pre-send mail tips, dry-run preview, session rate limiting, recipient allowlist
|
|
36
|
-
- **Token-optimised**:
|
|
36
|
+
- **Token-optimised**: 22 tools instead of 55 saves ~11,000 tokens per turn (~64% reduction), improving AI accuracy and context efficiency
|
|
37
37
|
- These safeguards reduce risk but are not foolproof — always review actions before approving
|
|
38
38
|
|
|
39
39
|
## Quick Start
|
|
@@ -58,7 +58,7 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
|
|
|
58
58
|
## Tool Categories
|
|
59
59
|
|
|
60
60
|
- **Authentication (1 tool)**: `auth` — OAuth flow, status, about
|
|
61
|
-
- **Email (
|
|
61
|
+
- **Email (8 tools)**: `search-emails`, `read-email`, `send-email`, `draft`, `update-email`, `attachments`, `export`, `get-mail-tips`
|
|
62
62
|
- **Calendar (3 tools)**: `list-events`, `create-event`, `manage-event`
|
|
63
63
|
- **Contacts (2 tools)**: `manage-contact`, `search-people`
|
|
64
64
|
- **Folders (1 tool)**: `folders` — list, create, move, stats
|
|
@@ -70,7 +70,7 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
|
|
|
70
70
|
## Documentation
|
|
71
71
|
|
|
72
72
|
- [README](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/README.md): Full documentation including setup, Azure configuration, and usage
|
|
73
|
-
- [Tools Reference](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/quickrefs/tools-reference.md): All
|
|
73
|
+
- [Tools Reference](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/quickrefs/tools-reference.md): All 22 tools with parameters and safety annotations
|
|
74
74
|
- [CLAUDE.md](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CLAUDE.md): Quick reference for development
|
|
75
75
|
- [CONTRIBUTING](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CONTRIBUTING.md): Contribution guidelines
|
|
76
76
|
- [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebearapps/outlook-assistant",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"mcpName": "io.github.littlebearapps/outlook-assistant",
|
|
5
|
-
"description": "Outlook Assistant — MCP server with
|
|
5
|
+
"description": "Outlook Assistant — MCP server with 22 tools for email, calendar, contacts, and settings via Microsoft Graph API",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"outlook-assistant": "./index.js"
|
package/utils/field-presets.js
CHANGED
|
@@ -107,6 +107,26 @@ const FIELD_PRESETS = {
|
|
|
107
107
|
'changeKey',
|
|
108
108
|
],
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Draft fields for viewing/editing drafts
|
|
112
|
+
* Use case: Draft creation, update, listing
|
|
113
|
+
*/
|
|
114
|
+
draft: [
|
|
115
|
+
'id',
|
|
116
|
+
'subject',
|
|
117
|
+
'from',
|
|
118
|
+
'toRecipients',
|
|
119
|
+
'ccRecipients',
|
|
120
|
+
'bccRecipients',
|
|
121
|
+
'body',
|
|
122
|
+
'bodyPreview',
|
|
123
|
+
'lastModifiedDateTime',
|
|
124
|
+
'isDraft',
|
|
125
|
+
'importance',
|
|
126
|
+
'hasAttachments',
|
|
127
|
+
'conversationId',
|
|
128
|
+
],
|
|
129
|
+
|
|
110
130
|
/**
|
|
111
131
|
* Search result fields (optimized for relevance display)
|
|
112
132
|
* Use case: Search results with context
|
package/utils/graph-api.js
CHANGED
|
@@ -7,6 +7,7 @@ const mockData = require('./mock-data');
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Makes a request to the Microsoft Graph API
|
|
10
|
+
* In test mode (USE_TEST_MODE=true), routes to mock data instead of the real API.
|
|
10
11
|
* @param {string} accessToken - The access token for authentication
|
|
11
12
|
* @param {string} method - HTTP method (GET, POST, etc.)
|
|
12
13
|
* @param {string} path - API endpoint path
|
|
@@ -14,6 +15,8 @@ const mockData = require('./mock-data');
|
|
|
14
15
|
* @param {object} queryParams - Query parameters
|
|
15
16
|
* @param {object} extraHeaders - Additional headers (e.g. Prefer for immutable IDs)
|
|
16
17
|
* @returns {Promise<object>} - The API response
|
|
18
|
+
* @throws {Error} 'UNAUTHORIZED' if the server returns HTTP 401 (token expired or invalid)
|
|
19
|
+
* @throws {Error} If the HTTP status is outside 2xx, or if JSON parsing or network fails
|
|
17
20
|
*/
|
|
18
21
|
async function callGraphAPI(
|
|
19
22
|
accessToken,
|
|
@@ -153,6 +156,8 @@ async function callGraphAPI(
|
|
|
153
156
|
* @param {object} queryParams - Initial query parameters
|
|
154
157
|
* @param {number} maxCount - Maximum number of items to retrieve (0 = all)
|
|
155
158
|
* @returns {Promise<object>} - Combined API response with all items
|
|
159
|
+
* @throws {Error} If method is not 'GET'
|
|
160
|
+
* @throws {Error} If any page request fails for any other reason
|
|
156
161
|
*/
|
|
157
162
|
async function callGraphAPIPaginated(
|
|
158
163
|
accessToken,
|
|
@@ -268,9 +273,12 @@ async function callGraphAPIBatch(accessToken, requests) {
|
|
|
268
273
|
|
|
269
274
|
/**
|
|
270
275
|
* Calls Graph API to get raw MIME content (for email export)
|
|
276
|
+
* In test mode (USE_TEST_MODE=true), returns mock MIME content instead of calling the real API.
|
|
271
277
|
* @param {string} accessToken - The access token for authentication
|
|
272
278
|
* @param {string} emailId - The email ID to export
|
|
273
279
|
* @returns {Promise<string>} - Raw MIME content as string
|
|
280
|
+
* @throws {Error} 'UNAUTHORIZED' if the server returns HTTP 401 (token expired or invalid)
|
|
281
|
+
* @throws {Error} If the HTTP status is outside 2xx or a network error occurs
|
|
274
282
|
*/
|
|
275
283
|
async function callGraphAPIRaw(accessToken, emailId) {
|
|
276
284
|
// Test mode: return mock MIME content
|