nothumanallowed 14.1.57 → 14.1.59
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": "14.1.
|
|
3
|
+
"version": "14.1.59",
|
|
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 = '14.1.
|
|
8
|
+
export const VERSION = '14.1.59';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -96,7 +96,92 @@ async function persistGmailMessageToDb(config, msg) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
export function register(router) {
|
|
99
|
-
// ── Gmail
|
|
99
|
+
// ── Gmail DB-first routes ──────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
// GET /api/gmail/messages — reads from local SQLite DB (fast, offline)
|
|
102
|
+
router.get('/api/gmail/messages', async (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const { SQLITE_AVAILABLE, listMessages } = await import('../../services/email-db.mjs');
|
|
105
|
+
if (!SQLITE_AVAILABLE) return sendJSON(res, 200, { messages: [], total: 0, dbUnavailable: true });
|
|
106
|
+
|
|
107
|
+
const url = new URL(req.url, 'http://localhost');
|
|
108
|
+
const folder = url.searchParams.get('folder') || 'INBOX';
|
|
109
|
+
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
110
|
+
const offset = parseInt(url.searchParams.get('offset') || '0');
|
|
111
|
+
const search = url.searchParams.get('search') || '';
|
|
112
|
+
|
|
113
|
+
const result = listMessages('google', null, limit, offset, search, folder);
|
|
114
|
+
sendJSON(res, 200, { messages: result.messages ?? [], total: result.total ?? 0, source: 'db' });
|
|
115
|
+
} catch (e) { sendJSON(res, 200, { messages: [], total: 0, error: e.message }); }
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// POST /api/gmail/sync — fetches from Gmail API and saves to local DB
|
|
119
|
+
router.post('/api/gmail/sync', async (req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
const body = await parseBody(req);
|
|
122
|
+
const config = loadConfig();
|
|
123
|
+
|
|
124
|
+
if (!config.google?.clientId && !config.google?.tokens?.access_token) {
|
|
125
|
+
return sendJSON(res, 200, { synced: 0, authRequired: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const folder = (body.folder || 'INBOX').toUpperCase();
|
|
129
|
+
const limit = body.limit || 50;
|
|
130
|
+
|
|
131
|
+
const { getAllEmails } = await import('../../services/google-gmail.mjs');
|
|
132
|
+
const emails = await getAllEmails(config, folder, limit);
|
|
133
|
+
|
|
134
|
+
// getAllEmails already calls cacheMessages internally which saves to SQLite DB
|
|
135
|
+
// Also persist via persistGmailMessageToDb for body_html
|
|
136
|
+
for (const msg of emails) {
|
|
137
|
+
await persistGmailMessageToDb(config, msg).catch(() => {});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
sendJSON(res, 200, { synced: emails.length, folder });
|
|
141
|
+
} catch (e) {
|
|
142
|
+
sendJSON(res, 200, { synced: 0, error: e.message });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// GET /api/gmail/message — read single message from DB, fallback to Gmail API
|
|
147
|
+
router.get('/api/gmail/message', async (req, res) => {
|
|
148
|
+
try {
|
|
149
|
+
const url = new URL(req.url, 'http://localhost');
|
|
150
|
+
const id = url.searchParams.get('id');
|
|
151
|
+
if (!id) return sendError(res, 400, 'id required');
|
|
152
|
+
|
|
153
|
+
// Try DB first
|
|
154
|
+
const { getMessage: dbGetMessage } = await import('../../services/email-db.mjs');
|
|
155
|
+
const dbMsg = dbGetMessage(id);
|
|
156
|
+
if (dbMsg && (dbMsg.body_text || dbMsg.body_html)) {
|
|
157
|
+
return sendJSON(res, 200, { message: dbMsg, source: 'db' });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fallback to Gmail API
|
|
161
|
+
const config = loadConfig();
|
|
162
|
+
const msg = await getMessage(config, id);
|
|
163
|
+
if (msg) {
|
|
164
|
+
persistGmailMessageToDb(config, msg).catch(() => {});
|
|
165
|
+
return sendJSON(res, 200, {
|
|
166
|
+
message: {
|
|
167
|
+
id: msg.id,
|
|
168
|
+
subject: msg.subject,
|
|
169
|
+
from_address: msg.from ? (msg.from.match(/<(.+?)>$/)?.[1] || msg.from) : '',
|
|
170
|
+
from_name: msg.from ? (msg.from.match(/^(.+?)\s*<.+>$/)?.[1]?.replace(/['"]/g, '') || '') : '',
|
|
171
|
+
to_addresses: msg.to || '',
|
|
172
|
+
internal_date: msg.date || '',
|
|
173
|
+
body_text: msg.body || '',
|
|
174
|
+
body_html: msg.bodyHtml || '',
|
|
175
|
+
body_preview: msg.snippet || '',
|
|
176
|
+
},
|
|
177
|
+
source: 'gmail_api',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
sendJSON(res, 200, { message: null });
|
|
181
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ── Gmail legacy ──────────────────────────────────────────────────────
|
|
100
185
|
|
|
101
186
|
router.get('/api/emails', async (req, res) => {
|
|
102
187
|
try {
|
|
@@ -325,12 +410,12 @@ export function register(router) {
|
|
|
325
410
|
const { listMessages } = await import('../../services/email-db.mjs');
|
|
326
411
|
const url = new URL(req.url, 'http://localhost');
|
|
327
412
|
const accountId = url.searchParams.get('accountId');
|
|
328
|
-
const
|
|
413
|
+
const labelId = url.searchParams.get('labelId') || null;
|
|
414
|
+
const folder = url.searchParams.get('folder') || null;
|
|
329
415
|
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
330
416
|
const offset = parseInt(url.searchParams.get('offset') || '0');
|
|
331
417
|
const search = url.searchParams.get('search') || '';
|
|
332
|
-
|
|
333
|
-
const result = listMessages(accountId, null, limit, offset, search);
|
|
418
|
+
const result = listMessages(accountId, labelId, limit, offset, search, folder);
|
|
334
419
|
sendJSON(res, 200, { messages: result.messages ?? result, total: result.total ?? 0 });
|
|
335
420
|
} catch (e) { sendError(res, 500, e.message); }
|
|
336
421
|
});
|
|
@@ -565,13 +565,13 @@ export function getMessageLabels(messageId) {
|
|
|
565
565
|
|
|
566
566
|
// ── MESSAGE QUERIES ────────────────────────────────────────────────────────
|
|
567
567
|
|
|
568
|
-
export function listMessages(accountId, labelId, limit, offset, search) {
|
|
568
|
+
export function listMessages(accountId, labelId, limit, offset, search, folder) {
|
|
569
569
|
const db = getDb();
|
|
570
570
|
let where = 'm.account_id = ? AND m.permanently_deleted = 0';
|
|
571
571
|
const params = [accountId];
|
|
572
572
|
|
|
573
573
|
if (labelId) {
|
|
574
|
-
//
|
|
574
|
+
// Filter by label — also include messages saved with matching imap_folder_path
|
|
575
575
|
const lbl = db.prepare('SELECT system_type FROM email_labels WHERE id = ?').get(labelId);
|
|
576
576
|
const folderFallback = lbl?.system_type ? lbl.system_type.charAt(0).toUpperCase() + lbl.system_type.slice(1) : null;
|
|
577
577
|
if (folderFallback) {
|
|
@@ -581,12 +581,20 @@ export function listMessages(accountId, labelId, limit, offset, search) {
|
|
|
581
581
|
where += ' AND EXISTS (SELECT 1 FROM email_message_labels j WHERE j.message_id = m.id AND j.label_id = ?)';
|
|
582
582
|
params.push(labelId);
|
|
583
583
|
}
|
|
584
|
+
} else if (folder) {
|
|
585
|
+
// Filter by folder path (used by Gmail — messages stored with imap_folder_path like INBOX, SENT, etc.)
|
|
586
|
+
where += ' AND m.imap_folder_path = ?';
|
|
587
|
+
params.push(folder.toUpperCase());
|
|
584
588
|
}
|
|
585
589
|
if (search) {
|
|
586
|
-
where += ' AND (m.subject LIKE ? OR m.from_address LIKE ? OR m.from_name LIKE ? OR m.body_preview LIKE ?)';
|
|
590
|
+
where += ' AND (m.subject LIKE ? OR m.from_address LIKE ? OR m.from_name LIKE ? OR m.body_preview LIKE ? OR m.body_text LIKE ?)';
|
|
587
591
|
const q = `%${search}%`;
|
|
588
|
-
params.push(q, q, q, q);
|
|
592
|
+
params.push(q, q, q, q, q);
|
|
589
593
|
}
|
|
594
|
+
|
|
595
|
+
const countWhere = where;
|
|
596
|
+
const countParams = [...params];
|
|
597
|
+
|
|
590
598
|
params.push(limit || 50, offset || 0);
|
|
591
599
|
|
|
592
600
|
const rows = db.prepare(`
|
|
@@ -601,7 +609,7 @@ export function listMessages(accountId, labelId, limit, offset, search) {
|
|
|
601
609
|
LIMIT ? OFFSET ?
|
|
602
610
|
`).all(...params);
|
|
603
611
|
|
|
604
|
-
const total = db.prepare(`SELECT COUNT(*) as cnt FROM email_messages m WHERE ${
|
|
612
|
+
const total = db.prepare(`SELECT COUNT(*) as cnt FROM email_messages m WHERE ${countWhere}`).get(...countParams);
|
|
605
613
|
|
|
606
614
|
return { messages: rows, total: total?.cnt || 0 };
|
|
607
615
|
}
|
|
@@ -353,17 +353,34 @@ function parseMessage(raw) {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
let body = '';
|
|
356
|
+
let bodyHtml = '';
|
|
357
|
+
|
|
358
|
+
// Recursive function to find parts in nested multipart structures
|
|
359
|
+
function findParts(parts) {
|
|
360
|
+
if (!parts) return;
|
|
361
|
+
for (const p of parts) {
|
|
362
|
+
if (p.mimeType === 'text/plain' && p.body?.data && !body) {
|
|
363
|
+
body = Buffer.from(p.body.data, 'base64url').toString('utf-8');
|
|
364
|
+
} else if (p.mimeType === 'text/html' && p.body?.data && !bodyHtml) {
|
|
365
|
+
bodyHtml = Buffer.from(p.body.data, 'base64url').toString('utf-8');
|
|
366
|
+
} else if (p.parts) {
|
|
367
|
+
findParts(p.parts);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
356
372
|
if (raw.payload?.body?.data) {
|
|
357
|
-
|
|
373
|
+
// Single-part message
|
|
374
|
+
const content = Buffer.from(raw.payload.body.data, 'base64url').toString('utf-8');
|
|
375
|
+
if (raw.payload.mimeType === 'text/html') bodyHtml = content;
|
|
376
|
+
else body = content;
|
|
358
377
|
} else if (raw.payload?.parts) {
|
|
359
|
-
|
|
360
|
-
if (textPart?.body?.data) {
|
|
361
|
-
body = Buffer.from(textPart.body.data, 'base64url').toString('utf-8');
|
|
362
|
-
}
|
|
378
|
+
findParts(raw.payload.parts);
|
|
363
379
|
}
|
|
364
380
|
|
|
365
381
|
// Extract URLs from body
|
|
366
|
-
const
|
|
382
|
+
const textContent = body || bodyHtml.replace(/<[^>]+>/g, '');
|
|
383
|
+
const urls = (textContent.match(/https?:\/\/[^\s<>"']+/g) || []).slice(0, 10);
|
|
367
384
|
|
|
368
385
|
return {
|
|
369
386
|
id: raw.id,
|
|
@@ -373,7 +390,8 @@ function parseMessage(raw) {
|
|
|
373
390
|
subject: headers.subject || '(no subject)',
|
|
374
391
|
date: headers.date || '',
|
|
375
392
|
snippet: raw.snippet || '',
|
|
376
|
-
body: body.slice(0, 5000),
|
|
393
|
+
body: body.slice(0, 5000),
|
|
394
|
+
bodyHtml,
|
|
377
395
|
urls,
|
|
378
396
|
labels: raw.labelIds || [],
|
|
379
397
|
isUnread: (raw.labelIds || []).includes('UNREAD'),
|
|
@@ -446,9 +464,9 @@ async function cacheMessages(messages, folder = 'INBOX') {
|
|
|
446
464
|
const insertStmt = db.prepare(`
|
|
447
465
|
INSERT OR REPLACE INTO email_messages
|
|
448
466
|
(id, account_id, folder_id, imap_folder_path, uid, message_id, thread_id,
|
|
449
|
-
subject, from_address, from_name, to_addresses, body_text, body_preview,
|
|
467
|
+
subject, from_address, from_name, to_addresses, body_text, body_html, body_preview,
|
|
450
468
|
internal_date, has_attachments, source)
|
|
451
|
-
VALUES (
|
|
469
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
452
470
|
`);
|
|
453
471
|
const stateStmt = db.prepare(`
|
|
454
472
|
INSERT OR REPLACE INTO email_message_state (message_id, is_read, is_starred)
|
|
@@ -457,15 +475,15 @@ async function cacheMessages(messages, folder = 'INBOX') {
|
|
|
457
475
|
|
|
458
476
|
const insertAll = db.transaction((msgs) => {
|
|
459
477
|
for (const msg of msgs) {
|
|
460
|
-
const msgId = msg.id;
|
|
478
|
+
const msgId = msg.id;
|
|
461
479
|
insertStmt.run(
|
|
462
480
|
msgId, googleAccount.id, folderId, folderKey,
|
|
463
|
-
Date.now() + Math.random() * 1000 | 0,
|
|
481
|
+
Date.now() + Math.random() * 1000 | 0,
|
|
464
482
|
msg.id, msg.threadId || msg.id,
|
|
465
483
|
msg.subject || '(no subject)',
|
|
466
484
|
extractEmail(msg.from), extractName(msg.from),
|
|
467
485
|
msg.to || '',
|
|
468
|
-
msg.body || '', msg.snippet || '',
|
|
486
|
+
msg.body || '', msg.bodyHtml || '', msg.snippet || '',
|
|
469
487
|
new Date(msg.date || Date.now()).toISOString(),
|
|
470
488
|
0, 'gmail_api'
|
|
471
489
|
);
|