@remit/mailbox-service 0.0.111 → 0.0.113
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/dist/account-credentials.d.ts +83 -0
- package/dist/account-credentials.js +104 -0
- package/dist/attribute-mapper.d.ts +36 -0
- package/dist/attribute-mapper.js +119 -0
- package/dist/body-parse.d.ts +28 -0
- package/{src/body-parse.ts → dist/body-parse.js} +18 -26
- package/dist/body-part-mapper.d.ts +92 -0
- package/dist/body-part-mapper.js +301 -0
- package/dist/body-sync-queue.d.ts +59 -0
- package/dist/body-sync-queue.js +56 -0
- package/dist/body-sync-terminal.d.ts +58 -0
- package/dist/body-sync-terminal.js +95 -0
- package/dist/body-sync.d.ts +351 -0
- package/dist/body-sync.js +1204 -0
- package/dist/calendar-parts.d.ts +23 -0
- package/dist/calendar-parts.js +18 -0
- package/dist/classification-backfill.d.ts +91 -0
- package/dist/classification-backfill.js +180 -0
- package/dist/classify-message.d.ts +31 -0
- package/dist/classify-message.js +89 -0
- package/dist/connection-factory.d.ts +54 -0
- package/dist/connection-factory.js +54 -0
- package/dist/connection-test.d.ts +41 -0
- package/dist/connection-test.js +152 -0
- package/dist/external-move.d.ts +94 -0
- package/dist/external-move.js +173 -0
- package/dist/filter-operations.d.ts +42 -0
- package/dist/filter-operations.js +211 -0
- package/dist/filters/filter-config.d.ts +14 -0
- package/dist/filters/filter-config.js +10 -0
- package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
- package/dist/filters/list-id.js +26 -0
- package/dist/filters/match.d.ts +69 -0
- package/dist/filters/match.js +165 -0
- package/dist/filters/pipeline.d.ts +64 -0
- package/dist/filters/pipeline.js +100 -0
- package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +19 -77
- package/dist/flag-push-terminal.js +63 -0
- package/dist/flag-push.d.ts +69 -0
- package/dist/flag-push.js +111 -0
- package/dist/flag-queue.d.ts +175 -0
- package/dist/flag-queue.js +291 -0
- package/dist/folder-operations.d.ts +39 -0
- package/dist/folder-operations.js +112 -0
- package/dist/folder-placement.d.ts +13 -0
- package/dist/folder-placement.js +23 -0
- package/dist/heuristics/authenticationResults.d.ts +10 -0
- package/dist/heuristics/authenticationResults.js +187 -0
- package/dist/heuristics/classifyByHeaders.d.ts +82 -0
- package/dist/heuristics/classifyByHeaders.js +365 -0
- package/dist/heuristics/classifyPlacement.d.ts +67 -0
- package/dist/heuristics/classifyPlacement.js +153 -0
- package/dist/heuristics/machineSenders.d.ts +21 -0
- package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
- package/dist/heuristics/senderMismatch.d.ts +81 -0
- package/dist/heuristics/senderMismatch.js +192 -0
- package/dist/heuristics/socialDomains.d.ts +14 -0
- package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
- package/dist/heuristics/transactionalDomains.d.ts +18 -0
- package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
- package/dist/imapflow-connection.d.ts +377 -0
- package/dist/imapflow-connection.js +1147 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +65 -0
- package/dist/list-id-backfill.d.ts +83 -0
- package/dist/list-id-backfill.js +132 -0
- package/dist/mailbox-cursor-rebuild.d.ts +74 -0
- package/dist/mailbox-cursor-rebuild.js +71 -0
- package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
- package/dist/mailbox-cursor.js +102 -0
- package/dist/mailbox-intent.d.ts +41 -0
- package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +39 -63
- package/dist/mailbox-management.d.ts +240 -0
- package/dist/mailbox-management.js +576 -0
- package/dist/mailbox-presence.d.ts +52 -0
- package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
- package/dist/mailbox-queue.d.ts +126 -0
- package/dist/mailbox-queue.js +214 -0
- package/dist/mailbox-sync.d.ts +93 -0
- package/dist/mailbox-sync.js +490 -0
- package/dist/mailbox-upstream.d.ts +38 -0
- package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
- package/dist/message-category.d.ts +51 -0
- package/{src/message-category.ts → dist/message-category.js} +28 -71
- package/dist/message-move.d.ts +285 -0
- package/dist/message-move.js +858 -0
- package/dist/message-parser.d.ts +14 -0
- package/dist/message-parser.js +17 -0
- package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
- package/dist/message-presence.js +29 -0
- package/dist/message-read.d.ts +51 -0
- package/dist/message-read.js +31 -0
- package/dist/message-sync.d.ts +345 -0
- package/dist/message-sync.js +1358 -0
- package/dist/mime-walker.d.ts +102 -0
- package/dist/mime-walker.js +198 -0
- package/dist/organize-job.d.ts +20 -0
- package/dist/organize-job.js +37 -0
- package/dist/outbox-attachment-content.d.ts +37 -0
- package/dist/outbox-attachment-content.js +50 -0
- package/dist/outbox-attachment-filename.d.ts +19 -0
- package/dist/outbox-attachment-filename.js +64 -0
- package/dist/outbox-attachment.d.ts +148 -0
- package/dist/outbox-attachment.js +273 -0
- package/dist/outbox-queue.d.ts +73 -0
- package/dist/outbox-queue.js +284 -0
- package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
- package/dist/outbox-status.js +18 -0
- package/dist/ownership.d.ts +24 -0
- package/dist/ownership.js +83 -0
- package/dist/placement-move-terminal.d.ts +67 -0
- package/dist/placement-move-terminal.js +88 -0
- package/dist/placement-move.d.ts +83 -0
- package/dist/placement-move.js +216 -0
- package/{src/placement-settled.ts → dist/placement-settled.d.ts} +19 -50
- package/dist/placement-settled.js +75 -0
- package/dist/quarantine.d.ts +95 -0
- package/dist/quarantine.js +131 -0
- package/dist/refusal.d.ts +31 -0
- package/dist/refusal.js +3 -0
- package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -101
- package/dist/restore-source-placement.js +79 -0
- package/dist/search-text-backfill.d.ts +15 -0
- package/dist/search-text-backfill.js +33 -0
- package/dist/snippet.d.ts +61 -0
- package/dist/snippet.js +129 -0
- package/dist/spam-report.d.ts +86 -0
- package/dist/spam-report.js +241 -0
- package/dist/sync-warning.d.ts +21 -0
- package/dist/sync-warning.js +26 -0
- package/dist/sync-watermarks.d.ts +138 -0
- package/dist/sync-watermarks.js +205 -0
- package/dist/test-helpers/fake-imap.d.ts +151 -0
- package/dist/test-helpers/fake-imap.js +508 -0
- package/dist/test-helpers/folder-roles.d.ts +34 -0
- package/dist/test-helpers/folder-roles.js +47 -0
- package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
- package/dist/test-helpers/mime-tree-generator.js +329 -0
- package/dist/test-helpers/test-store.d.ts +69 -0
- package/dist/test-helpers/test-store.js +193 -0
- package/dist/text/normalizer.d.ts +20 -0
- package/dist/text/normalizer.js +102 -0
- package/dist/thread-message-rows.d.ts +69 -0
- package/dist/thread-message-rows.js +40 -0
- package/dist/thread-root.d.ts +7 -0
- package/dist/thread-root.js +25 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.js +19 -0
- package/package.json +86 -73
- package/src/account-credentials.ts +0 -210
- package/src/adversarial-mime.e2e.test.ts +0 -307
- package/src/attribute-mapper.test.ts +0 -104
- package/src/attribute-mapper.ts +0 -148
- package/src/body-parse.test.ts +0 -55
- package/src/body-part-mapper.property.test.ts +0 -354
- package/src/body-part-mapper.ts +0 -360
- package/src/body-sync-auto-read-writeonce.test.ts +0 -219
- package/src/body-sync-calendar-suggestion.test.ts +0 -433
- package/src/body-sync-category-denormalize.test.ts +0 -352
- package/src/body-sync-category-override.test.ts +0 -501
- package/src/body-sync-category-writeonce.test.ts +0 -234
- package/src/body-sync-filter.test.ts +0 -237
- package/src/body-sync-message-gone.test.ts +0 -139
- package/src/body-sync-placement-flags.test.ts +0 -347
- package/src/body-sync-placement-writeonce.test.ts +0 -337
- package/src/body-sync-quarantine.test.ts +0 -276
- package/src/body-sync-queue.test.ts +0 -124
- package/src/body-sync-queue.ts +0 -131
- package/src/body-sync-resync.test.ts +0 -605
- package/src/body-sync-terminal.test.ts +0 -310
- package/src/body-sync-terminal.ts +0 -176
- package/src/body-sync-unclassifiable-once.test.ts +0 -260
- package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
- package/src/body-sync.ts +0 -1970
- package/src/calendar-parts.ts +0 -41
- package/src/classification-backfill.test.ts +0 -615
- package/src/classification-backfill.ts +0 -363
- package/src/classify-message.test.ts +0 -205
- package/src/classify-message.ts +0 -116
- package/src/connection-factory.ts +0 -102
- package/src/connection-test.ts +0 -208
- package/src/credentials.test.ts +0 -181
- package/src/external-move.test.ts +0 -369
- package/src/external-move.ts +0 -272
- package/src/filters/anchor-drift.ts +0 -55
- package/src/filters/filter-config.test.ts +0 -229
- package/src/filters/filter-config.ts +0 -69
- package/src/filters/list-id.test.ts +0 -50
- package/src/filters/match.test.ts +0 -429
- package/src/filters/match.ts +0 -274
- package/src/filters/message-embedder.test.ts +0 -39
- package/src/filters/message-embedder.ts +0 -33
- package/src/filters/pipeline.test.ts +0 -330
- package/src/filters/pipeline.ts +0 -284
- package/src/flag-push-terminal.test.ts +0 -209
- package/src/flag-push.test.ts +0 -408
- package/src/flag-push.ts +0 -198
- package/src/flag-queue-stars.test.ts +0 -171
- package/src/flag-queue.ts +0 -509
- package/src/folder-placement.ts +0 -56
- package/src/heuristics/authenticationResults.test.ts +0 -189
- package/src/heuristics/authenticationResults.ts +0 -212
- package/src/heuristics/classifyByHeaders.test.ts +0 -889
- package/src/heuristics/classifyByHeaders.ts +0 -467
- package/src/heuristics/classifyPlacement.test.ts +0 -366
- package/src/heuristics/classifyPlacement.ts +0 -211
- package/src/heuristics/classifyRealMail.test.ts +0 -242
- package/src/heuristics/machineSenders.test.ts +0 -101
- package/src/heuristics/senderMismatch.test.ts +0 -346
- package/src/heuristics/senderMismatch.ts +0 -259
- package/src/imapflow-connect-retry.test.ts +0 -134
- package/src/imapflow-connection.e2e.test.ts +0 -335
- package/src/imapflow-connection.integ.test.ts +0 -316
- package/src/imapflow-connection.test.ts +0 -469
- package/src/imapflow-connection.ts +0 -1475
- package/src/index.ts +0 -350
- package/src/list-id-backfill.test.ts +0 -573
- package/src/list-id-backfill.ts +0 -274
- package/src/mailbox-cursor-rebuild.test.ts +0 -171
- package/src/mailbox-cursor-rebuild.ts +0 -146
- package/src/mailbox-cursor.test.ts +0 -249
- package/src/mailbox-delete-intent.test.ts +0 -246
- package/src/mailbox-management.test.ts +0 -356
- package/src/mailbox-management.ts +0 -839
- package/src/mailbox-presence.test.ts +0 -72
- package/src/mailbox-queue.ts +0 -415
- package/src/mailbox-rename-intent.test.ts +0 -389
- package/src/mailbox-rename-subtree.test.ts +0 -664
- package/src/mailbox-sync.test.ts +0 -848
- package/src/mailbox-sync.ts +0 -724
- package/src/message-delete-junk-reconcile.test.ts +0 -117
- package/src/message-delete-no-trash.test.ts +0 -245
- package/src/message-delete-unsettled-placement.test.ts +0 -396
- package/src/message-move-copy.test.ts +0 -562
- package/src/message-move-empty-trash.test.ts +0 -326
- package/src/message-move-same-mailbox.test.ts +0 -139
- package/src/message-move-unsettled-placement.test.ts +0 -378
- package/src/message-move.ts +0 -1348
- package/src/message-parser.ts +0 -26
- package/src/message-presence.test.ts +0 -50
- package/src/message-sync-address-spoof.test.ts +0 -293
- package/src/message-sync-category.test.ts +0 -142
- package/src/message-sync-changedsince.test.ts +0 -840
- package/src/message-sync-flags.test.ts +0 -158
- package/src/message-sync-junk-harvest.test.ts +0 -244
- package/src/message-sync-quarantine.test.ts +0 -416
- package/src/message-sync-repoint.test.ts +0 -419
- package/src/message-sync.ts +0 -1963
- package/src/mime-walker.test.ts +0 -311
- package/src/mime-walker.ts +0 -305
- package/src/outbox-attachment-content.test.ts +0 -153
- package/src/outbox-attachment-content.ts +0 -104
- package/src/outbox-attachment-filename.test.ts +0 -132
- package/src/outbox-attachment-filename.ts +0 -76
- package/src/outbox-attachment.test.ts +0 -738
- package/src/outbox-attachment.ts +0 -536
- package/src/outbox-failed-recovery.test.ts +0 -370
- package/src/outbox-queue-settled-row.test.ts +0 -140
- package/src/outbox-queue.test.ts +0 -291
- package/src/outbox-queue.ts +0 -508
- package/src/pass-through-unit-of-work.ts +0 -17
- package/src/placement-move-terminal.test.ts +0 -300
- package/src/placement-move-terminal.ts +0 -148
- package/src/placement-move-unsettled.test.ts +0 -275
- package/src/placement-move.ts +0 -336
- package/src/placement-settled.test.ts +0 -147
- package/src/placement-transition-race.test.ts +0 -482
- package/src/quarantine.test.ts +0 -191
- package/src/quarantine.ts +0 -228
- package/src/snippet.test.ts +0 -213
- package/src/snippet.ts +0 -152
- package/src/spam-report.test.ts +0 -801
- package/src/spam-report.ts +0 -357
- package/src/stale-message-reconcile.test.ts +0 -82
- package/src/stale-message-reconcile.ts +0 -55
- package/src/sync-watermarks.test.ts +0 -409
- package/src/sync-watermarks.ts +0 -319
- package/src/system-flag-wire-format.test.ts +0 -77
- package/src/test-helpers/folder-roles.ts +0 -66
- package/src/test-helpers/isolated-mailbox.ts +0 -44
- package/src/test-helpers/mailfuzz-connection.ts +0 -27
- package/src/test-helpers/mime-tree-generator.ts +0 -461
- package/src/text/normalizer.test.ts +0 -223
- package/src/text/normalizer.ts +0 -140
- package/src/thread-message-rows.ts +0 -77
- package/src/types/mailparser-augment.d.ts +0 -15
- package/src/types/natural-porter-stemmer.d.ts +0 -50
- package/src/types/stopword.d.ts +0 -69
- package/src/types.ts +0 -371
- package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
- package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
- package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
- package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
- package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
- package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
- package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
- package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
- package/tsconfig.json +0 -8
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remit-mailbox-service
|
|
3
|
+
*
|
|
4
|
+
* IMAP mailbox synchronization service for Remit
|
|
5
|
+
*/
|
|
6
|
+
export { type AccountCredentialsDeps, type ConnectionStateValue, type CredentialResolution, encryptRefreshToken, hasStoredCredential, type MissingCredential, resolveConnectionCredentials, type StoredCredentialFields, } from "./account-credentials.js";
|
|
7
|
+
export { hasChildren, isNoSelect, type ParsedAttributes, parseImapAttributes, } from "./attribute-mapper.js";
|
|
8
|
+
export { BodyParseError, parseMessageBody, } from "./body-parse.js";
|
|
9
|
+
export { type BodySyncFolder, type BodySyncLogger, type BodySyncRoundFolder, BodySyncService, type ConnectionGetter, extractSnippet, type FetchBodyResult, type PlacementConfig, type QuarantineConfig, type SyncBodiesResult, toParsedBody, } from "./body-sync.js";
|
|
10
|
+
export { type BodySyncQueueConfig, type BodySyncQueueLogger, BodySyncQueueService, type RequestBodySyncInput, type SyncMessageBodyEvent, } from "./body-sync-queue.js";
|
|
11
|
+
export { isMessageBodySyncBroken, type ResolveExhaustedBodySyncDeps, type ResolveExhaustedBodySyncInput, type ResolveExhaustedBodySyncResult, resolveExhaustedBodySyncFailures, } from "./body-sync-terminal.js";
|
|
12
|
+
export { backfillClassifications, type ClassificationBackfillCheckpoint, type ClassificationBackfillCheckpointStore, type ClassificationBackfillDeps, type ClassificationBackfillLogger, type ClassificationBackfillOptions, type ClassificationBackfillProgress, type ClassificationBackfillResult, type ClassificationBackfillTotals, } from "./classification-backfill.js";
|
|
13
|
+
export { classifyParsedMessage, extractPrimaryFromEmail, } from "./classify-message.js";
|
|
14
|
+
export { createConnection, createConnectionFromAccount, createConnectionWithCredentials, createManagedConnectionFactory, type ManagedConnectionFactory, } from "./connection-factory.js";
|
|
15
|
+
export { type ImapTestConfig, type SmtpTestConfig, type TestResult, testImapConnection, testSmtpConnection, } from "./connection-test.js";
|
|
16
|
+
export { type CreatedFilter, createFilter, type FilterPage, type FilterPatch, listFilters, type NewFilter, resolveFilterUpdate, updateFilter, } from "./filter-operations.js";
|
|
17
|
+
export { extractListId, normalizeListId } from "./filters/list-id.js";
|
|
18
|
+
export { clauseMatches, type FilterMessage, type LiteralClauseNarrowing, literalClausesMatch, literalClauseTerms, NO_ACTION, selectMoveWinner, } from "./filters/match.js";
|
|
19
|
+
export { type FilterConfig, type FilterDecision, type FilterLogger, FilterPipeline, } from "./filters/pipeline.js";
|
|
20
|
+
export { type FlagPushConfig, type FlagPushEvent, type FlagPushLogger, type FlagPushOperationValue, FlagPushService, } from "./flag-push.js";
|
|
21
|
+
export { type FlagPushTerminalOutcome, type ResolveExhaustedFlagPushDeps, type ResolveExhaustedFlagPushInput, type ResolveExhaustedFlagPushResult, resolveExhaustedFlagPushFailure, } from "./flag-push-terminal.js";
|
|
22
|
+
export { type FlagQueueConfig, type FlagQueueLogger, FlagQueueService, type UpdateFlagsInput, type UpdateFlagsResult, } from "./flag-queue.js";
|
|
23
|
+
export { type CreateFolderDeps, changeFolderPath, createFolder, type FolderPathChange, type FolderPathDeps, type FolderPathOutcome, type NewFolder, type NewFolderOutcome, ownedFolder, } from "./folder-operations.js";
|
|
24
|
+
export { type FolderPlacementLookup, resolveAccountFolderPlacements, resolveFolderPlacement, } from "./folder-placement.js";
|
|
25
|
+
export { classifyByHeaders, extractAuthenticity, extractAuthResult, extractHasListUnsubscribe, extractProviderSpam, type MessageAuthenticity, type MessageAuthResult, type MessageProviderSpam, } from "./heuristics/classifyByHeaders.js";
|
|
26
|
+
export { classifyPlacement, type FolderPlacement, type PlacementAction, type PlacementVerdict, } from "./heuristics/classifyPlacement.js";
|
|
27
|
+
export { classifyDisplayNameCorrespondence, extractOffDomainLinkDomains, extractSenderMismatch, type SenderMismatchContext, type SenderMismatchSignals, } from "./heuristics/senderMismatch.js";
|
|
28
|
+
export { SOCIAL_DOMAINS } from "./heuristics/socialDomains.js";
|
|
29
|
+
export { TRANSACTIONAL_DOMAINS } from "./heuristics/transactionalDomains.js";
|
|
30
|
+
export { createImapFlowConnectionFromAccount, createImapFlowConnectionWithCredentials, ImapFlowConnection, } from "./imapflow-connection.js";
|
|
31
|
+
export { backfillListIds, type ListIdBackfillCheckpoint, type ListIdBackfillCheckpointStore, type ListIdBackfillDeps, type ListIdBackfillLogger, type ListIdBackfillOptions, type ListIdBackfillProgress, type ListIdBackfillResult, type ListIdBackfillTotals, } from "./list-id-backfill.js";
|
|
32
|
+
export { guardConnectionCursor, guardMailboxCursor, isCursorRebuildNeeded, type MailboxCursorCheck, type MailboxCursorGuardDeps, MailboxCursorPausedError, } from "./mailbox-cursor.js";
|
|
33
|
+
export { type CursorRebuildMatch, type CursorRebuildMatchResult, type CursorRebuildRow, type CursorRebuildSnapshot, matchCursorRebuild, } from "./mailbox-cursor-rebuild.js";
|
|
34
|
+
export { folderInFlight } from "./mailbox-intent.js";
|
|
35
|
+
export { type CreateMailboxInput, type DeleteMailboxInput, type MailboxManagementLogger, MailboxManagementService, type MailboxManagementSyncResult, parseMailboxPath, type RenameMailboxInput, validateMailboxOperation, validateMailboxPath, } from "./mailbox-management.js";
|
|
36
|
+
export { EVERY_MAILBOX_STATE, isFolderMutationInFlight, isMailboxMutationInFlight, } from "./mailbox-presence.js";
|
|
37
|
+
export { type CreateMailboxQueueInput, type MailboxManagementEvent, type MailboxQueueConfig, type MailboxQueueLogger, MailboxQueueService, } from "./mailbox-queue.js";
|
|
38
|
+
export { MailboxSyncService, type SyncAccountInfo, } from "./mailbox-sync.js";
|
|
39
|
+
export { FolderGoneUpstreamError, FolderRenameSettleError, isMailboxAbsentUpstream, isMailboxPresentUpstream, isMailboxRefusedUpstream, upstreamFailureReason, } from "./mailbox-upstream.js";
|
|
40
|
+
export { denormalizeMessageCategory, type MessageCategoryDenormalizeDeps, type MessageCategoryDenormalizeInput, type MessageCategoryValue, } from "./message-category.js";
|
|
41
|
+
export { type DeleteMessagesOutcome, type DeleteOptions, type MessageMoveConfig, type MessageMoveLogger, type MessageMoveQueueEvent, MessageMoveService, NoTrashMailboxError, requireTrashMailbox, StaleTrashAppointmentError, UnconfirmedTrashMailboxError, } from "./message-move.js";
|
|
42
|
+
export { type ParsedMessageContent, parseMessageContent, } from "./message-parser.js";
|
|
43
|
+
export { isMessageGoneFromOpenMailbox } from "./message-presence.js";
|
|
44
|
+
export { type BodyPartMaterializer, type BodySyncCue, type MaterializeLogger, type MaterializeResult, materializeBodyParts, } from "./message-read.js";
|
|
45
|
+
export { type ImapConnectionFactory, MessageSyncService, type SyncedMessage, type SyncMessagesResult, } from "./message-sync.js";
|
|
46
|
+
export { type OrganizeJobDeps, type OrganizeJobLogger, type OrganizeJobOwner, type OrganizeJobPredicate, type OrganizeJobRequest, queueOrganizeJob, } from "./organize-job.js";
|
|
47
|
+
export { type CompleteOutboxAttachmentInput, type CompleteOutboxAttachmentOutcome, type MintOutboxAttachmentInput, type MintOutboxAttachmentOutcome, OUTBOX_ATTACHMENT_MAX_COUNT, OUTBOX_ATTACHMENT_MAX_TOTAL_BYTES, type OutboxAttachmentConfig, type OutboxAttachmentRejectionDetail, type OutboxAttachmentRejectionReasonValue, type OutboxAttachmentReservation, OutboxAttachmentService, } from "./outbox-attachment.js";
|
|
48
|
+
export { FALLBACK_CONTENT_TYPE, normalizeAttachmentContentType, sanitizeAttachmentFilename, } from "./outbox-attachment-filename.js";
|
|
49
|
+
export { type CreateDraftInput, type OutboxQueueConfig, type OutboxQueueLogger, OutboxQueueService, type SendMessageEvent, type UpdateDraftInput, } from "./outbox-queue.js";
|
|
50
|
+
export { folderInAccount, type MessageOwnershipClient, ownedAccount, ownedMessages, settledFolder, } from "./ownership.js";
|
|
51
|
+
export { type PlacementMoveConfig, type PlacementMoveLogger, type PlacementMovePushEvent, PlacementMoveService, } from "./placement-move.js";
|
|
52
|
+
export { type PlacementMoveTerminalOutcome, type ResolveExhaustedPlacementMoveDeps, type ResolveExhaustedPlacementMoveInput, type ResolveExhaustedPlacementMoveResult, resolveExhaustedPlacementMoveFailure, } from "./placement-move-terminal.js";
|
|
53
|
+
export { carriesForeignUid, isPlacementUnsettled, type PlacementBinding, placementBindingOf, uidMailboxIdOf, } from "./placement-settled.js";
|
|
54
|
+
export { type QuarantineContext, QuarantinedUids, type QuarantineFailure, type QuarantineLogger, type QuarantineMessageShape, QuarantineService, resolveMailboxRole, shapeFromMessageData, } from "./quarantine.js";
|
|
55
|
+
export { type AccessMode, accept, invalid, type Refusable, type Refusal, type Refused, refuse, } from "./refusal.js";
|
|
56
|
+
export { type RestoreSourcePlacementDeps, type RestoreSourcePlacementInput, type RestoreSourcePlacementOutcome, restoreSourcePlacement, } from "./restore-source-placement.js";
|
|
57
|
+
export { backfillSearchText } from "./search-text-backfill.js";
|
|
58
|
+
export { extractSearchText, extractSnippetFromEmail, generateSnippet, normalizeSubject, removeQuotedContent, } from "./snippet.js";
|
|
59
|
+
export { MoveNotSettledError, NoJunkMailboxError, type SpamReportConfig, type SpamReportLogger, type SpamReportParams, SpamReportService, } from "./spam-report.js";
|
|
60
|
+
export { type ServerUids, type SyncWarningContext, type SyncWarningRound, SyncWarningService, } from "./sync-warning.js";
|
|
61
|
+
export { buildThreadMessageMoveRevert, buildThreadMessageTrashUpdate, buildThreadMessageUndelete, type ThreadMessageRowState, } from "./thread-message-rows.js";
|
|
62
|
+
export type { FlatMailboxInfo, IImapConnection, ImapBoxStatus, ImapConnectionConfig, ImapConnectionState, ImapNamespace, ImapNamespaces, MailboxSyncResult, MailConnectionErrorKind, MailCredentials, } from "./types.js";
|
|
63
|
+
export { MailConnectionError } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remit-mailbox-service
|
|
3
|
+
*
|
|
4
|
+
* IMAP mailbox synchronization service for Remit
|
|
5
|
+
*/
|
|
6
|
+
export { encryptRefreshToken, hasStoredCredential, resolveConnectionCredentials, } from "./account-credentials.js";
|
|
7
|
+
export { hasChildren, isNoSelect, parseImapAttributes, } from "./attribute-mapper.js";
|
|
8
|
+
export { BodyParseError, parseMessageBody, } from "./body-parse.js";
|
|
9
|
+
export { BodySyncService, extractSnippet, toParsedBody, } from "./body-sync.js";
|
|
10
|
+
export { BodySyncQueueService, } from "./body-sync-queue.js";
|
|
11
|
+
export { isMessageBodySyncBroken, resolveExhaustedBodySyncFailures, } from "./body-sync-terminal.js";
|
|
12
|
+
export { backfillClassifications, } from "./classification-backfill.js";
|
|
13
|
+
export { classifyParsedMessage, extractPrimaryFromEmail, } from "./classify-message.js";
|
|
14
|
+
// Connection factory
|
|
15
|
+
export { createConnection, createConnectionFromAccount, createConnectionWithCredentials, createManagedConnectionFactory, } from "./connection-factory.js";
|
|
16
|
+
// Connection testing
|
|
17
|
+
export { testImapConnection, testSmtpConnection, } from "./connection-test.js";
|
|
18
|
+
export { createFilter, listFilters, resolveFilterUpdate, updateFilter, } from "./filter-operations.js";
|
|
19
|
+
export { extractListId, normalizeListId } from "./filters/list-id.js";
|
|
20
|
+
export { clauseMatches, literalClausesMatch, literalClauseTerms, NO_ACTION, selectMoveWinner, } from "./filters/match.js";
|
|
21
|
+
export { FilterPipeline, } from "./filters/pipeline.js";
|
|
22
|
+
export { FlagPushService, } from "./flag-push.js";
|
|
23
|
+
export { resolveExhaustedFlagPushFailure, } from "./flag-push-terminal.js";
|
|
24
|
+
export { FlagQueueService, } from "./flag-queue.js";
|
|
25
|
+
export { changeFolderPath, createFolder, ownedFolder, } from "./folder-operations.js";
|
|
26
|
+
export { resolveAccountFolderPlacements, resolveFolderPlacement, } from "./folder-placement.js";
|
|
27
|
+
export { classifyByHeaders, extractAuthenticity, extractAuthResult, extractHasListUnsubscribe, extractProviderSpam, } from "./heuristics/classifyByHeaders.js";
|
|
28
|
+
export { classifyPlacement, } from "./heuristics/classifyPlacement.js";
|
|
29
|
+
export { classifyDisplayNameCorrespondence, extractOffDomainLinkDomains, extractSenderMismatch, } from "./heuristics/senderMismatch.js";
|
|
30
|
+
export { SOCIAL_DOMAINS } from "./heuristics/socialDomains.js";
|
|
31
|
+
export { TRANSACTIONAL_DOMAINS } from "./heuristics/transactionalDomains.js";
|
|
32
|
+
// IMAP connection (ImapFlow-based)
|
|
33
|
+
export { createImapFlowConnectionFromAccount, createImapFlowConnectionWithCredentials, ImapFlowConnection, } from "./imapflow-connection.js";
|
|
34
|
+
export { backfillListIds, } from "./list-id-backfill.js";
|
|
35
|
+
export { guardConnectionCursor, guardMailboxCursor, isCursorRebuildNeeded, MailboxCursorPausedError, } from "./mailbox-cursor.js";
|
|
36
|
+
export { matchCursorRebuild, } from "./mailbox-cursor-rebuild.js";
|
|
37
|
+
export { folderInFlight } from "./mailbox-intent.js";
|
|
38
|
+
export { MailboxManagementService, parseMailboxPath, validateMailboxOperation, validateMailboxPath, } from "./mailbox-management.js";
|
|
39
|
+
export { EVERY_MAILBOX_STATE, isFolderMutationInFlight, isMailboxMutationInFlight, } from "./mailbox-presence.js";
|
|
40
|
+
export { MailboxQueueService, } from "./mailbox-queue.js";
|
|
41
|
+
export { MailboxSyncService, } from "./mailbox-sync.js";
|
|
42
|
+
export { FolderGoneUpstreamError, FolderRenameSettleError, isMailboxAbsentUpstream, isMailboxPresentUpstream, isMailboxRefusedUpstream, upstreamFailureReason, } from "./mailbox-upstream.js";
|
|
43
|
+
export { denormalizeMessageCategory, } from "./message-category.js";
|
|
44
|
+
export { MessageMoveService, NoTrashMailboxError, requireTrashMailbox, StaleTrashAppointmentError, UnconfirmedTrashMailboxError, } from "./message-move.js";
|
|
45
|
+
export { parseMessageContent, } from "./message-parser.js";
|
|
46
|
+
export { isMessageGoneFromOpenMailbox } from "./message-presence.js";
|
|
47
|
+
export { materializeBodyParts, } from "./message-read.js";
|
|
48
|
+
export { MessageSyncService, } from "./message-sync.js";
|
|
49
|
+
export { queueOrganizeJob, } from "./organize-job.js";
|
|
50
|
+
export { OUTBOX_ATTACHMENT_MAX_COUNT, OUTBOX_ATTACHMENT_MAX_TOTAL_BYTES, OutboxAttachmentService, } from "./outbox-attachment.js";
|
|
51
|
+
export { FALLBACK_CONTENT_TYPE, normalizeAttachmentContentType, sanitizeAttachmentFilename, } from "./outbox-attachment-filename.js";
|
|
52
|
+
export { OutboxQueueService, } from "./outbox-queue.js";
|
|
53
|
+
export { folderInAccount, ownedAccount, ownedMessages, settledFolder, } from "./ownership.js";
|
|
54
|
+
export { PlacementMoveService, } from "./placement-move.js";
|
|
55
|
+
export { resolveExhaustedPlacementMoveFailure, } from "./placement-move-terminal.js";
|
|
56
|
+
export { carriesForeignUid, isPlacementUnsettled, placementBindingOf, uidMailboxIdOf, } from "./placement-settled.js";
|
|
57
|
+
export { QuarantinedUids, QuarantineService, resolveMailboxRole, shapeFromMessageData, } from "./quarantine.js";
|
|
58
|
+
export { accept, invalid, refuse, } from "./refusal.js";
|
|
59
|
+
export { restoreSourcePlacement, } from "./restore-source-placement.js";
|
|
60
|
+
export { backfillSearchText } from "./search-text-backfill.js";
|
|
61
|
+
export { extractSearchText, extractSnippetFromEmail, generateSnippet, normalizeSubject, removeQuotedContent, } from "./snippet.js";
|
|
62
|
+
export { MoveNotSettledError, NoJunkMailboxError, SpamReportService, } from "./spam-report.js";
|
|
63
|
+
export { SyncWarningService, } from "./sync-warning.js";
|
|
64
|
+
export { buildThreadMessageMoveRevert, buildThreadMessageTrashUpdate, buildThreadMessageUndelete, } from "./thread-message-rows.js";
|
|
65
|
+
export { MailConnectionError } from "./types.js";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { IAccountConfigRepository, IMessageRepository, IThreadMessageRepository } from "@remit/data-ports";
|
|
2
|
+
import type { StorageService } from "@remit/storage-service";
|
|
3
|
+
/**
|
|
4
|
+
* Where the full-corpus pass left off: the identity of the account it was
|
|
5
|
+
* working through, and the page cursor within that account. Resuming looks the
|
|
6
|
+
* account up by id and re-opens it at its saved cursor, rather than re-scanning
|
|
7
|
+
* the whole corpus from the top after an interruption. A checkpoint naming no
|
|
8
|
+
* configured account restarts the pass.
|
|
9
|
+
*/
|
|
10
|
+
export interface ListIdBackfillCheckpoint {
|
|
11
|
+
accountConfigId: string;
|
|
12
|
+
continuationToken?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Persistence for {@link ListIdBackfillCheckpoint}, injected so the pass stays
|
|
16
|
+
* testable with an in-memory fake; the real entrypoint backs it with a file.
|
|
17
|
+
* `clear()` runs once the whole corpus has been scanned, so a later run starts
|
|
18
|
+
* fresh rather than reading a stale finished-run checkpoint.
|
|
19
|
+
*/
|
|
20
|
+
export interface ListIdBackfillCheckpointStore {
|
|
21
|
+
load(): Promise<ListIdBackfillCheckpoint | undefined>;
|
|
22
|
+
save(checkpoint: ListIdBackfillCheckpoint): Promise<void>;
|
|
23
|
+
clear(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export interface ListIdBackfillDeps {
|
|
26
|
+
accountConfigService: Pick<IAccountConfigRepository, "listAll">;
|
|
27
|
+
threadMessageService: Pick<IThreadMessageRepository, "listByAccount" | "update">;
|
|
28
|
+
messageService: Pick<IMessageRepository, "get">;
|
|
29
|
+
storageService: Pick<StorageService, "retrieve">;
|
|
30
|
+
}
|
|
31
|
+
export interface ListIdBackfillLogger {
|
|
32
|
+
info(obj: Record<string, unknown>, msg: string): void;
|
|
33
|
+
error?(obj: Record<string, unknown>, msg: string): void;
|
|
34
|
+
}
|
|
35
|
+
export interface ListIdBackfillTotals {
|
|
36
|
+
scanned: number;
|
|
37
|
+
alreadySet: number;
|
|
38
|
+
skippedNoBody: number;
|
|
39
|
+
backfilled: number;
|
|
40
|
+
noListId: number;
|
|
41
|
+
failed: number;
|
|
42
|
+
}
|
|
43
|
+
export interface ListIdBackfillResult extends ListIdBackfillTotals {
|
|
44
|
+
failedThreadMessageIds: string[];
|
|
45
|
+
}
|
|
46
|
+
export interface ListIdBackfillProgress extends ListIdBackfillTotals {
|
|
47
|
+
accountConfigId: string;
|
|
48
|
+
}
|
|
49
|
+
export interface ListIdBackfillOptions {
|
|
50
|
+
/** Rows fetched per `listByAccount` page. */
|
|
51
|
+
batchSize?: number;
|
|
52
|
+
checkpointStore?: ListIdBackfillCheckpointStore;
|
|
53
|
+
logger?: ListIdBackfillLogger;
|
|
54
|
+
/** Called once per page, after that page's rows are settled. */
|
|
55
|
+
onProgress?: (progress: ListIdBackfillProgress) => void;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* One-time, resumable pass that derives `ThreadMessage.listId` for rows synced
|
|
59
|
+
* before header extraction shipped (issue #263). Read-only against the stored
|
|
60
|
+
* raw source: it never opens IMAP and never touches anything but the single
|
|
61
|
+
* `listId` field.
|
|
62
|
+
*
|
|
63
|
+
* A row is a candidate when `listId` is `undefined` — written before the
|
|
64
|
+
* column existed, or written since by a path that found no `List-Id` header
|
|
65
|
+
* (body-sync only sets the field when the header is present, so "no header"
|
|
66
|
+
* and "not yet backfilled" look the same on the row; re-checking an
|
|
67
|
+
* already-correct empty row is wasted work, never a wrong answer). A
|
|
68
|
+
* candidate whose Message has no `bodyStorageKey` yet is left alone — its
|
|
69
|
+
* body was never synced, so there is nothing local to read, and the ordinary
|
|
70
|
+
* sync path will populate `listId` for it once the body lands.
|
|
71
|
+
*
|
|
72
|
+
* Chunked by `listByAccount`'s existing keyset pagination, one account at a
|
|
73
|
+
* time in account-id order, so an interrupted run and its resume walk the same
|
|
74
|
+
* sequence. A failure reading or parsing one message's
|
|
75
|
+
* stored body is contained to that message — logged, counted, and the pass
|
|
76
|
+
* continues: one unreadable object must not strand the rest of the corpus.
|
|
77
|
+
*
|
|
78
|
+
* The shape a later pass over `Message.classificationState` should take
|
|
79
|
+
* (issue #331): a one-time, resumable, storage-only sweep that selects its
|
|
80
|
+
* cohort by an explicit field, so it can be run once, deliberately, after a
|
|
81
|
+
* classifier release — rather than a re-derivation folded into every sync pass.
|
|
82
|
+
*/
|
|
83
|
+
export declare const backfillListIds: (deps: ListIdBackfillDeps, options?: ListIdBackfillOptions) => Promise<ListIdBackfillResult>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { parseMessageBody } from "./body-parse.js";
|
|
2
|
+
import { extractListId } from "./filters/list-id.js";
|
|
3
|
+
const DEFAULT_BATCH_SIZE = 200;
|
|
4
|
+
const emptyTotals = () => ({
|
|
5
|
+
scanned: 0,
|
|
6
|
+
alreadySet: 0,
|
|
7
|
+
skippedNoBody: 0,
|
|
8
|
+
backfilled: 0,
|
|
9
|
+
noListId: 0,
|
|
10
|
+
failed: 0,
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Read the stored raw source, derive `List-Id`, and — only when one is
|
|
14
|
+
* present — write it. Kept as its own function (rather than inline in a
|
|
15
|
+
* try/catch) so the caller can contain a failure with `.then(fulfilled,
|
|
16
|
+
* rejected)` instead of a block catch, which keeps the `try` around the one
|
|
17
|
+
* call whose failure this contains.
|
|
18
|
+
*/
|
|
19
|
+
const deriveAndApplyListId = async (deps, accountConfigId, row, bodyStorageKey) => {
|
|
20
|
+
const body = await deps.storageService.retrieve(bodyStorageKey);
|
|
21
|
+
const parsed = await parseMessageBody(body);
|
|
22
|
+
const listId = extractListId(parsed);
|
|
23
|
+
if (listId) {
|
|
24
|
+
await deps.threadMessageService.update(accountConfigId, row.threadMessageId, { listId }, {
|
|
25
|
+
composites: {
|
|
26
|
+
sentDate: row.sentDate,
|
|
27
|
+
mailboxId: row.mailboxId,
|
|
28
|
+
isRead: row.isRead,
|
|
29
|
+
isDeleted: row.isDeleted,
|
|
30
|
+
hasStars: row.hasStars,
|
|
31
|
+
hasAttachment: row.hasAttachment,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return listId;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* One-time, resumable pass that derives `ThreadMessage.listId` for rows synced
|
|
39
|
+
* before header extraction shipped (issue #263). Read-only against the stored
|
|
40
|
+
* raw source: it never opens IMAP and never touches anything but the single
|
|
41
|
+
* `listId` field.
|
|
42
|
+
*
|
|
43
|
+
* A row is a candidate when `listId` is `undefined` — written before the
|
|
44
|
+
* column existed, or written since by a path that found no `List-Id` header
|
|
45
|
+
* (body-sync only sets the field when the header is present, so "no header"
|
|
46
|
+
* and "not yet backfilled" look the same on the row; re-checking an
|
|
47
|
+
* already-correct empty row is wasted work, never a wrong answer). A
|
|
48
|
+
* candidate whose Message has no `bodyStorageKey` yet is left alone — its
|
|
49
|
+
* body was never synced, so there is nothing local to read, and the ordinary
|
|
50
|
+
* sync path will populate `listId` for it once the body lands.
|
|
51
|
+
*
|
|
52
|
+
* Chunked by `listByAccount`'s existing keyset pagination, one account at a
|
|
53
|
+
* time in account-id order, so an interrupted run and its resume walk the same
|
|
54
|
+
* sequence. A failure reading or parsing one message's
|
|
55
|
+
* stored body is contained to that message — logged, counted, and the pass
|
|
56
|
+
* continues: one unreadable object must not strand the rest of the corpus.
|
|
57
|
+
*
|
|
58
|
+
* The shape a later pass over `Message.classificationState` should take
|
|
59
|
+
* (issue #331): a one-time, resumable, storage-only sweep that selects its
|
|
60
|
+
* cohort by an explicit field, so it can be run once, deliberately, after a
|
|
61
|
+
* classifier release — rather than a re-derivation folded into every sync pass.
|
|
62
|
+
*/
|
|
63
|
+
export const backfillListIds = async (deps, options = {}) => {
|
|
64
|
+
const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
65
|
+
const { logger, checkpointStore } = options;
|
|
66
|
+
const accounts = [...(await deps.accountConfigService.listAll())].sort((left, right) => left.accountConfigId.localeCompare(right.accountConfigId));
|
|
67
|
+
const startingCheckpoint = await checkpointStore?.load();
|
|
68
|
+
const checkpointedIndex = startingCheckpoint
|
|
69
|
+
? accounts.findIndex((account) => account.accountConfigId === startingCheckpoint.accountConfigId)
|
|
70
|
+
: -1;
|
|
71
|
+
if (startingCheckpoint && checkpointedIndex === -1) {
|
|
72
|
+
logger?.info({ accountConfigId: startingCheckpoint.accountConfigId }, "ListId backfill checkpoint names no configured account; restarting the pass");
|
|
73
|
+
}
|
|
74
|
+
const startIndex = checkpointedIndex === -1 ? 0 : checkpointedIndex;
|
|
75
|
+
const startContinuationToken = checkpointedIndex === -1
|
|
76
|
+
? undefined
|
|
77
|
+
: startingCheckpoint?.continuationToken;
|
|
78
|
+
const totals = emptyTotals();
|
|
79
|
+
const failedThreadMessageIds = [];
|
|
80
|
+
for (let accountIndex = startIndex; accountIndex < accounts.length; accountIndex++) {
|
|
81
|
+
const account = accounts[accountIndex];
|
|
82
|
+
let continuationToken = accountIndex === startIndex ? startContinuationToken : undefined;
|
|
83
|
+
do {
|
|
84
|
+
const page = await deps.threadMessageService.listByAccount(account.accountConfigId, { limit: batchSize, continuationToken });
|
|
85
|
+
totals.scanned += page.items.length;
|
|
86
|
+
const candidates = page.items.filter((row) => row.listId === undefined);
|
|
87
|
+
totals.alreadySet += page.items.length - candidates.length;
|
|
88
|
+
if (candidates.length > 0) {
|
|
89
|
+
const messages = await deps.messageService.get(candidates.map((row) => row.messageId));
|
|
90
|
+
const messageByMessageId = new Map(messages.map((message) => [message.messageId, message]));
|
|
91
|
+
for (const row of candidates) {
|
|
92
|
+
const message = messageByMessageId.get(row.messageId);
|
|
93
|
+
if (!message?.bodyStorageKey) {
|
|
94
|
+
totals.skippedNoBody++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const outcome = await deriveAndApplyListId(deps, account.accountConfigId, row, message.bodyStorageKey).then((listId) => ({ error: null, listId }), (error) => ({ error, listId: null }));
|
|
98
|
+
if (outcome.error !== null) {
|
|
99
|
+
totals.failed++;
|
|
100
|
+
failedThreadMessageIds.push(row.threadMessageId);
|
|
101
|
+
logger?.error?.({
|
|
102
|
+
threadMessageId: row.threadMessageId,
|
|
103
|
+
messageId: row.messageId,
|
|
104
|
+
error: outcome.error instanceof Error
|
|
105
|
+
? outcome.error.message
|
|
106
|
+
: String(outcome.error),
|
|
107
|
+
}, "ListId backfill failed for a message; leaving it for a later pass");
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (outcome.listId) {
|
|
111
|
+
totals.backfilled++;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
totals.noListId++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
continuationToken = page.continuationToken;
|
|
119
|
+
await checkpointStore?.save({
|
|
120
|
+
accountConfigId: account.accountConfigId,
|
|
121
|
+
continuationToken,
|
|
122
|
+
});
|
|
123
|
+
logger?.info({ accountConfigId: account.accountConfigId, ...totals }, "ListId backfill progress");
|
|
124
|
+
options.onProgress?.({
|
|
125
|
+
accountConfigId: account.accountConfigId,
|
|
126
|
+
...totals,
|
|
127
|
+
});
|
|
128
|
+
} while (continuationToken);
|
|
129
|
+
}
|
|
130
|
+
await checkpointStore?.clear();
|
|
131
|
+
return { ...totals, failedThreadMessageIds };
|
|
132
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure matching logic for the UIDVALIDITY cursor rebuild (issue #1272).
|
|
3
|
+
*
|
|
4
|
+
* Message identity is the Message-ID header; UIDs are cursor positions on
|
|
5
|
+
* the server's axis. When UIDVALIDITY changes, the axis is gone but the
|
|
6
|
+
* data isn't — this reconciles a fresh envelope-level snapshot (UID +
|
|
7
|
+
* Message-ID + INTERNALDATE, no body fetch) against the rows already stored
|
|
8
|
+
* for the mailbox, by identity rather than by position.
|
|
9
|
+
*/
|
|
10
|
+
/** One server message from the cheap envelope-only rebuild FETCH. */
|
|
11
|
+
export interface CursorRebuildSnapshot {
|
|
12
|
+
uid: number;
|
|
13
|
+
/** Raw RFC 822 Message-ID header; empty string when absent/unparseable. */
|
|
14
|
+
messageId: string;
|
|
15
|
+
/** INTERNALDATE in epoch milliseconds. */
|
|
16
|
+
internalDate: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* ThreadMessage identity + every composite-key attribute ElectroDB needs to
|
|
20
|
+
* run a conditional update against the row (see `buildThreadMessageMoveUpdate`
|
|
21
|
+
* in `message-move.ts` for the same pattern on a normal move). Carried
|
|
22
|
+
* alongside a {@link CursorRebuildRow} purely so a match can rewrite
|
|
23
|
+
* ThreadMessage.uid together with Message.uid — matching itself never reads
|
|
24
|
+
* these fields.
|
|
25
|
+
*/
|
|
26
|
+
export interface CursorRebuildThreadMessageRef {
|
|
27
|
+
accountConfigId: string;
|
|
28
|
+
threadMessageId: string;
|
|
29
|
+
sentDate: number;
|
|
30
|
+
mailboxId: string;
|
|
31
|
+
isRead: boolean;
|
|
32
|
+
isDeleted: boolean;
|
|
33
|
+
hasStars: boolean;
|
|
34
|
+
hasAttachment: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** One existing row for the mailbox, as needed to match it against a snapshot. */
|
|
37
|
+
export interface CursorRebuildRow {
|
|
38
|
+
/** Our internal messageId (the Message row's primary key). */
|
|
39
|
+
messageId: string;
|
|
40
|
+
/** Raw RFC 822 Message-ID header the row was created with. */
|
|
41
|
+
messageIdHeader: string;
|
|
42
|
+
/** INTERNALDATE in epoch milliseconds. */
|
|
43
|
+
internalDate: number;
|
|
44
|
+
/** The row's currently-stored UID (on the old, invalid axis). */
|
|
45
|
+
uid: number;
|
|
46
|
+
/** Present when the caller wants a match to also rewrite ThreadMessage.uid. */
|
|
47
|
+
threadMessage?: CursorRebuildThreadMessageRef;
|
|
48
|
+
}
|
|
49
|
+
export interface CursorRebuildMatch {
|
|
50
|
+
messageId: string;
|
|
51
|
+
oldUid: number;
|
|
52
|
+
newUid: number;
|
|
53
|
+
/** Carried through from the matched row so the caller can rewrite
|
|
54
|
+
* ThreadMessage.uid alongside Message.uid without a second lookup. */
|
|
55
|
+
threadMessage?: CursorRebuildThreadMessageRef;
|
|
56
|
+
}
|
|
57
|
+
export interface CursorRebuildMatchResult {
|
|
58
|
+
/** Rows whose Message-ID matched a server message under a different UID — rewrite in place. */
|
|
59
|
+
matched: CursorRebuildMatch[];
|
|
60
|
+
/** Server UIDs with no matching row — run through normal new-message sync. */
|
|
61
|
+
newUids: number[];
|
|
62
|
+
/** Rows with no matching server message — expunged; reconcile (#1283). */
|
|
63
|
+
staleMessageIds: string[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Match a fresh server snapshot against the mailbox's existing rows by
|
|
67
|
+
* Message-ID, falling back to an exact INTERNALDATE match for the (rare)
|
|
68
|
+
* messages with no usable Message-ID header on either side — mirrors the
|
|
69
|
+
* same headerless fallback `message-sync.ts` already uses for threading.
|
|
70
|
+
*
|
|
71
|
+
* Pure and side-effect free: the caller is responsible for acting on the
|
|
72
|
+
* three outcomes (rewrite / new-message sync / reconcile).
|
|
73
|
+
*/
|
|
74
|
+
export declare const matchCursorRebuild: (serverSnapshots: CursorRebuildSnapshot[], existingRows: CursorRebuildRow[]) => CursorRebuildMatchResult;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure matching logic for the UIDVALIDITY cursor rebuild (issue #1272).
|
|
3
|
+
*
|
|
4
|
+
* Message identity is the Message-ID header; UIDs are cursor positions on
|
|
5
|
+
* the server's axis. When UIDVALIDITY changes, the axis is gone but the
|
|
6
|
+
* data isn't — this reconciles a fresh envelope-level snapshot (UID +
|
|
7
|
+
* Message-ID + INTERNALDATE, no body fetch) against the rows already stored
|
|
8
|
+
* for the mailbox, by identity rather than by position.
|
|
9
|
+
*/
|
|
10
|
+
const pickClosestByInternalDate = (snapshot, candidates, consumed) => {
|
|
11
|
+
const available = candidates.filter((row) => !consumed.has(row.messageId));
|
|
12
|
+
if (available.length === 0)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (available.length === 1)
|
|
15
|
+
return available[0];
|
|
16
|
+
return available.reduce((best, row) => Math.abs(row.internalDate - snapshot.internalDate) <
|
|
17
|
+
Math.abs(best.internalDate - snapshot.internalDate)
|
|
18
|
+
? row
|
|
19
|
+
: best);
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Match a fresh server snapshot against the mailbox's existing rows by
|
|
23
|
+
* Message-ID, falling back to an exact INTERNALDATE match for the (rare)
|
|
24
|
+
* messages with no usable Message-ID header on either side — mirrors the
|
|
25
|
+
* same headerless fallback `message-sync.ts` already uses for threading.
|
|
26
|
+
*
|
|
27
|
+
* Pure and side-effect free: the caller is responsible for acting on the
|
|
28
|
+
* three outcomes (rewrite / new-message sync / reconcile).
|
|
29
|
+
*/
|
|
30
|
+
export const matchCursorRebuild = (serverSnapshots, existingRows) => {
|
|
31
|
+
const rowsByHeader = new Map();
|
|
32
|
+
const rowsByInternalDateOnly = new Map();
|
|
33
|
+
for (const row of existingRows) {
|
|
34
|
+
if (row.messageIdHeader) {
|
|
35
|
+
const list = rowsByHeader.get(row.messageIdHeader) ?? [];
|
|
36
|
+
list.push(row);
|
|
37
|
+
rowsByHeader.set(row.messageIdHeader, list);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const list = rowsByInternalDateOnly.get(row.internalDate) ?? [];
|
|
41
|
+
list.push(row);
|
|
42
|
+
rowsByInternalDateOnly.set(row.internalDate, list);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const consumed = new Set();
|
|
46
|
+
const matched = [];
|
|
47
|
+
const newUids = [];
|
|
48
|
+
for (const snapshot of serverSnapshots) {
|
|
49
|
+
const candidates = snapshot.messageId
|
|
50
|
+
? (rowsByHeader.get(snapshot.messageId) ?? [])
|
|
51
|
+
: (rowsByInternalDateOnly.get(snapshot.internalDate) ?? []);
|
|
52
|
+
const row = pickClosestByInternalDate(snapshot, candidates, consumed);
|
|
53
|
+
if (!row) {
|
|
54
|
+
newUids.push(snapshot.uid);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
consumed.add(row.messageId);
|
|
58
|
+
if (row.uid !== snapshot.uid) {
|
|
59
|
+
matched.push({
|
|
60
|
+
messageId: row.messageId,
|
|
61
|
+
oldUid: row.uid,
|
|
62
|
+
newUid: snapshot.uid,
|
|
63
|
+
...(row.threadMessage ? { threadMessage: row.threadMessage } : {}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const staleMessageIds = existingRows
|
|
68
|
+
.filter((row) => !consumed.has(row.messageId))
|
|
69
|
+
.map((row) => row.messageId);
|
|
70
|
+
return { matched, newUids, staleMessageIds };
|
|
71
|
+
};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { IMailboxRepository, MailboxItem } from "@remit/data-ports";
|
|
2
|
-
import {
|
|
3
|
-
import type { IImapConnection, ImapBoxStatus } from "./types.js";
|
|
4
|
-
|
|
2
|
+
import type { IImapConnection } from "./types.js";
|
|
5
3
|
/**
|
|
6
4
|
* Detection + trip for the UIDVALIDITY cursor-integrity state machine (issue
|
|
7
5
|
* #1272, epic #1281 invariant 5): `normal -> cursor_invalid -> rebuilding ->
|
|
@@ -18,18 +16,16 @@ import type { IImapConnection, ImapBoxStatus } from "./types.js";
|
|
|
18
16
|
* UIDVALIDITY right after opening the box, before issuing any command that
|
|
19
17
|
* references a stored UID.
|
|
20
18
|
*/
|
|
21
|
-
export type MailboxCursorCheck =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
export type MailboxCursorCheck = {
|
|
20
|
+
ok: true;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
/** The state the mailbox is (now) in — always non-`normal` here. */
|
|
24
|
+
state: "cursor_invalid" | "rebuilding";
|
|
25
|
+
};
|
|
29
26
|
export interface MailboxCursorGuardDeps {
|
|
30
|
-
|
|
27
|
+
mailboxService: Pick<IMailboxRepository, "update">;
|
|
31
28
|
}
|
|
32
|
-
|
|
33
29
|
/**
|
|
34
30
|
* `cursorState` is total per RFC 032 (defaults to `normal`) — but that default
|
|
35
31
|
* only applies to rows written after this field existed. A row persisted
|
|
@@ -39,10 +35,6 @@ export interface MailboxCursorGuardDeps {
|
|
|
39
35
|
* `normal` defensively rather than trusting the type.
|
|
40
36
|
*/
|
|
41
37
|
type MailboxCursorStateOrLegacyAbsent = MailboxItem["cursorState"] | undefined;
|
|
42
|
-
|
|
43
|
-
const isNormal = (state: MailboxCursorStateOrLegacyAbsent): boolean =>
|
|
44
|
-
state === undefined || state === MailboxCursorState.normal;
|
|
45
|
-
|
|
46
38
|
/**
|
|
47
39
|
* Compare a mailbox's stored UIDVALIDITY against what the server just served
|
|
48
40
|
* (from an `openBox`/`STATUS` response) and act:
|
|
@@ -60,41 +52,11 @@ const isNormal = (state: MailboxCursorStateOrLegacyAbsent): boolean =>
|
|
|
60
52
|
* (epic #1281 invariant 3), not a fault: log/metric and skip the outbound
|
|
61
53
|
* operation for this round rather than throwing.
|
|
62
54
|
*/
|
|
63
|
-
export const guardMailboxCursor
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
mailbox: Pick<MailboxItem, "mailboxId" | "uidValidity"> & {
|
|
67
|
-
cursorState: MailboxCursorStateOrLegacyAbsent;
|
|
68
|
-
},
|
|
69
|
-
servedUidValidity: number,
|
|
70
|
-
): Promise<MailboxCursorCheck> => {
|
|
71
|
-
if (!isNormal(mailbox.cursorState)) {
|
|
72
|
-
return {
|
|
73
|
-
ok: false,
|
|
74
|
-
state:
|
|
75
|
-
mailbox.cursorState === MailboxCursorState.rebuilding
|
|
76
|
-
? "rebuilding"
|
|
77
|
-
: "cursor_invalid",
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (mailbox.uidValidity !== servedUidValidity) {
|
|
82
|
-
await deps.mailboxService.update(accountId, mailbox.mailboxId, {
|
|
83
|
-
cursorState: MailboxCursorState.cursor_invalid,
|
|
84
|
-
});
|
|
85
|
-
return { ok: false, state: "cursor_invalid" };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { ok: true };
|
|
89
|
-
};
|
|
90
|
-
|
|
55
|
+
export declare const guardMailboxCursor: (deps: MailboxCursorGuardDeps, accountId: string, mailbox: Pick<MailboxItem, "mailboxId" | "uidValidity"> & {
|
|
56
|
+
cursorState: MailboxCursorStateOrLegacyAbsent;
|
|
57
|
+
}, servedUidValidity: number) => Promise<MailboxCursorCheck>;
|
|
91
58
|
/** True when a mailbox row is in either non-`normal` cursor state. */
|
|
92
|
-
export const isCursorRebuildNeeded
|
|
93
|
-
cursorState: MailboxCursorStateOrLegacyAbsent,
|
|
94
|
-
): boolean =>
|
|
95
|
-
cursorState === MailboxCursorState.cursor_invalid ||
|
|
96
|
-
cursorState === MailboxCursorState.rebuilding;
|
|
97
|
-
|
|
59
|
+
export declare const isCursorRebuildNeeded: (cursorState: MailboxCursorStateOrLegacyAbsent) => boolean;
|
|
98
60
|
/**
|
|
99
61
|
* Thrown by the `openBox` override on a {@link guardConnectionCursor}-wrapped
|
|
100
62
|
* connection when the mailbox cursor is (or just became) non-`normal`. Every
|
|
@@ -102,13 +64,10 @@ export const isCursorRebuildNeeded = (
|
|
|
102
64
|
* guardMailboxCursor} — catch it around the outbound operation and skip
|
|
103
65
|
* (ack/log/return), never let it surface as an infrastructure fault.
|
|
104
66
|
*/
|
|
105
|
-
export class MailboxCursorPausedError extends Error {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.name = "MailboxCursorPausedError";
|
|
109
|
-
}
|
|
67
|
+
export declare class MailboxCursorPausedError extends Error {
|
|
68
|
+
readonly state: "cursor_invalid" | "rebuilding";
|
|
69
|
+
constructor(state: "cursor_invalid" | "rebuilding");
|
|
110
70
|
}
|
|
111
|
-
|
|
112
71
|
/**
|
|
113
72
|
* Wrap a live `IImapConnection` so its `openBox` is the single, structural
|
|
114
73
|
* choke point for UIDVALIDITY cursor guarding (epic #1281 invariants 3 & 5).
|
|
@@ -134,44 +93,7 @@ export class MailboxCursorPausedError extends Error {
|
|
|
134
93
|
* `mailbox` is a snapshot taken once by the caller (they already need it for
|
|
135
94
|
* `mailboxPath`) — not re-fetched per call, so this adds no extra read.
|
|
136
95
|
*/
|
|
137
|
-
export const guardConnectionCursor
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
mailbox: Pick<MailboxItem, "mailboxId" | "uidValidity"> & {
|
|
142
|
-
cursorState: MailboxCursorStateOrLegacyAbsent;
|
|
143
|
-
},
|
|
144
|
-
): IImapConnection => {
|
|
145
|
-
const guardedOpenBox = async (
|
|
146
|
-
mailboxPath: string,
|
|
147
|
-
readOnly?: boolean,
|
|
148
|
-
): Promise<ImapBoxStatus> => {
|
|
149
|
-
if (!isNormal(mailbox.cursorState)) {
|
|
150
|
-
throw new MailboxCursorPausedError(
|
|
151
|
-
mailbox.cursorState === MailboxCursorState.rebuilding
|
|
152
|
-
? "rebuilding"
|
|
153
|
-
: "cursor_invalid",
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const boxStatus = await connection.openBox(mailboxPath, readOnly);
|
|
158
|
-
const check = await guardMailboxCursor(
|
|
159
|
-
deps,
|
|
160
|
-
accountId,
|
|
161
|
-
mailbox,
|
|
162
|
-
boxStatus.uidvalidity,
|
|
163
|
-
);
|
|
164
|
-
if (!check.ok) {
|
|
165
|
-
throw new MailboxCursorPausedError(check.state);
|
|
166
|
-
}
|
|
167
|
-
return boxStatus;
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
return new Proxy(connection, {
|
|
171
|
-
get(target, prop, receiver) {
|
|
172
|
-
if (prop === "openBox") return guardedOpenBox;
|
|
173
|
-
const value = Reflect.get(target, prop, receiver);
|
|
174
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
};
|
|
96
|
+
export declare const guardConnectionCursor: (connection: IImapConnection, deps: MailboxCursorGuardDeps, accountId: string, mailbox: Pick<MailboxItem, "mailboxId" | "uidValidity"> & {
|
|
97
|
+
cursorState: MailboxCursorStateOrLegacyAbsent;
|
|
98
|
+
}) => IImapConnection;
|
|
99
|
+
export {};
|