nothumanallowed 13.5.117 → 13.5.118

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": "13.5.117",
3
+ "version": "13.5.118",
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 = '13.5.117';
8
+ export const VERSION = '13.5.118';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -148,36 +148,51 @@ function stripQuotedReplies(text) {
148
148
  return text.split('\n').filter(l => !l.startsWith('>')).join('\n').trim();
149
149
  }
150
150
 
151
- export async function syncFolder(accountId, folderPath, fullResync) {
151
+ export async function syncFolder(accountId, folderPath, fullResync, limitMessages = 0) {
152
152
  const client = await getImapClient(accountId);
153
153
  const db = getDb();
154
154
  const folderMeta = getFolder(accountId, folderPath);
155
+
156
+ // Open mailbox to get uidValidity BEFORE getMailboxLock
155
157
  const lock = await client.getMailboxLock(folderPath);
156
158
 
157
159
  try {
158
160
  const mailbox = client.mailbox;
159
161
  const currentUidValidity = Number(mailbox?.uidValidity ?? 0);
162
+ const totalMessages = Number(mailbox?.exists ?? 0);
160
163
  const storedUidValidity = folderMeta?.uid_validity ?? null;
161
- const lastUid = (fullResync || storedUidValidity !== currentUidValidity) ? 0 : (folderMeta?.last_uid ?? 0);
164
+ const isFirstSync = storedUidValidity === null || fullResync;
165
+ const lastUid = isFirstSync ? 0 : (folderMeta?.last_uid ?? 0);
162
166
  const needsResync = fullResync || (storedUidValidity !== null && storedUidValidity !== currentUidValidity);
163
167
 
164
168
  if (needsResync && folderMeta) {
165
- // Clear existing messages for this folder on UID validity change
166
169
  db.prepare('DELETE FROM email_message_labels WHERE message_id IN (SELECT id FROM email_messages WHERE account_id = ? AND imap_folder_path = ?)').run(accountId, folderPath);
167
170
  db.prepare('DELETE FROM email_messages WHERE account_id = ? AND imap_folder_path = ?').run(accountId, folderPath);
168
171
  }
169
172
 
170
173
  const inboxLabel = getSystemLabel(accountId, 'inbox');
171
174
  const sentLabel = getSystemLabel(accountId, 'sent');
172
- const blockedLabel = getSystemLabel(accountId, 'spam');
173
175
 
174
176
  let newLastUid = lastUid;
175
177
  let synced = 0;
176
178
 
177
- const range = lastUid > 0 ? `${lastUid + 1}:*` : '1:*';
179
+ // On first sync: fetch only the most recent N messages by sequence number
180
+ // On incremental sync: fetch new UIDs since last known
181
+ let range;
182
+ if (isFirstSync && limitMessages > 0 && totalMessages > limitMessages) {
183
+ const seqStart = totalMessages - limitMessages + 1;
184
+ range = `${seqStart}:*`;
185
+ } else if (lastUid > 0) {
186
+ range = `${lastUid + 1}:*`;
187
+ } else {
188
+ range = '1:*';
189
+ }
178
190
 
191
+ // Phase 1: fetch headers only (fast — no body download)
192
+ const headerMap = new Map(); // uid → parsed header data
179
193
  for await (const msg of client.fetch(range, {
180
194
  uid: true, flags: true, envelope: true, internalDate: true, size: true,
195
+ bodyStructure: true,
181
196
  }, { uid: true })) {
182
197
  if (msg.uid <= lastUid) continue;
183
198
  newLastUid = Math.max(newLastUid, msg.uid);
@@ -186,36 +201,48 @@ export async function syncFolder(accountId, folderPath, fullResync) {
186
201
  const fromAddr = env.from?.[0]?.address ?? null;
187
202
  const fromName = env.from?.[0]?.name ?? null;
188
203
 
189
- // Skip blocked senders
190
- if (fromAddr && isSenderBlocked(accountId, fromAddr)) {
191
- synced++;
192
- continue;
193
- }
194
-
195
- // Check if already synced
204
+ if (fromAddr && isSenderBlocked(accountId, fromAddr)) { synced++; continue; }
196
205
  if (messageExists(accountId, folderPath, msg.uid)) { synced++; continue; }
197
206
 
198
- // Fetch full body for this message
207
+ headerMap.set(msg.uid, {
208
+ env, fromAddr, fromName,
209
+ internalDate: (msg.internalDate || new Date()).toISOString(),
210
+ size: msg.size || 0,
211
+ flags: msg.flags,
212
+ bodyStructure: msg.bodyStructure,
213
+ });
214
+ }
215
+
216
+ // Phase 2: for each new message, fetch body (individually, with error isolation)
217
+ const uidsToFetch = [...headerMap.keys()];
218
+ for (const uid of uidsToFetch) {
219
+ const hdr = headerMap.get(uid);
220
+ const env = hdr.env;
221
+ const fromAddr = hdr.fromAddr;
222
+ const fromName = hdr.fromName;
223
+
199
224
  let bodyText = null, bodyHtml = null, bodyPreview = '', attachments = [];
200
225
  let inReplyTo = null, references = [], msgId = null;
201
226
 
202
227
  try {
203
- const dl = await client.download(String(msg.uid), undefined, { uid: true });
228
+ const dl = await client.download(String(uid), undefined, { uid: true });
204
229
  if (dl) {
205
230
  const chunks = [];
206
231
  let totalSize = 0;
207
232
  for await (const chunk of dl.content) {
208
233
  totalSize += chunk.length;
209
- if (totalSize > 25 * 1024 * 1024) break; // 25MB cap
234
+ if (totalSize > 10 * 1024 * 1024) break; // 10MB cap per message
210
235
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
211
236
  }
212
237
  const raw = Buffer.concat(chunks);
213
- const parsed = await simpleParser(raw);
238
+ const parsed = await simpleParser(raw, { skipHtmlToText: false });
214
239
  msgId = parsed.messageId || null;
215
- inReplyTo = typeof parsed.inReplyTo === 'string' ? parsed.inReplyTo : (Array.isArray(parsed.inReplyTo) ? parsed.inReplyTo[0] : null);
216
- references = Array.isArray(parsed.references) ? parsed.references.slice(0, 20) : (parsed.references ? [parsed.references] : []);
217
- bodyText = parsed.text?.replace(/\x00/g, '') || null;
218
- bodyHtml = parsed.html?.replace(/\x00/g, '') || null;
240
+ inReplyTo = typeof parsed.inReplyTo === 'string' ? parsed.inReplyTo
241
+ : (Array.isArray(parsed.inReplyTo) ? parsed.inReplyTo[0] : null);
242
+ references = Array.isArray(parsed.references) ? parsed.references.slice(0, 20)
243
+ : (parsed.references ? [parsed.references] : []);
244
+ bodyText = (typeof parsed.text === 'string' ? parsed.text : null)?.replace(/\x00/g, '') || null;
245
+ bodyHtml = (typeof parsed.html === 'string' ? parsed.html : null)?.replace(/\x00/g, '') || null;
219
246
  bodyPreview = (bodyText || '').replace(/\s+/g, ' ').trim().slice(0, 255);
220
247
 
221
248
  if (parsed.attachments) {
@@ -226,30 +253,31 @@ export async function syncFolder(accountId, folderPath, fullResync) {
226
253
  size_bytes: att.size || 0,
227
254
  part_id: att.partId || '',
228
255
  content_id: att.contentId || null,
229
- content: att.size < 5 * 1024 * 1024 ? att.content : null,
256
+ content: (att.size || 0) < 5 * 1024 * 1024 ? att.content : null,
230
257
  });
231
258
  }
232
259
  }
233
260
  }
234
261
  } catch (err) {
235
- console.warn(`[email:imap] Failed to fetch body uid=${msg.uid}:`, err.message);
262
+ console.warn(`[email:imap] Failed to fetch body uid=${uid}:`, err.message);
263
+ // Continue with headers-only for this message
236
264
  }
237
265
 
238
266
  const toAddresses = (env.to || []).map(a => ({ address: a.address, name: a.name }));
239
267
  const ccAddresses = (env.cc || []).map(a => ({ address: a.address, name: a.name }));
240
268
  const bccAddresses = (env.bcc || []).map(a => ({ address: a.address, name: a.name }));
241
269
  const subject = env.subject || '';
242
- const internalDate = (msg.internalDate || new Date()).toISOString();
243
- const tid = threadId(msgId, inReplyTo, references, fromAddr, internalDate, subject);
244
- const hash = contentHash(msgId || '', fromAddr || '', internalDate);
270
+ const tid = threadId(msgId, inReplyTo, references, fromAddr, hdr.internalDate, subject);
271
+ const hash = contentHash(msgId || '', fromAddr || '', hdr.internalDate);
245
272
 
246
- const folderRec = upsertFolder(accountId, folderPath, folderPath, folderMeta?.folder_type || 'custom', currentUidValidity, newLastUid);
273
+ const folderRec = upsertFolder(accountId, folderPath, folderPath,
274
+ folderMeta?.folder_type || 'custom', currentUidValidity, newLastUid);
247
275
 
248
276
  const msgDbId = insertMessage({
249
277
  account_id: accountId,
250
278
  folder_id: folderRec,
251
279
  imap_folder_path: folderPath,
252
- uid: msg.uid,
280
+ uid,
253
281
  message_id: msgId,
254
282
  in_reply_to: inReplyTo,
255
283
  references_list: references,
@@ -264,42 +292,41 @@ export async function syncFolder(accountId, folderPath, fullResync) {
264
292
  body_html: bodyHtml,
265
293
  body_preview: bodyPreview,
266
294
  body_reply_only: stripQuotedReplies(bodyText),
267
- size_bytes: msg.size || 0,
295
+ size_bytes: hdr.size,
268
296
  has_attachments: attachments.length > 0,
269
297
  content_hash: hash,
270
- internal_date: internalDate,
298
+ internal_date: hdr.internalDate,
271
299
  source: 'imap',
272
300
  });
273
301
 
274
302
  if (attachments.length > 0) insertAttachments(msgDbId, attachments);
275
303
 
276
- // Apply archiving rules (first match wins, removes from inbox)
277
304
  const archived = applyArchivingRules(accountId, msgDbId, fromAddr, subject);
278
305
  if (!archived) {
279
- // Assign label based on folder type
280
- const isSent = folderPath.toLowerCase().includes('sent') || folderPath.toLowerCase().includes('inviati');
281
- const targetLabel = isSent ? sentLabel : inboxLabel;
306
+ const isSentFolder = folderPath.toLowerCase().includes('sent') || folderPath.toLowerCase().includes('inviati');
307
+ const targetLabel = isSentFolder ? sentLabel : inboxLabel;
282
308
  if (targetLabel) addMessageToLabel(msgDbId, targetLabel.id);
283
309
  }
284
310
 
285
311
  synced++;
286
312
  }
287
313
 
288
- // Update folder sync state
289
- upsertFolder(accountId, folderPath,
290
- folderPath, folderMeta?.folder_type || 'custom',
291
- currentUidValidity, newLastUid);
314
+ upsertFolder(accountId, folderPath, folderPath,
315
+ folderMeta?.folder_type || 'custom', currentUidValidity, newLastUid);
292
316
 
293
- return { synced, lastUid: newLastUid };
317
+ updateLabelCounts(accountId);
318
+ return { synced, lastUid: newLastUid, total: totalMessages };
294
319
  } finally {
295
320
  lock.release();
296
321
  }
297
322
  }
298
323
 
299
- export async function syncAccount(accountId) {
324
+ // First sync: cap to last 200 messages per folder to avoid blocking for minutes
325
+ const FIRST_SYNC_LIMIT = 200;
326
+
327
+ export async function syncAccount(accountId, opts = {}) {
300
328
  setSyncStatus(accountId, 'syncing', null);
301
329
  try {
302
- // List folders from IMAP and sync INBOX + Sent
303
330
  const folders = await listImapFolders(accountId);
304
331
  const priority = ['inbox', 'sent'];
305
332
  const toSync = [
@@ -310,8 +337,10 @@ export async function syncAccount(accountId) {
310
337
  let totalSynced = 0;
311
338
  for (const f of toSync) {
312
339
  try {
313
- const result = await syncFolder(accountId, f.path, false);
340
+ const limit = opts.full ? 0 : FIRST_SYNC_LIMIT;
341
+ const result = await syncFolder(accountId, f.path, false, limit);
314
342
  totalSynced += result.synced;
343
+ console.log(`[email:sync] ${f.path}: ${result.synced} new messages (total on server: ${result.total})`);
315
344
  } catch (err) {
316
345
  console.warn(`[email:imap] Sync folder ${f.path} failed:`, err.message);
317
346
  }