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/README.md CHANGED
@@ -1,154 +1,629 @@
1
- # node-milter
2
- node.js bindings for postfix milters
3
-
4
- this addon produces libmilter callbacks in node.js so that you don't have to be
5
- a C programmer to use postfix with libmilter.
6
-
7
- when its main function is called, libmilter creates a threaded daemon where each
8
- mail session has one unique thread in your program servicing it. this main is
9
- therefore sequestered away in a libuv worker to allow node.js to continue to
10
- function normally.
11
-
12
- your libmilter event callbacks are called from these threads with a context
13
- pointer to distinguish them from each other. you must return a code at the end
14
- of the event implementation, which tells postfix what to do next, continue,
15
- reject, tempfail, etc.
16
-
17
- this milter blocks itself during each of those events. it wraps the event into
18
- a localized context object and queues that, signals to libuv that it's time
19
- to do some node.js work, and goes to sleep until the pthread condition is met.
20
- thus multiple libmilter sessions may block on a single stage of the event, but,
21
- they are in their own threads so it's mostly ok.
22
-
23
- when libuv runs the async worker to service the queue on the other side, we are
24
- now back in node.js-land (a scope) and can make js callbacks, wrap event data in
25
- js objects, and so on. but it is required that the js callbacks return a
26
- decision for postfix immediately, so your js callbacks cannot be async (yet).
27
-
28
-
29
- QUICKSTART
30
-
31
- creating a milter daemon.
32
-
33
- milter.start(
34
- string smtpd_milter_description,
35
- number flags,
36
- function(env, f0, f1, f2, f3) negotiate,
37
- function(env,host,addr) connect,
38
- function(env,command) unknown,
39
- function(env,identity) helo,
40
- function(env,address) mailfrom,
41
- function(env,address) rcptto,
42
- function(env) data,
43
- function(env,name,value) header,
44
- function(env) eoh,
45
- function(env,buffer,length) segment,
46
- function(env) eom,
47
- function(env) abort,
48
- function(env) close,
1
+
2
+ ![Milter.js](res/milterjs-logo.svg)
3
+
4
+ # Milter.js
5
+
6
+ [![NPM Package](https://img.shields.io/npm/v/milter.svg?style=flat)](https://www.npmjs.com/package/milter)
7
+ [![MIT License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ **Make your mail server programmable with JavaScript.**
10
+
11
+ Your mail server already knows how to receive and deliver email. Your application knows your users, your data, and your rules. Connecting the two should be simple.
12
+
13
+ Want to check a sender against your database before accepting a message? Ask an AI model whether an email is spam? Collect DMARC reports, inspect attachments, or mark messages for delivery to a particular folder? The tools are already available in Node.js. Milter.js brings them into your mail server's processing flow.
14
+
15
+ **Milter.js lets you inspect and act on email as it arrives.** Write ordinary JavaScript handlers for the sender, recipients, headers, or body. Call an API, query a database, or use an npm package, then decide what happens next.
16
+
17
+ ```javascript
18
+ server.on("bodyEnd", async (body, ctx) => {
19
+ const spam = await classifyMessage(body, ctx.headers);
20
+
21
+ if (spam) {
22
+ ctx.addHeader("X-Spam-Flag", "YES");
23
+ }
24
+
25
+ return "accept";
26
+ });
27
+ ```
28
+
29
+ Here, `classifyMessage` is your own function. It could use a few rules, a local model, or an external service. Milter.js connects that logic to the mail server.
30
+
31
+ ## What You Can Build
32
+
33
+ - **Custom spam filters:** combine your own rules with OpenAI, Ollama, or another classifier.
34
+ - **Sender checks:** use SPF and DKIM results, domain intelligence, or your own allowlists and blocklists.
35
+ - **Mail automation:** collect reports and pass message data to your applications.
36
+ - **Message routing:** add headers that your delivery system uses to select a mailbox or folder.
37
+ - **Message rewriting:** update headers, change recipients or the envelope sender, and replace message bodies.
38
+
39
+ You control the policy. Milter.js handles the Milter protocol, connection state, and communication with the mail server.
40
+
41
+ ## How It Works
42
+
43
+ A *milter* is a program a mail server consults while processing an email. The mail server reports each requested stage of the SMTP transaction, and the filter returns a decision.
44
+
45
+ With Milter.js, those stages become JavaScript events:
46
+
47
+ | Event | What you receive |
48
+ | --- | --- |
49
+ | `connect` | Information about the sending connection |
50
+ | `helo` | The sender's HELO or EHLO name |
51
+ | `mail` | The envelope sender |
52
+ | `rcpt` | An envelope recipient |
53
+ | `headers` | The message headers |
54
+ | `bodyEnd` | The collected message body |
55
+
56
+ Register the events you need. Handlers can be synchronous or asynchronous.
57
+
58
+ Milter.js runs alongside a Milter-compatible mail server such as Postfix or Sendmail. The mail server handles SMTP and delivery; your handlers provide the filtering logic. MIME parsing and spam classification can be added through the packages or services you choose.
59
+
60
+ ## Installation
61
+
62
+ Requires **Node.js 22.19.0 or newer**.
63
+
64
+ ```bash
65
+ npm install milter
66
+ ```
67
+
68
+ ES modules:
69
+
70
+ ```javascript
71
+ import { MilterServer, Decision } from "milter";
72
+ ```
73
+
74
+ CommonJS:
75
+
76
+ ```javascript
77
+ const { MilterServer, Decision } = require("milter");
78
+ ```
79
+
80
+ TypeScript declarations are included.
81
+
82
+ ## Your First Filter
83
+
84
+ Save this as `filter.mjs`:
85
+
86
+ ```javascript
87
+ import { MilterServer } from "milter";
88
+
89
+ const server = new MilterServer({
90
+ host: "127.0.0.1",
91
+ port: 7357
92
+ });
93
+
94
+ server.on("mail", (from) => {
95
+ console.log("Envelope sender:", from[0]);
96
+ return "continue";
97
+ });
98
+
99
+ server.on("headers", (headers) => {
100
+ console.log("Subject:", headers.subject ?? []);
101
+ return "continue";
102
+ });
103
+
104
+ server.on("bodyEnd", (_body, ctx) => {
105
+ ctx.addHeader("X-Processed-By", "Milter.js");
106
+ return "accept";
107
+ });
108
+
109
+ server.on("error", (error, ctx) => {
110
+ console.error("Milter error", ctx?.id, error);
111
+ });
112
+
113
+ await server.listen();
114
+ console.log("Milter listening on 127.0.0.1:7357");
115
+ ```
116
+
117
+ Run it:
118
+
119
+ ```bash
120
+ node filter.mjs
121
+ ```
122
+
123
+ For Postfix, add these settings to `main.cf`:
124
+
125
+ ```conf
126
+ smtpd_milters = inet:127.0.0.1:7357
127
+ non_smtpd_milters = inet:127.0.0.1:7357
128
+ milter_protocol = 6
129
+ milter_default_action = accept
130
+ ```
131
+
132
+ Then reload Postfix:
133
+
134
+ ```bash
135
+ postfix reload
136
+ ```
137
+
138
+ The example adds an `X-Processed-By` header to messages that pass through the filter. `milter_default_action = accept` tells Postfix to keep accepting mail if the filter is unavailable; choose `tempfail` if mail should wait until filtering is available again.
139
+
140
+ ## Making Decisions
141
+
142
+ Return a decision from an event handler:
143
+
144
+ | Return value | Meaning |
145
+ | --- | --- |
146
+ | `"continue"` | Continue filtering at the next stage. |
147
+ | `"accept"` | Accept and stop further filtering by this milter for the current message. |
148
+ | `"reject"` | Reject permanently. |
149
+ | `"tempfail"` | Defer delivery so the sending server can retry later. |
150
+ | `"discard"` | Accept the SMTP transaction, then silently discard the message. |
151
+ | `undefined` | Use `defaultDecision`, which is `"continue"` by default. |
152
+
153
+ Use `"continue"` when later handlers still need to inspect the message. For example, accepting at the sender stage ends this milter's filtering before its body handler runs.
154
+
155
+ The `Decision` helper provides equivalent reply objects and custom SMTP replies:
156
+
157
+ ```javascript
158
+ import { Decision } from "milter";
159
+
160
+ server.on("rcpt", (to) => {
161
+ const address = (to[0] ?? "").replace(/^<|>$/g, "");
162
+
163
+ if (address === "retired@example.com") {
164
+ return Decision.replyCode(
165
+ "550",
166
+ "This mailbox no longer accepts mail",
167
+ "5.1.1"
49
168
  );
169
+ }
170
+
171
+ return Decision.continue();
172
+ });
173
+ ```
174
+
175
+ `Decision.accept()`, `reject()`, `tempfail()`, and `discard()` are also available.
176
+
177
+ ## Working with Messages
178
+
179
+ ### Check the Sender
180
+
181
+ The `mail` event receives the decoded `MAIL FROM` fields as an array. The first field contains the envelope sender.
182
+
183
+ ```javascript
184
+ server.on("mail", (from) => {
185
+ const address = (from[0] ?? "")
186
+ .replace(/^<|>$/g, "")
187
+ .toLowerCase();
188
+
189
+ if (address.endsWith("@blocked.example")) {
190
+ return "reject";
191
+ }
192
+
193
+ return "continue";
194
+ });
195
+ ```
196
+
197
+ The envelope sender is separate from the visible `From` header.
198
+
199
+ ### Read Headers
200
+
201
+ Header names are lowercase, and each value is an array because a header can appear more than once.
202
+
203
+ ```javascript
204
+ server.on("headers", (headers) => {
205
+ console.log("Subject:", headers.subject ?? []);
206
+ console.log("Content-Type:", headers["content-type"] ?? []);
207
+ });
208
+ ```
209
+
210
+ Use `headerLine(name, value, ctx)` to process headers individually.
211
+
212
+ ### Inspect the Body
213
+
214
+ By default, Milter.js collects body chunks and passes the complete body to `bodyEnd` as a `Buffer`:
215
+
216
+ ```javascript
217
+ server.on("bodyEnd", (body, ctx) => {
218
+ console.log("Body bytes:", body.length);
219
+ console.log("Headers:", ctx.headers);
220
+ return "accept";
221
+ });
222
+ ```
223
+
224
+ This is the raw message body, which can contain MIME parts and encoded content. Use a MIME parser when you need decoded text or attachments.
225
+
226
+ ### Modify a Message
227
+
228
+ Request the actions your filter needs, then make changes in `bodyEnd`:
229
+
230
+ ```javascript
231
+ import { MilterServer, SMFIF } from "milter";
232
+
233
+ const server = new MilterServer({
234
+ host: "127.0.0.1",
235
+ port: 7357,
236
+ actions:
237
+ SMFIF.ADDHDRS |
238
+ SMFIF.CHGHDRS |
239
+ SMFIF.ADDRCPT |
240
+ SMFIF.DELRCPT
241
+ });
242
+
243
+ server.on("bodyEnd", (_body, ctx) => {
244
+ ctx.addHeader("X-Filtered", "yes");
245
+ ctx.changeHeader("Subject", 1, "[Filtered] Message");
246
+ ctx.addRecipient("archive@example.com");
247
+ ctx.deleteRecipient("old-address@example.com");
248
+ return "accept";
249
+ });
250
+
251
+ await server.listen();
252
+ ```
253
+
254
+ The mail server must support and agree to each requested action. By default, `MilterServer` requests `ADDHDRS | CHGBODY | ADDRCPT`.
255
+
256
+ ### Deliver Spam to a Folder
257
+
258
+ A filter can mark a message as spam and still accept it. The delivery system can then place it in `Junk`.
259
+
260
+ The included LLM spam-filter examples remove incoming copies of their spam-result headers before writing their own results. Classified spam receives headers such as:
261
+
262
+ ```text
263
+ X-Spam-Flag: YES
264
+ X-Spam-Status: Yes
265
+ X-Spam-Score: 0.950
266
+ X-LLM-Spam-Action: move-to-spam
267
+ ```
268
+
269
+ A Dovecot Sieve rule can use that result:
270
+
271
+ ```sieve
272
+ require ["fileinto"];
273
+
274
+ if header :is "X-LLM-Spam-Action" "move-to-spam" {
275
+ fileinto "Junk";
276
+ stop;
277
+ }
278
+ ```
279
+
280
+ Install and compile the rule according to your Dovecot configuration. Ensure that routing headers are set by your trusted filter rather than accepted unchanged from incoming mail.
281
+
282
+ See the [Postfix and Dovecot routing guide](https://raw.org/software/administration/llm-spam-filter-with-postfix-dovecot/) for a complete setup.
283
+
284
+ ## SPF Checks
285
+
286
+ Enable SPF checking with `useSpf()`:
287
+
288
+ ```javascript
289
+ server.useSpf({ mta: "mx.receiver.example" });
290
+
291
+ server.on("mail", (_from, ctx) => {
292
+ const result = ctx.spf.status.result;
293
+
294
+ if (result === "fail") {
295
+ return "reject";
296
+ }
297
+
298
+ if (result === "temperror") {
299
+ return "tempfail";
300
+ }
301
+
302
+ return "continue";
303
+ });
304
+ ```
305
+
306
+ The check runs before your `mail` handler and stores its result in `ctx.spf`. It does not accept or reject mail automatically: your handler decides how to use the result.
307
+
308
+ For a configurable decision mapping:
309
+
310
+ ```javascript
311
+ import { spfDecision } from "milter";
312
+
313
+ server.on("mail", (_from, ctx) => spfDecision(ctx.spf, {
314
+ fail: "reject",
315
+ temperror: "tempfail"
316
+ }));
317
+ ```
318
+
319
+ Use either approach above. The result remains available in subsequent handlers. For explicit control over when checking runs, use the exported `checkSpf(from, ctx, options)` helper.
320
+
321
+ ## DKIM Verification and Signing
322
+
323
+ ### Verify Incoming Messages
324
+
325
+ ```javascript
326
+ server.useDkim();
327
+
328
+ server.on("bodyEnd", (_body, ctx) => {
329
+ const passed = ctx.dkim.results.some((signature) => (
330
+ signature.status.result === "pass"
331
+ ));
332
+
333
+ console.log("Has a passing DKIM signature:", passed);
334
+ return "continue";
335
+ });
336
+ ```
337
+
338
+ Verification runs before `bodyEnd` and stores its results in `ctx.dkim`. As with SPF, it leaves the decision to your handler. This example records the result without requiring every message to have a passing signature.
339
+
340
+ Body collection must remain enabled. Use `verifyDkim(body, ctx, options)` if you want to run verification explicitly.
341
+
342
+ ### Sign a Message
343
+
344
+ `signDkim()` signs a complete RFC 822 message, including its headers and body:
345
+
346
+ ```javascript
347
+ import fs from "node:fs";
348
+ import { signDkim } from "milter";
349
+
350
+ const message = fs.readFileSync("./message.eml", "utf8");
351
+
352
+ const result = await signDkim(message, {
353
+ signingDomain: "example.com",
354
+ selector: "mail",
355
+ privateKey: fs.readFileSync("./dkim-private.pem")
356
+ });
357
+
358
+ if (result.errors.length > 0) {
359
+ throw result.errors[0];
360
+ }
361
+
362
+ const signedMessage = result.signatures + message;
363
+ ```
364
+
365
+ `result.signatures` contains complete `DKIM-Signature` header lines, including their terminating line breaks. Finish changes to the message before signing; later changes to signed content can invalidate the signature.
366
+
367
+ ## Repository Examples
368
+
369
+ The repository includes examples for inspecting Milter events, collecting DMARC reports, storing reports in JSONL or a database, classifying spam, and checking domains with the domaindata API.
370
+
371
+ After cloning the repository:
372
+
373
+ ```bash
374
+ npm install
375
+ ```
376
+
377
+ Choose an example:
378
+
379
+ | Command | Example |
380
+ | --- | --- |
381
+ | `npm run example:dmarc` | Collect DMARC aggregate reports. |
382
+ | `npm run example:dmarc:db` | Store reports through a JSONL, MySQL, or PostgreSQL adapter. |
383
+ | `npm run example:chatgpt-spamfilter` | Classify spam with OpenAI. |
384
+ | `npm run example:ollama-spamfilter` | Classify spam with Ollama. |
385
+ | `npm run example:domaindata-holo-check` | Check domains with the domaindata API. |
386
+
387
+ Choose a DMARC storage adapter with environment variables:
388
+
389
+ ```bash
390
+ DMARC_DB=jsonl npm run example:dmarc:db
391
+
392
+ DMARC_DB=mysql MYSQL_HOST=127.0.0.1 MYSQL_USER=master MYSQL_PASSWORD=secret MYSQL_DB=mail npm run example:dmarc:db
393
+
394
+ DMARC_DB=postgres PGHOST=127.0.0.1 PGUSER=postgres PGPASSWORD=secret PGDATABASE=mail npm run example:dmarc:db
395
+ ```
396
+
397
+ The spam-filter examples include opinionated rules before the AI check: decoded Cyrillic or Han characters and attachments ending in `.exe`, `.bin`, or `.html` trigger an SMTP 550 rejection without calling the model. Adapt these example policies to your users. They are not default filtering rules imposed by Milter.js.
398
+
399
+ ## API Reference
400
+
401
+ ### Events
402
+
403
+ Protocol handlers receive the event payload first and `MilterContext` last.
404
+
405
+ | Event signature | Description |
406
+ | --- | --- |
407
+ | `connect(info, ctx)` | New SMTP connection. `info` contains `hostname`, `family`, and, where available, `address` and `port`. |
408
+ | `helo(helo, ctx)` | HELO or EHLO name. |
409
+ | `mail(from, ctx)` | Decoded `MAIL FROM` fields as a string array; also available as `ctx.from`. |
410
+ | `rcpt(to, ctx)` | Decoded `RCPT TO` fields as a string array; the latest is also available as `ctx.to`. |
411
+ | `headerLine(name, value, ctx)` | One message header. |
412
+ | `headers(headers, ctx)` | End of headers, with a cloned map of lowercase names to arrays of values. |
413
+ | `bodyChunk(chunk, ctx)` | One body chunk as a `Buffer`. |
414
+ | `bodyEnd(body, ctx)` | End of message, with the collected body. Use this stage for message mutations. |
415
+ | `data(raw, ctx)` | Milter DATA payload decoded as UTF-8 text. |
416
+ | `macro(command, values, ctx)` | MTA macros as a command byte and a string-to-string map. |
417
+ | `abort(ctx)` | Current message aborted; message-specific context state is reset. |
418
+ | `close(ctx)` | Milter session closed. |
419
+ | `unknown(command, data, ctx)` | Unsupported or unknown protocol command and its raw payload. |
420
+ | `error(error, ctx)` | Server, socket, parser, handler, or body-limit error. `ctx` may be undefined. |
421
+
422
+ `header` remains an alias for `headers`; use `headers` in new filters.
423
+
424
+ Milter.js requests registered protocol callbacks during negotiation, so a filter does not need to subscribe to every stage.
425
+
426
+ ### Multiple Handlers and Replies
427
+
428
+ Handlers for the same event run in registration order. **The last handler's return value determines the protocol decision.** An earlier handler returning `"reject"` does not make it the final decision if another handler follows it. Keep the decision in one handler per event when combining checks.
429
+
430
+ In addition to decision strings, handlers can return reply objects such as `{ command, data? }`. Returning `null` deliberately sends no protocol response; it is an advanced option, not an alias for `"continue"`.
431
+
432
+ Context methods such as `ctx.reject()` write a response immediately. Do not combine an immediate context decision with a returned decision for the same event.
433
+
434
+ ### MilterContext
435
+
436
+ Each connection has one context object. Message-related fields describe the current message.
437
+
438
+ | Property or method | Description |
439
+ | --- | --- |
440
+ | `id` | Monotonically increasing connection identifier. |
441
+ | `socket` | Node.js socket, or `null` after disconnect. |
442
+ | `headers` | Lowercase header map with arrays of values. |
443
+ | `macros` | MTA macros grouped by protocol command. |
444
+ | `from` | Current envelope sender fields. |
445
+ | `to` | Most recent envelope recipient fields. |
446
+ | `spf` | SPF result when SPF checking is enabled. |
447
+ | `dkim` | DKIM results when verification is enabled. |
448
+ | `getPhase()` | Current protocol phase. |
449
+ | `can(action)` | Whether the MTA negotiated an action. |
450
+ | `getBody()` | Collected body as a `Buffer`. |
451
+
452
+ Immediate response methods:
453
+
454
+ ```javascript
455
+ ctx.continue();
456
+ ctx.accept();
457
+ ctx.reject();
458
+ ctx.discard();
459
+ ctx.tempfail();
460
+ ctx.replyCode(code, message, enhancedCode);
461
+ ctx.progress();
462
+ ```
463
+
464
+ Message mutation methods:
465
+
466
+ ```javascript
467
+ ctx.addHeader(name, value);
468
+ ctx.insertHeader(index, name, value);
469
+ ctx.changeHeader(name, index, value);
470
+ ctx.addRecipient(address);
471
+ ctx.deleteRecipient(address);
472
+ ctx.replaceBody(chunk);
473
+ ctx.quarantine(reason);
474
+ ctx.setSender(sender);
475
+ ```
476
+
477
+ The `value` argument of `changeHeader()` and the `reason` argument of `quarantine()` are optional.
478
+
479
+ Mutation helpers check negotiated capabilities and, by default, the current protocol phase. Invalid calls throw `MilterActionError` with code `E_MILTER_ACTION_CAPABILITY` or `E_MILTER_ACTION_STAGE`.
480
+
481
+ `enforceActionStages: false` disables stage checks. Capability checks remain active.
482
+
483
+ ### Server Options
484
+
485
+ | Option | Default | Description |
486
+ | --- | --- | --- |
487
+ | `socketPath` | — | UNIX socket path. Required if `port` is absent. |
488
+ | `host` | `127.0.0.1` | TCP bind address. |
489
+ | `port` | — | TCP port. Required if `socketPath` is absent. |
490
+ | `actions` | `ADDHDRS \| CHGBODY \| ADDRCPT` | Requested mutation capabilities. |
491
+ | `unlinkOnStart` | `true` | Remove an existing UNIX socket before listening. |
492
+ | `chmod` | `0o777` | UNIX socket permissions; `false` leaves permissions unchanged. |
493
+ | `collectBody` | `true` | Collect body chunks for `bodyEnd`. |
494
+ | `maxBodyBytes` | `33554432` | Maximum collected body size: 32 MiB. |
495
+ | `defaultDecision` | `"continue"` | Decision when a handler returns `undefined`. |
496
+ | `enforceActionStages` | `true` | Check that mutations happen at a valid stage. |
497
+ | `logger` | `ConsoleLogger` | Logger implementing `debug`, `info`, `warn`, and `error`. |
498
+
499
+ For streaming filters, disable collection and process chunks as they arrive:
500
+
501
+ ```javascript
502
+ const server = new MilterServer({
503
+ host: "127.0.0.1",
504
+ port: 7357,
505
+ collectBody: false
506
+ });
507
+
508
+ server.on("bodyChunk", (chunk) => {
509
+ console.log("Received body chunk:", chunk.length);
510
+ return "continue";
511
+ });
512
+ ```
513
+
514
+ With `collectBody: false`, the buffer passed to `bodyEnd` is empty. DKIM verification requires body collection.
515
+
516
+ ### Server Methods
517
+
518
+ | Method | Description |
519
+ | --- | --- |
520
+ | `on(event, handler)` | Register a handler; returns the server. |
521
+ | `once(event, handler)` | Register a handler that removes itself before its first invocation. |
522
+ | `off(event, handler)` | Remove a handler; returns the server. |
523
+ | `listen(socketPathOverride?)` | Start listening; an optional argument overrides the UNIX socket path. |
524
+ | `close()` | Stop accepting connections; resolves when the underlying Node.js server closes. |
50
525
 
526
+ ### Compatibility Class
51
527
 
52
- other controls are available when creating a milter.
528
+ The `Milter` compatibility class enables `ACTION_ALL` unless you provide `actions` explicitly:
53
529
 
54
- completing a callback.
530
+ ```javascript
531
+ import { Milter } from "milter";
55
532
 
56
- connect = function (env, host, addr) {
57
- /* ... */
58
- env.done(decision);
59
- }
533
+ const server = new Milter({
534
+ socketPath: "/run/example-milter.sock"
535
+ });
536
+ ```
60
537
 
538
+ Prefer `MilterServer` with the specific actions your filter needs for new code.
61
539
 
62
- the allowed filter decisions for all callbacks except negotiate.
63
-
64
- milter.SMFIS_CONTINUE
65
- milter.SMFIS_REJECT
66
- milter.SMFIS_DISCARD
67
- milter.SMFIS_ACCEPT
68
- milter.SMFIS_TEMPFAIL
69
- milter.SMFIS_NOREPLY
70
- milter.SMFIS_SKIP
540
+ ### Protocol Constants
71
541
 
542
+ Constants are available from `milter` and `milter/constants`:
72
543
 
73
- negotiate shall return one of these decisions instead. see env.negotiate() for
74
- use of the CONTINUE return code.
544
+ ```javascript
545
+ import {
546
+ ACTION_ALL,
547
+ SMFI_VERSION,
548
+ SMFIA,
549
+ SMFIC,
550
+ SMFIF,
551
+ SMFIP,
552
+ SMFIR
553
+ } from "milter/constants";
554
+ ```
75
555
 
76
- milter.SMFIS_ALL_OPTS
77
- milter.SMFIS_CONTINUE
78
- milter.SMFIS_REJECT
556
+ | Constant | Meaning |
557
+ | --- | --- |
558
+ | `SMFIA` | Connection address families. |
559
+ | `SMFIC` | Commands sent by the mail server. |
560
+ | `SMFIR` | Replies sent by the filter. |
561
+ | `SMFIF` | Message mutation capabilities. |
562
+ | `SMFIP` | Callback suppression flags. |
563
+ | `ACTION_ALL` | All supported mutation capabilities combined. |
564
+ | `SMFI_VERSION` | Supported protocol version: 6. |
79
565
 
566
+ Low-level `frame()` and `send()` helpers are also exported for protocol tooling and tests.
80
567
 
81
- other envelope methods.
82
- access to message modifiers is allowed during the EOM event with these methods.
568
+ ## UNIX Sockets and Sendmail
83
569
 
84
- env.addheader(name, value)
85
- env.chgheader(name, refcount, value)
86
- env.insheader(index, name, value)
87
- env.replacebody(newbody)
88
- env.addrcpt(recipient)
89
- env.addrcpt_par(recipient, args)
90
- env.delrcpt(recipient)
91
- env.chgfrom(envfrom, args)
92
- env.quarantine(reason)
93
- env.progress()
570
+ For a local mail server, you can use a UNIX socket instead of TCP:
94
571
 
572
+ ```javascript
573
+ const server = new MilterServer({
574
+ socketPath: "/run/example-milter.sock"
575
+ });
95
576
 
96
- changing the symbol list is allowed during negotiate.
577
+ await server.listen();
578
+ ```
97
579
 
98
- env.setsymlist(stage, macrolist)
580
+ Ensure the mail server can access the socket and its parent directories. If Postfix runs chrooted, the socket must be visible inside that chroot. The TCP configuration in the quick start avoids UNIX socket path differences.
99
581
 
582
+ For Sendmail, reference the same socket in `sendmail.mc`:
100
583
 
101
- because pointers cannot be wrapped in node.js addons, an additional method in
102
- the addon implementation that has no analog in libmilter exists to facilitate
103
- changing these settings so that the pointers made available to the xxfi_negotiate
104
- callback can be changed after node.js returns control to libmilter. call this
105
- method BEFORE calling env.done(), also, the values changed by this function are
106
- ignored unless env.done() is called with SMFIS_CONTINUE.
584
+ ```m4
585
+ INPUT_MAIL_FILTER(`example', `S=local:/run/example-milter.sock, F=T, T=S:4m;R:4m;E:10m')dnl
586
+ ```
107
587
 
108
- env.negotiate(f0, f1, f2, f3)
588
+ Rebuild and reload the Sendmail configuration using your operating system's procedure.
109
589
 
590
+ ## Development
110
591
 
111
- retrieving a macro is allowed during any event. will return an empty string if
112
- the macro doesn't have a value.
592
+ Install dependencies and build:
113
593
 
114
- env.getsymval(symname)
594
+ ```bash
595
+ npm install
596
+ npm run build
597
+ ```
115
598
 
599
+ The build generates ESM, CommonJS, and TypeScript declaration files in `dist`.
116
600
 
117
- changing the server's smtp reply is allowed during any event other than connect.
118
- i believe this should also exclude negotiate, so i enforce that as well.
601
+ Run linting, type checks, the build, and tests:
119
602
 
120
- env.setreply(rcode, xcode, message)
121
- env.setmlreply(rcode, xcode, lines)
603
+ ```bash
604
+ npm run check
605
+ ```
122
606
 
607
+ Run only the tests:
123
608
 
124
- ANOMALIES
609
+ ```bash
610
+ npm test
611
+ ```
125
612
 
126
- libmilter uses globals and is not thread-safe. you cannot use multiprocessing
127
- features in node with this addon.
613
+ ### Version Compatibility
128
614
 
615
+ This README describes the API in this repository. When using a published npm release, refer to the documentation for that version: event names, defaults, and mutation helpers can differ between releases.
129
616
 
130
- the milter will register in postfix with the name "node-bindings". the name
131
- "node-milter" is intended to give the project a sensible npm identity.
617
+ ## Sponsor
132
618
 
133
- sources implicitly depend on pthreads yet don't use their #include files by name.
134
- - libmilter explicitly uses pthreads, and libuv is implicitly using them. the
135
- code compiles because of this happy coincidence.
136
- - you can't use this on any non-POSIX platforms yet. but then again, you can't
137
- use milters there, either.
138
- - this cannot be worked around by switching the relevant code over to libuv
139
- without also rewriting all of libmilter. i looked. it's not worth it.
619
+ Development and maintenance of **Milter.js** are supported by [Maail](https://maail.co/).
140
620
 
141
- node.js buffers are created during the message data event (the client has already
142
- send command "DATA", and now a chunk of the message data has arrived) using the
143
- i-suspect-is-soon-to-be-deprecated method Buffer::Use(), which should have been
144
- named Buffer::New() like the others, by passing (const unsigned char *) as the
145
- expected (char *) which is probably stupid. it is unclear to me why there is no
146
- Buffer::New() that simply accepts (void *) like all the real POSIX C buffer-
147
- manipulating functions. whatever.
621
+ <a href="https://maail.co/"><img src="res/maail-logo.svg" alt="Maail" width="80"></a>
148
622
 
623
+ Maail is a privacy-first European email intelligence API. Its support helps keep Milter.js maintained and freely available.
149
624
 
150
- ERRATA
625
+ ## Copyright and Licensing
151
626
 
152
- this link is gold.
627
+ Copyright (c) 2026, [Robert Eisele](https://raw.org/)
153
628
 
154
- https://strongloop.com/strongblog/node-js-v0-12-c-apis-breaking/
629
+ Licensed under the MIT license.