nothumanallowed 15.1.44 → 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.44",
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.44';
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
 
@@ -85,37 +85,50 @@ function createImapClient(label, creds, accountId, secureOverride) {
85
85
  return client;
86
86
  }
87
87
 
88
- // Match the most common "wrong TLS mode" failure modes from ImapFlow.
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.
89
92
  function _looksLikeTlsMismatch(err) {
90
93
  const msg = (err && err.message ? err.message : String(err)).toLowerCase();
91
- return /greeting|tls|ssl|wrong version number|protocol|enotconn|econnreset|handshake/.test(msg);
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);
92
95
  }
93
96
 
94
- export async function getImapClient(accountId) {
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) {
95
103
  const existing = syncClients.get(accountId);
96
- if (existing?.usable) return existing;
104
+ if (existing?.usable && override === undefined) return existing;
97
105
  if (existing) { syncClients.delete(accountId); try { await existing.logout(); } catch {} }
98
106
 
99
107
  const creds = getAccountCredentials(accountId);
100
108
  if (!creds || !creds.imap_host) throw new Error(`No IMAP credentials for account ${accountId}`);
101
109
  const label = `sync:${accountId.slice(0, 8)}`;
102
110
 
103
- // First attempt with the heuristic-derived TLS mode.
104
- let client = createImapClient(label, creds, accountId);
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);
105
121
  try {
106
122
  await client.connect();
123
+ lastGoodSecure.set(accountId, firstSecure);
107
124
  } 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)) {
125
+ if (_looksLikeTlsMismatch(err) && override === undefined) {
113
126
  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}`);
127
+ const fallbackSecure = !firstSecure;
128
+ console.warn(`[email:imap] ${label} TLS mismatch (${err.message.slice(0, 100)}). Retrying with secure=${fallbackSecure}`);
117
129
  client = createImapClient(label, creds, accountId, fallbackSecure);
118
130
  await client.connect();
131
+ lastGoodSecure.set(accountId, fallbackSecure);
119
132
  } else {
120
133
  throw err;
121
134
  }
@@ -392,12 +405,25 @@ export async function syncFolder(accountId, folderPath, fullResync, limitMessage
392
405
  }
393
406
  }
394
407
 
395
- // First sync: cap to last 200 messages per folder to avoid blocking for minutes
396
- 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;
397
414
 
398
415
  export async function syncAccount(accountId, opts = {}) {
399
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;
400
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
+ }
401
427
  const folders = await listImapFolders(accountId);
402
428
  const priority = ['inbox', 'sent'];
403
429
  const toSync = [
@@ -408,17 +434,28 @@ export async function syncAccount(accountId, opts = {}) {
408
434
  let totalSynced = 0;
409
435
  for (const f of toSync) {
410
436
  try {
411
- const limit = opts.full ? 0 : FIRST_SYNC_LIMIT;
437
+ const limit = opts.full === false ? 200 : FIRST_SYNC_LIMIT;
412
438
  const result = await syncFolder(accountId, f.path, false, limit, f.folderType);
413
439
  totalSynced += result.synced;
414
440
  console.log(`[email:sync] ${f.path}: ${result.synced} new messages (total on server: ${result.total})`);
415
441
  } catch (err) {
416
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;
417
446
  }
418
447
  }
419
448
  setSyncStatus(accountId, 'idle', null);
420
449
  return { synced: totalSynced };
421
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
+ }
422
459
  setSyncStatus(accountId, 'error', err.message);
423
460
  throw err;
424
461
  }