@littlebearapps/outlook-assistant 3.6.0 → 3.7.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 +1 -1
- package/advanced/index.js +5 -5
- package/auth/device-code.js +3 -1
- package/auth/oauth-server.js +4 -4
- package/auth/token-manager.js +2 -2
- package/calendar/list.js +3 -3
- package/categories/index.js +4 -4
- package/contacts/index.js +11 -18
- package/email/conversations.js +19 -17
- package/email/delta.js +1 -1
- package/email/draft.js +6 -8
- package/email/folder-utils.js +1 -1
- package/email/headers.js +10 -10
- package/email/list.js +1 -1
- package/email/mime.js +13 -12
- package/email/search.js +3 -2
- package/email/send.js +1 -1
- package/folder/list.js +1 -1
- package/llms.txt +2 -1
- package/package.json +1 -1
- package/rules/create.js +85 -161
- package/rules/index.js +162 -29
- package/rules/list.js +144 -62
- package/rules/rule-builder.js +284 -0
- package/rules/update.js +213 -0
- package/settings/index.js +10 -22
- package/utils/graph-api.js +1 -1
- package/utils/response-formatter.js +8 -6
- package/utils/safety.js +113 -0
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
|
|
|
67
67
|
| **Categories** | 3 | `manage-category` (CRUD), `apply-category`, `manage-focused-inbox` |
|
|
68
68
|
| **Settings** | 1 | `mailbox-settings` (get/set auto-replies/set working hours) |
|
|
69
69
|
| **Folder** | 1 | `folders` (list/create/move/stats/delete) |
|
|
70
|
-
| **Rules** | 1 | `manage-rules` (list/create/reorder/delete) |
|
|
70
|
+
| **Rules** | 1 | `manage-rules` (list/create/update/reorder/delete) |
|
|
71
71
|
| **Advanced** | 2 | `access-shared-mailbox`, `find-meeting-rooms` |
|
|
72
72
|
| **Auth** | 1 | `auth` (status/authenticate/about) |
|
|
73
73
|
|
package/advanced/index.js
CHANGED
|
@@ -96,7 +96,7 @@ async function handleAccessSharedMailbox(args) {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
const output = [];
|
|
100
100
|
output.push(`# Shared Mailbox: ${sharedMailbox}`);
|
|
101
101
|
output.push(`**Folder**: ${mailFolder} | **Count**: ${messages.length}\n`);
|
|
102
102
|
|
|
@@ -241,7 +241,7 @@ async function handleSetMessageFlag(args) {
|
|
|
241
241
|
timeZone: DEFAULT_TIMEZONE,
|
|
242
242
|
};
|
|
243
243
|
} else {
|
|
244
|
-
const startOfDay = dueDt.split('T')[0]
|
|
244
|
+
const startOfDay = `${dueDt.split('T')[0]}T09:00:00`;
|
|
245
245
|
flag.startDateTime = {
|
|
246
246
|
dateTime: startOfDay,
|
|
247
247
|
timeZone: DEFAULT_TIMEZONE,
|
|
@@ -269,7 +269,7 @@ async function handleSetMessageFlag(args) {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
|
|
272
|
+
const output = [];
|
|
273
273
|
|
|
274
274
|
if (results.length > 0) {
|
|
275
275
|
output.push(`Flagged ${results.length} message(s) for follow-up`);
|
|
@@ -377,7 +377,7 @@ async function handleClearMessageFlag(args) {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
const action = markComplete ? 'marked complete' : 'cleared';
|
|
380
|
-
|
|
380
|
+
const output = [];
|
|
381
381
|
|
|
382
382
|
if (results.length > 0) {
|
|
383
383
|
output.push(`${results.length} message(s) ${action}`);
|
|
@@ -504,7 +504,7 @@ async function handleFindMeetingRooms(args) {
|
|
|
504
504
|
};
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
-
|
|
507
|
+
const output = [];
|
|
508
508
|
output.push(`# Meeting Rooms (${rooms.length})\n`);
|
|
509
509
|
|
|
510
510
|
if (verbosity === 'minimal') {
|
package/auth/device-code.js
CHANGED
|
@@ -92,7 +92,9 @@ async function pollForToken(clientId, deviceCode, interval, expiresIn) {
|
|
|
92
92
|
let pollInterval = interval;
|
|
93
93
|
|
|
94
94
|
while (Date.now() < deadline) {
|
|
95
|
-
await new Promise((resolve) =>
|
|
95
|
+
await new Promise((resolve) => {
|
|
96
|
+
setTimeout(resolve, pollInterval * 1000);
|
|
97
|
+
});
|
|
96
98
|
|
|
97
99
|
const postData = querystring.stringify({
|
|
98
100
|
client_id: clientId,
|
package/auth/oauth-server.js
CHANGED
|
@@ -112,16 +112,16 @@ function setupOAuthRoutes(
|
|
|
112
112
|
// Since this is a module, actual session handling is outside its direct scope,
|
|
113
113
|
// but it's crucial for the consuming application to handle state verification.
|
|
114
114
|
|
|
115
|
-
const authorizationUrl =
|
|
116
|
-
|
|
117
|
-
querystring.stringify({
|
|
115
|
+
const authorizationUrl = `${authConfig.authEndpoint}?${querystring.stringify(
|
|
116
|
+
{
|
|
118
117
|
client_id: authConfig.clientId,
|
|
119
118
|
response_type: 'code',
|
|
120
119
|
redirect_uri: authConfig.redirectUri,
|
|
121
120
|
scope: authConfig.scopes.join(' '),
|
|
122
121
|
response_mode: 'query',
|
|
123
122
|
state: state,
|
|
124
|
-
}
|
|
123
|
+
}
|
|
124
|
+
)}`;
|
|
125
125
|
res.redirect(authorizationUrl);
|
|
126
126
|
});
|
|
127
127
|
|
package/auth/token-manager.js
CHANGED
|
@@ -88,8 +88,8 @@ function getAccessToken() {
|
|
|
88
88
|
*/
|
|
89
89
|
function createTestTokens() {
|
|
90
90
|
const testTokens = {
|
|
91
|
-
access_token:
|
|
92
|
-
refresh_token:
|
|
91
|
+
access_token: `test_access_token_${Date.now()}`,
|
|
92
|
+
refresh_token: `test_refresh_token_${Date.now()}`,
|
|
93
93
|
expires_at: Date.now() + 3600 * 1000, // 1 hour
|
|
94
94
|
};
|
|
95
95
|
|
package/calendar/list.js
CHANGED
|
@@ -18,7 +18,7 @@ async function handleListEvents(args) {
|
|
|
18
18
|
const accessToken = await ensureAuthenticated();
|
|
19
19
|
|
|
20
20
|
// Build API endpoint
|
|
21
|
-
|
|
21
|
+
const endpoint = 'me/events';
|
|
22
22
|
|
|
23
23
|
// Add query parameters
|
|
24
24
|
const queryParams = {
|
|
@@ -54,10 +54,10 @@ async function handleListEvents(args) {
|
|
|
54
54
|
.map((event, index) => {
|
|
55
55
|
const startDt = event.start.dateTime.endsWith('Z')
|
|
56
56
|
? event.start.dateTime
|
|
57
|
-
: event.start.dateTime
|
|
57
|
+
: `${event.start.dateTime}Z`;
|
|
58
58
|
const endDt = event.end.dateTime.endsWith('Z')
|
|
59
59
|
? event.end.dateTime
|
|
60
|
-
: event.end.dateTime
|
|
60
|
+
: `${event.end.dateTime}Z`;
|
|
61
61
|
const startDate = new Date(startDt).toLocaleString('en-AU', {
|
|
62
62
|
timeZone: tz,
|
|
63
63
|
dateStyle: 'medium',
|
package/categories/index.js
CHANGED
|
@@ -106,7 +106,7 @@ async function handleListCategories(args) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// Format output based on verbosity
|
|
109
|
-
|
|
109
|
+
const output = [];
|
|
110
110
|
output.push(`# Master Categories (${categories.length})\n`);
|
|
111
111
|
|
|
112
112
|
if (outputVerbosity === 'minimal') {
|
|
@@ -117,7 +117,7 @@ async function handleListCategories(args) {
|
|
|
117
117
|
categories.forEach((cat) => {
|
|
118
118
|
const colorName = COLOR_NAMES[cat.color] || cat.color;
|
|
119
119
|
const idDisplay =
|
|
120
|
-
outputVerbosity === 'full' ? cat.id : cat.id.substring(0, 8)
|
|
120
|
+
outputVerbosity === 'full' ? cat.id : `${cat.id.substring(0, 8)}...`;
|
|
121
121
|
output.push(`| ${cat.displayName} | ${colorName} | ${idDisplay} |`);
|
|
122
122
|
});
|
|
123
123
|
}
|
|
@@ -503,7 +503,7 @@ async function handleApplyCategory(args) {
|
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
-
|
|
506
|
+
const output = [];
|
|
507
507
|
|
|
508
508
|
if (results.length > 0) {
|
|
509
509
|
output.push(
|
|
@@ -591,7 +591,7 @@ async function handleGetFocusedInboxOverrides(args) {
|
|
|
591
591
|
};
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
|
|
594
|
+
const output = [];
|
|
595
595
|
output.push(`# Focused Inbox Overrides (${overrides.length})\n`);
|
|
596
596
|
|
|
597
597
|
// Group by classification
|
package/contacts/index.js
CHANGED
|
@@ -66,10 +66,12 @@ function formatContact(contact, verbosity = 'standard') {
|
|
|
66
66
|
// Phone numbers
|
|
67
67
|
const phones = [];
|
|
68
68
|
if (contact.mobilePhone) phones.push(`Mobile: ${contact.mobilePhone}`);
|
|
69
|
-
if (contact.businessPhones?.length > 0)
|
|
69
|
+
if (contact.businessPhones?.length > 0) {
|
|
70
70
|
phones.push(`Work: ${contact.businessPhones[0]}`);
|
|
71
|
-
|
|
71
|
+
}
|
|
72
|
+
if (contact.homePhones?.length > 0) {
|
|
72
73
|
phones.push(`Home: ${contact.homePhones[0]}`);
|
|
74
|
+
}
|
|
73
75
|
if (phones.length > 0) {
|
|
74
76
|
lines.push(`**Phone**: ${phones.join(' | ')}`);
|
|
75
77
|
}
|
|
@@ -86,8 +88,9 @@ function formatContact(contact, verbosity = 'standard') {
|
|
|
86
88
|
// Full verbosity extras
|
|
87
89
|
if (verbosity === 'full') {
|
|
88
90
|
if (contact.department) lines.push(`**Department**: ${contact.department}`);
|
|
89
|
-
if (contact.officeLocation)
|
|
91
|
+
if (contact.officeLocation) {
|
|
90
92
|
lines.push(`**Office**: ${contact.officeLocation}`);
|
|
93
|
+
}
|
|
91
94
|
if (contact.birthday) lines.push(`**Birthday**: ${contact.birthday}`);
|
|
92
95
|
|
|
93
96
|
// Addresses
|
|
@@ -130,12 +133,7 @@ async function handleListContacts(args) {
|
|
|
130
133
|
try {
|
|
131
134
|
const accessToken = await ensureAuthenticated();
|
|
132
135
|
|
|
133
|
-
const fields =
|
|
134
|
-
verbosity === 'full'
|
|
135
|
-
? CONTACT_FIELDS.full
|
|
136
|
-
: verbosity === 'minimal'
|
|
137
|
-
? CONTACT_FIELDS.minimal
|
|
138
|
-
: CONTACT_FIELDS.list;
|
|
136
|
+
const fields = CONTACT_FIELDS[verbosity] || CONTACT_FIELDS.list;
|
|
139
137
|
const endpoint = folder
|
|
140
138
|
? `me/contactFolders/${folder}/contacts`
|
|
141
139
|
: 'me/contacts';
|
|
@@ -155,7 +153,7 @@ async function handleListContacts(args) {
|
|
|
155
153
|
);
|
|
156
154
|
const contacts = response.value || [];
|
|
157
155
|
|
|
158
|
-
|
|
156
|
+
const output = [];
|
|
159
157
|
output.push(`# Contacts\n`);
|
|
160
158
|
output.push(`**Total**: ${contacts.length}`);
|
|
161
159
|
output.push('');
|
|
@@ -202,12 +200,7 @@ async function handleSearchContacts(args) {
|
|
|
202
200
|
try {
|
|
203
201
|
const accessToken = await ensureAuthenticated();
|
|
204
202
|
|
|
205
|
-
const fields =
|
|
206
|
-
verbosity === 'full'
|
|
207
|
-
? CONTACT_FIELDS.full
|
|
208
|
-
: verbosity === 'minimal'
|
|
209
|
-
? CONTACT_FIELDS.minimal
|
|
210
|
-
: CONTACT_FIELDS.list;
|
|
203
|
+
const fields = CONTACT_FIELDS[verbosity] || CONTACT_FIELDS.list;
|
|
211
204
|
const endpoint = 'me/contacts';
|
|
212
205
|
|
|
213
206
|
// Build filter for name — emailAddresses/any() lambda is unreliable on personal accounts
|
|
@@ -259,7 +252,7 @@ async function handleSearchContacts(args) {
|
|
|
259
252
|
}
|
|
260
253
|
const contacts = response.value || [];
|
|
261
254
|
|
|
262
|
-
|
|
255
|
+
const output = [];
|
|
263
256
|
output.push(`# Contact Search Results\n`);
|
|
264
257
|
output.push(`**Query**: "${query}"`);
|
|
265
258
|
output.push(`**Found**: ${contacts.length}`);
|
|
@@ -559,7 +552,7 @@ async function handleSearchPeople(args) {
|
|
|
559
552
|
);
|
|
560
553
|
const people = response.value || [];
|
|
561
554
|
|
|
562
|
-
|
|
555
|
+
const output = [];
|
|
563
556
|
output.push(`# People Search Results\n`);
|
|
564
557
|
output.push(`**Query**: "${query}"`);
|
|
565
558
|
output.push(`**Found**: ${people.length} (sorted by relevance)`);
|
package/email/conversations.js
CHANGED
|
@@ -175,12 +175,12 @@ async function handleListConversations(args) {
|
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
// Convert to array and sort by most recent
|
|
178
|
-
|
|
178
|
+
const conversationList = Array.from(conversations.values())
|
|
179
179
|
.sort((a, b) => new Date(b.lastDate) - new Date(a.lastDate))
|
|
180
180
|
.slice(0, count);
|
|
181
181
|
|
|
182
182
|
// Format output
|
|
183
|
-
|
|
183
|
+
const output = [];
|
|
184
184
|
output.push(`# Email Conversations\n`);
|
|
185
185
|
output.push(`**Folder**: ${folder}`);
|
|
186
186
|
output.push(`**Conversations**: ${conversationList.length}`);
|
|
@@ -314,7 +314,7 @@ async function handleGetConversation(args) {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
// Format output
|
|
317
|
-
|
|
317
|
+
const output = [];
|
|
318
318
|
output.push(`# Email Conversation\n`);
|
|
319
319
|
output.push(`**Subject**: ${messages[0].subject || '(no subject)'}`);
|
|
320
320
|
output.push(`**Messages**: ${messages.length}`);
|
|
@@ -463,8 +463,8 @@ async function handleExportConversation(args) {
|
|
|
463
463
|
const date = formatDateForFilename(messages[0].receivedDateTime);
|
|
464
464
|
const filenameBase = `${date}_${subject}_conversation`;
|
|
465
465
|
|
|
466
|
-
|
|
467
|
-
|
|
466
|
+
const exportedFiles = [];
|
|
467
|
+
const exportStats = { messages: messages.length, attachments: 0, bytes: 0 };
|
|
468
468
|
|
|
469
469
|
switch (format) {
|
|
470
470
|
case 'eml': {
|
|
@@ -497,8 +497,8 @@ async function handleExportConversation(args) {
|
|
|
497
497
|
for (const msg of messages) {
|
|
498
498
|
const mimeContent = await callGraphAPIRaw(accessToken, msg.id);
|
|
499
499
|
const from = msg.from?.emailAddress?.address || 'unknown@unknown.com';
|
|
500
|
-
const
|
|
501
|
-
const mboxDate =
|
|
500
|
+
const msgDate = new Date(msg.receivedDateTime);
|
|
501
|
+
const mboxDate = msgDate.toUTCString().replace('GMT', '+0000');
|
|
502
502
|
|
|
503
503
|
// MBOX format: From line + MIME content + blank line
|
|
504
504
|
mboxContent += `From ${from} ${mboxDate}\n`;
|
|
@@ -515,7 +515,7 @@ async function handleExportConversation(args) {
|
|
|
515
515
|
case 'markdown': {
|
|
516
516
|
// Export as threaded Markdown document
|
|
517
517
|
const mdPath = path.join(resolvedDir, `${filenameBase}.md`);
|
|
518
|
-
|
|
518
|
+
const mdContent = [];
|
|
519
519
|
|
|
520
520
|
mdContent.push(
|
|
521
521
|
`# Email Conversation: ${messages[0].subject || '(no subject)'}\n`
|
|
@@ -600,7 +600,7 @@ async function handleExportConversation(args) {
|
|
|
600
600
|
case 'html': {
|
|
601
601
|
// Export as HTML document
|
|
602
602
|
const htmlPath = path.join(resolvedDir, `${filenameBase}.html`);
|
|
603
|
-
|
|
603
|
+
const htmlContent = [];
|
|
604
604
|
|
|
605
605
|
htmlContent.push('<!DOCTYPE html>');
|
|
606
606
|
htmlContent.push('<html><head>');
|
|
@@ -674,14 +674,16 @@ async function handleExportConversation(args) {
|
|
|
674
674
|
}
|
|
675
675
|
|
|
676
676
|
// Format result
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
677
|
+
let sizeFormatted;
|
|
678
|
+
if (exportStats.bytes < 1024) {
|
|
679
|
+
sizeFormatted = `${exportStats.bytes} B`;
|
|
680
|
+
} else if (exportStats.bytes < 1024 * 1024) {
|
|
681
|
+
sizeFormatted = `${(exportStats.bytes / 1024).toFixed(1)} KB`;
|
|
682
|
+
} else {
|
|
683
|
+
sizeFormatted = `${(exportStats.bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const output = [];
|
|
685
687
|
output.push(`# Conversation Exported\n`);
|
|
686
688
|
output.push(`**Subject**: ${messages[0].subject || '(no subject)'}`);
|
|
687
689
|
output.push(`**Format**: ${format.toUpperCase()}`);
|
package/email/delta.js
CHANGED
|
@@ -88,7 +88,7 @@ async function handleListEmailsDelta(args) {
|
|
|
88
88
|
|
|
89
89
|
// Build response
|
|
90
90
|
const isInitialSync = !deltaToken;
|
|
91
|
-
const hasMoreChanges =
|
|
91
|
+
const hasMoreChanges = Boolean(nextLink);
|
|
92
92
|
const newDeltaToken = deltaLink || nextLink;
|
|
93
93
|
|
|
94
94
|
// Format output based on verbosity
|
package/email/draft.js
CHANGED
|
@@ -83,8 +83,9 @@ function formatDraftResponse(draft, actionLabel) {
|
|
|
83
83
|
text += `**ID**: \`${draft.id}\`\n`;
|
|
84
84
|
if (draft.subject) text += `**Subject**: ${draft.subject}\n`;
|
|
85
85
|
if (to) text += `**To**: ${to}\n`;
|
|
86
|
-
if (draft.lastModifiedDateTime)
|
|
86
|
+
if (draft.lastModifiedDateTime) {
|
|
87
87
|
text += `**Modified**: ${draft.lastModifiedDateTime}\n`;
|
|
88
|
+
}
|
|
88
89
|
if (draft.hasAttachments) text += `**Attachments**: yes\n`;
|
|
89
90
|
|
|
90
91
|
return {
|
|
@@ -169,13 +170,10 @@ async function handleCreateDraft(args) {
|
|
|
169
170
|
content: [
|
|
170
171
|
{
|
|
171
172
|
type: 'text',
|
|
172
|
-
text:
|
|
173
|
-
|
|
174
|
-
'
|
|
175
|
-
|
|
176
|
-
'Email NOT sent',
|
|
177
|
-
'Draft NOT saved'
|
|
178
|
-
),
|
|
173
|
+
text: `${tipsText}\n\n---\n\n${preview.content[0].text.replace(
|
|
174
|
+
'Email NOT sent',
|
|
175
|
+
'Draft NOT saved'
|
|
176
|
+
)}`,
|
|
179
177
|
},
|
|
180
178
|
],
|
|
181
179
|
_meta: { mailTips: tipsResult._meta },
|
package/email/folder-utils.js
CHANGED
|
@@ -54,7 +54,7 @@ const WELL_KNOWN_FOLDERS = {
|
|
|
54
54
|
async function resolveFolderPath(accessToken, folderName) {
|
|
55
55
|
// Default to inbox if no folder specified
|
|
56
56
|
if (!folderName) {
|
|
57
|
-
return WELL_KNOWN_FOLDERS
|
|
57
|
+
return WELL_KNOWN_FOLDERS.inbox;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// Check if it's a well-known folder (case-insensitive)
|
package/email/headers.js
CHANGED
|
@@ -49,7 +49,7 @@ function formatHeaders(headers, options = {}) {
|
|
|
49
49
|
return '*No headers available*';
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
const output = [];
|
|
53
53
|
|
|
54
54
|
if (groupByType) {
|
|
55
55
|
// Group headers by category
|
|
@@ -65,7 +65,7 @@ function formatHeaders(headers, options = {}) {
|
|
|
65
65
|
headers.forEach((h) => {
|
|
66
66
|
const name = h.name;
|
|
67
67
|
if (['Message-ID', 'In-Reply-To', 'References'].includes(name)) {
|
|
68
|
-
groups
|
|
68
|
+
groups.Threading.push(h);
|
|
69
69
|
} else if (
|
|
70
70
|
[
|
|
71
71
|
'Authentication-Results',
|
|
@@ -74,9 +74,9 @@ function formatHeaders(headers, options = {}) {
|
|
|
74
74
|
'Received-SPF',
|
|
75
75
|
].includes(name)
|
|
76
76
|
) {
|
|
77
|
-
groups
|
|
77
|
+
groups.Authentication.push(h);
|
|
78
78
|
} else if (name === 'Received' || name.startsWith('X-MS-Exchange')) {
|
|
79
|
-
groups
|
|
79
|
+
groups.Delivery.push(h);
|
|
80
80
|
} else if (
|
|
81
81
|
name.includes('Antispam') ||
|
|
82
82
|
name.includes('SCL') ||
|
|
@@ -88,9 +88,9 @@ function formatHeaders(headers, options = {}) {
|
|
|
88
88
|
name
|
|
89
89
|
)
|
|
90
90
|
) {
|
|
91
|
-
groups
|
|
91
|
+
groups.Content.push(h);
|
|
92
92
|
} else {
|
|
93
|
-
groups
|
|
93
|
+
groups.Other.push(h);
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
96
|
|
|
@@ -100,7 +100,7 @@ function formatHeaders(headers, options = {}) {
|
|
|
100
100
|
groupHeaders.forEach((h) => {
|
|
101
101
|
// Truncate very long values
|
|
102
102
|
const value =
|
|
103
|
-
h.value.length > 500 ? h.value.substring(0, 500)
|
|
103
|
+
h.value.length > 500 ? `${h.value.substring(0, 500)}...` : h.value;
|
|
104
104
|
output.push(`**${h.name}**: \`${value}\``);
|
|
105
105
|
});
|
|
106
106
|
}
|
|
@@ -123,7 +123,7 @@ function formatHeaders(headers, options = {}) {
|
|
|
123
123
|
output.push('## Key Headers\n');
|
|
124
124
|
importantHeaders.forEach((h) => {
|
|
125
125
|
const value =
|
|
126
|
-
h.value.length > 300 ? h.value.substring(0, 300)
|
|
126
|
+
h.value.length > 300 ? `${h.value.substring(0, 300)}...` : h.value;
|
|
127
127
|
output.push(`**${h.name}**:`);
|
|
128
128
|
output.push('```');
|
|
129
129
|
output.push(value);
|
|
@@ -135,7 +135,7 @@ function formatHeaders(headers, options = {}) {
|
|
|
135
135
|
output.push('\n## All Other Headers\n');
|
|
136
136
|
otherHeaders.forEach((h) => {
|
|
137
137
|
const value =
|
|
138
|
-
h.value.length > 200 ? h.value.substring(0, 200)
|
|
138
|
+
h.value.length > 200 ? `${h.value.substring(0, 200)}...` : h.value;
|
|
139
139
|
output.push(`- **${h.name}**: \`${value}\``);
|
|
140
140
|
});
|
|
141
141
|
}
|
|
@@ -249,7 +249,7 @@ async function handleGetEmailHeaders(args) {
|
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
// Build formatted output
|
|
252
|
-
|
|
252
|
+
const output = [];
|
|
253
253
|
output.push(`# Email Headers\n`);
|
|
254
254
|
output.push(`**Subject**: ${email.subject || '(no subject)'}`);
|
|
255
255
|
output.push(
|
package/email/list.js
CHANGED
|
@@ -89,7 +89,7 @@ async function handleListEmails(args) {
|
|
|
89
89
|
const meta = {
|
|
90
90
|
returned: response.value.length,
|
|
91
91
|
totalAvailable: response['@odata.count'] || null,
|
|
92
|
-
hasMore:
|
|
92
|
+
hasMore: Boolean(response['@odata.nextLink']),
|
|
93
93
|
verbosity: verbosity,
|
|
94
94
|
};
|
|
95
95
|
|
package/email/mime.js
CHANGED
|
@@ -30,7 +30,7 @@ function parseMimeHeaders(mimeContent) {
|
|
|
30
30
|
lines.forEach((line) => {
|
|
31
31
|
if (line.startsWith(' ') || line.startsWith('\t')) {
|
|
32
32
|
// Continuation of previous header
|
|
33
|
-
currentValue +=
|
|
33
|
+
currentValue += ` ${line.trim()}`;
|
|
34
34
|
} else {
|
|
35
35
|
// Save previous header
|
|
36
36
|
if (currentHeader) {
|
|
@@ -57,6 +57,12 @@ function parseMimeHeaders(mimeContent) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function formatBytes(bytes) {
|
|
61
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
62
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
63
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
64
|
+
}
|
|
65
|
+
|
|
60
66
|
/**
|
|
61
67
|
* Get MIME content size info
|
|
62
68
|
* @param {string} mimeContent - Raw MIME content
|
|
@@ -68,12 +74,7 @@ function getMimeStats(mimeContent) {
|
|
|
68
74
|
|
|
69
75
|
return {
|
|
70
76
|
bytes,
|
|
71
|
-
formattedSize:
|
|
72
|
-
bytes < 1024
|
|
73
|
-
? `${bytes} B`
|
|
74
|
-
: bytes < 1024 * 1024
|
|
75
|
-
? `${(bytes / 1024).toFixed(1)} KB`
|
|
76
|
-
: `${(bytes / (1024 * 1024)).toFixed(2)} MB`,
|
|
77
|
+
formattedSize: formatBytes(bytes),
|
|
77
78
|
lines,
|
|
78
79
|
};
|
|
79
80
|
}
|
|
@@ -184,17 +185,17 @@ async function handleGetMimeContent(args) {
|
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
// Build response
|
|
187
|
-
|
|
188
|
+
const output = [];
|
|
188
189
|
output.push(`# MIME Content${headersOnly ? ' (Headers Only)' : ''}\n`);
|
|
189
190
|
output.push(`**Size**: ${stats.formattedSize}`);
|
|
190
191
|
output.push(`**Lines**: ${stats.lines}`);
|
|
191
192
|
|
|
192
193
|
// Show key headers
|
|
193
|
-
if (parsed.headers
|
|
194
|
-
output.push(`**Subject**: ${parsed.headers
|
|
194
|
+
if (parsed.headers.Subject) {
|
|
195
|
+
output.push(`**Subject**: ${parsed.headers.Subject}`);
|
|
195
196
|
}
|
|
196
|
-
if (parsed.headers
|
|
197
|
-
output.push(`**From**: ${parsed.headers
|
|
197
|
+
if (parsed.headers.From) {
|
|
198
|
+
output.push(`**From**: ${parsed.headers.From}`);
|
|
198
199
|
}
|
|
199
200
|
if (parsed.headers['Content-Type']) {
|
|
200
201
|
output.push(
|
package/email/search.js
CHANGED
|
@@ -166,7 +166,7 @@ async function progressiveSearch(
|
|
|
166
166
|
) {
|
|
167
167
|
// Skip directly to boolean-only filter (step 3) — combined search is redundant
|
|
168
168
|
console.error('Only boolean filters provided, skipping combined search');
|
|
169
|
-
} else
|
|
169
|
+
} else {
|
|
170
170
|
try {
|
|
171
171
|
const params = buildSearchParams(
|
|
172
172
|
searchTerms,
|
|
@@ -193,6 +193,7 @@ async function progressiveSearch(
|
|
|
193
193
|
} catch (error) {
|
|
194
194
|
console.error(`Combined search failed: ${error.message}`);
|
|
195
195
|
}
|
|
196
|
+
}
|
|
196
197
|
|
|
197
198
|
// 2. Try each search term individually, starting with most specific
|
|
198
199
|
const searchPriority = ['from', 'to', 'subject', 'query'];
|
|
@@ -545,7 +546,7 @@ function formatSearchResults(response, folder, verbosity) {
|
|
|
545
546
|
const meta = {
|
|
546
547
|
returned: response.value.length,
|
|
547
548
|
totalAvailable: response['@odata.count'] || null,
|
|
548
|
-
hasMore:
|
|
549
|
+
hasMore: Boolean(response['@odata.nextLink']),
|
|
549
550
|
verbosity: verbosity,
|
|
550
551
|
};
|
|
551
552
|
|
package/email/send.js
CHANGED
|
@@ -138,7 +138,7 @@ async function handleSendEmail(args) {
|
|
|
138
138
|
content: [
|
|
139
139
|
{
|
|
140
140
|
type: 'text',
|
|
141
|
-
text: tipsText
|
|
141
|
+
text: `${tipsText}\n\n---\n\n${preview.content[0].text}`,
|
|
142
142
|
},
|
|
143
143
|
],
|
|
144
144
|
_meta: { mailTips: tipsResult._meta },
|
package/folder/list.js
CHANGED
|
@@ -272,7 +272,7 @@ function formatFolderHierarchy(folders, includeItemCounts) {
|
|
|
272
272
|
// Add children
|
|
273
273
|
const childLines = folder.children
|
|
274
274
|
.map((childId) => formatSubtree(childId, level + 1))
|
|
275
|
-
.filter((
|
|
275
|
+
.filter((childLine) => childLine.length > 0)
|
|
276
276
|
.join('\n');
|
|
277
277
|
|
|
278
278
|
return childLines.length > 0 ? `${line}\n${childLines}` : line;
|
package/llms.txt
CHANGED
|
@@ -33,6 +33,7 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
33
33
|
|
|
34
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
|
+
- **Rule protections**: dry-run preview on create/update, rate limiting, recipient allowlist on forward/redirect, no permanent-delete action
|
|
36
37
|
- **Token-optimised**: 22 tools instead of 55 saves ~11,000 tokens per turn (~64% reduction), improving AI accuracy and context efficiency
|
|
37
38
|
- These safeguards reduce risk but are not foolproof — always review actions before approving
|
|
38
39
|
|
|
@@ -62,7 +63,7 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
|
|
|
62
63
|
- **Calendar (3 tools)**: `list-events`, `create-event`, `manage-event`
|
|
63
64
|
- **Contacts (2 tools)**: `manage-contact`, `search-people`
|
|
64
65
|
- **Folders (1 tool)**: `folders` — list, create, move, stats
|
|
65
|
-
- **Rules (1 tool)**: `manage-rules` — list, create, reorder
|
|
66
|
+
- **Rules (1 tool)**: `manage-rules` — list, create, update, reorder, delete
|
|
66
67
|
- **Categories (3 tools)**: `manage-category`, `apply-category`, `manage-focused-inbox`
|
|
67
68
|
- **Settings (1 tool)**: `mailbox-settings` — get, set auto-replies, set working hours
|
|
68
69
|
- **Advanced (2 tools)**: `access-shared-mailbox`, `find-meeting-rooms`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebearapps/outlook-assistant",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"mcpName": "io.github.littlebearapps/outlook-assistant",
|
|
5
5
|
"description": "Outlook Assistant — MCP server with 22 tools for email, calendar, contacts, and settings via Microsoft Graph API",
|
|
6
6
|
"main": "index.js",
|