diff --git a/content/momentum/4/modules/index.md b/content/momentum/4/modules/index.md index e9a6bad7..6168409d 100644 --- a/content/momentum/4/modules/index.md +++ b/content/momentum/4/modules/index.md @@ -79,6 +79,7 @@ description: "Table of Contents 71 1 Introduction 71 2 ac auth Authentication Ha | [smtp_cbv](/momentum/4/modules/smtp-cbv) | SMTP Callback Verification | | [smtp_rcptto_proxy](/momentum/4/modules/smtp-rcptto-proxy) | SMTP Recipient-To Proxy | | [smtpapi](/momentum/4/modules/smtpapi) | SMTP Engagement Tracking | +| [spamassassin](/momentum/4/modules/spamassassin) | SpamAssassin Client | | [spf Modules](/momentum/4/modules/spf) | spf_macros, spf_v1, and senderid (SPF v2) | | [static-routes](/momentum/4/modules/static-routes) | Static Routes | | [suppress_spool](/momentum/4/modules/suppress-spool) | Deferred Message Spooling | diff --git a/content/momentum/4/modules/spamassassin.md b/content/momentum/4/modules/spamassassin.md new file mode 100644 index 00000000..df55a32d --- /dev/null +++ b/content/momentum/4/modules/spamassassin.md @@ -0,0 +1,255 @@ +--- +lastUpdated: "05/13/2026" +title: "spamassassin – SpamAssassin Client" +description: "The spamassassin module is a SpamAssassin client introduced in Momentum 5.0. It connects to an external spamd daemon using the SPAMC protocol, scans a message, and stores the verdict, score, threshold, and symbol list as message context variables that policy and hooks can consume." +--- + +The spamassassin module is a SpamAssassin client introduced in Momentum 5.0. +It connects to an external spamd daemon using the SPAMC protocol, scans a +message, and stores the verdict, score, threshold, and symbol list as +message context variables that policy and hooks can consume. + +> **NOTE:** This module replaces the legacy, Sieve-based `spamc` module, +> which was never a supported product feature. New deployments should use +> `spamassassin`. + +> **NOTE:** The spamassassin module does not bundle the SpamAssassin engine. +> You must install and operate `spamd` separately by following the upstream +> [Apache SpamAssassin documentation](https://spamassassin.apache.org/), +> then point this module at it via the `daemon` configuration option. + +### Configuration + +The spamassassin module is a singleton in the global scope and is declared +without an instance name. The following is a minimal configuration: + + + +``` +spamassassin { + daemon = "127.0.0.1:783" + timeout = 30 + max_size = 51200 +} +``` + +The module reads its configuration during initialization and resolves +`daemon` into a sockaddr at that time; changes to these options require +a configuration reload to take effect. + +The following configuration options are available: + +
+ +
daemon
+ +
+ +The `host:port` address of the spamd daemon to connect to. Either an IPv4 +or IPv6 literal address (or a hostname that resolves to one) may be used. +Defaults to `"localhost:783"`. + +
+ +
timeout
+ +
+ +Per-I/O timeout, in seconds, applied to each `poll` while writing the +request to spamd and while reading the response. Defaults to `30`. + +
+ +
max_size
+ +
+ +The maximum number of body bytes sent to spamd for any one message. +Messages whose body exceeds this size are truncated at this boundary +before being sent; the remainder is not scanned. Defaults to `51200` +(50 KiB). + +
+ +
+ +### Protocol + +The module uses the SPAMC `SYMBOLS` request (`SYMBOLS SPAMC/1.2`) with +a `Content-Length` header. It expects a `SPAMD/1.1` response with a zero +status code, an `EX_OK` indicator, and a `Spam:` line of the form + +``` +Spam: true ; 7.12 / 5.00 +``` + +followed by the matched rule symbols on the next line. Any other response +shape, a non-zero status, or an I/O failure causes the scan to be marked +as failed (see `spamc_status` below). + +### Message Context Variables + +After `spamc_scan` returns, the following variables are available on the +message in the `ECMESS_CTX_MESS` scope. Policy and hooks read them with +`msys.core.ec_message_context_get(msg, msys.core.ECMESS_CTX_MESS, "")` +or the equivalent C API. + +
+ +
spamc_status
+ +
+ +`"ok"` if the scan completed and a valid `SPAMD/1.1` response was parsed; +`"failed"` otherwise. When the status is `"failed"`, the underlying error +is logged to the paniclog and the remaining variables below are not set +for the current scan. + +
+ +
spamc_spam
+ +
+ +`"true"` if spamd classified the message as spam; `"false"` otherwise. +Set only when `spamc_status` is `"ok"`. + +
+ +
spamc_score
+ +
+ +The SpamAssassin score reported by spamd, formatted as `"%.2f"`. Set only +when `spamc_status` is `"ok"`. + +
+ +
spamc_thresh
+ +
+ +The required score (spam threshold) reported by spamd, formatted as +`"%.2f"`. Set only when `spamc_status` is `"ok"`. + +
+ +
spamc_symbols
+ +
+ +The comma-separated list of SpamAssassin rule symbols that matched the +message, as returned on the line following the `Spam:` line. Set only +when `spamc_status` is `"ok"`. + +
+ +
+ +### Programmatic Use + +**NOTE:** Scanning is **not** automatic. The spamassassin module does not +register any validation hooks of its own; nothing happens until something +explicitly calls the scan entry point on a message. That call is most +commonly made from a Lua policy hook that runs after the message body has +been spooled. + +#### Lua + +The scan entry point is exposed to Lua under the legacy `msys.spamc` +namespace (kept for compatibility with policy written against the older +Sieve-based `spamc` module): + +``` +msys.spamc.spamc_scan(msg) +``` + +The call is synchronous and blocking with respect to the `spamd` +exchange; invoke it from a hook that runs in an async/IO context, such as +`validate_data_spool`, where the message body has been spooled and a +blocking call is safe. After it returns, read the `spamc_*` variables off +the message context and act on them. + + + +The following scriptlet shows the canonical pattern — call the scan, +branch on `spamc_status`, then on `spamc_spam`, and use `spamc_score`, +`spamc_thresh`, and `spamc_symbols` to shape the SMTP response or +downstream policy: + +```lua +require("msys.core"); +require("msys.extended.message"); +require("msys.spamc"); + +local mod = {}; + +function mod:validate_data_spool(msg, ac, vctx) + msys.spamc.spamc_scan(msg) + + local status = msg:context_get(msys.core.ECMESS_CTX_MESS, "spamc_status") + local is_spam = msg:context_get(msys.core.ECMESS_CTX_MESS, "spamc_spam") + local score = msg:context_get(msys.core.ECMESS_CTX_MESS, "spamc_score") + local thresh = msg:context_get(msys.core.ECMESS_CTX_MESS, "spamc_thresh") + local symbols = msg:context_get(msys.core.ECMESS_CTX_MESS, "spamc_symbols") + + if status == "failed" then + -- spamd was unreachable or returned an unparseable response; + -- the underlying error is already in the paniclog. + vctx:set_code(451, "spamc error") + elseif is_spam == "true" then + vctx:set_code(550, + string.format("spam %s %s %s", score, thresh, symbols)) + else + vctx:set_code(250, + string.format("ham %s %s %s", score, thresh, symbols)) + end + + return msys.core.VALIDATE_CONT +end + +msys.registerModule("spamc_scan", mod); +``` + +Instead of setting SMTP reply code according to the spam scan status, an +integration may prefer to tag the message and let a later hook deliver it +— for example, writing the verdict into a custom header so that +downstream consumers can act on it: + +```lua +local spam_header = "X-MSYS-Spam-Status" +if status == "ok" then + if is_spam == "true" then + msg:header(spam_header, string.format("Yes, score=%s required=%s tests=%s", + score, thresh, symbols)) + else + msg:header(spam_header, string.format("No, score=%s required=%s tests=%s", + score, thresh, symbols)) + end +else + msg:header(spam_header, "failed") +end +``` + +#### C + +The Lua binding is a thin wrapper over the C entry point declared in +`modules/generic/ec_spamassassin.h`: + +``` +SPAMC_EXPORT(void) spamc_scan(ec_message *m); +``` + +The same blocking-I/O contract applies — call it from an async thread +(for example, via `sp_async_thread_pool_run`) so the main scheduler is +not held up while `spamd` processes the message. + +### Notes + +This module is a singleton and does not accept per-instance +configuration. Loading it more than once is not supported. + +For deployment, install SpamAssassin engine separately and ensure `spamd` +is reachable at the address configured under `daemon` before starting +Momentum; otherwise every scan will be recorded with +`spamc_status = "failed"`. diff --git a/content/momentum/4/modules/summary-all-modules.md b/content/momentum/4/modules/summary-all-modules.md index 93c0aaaf..ef61a88f 100644 --- a/content/momentum/4/modules/summary-all-modules.md +++ b/content/momentum/4/modules/summary-all-modules.md @@ -93,6 +93,7 @@ All modules are listed alphabetically with a brief description. Singleton module | [“smtp_auth_proxy - SMTP Authentication Proxy”](/momentum/4/modules/smtp-auth-proxy) | 4.2 | Allow edge SMTP servers to forward SMTP AUTH requests to SMTP servers |  ✓ |   |   |   | | [“smtp_cbv – SMTP Callback Verification”](/momentum/4/modules/smtp-cbv) | 4.0 | Perform SMTP Callback Verification |   |   |  ✓ |   | | [“smtp_rcptto_proxy - SMTP Recipient-To Proxy”](/momentum/4/modules/smtp-rcptto-proxy) | 4.2 | Validate a Lua recipient by doing an SMTP call-forward |   |   |   |   | +| [“spamassassin – SpamAssassin Client”](/momentum/4/modules/spamassassin) (*singleton*) | 5.0 | SpamAssassin client; scans messages via an external `spamd` daemon and stores the verdict, score, threshold, and matched symbols in message context variables. Replaces the deprecated `spamc` Sieve-based module. | | | | | | [spf_macros](/momentum/4/modules/spf) (*singleton*) | 4.0 | Generic macro service for SPF |  ✓ |   |   |   | | [spf_v1](/momentum/4/modules/spf) | 4.0 | Use Sender Policy Framework |   |   |   | [“scriptlet - Lua Policy Scripts”](/momentum/4/modules/scriptlet) | | [“static-routes - Static Routes”](/momentum/4/modules/static-routes) | 4.2 | Route traffic to a given server by IP address and port |  ✓ |   |   |   |