]> git.ipfire.org Git - thirdparty/rspamd.git/log
thirdparty/rspamd.git
12 days ago[Test] WebUI: widen throughput counter E2E range 6146/head
Alexander Moisseev [Sat, 25 Jul 2026 10:12:30 +0000 (13:12 +0300)] 
[Test] WebUI: widen throughput counter E2E range

#rrd-total-value is an integral of the message rate (truncated per
series and summed over the 6 action series), an approximation that can
overshoot the messages scanned near row boundaries. CI observed the
total reach base+4 while only +2 were actually scanned (the exact
"Scanned" counter test still passed), exceeding the previous +3 cap.
Allow +4.

13 days ago[Test] WebUI: fix race in scan reconnect E2E test
Alexander Moisseev [Fri, 24 Jul 2026 11:19:29 +0000 (14:19 +0300)] 
[Test] WebUI: fix race in scan reconnect E2E test

After login, #navBar loses d-none from the /auth "complete"
callback, which can fire before Bootstrap has finished the async
fade-out of #connectDialog. The lingering .show then intercepts the
immediate nav click, exhausting the 30s budget on WebKit under CI
load. Wait for the dialog to be fully hidden before interacting with
the nav, mirroring the explicit-wait pattern from 735609c1.

13 days agoMerge branch 'master' into userinfo
Alexander Moisseev [Fri, 24 Jul 2026 10:24:06 +0000 (13:24 +0300)] 
Merge branch 'master' into userinfo

Resolve conflict in src/libserver/maps/map.c: take master's zstd
decompression refactor (rspamd_zstd_decompress_bounded) and re-apply
credential redaction (uri -> uri_log) in the new error paths.

13 days ago[Fix] lua_content/pdf: emit newlines for Td/TD line breaks
Vsevolod Stakhov [Fri, 24 Jul 2026 09:30:43 +0000 (10:30 +0100)] 
[Fix] lua_content/pdf: emit newlines for Td/TD line breaks

The Td/TD text positioning operators never produced a newline: the
grammar did not capture the operator name, so the handler compared a
numeric operand against the string 'Td' (never true) and additionally
read the wrong operand. As a result consecutive text lines were
concatenated, e.g. "...June 1, 2026." + "You may obtain..." became
"2026.You", which the URL parser mis-detected as http://2026.you and
raised false positives.

Capture the Td/TD operator, split the graphics ternary operators (d/m/l)
into their own dropped branch, and render a newline when the vertical
displacement (ty) is non-zero. Adds a regression unit test.

2 weeks ago[Fix] html: correct image style dimension parsing
Vsevolod Stakhov [Thu, 23 Jul 2026 13:31:34 +0000 (14:31 +0100)] 
[Fix] html: correct image style dimension parsing

The style dimension parser found a digit but called rspamd_strtoul from
the start of the substring, ignored its return value, and assigned the
result. For substrings longer than 22 chars rspamd_strtoul returns
before writing *value, leaving img->height/width set from an
uninitialized variable; shorter CSS parsed to zero because the substring
began with ':' or whitespace.

Parse the digit run starting at the located digit, bound the length to
that run, and only assign on a successful parse.

2 weeks ago[Feature] lua_http: forbid_local option to block requests to local networks
Vsevolod Stakhov [Thu, 23 Jul 2026 13:13:18 +0000 (14:13 +0100)] 
[Feature] lua_http: forbid_local option to block requests to local networks

rspamd_http.request() connects to whatever the URL or its DNS resolution
yields, including loopback, link-local and RFC1918 destinations. Add a
forbid_local option enforced at the single connection chokepoint (after
keepalive, upstream, numeric and DNS address selection) using
rspamd_ip_is_local_cfg, i.e. the address class check plus the
configurable local_addrs radix.

Enable it by default in url_redirector: message URLs and their redirect
targets are attacker controlled, and hop limits alone do not stop a
crafted redirect from probing cloud metadata endpoints or internal
services. Operators resolving internal redirectors can set
forbid_local = false.

2 weeks ago[Fix] lua_http: deliver errors to coroutine callers
Vsevolod Stakhov [Thu, 23 Jul 2026 13:13:06 +0000 (14:13 +0100)] 
[Fix] lua_http: deliver errors to coroutine callers

DNS-stage failures (resolve error, no records, connection refused) went
through lua_http_push_error, which unconditionally invoked the callback
reference; coroutine-style requests have no callback (cbref == -1), so
the error was swallowed and the yielded thread was never resumed. Resume
the thread with (err, nil) instead, mirroring the connection error path.

Synchronous failures (unparseable URL, blocked session, immediate
connection or DNS-send failure) returned a bare false, which coroutine
callers read as err = false, response = nil - a crash on response.code
for every caller checking 'if not err'. Return (err, nil) there as well.

2 weeks ago[Fix] http: enforce read deadline when data is pending at timer expiry
Vsevolod Stakhov [Thu, 23 Jul 2026 12:57:19 +0000 (13:57 +0100)] 
[Fix] http: enforce read deadline when data is pending at timer expiry

The read timer is a one-shot ev_timer. When it expired while data was
already waiting in the socket, the handler drained and parsed that data;
if the message remained incomplete, the connection continued with only
the I/O watcher active and no deadline at all. Report a 408 instead,
unless the drained bytes completed the message (finished or reset into
the keepalive pool).

Add a deterministic lifecycle test driving the expiry window via the
fake clock plus a max-priority ev_check writer, covering the incomplete,
salvaged and no-data cases.

2 weeks ago[Fix] lua_http: bound HTTP responses by default
Vsevolod Stakhov [Thu, 23 Jul 2026 12:42:40 +0000 (13:42 +0100)] 
[Fix] lua_http: bound HTTP responses by default

Lua HTTP client responses were unlimited unless a plugin explicitly
passed max_size, and a negative max_size was converted straight to an
unsigned gsize, silently producing an effectively unlimited cap.

Add a max_lua_http_response option (256Mb by default, 0 disables)
applied as the reply body limit whenever a request does not specify
max_size; an explicit max_size of 0 still disables the limit per
request. Reject negative max_size values with a logged error instead
of letting them wrap.

2 weeks ago[Fix] zstd: share one bounded decompression helper across HTTP, proxy and maps
Vsevolod Stakhov [Thu, 23 Jul 2026 11:55:31 +0000 (12:55 +0100)] 
[Fix] zstd: share one bounded decompression helper across HTTP, proxy and maps

The streaming zstd decompression loop was copy-pasted in seven places
with inconsistent bounding. The proxy had no output ceiling at all: on
request/response bodies (proxy_request_decompress) and on the v3
multipart result and body parts, a zstd bomb from a client or backend
could balloon memory without limit. The task and v3 protocol paths were
bounded but enforced the limit only when the buffer filled, letting the
output overshoot max_message up to twofold, and never validated the
final length. The map shm-cache, file-cache and static paths grew their
buffers with no ceiling.

Add rspamd_zstd_decompress_bounded that validates the frame-advertised
size before any allocation, caps buffer growth strictly at the limit,
drains pending decoder output after the input is consumed and frees the
buffer on every error path. Convert all seven sites to it: max_message
bounds the task, protocol v3 and proxy paths; max_map_size bounds the
map cache and static paths (the remote HTTP map path keeps its per-map
override). Cover the helper with unit tests including advertised-size
and unknown-size zstd bombs.

2 weeks ago[Fix] map: bound remote HTTP map sizes, compressed and decompressed
Vsevolod Stakhov [Thu, 23 Jul 2026 11:39:06 +0000 (12:39 +0100)] 
[Fix] map: bound remote HTTP map sizes, compressed and decompressed

Remote HTTP maps had no size ceiling: the map client connections never
called rspamd_http_connection_set_max_size, so a map server could feed
an arbitrarily large body that is fully retained in shared memory, and
the zstd path allocated the output buffer straight from the
frame-advertised decompressed size, then doubled it without limit, so a
small zstd bomb could balloon memory indefinitely.

Add a max_map_size option (256Mb by default, 0 disables) applied as the
HTTP body limit on both map client connections, reject frames
advertising a larger decompressed size upfront and enforce the same
ceiling in the streaming growth loop. The limit can be overridden per
map with max_size in the maps { } block, like the existing timeout and
keepalive knobs.

2 weeks ago[Fix] http: bound request bodies on controller, proxy and control sockets
Vsevolod Stakhov [Thu, 23 Jul 2026 11:26:25 +0000 (12:26 +0100)] 
[Fix] http: bound request bodies on controller, proxy and control sockets

The normal worker caps HTTP bodies at cfg->max_message, but the
controller router, the proxy client connection and the main control
socket never called rspamd_http_connection_set_max_size. A declared
Content-Length caused an unbounded preallocation and chunked input
could grow until the connection ended, all before routing or
authentication.

Add a max_size knob to the HTTP router applied to every accepted
connection, and set cfg->max_message as the body cap on the
controller, proxy and control sockets. Oversized requests are now
rejected with 413 (controller) or a closed connection (proxy).

2 weeks ago[Fix] task: bound zstd decompression by max_message and plug error-path leak
Vsevolod Stakhov [Thu, 23 Jul 2026 11:18:42 +0000 (12:18 +0100)] 
[Fix] task: bound zstd decompression by max_message and plug error-path leak

The legacy compressed-message path in rspamd_task_load_message allocated
the output buffer straight from the frame-advertised decompressed size
and doubled it without any ceiling, so a zstd bomb within the HTTP
max_size limit could balloon memory far beyond max_message. The
decompression-error return also leaked the output buffer, which was
registered with the task pool only on success.

Reject frames advertising more than max_message upfront, enforce
max_message in the buffer growth loop, and free the buffer on all error
returns. Apply the same upfront clamp to the v3 multipart path and honor
max_message == 0 as unlimited there, matching the HTTP layer convention.

2 weeks ago[Fix] html: bound attributes per tag and per task
Vsevolod Stakhov [Thu, 23 Jul 2026 10:29:42 +0000 (11:29 +0100)] 
[Fix] html: bound attributes per tag and per task

2 weeks ago[Fix] url: enforce max_urls at the central insertion boundary
Vsevolod Stakhov [Thu, 23 Jul 2026 10:22:52 +0000 (11:22 +0100)] 
[Fix] url: enforce max_urls at the central insertion boundary

All URL sources (HTML links, query URLs, images, displayed URLs,
subject URLs, Lua-injected URLs) now respect the configured max_urls
limit via rspamd_url_set_add_or_increase/add_or_return, not just
plain-text callbacks. Text pre-checks fixed from > to >= so the cap
is exact. Also remove a duplicate part-url add for newly-seen text
URLs introduced in 80f7567fb.

2 weeks ago[Fix] message: make newline metadata budget task-global, fix flag collision
Vsevolod Stakhov [Thu, 23 Jul 2026 10:10:56 +0000 (11:10 +0100)] 
[Fix] message: make newline metadata budget task-global, fix flag collision

2 weeks ago[Fix] html: cap synthetic tags and fix balance loop
Vsevolod Stakhov [Thu, 23 Jul 2026 10:01:21 +0000 (11:01 +0100)] 
[Fix] html: cap synthetic tags and fix balance loop

2 weeks ago[Fix] 7zip: bound folder count and guarantee parser progress
Vsevolod Stakhov [Thu, 23 Jul 2026 09:52:50 +0000 (10:52 +0100)] 
[Fix] 7zip: bound folder count and guarantee parser progress

The folder-count limit was applied only in the internal-folders branch,
so the external branch preserved an unchecked 64-bit count. The later
kCodersUnPackSize loop's error branch then logged once per folder
without consuming input, allowing a non-progressing CPU/log loop that
the synchronous parser cannot be interrupted from by the task timeout.

- Apply the > 8192 limit immediately after reading NumFolders, before
  the external/internal split.
- Reject external-folder metadata instead of accepting it with
  inconsistent downstream state.
- Replace the non-progressing error branch with an early return so every
  parsing-loop iteration advances input or bails out.
- Bounds-check *p (the External flag) after the varint read.
- Make the uint64 -> unsigned int folder-count narrowing explicit.
- Move both folder_nstreams tables from g_alloca/g_malloc to the task
  mempool to reduce stack risk and drop manual frees.

2 weeks ago[Fix] content_type: bound parameters per header
Vsevolod Stakhov [Thu, 23 Jul 2026 09:32:23 +0000 (10:32 +0100)] 
[Fix] content_type: bound parameters per header

A single Content-Type or Content-Disposition field can carry an
unbounded number of parameters. Each one materialises a separate pool
object plus a hash/list entry, so a message bounded at max_message (50
MiB by default) could amplify into millions of allocations from one
folded header (~20x for Content-Type, more for Content-Disposition
which copies name and value per param). The existing header-count cap
limits header *fields*, not parameters within a field, so it does not
help here.

Cap the number of parameters materialised per header at 1024 (real
messages use a handful). Content-Type flags the truncation as BROKEN;
Content-Disposition has no flags field and simply stops. The counter
counts parameters, not distinct names, so a same-name flood is bounded
too.

Also fix rspamd_cmp_pieces: it subtracted two unsigned RFC 2231
continuation ids and truncated to int32_t, which overflows when the
ids differ by more than INT32_MAX and reverses the reconstruction
order. Use an explicit comparison instead.

Adds a C++ unit suite covering the parameter caps and the ordering fix.

2 weeks ago[Fix] message: bound per-part newline metadata
Vsevolod Stakhov [Wed, 22 Jul 2026 21:38:08 +0000 (22:38 +0100)] 
[Fix] message: bound per-part newline metadata

Newline normalization recorded every newline as a pointer array entry
plus a mempool exception object and a GList node, then sorted the
whole exceptions list: a newline-dense body turned a few MiB of input
into hundreds of MiB of heap and millions of allocations. Cap the
recorded newline positions at 100k per part (text normalization and
line counters are unaffected), set a part flag on truncation and
expose it as newlines_truncated in textpart:get_stats().

2 weeks ago[Fix] Redact credentials from map error logs
Alexander Moisseev [Wed, 22 Jul 2026 16:11:53 +0000 (19:11 +0300)] 
[Fix] Redact credentials from map error logs

Map URIs may carry HTTP basic-auth userinfo ("user:pass@host").
It was logged verbatim in CRITICAL (msg_err_*) messages, which feed
the /errors ring buffer exposed via the controller, leaking credentials
to anyone reading it. Add a redacted uri_log copy (userinfo replaced
with "***@") used in error logs; the real uri is kept intact as it
serves as the "maps { <uri> {} }" configuration key and carries the
userinfo needed for the Authorization header.

2 weeks ago[Fix] Bound alt-part linking and fasttext langdet cost
Vsevolod Stakhov [Wed, 22 Jul 2026 13:37:46 +0000 (14:37 +0100)] 
[Fix] Bound alt-part linking and fasttext langdet cost

Alternative text part linking used one subtree scan per text part, so
a crafted multipart/alternative with thousands of sibling text parts
made linking quadratic (~100M sibling visits at the MIME part cap).
All searches of a task now share a single visit budget; when it is
exhausted the remaining parts are left unlinked with a warning.

Fasttext language detection fed up to 1M words into the model with no
bound on word length or on the resulting token count. As the UTF
tokenizer intentionally does not enforce max_word_len, a crafted body
made of huge unspaced words expanded into 3-4 subword ngram ids per
character: hundreds of megabytes of heap and matching vector-add work
in predict() from one message. Skip overlong words, cap the total
tokens fed to the model, and refuse OOV subword expansion in the shim
for words longer than any real dictionary entry.

2 weeks ago[Fix] message: link plain text parts to their HTML alternative
Vsevolod Stakhov [Wed, 22 Jul 2026 13:37:02 +0000 (14:37 +0100)] 
[Fix] message: link plain text parts to their HTML alternative

The alternative part search compared the raw IS_TEXT_PART_HTML flag
value (0 or 4) with the boolean want_html argument (0 or 1), so a
text/plain part never matched its text/html sibling: only the
html-to-plain direction of alt_text_part was ever populated.

2 weeks ago[Fix] html: eliminate DOM recursion to survive deeply nested messages
Vsevolod Stakhov [Wed, 22 Jul 2026 12:44:09 +0000 (13:44 +0100)] 
[Fix] html: eliminate DOM recursion to survive deeply nested messages

Convert html_append_tag_content, traverse_block_tags and
html_debug_structure from native-stack recursion to explicit heap
stacks, and maintain html_tag::depth incrementally instead of walking
the parent chain per tag. With max_tags = 8192 a message can legally
nest ~8190 elements, which overflowed 512KB worker-thread stacks.

Add a regression test parsing 8190 nested divs.

2 weeks ago[Fix] images: avoid quadratic Content-ID linking
Vsevolod Stakhov [Wed, 22 Jul 2026 12:20:05 +0000 (13:20 +0100)] 
[Fix] images: avoid quadratic Content-ID linking

2 weeks ago[Fix] archives: bound metadata resource usage
Vsevolod Stakhov [Wed, 22 Jul 2026 12:07:02 +0000 (13:07 +0100)] 
[Fix] archives: bound metadata resource usage

2 weeks ago[Fix] mime_headers: bound parser resource usage
Vsevolod Stakhov [Wed, 22 Jul 2026 11:35:19 +0000 (12:35 +0100)] 
[Fix] mime_headers: bound parser resource usage

2 weeks ago[Fix] mime_parser: bound parser resource usage
Vsevolod Stakhov [Wed, 22 Jul 2026 11:20:32 +0000 (12:20 +0100)] 
[Fix] mime_parser: bound parser resource usage

2 weeks agoMerge pull request #6124 from moisseev/jquery
Vsevolod Stakhov [Wed, 22 Jul 2026 09:10:26 +0000 (10:10 +0100)] 
Merge pull request #6124 from moisseev/jquery

[Rework] Remove jQuery from the WebUI

2 weeks agoRevert "[Test] Fix race in history reset e2e test"
Vsevolod Stakhov [Wed, 22 Jul 2026 09:09:55 +0000 (10:09 +0100)] 
Revert "[Test] Fix race in history reset e2e test"

This reverts commit 8d934a9790956fd21b89532e5ecfe2e00115285c.

2 weeks ago[Minor] Update version to 4.1.3
Vsevolod Stakhov [Tue, 21 Jul 2026 17:54:26 +0000 (18:54 +0100)] 
[Minor] Update version to 4.1.3

2 weeks agoRelease 4.1.2 4.1.2
Vsevolod Stakhov [Tue, 21 Jul 2026 17:53:03 +0000 (18:53 +0100)] 
Release 4.1.2

2 weeks agoMerge pull request #6142 from WRMSRwasTaken/master
Vsevolod Stakhov [Tue, 21 Jul 2026 16:14:26 +0000 (17:14 +0100)] 
Merge pull request #6142 from WRMSRwasTaken/master

Fix synchronous context_augment firing two GPT requests for one model

2 weeks agoMerge pull request #6144 from rspamd/vstakhov-settings-policy
Vsevolod Stakhov [Tue, 21 Jul 2026 16:03:45 +0000 (17:03 +0100)] 
Merge pull request #6144 from rspamd/vstakhov-settings-policy

[Feature] Settings: policy option for additive symbols_enabled + strict explicit_enable gating

2 weeks ago[CritFix] mime_parser: bound message/rfc822 recursion depth
Vsevolod Stakhov [Tue, 21 Jul 2026 16:01:25 +0000 (17:01 +0100)] 
[CritFix] mime_parser: bound message/rfc822 recursion depth

A chain of bare "Content-Type: message/rfc822" wrappers recursed through
rspamd_mime_parse_message's own MESSAGE branch without ever tripping the
max_nested (64) limit. When descending into an embedded message the
parser allocates a fresh runtime (nst) and seeds nst->nesting from the
parent's value *before* incrementing, then increments the parent runtime
st which is not the one carried into the recursion. The multipart branch
and rspamd_mime_process_multipart_node bump the descended-through runtime
(and push it onto the stack so the recursive call's cleanup pops and
decrements it), but the message branch did neither, so nst->nesting
stayed at its initial value at every level and the entry guard never
fired.

The result was recursion bounded only by message size (~34 bytes per
level): deeply nested messages exhaust the worker stack (remote,
unauthenticated DoS via HTTP submission or SMTP/milter delivery), and the
per-level boundary pre-scan made even non-crashing depths quadratic in
CPU. The earlier S/MIME fix (f6536945) only covered the pkcs7-mime
re-entry path and did not address this.

Push npart onto nst->stack and bump nst->nesting in the message branch,
mirroring the multipart branch; the recursive call's existing cleanup
balances both. Bounding the depth to max_nested also caps the number of
preprocess passes, eliminating the quadratic-CPU vector.

Add a Lua unit regression test that feeds a 500-level message/rfc822
chain and asserts the parsed part count stays capped near max_nested.

Reported by @gronke.

2 weeks ago[Feature] Configdump: show symcache flags in --symbol-details 6144/head
Vsevolod Stakhov [Mon, 20 Jul 2026 10:15:30 +0000 (11:15 +0100)] 
[Feature] Configdump: show symcache flags in --symbol-details

Add a `flags` array (fine, empty, explicit_disable, explicit_enable,
ignore_passthrough, nostat, idempotent, mime, trivial, skip, composite,
ghost, coro) to the per-symbol details, so it is possible to check
whether a symbol requires explicit enabling; structural types are
already covered by the `type` field.

2 weeks ago[Test] Settings: cover explicit_enable gating and implicit_allow policy
Vsevolod Stakhov [Mon, 20 Jul 2026 10:02:02 +0000 (11:02 +0100)] 
[Test] Settings: cover explicit_enable gating and implicit_allow policy

New functional test cases:
  * explicit_enable symbols do not run without settings, under an
    implicit-allow settings id, or with raw settings that do not list
    them
  * whitelist settings and task:enable_symbol() unlock them
  * policy = implicit_allow keeps all symbols enabled while unlocking
    listed explicit_enable symbols and disabling listed symbols, both
    for a registered settings id and for raw settings

2 weeks ago[Feature] Settings: add policy option and strict explicit_enable gating
Vsevolod Stakhov [Mon, 20 Jul 2026 10:01:51 +0000 (11:01 +0100)] 
[Feature] Settings: add policy option and strict explicit_enable gating

Make symbols with the explicit_enable flag actually require explicit
enabling: previously they were executed whenever any settings were
applied to a task, as the implicit-allow settings branch in
cache_item::is_allowed never consulted the flag, and any non-null
task->settings unlocked such symbols regardless of content.

Now explicit_enable symbols run only when:
  * listed in symbols_enabled of the applied settings (any policy), or
  * force-enabled via task:enable_symbol()

To make it possible to combine the default-allow behaviour with
selectively enabled explicit_enable symbols and disabled symbols,
introduce a settings policy option in the apply block:

  apply {
    policy = "implicit_allow";
    symbols_enabled = ["MY_EXPLICIT_SYM"]; # additive, unlocks
    symbols_disabled = ["SOME_SYM"];       # subtractive
  }

With policy = implicit_allow, symbols_enabled no longer switches the
settings to whitelist mode: all symbols stay enabled, the listed ones
are merely unlocked. The policy is also honoured for raw settings
passed via the Settings header.

Additionally, symbols listed in symbols_enabled are now always tracked
as force-enabled (previously only when the settings element was already
attached to the task, which missed the map-driven settings path where
the id is set after settings are applied).

2 weeks ago[Fix] Fuzzy: skip injected (computed) parts on learn and check
Vsevolod Stakhov [Sat, 18 Jul 2026 09:26:39 +0000 (10:26 +0100)] 
[Fix] Fuzzy: skip injected (computed) parts on learn and check

2 weeks agoMerge pull request #6141 from rspamd/vstakhov-aliases-from-rewrite
Vsevolod Stakhov [Fri, 17 Jul 2026 10:59:21 +0000 (11:59 +0100)] 
Merge pull request #6141 from rspamd/vstakhov-aliases-from-rewrite

Preserve the sender identity from alias rewriting (DMARC/SPF, issue 6137)

2 weeks agoMerge pull request #6143 from rspamd/vstakhov-semicolon-addr-list
Vsevolod Stakhov [Fri, 17 Jul 2026 10:59:01 +0000 (11:59 +0100)] 
Merge pull request #6143 from rspamd/vstakhov-semicolon-addr-list

Split MIME address lists on semicolons as well as commas (issue 6139)

3 weeks ago[Test] Add unit tests for the MIME address list parser 6143/head
Vsevolod Stakhov [Thu, 16 Jul 2026 11:28:26 +0000 (12:28 +0100)] 
[Test] Add unit tests for the MIME address list parser

Covers single addresses, display names (plain, quoted, UTF-8),
comments, comma and semicolon lists, separators inside quoted strings
and unquoted display names, group constructs and no-address inputs.

Issue: #6139

3 weeks ago[Fix] Split MIME address lists on semicolons as well as commas
Vsevolod Stakhov [Thu, 16 Jul 2026 11:28:23 +0000 (12:28 +0100)] 
[Fix] Split MIME address lists on semicolons as well as commas

A semicolon is not an RFC 5322 address-list separator (it only
terminates a group construct), but Outlook-style ;-separated recipient
lists are pervasive in real mail and most MTAs (e.g. Postfix) split
them. Previously such a list was parsed as a single malformed address
whose local part swallowed the semicolon and the next address, breaking
recipient counts and producing false invalid-address signals.

Treat ';' exactly like ',' in the list parser. This mirrors the
existing comma behaviour in every edge case: semicolons inside quoted
strings are preserved, an unquoted semicolon in a display name drops
the preceding phrase just like a comma does, an empty group
(undisclosed-recipients:;) now yields no addresses like the comma
variant, and the trailing ';' of a group construct no longer leaks
into the last member's domain.

Issue: #6139

3 weeks agoFix synchronous context_augment firing two GPT requests for one model 6142/head
Marcel Menzel [Thu, 16 Jul 2026 10:55:55 +0000 (10:55 +0000)] 
Fix synchronous context_augment firing two GPT requests for one model

When your context_augment function calls the callback synchronously
(e.g., return function(task, content, cb) cb('some context') end),
the pending_fetches counter drops to 0 inside the callback,
triggering maybe_proceed() which calls proceed().

Then control returns to the main flow where the now-redundant guard
also sees pending_fetches == 0 and calls proceed(nil) a second time
— this time without the context snippet.

The else branch (line 1324) already handles the case where none
of the context features are enabled, so the removed fallback
was purely a duplicate path for synchronous callbacks.

3 weeks ago[Test] Use unique envelope senders in aliases From tests 6141/head
Vsevolod Stakhov [Thu, 16 Jul 2026 10:31:09 +0000 (11:31 +0100)] 
[Test] Use unique envelope senders in aliases From tests

The three From-identity tests shared one envelope From/Rcpt pair, so the
greylist module (meta key = envelope from + rcpts + ip) soft-rejected
the third scan of the pair within its retry window, skipping the filters
stage and losing the GET_FROM symbol. This only manifested with tight
test pacing (pabot shard in CI); give each test a unique envelope
sender, as the rest of the suite does.

3 weeks ago[Minor] Selectors: document the 'orig' address flavour
Vsevolod Stakhov [Thu, 16 Jul 2026 10:13:10 +0000 (11:13 +0100)] 
[Minor] Selectors: document the 'orig' address flavour

The from/rcpts extractors pass their arguments through to task:get_from
and task:get_recipients, so from('mime', 'orig') already selects the
address as it was seen in the message; document it and add a functional
test exercising the flavour through the selector pipeline.

3 weeks ago[Fix] Forged recipients: compare addresses as they were transmitted
Vsevolod Stakhov [Thu, 16 Jul 2026 09:47:27 +0000 (10:47 +0100)] 
[Fix] Forged recipients: compare addresses as they were transmitted

The rule compared the rewritten envelope view against the wire MIME
headers, so a recipient alias mapping to another domain (legal for
recipients) could make a perfectly matching To header look forged.
Compare the envelope and the headers at the same altitude: both as they
were seen in the message, which is also what this rule checked before
alias rewriting was applied to tasks.

Issue: #6137

3 weeks ago[Test] Add functional tests for From identity preservation
Vsevolod Stakhov [Thu, 16 Jul 2026 09:02:40 +0000 (10:02 +0100)] 
[Test] Add functional tests for From identity preservation

- aliases: googlemail.com From keeps its domain while the user part is
  canonicalized; a cross-domain virtual alias on the From is discarded;
  a same-domain alias on the From local part still applies
- get_from: the 'orig' flavour returns the wire From (and only it) after
  a task:set_from rewrite, and is a no-op without one
- dmarc: alignment and policy lookup use the wire From domain even when
  a prefilter rewrote the MIME From

The REWRITE_MIME_FROM test prefilter is gated on a request header so it
stays inert in merged-suite tests that do not opt in.

Issue: #6137

3 weeks ago[Fix] SPF: check the original envelope sender
Vsevolod Stakhov [Thu, 16 Jul 2026 09:02:27 +0000 (10:02 +0100)] 
[Fix] SPF: check the original envelope sender

SPF evaluates the RFC5321.MailFrom as it was transmitted (including
local-part macros), so resolve the SPF record and credentials from the
original envelope sender even if the envelope from has been rewritten,
e.g. by the aliases module.

Issue: #6137

3 weeks ago[Fix] DMARC: evaluate the From and envelope domains as seen in the message
Vsevolod Stakhov [Thu, 16 Jul 2026 09:02:24 +0000 (10:02 +0100)] 
[Fix] DMARC: evaluate the From and envelope domains as seen in the message

DMARC must align identifiers and look up the policy for the RFC5322.From
domain as it appears in the message, and check SPF alignment against the
RFC5321.MailFrom as transmitted. Use the 'orig' address flavour so that
rewrites done via task:set_from (e.g. by the aliases module) do not
affect alignment, policy lookup or rua report attribution.

Issue: #6137

3 weeks ago[Rework] Lua API: make the 'orig' address flavour return wire addresses
Vsevolod Stakhov [Thu, 16 Jul 2026 09:02:08 +0000 (10:02 +0100)] 
[Rework] Lua API: make the 'orig' address flavour return wire addresses

Previously task:get_from({'mime', 'orig'}) returned both the preserved
original addresses and the rewritten ones appended by task:set_from,
making the flavour unusable for consumers that need the message as it
arrived (a duplicate-From check would misfire on the combined list, and
Lua could not tell the entries apart). Now, when the 'orig' flavour is
requested and originals were preserved by a rewrite, only the original
addresses are returned; without a rewrite the behaviour is unchanged.

Also expose the 'aliased' and 'original' email address flags to Lua and
document the flavour.

3 weeks ago[Fix] Aliases: refuse to rewrite From domains
Vsevolod Stakhov [Thu, 16 Jul 2026 09:01:55 +0000 (10:01 +0100)] 
[Fix] Aliases: refuse to rewrite From domains

The From address is the sender's identity: SPF, DKIM and DMARC all key
on its domain. Service rules and alias resolution (e.g. a virtual alias
mapping to another domain) may only touch the local part of the From
address; any rewrite that would change the domain is now discarded.
Recipient rewriting is unaffected: recipients are interpreted by the
receiving host, and no authentication protocol keys on them.

Also fix a spurious task:set_from call: the set_from decision used the
callback-wide alias_resolved flag, so a rewrite of any earlier address
(e.g. an envelope recipient) caused the unmodified MIME From to be
marked as aliased and duplicated.

Issue: #6137

3 weeks ago[Fix] Aliases: never rewrite googlemail.com domain to gmail.com
Vsevolod Stakhov [Thu, 16 Jul 2026 09:01:40 +0000 (10:01 +0100)] 
[Fix] Aliases: never rewrite googlemail.com domain to gmail.com

googlemail.com and gmail.com are DNS-distinct domains with independent
SPF and DMARC records (gmail.com publishes p=none while googlemail.com
publishes p=quarantine), so substituting one for the other breaks DKIM
alignment, changes the effective DMARC policy and misattributes DMARC
reports. Keep the gmail.com user-part canonicalization (dots and plus
tags) for googlemail.com addresses, but preserve the domain.

The domain equivalence remains available in lua_util.remove_email_aliases
for dedup-only consumers such as email hash lookups in RBL checks.

Issue: #6137

3 weeks agoMerge pull request #6132 from errror/master 5807/head
Patrick C. [Wed, 15 Jul 2026 16:20:21 +0000 (18:20 +0200)] 
Merge pull request #6132 from errror/master

Fix for PR #5985: sleep in callback context

3 weeks agoMerge pull request #6133 from rspamd/dependabot/pip/contrib/neural-embedding-service...
Vsevolod Stakhov [Wed, 15 Jul 2026 16:20:02 +0000 (17:20 +0100)] 
Merge pull request #6133 from rspamd/dependabot/pip/contrib/neural-embedding-service/transformers-5.5.0

Bump transformers from 5.3.0 to 5.5.0 in /contrib/neural-embedding-service

3 weeks agoBump transformers in /contrib/neural-embedding-service 6133/head
dependabot[bot] [Mon, 13 Jul 2026 20:55:09 +0000 (20:55 +0000)] 
Bump transformers in /contrib/neural-embedding-service

Bumps [transformers](https://github.com/huggingface/transformers) from 5.3.0 to 5.5.0.
- [Release notes](https://github.com/huggingface/transformers/releases)
- [Commits](https://github.com/huggingface/transformers/compare/v5.3.0...v5.5.0)

---
updated-dependencies:
- dependency-name: transformers
  dependency-version: 5.5.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
3 weeks ago[Fix] Lua 5.5 compatibility: do not assign to for-loop variables
Vsevolod Stakhov [Sat, 11 Jul 2026 09:30:34 +0000 (10:30 +0100)] 
[Fix] Lua 5.5 compatibility: do not assign to for-loop variables

Lua 5.5 makes for-loop control variables read-only, so assigning to
them is now a compile-time error. Rebind the value to a local (or pass
it as a closure parameter) in all remaining places; found by
parse-checking the whole tree with luac 5.5.

3 weeks ago[Test] Fix race in history reset e2e test
Vsevolod Stakhov [Sat, 11 Jul 2026 09:19:38 +0000 (10:19 +0100)] 
[Test] Fix race in history reset e2e test

Register waitForResponse listeners before clicking the reset button:
on localhost the historyreset response arrives before a listener
registered after the click can see it, which made the legacy
(Playwright 1.45.3) CI leg fail deterministically.

3 weeks agoMerge pull request #6130 from AmateurECE/bugfix/lua-lupa-5.5-compatibility-word-loop...
Vsevolod Stakhov [Sat, 11 Jul 2026 08:39:22 +0000 (09:39 +0100)] 
Merge pull request #6130 from AmateurECE/bugfix/lua-lupa-5.5-compatibility-word-loop-variable

fix(lua-lupa): Use local loop variable

3 weeks agoMerge pull request #6123 from moisseev/test
Vsevolod Stakhov [Sat, 11 Jul 2026 07:39:11 +0000 (08:39 +0100)] 
Merge pull request #6123 from moisseev/test

[Test] Update dev dependencies

3 weeks agoMerge pull request #6126 from rspamd/vstakhov-composite-conditions
Vsevolod Stakhov [Sat, 11 Jul 2026 07:38:56 +0000 (08:38 +0100)] 
Merge pull request #6126 from rspamd/vstakhov-composite-conditions

[Feature] composites: per-symbol Lua conditions and explicit dependencies

3 weeks agoMerge pull request #6127 from rspamd/vstakhov-selectors-anchor-parse
Vsevolod Stakhov [Sat, 11 Jul 2026 07:38:43 +0000 (08:38 +0100)] 
Merge pull request #6127 from rspamd/vstakhov-selectors-anchor-parse

[Fix] lua_selectors: reject selectors that parse only as a prefix

3 weeks agofix(lua-lupa): Use local loop variable 6130/head
Ethan D. Twardy [Fri, 10 Jul 2026 23:03:58 +0000 (18:03 -0500)] 
fix(lua-lupa): Use local loop variable

Lua 5.5 makes `for` expression variables constant. The recent upgrade to
lua 5.5 broke lua-lupa, which previously attempted to modify the loop
variable. This caused an unrecoverable error on startup:

rspamd_lua_require_function: require of lua_util.jinja_template failed: error loading module 'lupa' from file '/usr/share/rspamd/lualib/lupa.lua':\x0A\x09/usr/share/rspamd/lualib/lupa.lua:1858: attempt to assign to const variable 'word'

3 weeks ago[Minor] WebUI: drop unreachable 304 success branch 6124/head
Alexander Moisseev [Fri, 10 Jul 2026 16:31:49 +0000 (19:31 +0300)] 
[Minor] WebUI: drop unreachable 304 success branch

The Rspamd controller sends "Cache-Control: no-store" on every WebUI
response (controller.c, issue #3330) and never emits Last-Modified/ETag
for these endpoints, so a browser never revalidates and a 304 response
is impossible. The `|| xhr.status === 304` in queryServer was dead code
copied from jQuery's default success range.

Remove it so queryServer, the /stat connect probe and the legacy /auth
probe all treat 200-299 as success. Treating 304 as an error is also
safer: a 304 body is empty, and success would have fed it to the JSON
parser.

3 weeks ago[Fix] WebUI: route a malformed /stat response to the login dialog
Alexander Moisseev [Fri, 10 Jul 2026 16:13:34 +0000 (19:13 +0300)] 
[Fix] WebUI: route a malformed /stat response to the login dialog

The connect probe swallowed JSON parse errors (parseJsonSafe returned {}),
so a 2xx /stat with a non-JSON body (truncated by a proxy, a captive-portal
page, corruption) loaded an empty, half-loaded main UI with a "Cannot get
server status" alert and no login path. The former jQuery content-type
parse raised parsererror and fell through to the login form.

Parse the body explicitly and, on any failure, show the connect dialog —
matching the pre-migration behaviour and covering the broad case (any
non-JSON 2xx, not only application/json). Removes the now-unused
parseJsonSafe helper.

3 weeks ago[Fix] WebUI: restore smooth scroll on the Scan tab
Alexander Moisseev [Fri, 10 Jul 2026 15:28:13 +0000 (18:28 +0300)] 
[Fix] WebUI: restore smooth scroll on the Scan tab

The jQuery-removal replaced .animate({scrollTop}) with
window.scrollTo({behavior:"smooth"}), which honours prefers-reduced-motion
and is interruptible by Tabulator's reflows. The former jQuery tween
ignored the preference and re-set scrollTop every frame, so it was always
smooth.

Add a requestAnimationFrame-based smoothScrollTo (400ms easeInOut, matching
jQuery's default) and use it for both Scan scrolls (scan result and the
"Clean" reset). Like the rest of the UI's animations it does not honour
prefers-reduced-motion, preserving the previous behaviour.

3 weeks ago[Fix] WebUI: stop refresh spinner restarting mid fan-out
Alexander Moisseev [Fri, 10 Jul 2026 15:18:42 +0000 (18:18 +0300)] 
[Fix] WebUI: stop refresh spinner restarting mid fan-out

fireAjaxComplete (the active-request counter decrement) ran at the top of
finish(), before the success callback. During an "All SERVERS" query the
count therefore dipped to 0 between the neighbours probe and the
per-neighbour requests: ajaxStart re-fired and re-added fa-spin,
restarting the spinner's CSS animation. jQuery decremented after success,
keeping the count above zero for the whole fan-out.

Move the decrement to the end of finish(), after the success/complete
callbacks. This also removes the rapid fa-spin class churn that was
interrupting the Scan tab's smooth scroll.

3 weeks ago[Fix] WebUI: address jQuery-removal review comments
Alexander Moisseev [Fri, 10 Jul 2026 14:03:25 +0000 (17:03 +0300)] 
[Fix] WebUI: address jQuery-removal review comments

- stats.js: guard the optional fuzzy_hashes key in addFuzzyStorage
  (Object.entries/keys of undefined threw on every Status refresh when
  fuzzy_check was disabled or a neighbour was down), matching the guard
  addStatfiles already had.
- common.js: cancel the hide/show slide animations once the final state
  is committed, so the fill:"forwards" no longer pins height:0 — a later
  re-show was measuring the pinned height and animating 0→0.
- common.js: treat a single Element (incl. <select>/<form>, which expose
  a numeric .length) as one node in toElements, not a collection.
- common.js: tolerate a null neighbours body before Object.keys.
- rspamd.js: null-guard the active-tab lookup in two spots, where a
  missing active tab used to throw.
- upload.js: drop the dead params:{processData:false} (queryServer no
  longer form-encodes).
- common.js: document the statusCode handler signature.

3 weeks ago[Test] selectors: cases for partially-parsed selectors 6127/head
Vsevolod Stakhov [Fri, 10 Jul 2026 13:16:22 +0000 (14:16 +0100)] 
[Test] selectors: cases for partially-parsed selectors

Negative cases: a second ':' method, a stray closing paren and junk
after a ';' list element must be rejected. Control cases: valid
method/transform chains, selector lists and trailing whitespace must
keep parsing.

3 weeks ago[Fix] lua_selectors: reject selectors that parse only as a prefix
Vsevolod Stakhov [Fri, 10 Jul 2026 13:16:11 +0000 (14:16 +0100)] 
[Fix] lua_selectors: reject selectors that parse only as a prefix

The selector grammar was not anchored to the end of input, so lpeg
matched the longest valid prefix and silently dropped the rest of the
string. E.g. 'from("smtp"):domain:lower' (a second ':' cannot parse)
was accepted and evaluated as 'from("smtp"):domain', and any trailing
garbage after a selector or a ';' list element was ignored. All
consumers (multimap, rbl, ratelimit, settings, reputation, the
controller selector check used by the WebUI) reported such selectors
as valid while evaluating only the prefix.

Append an lpeg.Cp() capture after the grammar (plus optional trailing
whitespace) and make parse_selector require the whole input to be
consumed, logging the position and the unparsed tail otherwise.

User-visible change: selectors that previously loaded thanks to the
silent truncation now fail configuration load with an error pointing
at the offending token. Such selectors were never evaluating as
written, so failing loudly is the correct behaviour.

3 weeks ago[Fix] rspamd_symcache.h: keep C linkage of lua.h for C++ includers 6126/head
Vsevolod Stakhov [Fri, 10 Jul 2026 07:38:03 +0000 (08:38 +0100)] 
[Fix] rspamd_symcache.h: keep C linkage of lua.h for C++ includers

The raw lua.h include sat outside extern "C", so any C++ unit that
pulled rspamd_symcache.h before lua/lua_common.h got C++-mangled
declarations of the Lua C API and failed to link. Including
lua_common.h itself is not an option due to the include cycle via
rspamd.h -> cfg_file.h -> rspamd_symcache.h, so wrap lua.h the same
way lua_common.h does. This also lets composites sources include
lua_common.h in the natural position instead of first.

3 weeks ago[Test] composites: functional cases for Lua conditions
Vsevolod Stakhov [Fri, 10 Jul 2026 07:32:18 +0000 (08:32 +0100)] 
[Test] composites: functional cases for Lua conditions

Cover: option-inspecting condition, cross-symbol join on a shared
option value, numeric return as atom weight combined with an
expression limit, false condition, and depends_on deferring a
composite whose condition consults a postfilter symbol.

3 weeks ago[Feature] composites: per-symbol Lua conditions and explicit dependencies
Vsevolod Stakhov [Fri, 10 Jul 2026 07:32:07 +0000 (08:32 +0100)] 
[Feature] composites: per-symbol Lua conditions and explicit dependencies

Allow gating composite atoms with synchronous Lua functions, by analogy
with re_conditions of mime expressions:

  rspamd_config:add_composite('C', {
    expression = 'SYM_A & SYM_B',
    conditions = {
      SYM_A = function(task, symbol) ... end,
    },
    depends_on = { 'SOME_POSTFILTER_SYM' },
  })

A condition is called as f(task, symbol) where symbol is a table in the
task:get_symbol() layout; it may return true/false or a number used as
the atom weight. The condition is ANDed with option filters of the atom
and a failed condition is treated exactly as a missing symbol, so
removal policies are not applied. Since a condition can consult symbols
invisible to the expression, the optional depends_on list feeds the
first/second pass placement as if those symbols were expression atoms.

rspamd_config:add_composite() now also accepts a full definition table,
and rspamd_lua_push_symbol_result() is exported for reuse.

4 weeks ago[Test] phishing: no FP for same label under different suffix
Vsevolod Stakhov [Wed, 8 Jul 2026 12:59:27 +0000 (13:59 +0100)] 
[Test] phishing: no FP for same label under different suffix

Regression test: brand.co.za displayed over a brand.com href must not
fire PHISHING (uses co.za since the functional test tld file has no
co.uk)

4 weeks ago[Fix] phishing: don't penalise same label under another TLD
Vsevolod Stakhov [Wed, 8 Jul 2026 12:59:15 +0000 (13:59 +0100)] 
[Fix] phishing: don't penalise same label under another TLD

Three fixes for cross-TLD brand false positives
(e.g. brand.co.uk displayed over a brand.com href):

- Normalise the DMARC_POLICY_ALLOW domain to eSLD before comparing it
  with the link target tld: DMARC reports the raw From domain (possibly
  a subdomain), so the existing exclusion for authenticated mail never
  matched
- Compare registrable labels instead of stripping the last dot
  component: the old code only handled single-label suffixes, so
  brand.co.uk vs brand.com was levenshtein-compared as full strings and
  scored full weight; now the identical label yields weight 0
- Check strict_domains regardless of the computed weight (gated on
  tld ~= ptld, equivalent to the old reachable behaviour), so displaying
  a strictly protected domain over a same-label different-suffix target
  still fires at full weight

4 weeks ago[Test] lua_cryptobox: unit tests for secretbox nonce padding
Vsevolod Stakhov [Wed, 8 Jul 2026 11:55:20 +0000 (12:55 +0100)] 
[Test] lua_cryptobox: unit tests for secretbox nonce padding

Guard against the regression fixed in #6122: a short nonce must produce
the same ciphertext as the explicitly zero-padded 24-byte nonce, and
short/padded forms must decrypt each other. Both assertions fail on the
unpatched code as they read past the nonce buffer.

4 weeks ago[Fix] lua_cryptobox: pass padded nonce to secretbox encrypt/decrypt
Jamon Camisso [Wed, 8 Jul 2026 11:54:31 +0000 (07:54 -0400)] 
[Fix] lua_cryptobox: pass padded nonce to secretbox encrypt/decrypt

Both secretbox encrypt and decrypt built a zero-padded 24-byte real_nonce
but passed the original short nonce pointer to libsodium, reading up to
23 bytes out of bounds and producing ciphertexts that depend on adjacent
memory contents.

Fixes #6121

4 weeks agoMerge pull request #6119 from rspamd/dependabot/pip/contrib/neural-embedding-service...
Vsevolod Stakhov [Wed, 8 Jul 2026 11:38:22 +0000 (12:38 +0100)] 
Merge pull request #6119 from rspamd/dependabot/pip/contrib/neural-embedding-service/transformers-5.3.0

Bump transformers from 5.0.0rc3 to 5.3.0 in /contrib/neural-embedding-service

4 weeks ago[Minor] maps: move glob pattern anchoring into rspamd_str_regexp_escape
Vsevolod Stakhov [Wed, 8 Jul 2026 09:09:41 +0000 (10:09 +0100)] 
[Minor] maps: move glob pattern anchoring into rspamd_str_regexp_escape

Add RSPAMD_REGEXP_ESCAPE_ANCHOR flag that wraps the escaped pattern
into ^(?:...)$ within the same allocation instead of a second
g_strdup_printf in the glob map insertion path.

4 weeks ago[Fix] maps: anchor glob map patterns to match the whole subject
Vsevolod Stakhov [Wed, 8 Jul 2026 08:52:53 +0000 (09:52 +0100)] 
[Fix] maps: anchor glob map patterns to match the whole subject

Glob map entries were compiled into unanchored regexps and matched
with substring search semantics, so a `t.co` entry matched
`walmart.com` and `*.bit.ly` matched `foo.bit.ly.evil.com`.
Wrap the translated pattern into `^(?:...)$` at map load time so
glob entries match the subject as a whole: bare names match exactly,
wildcards match only what they say.

This affects all glob maps: multimap glob/glob_multi, url_redirector
redirector_hosts_map, dkim_signing/arc signing_table and key_table,
mx_check exclusions and the rbl glob returncodes matcher. Maps that
relied on the accidental substring behaviour must now use explicit
wildcards.

Fixes #6125

4 weeks ago[Test] WebUI: fix race in history reset E2E test
Alexander Moisseev [Sun, 5 Jul 2026 19:17:18 +0000 (22:17 +0300)] 
[Test] WebUI: fix race in history reset E2E test

Register the historyreset/history response listeners before the click
via Promise.all. waitForResponse only catches responses arriving after
it is set up, and the fast local historyreset round-trip could beat a
listener attached post-click, causing a flaky 10s timeout under CI load.

4 weeks agoMerge pull request #6120 from moisseev/tabulator
Vsevolod Stakhov [Sun, 5 Jul 2026 17:47:12 +0000 (18:47 +0100)] 
Merge pull request #6120 from moisseev/tabulator

WebUI: migrate from FooTable to Tabulator

4 weeks ago[Rework] WebUI: drop jQuery dependency
Alexander Moisseev [Sun, 5 Jul 2026 16:23:36 +0000 (19:23 +0300)] 
[Rework] WebUI: drop jQuery dependency

Remove jQuery from the RequireJS config (paths, app/bootstrap shims) and
delete the vendored jquery-3.7.1.min.js. All app modules now load without
jQuery; the bootstrap shim's stale "Popovers require jQuery" comment and
deps entry are removed.

Drive-by: rename the modalDialog data-backdrop attribute to its Bootstrap
5 name data-bs-backdrop, so the static-backdrop behaviour is actually
applied (the BS4 attribute has been silently inert).

4 weeks ago[Rework] WebUI: remove jQuery from libft module
Alexander Moisseev [Sun, 5 Jul 2026 15:53:54 +0000 (18:53 +0300)] 
[Rework] WebUI: remove jQuery from libft module

Migrate libft.js off jQuery. $("<tag>", {hash}) element construction
becomes common.el, $.extend becomes spread, $.each becomes forEach.
The column-options dropdown's namespaced hidden.bs.dropdown.rspamd
rebind (jQuery namespaces) becomes a common.data-tracked
addEventListener that removes the previous handler before re-binding;
the delegated fuzzy-hash buttons (re-bound every render) use a cleanup
map so common.delegate doesn't stack. $(arr).not(arr) set-diffs become
Array.some/ includes, and .siblings()/.attr()/.html()/.val() move to
native DOM.

4 weeks ago[Rework] WebUI: remove jQuery from main module
Alexander Moisseev [Sun, 5 Jul 2026 14:31:39 +0000 (17:31 +0300)] 
[Rework] WebUI: remove jQuery from main module

Migrate rspamd.js off jQuery. The /stat connect probe moves to a raw
XHR (avoiding common.query's error logging on the normal "not logged
in" path); $.ajaxSetup and $(document).ajaxStart/Complete are dropped
since no $.ajax calls remain. tabClick disables/enables the nav bar
controls via attribute+class helpers (li/a need both for Bootstrap);
:radio + .val([v]) and .data("value") become checkRadio / activeMenuValue
helpers; delegated handlers move to common.delegate; BS5 events move to
addEventListener (with {once:true} replacing self-removing .on/.off);
the popover content uses cloneNode and the outside-click dismiss uses
.contains(). Form/input re-binds become onsubmit/oninput properties.

4 weeks ago[Rework] WebUI: remove jQuery from stats module
Alexander Moisseev [Sun, 5 Jul 2026 13:51:58 +0000 (16:51 +0300)] 
[Rework] WebUI: remove jQuery from stats module

Migrate stats.js off jQuery. The delegated mouseenter/mouseleave hover
highlighting (which doesn't bubble natively) is reimplemented on
mouseover/mouseout with a relatedTarget-containment check; the jQuery
sibling-traversal chains (prevAll/nextAll/slice/find) move to native
sibling walks. $.each becomes Object.entries/forEach, $(html).appendTo
becomes insertAdjacentHTML, and $.wrapAll becomes common.el DOM
construction. The legacy /auth fan-out keeps a raw XHR to preserve its
custom error message and alerted-session dedup; $.when().always becomes
Promise.all().finally. Adds common.getAjaxTimeout so direct XHRs honour
the configured timeout.

4 weeks ago[Rework] WebUI: remove jQuery from upload module
Alexander Moisseev [Sun, 5 Jul 2026 13:18:43 +0000 (16:18 +0300)] 
[Rework] WebUI: remove jQuery from upload module

Migrate upload.js off jQuery: $.Deferred/$.when to Promise/Promise.all,
$.trim to trim, :selected to selectedIndex, multi-arg :not() to CSS,
.val/.prop/.attr/.find/.append/.empty/.children/.closest/.parent to
native DOM, .animate({scrollTop}) to scrollTo, and $("<option>") to
common.el. Option metadata stored via .data() (objects/arrays) moves
to common.data; the fuzzy-picker change handler is bound once instead
of re-bound on every refresh.

4 weeks ago[Rework] WebUI: remove jQuery from config module
Alexander Moisseev [Sun, 5 Jul 2026 17:24:31 +0000 (20:24 +0300)] 
[Rework] WebUI: remove jQuery from config module

Migrate config.js off jQuery: HTML-string/$("<tag>") element building
to common.el, $.each to forEach, the :input/:checked pseudos to CSS
selectors, .data() round-trips (object values) to common.data, the
delegated .map-link handler to common.delegate, and .attr()/.val()/
.on()/.html()/.remove() to native DOM and addEventListener.

Map cells and the modal title now escape server data via textContent
(consistent with the leaf-modules hardening).

4 weeks ago[Rework] WebUI: remove jQuery from leaf modules
Alexander Moisseev [Sun, 5 Jul 2026 12:12:52 +0000 (15:12 +0300)] 
[Rework] WebUI: remove jQuery from leaf modules

Migrate graph.js, selectors.js, history.js, and symbols.js off
jQuery: $.extend/$.each/$.trim and $().val/prop/attr/hasClass/is/
addClass/removeClass/change/click/on to native DOM and the shared
common.el / common.delegate helpers added in the common.js stage.

Delegated handlers on Tabulator-rendered DOM (.scorebar) move to
common.delegate; the repeated button disable/enable patterns
collapse into small per-module helpers.

jQuery remains loaded for the remaining modules (config, libft,
stats, upload, rspamd) — removed in later stages.

4 weeks ago[Rework] WebUI: drop jQuery from common.js
Alexander Moisseev [Sun, 5 Jul 2026 10:25:52 +0000 (13:25 +0300)] 
[Rework] WebUI: drop jQuery from common.js

Rewrite the AJAX layer (query/queryServer) on XMLHttpRequest,
preserving jQuery's semantics: neighbours fan-out and aggregation,
per-server error dispatch, statusCode, timeout, GET data appended
to the URL as a query string, default Content-Type for POST bodies,
NProgress download progress, and jqXHR-compatible callback arguments.

Add a spinner pub/sub (onAjaxStart/onAjaxComplete with an in-flight
counter honoring global:false) and setAjaxTimeout, plus shared
vanilla helpers: el, delegate, data. Migrate alertMessage, hide,
show, toggle (Web Animations API), the error-log UI and fileUtils
to plain DOM.

Bridge rspamd.js to the new APIs (setAjaxTimeout, onAjaxStart/
onAjaxComplete) alongside the jQuery hooks, which stay for the
remaining direct $.ajax calls until a later stage. jQuery is still
loaded; this is stage 1 of the staged removal.

4 weeks ago[Rework] WebUI: migrate BS5 off jQuery bridge
Alexander Moisseev [Sat, 4 Jul 2026 15:38:58 +0000 (18:38 +0300)] 
[Rework] WebUI: migrate BS5 off jQuery bridge

Replace the Bootstrap 5 jQuery bridge (.modal/.popover/.trigger
"click.bs.dropdown") with the vanilla constructor API
(bootstrap.{Modal,Popover,Dropdown}.getOrCreateInstance) in
rspamd.js, config.js and libft.js, capturing the bootstrap module
where needed.

Prerequisite for removing jQuery: the bridge only activates when
jQuery is loaded, while the vanilla API works regardless. With jQuery
still present the existing $(el).on("*.bs.*") subscriptions keep
firing, so those stay jQuery for now and move to addEventListener
together with the rest of the jQuery removal. The main.js shim
deps:["jquery"] becomes redundant and is dropped in the final stage.

4 weeks ago[Rework] WebUI: inline vanilla sticky tabs
Alexander Moisseev [Sat, 4 Jul 2026 13:49:24 +0000 (16:49 +0300)] 
[Rework] WebUI: inline vanilla sticky tabs

Replace the jquery.stickytabs plugin with an inline vanilla
implementation using the BS5 Tab API, removing the last jQuery
bridge dependency of that plugin. Behavior (URL hash persistence
and initial-tab fallback) is preserved.

4 weeks ago[Minor] WebUI: update D3Evolution, D3Pie
Alexander Moisseev [Sat, 4 Jul 2026 12:26:10 +0000 (15:26 +0300)] 
[Minor] WebUI: update D3Evolution, D3Pie

D3Evolution 2.0.4 -> 2.1.0, D3Pie 1.1.2 -> 1.2.0. Both libraries
dropped the jQuery dependency (replaced $.extend with an internal
deep-merge), so drop the stale "jquery" shim deps in main.js.

4 weeks ago[Fix] WebUI: replace BS3 form-group with mb-3 6120/head
Alexander Moisseev [Sun, 5 Jul 2026 17:08:02 +0000 (20:08 +0300)] 
[Fix] WebUI: replace BS3 form-group with mb-3

Removing footable.standalone.min.css dropped its .form-group
{margin-bottom:15px} rule, collapsing the gap above the buttons on
the selectors tab (and the action-score rows on the configuration
tab). .form-group is a Bootstrap 3 class with no meaning in BS5;
replace it with the mb-3 spacing utility that provides the same gap.

4 weeks ago[Fix] lua_selectors: don't crash on a missing method call
Vsevolod Stakhov [Sun, 5 Jul 2026 11:43:13 +0000 (12:43 +0100)] 
[Fix] lua_selectors: don't crash on a missing method call

Selectors like `time:digest` (method syntax used where a transform
was meant) aborted the whole selector at scan time with an uncaught
Lua error:

  Cannot run callback: .../lua_selectors/init.lua: attempt to call
  a nil value

The `:` separator always compiles to a method call on the extracted
value; when the value has no such method, the lookup yields nil and
the unguarded call crashed the callback. Guard the lookup (via pcall,
since indexing some types raises) and yield no value instead, logging
an error that hints at the `.name` transform syntax when the name
matches a known transform. Valid method calls (e.g. `:gsub`,
`:lower`) and table field access are unaffected.

4 weeks ago[Test] Update dev dependencies 6123/head
Alexander Moisseev [Fri, 3 Jul 2026 14:59:00 +0000 (17:59 +0300)] 
[Test] Update dev dependencies

- Update ESLint to 10.6.0, stylelint to 17.14.0, and related packages

5 weeks ago[Fix] WebUI: release scroll-preservation listeners
Alexander Moisseev [Thu, 2 Jul 2026 15:03:04 +0000 (18:03 +0300)] 
[Fix] WebUI: release scroll-preservation listeners

installScrollPreservation bound listeners on the mount element and
window without removing the previous set. Each column-options rebuild
(destroy + re-init on the persistent mount element) stacked a new set
that was never released — a listener/memory leak on the history and
scan tables. Mirror bindRowClickToggle's WeakMap guard: key the set by
mount element and tear it down before re-binding.

5 weeks agoMerge pull request #6118 from rspamd/vstakhov-static-embed
Vsevolod Stakhov [Thu, 2 Jul 2026 14:16:38 +0000 (15:16 +0100)] 
Merge pull request #6118 from rspamd/vstakhov-static-embed

[Feature] neural: static_embed provider (WordPiece + static matrix)

5 weeks ago[Minor] WebUI: update Tabulator to 6.5.2
Alexander Moisseev [Thu, 2 Jul 2026 13:08:38 +0000 (16:08 +0300)] 
[Minor] WebUI: update Tabulator to 6.5.2

5 weeks ago[Minor] WebUI: compact header-filter inputs
Alexander Moisseev [Thu, 2 Jul 2026 12:25:34 +0000 (15:25 +0300)] 
[Minor] WebUI: compact header-filter inputs