@push.rocks/smartmta 6.5.2 → 8.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 (84) hide show
  1. package/changelog.md +40 -0
  2. package/dist_rust/mailer-bin_linux_amd64 +0 -0
  3. package/dist_rust/mailer-bin_linux_arm64 +0 -0
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/functions.errors.d.ts +3 -0
  6. package/dist_ts/functions.errors.js +8 -0
  7. package/dist_ts/index.d.ts +3 -0
  8. package/dist_ts/index.js +4 -1
  9. package/dist_ts/mail/core/classes.bouncemanager.d.ts +16 -12
  10. package/dist_ts/mail/core/classes.bouncemanager.js +146 -129
  11. package/dist_ts/mail/core/classes.email.js +15 -13
  12. package/dist_ts/mail/core/classes.emailvalidator.d.ts +3 -3
  13. package/dist_ts/mail/core/classes.emailvalidator.js +7 -5
  14. package/dist_ts/mail/core/classes.templatemanager.d.ts +10 -6
  15. package/dist_ts/mail/core/classes.templatemanager.js +35 -51
  16. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +92 -22
  17. package/dist_ts/mail/delivery/classes.delivery.queue.js +738 -151
  18. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +13 -7
  19. package/dist_ts/mail/delivery/classes.delivery.system.js +458 -145
  20. package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +9 -8
  21. package/dist_ts/mail/delivery/functions.safe-observers.d.ts +10 -0
  22. package/dist_ts/mail/delivery/functions.safe-observers.js +37 -0
  23. package/dist_ts/mail/delivery/interfaces.d.ts +21 -0
  24. package/dist_ts/mail/delivery/interfaces.js +1 -1
  25. package/dist_ts/mail/interfaces.storage.d.ts +37 -6
  26. package/dist_ts/mail/interfaces.storage.js +33 -3
  27. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +10 -6
  28. package/dist_ts/mail/routing/classes.dkim.manager.js +47 -31
  29. package/dist_ts/mail/routing/classes.dns.manager.d.ts +7 -5
  30. package/dist_ts/mail/routing/classes.dns.manager.js +22 -11
  31. package/dist_ts/mail/routing/classes.email.action.executor.d.ts +2 -1
  32. package/dist_ts/mail/routing/classes.email.action.executor.js +45 -16
  33. package/dist_ts/mail/routing/classes.email.router.d.ts +5 -2
  34. package/dist_ts/mail/routing/classes.email.router.js +17 -12
  35. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +12 -6
  36. package/dist_ts/mail/routing/classes.unified.email.server.js +69 -78
  37. package/dist_ts/mail/routing/interfaces.d.ts +0 -2
  38. package/dist_ts/mail/security/classes.dkimcreator.d.ts +22 -45
  39. package/dist_ts/mail/security/classes.dkimcreator.js +93 -296
  40. package/dist_ts/mail/security/classes.spfverifier.js +5 -3
  41. package/dist_ts/paths.d.ts +0 -12
  42. package/dist_ts/paths.js +3 -36
  43. package/dist_ts/plugins.d.ts +2 -5
  44. package/dist_ts/plugins.js +3 -6
  45. package/dist_ts/security/classes.contentscanner.js +14 -12
  46. package/dist_ts/security/classes.ipreputationchecker.d.ts +9 -6
  47. package/dist_ts/security/classes.ipreputationchecker.js +42 -93
  48. package/dist_ts/security/classes.rustsecuritybridge.d.ts +52 -4
  49. package/dist_ts/security/classes.rustsecuritybridge.js +201 -4
  50. package/dist_ts/security/classes.securitylogger.js +7 -5
  51. package/dist_ts/security/index.d.ts +1 -1
  52. package/dist_ts/security/index.js +2 -2
  53. package/package.json +8 -9
  54. package/readme.hints.md +4 -3
  55. package/readme.md +50 -18
  56. package/readme.plan.md +6 -0
  57. package/ts/00_commitinfo_data.ts +1 -1
  58. package/ts/functions.errors.ts +8 -0
  59. package/ts/index.ts +3 -0
  60. package/ts/mail/core/classes.bouncemanager.ts +188 -155
  61. package/ts/mail/core/classes.email.ts +20 -14
  62. package/ts/mail/core/classes.emailvalidator.ts +9 -7
  63. package/ts/mail/core/classes.templatemanager.ts +42 -57
  64. package/ts/mail/delivery/classes.delivery.queue.ts +996 -185
  65. package/ts/mail/delivery/classes.delivery.system.ts +583 -170
  66. package/ts/mail/delivery/classes.unified.rate.limiter.ts +9 -8
  67. package/ts/mail/delivery/functions.safe-observers.ts +45 -0
  68. package/ts/mail/delivery/interfaces.ts +27 -1
  69. package/ts/mail/interfaces.storage.ts +64 -10
  70. package/ts/mail/routing/classes.dkim.manager.ts +65 -40
  71. package/ts/mail/routing/classes.dns.manager.ts +39 -16
  72. package/ts/mail/routing/classes.email.action.executor.ts +64 -17
  73. package/ts/mail/routing/classes.email.router.ts +20 -13
  74. package/ts/mail/routing/classes.unified.email.server.ts +107 -86
  75. package/ts/mail/routing/interfaces.ts +0 -2
  76. package/ts/mail/security/classes.dkimcreator.ts +150 -355
  77. package/ts/mail/security/classes.spfverifier.ts +4 -2
  78. package/ts/paths.ts +2 -41
  79. package/ts/plugins.ts +1 -6
  80. package/ts/security/classes.contentscanner.ts +14 -12
  81. package/ts/security/classes.ipreputationchecker.ts +46 -99
  82. package/ts/security/classes.rustsecuritybridge.ts +272 -6
  83. package/ts/security/classes.securitylogger.ts +6 -4
  84. package/ts/security/index.ts +5 -1
@@ -1,19 +1,10 @@
1
1
  import * as plugins from '../../plugins.js';
2
- import * as paths from '../../paths.js';
3
- import { type IStorageManagerLike, hasStorageManagerMethods } from '../interfaces.storage.js';
2
+ import type { IStorageManager } from '../interfaces.storage.js';
4
3
 
5
4
  import { Email } from '../core/classes.email.js';
6
- // MtaService reference removed
7
5
 
8
- const readFile = plugins.util.promisify(plugins.fs.readFile);
9
- const writeFile = plugins.util.promisify(plugins.fs.writeFile);
10
6
  const generateKeyPair = plugins.util.promisify(plugins.crypto.generateKeyPair);
11
7
 
12
- export interface IKeyPaths {
13
- privateKeyPath: string;
14
- publicKeyPath: string;
15
- }
16
-
17
8
  export interface IDkimKeyMetadata {
18
9
  domain: string;
19
10
  selector: string;
@@ -23,80 +14,60 @@ export interface IDkimKeyMetadata {
23
14
  keySize: number;
24
15
  }
25
16
 
17
+ export type TDkimKeyType = 'rsa' | 'ed25519';
18
+
19
+ export interface IDkimSigningMaterial {
20
+ privateKey: string;
21
+ publicKey: string;
22
+ keyType: TDkimKeyType;
23
+ }
24
+
26
25
  export class DKIMCreator {
27
- private keysDir: string;
28
- private storageManager?: IStorageManagerLike;
26
+ constructor(private readonly storageManager: IStorageManager) {}
29
27
 
30
- constructor(keysDir = paths.keysDir, storageManager?: IStorageManagerLike) {
31
- this.keysDir = keysDir;
32
- this.storageManager = storageManager;
28
+ private legacyKey(domain: string, filename: 'private.key' | 'public.key'): string {
29
+ return `/email/dkim/${domain}/${filename}`;
33
30
  }
34
31
 
35
- private async writeKeyPairToFilesystem(
36
- privateKeyPath: string,
37
- publicKeyPath: string,
38
- privateKey: string,
39
- publicKey: string,
40
- ): Promise<void> {
41
- await Promise.all([writeFile(privateKeyPath, privateKey), writeFile(publicKeyPath, publicKey)]);
32
+ private selectorKey(
33
+ domain: string,
34
+ selector: string,
35
+ filename: 'private.key' | 'public.key',
36
+ ): string {
37
+ return selector === 'default'
38
+ ? this.legacyKey(domain, filename)
39
+ : `/email/dkim/${domain}/${selector}/${filename}`;
42
40
  }
43
41
 
44
- private async storeLegacyKeysToStorage(domain: string, privateKey: string, publicKey: string): Promise<void> {
45
- if (!hasStorageManagerMethods(this.storageManager, ['set'])) {
46
- return;
47
- }
48
- await Promise.all([
49
- this.storageManager.set(`/email/dkim/${domain}/private.key`, privateKey),
50
- this.storageManager.set(`/email/dkim/${domain}/public.key`, publicKey),
51
- ]);
42
+ private metadataKey(domain: string, selector: string): string {
43
+ return `/email/dkim/${domain}/${selector}/metadata`;
52
44
  }
53
45
 
54
- private async storeSelectorKeysToStorage(
46
+ private async storeKeyPair(
55
47
  domain: string,
56
48
  selector: string,
57
49
  privateKey: string,
58
50
  publicKey: string,
59
51
  ): Promise<void> {
60
- if (!hasStorageManagerMethods(this.storageManager, ['set'])) {
61
- return;
62
- }
63
52
  await Promise.all([
64
- this.storageManager.set(`/email/dkim/${domain}/${selector}/private.key`, privateKey),
65
- this.storageManager.set(`/email/dkim/${domain}/${selector}/public.key`, publicKey),
53
+ this.storageManager.set(this.selectorKey(domain, selector, 'private.key'), privateKey),
54
+ this.storageManager.set(this.selectorKey(domain, selector, 'public.key'), publicKey),
66
55
  ]);
67
56
  }
68
57
 
69
- public async getKeyPathsForDomain(domainArg: string): Promise<IKeyPaths> {
70
- return {
71
- privateKeyPath: plugins.path.join(this.keysDir, `${domainArg}-private.pem`),
72
- publicKeyPath: plugins.path.join(this.keysDir, `${domainArg}-public.pem`),
73
- };
58
+ public async handleDKIMKeysForDomain(domain: string): Promise<void> {
59
+ await this.handleDKIMKeysForSelector(domain, 'default');
74
60
  }
75
61
 
76
- // Check if a DKIM key is present and creates one and stores it to disk otherwise
77
- public async handleDKIMKeysForDomain(domainArg: string): Promise<void> {
78
- try {
79
- await this.readDKIMKeys(domainArg);
80
- } catch (error) {
81
- console.log(`No DKIM keys found for ${domainArg}. Generating...`);
82
- await this.createAndStoreDKIMKeys(domainArg);
83
- const dnsValue = await this.getDNSRecordForDomain(domainArg);
84
- await plugins.smartfs.directory(paths.dnsRecordsDir).recursive().create();
85
- await plugins.smartfs.file(plugins.path.join(paths.dnsRecordsDir, `${domainArg}.dkimrecord.json`)).write(JSON.stringify(dnsValue, null, 2));
86
- }
87
- }
88
-
89
- public async handleDKIMKeysForSelector(domainArg: string, selector: string = 'default', keySize: number = 2048): Promise<void> {
90
- if (selector === 'default') {
91
- await this.handleDKIMKeysForDomain(domainArg);
92
- return;
93
- }
94
-
62
+ public async handleDKIMKeysForSelector(
63
+ domain: string,
64
+ selector = 'default',
65
+ keySize = 2048,
66
+ ): Promise<void> {
95
67
  try {
96
- await this.readDKIMKeysForSelector(domainArg, selector);
68
+ await this.readDKIMKeysForSelector(domain, selector);
97
69
  } catch {
98
- console.log(`No DKIM keys found for ${domainArg}/${selector}. Generating...`);
99
- await this.createAndStoreDKIMKeysForSelector(domainArg, selector, keySize);
70
+ await this.createAndStoreDKIMKeysForSelector(domain, selector, keySize);
100
71
  }
101
72
  }
102
73
 
@@ -105,367 +76,191 @@ export class DKIMCreator {
105
76
  await this.handleDKIMKeysForDomain(domain);
106
77
  }
107
78
 
108
- // Read DKIM keys - always use storage manager, migrate from filesystem if needed
109
- public async readDKIMKeys(domainArg: string): Promise<{ privateKey: string; publicKey: string }> {
110
- // Try to read from storage manager first
111
- if (hasStorageManagerMethods(this.storageManager, ['get', 'set'])) {
112
- try {
113
- const [privateKey, publicKey] = await Promise.all([
114
- this.storageManager.get(`/email/dkim/${domainArg}/private.key`),
115
- this.storageManager.get(`/email/dkim/${domainArg}/public.key`)
116
- ]);
117
-
118
- if (privateKey && publicKey) {
119
- return { privateKey, publicKey };
120
- }
121
- } catch (error) {
122
- // Fall through to migration check
123
- }
124
-
125
- // Check if keys exist in filesystem and migrate them to storage manager
126
- const keyPaths = await this.getKeyPathsForDomain(domainArg);
127
- try {
128
- const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
129
- readFile(keyPaths.privateKeyPath),
130
- readFile(keyPaths.publicKeyPath),
131
- ]);
132
-
133
- // Convert the buffers to strings
134
- const privateKey = privateKeyBuffer.toString();
135
- const publicKey = publicKeyBuffer.toString();
136
-
137
- // Migrate to storage manager
138
- console.log(`Migrating DKIM keys for ${domainArg} from filesystem to StorageManager`);
139
- await this.storeLegacyKeysToStorage(domainArg, privateKey, publicKey);
140
-
141
- return { privateKey, publicKey };
142
- } catch (error) {
143
- if (error.code === 'ENOENT') {
144
- // Keys don't exist anywhere
145
- throw new Error(`DKIM keys not found for domain ${domainArg}`);
146
- }
147
- throw error;
148
- }
149
- } else {
150
- // No storage manager, use filesystem directly
151
- const keyPaths = await this.getKeyPathsForDomain(domainArg);
152
- const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
153
- readFile(keyPaths.privateKeyPath),
154
- readFile(keyPaths.publicKeyPath),
155
- ]);
79
+ public async readDKIMKeys(domain: string): Promise<{ privateKey: string; publicKey: string }> {
80
+ return this.readDKIMKeysForSelector(domain, 'default');
81
+ }
156
82
 
157
- const privateKey = privateKeyBuffer.toString();
158
- const publicKey = publicKeyBuffer.toString();
159
-
160
- return { privateKey, publicKey };
83
+ public async readDKIMKeysForSelector(
84
+ domain: string,
85
+ selector: string,
86
+ ): Promise<{ privateKey: string; publicKey: string }> {
87
+ const [privateKey, publicKey] = await Promise.all([
88
+ this.storageManager.get(this.selectorKey(domain, selector, 'private.key')),
89
+ this.storageManager.get(this.selectorKey(domain, selector, 'public.key')),
90
+ ]);
91
+ if (!privateKey || !publicKey) {
92
+ throw new Error(`DKIM keys not found for domain ${domain} with selector ${selector}`);
161
93
  }
94
+ return { privateKey, publicKey };
162
95
  }
163
96
 
164
- // Create an RSA DKIM key pair - changed to public for API access
165
- public async createDKIMKeys(keySize: number = 2048): Promise<{ privateKey: string; publicKey: string }> {
97
+ public async readValidatedDKIMKeysForSelector(
98
+ domain: string,
99
+ selector: string,
100
+ ): Promise<IDkimSigningMaterial> {
101
+ const keys = await this.readDKIMKeysForSelector(domain, selector);
102
+ let privateKeyObject: plugins.crypto.KeyObject;
103
+ let publicKeyObject: plugins.crypto.KeyObject;
104
+ try {
105
+ privateKeyObject = plugins.crypto.createPrivateKey(keys.privateKey);
106
+ publicKeyObject = plugins.crypto.createPublicKey(keys.publicKey);
107
+ } catch (error) {
108
+ throw new Error(
109
+ `Invalid DKIM key material for domain ${domain} with selector ${selector}: ${(error as Error).message}`,
110
+ );
111
+ }
112
+
113
+ const keyType = privateKeyObject.asymmetricKeyType;
114
+ if (keyType !== 'rsa' && keyType !== 'ed25519') {
115
+ throw new Error(
116
+ `Unsupported DKIM key type for domain ${domain} with selector ${selector}: ${keyType || 'unknown'}`,
117
+ );
118
+ }
119
+ if (publicKeyObject.asymmetricKeyType !== keyType) {
120
+ throw new Error(`DKIM public/private key type mismatch for domain ${domain} with selector ${selector}`);
121
+ }
122
+
123
+ const derivedPublicKey = plugins.crypto.createPublicKey(keys.privateKey).export({
124
+ type: 'spki',
125
+ format: 'der',
126
+ }) as Buffer;
127
+ const suppliedPublicKey = publicKeyObject.export({ type: 'spki', format: 'der' }) as Buffer;
128
+ if (!derivedPublicKey.equals(suppliedPublicKey)) {
129
+ throw new Error(`DKIM public/private key mismatch for domain ${domain} with selector ${selector}`);
130
+ }
131
+
132
+ return { ...keys, keyType };
133
+ }
134
+
135
+ public async createDKIMKeys(
136
+ keySize = 2048,
137
+ ): Promise<{ privateKey: string; publicKey: string }> {
166
138
  const { privateKey, publicKey } = await generateKeyPair('rsa', {
167
139
  modulusLength: keySize,
168
140
  publicKeyEncoding: { type: 'spki', format: 'pem' },
169
141
  privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
170
142
  });
171
-
172
143
  return { privateKey, publicKey };
173
144
  }
174
145
 
175
- // Create an Ed25519 DKIM key pair (RFC 8463)
176
146
  public async createEd25519Keys(): Promise<{ privateKey: string; publicKey: string }> {
177
147
  const { privateKey, publicKey } = await generateKeyPair('ed25519', {
178
148
  publicKeyEncoding: { type: 'spki', format: 'pem' },
179
149
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
180
150
  });
181
-
182
151
  return { privateKey, publicKey };
183
152
  }
184
153
 
185
- // Create a DKIM key pair and store it to disk - changed to public for API access
186
- public async createAndStoreDKIMKeys(domain: string, keySize: number = 2048): Promise<void> {
187
- const { privateKey, publicKey } = await this.createDKIMKeys(keySize);
188
- const keyPaths = await this.getKeyPathsForDomain(domain);
189
- await this.storeLegacyKeysToStorage(domain, privateKey, publicKey);
190
- await this.writeKeyPairToFilesystem(keyPaths.privateKeyPath, keyPaths.publicKeyPath, privateKey, publicKey);
191
- await this.saveKeyMetadata({
192
- domain,
193
- selector: 'default',
194
- createdAt: Date.now(),
195
- keySize,
196
- });
197
- console.log(`DKIM keys for ${domain} created and stored.`);
154
+ public async createAndStoreDKIMKeys(domain: string, keySize = 2048): Promise<void> {
155
+ await this.createAndStoreDKIMKeysForSelector(domain, 'default', keySize);
198
156
  }
199
157
 
200
158
  public async createAndStoreDKIMKeysForSelector(
201
159
  domain: string,
202
160
  selector: string,
203
- keySize: number = 2048,
161
+ keySize = 2048,
204
162
  ): Promise<void> {
205
- if (selector === 'default') {
206
- await this.createAndStoreDKIMKeys(domain, keySize);
207
- return;
208
- }
209
-
210
163
  const { privateKey, publicKey } = await this.createDKIMKeys(keySize);
211
- const keyPaths = await this.getKeyPathsForSelector(domain, selector);
212
- await this.storeSelectorKeysToStorage(domain, selector, privateKey, publicKey);
213
- await this.writeKeyPairToFilesystem(keyPaths.privateKeyPath, keyPaths.publicKeyPath, privateKey, publicKey);
164
+ await this.storeKeyPair(domain, selector, privateKey, publicKey);
214
165
  await this.saveKeyMetadata({
215
166
  domain,
216
167
  selector,
217
168
  createdAt: Date.now(),
218
169
  keySize,
219
170
  });
220
- console.log(`DKIM keys for ${domain}/${selector} created and stored.`);
221
171
  }
222
172
 
223
- // Changed to public for API access
224
173
  public async getDNSRecordForDomain(
225
- domainArg: string,
226
- selector: string = 'default',
174
+ domain: string,
175
+ selector = 'default',
227
176
  ): Promise<plugins.tsclass.network.IDnsRecord> {
228
- await this.handleDKIMKeysForSelector(domainArg, selector);
229
- return this.getDNSRecordForSelector(domainArg, selector);
177
+ await this.handleDKIMKeysForSelector(domain, selector);
178
+ return this.getDNSRecordForSelector(domain, selector);
230
179
  }
231
180
 
232
- /**
233
- * Get DKIM key metadata for a domain
234
- */
235
- private async getKeyMetadata(domain: string, selector: string = 'default'): Promise<IDkimKeyMetadata | null> {
236
- if (!hasStorageManagerMethods(this.storageManager, ['get'])) {
237
- return null;
238
- }
239
-
240
- const metadataKey = `/email/dkim/${domain}/${selector}/metadata`;
241
- const metadataStr = await this.storageManager.get(metadataKey);
242
-
243
- if (!metadataStr) {
244
- return null;
245
- }
246
-
247
- return JSON.parse(metadataStr) as IDkimKeyMetadata;
181
+ private async getKeyMetadata(
182
+ domain: string,
183
+ selector = 'default',
184
+ ): Promise<IDkimKeyMetadata | null> {
185
+ const metadata = await this.storageManager.get(this.metadataKey(domain, selector));
186
+ return metadata ? JSON.parse(metadata) as IDkimKeyMetadata : null;
248
187
  }
249
188
 
250
- /**
251
- * Save DKIM key metadata
252
- */
253
189
  private async saveKeyMetadata(metadata: IDkimKeyMetadata): Promise<void> {
254
- if (!hasStorageManagerMethods(this.storageManager, ['set'])) {
255
- return;
256
- }
257
-
258
- const metadataKey = `/email/dkim/${metadata.domain}/${metadata.selector}/metadata`;
259
- await this.storageManager.set(metadataKey, JSON.stringify(metadata));
190
+ await this.storageManager.set(
191
+ this.metadataKey(metadata.domain, metadata.selector),
192
+ JSON.stringify(metadata),
193
+ );
260
194
  }
261
195
 
262
- /**
263
- * Check if DKIM keys need rotation
264
- */
265
- public async needsRotation(domain: string, selector: string = 'default', rotationIntervalDays: number = 90): Promise<boolean> {
196
+ public async needsRotation(
197
+ domain: string,
198
+ selector = 'default',
199
+ rotationIntervalDays = 90,
200
+ ): Promise<boolean> {
266
201
  const metadata = await this.getKeyMetadata(domain, selector);
267
-
268
- if (!metadata) {
269
- // No metadata means old keys, should rotate
270
- return true;
271
- }
272
-
273
- const now = Date.now();
274
- const keyAgeMs = now - metadata.createdAt;
275
- const keyAgeDays = keyAgeMs / (1000 * 60 * 60 * 24);
276
-
277
- return keyAgeDays >= rotationIntervalDays;
202
+ if (!metadata) return true;
203
+ return (Date.now() - metadata.createdAt) / (1000 * 60 * 60 * 24) >= rotationIntervalDays;
278
204
  }
279
205
 
280
- /**
281
- * Rotate DKIM keys for a domain
282
- */
283
- public async rotateDkimKeys(domain: string, currentSelector: string = 'default', keySize: number = 2048): Promise<string> {
284
- console.log(`Rotating DKIM keys for ${domain}...`);
285
-
286
- // Generate new selector based on date
206
+ public async rotateDkimKeys(
207
+ domain: string,
208
+ currentSelector = 'default',
209
+ keySize = 2048,
210
+ ): Promise<string> {
287
211
  const now = new Date();
288
212
  const newSelector = `key${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}`;
289
-
290
- // Create new keys with custom key size
291
213
  const { privateKey, publicKey } = await this.createDKIMKeys(keySize);
292
-
293
- // Store new keys with new selector
294
- const newKeyPaths = await this.getKeyPathsForSelector(domain, newSelector);
295
-
296
- // Store in storage manager if available
297
- await this.storeSelectorKeysToStorage(domain, newSelector, privateKey, publicKey);
298
-
299
- // Also store to filesystem
300
- await this.writeKeyPairToFilesystem(newKeyPaths.privateKeyPath, newKeyPaths.publicKeyPath, privateKey, publicKey);
301
-
302
- // Save metadata for new keys
303
- const metadata: IDkimKeyMetadata = {
214
+ await this.storeKeyPair(domain, newSelector, privateKey, publicKey);
215
+ await this.saveKeyMetadata({
304
216
  domain,
305
217
  selector: newSelector,
306
218
  createdAt: Date.now(),
307
219
  previousSelector: currentSelector,
308
- keySize
309
- };
310
- await this.saveKeyMetadata(metadata);
311
-
312
- // Update metadata for old keys
220
+ keySize,
221
+ });
222
+
313
223
  const oldMetadata = await this.getKeyMetadata(domain, currentSelector);
314
224
  if (oldMetadata) {
315
225
  oldMetadata.rotatedAt = Date.now();
316
226
  await this.saveKeyMetadata(oldMetadata);
317
227
  }
318
-
319
- console.log(`DKIM keys rotated for ${domain}. New selector: ${newSelector}`);
320
228
  return newSelector;
321
229
  }
322
230
 
323
- /**
324
- * Get key paths for a specific selector
325
- */
326
- public async getKeyPathsForSelector(domain: string, selector: string): Promise<IKeyPaths> {
327
- return {
328
- privateKeyPath: plugins.path.join(this.keysDir, `${domain}-${selector}-private.pem`),
329
- publicKeyPath: plugins.path.join(this.keysDir, `${domain}-${selector}-public.pem`),
330
- };
331
- }
332
-
333
- /**
334
- * Read DKIM keys for a specific selector
335
- */
336
- public async readDKIMKeysForSelector(domain: string, selector: string): Promise<{ privateKey: string; publicKey: string }> {
337
- // Try to read from storage manager first
338
- if (hasStorageManagerMethods(this.storageManager, ['get', 'set'])) {
339
- try {
340
- const [privateKey, publicKey] = await Promise.all([
341
- this.storageManager.get(`/email/dkim/${domain}/${selector}/private.key`),
342
- this.storageManager.get(`/email/dkim/${domain}/${selector}/public.key`)
343
- ]);
344
-
345
- if (privateKey && publicKey) {
346
- return { privateKey, publicKey };
347
- }
348
-
349
- if (selector === 'default') {
350
- return await this.readDKIMKeys(domain);
351
- }
352
- } catch (error) {
353
- // Fall through to migration check
354
- }
355
-
356
- // Check if keys exist in filesystem and migrate them to storage manager
357
- const keyPaths = await this.getKeyPathsForSelector(domain, selector);
358
- try {
359
- const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
360
- readFile(keyPaths.privateKeyPath),
361
- readFile(keyPaths.publicKeyPath),
362
- ]);
363
-
364
- const privateKey = privateKeyBuffer.toString();
365
- const publicKey = publicKeyBuffer.toString();
366
-
367
- // Migrate to storage manager
368
- console.log(`Migrating DKIM keys for ${domain}/${selector} from filesystem to StorageManager`);
369
- await this.storeSelectorKeysToStorage(domain, selector, privateKey, publicKey);
370
-
371
- return { privateKey, publicKey };
372
- } catch (error) {
373
- if (error.code === 'ENOENT') {
374
- throw new Error(`DKIM keys not found for domain ${domain} with selector ${selector}`);
375
- }
376
- throw error;
377
- }
378
- } else {
379
- // No storage manager, use filesystem directly
380
- if (selector === 'default') {
381
- return this.readDKIMKeys(domain);
382
- }
383
- const keyPaths = await this.getKeyPathsForSelector(domain, selector);
384
- const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([
385
- readFile(keyPaths.privateKeyPath),
386
- readFile(keyPaths.publicKeyPath),
387
- ]);
388
-
389
- const privateKey = privateKeyBuffer.toString();
390
- const publicKey = publicKeyBuffer.toString();
391
-
392
- return { privateKey, publicKey };
393
- }
394
- }
395
-
396
- /**
397
- * Get DNS record for a specific selector
398
- */
399
- public async getDNSRecordForSelector(domain: string, selector: string): Promise<plugins.tsclass.network.IDnsRecord> {
400
- const keys = await this.readDKIMKeysForSelector(domain, selector);
401
-
402
- // Remove the PEM header and footer and newlines
403
- const pemHeader = '-----BEGIN PUBLIC KEY-----';
404
- const pemFooter = '-----END PUBLIC KEY-----';
405
- const keyContents = keys.publicKey
406
- .replace(pemHeader, '')
407
- .replace(pemFooter, '')
408
- .replace(/\n/g, '');
409
-
410
- // Detect key type from PEM header
411
- const keyAlgo = keys.privateKey.includes('ED25519') || keys.publicKey.length < 200 ? 'ed25519' : 'rsa';
412
-
413
- // Generate the DKIM DNS TXT record
414
- const dnsRecordValue = `v=DKIM1; h=sha256; k=${keyAlgo}; p=${keyContents}`;
231
+ public async getDNSRecordForSelector(
232
+ domain: string,
233
+ selector: string,
234
+ ): Promise<plugins.tsclass.network.IDnsRecord> {
235
+ const material = await this.readValidatedDKIMKeysForSelector(domain, selector);
236
+ const keyContents = material.publicKey
237
+ .replace('-----BEGIN PUBLIC KEY-----', '')
238
+ .replace('-----END PUBLIC KEY-----', '')
239
+ .replace(/\s/g, '');
415
240
 
416
241
  return {
417
242
  name: `${selector}._domainkey.${domain}`,
418
243
  type: 'TXT',
419
- dnsSecEnabled: null,
420
- value: dnsRecordValue,
244
+ dnsSecEnabled: false,
245
+ value: `v=DKIM1; h=sha256; k=${material.keyType}; p=${keyContents}`,
421
246
  };
422
247
  }
423
248
 
424
- /**
425
- * Clean up old DKIM keys after grace period
426
- */
427
- public async cleanupOldKeys(domain: string, gracePeriodDays: number = 30): Promise<void> {
428
- if (!hasStorageManagerMethods(this.storageManager, ['get', 'list', 'delete'])) {
429
- console.log(`StorageManager for ${domain} does not support list/delete. Skipping DKIM cleanup.`);
430
- return;
431
- }
432
-
433
- // List all selectors for the domain
434
- const metadataKeys = await this.storageManager.list(`/email/dkim/${domain}/`);
435
-
436
- for (const key of metadataKeys) {
437
- if (key.endsWith('/metadata')) {
438
- const metadataStr = await this.storageManager.get(key);
439
- if (metadataStr) {
440
- const metadata = JSON.parse(metadataStr) as IDkimKeyMetadata;
441
-
442
- // Check if key is rotated and past grace period
443
- if (metadata.rotatedAt) {
444
- const gracePeriodMs = gracePeriodDays * 24 * 60 * 60 * 1000;
445
- const now = Date.now();
446
-
447
- if (now - metadata.rotatedAt > gracePeriodMs) {
448
- console.log(`Cleaning up old DKIM keys for ${domain} selector ${metadata.selector}`);
449
-
450
- // Delete key files
451
- const keyPaths = await this.getKeyPathsForSelector(domain, metadata.selector);
452
- try {
453
- await plugins.fs.promises.unlink(keyPaths.privateKeyPath);
454
- await plugins.fs.promises.unlink(keyPaths.publicKeyPath);
455
- } catch (error) {
456
- console.warn(`Failed to delete old key files: ${error.message}`);
457
- }
458
-
459
- // Delete selector-specific storage keys and metadata
460
- await Promise.all([
461
- this.storageManager.delete(`/email/dkim/${domain}/${metadata.selector}/private.key`),
462
- this.storageManager.delete(`/email/dkim/${domain}/${metadata.selector}/public.key`),
463
- ]);
464
- await this.storageManager.delete(key);
465
- }
466
- }
467
- }
468
- }
249
+ public async cleanupOldKeys(domain: string, gracePeriodDays = 30): Promise<void> {
250
+ const cutoff = Date.now() - gracePeriodDays * 24 * 60 * 60 * 1000;
251
+ const keys = await this.storageManager.list(`/email/dkim/${domain}/`);
252
+ const metadataKeys = keys.filter((key) => key.endsWith('/metadata'));
253
+
254
+ for (const metadataKey of metadataKeys) {
255
+ const serialized = await this.storageManager.get(metadataKey);
256
+ if (!serialized) continue;
257
+ const metadata = JSON.parse(serialized) as IDkimKeyMetadata;
258
+ if (!metadata.rotatedAt || metadata.rotatedAt >= cutoff) continue;
259
+ await Promise.all([
260
+ this.storageManager.delete(this.selectorKey(domain, metadata.selector, 'private.key')),
261
+ this.storageManager.delete(this.selectorKey(domain, metadata.selector, 'public.key')),
262
+ this.storageManager.delete(metadataKey),
263
+ ]);
469
264
  }
470
265
  }
471
266
  }
@@ -1,4 +1,5 @@
1
1
  import { logger } from '../../logger.js';
2
+ import { getErrorMessage } from '../../functions.errors.js';
2
3
 
3
4
  /**
4
5
  * SPF result qualifiers
@@ -116,9 +117,10 @@ export class SpfVerifier {
116
117
 
117
118
  return spfRecord;
118
119
  } catch (error) {
119
- logger.log('error', `Error parsing SPF record: ${error.message}`, {
120
+ const errorMessage = getErrorMessage(error);
121
+ logger.log('error', `Error parsing SPF record: ${errorMessage}`, {
120
122
  record,
121
- error: error.message
123
+ error: errorMessage
122
124
  });
123
125
  return null;
124
126
  }