@remit/data-ports 0.0.8 → 0.0.9
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/package.json +3 -2
- package/src/id.test.ts +55 -0
- package/src/id.ts +43 -0
- package/src/index.ts +3 -0
- package/src/interfaces/quarantine.ts +10 -0
- package/src/types.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/data-ports",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"test:typecheck": "tsgo --noEmit"
|
|
34
|
+
"test:typecheck": "tsgo --noEmit",
|
|
35
|
+
"test:run": "node --import tsx --test 'src/**/*.test.ts'"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"@remit/domain-enums": "*",
|
package/src/id.test.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { deriveQuarantineId, quarantineMessageIdHash } from "./id.js";
|
|
4
|
+
|
|
5
|
+
const ACCOUNT = "acct-1";
|
|
6
|
+
const MAILBOX = "mbx-1";
|
|
7
|
+
|
|
8
|
+
describe("deriveQuarantineId", () => {
|
|
9
|
+
it("is stable for the same message", () => {
|
|
10
|
+
assert.equal(
|
|
11
|
+
deriveQuarantineId(ACCOUNT, MAILBOX, 1_700_000_000, 40217),
|
|
12
|
+
deriveQuarantineId(ACCOUNT, MAILBOX, 1_700_000_000, 40217),
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("separates the same uid across a UIDVALIDITY bump", () => {
|
|
17
|
+
// A mailbox keeps its mailboxId when the server bumps UIDVALIDITY, so
|
|
18
|
+
// without this the stale entry would name the new message and suppress
|
|
19
|
+
// it from a sync round — losing mail rather than a diagnostic.
|
|
20
|
+
assert.notEqual(
|
|
21
|
+
deriveQuarantineId(ACCOUNT, MAILBOX, 1_700_000_000, 40217),
|
|
22
|
+
deriveQuarantineId(ACCOUNT, MAILBOX, 1_700_000_001, 40217),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("separates mailboxes and accounts", () => {
|
|
27
|
+
const base = deriveQuarantineId(ACCOUNT, MAILBOX, 1, 40217);
|
|
28
|
+
assert.notEqual(base, deriveQuarantineId(ACCOUNT, "mbx-2", 1, 40217));
|
|
29
|
+
assert.notEqual(base, deriveQuarantineId("acct-2", MAILBOX, 1, 40217));
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("quarantineMessageIdHash", () => {
|
|
34
|
+
it("hashes a real Message-ID to a pinned sha256 value", () => {
|
|
35
|
+
const hash = quarantineMessageIdHash("<abc@example.com>");
|
|
36
|
+
assert.match(hash ?? "", /^sha256:[0-9a-f]{64}$/);
|
|
37
|
+
assert.equal(hash, quarantineMessageIdHash("<abc@example.com>"));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("distinguishes different Message-IDs", () => {
|
|
41
|
+
assert.notEqual(
|
|
42
|
+
quarantineMessageIdHash("<a@example.com>"),
|
|
43
|
+
quarantineMessageIdHash("<b@example.com>"),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("refuses to hash a Message-ID the sync path never had", () => {
|
|
48
|
+
// The sync path coerces a missing Message-ID to "". Hashing that would
|
|
49
|
+
// give every such message one shared hash and correlate unrelated
|
|
50
|
+
// reports — the opposite of what the field is for.
|
|
51
|
+
for (const absent of [undefined, "", " ", "<>"]) {
|
|
52
|
+
assert.equal(quarantineMessageIdHash(absent), undefined);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
package/src/id.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import shortUuid from "short-uuid";
|
|
2
3
|
import { v5 as uuidv5 } from "uuid";
|
|
3
4
|
import type { MessageIdSource } from "./types.js";
|
|
@@ -103,3 +104,45 @@ export const deriveBodyPartParameterId = (
|
|
|
103
104
|
`bodypartparam:${messageId}:${partPath}:${parameterName.toLowerCase()}`,
|
|
104
105
|
REMIT_NAMESPACE,
|
|
105
106
|
);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Identity of a quarantined message (issue #72). Derived rather than generated
|
|
110
|
+
* so re-quarantining the same message rewrites its own row: the sync path can
|
|
111
|
+
* write without first checking whether an entry already exists.
|
|
112
|
+
*
|
|
113
|
+
* `uidValidity` is part of the name because a uid alone does not identify a
|
|
114
|
+
* message — only uidValidity + uid does, forever (RFC 9051 2.3.1.1). A mailbox
|
|
115
|
+
* keeps its `mailboxId` across a UIDVALIDITY bump (the sync path updates the
|
|
116
|
+
* same row), so without this a stale entry would derive the same id as a later,
|
|
117
|
+
* unrelated message and suppress it from a sync round.
|
|
118
|
+
*/
|
|
119
|
+
export const deriveQuarantineId = (
|
|
120
|
+
accountId: string,
|
|
121
|
+
mailboxId: string,
|
|
122
|
+
uidValidity: number,
|
|
123
|
+
uid: number,
|
|
124
|
+
): string =>
|
|
125
|
+
base36uuidv5(
|
|
126
|
+
`quarantine:${accountId}:${mailboxId}:${uidValidity}:${uid}`,
|
|
127
|
+
REMIT_NAMESPACE,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The `sha256:`-prefixed Message-ID hash a quarantine record carries, or
|
|
132
|
+
* undefined when the message declared no usable Message-ID.
|
|
133
|
+
*
|
|
134
|
+
* The sync path coerces a missing Message-ID to `""`, and hashing that would
|
|
135
|
+
* give every Message-ID-less message the same hash — so the one field whose
|
|
136
|
+
* purpose is correlating reports of the same message would silently correlate
|
|
137
|
+
* unrelated ones. Absent is the honest value, and this is the only way the
|
|
138
|
+
* value is produced, so the empty hash cannot be written by accident.
|
|
139
|
+
*/
|
|
140
|
+
export const quarantineMessageIdHash = (
|
|
141
|
+
messageIdHeader: string | undefined,
|
|
142
|
+
): string | undefined => {
|
|
143
|
+
if (!messageIdHeader || !isValidMessageId(messageIdHeader)) return undefined;
|
|
144
|
+
const digest = createHash("sha256")
|
|
145
|
+
.update(messageIdHeader.trim())
|
|
146
|
+
.digest("hex");
|
|
147
|
+
return `sha256:${digest}`;
|
|
148
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type { IMessageLabelRepository } from "./interfaces/message-label.js";
|
|
|
17
17
|
export type { IMessagePlacementMoveRepository } from "./interfaces/message-placement-move.js";
|
|
18
18
|
export type { IOrganizeJobRequestRepository } from "./interfaces/organize-job-request.js";
|
|
19
19
|
export type { IOutboxMessageRepository } from "./interfaces/outbox-message.js";
|
|
20
|
+
export type { IQuarantineRepository } from "./interfaces/quarantine.js";
|
|
20
21
|
export type { IThreadMessageRepository } from "./interfaces/thread-message.js";
|
|
21
22
|
export type {
|
|
22
23
|
IUnitOfWork,
|
|
@@ -80,6 +81,8 @@ export type {
|
|
|
80
81
|
OutboxMessageItem,
|
|
81
82
|
PutMessageFlagPushInput,
|
|
82
83
|
PutMessagePlacementMoveInput,
|
|
84
|
+
QuarantineItem,
|
|
85
|
+
QuarantineMimeNodeItem,
|
|
83
86
|
RawMessageStorageItem,
|
|
84
87
|
ResultList,
|
|
85
88
|
SearchOptions,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { QuarantineItem } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export interface IQuarantineRepository {
|
|
4
|
+
/**
|
|
5
|
+
* Every quarantined message for one user, newest first. Unpaginated: the
|
|
6
|
+
* list is small by design, and it is read whole both by the settings
|
|
7
|
+
* surface and by a sync round that keeps it in memory.
|
|
8
|
+
*/
|
|
9
|
+
listByAccountConfigId(accountConfigId: string): Promise<QuarantineItem[]>;
|
|
10
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
MessageSchema,
|
|
23
23
|
OrganizeJobRequestSchema,
|
|
24
24
|
OutboxMessageSchema,
|
|
25
|
+
QuarantineSchema,
|
|
25
26
|
RawMessageStorageSchema,
|
|
26
27
|
ThreadMessageSchema,
|
|
27
28
|
} from "@remit/api-zod-schemas";
|
|
@@ -85,6 +86,8 @@ export type MessagePlacementMoveItem = z.infer<
|
|
|
85
86
|
typeof MessagePlacementMoveSchema
|
|
86
87
|
>;
|
|
87
88
|
export type MessageFlagPushItem = z.infer<typeof MessageFlagPushSchema>;
|
|
89
|
+
export type QuarantineItem = z.infer<typeof QuarantineSchema>;
|
|
90
|
+
export type QuarantineMimeNodeItem = QuarantineItem["structure"][number];
|
|
88
91
|
|
|
89
92
|
export type MailboxSpecialUseValue = z.infer<typeof MailboxSpecialUseSchema>;
|
|
90
93
|
|