@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.
@@ -1,41 +1,10 @@
1
- /**
2
- * Connection testing utilities for IMAP and SMTP servers
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>;
@@ -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
- * Build nodemailer auth object from credentials.
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
- secure: config.secure,
129
- auth: buildSmtpAuth(config.user, config.credentials),
130
- connectionTimeout: TEST_TIMEOUT_MS,
131
- greetingTimeout: TEST_TIMEOUT_MS,
132
- socketTimeout: TEST_TIMEOUT_MS,
7
+ tls: config.secure,
8
+ user: config.user,
9
+ credentials: config.credentials,
10
+ transport,
11
+ connTimeout: TEST_TIMEOUT_MS,
133
12
  });
134
- const attempt = transport
135
- .verify()
136
- .then(() => ({ success: true }))
137
- .catch((error) => {
138
- if (error instanceof MailConnectionError) {
139
- return { success: false, error: error.message };
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
- try {
182
- await this.attemptConnect();
178
+ const failure = await this.connectOnce().then(() => null, (error) => error);
179
+ if (failure === null) {
183
180
  return;
184
181
  }
185
- catch (error) {
186
- const classified = classifyImapError(error, `${this.config.host}:${this.config.port}`);
187
- // Auth errors won't fix themselves on retry — throw immediately.
188
- if (classified?.kind === "auth") {
189
- throw classified;
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, type SmtpTestConfig, type TestResult, testImapConnection, testSmtpConnection, } from "./connection-test.js";
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, testSmtpConnection, } from "./connection-test.js";
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.115",
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
  },