nothumanallowed 13.5.117 → 13.5.119

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.119",
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.119';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -448,8 +448,9 @@ export function insertMessage(data) {
448
448
  data.has_attachments ? 1 : 0, data.content_hash || null,
449
449
  data.internal_date, data.source || 'imap'
450
450
  );
451
- // Init read state (unread)
452
- db.prepare('INSERT OR IGNORE INTO email_message_state (message_id, is_read, is_starred) VALUES (?, 0, 0)').run(id);
451
+ // Init read state from IMAP \Seen flag
452
+ const isRead = data.imap_seen ? 1 : 0;
453
+ db.prepare('INSERT OR IGNORE INTO email_message_state (message_id, is_read, is_starred) VALUES (?, ?, 0)').run(id, isRead);
453
454
  return id;
454
455
  }
455
456
 
@@ -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,42 @@ 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,
299
+ imap_seen: hdr.flags?.has('\\Seen') ?? false,
271
300
  source: 'imap',
272
301
  });
273
302
 
274
303
  if (attachments.length > 0) insertAttachments(msgDbId, attachments);
275
304
 
276
- // Apply archiving rules (first match wins, removes from inbox)
277
305
  const archived = applyArchivingRules(accountId, msgDbId, fromAddr, subject);
278
306
  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;
307
+ const isSentFolder = folderPath.toLowerCase().includes('sent') || folderPath.toLowerCase().includes('inviati');
308
+ const targetLabel = isSentFolder ? sentLabel : inboxLabel;
282
309
  if (targetLabel) addMessageToLabel(msgDbId, targetLabel.id);
283
310
  }
284
311
 
285
312
  synced++;
286
313
  }
287
314
 
288
- // Update folder sync state
289
- upsertFolder(accountId, folderPath,
290
- folderPath, folderMeta?.folder_type || 'custom',
291
- currentUidValidity, newLastUid);
315
+ upsertFolder(accountId, folderPath, folderPath,
316
+ folderMeta?.folder_type || 'custom', currentUidValidity, newLastUid);
292
317
 
293
- return { synced, lastUid: newLastUid };
318
+ updateLabelCounts(accountId);
319
+ return { synced, lastUid: newLastUid, total: totalMessages };
294
320
  } finally {
295
321
  lock.release();
296
322
  }
297
323
  }
298
324
 
299
- export async function syncAccount(accountId) {
325
+ // First sync: cap to last 200 messages per folder to avoid blocking for minutes
326
+ const FIRST_SYNC_LIMIT = 200;
327
+
328
+ export async function syncAccount(accountId, opts = {}) {
300
329
  setSyncStatus(accountId, 'syncing', null);
301
330
  try {
302
- // List folders from IMAP and sync INBOX + Sent
303
331
  const folders = await listImapFolders(accountId);
304
332
  const priority = ['inbox', 'sent'];
305
333
  const toSync = [
@@ -310,8 +338,10 @@ export async function syncAccount(accountId) {
310
338
  let totalSynced = 0;
311
339
  for (const f of toSync) {
312
340
  try {
313
- const result = await syncFolder(accountId, f.path, false);
341
+ const limit = opts.full ? 0 : FIRST_SYNC_LIMIT;
342
+ const result = await syncFolder(accountId, f.path, false, limit);
314
343
  totalSynced += result.synced;
344
+ console.log(`[email:sync] ${f.path}: ${result.synced} new messages (total on server: ${result.total})`);
315
345
  } catch (err) {
316
346
  console.warn(`[email:imap] Sync folder ${f.path} failed:`, err.message);
317
347
  }
@@ -1463,18 +1463,83 @@ function emailRenderReadingPane(pane, m) {
1463
1463
  pane.innerHTML = '<div style="display:flex;flex-direction:column;height:100%">' + h + '</div>';
1464
1464
  }
1465
1465
 
1466
+ var EMAIL_TEMPLATES = [
1467
+ {
1468
+ id: 'promo_product',
1469
+ name: 'Promozione prodotto',
1470
+ subject: 'Scopri la nostra offerta su [PRODOTTO]',
1471
+ html: '<table width="100%" cellpadding="0" cellspacing="0" 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;font-size:26px;letter-spacing:-0.5px">[AZIENDA]</h1></td></tr><tr><td style="padding:40px;background:#ffffff"><h2 style="color:#1a1a2e;font-size:22px;margin:0 0 16px">[TITOLO OFFERTA]</h2><p style="color:#444;font-size:15px;line-height:1.7;margin:0 0 24px">[DESCRIZIONE PRODOTTO/SERVIZIO]</p><p style="color:#444;font-size:15px;line-height:1.7;margin:0 0 32px">[DETTAGLIO BENEFICI O SPECIFICHE]</p><table cellpadding="0" cellspacing="0"><tr><td style="background:#00ff9d;border-radius:6px;padding:14px 32px"><a href="[LINK_CTA]" style="color:#1a1a2e;font-weight:700;font-size:15px;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;margin:0">[AZIENDA] &bull; [INDIRIZZO] &bull; <a href="mailto:[EMAIL]" style="color:#888">[EMAIL]</a></p><p style="color:#bbb;font-size:11px;margin:8px 0 0"><a href="[UNSUBSCRIBE_LINK]" style="color:#bbb">Disiscriviti</a></p></td></tr></table>',
1472
+ },
1473
+ {
1474
+ id: 'newsletter',
1475
+ name: 'Newsletter mensile',
1476
+ subject: '[AZIENDA] Newsletter — [MESE] [ANNO]',
1477
+ html: '<table width="100%" cellpadding="0" cellspacing="0" 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:#ffffff;margin:0;font-size:22px">[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:#ffffff"><h2 style="color:#1a1a2e;font-size:20px;margin:0 0 12px">[TITOLO PRINCIPALE]</h2><p style="color:#555;font-size:14px;line-height:1.8;margin:0 0 28px">[TESTO PRINCIPALE — racconta la novità principale del mese]</p><hr style="border:none;border-top:1px solid #eee;margin:0 0 28px"><h3 style="color:#1a1a2e;font-size:16px;margin:0 0 10px">[TITOLO SEZIONE 2]</h3><p style="color:#555;font-size:14px;line-height:1.8;margin:0 0 28px">[TESTO SEZIONE 2]</p><hr style="border:none;border-top:1px solid #eee;margin:0 0 28px"><h3 style="color:#1a1a2e;font-size:16px;margin:0 0 10px">[TITOLO SEZIONE 3]</h3><p style="color:#555;font-size:14px;line-height:1.8;margin:0 0 28px">[TESTO SEZIONE 3]</p><table cellpadding="0" cellspacing="0"><tr><td style="background:#1a1a2e;border-radius:6px;padding:12px 28px"><a href="[LINK]" style="color:#00ff9d;font-weight:700;font-size:14px;text-decoration:none">Leggi di più &rarr;</a></td></tr></table></td></tr><tr><td style="padding:20px 40px;background:#f9f9f9;text-align:center"><p style="color:#999;font-size:12px;margin:0">&copy; [ANNO] [AZIENDA] &bull; <a href="[UNSUBSCRIBE_LINK]" style="color:#999">Disiscriviti</a></p></td></tr></table>',
1478
+ },
1479
+ {
1480
+ id: 'follow_up',
1481
+ name: 'Follow-up commerciale',
1482
+ subject: 'Seguito alla nostra conversazione — [ARGOMENTO]',
1483
+ html: '<table width="100%" cellpadding="0" cellspacing="0" style="max-width:580px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="padding:40px"><p style="color:#333;font-size:15px;line-height:1.7;margin:0 0 16px">Gentile [NOME],</p><p style="color:#333;font-size:15px;line-height:1.7;margin:0 0 16px">la contatto in seguito a [CONTESTO: fiera / chiamata / incontro del GIORNO].</p><p style="color:#333;font-size:15px;line-height:1.7;margin:0 0 16px">[CORPO PRINCIPALE: riassumi la proposta, il valore offerto, eventuali prossimi passi]</p><p style="color:#333;font-size:15px;line-height:1.7;margin:0 0 32px">[CHIUSURA: proposta di call / richiesta feedback / disponibilit&agrave; a incontro]</p><p style="color:#333;font-size:15px;line-height:1.7;margin:0 0 4px">Cordiali saluti,</p><p style="color:#1a1a2e;font-size:15px;font-weight:700;margin:0">[NOME MITTENTE]</p><p style="color:#888;font-size:13px;margin:4px 0 0">[RUOLO] &bull; [AZIENDA] &bull; [TELEFONO]</p></td></tr></table>',
1484
+ },
1485
+ {
1486
+ id: 'offerta',
1487
+ name: 'Invio offerta / preventivo',
1488
+ subject: 'Offerta [NUMERO] — [OGGETTO FORNITURA]',
1489
+ html: '<table width="100%" cellpadding="0" cellspacing="0" 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;font-size:20px">[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:#ffffff"><p style="color:#333;font-size:15px;line-height:1.7;margin:0 0 16px">Gentile [NOME],</p><p style="color:#333;font-size:15px;line-height:1.7;margin:0 0 24px">in risposta alla Vostra richiesta, siamo lieti di sottoporre la seguente offerta:</p><table width="100%" cellpadding="10" cellspacing="0" style="border-collapse:collapse;margin-bottom:24px"><tr style="background:#f0f0f0"><th style="text-align:left;font-size:13px;color:#333;border-bottom:2px solid #ddd">Descrizione</th><th style="text-align:right;font-size:13px;color:#333;border-bottom:2px solid #ddd">Qt&agrave;</th><th style="text-align:right;font-size:13px;color:#333;border-bottom:2px solid #ddd">Prezzo unit.</th><th style="text-align:right;font-size:13px;color:#333;border-bottom:2px solid #ddd">Totale</th></tr><tr><td style="font-size:13px;color:#444;border-bottom:1px solid #eee">[ARTICOLO 1]</td><td style="text-align:right;font-size:13px;color:#444;border-bottom:1px solid #eee">[QT]</td><td style="text-align:right;font-size:13px;color:#444;border-bottom:1px solid #eee">&euro; [PREZZO]</td><td style="text-align:right;font-size:13px;color:#444;border-bottom:1px solid #eee">&euro; [TOT]</td></tr><tr><td style="font-size:13px;color:#444;border-bottom:1px solid #eee">[ARTICOLO 2]</td><td style="text-align:right;font-size:13px;color:#444;border-bottom:1px solid #eee">[QT]</td><td style="text-align:right;font-size:13px;color:#444;border-bottom:1px solid #eee">&euro; [PREZZO]</td><td style="text-align:right;font-size:13px;color:#444;border-bottom:1px solid #eee">&euro; [TOT]</td></tr><tr style="background:#f9f9f9"><td colspan="3" style="text-align:right;font-weight:700;font-size:14px;color:#1a1a2e;padding-top:12px">Totale IVA esclusa</td><td style="text-align:right;font-weight:700;font-size:14px;color:#1a1a2e;padding-top:12px">&euro; [TOTALE]</td></tr></table><p style="color:#555;font-size:13px;line-height:1.7;margin:0 0 8px"><strong>Condizioni di pagamento:</strong> [PAGAMENTO]</p><p style="color:#555;font-size:13px;line-height:1.7;margin:0 0 8px"><strong>Tempi di consegna:</strong> [CONSEGNA]</p><p style="color:#555;font-size:13px;line-height:1.7;margin:0 0 24px"><strong>Validit&agrave; offerta:</strong> [VALIDITA]</p><p style="color:#333;font-size:14px;line-height:1.7;margin:0 0 4px">Resto a disposizione per qualsiasi chiarimento.</p><p style="color:#333;font-size:14px;line-height:1.7;margin:0 0 4px">Cordiali saluti,</p><p style="color:#1a1a2e;font-size:14px;font-weight:700;margin:0">[NOME MITTENTE]</p><p style="color:#888;font-size:12px;margin:4px 0 0">[AZIENDA] &bull; [EMAIL] &bull; [TELEFONO]</p></td></tr></table>',
1490
+ },
1491
+ {
1492
+ id: 'evento',
1493
+ name: 'Invito evento / webinar',
1494
+ subject: 'Sei invitato: [NOME EVENTO] — [DATA]',
1495
+ html: '<table width="100%" cellpadding="0" cellspacing="0" 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 0 8px;font-size:28px;letter-spacing:-0.5px">[NOME EVENTO]</h1><p style="color:#aaffcc;margin:0;font-size:16px">[DATA] &bull; [ORA] &bull; [LUOGO / Online]</p></td></tr><tr><td style="padding:40px;background:#ffffff;text-align:center"><p style="color:#444;font-size:15px;line-height:1.8;max-width:460px;margin:0 auto 28px">[DESCRIZIONE EVENTO: di cosa si tratta, a chi &egrave; rivolto, perch&eacute; non perderselo]</p><p style="color:#555;font-size:14px;line-height:1.8;max-width:460px;margin:0 auto 32px">[AGENDA O PUNTI CHIAVE]</p><table cellpadding="0" cellspacing="0" style="margin:0 auto 32px"><tr><td style="background:#00ff9d;border-radius:8px;padding:14px 40px"><a href="[LINK_REGISTRAZIONE]" style="color:#0f0f1a;font-weight:700;font-size:15px;text-decoration:none">Registrati ora &rarr;</a></td></tr></table><p style="color:#999;font-size:12px;margin:0">Posti limitati &bull; Registrazione gratuita</p></td></tr><tr><td style="padding:20px 40px;background:#f5f5f5;text-align:center"><p style="color:#bbb;font-size:11px;margin:0">[AZIENDA] &bull; <a href="[UNSUBSCRIBE_LINK]" style="color:#bbb">Disiscriviti</a></p></td></tr></table>',
1496
+ },
1497
+ {
1498
+ id: 'ringraziamento',
1499
+ name: 'Ringraziamento cliente',
1500
+ subject: 'Grazie per la fiducia, [NOME]',
1501
+ html: '<table width="100%" cellpadding="0" cellspacing="0" style="max-width:580px;margin:0 auto;font-family:Arial,sans-serif"><tr><td style="background:#0f0f1a;padding:32px 40px;text-align:center"><p style="color:#00ff9d;font-size:36px;margin:0">&#128591;</p><h1 style="color:#ffffff;font-size:22px;margin:8px 0 0">Grazie, [NOME]!</h1></td></tr><tr><td style="padding:40px;background:#ffffff"><p style="color:#333;font-size:15px;line-height:1.8;margin:0 0 16px">Volevamo ringraziarti personalmente per [MOTIVO: il tuo acquisto / la tua fiducia / anni di collaborazione].</p><p style="color:#333;font-size:15px;line-height:1.8;margin:0 0 16px">[MESSAGGIO PERSONALE: cosa significa per noi, cosa ci impegniamo a fare, cosa offriamo come segno di riconoscimento]</p><p style="color:#333;font-size:15px;line-height:1.8;margin:0 0 32px">[CHIUSURA: invito a restare in contatto, disponibilit&agrave;]</p><p style="color:#333;font-size:15px;margin:0 0 4px">Con stima,</p><p style="color:#1a1a2e;font-size:15px;font-weight:700;margin:0">[NOME MITTENTE]</p><p style="color:#888;font-size:13px;margin:4px 0 0">[RUOLO] &bull; [AZIENDA]</p></td></tr></table>',
1502
+ },
1503
+ ];
1504
+
1505
+ function emailLoadTemplate(idx) {
1506
+ var t = EMAIL_TEMPLATES[idx];
1507
+ if (!t) return;
1508
+ var subj = document.getElementById('compSubject');
1509
+ if (subj && !subj.value) subj.value = t.subject;
1510
+ if (emailState.quillEditor) {
1511
+ emailState.quillEditor.clipboard.dangerouslyPasteHTML(t.html);
1512
+ }
1513
+ var dd = document.getElementById('compTemplateDrop');
1514
+ if (dd) dd.style.display = 'none';
1515
+ }
1516
+
1517
+ function emailToggleTemplates() {
1518
+ var dd = document.getElementById('compTemplateDrop');
1519
+ if (!dd) return;
1520
+ dd.style.display = dd.style.display === 'none' ? 'block' : 'none';
1521
+ }
1522
+
1466
1523
  function emailOpenCompose(opts) {
1467
1524
  var pane = document.getElementById('emailPane');
1468
1525
  if (!pane) return;
1469
1526
  opts = opts || {};
1470
1527
 
1471
- var h = '<div style="display:flex;flex-direction:column;height:100%;padding:14px;gap:10px">';
1528
+ var h = '<div style="display:flex;flex-direction:column;height:100%;padding:14px;gap:8px">';
1472
1529
  h += '<div style="font-size:13px;font-weight:700;color:var(--bright);border-bottom:1px solid var(--border);padding-bottom:8px">Compose</div>';
1473
1530
  h += '<input id="compTo" type="text" placeholder="To" value="' + esc(opts.to || '') + '" style="padding:7px 10px;font-size:12px;background:var(--bg);color:var(--fg);border:1px solid var(--border2);border-radius:5px">';
1474
1531
  h += '<input id="compCc" type="text" placeholder="Cc" style="padding:7px 10px;font-size:12px;background:var(--bg);color:var(--fg);border:1px solid var(--border2);border-radius:5px">';
1475
1532
  h += '<input id="compSubject" type="text" placeholder="Subject" value="' + esc(opts.subject || '') + '" style="padding:7px 10px;font-size:12px;background:var(--bg);color:var(--fg);border:1px solid var(--border2);border-radius:5px">';
1476
1533
  h += '<input type="hidden" id="compInReplyTo" value="' + esc(opts.inReplyTo || '') + '">';
1477
1534
  h += '<input type="hidden" id="compReplyToId" value="' + esc(opts.replyTo || '') + '">';
1535
+ // Template picker
1536
+ h += '<div style="position:relative">';
1537
+ h += '<button onclick="emailToggleTemplates()" style="padding:5px 12px;font-size:11px;background:var(--bg);color:var(--cyan);border:1px solid var(--cyan);border-radius:5px;cursor:pointer">&#128196; Template marketing</button>';
1538
+ h += '<div id="compTemplateDrop" style="display:none;position:absolute;top:100%;left:0;z-index:200;background:var(--bg2);border:1px solid var(--border);border-radius:6px;min-width:240px;padding:4px 0;margin-top:4px;box-shadow:0 4px 16px rgba(0,0,0,0.3)">';
1539
+ for (var ti = 0; ti < EMAIL_TEMPLATES.length; ti++) {
1540
+ h += '<div class="tpl-item" onclick="emailLoadTemplate(' + ti + ')" style="padding:9px 16px;font-size:12px;cursor:pointer;color:var(--fg);border-bottom:1px solid var(--border)"><strong>' + esc(EMAIL_TEMPLATES[ti].name) + '</strong><div style="font-size:10px;color:var(--dim);margin-top:2px">' + esc(EMAIL_TEMPLATES[ti].subject.slice(0, 50)) + '</div></div>';
1541
+ }
1542
+ h += '</div></div>';
1478
1543
  // Quill editor container
1479
1544
  h += '<div id="quillContainer" style="flex:1;min-height:200px;background:#fff;border-radius:5px;overflow:auto"></div>';
1480
1545
  // Toolbar