@serve.zone/dcrouter 15.4.1 → 15.6.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/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +2 -3
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +2 -0
- package/dist_ts/dns/classes.dns-server-runtime.js +78 -60
- package/dist_ts/email/classes.accepted-email-spool.d.ts +17 -1
- package/dist_ts/email/classes.accepted-email-spool.js +65 -1
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +36 -2
- package/dist_ts/email/classes.workapp-mail-manager.js +441 -13
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +16 -1
- package/dist_ts_migrations/index.js +32 -18
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +6 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +1 -2
- package/ts/dns/classes.dns-server-runtime.ts +85 -67
- package/ts/email/classes.accepted-email-spool.ts +84 -0
- package/ts/email/classes.workapp-mail-manager.ts +517 -13
- package/ts/opsserver/handlers/gatewayclient.handler.ts +35 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -73,86 +73,99 @@ export class DnsServerRuntime {
|
|
|
73
73
|
});
|
|
74
74
|
this.dcRouterRef.dnsServer = dnsServer;
|
|
75
75
|
|
|
76
|
-
// Start the DNS server
|
|
76
|
+
// Start the DNS server. smartdns owns UDP and DNS-over-TCP; dcrouter
|
|
77
|
+
// keeps DNS-over-HTTPS routed through SmartProxy manual HTTPS mode.
|
|
77
78
|
await dnsServer.start();
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
dnsServer.on('query', (event: plugins.smartdns.dnsServerMod.IDnsQueryCompletedEvent) => {
|
|
91
|
-
// Metrics tracking
|
|
92
|
-
for (const question of event.questions) {
|
|
93
|
-
this.dcRouterRef.metricsManager?.trackDnsQuery(
|
|
94
|
-
question.type,
|
|
95
|
-
question.name,
|
|
96
|
-
false,
|
|
97
|
-
event.responseTimeMs,
|
|
98
|
-
event.answered,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
79
|
+
try {
|
|
80
|
+
logger.log('info', `DNS server started on UDP/TCP ${vmIpAddress}:53`);
|
|
81
|
+
|
|
82
|
+
// Wire DNS query events to MetricsManager and logger with adaptive rate limiting
|
|
83
|
+
if (this.dcRouterRef.metricsManager) {
|
|
84
|
+
const flushDnsBatch = () => {
|
|
85
|
+
if (this.batchCount > 0) {
|
|
86
|
+
logger.log('info', `DNS: ${this.batchCount} queries processed (rate limited)`, { zone: 'dns' });
|
|
87
|
+
this.batchCount = 0;
|
|
88
|
+
}
|
|
89
|
+
this.batchTimer = null;
|
|
90
|
+
};
|
|
101
91
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
92
|
+
dnsServer.on('query', (event: plugins.smartdns.dnsServerMod.IDnsQueryCompletedEvent) => {
|
|
93
|
+
// Metrics tracking
|
|
94
|
+
for (const question of event.questions) {
|
|
95
|
+
this.dcRouterRef.metricsManager?.trackDnsQuery(
|
|
96
|
+
question.type,
|
|
97
|
+
question.name,
|
|
98
|
+
false,
|
|
99
|
+
event.responseTimeMs,
|
|
100
|
+
event.answered,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
108
103
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.batchCount++;
|
|
115
|
-
if (!this.batchTimer) {
|
|
116
|
-
this.batchTimer = setTimeout(flushDnsBatch, 5000);
|
|
104
|
+
// Adaptive logging: individual logs up to 2/sec, then batch
|
|
105
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
106
|
+
if (nowSec !== this.logWindowSecond) {
|
|
107
|
+
this.logWindowSecond = nowSec;
|
|
108
|
+
this.logWindowCount = 0;
|
|
117
109
|
}
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
110
|
|
|
122
|
-
|
|
123
|
-
|
|
111
|
+
if (this.logWindowCount < 2) {
|
|
112
|
+
this.logWindowCount++;
|
|
113
|
+
const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', ');
|
|
114
|
+
logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' });
|
|
115
|
+
} else {
|
|
116
|
+
this.batchCount++;
|
|
117
|
+
if (!this.batchTimer) {
|
|
118
|
+
this.batchTimer = setTimeout(flushDnsBatch, 5000);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
// Validate DNS configuration
|
|
125
|
+
await this.validateConfiguration();
|
|
127
126
|
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
// Generate and register authoritative records
|
|
128
|
+
const authoritativeRecords = await this.generateAuthoritativeRecords();
|
|
130
129
|
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
// Generate email DNS records
|
|
131
|
+
const emailDnsRecords = await this.generateEmailDnsRecords();
|
|
133
132
|
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
// Ensure DKIM keys exist for internal-dns domains before generating records.
|
|
134
|
+
await this.initializeDkimForEmailDomains();
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (options.dnsRecords && options.dnsRecords.length > 0) {
|
|
140
|
-
allRecords.push(...options.dnsRecords);
|
|
141
|
-
}
|
|
136
|
+
// Generate DKIM records directly from smartmta.
|
|
137
|
+
const dkimRecords = await this.loadDkimRecords();
|
|
142
138
|
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
// Combine all records: authoritative, email, DKIM, and user-defined
|
|
140
|
+
const allRecords: TDnsRecordSeed[] = [...authoritativeRecords, ...emailDnsRecords, ...dkimRecords];
|
|
141
|
+
if (options.dnsRecords && options.dnsRecords.length > 0) {
|
|
142
|
+
allRecords.push(...options.dnsRecords);
|
|
143
|
+
}
|
|
145
144
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
// Apply proxy IP replacement if configured
|
|
146
|
+
await this.applyProxyIpReplacement(allRecords);
|
|
147
|
+
|
|
148
|
+
// Register all DNS records
|
|
149
|
+
if (allRecords.length > 0) {
|
|
150
|
+
this.registerRecords(allRecords);
|
|
151
|
+
logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${dkimRecords.length} DKIM, ${options.dnsRecords?.length || 0} user-defined)`);
|
|
152
|
+
}
|
|
151
153
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
// Hand the DnsServer to DnsManager so DB-backed local records on
|
|
155
|
+
// dcrouter-hosted domains get registered too.
|
|
156
|
+
if (this.dcRouterRef.dnsManager) {
|
|
157
|
+
await this.dcRouterRef.dnsManager.attachDnsServer(dnsServer);
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
dnsServer.removeAllListeners();
|
|
161
|
+
await dnsServer.stop().catch((stopError: Error) => {
|
|
162
|
+
logger.log('warn', `Failed to stop DNS server after setup failure: ${stopError.message}`);
|
|
163
|
+
});
|
|
164
|
+
if (this.dcRouterRef.dnsServer === dnsServer) {
|
|
165
|
+
this.dcRouterRef.dnsServer = undefined;
|
|
166
|
+
}
|
|
167
|
+
this.flushQueryLogBatch();
|
|
168
|
+
throw error;
|
|
156
169
|
}
|
|
157
170
|
}
|
|
158
171
|
|
|
@@ -204,6 +217,11 @@ export class DnsServerRuntime {
|
|
|
204
217
|
}
|
|
205
218
|
}
|
|
206
219
|
|
|
220
|
+
/** Stop runtime-owned bookkeeping. smartdns owns network listener cleanup. */
|
|
221
|
+
public async stop(): Promise<void> {
|
|
222
|
+
this.flushQueryLogBatch();
|
|
223
|
+
}
|
|
224
|
+
|
|
207
225
|
private registerRecords(records: TDnsRecordSeed[]): void {
|
|
208
226
|
const dnsServer = this.dcRouterRef.dnsServer;
|
|
209
227
|
if (!dnsServer) return;
|
|
@@ -50,6 +50,18 @@ type TStoredCachedEmailRouteData = {
|
|
|
50
50
|
session?: TStoredCachedEmailSession;
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
export interface IAcceptRawEmailOptions {
|
|
54
|
+
rawMessage: string | plugins.buffer.Buffer;
|
|
55
|
+
envelope: {
|
|
56
|
+
mailFrom: string;
|
|
57
|
+
rcptTo: string[];
|
|
58
|
+
};
|
|
59
|
+
session: IExtendedSmtpSession;
|
|
60
|
+
messageId?: string;
|
|
61
|
+
subject?: string;
|
|
62
|
+
processAfterAccept?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
/**
|
|
54
66
|
* Accept-then-spool pipeline for inbound SMTP messages: persists accepted
|
|
55
67
|
* messages as CachedEmail docs, replays them through SmartMTA on an interval,
|
|
@@ -131,6 +143,55 @@ export class AcceptedEmailSpool {
|
|
|
131
143
|
};
|
|
132
144
|
}
|
|
133
145
|
|
|
146
|
+
public async acceptRawMessage(optionsArg: IAcceptRawEmailOptions): Promise<IMessageAcceptanceDecision & { spoolItemId: string }> {
|
|
147
|
+
if (!this.dcRouterRef.dcRouterDb?.isReady()) {
|
|
148
|
+
throw new Error('DcRouterDb is not available for email acceptance');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const rawMessage = plugins.buffer.Buffer.isBuffer(optionsArg.rawMessage)
|
|
152
|
+
? optionsArg.rawMessage.toString('utf8')
|
|
153
|
+
: optionsArg.rawMessage;
|
|
154
|
+
const cachedEmail = CachedEmail.createNew();
|
|
155
|
+
cachedEmail.messageId = optionsArg.messageId || this.extractHeader(rawMessage, 'message-id') || cachedEmail.id;
|
|
156
|
+
cachedEmail.from = optionsArg.envelope.mailFrom;
|
|
157
|
+
cachedEmail.to = optionsArg.envelope.rcptTo;
|
|
158
|
+
cachedEmail.cc = [];
|
|
159
|
+
cachedEmail.bcc = [];
|
|
160
|
+
cachedEmail.subject = optionsArg.subject || this.extractHeader(rawMessage, 'subject') || '';
|
|
161
|
+
cachedEmail.rawContent = this.setDcRouterCacheIdHeader(rawMessage, cachedEmail.id);
|
|
162
|
+
cachedEmail.status = 'pending';
|
|
163
|
+
cachedEmail.nextAttempt = new Date();
|
|
164
|
+
cachedEmail.routeData = JSON.stringify({
|
|
165
|
+
acceptedAt: new Date().toISOString(),
|
|
166
|
+
session: {
|
|
167
|
+
id: optionsArg.session.id,
|
|
168
|
+
remoteAddress: optionsArg.session.remoteAddress,
|
|
169
|
+
clientHostname: optionsArg.session.clientHostname,
|
|
170
|
+
secure: !!optionsArg.session.secure,
|
|
171
|
+
authenticated: !!optionsArg.session.authenticated,
|
|
172
|
+
user: optionsArg.session.user,
|
|
173
|
+
envelope: optionsArg.session.envelope,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
cachedEmail.updateSenderDomain();
|
|
177
|
+
await cachedEmail.save();
|
|
178
|
+
|
|
179
|
+
if (optionsArg.processAfterAccept !== false) {
|
|
180
|
+
this.run();
|
|
181
|
+
} else {
|
|
182
|
+
cachedEmail.markDelivered();
|
|
183
|
+
await cachedEmail.save();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
accepted: true,
|
|
188
|
+
spoolItemId: cachedEmail.id,
|
|
189
|
+
smtpCode: 250,
|
|
190
|
+
smtpMessage: '2.0.0 Message accepted for delivery',
|
|
191
|
+
continueProcessing: false,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
134
195
|
/** Start the interval-driven spool processor and trigger an immediate run. */
|
|
135
196
|
public start(): void {
|
|
136
197
|
this.clearSpoolTimer();
|
|
@@ -267,6 +328,19 @@ export class AcceptedEmailSpool {
|
|
|
267
328
|
await cachedEmail.save();
|
|
268
329
|
|
|
269
330
|
try {
|
|
331
|
+
if (plugins.buffer.Buffer.isBuffer(emailData)) {
|
|
332
|
+
const handledByTypedEndpoint = await this.dcRouterRef.workAppMailManager?.deliverCachedEmailToTypedEndpoint(
|
|
333
|
+
cachedEmail,
|
|
334
|
+
emailData,
|
|
335
|
+
session,
|
|
336
|
+
);
|
|
337
|
+
if (handledByTypedEndpoint) {
|
|
338
|
+
cachedEmail.markDelivered();
|
|
339
|
+
await cachedEmail.save();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
270
344
|
await emailServer.processEmailByMode(emailData, session);
|
|
271
345
|
if (session.matchedRoute?.action.type === 'forward') {
|
|
272
346
|
cachedEmail.markDelivered();
|
|
@@ -333,6 +407,16 @@ export class AcceptedEmailSpool {
|
|
|
333
407
|
}
|
|
334
408
|
}
|
|
335
409
|
|
|
410
|
+
private extractHeader(rawMessageArg: string, headerNameArg: string): string | undefined {
|
|
411
|
+
const lowerName = `${headerNameArg.toLowerCase()}:`;
|
|
412
|
+
for (const line of rawMessageArg.split(/\r?\n/)) {
|
|
413
|
+
if (!line) return undefined;
|
|
414
|
+
if (line.toLowerCase().startsWith(lowerName)) {
|
|
415
|
+
return line.slice(lowerName.length).trim();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
336
420
|
private async updateAcceptedEmailFromQueueItem(
|
|
337
421
|
item: TSmartMtaQueueItemLike,
|
|
338
422
|
status: 'queued' | 'delivered' | 'failed',
|