nothumanallowed 15.1.45 → 15.1.46

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": "nothumanallowed",
3
- "version": "15.1.45",
3
+ "version": "15.1.46",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '15.1.45';
8
+ export const VERSION = '15.1.46';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -57,13 +57,21 @@ function notifyIdle(accountId, folder) {
57
57
  for (const h of idleHandlers) { try { h(accountId, folder); } catch {} }
58
58
  }
59
59
 
60
- function createImapClient(label, creds, accountId, secureOverride) {
60
+ function createImapClient(label, creds, accountId, secureOverride, opts = {}) {
61
61
  const port = parseInt(creds.imap_port, 10) || 993;
62
62
  // Default heuristic: 993/465 use implicit TLS, anything else uses STARTTLS.
63
63
  // `secureOverride` lets the auto-fallback path force the opposite mode.
64
64
  const isSecure = typeof secureOverride === 'boolean'
65
65
  ? secureOverride
66
66
  : (port === 993 || port === 465);
67
+ // TLS hardening configurable per attempt. The `legacy` flag drops the
68
+ // minimum TLS version to v1.0 — required by some old / self-hosted IMAP
69
+ // servers (mail.dimensione-server.it, postfix on legacy CentOS, etc.)
70
+ // that still refuse TLS 1.2+ ClientHello.
71
+ const tlsOpts = {
72
+ rejectUnauthorized: false,
73
+ ...(opts.legacy ? { minVersion: 'TLSv1', maxVersion: 'TLSv1.3' } : {}),
74
+ };
67
75
  const client = new ImapFlow({
68
76
  host: creds.imap_host,
69
77
  port,
@@ -72,11 +80,13 @@ function createImapClient(label, creds, accountId, secureOverride) {
72
80
  logger: false,
73
81
  clientInfo: { name: 'NHA-Mail', version: '1.0.0' },
74
82
  emitLogs: false,
75
- // Bounded timeouts so a misconfigured server doesn't hang the UI.
76
- connectionTimeout: 15000,
77
- greetingTimeout: 10000,
78
- socketTimeout: 60000,
79
- tls: { rejectUnauthorized: false }, // always set — safe for self-signed certs too
83
+ // Generous timeouts first sync from a slow server can take a while
84
+ // before it even responds with the greeting. The previous 10s was too
85
+ // tight for ISP-hosted mailservers with throttling on cold connections.
86
+ connectionTimeout: 30000,
87
+ greetingTimeout: 30000,
88
+ socketTimeout: 120000,
89
+ tls: tlsOpts,
80
90
  });
81
91
  client.on('error', (err) => {
82
92
  console.error(`[email:imap] ${label} error:`, err.message);
@@ -99,6 +109,10 @@ function _looksLikeTlsMismatch(err) {
99
109
  // a given server, and ensures the cached `syncClients` entry stays usable.
100
110
  const lastGoodSecure = new Map(); // accountId → boolean
101
111
 
112
+ // Per-account memory of the FULL working profile, not just secure flag.
113
+ // Some servers need legacy TLS — we don't want to re-discover that every sync.
114
+ const lastGoodProfile = new Map(); // accountId → { secure, legacy }
115
+
102
116
  export async function getImapClient(accountId, override) {
103
117
  const existing = syncClients.get(accountId);
104
118
  if (existing?.usable && override === undefined) return existing;
@@ -108,67 +122,102 @@ export async function getImapClient(accountId, override) {
108
122
  if (!creds || !creds.imap_host) throw new Error(`No IMAP credentials for account ${accountId}`);
109
123
  const label = `sync:${accountId.slice(0, 8)}`;
110
124
 
111
- // Pick initial TLS mode: explicit override > remembered-good > port heuristic.
125
+ // Build the candidate list up to 4 strategies, ordered most→least likely:
126
+ // 1. heuristic secure + modern TLS
127
+ // 2. opposite secure + modern TLS
128
+ // 3. heuristic secure + legacy TLS (TLSv1.0+)
129
+ // 4. opposite secure + legacy TLS
130
+ // If a remembered-good profile exists, prepend it so warm syncs are 1 attempt.
112
131
  const port = parseInt(creds.imap_port, 10) || 993;
113
132
  const heuristicSecure = port === 993 || port === 465;
114
- const remembered = lastGoodSecure.get(accountId);
115
- const firstSecure = (typeof override === 'boolean')
116
- ? override
117
- : (typeof remembered === 'boolean' ? remembered : heuristicSecure);
133
+ let strategies;
134
+ if (typeof override === 'boolean') {
135
+ // Explicit override (used by the outer syncAccount retry): obey it
136
+ // exactly, but still try legacy TLS as a fallback within that mode.
137
+ strategies = [
138
+ { secure: override, legacy: false, why: 'override-modern' },
139
+ { secure: override, legacy: true, why: 'override-legacy' },
140
+ ];
141
+ } else {
142
+ strategies = [
143
+ { secure: heuristicSecure, legacy: false, why: 'heuristic-modern' },
144
+ { secure: !heuristicSecure, legacy: false, why: 'opposite-modern' },
145
+ { secure: heuristicSecure, legacy: true, why: 'heuristic-legacy' },
146
+ { secure: !heuristicSecure, legacy: true, why: 'opposite-legacy' },
147
+ ];
148
+ const remembered = lastGoodProfile.get(accountId);
149
+ if (remembered) {
150
+ strategies = [
151
+ { secure: remembered.secure, legacy: !!remembered.legacy, why: 'remembered' },
152
+ ...strategies.filter(s => !(s.secure === remembered.secure && s.legacy === !!remembered.legacy)),
153
+ ];
154
+ }
155
+ }
118
156
 
119
- // First attempt.
120
- let client = createImapClient(label, creds, accountId, firstSecure);
121
- try {
122
- await client.connect();
123
- lastGoodSecure.set(accountId, firstSecure);
124
- } catch (err) {
125
- if (_looksLikeTlsMismatch(err) && override === undefined) {
126
- try { await client.logout(); } catch {}
127
- const fallbackSecure = !firstSecure;
128
- console.warn(`[email:imap] ${label} TLS mismatch (${err.message.slice(0, 100)}). Retrying with secure=${fallbackSecure}`);
129
- client = createImapClient(label, creds, accountId, fallbackSecure);
157
+ const errors = [];
158
+ for (const s of strategies) {
159
+ const client = createImapClient(label, creds, accountId, s.secure, { legacy: s.legacy });
160
+ try {
130
161
  await client.connect();
131
- lastGoodSecure.set(accountId, fallbackSecure);
132
- } else {
133
- throw err;
162
+ lastGoodProfile.set(accountId, { secure: s.secure, legacy: s.legacy });
163
+ if (s.why !== 'remembered' && s.why !== 'heuristic-modern') {
164
+ console.warn(`[email:imap] ${label} connected via ${s.why} (secure=${s.secure}, legacy=${s.legacy})`);
165
+ }
166
+ syncClients.set(accountId, client);
167
+ return client;
168
+ } catch (err) {
169
+ errors.push(`${s.why}: ${err.message.slice(0, 120)}`);
170
+ try { await client.logout(); } catch {}
171
+ // If the error is clearly NOT TLS-related (e.g. auth failure, DNS),
172
+ // stop trying — more TLS combos won't help.
173
+ if (!_looksLikeTlsMismatch(err) && !/timeout|hang/i.test(err.message)) {
174
+ throw err;
175
+ }
134
176
  }
135
177
  }
136
- syncClients.set(accountId, client);
137
- return client;
178
+ throw new Error(`IMAP connection failed after ${strategies.length} attempts:\n${errors.join('\n')}`);
138
179
  }
139
180
 
140
181
  // Dry-run connectivity test used by Settings UI (no DB writes, no persistent
141
- // client). Returns { ok, secure, message }.
182
+ // client). Tries up to 4 TLS-mode combinations and returns the first that
183
+ // works, along with which one. Errors include the full attempt log so the
184
+ // user can paste it back to support if everything failed.
142
185
  export async function testImapConnection(creds) {
143
186
  if (!creds?.imap_host) throw new Error('imap_host required');
144
187
  if (!creds?.username || !creds?.password) throw new Error('Username and password required');
145
188
  const port = parseInt(creds.imap_port, 10) || 993;
146
- const tryConnect = async (secure) => {
147
- const client = createImapClient('test', creds, null, secure);
189
+ const heuristicSecure = port === 993 || port === 465;
190
+ const strategies = [
191
+ { secure: heuristicSecure, legacy: false, why: 'heuristic-modern' },
192
+ { secure: !heuristicSecure, legacy: false, why: 'opposite-modern' },
193
+ { secure: heuristicSecure, legacy: true, why: 'heuristic-legacy' },
194
+ { secure: !heuristicSecure, legacy: true, why: 'opposite-legacy' },
195
+ ];
196
+ const errors = [];
197
+ for (const s of strategies) {
198
+ const client = createImapClient('test', creds, null, s.secure, { legacy: s.legacy });
148
199
  try {
149
200
  await client.connect();
150
201
  const list = await client.list();
151
- await client.logout();
152
- return { ok: true, secure, folderCount: list.length };
202
+ try { await client.logout(); } catch {}
203
+ const mode = `${s.secure ? 'implicit TLS' : 'STARTTLS'}${s.legacy ? ' (legacy ≥TLSv1.0)' : ''}`;
204
+ return {
205
+ ok: true,
206
+ secure: s.secure,
207
+ legacy: s.legacy,
208
+ folderCount: list.length,
209
+ message: `Connesso con modalità: ${mode}`,
210
+ };
153
211
  } catch (err) {
212
+ errors.push(`• ${s.why}: ${err.message.slice(0, 160)}`);
154
213
  try { await client.logout(); } catch {}
155
- throw err;
156
- }
157
- };
158
- const firstSecure = port === 993 || port === 465;
159
- try {
160
- return await tryConnect(firstSecure);
161
- } catch (err) {
162
- if (_looksLikeTlsMismatch(err)) {
163
- try {
164
- const r = await tryConnect(!firstSecure);
165
- return { ...r, message: `Fallback TLS=${!firstSecure} (heuristic was wrong for this server)` };
166
- } catch (err2) {
167
- throw new Error(`IMAP test failed (both TLS modes): ${err.message} / ${err2.message}`);
214
+ if (!_looksLikeTlsMismatch(err) && !/timeout|hang/i.test(err.message)) {
215
+ // Hard error (auth / DNS): bail with that exact message.
216
+ throw err;
168
217
  }
169
218
  }
170
- throw err;
171
219
  }
220
+ throw new Error(`IMAP test failed dopo ${strategies.length} tentativi:\n${errors.join('\n')}`);
172
221
  }
173
222
 
174
223
  export async function closeImapClient(accountId) {