@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.
@@ -73,86 +73,99 @@ export class DnsServerRuntime {
73
73
  });
74
74
  this.dcRouterRef.dnsServer = dnsServer;
75
75
 
76
- // Start the DNS server (UDP only)
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
- logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`);
79
-
80
- // Wire DNS query events to MetricsManager and logger with adaptive rate limiting
81
- if (this.dcRouterRef.metricsManager) {
82
- const flushDnsBatch = () => {
83
- if (this.batchCount > 0) {
84
- logger.log('info', `DNS: ${this.batchCount} queries processed (rate limited)`, { zone: 'dns' });
85
- this.batchCount = 0;
86
- }
87
- this.batchTimer = null;
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
- // Adaptive logging: individual logs up to 2/sec, then batch
103
- const nowSec = Math.floor(Date.now() / 1000);
104
- if (nowSec !== this.logWindowSecond) {
105
- this.logWindowSecond = nowSec;
106
- this.logWindowCount = 0;
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
- if (this.logWindowCount < 2) {
110
- this.logWindowCount++;
111
- const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', ');
112
- logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' });
113
- } else {
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
- // Validate DNS configuration
123
- await this.validateConfiguration();
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
- // Generate and register authoritative records
126
- const authoritativeRecords = await this.generateAuthoritativeRecords();
124
+ // Validate DNS configuration
125
+ await this.validateConfiguration();
127
126
 
128
- // Generate email DNS records
129
- const emailDnsRecords = await this.generateEmailDnsRecords();
127
+ // Generate and register authoritative records
128
+ const authoritativeRecords = await this.generateAuthoritativeRecords();
130
129
 
131
- // Ensure DKIM keys exist for internal-dns domains before generating records.
132
- await this.initializeDkimForEmailDomains();
130
+ // Generate email DNS records
131
+ const emailDnsRecords = await this.generateEmailDnsRecords();
133
132
 
134
- // Generate DKIM records directly from smartmta.
135
- const dkimRecords = await this.loadDkimRecords();
133
+ // Ensure DKIM keys exist for internal-dns domains before generating records.
134
+ await this.initializeDkimForEmailDomains();
136
135
 
137
- // Combine all records: authoritative, email, DKIM, and user-defined
138
- const allRecords: TDnsRecordSeed[] = [...authoritativeRecords, ...emailDnsRecords, ...dkimRecords];
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
- // Apply proxy IP replacement if configured
144
- await this.applyProxyIpReplacement(allRecords);
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
- // Register all DNS records
147
- if (allRecords.length > 0) {
148
- this.registerRecords(allRecords);
149
- logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${dkimRecords.length} DKIM, ${options.dnsRecords?.length || 0} user-defined)`);
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
- // Hand the DnsServer to DnsManager so DB-backed local records on
153
- // dcrouter-hosted domains get registered too.
154
- if (this.dcRouterRef.dnsManager) {
155
- await this.dcRouterRef.dnsManager.attachDnsServer(dnsServer);
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',