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