Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) refuses a rule's Run function action when the rule is saved unless it names a plain function, for a rule of every owner, over COM and REST, and setting one on a domain's or an account's rule is held to the server administrator alone, where up to 6.3.6 a credential confined to named domains, a domain administrator among them, could set it. An event script that goes on in a transaction after Database.ExecuteSQL has failed is refused every statement after it once the database has ended the transaction, and Database.CommitTransaction then fails: call Database.RollbackTransaction and run the whole transaction again, or stop. A ScriptTimeout above 4,294,967 seconds is taken as that, where it ended every Windows event script at once, and the VBScript calls of OnClientValidatePassword, OnExternalAccountDownload and OnDeliveryFailed are quoted as every other call is. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) adds a sixteenth event, OnIMAPMessageMoved, called once for each message an IMAP MOVE or UID MOVE moved between two folders of the account signed in, after the command and off the IMAP thread, with the account and thirteen values, in VBScript and JScript on Windows and JavaScript on Linux; it is off as shipped, and IMAPMessageMovedEvents (COM Settings.Scripting.IMAPMessageMovedEvents, imap_message_moved_events in PUT /api/v1/settings/scripting) switches it on (EventScripts.md). Work that outlasts a stop of the servers now writes its interruption to the debug log, not the ERROR log, so OnError is not called for it, and an event script's call that meets a stop fails with "The server is stopping. The request was not completed.", its script error going to the debug log. See the 6.3.8 announcement.
This topic covers event scripts in hMailServer 6.3.4: the languages, switching scripting on, the fifteen events, the objects, the limits and errors, and the 6.3.4 security fix. If you run a Windows server older than 6.3.4 with event scripting on, that fix is the one thing to know first: a value written into a JScript handler's call, a password among them, could run code inside the server, and so could the name in a rule's Run function action, in either language. The section on CVE-2026-100741 below says what to do until the server is upgraded.
What an event script is
An event script is one file, EventHandlers.vbs or EventHandlers.js, in the server's Events directory, holding functions named after the events they handle. When scripting is on, the server calls the function for each event as it happens, hands it the objects the event is about (the client, the message, the account) and reads back what the function decided through the Result object. A rule's Run function action calls a function of the same file by name, and it too needs scripting on.
Scripting is off as shipped, and with it off no handler is called. The file is loaded all the same: at start, at every reload, at Reload() and at POST /api/v1/settings/scripting/reload. Loading a file runs its code outside functions, once to compile it and once more for each of the fifteen handlers looked for, so keep the top level to declarations. A top level that throws, or runs past the timeout, is reported as a load that failed (HM5016, HM5710 or HM5021), whether scripting is on or not.
A handler runs inside the server, on the thread that is doing the work, as the service's account: LocalSystem on a Windows installation that began before 6.3.4, NT SERVICE\hMailServer on a new one from 6.3.4, the hmailserver user on Linux. Everything behind a slow script waits, so try a script first on a server not carrying your mail.
Which language runs where
Language
Windows
Linux
Engine
File
VBScript
runs
refused at load
Windows Active Scripting (vbscript.dll)
EventHandlers.vbs
JScript
runs
refused at load
Windows Active Scripting (jscript.dll), ECMAScript 3
EventHandlers.js
JavaScript
refused at load
runs
QuickJS 2025-04-26, built into the server, ES2023
EventHandlers.js
Up to 6.3.3 a Linux server had no script engine and refused any script; since 6.3.4 it runs JavaScript in QuickJS, with the same fifteen events, function names and objects. Windows keeps VBScript and JScript unchanged.
The language is one setting: Settings.Scripting.Language over COM (Windows only), "language" in PUT /api/v1/settings/scripting over REST, and the Language (VBScript, JScript or JavaScript) field on the Control Panel's Advanced page. Since 6.3.4 it accepts all three words on both platforms, in any case, and refuses any other word without storing anything. It is seeded VBScript on every platform, so a Linux server switching scripting on sets JavaScript first.
Storing the language loads nothing. The script in force goes on running, in the language it was loaded in, until the next load. A language the build does not run is refused when the script is loaded: one line in the ERROR log, HM5016 at a start or HM5710 at a reload (the earlier script then stays in force), naming the language to use, once per load. While scripting is switched off this refusal is not reported, so switch scripting on and reload before reading the log for it.
GET /api/v1/status says which languages a build runs, under build.script_languages: ["VBScript","JScript"] on Windows, ["JavaScript"] on Linux.
Switching it on
On Windows there are two Control Panel pages under Maintenance: Advanced, whose Scripting tab turns the engine on and chooses the language, and Event scripts, which edits the file. Save & reload writes a .bak first, then reloads and checks the syntax. Over COM the switch is Settings.Scripting.Enabled; Settings.Scripting.Reload() re-reads the file and CheckSyntax() returns the error text, where empty means clean. There is one file and no include mechanism.
On Linux, from the command line:
Set the language: hmctl settings set scripting --set-string language=JavaScript
Write the file as the service's user:
sudo -u hmailserver tee /var/lib/hmailserver/events/EventHandlers.js <<'EOF'
function OnAcceptMessage(client, message) {
if (message.Subject.includes('[URGENT]') && !client.Authenticated) {
Result.Value = 2;
Result.Message = 'Urgent mail comes from people who have logged on.';
}
}
EOF
Switch scripting on: hmctl settings set scripting --set enabled=true
Confirm which file the server reads: hmctl settings get scripting current_script_file
Load it with POST /api/v1/settings/scripting/reload. POST /api/v1/settings/scripting/check answers {"result":""} when the file compiles and the error, with its line, when it does not.
The fifteen events
Every handler is optional. When the script is loaded the server checks which of these names the file declares and fires only those. Names are matched exactly: OnErrorLog is not a handler, the name is OnError.
Handler
Fires when
Called with
What Result.Value does
OnClientConnect(client)
any TCP connection is accepted, before a greeting
the client
1 closes the connection at once
OnHELO(client)
the SMTP client sent HELO or EHLO
the client
1 answers 554 5.7.1 Rejected; 2 answers 554 5.7.1 with Result.Message; 3 answers 453 4.7.0 with Result.Message
OnClientLogon(client)
an SMTP, POP3 or IMAP client finished a logon attempt, successful or not
the client; client.Authenticated says how the logon went
nothing: the logon has been decided
OnClientValidatePassword(account, password)
a password is being checked, before the built-in check
the account and the password typed
0 lets the client in; 1 turns it away; 2 (as given) leaves it to the stored password
OnSMTPData(client, message)
the DATA command arrived, before the body
the client and the message
as OnHELO
OnAcceptMessage(client, message)
the body has been received and is about to be queued
the client and the message
as OnHELO
OnRecipientUnknown(client, message)
a RCPT TO named an address this server does not host
the client and the message
nothing
OnTooManyInvalidCommands(client, message)
an SMTP client sent too many invalid commands
the client and the message
nothing
OnDeliveryStart(message)
the first step of a delivery attempt, before virus scanning
the message
1 deletes the message
OnDeliverMessage(message)
after virus scanning and global rules, before the recipient split
the message
1 deletes the message
OnDeliveryFailed(message, recipient, error)
once per failed recipient; may fire several times per message
the message, the recipient, the error text
nothing
OnExternalAccountDownload(fetchAccount, message, uid)
a message was fetched from an external POP3 or IMAP account
the fetch account, the message or null, its UID on the remote server
1 deletes it on the remote server now; 2 after Result.Parameter days; 3 never
OnBackupCompleted()
a backup finished
nothing
nothing
OnBackupFailed(reason)
a backup failed
the reason
nothing
OnError(severity, code, source, description)
anything is written to the ERROR log
the error, as the ERROR log has it
nothing
OnError's severity is 1 Critical, 2 High, 3 Medium, 4 Low, and its code is the HM<nnnn> number without the prefix.
OnClientValidatePassword setting 0 is a full authentication bypass for that account, on every protocol. A handler that bridges to another directory should fail closed: set 1 when the lookup errors, never 0.
The objects a handler is given
The objects an event has are also globals of the same names, HMAILSERVER_CLIENT, HMAILSERVER_MESSAGE, HMAILSERVER_ACCOUNT, HMAILSERVER_FETCHACCOUNT, Result and EventLog, and the parameter and the global are the same object. An object an event does not have is null as a parameter; as a global it may not be defined at all (the message of an external download that failed), and reading an undefined global throws a ReferenceError, so a handler reads its parameter.
Do not use Result in the seven events that are notifications. EventScripts.md says it is not defined at all in OnClientLogon, OnRecipientUnknown, OnTooManyInvalidCommands, OnDeliveryFailed and OnError; the public documentation adds that the server makes no Result for OnBackupCompleted and OnBackupFailed.
Object
Members
On JavaScript (Linux)
Client
Port, IPAddress, Username, HELO, Authenticated, EncryptedConnection, CipherVersion, CipherName, CipherBits, SessionID
the same, all read-only
Message
ID, Filename, Subject, From, FromAddress, To, CC, Date, Body, HTMLBody, Charset, Recipients, AddRecipient(name, address), ClearRecipients(), HeaderValue(name), HasBodyType(type), Flag(flag), Size (KB), State, DeliveryAttempt, UID, InternalDate, Copy(folderId), RefreshContent(), Save(), Attachments, Headers
Attachments and Headers are not there; SetHeaderValue(name, value) and SetFlag(flag, on) write; InternalDate is text
Result
Value, Message, Parameter
Value and Parameter take a whole Number only
EventLog
Write(text)
the same
FetchAccount
the COM FetchAccount
readable, not assignable; Password, Save(), DownloadNow() and Delete() are not there
Account
the COM Account, freshly loaded
the properties that describe the account are readable, not assignable; Password, the collections and every method are not there
Save() is what writes a change to disk. Changes to the subject, the body and the headers are written to the message file by Save(), on both platforms; a handler that does not call it changes nothing on disk. Do not call it in OnSMTPData: the public documentation says the message is still being processed there and saving corrupts it, and that only the character set, the file name, the envelope sender and the recipients are reliable at that point. Change a message in OnAcceptMessage.
Result is read after the handler returns. On JavaScript a string ('1' too), a boolean, null, undefined, NaN, a fraction or an object assigned to Value or Parameter throws a TypeError, and the event goes on with the Result it had. So in OnClientValidatePassword, Result.Value = decisions[user] for a user the table does not have leaves 2, the stored password. On Windows, COM turns a boolean, undefined, a fraction and a string of digits into numbers, and undefined into 0: the same line in JScript lets that client in.
EventLog.Write is the debugger. It appends a line to hmailserver_events.log in the log directory, and there is no breakpoint.
A rule's Run function
A rule's Run script function action calls the named function as <function>(HMAILSERVER_MESSAGE), then reloads the message from disk, so anything the function saves is seen by the rest of the rule set. Since 6.3.4 it works on Linux, where the function is a JavaScript one in EventHandlers.js.
Since 6.3.4 the action names the function alone, as a plain identifier: ASCII letters, digits, _ and $, not starting with a digit. StampReceivedDate qualifies; StampReceivedDate(x) does not. On Windows, in VBScript or JScript, any other name is refused as HM5018 each time the rule matches and the function is not run. On Linux such a name is never evaluated and finds no function: nothing is run, and the ERROR log has a Script Error line, not HM5018.
A ported handler
JScript and JavaScript share the file name, so a JScript file carried from Windows to Linux is the file the JavaScript engine is given. EventScripts.md's example starts from VBScript on Windows:
Sub OnAcceptMessage(oClient, oMessage)
If InStr(oMessage.HeaderValue("X-Spam-Flag"), "YES") > 0 And Not oClient.Authenticated Then
oMessage.HeaderValue("X-Quarantine") = "yes"
oMessage.Subject = "[SPAM] " & oMessage.Subject
oMessage.Save
EventLog.Write("Tagged spam from " & oClient.IPAddress)
End If
End Sub
The same handler for a Linux server, in EventHandlers.js:
function OnAcceptMessage(client, message) {
if (message.HeaderValue('X-Spam-Flag').includes('YES') && !client.Authenticated) {
message.SetHeaderValue('X-Quarantine', 'yes');
message.Subject = `[SPAM] ${message.Subject}`;
message.Save();
EventLog.Write(`Tagged spam from ${client.IPAddress}`);
}
}
What changes in a port:
Parameterised properties have no assignment. message.HeaderValue('X') = 'v' becomes message.SetHeaderValue('X', 'v'), and message.Flag(n) = true becomes message.SetFlag(n, true). The reading forms are unchanged.
Names are case-sensitive and spelled as the IDL spells them. JScript found message.FileName; JavaScript needs message.Filename.
No CreateObject, no new ActiveXObject. Both exist on JavaScript only to throw a TypeError that says so.
null, not Nothing, for an object an event does not have.
The limits
A fresh engine for every event, on Windows and Linux alike: the whole file is parsed and run, the handler is called, and the engine is freed before the event goes on, so nothing a script makes outlives the event.
No file system, no network, no processes on JavaScript. QuickJS keeps these in quickjs-libc.c, and that file is not in the tree. No module loader is installed, so import() is refused and an import statement is a syntax error; there is no print and no console. What a handler can change is what its objects change: a message's file through Save(), a copy into a folder through Copy(), the event log through EventLog.Write.
On Windows, ScriptAllowedObjects decides what a script may create with CreateObject or new ActiveXObject. Its shipped value is *, every class. Empty means no class beyond the server's own objects, and a comma-separated list of ProgIDs means only those. Up to 6.3.3 it applied at a service restart; since 6.3.4 a save applies it. It has nothing to apply to on Linux.
ScriptTimeout is in seconds, with 0 for no limit. It is kept in the settings store: set it with PUT /api/v1/settings/ini/ScriptTimeout, COM Settings.SetIniSetting or the Control Panel, not by editing hMailServer.ini.
Limit
Platform
Value
When a handler reaches it
ScriptTimeout
Windows
60 seconds by default
HM5019; the interrupt cannot release a handler blocked inside a COM call
ScriptTimeout
Linux
60 seconds by default
HM7341; the event goes on with whatever Result the handler had set
Memory
Linux
64 MB for each run, a constant
HM7342; the event goes on
Stack
Linux
512 KB of the calling thread's stack
a runaway recursion throws an InternalError
The Linux timeout does not stop one call into a built-in that loops in the engine's C code without polling, such as includes or indexOf on an object whose length is huge: do not call an array method on an object whose length the handler has not checked.
Errors, and where they are written
What
Where
A script that does not compile, or whose top level throws
HM5016 at start (nothing loaded), HM5710 at a reload (the earlier script stays in force)
A top level that runs past the timeout
HM5021, and the load refused
An exception a handler does not catch
one Script Error line in the ERROR log; the event goes on
The load is all or nothing on every engine, so a typo in a reload never switches a working OnClientValidatePassword off.
The 6.3.4 security fix: CVE-2026-100741
Up to 6.3.3 a password could run code inside the server. On Windows, with event scripting on, the language JScript and an OnClientValidatePassword handler in EventHandlers.js, any client that could reach a sign-in and name an existing, active account could have JScript of its own run inside hMailServer.exe, with the rights of the service's account, before it had authenticated. The server built each call as source text and escaped the apostrophe but not the backslash before it. With ScriptAllowedObjects at its shipped *, that script could create any COM object, WScript.Shell among them.
Path
Who could use it
Open in
The password given to OnClientValidatePassword
any client that can reach a sign-in
JScript
The UID given to OnExternalAccountDownload
a remote POP3 server
JScript
The error text and failed recipient given to OnDeliveryFailed
a remote mail server
JScript
The name in a rule's Run function action
whoever may edit a rule, a domain's own administrator included
JScript and VBScript
Which servers are affected. The flaw is CVE-2026-100741; the record was not yet public when this was written. The flaw is in every release from 6.0.0 to 6.3.3: the escaping was checked at every one of those tags (GitLab issue 27). Treat any server older than 6.3.4 with event scripting on and JScript as its language as affected. A rule's Run function action wrote its function name into the call unescaped, in either language, so VBScript servers too.
What 6.3.4 changes. Every JScript value is escaped in one helper, ScriptServer::EscapeForJScriptLiteral: every backslash first, then both quotes, CR, LF, U+2028, U+2029 and NUL; OnBackupFailed's reason, which neither language escaped and the table above does not list, is escaped in both. A rule's Run function name must be a plain identifier. On Linux the handler is called with values as values, so nothing is quoted into source text. A JScript handler now receives every value exactly as it was, backslashes included; one that undid the old half-escaping by hand must stop doing so.
Until you can upgrade, switch event scripting off. That is the only measure that closes every path, in either language. A server that cannot do without its scripts can narrow what is open:
Take the OnClientValidatePassword, OnExternalAccountDownload and OnDeliveryFailed handlers out of a JScript EventHandlers.js and reload the scripts. Check the reload: one that fails (HM5710 in the ERROR log) keeps the earlier script in force, those handlers included.
In either language, let nobody but the server administrator make or edit a rule with a Run function action. Roles.md lets an account's rules be written over COM by the server administrator, an administrator of its domain, or the account itself, so "whoever may edit a rule" is wider than administrators.
Narrow ScriptAllowedObjects from *, which limits what an injected script can create. The key exists from 6.2.25, and before 6.3.4 it applies only after a service restart.
Moving a handler to VBScript closes the password, UID and error-text paths and leaves the rule path open.
Before you upgrade, check your rules' Run function actions and put the function's bare name in any that does not qualify. After it, an HM5018 in the ERROR log (on Linux, a Script Error line naming the function) means one was missed. Upgrading to 6.3.4 covers the upgrade itself.
Reporting a vulnerability goes through a confidential GitLab issue or the Service Desk address contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, and not through a public issue or this forum.
What it does not do
OnClientValidatePassword is not consulted for SCRAM, which has no password to give it.
Where to read more
EventScripts.md: the member-by-member contract of every object, and the engine.
The COM API and Scripting, sections 23.6 to 23.10.
Rules and Sieve for the Run script function action, and Security Hardening, section 17.11.
The public documentation's event scripts chapter.
On this forum: hMailServer on Linux: what you get and what you do not, the 6.3.4 security announcement, the COM API on Windows and rules and the Run function action.