@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.
- package/changelog.md +40 -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/functions.errors.d.ts +3 -0
- package/dist_ts/functions.errors.js +8 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -1
- package/dist_ts/mail/core/classes.bouncemanager.d.ts +16 -12
- package/dist_ts/mail/core/classes.bouncemanager.js +146 -129
- package/dist_ts/mail/core/classes.email.js +15 -13
- package/dist_ts/mail/core/classes.emailvalidator.d.ts +3 -3
- package/dist_ts/mail/core/classes.emailvalidator.js +7 -5
- 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 +92 -22
- package/dist_ts/mail/delivery/classes.delivery.queue.js +738 -151
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +13 -7
- package/dist_ts/mail/delivery/classes.delivery.system.js +458 -145
- package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +9 -8
- package/dist_ts/mail/delivery/functions.safe-observers.d.ts +10 -0
- package/dist_ts/mail/delivery/functions.safe-observers.js +37 -0
- package/dist_ts/mail/delivery/interfaces.d.ts +21 -0
- package/dist_ts/mail/delivery/interfaces.js +1 -1
- 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 +10 -6
- package/dist_ts/mail/routing/classes.dkim.manager.js +47 -31
- package/dist_ts/mail/routing/classes.dns.manager.d.ts +7 -5
- package/dist_ts/mail/routing/classes.dns.manager.js +22 -11
- package/dist_ts/mail/routing/classes.email.action.executor.d.ts +2 -1
- package/dist_ts/mail/routing/classes.email.action.executor.js +45 -16
- package/dist_ts/mail/routing/classes.email.router.d.ts +5 -2
- package/dist_ts/mail/routing/classes.email.router.js +17 -12
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +12 -6
- package/dist_ts/mail/routing/classes.unified.email.server.js +69 -78
- package/dist_ts/mail/routing/interfaces.d.ts +0 -2
- package/dist_ts/mail/security/classes.dkimcreator.d.ts +22 -45
- package/dist_ts/mail/security/classes.dkimcreator.js +93 -296
- package/dist_ts/mail/security/classes.spfverifier.js +5 -3
- 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 +14 -12
- package/dist_ts/security/classes.ipreputationchecker.d.ts +9 -6
- package/dist_ts/security/classes.ipreputationchecker.js +42 -93
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +52 -4
- package/dist_ts/security/classes.rustsecuritybridge.js +201 -4
- package/dist_ts/security/classes.securitylogger.js +7 -5
- package/dist_ts/security/index.d.ts +1 -1
- package/dist_ts/security/index.js +2 -2
- package/package.json +8 -9
- package/readme.hints.md +4 -3
- package/readme.md +50 -18
- package/readme.plan.md +6 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/functions.errors.ts +8 -0
- package/ts/index.ts +3 -0
- package/ts/mail/core/classes.bouncemanager.ts +188 -155
- package/ts/mail/core/classes.email.ts +20 -14
- package/ts/mail/core/classes.emailvalidator.ts +9 -7
- package/ts/mail/core/classes.templatemanager.ts +42 -57
- package/ts/mail/delivery/classes.delivery.queue.ts +996 -185
- package/ts/mail/delivery/classes.delivery.system.ts +583 -170
- package/ts/mail/delivery/classes.unified.rate.limiter.ts +9 -8
- package/ts/mail/delivery/functions.safe-observers.ts +45 -0
- package/ts/mail/delivery/interfaces.ts +27 -1
- package/ts/mail/interfaces.storage.ts +64 -10
- package/ts/mail/routing/classes.dkim.manager.ts +65 -40
- package/ts/mail/routing/classes.dns.manager.ts +39 -16
- package/ts/mail/routing/classes.email.action.executor.ts +64 -17
- package/ts/mail/routing/classes.email.router.ts +20 -13
- package/ts/mail/routing/classes.unified.email.server.ts +107 -86
- package/ts/mail/routing/interfaces.ts +0 -2
- package/ts/mail/security/classes.dkimcreator.ts +150 -355
- package/ts/mail/security/classes.spfverifier.ts +4 -2
- package/ts/paths.ts +2 -41
- package/ts/plugins.ts +1 -6
- package/ts/security/classes.contentscanner.ts +14 -12
- package/ts/security/classes.ipreputationchecker.ts +46 -99
- package/ts/security/classes.rustsecuritybridge.ts +272 -6
- package/ts/security/classes.securitylogger.ts +6 -4
- package/ts/security/index.ts +5 -1
|
@@ -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;
|
|
@@ -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
|
|
28
|
-
private storageManager?: IStorageManagerLike;
|
|
26
|
+
constructor(private readonly storageManager: IStorageManager) {}
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
-
]);
|
|
42
|
+
private metadataKey(domain: string, selector: string): string {
|
|
43
|
+
return `/email/dkim/${domain}/${selector}/metadata`;
|
|
52
44
|
}
|
|
53
45
|
|
|
54
|
-
private async
|
|
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(
|
|
65
|
-
this.storageManager.set(
|
|
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
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
public async handleDKIMKeysForSelector(
|
|
63
|
+
domain: string,
|
|
64
|
+
selector = 'default',
|
|
65
|
+
keySize = 2048,
|
|
66
|
+
): Promise<void> {
|
|
95
67
|
try {
|
|
96
|
-
await this.readDKIMKeysForSelector(
|
|
68
|
+
await this.readDKIMKeysForSelector(domain, selector);
|
|
97
69
|
} catch {
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
]);
|
|
79
|
+
public async readDKIMKeys(domain: string): Promise<{ privateKey: string; publicKey: string }> {
|
|
80
|
+
return this.readDKIMKeysForSelector(domain, 'default');
|
|
81
|
+
}
|
|
156
82
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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.`);
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
selector
|
|
174
|
+
domain: string,
|
|
175
|
+
selector = 'default',
|
|
227
176
|
): Promise<plugins.tsclass.network.IDnsRecord> {
|
|
228
|
-
await this.handleDKIMKeysForSelector(
|
|
229
|
-
return this.getDNSRecordForSelector(
|
|
177
|
+
await this.handleDKIMKeysForSelector(domain, selector);
|
|
178
|
+
return this.getDNSRecordForSelector(domain, selector);
|
|
230
179
|
}
|
|
231
180
|
|
|
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;
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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:
|
|
420
|
-
value:
|
|
244
|
+
dnsSecEnabled: false,
|
|
245
|
+
value: `v=DKIM1; h=sha256; k=${material.keyType}; p=${keyContents}`,
|
|
421
246
|
};
|
|
422
247
|
}
|
|
423
248
|
|
|
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
|
-
}
|
|
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
|
-
|
|
120
|
+
const errorMessage = getErrorMessage(error);
|
|
121
|
+
logger.log('error', `Error parsing SPF record: ${errorMessage}`, {
|
|
120
122
|
record,
|
|
121
|
-
error:
|
|
123
|
+
error: errorMessage
|
|
122
124
|
});
|
|
123
125
|
return null;
|
|
124
126
|
}
|