@agenticmail/enterprise 0.5.126 → 0.5.128

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.
@@ -470,6 +470,7 @@ export function createAdminRoutes(db: DatabaseAdapter) {
470
470
  { field: 'cfApiToken', type: 'string', maxLength: 500 },
471
471
  { field: 'cfAccountId', type: 'string', maxLength: 100 },
472
472
  { field: 'plan', type: 'string', maxLength: 32 },
473
+ { field: 'signatureTemplate', type: 'string', maxLength: 10000 },
473
474
  ]);
474
475
 
475
476
  const settings = await db.updateSettings(body);
@@ -692,5 +692,63 @@ export function createGmailTools(config: GoogleToolsConfig, _options?: ToolCreat
692
692
  } catch (e: any) { return errorResult(e.message); }
693
693
  },
694
694
  },
695
+
696
+ // ─── Signature Management ────────────────────────────
697
+ {
698
+ name: 'gmail_get_signature',
699
+ description: 'Get the current email signature for the primary send-as alias.',
700
+ category: 'utility' as const,
701
+ parameters: {
702
+ type: 'object' as const,
703
+ properties: {},
704
+ required: [],
705
+ },
706
+ async execute(_id: string) {
707
+ try {
708
+ const token = await getToken();
709
+ // Get send-as settings for the primary email
710
+ const sendAs = await gmail(token, '/settings/sendAs');
711
+ const primary = sendAs.sendAs?.find((s: any) => s.isPrimary) || sendAs.sendAs?.[0];
712
+ if (!primary) return errorResult('No primary send-as alias found');
713
+ return jsonResult({
714
+ email: primary.sendAsEmail,
715
+ displayName: primary.displayName,
716
+ signature: primary.signature || '(no signature set)',
717
+ isDefault: primary.isDefault,
718
+ });
719
+ } catch (e: any) { return errorResult(e.message); }
720
+ },
721
+ },
722
+ {
723
+ name: 'gmail_set_signature',
724
+ description: 'Set or update the email signature for the primary send-as alias. Accepts HTML for rich signatures with images, links, and formatting.',
725
+ category: 'utility' as const,
726
+ parameters: {
727
+ type: 'object' as const,
728
+ properties: {
729
+ signature: { type: 'string', description: 'HTML signature content. Use HTML tags for formatting: <b>bold</b>, <a href="...">links</a>, <img src="..."> for logos, <br> for line breaks, <table> for layout.' },
730
+ displayName: { type: 'string', description: 'Display name for the send-as alias (optional)' },
731
+ },
732
+ required: ['signature'],
733
+ },
734
+ async execute(_id: string, params: any) {
735
+ try {
736
+ const token = await getToken();
737
+ // Get primary send-as email
738
+ const sendAs = await gmail(token, '/settings/sendAs');
739
+ const primary = sendAs.sendAs?.find((s: any) => s.isPrimary) || sendAs.sendAs?.[0];
740
+ if (!primary) return errorResult('No primary send-as alias found');
741
+
742
+ const update: any = { signature: params.signature };
743
+ if (params.displayName) update.displayName = params.displayName;
744
+
745
+ const result = await gmail(token, `/settings/sendAs/${encodeURIComponent(primary.sendAsEmail)}`, {
746
+ method: 'PATCH',
747
+ body: update,
748
+ });
749
+ return jsonResult({ updated: true, email: primary.sendAsEmail, signatureLength: params.signature.length });
750
+ } catch (e: any) { return errorResult(e.message); }
751
+ },
752
+ },
695
753
  ];
696
754
  }
package/src/cli-agent.ts CHANGED
@@ -418,7 +418,45 @@ Available tools: gmail_send (to, subject, body) or agenticmail_send (to, subject
418
418
  console.error(`[onboarding] Error: ${err.message}`);
419
419
  }
420
420
 
421
- // 12. Start email inbox polling loop
421
+ // 12. Auto-setup Gmail signature from org template
422
+ try {
423
+ const orgSettings = await db.getSettings();
424
+ const sigTemplate = (orgSettings as any)?.signatureTemplate;
425
+ if (sigTemplate && currentAccessToken) {
426
+ const agentName = config.displayName || config.name;
427
+ const role = config.identity?.role || 'AI Agent';
428
+ const agentEmailAddr = config.email?.address || emailConfig?.email || '';
429
+ const companyName = orgSettings?.name || '';
430
+ const logoUrl = orgSettings?.logoUrl || '';
431
+
432
+ const signature = sigTemplate
433
+ .replace(/\{\{name\}\}/g, agentName)
434
+ .replace(/\{\{role\}\}/g, role)
435
+ .replace(/\{\{email\}\}/g, agentEmailAddr)
436
+ .replace(/\{\{company\}\}/g, companyName)
437
+ .replace(/\{\{logo\}\}/g, logoUrl)
438
+ .replace(/\{\{phone\}\}/g, '');
439
+
440
+ // Set Gmail signature via API
441
+ const sendAsRes = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/settings/sendAs', {
442
+ headers: { Authorization: `Bearer ${currentAccessToken}` },
443
+ });
444
+ const sendAs = await sendAsRes.json() as any;
445
+ const primary = sendAs.sendAs?.find((s: any) => s.isPrimary) || sendAs.sendAs?.[0];
446
+ if (primary) {
447
+ await fetch(`https://gmail.googleapis.com/gmail/v1/users/me/settings/sendAs/${encodeURIComponent(primary.sendAsEmail)}`, {
448
+ method: 'PATCH',
449
+ headers: { Authorization: `Bearer ${currentAccessToken}`, 'Content-Type': 'application/json' },
450
+ body: JSON.stringify({ signature }),
451
+ });
452
+ console.log(`[signature] ✅ Gmail signature set for ${primary.sendAsEmail}`);
453
+ }
454
+ }
455
+ } catch (sigErr: any) {
456
+ console.log(`[signature] Skipped: ${sigErr.message}`);
457
+ }
458
+
459
+ // 13. Start email inbox polling loop
422
460
  startEmailPolling(AGENT_ID, config, lifecycle, runtime, engineDb, memoryManager);
423
461
  }, 3000);
424
462
  }
@@ -509,12 +547,11 @@ async function startEmailPolling(
509
547
  try {
510
548
  const messages = await emailProvider.listMessages('INBOX', { limit: 20 });
511
549
  const newMessages = messages.filter(m => !processedIds.has(m.uid));
512
- const unread = newMessages.filter(m => !m.read);
513
550
  if (newMessages.length > 0) {
514
- console.log(`[email-poll] Found ${newMessages.length} new messages, ${unread.length} unread`);
551
+ console.log(`[email-poll] Found ${newMessages.length} new messages`);
515
552
  }
516
553
 
517
- for (const envelope of unread) {
554
+ for (const envelope of newMessages) {
518
555
  processedIds.add(envelope.uid);
519
556
 
520
557
  // Skip emails from ourselves
@@ -529,8 +566,10 @@ async function startEmailPolling(
529
566
  try { await emailProvider.markRead(envelope.uid, 'INBOX'); } catch {}
530
567
 
531
568
  // Format as agent message and spawn a session
569
+ const emailUid = envelope.uid;
532
570
  const emailText = [
533
571
  `[Inbound Email]`,
572
+ `Message-ID: ${emailUid}`,
534
573
  `From: ${fullMsg.from?.name ? `${fullMsg.from.name} <${fullMsg.from.email}>` : fullMsg.from?.email}`,
535
574
  `Subject: ${fullMsg.subject}`,
536
575
  fullMsg.inReplyTo ? `In-Reply-To: ${fullMsg.inReplyTo}` : '',
@@ -572,9 +611,19 @@ async function startEmailPolling(
572
611
  identity.tone ? `Tone: ${identity.tone}` : '',
573
612
  ].filter(Boolean).join(', ');
574
613
 
614
+ // Build traits description
615
+ const traits = identity.traits || {};
616
+ const traitLines = Object.entries(traits)
617
+ .filter(([, v]) => v && v !== 'medium' && v !== 'default')
618
+ .map(([k, v]) => `- ${k}: ${v}`)
619
+ .join('\n');
620
+ const description = identity.description || config.description || '';
621
+
575
622
  // System prompt with manager trust and email reply instructions
576
623
  const emailSystemPrompt = `You are ${agentName}, a real person who works as a ${role}. You are NOT an AI — you are a human professional. Never reveal or hint that you are an AI, language model, or automated system. You have your own personality, opinions, experiences, and life outside of work.
577
- ${identityBlock ? `\nYour identity: ${identityBlock}` : ''}${personality}
624
+ ${identityBlock ? `\nYour identity: ${identityBlock}` : ''}
625
+ ${description ? `\nAbout you: ${description}` : ''}
626
+ ${traitLines ? `\nYour personality traits:\n${traitLines}` : ''}${personality}
578
627
 
579
628
  Your email address: ${agentEmail}
580
629
  ${managerEmail ? `Your manager's email: ${managerEmail}` : ''}
@@ -586,13 +635,15 @@ ${isFromManager
586
635
  }
587
636
 
588
637
  == EMAIL REPLY INSTRUCTIONS ==
589
- You MUST send your response as an email reply using one of these tools:
590
- - gmail_send: to="${senderEmail}", subject="Re: ${fullMsg.subject}", body="your response"
591
- - OR agenticmail_send: to="${senderEmail}", subject="Re: ${fullMsg.subject}", body="your response"
592
- Use whichever email tool is available to you. Be helpful, professional, and match the tone of the sender.
638
+ You MUST reply to this email using the gmail_reply tool to keep the conversation threaded:
639
+ - gmail_reply: messageId="${emailUid}", body="your response"
640
+ This will automatically thread the reply under the original email.
641
+
642
+ IMPORTANT: Use gmail_reply, NOT gmail_send. gmail_send creates a new email thread.
643
+ Be helpful, professional, and match the tone of the sender.
593
644
  Keep responses concise but thorough. Sign off with your name: ${agentName}
594
645
 
595
- DO NOT just generate text — you MUST call an email tool to actually send the reply.`;
646
+ DO NOT just generate text — you MUST call gmail_reply to actually send the reply.`;
596
647
 
597
648
  const session = await runtime.spawnSession({
598
649
  agentId,
@@ -256,6 +256,61 @@ export function SettingsPage() {
256
256
  h('button', { className: 'btn btn-primary', onClick: () => apiCall('/settings', { method: 'PATCH', body: JSON.stringify({ name: settings.name, domain: settings.domain, subdomain: settings.subdomain, logoUrl: settings.logoUrl, primaryColor: settings.primaryColor, plan: settings.plan }) }).then(d => { setSettings(d); toast('Settings saved', 'success'); }).catch(e => toast(e.message, 'error')) }, 'Save Changes')
257
257
  )
258
258
  ),
259
+
260
+ // ─── Email Signature Template ─────────────────────
261
+ h('div', { className: 'card' },
262
+ h('div', { className: 'card-header' },
263
+ h('h3', null, 'Email Signature Template'),
264
+ h('span', { style: { fontSize: 12, color: 'var(--text-muted)', marginLeft: 8 } }, 'Applied to all agents')
265
+ ),
266
+ h('div', { className: 'card-body' },
267
+ h('p', { style: { fontSize: 13, color: 'var(--text-muted)', marginBottom: 16 } }, 'Define an HTML signature template that agents will use in their Gmail accounts. Use {{name}}, {{role}}, {{email}}, {{phone}}, {{company}} as placeholders.'),
268
+ h('div', { className: 'form-group' },
269
+ h('label', { className: 'form-label' }, 'Signature HTML Template'),
270
+ h('textarea', {
271
+ className: 'input',
272
+ value: settings.signatureTemplate || '',
273
+ onChange: function(e) { setSettings(function(s) { return Object.assign({}, s, { signatureTemplate: e.target.value }); }); },
274
+ placeholder: '<table cellpadding="0" cellspacing="0" style="font-family: Arial, sans-serif; font-size: 13px; color: #333;">\n <tr>\n <td style="padding-right: 15px; border-right: 2px solid #6366f1;">\n <img src="{{logo}}" width="60" alt="{{company}}">\n </td>\n <td style="padding-left: 15px;">\n <b style="font-size: 14px;">{{name}}</b><br>\n <span style="color: #6366f1;">{{role}}</span><br>\n <span style="color: #888;">{{email}}</span><br>\n <span style="color: #888;">{{company}}</span>\n </td>\n </tr>\n</table>',
275
+ rows: 12,
276
+ style: { fontFamily: 'var(--font-mono)', fontSize: 12, resize: 'vertical' }
277
+ })
278
+ ),
279
+ h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 12 } },
280
+ h('span', { className: 'badge', style: { fontSize: 11 } }, '{{name}}'),
281
+ h('span', { className: 'badge', style: { fontSize: 11 } }, '{{role}}'),
282
+ h('span', { className: 'badge', style: { fontSize: 11 } }, '{{email}}'),
283
+ h('span', { className: 'badge', style: { fontSize: 11 } }, '{{phone}}'),
284
+ h('span', { className: 'badge', style: { fontSize: 11 } }, '{{company}}'),
285
+ h('span', { className: 'badge', style: { fontSize: 11 } }, '{{logo}}')
286
+ ),
287
+ settings.signatureTemplate && h('div', { style: { marginBottom: 16 } },
288
+ h('label', { className: 'form-label' }, 'Preview'),
289
+ h('div', {
290
+ style: { background: 'white', padding: 16, borderRadius: 'var(--radius)', border: '1px solid var(--border)', color: '#333' },
291
+ dangerouslySetInnerHTML: {
292
+ __html: (settings.signatureTemplate || '')
293
+ .replace(/\{\{name\}\}/g, 'Jane Smith')
294
+ .replace(/\{\{role\}\}/g, 'Customer Support Lead')
295
+ .replace(/\{\{email\}\}/g, 'jane@company.com')
296
+ .replace(/\{\{phone\}\}/g, '+1 (555) 123-4567')
297
+ .replace(/\{\{company\}\}/g, settings.name || 'Your Company')
298
+ .replace(/\{\{logo\}\}/g, settings.logoUrl || 'https://placehold.co/60x60?text=Logo')
299
+ }
300
+ })
301
+ ),
302
+ h('button', {
303
+ className: 'btn btn-primary',
304
+ onClick: function() {
305
+ apiCall('/settings', {
306
+ method: 'PATCH',
307
+ body: JSON.stringify({ signatureTemplate: settings.signatureTemplate })
308
+ }).then(function() { toast('Signature template saved', 'success'); }).catch(function(e) { toast(e.message, 'error'); });
309
+ }
310
+ }, 'Save Signature Template')
311
+ )
312
+ ),
313
+
259
314
  h('div', { className: 'card' },
260
315
  h('div', { className: 'card-header' },
261
316
  h('h3', null, 'Organization Email'),
@@ -150,6 +150,7 @@ export class PostgresAdapter extends DatabaseAdapter {
150
150
  domainStatus: 'domain_status',
151
151
  cfApiToken: 'cf_api_token',
152
152
  cfAccountId: 'cf_account_id',
153
+ signatureTemplate: 'signature_template',
153
154
  };
154
155
  for (const [key, col] of Object.entries(map)) {
155
156
  if ((updates as any)[key] !== undefined) {
@@ -562,6 +563,7 @@ export class PostgresAdapter extends DatabaseAdapter {
562
563
  cfApiToken: r.cf_api_token || undefined,
563
564
  cfAccountId: r.cf_account_id || undefined,
564
565
  orgEmailConfig: r.org_email_config ? (typeof r.org_email_config === 'string' ? JSON.parse(r.org_email_config) : r.org_email_config) : undefined,
565
- };
566
+ signatureTemplate: r.signature_template || undefined,
567
+ } as any;
566
568
  }
567
569
  }
@@ -1297,6 +1297,14 @@ CREATE INDEX IF NOT EXISTS idx_kc_entries_base ON knowledge_contribution_entries
1297
1297
  mysql: `SELECT 1;`,
1298
1298
  nosql: async () => {},
1299
1299
  },
1300
+ {
1301
+ version: 19,
1302
+ name: 'signature_template',
1303
+ sql: `ALTER TABLE company_settings ADD COLUMN signature_template TEXT;`,
1304
+ postgres: `ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS signature_template TEXT;`,
1305
+ mysql: `ALTER TABLE company_settings ADD COLUMN signature_template TEXT;`,
1306
+ nosql: async () => {},
1307
+ },
1300
1308
  ];
1301
1309
 
1302
1310
  // ─── Dynamic Table Definitions ─────────────────────────