@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/src/spam-report.ts
DELETED
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IAccountRepository,
|
|
3
|
-
IAddressRepository,
|
|
4
|
-
IMailboxSpecialUseRepository,
|
|
5
|
-
IMessageRepository,
|
|
6
|
-
} from "@remit/data-ports";
|
|
7
|
-
import { deriveAddressId } from "@remit/data-ports/id";
|
|
8
|
-
import { AddressRole, MessageKeywordFlag } from "@remit/domain-enums";
|
|
9
|
-
import type { FlagPushService } from "./flag-push.js";
|
|
10
|
-
import type { MessageMoveService } from "./message-move.js";
|
|
11
|
-
import {
|
|
12
|
-
isPlacementUnsettled,
|
|
13
|
-
waitForPlacementToSettle,
|
|
14
|
-
} from "./placement-settled.js";
|
|
15
|
-
|
|
16
|
-
export interface SpamReportLogger {
|
|
17
|
-
info(obj: Record<string, unknown>, msg: string): void;
|
|
18
|
-
error(obj: Record<string, unknown>, msg: string): void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The one designed, user-facing outcome of `notSpam`'s R2 wait timing out —
|
|
23
|
-
* distinct from any other failure so a caller surfacing failures to the user
|
|
24
|
-
* (the bulk handlers in packages/backend) can allowlist this specific,
|
|
25
|
-
* pre-written message instead of relaying an arbitrary thrown error's text.
|
|
26
|
-
*/
|
|
27
|
-
export class MoveNotSettledError extends Error {
|
|
28
|
-
constructor(messageId: string) {
|
|
29
|
-
super(
|
|
30
|
-
`Message ${messageId}'s move to Junk has not settled yet; try again in a moment.`,
|
|
31
|
-
);
|
|
32
|
-
this.name = "MoveNotSettledError";
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* The account advertises no Junk folder and has none under any conventional
|
|
38
|
-
* name, so there is nowhere to file the report. A designed, user-facing
|
|
39
|
-
* outcome — the user has to create the folder, and no amount of retrying
|
|
40
|
-
* changes that — so it carries its own text through the bulk handler's
|
|
41
|
-
* allowlist rather than being flattened to the generic retry copy.
|
|
42
|
-
*/
|
|
43
|
-
export class NoJunkMailboxError extends Error {
|
|
44
|
-
constructor() {
|
|
45
|
-
super(
|
|
46
|
-
"This account has no Junk folder. Create one named Junk or Spam in your mail provider, then report this message again.",
|
|
47
|
-
);
|
|
48
|
-
this.name = "NoJunkMailboxError";
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const noopLogger: SpamReportLogger = {
|
|
53
|
-
info: () => {},
|
|
54
|
-
error: () => {},
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const DEFAULT_MOVE_SETTLE_TIMEOUT_MS = 5_000;
|
|
58
|
-
const DEFAULT_MOVE_SETTLE_POLL_MS = 250;
|
|
59
|
-
|
|
60
|
-
export interface SpamReportConfig {
|
|
61
|
-
messageService: IMessageRepository;
|
|
62
|
-
addressService: IAddressRepository;
|
|
63
|
-
accountService: IAccountRepository;
|
|
64
|
-
mailboxSpecialUseService: IMailboxSpecialUseRepository;
|
|
65
|
-
messageMoveService: MessageMoveService;
|
|
66
|
-
flagPushService: FlagPushService;
|
|
67
|
-
logger?: SpamReportLogger;
|
|
68
|
-
/** How long `notSpam` waits for a still-in-flight move before giving up. */
|
|
69
|
-
moveSettleTimeoutMs?: number;
|
|
70
|
-
moveSettlePollMs?: number;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface SpamReportParams {
|
|
74
|
-
accountConfigId: string;
|
|
75
|
-
accountId: string;
|
|
76
|
-
messageId: string;
|
|
77
|
-
setBy?: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export class SpamReportService {
|
|
81
|
-
private messageService: IMessageRepository;
|
|
82
|
-
private addressService: IAddressRepository;
|
|
83
|
-
private accountService: IAccountRepository;
|
|
84
|
-
private mailboxSpecialUseService: IMailboxSpecialUseRepository;
|
|
85
|
-
private messageMoveService: MessageMoveService;
|
|
86
|
-
private flagPushService: FlagPushService;
|
|
87
|
-
private log: SpamReportLogger;
|
|
88
|
-
private moveSettleTimeoutMs: number;
|
|
89
|
-
private moveSettlePollMs: number;
|
|
90
|
-
|
|
91
|
-
constructor(config: SpamReportConfig) {
|
|
92
|
-
this.messageService = config.messageService;
|
|
93
|
-
this.addressService = config.addressService;
|
|
94
|
-
this.accountService = config.accountService;
|
|
95
|
-
this.mailboxSpecialUseService = config.mailboxSpecialUseService;
|
|
96
|
-
this.messageMoveService = config.messageMoveService;
|
|
97
|
-
this.flagPushService = config.flagPushService;
|
|
98
|
-
this.log = config.logger ?? noopLogger;
|
|
99
|
-
this.moveSettleTimeoutMs =
|
|
100
|
-
config.moveSettleTimeoutMs ?? DEFAULT_MOVE_SETTLE_TIMEOUT_MS;
|
|
101
|
-
this.moveSettlePollMs =
|
|
102
|
-
config.moveSettlePollMs ?? DEFAULT_MOVE_SETTLE_POLL_MS;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* The sender to act on. `addressId` is derived rather than read off the
|
|
107
|
-
* envelope row so it always names the row `ensureSenderAddress` writes and
|
|
108
|
-
* the harvester writes — one derivation, one row, whatever the envelope
|
|
109
|
-
* happens to carry.
|
|
110
|
-
*/
|
|
111
|
-
private resolveFromAddress = async (
|
|
112
|
-
accountConfigId: string,
|
|
113
|
-
messageId: string,
|
|
114
|
-
): Promise<{
|
|
115
|
-
addressId: string;
|
|
116
|
-
normalizedEmail: string;
|
|
117
|
-
displayName: string;
|
|
118
|
-
}> => {
|
|
119
|
-
const description = await this.messageService.describe(messageId);
|
|
120
|
-
const from = description.envelopeAddress.find(
|
|
121
|
-
(a) => a.addressRole === AddressRole.From,
|
|
122
|
-
);
|
|
123
|
-
if (!from) {
|
|
124
|
-
throw new Error(`Message ${messageId} has no From address to act on`);
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
addressId: deriveAddressId(accountConfigId, from.normalizedEmail),
|
|
128
|
-
normalizedEmail: from.normalizedEmail,
|
|
129
|
-
displayName: from.displayName ?? "",
|
|
130
|
-
};
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* The list form answers `[]` for a row that is not there, where the single
|
|
135
|
-
* form throws — absence is a case both callers handle, not a fault.
|
|
136
|
-
*/
|
|
137
|
-
private senderIsKnown = async (
|
|
138
|
-
accountConfigId: string,
|
|
139
|
-
addressId: string,
|
|
140
|
-
): Promise<boolean> => {
|
|
141
|
-
const [known] = await this.addressService.getAddress(accountConfigId, [
|
|
142
|
-
addressId,
|
|
143
|
-
]);
|
|
144
|
-
return known !== undefined;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Blocking a sender owns the row it writes to. Harvesting is what ordinarily
|
|
149
|
-
* creates it, but a message whose harvest never landed — or whose row was
|
|
150
|
-
* taken by a cascade — still has a sender the user is entitled to block, and
|
|
151
|
-
* a missing row used to abort report-spam before the message ever reached
|
|
152
|
-
* Junk. The id is the harvester's own derivation, so this converges on the
|
|
153
|
-
* one row rather than minting a parallel one.
|
|
154
|
-
*
|
|
155
|
-
* A row that already exists is left exactly as it stands. Reporting spam is
|
|
156
|
-
* evidence about the sender, never about the account's standing record of
|
|
157
|
-
* them: an upsert here would carry whatever the upsert path does on
|
|
158
|
-
* conflict — today a display name chosen by the spammer, and any flag a
|
|
159
|
-
* later sighting-clearing rule hangs off it — into an operation that means
|
|
160
|
-
* the opposite.
|
|
161
|
-
*/
|
|
162
|
-
private ensureSenderAddress = async (
|
|
163
|
-
accountConfigId: string,
|
|
164
|
-
from: { addressId: string; normalizedEmail: string; displayName: string },
|
|
165
|
-
): Promise<void> => {
|
|
166
|
-
const separator = from.normalizedEmail.lastIndexOf("@");
|
|
167
|
-
const localPart = from.normalizedEmail.slice(0, separator);
|
|
168
|
-
const domain = from.normalizedEmail.slice(separator + 1);
|
|
169
|
-
if (separator < 0 || localPart.length === 0 || domain.length === 0) {
|
|
170
|
-
throw new Error(
|
|
171
|
-
`Sender address ${from.normalizedEmail} is not a usable email address`,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
if (await this.senderIsKnown(accountConfigId, from.addressId)) return;
|
|
175
|
-
await this.addressService.upsertAddress({
|
|
176
|
-
addressId: from.addressId,
|
|
177
|
-
accountConfigId,
|
|
178
|
-
localPart,
|
|
179
|
-
domain,
|
|
180
|
-
normalizedEmail: from.normalizedEmail,
|
|
181
|
-
normalizedCompound:
|
|
182
|
-
`${from.displayName.toLowerCase()} ${from.normalizedEmail}`.trim(),
|
|
183
|
-
displayName: from.displayName,
|
|
184
|
-
});
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
reportSpam = async (params: SpamReportParams): Promise<void> => {
|
|
188
|
-
const { accountConfigId, accountId, messageId, setBy } = params;
|
|
189
|
-
const now = Date.now();
|
|
190
|
-
|
|
191
|
-
// Resolved before anything is written: with nowhere to file the report,
|
|
192
|
-
// the operation cannot happen, and a blocked sender plus a `spamReport`
|
|
193
|
-
// stamp on a message still sitting in the inbox is a report that half
|
|
194
|
-
// happened. Fail here and the press changed nothing.
|
|
195
|
-
const junkMailbox =
|
|
196
|
-
await this.mailboxSpecialUseService.findJunkMailbox(accountId);
|
|
197
|
-
if (!junkMailbox) {
|
|
198
|
-
this.log.error({ accountId, messageId }, "No Junk mailbox for account");
|
|
199
|
-
throw new NoJunkMailboxError();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const from = await this.resolveFromAddress(accountConfigId, messageId);
|
|
203
|
-
const account = await this.accountService.get(accountId);
|
|
204
|
-
const isOwnAddress =
|
|
205
|
-
from.normalizedEmail.toLowerCase() === account.email.toLowerCase();
|
|
206
|
-
|
|
207
|
-
if (!isOwnAddress) {
|
|
208
|
-
await this.ensureSenderAddress(accountConfigId, from);
|
|
209
|
-
await this.addressService.mergeFlags(accountConfigId, from.addressId, {
|
|
210
|
-
blocked: { value: true, setAt: now, setBy },
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Captured before this call writes anything: `before.spamReport` tells
|
|
215
|
-
// apart the two reasons the message can already be in Junk. If it is
|
|
216
|
-
// already set, a PREVIOUS report-spam press put it there and owns
|
|
217
|
-
// `originalMailboxId` — that value is exactly what Undo needs and must
|
|
218
|
-
// survive a repeated press. Only when it is absent is the message in
|
|
219
|
-
// Junk for some unrelated reason (a provider filter, a manual move),
|
|
220
|
-
// making any `originalMailboxId` on the row stale.
|
|
221
|
-
const before = await this.messageService.get(messageId);
|
|
222
|
-
const hadSpamReportAlready = Boolean(before.spamReport);
|
|
223
|
-
|
|
224
|
-
await this.messageService.update(messageId, {
|
|
225
|
-
spamReport: { reportedAt: now },
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
const alreadyInJunk = before.mailboxId === junkMailbox.mailboxId;
|
|
229
|
-
|
|
230
|
-
await this.messageMoveService.moveMessage(
|
|
231
|
-
accountConfigId,
|
|
232
|
-
messageId,
|
|
233
|
-
junkMailbox.mailboxId,
|
|
234
|
-
accountId,
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
if (alreadyInJunk && before.originalMailboxId && !hadSpamReportAlready) {
|
|
238
|
-
// moveMessage's same-mailbox guard no-opped — it never touches
|
|
239
|
-
// originalMailboxId, so a stale value left by some earlier, unrelated
|
|
240
|
-
// move would otherwise survive and send a later notSpam to the wrong
|
|
241
|
-
// folder. This report-spam action established no move of its own.
|
|
242
|
-
//
|
|
243
|
-
// A transition off the row this call read (imap-mutations R3): the pair
|
|
244
|
-
// being cleared belongs to a placement, and a lane that has claimed the
|
|
245
|
-
// row since keeps its own — its settle manages the pair.
|
|
246
|
-
await this.messageService.transitionPlacement(
|
|
247
|
-
messageId,
|
|
248
|
-
{
|
|
249
|
-
status: before.status,
|
|
250
|
-
mailboxId: before.mailboxId,
|
|
251
|
-
uid: before.uid,
|
|
252
|
-
},
|
|
253
|
-
{ originalMailboxId: null, originalUid: null },
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
await this.flagPushService.flip({
|
|
258
|
-
accountId,
|
|
259
|
-
accountConfigId,
|
|
260
|
-
messageId,
|
|
261
|
-
mailboxId: junkMailbox.mailboxId,
|
|
262
|
-
flagName: MessageKeywordFlag.Junk,
|
|
263
|
-
operation: "add",
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
this.log.info(
|
|
267
|
-
{
|
|
268
|
-
accountId,
|
|
269
|
-
messageId,
|
|
270
|
-
addressId: from.addressId,
|
|
271
|
-
junkMailboxId: junkMailbox.mailboxId,
|
|
272
|
-
skippedBlock: isOwnAddress,
|
|
273
|
-
},
|
|
274
|
-
"Reported message as spam",
|
|
275
|
-
);
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
notSpam = async (params: SpamReportParams): Promise<void> => {
|
|
279
|
-
const { accountConfigId, accountId, messageId } = params;
|
|
280
|
-
|
|
281
|
-
// Unblocking a sender the account has no row for is already true, and
|
|
282
|
-
// minting one here would put an address nothing ever harvested into the
|
|
283
|
-
// address book — the withdrawal of a judgement is not a sighting.
|
|
284
|
-
const from = await this.resolveFromAddress(accountConfigId, messageId);
|
|
285
|
-
if (await this.senderIsKnown(accountConfigId, from.addressId)) {
|
|
286
|
-
await this.addressService.mergeFlags(accountConfigId, from.addressId, {
|
|
287
|
-
blocked: null,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const message = await this.messageService.get(messageId);
|
|
292
|
-
|
|
293
|
-
if (message.originalMailboxId) {
|
|
294
|
-
// Only wait on the move's settlement when there is actually a
|
|
295
|
-
// dependent write to make (the restore below) — a move in flight for
|
|
296
|
-
// an unrelated reason is not this operation's concern.
|
|
297
|
-
const settled = await waitForPlacementToSettle(
|
|
298
|
-
this.messageService,
|
|
299
|
-
messageId,
|
|
300
|
-
{
|
|
301
|
-
timeoutMs: this.moveSettleTimeoutMs,
|
|
302
|
-
pollMs: this.moveSettlePollMs,
|
|
303
|
-
},
|
|
304
|
-
);
|
|
305
|
-
if (isPlacementUnsettled(settled)) {
|
|
306
|
-
throw new MoveNotSettledError(messageId);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
await this.messageMoveService.restoreMessage(
|
|
310
|
-
accountConfigId,
|
|
311
|
-
messageId,
|
|
312
|
-
accountId,
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
// Re-read, then clear against what was read. The restore just wrote a
|
|
316
|
-
// placement, so the row here is not the one above; predicating on it is
|
|
317
|
-
// what keeps this from erasing a pair some other lane is relying on
|
|
318
|
-
// (imap-mutations R3).
|
|
319
|
-
const restored = await this.messageService.get(messageId);
|
|
320
|
-
await this.messageService.transitionPlacement(
|
|
321
|
-
messageId,
|
|
322
|
-
{
|
|
323
|
-
status: restored.status,
|
|
324
|
-
mailboxId: restored.mailboxId,
|
|
325
|
-
uid: restored.uid,
|
|
326
|
-
},
|
|
327
|
-
{ originalMailboxId: null, originalUid: null },
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const current = await this.messageService.get(messageId);
|
|
332
|
-
|
|
333
|
-
await this.flagPushService.flip({
|
|
334
|
-
accountId,
|
|
335
|
-
accountConfigId,
|
|
336
|
-
messageId,
|
|
337
|
-
mailboxId: current.mailboxId,
|
|
338
|
-
flagName: MessageKeywordFlag.Junk,
|
|
339
|
-
operation: "remove",
|
|
340
|
-
});
|
|
341
|
-
await this.flagPushService.flip({
|
|
342
|
-
accountId,
|
|
343
|
-
accountConfigId,
|
|
344
|
-
messageId,
|
|
345
|
-
mailboxId: current.mailboxId,
|
|
346
|
-
flagName: MessageKeywordFlag.NotJunk,
|
|
347
|
-
operation: "add",
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
await this.messageService.clearSpamReport(messageId);
|
|
351
|
-
|
|
352
|
-
this.log.info(
|
|
353
|
-
{ accountId, messageId, addressId: from.addressId },
|
|
354
|
-
"Undid spam report",
|
|
355
|
-
);
|
|
356
|
-
};
|
|
357
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type {
|
|
4
|
-
IMessageRepository,
|
|
5
|
-
IThreadMessageRepository,
|
|
6
|
-
} from "@remit/data-ports";
|
|
7
|
-
import {
|
|
8
|
-
reconcileStaleMessage,
|
|
9
|
-
type StaleMessageReconcileDeps,
|
|
10
|
-
} from "./stale-message-reconcile.js";
|
|
11
|
-
|
|
12
|
-
describe("reconcileStaleMessage", () => {
|
|
13
|
-
it("deletes the Message row and every ThreadMessage copy that points at it", async () => {
|
|
14
|
-
const deletedMessages: string[] = [];
|
|
15
|
-
const deletedThreadMessageKeys: Array<{
|
|
16
|
-
accountConfigId: string;
|
|
17
|
-
threadMessageId: string;
|
|
18
|
-
}> = [];
|
|
19
|
-
|
|
20
|
-
const deps: StaleMessageReconcileDeps = {
|
|
21
|
-
messageService: {
|
|
22
|
-
delete: async (messageId: string) => {
|
|
23
|
-
deletedMessages.push(messageId);
|
|
24
|
-
},
|
|
25
|
-
} as unknown as Pick<IMessageRepository, "delete">,
|
|
26
|
-
threadMessageService: {
|
|
27
|
-
findAllByMessageId: async (
|
|
28
|
-
accountConfigId: string,
|
|
29
|
-
messageId: string,
|
|
30
|
-
) => [
|
|
31
|
-
{ accountConfigId, threadMessageId: `tm-inbox-${messageId}` },
|
|
32
|
-
{ accountConfigId, threadMessageId: `tm-label-${messageId}` },
|
|
33
|
-
],
|
|
34
|
-
deleteMany: async (
|
|
35
|
-
keys: Array<{ accountConfigId: string; threadMessageId: string }>,
|
|
36
|
-
) => {
|
|
37
|
-
deletedThreadMessageKeys.push(...keys);
|
|
38
|
-
},
|
|
39
|
-
} as unknown as Pick<
|
|
40
|
-
IThreadMessageRepository,
|
|
41
|
-
"findAllByMessageId" | "deleteMany"
|
|
42
|
-
>,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const result = await reconcileStaleMessage(deps, "cfg-1", "msg-1");
|
|
46
|
-
|
|
47
|
-
assert.equal(result.threadMessagesDeleted, 2);
|
|
48
|
-
assert.deepEqual(deletedMessages, ["msg-1"]);
|
|
49
|
-
assert.deepEqual(deletedThreadMessageKeys, [
|
|
50
|
-
{ accountConfigId: "cfg-1", threadMessageId: "tm-inbox-msg-1" },
|
|
51
|
-
{ accountConfigId: "cfg-1", threadMessageId: "tm-label-msg-1" },
|
|
52
|
-
]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("skips the batch delete (never calls deleteMany with an empty array) when no copies exist", async () => {
|
|
56
|
-
const deletedMessages: string[] = [];
|
|
57
|
-
let deleteManyCalls = 0;
|
|
58
|
-
|
|
59
|
-
const deps: StaleMessageReconcileDeps = {
|
|
60
|
-
messageService: {
|
|
61
|
-
delete: async (messageId: string) => {
|
|
62
|
-
deletedMessages.push(messageId);
|
|
63
|
-
},
|
|
64
|
-
} as unknown as Pick<IMessageRepository, "delete">,
|
|
65
|
-
threadMessageService: {
|
|
66
|
-
findAllByMessageId: async () => [],
|
|
67
|
-
deleteMany: async () => {
|
|
68
|
-
deleteManyCalls++;
|
|
69
|
-
},
|
|
70
|
-
} as unknown as Pick<
|
|
71
|
-
IThreadMessageRepository,
|
|
72
|
-
"findAllByMessageId" | "deleteMany"
|
|
73
|
-
>,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const result = await reconcileStaleMessage(deps, "cfg-1", "msg-solo");
|
|
77
|
-
|
|
78
|
-
assert.equal(result.threadMessagesDeleted, 0);
|
|
79
|
-
assert.deepEqual(deletedMessages, ["msg-solo"]);
|
|
80
|
-
assert.equal(deleteManyCalls, 0);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IMessageRepository,
|
|
3
|
-
IThreadMessageRepository,
|
|
4
|
-
} from "@remit/data-ports";
|
|
5
|
-
|
|
6
|
-
export interface StaleMessageReconcileDeps {
|
|
7
|
-
messageService: Pick<IMessageRepository, "delete">;
|
|
8
|
-
threadMessageService: Pick<
|
|
9
|
-
IThreadMessageRepository,
|
|
10
|
-
"findAllByMessageId" | "deleteMany"
|
|
11
|
-
>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface StaleMessageReconcileResult {
|
|
15
|
-
threadMessagesDeleted: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Delete a Message row (and every ThreadMessage copy that points at it) once
|
|
20
|
-
* the caller has confirmed the message no longer exists on the IMAP server.
|
|
21
|
-
*
|
|
22
|
-
* A row outliving an upstream expunge (or a UIDVALIDITY change — #1272) is
|
|
23
|
-
* the EXPECTED terminal outcome for a body that can never be fetched (issue
|
|
24
|
-
* #1270 / epic #1281 invariant 3): expunges are routine, so cleaning up here
|
|
25
|
-
* is normal operation, not an incident. Deleting the row makes the existing
|
|
26
|
-
* missing-row 404 path (`MessageService.get` / `describe`) authoritative
|
|
27
|
-
* again — nothing renders the row, nothing re-arms a sync for it.
|
|
28
|
-
*
|
|
29
|
-
* Exported standalone, not inlined in the body-sync retry path, so #1272's
|
|
30
|
-
* UIDVALIDITY cursor rebuild can call the same reconciliation when it finds
|
|
31
|
-
* rows whose UIDs no longer resolve after a cursor rebuild.
|
|
32
|
-
*/
|
|
33
|
-
export const reconcileStaleMessage = async (
|
|
34
|
-
deps: StaleMessageReconcileDeps,
|
|
35
|
-
accountConfigId: string,
|
|
36
|
-
messageId: string,
|
|
37
|
-
): Promise<StaleMessageReconcileResult> => {
|
|
38
|
-
const threadMessages = await deps.threadMessageService.findAllByMessageId(
|
|
39
|
-
accountConfigId,
|
|
40
|
-
messageId,
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
if (threadMessages.length > 0) {
|
|
44
|
-
await deps.threadMessageService.deleteMany(
|
|
45
|
-
threadMessages.map((row) => ({
|
|
46
|
-
accountConfigId: row.accountConfigId,
|
|
47
|
-
threadMessageId: row.threadMessageId,
|
|
48
|
-
})),
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
await deps.messageService.delete(messageId);
|
|
53
|
-
|
|
54
|
-
return { threadMessagesDeleted: threadMessages.length };
|
|
55
|
-
};
|