@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.
Files changed (52) hide show
  1. package/.env.example +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +422 -0
  4. package/advanced/index.js +652 -0
  5. package/auth/index.js +32 -0
  6. package/auth/oauth-server.js +233 -0
  7. package/auth/token-manager.js +105 -0
  8. package/auth/token-storage.js +359 -0
  9. package/auth/tools.js +159 -0
  10. package/calendar/accept.js +72 -0
  11. package/calendar/cancel.js +72 -0
  12. package/calendar/create.js +115 -0
  13. package/calendar/decline.js +72 -0
  14. package/calendar/delete.js +67 -0
  15. package/calendar/index.js +130 -0
  16. package/calendar/list.js +108 -0
  17. package/categories/index.js +955 -0
  18. package/config.js +95 -0
  19. package/contacts/index.js +754 -0
  20. package/email/attachments.js +365 -0
  21. package/email/conversations.js +666 -0
  22. package/email/delta.js +210 -0
  23. package/email/export.js +572 -0
  24. package/email/folder-utils.js +192 -0
  25. package/email/headers.js +344 -0
  26. package/email/index.js +537 -0
  27. package/email/list.js +136 -0
  28. package/email/mark-as-read.js +114 -0
  29. package/email/mime.js +286 -0
  30. package/email/read.js +161 -0
  31. package/email/search.js +628 -0
  32. package/email/send.js +169 -0
  33. package/folder/create.js +137 -0
  34. package/folder/delete.js +108 -0
  35. package/folder/index.js +112 -0
  36. package/folder/list.js +289 -0
  37. package/folder/move.js +186 -0
  38. package/folder/stats.js +322 -0
  39. package/index.js +162 -0
  40. package/llms.txt +76 -0
  41. package/outlook-auth-server.js +384 -0
  42. package/package.json +97 -0
  43. package/rules/create.js +273 -0
  44. package/rules/index.js +276 -0
  45. package/rules/list.js +216 -0
  46. package/settings/index.js +678 -0
  47. package/utils/field-presets.js +311 -0
  48. package/utils/graph-api.js +268 -0
  49. package/utils/mock-data.js +154 -0
  50. package/utils/odata-helpers.js +33 -0
  51. package/utils/response-formatter.js +457 -0
  52. package/utils/safety.js +123 -0
@@ -0,0 +1,628 @@
1
+ /**
2
+ * Improved search emails functionality
3
+ *
4
+ * Token-efficient implementation with outputVerbosity support and Markdown formatting.
5
+ */
6
+ const _config = require('../config'); // Reserved for future use
7
+ const { callGraphAPI, callGraphAPIPaginated } = require('../utils/graph-api');
8
+ const { ensureAuthenticated } = require('../auth');
9
+ const { resolveFolderPath } = require('./folder-utils');
10
+ const {
11
+ formatEmailList,
12
+ VERBOSITY,
13
+ DEFAULT_LIMITS,
14
+ } = require('../utils/response-formatter');
15
+ const { getEmailFields } = require('../utils/field-presets');
16
+
17
+ /**
18
+ * Search emails handler
19
+ * @param {object} args - Tool arguments
20
+ * @param {string} [args.folder] - Folder to search (default: inbox)
21
+ * @param {number} [args.count] - Number of results (default: 10, max: 50)
22
+ * @param {string} [args.outputVerbosity] - minimal, standard, or full (default: standard)
23
+ * @param {string} [args.kqlQuery] - Raw KQL query for advanced users
24
+ * @returns {object} - MCP response with Markdown formatted content
25
+ */
26
+ async function handleSearchEmails(args) {
27
+ const folder = args.folder || 'inbox';
28
+
29
+ // Validate count
30
+ if (args.count !== undefined && args.count < 1) {
31
+ return {
32
+ content: [{ type: 'text', text: 'count must be at least 1.' }],
33
+ };
34
+ }
35
+
36
+ const requestedCount = args.count ?? DEFAULT_LIMITS.searchEmails; // Default 10
37
+ const verbosity = args.outputVerbosity || VERBOSITY.STANDARD;
38
+ const query = args.query || '';
39
+ const from = args.from || '';
40
+ const to = args.to || '';
41
+ const subject = args.subject || '';
42
+ const hasAttachments = args.hasAttachments;
43
+ const unreadOnly = args.unreadOnly;
44
+ const receivedAfter = args.receivedAfter || '';
45
+ const receivedBefore = args.receivedBefore || '';
46
+ const searchAllFolders = args.searchAllFolders || false;
47
+ const kqlQuery = args.kqlQuery || ''; // Raw KQL for advanced users
48
+
49
+ // Select fields based on verbosity
50
+ const selectFields = getEmailFields(
51
+ verbosity === VERBOSITY.FULL ? 'search' : 'list'
52
+ );
53
+
54
+ try {
55
+ // Get access token
56
+ const accessToken = await ensureAuthenticated();
57
+
58
+ // Determine endpoint - search all folders or specific folder
59
+ let endpoint;
60
+ if (searchAllFolders) {
61
+ endpoint = 'me/messages';
62
+ console.error('Searching across all mail folders');
63
+ } else {
64
+ endpoint = await resolveFolderPath(accessToken, folder);
65
+ console.error(`Using endpoint: ${endpoint} for folder: ${folder}`);
66
+ }
67
+
68
+ // Execute progressive search with pagination
69
+ const response = await progressiveSearch(
70
+ endpoint,
71
+ accessToken,
72
+ { query, from, to, subject, kqlQuery },
73
+ { hasAttachments, unreadOnly, receivedAfter, receivedBefore },
74
+ requestedCount,
75
+ selectFields
76
+ );
77
+
78
+ return formatSearchResults(response, folder, verbosity);
79
+ } catch (error) {
80
+ // Handle authentication errors
81
+ if (error.message === 'Authentication required') {
82
+ return {
83
+ content: [
84
+ {
85
+ type: 'text',
86
+ text: "Authentication required. Please use the 'authenticate' tool first.",
87
+ },
88
+ ],
89
+ };
90
+ }
91
+
92
+ // General error response
93
+ return {
94
+ content: [
95
+ {
96
+ type: 'text',
97
+ text: `Error searching emails: ${error.message}`,
98
+ },
99
+ ],
100
+ };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Execute a search with progressively simpler fallback strategies
106
+ * @param {string} endpoint - API endpoint
107
+ * @param {string} accessToken - Access token
108
+ * @param {object} searchTerms - Search terms (query, from, to, subject, kqlQuery)
109
+ * @param {object} filterTerms - Filter terms (hasAttachments, unreadOnly)
110
+ * @param {number} maxCount - Maximum number of results to retrieve
111
+ * @param {string} selectFields - Comma-separated field list for $select
112
+ * @returns {Promise<object>} - Search results
113
+ */
114
+ async function progressiveSearch(
115
+ endpoint,
116
+ accessToken,
117
+ searchTerms,
118
+ filterTerms,
119
+ maxCount,
120
+ selectFields
121
+ ) {
122
+ // Track search strategies attempted
123
+ const searchAttempts = [];
124
+
125
+ // 0. If raw KQL query provided, use it directly
126
+ if (searchTerms.kqlQuery) {
127
+ try {
128
+ console.error(`Attempting raw KQL search: "${searchTerms.kqlQuery}"`);
129
+ searchAttempts.push('raw-kql');
130
+
131
+ const kqlParams = {
132
+ $top: Math.min(50, maxCount),
133
+ $select: selectFields,
134
+ $search: `"${searchTerms.kqlQuery}"`,
135
+ };
136
+
137
+ const response = await callGraphAPIPaginated(
138
+ accessToken,
139
+ 'GET',
140
+ endpoint,
141
+ kqlParams,
142
+ maxCount
143
+ );
144
+ if (response.value && response.value.length > 0) {
145
+ console.error(
146
+ `Raw KQL search successful: found ${response.value.length} results`
147
+ );
148
+ return response;
149
+ }
150
+ } catch (error) {
151
+ console.error(`Raw KQL search failed: ${error.message}`);
152
+ }
153
+ }
154
+
155
+ // 1. Try combined search (most specific)
156
+ try {
157
+ const params = buildSearchParams(
158
+ searchTerms,
159
+ filterTerms,
160
+ Math.min(50, maxCount),
161
+ selectFields
162
+ );
163
+ console.error('Attempting combined search with params:', params);
164
+ searchAttempts.push('combined-search');
165
+
166
+ const response = await callGraphAPIPaginated(
167
+ accessToken,
168
+ 'GET',
169
+ endpoint,
170
+ params,
171
+ maxCount
172
+ );
173
+ if (response.value && response.value.length > 0) {
174
+ console.error(
175
+ `Combined search successful: found ${response.value.length} results`
176
+ );
177
+ return response;
178
+ }
179
+ } catch (error) {
180
+ console.error(`Combined search failed: ${error.message}`);
181
+ }
182
+
183
+ // 2. Try each search term individually, starting with most specific
184
+ const searchPriority = ['from', 'to', 'subject', 'query'];
185
+
186
+ for (const term of searchPriority) {
187
+ if (searchTerms[term]) {
188
+ try {
189
+ console.error(
190
+ `Attempting search with only ${term}: "${searchTerms[term]}"`
191
+ );
192
+ searchAttempts.push(`single-term-${term}`);
193
+
194
+ const simplifiedParams = {
195
+ $top: Math.min(50, maxCount),
196
+ $select: selectFields,
197
+ };
198
+
199
+ // Use $filter for from/to/subject (more reliable on personal accounts),
200
+ // $search for free-text query only.
201
+ // NOTE: $filter and $orderby cannot be used together on mailbox - Graph API limitation
202
+ if (term === 'from') {
203
+ if (searchTerms[term].includes('@')) {
204
+ simplifiedParams.$filter = `from/emailAddress/address eq '${searchTerms[term]}'`;
205
+ } else {
206
+ simplifiedParams.$filter = `contains(from/emailAddress/name, '${searchTerms[term]}')`;
207
+ }
208
+ } else if (term === 'to') {
209
+ if (searchTerms[term].includes('@')) {
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
+ }
214
+ } else if (term === 'subject') {
215
+ // Use $filter with contains() — $search silently fails on personal MS accounts
216
+ simplifiedParams.$filter = `contains(subject, '${searchTerms[term].replace(/'/g, "''")}')`;
217
+ } else if (term === 'query') {
218
+ simplifiedParams.$orderby = 'receivedDateTime desc';
219
+ simplifiedParams.$search = `"${searchTerms[term]}"`;
220
+ }
221
+
222
+ // Add boolean filters if applicable
223
+ addBooleanFilters(simplifiedParams, filterTerms);
224
+
225
+ const response = await callGraphAPIPaginated(
226
+ accessToken,
227
+ 'GET',
228
+ endpoint,
229
+ simplifiedParams,
230
+ maxCount
231
+ );
232
+ if (response.value && response.value.length > 0) {
233
+ console.error(
234
+ `Search with ${term} successful: found ${response.value.length} results`
235
+ );
236
+ return response;
237
+ }
238
+ } catch (error) {
239
+ console.error(`Search with ${term} failed: ${error.message}`);
240
+ }
241
+ }
242
+ }
243
+
244
+ // 3. Try with only boolean filters
245
+ if (filterTerms.hasAttachments === true || filterTerms.unreadOnly === true) {
246
+ try {
247
+ console.error('Attempting search with only boolean filters');
248
+ searchAttempts.push('boolean-filters-only');
249
+
250
+ const filterOnlyParams = {
251
+ $top: Math.min(50, maxCount),
252
+ $select: selectFields,
253
+ $orderby: 'receivedDateTime desc',
254
+ };
255
+
256
+ // Add the boolean filters
257
+ addBooleanFilters(filterOnlyParams, filterTerms);
258
+
259
+ const response = await callGraphAPIPaginated(
260
+ accessToken,
261
+ 'GET',
262
+ endpoint,
263
+ filterOnlyParams,
264
+ maxCount
265
+ );
266
+ console.error(
267
+ `Boolean filter search found ${response.value?.length || 0} results`
268
+ );
269
+ return response;
270
+ } catch (error) {
271
+ console.error(`Boolean filter search failed: ${error.message}`);
272
+ }
273
+ }
274
+
275
+ // 4. Final fallback: just get recent emails with pagination
276
+ console.error('All search strategies failed, falling back to recent emails');
277
+ searchAttempts.push('recent-emails');
278
+
279
+ const basicParams = {
280
+ $top: Math.min(50, maxCount),
281
+ $select: selectFields,
282
+ $orderby: 'receivedDateTime desc',
283
+ };
284
+
285
+ const response = await callGraphAPIPaginated(
286
+ accessToken,
287
+ 'GET',
288
+ endpoint,
289
+ basicParams,
290
+ maxCount
291
+ );
292
+ console.error(
293
+ `Fallback to recent emails found ${response.value?.length || 0} results`
294
+ );
295
+
296
+ // Add a note to the response about the search attempts
297
+ response._searchInfo = {
298
+ attemptsCount: searchAttempts.length,
299
+ strategies: searchAttempts,
300
+ originalTerms: searchTerms,
301
+ filterTerms: filterTerms,
302
+ };
303
+
304
+ return response;
305
+ }
306
+
307
+ /**
308
+ * Build search parameters from search terms and filter terms
309
+ * Uses $filter for email addresses (more reliable than $search)
310
+ * Uses $search only for general query and subject
311
+ * @param {object} searchTerms - Search terms (query, from, to, subject)
312
+ * @param {object} filterTerms - Filter terms (hasAttachments, unreadOnly)
313
+ * @param {number} count - Maximum number of results
314
+ * @param {string} selectFields - Comma-separated field list for $select
315
+ * @returns {object} - Query parameters
316
+ */
317
+ function buildSearchParams(searchTerms, filterTerms, count, selectFields) {
318
+ const params = {
319
+ $top: count,
320
+ $select: selectFields,
321
+ };
322
+
323
+ // Track if we're using email address filters (which are incompatible with $orderby)
324
+ let usesEmailFilter = false;
325
+
326
+ // Handle search terms - use $search only for free-text query
327
+ if (searchTerms.query) {
328
+ params.$search = `"${searchTerms.query}"`;
329
+ }
330
+
331
+ // Build filter conditions array - use $filter for structured fields (more reliable)
332
+ const filterConditions = [];
333
+
334
+ // Use $filter for subject — $search silently fails on personal MS accounts
335
+ if (searchTerms.subject) {
336
+ filterConditions.push(
337
+ `contains(subject, '${searchTerms.subject.replace(/'/g, "''")}')`
338
+ );
339
+ }
340
+
341
+ // Use $filter for from/to email addresses (much more reliable than $search)
342
+ // NOTE: $filter on email addresses is incompatible with $orderby - Graph API limitation
343
+ if (searchTerms.from) {
344
+ usesEmailFilter = true;
345
+ if (searchTerms.from.includes('@')) {
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
+ }
354
+ }
355
+
356
+ if (searchTerms.to) {
357
+ usesEmailFilter = true;
358
+ if (searchTerms.to.includes('@')) {
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
+ }
367
+ }
368
+
369
+ // Add boolean filters (these ARE compatible with $orderby)
370
+ if (filterTerms.hasAttachments === true) {
371
+ filterConditions.push('hasAttachments eq true');
372
+ }
373
+
374
+ if (filterTerms.unreadOnly === true) {
375
+ filterConditions.push('isRead eq false');
376
+ }
377
+
378
+ // Add date range filters
379
+ if (filterTerms.receivedAfter) {
380
+ try {
381
+ const afterDate = new Date(filterTerms.receivedAfter).toISOString();
382
+ filterConditions.push(`receivedDateTime ge ${afterDate}`);
383
+ } catch (_e) {
384
+ console.error(`Invalid receivedAfter date: ${filterTerms.receivedAfter}`);
385
+ }
386
+ }
387
+
388
+ if (filterTerms.receivedBefore) {
389
+ try {
390
+ const beforeDate = new Date(filterTerms.receivedBefore).toISOString();
391
+ filterConditions.push(`receivedDateTime le ${beforeDate}`);
392
+ } catch (_e) {
393
+ console.error(
394
+ `Invalid receivedBefore date: ${filterTerms.receivedBefore}`
395
+ );
396
+ }
397
+ }
398
+
399
+ // Only add $orderby if we're NOT using email address filters
400
+ if (!usesEmailFilter) {
401
+ params.$orderby = 'receivedDateTime desc';
402
+ }
403
+
404
+ // Combine all filter conditions
405
+ if (filterConditions.length > 0) {
406
+ params.$filter = filterConditions.join(' and ');
407
+ }
408
+
409
+ return params;
410
+ }
411
+
412
+ /**
413
+ * Add boolean and date filters to query parameters
414
+ * @param {object} params - Query parameters
415
+ * @param {object} filterTerms - Filter terms (hasAttachments, unreadOnly, receivedAfter, receivedBefore)
416
+ */
417
+ function addBooleanFilters(params, filterTerms) {
418
+ const filterConditions = [];
419
+
420
+ if (filterTerms.hasAttachments === true) {
421
+ filterConditions.push('hasAttachments eq true');
422
+ }
423
+
424
+ if (filterTerms.unreadOnly === true) {
425
+ filterConditions.push('isRead eq false');
426
+ }
427
+
428
+ // Add date range filters
429
+ if (filterTerms.receivedAfter) {
430
+ try {
431
+ const afterDate = new Date(filterTerms.receivedAfter).toISOString();
432
+ filterConditions.push(`receivedDateTime ge ${afterDate}`);
433
+ } catch (_e) {
434
+ console.error(`Invalid receivedAfter date: ${filterTerms.receivedAfter}`);
435
+ }
436
+ }
437
+
438
+ if (filterTerms.receivedBefore) {
439
+ try {
440
+ const beforeDate = new Date(filterTerms.receivedBefore).toISOString();
441
+ filterConditions.push(`receivedDateTime le ${beforeDate}`);
442
+ } catch (_e) {
443
+ console.error(
444
+ `Invalid receivedBefore date: ${filterTerms.receivedBefore}`
445
+ );
446
+ }
447
+ }
448
+
449
+ // Add $filter parameter if we have any filter conditions
450
+ if (filterConditions.length > 0) {
451
+ params.$filter = filterConditions.join(' and ');
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Format search results into Markdown using response-formatter utilities
457
+ * @param {object} response - The API response object
458
+ * @param {string} folder - Folder that was searched
459
+ * @param {string} verbosity - Output verbosity level
460
+ * @returns {object} - MCP response object
461
+ */
462
+ function formatSearchResults(response, folder, verbosity) {
463
+ if (!response.value || response.value.length === 0) {
464
+ return {
465
+ content: [
466
+ {
467
+ type: 'text',
468
+ text: `No emails found matching your search criteria.`,
469
+ },
470
+ ],
471
+ };
472
+ }
473
+
474
+ // Build metadata
475
+ const meta = {
476
+ returned: response.value.length,
477
+ totalAvailable: response['@odata.count'] || null,
478
+ hasMore: !!response['@odata.nextLink'],
479
+ verbosity: verbosity,
480
+ };
481
+
482
+ // Add search strategy info if available (for debugging)
483
+ let searchNote = '';
484
+ if (response._searchInfo) {
485
+ const strategy =
486
+ response._searchInfo.strategies[
487
+ response._searchInfo.strategies.length - 1
488
+ ];
489
+ if (strategy === 'recent-emails') {
490
+ searchNote =
491
+ '\n\n**Note**: Your search query could not be applied — showing recent emails instead. ' +
492
+ 'On personal Microsoft accounts, free-text `query` search may not work. ' +
493
+ 'Try using `subject`, `from`, `to`, or `kqlQuery` parameters for more reliable filtering.';
494
+ } else {
495
+ searchNote = `\n\n_Search strategy: ${strategy}_`;
496
+ }
497
+ }
498
+
499
+ // Format results using shared formatter
500
+ const formattedOutput = formatEmailList(
501
+ response.value,
502
+ `Search Results (${folder})`,
503
+ verbosity,
504
+ meta
505
+ );
506
+
507
+ return {
508
+ content: [
509
+ {
510
+ type: 'text',
511
+ text: formattedOutput + searchNote,
512
+ },
513
+ ],
514
+ _meta: meta,
515
+ };
516
+ }
517
+
518
+ /**
519
+ * Search for email by Message-ID header
520
+ * @param {object} args - Tool arguments
521
+ * @param {string} args.messageId - Full Message-ID header value (e.g., <abc123@example.com>)
522
+ * @param {string} [args.outputVerbosity] - Output detail level
523
+ * @returns {object} - MCP response with matching email(s)
524
+ */
525
+ async function handleSearchByMessageId(args) {
526
+ const messageId = args.messageId;
527
+ const verbosity = args.outputVerbosity || VERBOSITY.STANDARD;
528
+
529
+ if (!messageId) {
530
+ return {
531
+ content: [
532
+ {
533
+ type: 'text',
534
+ text: 'Message-ID is required. Provide the full Message-ID header value (e.g., <abc123@example.com>)',
535
+ },
536
+ ],
537
+ };
538
+ }
539
+
540
+ try {
541
+ const accessToken = await ensureAuthenticated();
542
+
543
+ // Search across all folders for the Message-ID
544
+ const selectFields = getEmailFields(
545
+ verbosity === VERBOSITY.FULL ? 'forensic' : 'read'
546
+ );
547
+
548
+ // Build filter - need to escape the Message-ID properly
549
+ // Graph API expects: internetMessageId eq '<value>'
550
+ const escapedMessageId = messageId.replace(/'/g, "''");
551
+
552
+ const params = {
553
+ $filter: `internetMessageId eq '${escapedMessageId}'`,
554
+ $select: selectFields,
555
+ $top: '10', // Usually only one match, but allow for edge cases
556
+ };
557
+
558
+ console.error(`Searching for Message-ID: ${messageId}`);
559
+
560
+ const response = await callGraphAPI(
561
+ accessToken,
562
+ 'GET',
563
+ 'me/messages',
564
+ null,
565
+ params
566
+ );
567
+
568
+ const emails = response.value || [];
569
+
570
+ if (emails.length === 0) {
571
+ return {
572
+ content: [
573
+ {
574
+ type: 'text',
575
+ text: `## No Email Found\n\nNo email found with Message-ID: \`${messageId}\`\n\n**Tips:**\n- Ensure the full Message-ID is provided including angle brackets\n- Message-ID format: \`<unique-id@domain.com>\`\n- The email may have been deleted or not yet synced`,
576
+ },
577
+ ],
578
+ };
579
+ }
580
+
581
+ // Format results
582
+ let resultText = `## Message-ID Search Results\n\n`;
583
+ resultText += `**Query:** \`${messageId}\`\n`;
584
+ resultText += `**Found:** ${emails.length} email(s)\n\n`;
585
+
586
+ // Use formatEmailList for consistent output
587
+ resultText += formatEmailList(emails, 'Match', verbosity);
588
+
589
+ return {
590
+ content: [
591
+ {
592
+ type: 'text',
593
+ text: resultText,
594
+ },
595
+ ],
596
+ _meta: {
597
+ messageId: messageId,
598
+ matchCount: emails.length,
599
+ emailIds: emails.map((e) => e.id),
600
+ },
601
+ };
602
+ } catch (error) {
603
+ if (error.message === 'Authentication required') {
604
+ return {
605
+ content: [
606
+ {
607
+ type: 'text',
608
+ text: "Authentication required. Please use the 'authenticate' tool first.",
609
+ },
610
+ ],
611
+ };
612
+ }
613
+
614
+ return {
615
+ content: [
616
+ {
617
+ type: 'text',
618
+ text: `Error searching by Message-ID: ${error.message}`,
619
+ },
620
+ ],
621
+ };
622
+ }
623
+ }
624
+
625
+ module.exports = {
626
+ handleSearchEmails,
627
+ handleSearchByMessageId,
628
+ };