nothumanallowed 15.1.44 → 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.44",
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.44';
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);
@@ -85,77 +95,129 @@ function createImapClient(label, creds, accountId, secureOverride) {
85
95
  return client;
86
96
  }
87
97
 
88
- // Match the most common "wrong TLS mode" failure modes from ImapFlow.
98
+ // Match the most common "wrong TLS mode" failure modes from ImapFlow / node
99
+ // tls. Errors can surface as cleartext OpenSSL output ("wrong version
100
+ // number"), as ImapFlow strings ("Failed to receive greeting"), or as plain
101
+ // socket errors when the server closed the connection mid-handshake.
89
102
  function _looksLikeTlsMismatch(err) {
90
103
  const msg = (err && err.message ? err.message : String(err)).toLowerCase();
91
- return /greeting|tls|ssl|wrong version number|protocol|enotconn|econnreset|handshake/.test(msg);
104
+ return /(greeting|tls|ssl|wrong\s*version|version\s*number|protocol|enotconn|econnreset|epipe|handshake|record_header|tlsany|alert|cert|disconnected|connection\s*closed)/i.test(msg);
92
105
  }
93
106
 
94
- export async function getImapClient(accountId) {
107
+ // Per-account memory of which TLS mode worked last time. This avoids paying
108
+ // the fallback cost on every sync once we've discovered the correct mode for
109
+ // a given server, and ensures the cached `syncClients` entry stays usable.
110
+ const lastGoodSecure = new Map(); // accountId → boolean
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
+
116
+ export async function getImapClient(accountId, override) {
95
117
  const existing = syncClients.get(accountId);
96
- if (existing?.usable) return existing;
118
+ if (existing?.usable && override === undefined) return existing;
97
119
  if (existing) { syncClients.delete(accountId); try { await existing.logout(); } catch {} }
98
120
 
99
121
  const creds = getAccountCredentials(accountId);
100
122
  if (!creds || !creds.imap_host) throw new Error(`No IMAP credentials for account ${accountId}`);
101
123
  const label = `sync:${accountId.slice(0, 8)}`;
102
124
 
103
- // First attempt with the heuristic-derived TLS mode.
104
- let client = createImapClient(label, creds, accountId);
105
- try {
106
- await client.connect();
107
- } catch (err) {
108
- // Auto-fallback: if the failure smells like a TLS-mode mismatch, retry
109
- // with the opposite mode before bubbling the error up to the user. The
110
- // most common case is port 993 misclassified (or a non-standard port
111
- // with implicit TLS) — the second try usually succeeds.
112
- if (_looksLikeTlsMismatch(err)) {
113
- try { await client.logout(); } catch {}
114
- const port = parseInt(creds.imap_port, 10) || 993;
115
- const fallbackSecure = !(port === 993 || port === 465);
116
- console.warn(`[email:imap] ${label} TLS mismatch (${err.message}). Retrying with secure=${fallbackSecure}`);
117
- client = createImapClient(label, creds, accountId, fallbackSecure);
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.
131
+ const port = parseInt(creds.imap_port, 10) || 993;
132
+ const heuristicSecure = port === 993 || port === 465;
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
+ }
156
+
157
+ const errors = [];
158
+ for (const s of strategies) {
159
+ const client = createImapClient(label, creds, accountId, s.secure, { legacy: s.legacy });
160
+ try {
118
161
  await client.connect();
119
- } else {
120
- 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
+ }
121
176
  }
122
177
  }
123
- syncClients.set(accountId, client);
124
- return client;
178
+ throw new Error(`IMAP connection failed after ${strategies.length} attempts:\n${errors.join('\n')}`);
125
179
  }
126
180
 
127
181
  // Dry-run connectivity test used by Settings UI (no DB writes, no persistent
128
- // 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.
129
185
  export async function testImapConnection(creds) {
130
186
  if (!creds?.imap_host) throw new Error('imap_host required');
131
187
  if (!creds?.username || !creds?.password) throw new Error('Username and password required');
132
188
  const port = parseInt(creds.imap_port, 10) || 993;
133
- const tryConnect = async (secure) => {
134
- 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 });
135
199
  try {
136
200
  await client.connect();
137
201
  const list = await client.list();
138
- await client.logout();
139
- 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
+ };
140
211
  } catch (err) {
212
+ errors.push(`• ${s.why}: ${err.message.slice(0, 160)}`);
141
213
  try { await client.logout(); } catch {}
142
- throw err;
143
- }
144
- };
145
- const firstSecure = port === 993 || port === 465;
146
- try {
147
- return await tryConnect(firstSecure);
148
- } catch (err) {
149
- if (_looksLikeTlsMismatch(err)) {
150
- try {
151
- const r = await tryConnect(!firstSecure);
152
- return { ...r, message: `Fallback TLS=${!firstSecure} (heuristic was wrong for this server)` };
153
- } catch (err2) {
154
- 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;
155
217
  }
156
218
  }
157
- throw err;
158
219
  }
220
+ throw new Error(`IMAP test failed dopo ${strategies.length} tentativi:\n${errors.join('\n')}`);
159
221
  }
160
222
 
161
223
  export async function closeImapClient(accountId) {
@@ -392,12 +454,25 @@ export async function syncFolder(accountId, folderPath, fullResync, limitMessage
392
454
  }
393
455
  }
394
456
 
395
- // First sync: cap to last 200 messages per folder to avoid blocking for minutes
396
- const FIRST_SYNC_LIMIT = 200;
457
+ // Download EVERY message on the server by default. Previously capped at 200
458
+ // to keep first-sync fast, but that surprised users who expected an Outlook-
459
+ // style "give me my whole mailbox". The sync still streams messages
460
+ // incrementally (headers → bodies) so the UI keeps updating, and never
461
+ // modifies the server — only the local SQLite mirror.
462
+ const FIRST_SYNC_LIMIT = 0;
397
463
 
398
464
  export async function syncAccount(accountId, opts = {}) {
399
465
  setSyncStatus(accountId, 'syncing', null);
466
+ // _retried is set by the recursion below — it forces the *opposite* TLS
467
+ // mode on the second attempt so we never spin twice on the same setting.
468
+ const tlsOverride = (typeof opts._forceSecure === 'boolean') ? opts._forceSecure : undefined;
400
469
  try {
470
+ // Prime the connection with the (optional) explicit TLS mode so any
471
+ // cached `syncClients` entry uses it for the whole sync.
472
+ if (tlsOverride !== undefined) {
473
+ await closeImapClient(accountId);
474
+ await getImapClient(accountId, tlsOverride);
475
+ }
401
476
  const folders = await listImapFolders(accountId);
402
477
  const priority = ['inbox', 'sent'];
403
478
  const toSync = [
@@ -408,17 +483,28 @@ export async function syncAccount(accountId, opts = {}) {
408
483
  let totalSynced = 0;
409
484
  for (const f of toSync) {
410
485
  try {
411
- const limit = opts.full ? 0 : FIRST_SYNC_LIMIT;
486
+ const limit = opts.full === false ? 200 : FIRST_SYNC_LIMIT;
412
487
  const result = await syncFolder(accountId, f.path, false, limit, f.folderType);
413
488
  totalSynced += result.synced;
414
489
  console.log(`[email:sync] ${f.path}: ${result.synced} new messages (total on server: ${result.total})`);
415
490
  } catch (err) {
416
491
  console.warn(`[email:imap] Sync folder ${f.path} failed:`, err.message);
492
+ // Per-folder TLS mismatch propagates up so the outer catch can
493
+ // restart the whole sync with the opposite secure mode.
494
+ if (_looksLikeTlsMismatch(err) && opts._retried !== true) throw err;
417
495
  }
418
496
  }
419
497
  setSyncStatus(accountId, 'idle', null);
420
498
  return { synced: totalSynced };
421
499
  } catch (err) {
500
+ if (_looksLikeTlsMismatch(err) && opts._retried !== true) {
501
+ const port = parseInt(getAccountCredentials(accountId)?.imap_port, 10) || 993;
502
+ const heuristic = port === 993 || port === 465;
503
+ const opposite = (typeof tlsOverride === 'boolean') ? !tlsOverride : !heuristic;
504
+ console.warn(`[email:imap] sync ${accountId.slice(0, 8)} TLS mismatch — retrying entire sync with secure=${opposite}`);
505
+ await closeImapClient(accountId);
506
+ return syncAccount(accountId, { ...opts, _retried: true, _forceSecure: opposite });
507
+ }
422
508
  setSyncStatus(accountId, 'error', err.message);
423
509
  throw err;
424
510
  }