mxroute-cli 0.3.2 → 1.0.0

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.
Files changed (121) hide show
  1. package/README.md +281 -11
  2. package/dist/commands/accounts-search.d.ts +1 -0
  3. package/dist/commands/accounts-search.js +66 -0
  4. package/dist/commands/accounts-search.js.map +1 -0
  5. package/dist/commands/accounts.js +39 -9
  6. package/dist/commands/accounts.js.map +1 -1
  7. package/dist/commands/aliases-sync.d.ts +1 -0
  8. package/dist/commands/aliases-sync.js +162 -0
  9. package/dist/commands/aliases-sync.js.map +1 -0
  10. package/dist/commands/autoresponder.js +7 -6
  11. package/dist/commands/autoresponder.js.map +1 -1
  12. package/dist/commands/backup.d.ts +1 -0
  13. package/dist/commands/backup.js +170 -0
  14. package/dist/commands/backup.js.map +1 -0
  15. package/dist/commands/bulk.js +52 -9
  16. package/dist/commands/bulk.js.map +1 -1
  17. package/dist/commands/catchall.js +2 -2
  18. package/dist/commands/catchall.js.map +1 -1
  19. package/dist/commands/cleanup.d.ts +1 -0
  20. package/dist/commands/cleanup.js +234 -0
  21. package/dist/commands/cleanup.js.map +1 -0
  22. package/dist/commands/config.js +9 -2
  23. package/dist/commands/config.js.map +1 -1
  24. package/dist/commands/credentials-export.d.ts +13 -0
  25. package/dist/commands/credentials-export.js +142 -0
  26. package/dist/commands/credentials-export.js.map +1 -0
  27. package/dist/commands/deprovision.d.ts +1 -0
  28. package/dist/commands/deprovision.js +125 -0
  29. package/dist/commands/deprovision.js.map +1 -0
  30. package/dist/commands/dns-setup.js +20 -3
  31. package/dist/commands/dns-setup.js.map +1 -1
  32. package/dist/commands/export-import.js +22 -1
  33. package/dist/commands/export-import.js.map +1 -1
  34. package/dist/commands/filters.js +2 -2
  35. package/dist/commands/filters.js.map +1 -1
  36. package/dist/commands/fix.js +1 -1
  37. package/dist/commands/fix.js.map +1 -1
  38. package/dist/commands/forwarders-validate.d.ts +1 -0
  39. package/dist/commands/forwarders-validate.js +190 -0
  40. package/dist/commands/forwarders-validate.js.map +1 -0
  41. package/dist/commands/forwarders.js +6 -5
  42. package/dist/commands/forwarders.js.map +1 -1
  43. package/dist/commands/lists.js +7 -6
  44. package/dist/commands/lists.js.map +1 -1
  45. package/dist/commands/mail.d.ts +15 -0
  46. package/dist/commands/mail.js +998 -0
  47. package/dist/commands/mail.js.map +1 -0
  48. package/dist/commands/migrate.js.map +1 -1
  49. package/dist/commands/onboard.js +11 -4
  50. package/dist/commands/onboard.js.map +1 -1
  51. package/dist/commands/password-audit.d.ts +1 -0
  52. package/dist/commands/password-audit.js +304 -0
  53. package/dist/commands/password-audit.js.map +1 -0
  54. package/dist/commands/password.d.ts +1 -0
  55. package/dist/commands/password.js +96 -0
  56. package/dist/commands/password.js.map +1 -0
  57. package/dist/commands/provision.d.ts +13 -0
  58. package/dist/commands/provision.js +306 -0
  59. package/dist/commands/provision.js.map +1 -0
  60. package/dist/commands/quota-policy.d.ts +3 -0
  61. package/dist/commands/quota-policy.js +192 -0
  62. package/dist/commands/quota-policy.js.map +1 -0
  63. package/dist/commands/quota.js +1 -1
  64. package/dist/commands/quota.js.map +1 -1
  65. package/dist/commands/rate-limit.d.ts +2 -0
  66. package/dist/commands/rate-limit.js +121 -0
  67. package/dist/commands/rate-limit.js.map +1 -0
  68. package/dist/commands/reputation.d.ts +1 -0
  69. package/dist/commands/reputation.js +265 -0
  70. package/dist/commands/reputation.js.map +1 -0
  71. package/dist/commands/schedule.d.ts +3 -0
  72. package/dist/commands/schedule.js +270 -0
  73. package/dist/commands/schedule.js.map +1 -0
  74. package/dist/commands/send.js +2 -1
  75. package/dist/commands/send.js.map +1 -1
  76. package/dist/commands/setup.js +22 -0
  77. package/dist/commands/setup.js.map +1 -1
  78. package/dist/commands/share.js +20 -10
  79. package/dist/commands/share.js.map +1 -1
  80. package/dist/commands/smtp-debug.d.ts +1 -0
  81. package/dist/commands/smtp-debug.js +188 -0
  82. package/dist/commands/smtp-debug.js.map +1 -0
  83. package/dist/commands/spam.js +3 -3
  84. package/dist/commands/spam.js.map +1 -1
  85. package/dist/commands/ssl-check.d.ts +1 -0
  86. package/dist/commands/ssl-check.js +145 -0
  87. package/dist/commands/ssl-check.js.map +1 -0
  88. package/dist/commands/status.js +53 -1
  89. package/dist/commands/status.js.map +1 -1
  90. package/dist/commands/templates.d.ts +4 -0
  91. package/dist/commands/templates.js +310 -0
  92. package/dist/commands/templates.js.map +1 -0
  93. package/dist/commands/test-delivery.d.ts +1 -0
  94. package/dist/commands/test-delivery.js +87 -0
  95. package/dist/commands/test-delivery.js.map +1 -0
  96. package/dist/commands/usage-history.d.ts +1 -0
  97. package/dist/commands/usage-history.js +173 -0
  98. package/dist/commands/usage-history.js.map +1 -0
  99. package/dist/commands/webhook.js +18 -3
  100. package/dist/commands/webhook.js.map +1 -1
  101. package/dist/commands/welcome-send.d.ts +11 -0
  102. package/dist/commands/welcome-send.js +163 -0
  103. package/dist/commands/welcome-send.js.map +1 -0
  104. package/dist/index.js +362 -0
  105. package/dist/index.js.map +1 -1
  106. package/dist/mcp.js +1940 -10
  107. package/dist/mcp.js.map +1 -1
  108. package/dist/utils/config.js +1 -1
  109. package/dist/utils/config.js.map +1 -1
  110. package/dist/utils/directadmin.js +23 -2
  111. package/dist/utils/directadmin.js.map +1 -1
  112. package/dist/utils/imap.d.ts +61 -0
  113. package/dist/utils/imap.js +426 -0
  114. package/dist/utils/imap.js.map +1 -0
  115. package/dist/utils/mime.d.ts +50 -0
  116. package/dist/utils/mime.js +369 -0
  117. package/dist/utils/mime.js.map +1 -0
  118. package/dist/utils/shared.d.ts +10 -0
  119. package/dist/utils/shared.js +28 -1
  120. package/dist/utils/shared.js.map +1 -1
  121. package/package.json +1 -1
package/dist/mcp.js CHANGED
@@ -1,5 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
3
36
  Object.defineProperty(exports, "__esModule", { value: true });
4
37
  process.removeAllListeners('warning');
5
38
  process.on('warning', (w) => {
@@ -11,8 +44,11 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
11
44
  const zod_1 = require("zod");
12
45
  const config_1 = require("./utils/config");
13
46
  const api_1 = require("./utils/api");
47
+ const imap_1 = require("./utils/imap");
48
+ const mime_1 = require("./utils/mime");
14
49
  const directadmin_1 = require("./utils/directadmin");
15
50
  const dns_1 = require("./utils/dns");
51
+ const directadmin_2 = require("./utils/directadmin");
16
52
  function getCreds() {
17
53
  const config = (0, config_1.getConfig)();
18
54
  if (!config.daUsername || !config.daLoginKey) {
@@ -25,6 +61,9 @@ const server = new mcp_js_1.McpServer({
25
61
  name: 'mxroute',
26
62
  version: pkg.version,
27
63
  });
64
+ function escapeHtml(s) {
65
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
66
+ }
28
67
  // ─── Domain Tools ────────────────────────────────────────
29
68
  server.tool('list_domains', 'List all domains on your MXroute account', {}, async () => {
30
69
  const creds = getCreds();
@@ -566,28 +605,1919 @@ server.tool('get_quota', 'Get account-wide quota and usage information', {}, asy
566
605
  const [usage, config] = await Promise.all([(0, directadmin_1.getQuotaUsage)(creds), (0, directadmin_1.getUserConfig)(creds)]);
567
606
  return { content: [{ type: 'text', text: JSON.stringify({ usage, limits: config }, null, 2) }] };
568
607
  });
608
+ // ─── Profile Tools ──────────────────────────────────────
609
+ server.tool('list_profiles', 'List all configured mail profiles. Use profile names with other mail tools to operate on different accounts.', {}, async () => {
610
+ const config = (0, config_1.getConfig)();
611
+ const profiles = (0, config_1.getProfiles)();
612
+ const profileList = Object.entries(profiles).map(([name, p]) => ({
613
+ name,
614
+ server: p.server,
615
+ username: p.username,
616
+ domain: p.domain,
617
+ isActive: name === config.activeProfile,
618
+ }));
619
+ return {
620
+ content: [
621
+ {
622
+ type: 'text',
623
+ text: JSON.stringify({ activeProfile: config.activeProfile, profiles: profileList }, null, 2),
624
+ },
625
+ ],
626
+ };
627
+ });
569
628
  // ─── Send Email Tools ────────────────────────────────────
570
- server.tool('send_email', 'Send an email via MXroute SMTP API', {
571
- to: zod_1.z.string().describe('Recipient email address'),
629
+ server.tool('send_email', 'Send an email via MXroute SMTP. Supports CC, BCC, and plain text or HTML body.', {
630
+ to: zod_1.z.string().describe('Recipient email address (comma-separated for multiple)'),
572
631
  subject: zod_1.z.string().describe('Email subject'),
573
632
  body: zod_1.z.string().describe('Email body (HTML supported)'),
574
633
  from: zod_1.z.string().optional().describe('Sender email (defaults to configured username)'),
575
- }, async ({ to, subject, body, from }) => {
576
- const config = (0, config_1.getConfig)();
577
- if (!config.server || !config.username || !config.password) {
578
- throw new Error('SMTP not configured. Run "mxroute config setup" first.');
634
+ cc: zod_1.z.string().optional().describe('CC recipients (comma-separated)'),
635
+ bcc: zod_1.z.string().optional().describe('BCC recipients (comma-separated)'),
636
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
637
+ }, async ({ to, subject, body, from, cc, bcc, profile }) => {
638
+ const smtp = resolveSmtpConfig(profile);
639
+ // If CC/BCC provided, use direct SMTP; otherwise use simple API
640
+ if (cc || bcc) {
641
+ const { buildMimeMessage } = await Promise.resolve().then(() => __importStar(require('./utils/mime')));
642
+ const mime = buildMimeMessage({
643
+ from: from || smtp.username,
644
+ to,
645
+ cc: cc || undefined,
646
+ bcc: bcc || undefined,
647
+ subject,
648
+ htmlBody: body,
649
+ });
650
+ await smtpSend(smtp, to, cc, bcc, mime);
651
+ return {
652
+ content: [
653
+ {
654
+ type: 'text',
655
+ text: JSON.stringify({ success: true, to, cc, bcc, message: 'Email sent' }, null, 2),
656
+ },
657
+ ],
658
+ };
579
659
  }
580
660
  const result = await (0, api_1.sendEmail)({
581
- server: `${config.server}.mxrouting.net`,
582
- username: config.username,
583
- password: config.password,
584
- from: from || config.username,
661
+ server: `${smtp.server}.mxrouting.net`,
662
+ username: smtp.username,
663
+ password: smtp.password,
664
+ from: from || smtp.username,
585
665
  to,
586
666
  subject,
587
667
  body,
588
668
  });
589
669
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
590
670
  });
671
+ // ─── SMTP Send Helper ───────────────────────────────────
672
+ async function smtpSend(config, to, cc, bcc, mime) {
673
+ const tls = await Promise.resolve().then(() => __importStar(require('tls')));
674
+ const allRecipients = [
675
+ ...to.split(',').map((e) => e.trim()),
676
+ ...(cc ? cc.split(',').map((e) => e.trim()) : []),
677
+ ...(bcc ? bcc.split(',').map((e) => e.trim()) : []),
678
+ ].filter(Boolean);
679
+ // Reject CRLF in email addresses to prevent SMTP command injection
680
+ for (const addr of allRecipients) {
681
+ if (/[\r\n]/.test(addr)) {
682
+ throw new Error('Invalid email address: contains newline characters');
683
+ }
684
+ }
685
+ return new Promise((resolve, reject) => {
686
+ const socket = tls.connect({ host: `${config.server}.mxrouting.net`, port: 465, servername: `${config.server}.mxrouting.net` }, () => {
687
+ let buffer = '';
688
+ let step = 0;
689
+ let rcptIdx = 0;
690
+ socket.setEncoding('utf-8');
691
+ socket.on('data', (data) => {
692
+ buffer += data;
693
+ if (!buffer.includes('\r\n'))
694
+ return;
695
+ const lines = buffer.split('\r\n');
696
+ buffer = lines.pop() || '';
697
+ for (const line of lines) {
698
+ if (!line)
699
+ continue;
700
+ const code = parseInt(line.substring(0, 3), 10);
701
+ if (step === 0 && code === 220) {
702
+ socket.write(`EHLO mxroute-cli\r\n`);
703
+ step = 1;
704
+ }
705
+ else if (step === 1 && code === 250) {
706
+ socket.write(`AUTH LOGIN\r\n`);
707
+ step = 2;
708
+ }
709
+ else if (step === 2 && code === 334) {
710
+ socket.write(Buffer.from(config.username).toString('base64') + '\r\n');
711
+ step = 3;
712
+ }
713
+ else if (step === 3 && code === 334) {
714
+ socket.write(Buffer.from(config.password).toString('base64') + '\r\n');
715
+ step = 4;
716
+ }
717
+ else if (step === 4 && code === 235) {
718
+ socket.write(`MAIL FROM:<${config.username}>\r\n`);
719
+ step = 5;
720
+ }
721
+ else if (step === 5 && code === 250) {
722
+ socket.write(`RCPT TO:<${allRecipients[rcptIdx]}>\r\n`);
723
+ step = 6;
724
+ }
725
+ else if (step === 6 && code === 250) {
726
+ rcptIdx++;
727
+ if (rcptIdx < allRecipients.length) {
728
+ socket.write(`RCPT TO:<${allRecipients[rcptIdx]}>\r\n`);
729
+ }
730
+ else {
731
+ socket.write('DATA\r\n');
732
+ step = 7;
733
+ }
734
+ }
735
+ else if (step === 7 && code === 354) {
736
+ socket.write(mime + '\r\n.\r\n');
737
+ step = 8;
738
+ }
739
+ else if (step === 8 && code === 250) {
740
+ socket.write('QUIT\r\n');
741
+ step = 9;
742
+ resolve();
743
+ }
744
+ else if (code >= 400) {
745
+ socket.destroy();
746
+ reject(new Error(`SMTP error ${code}: ${line}`));
747
+ }
748
+ }
749
+ });
750
+ socket.on('error', reject);
751
+ socket.setTimeout(30000, () => {
752
+ socket.destroy();
753
+ reject(new Error('SMTP connection timed out'));
754
+ });
755
+ });
756
+ });
757
+ }
758
+ // ─── Mail / IMAP Tools ──────────────────────────────────
759
+ function resolveSmtpConfig(profileName) {
760
+ const config = (0, config_1.getConfig)();
761
+ if (profileName) {
762
+ const profiles = (0, config_1.getProfiles)();
763
+ const profile = profiles[profileName];
764
+ if (!profile) {
765
+ throw new Error(`Profile "${profileName}" not found. Available: ${Object.keys(profiles).join(', ') || 'none'}`);
766
+ }
767
+ if (!profile.server || !profile.username || !profile.password) {
768
+ throw new Error(`Profile "${profileName}" has incomplete SMTP credentials.`);
769
+ }
770
+ return { server: profile.server, username: profile.username, password: profile.password };
771
+ }
772
+ if (!config.server || !config.username || !config.password) {
773
+ throw new Error('SMTP/IMAP not configured. Run "mxroute config smtp" first.');
774
+ }
775
+ return { server: config.server, username: config.username, password: config.password };
776
+ }
777
+ function getImapConfig(profileName) {
778
+ const smtp = resolveSmtpConfig(profileName);
779
+ return {
780
+ host: `${smtp.server}.mxrouting.net`,
781
+ port: 993,
782
+ user: smtp.username,
783
+ password: smtp.password,
784
+ };
785
+ }
786
+ async function withImap(fn, profileName) {
787
+ const imapConfig = getImapConfig(profileName);
788
+ const client = new imap_1.ImapClient(imapConfig);
789
+ try {
790
+ await client.connect();
791
+ await client.login();
792
+ return await fn(client);
793
+ }
794
+ catch (err) {
795
+ const msg = err?.message || String(err);
796
+ if (msg.includes('ECONNREFUSED') || msg.includes('ETIMEDOUT') || msg.includes('ENOTFOUND')) {
797
+ throw new Error(`IMAP connection failed: unable to reach ${imapConfig.host}:${imapConfig.port}. Check your server settings.`);
798
+ }
799
+ if (msg.includes('NO [AUTHENTICATIONFAILED]') ||
800
+ msg.includes('Invalid credentials') ||
801
+ msg.includes('Login failed')) {
802
+ throw new Error(`IMAP authentication failed for ${imapConfig.user}. Check your password or profile settings.`);
803
+ }
804
+ throw err;
805
+ }
806
+ finally {
807
+ try {
808
+ await client.logout();
809
+ }
810
+ catch {
811
+ /* ignore logout errors */
812
+ }
813
+ client.disconnect();
814
+ }
815
+ }
816
+ server.tool('list_messages', 'List recent emails in a mailbox folder. Returns message UIDs, senders, subjects, dates, read status, and sizes. Use UIDs with read_email, reply_email, forward_email, delete_email, and move_email tools.', {
817
+ folder: zod_1.z.string().optional().describe('Folder name (default: INBOX)'),
818
+ count: zod_1.z.number().optional().describe('Number of messages to fetch (default: 25, max: 100)'),
819
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
820
+ }, async ({ folder, count, profile }) => {
821
+ const targetFolder = folder || 'INBOX';
822
+ const limit = Math.min(count || 25, 100);
823
+ const result = await withImap(async (client) => {
824
+ const info = await client.selectFolder(targetFolder);
825
+ if (info.exists === 0) {
826
+ return { folder: targetFolder, total: 0, unread: 0, messages: [] };
827
+ }
828
+ const fetchCount = Math.min(info.exists, limit);
829
+ const envelopes = await client.fetchEnvelopes(info.exists, fetchCount);
830
+ envelopes.sort((a, b) => b.seq - a.seq);
831
+ return {
832
+ folder: targetFolder,
833
+ total: info.exists,
834
+ recent: info.recent,
835
+ messages: envelopes.map((env) => ({
836
+ uid: env.uid,
837
+ from: env.from,
838
+ to: env.to,
839
+ subject: env.subject,
840
+ date: env.date,
841
+ isRead: env.flags.includes('\\Seen'),
842
+ isFlagged: env.flags.includes('\\Flagged'),
843
+ size: env.size,
844
+ sizeFormatted: (0, mime_1.formatFileSize)(env.size),
845
+ })),
846
+ };
847
+ }, profile);
848
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
849
+ });
850
+ server.tool('read_email', 'Read the full content of an email by UID. Returns headers, text body, HTML body, and attachment metadata. Automatically marks the message as read.', {
851
+ uid: zod_1.z.number().describe('Message UID (from list_messages)'),
852
+ folder: zod_1.z.string().optional().describe('Folder name (default: INBOX)'),
853
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
854
+ }, async ({ uid, folder, profile }) => {
855
+ const targetFolder = folder || 'INBOX';
856
+ const result = await withImap(async (client) => {
857
+ await client.selectFolder(targetFolder);
858
+ await client.setFlags(uid, '\\Seen');
859
+ const rawBody = await client.fetchBody(uid);
860
+ const msg = (0, mime_1.parseMessage)(rawBody);
861
+ return {
862
+ uid,
863
+ folder: targetFolder,
864
+ from: msg.from,
865
+ to: msg.to,
866
+ cc: msg.cc || undefined,
867
+ subject: msg.subject,
868
+ date: msg.date,
869
+ messageId: msg.messageId,
870
+ inReplyTo: msg.inReplyTo || undefined,
871
+ textBody: msg.textBody || (msg.htmlBody ? (0, mime_1.htmlToText)(msg.htmlBody) : ''),
872
+ htmlBody: msg.htmlBody || undefined,
873
+ attachments: msg.attachments.map((a) => ({
874
+ filename: a.filename,
875
+ contentType: a.contentType,
876
+ size: a.size,
877
+ sizeFormatted: (0, mime_1.formatFileSize)(a.size),
878
+ })),
879
+ };
880
+ }, profile);
881
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
882
+ });
883
+ server.tool('search_emails', 'Search emails by subject, sender, or body text. Returns matching message UIDs and envelopes. Use UIDs with read_email for full content.', {
884
+ query: zod_1.z.string().describe('Search term (searches subject, from, and body)'),
885
+ folder: zod_1.z.string().optional().describe('Folder to search (default: INBOX)'),
886
+ limit: zod_1.z.number().optional().describe('Max results (default: 50)'),
887
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
888
+ }, async ({ query, folder, limit, profile }) => {
889
+ const targetFolder = folder || 'INBOX';
890
+ const maxResults = Math.min(limit || 50, 100);
891
+ const result = await withImap(async (client) => {
892
+ await client.selectFolder(targetFolder);
893
+ const sanitized = query.replace(/[\r\n]/g, '').replace(/"/g, '\\"');
894
+ const criteria = `OR OR SUBJECT "${sanitized}" FROM "${sanitized}" BODY "${sanitized}"`;
895
+ const uids = await client.search(criteria);
896
+ if (uids.length === 0) {
897
+ return { folder: targetFolder, query, totalMatches: 0, messages: [] };
898
+ }
899
+ const limitedUids = uids.slice(-maxResults);
900
+ const envelopes = await client.fetchEnvelopesByUid(limitedUids);
901
+ envelopes.sort((a, b) => b.uid - a.uid);
902
+ return {
903
+ folder: targetFolder,
904
+ query,
905
+ totalMatches: uids.length,
906
+ messages: envelopes.map((env) => ({
907
+ uid: env.uid,
908
+ from: env.from,
909
+ to: env.to,
910
+ subject: env.subject,
911
+ date: env.date,
912
+ isRead: env.flags.includes('\\Seen'),
913
+ size: env.size,
914
+ })),
915
+ };
916
+ }, profile);
917
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
918
+ });
919
+ server.tool('reply_email', 'Reply to an email. Fetches the original message, composes a reply with proper In-Reply-To headers and quoted text, and sends it.', {
920
+ uid: zod_1.z.number().describe('UID of the message to reply to'),
921
+ body: zod_1.z.string().describe('Reply body text'),
922
+ folder: zod_1.z.string().optional().describe('Folder containing the message (default: INBOX)'),
923
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
924
+ }, async ({ uid, body, folder, profile }) => {
925
+ const targetFolder = folder || 'INBOX';
926
+ const smtp = resolveSmtpConfig(profile);
927
+ const originalMsg = await withImap(async (client) => {
928
+ await client.selectFolder(targetFolder);
929
+ const rawBody = await client.fetchBody(uid);
930
+ return (0, mime_1.parseMessage)(rawBody);
931
+ }, profile);
932
+ const replyTo = originalMsg.from.match(/<([^>]+)>/)?.[1] || originalMsg.from;
933
+ const replySubject = originalMsg.subject.startsWith('Re:') ? originalMsg.subject : `Re: ${originalMsg.subject}`;
934
+ const originalBody = originalMsg.textBody || (0, mime_1.htmlToText)(originalMsg.htmlBody);
935
+ const quoted = originalBody
936
+ .split('\n')
937
+ .map((l) => `> ${l}`)
938
+ .join('\n');
939
+ const fullBody = `${body.trim()}\n\nOn ${originalMsg.date}, ${originalMsg.from} wrote:\n${quoted}`;
940
+ const result = await (0, api_1.sendEmail)({
941
+ server: `${smtp.server}.mxrouting.net`,
942
+ username: smtp.username,
943
+ password: smtp.password,
944
+ from: smtp.username,
945
+ to: replyTo,
946
+ subject: replySubject,
947
+ body: `<pre style="font-family: system-ui, sans-serif; white-space: pre-wrap;">${escapeHtml(fullBody)}</pre>`,
948
+ });
949
+ return {
950
+ content: [
951
+ {
952
+ type: 'text',
953
+ text: JSON.stringify({
954
+ success: result.success,
955
+ to: replyTo,
956
+ subject: replySubject,
957
+ message: result.success ? 'Reply sent' : result.message,
958
+ }, null, 2),
959
+ },
960
+ ],
961
+ };
962
+ });
963
+ server.tool('forward_email', 'Forward an email to another recipient. Includes the original message headers and body.', {
964
+ uid: zod_1.z.number().describe('UID of the message to forward'),
965
+ to: zod_1.z.string().describe('Recipient email address'),
966
+ note: zod_1.z.string().optional().describe('Optional note to add before the forwarded message'),
967
+ folder: zod_1.z.string().optional().describe('Folder containing the message (default: INBOX)'),
968
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
969
+ }, async ({ uid, to, note, folder, profile }) => {
970
+ const targetFolder = folder || 'INBOX';
971
+ const smtp = resolveSmtpConfig(profile);
972
+ const originalMsg = await withImap(async (client) => {
973
+ await client.selectFolder(targetFolder);
974
+ const rawBody = await client.fetchBody(uid);
975
+ return (0, mime_1.parseMessage)(rawBody);
976
+ }, profile);
977
+ const originalBody = originalMsg.textBody || (0, mime_1.htmlToText)(originalMsg.htmlBody);
978
+ const forwarded = [
979
+ note ? `${note.trim()}\n\n` : '',
980
+ '---------- Forwarded message ----------',
981
+ `From: ${originalMsg.from}`,
982
+ `Date: ${originalMsg.date}`,
983
+ `Subject: ${originalMsg.subject}`,
984
+ `To: ${originalMsg.to}`,
985
+ '',
986
+ originalBody,
987
+ ].join('\n');
988
+ const result = await (0, api_1.sendEmail)({
989
+ server: `${smtp.server}.mxrouting.net`,
990
+ username: smtp.username,
991
+ password: smtp.password,
992
+ from: smtp.username,
993
+ to,
994
+ subject: `Fwd: ${originalMsg.subject}`,
995
+ body: `<pre style="font-family: system-ui, sans-serif; white-space: pre-wrap;">${escapeHtml(forwarded)}</pre>`,
996
+ });
997
+ return {
998
+ content: [
999
+ {
1000
+ type: 'text',
1001
+ text: JSON.stringify({
1002
+ success: result.success,
1003
+ to,
1004
+ subject: `Fwd: ${originalMsg.subject}`,
1005
+ message: result.success ? 'Email forwarded' : result.message,
1006
+ }, null, 2),
1007
+ },
1008
+ ],
1009
+ };
1010
+ });
1011
+ server.tool('delete_email', 'Delete an email by UID. Marks it as deleted and expunges.', {
1012
+ uid: zod_1.z.number().describe('Message UID to delete'),
1013
+ folder: zod_1.z.string().optional().describe('Folder name (default: INBOX)'),
1014
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
1015
+ }, async ({ uid, folder, profile }) => {
1016
+ const targetFolder = folder || 'INBOX';
1017
+ await withImap(async (client) => {
1018
+ await client.selectFolder(targetFolder);
1019
+ await client.deleteMessage(uid);
1020
+ }, profile);
1021
+ return {
1022
+ content: [{ type: 'text', text: JSON.stringify({ success: true, uid, message: 'Message deleted' }, null, 2) }],
1023
+ };
1024
+ });
1025
+ server.tool('move_email', 'Move an email to a different folder.', {
1026
+ uid: zod_1.z.number().describe('Message UID to move'),
1027
+ destination: zod_1.z.string().describe('Destination folder name'),
1028
+ folder: zod_1.z.string().optional().describe('Source folder (default: INBOX)'),
1029
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
1030
+ }, async ({ uid, destination, folder, profile }) => {
1031
+ const sourceFolder = folder || 'INBOX';
1032
+ await withImap(async (client) => {
1033
+ await client.selectFolder(sourceFolder);
1034
+ await client.moveMessage(uid, destination);
1035
+ }, profile);
1036
+ return {
1037
+ content: [
1038
+ {
1039
+ type: 'text',
1040
+ text: JSON.stringify({ success: true, uid, from: sourceFolder, to: destination, message: 'Message moved' }, null, 2),
1041
+ },
1042
+ ],
1043
+ };
1044
+ });
1045
+ server.tool('mark_email', 'Mark an email as read, unread, flagged (starred), or unflagged. Supports multiple status changes at once.', {
1046
+ uid: zod_1.z.number().describe('Message UID'),
1047
+ status: zod_1.z
1048
+ .enum(['read', 'unread', 'flagged', 'unflagged'])
1049
+ .describe('Mark as "read", "unread", "flagged", or "unflagged"'),
1050
+ folder: zod_1.z.string().optional().describe('Folder name (default: INBOX)'),
1051
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
1052
+ }, async ({ uid, status, folder, profile }) => {
1053
+ const targetFolder = folder || 'INBOX';
1054
+ await withImap(async (client) => {
1055
+ await client.selectFolder(targetFolder);
1056
+ switch (status) {
1057
+ case 'read':
1058
+ await client.setFlags(uid, '\\Seen');
1059
+ break;
1060
+ case 'unread':
1061
+ await client.setFlags(uid, '\\Seen', '-');
1062
+ break;
1063
+ case 'flagged':
1064
+ await client.setFlags(uid, '\\Flagged');
1065
+ break;
1066
+ case 'unflagged':
1067
+ await client.setFlags(uid, '\\Flagged', '-');
1068
+ break;
1069
+ }
1070
+ }, profile);
1071
+ return {
1072
+ content: [
1073
+ {
1074
+ type: 'text',
1075
+ text: JSON.stringify({ success: true, uid, status, message: `Marked as ${status}` }, null, 2),
1076
+ },
1077
+ ],
1078
+ };
1079
+ });
1080
+ server.tool('get_unread_count', 'Get the number of unread messages in a folder. Quick check without fetching message details.', {
1081
+ folder: zod_1.z.string().optional().describe('Folder name (default: INBOX)'),
1082
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
1083
+ }, async ({ folder, profile }) => {
1084
+ const targetFolder = folder || 'INBOX';
1085
+ const result = await withImap(async (client) => {
1086
+ const info = await client.selectFolder(targetFolder);
1087
+ const unreadUids = await client.search('UNSEEN');
1088
+ return {
1089
+ folder: targetFolder,
1090
+ total: info.exists,
1091
+ unread: unreadUids.length,
1092
+ recent: info.recent,
1093
+ };
1094
+ }, profile);
1095
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1096
+ });
1097
+ server.tool('list_mail_folders', 'List all IMAP mailbox folders (Inbox, Sent, Drafts, Trash, custom folders, etc.)', {
1098
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
1099
+ }, async ({ profile }) => {
1100
+ const result = await withImap(async (client) => {
1101
+ const folders = await client.listFolders();
1102
+ return {
1103
+ folders: folders.map((f) => ({
1104
+ name: f.name,
1105
+ delimiter: f.delimiter,
1106
+ flags: f.flags,
1107
+ selectable: !f.flags.includes('\\Noselect'),
1108
+ })),
1109
+ };
1110
+ }, profile);
1111
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1112
+ });
1113
+ server.tool('create_mail_folder', 'Create a new IMAP mailbox folder.', {
1114
+ name: zod_1.z.string().describe('Folder name to create'),
1115
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
1116
+ }, async ({ name, profile }) => {
1117
+ await withImap(async (client) => {
1118
+ await client.createFolder(name);
1119
+ }, profile);
1120
+ return {
1121
+ content: [
1122
+ { type: 'text', text: JSON.stringify({ success: true, folder: name, message: 'Folder created' }, null, 2) },
1123
+ ],
1124
+ };
1125
+ });
1126
+ server.tool('delete_mail_folder', 'Delete an IMAP mailbox folder and all its contents.', {
1127
+ name: zod_1.z.string().describe('Folder name to delete'),
1128
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
1129
+ }, async ({ name, profile }) => {
1130
+ await withImap(async (client) => {
1131
+ await client.deleteFolder(name);
1132
+ }, profile);
1133
+ return {
1134
+ content: [
1135
+ { type: 'text', text: JSON.stringify({ success: true, folder: name, message: 'Folder deleted' }, null, 2) },
1136
+ ],
1137
+ };
1138
+ });
1139
+ // ─── Attachment Tools ────────────────────────────────────
1140
+ server.tool('download_attachment', 'Download an attachment from an email by UID and attachment index. Returns the attachment content as base64.', {
1141
+ uid: zod_1.z.number().describe('Message UID (from list_messages or read_email)'),
1142
+ index: zod_1.z.number().describe('Attachment index (0-based, from read_email attachments array)'),
1143
+ folder: zod_1.z.string().optional().describe('Folder name (default: INBOX)'),
1144
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
1145
+ }, async ({ uid, index, folder, profile }) => {
1146
+ const targetFolder = folder || 'INBOX';
1147
+ const result = await withImap(async (client) => {
1148
+ await client.selectFolder(targetFolder);
1149
+ const rawBody = await client.fetchBody(uid);
1150
+ const msg = (0, mime_1.parseMessage)(rawBody);
1151
+ if (msg.attachments.length === 0) {
1152
+ throw new Error('This email has no attachments.');
1153
+ }
1154
+ if (index < 0 || index >= msg.attachments.length) {
1155
+ throw new Error(`Invalid attachment index ${index}. This email has ${msg.attachments.length} attachment(s) (0-${msg.attachments.length - 1}).`);
1156
+ }
1157
+ const att = msg.attachments[index];
1158
+ return {
1159
+ uid,
1160
+ folder: targetFolder,
1161
+ filename: att.filename,
1162
+ contentType: att.contentType,
1163
+ size: att.size,
1164
+ sizeFormatted: (0, mime_1.formatFileSize)(att.size),
1165
+ contentBase64: att.content.toString('base64'),
1166
+ };
1167
+ }, profile);
1168
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1169
+ });
1170
+ // ─── Bulk Operation Tools ────────────────────────────────
1171
+ server.tool('bulk_mark', 'Mark multiple emails at once. Efficient batch operation for read/unread/flagged/unflagged.', {
1172
+ uids: zod_1.z.array(zod_1.z.number()).describe('Array of message UIDs to mark'),
1173
+ status: zod_1.z.enum(['read', 'unread', 'flagged', 'unflagged']).describe('Status to apply'),
1174
+ folder: zod_1.z.string().optional().describe('Folder name (default: INBOX)'),
1175
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
1176
+ }, async ({ uids, status, folder, profile }) => {
1177
+ const targetFolder = folder || 'INBOX';
1178
+ await withImap(async (client) => {
1179
+ await client.selectFolder(targetFolder);
1180
+ for (const uid of uids) {
1181
+ switch (status) {
1182
+ case 'read':
1183
+ await client.setFlags(uid, '\\Seen');
1184
+ break;
1185
+ case 'unread':
1186
+ await client.setFlags(uid, '\\Seen', '-');
1187
+ break;
1188
+ case 'flagged':
1189
+ await client.setFlags(uid, '\\Flagged');
1190
+ break;
1191
+ case 'unflagged':
1192
+ await client.setFlags(uid, '\\Flagged', '-');
1193
+ break;
1194
+ }
1195
+ }
1196
+ }, profile);
1197
+ return {
1198
+ content: [
1199
+ {
1200
+ type: 'text',
1201
+ text: JSON.stringify({ success: true, count: uids.length, status, message: `${uids.length} messages marked as ${status}` }, null, 2),
1202
+ },
1203
+ ],
1204
+ };
1205
+ });
1206
+ server.tool('bulk_delete', 'Delete multiple emails at once. Messages are permanently removed.', {
1207
+ uids: zod_1.z.array(zod_1.z.number()).describe('Array of message UIDs to delete'),
1208
+ folder: zod_1.z.string().optional().describe('Folder name (default: INBOX)'),
1209
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
1210
+ }, async ({ uids, folder, profile }) => {
1211
+ const targetFolder = folder || 'INBOX';
1212
+ await withImap(async (client) => {
1213
+ await client.selectFolder(targetFolder);
1214
+ for (const uid of uids) {
1215
+ await client.deleteMessage(uid);
1216
+ }
1217
+ }, profile);
1218
+ return {
1219
+ content: [
1220
+ {
1221
+ type: 'text',
1222
+ text: JSON.stringify({ success: true, count: uids.length, message: `${uids.length} messages deleted` }, null, 2),
1223
+ },
1224
+ ],
1225
+ };
1226
+ });
1227
+ server.tool('bulk_move', 'Move multiple emails to another folder at once.', {
1228
+ uids: zod_1.z.array(zod_1.z.number()).describe('Array of message UIDs to move'),
1229
+ destination: zod_1.z.string().describe('Destination folder name'),
1230
+ folder: zod_1.z.string().optional().describe('Source folder name (default: INBOX)'),
1231
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
1232
+ }, async ({ uids, destination, folder, profile }) => {
1233
+ const sourceFolder = folder || 'INBOX';
1234
+ await withImap(async (client) => {
1235
+ await client.selectFolder(sourceFolder);
1236
+ for (const uid of uids) {
1237
+ await client.moveMessage(uid, destination);
1238
+ }
1239
+ }, profile);
1240
+ return {
1241
+ content: [
1242
+ {
1243
+ type: 'text',
1244
+ text: JSON.stringify({
1245
+ success: true,
1246
+ count: uids.length,
1247
+ from: sourceFolder,
1248
+ to: destination,
1249
+ message: `${uids.length} messages moved`,
1250
+ }, null, 2),
1251
+ },
1252
+ ],
1253
+ };
1254
+ });
1255
+ // ─── Utility / Diagnostics Tools ─────────────────────────
1256
+ server.tool('security_audit', 'Run a comprehensive security audit across all domains. Checks MX, SPF, DKIM, DMARC, catch-all, forwarding loops, and account counts. Returns a scored report.', {}, async () => {
1257
+ const creds = getCreds();
1258
+ const config = (0, config_1.getConfig)();
1259
+ const domains = await (0, directadmin_1.listDomains)(creds);
1260
+ const results = [];
1261
+ let score = 100;
1262
+ for (const domain of domains) {
1263
+ const mx = await (0, dns_1.checkMxRecords)(domain, config.server);
1264
+ results.push({ domain, check: 'MX Records', status: mx.status, message: mx.message });
1265
+ if (mx.status === 'fail')
1266
+ score -= 15;
1267
+ else if (mx.status === 'warn')
1268
+ score -= 5;
1269
+ const spf = await (0, dns_1.checkSpfRecord)(domain);
1270
+ results.push({ domain, check: 'SPF Record', status: spf.status, message: spf.message });
1271
+ if (spf.status === 'fail')
1272
+ score -= 15;
1273
+ else if (spf.status === 'warn')
1274
+ score -= 5;
1275
+ const dkim = await (0, dns_1.checkDkimRecord)(domain);
1276
+ results.push({ domain, check: 'DKIM Record', status: dkim.status, message: dkim.message });
1277
+ if (dkim.status === 'fail')
1278
+ score -= 15;
1279
+ const dmarc = await (0, dns_1.checkDmarcRecord)(domain);
1280
+ results.push({ domain, check: 'DMARC Record', status: dmarc.status, message: dmarc.message });
1281
+ if (dmarc.status === 'fail')
1282
+ score -= 10;
1283
+ else if (dmarc.status === 'warn')
1284
+ score -= 5;
1285
+ try {
1286
+ const catchAll = await (0, directadmin_1.getCatchAll)(creds, domain);
1287
+ if (catchAll && catchAll !== ':fail:' && catchAll !== ':blackhole:') {
1288
+ results.push({ domain, check: 'Catch-All', status: 'warn', message: `Enabled: ${catchAll} — attracts spam` });
1289
+ score -= 5;
1290
+ }
1291
+ else {
1292
+ results.push({ domain, check: 'Catch-All', status: 'pass', message: 'Disabled' });
1293
+ }
1294
+ }
1295
+ catch {
1296
+ results.push({ domain, check: 'Catch-All', status: 'info', message: 'Could not check' });
1297
+ }
1298
+ try {
1299
+ const forwarders = await (0, directadmin_1.listForwarders)(creds, domain);
1300
+ for (const fwd of forwarders) {
1301
+ try {
1302
+ const dest = await (0, directadmin_1.getForwarderDestination)(creds, domain, fwd);
1303
+ if (dest.includes(`@${domain}`)) {
1304
+ results.push({ domain, check: 'Forwarding Loop', status: 'warn', message: `${fwd}@${domain} → ${dest}` });
1305
+ score -= 3;
1306
+ }
1307
+ }
1308
+ catch {
1309
+ /* skip */
1310
+ }
1311
+ }
1312
+ }
1313
+ catch {
1314
+ /* skip */
1315
+ }
1316
+ try {
1317
+ const accounts = await (0, directadmin_1.listEmailAccounts)(creds, domain);
1318
+ results.push({ domain, check: 'Account Count', status: 'info', message: `${accounts.length} accounts` });
1319
+ }
1320
+ catch {
1321
+ /* skip */
1322
+ }
1323
+ }
1324
+ score = Math.max(0, Math.min(100, score));
1325
+ const rating = score >= 90 ? 'Excellent' : score >= 70 ? 'Good' : score >= 50 ? 'Fair' : 'Poor';
1326
+ return {
1327
+ content: [{ type: 'text', text: JSON.stringify({ score, rating, domains: domains.length, results }, null, 2) }],
1328
+ };
1329
+ });
1330
+ server.tool('check_reputation', 'Analyze sender reputation for a domain by checking SPF, DKIM, DMARC, MX records, and blacklists. Returns a scored assessment.', {
1331
+ domain: zod_1.z.string().describe('Domain to check'),
1332
+ }, async ({ domain }) => {
1333
+ const config = (0, config_1.getConfig)();
1334
+ const dns = await Promise.resolve().then(() => __importStar(require('dns')));
1335
+ const resolveTxt = (d) => new Promise((resolve) => dns.resolveTxt(d, (err, records) => resolve(err ? [] : records || [])));
1336
+ const resolveA = (d) => new Promise((resolve) => dns.resolve4(d, (err, addrs) => resolve(err ? [] : addrs || [])));
1337
+ const resolveMxRecords = (d) => new Promise((resolve) => dns.resolveMx(d, (err, addrs) => resolve(err ? [] : addrs || [])));
1338
+ const checks = [];
1339
+ // SPF
1340
+ const txtRecords = await resolveTxt(domain);
1341
+ const spfRecord = txtRecords.flat().find((r) => r.startsWith('v=spf1'));
1342
+ if (spfRecord) {
1343
+ const hasHardFail = spfRecord.includes('-all');
1344
+ checks.push({
1345
+ name: 'SPF',
1346
+ status: hasHardFail ? 'pass' : 'warn',
1347
+ detail: spfRecord,
1348
+ score: hasHardFail ? 20 : 10,
1349
+ });
1350
+ }
1351
+ else {
1352
+ checks.push({ name: 'SPF', status: 'fail', detail: 'No SPF record', score: 0 });
1353
+ }
1354
+ // DKIM
1355
+ const dkimSelectors = ['x', 'default', 'google', 'selector1', 'selector2'];
1356
+ let dkimFound = false;
1357
+ for (const sel of dkimSelectors) {
1358
+ const dkimRecs = await resolveTxt(`${sel}._domainkey.${domain}`);
1359
+ if (dkimRecs.length > 0 && dkimRecs.flat().some((r) => r.includes('DKIM1'))) {
1360
+ checks.push({ name: 'DKIM', status: 'pass', detail: `Found (selector: ${sel})`, score: 20 });
1361
+ dkimFound = true;
1362
+ break;
1363
+ }
1364
+ }
1365
+ if (!dkimFound)
1366
+ checks.push({ name: 'DKIM', status: 'warn', detail: 'Not found', score: 0 });
1367
+ // DMARC
1368
+ const dmarcRecs = await resolveTxt(`_dmarc.${domain}`);
1369
+ const dmarcRec = dmarcRecs.flat().find((r) => r.startsWith('v=DMARC1'));
1370
+ if (dmarcRec) {
1371
+ const hasReject = dmarcRec.includes('p=reject');
1372
+ const hasQuarantine = dmarcRec.includes('p=quarantine');
1373
+ checks.push({
1374
+ name: 'DMARC',
1375
+ status: hasReject || hasQuarantine ? 'pass' : 'warn',
1376
+ detail: dmarcRec,
1377
+ score: hasReject ? 20 : hasQuarantine ? 15 : 5,
1378
+ });
1379
+ }
1380
+ else {
1381
+ checks.push({ name: 'DMARC', status: 'fail', detail: 'No DMARC record', score: 0 });
1382
+ }
1383
+ // MX
1384
+ const mxRecs = await resolveMxRecords(domain);
1385
+ if (mxRecs.length > 0) {
1386
+ checks.push({ name: 'MX', status: 'pass', detail: mxRecs.map((r) => r.exchange).join(', '), score: 15 });
1387
+ }
1388
+ else {
1389
+ checks.push({ name: 'MX', status: 'fail', detail: 'No MX records', score: 0 });
1390
+ }
1391
+ // Blacklists
1392
+ const serverHost = config.server ? `${config.server}.mxrouting.net` : null;
1393
+ if (serverHost) {
1394
+ const ips = await resolveA(serverHost);
1395
+ if (ips.length > 0) {
1396
+ const reversed = ips[0].split('.').reverse().join('.');
1397
+ const blacklists = ['zen.spamhaus.org', 'bl.spamcop.net', 'b.barracudacentral.org'];
1398
+ let listed = false;
1399
+ for (const bl of blacklists) {
1400
+ try {
1401
+ const res = await resolveA(`${reversed}.${bl}`);
1402
+ if (res.length > 0) {
1403
+ listed = true;
1404
+ checks.push({ name: 'Blacklist', status: 'fail', detail: `Listed on ${bl}`, score: 0 });
1405
+ }
1406
+ }
1407
+ catch {
1408
+ /* not listed */
1409
+ }
1410
+ }
1411
+ if (!listed)
1412
+ checks.push({ name: 'Blacklist', status: 'pass', detail: 'Not listed', score: 15 });
1413
+ checks.push({ name: 'Server IP', status: 'pass', detail: `${serverHost} → ${ips[0]}`, score: 10 });
1414
+ }
1415
+ }
1416
+ const totalScore = Math.min(checks.reduce((s, c) => s + c.score, 0), 100);
1417
+ const rating = totalScore >= 90 ? 'Excellent' : totalScore >= 70 ? 'Good' : totalScore >= 50 ? 'Fair' : 'Poor';
1418
+ return {
1419
+ content: [{ type: 'text', text: JSON.stringify({ domain, score: totalScore, rating, checks }, null, 2) }],
1420
+ };
1421
+ });
1422
+ server.tool('ssl_check', 'Check SSL/TLS certificates on IMAP (993), SMTP (465), POP3 (995), and DirectAdmin (2222) ports.', {
1423
+ server: zod_1.z.string().optional().describe('Server hostname (defaults to configured server)'),
1424
+ }, async ({ server: serverArg }) => {
1425
+ const config = (0, config_1.getConfig)();
1426
+ const serverName = serverArg || config.server;
1427
+ if (!serverName)
1428
+ throw new Error('No server specified. Run "mxroute config setup" first.');
1429
+ const host = serverName.includes('.') ? serverName : `${serverName}.mxrouting.net`;
1430
+ const tls = await Promise.resolve().then(() => __importStar(require('tls')));
1431
+ const ports = [
1432
+ { port: 993, label: 'IMAP' },
1433
+ { port: 465, label: 'SMTP' },
1434
+ { port: 995, label: 'POP3' },
1435
+ { port: 2222, label: 'DirectAdmin' },
1436
+ ];
1437
+ const results = [];
1438
+ for (const { port, label } of ports) {
1439
+ try {
1440
+ const info = await new Promise((resolve, reject) => {
1441
+ const socket = tls.connect({ host, port, servername: host, rejectUnauthorized: false }, () => {
1442
+ const cert = socket.getPeerCertificate();
1443
+ const protocol = socket.getProtocol() || 'unknown';
1444
+ const cipher = socket.getCipher();
1445
+ const validTo = new Date(cert.valid_to);
1446
+ const daysRemaining = Math.floor((validTo.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
1447
+ resolve({
1448
+ subject: String(cert.subject?.CN || ''),
1449
+ issuer: String(cert.issuer?.O || cert.issuer?.CN || ''),
1450
+ validFrom: cert.valid_from,
1451
+ validTo: cert.valid_to,
1452
+ daysRemaining,
1453
+ protocol,
1454
+ cipher: cipher ? cipher.name : 'unknown',
1455
+ });
1456
+ socket.destroy();
1457
+ });
1458
+ socket.setTimeout(10000);
1459
+ socket.on('timeout', () => {
1460
+ socket.destroy();
1461
+ reject(new Error('Timed out'));
1462
+ });
1463
+ socket.on('error', reject);
1464
+ });
1465
+ results.push({
1466
+ port,
1467
+ label,
1468
+ status: info.daysRemaining <= 0 ? 'expired' : info.daysRemaining <= 14 ? 'expiring_soon' : 'valid',
1469
+ ...info,
1470
+ });
1471
+ }
1472
+ catch (err) {
1473
+ results.push({ port, label, status: 'error', error: err.message });
1474
+ }
1475
+ }
1476
+ return { content: [{ type: 'text', text: JSON.stringify({ host, certificates: results }, null, 2) }] };
1477
+ });
1478
+ server.tool('test_delivery', 'Send a test email to yourself and measure delivery time. Validates SMTP connectivity.', {
1479
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
1480
+ }, async ({ profile }) => {
1481
+ const smtp = resolveSmtpConfig(profile);
1482
+ const testId = Math.random().toString(36).substring(2, 10);
1483
+ const subject = `MXroute Delivery Test [${testId}]`;
1484
+ const startTime = Date.now();
1485
+ const result = await (0, api_1.sendEmail)({
1486
+ server: `${smtp.server}.mxrouting.net`,
1487
+ username: smtp.username,
1488
+ password: smtp.password,
1489
+ from: smtp.username,
1490
+ to: smtp.username,
1491
+ subject,
1492
+ body: `<p>Test ID: ${testId}. Sent: ${new Date().toISOString()}</p>`,
1493
+ });
1494
+ const duration = Date.now() - startTime;
1495
+ const rating = duration > 5000 ? 'Slow' : duration > 2000 ? 'Average' : duration > 1000 ? 'Good' : 'Excellent';
1496
+ return {
1497
+ content: [
1498
+ {
1499
+ type: 'text',
1500
+ text: JSON.stringify({
1501
+ success: result.success,
1502
+ testId,
1503
+ subject,
1504
+ durationMs: duration,
1505
+ rating,
1506
+ message: result.success ? 'Check inbox for test email' : result.message,
1507
+ }, null, 2),
1508
+ },
1509
+ ],
1510
+ };
1511
+ });
1512
+ server.tool('check_rate_limit', 'Show current SMTP sending rate usage. Tracks sends via CLI against the 400 emails/hour limit.', {}, async () => {
1513
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1514
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
1515
+ const { getConfigPath } = await Promise.resolve().then(() => __importStar(require('./utils/config')));
1516
+ const rateFile = path.join(path.dirname(getConfigPath()), '.mxroute-send-log.json');
1517
+ let sends = [];
1518
+ try {
1519
+ sends = JSON.parse(fs.readFileSync(rateFile, 'utf-8')).sends || [];
1520
+ }
1521
+ catch {
1522
+ /* no log */
1523
+ }
1524
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
1525
+ sends = sends.filter((t) => t > oneHourAgo);
1526
+ const limit = 400;
1527
+ const remaining = Math.max(0, limit - sends.length);
1528
+ const usagePercent = Math.round((sends.length / limit) * 100);
1529
+ return {
1530
+ content: [
1531
+ {
1532
+ type: 'text',
1533
+ text: JSON.stringify({
1534
+ sentThisHour: sends.length,
1535
+ limit,
1536
+ remaining,
1537
+ usagePercent,
1538
+ status: usagePercent >= 90 ? 'critical' : usagePercent >= 70 ? 'warning' : 'healthy',
1539
+ }, null, 2),
1540
+ },
1541
+ ],
1542
+ };
1543
+ });
1544
+ server.tool('search_accounts', 'Search for email accounts by name or address across all domains.', {
1545
+ query: zod_1.z.string().describe('Search term (matches against username and email address)'),
1546
+ }, async ({ query }) => {
1547
+ const creds = getCreds();
1548
+ const domains = await (0, directadmin_1.listDomains)(creds);
1549
+ const results = [];
1550
+ const searchLower = query.toLowerCase();
1551
+ for (const domain of domains) {
1552
+ try {
1553
+ const accounts = await (0, directadmin_1.listEmailAccounts)(creds, domain);
1554
+ for (const user of accounts) {
1555
+ const email = `${user}@${domain}`;
1556
+ if (email.toLowerCase().includes(searchLower)) {
1557
+ results.push({ email, domain, user });
1558
+ }
1559
+ }
1560
+ }
1561
+ catch {
1562
+ /* skip */
1563
+ }
1564
+ }
1565
+ return {
1566
+ content: [
1567
+ {
1568
+ type: 'text',
1569
+ text: JSON.stringify({ query, domainsSearched: domains.length, matches: results.length, results }, null, 2),
1570
+ },
1571
+ ],
1572
+ };
1573
+ });
1574
+ server.tool('validate_forwarders', 'Validate all forwarders for a domain by checking destination MX/A records.', {
1575
+ domain: zod_1.z.string().describe('Domain to validate forwarders for'),
1576
+ }, async ({ domain }) => {
1577
+ const creds = getCreds();
1578
+ const dns = await Promise.resolve().then(() => __importStar(require('dns')));
1579
+ const resolveMxRecords = (d) => new Promise((resolve) => dns.resolveMx(d, (err, addrs) => resolve(err ? [] : addrs || [])));
1580
+ const forwarders = await (0, directadmin_1.listForwarders)(creds, domain);
1581
+ const results = [];
1582
+ for (const fwd of forwarders) {
1583
+ try {
1584
+ const dest = await (0, directadmin_1.getForwarderDestination)(creds, domain, fwd);
1585
+ const destinations = dest
1586
+ .split(',')
1587
+ .map((d) => d.trim())
1588
+ .filter(Boolean);
1589
+ for (const destEmail of destinations) {
1590
+ const atIdx = destEmail.lastIndexOf('@');
1591
+ if (atIdx === -1)
1592
+ continue;
1593
+ const destDomain = destEmail.substring(atIdx + 1);
1594
+ if (destDomain === domain) {
1595
+ results.push({
1596
+ forwarder: `${fwd}@${domain}`,
1597
+ destination: destEmail,
1598
+ status: 'warn',
1599
+ issue: 'Forwards to same domain',
1600
+ });
1601
+ }
1602
+ else {
1603
+ const mx = await resolveMxRecords(destDomain);
1604
+ if (mx.length === 0) {
1605
+ results.push({
1606
+ forwarder: `${fwd}@${domain}`,
1607
+ destination: destEmail,
1608
+ status: 'fail',
1609
+ issue: `No MX records for ${destDomain}`,
1610
+ });
1611
+ }
1612
+ else {
1613
+ results.push({ forwarder: `${fwd}@${domain}`, destination: destEmail, status: 'pass' });
1614
+ }
1615
+ }
1616
+ }
1617
+ }
1618
+ catch {
1619
+ /* skip */
1620
+ }
1621
+ }
1622
+ const valid = results.filter((r) => r.status === 'pass').length;
1623
+ const invalid = results.filter((r) => r.status === 'fail').length;
1624
+ return {
1625
+ content: [
1626
+ { type: 'text', text: JSON.stringify({ domain, total: results.length, valid, invalid, results }, null, 2) },
1627
+ ],
1628
+ };
1629
+ });
1630
+ server.tool('cleanup_audit', 'Scan for orphaned forwarders, unused autoresponders, misconfigured catch-all, and account/forwarder conflicts across all domains.', {}, async () => {
1631
+ const creds = getCreds();
1632
+ const domains = await (0, directadmin_1.listDomains)(creds);
1633
+ const dns = await Promise.resolve().then(() => __importStar(require('dns')));
1634
+ const resolveMxRecords = (d) => new Promise((resolve) => dns.resolveMx(d, (err, addrs) => resolve(err ? [] : addrs || [])));
1635
+ const issues = [];
1636
+ for (const domain of domains) {
1637
+ let accounts = [];
1638
+ let forwarders = [];
1639
+ let autoresponders = [];
1640
+ try {
1641
+ accounts = await (0, directadmin_1.listEmailAccounts)(creds, domain);
1642
+ }
1643
+ catch {
1644
+ /* skip */
1645
+ }
1646
+ try {
1647
+ forwarders = await (0, directadmin_1.listForwarders)(creds, domain);
1648
+ }
1649
+ catch {
1650
+ /* skip */
1651
+ }
1652
+ try {
1653
+ autoresponders = await (0, directadmin_1.listAutoresponders)(creds, domain);
1654
+ }
1655
+ catch {
1656
+ /* skip */
1657
+ }
1658
+ for (const fwd of forwarders) {
1659
+ try {
1660
+ const dest = await (0, directadmin_1.getForwarderDestination)(creds, domain, fwd);
1661
+ for (const destEmail of dest
1662
+ .split(',')
1663
+ .map((d) => d.trim())
1664
+ .filter(Boolean)) {
1665
+ const atIdx = destEmail.lastIndexOf('@');
1666
+ if (atIdx === -1)
1667
+ continue;
1668
+ const destDomain = destEmail.substring(atIdx + 1);
1669
+ const destUser = destEmail.substring(0, atIdx);
1670
+ if (destDomain === domain && !accounts.includes(destUser)) {
1671
+ issues.push({
1672
+ type: 'orphaned',
1673
+ severity: 'medium',
1674
+ domain,
1675
+ resource: `${fwd}@${domain} → ${destEmail}`,
1676
+ description: 'Forwards to non-existent local account',
1677
+ });
1678
+ }
1679
+ else if (destDomain !== domain) {
1680
+ const mx = await resolveMxRecords(destDomain);
1681
+ if (mx.length === 0) {
1682
+ issues.push({
1683
+ type: 'orphaned',
1684
+ severity: 'high',
1685
+ domain,
1686
+ resource: `${fwd}@${domain} → ${destEmail}`,
1687
+ description: `Destination domain "${destDomain}" has no MX records`,
1688
+ });
1689
+ }
1690
+ }
1691
+ }
1692
+ }
1693
+ catch {
1694
+ /* skip */
1695
+ }
1696
+ }
1697
+ for (const ar of autoresponders) {
1698
+ if (!accounts.includes(ar)) {
1699
+ issues.push({
1700
+ type: 'orphaned',
1701
+ severity: 'low',
1702
+ domain,
1703
+ resource: `autoresponder: ${ar}@${domain}`,
1704
+ description: 'Autoresponder for non-existent account',
1705
+ });
1706
+ }
1707
+ }
1708
+ for (const fwd of forwarders) {
1709
+ if (accounts.includes(fwd)) {
1710
+ issues.push({
1711
+ type: 'conflict',
1712
+ severity: 'low',
1713
+ domain,
1714
+ resource: `${fwd}@${domain}`,
1715
+ description: 'Account and forwarder share same name',
1716
+ });
1717
+ }
1718
+ }
1719
+ try {
1720
+ const catchAllVal = await (0, directadmin_1.getCatchAll)(creds, domain);
1721
+ if (catchAllVal &&
1722
+ catchAllVal !== ':fail:' &&
1723
+ catchAllVal !== ':blackhole:' &&
1724
+ !accounts.includes(catchAllVal)) {
1725
+ issues.push({
1726
+ type: 'misconfigured',
1727
+ severity: 'medium',
1728
+ domain,
1729
+ resource: `catch-all: ${catchAllVal}`,
1730
+ description: 'Catch-all points to non-existent account',
1731
+ });
1732
+ }
1733
+ }
1734
+ catch {
1735
+ /* skip */
1736
+ }
1737
+ }
1738
+ return {
1739
+ content: [
1740
+ {
1741
+ type: 'text',
1742
+ text: JSON.stringify({
1743
+ domainsScanned: domains.length,
1744
+ totalIssues: issues.length,
1745
+ high: issues.filter((i) => i.severity === 'high').length,
1746
+ medium: issues.filter((i) => i.severity === 'medium').length,
1747
+ low: issues.filter((i) => i.severity === 'low').length,
1748
+ issues,
1749
+ }, null, 2),
1750
+ },
1751
+ ],
1752
+ };
1753
+ });
1754
+ server.tool('health_check', 'Run comprehensive health diagnostics: config validation, API connectivity, DNS checks across all domains, and quota status.', {}, async () => {
1755
+ const config = (0, config_1.getConfig)();
1756
+ const checks = [];
1757
+ // Config
1758
+ checks.push({
1759
+ category: 'Config',
1760
+ check: 'Server',
1761
+ status: config.server ? 'pass' : 'fail',
1762
+ message: config.server ? `${config.server}.mxrouting.net` : 'Not configured',
1763
+ });
1764
+ checks.push({
1765
+ category: 'Config',
1766
+ check: 'SMTP',
1767
+ status: config.username ? 'pass' : 'info',
1768
+ message: config.username || 'Not configured',
1769
+ });
1770
+ // DirectAdmin API
1771
+ if (config.daUsername && config.daLoginKey) {
1772
+ try {
1773
+ const result = await (0, directadmin_2.testAuth)({
1774
+ server: config.server,
1775
+ username: config.daUsername,
1776
+ loginKey: config.daLoginKey,
1777
+ });
1778
+ checks.push({
1779
+ category: 'API',
1780
+ check: 'DirectAdmin Auth',
1781
+ status: result.success ? 'pass' : 'fail',
1782
+ message: result.success ? `Authenticated as ${config.daUsername}` : result.message || 'Auth failed',
1783
+ });
1784
+ }
1785
+ catch (err) {
1786
+ checks.push({ category: 'API', check: 'DirectAdmin Auth', status: 'fail', message: err.message });
1787
+ }
1788
+ // DNS for all domains
1789
+ try {
1790
+ const creds = getCreds();
1791
+ const domains = await (0, directadmin_1.listDomains)(creds);
1792
+ for (const domain of domains) {
1793
+ try {
1794
+ const results = await (0, dns_1.runFullDnsCheck)(domain, config.server);
1795
+ const passed = results.filter((r) => r.status === 'pass').length;
1796
+ const failed = results.filter((r) => r.status === 'fail').length;
1797
+ checks.push({
1798
+ category: 'DNS',
1799
+ check: domain,
1800
+ status: failed > 0 ? 'fail' : 'pass',
1801
+ message: `${passed}/${results.length} passed${failed > 0 ? `, ${failed} failed` : ''}`,
1802
+ });
1803
+ }
1804
+ catch {
1805
+ checks.push({ category: 'DNS', check: domain, status: 'fail', message: 'DNS check failed' });
1806
+ }
1807
+ }
1808
+ // Quota
1809
+ try {
1810
+ const usage = await (0, directadmin_1.getQuotaUsage)(creds);
1811
+ const diskUsed = Number(usage.quota || usage.disk || 0);
1812
+ checks.push({ category: 'Quota', check: 'Disk', status: 'pass', message: `${diskUsed} MB used` });
1813
+ }
1814
+ catch {
1815
+ checks.push({ category: 'Quota', check: 'Disk', status: 'warn', message: 'Could not fetch' });
1816
+ }
1817
+ }
1818
+ catch {
1819
+ checks.push({ category: 'DNS', check: 'Domains', status: 'fail', message: 'Could not fetch domains' });
1820
+ }
1821
+ }
1822
+ const issues = checks.filter((c) => c.status === 'fail').length;
1823
+ return {
1824
+ content: [{ type: 'text', text: JSON.stringify({ healthy: issues === 0, issues, checks }, null, 2) }],
1825
+ };
1826
+ });
1827
+ server.tool('password_audit', 'Test a password against common weakness patterns and strength criteria. Returns issues and a strength score.', {
1828
+ password: zod_1.z.string().describe('Password to test'),
1829
+ }, async ({ password }) => {
1830
+ const issues = [];
1831
+ if (password.length < 8)
1832
+ issues.push('Too short (< 8 characters)');
1833
+ if (password.length < 12)
1834
+ issues.push('Could be longer (< 12 characters)');
1835
+ if (!/[A-Z]/.test(password))
1836
+ issues.push('No uppercase letters');
1837
+ if (!/[a-z]/.test(password))
1838
+ issues.push('No lowercase letters');
1839
+ if (!/[0-9]/.test(password))
1840
+ issues.push('No numbers');
1841
+ if (!/[^A-Za-z0-9]/.test(password))
1842
+ issues.push('No special characters');
1843
+ const common = [
1844
+ 'password',
1845
+ '123456',
1846
+ '12345678',
1847
+ 'qwerty',
1848
+ 'abc123',
1849
+ 'admin',
1850
+ 'letmein',
1851
+ 'welcome',
1852
+ 'changeme',
1853
+ 'test123',
1854
+ ];
1855
+ if (common.includes(password.toLowerCase()))
1856
+ issues.push('Common/dictionary password');
1857
+ if (/^(.)\1+$/.test(password))
1858
+ issues.push('Repeated character pattern');
1859
+ if (/^[a-z]+$/i.test(password) || /^[0-9]+$/.test(password))
1860
+ issues.push('Single character class');
1861
+ const strength = Math.max(0, 100 - issues.length * 15);
1862
+ const rating = strength >= 70 ? 'strong' : strength >= 40 ? 'moderate' : 'weak';
1863
+ return { content: [{ type: 'text', text: JSON.stringify({ strength, rating, issues }, null, 2) }] };
1864
+ });
1865
+ server.tool('analyze_headers', 'Analyze raw email headers to extract routing hops, authentication results (SPF/DKIM/DMARC), spam scores, and transit time.', {
1866
+ headers: zod_1.z.string().describe('Raw email headers text'),
1867
+ }, async ({ headers }) => {
1868
+ const lines = headers
1869
+ .replace(/\r\n/g, '\n')
1870
+ .replace(/\n[ \t]+/g, ' ')
1871
+ .split('\n')
1872
+ .filter((l) => l.trim());
1873
+ // Parse routing hops
1874
+ const hops = [];
1875
+ for (const line of lines) {
1876
+ if (!line.toLowerCase().startsWith('received:'))
1877
+ continue;
1878
+ const content = line.slice('received:'.length).trim();
1879
+ const fromMatch = content.match(/from\s+(\S+)/i);
1880
+ const byMatch = content.match(/by\s+(\S+)/i);
1881
+ const withMatch = content.match(/with\s+(\S+)/i);
1882
+ const dateMatch = content.match(/;\s*(.+)$/);
1883
+ hops.push({
1884
+ from: fromMatch?.[1] || '',
1885
+ by: byMatch?.[1] || 'unknown',
1886
+ protocol: withMatch?.[1] || '',
1887
+ timestamp: dateMatch?.[1]?.trim() || '',
1888
+ });
1889
+ }
1890
+ hops.reverse();
1891
+ // Parse auth results
1892
+ const authResults = [];
1893
+ for (const line of lines) {
1894
+ if (!line.toLowerCase().startsWith('authentication-results:'))
1895
+ continue;
1896
+ const parts = line.slice('authentication-results:'.length).trim().split(';').slice(1);
1897
+ for (const part of parts) {
1898
+ const match = part.trim().match(/^(\w+)=(\w+)\s*(.*)/);
1899
+ if (match)
1900
+ authResults.push({ method: match[1], result: match[2], details: match[3] || '' });
1901
+ }
1902
+ }
1903
+ // Key headers
1904
+ const findHeader = (name) => {
1905
+ const prefix = name.toLowerCase() + ':';
1906
+ for (const l of lines) {
1907
+ if (l.toLowerCase().startsWith(prefix))
1908
+ return l.slice(prefix.length).trim();
1909
+ }
1910
+ return null;
1911
+ };
1912
+ const spamScore = findHeader('x-spam-score') || findHeader('x-spam-status');
1913
+ return {
1914
+ content: [
1915
+ {
1916
+ type: 'text',
1917
+ text: JSON.stringify({
1918
+ hops: hops.length,
1919
+ route: hops,
1920
+ authentication: authResults,
1921
+ from: findHeader('from'),
1922
+ to: findHeader('to'),
1923
+ subject: findHeader('subject'),
1924
+ date: findHeader('date'),
1925
+ messageId: findHeader('message-id'),
1926
+ returnPath: findHeader('return-path'),
1927
+ spamScore: spamScore || null,
1928
+ }, null, 2),
1929
+ },
1930
+ ],
1931
+ };
1932
+ });
1933
+ server.tool('export_config', 'Export domain configuration (accounts, forwarders, autoresponders, catch-all) as JSON for backup.', {
1934
+ domain: zod_1.z.string().describe('Domain to export'),
1935
+ }, async ({ domain }) => {
1936
+ const creds = getCreds();
1937
+ const data = {
1938
+ domain,
1939
+ exportedAt: new Date().toISOString(),
1940
+ accounts: [],
1941
+ forwarders: [],
1942
+ autoresponders: [],
1943
+ catchAll: null,
1944
+ };
1945
+ try {
1946
+ data.accounts = await (0, directadmin_1.listEmailAccounts)(creds, domain);
1947
+ }
1948
+ catch {
1949
+ /* skip */
1950
+ }
1951
+ try {
1952
+ const fwds = await (0, directadmin_1.listForwarders)(creds, domain);
1953
+ for (const fwd of fwds) {
1954
+ try {
1955
+ const dest = await (0, directadmin_1.getForwarderDestination)(creds, domain, fwd);
1956
+ data.forwarders.push({ name: fwd, destination: dest });
1957
+ }
1958
+ catch {
1959
+ /* skip */
1960
+ }
1961
+ }
1962
+ }
1963
+ catch {
1964
+ /* skip */
1965
+ }
1966
+ try {
1967
+ data.autoresponders = await (0, directadmin_1.listAutoresponders)(creds, domain);
1968
+ }
1969
+ catch {
1970
+ /* skip */
1971
+ }
1972
+ try {
1973
+ data.catchAll = await (0, directadmin_1.getCatchAll)(creds, domain);
1974
+ }
1975
+ catch {
1976
+ /* skip */
1977
+ }
1978
+ try {
1979
+ data.spamConfig = await (0, directadmin_1.getSpamConfig)(creds, domain);
1980
+ }
1981
+ catch {
1982
+ /* skip */
1983
+ }
1984
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
1985
+ });
1986
+ server.tool('list_templates', 'List all saved email templates.', {}, async () => {
1987
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1988
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
1989
+ const os = await Promise.resolve().then(() => __importStar(require('os')));
1990
+ const dir = path.join(os.homedir(), '.config', 'mxroute-cli', 'templates');
1991
+ let templates = [];
1992
+ try {
1993
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
1994
+ templates = files
1995
+ .map((f) => {
1996
+ try {
1997
+ return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
1998
+ }
1999
+ catch {
2000
+ return null;
2001
+ }
2002
+ })
2003
+ .filter(Boolean);
2004
+ }
2005
+ catch {
2006
+ /* no templates dir */
2007
+ }
2008
+ return {
2009
+ content: [
2010
+ {
2011
+ type: 'text',
2012
+ text: JSON.stringify({
2013
+ count: templates.length,
2014
+ templates: templates.map((t) => ({
2015
+ name: t.name,
2016
+ subject: t.subject,
2017
+ isHtml: t.isHtml,
2018
+ variables: t.variables,
2019
+ createdAt: t.createdAt,
2020
+ })),
2021
+ }, null, 2),
2022
+ },
2023
+ ],
2024
+ };
2025
+ });
2026
+ server.tool('save_template', 'Save an email template with optional {{variable}} placeholders.', {
2027
+ name: zod_1.z.string().describe('Template name (letters, numbers, hyphens, underscores)'),
2028
+ subject: zod_1.z.string().describe('Email subject line (supports {{variable}} syntax)'),
2029
+ body: zod_1.z.string().describe('Email body (supports {{variable}} syntax)'),
2030
+ isHtml: zod_1.z.boolean().optional().describe('Whether the body is HTML (default: false)'),
2031
+ }, async ({ name, subject, body, isHtml }) => {
2032
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
2033
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
2034
+ const os = await Promise.resolve().then(() => __importStar(require('os')));
2035
+ const dir = path.join(os.homedir(), '.config', 'mxroute-cli', 'templates');
2036
+ if (!/^[a-zA-Z0-9_-]+$/.test(name))
2037
+ throw new Error('Template name must use only letters, numbers, hyphens, underscores');
2038
+ if (!fs.existsSync(dir))
2039
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
2040
+ const variables = [
2041
+ ...new Set([...(subject.match(/\{\{(\w+)\}\}/g) || []), ...(body.match(/\{\{(\w+)\}\}/g) || [])].map((m) => m.replace(/\{\{|\}\}/g, ''))),
2042
+ ];
2043
+ const template = { name, subject, body, isHtml: isHtml || false, variables, createdAt: new Date().toISOString() };
2044
+ fs.writeFileSync(path.join(dir, `${name}.json`), JSON.stringify(template, null, 2), { mode: 0o600 });
2045
+ return {
2046
+ content: [
2047
+ { type: 'text', text: JSON.stringify({ success: true, name, variables, message: 'Template saved' }, null, 2) },
2048
+ ],
2049
+ };
2050
+ });
2051
+ server.tool('send_template', 'Send an email using a saved template. Variables are replaced with provided values.', {
2052
+ template: zod_1.z.string().describe('Template name'),
2053
+ to: zod_1.z.string().describe('Recipient email'),
2054
+ variables: zod_1.z
2055
+ .record(zod_1.z.string(), zod_1.z.string())
2056
+ .optional()
2057
+ .describe('Variable values as key-value pairs, e.g. {"name": "John"}'),
2058
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
2059
+ }, async ({ template: templateName, to, variables, profile }) => {
2060
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
2061
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
2062
+ const os = await Promise.resolve().then(() => __importStar(require('os')));
2063
+ const smtp = resolveSmtpConfig(profile);
2064
+ const filePath = path.join(os.homedir(), '.config', 'mxroute-cli', 'templates', `${templateName}.json`);
2065
+ let tmpl;
2066
+ try {
2067
+ tmpl = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
2068
+ }
2069
+ catch {
2070
+ throw new Error(`Template "${templateName}" not found`);
2071
+ }
2072
+ let finalSubject = tmpl.subject;
2073
+ let finalBody = tmpl.body;
2074
+ if (variables) {
2075
+ for (const [key, value] of Object.entries(variables)) {
2076
+ finalSubject = finalSubject.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
2077
+ finalBody = finalBody.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
2078
+ }
2079
+ }
2080
+ const result = await (0, api_1.sendEmail)({
2081
+ server: `${smtp.server}.mxrouting.net`,
2082
+ username: smtp.username,
2083
+ password: smtp.password,
2084
+ from: smtp.username,
2085
+ to,
2086
+ subject: finalSubject,
2087
+ body: tmpl.isHtml ? finalBody : `<pre style="font-family: system-ui, sans-serif;">${finalBody}</pre>`,
2088
+ });
2089
+ return {
2090
+ content: [
2091
+ {
2092
+ type: 'text',
2093
+ text: JSON.stringify({ success: result.success, to, subject: finalSubject, template: templateName }, null, 2),
2094
+ },
2095
+ ],
2096
+ };
2097
+ });
2098
+ server.tool('delete_template', 'Delete a saved email template.', {
2099
+ name: zod_1.z.string().describe('Template name to delete'),
2100
+ }, async ({ name }) => {
2101
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
2102
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
2103
+ const os = await Promise.resolve().then(() => __importStar(require('os')));
2104
+ const filePath = path.join(os.homedir(), '.config', 'mxroute-cli', 'templates', `${name}.json`);
2105
+ try {
2106
+ fs.unlinkSync(filePath);
2107
+ }
2108
+ catch {
2109
+ throw new Error(`Template "${name}" not found`);
2110
+ }
2111
+ return {
2112
+ content: [{ type: 'text', text: JSON.stringify({ success: true, name, message: 'Template deleted' }, null, 2) }],
2113
+ };
2114
+ });
2115
+ server.tool('usage_history', 'Get current disk, bandwidth, and account usage with historical trend data if available.', {}, async () => {
2116
+ const creds = getCreds();
2117
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
2118
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
2119
+ const { getConfigPath } = await Promise.resolve().then(() => __importStar(require('./utils/config')));
2120
+ const [usage, userCfg] = await Promise.all([(0, directadmin_1.getQuotaUsage)(creds), (0, directadmin_1.getUserConfig)(creds)]);
2121
+ const snapshot = {
2122
+ timestamp: new Date().toISOString(),
2123
+ disk: Number(usage.quota || usage.disk || 0),
2124
+ diskLimit: Number(userCfg.quota || userCfg.disk || 0),
2125
+ bandwidth: Number(usage.bandwidth || 0),
2126
+ bandwidthLimit: Number(userCfg.bandwidth || 0),
2127
+ emails: Number(usage.nemails || usage.email || 0),
2128
+ domains: Number(usage.vdomains || usage.ndomains || usage.domains || 0),
2129
+ };
2130
+ // Load and update history
2131
+ const historyPath = path.join(path.dirname(getConfigPath()), 'usage-history.json');
2132
+ let history = [];
2133
+ try {
2134
+ history = JSON.parse(fs.readFileSync(historyPath, 'utf-8'));
2135
+ }
2136
+ catch {
2137
+ /* no history */
2138
+ }
2139
+ const lastEntry = history[history.length - 1];
2140
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
2141
+ if (!lastEntry || new Date(lastEntry.timestamp).getTime() < oneHourAgo) {
2142
+ history.push(snapshot);
2143
+ history = history.slice(-90);
2144
+ try {
2145
+ fs.writeFileSync(historyPath, JSON.stringify(history, null, 2), { mode: 0o600 });
2146
+ }
2147
+ catch {
2148
+ /* skip */
2149
+ }
2150
+ }
2151
+ return {
2152
+ content: [
2153
+ {
2154
+ type: 'text',
2155
+ text: JSON.stringify({ current: snapshot, historyEntries: history.length, history: history.slice(-10) }, null, 2),
2156
+ },
2157
+ ],
2158
+ };
2159
+ });
2160
+ // ─── Business Provisioning Tools ─────────────────────────
2161
+ server.tool('self_service_password', 'Change an email account password. Requires the domain, username, and new password. Uses DirectAdmin API.', {
2162
+ domain: zod_1.z.string().describe('Domain (e.g. example.com)'),
2163
+ username: zod_1.z.string().describe('Username part before @'),
2164
+ password: zod_1.z.string().describe('New password (min 8 chars, mix of upper/lower/numbers)'),
2165
+ }, async ({ domain, username, password }) => {
2166
+ const creds = getCreds();
2167
+ if (password.length < 8) {
2168
+ return { content: [{ type: 'text', text: JSON.stringify({ error: 'Password must be at least 8 characters' }) }] };
2169
+ }
2170
+ const result = await (0, directadmin_1.changeEmailPassword)(creds, domain, username, password);
2171
+ if (result.error && result.error !== '0') {
2172
+ return {
2173
+ content: [
2174
+ {
2175
+ type: 'text',
2176
+ text: JSON.stringify({ error: result.text || result.details || 'Failed to change password' }),
2177
+ },
2178
+ ],
2179
+ };
2180
+ }
2181
+ return {
2182
+ content: [
2183
+ {
2184
+ type: 'text',
2185
+ text: JSON.stringify({
2186
+ success: true,
2187
+ account: `${username}@${domain}`,
2188
+ message: 'Password changed successfully',
2189
+ }),
2190
+ },
2191
+ ],
2192
+ };
2193
+ });
2194
+ server.tool('provision_plan', 'Dry-run a provisioning manifest. Shows what accounts and forwarders would be created or skipped. Accepts manifest as JSON object.', {
2195
+ manifest: zod_1.z
2196
+ .object({
2197
+ company: zod_1.z.string().optional(),
2198
+ domains: zod_1.z.array(zod_1.z.object({
2199
+ name: zod_1.z.string(),
2200
+ accounts: zod_1.z
2201
+ .array(zod_1.z.object({
2202
+ user: zod_1.z.string(),
2203
+ password: zod_1.z.string().optional(),
2204
+ quota: zod_1.z.number().optional(),
2205
+ }))
2206
+ .optional(),
2207
+ forwarders: zod_1.z
2208
+ .array(zod_1.z.object({
2209
+ from: zod_1.z.string(),
2210
+ to: zod_1.z.string(),
2211
+ }))
2212
+ .optional(),
2213
+ })),
2214
+ })
2215
+ .describe('Provisioning manifest'),
2216
+ }, async ({ manifest }) => {
2217
+ const creds = getCreds();
2218
+ const plan = [];
2219
+ for (const domain of manifest.domains) {
2220
+ const existingAccounts = await (0, directadmin_1.listEmailAccounts)(creds, domain.name).catch(() => []);
2221
+ const existingForwarders = await (0, directadmin_1.listForwarders)(creds, domain.name).catch(() => []);
2222
+ for (const acct of domain.accounts || []) {
2223
+ if (existingAccounts.includes(acct.user)) {
2224
+ plan.push({
2225
+ action: 'SKIP',
2226
+ type: 'account',
2227
+ resource: `${acct.user}@${domain.name}`,
2228
+ reason: 'already exists',
2229
+ });
2230
+ }
2231
+ else {
2232
+ plan.push({ action: 'CREATE', type: 'account', resource: `${acct.user}@${domain.name}` });
2233
+ }
2234
+ }
2235
+ for (const fwd of domain.forwarders || []) {
2236
+ if (existingForwarders.includes(fwd.from)) {
2237
+ plan.push({
2238
+ action: 'SKIP',
2239
+ type: 'forwarder',
2240
+ resource: `${fwd.from}@${domain.name}`,
2241
+ reason: 'already exists',
2242
+ });
2243
+ }
2244
+ else {
2245
+ plan.push({ action: 'CREATE', type: 'forwarder', resource: `${fwd.from}@${domain.name} → ${fwd.to}` });
2246
+ }
2247
+ }
2248
+ }
2249
+ const creates = plan.filter((p) => p.action === 'CREATE').length;
2250
+ const skips = plan.filter((p) => p.action === 'SKIP').length;
2251
+ return {
2252
+ content: [{ type: 'text', text: JSON.stringify({ plan, summary: { create: creates, skip: skips } }, null, 2) }],
2253
+ };
2254
+ });
2255
+ server.tool('provision_execute', 'Execute a provisioning manifest. Creates accounts and forwarders. Generates random passwords for accounts without one. Returns created resources with credentials.', {
2256
+ manifest: zod_1.z
2257
+ .object({
2258
+ company: zod_1.z.string().optional(),
2259
+ domains: zod_1.z.array(zod_1.z.object({
2260
+ name: zod_1.z.string(),
2261
+ accounts: zod_1.z
2262
+ .array(zod_1.z.object({
2263
+ user: zod_1.z.string(),
2264
+ password: zod_1.z.string().optional(),
2265
+ quota: zod_1.z.number().optional(),
2266
+ }))
2267
+ .optional(),
2268
+ forwarders: zod_1.z
2269
+ .array(zod_1.z.object({
2270
+ from: zod_1.z.string(),
2271
+ to: zod_1.z.string(),
2272
+ }))
2273
+ .optional(),
2274
+ })),
2275
+ })
2276
+ .describe('Provisioning manifest'),
2277
+ }, async ({ manifest }) => {
2278
+ const creds = getCreds();
2279
+ const crypto = await Promise.resolve().then(() => __importStar(require('crypto')));
2280
+ const generatePassword = () => {
2281
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%';
2282
+ let pw = '';
2283
+ const bytes = crypto.randomBytes(16);
2284
+ for (let i = 0; i < 16; i++)
2285
+ pw += chars[bytes[i] % chars.length];
2286
+ return pw;
2287
+ };
2288
+ const results = [];
2289
+ for (const domain of manifest.domains) {
2290
+ const existingAccounts = await (0, directadmin_1.listEmailAccounts)(creds, domain.name).catch(() => []);
2291
+ const existingForwarders = await (0, directadmin_1.listForwarders)(creds, domain.name).catch(() => []);
2292
+ for (const acct of domain.accounts || []) {
2293
+ if (existingAccounts.includes(acct.user)) {
2294
+ results.push({ type: 'account', resource: `${acct.user}@${domain.name}`, status: 'skipped' });
2295
+ continue;
2296
+ }
2297
+ const pw = acct.password || generatePassword();
2298
+ try {
2299
+ await (0, directadmin_1.createEmailAccount)(creds, domain.name, acct.user, pw, acct.quota || 0);
2300
+ results.push({ type: 'account', resource: `${acct.user}@${domain.name}`, status: 'created', password: pw });
2301
+ }
2302
+ catch (err) {
2303
+ results.push({
2304
+ type: 'account',
2305
+ resource: `${acct.user}@${domain.name}`,
2306
+ status: 'failed',
2307
+ error: err.message,
2308
+ });
2309
+ }
2310
+ }
2311
+ for (const fwd of domain.forwarders || []) {
2312
+ if (existingForwarders.includes(fwd.from)) {
2313
+ results.push({ type: 'forwarder', resource: `${fwd.from}@${domain.name}`, status: 'skipped' });
2314
+ continue;
2315
+ }
2316
+ try {
2317
+ await (0, directadmin_1.createForwarder)(creds, domain.name, fwd.from, fwd.to);
2318
+ results.push({ type: 'forwarder', resource: `${fwd.from}@${domain.name} → ${fwd.to}`, status: 'created' });
2319
+ }
2320
+ catch (err) {
2321
+ results.push({
2322
+ type: 'forwarder',
2323
+ resource: `${fwd.from}@${domain.name}`,
2324
+ status: 'failed',
2325
+ error: err.message,
2326
+ });
2327
+ }
2328
+ }
2329
+ }
2330
+ const created = results.filter((r) => r.status === 'created').length;
2331
+ const skipped = results.filter((r) => r.status === 'skipped').length;
2332
+ const failed = results.filter((r) => r.status === 'failed').length;
2333
+ return {
2334
+ content: [{ type: 'text', text: JSON.stringify({ results, summary: { created, skipped, failed } }, null, 2) }],
2335
+ };
2336
+ });
2337
+ server.tool('provision_generate', 'Generate a provisioning manifest from an existing domain configuration. Returns JSON manifest with current accounts and forwarders.', {
2338
+ domain: zod_1.z.string().describe('Domain to generate manifest from'),
2339
+ }, async ({ domain }) => {
2340
+ const creds = getCreds();
2341
+ const accounts = await (0, directadmin_1.listEmailAccounts)(creds, domain).catch(() => []);
2342
+ const fwdNames = await (0, directadmin_1.listForwarders)(creds, domain).catch(() => []);
2343
+ const forwarders = [];
2344
+ for (const fwd of fwdNames) {
2345
+ try {
2346
+ const dest = await (0, directadmin_1.getForwarderDestination)(creds, domain, fwd);
2347
+ forwarders.push({ from: fwd, to: dest });
2348
+ }
2349
+ catch {
2350
+ /* skip */
2351
+ }
2352
+ }
2353
+ const manifest = {
2354
+ company: '',
2355
+ domains: [
2356
+ {
2357
+ name: domain,
2358
+ accounts: accounts.map((a) => ({ user: a, quota: 0 })),
2359
+ forwarders,
2360
+ },
2361
+ ],
2362
+ };
2363
+ return { content: [{ type: 'text', text: JSON.stringify(manifest, null, 2) }] };
2364
+ });
2365
+ server.tool('welcome_send', 'Send a welcome email with setup instructions to an email account. Includes IMAP/SMTP settings and webmail links.', {
2366
+ to: zod_1.z.string().describe('Recipient email address'),
2367
+ company_name: zod_1.z.string().optional().describe('Company name for branding'),
2368
+ include_password: zod_1.z.string().optional().describe('Password to include in welcome email'),
2369
+ profile: zod_1.z.string().optional().describe('Named profile to use (default: active profile)'),
2370
+ }, async ({ to, company_name, include_password, profile }) => {
2371
+ const smtp = resolveSmtpConfig(profile);
2372
+ const heading = company_name || 'Your Email Account';
2373
+ const domain = to.split('@')[1] || '';
2374
+ const passwordSection = include_password
2375
+ ? `<p><strong>Temporary Password:</strong> <code>${include_password}</code><br><em>Please change this immediately after first login.</em></p>`
2376
+ : '';
2377
+ const html = `<div style="font-family: system-ui, sans-serif; max-width: 560px; margin: 0 auto; padding: 32px;">
2378
+ <h2 style="color: #6C63FF;">${heading}</h2>
2379
+ <p>Your email account <strong>${to}</strong> is ready!</p>
2380
+ ${passwordSection}
2381
+ <h3>Server Settings</h3>
2382
+ <table style="border-collapse: collapse; width: 100%;">
2383
+ <tr><td style="padding: 6px; border: 1px solid #ddd;"><strong>IMAP Server</strong></td><td style="padding: 6px; border: 1px solid #ddd;">${smtp.server}.mxrouting.net</td></tr>
2384
+ <tr><td style="padding: 6px; border: 1px solid #ddd;"><strong>IMAP Port</strong></td><td style="padding: 6px; border: 1px solid #ddd;">993 (SSL/TLS)</td></tr>
2385
+ <tr><td style="padding: 6px; border: 1px solid #ddd;"><strong>SMTP Server</strong></td><td style="padding: 6px; border: 1px solid #ddd;">${smtp.server}.mxrouting.net</td></tr>
2386
+ <tr><td style="padding: 6px; border: 1px solid #ddd;"><strong>SMTP Port</strong></td><td style="padding: 6px; border: 1px solid #ddd;">465 (SSL/TLS)</td></tr>
2387
+ <tr><td style="padding: 6px; border: 1px solid #ddd;"><strong>Username</strong></td><td style="padding: 6px; border: 1px solid #ddd;">${to}</td></tr>
2388
+ </table>
2389
+ <h3>Webmail Access</h3>
2390
+ <p>Access your email in a browser: <a href="https://mail.${domain}">https://mail.${domain}</a></p>
2391
+ <h3>Email Client Setup</h3>
2392
+ <ul>
2393
+ <li><strong>Outlook:</strong> Add Account → IMAP → use settings above</li>
2394
+ <li><strong>Apple Mail:</strong> Preferences → Accounts → Add → Other Mail</li>
2395
+ <li><strong>Thunderbird:</strong> Account Settings → Add Mail Account</li>
2396
+ <li><strong>Mobile:</strong> Settings → Mail → Add Account → Other</li>
2397
+ </ul>
2398
+ </div>`;
2399
+ const { buildMimeMessage } = await Promise.resolve().then(() => __importStar(require('./utils/mime')));
2400
+ const mime = buildMimeMessage({
2401
+ from: smtp.username,
2402
+ to,
2403
+ subject: `Welcome to ${heading} — Your Email Setup Instructions`,
2404
+ textBody: `Your email account ${to} is ready. IMAP: ${smtp.server}.mxrouting.net:993 (SSL). SMTP: ${smtp.server}.mxrouting.net:465 (SSL). Webmail: https://mail.${domain}`,
2405
+ htmlBody: html,
2406
+ });
2407
+ await smtpSend(smtp, to, undefined, undefined, mime);
2408
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, to, message: 'Welcome email sent' }) }] };
2409
+ });
2410
+ server.tool('credentials_export', 'Export account credentials for a domain. Returns structured data with email addresses, server settings, and connection details.', {
2411
+ domain: zod_1.z.string().describe('Domain to export credentials for'),
2412
+ format: zod_1.z.enum(['json', 'csv', '1password']).optional().describe('Export format (default: json)'),
2413
+ }, async ({ domain, format }) => {
2414
+ const creds = getCreds();
2415
+ const config = (0, config_1.getConfig)();
2416
+ const accounts = await (0, directadmin_1.listEmailAccounts)(creds, domain);
2417
+ const server = config.server || '';
2418
+ const data = accounts.map((user) => ({
2419
+ email: `${user}@${domain}`,
2420
+ server: `${server}.mxrouting.net`,
2421
+ imapPort: 993,
2422
+ smtpPort: 465,
2423
+ webmail: `https://mail.${domain}`,
2424
+ }));
2425
+ if (format === 'csv') {
2426
+ const csv = 'email,server,imap_port,smtp_port,webmail\n' +
2427
+ data.map((d) => `${d.email},${d.server},${d.imapPort},${d.smtpPort},${d.webmail}`).join('\n');
2428
+ return { content: [{ type: 'text', text: csv }] };
2429
+ }
2430
+ if (format === '1password') {
2431
+ const csv = 'Title,Website,Username,Password,Notes\n' +
2432
+ data
2433
+ .map((d) => `${d.email},${d.webmail},${d.email},SET_BY_ADMIN,"IMAP: ${d.server}:993 SMTP: ${d.server}:465"`)
2434
+ .join('\n');
2435
+ return { content: [{ type: 'text', text: csv }] };
2436
+ }
2437
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
2438
+ });
2439
+ server.tool('deprovision_account', 'Offboard an employee email account. Can forward their email to another account, or delete the account entirely.', {
2440
+ domain: zod_1.z.string().describe('Domain'),
2441
+ username: zod_1.z.string().describe('Username to offboard (part before @)'),
2442
+ action: zod_1.z.enum(['forward', 'delete']).describe('What to do: forward emails to someone else, or delete account'),
2443
+ forward_to: zod_1.z.string().optional().describe('Email to forward to (required if action is "forward")'),
2444
+ }, async ({ domain, username, action, forward_to }) => {
2445
+ const creds = getCreds();
2446
+ const results = [];
2447
+ if (action === 'forward') {
2448
+ if (!forward_to) {
2449
+ return {
2450
+ content: [
2451
+ { type: 'text', text: JSON.stringify({ error: 'forward_to is required when action is "forward"' }) },
2452
+ ],
2453
+ };
2454
+ }
2455
+ try {
2456
+ await (0, directadmin_1.createForwarder)(creds, domain, username, forward_to);
2457
+ results.push(`Created forwarder: ${username}@${domain} → ${forward_to}`);
2458
+ }
2459
+ catch (err) {
2460
+ results.push(`Failed to create forwarder: ${err.message}`);
2461
+ }
2462
+ try {
2463
+ await (0, directadmin_1.deleteEmailAccount)(creds, domain, username);
2464
+ results.push(`Deleted account: ${username}@${domain}`);
2465
+ }
2466
+ catch (err) {
2467
+ results.push(`Failed to delete account: ${err.message}`);
2468
+ }
2469
+ }
2470
+ else {
2471
+ try {
2472
+ await (0, directadmin_1.deleteEmailAccount)(creds, domain, username);
2473
+ results.push(`Deleted account: ${username}@${domain}`);
2474
+ }
2475
+ catch (err) {
2476
+ results.push(`Failed to delete account: ${err.message}`);
2477
+ }
2478
+ }
2479
+ return { content: [{ type: 'text', text: JSON.stringify({ account: `${username}@${domain}`, action, results }) }] };
2480
+ });
2481
+ server.tool('quota_policy_apply', 'Apply a quota policy to accounts on a domain. Set uniform quota or per-account quotas.', {
2482
+ domain: zod_1.z.string().describe('Domain'),
2483
+ quota_mb: zod_1.z.number().optional().describe('Uniform quota in MB to apply to all accounts'),
2484
+ rules: zod_1.z
2485
+ .array(zod_1.z.object({
2486
+ pattern: zod_1.z.string().describe('Account pattern (* for wildcard, e.g. "admin*" or "*")'),
2487
+ quota: zod_1.z.number().describe('Quota in MB'),
2488
+ }))
2489
+ .optional()
2490
+ .describe('Quota rules applied in order, first match wins'),
2491
+ }, async ({ domain, quota_mb, rules }) => {
2492
+ const creds = getCreds();
2493
+ const accounts = await (0, directadmin_1.listEmailAccounts)(creds, domain);
2494
+ const results = [];
2495
+ const matchPattern = (name, pattern) => {
2496
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
2497
+ return regex.test(name);
2498
+ };
2499
+ for (const user of accounts) {
2500
+ let targetQuota = quota_mb;
2501
+ if (!targetQuota && rules) {
2502
+ for (const rule of rules) {
2503
+ if (matchPattern(user, rule.pattern)) {
2504
+ targetQuota = rule.quota;
2505
+ break;
2506
+ }
2507
+ }
2508
+ }
2509
+ if (targetQuota === undefined)
2510
+ continue;
2511
+ try {
2512
+ await (0, directadmin_1.changeEmailQuota)(creds, domain, user, targetQuota);
2513
+ results.push({ account: `${user}@${domain}`, quota: targetQuota, status: 'applied' });
2514
+ }
2515
+ catch (err) {
2516
+ results.push({ account: `${user}@${domain}`, quota: targetQuota, status: `failed: ${err.message}` });
2517
+ }
2518
+ }
2519
+ return { content: [{ type: 'text', text: JSON.stringify({ domain, results, total: results.length }, null, 2) }] };
2520
+ });
591
2521
  // ─── Start server ────────────────────────────────────────
592
2522
  async function main() {
593
2523
  const transport = new stdio_js_1.StdioServerTransport();