nothumanallowed 13.5.122 → 13.5.124
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/commands/chat.mjs +1 -1
- package/src/commands/ui.mjs +1 -1
- package/src/constants.mjs +1 -1
- package/src/services/tool-executor.mjs +181 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "13.5.
|
|
3
|
+
"version": "13.5.124",
|
|
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/commands/chat.mjs
CHANGED
|
@@ -613,7 +613,7 @@ export async function cmdChat(args) {
|
|
|
613
613
|
terminal: true,
|
|
614
614
|
});
|
|
615
615
|
|
|
616
|
-
const systemPrompt = buildSystemPrompt('NHA Chat', CHAT_PERSONA, config, initialContext);
|
|
616
|
+
const systemPrompt = await buildSystemPrompt('NHA Chat', CHAT_PERSONA, config, initialContext);
|
|
617
617
|
|
|
618
618
|
rl.on('close', () => {
|
|
619
619
|
console.log(`\n ${D}Session ended. Goodbye.${NC}\n`);
|
package/src/commands/ui.mjs
CHANGED
|
@@ -257,7 +257,7 @@ export async function cmdUI(args) {
|
|
|
257
257
|
`You help the user manage their emails, calendar, tasks, GitHub issues, Notion pages, and Slack channels through natural conversation. ` +
|
|
258
258
|
`Be concise, helpful, and proactive. When presenting data, format it clearly. ` +
|
|
259
259
|
`Never output raw JSON to the user.`;
|
|
260
|
-
const chatSystemPrompt = buildSystemPrompt('NHA UI', UI_PERSONA, config);
|
|
260
|
+
const chatSystemPrompt = await buildSystemPrompt('NHA UI', UI_PERSONA, config);
|
|
261
261
|
|
|
262
262
|
// ── Route Handlers ──────────────────────────────────────────────────────
|
|
263
263
|
|
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.124';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -67,6 +67,10 @@ export const DESTRUCTIVE_ACTIONS = new Set([
|
|
|
67
67
|
'gmail_reply',
|
|
68
68
|
'gmail_delete',
|
|
69
69
|
'imap_send',
|
|
70
|
+
'imap_reply',
|
|
71
|
+
'imap_bulk_send',
|
|
72
|
+
'imap_send_template',
|
|
73
|
+
'imap_trash',
|
|
70
74
|
'calendar_create',
|
|
71
75
|
'calendar_move',
|
|
72
76
|
'calendar_update',
|
|
@@ -375,13 +379,14 @@ TOOLS:
|
|
|
375
379
|
--- IMAP EMAIL (custom accounts) ---
|
|
376
380
|
|
|
377
381
|
64. imap_list(accountId: string, labelId?: string, search?: string, limit?: number)
|
|
378
|
-
List emails from a custom IMAP account
|
|
379
|
-
|
|
382
|
+
List emails from a custom IMAP account. accountId is known from the IMAP ACCOUNTS section above — use it directly.
|
|
383
|
+
search does full-text match on subject, from_address, from_name, body. Use it to filter by sender name/domain.
|
|
384
|
+
Example: search="zeli" finds all emails from *@zeli.it or with "zeli" in subject/body.
|
|
380
385
|
Returns: [{id, subject, from_address, from_name, internal_date, body_preview, is_read, is_starred}]
|
|
381
386
|
|
|
382
387
|
65. imap_accounts()
|
|
383
|
-
List all configured IMAP accounts.
|
|
384
|
-
|
|
388
|
+
List all configured IMAP accounts. NOTE: accounts are already listed in the IMAP ACCOUNTS section above.
|
|
389
|
+
Only call this if you need to refresh or the section is missing.
|
|
385
390
|
|
|
386
391
|
66. imap_read(messageId: string)
|
|
387
392
|
Read a full email message from the local DB by its id. Returns subject, from, to, body_text, body_html, attachments.
|
|
@@ -400,6 +405,43 @@ TOOLS:
|
|
|
400
405
|
70. imap_mark_read(messageId: string, isRead?: boolean)
|
|
401
406
|
Mark a local message as read or unread. Does NOT touch the IMAP server. Default isRead=true.
|
|
402
407
|
|
|
408
|
+
71. imap_reply(accountId: string, messageId: string, bodyHtml: string, cc?: string)
|
|
409
|
+
Reply to an existing email. Automatically sets In-Reply-To and References headers for proper threading.
|
|
410
|
+
Fetches the original message from DB to build correct subject (Re: ...) and recipient.
|
|
411
|
+
ALWAYS confirm with user before sending.
|
|
412
|
+
|
|
413
|
+
72. imap_thread(accountId: string, threadId: string)
|
|
414
|
+
Read all messages in a thread. Returns them in chronological order with subject, from, date, body.
|
|
415
|
+
Use imap_read() to get the threadId from a message.
|
|
416
|
+
|
|
417
|
+
73. imap_search(accountId: string, query: string, limit?: number)
|
|
418
|
+
Full-text search across all synced emails (subject, body_preview, from_address, from_name).
|
|
419
|
+
query is a plain text string — use sender name, domain, or keyword.
|
|
420
|
+
Examples: "zeli" finds emails from *@zeli.it. "fattura" finds emails with that word. limit defaults to 20.
|
|
421
|
+
|
|
422
|
+
74. imap_mark_starred(messageId: string, isStarred?: boolean)
|
|
423
|
+
Star or unstar a message locally. Default isStarred=true.
|
|
424
|
+
|
|
425
|
+
75. imap_trash(messageId: string)
|
|
426
|
+
Move a message to local Trash (soft delete — does NOT touch IMAP server). ALWAYS confirm.
|
|
427
|
+
|
|
428
|
+
76. imap_draft(accountId: string, to: string, subject: string, bodyHtml: string)
|
|
429
|
+
Save a draft locally. Safe — does not send anything.
|
|
430
|
+
|
|
431
|
+
77. imap_send_template(accountId: string, to: string, templateId: string, vars: object)
|
|
432
|
+
Send an email using a built-in marketing template. templateId is one of:
|
|
433
|
+
"promo_product" | "newsletter" | "follow_up" | "offerta" | "evento" | "ringraziamento"
|
|
434
|
+
vars is a key-value object to replace [PLACEHOLDERS] in the template, e.g.:
|
|
435
|
+
{"AZIENDA": "Zeli Srl", "TITOLO OFFERTA": "Sconto 20%", "LINK_CTA": "https://..."}
|
|
436
|
+
ALWAYS confirm with user before sending.
|
|
437
|
+
|
|
438
|
+
78. imap_bulk_send(accountId: string, recipients: string[], subject: string, templateId: string, vars: object, perRecipientVars?: object)
|
|
439
|
+
Send a templated email to multiple recipients (marketing campaign).
|
|
440
|
+
recipients: array of email addresses.
|
|
441
|
+
vars: global placeholders applied to all.
|
|
442
|
+
perRecipientVars: optional object keyed by email with per-recipient overrides, e.g. {"mario@co.it": {"NOME": "Mario"}}.
|
|
443
|
+
Sends one-by-one with 1s delay to avoid spam filters. ALWAYS confirm with user — shows recipient count.
|
|
444
|
+
|
|
403
445
|
--- CANVAS ---
|
|
404
446
|
|
|
405
447
|
71. canvas_render(html: string, title?: string)
|
|
@@ -690,7 +732,7 @@ function driveIcon(type) {
|
|
|
690
732
|
* @param {string} [initialContext] — optional preloaded context (today's events, etc.)
|
|
691
733
|
* @returns {string}
|
|
692
734
|
*/
|
|
693
|
-
export function buildSystemPrompt(persona, personaDescription, config, initialContext) {
|
|
735
|
+
export async function buildSystemPrompt(persona, personaDescription, config, initialContext) {
|
|
694
736
|
const today = new Date().toISOString().split('T')[0];
|
|
695
737
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
696
738
|
|
|
@@ -734,6 +776,22 @@ export function buildSystemPrompt(persona, personaDescription, config, initialCo
|
|
|
734
776
|
}
|
|
735
777
|
}
|
|
736
778
|
|
|
779
|
+
// Inject IMAP accounts so the AI knows accountIds without calling imap_accounts()
|
|
780
|
+
try {
|
|
781
|
+
const { listAccounts } = await import('./email-db.mjs').catch(() => ({ listAccounts: null }));
|
|
782
|
+
if (listAccounts) {
|
|
783
|
+
const imapAccounts = listAccounts();
|
|
784
|
+
if (imapAccounts.length > 0) {
|
|
785
|
+
prompt += `\n\n--- IMAP EMAIL ACCOUNTS (custom, already configured) ---\n`;
|
|
786
|
+
prompt += `Use these accountIds directly — do NOT call imap_accounts() first.\n`;
|
|
787
|
+
for (const a of imapAccounts) {
|
|
788
|
+
prompt += `accountId: "${a.id}" | email: ${a.email_address} | name: "${a.display_name}" | status: ${a.sync_status}\n`;
|
|
789
|
+
}
|
|
790
|
+
prompt += `When the user mentions their company name, email domain, or display name, map it to the correct accountId above.`;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
} catch {}
|
|
794
|
+
|
|
737
795
|
if (initialContext) {
|
|
738
796
|
prompt += `\n\n--- CURRENT CONTEXT (fetched at session start) ---\n${initialContext}`;
|
|
739
797
|
}
|
|
@@ -940,6 +998,124 @@ export async function executeTool(action, params, config) {
|
|
|
940
998
|
return `Message marked as ${params.isRead !== false ? 'read' : 'unread'}.`;
|
|
941
999
|
}
|
|
942
1000
|
|
|
1001
|
+
case 'imap_reply': {
|
|
1002
|
+
if (!params.accountId || !params.messageId || !params.bodyHtml) return 'accountId, messageId, bodyHtml required.';
|
|
1003
|
+
const { getMessage: imapGetMsg2 } = await import('./email-db.mjs');
|
|
1004
|
+
const { sendEmail: imapSendReply } = await import('./email-smtp.mjs');
|
|
1005
|
+
const orig = imapGetMsg2(params.messageId);
|
|
1006
|
+
if (!orig) return 'Original message not found in local DB.';
|
|
1007
|
+
const replySubject = orig.subject?.startsWith('Re:') ? orig.subject : 'Re: ' + (orig.subject || '');
|
|
1008
|
+
let refs = [];
|
|
1009
|
+
try { refs = JSON.parse(orig.references_list || '[]'); } catch {}
|
|
1010
|
+
if (orig.message_id) refs.push(orig.message_id);
|
|
1011
|
+
const result = await imapSendReply(params.accountId, {
|
|
1012
|
+
to: orig.from_address,
|
|
1013
|
+
cc: params.cc || null,
|
|
1014
|
+
subject: replySubject,
|
|
1015
|
+
bodyHtml: params.bodyHtml,
|
|
1016
|
+
inReplyTo: orig.message_id || null,
|
|
1017
|
+
references: refs,
|
|
1018
|
+
});
|
|
1019
|
+
return `Reply sent to ${orig.from_address}. Message-ID: ${result.messageId}`;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
case 'imap_thread': {
|
|
1023
|
+
if (!params.accountId || !params.threadId) return 'accountId and threadId required.';
|
|
1024
|
+
const { getThread: imapGetThread } = await import('./email-db.mjs');
|
|
1025
|
+
const msgs = imapGetThread(params.threadId, params.accountId);
|
|
1026
|
+
if (!msgs.length) return 'No messages found in thread.';
|
|
1027
|
+
return msgs.map((m, i) => {
|
|
1028
|
+
const body = m.body_reply_only || m.body_text || m.body_preview || '(empty)';
|
|
1029
|
+
return `--- Message ${i + 1} ---\nFrom: ${m.from_name ? m.from_name + ' <' + m.from_address + '>' : m.from_address}\nDate: ${m.internal_date}\n\n${body.slice(0, 1500)}`;
|
|
1030
|
+
}).join('\n\n');
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
case 'imap_search': {
|
|
1034
|
+
if (!params.accountId || !params.query) return 'accountId and query required.';
|
|
1035
|
+
const { listMessages: imapSearch } = await import('./email-db.mjs');
|
|
1036
|
+
const result = imapSearch(params.accountId, null, params.limit || 20, 0, params.query);
|
|
1037
|
+
if (!result.messages.length) return `No messages found for "${params.query}".`;
|
|
1038
|
+
return result.messages.map(m =>
|
|
1039
|
+
`[${m.id}] ${m.is_read ? '' : '[UNREAD] '}From: ${m.from_name || m.from_address} | ${m.subject} | ${(m.internal_date || '').slice(0, 10)}\n ${(m.body_preview || '').slice(0, 100)}`
|
|
1040
|
+
).join('\n\n') + `\n\n(${result.total} total matches)`;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
case 'imap_mark_starred': {
|
|
1044
|
+
if (!params.messageId) return 'messageId required.';
|
|
1045
|
+
const { markStarred } = await import('./email-db.mjs');
|
|
1046
|
+
markStarred(params.messageId, params.isStarred !== false);
|
|
1047
|
+
return `Message ${params.isStarred !== false ? 'starred' : 'unstarred'}.`;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
case 'imap_trash': {
|
|
1051
|
+
if (!params.messageId) return 'messageId required.';
|
|
1052
|
+
const { softDelete } = await import('./email-db.mjs');
|
|
1053
|
+
softDelete(params.messageId);
|
|
1054
|
+
return 'Message moved to local Trash. Not deleted from IMAP server.';
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
case 'imap_draft': {
|
|
1058
|
+
if (!params.accountId || !params.to || !params.subject) return 'accountId, to, subject required.';
|
|
1059
|
+
const { saveDraft } = await import('./email-db.mjs');
|
|
1060
|
+
const id = saveDraft(params.accountId, {
|
|
1061
|
+
to: [{ address: params.to }],
|
|
1062
|
+
subject: params.subject,
|
|
1063
|
+
body_html: params.bodyHtml || '',
|
|
1064
|
+
});
|
|
1065
|
+
return `Draft saved with id: ${id}`;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
case 'imap_send_template': {
|
|
1069
|
+
if (!params.accountId || !params.to || !params.templateId || !params.vars) return 'accountId, to, templateId, vars required.';
|
|
1070
|
+
const { sendEmail: imapSendTpl } = await import('./email-smtp.mjs');
|
|
1071
|
+
const TEMPLATES = {
|
|
1072
|
+
promo_product: { subject: 'Scopri la nostra offerta su [PRODOTTO]', html: '<table width="100%" style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="background:#1a1a2e;padding:32px 40px;text-align:center"><h1 style="color:#00ff9d;margin:0">[AZIENDA]</h1></td></tr><tr><td style="padding:40px;background:#fff"><h2 style="color:#1a1a2e">[TITOLO OFFERTA]</h2><p style="color:#444;line-height:1.7">[DESCRIZIONE PRODOTTO/SERVIZIO]</p><p style="color:#444;line-height:1.7">[DETTAGLIO BENEFICI O SPECIFICHE]</p><table><tr><td style="background:#00ff9d;border-radius:6px;padding:14px 32px"><a href="[LINK_CTA]" style="color:#1a1a2e;font-weight:700;text-decoration:none">[TESTO CTA]</a></td></tr></table></td></tr><tr><td style="padding:24px 40px;background:#f5f5f5;text-align:center"><p style="color:#888;font-size:12px">[AZIENDA] • [INDIRIZZO] • [EMAIL]</p></td></tr></table>' },
|
|
1073
|
+
newsletter: { subject: '[AZIENDA] Newsletter — [MESE] [ANNO]', html: '<table width="100%" style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="background:#0f0f1a;padding:28px 40px;border-bottom:3px solid #00ff9d"><h1 style="color:#fff;margin:0">[AZIENDA]</h1><p style="color:#00ff9d;margin:6px 0 0;font-size:13px">Newsletter [MESE] [ANNO]</p></td></tr><tr><td style="padding:36px 40px;background:#fff"><h2 style="color:#1a1a2e">[TITOLO PRINCIPALE]</h2><p style="color:#555;line-height:1.8">[TESTO PRINCIPALE]</p><hr style="border:none;border-top:1px solid #eee;margin:24px 0"><h3 style="color:#1a1a2e">[TITOLO SEZIONE 2]</h3><p style="color:#555;line-height:1.8">[TESTO SEZIONE 2]</p></td></tr><tr><td style="padding:20px 40px;background:#f9f9f9;text-align:center"><p style="color:#999;font-size:12px">© [ANNO] [AZIENDA]</p></td></tr></table>' },
|
|
1074
|
+
follow_up: { subject: 'Seguito alla nostra conversazione — [ARGOMENTO]', html: '<table width="100%" style="max-width:580px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="padding:40px"><p style="color:#333;line-height:1.7">Gentile [NOME],</p><p style="color:#333;line-height:1.7">la contatto in seguito a [CONTESTO].</p><p style="color:#333;line-height:1.7">[CORPO PRINCIPALE]</p><p style="color:#333;line-height:1.7">[CHIUSURA]</p><p style="color:#333">Cordiali saluti,</p><p style="color:#1a1a2e;font-weight:700">[NOME MITTENTE]</p><p style="color:#888;font-size:13px">[RUOLO] • [AZIENDA] • [TELEFONO]</p></td></tr></table>' },
|
|
1075
|
+
offerta: { subject: 'Offerta [NUMERO] — [OGGETTO FORNITURA]', html: '<table width="100%" style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="background:#1a1a2e;padding:24px 40px"><h1 style="color:#00ff9d;margin:0">[AZIENDA]</h1><p style="color:#aaa;margin:4px 0 0;font-size:12px">Offerta n. [NUMERO] del [DATA]</p></td></tr><tr><td style="padding:36px 40px;background:#fff"><p style="color:#333;line-height:1.7">Gentile [NOME],</p><p style="color:#555;font-size:13px"><strong>Condizioni di pagamento:</strong> [PAGAMENTO]</p><p style="color:#555;font-size:13px"><strong>Tempi di consegna:</strong> [CONSEGNA]</p><p style="color:#555;font-size:13px"><strong>Validita offerta:</strong> [VALIDITA]</p><p style="color:#333">Cordiali saluti,</p><p style="color:#1a1a2e;font-weight:700">[NOME MITTENTE]</p></td></tr></table>' },
|
|
1076
|
+
evento: { subject: 'Sei invitato: [NOME EVENTO] — [DATA]', html: '<table width="100%" style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="background:linear-gradient(135deg,#0f0f1a,#1a2a1a);padding:48px 40px;text-align:center"><h1 style="color:#00ff9d;margin:0">[NOME EVENTO]</h1><p style="color:#aaffcc;margin:8px 0 0">[DATA] • [ORA] • [LUOGO]</p></td></tr><tr><td style="padding:40px;background:#fff;text-align:center"><p style="color:#444;line-height:1.8;max-width:460px;margin:0 auto 28px">[DESCRIZIONE EVENTO]</p><table style="margin:0 auto"><tr><td style="background:#00ff9d;border-radius:8px;padding:14px 40px"><a href="[LINK_REGISTRAZIONE]" style="color:#0f0f1a;font-weight:700;text-decoration:none">Registrati ora</a></td></tr></table></td></tr></table>' },
|
|
1077
|
+
ringraziamento: { subject: 'Grazie per la fiducia, [NOME]', html: '<table width="100%" style="max-width:580px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="background:#0f0f1a;padding:32px 40px;text-align:center"><h1 style="color:#fff;font-size:22px">Grazie, [NOME]!</h1></td></tr><tr><td style="padding:40px;background:#fff"><p style="color:#333;line-height:1.8">Volevamo ringraziarti per [MOTIVO].</p><p style="color:#333;line-height:1.8">[MESSAGGIO PERSONALE]</p><p style="color:#333">Con stima,</p><p style="color:#1a1a2e;font-weight:700">[NOME MITTENTE]</p></td></tr></table>' },
|
|
1078
|
+
};
|
|
1079
|
+
const tpl = TEMPLATES[params.templateId];
|
|
1080
|
+
if (!tpl) return `Unknown templateId "${params.templateId}". Valid: ${Object.keys(TEMPLATES).join(', ')}`;
|
|
1081
|
+
const applyVars = (str, vars) => Object.entries(vars).reduce((s, [k, v]) => s.split('[' + k + ']').join(v || ''), str);
|
|
1082
|
+
const subject = applyVars(tpl.subject, params.vars);
|
|
1083
|
+
const html = applyVars(tpl.html, params.vars);
|
|
1084
|
+
const result = await imapSendTpl(params.accountId, { to: params.to, subject, bodyHtml: html });
|
|
1085
|
+
return `Template email "${params.templateId}" sent to ${params.to}. Message-ID: ${result.messageId}`;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
case 'imap_bulk_send': {
|
|
1089
|
+
if (!params.accountId || !params.recipients?.length || !params.subject || !params.templateId) return 'accountId, recipients, subject, templateId required.';
|
|
1090
|
+
const { sendEmail: imapSendBulk } = await import('./email-smtp.mjs');
|
|
1091
|
+
const BULK_TEMPLATES = {
|
|
1092
|
+
promo_product: '<table width="100%" style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="background:#1a1a2e;padding:32px 40px;text-align:center"><h1 style="color:#00ff9d;margin:0">[AZIENDA]</h1></td></tr><tr><td style="padding:40px;background:#fff"><h2 style="color:#1a1a2e">[TITOLO OFFERTA]</h2><p style="color:#444;line-height:1.7">[DESCRIZIONE PRODOTTO/SERVIZIO]</p><table><tr><td style="background:#00ff9d;border-radius:6px;padding:14px 32px"><a href="[LINK_CTA]" style="color:#1a1a2e;font-weight:700;text-decoration:none">[TESTO CTA]</a></td></tr></table></td></tr></table>',
|
|
1093
|
+
newsletter: '<table width="100%" style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="background:#0f0f1a;padding:28px 40px;border-bottom:3px solid #00ff9d"><h1 style="color:#fff;margin:0">[AZIENDA]</h1></td></tr><tr><td style="padding:36px 40px;background:#fff"><h2 style="color:#1a1a2e">[TITOLO PRINCIPALE]</h2><p style="color:#555;line-height:1.8">[TESTO PRINCIPALE]</p></td></tr></table>',
|
|
1094
|
+
follow_up: '<table width="100%" style="max-width:580px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="padding:40px"><p style="color:#333;line-height:1.7">Gentile [NOME],</p><p style="color:#333;line-height:1.7">[CORPO PRINCIPALE]</p><p style="color:#1a1a2e;font-weight:700">[NOME MITTENTE]</p></td></tr></table>',
|
|
1095
|
+
offerta: '<table width="100%" style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="background:#1a1a2e;padding:24px 40px"><h1 style="color:#00ff9d;margin:0">[AZIENDA]</h1></td></tr><tr><td style="padding:36px 40px;background:#fff"><p style="color:#333;line-height:1.7">Gentile [NOME],</p><p style="color:#555">[CORPO PRINCIPALE]</p><p style="color:#1a1a2e;font-weight:700">[NOME MITTENTE]</p></td></tr></table>',
|
|
1096
|
+
evento: '<table width="100%" style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="background:#0f0f1a;padding:48px 40px;text-align:center"><h1 style="color:#00ff9d;margin:0">[NOME EVENTO]</h1><p style="color:#aaffcc">[DATA]</p></td></tr><tr><td style="padding:40px;background:#fff;text-align:center"><p style="color:#444;line-height:1.8">[DESCRIZIONE EVENTO]</p><table style="margin:0 auto"><tr><td style="background:#00ff9d;border-radius:8px;padding:14px 40px"><a href="[LINK_REGISTRAZIONE]" style="color:#0f0f1a;font-weight:700;text-decoration:none">Registrati</a></td></tr></table></td></tr></table>',
|
|
1097
|
+
ringraziamento: '<table width="100%" style="max-width:580px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="background:#0f0f1a;padding:32px 40px;text-align:center"><h1 style="color:#fff">Grazie, [NOME]!</h1></td></tr><tr><td style="padding:40px;background:#fff"><p style="color:#333;line-height:1.8">[MESSAGGIO PERSONALE]</p><p style="color:#1a1a2e;font-weight:700">[NOME MITTENTE]</p></td></tr></table>',
|
|
1098
|
+
};
|
|
1099
|
+
const baseTplHtml = BULK_TEMPLATES[params.templateId];
|
|
1100
|
+
if (!baseTplHtml) return `Unknown templateId. Valid: ${Object.keys(BULK_TEMPLATES).join(', ')}`;
|
|
1101
|
+
const applyVars2 = (str, vars) => Object.entries(vars).reduce((s, [k, v]) => s.split('[' + k + ']').join(v || ''), str);
|
|
1102
|
+
const results = [];
|
|
1103
|
+
for (const email of params.recipients) {
|
|
1104
|
+
const perVars = { ...(params.vars || {}), ...((params.perRecipientVars || {})[email] || {}) };
|
|
1105
|
+
const subject = applyVars2(params.subject, perVars);
|
|
1106
|
+
const html = applyVars2(baseTplHtml, perVars);
|
|
1107
|
+
try {
|
|
1108
|
+
await imapSendBulk(params.accountId, { to: email, subject, bodyHtml: html });
|
|
1109
|
+
results.push(`OK: ${email}`);
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
results.push(`FAIL: ${email} — ${e.message}`);
|
|
1112
|
+
}
|
|
1113
|
+
await new Promise(r => setTimeout(r, 1200)); // 1.2s delay between sends
|
|
1114
|
+
}
|
|
1115
|
+
const ok = results.filter(r => r.startsWith('OK')).length;
|
|
1116
|
+
return `Bulk send complete: ${ok}/${params.recipients.length} sent.\n${results.join('\n')}`;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
943
1119
|
// ── Calendar ──────────────────────────────────────────────────────────
|
|
944
1120
|
case 'calendar_today': {
|
|
945
1121
|
const events = await getTodayEvents(config);
|