@remit/mailbox-service 0.0.115 → 0.0.116
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/connection-test.d.ts +3 -34
- package/dist/connection-test.js +14 -147
- package/dist/imapflow-connection.d.ts +5 -0
- package/dist/imapflow-connection.js +24 -22
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +1 -3
- package/package.json +1 -3
|
@@ -1,41 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Used to validate credentials before saving account configuration.
|
|
5
|
-
*/
|
|
6
|
-
import type { MailCredentials } from "./types.js";
|
|
7
|
-
export interface TestResult {
|
|
8
|
-
success: boolean;
|
|
9
|
-
error?: string;
|
|
10
|
-
}
|
|
1
|
+
import type { TestResult } from "@remit/data-ports";
|
|
2
|
+
import type { ImapTransport, MailCredentials } from "./types.js";
|
|
11
3
|
export interface ImapTestConfig {
|
|
12
|
-
host: string;
|
|
13
|
-
port: number;
|
|
14
|
-
secure: boolean;
|
|
15
|
-
startTls?: boolean;
|
|
16
|
-
credentials: MailCredentials;
|
|
17
|
-
user: string;
|
|
18
|
-
}
|
|
19
|
-
export interface SmtpTestConfig {
|
|
20
4
|
host: string;
|
|
21
5
|
port: number;
|
|
22
6
|
secure: boolean;
|
|
23
7
|
credentials: MailCredentials;
|
|
24
8
|
user: string;
|
|
25
9
|
}
|
|
26
|
-
|
|
27
|
-
* Test IMAP connection with provided credentials.
|
|
28
|
-
*
|
|
29
|
-
* Uses a raw socket to send AUTH PLAIN directly — bypasses the CAPABILITY
|
|
30
|
-
* round-trip that IMAP clients typically perform first, avoiding deadlocks
|
|
31
|
-
* with servers that pipeline their tagged responses.
|
|
32
|
-
*
|
|
33
|
-
* Supports plaintext, direct TLS (secure: true), and STARTTLS (startTls: true).
|
|
34
|
-
*/
|
|
35
|
-
export declare const testImapConnection: (config: ImapTestConfig) => Promise<TestResult>;
|
|
36
|
-
/**
|
|
37
|
-
* Test SMTP connection with provided credentials
|
|
38
|
-
*
|
|
39
|
-
* Uses nodemailer's verify() method to test authentication.
|
|
40
|
-
*/
|
|
41
|
-
export declare const testSmtpConnection: (config: SmtpTestConfig) => Promise<TestResult>;
|
|
10
|
+
export declare const testImapConnection: (config: ImapTestConfig, transport: ImapTransport) => Promise<TestResult>;
|
package/dist/connection-test.js
CHANGED
|
@@ -1,152 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
* Connection testing utilities for IMAP and SMTP servers
|
|
3
|
-
*
|
|
4
|
-
* Used to validate credentials before saving account configuration.
|
|
5
|
-
*/
|
|
6
|
-
import { createConnection as createNetConnection } from "node:net";
|
|
7
|
-
import { connect as tlsConnect } from "node:tls";
|
|
8
|
-
import { createTransport } from "nodemailer";
|
|
9
|
-
import { MailConnectionError } from "./types.js";
|
|
10
|
-
// TODO(#468): Wire OAuth testConnection once authType/oauthRefreshTokenHash are
|
|
11
|
-
// available on Account. For OAuth accounts, mint a token via MailOAuthService
|
|
12
|
-
// before calling testImapConnection / testSmtpConnection, and map
|
|
13
|
-
// RefreshTokenError(reauth-required) to MailConnectionError("auth", ...).
|
|
1
|
+
import { ImapFlowConnection } from "./imapflow-connection.js";
|
|
14
2
|
const TEST_TIMEOUT_MS = 12_000;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
const buildSmtpAuth = (user, credentials) => {
|
|
19
|
-
if (credentials.kind === "password") {
|
|
20
|
-
return { user, pass: credentials.password };
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
type: "OAuth2",
|
|
24
|
-
user,
|
|
25
|
-
accessToken: credentials.accessToken,
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Test IMAP connection with provided credentials.
|
|
30
|
-
*
|
|
31
|
-
* Uses a raw socket to send AUTH PLAIN directly — bypasses the CAPABILITY
|
|
32
|
-
* round-trip that IMAP clients typically perform first, avoiding deadlocks
|
|
33
|
-
* with servers that pipeline their tagged responses.
|
|
34
|
-
*
|
|
35
|
-
* Supports plaintext, direct TLS (secure: true), and STARTTLS (startTls: true).
|
|
36
|
-
*/
|
|
37
|
-
export const testImapConnection = async (config) => {
|
|
38
|
-
if (config.credentials.kind !== "password") {
|
|
39
|
-
return { success: false, error: "OAuth not supported for connection test" };
|
|
40
|
-
}
|
|
41
|
-
const { user, credentials } = config;
|
|
42
|
-
const authPlain = Buffer.from(`\x00${user}\x00${credentials.password}`).toString("base64");
|
|
43
|
-
return new Promise((resolve) => {
|
|
44
|
-
let settled = false;
|
|
45
|
-
const done = (result) => {
|
|
46
|
-
if (settled)
|
|
47
|
-
return;
|
|
48
|
-
settled = true;
|
|
49
|
-
clearTimeout(timeoutId);
|
|
50
|
-
resolve(result);
|
|
51
|
-
};
|
|
52
|
-
const timeoutId = setTimeout(() => done({ success: false, error: "Connection timed out" }), TEST_TIMEOUT_MS);
|
|
53
|
-
let buf = "";
|
|
54
|
-
let phase = "greeting";
|
|
55
|
-
// biome-ignore lint/suspicious/noExplicitAny: socket type varies between net.Socket and tls.TLSSocket
|
|
56
|
-
let sock;
|
|
57
|
-
const onData = (chunk) => {
|
|
58
|
-
buf += chunk.toString();
|
|
59
|
-
const lines = buf.split("\r\n");
|
|
60
|
-
buf = lines.pop() ?? "";
|
|
61
|
-
for (const line of lines) {
|
|
62
|
-
if (phase === "greeting" && /^\* OK/i.test(line)) {
|
|
63
|
-
if (config.startTls) {
|
|
64
|
-
phase = "starttls";
|
|
65
|
-
sock.write("A001 STARTTLS\r\n");
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
phase = "auth";
|
|
69
|
-
sock.write(`A001 AUTHENTICATE PLAIN ${authPlain}\r\n`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
else if (phase === "starttls" && /^A001 OK/i.test(line)) {
|
|
73
|
-
phase = "tls-upgrade";
|
|
74
|
-
// Upgrade socket to TLS in place
|
|
75
|
-
const tlsSock = tlsConnect({
|
|
76
|
-
socket: sock,
|
|
77
|
-
host: config.host,
|
|
78
|
-
rejectUnauthorized: false,
|
|
79
|
-
});
|
|
80
|
-
tlsSock.on("data", onData);
|
|
81
|
-
tlsSock.on("error", onError);
|
|
82
|
-
tlsSock.once("secureConnect", () => {
|
|
83
|
-
phase = "auth";
|
|
84
|
-
sock = tlsSock;
|
|
85
|
-
sock.write(`A002 AUTHENTICATE PLAIN ${authPlain}\r\n`);
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
else if (phase === "auth" &&
|
|
89
|
-
(/^A00[12] OK/i.test(line) || /^A001 OK/i.test(line))) {
|
|
90
|
-
phase = "done";
|
|
91
|
-
sock.write("A099 LOGOUT\r\n");
|
|
92
|
-
done({ success: true });
|
|
93
|
-
}
|
|
94
|
-
else if (phase === "auth" &&
|
|
95
|
-
(/^A00[12] NO/i.test(line) || /^A00[12] BAD/i.test(line))) {
|
|
96
|
-
phase = "done";
|
|
97
|
-
sock.destroy();
|
|
98
|
-
done({ success: false, error: "Authentication failed" });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
const onError = (err) => {
|
|
103
|
-
done({ success: false, error: err.message });
|
|
104
|
-
};
|
|
105
|
-
if (config.secure) {
|
|
106
|
-
sock = tlsConnect({
|
|
107
|
-
host: config.host,
|
|
108
|
-
port: config.port,
|
|
109
|
-
rejectUnauthorized: false,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
sock = createNetConnection({ host: config.host, port: config.port });
|
|
114
|
-
}
|
|
115
|
-
sock.on("data", onData);
|
|
116
|
-
sock.on("error", onError);
|
|
117
|
-
});
|
|
118
|
-
};
|
|
119
|
-
/**
|
|
120
|
-
* Test SMTP connection with provided credentials
|
|
121
|
-
*
|
|
122
|
-
* Uses nodemailer's verify() method to test authentication.
|
|
123
|
-
*/
|
|
124
|
-
export const testSmtpConnection = async (config) => {
|
|
125
|
-
const transport = createTransport({
|
|
3
|
+
export const testImapConnection = async (config, transport) => {
|
|
4
|
+
const connection = new ImapFlowConnection({
|
|
126
5
|
host: config.host,
|
|
127
6
|
port: config.port,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
7
|
+
tls: config.secure,
|
|
8
|
+
user: config.user,
|
|
9
|
+
credentials: config.credentials,
|
|
10
|
+
transport,
|
|
11
|
+
connTimeout: TEST_TIMEOUT_MS,
|
|
133
12
|
});
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
return {
|
|
142
|
-
success: false,
|
|
143
|
-
error: error instanceof Error ? error.message : "Connection failed",
|
|
144
|
-
};
|
|
145
|
-
})
|
|
146
|
-
.finally(() => transport.close());
|
|
147
|
-
const timeout = new Promise((resolve) => setTimeout(() => {
|
|
148
|
-
transport.close();
|
|
149
|
-
resolve({ success: false, error: "Connection timed out" });
|
|
150
|
-
}, TEST_TIMEOUT_MS));
|
|
151
|
-
return Promise.race([attempt, timeout]);
|
|
13
|
+
const result = await connection.connectOnce().then(() => ({ success: true }), (error) => ({
|
|
14
|
+
success: false,
|
|
15
|
+
error: error instanceof Error ? error.message : "Connection failed",
|
|
16
|
+
}));
|
|
17
|
+
await connection.disconnect();
|
|
18
|
+
return result;
|
|
152
19
|
};
|
|
@@ -79,6 +79,11 @@ export declare class ImapFlowConnection {
|
|
|
79
79
|
* Authentication errors are not retried.
|
|
80
80
|
*/
|
|
81
81
|
connect: () => Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Connect once, without retrying. A failure is classified into a
|
|
84
|
+
* MailConnectionError where possible and leaves the connection closed.
|
|
85
|
+
*/
|
|
86
|
+
connectOnce: () => Promise<void>;
|
|
82
87
|
/**
|
|
83
88
|
* Single connection attempt
|
|
84
89
|
*/
|
|
@@ -172,36 +172,36 @@ export class ImapFlowConnection {
|
|
|
172
172
|
* Authentication errors are not retried.
|
|
173
173
|
*/
|
|
174
174
|
connect = async () => {
|
|
175
|
-
if (this.client) {
|
|
176
|
-
throw new Error("Already connected");
|
|
177
|
-
}
|
|
178
175
|
const maxRetries = 3;
|
|
179
176
|
const baseDelayMs = 1000;
|
|
180
177
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
const failure = await this.connectOnce().then(() => null, (error) => error);
|
|
179
|
+
if (failure === null) {
|
|
183
180
|
return;
|
|
184
181
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
// Final attempt exhausted: throw the classified error if we have
|
|
192
|
-
// one (network failures), otherwise let the raw error bubble.
|
|
193
|
-
if (attempt === maxRetries) {
|
|
194
|
-
throw classified ?? error;
|
|
195
|
-
}
|
|
196
|
-
// Network and unknown errors fall through to retry-with-backoff.
|
|
197
|
-
// Exponential backoff: 1s, 2s, 4s
|
|
198
|
-
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
199
|
-
await this.sleep(delay);
|
|
200
|
-
// Reset state for retry
|
|
201
|
-
this.cleanup();
|
|
182
|
+
if (failure instanceof MailConnectionError && failure.kind === "auth") {
|
|
183
|
+
throw failure;
|
|
184
|
+
}
|
|
185
|
+
if (attempt === maxRetries) {
|
|
186
|
+
throw failure;
|
|
202
187
|
}
|
|
188
|
+
await this.sleep(baseDelayMs * 2 ** (attempt - 1));
|
|
203
189
|
}
|
|
204
190
|
};
|
|
191
|
+
/**
|
|
192
|
+
* Connect once, without retrying. A failure is classified into a
|
|
193
|
+
* MailConnectionError where possible and leaves the connection closed.
|
|
194
|
+
*/
|
|
195
|
+
connectOnce = async () => {
|
|
196
|
+
if (this.client) {
|
|
197
|
+
throw new Error("Already connected");
|
|
198
|
+
}
|
|
199
|
+
await this.attemptConnect().catch((error) => {
|
|
200
|
+
this.cleanup();
|
|
201
|
+
throw (classifyImapError(error, `${this.config.host}:${this.config.port}`) ??
|
|
202
|
+
error);
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
205
|
/**
|
|
206
206
|
* Single connection attempt
|
|
207
207
|
*/
|
|
@@ -225,6 +225,8 @@ export class ImapFlowConnection {
|
|
|
225
225
|
port: this.config.port,
|
|
226
226
|
secure: this.config.tls,
|
|
227
227
|
transport: this.config.transport,
|
|
228
|
+
connectionTimeout: this.config.connTimeout,
|
|
229
|
+
greetingTimeout: this.config.connTimeout,
|
|
228
230
|
servername: this.config.tlsOptions?.servername,
|
|
229
231
|
auth,
|
|
230
232
|
tls: tlsOptions,
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { isMessageBodySyncBroken, type ResolveExhaustedBodySyncDeps, type Resolv
|
|
|
12
12
|
export { backfillClassifications, type ClassificationBackfillCheckpoint, type ClassificationBackfillCheckpointStore, type ClassificationBackfillDeps, type ClassificationBackfillLogger, type ClassificationBackfillOptions, type ClassificationBackfillProgress, type ClassificationBackfillResult, type ClassificationBackfillTotals, } from "./classification-backfill.js";
|
|
13
13
|
export { classifyParsedMessage, extractPrimaryFromEmail, } from "./classify-message.js";
|
|
14
14
|
export { type ConnectionScope, connectionScope, createConnection, createConnectionWithCredentials, } from "./connection-factory.js";
|
|
15
|
-
export { type ImapTestConfig,
|
|
15
|
+
export { type ImapTestConfig, testImapConnection, } from "./connection-test.js";
|
|
16
16
|
export { type CreatedFilter, createFilter, type FilterPage, type FilterPatch, listFilters, type NewFilter, resolveFilterUpdate, updateFilter, } from "./filter-operations.js";
|
|
17
17
|
export { extractListId, normalizeListId } from "./filters/list-id.js";
|
|
18
18
|
export { clauseMatches, type FilterMessage, type LiteralClauseNarrowing, literalClausesMatch, literalClauseTerms, NO_ACTION, selectMoveWinner, } from "./filters/match.js";
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { classifyParsedMessage, extractPrimaryFromEmail, } from "./classify-mess
|
|
|
14
14
|
// Connection factory
|
|
15
15
|
export { connectionScope, createConnection, createConnectionWithCredentials, } from "./connection-factory.js";
|
|
16
16
|
// Connection testing
|
|
17
|
-
export { testImapConnection,
|
|
17
|
+
export { testImapConnection, } from "./connection-test.js";
|
|
18
18
|
export { createFilter, listFilters, resolveFilterUpdate, updateFilter, } from "./filter-operations.js";
|
|
19
19
|
export { extractListId, normalizeListId } from "./filters/list-id.js";
|
|
20
20
|
export { clauseMatches, literalClausesMatch, literalClauseTerms, NO_ACTION, selectMoveWinner, } from "./filters/match.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -47,10 +47,8 @@ export interface ImapConnectionConfig {
|
|
|
47
47
|
rejectUnauthorized?: boolean;
|
|
48
48
|
servername?: string;
|
|
49
49
|
};
|
|
50
|
-
/** Connection timeout in milliseconds */
|
|
50
|
+
/** Connection and greeting timeout in milliseconds */
|
|
51
51
|
connTimeout?: number;
|
|
52
|
-
/** Authentication timeout in milliseconds */
|
|
53
|
-
authTimeout?: number;
|
|
54
52
|
}
|
|
55
53
|
/**
|
|
56
54
|
* IMAP connection state
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/mailbox-service",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.116",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -61,7 +61,6 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"imapflow": "^2.9.1",
|
|
63
63
|
"mailparser": "3.12.2",
|
|
64
|
-
"nodemailer": "^10.0.6",
|
|
65
64
|
"p-map": "^7.0.4",
|
|
66
65
|
"tldts": "^7.4.9",
|
|
67
66
|
"@remit/data-ports": "*",
|
|
@@ -73,7 +72,6 @@
|
|
|
73
72
|
"natural": "^8.1.1",
|
|
74
73
|
"stopword": "^3.1.5",
|
|
75
74
|
"@types/mailparser": "^3.4.6",
|
|
76
|
-
"@types/nodemailer": "^8.0.1",
|
|
77
75
|
"@types/stopword": "^2.0.3",
|
|
78
76
|
"@remit/calendar-service": "*"
|
|
79
77
|
},
|