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 +1 -1
- package/src/constants.mjs +1 -1
- package/src/services/email-imap.mjs +137 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "15.1.
|
|
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.
|
|
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
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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).
|
|
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
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
396
|
-
|
|
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 ?
|
|
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
|
}
|