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