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 +1 -1
- package/src/constants.mjs +1 -1
- package/src/services/email-imap.mjs +70 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "13.5.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 >
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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=${
|
|
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
|
|
243
|
-
const
|
|
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,
|
|
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
|
|
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:
|
|
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
|
-
|
|
280
|
-
const
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
folderPath, folderMeta?.folder_type || 'custom',
|
|
291
|
-
currentUidValidity, newLastUid);
|
|
314
|
+
upsertFolder(accountId, folderPath, folderPath,
|
|
315
|
+
folderMeta?.folder_type || 'custom', currentUidValidity, newLastUid);
|
|
292
316
|
|
|
293
|
-
|
|
317
|
+
updateLabelCounts(accountId);
|
|
318
|
+
return { synced, lastUid: newLastUid, total: totalMessages };
|
|
294
319
|
} finally {
|
|
295
320
|
lock.release();
|
|
296
321
|
}
|
|
297
322
|
}
|
|
298
323
|
|
|
299
|
-
|
|
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
|
|
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
|
}
|