@agenticmail/enterprise 0.5.53 → 0.5.54

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.
@@ -389,5 +389,313 @@ export function createAgentRoutes(opts: {
389
389
  });
390
390
  });
391
391
 
392
+ // ─── Email Configuration ──────────────────────────────
393
+
394
+ /**
395
+ * GET /bridge/agents/:id/email-config — Get agent's email configuration (without password).
396
+ */
397
+ router.get('/bridge/agents/:id/email-config', (c) => {
398
+ const agentId = c.req.param('id');
399
+ const managed = lifecycle.getAgent(agentId);
400
+ if (!managed) return c.json({ error: 'Agent not found' }, 404);
401
+
402
+ const emailConfig = managed.config?.emailConfig || null;
403
+ if (!emailConfig) return c.json({ configured: false });
404
+
405
+ // Return config without sensitive data
406
+ return c.json({
407
+ configured: true,
408
+ provider: emailConfig.provider,
409
+ email: emailConfig.email,
410
+ status: emailConfig.status || 'unknown',
411
+ // IMAP details (no password)
412
+ imapHost: emailConfig.imapHost,
413
+ imapPort: emailConfig.imapPort,
414
+ smtpHost: emailConfig.smtpHost,
415
+ smtpPort: emailConfig.smtpPort,
416
+ // OAuth details (no tokens)
417
+ oauthProvider: emailConfig.oauthProvider,
418
+ oauthClientId: emailConfig.oauthClientId,
419
+ oauthConfigured: !!emailConfig.oauthAccessToken,
420
+ lastConnected: emailConfig.lastConnected,
421
+ lastError: emailConfig.lastError,
422
+ });
423
+ });
424
+
425
+ /**
426
+ * PUT /bridge/agents/:id/email-config — Set or update agent's email configuration.
427
+ *
428
+ * Supports three modes:
429
+ * 1. IMAP/SMTP: { provider: 'imap', email, password, imapHost, smtpHost, ... }
430
+ * 2. Microsoft OAuth: { provider: 'microsoft', oauthClientId, oauthClientSecret, oauthTenantId }
431
+ * 3. Google OAuth: { provider: 'google', oauthClientId, oauthClientSecret }
432
+ *
433
+ * For IMAP, auto-detects settings for known providers (Microsoft 365, Gmail, etc.)
434
+ */
435
+ router.put('/bridge/agents/:id/email-config', async (c) => {
436
+ const agentId = c.req.param('id');
437
+ const managed = lifecycle.getAgent(agentId);
438
+ if (!managed) return c.json({ error: 'Agent not found' }, 404);
439
+
440
+ const body = await c.req.json();
441
+ const { provider, email, password, imapHost, imapPort, smtpHost, smtpPort, preset,
442
+ oauthClientId, oauthClientSecret, oauthTenantId, oauthRedirectUri } = body;
443
+
444
+ if (!provider) return c.json({ error: 'provider is required (imap, microsoft, or google)' }, 400);
445
+ if (!email && provider === 'imap') return c.json({ error: 'email is required' }, 400);
446
+
447
+ const emailConfig: any = {
448
+ provider,
449
+ email: email || managed.config?.identity?.email || managed.config?.email,
450
+ updatedAt: new Date().toISOString(),
451
+ };
452
+
453
+ if (provider === 'imap') {
454
+ // Auto-detect IMAP/SMTP from well-known providers
455
+ if (preset && !imapHost) {
456
+ const PRESETS: Record<string, any> = {
457
+ 'microsoft365': { imapHost: 'outlook.office365.com', imapPort: 993, smtpHost: 'smtp.office365.com', smtpPort: 587 },
458
+ 'gmail': { imapHost: 'imap.gmail.com', imapPort: 993, smtpHost: 'smtp.gmail.com', smtpPort: 587 },
459
+ 'yahoo': { imapHost: 'imap.mail.yahoo.com', imapPort: 993, smtpHost: 'smtp.mail.yahoo.com', smtpPort: 465 },
460
+ 'zoho': { imapHost: 'imap.zoho.com', imapPort: 993, smtpHost: 'smtp.zoho.com', smtpPort: 587 },
461
+ 'fastmail': { imapHost: 'imap.fastmail.com', imapPort: 993, smtpHost: 'smtp.fastmail.com', smtpPort: 587 },
462
+ 'icloud': { imapHost: 'imap.mail.me.com', imapPort: 993, smtpHost: 'smtp.mail.me.com', smtpPort: 587 },
463
+ };
464
+ const presetConfig = PRESETS[preset];
465
+ if (presetConfig) Object.assign(emailConfig, presetConfig);
466
+ else return c.json({ error: `Unknown preset: ${preset}. Valid: ${Object.keys(PRESETS).join(', ')}` }, 400);
467
+ } else {
468
+ emailConfig.imapHost = imapHost;
469
+ emailConfig.imapPort = imapPort || 993;
470
+ emailConfig.smtpHost = smtpHost;
471
+ emailConfig.smtpPort = smtpPort || 587;
472
+ }
473
+
474
+ if (!emailConfig.imapHost || !emailConfig.smtpHost) {
475
+ return c.json({ error: 'imapHost and smtpHost are required (or use a preset)' }, 400);
476
+ }
477
+
478
+ if (password) {
479
+ emailConfig.password = password; // stored encrypted in production
480
+ }
481
+
482
+ emailConfig.status = 'configured';
483
+
484
+ } else if (provider === 'microsoft') {
485
+ // Microsoft OAuth (Azure AD / Entra ID)
486
+ emailConfig.oauthProvider = 'microsoft';
487
+ emailConfig.oauthClientId = oauthClientId;
488
+ emailConfig.oauthClientSecret = oauthClientSecret;
489
+ emailConfig.oauthTenantId = oauthTenantId || 'common';
490
+ emailConfig.oauthRedirectUri = oauthRedirectUri || '';
491
+ emailConfig.oauthScopes = ['https://graph.microsoft.com/Mail.ReadWrite', 'https://graph.microsoft.com/Mail.Send', 'offline_access'];
492
+
493
+ if (!oauthClientId) return c.json({ error: 'oauthClientId is required for Microsoft OAuth' }, 400);
494
+
495
+ // Build the authorization URL
496
+ const authUrl = `https://login.microsoftonline.com/${emailConfig.oauthTenantId}/oauth2/v2.0/authorize?` +
497
+ `client_id=${encodeURIComponent(oauthClientId)}&response_type=code&` +
498
+ `redirect_uri=${encodeURIComponent(emailConfig.oauthRedirectUri)}&` +
499
+ `scope=${encodeURIComponent(emailConfig.oauthScopes.join(' '))}&` +
500
+ `state=${agentId}&prompt=consent`;
501
+ emailConfig.oauthAuthUrl = authUrl;
502
+ emailConfig.status = 'awaiting_oauth';
503
+
504
+ } else if (provider === 'google') {
505
+ // Google OAuth (Google Workspace)
506
+ emailConfig.oauthProvider = 'google';
507
+ emailConfig.oauthClientId = oauthClientId;
508
+ emailConfig.oauthClientSecret = oauthClientSecret;
509
+ emailConfig.oauthRedirectUri = oauthRedirectUri || '';
510
+ emailConfig.oauthScopes = ['https://www.googleapis.com/auth/gmail.modify', 'https://www.googleapis.com/auth/gmail.send'];
511
+
512
+ if (!oauthClientId) return c.json({ error: 'oauthClientId is required for Google OAuth' }, 400);
513
+
514
+ const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
515
+ `client_id=${encodeURIComponent(oauthClientId)}&response_type=code&` +
516
+ `redirect_uri=${encodeURIComponent(emailConfig.oauthRedirectUri)}&` +
517
+ `scope=${encodeURIComponent(emailConfig.oauthScopes.join(' '))}&` +
518
+ `access_type=offline&prompt=consent&state=${agentId}`;
519
+ emailConfig.oauthAuthUrl = authUrl;
520
+ emailConfig.status = 'awaiting_oauth';
521
+ } else {
522
+ return c.json({ error: `Unknown provider: ${provider}. Valid: imap, microsoft, google` }, 400);
523
+ }
524
+
525
+ // Save to agent config
526
+ const _managed = lifecycle.getAgent(agentId); if (_managed) { _managed.config.emailConfig = emailConfig; _managed.updatedAt = new Date().toISOString(); }
527
+
528
+ return c.json({
529
+ success: true,
530
+ emailConfig: {
531
+ provider: emailConfig.provider,
532
+ email: emailConfig.email,
533
+ status: emailConfig.status,
534
+ oauthAuthUrl: emailConfig.oauthAuthUrl || undefined,
535
+ },
536
+ });
537
+ });
538
+
539
+ /**
540
+ * POST /bridge/agents/:id/email-config/oauth-callback — Exchange OAuth code for tokens.
541
+ * Called after user completes the OAuth consent flow.
542
+ */
543
+ router.post('/bridge/agents/:id/email-config/oauth-callback', async (c) => {
544
+ const agentId = c.req.param('id');
545
+ const managed = lifecycle.getAgent(agentId);
546
+ if (!managed) return c.json({ error: 'Agent not found' }, 404);
547
+
548
+ const { code } = await c.req.json();
549
+ if (!code) return c.json({ error: 'OAuth authorization code is required' }, 400);
550
+
551
+ const emailConfig = managed.config?.emailConfig;
552
+ if (!emailConfig) return c.json({ error: 'No email config found — configure email first' }, 400);
553
+
554
+ try {
555
+ if (emailConfig.oauthProvider === 'microsoft') {
556
+ const tokenRes = await fetch(`https://login.microsoftonline.com/${emailConfig.oauthTenantId || 'common'}/oauth2/v2.0/token`, {
557
+ method: 'POST',
558
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
559
+ body: new URLSearchParams({
560
+ client_id: emailConfig.oauthClientId,
561
+ client_secret: emailConfig.oauthClientSecret,
562
+ code,
563
+ redirect_uri: emailConfig.oauthRedirectUri,
564
+ grant_type: 'authorization_code',
565
+ scope: emailConfig.oauthScopes.join(' '),
566
+ }),
567
+ });
568
+
569
+ if (!tokenRes.ok) {
570
+ const errText = await tokenRes.text();
571
+ return c.json({ error: `Microsoft token exchange failed: ${errText}` }, 400);
572
+ }
573
+
574
+ const tokens = await tokenRes.json() as any;
575
+ emailConfig.oauthAccessToken = tokens.access_token;
576
+ emailConfig.oauthRefreshToken = tokens.refresh_token;
577
+ emailConfig.oauthTokenExpiry = new Date(Date.now() + (tokens.expires_in * 1000)).toISOString();
578
+
579
+ // Get the user's email from Graph
580
+ try {
581
+ const profileRes = await fetch('https://graph.microsoft.com/v1.0/me?$select=mail,displayName', {
582
+ headers: { Authorization: `Bearer ${tokens.access_token}` },
583
+ });
584
+ if (profileRes.ok) {
585
+ const profile = await profileRes.json() as any;
586
+ if (profile.mail) emailConfig.email = profile.mail;
587
+ }
588
+ } catch {}
589
+
590
+ } else if (emailConfig.oauthProvider === 'google') {
591
+ const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
592
+ method: 'POST',
593
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
594
+ body: new URLSearchParams({
595
+ client_id: emailConfig.oauthClientId,
596
+ client_secret: emailConfig.oauthClientSecret,
597
+ code,
598
+ redirect_uri: emailConfig.oauthRedirectUri,
599
+ grant_type: 'authorization_code',
600
+ }),
601
+ });
602
+
603
+ if (!tokenRes.ok) {
604
+ const errText = await tokenRes.text();
605
+ return c.json({ error: `Google token exchange failed: ${errText}` }, 400);
606
+ }
607
+
608
+ const tokens = await tokenRes.json() as any;
609
+ emailConfig.oauthAccessToken = tokens.access_token;
610
+ emailConfig.oauthRefreshToken = tokens.refresh_token;
611
+ emailConfig.oauthTokenExpiry = new Date(Date.now() + (tokens.expires_in * 1000)).toISOString();
612
+
613
+ // Get the user's email from Gmail
614
+ try {
615
+ const profileRes = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/profile', {
616
+ headers: { Authorization: `Bearer ${tokens.access_token}` },
617
+ });
618
+ if (profileRes.ok) {
619
+ const profile = await profileRes.json() as any;
620
+ if (profile.emailAddress) emailConfig.email = profile.emailAddress;
621
+ }
622
+ } catch {}
623
+ }
624
+
625
+ emailConfig.status = 'connected';
626
+ emailConfig.lastConnected = new Date().toISOString();
627
+ emailConfig.lastError = null;
628
+ const _managed = lifecycle.getAgent(agentId); if (_managed) { _managed.config.emailConfig = emailConfig; _managed.updatedAt = new Date().toISOString(); }
629
+
630
+ return c.json({ success: true, email: emailConfig.email, status: 'connected' });
631
+ } catch (err: any) {
632
+ emailConfig.status = 'error';
633
+ emailConfig.lastError = err.message;
634
+ const _managed = lifecycle.getAgent(agentId); if (_managed) { _managed.config.emailConfig = emailConfig; _managed.updatedAt = new Date().toISOString(); }
635
+ return c.json({ error: err.message }, 500);
636
+ }
637
+ });
638
+
639
+ /**
640
+ * POST /bridge/agents/:id/email-config/test — Test email connection.
641
+ */
642
+ router.post('/bridge/agents/:id/email-config/test', async (c) => {
643
+ const agentId = c.req.param('id');
644
+ const managed = lifecycle.getAgent(agentId);
645
+ if (!managed) return c.json({ error: 'Agent not found' }, 404);
646
+
647
+ const emailConfig = managed.config?.emailConfig;
648
+ if (!emailConfig) return c.json({ error: 'No email config found' }, 400);
649
+
650
+ try {
651
+ if (emailConfig.provider === 'imap') {
652
+ // Test IMAP connection
653
+ const { ImapFlow } = await import('imapflow');
654
+ const client = new (ImapFlow as any)({
655
+ host: emailConfig.imapHost,
656
+ port: emailConfig.imapPort || 993,
657
+ secure: true,
658
+ auth: { user: emailConfig.email, pass: emailConfig.password },
659
+ logger: false,
660
+ });
661
+ await client.connect();
662
+ const status = await client.status('INBOX', { messages: true, unseen: true });
663
+ await client.logout();
664
+
665
+ return c.json({ success: true, inbox: { total: status.messages, unread: status.unseen } });
666
+ } else if (emailConfig.provider === 'microsoft' && emailConfig.oauthAccessToken) {
667
+ const res = await fetch('https://graph.microsoft.com/v1.0/me/mailFolders/inbox?$select=totalItemCount,unreadItemCount', {
668
+ headers: { Authorization: `Bearer ${emailConfig.oauthAccessToken}` },
669
+ });
670
+ if (!res.ok) throw new Error(`Graph API ${res.status}`);
671
+ const data = await res.json() as any;
672
+ return c.json({ success: true, inbox: { total: data.totalItemCount, unread: data.unreadItemCount } });
673
+ } else if (emailConfig.provider === 'google' && emailConfig.oauthAccessToken) {
674
+ const res = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/profile', {
675
+ headers: { Authorization: `Bearer ${emailConfig.oauthAccessToken}` },
676
+ });
677
+ if (!res.ok) throw new Error(`Gmail API ${res.status}`);
678
+ const data = await res.json() as any;
679
+ return c.json({ success: true, email: data.emailAddress, totalMessages: data.messagesTotal });
680
+ } else {
681
+ return c.json({ error: 'Provider not fully configured or unsupported' }, 400);
682
+ }
683
+ } catch (err: any) {
684
+ return c.json({ success: false, error: err.message }, 200);
685
+ }
686
+ });
687
+
688
+ /**
689
+ * DELETE /bridge/agents/:id/email-config — Disconnect email.
690
+ */
691
+ router.delete('/bridge/agents/:id/email-config', (c) => {
692
+ const agentId = c.req.param('id');
693
+ const managed = lifecycle.getAgent(agentId);
694
+ if (!managed) return c.json({ error: 'Agent not found' }, 404);
695
+
696
+ const _m = lifecycle.getAgent(agentId); if (_m) { _m.config.emailConfig = null; _m.updatedAt = new Date().toISOString(); }
697
+ return c.json({ success: true });
698
+ });
699
+
392
700
  return router;
393
701
  }