@push.rocks/smartmta 6.5.1 → 7.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.
- package/changelog.md +20 -0
- package/dist_rust/mailer-bin_linux_amd64 +0 -0
- package/dist_rust/mailer-bin_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -1
- package/dist_ts/mail/core/classes.bouncemanager.d.ts +5 -12
- package/dist_ts/mail/core/classes.bouncemanager.js +27 -92
- package/dist_ts/mail/core/classes.email.js +2 -2
- package/dist_ts/mail/core/classes.templatemanager.d.ts +10 -6
- package/dist_ts/mail/core/classes.templatemanager.js +35 -51
- package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +15 -20
- package/dist_ts/mail/delivery/classes.delivery.queue.js +196 -114
- package/dist_ts/mail/delivery/classes.delivery.system.js +5 -5
- package/dist_ts/mail/interfaces.storage.d.ts +37 -6
- package/dist_ts/mail/interfaces.storage.js +33 -3
- package/dist_ts/mail/routing/classes.dkim.manager.d.ts +2 -2
- package/dist_ts/mail/routing/classes.dkim.manager.js +2 -3
- package/dist_ts/mail/routing/classes.dns.manager.d.ts +2 -2
- package/dist_ts/mail/routing/classes.dns.manager.js +1 -1
- package/dist_ts/mail/routing/classes.email.router.d.ts +2 -2
- package/dist_ts/mail/routing/classes.email.router.js +4 -5
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +8 -6
- package/dist_ts/mail/routing/classes.unified.email.server.js +12 -41
- package/dist_ts/mail/routing/interfaces.d.ts +0 -2
- package/dist_ts/mail/security/classes.dkimcreator.d.ts +15 -45
- package/dist_ts/mail/security/classes.dkimcreator.js +66 -294
- package/dist_ts/paths.d.ts +0 -12
- package/dist_ts/paths.js +3 -36
- package/dist_ts/plugins.d.ts +2 -5
- package/dist_ts/plugins.js +3 -6
- package/dist_ts/security/classes.contentscanner.js +1 -2
- package/dist_ts/security/classes.ipreputationchecker.d.ts +6 -6
- package/dist_ts/security/classes.ipreputationchecker.js +24 -84
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +3 -3
- package/package.json +2 -3
- package/readme.md +12 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +4 -0
- package/ts/mail/core/classes.bouncemanager.ts +31 -110
- package/ts/mail/core/classes.email.ts +1 -1
- package/ts/mail/core/classes.templatemanager.ts +42 -57
- package/ts/mail/delivery/classes.delivery.queue.ts +264 -135
- package/ts/mail/delivery/classes.delivery.system.ts +7 -5
- package/ts/mail/interfaces.storage.ts +64 -10
- package/ts/mail/routing/classes.dkim.manager.ts +3 -3
- package/ts/mail/routing/classes.dns.manager.ts +3 -3
- package/ts/mail/routing/classes.email.router.ts +6 -6
- package/ts/mail/routing/classes.unified.email.server.ts +23 -44
- package/ts/mail/routing/interfaces.ts +0 -2
- package/ts/mail/security/classes.dkimcreator.ts +104 -352
- package/ts/paths.ts +2 -41
- package/ts/plugins.ts +1 -6
- package/ts/security/classes.contentscanner.ts +0 -1
- package/ts/security/classes.ipreputationchecker.ts +26 -90
- package/ts/security/classes.rustsecuritybridge.ts +3 -3
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
|
-
import
|
|
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;
|
|
@@ -24,79 +15,51 @@ export interface IDkimKeyMetadata {
|
|
|
24
15
|
}
|
|
25
16
|
|
|
26
17
|
export class DKIMCreator {
|
|
27
|
-
private
|
|
28
|
-
private storageManager?: IStorageManagerLike;
|
|
18
|
+
constructor(private readonly storageManager: IStorageManager) {}
|
|
29
19
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.storageManager = storageManager;
|
|
20
|
+
private legacyKey(domain: string, filename: 'private.key' | 'public.key'): string {
|
|
21
|
+
return `/email/dkim/${domain}/${filename}`;
|
|
33
22
|
}
|
|
34
23
|
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
private selectorKey(
|
|
25
|
+
domain: string,
|
|
26
|
+
selector: string,
|
|
27
|
+
filename: 'private.key' | 'public.key',
|
|
28
|
+
): string {
|
|
29
|
+
return selector === 'default'
|
|
30
|
+
? this.legacyKey(domain, filename)
|
|
31
|
+
: `/email/dkim/${domain}/${selector}/${filename}`;
|
|
42
32
|
}
|
|
43
33
|
|
|
44
|
-
private
|
|
45
|
-
|
|
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
|
-
]);
|
|
34
|
+
private metadataKey(domain: string, selector: string): string {
|
|
35
|
+
return `/email/dkim/${domain}/${selector}/metadata`;
|
|
52
36
|
}
|
|
53
37
|
|
|
54
|
-
private async
|
|
38
|
+
private async storeKeyPair(
|
|
55
39
|
domain: string,
|
|
56
40
|
selector: string,
|
|
57
41
|
privateKey: string,
|
|
58
42
|
publicKey: string,
|
|
59
43
|
): Promise<void> {
|
|
60
|
-
if (!hasStorageManagerMethods(this.storageManager, ['set'])) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
44
|
await Promise.all([
|
|
64
|
-
this.storageManager.set(
|
|
65
|
-
this.storageManager.set(
|
|
45
|
+
this.storageManager.set(this.selectorKey(domain, selector, 'private.key'), privateKey),
|
|
46
|
+
this.storageManager.set(this.selectorKey(domain, selector, 'public.key'), publicKey),
|
|
66
47
|
]);
|
|
67
48
|
}
|
|
68
49
|
|
|
69
|
-
public async
|
|
70
|
-
|
|
71
|
-
privateKeyPath: plugins.path.join(this.keysDir, `${domainArg}-private.pem`),
|
|
72
|
-
publicKeyPath: plugins.path.join(this.keysDir, `${domainArg}-public.pem`),
|
|
73
|
-
};
|
|
50
|
+
public async handleDKIMKeysForDomain(domain: string): Promise<void> {
|
|
51
|
+
await this.handleDKIMKeysForSelector(domain, 'default');
|
|
74
52
|
}
|
|
75
53
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
54
|
+
public async handleDKIMKeysForSelector(
|
|
55
|
+
domain: string,
|
|
56
|
+
selector = 'default',
|
|
57
|
+
keySize = 2048,
|
|
58
|
+
): Promise<void> {
|
|
95
59
|
try {
|
|
96
|
-
await this.readDKIMKeysForSelector(
|
|
60
|
+
await this.readDKIMKeysForSelector(domain, selector);
|
|
97
61
|
} catch {
|
|
98
|
-
|
|
99
|
-
await this.createAndStoreDKIMKeysForSelector(domainArg, selector, keySize);
|
|
62
|
+
await this.createAndStoreDKIMKeysForSelector(domain, selector, keySize);
|
|
100
63
|
}
|
|
101
64
|
}
|
|
102
65
|
|
|
@@ -105,367 +68,156 @@ export class DKIMCreator {
|
|
|
105
68
|
await this.handleDKIMKeysForDomain(domain);
|
|
106
69
|
}
|
|
107
70
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
]);
|
|
71
|
+
public async readDKIMKeys(domain: string): Promise<{ privateKey: string; publicKey: string }> {
|
|
72
|
+
return this.readDKIMKeysForSelector(domain, 'default');
|
|
73
|
+
}
|
|
156
74
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
75
|
+
public async readDKIMKeysForSelector(
|
|
76
|
+
domain: string,
|
|
77
|
+
selector: string,
|
|
78
|
+
): Promise<{ privateKey: string; publicKey: string }> {
|
|
79
|
+
const [privateKey, publicKey] = await Promise.all([
|
|
80
|
+
this.storageManager.get(this.selectorKey(domain, selector, 'private.key')),
|
|
81
|
+
this.storageManager.get(this.selectorKey(domain, selector, 'public.key')),
|
|
82
|
+
]);
|
|
83
|
+
if (!privateKey || !publicKey) {
|
|
84
|
+
throw new Error(`DKIM keys not found for domain ${domain} with selector ${selector}`);
|
|
161
85
|
}
|
|
86
|
+
return { privateKey, publicKey };
|
|
162
87
|
}
|
|
163
88
|
|
|
164
|
-
|
|
165
|
-
|
|
89
|
+
public async createDKIMKeys(
|
|
90
|
+
keySize = 2048,
|
|
91
|
+
): Promise<{ privateKey: string; publicKey: string }> {
|
|
166
92
|
const { privateKey, publicKey } = await generateKeyPair('rsa', {
|
|
167
93
|
modulusLength: keySize,
|
|
168
94
|
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
169
95
|
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
|
|
170
96
|
});
|
|
171
|
-
|
|
172
97
|
return { privateKey, publicKey };
|
|
173
98
|
}
|
|
174
99
|
|
|
175
|
-
// Create an Ed25519 DKIM key pair (RFC 8463)
|
|
176
100
|
public async createEd25519Keys(): Promise<{ privateKey: string; publicKey: string }> {
|
|
177
101
|
const { privateKey, publicKey } = await generateKeyPair('ed25519', {
|
|
178
102
|
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
179
103
|
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
180
104
|
});
|
|
181
|
-
|
|
182
105
|
return { privateKey, publicKey };
|
|
183
106
|
}
|
|
184
107
|
|
|
185
|
-
|
|
186
|
-
|
|
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.`);
|
|
108
|
+
public async createAndStoreDKIMKeys(domain: string, keySize = 2048): Promise<void> {
|
|
109
|
+
await this.createAndStoreDKIMKeysForSelector(domain, 'default', keySize);
|
|
198
110
|
}
|
|
199
111
|
|
|
200
112
|
public async createAndStoreDKIMKeysForSelector(
|
|
201
113
|
domain: string,
|
|
202
114
|
selector: string,
|
|
203
|
-
keySize
|
|
115
|
+
keySize = 2048,
|
|
204
116
|
): Promise<void> {
|
|
205
|
-
if (selector === 'default') {
|
|
206
|
-
await this.createAndStoreDKIMKeys(domain, keySize);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
117
|
const { privateKey, publicKey } = await this.createDKIMKeys(keySize);
|
|
211
|
-
|
|
212
|
-
await this.storeSelectorKeysToStorage(domain, selector, privateKey, publicKey);
|
|
213
|
-
await this.writeKeyPairToFilesystem(keyPaths.privateKeyPath, keyPaths.publicKeyPath, privateKey, publicKey);
|
|
118
|
+
await this.storeKeyPair(domain, selector, privateKey, publicKey);
|
|
214
119
|
await this.saveKeyMetadata({
|
|
215
120
|
domain,
|
|
216
121
|
selector,
|
|
217
122
|
createdAt: Date.now(),
|
|
218
123
|
keySize,
|
|
219
124
|
});
|
|
220
|
-
console.log(`DKIM keys for ${domain}/${selector} created and stored.`);
|
|
221
125
|
}
|
|
222
126
|
|
|
223
|
-
// Changed to public for API access
|
|
224
127
|
public async getDNSRecordForDomain(
|
|
225
|
-
|
|
226
|
-
selector
|
|
128
|
+
domain: string,
|
|
129
|
+
selector = 'default',
|
|
227
130
|
): Promise<plugins.tsclass.network.IDnsRecord> {
|
|
228
|
-
await this.handleDKIMKeysForSelector(
|
|
229
|
-
return this.getDNSRecordForSelector(
|
|
131
|
+
await this.handleDKIMKeysForSelector(domain, selector);
|
|
132
|
+
return this.getDNSRecordForSelector(domain, selector);
|
|
230
133
|
}
|
|
231
134
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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;
|
|
135
|
+
private async getKeyMetadata(
|
|
136
|
+
domain: string,
|
|
137
|
+
selector = 'default',
|
|
138
|
+
): Promise<IDkimKeyMetadata | null> {
|
|
139
|
+
const metadata = await this.storageManager.get(this.metadataKey(domain, selector));
|
|
140
|
+
return metadata ? JSON.parse(metadata) as IDkimKeyMetadata : null;
|
|
248
141
|
}
|
|
249
142
|
|
|
250
|
-
/**
|
|
251
|
-
* Save DKIM key metadata
|
|
252
|
-
*/
|
|
253
143
|
private async saveKeyMetadata(metadata: IDkimKeyMetadata): Promise<void> {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const metadataKey = `/email/dkim/${metadata.domain}/${metadata.selector}/metadata`;
|
|
259
|
-
await this.storageManager.set(metadataKey, JSON.stringify(metadata));
|
|
144
|
+
await this.storageManager.set(
|
|
145
|
+
this.metadataKey(metadata.domain, metadata.selector),
|
|
146
|
+
JSON.stringify(metadata),
|
|
147
|
+
);
|
|
260
148
|
}
|
|
261
149
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
150
|
+
public async needsRotation(
|
|
151
|
+
domain: string,
|
|
152
|
+
selector = 'default',
|
|
153
|
+
rotationIntervalDays = 90,
|
|
154
|
+
): Promise<boolean> {
|
|
266
155
|
const metadata = await this.getKeyMetadata(domain, selector);
|
|
267
|
-
|
|
268
|
-
|
|
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;
|
|
156
|
+
if (!metadata) return true;
|
|
157
|
+
return (Date.now() - metadata.createdAt) / (1000 * 60 * 60 * 24) >= rotationIntervalDays;
|
|
278
158
|
}
|
|
279
159
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// Generate new selector based on date
|
|
160
|
+
public async rotateDkimKeys(
|
|
161
|
+
domain: string,
|
|
162
|
+
currentSelector = 'default',
|
|
163
|
+
keySize = 2048,
|
|
164
|
+
): Promise<string> {
|
|
287
165
|
const now = new Date();
|
|
288
166
|
const newSelector = `key${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
289
|
-
|
|
290
|
-
// Create new keys with custom key size
|
|
291
167
|
const { privateKey, publicKey } = await this.createDKIMKeys(keySize);
|
|
292
|
-
|
|
293
|
-
|
|
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 = {
|
|
168
|
+
await this.storeKeyPair(domain, newSelector, privateKey, publicKey);
|
|
169
|
+
await this.saveKeyMetadata({
|
|
304
170
|
domain,
|
|
305
171
|
selector: newSelector,
|
|
306
172
|
createdAt: Date.now(),
|
|
307
173
|
previousSelector: currentSelector,
|
|
308
|
-
keySize
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
// Update metadata for old keys
|
|
174
|
+
keySize,
|
|
175
|
+
});
|
|
176
|
+
|
|
313
177
|
const oldMetadata = await this.getKeyMetadata(domain, currentSelector);
|
|
314
178
|
if (oldMetadata) {
|
|
315
179
|
oldMetadata.rotatedAt = Date.now();
|
|
316
180
|
await this.saveKeyMetadata(oldMetadata);
|
|
317
181
|
}
|
|
318
|
-
|
|
319
|
-
console.log(`DKIM keys rotated for ${domain}. New selector: ${newSelector}`);
|
|
320
182
|
return newSelector;
|
|
321
183
|
}
|
|
322
184
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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> {
|
|
185
|
+
public async getDNSRecordForSelector(
|
|
186
|
+
domain: string,
|
|
187
|
+
selector: string,
|
|
188
|
+
): Promise<plugins.tsclass.network.IDnsRecord> {
|
|
400
189
|
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
190
|
const keyContents = keys.publicKey
|
|
406
|
-
.replace(
|
|
407
|
-
.replace(
|
|
408
|
-
.replace(/\
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
// Generate the DKIM DNS TXT record
|
|
414
|
-
const dnsRecordValue = `v=DKIM1; h=sha256; k=${keyAlgo}; p=${keyContents}`;
|
|
191
|
+
.replace('-----BEGIN PUBLIC KEY-----', '')
|
|
192
|
+
.replace('-----END PUBLIC KEY-----', '')
|
|
193
|
+
.replace(/\s/g, '');
|
|
194
|
+
const keyAlgo = keys.privateKey.includes('ED25519') || keys.publicKey.length < 200
|
|
195
|
+
? 'ed25519'
|
|
196
|
+
: 'rsa';
|
|
415
197
|
|
|
416
198
|
return {
|
|
417
199
|
name: `${selector}._domainkey.${domain}`,
|
|
418
200
|
type: 'TXT',
|
|
419
201
|
dnsSecEnabled: null,
|
|
420
|
-
value:
|
|
202
|
+
value: `v=DKIM1; h=sha256; k=${keyAlgo}; p=${keyContents}`,
|
|
421
203
|
};
|
|
422
204
|
}
|
|
423
205
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
}
|
|
206
|
+
public async cleanupOldKeys(domain: string, gracePeriodDays = 30): Promise<void> {
|
|
207
|
+
const cutoff = Date.now() - gracePeriodDays * 24 * 60 * 60 * 1000;
|
|
208
|
+
const keys = await this.storageManager.list(`/email/dkim/${domain}/`);
|
|
209
|
+
const metadataKeys = keys.filter((key) => key.endsWith('/metadata'));
|
|
210
|
+
|
|
211
|
+
for (const metadataKey of metadataKeys) {
|
|
212
|
+
const serialized = await this.storageManager.get(metadataKey);
|
|
213
|
+
if (!serialized) continue;
|
|
214
|
+
const metadata = JSON.parse(serialized) as IDkimKeyMetadata;
|
|
215
|
+
if (!metadata.rotatedAt || metadata.rotatedAt >= cutoff) continue;
|
|
216
|
+
await Promise.all([
|
|
217
|
+
this.storageManager.delete(this.selectorKey(domain, metadata.selector, 'private.key')),
|
|
218
|
+
this.storageManager.delete(this.selectorKey(domain, metadata.selector, 'public.key')),
|
|
219
|
+
this.storageManager.delete(metadataKey),
|
|
220
|
+
]);
|
|
469
221
|
}
|
|
470
222
|
}
|
|
471
223
|
}
|
package/ts/paths.ts
CHANGED
|
@@ -1,48 +1,9 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
3
|
+
// Code and packaged executable paths only. SmartMTA data is supplied through
|
|
4
|
+
// managed storage interfaces and never persisted directly to the filesystem.
|
|
5
5
|
export const packageDir = plugins.path.join(
|
|
6
6
|
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
|
7
7
|
'../'
|
|
8
8
|
);
|
|
9
9
|
export const distServe = plugins.path.join(packageDir, './dist_serve');
|
|
10
|
-
|
|
11
|
-
// Configure data directory with environment variable or default to .nogit/data
|
|
12
|
-
const DEFAULT_DATA_PATH = '.nogit/data';
|
|
13
|
-
export const dataDir = process.env.DATA_DIR
|
|
14
|
-
? process.env.DATA_DIR
|
|
15
|
-
: plugins.path.join(baseDir, DEFAULT_DATA_PATH);
|
|
16
|
-
|
|
17
|
-
// MTA directories
|
|
18
|
-
export const keysDir = plugins.path.join(dataDir, 'keys');
|
|
19
|
-
export const dnsRecordsDir = plugins.path.join(dataDir, 'dns');
|
|
20
|
-
export const sentEmailsDir = plugins.path.join(dataDir, 'emails', 'sent');
|
|
21
|
-
export const receivedEmailsDir = plugins.path.join(dataDir, 'emails', 'received');
|
|
22
|
-
export const failedEmailsDir = plugins.path.join(dataDir, 'emails', 'failed'); // For failed emails
|
|
23
|
-
export const logsDir = plugins.path.join(dataDir, 'logs'); // For logs
|
|
24
|
-
|
|
25
|
-
// Email template directories
|
|
26
|
-
export const emailTemplatesDir = plugins.path.join(dataDir, 'templates', 'email');
|
|
27
|
-
export const MtaAttachmentsDir = plugins.path.join(dataDir, 'attachments'); // For email attachments
|
|
28
|
-
|
|
29
|
-
// Configuration path
|
|
30
|
-
export const configPath = process.env.CONFIG_PATH
|
|
31
|
-
? process.env.CONFIG_PATH
|
|
32
|
-
: plugins.path.join(baseDir, 'config.json');
|
|
33
|
-
|
|
34
|
-
// Create directories if they don't exist
|
|
35
|
-
export async function ensureDirectories() {
|
|
36
|
-
// Ensure data directories
|
|
37
|
-
await plugins.smartfs.directory(dataDir).recursive().create();
|
|
38
|
-
await plugins.smartfs.directory(keysDir).recursive().create();
|
|
39
|
-
await plugins.smartfs.directory(dnsRecordsDir).recursive().create();
|
|
40
|
-
await plugins.smartfs.directory(sentEmailsDir).recursive().create();
|
|
41
|
-
await plugins.smartfs.directory(receivedEmailsDir).recursive().create();
|
|
42
|
-
await plugins.smartfs.directory(failedEmailsDir).recursive().create();
|
|
43
|
-
await plugins.smartfs.directory(logsDir).recursive().create();
|
|
44
|
-
|
|
45
|
-
// Ensure email template directories
|
|
46
|
-
await plugins.smartfs.directory(emailTemplatesDir).recursive().create();
|
|
47
|
-
await plugins.smartfs.directory(MtaAttachmentsDir).recursive().create();
|
|
48
|
-
}
|
package/ts/plugins.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// node native
|
|
2
2
|
import * as dns from 'dns';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
3
|
import * as crypto from 'crypto';
|
|
5
4
|
import * as http from 'http';
|
|
6
5
|
import * as net from 'net';
|
|
@@ -11,7 +10,6 @@ import * as util from 'util';
|
|
|
11
10
|
|
|
12
11
|
export {
|
|
13
12
|
dns,
|
|
14
|
-
fs,
|
|
15
13
|
crypto,
|
|
16
14
|
http,
|
|
17
15
|
net,
|
|
@@ -23,15 +21,12 @@ export {
|
|
|
23
21
|
|
|
24
22
|
// @push.rocks scope
|
|
25
23
|
import * as smartfile from '@push.rocks/smartfile';
|
|
26
|
-
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
|
|
27
24
|
import * as smartlog from '@push.rocks/smartlog';
|
|
28
25
|
import * as smartmail from '@push.rocks/smartmail';
|
|
29
26
|
import * as smartpath from '@push.rocks/smartpath';
|
|
30
27
|
import * as smartrust from '@push.rocks/smartrust';
|
|
31
28
|
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
export { smartfile, SmartFs, smartlog, smartmail, smartpath, smartrust };
|
|
29
|
+
export { smartfile, smartlog, smartmail, smartpath, smartrust };
|
|
35
30
|
|
|
36
31
|
// Define SmartLog types for use in error handling
|
|
37
32
|
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|