@littlebearapps/outlook-assistant 3.5.2 → 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/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 += ' ' + line.trim();
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
- let output = [];
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['Subject']) {
194
- output.push(`**Subject**: ${parsed.headers['Subject']}`);
194
+ if (parsed.headers.Subject) {
195
+ output.push(`**Subject**: ${parsed.headers.Subject}`);
195
196
  }
196
- if (parsed.headers['From']) {
197
- output.push(`**From**: ${parsed.headers['From']}`);
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: !!response['@odata.nextLink'],
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 + '\n\n---\n\n' + preview.content[0].text,
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((line) => line.length > 0)
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
@@ -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**: 21 tools across 9 modules (reduced from 55 for optimal AI performance)
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
- - 7 email tools covering search, conversations, attachments, bulk export, and pre-send mail tips
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,10 @@ Built by [Little Bear Apps](https://littlebearapps.com).
31
31
 
32
32
  ## Safety & Token Efficiency
33
33
 
34
- - **MCP safety annotations** on all 21 tools — AI clients auto-approve reads and prompt for destructive operations
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**: 21 tools instead of 55 saves ~11,000 tokens per turn (~64% reduction), improving AI accuracy and context efficiency
36
+ - **Rule protections**: dry-run preview on create/update, rate limiting, recipient allowlist on forward/redirect, no permanent-delete action
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
 
39
40
  ## Quick Start
@@ -58,11 +59,11 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
58
59
  ## Tool Categories
59
60
 
60
61
  - **Authentication (1 tool)**: `auth` — OAuth flow, status, about
61
- - **Email (7 tools)**: `search-emails`, `read-email`, `send-email`, `update-email`, `attachments`, `export`, `get-mail-tips`
62
+ - **Email (8 tools)**: `search-emails`, `read-email`, `send-email`, `draft`, `update-email`, `attachments`, `export`, `get-mail-tips`
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`
@@ -70,7 +71,7 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
70
71
  ## Documentation
71
72
 
72
73
  - [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 21 tools with parameters and safety annotations
74
+ - [Tools Reference](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/quickrefs/tools-reference.md): All 22 tools with parameters and safety annotations
74
75
  - [CLAUDE.md](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CLAUDE.md): Quick reference for development
75
76
  - [CONTRIBUTING](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CONTRIBUTING.md): Contribution guidelines
76
77
  - [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.5.2",
3
+ "version": "3.7.0",
4
4
  "mcpName": "io.github.littlebearapps/outlook-assistant",
5
- "description": "Outlook Assistant — MCP server with 21 tools for email, calendar, contacts, and settings via Microsoft Graph API",
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/rules/create.js CHANGED
@@ -3,8 +3,16 @@
3
3
  */
4
4
  const { callGraphAPI } = require('../utils/graph-api');
5
5
  const { ensureAuthenticated } = require('../auth');
6
- const { getFolderIdByName } = require('../email/folder-utils');
6
+ const { checkRateLimit } = require('../utils/safety');
7
+ const { formatRuleDryRunPreview } = require('../utils/safety');
7
8
  const { getInboxRules } = require('./list');
9
+ const {
10
+ buildConditions,
11
+ buildActions,
12
+ buildExceptions,
13
+ hasAnyCondition,
14
+ hasAnyAction,
15
+ } = require('./rule-builder');
8
16
 
9
17
  /**
10
18
  * Create rule handler
@@ -12,18 +20,13 @@ const { getInboxRules } = require('./list');
12
20
  * @returns {object} - MCP response
13
21
  */
14
22
  async function handleCreateRule(args) {
15
- const {
16
- name,
17
- fromAddresses,
18
- containsSubject,
19
- hasAttachments,
20
- moveToFolder,
21
- markAsRead,
22
- isEnabled = true,
23
- sequence,
24
- } = args;
23
+ const { name, isEnabled = true, sequence, dryRun } = args;
25
24
 
26
- // Add validation for sequence parameter
25
+ // Rate limit rule creation
26
+ const rateLimitError = checkRateLimit('manage-rules');
27
+ if (rateLimitError) return rateLimitError;
28
+
29
+ // Validate sequence parameter
27
30
  if (sequence !== undefined && (isNaN(sequence) || sequence < 1)) {
28
31
  return {
29
32
  content: [
@@ -46,202 +49,96 @@ async function handleCreateRule(args) {
46
49
  };
47
50
  }
48
51
 
49
- // Validate that at least one condition or action is specified
50
- const hasCondition =
51
- fromAddresses || containsSubject || hasAttachments === true;
52
- const hasAction = moveToFolder || markAsRead === true;
53
-
54
- if (!hasCondition) {
52
+ if (!hasAnyCondition(args)) {
55
53
  return {
56
54
  content: [
57
55
  {
58
56
  type: 'text',
59
- text: 'At least one condition is required. Specify fromAddresses, containsSubject, or hasAttachments.',
57
+ text: 'At least one condition is required. Available conditions: fromAddresses, containsSubject, bodyContains, bodyOrSubjectContains, senderContains, recipientContains, sentToAddresses, hasAttachments, importance, sensitivity, sentToMe, sentOnlyToMe, sentCcMe, isAutomaticReply.',
60
58
  },
61
59
  ],
62
60
  };
63
61
  }
64
62
 
65
- if (!hasAction) {
63
+ if (!hasAnyAction(args)) {
66
64
  return {
67
65
  content: [
68
66
  {
69
67
  type: 'text',
70
- text: 'At least one action is required. Specify moveToFolder or markAsRead.',
68
+ text: 'At least one action is required. Available actions: moveToFolder, copyToFolder, markAsRead, markImportance, forwardTo, redirectTo, assignCategories, stopProcessingRules, deleteMessage.',
71
69
  },
72
70
  ],
73
71
  };
74
72
  }
75
73
 
76
74
  try {
77
- // Get access token
78
75
  const accessToken = await ensureAuthenticated();
79
76
 
80
- // Create rule
81
- const result = await createInboxRule(accessToken, {
82
- name,
83
- fromAddresses,
84
- containsSubject,
85
- hasAttachments,
86
- moveToFolder,
87
- markAsRead,
88
- isEnabled,
89
- sequence,
90
- });
91
-
92
- let responseText = result.message;
77
+ // Build rule components
78
+ const { conditions, warnings: condWarnings } = buildConditions(args);
79
+ const { actions, warnings: actWarnings } = await buildActions(
80
+ args,
81
+ accessToken
82
+ );
83
+ const { exceptions, warnings: excWarnings } = buildExceptions(args);
93
84
 
94
- // Add a tip about sequence if it wasn't provided
95
- if (!sequence && !result.error) {
96
- responseText +=
97
- "\n\nTip: You can specify a 'sequence' parameter when creating rules to control their execution order. Lower sequence numbers run first.";
98
- }
85
+ const allWarnings = [...condWarnings, ...actWarnings, ...excWarnings];
99
86
 
100
- return {
101
- content: [
102
- {
103
- type: 'text',
104
- text: responseText,
105
- },
106
- ],
107
- };
108
- } catch (error) {
109
- if (error.message === 'Authentication required') {
87
+ // Check for fatal warnings (folder not found = no valid action)
88
+ const folderNotFound = actWarnings.some((w) => w.includes('not found'));
89
+ if (folderNotFound && Object.keys(actions).length === 0) {
110
90
  return {
111
91
  content: [
112
92
  {
113
93
  type: 'text',
114
- text: "Authentication required. Please use the 'authenticate' tool first.",
94
+ text: actWarnings.filter((w) => w.includes('not found')).join('\n'),
115
95
  },
116
96
  ],
117
97
  };
118
98
  }
119
99
 
120
- return {
121
- content: [
122
- {
123
- type: 'text',
124
- text: `Error creating rule: ${error.message}`,
125
- },
126
- ],
127
- };
128
- }
129
- }
130
-
131
- /**
132
- * Create a new inbox rule
133
- * @param {string} accessToken - Access token
134
- * @param {object} ruleOptions - Rule creation options
135
- * @returns {Promise<object>} - Result object with status and message
136
- */
137
- async function createInboxRule(accessToken, ruleOptions) {
138
- try {
139
- const {
140
- name,
141
- fromAddresses,
142
- containsSubject,
143
- hasAttachments,
144
- moveToFolder,
145
- markAsRead,
146
- isEnabled,
147
- sequence,
148
- } = ruleOptions;
149
-
150
- // Get existing rules to determine sequence if not provided
100
+ // Determine sequence
151
101
  let ruleSequence = sequence;
152
102
  if (!ruleSequence) {
103
+ ruleSequence = 100;
153
104
  try {
154
- // Default to 100 if we can't get existing rules
155
- ruleSequence = 100;
156
-
157
- // Get existing rules to find highest sequence
158
105
  const existingRules = await getInboxRules(accessToken);
159
106
  if (existingRules && existingRules.length > 0) {
160
- // Find the highest sequence
161
107
  const highestSequence = Math.max(
162
108
  ...existingRules.map((r) => r.sequence || 0)
163
109
  );
164
- // Set new rule sequence to be higher
165
110
  ruleSequence = Math.max(highestSequence + 1, 100);
166
- console.error(
167
- `Auto-generated sequence: ${ruleSequence} (based on highest existing: ${highestSequence})`
168
- );
169
111
  }
170
- } catch (sequenceError) {
171
- console.error(
172
- `Error determining rule sequence: ${sequenceError.message}`
173
- );
174
- // Fall back to default value
112
+ } catch (_sequenceError) {
175
113
  ruleSequence = 100;
176
114
  }
177
115
  }
178
-
179
- console.error(`Using rule sequence: ${ruleSequence}`);
180
-
181
- // Make sure sequence is a positive integer
182
116
  ruleSequence = Math.max(1, Math.floor(ruleSequence));
183
117
 
184
- // Build rule object with sequence
118
+ // Build the rule object
185
119
  const rule = {
186
120
  displayName: name,
187
121
  isEnabled: isEnabled === true,
188
122
  sequence: ruleSequence,
189
- conditions: {},
190
- actions: {},
123
+ conditions,
124
+ actions,
191
125
  };
192
126
 
193
- // Add conditions
194
- if (fromAddresses) {
195
- // Parse email addresses
196
- const emailAddresses = fromAddresses
197
- .split(',')
198
- .map((email) => email.trim())
199
- .filter((email) => email)
200
- .map((email) => ({
201
- emailAddress: {
202
- address: email,
203
- },
204
- }));
205
-
206
- if (emailAddresses.length > 0) {
207
- rule.conditions.fromAddresses = emailAddresses;
208
- }
209
- }
210
-
211
- if (containsSubject) {
212
- rule.conditions.subjectContains = [containsSubject];
213
- }
214
-
215
- if (hasAttachments === true) {
216
- rule.conditions.hasAttachment = true;
127
+ // Add exceptions if any were specified
128
+ if (Object.keys(exceptions).length > 0) {
129
+ rule.exceptions = exceptions;
217
130
  }
218
131
 
219
- // Add actions
220
- if (moveToFolder) {
221
- // Get folder ID
222
- try {
223
- const folderId = await getFolderIdByName(accessToken, moveToFolder);
224
- if (!folderId) {
225
- return {
226
- success: false,
227
- message: `Target folder "${moveToFolder}" not found. Please specify a valid folder name.`,
228
- };
229
- }
230
-
231
- rule.actions.moveToFolder = folderId;
232
- } catch (folderError) {
233
- console.error(
234
- `Error resolving folder "${moveToFolder}": ${folderError.message}`
235
- );
236
- return {
237
- success: false,
238
- message: `Error resolving folder "${moveToFolder}": ${folderError.message}`,
239
- };
132
+ // Dry-run: preview without creating
133
+ if (dryRun) {
134
+ const preview = formatRuleDryRunPreview(rule);
135
+ let text = `DRY RUN — Rule preview (not created):\n\n${preview}`;
136
+ if (allWarnings.length > 0) {
137
+ text += `\n\nWarnings:\n${allWarnings.map((w) => `- ${w}`).join('\n')}`;
240
138
  }
241
- }
242
-
243
- if (markAsRead === true) {
244
- rule.actions.markAsRead = true;
139
+ return {
140
+ content: [{ type: 'text', text }],
141
+ };
245
142
  }
246
143
 
247
144
  // Create the rule
@@ -253,20 +150,47 @@ async function createInboxRule(accessToken, ruleOptions) {
253
150
  );
254
151
 
255
152
  if (response && response.id) {
153
+ let text = `Successfully created rule "${name}" with sequence ${ruleSequence}.`;
154
+ if (allWarnings.length > 0) {
155
+ text += `\n\nNotes:\n${allWarnings.map((w) => `- ${w}`).join('\n')}`;
156
+ }
157
+ if (!sequence) {
158
+ text +=
159
+ "\n\nTip: You can specify a 'sequence' parameter when creating rules to control their execution order. Lower sequence numbers run first.";
160
+ }
256
161
  return {
257
- success: true,
258
- message: `Successfully created rule "${name}" with sequence ${ruleSequence}.`,
259
- ruleId: response.id,
162
+ content: [{ type: 'text', text }],
260
163
  };
261
- } else {
164
+ }
165
+
166
+ return {
167
+ content: [
168
+ {
169
+ type: 'text',
170
+ text: "Failed to create rule. The server didn't return a rule ID.",
171
+ },
172
+ ],
173
+ };
174
+ } catch (error) {
175
+ if (error.message === 'Authentication required') {
262
176
  return {
263
- success: false,
264
- message: "Failed to create rule. The server didn't return a rule ID.",
177
+ content: [
178
+ {
179
+ type: 'text',
180
+ text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
181
+ },
182
+ ],
265
183
  };
266
184
  }
267
- } catch (error) {
268
- console.error(`Error creating rule: ${error.message}`);
269
- throw error;
185
+
186
+ return {
187
+ content: [
188
+ {
189
+ type: 'text',
190
+ text: `Error creating rule: ${error.message}`,
191
+ },
192
+ ],
193
+ };
270
194
  }
271
195
  }
272
196