@push.rocks/smartmta 5.1.3 → 5.2.1

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 (98) hide show
  1. package/changelog.md +15 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/index.d.ts +3 -0
  5. package/dist_ts/index.js +4 -0
  6. package/dist_ts/logger.d.ts +17 -0
  7. package/dist_ts/logger.js +76 -0
  8. package/dist_ts/mail/core/classes.bouncemanager.d.ts +185 -0
  9. package/dist_ts/mail/core/classes.bouncemanager.js +569 -0
  10. package/dist_ts/mail/core/classes.email.d.ts +291 -0
  11. package/dist_ts/mail/core/classes.email.js +802 -0
  12. package/dist_ts/mail/core/classes.emailvalidator.d.ts +61 -0
  13. package/dist_ts/mail/core/classes.emailvalidator.js +184 -0
  14. package/dist_ts/mail/core/classes.templatemanager.d.ts +95 -0
  15. package/dist_ts/mail/core/classes.templatemanager.js +240 -0
  16. package/dist_ts/mail/core/index.d.ts +4 -0
  17. package/dist_ts/mail/core/index.js +6 -0
  18. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +163 -0
  19. package/dist_ts/mail/delivery/classes.delivery.queue.js +488 -0
  20. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +160 -0
  21. package/dist_ts/mail/delivery/classes.delivery.system.js +630 -0
  22. package/dist_ts/mail/delivery/classes.unified.rate.limiter.d.ts +200 -0
  23. package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +820 -0
  24. package/dist_ts/mail/delivery/index.d.ts +4 -0
  25. package/dist_ts/mail/delivery/index.js +6 -0
  26. package/dist_ts/mail/delivery/interfaces.d.ts +140 -0
  27. package/dist_ts/mail/delivery/interfaces.js +17 -0
  28. package/dist_ts/mail/index.d.ts +7 -0
  29. package/dist_ts/mail/index.js +12 -0
  30. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +25 -0
  31. package/dist_ts/mail/routing/classes.dkim.manager.js +127 -0
  32. package/dist_ts/mail/routing/classes.dns.manager.d.ts +79 -0
  33. package/dist_ts/mail/routing/classes.dns.manager.js +415 -0
  34. package/dist_ts/mail/routing/classes.domain.registry.d.ts +54 -0
  35. package/dist_ts/mail/routing/classes.domain.registry.js +119 -0
  36. package/dist_ts/mail/routing/classes.email.action.executor.d.ts +33 -0
  37. package/dist_ts/mail/routing/classes.email.action.executor.js +137 -0
  38. package/dist_ts/mail/routing/classes.email.router.d.ts +171 -0
  39. package/dist_ts/mail/routing/classes.email.router.js +494 -0
  40. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +241 -0
  41. package/dist_ts/mail/routing/classes.unified.email.server.js +935 -0
  42. package/dist_ts/mail/routing/index.d.ts +7 -0
  43. package/dist_ts/mail/routing/index.js +9 -0
  44. package/dist_ts/mail/routing/interfaces.d.ts +187 -0
  45. package/dist_ts/mail/routing/interfaces.js +2 -0
  46. package/dist_ts/mail/security/classes.dkimcreator.d.ts +72 -0
  47. package/dist_ts/mail/security/classes.dkimcreator.js +360 -0
  48. package/dist_ts/mail/security/classes.spfverifier.d.ts +62 -0
  49. package/dist_ts/mail/security/classes.spfverifier.js +87 -0
  50. package/dist_ts/mail/security/index.d.ts +2 -0
  51. package/dist_ts/mail/security/index.js +4 -0
  52. package/dist_ts/paths.d.ts +14 -0
  53. package/dist_ts/paths.js +39 -0
  54. package/dist_ts/plugins.d.ts +24 -0
  55. package/dist_ts/plugins.js +28 -0
  56. package/dist_ts/security/classes.contentscanner.d.ts +130 -0
  57. package/dist_ts/security/classes.contentscanner.js +338 -0
  58. package/dist_ts/security/classes.ipreputationchecker.d.ts +73 -0
  59. package/dist_ts/security/classes.ipreputationchecker.js +263 -0
  60. package/dist_ts/security/classes.rustsecuritybridge.d.ts +403 -0
  61. package/dist_ts/security/classes.rustsecuritybridge.js +502 -0
  62. package/dist_ts/security/classes.securitylogger.d.ts +140 -0
  63. package/dist_ts/security/classes.securitylogger.js +235 -0
  64. package/dist_ts/security/index.d.ts +4 -0
  65. package/dist_ts/security/index.js +5 -0
  66. package/package.json +6 -1
  67. package/ts/00_commitinfo_data.ts +8 -0
  68. package/ts/index.ts +3 -0
  69. package/ts/logger.ts +91 -0
  70. package/ts/mail/core/classes.bouncemanager.ts +731 -0
  71. package/ts/mail/core/classes.email.ts +942 -0
  72. package/ts/mail/core/classes.emailvalidator.ts +239 -0
  73. package/ts/mail/core/classes.templatemanager.ts +320 -0
  74. package/ts/mail/core/index.ts +5 -0
  75. package/ts/mail/delivery/classes.delivery.queue.ts +645 -0
  76. package/ts/mail/delivery/classes.delivery.system.ts +816 -0
  77. package/ts/mail/delivery/classes.unified.rate.limiter.ts +1053 -0
  78. package/ts/mail/delivery/index.ts +5 -0
  79. package/ts/mail/delivery/interfaces.ts +167 -0
  80. package/ts/mail/index.ts +17 -0
  81. package/ts/mail/routing/classes.dkim.manager.ts +157 -0
  82. package/ts/mail/routing/classes.dns.manager.ts +573 -0
  83. package/ts/mail/routing/classes.domain.registry.ts +139 -0
  84. package/ts/mail/routing/classes.email.action.executor.ts +175 -0
  85. package/ts/mail/routing/classes.email.router.ts +575 -0
  86. package/ts/mail/routing/classes.unified.email.server.ts +1207 -0
  87. package/ts/mail/routing/index.ts +9 -0
  88. package/ts/mail/routing/interfaces.ts +202 -0
  89. package/ts/mail/security/classes.dkimcreator.ts +447 -0
  90. package/ts/mail/security/classes.spfverifier.ts +126 -0
  91. package/ts/mail/security/index.ts +3 -0
  92. package/ts/paths.ts +48 -0
  93. package/ts/plugins.ts +53 -0
  94. package/ts/security/classes.contentscanner.ts +400 -0
  95. package/ts/security/classes.ipreputationchecker.ts +315 -0
  96. package/ts/security/classes.rustsecuritybridge.ts +964 -0
  97. package/ts/security/classes.securitylogger.ts +299 -0
  98. package/ts/security/index.ts +40 -0
@@ -0,0 +1,9 @@
1
+ // Email routing components
2
+ export * from './classes.email.router.js';
3
+ export * from './classes.unified.email.server.js';
4
+ export * from './classes.dns.manager.js';
5
+ export * from './interfaces.js';
6
+ export * from './classes.domain.registry.js';
7
+ export * from './classes.email.action.executor.js';
8
+ export * from './classes.dkim.manager.js';
9
+
@@ -0,0 +1,202 @@
1
+ import type { Email } from '../core/classes.email.js';
2
+ import type { IExtendedSmtpSession } from './classes.unified.email.server.js';
3
+
4
+ /**
5
+ * Route configuration for email routing
6
+ */
7
+ export interface IEmailRoute {
8
+ /** Route identifier */
9
+ name: string;
10
+ /** Order of evaluation (higher priority evaluated first, default: 0) */
11
+ priority?: number;
12
+ /** Conditions to match */
13
+ match: IEmailMatch;
14
+ /** Action to take when matched */
15
+ action: IEmailAction;
16
+ }
17
+
18
+ /**
19
+ * Match criteria for email routing
20
+ */
21
+ export interface IEmailMatch {
22
+ /** Email patterns to match recipients: "*@example.com", "admin@*" */
23
+ recipients?: string | string[];
24
+ /** Email patterns to match senders */
25
+ senders?: string | string[];
26
+ /** IP addresses or CIDR ranges to match */
27
+ clientIp?: string | string[];
28
+ /** Require authentication status */
29
+ authenticated?: boolean;
30
+
31
+ // Optional advanced matching
32
+ /** Headers to match */
33
+ headers?: Record<string, string | RegExp>;
34
+ /** Message size range */
35
+ sizeRange?: { min?: number; max?: number };
36
+ /** Subject line patterns */
37
+ subject?: string | RegExp;
38
+ /** Has attachments */
39
+ hasAttachments?: boolean;
40
+ }
41
+
42
+ /**
43
+ * Action to take when route matches
44
+ */
45
+ export interface IEmailAction {
46
+ /** Type of action to perform */
47
+ type: 'forward' | 'deliver' | 'reject' | 'process';
48
+
49
+ /** Forward action configuration */
50
+ forward?: {
51
+ /** Target host to forward to */
52
+ host: string;
53
+ /** Target port (default: 25) */
54
+ port?: number;
55
+ /** Authentication credentials */
56
+ auth?: {
57
+ user: string;
58
+ pass: string;
59
+ };
60
+ /** Preserve original headers */
61
+ preserveHeaders?: boolean;
62
+ /** Additional headers to add */
63
+ addHeaders?: Record<string, string>;
64
+ };
65
+
66
+ /** Reject action configuration */
67
+ reject?: {
68
+ /** SMTP response code */
69
+ code: number;
70
+ /** SMTP response message */
71
+ message: string;
72
+ };
73
+
74
+ /** Process action configuration */
75
+ process?: {
76
+ /** Enable content scanning */
77
+ scan?: boolean;
78
+ /** Enable DKIM signing */
79
+ dkim?: boolean;
80
+ /** Delivery queue priority */
81
+ queue?: 'normal' | 'priority' | 'bulk';
82
+ };
83
+
84
+ /** Options for various action types */
85
+ options?: {
86
+ /** MTA specific options */
87
+ mtaOptions?: {
88
+ domain?: string;
89
+ allowLocalDelivery?: boolean;
90
+ localDeliveryPath?: string;
91
+ dkimSign?: boolean;
92
+ dkimOptions?: {
93
+ domainName: string;
94
+ keySelector: string;
95
+ privateKey?: string;
96
+ };
97
+ smtpBanner?: string;
98
+ maxConnections?: number;
99
+ connTimeout?: number;
100
+ spoolDir?: string;
101
+ };
102
+ /** Content scanning configuration */
103
+ contentScanning?: boolean;
104
+ scanners?: Array<{
105
+ type: 'spam' | 'virus' | 'attachment';
106
+ threshold?: number;
107
+ action: 'tag' | 'reject';
108
+ blockedExtensions?: string[];
109
+ }>;
110
+ /** Email transformations */
111
+ transformations?: Array<{
112
+ type: string;
113
+ header?: string;
114
+ value?: string;
115
+ domains?: string[];
116
+ append?: boolean;
117
+ [key: string]: any;
118
+ }>;
119
+ };
120
+
121
+ /** Delivery options (applies to forward/process/deliver) */
122
+ delivery?: {
123
+ /** Rate limit (messages per minute) */
124
+ rateLimit?: number;
125
+ /** Number of retry attempts */
126
+ retries?: number;
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Context for route evaluation
132
+ */
133
+ export interface IEmailContext {
134
+ /** The email being routed */
135
+ email: Email;
136
+ /** The SMTP session */
137
+ session: IExtendedSmtpSession;
138
+ }
139
+
140
+ /**
141
+ * Email domain configuration
142
+ */
143
+ export interface IEmailDomainConfig {
144
+ /** Domain name */
145
+ domain: string;
146
+
147
+ /** DNS handling mode */
148
+ dnsMode: 'forward' | 'internal-dns' | 'external-dns';
149
+
150
+ /** DNS configuration based on mode */
151
+ dns?: {
152
+ /** For 'forward' mode */
153
+ forward?: {
154
+ /** Skip DNS validation (default: false) */
155
+ skipDnsValidation?: boolean;
156
+ /** Target server's expected domain */
157
+ targetDomain?: string;
158
+ };
159
+
160
+ /** For 'internal-dns' mode */
161
+ internal?: {
162
+ /** TTL for DNS records in seconds (default: 3600) */
163
+ ttl?: number;
164
+ /** MX record priority (default: 10) */
165
+ mxPriority?: number;
166
+ };
167
+
168
+ /** For 'external-dns' mode */
169
+ external?: {
170
+ /** Custom DNS servers (default: system DNS) */
171
+ servers?: string[];
172
+ /** Which records to validate (default: ['MX', 'SPF', 'DKIM', 'DMARC']) */
173
+ requiredRecords?: ('MX' | 'SPF' | 'DKIM' | 'DMARC')[];
174
+ };
175
+ };
176
+
177
+ /** Per-domain DKIM settings (DKIM always enabled) */
178
+ dkim?: {
179
+ /** DKIM selector (default: 'default') */
180
+ selector?: string;
181
+ /** Key size in bits (default: 2048) */
182
+ keySize?: number;
183
+ /** Automatically rotate keys (default: false) */
184
+ rotateKeys?: boolean;
185
+ /** Days between key rotations (default: 90) */
186
+ rotationInterval?: number;
187
+ };
188
+
189
+ /** Per-domain rate limits */
190
+ rateLimits?: {
191
+ outbound?: {
192
+ messagesPerMinute?: number;
193
+ messagesPerHour?: number;
194
+ messagesPerDay?: number;
195
+ };
196
+ inbound?: {
197
+ messagesPerMinute?: number;
198
+ connectionsPerIp?: number;
199
+ recipientsPerMessage?: number;
200
+ };
201
+ };
202
+ }
@@ -0,0 +1,447 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import * as paths from '../../paths.js';
3
+
4
+ import { Email } from '../core/classes.email.js';
5
+ // MtaService reference removed
6
+
7
+ const readFile = plugins.util.promisify(plugins.fs.readFile);
8
+ const writeFile = plugins.util.promisify(plugins.fs.writeFile);
9
+ const generateKeyPair = plugins.util.promisify(plugins.crypto.generateKeyPair);
10
+
11
+ export interface IKeyPaths {
12
+ privateKeyPath: string;
13
+ publicKeyPath: string;
14
+ }
15
+
16
+ export interface IDkimKeyMetadata {
17
+ domain: string;
18
+ selector: string;
19
+ createdAt: number;
20
+ rotatedAt?: number;
21
+ previousSelector?: string;
22
+ keySize: number;
23
+ }
24
+
25
+ export class DKIMCreator {
26
+ private keysDir: string;
27
+ private storageManager?: any; // StorageManager instance
28
+
29
+ constructor(keysDir = paths.keysDir, storageManager?: any) {
30
+ this.keysDir = keysDir;
31
+ this.storageManager = storageManager;
32
+ }
33
+
34
+ public async getKeyPathsForDomain(domainArg: string): Promise<IKeyPaths> {
35
+ return {
36
+ privateKeyPath: plugins.path.join(this.keysDir, `${domainArg}-private.pem`),
37
+ publicKeyPath: plugins.path.join(this.keysDir, `${domainArg}-public.pem`),
38
+ };
39
+ }
40
+
41
+ // Check if a DKIM key is present and creates one and stores it to disk otherwise
42
+ public async handleDKIMKeysForDomain(domainArg: string): Promise<void> {
43
+ try {
44
+ await this.readDKIMKeys(domainArg);
45
+ } catch (error) {
46
+ console.log(`No DKIM keys found for ${domainArg}. Generating...`);
47
+ await this.createAndStoreDKIMKeys(domainArg);
48
+ const dnsValue = await this.getDNSRecordForDomain(domainArg);
49
+ await plugins.smartfs.directory(paths.dnsRecordsDir).recursive().create();
50
+ await plugins.smartfs.file(plugins.path.join(paths.dnsRecordsDir, `${domainArg}.dkimrecord.json`)).write(JSON.stringify(dnsValue, null, 2));
51
+ }
52
+ }
53
+
54
+ public async handleDKIMKeysForEmail(email: Email): Promise<void> {
55
+ const domain = email.from.split('@')[1];
56
+ await this.handleDKIMKeysForDomain(domain);
57
+ }
58
+
59
+ // Read DKIM keys - always use storage manager, migrate from filesystem if needed
60
+ public async readDKIMKeys(domainArg: string): Promise<{ privateKey: string; publicKey: string }> {
61
+ // Try to read from storage manager first
62
+ if (this.storageManager) {
63
+ try {
64
+ const [privateKey, publicKey] = await Promise.all([
65
+ this.storageManager.get(`/email/dkim/${domainArg}/private.key`),
66
+ this.storageManager.get(`/email/dkim/${domainArg}/public.key`)
67
+ ]);
68
+
69
+ if (privateKey && publicKey) {
70
+ return { privateKey, publicKey };
71
+ }
72
+ } catch (error) {
73
+ // Fall through to migration check
74
+ }
75
+
76
+ // Check if keys exist in filesystem and migrate them to storage manager
77
+ const keyPaths = await this.getKeyPathsForDomain(domainArg);
78
+ try {
79
+ const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
80
+ readFile(keyPaths.privateKeyPath),
81
+ readFile(keyPaths.publicKeyPath),
82
+ ]);
83
+
84
+ // Convert the buffers to strings
85
+ const privateKey = privateKeyBuffer.toString();
86
+ const publicKey = publicKeyBuffer.toString();
87
+
88
+ // Migrate to storage manager
89
+ console.log(`Migrating DKIM keys for ${domainArg} from filesystem to StorageManager`);
90
+ await Promise.all([
91
+ this.storageManager.set(`/email/dkim/${domainArg}/private.key`, privateKey),
92
+ this.storageManager.set(`/email/dkim/${domainArg}/public.key`, publicKey)
93
+ ]);
94
+
95
+ return { privateKey, publicKey };
96
+ } catch (error) {
97
+ if (error.code === 'ENOENT') {
98
+ // Keys don't exist anywhere
99
+ throw new Error(`DKIM keys not found for domain ${domainArg}`);
100
+ }
101
+ throw error;
102
+ }
103
+ } else {
104
+ // No storage manager, use filesystem directly
105
+ const keyPaths = await this.getKeyPathsForDomain(domainArg);
106
+ const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
107
+ readFile(keyPaths.privateKeyPath),
108
+ readFile(keyPaths.publicKeyPath),
109
+ ]);
110
+
111
+ const privateKey = privateKeyBuffer.toString();
112
+ const publicKey = publicKeyBuffer.toString();
113
+
114
+ return { privateKey, publicKey };
115
+ }
116
+ }
117
+
118
+ // Create an RSA DKIM key pair - changed to public for API access
119
+ public async createDKIMKeys(): Promise<{ privateKey: string; publicKey: string }> {
120
+ const { privateKey, publicKey } = await generateKeyPair('rsa', {
121
+ modulusLength: 2048,
122
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
123
+ privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
124
+ });
125
+
126
+ return { privateKey, publicKey };
127
+ }
128
+
129
+ // Create an Ed25519 DKIM key pair (RFC 8463)
130
+ public async createEd25519Keys(): Promise<{ privateKey: string; publicKey: string }> {
131
+ const { privateKey, publicKey } = await generateKeyPair('ed25519', {
132
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
133
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
134
+ });
135
+
136
+ return { privateKey, publicKey };
137
+ }
138
+
139
+ // Store a DKIM key pair - uses storage manager if available, else disk
140
+ public async storeDKIMKeys(
141
+ privateKey: string,
142
+ publicKey: string,
143
+ privateKeyPath: string,
144
+ publicKeyPath: string
145
+ ): Promise<void> {
146
+ // Store in storage manager if available
147
+ if (this.storageManager) {
148
+ // Extract domain from path (e.g., /path/to/keys/example.com-private.pem -> example.com)
149
+ const match = privateKeyPath.match(/\/([^\/]+)-private\.pem$/);
150
+ if (match) {
151
+ const domain = match[1];
152
+ await Promise.all([
153
+ this.storageManager.set(`/email/dkim/${domain}/private.key`, privateKey),
154
+ this.storageManager.set(`/email/dkim/${domain}/public.key`, publicKey)
155
+ ]);
156
+ }
157
+ }
158
+
159
+ // Also store to filesystem for backward compatibility
160
+ await Promise.all([writeFile(privateKeyPath, privateKey), writeFile(publicKeyPath, publicKey)]);
161
+ }
162
+
163
+ // Create a DKIM key pair and store it to disk - changed to public for API access
164
+ public async createAndStoreDKIMKeys(domain: string): Promise<void> {
165
+ const { privateKey, publicKey } = await this.createDKIMKeys();
166
+ const keyPaths = await this.getKeyPathsForDomain(domain);
167
+ await this.storeDKIMKeys(
168
+ privateKey,
169
+ publicKey,
170
+ keyPaths.privateKeyPath,
171
+ keyPaths.publicKeyPath
172
+ );
173
+ console.log(`DKIM keys for ${domain} created and stored.`);
174
+ }
175
+
176
+ // Changed to public for API access
177
+ public async getDNSRecordForDomain(domainArg: string): Promise<plugins.tsclass.network.IDnsRecord> {
178
+ await this.handleDKIMKeysForDomain(domainArg);
179
+ const keys = await this.readDKIMKeys(domainArg);
180
+
181
+ // Remove the PEM header and footer and newlines
182
+ const pemHeader = '-----BEGIN PUBLIC KEY-----';
183
+ const pemFooter = '-----END PUBLIC KEY-----';
184
+ const keyContents = keys.publicKey
185
+ .replace(pemHeader, '')
186
+ .replace(pemFooter, '')
187
+ .replace(/\n/g, '');
188
+
189
+ // Detect key type from PEM header
190
+ const keyAlgo = keys.privateKey.includes('ED25519') || keys.publicKey.length < 200 ? 'ed25519' : 'rsa';
191
+
192
+ // Now generate the DKIM DNS TXT record
193
+ const dnsRecordValue = `v=DKIM1; h=sha256; k=${keyAlgo}; p=${keyContents}`;
194
+
195
+ return {
196
+ name: `mta._domainkey.${domainArg}`,
197
+ type: 'TXT',
198
+ dnsSecEnabled: null,
199
+ value: dnsRecordValue,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Get DKIM key metadata for a domain
205
+ */
206
+ private async getKeyMetadata(domain: string, selector: string = 'default'): Promise<IDkimKeyMetadata | null> {
207
+ if (!this.storageManager) {
208
+ return null;
209
+ }
210
+
211
+ const metadataKey = `/email/dkim/${domain}/${selector}/metadata`;
212
+ const metadataStr = await this.storageManager.get(metadataKey);
213
+
214
+ if (!metadataStr) {
215
+ return null;
216
+ }
217
+
218
+ return JSON.parse(metadataStr) as IDkimKeyMetadata;
219
+ }
220
+
221
+ /**
222
+ * Save DKIM key metadata
223
+ */
224
+ private async saveKeyMetadata(metadata: IDkimKeyMetadata): Promise<void> {
225
+ if (!this.storageManager) {
226
+ return;
227
+ }
228
+
229
+ const metadataKey = `/email/dkim/${metadata.domain}/${metadata.selector}/metadata`;
230
+ await this.storageManager.set(metadataKey, JSON.stringify(metadata));
231
+ }
232
+
233
+ /**
234
+ * Check if DKIM keys need rotation
235
+ */
236
+ public async needsRotation(domain: string, selector: string = 'default', rotationIntervalDays: number = 90): Promise<boolean> {
237
+ const metadata = await this.getKeyMetadata(domain, selector);
238
+
239
+ if (!metadata) {
240
+ // No metadata means old keys, should rotate
241
+ return true;
242
+ }
243
+
244
+ const now = Date.now();
245
+ const keyAgeMs = now - metadata.createdAt;
246
+ const keyAgeDays = keyAgeMs / (1000 * 60 * 60 * 24);
247
+
248
+ return keyAgeDays >= rotationIntervalDays;
249
+ }
250
+
251
+ /**
252
+ * Rotate DKIM keys for a domain
253
+ */
254
+ public async rotateDkimKeys(domain: string, currentSelector: string = 'default', keySize: number = 2048): Promise<string> {
255
+ console.log(`Rotating DKIM keys for ${domain}...`);
256
+
257
+ // Generate new selector based on date
258
+ const now = new Date();
259
+ const newSelector = `key${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}`;
260
+
261
+ // Create new keys with custom key size
262
+ const { privateKey, publicKey } = await generateKeyPair('rsa', {
263
+ modulusLength: keySize,
264
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
265
+ privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
266
+ });
267
+
268
+ // Store new keys with new selector
269
+ const newKeyPaths = await this.getKeyPathsForSelector(domain, newSelector);
270
+
271
+ // Store in storage manager if available
272
+ if (this.storageManager) {
273
+ await Promise.all([
274
+ this.storageManager.set(`/email/dkim/${domain}/${newSelector}/private.key`, privateKey),
275
+ this.storageManager.set(`/email/dkim/${domain}/${newSelector}/public.key`, publicKey)
276
+ ]);
277
+ }
278
+
279
+ // Also store to filesystem
280
+ await this.storeDKIMKeys(
281
+ privateKey,
282
+ publicKey,
283
+ newKeyPaths.privateKeyPath,
284
+ newKeyPaths.publicKeyPath
285
+ );
286
+
287
+ // Save metadata for new keys
288
+ const metadata: IDkimKeyMetadata = {
289
+ domain,
290
+ selector: newSelector,
291
+ createdAt: Date.now(),
292
+ previousSelector: currentSelector,
293
+ keySize
294
+ };
295
+ await this.saveKeyMetadata(metadata);
296
+
297
+ // Update metadata for old keys
298
+ const oldMetadata = await this.getKeyMetadata(domain, currentSelector);
299
+ if (oldMetadata) {
300
+ oldMetadata.rotatedAt = Date.now();
301
+ await this.saveKeyMetadata(oldMetadata);
302
+ }
303
+
304
+ console.log(`DKIM keys rotated for ${domain}. New selector: ${newSelector}`);
305
+ return newSelector;
306
+ }
307
+
308
+ /**
309
+ * Get key paths for a specific selector
310
+ */
311
+ public async getKeyPathsForSelector(domain: string, selector: string): Promise<IKeyPaths> {
312
+ return {
313
+ privateKeyPath: plugins.path.join(this.keysDir, `${domain}-${selector}-private.pem`),
314
+ publicKeyPath: plugins.path.join(this.keysDir, `${domain}-${selector}-public.pem`),
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Read DKIM keys for a specific selector
320
+ */
321
+ public async readDKIMKeysForSelector(domain: string, selector: string): Promise<{ privateKey: string; publicKey: string }> {
322
+ // Try to read from storage manager first
323
+ if (this.storageManager) {
324
+ try {
325
+ const [privateKey, publicKey] = await Promise.all([
326
+ this.storageManager.get(`/email/dkim/${domain}/${selector}/private.key`),
327
+ this.storageManager.get(`/email/dkim/${domain}/${selector}/public.key`)
328
+ ]);
329
+
330
+ if (privateKey && publicKey) {
331
+ return { privateKey, publicKey };
332
+ }
333
+ } catch (error) {
334
+ // Fall through to migration check
335
+ }
336
+
337
+ // Check if keys exist in filesystem and migrate them to storage manager
338
+ const keyPaths = await this.getKeyPathsForSelector(domain, selector);
339
+ try {
340
+ const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
341
+ readFile(keyPaths.privateKeyPath),
342
+ readFile(keyPaths.publicKeyPath),
343
+ ]);
344
+
345
+ const privateKey = privateKeyBuffer.toString();
346
+ const publicKey = publicKeyBuffer.toString();
347
+
348
+ // Migrate to storage manager
349
+ console.log(`Migrating DKIM keys for ${domain}/${selector} from filesystem to StorageManager`);
350
+ await Promise.all([
351
+ this.storageManager.set(`/email/dkim/${domain}/${selector}/private.key`, privateKey),
352
+ this.storageManager.set(`/email/dkim/${domain}/${selector}/public.key`, publicKey)
353
+ ]);
354
+
355
+ return { privateKey, publicKey };
356
+ } catch (error) {
357
+ if (error.code === 'ENOENT') {
358
+ throw new Error(`DKIM keys not found for domain ${domain} with selector ${selector}`);
359
+ }
360
+ throw error;
361
+ }
362
+ } else {
363
+ // No storage manager, use filesystem directly
364
+ const keyPaths = await this.getKeyPathsForSelector(domain, selector);
365
+ const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
366
+ readFile(keyPaths.privateKeyPath),
367
+ readFile(keyPaths.publicKeyPath),
368
+ ]);
369
+
370
+ const privateKey = privateKeyBuffer.toString();
371
+ const publicKey = publicKeyBuffer.toString();
372
+
373
+ return { privateKey, publicKey };
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Get DNS record for a specific selector
379
+ */
380
+ public async getDNSRecordForSelector(domain: string, selector: string): Promise<plugins.tsclass.network.IDnsRecord> {
381
+ const keys = await this.readDKIMKeysForSelector(domain, selector);
382
+
383
+ // Remove the PEM header and footer and newlines
384
+ const pemHeader = '-----BEGIN PUBLIC KEY-----';
385
+ const pemFooter = '-----END PUBLIC KEY-----';
386
+ const keyContents = keys.publicKey
387
+ .replace(pemHeader, '')
388
+ .replace(pemFooter, '')
389
+ .replace(/\n/g, '');
390
+
391
+ // Detect key type from PEM header
392
+ const keyAlgo = keys.privateKey.includes('ED25519') || keys.publicKey.length < 200 ? 'ed25519' : 'rsa';
393
+
394
+ // Generate the DKIM DNS TXT record
395
+ const dnsRecordValue = `v=DKIM1; h=sha256; k=${keyAlgo}; p=${keyContents}`;
396
+
397
+ return {
398
+ name: `${selector}._domainkey.${domain}`,
399
+ type: 'TXT',
400
+ dnsSecEnabled: null,
401
+ value: dnsRecordValue,
402
+ };
403
+ }
404
+
405
+ /**
406
+ * Clean up old DKIM keys after grace period
407
+ */
408
+ public async cleanupOldKeys(domain: string, gracePeriodDays: number = 30): Promise<void> {
409
+ if (!this.storageManager) {
410
+ return;
411
+ }
412
+
413
+ // List all selectors for the domain
414
+ const metadataKeys = await this.storageManager.list(`/email/dkim/${domain}/`);
415
+
416
+ for (const key of metadataKeys) {
417
+ if (key.endsWith('/metadata')) {
418
+ const metadataStr = await this.storageManager.get(key);
419
+ if (metadataStr) {
420
+ const metadata = JSON.parse(metadataStr) as IDkimKeyMetadata;
421
+
422
+ // Check if key is rotated and past grace period
423
+ if (metadata.rotatedAt) {
424
+ const gracePeriodMs = gracePeriodDays * 24 * 60 * 60 * 1000;
425
+ const now = Date.now();
426
+
427
+ if (now - metadata.rotatedAt > gracePeriodMs) {
428
+ console.log(`Cleaning up old DKIM keys for ${domain} selector ${metadata.selector}`);
429
+
430
+ // Delete key files
431
+ const keyPaths = await this.getKeyPathsForSelector(domain, metadata.selector);
432
+ try {
433
+ await plugins.fs.promises.unlink(keyPaths.privateKeyPath);
434
+ await plugins.fs.promises.unlink(keyPaths.publicKeyPath);
435
+ } catch (error) {
436
+ console.warn(`Failed to delete old key files: ${error.message}`);
437
+ }
438
+
439
+ // Delete metadata
440
+ await this.storageManager.delete(key);
441
+ }
442
+ }
443
+ }
444
+ }
445
+ }
446
+ }
447
+ }