nothumanallowed 15.1.43 → 15.1.45

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.43",
3
+ "version": "15.1.45",
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.43';
8
+ export const VERSION = '15.1.45';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -416,6 +416,42 @@ export function register(router) {
416
416
  } catch (e) { sendError(res, 500, e.message); }
417
417
  });
418
418
 
419
+ // POST /api/imap/test — dry-run connection test (no DB writes).
420
+ // Body: { imap_host, imap_port, smtp_host, smtp_port, username, password,
421
+ // email_address, from_name, sendTest? }
422
+ // Returns: { imap: {ok, secure, ...}, smtp: {ok, secure, sent, messageId, ...} }
423
+ // The UI uses this for the "Test connection" / "Send test email" buttons
424
+ // before persisting an account — same UX as Outlook / Thunderbird.
425
+ router.post('/api/imap/test', async (req, res) => {
426
+ try {
427
+ const body = await parseBody(req);
428
+ const creds = {
429
+ imap_host: body.imap_host,
430
+ imap_port: body.imap_port,
431
+ smtp_host: body.smtp_host,
432
+ smtp_port: body.smtp_port,
433
+ username: body.username,
434
+ password: body.password,
435
+ email_address: body.email_address,
436
+ from_name: body.from_name,
437
+ };
438
+ const out = { imap: null, smtp: null };
439
+ try {
440
+ const { testImapConnection } = await import('../../services/email-imap.mjs');
441
+ out.imap = await testImapConnection(creds);
442
+ } catch (e) {
443
+ out.imap = { ok: false, error: e.message };
444
+ }
445
+ try {
446
+ const { testSmtpConnection } = await import('../../services/email-smtp.mjs');
447
+ out.smtp = await testSmtpConnection(creds, { sendTest: !!body.sendTest });
448
+ } catch (e) {
449
+ out.smtp = { ok: false, error: e.message };
450
+ }
451
+ sendJSON(res, 200, out);
452
+ } catch (e) { sendError(res, 500, e.message); }
453
+ });
454
+
419
455
  // POST /api/imap/sync (body: { accountId, force })
420
456
  router.post('/api/imap/sync', async (req, res) => {
421
457
  try {
@@ -57,9 +57,13 @@ 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) {
60
+ function createImapClient(label, creds, accountId, secureOverride) {
61
61
  const port = parseInt(creds.imap_port, 10) || 993;
62
- const isSecure = port === 993 || port === 465;
62
+ // Default heuristic: 993/465 use implicit TLS, anything else uses STARTTLS.
63
+ // `secureOverride` lets the auto-fallback path force the opposite mode.
64
+ const isSecure = typeof secureOverride === 'boolean'
65
+ ? secureOverride
66
+ : (port === 993 || port === 465);
63
67
  const client = new ImapFlow({
64
68
  host: creds.imap_host,
65
69
  port,
@@ -68,6 +72,10 @@ function createImapClient(label, creds, accountId) {
68
72
  logger: false,
69
73
  clientInfo: { name: 'NHA-Mail', version: '1.0.0' },
70
74
  emitLogs: false,
75
+ // Bounded timeouts so a misconfigured server doesn't hang the UI.
76
+ connectionTimeout: 15000,
77
+ greetingTimeout: 10000,
78
+ socketTimeout: 60000,
71
79
  tls: { rejectUnauthorized: false }, // always set — safe for self-signed certs too
72
80
  });
73
81
  client.on('error', (err) => {
@@ -77,19 +85,92 @@ function createImapClient(label, creds, accountId) {
77
85
  return client;
78
86
  }
79
87
 
80
- export async function getImapClient(accountId) {
88
+ // Match the most common "wrong TLS mode" failure modes from ImapFlow / node
89
+ // tls. Errors can surface as cleartext OpenSSL output ("wrong version
90
+ // number"), as ImapFlow strings ("Failed to receive greeting"), or as plain
91
+ // socket errors when the server closed the connection mid-handshake.
92
+ function _looksLikeTlsMismatch(err) {
93
+ const msg = (err && err.message ? err.message : String(err)).toLowerCase();
94
+ 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);
95
+ }
96
+
97
+ // Per-account memory of which TLS mode worked last time. This avoids paying
98
+ // the fallback cost on every sync once we've discovered the correct mode for
99
+ // a given server, and ensures the cached `syncClients` entry stays usable.
100
+ const lastGoodSecure = new Map(); // accountId → boolean
101
+
102
+ export async function getImapClient(accountId, override) {
81
103
  const existing = syncClients.get(accountId);
82
- if (existing?.usable) return existing;
104
+ if (existing?.usable && override === undefined) return existing;
83
105
  if (existing) { syncClients.delete(accountId); try { await existing.logout(); } catch {} }
84
106
 
85
107
  const creds = getAccountCredentials(accountId);
86
108
  if (!creds || !creds.imap_host) throw new Error(`No IMAP credentials for account ${accountId}`);
87
- const client = createImapClient(`sync:${accountId.slice(0, 8)}`, creds, accountId);
88
- await client.connect();
109
+ const label = `sync:${accountId.slice(0, 8)}`;
110
+
111
+ // Pick initial TLS mode: explicit override > remembered-good > port heuristic.
112
+ const port = parseInt(creds.imap_port, 10) || 993;
113
+ 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);
118
+
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);
130
+ await client.connect();
131
+ lastGoodSecure.set(accountId, fallbackSecure);
132
+ } else {
133
+ throw err;
134
+ }
135
+ }
89
136
  syncClients.set(accountId, client);
90
137
  return client;
91
138
  }
92
139
 
140
+ // Dry-run connectivity test used by Settings UI (no DB writes, no persistent
141
+ // client). Returns { ok, secure, message }.
142
+ export async function testImapConnection(creds) {
143
+ if (!creds?.imap_host) throw new Error('imap_host required');
144
+ if (!creds?.username || !creds?.password) throw new Error('Username and password required');
145
+ const port = parseInt(creds.imap_port, 10) || 993;
146
+ const tryConnect = async (secure) => {
147
+ const client = createImapClient('test', creds, null, secure);
148
+ try {
149
+ await client.connect();
150
+ const list = await client.list();
151
+ await client.logout();
152
+ return { ok: true, secure, folderCount: list.length };
153
+ } catch (err) {
154
+ 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}`);
168
+ }
169
+ }
170
+ throw err;
171
+ }
172
+ }
173
+
93
174
  export async function closeImapClient(accountId) {
94
175
  const c = syncClients.get(accountId);
95
176
  if (c) { try { await c.logout(); } catch {}; syncClients.delete(accountId); }
@@ -324,12 +405,25 @@ export async function syncFolder(accountId, folderPath, fullResync, limitMessage
324
405
  }
325
406
  }
326
407
 
327
- // First sync: cap to last 200 messages per folder to avoid blocking for minutes
328
- const FIRST_SYNC_LIMIT = 200;
408
+ // Download EVERY message on the server by default. Previously capped at 200
409
+ // to keep first-sync fast, but that surprised users who expected an Outlook-
410
+ // style "give me my whole mailbox". The sync still streams messages
411
+ // incrementally (headers → bodies) so the UI keeps updating, and never
412
+ // modifies the server — only the local SQLite mirror.
413
+ const FIRST_SYNC_LIMIT = 0;
329
414
 
330
415
  export async function syncAccount(accountId, opts = {}) {
331
416
  setSyncStatus(accountId, 'syncing', null);
417
+ // _retried is set by the recursion below — it forces the *opposite* TLS
418
+ // mode on the second attempt so we never spin twice on the same setting.
419
+ const tlsOverride = (typeof opts._forceSecure === 'boolean') ? opts._forceSecure : undefined;
332
420
  try {
421
+ // Prime the connection with the (optional) explicit TLS mode so any
422
+ // cached `syncClients` entry uses it for the whole sync.
423
+ if (tlsOverride !== undefined) {
424
+ await closeImapClient(accountId);
425
+ await getImapClient(accountId, tlsOverride);
426
+ }
333
427
  const folders = await listImapFolders(accountId);
334
428
  const priority = ['inbox', 'sent'];
335
429
  const toSync = [
@@ -340,17 +434,28 @@ export async function syncAccount(accountId, opts = {}) {
340
434
  let totalSynced = 0;
341
435
  for (const f of toSync) {
342
436
  try {
343
- const limit = opts.full ? 0 : FIRST_SYNC_LIMIT;
437
+ const limit = opts.full === false ? 200 : FIRST_SYNC_LIMIT;
344
438
  const result = await syncFolder(accountId, f.path, false, limit, f.folderType);
345
439
  totalSynced += result.synced;
346
440
  console.log(`[email:sync] ${f.path}: ${result.synced} new messages (total on server: ${result.total})`);
347
441
  } catch (err) {
348
442
  console.warn(`[email:imap] Sync folder ${f.path} failed:`, err.message);
443
+ // Per-folder TLS mismatch propagates up so the outer catch can
444
+ // restart the whole sync with the opposite secure mode.
445
+ if (_looksLikeTlsMismatch(err) && opts._retried !== true) throw err;
349
446
  }
350
447
  }
351
448
  setSyncStatus(accountId, 'idle', null);
352
449
  return { synced: totalSynced };
353
450
  } catch (err) {
451
+ if (_looksLikeTlsMismatch(err) && opts._retried !== true) {
452
+ const port = parseInt(getAccountCredentials(accountId)?.imap_port, 10) || 993;
453
+ const heuristic = port === 993 || port === 465;
454
+ const opposite = (typeof tlsOverride === 'boolean') ? !tlsOverride : !heuristic;
455
+ console.warn(`[email:imap] sync ${accountId.slice(0, 8)} TLS mismatch — retrying entire sync with secure=${opposite}`);
456
+ await closeImapClient(accountId);
457
+ return syncAccount(accountId, { ...opts, _retried: true, _forceSecure: opposite });
458
+ }
354
459
  setSyncStatus(accountId, 'error', err.message);
355
460
  throw err;
356
461
  }
@@ -18,9 +18,9 @@ function threadId(messageId) {
18
18
  return createHash('sha1').update(messageId).digest('hex');
19
19
  }
20
20
 
21
- function getTransporter(creds) {
21
+ function getTransporter(creds, secureOverride) {
22
22
  const port = parseInt(creds.smtp_port, 10) || 587;
23
- const isSecure = port === 465;
23
+ const isSecure = typeof secureOverride === 'boolean' ? secureOverride : (port === 465);
24
24
  return createTransport({
25
25
  host: creds.smtp_host,
26
26
  port,
@@ -33,6 +33,53 @@ function getTransporter(creds) {
33
33
  });
34
34
  }
35
35
 
36
+ // Dry-run SMTP test used by Settings UI. Verifies auth, optionally sends a
37
+ // real "Hello from NHA" message to the configured address. Same TLS-mode
38
+ // auto-fallback pattern as IMAP.
39
+ export async function testSmtpConnection(creds, { sendTest = false } = {}) {
40
+ if (!creds?.smtp_host) throw new Error('smtp_host required');
41
+ if (!creds?.username || !creds?.password) throw new Error('Username and password required');
42
+ const port = parseInt(creds.smtp_port, 10) || 587;
43
+ const firstSecure = port === 465;
44
+ const trySend = async (secure) => {
45
+ const t = getTransporter(creds, secure);
46
+ try {
47
+ await t.verify();
48
+ let sendInfo = null;
49
+ if (sendTest) {
50
+ const to = creds.email_address || creds.username;
51
+ const from = creds.from_name ? `${creds.from_name} <${creds.email_address || creds.username}>` : (creds.email_address || creds.username);
52
+ sendInfo = await t.sendMail({
53
+ from,
54
+ to,
55
+ subject: 'NHA — Email di prova',
56
+ text: `Questo è un messaggio di prova inviato da NotHumanAllowed (${new Date().toISOString()}).\n\nSe lo vedi, la configurazione SMTP è corretta.`,
57
+ html: `<p>Questo è un messaggio di prova inviato da <strong>NotHumanAllowed</strong> (${new Date().toISOString()}).</p><p>Se lo vedi, la configurazione SMTP è corretta.</p>`,
58
+ });
59
+ }
60
+ try { t.close(); } catch {}
61
+ return { ok: true, secure, sent: !!sendInfo, messageId: sendInfo?.messageId || null };
62
+ } catch (err) {
63
+ try { t.close(); } catch {}
64
+ throw err;
65
+ }
66
+ };
67
+ try {
68
+ return await trySend(firstSecure);
69
+ } catch (err) {
70
+ const looksTls = /greeting|tls|ssl|wrong version number|protocol|handshake/i.test(err.message || '');
71
+ if (looksTls) {
72
+ try {
73
+ const r = await trySend(!firstSecure);
74
+ return { ...r, message: `Fallback TLS=${!firstSecure} (heuristic was wrong for this server)` };
75
+ } catch (err2) {
76
+ throw new Error(`SMTP test failed (both TLS modes): ${err.message} / ${err2.message}`);
77
+ }
78
+ }
79
+ throw err;
80
+ }
81
+ }
82
+
36
83
  /**
37
84
  * Send an email.
38
85
  *