nothumanallowed 14.1.48 → 14.1.49
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/server/routes/email.mjs +11 -10
- package/src/services/google-gmail.mjs +71 -40
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "14.1.
|
|
3
|
+
"version": "14.1.49",
|
|
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.49';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -38,16 +38,17 @@ export function register(router) {
|
|
|
38
38
|
const offset = parseInt(url.searchParams.get('page') || url.searchParams.get('offset') || '0') * (url.searchParams.get('page') ? limit : 1);
|
|
39
39
|
try {
|
|
40
40
|
// getAllEmails returns ALL messages (read + unread) for local-first architecture
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
// Works for ALL folders: INBOX, SENT, DRAFTS, SPAM, TRASH, STARRED, IMPORTANT
|
|
42
|
+
const { getAllEmails } = await import('../../services/google-gmail.mjs');
|
|
43
|
+
const gmailFolder = folder.toUpperCase() === 'INBOX' ? 'INBOX'
|
|
44
|
+
: folder.toUpperCase() === 'SENT' ? 'SENT'
|
|
45
|
+
: folder.toUpperCase() === 'DRAFTS' ? 'DRAFTS'
|
|
46
|
+
: folder.toUpperCase() === 'SPAM' ? 'SPAM'
|
|
47
|
+
: folder.toUpperCase() === 'TRASH' ? 'TRASH'
|
|
48
|
+
: folder.toUpperCase() === 'STARRED' ? 'STARRED'
|
|
49
|
+
: folder.toUpperCase() === 'IMPORTANT' ? 'IMPORTANT'
|
|
50
|
+
: folder;
|
|
51
|
+
const emails = await getAllEmails(config, gmailFolder, limit + offset);
|
|
51
52
|
sendJSON(res, 200, { emails: emails.slice(offset, offset + limit), total: emails.length });
|
|
52
53
|
} catch (providerErr) {
|
|
53
54
|
if (providerErr.message?.includes('No mail provider') || providerErr.message?.includes('token')) {
|
|
@@ -123,7 +123,7 @@ export async function getAllEmails(config, folder = 'INBOX', maxResults = 200) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// Cache messages locally - this ensures local-first access
|
|
126
|
-
await cacheMessages(messages);
|
|
126
|
+
await cacheMessages(messages, folder);
|
|
127
127
|
return messages;
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -384,7 +384,7 @@ function parseMessage(raw) {
|
|
|
384
384
|
|
|
385
385
|
// ── Local Cache ────────────────────────────────────────────────────────────
|
|
386
386
|
|
|
387
|
-
async function cacheMessages(messages) {
|
|
387
|
+
async function cacheMessages(messages, folder = 'INBOX') {
|
|
388
388
|
// Cache as JSON files (legacy ops system)
|
|
389
389
|
fs.mkdirSync(INBOX_DIR, { recursive: true });
|
|
390
390
|
for (const msg of messages) {
|
|
@@ -392,58 +392,89 @@ async function cacheMessages(messages) {
|
|
|
392
392
|
fs.writeFileSync(file, JSON.stringify(msg, null, 2), { mode: 0o600 });
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
// ENTERPRISE LOCAL-FIRST: Also save to
|
|
395
|
+
// ENTERPRISE LOCAL-FIRST: Also save to SQLite database for Web UI offline access
|
|
396
396
|
try {
|
|
397
|
-
if (!messages.length) return;
|
|
397
|
+
if (!messages.length) return;
|
|
398
|
+
|
|
399
|
+
const { SQLITE_AVAILABLE, getDb, ensureGoogleAccount, upsertFolder } = await import('./email-db.mjs');
|
|
400
|
+
if (!SQLITE_AVAILABLE) return;
|
|
398
401
|
|
|
399
|
-
const
|
|
402
|
+
const db = getDb();
|
|
400
403
|
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
+
// Determine the user's own email from config (reliable source)
|
|
405
|
+
let accountEmail = 'gmail@account';
|
|
406
|
+
try {
|
|
407
|
+
const os = await import('os');
|
|
408
|
+
const cfgPath = path.join(os.default.homedir(), '.nha', 'config.json');
|
|
409
|
+
if (fs.existsSync(cfgPath)) {
|
|
410
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
411
|
+
accountEmail = cfg.google?.email || cfg.email || accountEmail;
|
|
412
|
+
}
|
|
413
|
+
} catch {}
|
|
414
|
+
// Fallback: extract from 'to' field of first inbox message
|
|
415
|
+
if (accountEmail === 'gmail@account' && messages[0]) {
|
|
416
|
+
accountEmail = extractEmail(messages[0].to) || accountEmail;
|
|
417
|
+
}
|
|
404
418
|
|
|
405
|
-
// Ensure Google account exists
|
|
419
|
+
// Ensure Google account exists — use transaction for atomicity
|
|
406
420
|
const googleAccount = ensureGoogleAccount({
|
|
407
421
|
id: 'google',
|
|
408
422
|
display_name: 'Gmail',
|
|
409
423
|
email_address: accountEmail,
|
|
410
424
|
imap_host: 'gmail.googleapis.com',
|
|
411
425
|
imap_port: 993,
|
|
412
|
-
imap_secure: true,
|
|
413
426
|
is_active: 1
|
|
414
427
|
});
|
|
428
|
+
if (!googleAccount) return;
|
|
429
|
+
|
|
430
|
+
// Map folder to type and display name
|
|
431
|
+
const folderMap = {
|
|
432
|
+
'INBOX': { name: 'Inbox', type: 'inbox' },
|
|
433
|
+
'SENT': { name: 'Sent', type: 'sent' },
|
|
434
|
+
'DRAFTS': { name: 'Drafts', type: 'drafts' },
|
|
435
|
+
'SPAM': { name: 'Spam', type: 'spam' },
|
|
436
|
+
'TRASH': { name: 'Trash', type: 'trash' },
|
|
437
|
+
'STARRED': { name: 'Starred', type: 'starred' },
|
|
438
|
+
'IMPORTANT': { name: 'Important', type: 'important' },
|
|
439
|
+
};
|
|
440
|
+
const folderKey = folder.toUpperCase();
|
|
441
|
+
const folderInfo = folderMap[folderKey] || { name: folder, type: 'custom' };
|
|
442
|
+
const folderId = upsertFolder(googleAccount.id, folderKey, folderInfo.name, folderInfo.type, 1, 0);
|
|
443
|
+
if (!folderId) return;
|
|
444
|
+
|
|
445
|
+
// Batch insert all messages in a single transaction (fast + atomic)
|
|
446
|
+
const insertStmt = db.prepare(`
|
|
447
|
+
INSERT OR REPLACE INTO email_messages
|
|
448
|
+
(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,
|
|
450
|
+
internal_date, has_attachments, source)
|
|
451
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
452
|
+
`);
|
|
453
|
+
const stateStmt = db.prepare(`
|
|
454
|
+
INSERT OR REPLACE INTO email_message_state (message_id, is_read, is_starred)
|
|
455
|
+
VALUES (?, ?, ?)
|
|
456
|
+
`);
|
|
457
|
+
|
|
458
|
+
const insertAll = db.transaction((msgs) => {
|
|
459
|
+
for (const msg of msgs) {
|
|
460
|
+
const msgId = msg.id; // Use Gmail message ID as primary key (stable, unique)
|
|
461
|
+
insertStmt.run(
|
|
462
|
+
msgId, googleAccount.id, folderId, folderKey,
|
|
463
|
+
Date.now() + Math.random() * 1000 | 0, // unique uid
|
|
464
|
+
msg.id, msg.threadId || msg.id,
|
|
465
|
+
msg.subject || '(no subject)',
|
|
466
|
+
extractEmail(msg.from), extractName(msg.from),
|
|
467
|
+
msg.to || '',
|
|
468
|
+
msg.body || '', msg.snippet || '',
|
|
469
|
+
new Date(msg.date || Date.now()).toISOString(),
|
|
470
|
+
0, 'gmail_api'
|
|
471
|
+
);
|
|
472
|
+
stateStmt.run(msgId, msg.isUnread ? 0 : 1, msg.isImportant ? 1 : 0);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
415
475
|
|
|
416
|
-
|
|
417
|
-
throw new Error('Failed to create/get Google account');
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Ensure INBOX folder exists for Gmail account
|
|
421
|
-
const inboxFolderId = upsertFolder(googleAccount.id, 'INBOX', 'Inbox', 'inbox', 1, 0);
|
|
422
|
-
|
|
423
|
-
if (!inboxFolderId) {
|
|
424
|
-
throw new Error('Failed to create/get INBOX folder');
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Save each message to IMAP database
|
|
428
|
-
for (const msg of messages) {
|
|
429
|
-
saveMessage(googleAccount.id, {
|
|
430
|
-
folder_id: inboxFolderId,
|
|
431
|
-
message_id: msg.id,
|
|
432
|
-
thread_id: msg.threadId || msg.id,
|
|
433
|
-
subject: msg.subject || '(no subject)',
|
|
434
|
-
from_name: extractName(msg.from),
|
|
435
|
-
from_address: extractEmail(msg.from),
|
|
436
|
-
to_addresses: msg.to || '',
|
|
437
|
-
body_text: msg.body || '',
|
|
438
|
-
body_preview: msg.snippet || '',
|
|
439
|
-
internal_date: new Date(msg.date || Date.now()).toISOString(),
|
|
440
|
-
is_read: !msg.isUnread,
|
|
441
|
-
is_starred: !!msg.isImportant,
|
|
442
|
-
has_attachments: false
|
|
443
|
-
});
|
|
444
|
-
}
|
|
476
|
+
insertAll(messages);
|
|
445
477
|
} catch (e) {
|
|
446
|
-
// Fallback gracefully if IMAP DB is not available
|
|
447
478
|
console.warn('[GMAIL CACHE] Failed to save to IMAP database:', e.message);
|
|
448
479
|
}
|
|
449
480
|
}
|