@littlebearapps/outlook-assistant 3.6.0 → 3.7.1

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 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
- let output = [];
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] + 'T09:00:00';
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
- let output = [];
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
- let output = [];
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
- let output = [];
507
+ const output = [];
508
508
  output.push(`# Meeting Rooms (${rooms.length})\n`);
509
509
 
510
510
  if (verbosity === 'minimal') {
@@ -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) => setTimeout(resolve, pollInterval * 1000));
95
+ await new Promise((resolve) => {
96
+ setTimeout(resolve, pollInterval * 1000);
97
+ });
96
98
 
97
99
  const postData = querystring.stringify({
98
100
  client_id: clientId,
@@ -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
- `${authConfig.authEndpoint}?` +
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
 
@@ -88,8 +88,8 @@ function getAccessToken() {
88
88
  */
89
89
  function createTestTokens() {
90
90
  const testTokens = {
91
- access_token: 'test_access_token_' + Date.now(),
92
- refresh_token: 'test_refresh_token_' + Date.now(),
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
- let endpoint = 'me/events';
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 + 'Z';
57
+ : `${event.start.dateTime}Z`;
58
58
  const endDt = event.end.dateTime.endsWith('Z')
59
59
  ? event.end.dateTime
60
- : event.end.dateTime + 'Z';
60
+ : `${event.end.dateTime}Z`;
61
61
  const startDate = new Date(startDt).toLocaleString('en-AU', {
62
62
  timeZone: tz,
63
63
  dateStyle: 'medium',
@@ -106,7 +106,7 @@ async function handleListCategories(args) {
106
106
  }
107
107
 
108
108
  // Format output based on verbosity
109
- let output = [];
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
- let output = [];
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
- let output = [];
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
- if (contact.homePhones?.length > 0)
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
- let output = [];
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
- let output = [];
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
- let output = [];
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)`);
@@ -175,12 +175,12 @@ async function handleListConversations(args) {
175
175
  });
176
176
 
177
177
  // Convert to array and sort by most recent
178
- let conversationList = Array.from(conversations.values())
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
- let output = [];
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
- let output = [];
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
- let exportedFiles = [];
467
- let exportStats = { messages: messages.length, attachments: 0, bytes: 0 };
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 date = new Date(msg.receivedDateTime);
501
- const mboxDate = date.toUTCString().replace('GMT', '+0000');
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
- let mdContent = [];
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
- let htmlContent = [];
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
- const sizeFormatted =
678
- exportStats.bytes < 1024
679
- ? `${exportStats.bytes} B`
680
- : exportStats.bytes < 1024 * 1024
681
- ? `${(exportStats.bytes / 1024).toFixed(1)} KB`
682
- : `${(exportStats.bytes / (1024 * 1024)).toFixed(2)} MB`;
683
-
684
- let output = [];
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 = !!nextLink;
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
- tipsText +
174
- '\n\n---\n\n' +
175
- preview.content[0].text.replace(
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 },
@@ -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['inbox'];
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
- let output = [];
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['Threading'].push(h);
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['Authentication'].push(h);
77
+ groups.Authentication.push(h);
78
78
  } else if (name === 'Received' || name.startsWith('X-MS-Exchange')) {
79
- groups['Delivery'].push(h);
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['Content'].push(h);
91
+ groups.Content.push(h);
92
92
  } else {
93
- groups['Other'].push(h);
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) + '...' : h.value;
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) + '...' : h.value;
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) + '...' : h.value;
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
- let output = [];
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: !!response['@odata.nextLink'],
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 += ' ' + 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(