nothumanallowed 15.1.43 → 15.1.44

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.44",
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.44';
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,6 +85,12 @@ function createImapClient(label, creds, accountId) {
77
85
  return client;
78
86
  }
79
87
 
88
+ // Match the most common "wrong TLS mode" failure modes from ImapFlow.
89
+ function _looksLikeTlsMismatch(err) {
90
+ const msg = (err && err.message ? err.message : String(err)).toLowerCase();
91
+ return /greeting|tls|ssl|wrong version number|protocol|enotconn|econnreset|handshake/.test(msg);
92
+ }
93
+
80
94
  export async function getImapClient(accountId) {
81
95
  const existing = syncClients.get(accountId);
82
96
  if (existing?.usable) return existing;
@@ -84,12 +98,66 @@ export async function getImapClient(accountId) {
84
98
 
85
99
  const creds = getAccountCredentials(accountId);
86
100
  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();
101
+ const label = `sync:${accountId.slice(0, 8)}`;
102
+
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);
118
+ await client.connect();
119
+ } else {
120
+ throw err;
121
+ }
122
+ }
89
123
  syncClients.set(accountId, client);
90
124
  return client;
91
125
  }
92
126
 
127
+ // Dry-run connectivity test used by Settings UI (no DB writes, no persistent
128
+ // client). Returns { ok, secure, message }.
129
+ export async function testImapConnection(creds) {
130
+ if (!creds?.imap_host) throw new Error('imap_host required');
131
+ if (!creds?.username || !creds?.password) throw new Error('Username and password required');
132
+ const port = parseInt(creds.imap_port, 10) || 993;
133
+ const tryConnect = async (secure) => {
134
+ const client = createImapClient('test', creds, null, secure);
135
+ try {
136
+ await client.connect();
137
+ const list = await client.list();
138
+ await client.logout();
139
+ return { ok: true, secure, folderCount: list.length };
140
+ } catch (err) {
141
+ 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}`);
155
+ }
156
+ }
157
+ throw err;
158
+ }
159
+ }
160
+
93
161
  export async function closeImapClient(accountId) {
94
162
  const c = syncClients.get(accountId);
95
163
  if (c) { try { await c.logout(); } catch {}; syncClients.delete(accountId); }
@@ -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
  *