milter 1.0.0 → 3.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/dist/index.js ADDED
@@ -0,0 +1,914 @@
1
+ // src/constants.ts
2
+ var SMFI_VERSION = 6;
3
+ var SMFIA = {
4
+ UNKNOWN: "U",
5
+ UNIX: "L",
6
+ INET: "4",
7
+ INET6: "6"
8
+ };
9
+ var SMFIC = {
10
+ CONNECT: "C",
11
+ OPTNEG: "O",
12
+ HELO: "H",
13
+ HEADER: "L",
14
+ EOH: "N",
15
+ BODY: "B",
16
+ BODYEOB: "E",
17
+ MAIL: "M",
18
+ RCPT: "R",
19
+ ABORT: "A",
20
+ MACRO: "D",
21
+ QUIT: "Q",
22
+ DATA: "T",
23
+ UNKNOWN: "U"
24
+ };
25
+ var SMFIR = {
26
+ ACCEPT: "a",
27
+ CONTINUE: "c",
28
+ DISCARD: "d",
29
+ REJECT: "r",
30
+ TEMPFAIL: "t",
31
+ REPLYCODE: "y",
32
+ ADDRCPT: "+",
33
+ DELRCPT: "-",
34
+ REPLBODY: "b",
35
+ ADDHEADER: "h",
36
+ INSHEADER: "i",
37
+ CHGHEADER: "m",
38
+ PROGRESS: "p",
39
+ QUARANTINE: "q",
40
+ SETSENDER: "s"
41
+ };
42
+ var SMFIF = {
43
+ ADDHDRS: 1,
44
+ CHGBODY: 2,
45
+ ADDRCPT: 4,
46
+ DELRCPT: 8,
47
+ CHGHDRS: 16,
48
+ QUARANTINE: 32,
49
+ SETSENDER: 64
50
+ };
51
+ var ACTION_ALL = SMFIF.ADDHDRS | SMFIF.CHGBODY | SMFIF.ADDRCPT | SMFIF.DELRCPT | SMFIF.CHGHDRS | SMFIF.QUARANTINE | SMFIF.SETSENDER;
52
+ var SMFIP = {
53
+ NOCONNECT: 1,
54
+ NOHELO: 2,
55
+ NOMAIL: 4,
56
+ NORCPT: 8,
57
+ NOBODY: 16,
58
+ NOHDRS: 32,
59
+ NOEOH: 64,
60
+ NONE: 127
61
+ };
62
+ var DISABLE_ALL_CALLBACKS = SMFIP.NOCONNECT | SMFIP.NOHELO | SMFIP.NOMAIL | SMFIP.NORCPT | SMFIP.NOBODY | SMFIP.NOHDRS | SMFIP.NOEOH;
63
+
64
+ // src/frames.ts
65
+ function frame(command, data = Buffer.alloc(0)) {
66
+ const len = Buffer.alloc(4);
67
+ len.writeUInt32BE(data.length + 1, 0);
68
+ return Buffer.concat([len, Buffer.from(command, "ascii"), data]);
69
+ }
70
+ function send(socket, command, data) {
71
+ if (!socket) {
72
+ return;
73
+ }
74
+ socket.write(frame(command, data ?? Buffer.alloc(0)));
75
+ }
76
+
77
+ // src/context.ts
78
+ var MilterActionError = class extends Error {
79
+ code;
80
+ constructor(code, message) {
81
+ super(message);
82
+ this.name = "MilterActionError";
83
+ this.code = code;
84
+ }
85
+ };
86
+ var EOM_ONLY_PHASES = /* @__PURE__ */ new Set(["bodyEnd"]);
87
+ var MilterContext = class {
88
+ id;
89
+ collectBody;
90
+ maxBodyBytes;
91
+ enforceActionStages;
92
+ chunks = [];
93
+ bodyBytes = 0;
94
+ phase = "connect";
95
+ negotiatedActions = 0;
96
+ socket;
97
+ headers = {};
98
+ headerLines = [];
99
+ macros = {};
100
+ connection = null;
101
+ helo = null;
102
+ dkim = null;
103
+ spf = null;
104
+ from = null;
105
+ to = null;
106
+ constructor(id, socket, collectBody, maxBodyBytes, enforceActionStages) {
107
+ this.id = id;
108
+ this.socket = socket;
109
+ this.collectBody = collectBody;
110
+ this.maxBodyBytes = maxBodyBytes;
111
+ this.enforceActionStages = enforceActionStages;
112
+ }
113
+ setPhase(phase) {
114
+ this.phase = phase;
115
+ }
116
+ getPhase() {
117
+ return this.phase;
118
+ }
119
+ setNegotiatedActions(actions) {
120
+ this.negotiatedActions = actions >>> 0;
121
+ }
122
+ can(action) {
123
+ return (this.negotiatedActions & action) === action;
124
+ }
125
+ appendHeader(name, value) {
126
+ const key = name.toLowerCase();
127
+ this.headerLines.push([name, value]);
128
+ if (!this.headers[key]) {
129
+ this.headers[key] = [];
130
+ }
131
+ this.headers[key].push(value);
132
+ }
133
+ appendBody(chunk) {
134
+ if (!this.collectBody) {
135
+ return;
136
+ }
137
+ this.bodyBytes += chunk.length;
138
+ if (this.bodyBytes > this.maxBodyBytes) {
139
+ throw new Error(
140
+ `Body limit exceeded for connection ${this.id}: ${this.bodyBytes} > ${this.maxBodyBytes}`
141
+ );
142
+ }
143
+ this.chunks.push(Buffer.from(chunk));
144
+ }
145
+ getBody() {
146
+ return Buffer.concat(this.chunks);
147
+ }
148
+ resetMessage() {
149
+ this.headers = {};
150
+ this.headerLines = [];
151
+ this.macros = {};
152
+ this.dkim = null;
153
+ this.spf = null;
154
+ this.from = null;
155
+ this.to = null;
156
+ this.chunks = [];
157
+ this.bodyBytes = 0;
158
+ this.phase = "connect";
159
+ }
160
+ requireAction(action, actionName, phases) {
161
+ if ((this.negotiatedActions & action) !== action) {
162
+ throw new MilterActionError(
163
+ "E_MILTER_ACTION_CAPABILITY",
164
+ `${actionName} not permitted by negotiated action flags`
165
+ );
166
+ }
167
+ if (this.enforceActionStages && phases && !phases.has(this.phase)) {
168
+ throw new MilterActionError(
169
+ "E_MILTER_ACTION_STAGE",
170
+ `${actionName} is not allowed during phase '${this.phase}'`
171
+ );
172
+ }
173
+ }
174
+ send(command, data) {
175
+ send(this.socket, command, data);
176
+ }
177
+ continue() {
178
+ this.send(SMFIR.CONTINUE);
179
+ }
180
+ accept() {
181
+ this.send(SMFIR.ACCEPT);
182
+ }
183
+ reject() {
184
+ this.send(SMFIR.REJECT);
185
+ }
186
+ discard() {
187
+ this.send(SMFIR.DISCARD);
188
+ }
189
+ tempfail() {
190
+ this.send(SMFIR.TEMPFAIL);
191
+ }
192
+ progress() {
193
+ this.send(SMFIR.PROGRESS);
194
+ }
195
+ addHeader(name, value) {
196
+ this.requireAction(SMFIF.ADDHDRS, "addHeader", EOM_ONLY_PHASES);
197
+ this.send(SMFIR.ADDHEADER, Buffer.from(`${name}\0${value}\0`, "utf8"));
198
+ }
199
+ insertHeader(index, name, value) {
200
+ this.requireAction(SMFIF.ADDHDRS, "insertHeader", EOM_ONLY_PHASES);
201
+ const suffix = Buffer.from(`${name}\0${value}\0`, "utf8");
202
+ const data = Buffer.alloc(4 + suffix.length);
203
+ data.writeUInt32BE(index >>> 0, 0);
204
+ suffix.copy(data, 4);
205
+ this.send(SMFIR.INSHEADER, data);
206
+ }
207
+ changeHeader(name, index, value = "") {
208
+ this.requireAction(SMFIF.CHGHDRS, "changeHeader", EOM_ONLY_PHASES);
209
+ const suffix = Buffer.from(`${name}\0${value}\0`, "utf8");
210
+ const data = Buffer.alloc(4 + suffix.length);
211
+ data.writeUInt32BE(index >>> 0, 0);
212
+ suffix.copy(data, 4);
213
+ this.send(SMFIR.CHGHEADER, data);
214
+ }
215
+ addRecipient(address) {
216
+ this.requireAction(SMFIF.ADDRCPT, "addRecipient", EOM_ONLY_PHASES);
217
+ this.send(SMFIR.ADDRCPT, Buffer.from(`${address}\0`, "utf8"));
218
+ }
219
+ deleteRecipient(address) {
220
+ this.requireAction(SMFIF.DELRCPT, "deleteRecipient", EOM_ONLY_PHASES);
221
+ this.send(SMFIR.DELRCPT, Buffer.from(`${address}\0`, "utf8"));
222
+ }
223
+ replaceBody(chunk) {
224
+ this.requireAction(SMFIF.CHGBODY, "replaceBody", EOM_ONLY_PHASES);
225
+ const payload = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
226
+ this.send(SMFIR.REPLBODY, payload);
227
+ }
228
+ quarantine(reason = "") {
229
+ this.requireAction(SMFIF.QUARANTINE, "quarantine", EOM_ONLY_PHASES);
230
+ this.send(SMFIR.QUARANTINE, Buffer.from(`${reason}\0`, "utf8"));
231
+ }
232
+ setSender(sender) {
233
+ this.requireAction(SMFIF.SETSENDER, "setSender", EOM_ONLY_PHASES);
234
+ this.send(SMFIR.SETSENDER, Buffer.from(`${sender}\0`, "utf8"));
235
+ }
236
+ replyCode(code, message, enhancedCode = "5.7.1") {
237
+ this.send(SMFIR.REPLYCODE, Buffer.from(`${code} ${enhancedCode} ${message}\0`, "utf8"));
238
+ }
239
+ };
240
+
241
+ // src/decisions.ts
242
+ var DECISION_TO_COMMAND = {
243
+ continue: SMFIR.CONTINUE,
244
+ accept: SMFIR.ACCEPT,
245
+ reject: SMFIR.REJECT,
246
+ discard: SMFIR.DISCARD,
247
+ tempfail: SMFIR.TEMPFAIL
248
+ };
249
+ var Decision = {
250
+ continue() {
251
+ return { command: SMFIR.CONTINUE };
252
+ },
253
+ accept() {
254
+ return { command: SMFIR.ACCEPT };
255
+ },
256
+ reject() {
257
+ return { command: SMFIR.REJECT };
258
+ },
259
+ discard() {
260
+ return { command: SMFIR.DISCARD };
261
+ },
262
+ tempfail() {
263
+ return { command: SMFIR.TEMPFAIL };
264
+ },
265
+ replyCode(code, message, enhancedCode = "5.7.1") {
266
+ const payload = Buffer.from(`${code} ${enhancedCode} ${message}\0`, "utf8");
267
+ return {
268
+ command: SMFIR.REPLYCODE,
269
+ data: payload
270
+ };
271
+ }
272
+ };
273
+ function normalizeDecision(decision, fallback) {
274
+ if (decision === null) {
275
+ return null;
276
+ }
277
+ if (decision === void 0) {
278
+ return { command: DECISION_TO_COMMAND[fallback] };
279
+ }
280
+ if (typeof decision === "string") {
281
+ return { command: DECISION_TO_COMMAND[decision] };
282
+ }
283
+ if (decision.data === void 0) {
284
+ return { command: decision.command };
285
+ }
286
+ return {
287
+ command: decision.command,
288
+ data: decision.data
289
+ };
290
+ }
291
+
292
+ // src/dkim.ts
293
+ import { dkimSign } from "mailauth/lib/dkim/sign.js";
294
+ import { dkimVerify } from "mailauth/lib/dkim/verify.js";
295
+ function normalizeSender(from) {
296
+ const value = from?.[0]?.trim();
297
+ if (!value || value === "<>") {
298
+ return void 0;
299
+ }
300
+ if (value.startsWith("<") && value.endsWith(">")) {
301
+ return value.slice(1, -1).trim() || void 0;
302
+ }
303
+ return value;
304
+ }
305
+ function buildMessage(ctx, body) {
306
+ const headers = ctx.headerLines.map(([name, value]) => `${name}: ${value}`).join("\r\n");
307
+ return Buffer.concat([
308
+ Buffer.from(`${headers}\r
309
+ \r
310
+ `, "utf8"),
311
+ body
312
+ ]);
313
+ }
314
+ async function verifyDkim(body, ctx, options = {}) {
315
+ const sender = options.sender ?? normalizeSender(ctx.from);
316
+ const result = await dkimVerify(buildMessage(ctx, body), {
317
+ ...options,
318
+ ...sender === void 0 ? {} : { sender }
319
+ });
320
+ ctx.dkim = result;
321
+ return result;
322
+ }
323
+ async function signDkim(message, options) {
324
+ const signatureData = options.signatureData?.length ? options.signatureData.map((entry) => ({ ...entry })) : [{
325
+ signingDomain: options.signingDomain,
326
+ selector: options.selector,
327
+ privateKey: options.privateKey,
328
+ ...options.canonicalization === void 0 ? {} : { canonicalization: options.canonicalization },
329
+ ...options.algorithm === void 0 ? {} : { algorithm: options.algorithm },
330
+ ...options.maxBodyLength === void 0 ? {} : { maxBodyLength: options.maxBodyLength },
331
+ ...options.identity === void 0 ? {} : { identity: options.identity }
332
+ }];
333
+ return dkimSign(message, {
334
+ ...options,
335
+ signatureData
336
+ });
337
+ }
338
+
339
+ // src/logger.ts
340
+ var ConsoleLogger = class {
341
+ prefix;
342
+ constructor(prefix = "[MILTER]") {
343
+ this.prefix = prefix;
344
+ }
345
+ debug(...args) {
346
+ console.debug(this.prefix, ...args);
347
+ }
348
+ info(...args) {
349
+ console.info(this.prefix, ...args);
350
+ }
351
+ warn(...args) {
352
+ console.warn(this.prefix, ...args);
353
+ }
354
+ error(...args) {
355
+ console.error(this.prefix, ...args);
356
+ }
357
+ };
358
+
359
+ // src/server.ts
360
+ import fs from "fs";
361
+ import net from "net";
362
+
363
+ // src/spf.ts
364
+ import { spf as verifySpf } from "mailauth/lib/spf/index.js";
365
+ var DEFAULT_POLICY = {
366
+ pass: "continue",
367
+ fail: "reject",
368
+ softfail: "continue",
369
+ neutral: "continue",
370
+ none: "continue",
371
+ temperror: "tempfail",
372
+ permerror: "continue"
373
+ };
374
+ function normalizeSender2(from) {
375
+ const value = (typeof from === "string" ? from : from[0])?.trim();
376
+ if (!value || value === "<>") {
377
+ return void 0;
378
+ }
379
+ if (value.startsWith("<") && value.endsWith(">")) {
380
+ return value.slice(1, -1).trim() || void 0;
381
+ }
382
+ return value;
383
+ }
384
+ async function checkSpf(from, ctx, options = {}) {
385
+ const ip = ctx.connection?.address;
386
+ if (!ip) {
387
+ throw new Error("SPF requires the client IP from the Milter CONNECT event.");
388
+ }
389
+ const sender = normalizeSender2(from);
390
+ const helo = ctx.helo ?? ctx.connection?.hostname;
391
+ const result = await verifySpf({
392
+ ip,
393
+ ...sender === void 0 ? {} : { sender },
394
+ ...helo === void 0 ? {} : { helo },
395
+ ...options.mta === void 0 ? {} : { mta: options.mta },
396
+ ...options.maxResolveCount === void 0 ? {} : { maxResolveCount: options.maxResolveCount },
397
+ ...options.maxVoidCount === void 0 ? {} : { maxVoidCount: options.maxVoidCount },
398
+ ...options.resolver === void 0 ? {} : { resolver: options.resolver }
399
+ });
400
+ ctx.spf = result;
401
+ return result;
402
+ }
403
+ function spfDecision(result, policy = {}) {
404
+ const code = result.status.result;
405
+ return policy[code] ?? DEFAULT_POLICY[code] ?? "continue";
406
+ }
407
+
408
+ // src/utils.ts
409
+ function readCString(buf, start = 0) {
410
+ if (start >= buf.length) {
411
+ return {
412
+ value: "",
413
+ next: buf.length
414
+ };
415
+ }
416
+ const end = buf.indexOf(0, start);
417
+ if (end === -1) {
418
+ return {
419
+ value: buf.toString("utf8", start),
420
+ next: buf.length
421
+ };
422
+ }
423
+ return {
424
+ value: buf.toString("utf8", start, end),
425
+ next: end + 1
426
+ };
427
+ }
428
+ function readCStringList(buf, start = 0) {
429
+ const out = [];
430
+ let offset = start;
431
+ while (offset < buf.length) {
432
+ const { value, next } = readCString(buf, offset);
433
+ if (next === offset) {
434
+ break;
435
+ }
436
+ offset = next;
437
+ if (value.length === 0) {
438
+ if (offset >= buf.length) {
439
+ break;
440
+ }
441
+ continue;
442
+ }
443
+ out.push(value);
444
+ }
445
+ return out;
446
+ }
447
+ function readCStringPairs(buf, start = 0) {
448
+ const list = readCStringList(buf, start);
449
+ const map = {};
450
+ for (let i = 0; i + 1 < list.length; i += 2) {
451
+ const key = list[i];
452
+ const value = list[i + 1];
453
+ if (key && value) {
454
+ map[key] = value;
455
+ }
456
+ }
457
+ return map;
458
+ }
459
+ function parseConnectInfo(payload) {
460
+ const host = readCString(payload, 0);
461
+ const familyByte = payload.toString("ascii", host.next, host.next + 1);
462
+ if (familyByte === SMFIA.UNKNOWN || host.next >= payload.length) {
463
+ return {
464
+ hostname: host.value,
465
+ family: "UNKNOWN"
466
+ };
467
+ }
468
+ if (host.next + 3 > payload.length) {
469
+ return {
470
+ hostname: host.value,
471
+ family: "UNKNOWN"
472
+ };
473
+ }
474
+ const port = payload.readUInt16BE(host.next + 1);
475
+ const addr = readCString(payload, host.next + 3).value;
476
+ const family = familyByte === SMFIA.UNIX ? "UNIX" : familyByte === SMFIA.INET ? "INET" : familyByte === SMFIA.INET6 ? "INET6" : "UNKNOWN";
477
+ return {
478
+ hostname: host.value,
479
+ family,
480
+ address: addr,
481
+ port
482
+ };
483
+ }
484
+ function cloneHeaders(headers) {
485
+ const out = {};
486
+ for (const [name, values] of Object.entries(headers)) {
487
+ out[name] = [...values];
488
+ }
489
+ return out;
490
+ }
491
+
492
+ // src/server.ts
493
+ var DEFAULT_OPTIONS = {
494
+ socketPath: void 0,
495
+ host: "127.0.0.1",
496
+ port: void 0,
497
+ actions: SMFIF.ADDHDRS | SMFIF.CHGBODY | SMFIF.ADDRCPT,
498
+ unlinkOnStart: true,
499
+ chmod: 511,
500
+ collectBody: true,
501
+ maxBodyBytes: 32 * 1024 * 1024,
502
+ enforceActionStages: true,
503
+ defaultDecision: "continue",
504
+ logger: new ConsoleLogger("[MILTER]")
505
+ };
506
+ function createHandlerMap() {
507
+ return {
508
+ connect: /* @__PURE__ */ new Set(),
509
+ helo: /* @__PURE__ */ new Set(),
510
+ mail: /* @__PURE__ */ new Set(),
511
+ rcpt: /* @__PURE__ */ new Set(),
512
+ headerLine: /* @__PURE__ */ new Set(),
513
+ headers: /* @__PURE__ */ new Set(),
514
+ header: /* @__PURE__ */ new Set(),
515
+ bodyChunk: /* @__PURE__ */ new Set(),
516
+ bodyEnd: /* @__PURE__ */ new Set(),
517
+ data: /* @__PURE__ */ new Set(),
518
+ macro: /* @__PURE__ */ new Set(),
519
+ abort: /* @__PURE__ */ new Set(),
520
+ close: /* @__PURE__ */ new Set(),
521
+ unknown: /* @__PURE__ */ new Set(),
522
+ error: /* @__PURE__ */ new Set()
523
+ };
524
+ }
525
+ var MilterServer = class {
526
+ server;
527
+ handlers;
528
+ options;
529
+ nextConnectionId = 1;
530
+ dkimOptions = null;
531
+ spfOptions = null;
532
+ constructor(options = {}) {
533
+ this.options = {
534
+ ...DEFAULT_OPTIONS,
535
+ ...options,
536
+ logger: options.logger ?? DEFAULT_OPTIONS.logger
537
+ };
538
+ if (!this.options.socketPath && this.options.port === void 0) {
539
+ throw new Error("Either socketPath or port must be provided.");
540
+ }
541
+ this.handlers = createHandlerMap();
542
+ this.server = net.createServer((socket) => {
543
+ this.handleConnection(socket);
544
+ });
545
+ this.server.on("error", (err) => {
546
+ this.options.logger.error("Server error", err);
547
+ this.emitInternalError(void 0, err);
548
+ });
549
+ }
550
+ on(event, handler) {
551
+ this.handlers[event].add(handler);
552
+ return this;
553
+ }
554
+ off(event, handler) {
555
+ this.handlers[event].delete(handler);
556
+ return this;
557
+ }
558
+ once(event, handler) {
559
+ const wrap = (async (...args) => {
560
+ this.off(event, wrap);
561
+ return handler(...args);
562
+ });
563
+ this.on(event, wrap);
564
+ return this;
565
+ }
566
+ useSpf(options = {}) {
567
+ this.spfOptions = options;
568
+ return this;
569
+ }
570
+ useDkim(options = {}) {
571
+ if (!this.options.collectBody) {
572
+ throw new Error("DKIM verification requires collectBody to be enabled.");
573
+ }
574
+ this.dkimOptions = options;
575
+ return this;
576
+ }
577
+ async listen(socketPathOverride) {
578
+ if (socketPathOverride) {
579
+ this.options.socketPath = socketPathOverride;
580
+ }
581
+ if (this.options.socketPath) {
582
+ if (this.options.unlinkOnStart && fs.existsSync(this.options.socketPath)) {
583
+ fs.unlinkSync(this.options.socketPath);
584
+ }
585
+ await new Promise((resolve, reject) => {
586
+ this.server.listen(this.options.socketPath, (err) => {
587
+ if (err) {
588
+ reject(err);
589
+ return;
590
+ }
591
+ if (this.options.chmod !== false) {
592
+ fs.chmodSync(this.options.socketPath, this.options.chmod);
593
+ }
594
+ this.options.logger.info("Listening on", this.options.socketPath);
595
+ resolve();
596
+ });
597
+ });
598
+ return;
599
+ }
600
+ await new Promise((resolve, reject) => {
601
+ this.server.listen(this.options.port, this.options.host, (err) => {
602
+ if (err) {
603
+ reject(err);
604
+ return;
605
+ }
606
+ this.options.logger.info("Listening on", `${this.options.host}:${this.options.port}`);
607
+ resolve();
608
+ });
609
+ });
610
+ }
611
+ async close() {
612
+ await new Promise((resolve, reject) => {
613
+ this.server.close((err) => {
614
+ if (err) {
615
+ reject(err);
616
+ return;
617
+ }
618
+ resolve();
619
+ });
620
+ });
621
+ }
622
+ handleConnection(socket) {
623
+ const ctx = new MilterContext(
624
+ this.nextConnectionId++,
625
+ socket,
626
+ this.options.collectBody,
627
+ this.options.maxBodyBytes,
628
+ this.options.enforceActionStages
629
+ );
630
+ this.options.logger.info("MTA connected", ctx.id);
631
+ let closed = false;
632
+ let buffer = Buffer.alloc(0);
633
+ let pipeline = Promise.resolve();
634
+ const emitClose = () => {
635
+ if (closed) {
636
+ return;
637
+ }
638
+ closed = true;
639
+ void this.emit("close", ctx);
640
+ };
641
+ socket.on("error", (err) => {
642
+ this.options.logger.error("Socket error", ctx.id, err);
643
+ this.emitInternalError(ctx, err);
644
+ });
645
+ socket.on("close", () => {
646
+ ctx.socket = null;
647
+ emitClose();
648
+ });
649
+ socket.on("end", () => {
650
+ this.options.logger.info("MTA disconnected", ctx.id);
651
+ ctx.socket = null;
652
+ emitClose();
653
+ });
654
+ socket.on("data", (chunk) => {
655
+ buffer = Buffer.concat([buffer, chunk]);
656
+ while (buffer.length >= 5) {
657
+ const len = buffer.readUInt32BE(0);
658
+ if (len < 1) {
659
+ this.options.logger.warn("Invalid frame length", len, "connection", ctx.id);
660
+ socket.destroy();
661
+ return;
662
+ }
663
+ const total = 4 + len;
664
+ if (buffer.length < total) {
665
+ break;
666
+ }
667
+ const command = buffer.toString("ascii", 4, 5);
668
+ const data = buffer.subarray(5, total);
669
+ buffer = buffer.subarray(total);
670
+ pipeline = pipeline.then(() => this.parsePacket(ctx, command, data)).catch((err) => {
671
+ this.options.logger.error("Packet handling error", ctx.id, err);
672
+ this.emitInternalError(ctx, err);
673
+ send(ctx.socket, "t");
674
+ });
675
+ }
676
+ });
677
+ }
678
+ async parsePacket(ctx, command, data) {
679
+ switch (command) {
680
+ case SMFIC.OPTNEG: {
681
+ if (data.length < 12) {
682
+ this.options.logger.warn("Invalid OPTNEG payload length", data.length, "connection", ctx.id);
683
+ return;
684
+ }
685
+ const mtaVersion = data.readUInt32BE(0);
686
+ const mtaActions = data.readUInt32BE(4);
687
+ const mtaProtocol = data.readUInt32BE(8);
688
+ const version = Math.min(mtaVersion, SMFI_VERSION);
689
+ const actions = (this.options.actions & mtaActions) >>> 0;
690
+ const protocol = (this.computeProtocolMask() & mtaProtocol) >>> 0;
691
+ ctx.setNegotiatedActions(actions);
692
+ const payload = Buffer.alloc(12);
693
+ payload.writeUInt32BE(version, 0);
694
+ payload.writeUInt32BE(actions, 4);
695
+ payload.writeUInt32BE(protocol, 8);
696
+ send(ctx.socket, SMFIC.OPTNEG, payload);
697
+ return;
698
+ }
699
+ case SMFIC.CONNECT: {
700
+ ctx.setPhase("connect");
701
+ const info = parseConnectInfo(data);
702
+ ctx.connection = info;
703
+ const decision = await this.emit("connect", info, ctx);
704
+ this.respond(ctx, decision);
705
+ return;
706
+ }
707
+ case SMFIC.HELO: {
708
+ ctx.setPhase("helo");
709
+ const helo = readCString(data, 0).value;
710
+ ctx.helo = helo;
711
+ const decision = await this.emit("helo", helo, ctx);
712
+ this.respond(ctx, decision);
713
+ return;
714
+ }
715
+ case SMFIC.MAIL: {
716
+ ctx.setPhase("mail");
717
+ const from = readCStringList(data);
718
+ ctx.from = from;
719
+ if (this.spfOptions) {
720
+ await checkSpf(from, ctx, this.spfOptions);
721
+ }
722
+ const decision = await this.emit("mail", from, ctx);
723
+ this.respond(ctx, decision);
724
+ return;
725
+ }
726
+ case SMFIC.RCPT: {
727
+ ctx.setPhase("rcpt");
728
+ const to = readCStringList(data);
729
+ ctx.to = to;
730
+ const decision = await this.emit("rcpt", to, ctx);
731
+ this.respond(ctx, decision);
732
+ return;
733
+ }
734
+ case SMFIC.HEADER: {
735
+ ctx.setPhase("headerLine");
736
+ const p = readCString(data, 0);
737
+ const q = readCString(data, p.next);
738
+ if (p.value.length > 0) {
739
+ ctx.appendHeader(p.value, q.value);
740
+ const decision = await this.emit("headerLine", p.value, q.value, ctx);
741
+ this.respond(ctx, decision);
742
+ return;
743
+ }
744
+ this.respond(ctx, void 0);
745
+ return;
746
+ }
747
+ case SMFIC.EOH: {
748
+ ctx.setPhase("eoh");
749
+ const headers = cloneHeaders(ctx.headers);
750
+ const legacy = await this.emit("header", headers, ctx);
751
+ const modern = await this.emit("headers", headers, ctx);
752
+ this.respond(ctx, modern ?? legacy);
753
+ return;
754
+ }
755
+ case SMFIC.BODY: {
756
+ ctx.setPhase("bodyChunk");
757
+ try {
758
+ ctx.appendBody(data);
759
+ } catch (err) {
760
+ this.emitInternalError(ctx, err);
761
+ this.respond(ctx, "tempfail");
762
+ return;
763
+ }
764
+ const decision = await this.emit("bodyChunk", Buffer.from(data), ctx);
765
+ this.respond(ctx, decision);
766
+ return;
767
+ }
768
+ case SMFIC.BODYEOB: {
769
+ ctx.setPhase("bodyEnd");
770
+ const body = ctx.getBody();
771
+ if (this.dkimOptions) {
772
+ await verifyDkim(body, ctx, this.dkimOptions);
773
+ }
774
+ const decision = await this.emit("bodyEnd", body, ctx);
775
+ this.respond(ctx, decision);
776
+ return;
777
+ }
778
+ case SMFIC.DATA: {
779
+ ctx.setPhase("data");
780
+ const decision = await this.emit("data", data.toString("utf8"), ctx);
781
+ this.respond(ctx, decision);
782
+ return;
783
+ }
784
+ case SMFIC.MACRO: {
785
+ ctx.setPhase("macro");
786
+ const cmd = data.toString("ascii", 0, 1);
787
+ const map = readCStringPairs(data, 1);
788
+ ctx.macros[cmd] = map;
789
+ await this.emit("macro", cmd, map, ctx);
790
+ return;
791
+ }
792
+ case SMFIC.ABORT: {
793
+ ctx.setPhase("abort");
794
+ ctx.resetMessage();
795
+ await this.emit("abort", ctx);
796
+ return;
797
+ }
798
+ case SMFIC.QUIT: {
799
+ ctx.setPhase("close");
800
+ await this.emit("close", ctx);
801
+ if (ctx.socket) {
802
+ ctx.socket.end();
803
+ ctx.socket = null;
804
+ }
805
+ return;
806
+ }
807
+ case SMFIC.UNKNOWN: {
808
+ ctx.setPhase("unknown");
809
+ const decision = await this.emit("unknown", command, data, ctx);
810
+ this.respond(ctx, decision);
811
+ return;
812
+ }
813
+ default: {
814
+ ctx.setPhase("unknown");
815
+ this.options.logger.warn("Unknown command", command, "connection", ctx.id);
816
+ const decision = await this.emit("unknown", command, data, ctx);
817
+ this.respond(ctx, decision);
818
+ }
819
+ }
820
+ }
821
+ computeProtocolMask() {
822
+ let flags = DISABLE_ALL_CALLBACKS;
823
+ if (this.handlers.connect.size > 0) {
824
+ flags &= ~SMFIP.NOCONNECT;
825
+ }
826
+ if (this.handlers.helo.size > 0) {
827
+ flags &= ~SMFIP.NOHELO;
828
+ }
829
+ if (this.handlers.mail.size > 0 || this.spfOptions) {
830
+ flags &= ~SMFIP.NOMAIL;
831
+ }
832
+ if (this.handlers.rcpt.size > 0) {
833
+ flags &= ~SMFIP.NORCPT;
834
+ }
835
+ if (this.handlers.bodyChunk.size > 0 || this.handlers.bodyEnd.size > 0 || this.dkimOptions) {
836
+ flags &= ~SMFIP.NOBODY;
837
+ }
838
+ if (this.handlers.headerLine.size > 0 || this.handlers.headers.size > 0 || this.handlers.header.size > 0 || this.dkimOptions) {
839
+ flags &= ~SMFIP.NOHDRS;
840
+ }
841
+ if (this.handlers.headers.size > 0 || this.handlers.header.size > 0) {
842
+ flags &= ~SMFIP.NOEOH;
843
+ }
844
+ return flags >>> 0;
845
+ }
846
+ respond(ctx, decision) {
847
+ const normalized = normalizeDecision(decision, this.options.defaultDecision);
848
+ if (!normalized) {
849
+ return;
850
+ }
851
+ send(ctx.socket, normalized.command, normalized.data);
852
+ }
853
+ async emit(event, ...args) {
854
+ const callbacks = this.handlers[event];
855
+ if (!callbacks || callbacks.size === 0) {
856
+ return void 0;
857
+ }
858
+ let result;
859
+ for (const fn of callbacks) {
860
+ result = await fn(...args);
861
+ }
862
+ return result;
863
+ }
864
+ emitInternalError(ctx, err) {
865
+ const callbacks = this.handlers.error;
866
+ if (callbacks.size === 0) {
867
+ return;
868
+ }
869
+ for (const fn of callbacks) {
870
+ Promise.resolve(fn(err, ctx)).catch(() => {
871
+ this.options.logger.error("Error handler failed", err);
872
+ });
873
+ }
874
+ }
875
+ };
876
+ var Milter = class extends MilterServer {
877
+ constructor(options = {}) {
878
+ const resolved = {
879
+ actions: ACTION_ALL,
880
+ ...options
881
+ };
882
+ super(resolved);
883
+ }
884
+ };
885
+ export {
886
+ ACTION_ALL,
887
+ ConsoleLogger,
888
+ DISABLE_ALL_CALLBACKS,
889
+ Decision,
890
+ Milter,
891
+ MilterActionError,
892
+ MilterContext,
893
+ MilterServer,
894
+ SMFIA,
895
+ SMFIC,
896
+ SMFIF,
897
+ SMFIP,
898
+ SMFIR,
899
+ SMFI_VERSION,
900
+ checkSpf,
901
+ cloneHeaders,
902
+ MilterServer as default,
903
+ frame,
904
+ normalizeDecision,
905
+ parseConnectInfo,
906
+ readCString,
907
+ readCStringList,
908
+ readCStringPairs,
909
+ send,
910
+ signDkim,
911
+ spfDecision,
912
+ verifyDkim
913
+ };
914
+ //# sourceMappingURL=index.js.map