@remit/mailbox-service 0.0.83 → 0.0.85
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/mailbox-service",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.85",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"types": "./src/body-sync-queue.ts",
|
|
14
14
|
"default": "./src/body-sync-queue.ts"
|
|
15
15
|
},
|
|
16
|
+
"./imapflow-connection": {
|
|
17
|
+
"types": "./src/imapflow-connection.ts",
|
|
18
|
+
"default": "./src/imapflow-connection.ts"
|
|
19
|
+
},
|
|
16
20
|
"./account-credentials": {
|
|
17
21
|
"types": "./src/account-credentials.ts",
|
|
18
22
|
"default": "./src/account-credentials.ts"
|
|
@@ -105,4 +105,30 @@ describe("ImapFlowConnection.connect() retry policy", () => {
|
|
|
105
105
|
assert.equal(attempt.mock.callCount(), 1);
|
|
106
106
|
assert.equal(sleep.mock.callCount(), 0);
|
|
107
107
|
});
|
|
108
|
+
|
|
109
|
+
it("carries the server's own refusal, which is not always a re-auth", async () => {
|
|
110
|
+
const connection = new ImapFlowConnection(baseConfig);
|
|
111
|
+
const priv = connection as unknown as WithPrivates;
|
|
112
|
+
|
|
113
|
+
// imapflow's shape for a tagged NO: the message is its own, the server's
|
|
114
|
+
// words are on `responseText`. Signing in again fixes nothing here — the
|
|
115
|
+
// tenant has IMAP switched off — so the text has to reach the user.
|
|
116
|
+
mock.method(priv, "attemptConnect", async () => {
|
|
117
|
+
throw Object.assign(new Error("Command failed"), {
|
|
118
|
+
authenticationFailed: true,
|
|
119
|
+
responseText:
|
|
120
|
+
"LOGIN failed. User is authenticated but not connected. SmtpClientAuthentication is disabled for the Tenant.",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await assert.rejects(
|
|
125
|
+
() => connection.connect(),
|
|
126
|
+
(error: unknown) => {
|
|
127
|
+
assert.ok(error instanceof MailConnectionError);
|
|
128
|
+
assert.equal(error.kind, "auth");
|
|
129
|
+
assert.match(error.message, /SmtpClientAuthentication is disabled/);
|
|
130
|
+
return true;
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
});
|
|
108
134
|
});
|
|
@@ -1391,6 +1391,20 @@ const buildImapAuth = (
|
|
|
1391
1391
|
throw new Error(`Unknown credential kind: ${JSON.stringify(_exhaustive)}`);
|
|
1392
1392
|
};
|
|
1393
1393
|
|
|
1394
|
+
/**
|
|
1395
|
+
* The server's own words for the failure: the text of the tagged NO/BAD, which
|
|
1396
|
+
* imapflow hangs off the error as `responseText`. It is the server's side of
|
|
1397
|
+
* the exchange only — never the command we sent, so no credential can ride out
|
|
1398
|
+
* on it. "SmtpClientAuthentication is disabled" and "User is authenticated but
|
|
1399
|
+
* not connected" arrive this way, and neither is fixed by re-authenticating, so
|
|
1400
|
+
* dropping the text leaves the account card telling the user to do the one
|
|
1401
|
+
* thing that cannot help.
|
|
1402
|
+
*/
|
|
1403
|
+
const serverResponseText = (error: Error): string => {
|
|
1404
|
+
const text = (error as { responseText?: unknown }).responseText;
|
|
1405
|
+
return typeof text === "string" ? text.trim() : "";
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1394
1408
|
/**
|
|
1395
1409
|
* Classify a raw IMAP error into a MailConnectionError, or return null if
|
|
1396
1410
|
* the error is not recognisable (let it bubble as-is).
|
|
@@ -1416,7 +1430,13 @@ const classifyImapError = (
|
|
|
1416
1430
|
msg.includes("AUTHENTICATE") ||
|
|
1417
1431
|
(error as { authenticationFailed?: boolean }).authenticationFailed === true
|
|
1418
1432
|
) {
|
|
1419
|
-
|
|
1433
|
+
const serverText = serverResponseText(error);
|
|
1434
|
+
return new MailConnectionError(
|
|
1435
|
+
"auth",
|
|
1436
|
+
serverText
|
|
1437
|
+
? `IMAP authentication failed: ${serverText}`
|
|
1438
|
+
: "IMAP authentication failed",
|
|
1439
|
+
);
|
|
1420
1440
|
}
|
|
1421
1441
|
|
|
1422
1442
|
// Network-level failures — NEVER include the original message as it may
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import type { MessageItem } from "@remit/data-ports";
|
|
4
|
+
import { MessageStatus, MessageSyncStatus } from "@remit/domain-enums";
|
|
5
|
+
import { placementBindingOf } from "./placement-settled.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pins `placementBindingOf` directly. It is the guard #1098/#1143 depend on and
|
|
9
|
+
* only end-to-end specs covered it, so a change made for the client's benefit
|
|
10
|
+
* could have moved it without anything going red.
|
|
11
|
+
*
|
|
12
|
+
* Note what these cases say about `syncStatus: failed`: it answers `abandoned`
|
|
13
|
+
* here for a row that may well be mid-retry. That is correct for a guard whose
|
|
14
|
+
* consequence is a temporary refusal, and it is exactly why no user-facing
|
|
15
|
+
* statement may be derived from that field alone.
|
|
16
|
+
*/
|
|
17
|
+
const row = (over: Partial<MessageItem>): MessageItem =>
|
|
18
|
+
({
|
|
19
|
+
messageId: "m-1",
|
|
20
|
+
mailboxId: "mbx-dest",
|
|
21
|
+
uid: 7,
|
|
22
|
+
status: MessageStatus.moving,
|
|
23
|
+
syncStatus: MessageSyncStatus.pending,
|
|
24
|
+
originalMailboxId: "mbx-src",
|
|
25
|
+
originalUid: 7,
|
|
26
|
+
...over,
|
|
27
|
+
}) as unknown as MessageItem;
|
|
28
|
+
|
|
29
|
+
describe("placementBindingOf", () => {
|
|
30
|
+
test("a settled row binds consistently", () => {
|
|
31
|
+
assert.equal(
|
|
32
|
+
placementBindingOf(row({ status: MessageStatus.active })),
|
|
33
|
+
"consistent",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("a row whose uid still names the source is in flight", () => {
|
|
38
|
+
assert.equal(placementBindingOf(row({})), "in_flight");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("`failed` on a foreign-uid row is refused as abandoned", () => {
|
|
42
|
+
assert.equal(
|
|
43
|
+
placementBindingOf(row({ syncStatus: MessageSyncStatus.failed })),
|
|
44
|
+
"abandoned",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a moving row that was never moved binds consistently", () => {
|
|
49
|
+
assert.equal(
|
|
50
|
+
placementBindingOf(
|
|
51
|
+
row({ originalUid: undefined, originalMailboxId: undefined }),
|
|
52
|
+
),
|
|
53
|
+
"consistent",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a moving row whose uid has been repointed binds consistently", () => {
|
|
58
|
+
assert.equal(placementBindingOf(row({ uid: 42 })), "consistent");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("`failed` alone, with no foreign uid, is still consistent", () => {
|
|
62
|
+
assert.equal(
|
|
63
|
+
placementBindingOf(
|
|
64
|
+
row({
|
|
65
|
+
status: MessageStatus.active,
|
|
66
|
+
syncStatus: MessageSyncStatus.failed,
|
|
67
|
+
}),
|
|
68
|
+
),
|
|
69
|
+
"consistent",
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
});
|