emailengine-app 2.61.4 → 2.62.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/CHANGELOG.md +87 -0
- package/data/google-crawlers.json +1 -1
- package/lib/account.js +20 -7
- package/lib/api-routes/account-routes.js +28 -5
- package/lib/api-routes/chat-routes.js +1 -1
- package/lib/api-routes/export-routes.js +316 -0
- package/lib/api-routes/message-routes.js +28 -23
- package/lib/api-routes/template-routes.js +28 -7
- package/lib/arf-detect.js +1 -1
- package/lib/consts.js +16 -0
- package/lib/db.js +3 -0
- package/lib/email-client/base-client.js +6 -4
- package/lib/email-client/gmail-client.js +204 -33
- package/lib/email-client/imap/mailbox.js +99 -8
- package/lib/email-client/imap/subconnection.js +5 -5
- package/lib/email-client/imap-client.js +76 -16
- package/lib/email-client/message-builder.js +3 -1
- package/lib/email-client/notification-handler.js +12 -9
- package/lib/email-client/outlook-client.js +362 -69
- package/lib/email-client/smtp-pool-manager.js +1 -1
- package/lib/export.js +528 -0
- package/lib/oauth/gmail.js +21 -13
- package/lib/oauth/mail-ru.js +23 -10
- package/lib/oauth/outlook.js +26 -16
- package/lib/oauth/pubsub/google.js +5 -0
- package/lib/routes-ui.js +236 -2
- package/lib/schemas.js +260 -80
- package/lib/stream-encrypt.js +263 -0
- package/lib/tools.js +30 -4
- package/lib/ui-routes/account-routes.js +24 -1
- package/lib/ui-routes/admin-config-routes.js +11 -4
- package/lib/ui-routes/admin-entities-routes.js +18 -0
- package/lib/webhooks.js +16 -20
- package/package.json +17 -17
- package/sbom.json +1 -1
- package/server.js +41 -5
- package/static/js/ace/ace.js +1 -1
- package/static/js/ace/ext-language_tools.js +1 -1
- package/static/licenses.html +47 -127
- package/translations/de.mo +0 -0
- package/translations/de.po +63 -36
- package/translations/en.mo +0 -0
- package/translations/en.po +64 -37
- package/translations/et.mo +0 -0
- package/translations/et.po +63 -36
- package/translations/fr.mo +0 -0
- package/translations/fr.po +63 -36
- package/translations/ja.mo +0 -0
- package/translations/ja.po +63 -36
- package/translations/messages.pot +88 -55
- package/translations/nl.mo +0 -0
- package/translations/nl.po +63 -36
- package/translations/pl.mo +0 -0
- package/translations/pl.po +63 -36
- package/views/accounts/account.hbs +375 -2
- package/views/config/service.hbs +35 -0
- package/workers/api.js +124 -45
- package/workers/documents.js +1 -0
- package/workers/export.js +926 -0
- package/workers/imap.js +29 -0
- package/workers/submit.js +25 -5
- package/workers/webhooks.js +11 -2
|
@@ -30,7 +30,8 @@ const {
|
|
|
30
30
|
OUTLOOK_RETRY_MAX_DELAY,
|
|
31
31
|
MESSAGE_UPDATED_NOTIFY,
|
|
32
32
|
MESSAGE_DELETED_NOTIFY,
|
|
33
|
-
MESSAGE_MISSING_NOTIFY
|
|
33
|
+
MESSAGE_MISSING_NOTIFY,
|
|
34
|
+
DEFAULT_OUTLOOK_EXPORT_BATCH_SIZE
|
|
34
35
|
} = require('../consts');
|
|
35
36
|
|
|
36
37
|
// Maximum number of operations in a single batch request to Microsoft Graph API
|
|
@@ -340,51 +341,66 @@ class OutlookClient extends BaseClient {
|
|
|
340
341
|
}
|
|
341
342
|
|
|
342
343
|
// Format mailbox data for API response
|
|
343
|
-
let mailboxes = mailboxListing
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
344
|
+
let mailboxes = mailboxListing.map(entry => {
|
|
345
|
+
let folderData = {
|
|
346
|
+
id: entry.id,
|
|
347
|
+
path: entry.pathName,
|
|
348
|
+
delimiter: '/',
|
|
349
|
+
parentPath: entry.parentPath,
|
|
350
|
+
name: entry.displayName,
|
|
351
|
+
listed: true,
|
|
352
|
+
subscribed: true
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
if (entry.specialUse) {
|
|
356
|
+
folderData.specialUse = entry.specialUse;
|
|
357
|
+
folderData.specialUseSource = 'extension';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Include message counts if requested
|
|
361
|
+
if (options?.statusQuery?.messages) {
|
|
362
|
+
folderData.status = {
|
|
363
|
+
messages: entry.totalItemCount,
|
|
364
|
+
unseen: entry.unreadItemCount
|
|
353
365
|
};
|
|
366
|
+
}
|
|
354
367
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
folderData.specialUseSource = 'extension';
|
|
358
|
-
}
|
|
368
|
+
return folderData;
|
|
369
|
+
});
|
|
359
370
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
371
|
+
// Add virtual "All Mail" folder for Outlook API
|
|
372
|
+
// This allows exporting all messages without scanning individual folders
|
|
373
|
+
mailboxes.unshift({
|
|
374
|
+
id: 'virtual_all',
|
|
375
|
+
path: '\\All',
|
|
376
|
+
delimiter: '/',
|
|
377
|
+
parentPath: '',
|
|
378
|
+
name: 'All Mail',
|
|
379
|
+
listed: true,
|
|
380
|
+
subscribed: false,
|
|
381
|
+
noSelect: true,
|
|
382
|
+
specialUse: '\\All',
|
|
383
|
+
specialUseSource: 'extension'
|
|
384
|
+
});
|
|
367
385
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return 1;
|
|
376
|
-
}
|
|
386
|
+
mailboxes.sort((a, b) => {
|
|
387
|
+
// INBOX always comes first
|
|
388
|
+
if (a.path === 'INBOX' || a.specialUse === '\\Inbox') {
|
|
389
|
+
return -1;
|
|
390
|
+
} else if (b.path === 'INBOX' || b.specialUse === '\\Inbox') {
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
377
393
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
394
|
+
// Special use folders come before regular folders
|
|
395
|
+
if (a.specialUse && !b.specialUse) {
|
|
396
|
+
return -1;
|
|
397
|
+
} else if (!a.specialUse && b.specialUse) {
|
|
398
|
+
return 1;
|
|
399
|
+
}
|
|
384
400
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
401
|
+
// Alphabetical sorting for the rest
|
|
402
|
+
return a.path.toLowerCase().localeCompare(b.path.toLowerCase());
|
|
403
|
+
});
|
|
388
404
|
|
|
389
405
|
return mailboxes;
|
|
390
406
|
}
|
|
@@ -395,6 +411,8 @@ class OutlookClient extends BaseClient {
|
|
|
395
411
|
*/
|
|
396
412
|
async listMessages(query, options) {
|
|
397
413
|
options = options || {};
|
|
414
|
+
// Support metadataOnly passed via query (from export worker)
|
|
415
|
+
options.metadataOnly = options.metadataOnly || query.metadataOnly;
|
|
398
416
|
|
|
399
417
|
await this.prepare();
|
|
400
418
|
|
|
@@ -447,6 +465,53 @@ class OutlookClient extends BaseClient {
|
|
|
447
465
|
}
|
|
448
466
|
}
|
|
449
467
|
|
|
468
|
+
// Build select fields based on detail level
|
|
469
|
+
let selectFields;
|
|
470
|
+
let expandFields;
|
|
471
|
+
|
|
472
|
+
if (options.minimalFields) {
|
|
473
|
+
selectFields = ['id', 'conversationId', 'receivedDateTime'];
|
|
474
|
+
} else if (options.metadataOnly) {
|
|
475
|
+
selectFields = [
|
|
476
|
+
'id',
|
|
477
|
+
'conversationId',
|
|
478
|
+
'receivedDateTime',
|
|
479
|
+
'isRead',
|
|
480
|
+
'isDraft',
|
|
481
|
+
'flag',
|
|
482
|
+
'body',
|
|
483
|
+
'subject',
|
|
484
|
+
'from',
|
|
485
|
+
'replyTo',
|
|
486
|
+
'sender',
|
|
487
|
+
'internetMessageId'
|
|
488
|
+
];
|
|
489
|
+
} else {
|
|
490
|
+
selectFields = [
|
|
491
|
+
'id',
|
|
492
|
+
'conversationId',
|
|
493
|
+
'receivedDateTime',
|
|
494
|
+
'isRead',
|
|
495
|
+
'isDraft',
|
|
496
|
+
'flag',
|
|
497
|
+
'body',
|
|
498
|
+
'subject',
|
|
499
|
+
'from',
|
|
500
|
+
'replyTo',
|
|
501
|
+
'sender',
|
|
502
|
+
'toRecipients',
|
|
503
|
+
'ccRecipients',
|
|
504
|
+
'bccRecipients',
|
|
505
|
+
'internetMessageId',
|
|
506
|
+
'bodyPreview'
|
|
507
|
+
];
|
|
508
|
+
expandFields = 'attachments($select=id,name,contentType,size,isInline,microsoft.graph.fileAttachment/contentId)';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!folder) {
|
|
512
|
+
selectFields.push('parentFolderId');
|
|
513
|
+
}
|
|
514
|
+
|
|
450
515
|
// Build Graph API query parameters
|
|
451
516
|
let requestQuery = {
|
|
452
517
|
$count: true,
|
|
@@ -454,30 +519,8 @@ class OutlookClient extends BaseClient {
|
|
|
454
519
|
$skip: page * pageSize,
|
|
455
520
|
$skiptoken,
|
|
456
521
|
$orderBy: 'receivedDateTime desc',
|
|
457
|
-
$select: (
|
|
458
|
-
|
|
459
|
-
: [
|
|
460
|
-
'id',
|
|
461
|
-
'conversationId',
|
|
462
|
-
'receivedDateTime',
|
|
463
|
-
'isRead',
|
|
464
|
-
'isDraft',
|
|
465
|
-
'flag',
|
|
466
|
-
'body',
|
|
467
|
-
'subject',
|
|
468
|
-
'from',
|
|
469
|
-
'replyTo',
|
|
470
|
-
'sender',
|
|
471
|
-
'toRecipients',
|
|
472
|
-
'ccRecipients',
|
|
473
|
-
'bccRecipients',
|
|
474
|
-
'internetMessageId',
|
|
475
|
-
'bodyPreview'
|
|
476
|
-
]
|
|
477
|
-
)
|
|
478
|
-
.concat(!folder ? 'parentFolderId' : [])
|
|
479
|
-
.join(','),
|
|
480
|
-
$expand: options.metadataOnly ? undefined : 'attachments($select=id,name,contentType,size,isInline,microsoft.graph.fileAttachment/contentId)'
|
|
522
|
+
$select: selectFields.join(','),
|
|
523
|
+
$expand: expandFields
|
|
481
524
|
};
|
|
482
525
|
|
|
483
526
|
let useOutlookSearch = false;
|
|
@@ -523,7 +566,11 @@ class OutlookClient extends BaseClient {
|
|
|
523
566
|
|
|
524
567
|
messages =
|
|
525
568
|
listing?.value?.map(messageData =>
|
|
526
|
-
this.formatMessage(messageData, {
|
|
569
|
+
this.formatMessage(messageData, {
|
|
570
|
+
path: quickResolveFolder(messageData.parentFolderId)?.pathName,
|
|
571
|
+
showPath: !folder,
|
|
572
|
+
minimalFields: options.minimalFields
|
|
573
|
+
})
|
|
527
574
|
) || [];
|
|
528
575
|
} catch (err) {
|
|
529
576
|
this.logger.error({
|
|
@@ -1571,6 +1618,229 @@ class OutlookClient extends BaseClient {
|
|
|
1571
1618
|
return formattedMessage;
|
|
1572
1619
|
}
|
|
1573
1620
|
|
|
1621
|
+
/**
|
|
1622
|
+
* Fetches multiple messages using MS Graph batch API for efficient export operations
|
|
1623
|
+
* Uses /$batch endpoint to send up to 20 message requests in a single HTTP call
|
|
1624
|
+
* @param {string[]} emailIds - Array of message IDs
|
|
1625
|
+
* @param {Object} options - Fetch options
|
|
1626
|
+
* @returns {Object[]} Array of results with messageId, data, and error fields
|
|
1627
|
+
*/
|
|
1628
|
+
async getMessages(emailIds, options) {
|
|
1629
|
+
options = options || {};
|
|
1630
|
+
await this.prepare();
|
|
1631
|
+
|
|
1632
|
+
// Pre-fetch folder listing once for all messages to avoid repeated lookups
|
|
1633
|
+
const mailboxListing = await this.getMailboxListing();
|
|
1634
|
+
const folderMap = new Map(mailboxListing.map(f => [f.id, f]));
|
|
1635
|
+
|
|
1636
|
+
const results = [];
|
|
1637
|
+
let idGen = 0;
|
|
1638
|
+
|
|
1639
|
+
// Build select fields for batch request
|
|
1640
|
+
const selectFields = [
|
|
1641
|
+
'id',
|
|
1642
|
+
'conversationId',
|
|
1643
|
+
'receivedDateTime',
|
|
1644
|
+
'isRead',
|
|
1645
|
+
'isDraft',
|
|
1646
|
+
'flag',
|
|
1647
|
+
'body',
|
|
1648
|
+
'subject',
|
|
1649
|
+
'from',
|
|
1650
|
+
'replyTo',
|
|
1651
|
+
'sender',
|
|
1652
|
+
'toRecipients',
|
|
1653
|
+
'ccRecipients',
|
|
1654
|
+
'bccRecipients',
|
|
1655
|
+
'internetMessageId',
|
|
1656
|
+
'bodyPreview',
|
|
1657
|
+
'internetMessageHeaders',
|
|
1658
|
+
'parentFolderId',
|
|
1659
|
+
'categories'
|
|
1660
|
+
].join(',');
|
|
1661
|
+
|
|
1662
|
+
const expandFields = 'attachments($select=id,name,contentType,size,isInline,microsoft.graph.fileAttachment/contentId)';
|
|
1663
|
+
|
|
1664
|
+
// Build Prefer header for body content type
|
|
1665
|
+
// Note: IdType="ImmutableId" is added by requestWithRetry automatically
|
|
1666
|
+
let preferHeader = '';
|
|
1667
|
+
if (options.textType === 'plain') {
|
|
1668
|
+
preferHeader = 'outlook.body-content-type="text"';
|
|
1669
|
+
} else if (options.textType === 'html') {
|
|
1670
|
+
preferHeader = 'outlook.body-content-type="html"';
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Get configured batch size, capped at MS Graph API limit
|
|
1674
|
+
const settingsBatchSize = await settings.get('outlookExportBatchSize');
|
|
1675
|
+
const batchSize = Math.min(settingsBatchSize || DEFAULT_OUTLOOK_EXPORT_BATCH_SIZE, MAX_BATCH_SIZE);
|
|
1676
|
+
|
|
1677
|
+
for (let i = 0; i < emailIds.length; i += batchSize) {
|
|
1678
|
+
const batch = emailIds.slice(i, i + batchSize);
|
|
1679
|
+
const messageMap = new Map();
|
|
1680
|
+
|
|
1681
|
+
// Build batch request with up to MAX_BATCH_SIZE individual requests
|
|
1682
|
+
const requests = batch.map(emailId => {
|
|
1683
|
+
const reqId = `msg_${++idGen}`;
|
|
1684
|
+
messageMap.set(reqId, emailId);
|
|
1685
|
+
|
|
1686
|
+
const request = {
|
|
1687
|
+
id: reqId,
|
|
1688
|
+
method: 'GET',
|
|
1689
|
+
url: `/${this.oauth2UserPath}/messages/${emailId}?$select=${selectFields}&$expand=${expandFields}`
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
// Add Prefer header for body content type if specified
|
|
1693
|
+
if (preferHeader) {
|
|
1694
|
+
request.headers = { Prefer: preferHeader };
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
return request;
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
// Submit batch request - single HTTP call for up to 20 messages
|
|
1701
|
+
// Retry on transient errors (429 rate limit, 5xx server errors)
|
|
1702
|
+
const BATCH_MAX_RETRIES = 3;
|
|
1703
|
+
const BATCH_RETRY_BASE_DELAY = 5000;
|
|
1704
|
+
|
|
1705
|
+
let responseData;
|
|
1706
|
+
let batchSuccess = false;
|
|
1707
|
+
|
|
1708
|
+
for (let attempt = 0; attempt <= BATCH_MAX_RETRIES; attempt++) {
|
|
1709
|
+
try {
|
|
1710
|
+
responseData = await this.request('/$batch', 'post', { requests });
|
|
1711
|
+
batchSuccess = true;
|
|
1712
|
+
break;
|
|
1713
|
+
} catch (err) {
|
|
1714
|
+
const isRetryable = err.oauthRequest?.status === 429 || (err.oauthRequest?.status >= 500 && err.oauthRequest?.status < 600);
|
|
1715
|
+
|
|
1716
|
+
if (!isRetryable || attempt === BATCH_MAX_RETRIES) {
|
|
1717
|
+
// Non-retryable error or max retries reached - return errors for all messages in this batch
|
|
1718
|
+
for (const emailId of batch) {
|
|
1719
|
+
results.push({
|
|
1720
|
+
messageId: emailId,
|
|
1721
|
+
data: null,
|
|
1722
|
+
error: { message: err.message, code: err.code, statusCode: err.oauthRequest?.status }
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
break;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// Wait before retrying with exponential backoff + jitter
|
|
1729
|
+
const delay = BATCH_RETRY_BASE_DELAY * Math.pow(2, attempt) + Math.random() * 1000;
|
|
1730
|
+
this.logger.warn({
|
|
1731
|
+
msg: 'Batch request failed, retrying',
|
|
1732
|
+
account: this.account,
|
|
1733
|
+
attempt: attempt + 1,
|
|
1734
|
+
maxAttempts: BATCH_MAX_RETRIES + 1,
|
|
1735
|
+
statusCode: err.oauthRequest?.status,
|
|
1736
|
+
delayMs: Math.round(delay)
|
|
1737
|
+
});
|
|
1738
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
if (!batchSuccess) {
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// Process batch responses - match by request ID and track which were received
|
|
1747
|
+
const respondedIds = new Set();
|
|
1748
|
+
for (const response of responseData?.responses || []) {
|
|
1749
|
+
const emailId = messageMap.get(response.id);
|
|
1750
|
+
if (!emailId) {
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
respondedIds.add(response.id);
|
|
1755
|
+
|
|
1756
|
+
if (response.status >= 200 && response.status < 300 && response.body) {
|
|
1757
|
+
try {
|
|
1758
|
+
const messageData = response.body;
|
|
1759
|
+
|
|
1760
|
+
// Resolve folder from pre-fetched map (O(1) lookup)
|
|
1761
|
+
let path, specialUse;
|
|
1762
|
+
if (messageData.parentFolderId) {
|
|
1763
|
+
const folder = folderMap.get(messageData.parentFolderId);
|
|
1764
|
+
if (folder) {
|
|
1765
|
+
path = folder.pathName;
|
|
1766
|
+
specialUse = folder.specialUse;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// Process email headers
|
|
1771
|
+
if (messageData.internetMessageHeaders) {
|
|
1772
|
+
let headers = {};
|
|
1773
|
+
for (let header of messageData.internetMessageHeaders || []) {
|
|
1774
|
+
let { name, value } = header;
|
|
1775
|
+
name = (name || '').toString().trim().toLowerCase();
|
|
1776
|
+
value = (value || '').toString().trim();
|
|
1777
|
+
if (!(name in headers)) {
|
|
1778
|
+
headers[name] = [];
|
|
1779
|
+
}
|
|
1780
|
+
if (!Array.isArray(headers[name])) {
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
headers[name].push(value);
|
|
1784
|
+
if (name === 'in-reply-to') {
|
|
1785
|
+
messageData.inReplyTo = value;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
messageData.headers = headers;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
const formattedMessage = this.formatMessage(messageData, {
|
|
1792
|
+
extended: true,
|
|
1793
|
+
path,
|
|
1794
|
+
textType: options.textType,
|
|
1795
|
+
showPath: options.showPath
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
if (specialUse) {
|
|
1799
|
+
formattedMessage.messageSpecialUse = specialUse;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
results.push({
|
|
1803
|
+
messageId: emailId,
|
|
1804
|
+
data: formattedMessage,
|
|
1805
|
+
error: null
|
|
1806
|
+
});
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
results.push({
|
|
1809
|
+
messageId: emailId,
|
|
1810
|
+
data: null,
|
|
1811
|
+
error: { message: err.message, code: err.code }
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
} else {
|
|
1815
|
+
// Individual message error within the batch
|
|
1816
|
+
const graphError = response.body?.error;
|
|
1817
|
+
results.push({
|
|
1818
|
+
messageId: emailId,
|
|
1819
|
+
data: null,
|
|
1820
|
+
error: {
|
|
1821
|
+
message: graphError?.message || 'Request failed',
|
|
1822
|
+
code: graphError?.code,
|
|
1823
|
+
statusCode: response.status
|
|
1824
|
+
}
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// Reconcile: detect messages that got no response from the batch API
|
|
1830
|
+
for (const [reqId, emailId] of messageMap) {
|
|
1831
|
+
if (!respondedIds.has(reqId)) {
|
|
1832
|
+
results.push({
|
|
1833
|
+
messageId: emailId,
|
|
1834
|
+
data: null,
|
|
1835
|
+
error: { message: 'No response received from batch API', code: 'EMISSING_RESPONSE' }
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
return results;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1574
1844
|
/**
|
|
1575
1845
|
* Get message text content only
|
|
1576
1846
|
* More efficient than getMessage when only text is needed
|
|
@@ -1765,7 +2035,7 @@ class OutlookClient extends BaseClient {
|
|
|
1765
2035
|
case 'attachments': {
|
|
1766
2036
|
messageUploadObj.attachments = [];
|
|
1767
2037
|
let attachmentCounter = 0;
|
|
1768
|
-
for (let attachment of emailObject.attachments) {
|
|
2038
|
+
for (let attachment of emailObject.attachments || []) {
|
|
1769
2039
|
let attachmentEntry = {
|
|
1770
2040
|
'@odata.type': '#microsoft.graph.fileAttachment'
|
|
1771
2041
|
};
|
|
@@ -2088,7 +2358,8 @@ class OutlookClient extends BaseClient {
|
|
|
2088
2358
|
await this.redis
|
|
2089
2359
|
.multi()
|
|
2090
2360
|
.hset(data.feedbackKey, 'success', 'true')
|
|
2091
|
-
.expire(1 * 60 * 60)
|
|
2361
|
+
.expire(data.feedbackKey, 1 * 60 * 60)
|
|
2362
|
+
.exec();
|
|
2092
2363
|
}
|
|
2093
2364
|
|
|
2094
2365
|
return {
|
|
@@ -2963,7 +3234,7 @@ class OutlookClient extends BaseClient {
|
|
|
2963
3234
|
try {
|
|
2964
3235
|
outlookSubscription = JSON.parse(outlookSubscription);
|
|
2965
3236
|
} catch (err) {
|
|
2966
|
-
|
|
3237
|
+
outlookSubscription = {};
|
|
2967
3238
|
}
|
|
2968
3239
|
}
|
|
2969
3240
|
|
|
@@ -3092,13 +3363,35 @@ class OutlookClient extends BaseClient {
|
|
|
3092
3363
|
* Handles all message properties and attachments
|
|
3093
3364
|
*/
|
|
3094
3365
|
formatMessage(messageData, options) {
|
|
3095
|
-
let { extended, path, textType, showPath } = options || {};
|
|
3366
|
+
let { extended, path, textType, showPath, minimalFields } = options || {};
|
|
3096
3367
|
|
|
3097
3368
|
let date = messageData.receivedDateTime ? new Date(messageData.receivedDateTime) : undefined;
|
|
3098
3369
|
if (date?.toString() === 'Invalid Date') {
|
|
3099
3370
|
date = undefined;
|
|
3100
3371
|
}
|
|
3101
3372
|
|
|
3373
|
+
// For minimalFields mode, return only essential fields for export indexing
|
|
3374
|
+
if (minimalFields) {
|
|
3375
|
+
// Derive a numeric uid from message ID for score tiebreaker
|
|
3376
|
+
// Graph API doesn't have IMAP-style UIDs, so we hash the ID
|
|
3377
|
+
let uid = 0;
|
|
3378
|
+
if (messageData.id) {
|
|
3379
|
+
for (let i = 0; i < messageData.id.length; i++) {
|
|
3380
|
+
uid = ((uid << 5) - uid + messageData.id.charCodeAt(i)) | 0;
|
|
3381
|
+
}
|
|
3382
|
+
uid = Math.abs(uid);
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
return {
|
|
3386
|
+
id: messageData.id,
|
|
3387
|
+
emailId: messageData.id,
|
|
3388
|
+
threadId: messageData.conversationId || undefined,
|
|
3389
|
+
date: date ? date.toISOString() : undefined,
|
|
3390
|
+
uid,
|
|
3391
|
+
size: 0 // Graph API doesn't provide size in minimal listing
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3102
3395
|
// Map Graph API flags to IMAP flags
|
|
3103
3396
|
const flags = [];
|
|
3104
3397
|
if (messageData.isRead) {
|
|
@@ -54,7 +54,7 @@ function setupTransporterHandlers(transporter, poolKey) {
|
|
|
54
54
|
// Handle transport errors by removing from pool
|
|
55
55
|
transporter.once('error', err => {
|
|
56
56
|
// Not sure what happened, but do not re-use this transporter object anymore
|
|
57
|
-
logger.
|
|
57
|
+
logger.warn({ msg: 'Transporter failed', err });
|
|
58
58
|
SMTP_POOLS.delete(poolKey);
|
|
59
59
|
SMTP_POOL_LAST_USED.delete(poolKey);
|
|
60
60
|
try {
|