Repository navigation
The COM API and Scripting
This page began as a chapter of the 6.2.10 manual and has been corrected for 6.2.28; what the tree after 6.3.3 adds to the object model (section 23.3a, and the notes in 23.1 and 23.2) was read from the IDL at master
540347500on 24 September 2026. The Control Panel's pages are grouped differently now, so the paths below use today's groups; the TLS ports 465, 993 and 995 exist only after you create them on the TCP/IP ports page (a fresh install seeds 25, 587, 110 and 143); and everything added since 6.2.10 is in Changes-Since-6210. Where a value here disagrees with the Settings Reference, which is generated from the code, the reference is right.
hMailServer exposes a full COM API. Anything the Control Panel can do, a script can do - it is the same interface. The Control Panel is a COM client and nothing more.
There are two quite different things on this page and it is worth keeping them apart:
| The COM API | Event scripts | |
|---|---|---|
| Who runs it | You, from PowerShell, VBScript, C#, anything that speaks COM | The server, in-process, while mail is moving |
| When | Whenever you like | At fifteen defined points in the mail flow |
| Runs as | You | The service account, usually LocalSystem |
| Failure costs | Your script | Delivery for every message behind it |
| Turned on by | Nothing; it is always there | Settings.Scripting.Enabled |
Everything up to section 23.5 is the API. Section 23.6 onwards is event scripts.
For the full object-by-object reference - the coclasses (94 in 6.3.3, 100 in the tree
after it), every route of the REST API, and what each Settings sub-object holds - see APIs Reference.
- One type library,
hMailServer, with 94 coclasses in 6.3.3 and 100 in the tree after it, not yet released (section 23.3a). The root isApplication(ProgIDhMailServer.Application). - Every interface is a dual
IDispatchinterface, so late binding works without registering the type library.CreateObject,New-Object -ComObjectand C#dynamicall work on a machine where only the server is installed. - Early binding - a C# interop assembly, a
Dim x As hMailServer.Account- depends on vtable order. New members are therefore always appended, never inserted, so an interop assembly built against an older release keeps working. - The installer's admintools component exists only to register the type library, so that scripts on another machine can administer this one. If you do not script remotely, you do not need it.
# Local
$app = New-Object -ComObject 'hMailServer.Application'
# Remote, over DCOM, against a machine where admintools is registered
$app = [Activator]::CreateInstance([Type]::GetTypeFromProgID('hMailServer.Application', 'mail01'))The Control Panel connects to a remote host exactly that way, which is why it needs no agent on the far end.
$app = New-Object -ComObject 'hMailServer.Application'
$account = $app.Authenticate('Administrator', 'your-admin-password')
if (-not $account) { throw 'Authentication failed.' }Authenticate returns an Account object, or $null on failure. Three
credentials work:
| Sign in as | Gets |
|---|---|
Administrator with [Security] AdministratorPassword from hMailServer.INI
|
Server administrator |
A mailbox address whose AdminLevel is 2 |
Server administrator |
A mailbox address whose AdminLevel is 1 |
Domain administrator: their own domain only |
Two things about this that are not obvious:
If the administrator password is empty, and no administrator TOTP secret is
enrolled, every COM session starts already authenticated as a server
administrator - COMAuthentication::AttempAnonymousAuthentication creates a
dummy Administrator account. This is the historical behaviour and it is why
setting an administrator password is the first line of Security Hardening.
A second factor on the administrator credential changes the call you must
make. With Settings.EnrolAdministratorTOTP() used (6.2.27), Authenticate
refuses the administrator outright - a stolen password is not a credential on its
own - and you must call:
if ($app.AdministratorTOTPEnabled) { # readable BEFORE authenticating
$app.AuthenticateWithCode('Administrator', $password, $code) | Out-Null
} else {
$app.Authenticate('Administrator', $password) | Out-Null
}AuthenticateWithCode is the only path that can satisfy a second factor -
a mail client has nowhere to type one, which is exactly why an account with a
factor enrolled uses an app password for IMAP, POP3 and SMTP instead.
Each interface object loads its state through a private LoadSettings() that
first asks GetIsServerAdmin(). If you are not entitled to the object, that load
does nothing, the interface's pointers stay null, and every member then returns
an error HRESULT:
You do not have access to this property / method. Ensure that hMailServer.Application.Authenticate() is called with proper login credentials.
In PowerShell that surfaces as a terminating exception; in VBScript as a
trappable error; in C# as a COMException. In 6.2.10 this was a real defect
worth remembering: fifteen methods returned false from a function whose return
type is HRESULT, and false is 0, which is S_OK - so an unauthorized
caller got "success", and five of them had not written their out-parameter, so
"success" came with uninitialized memory. If you script against the COM API,
be on 6.2.10 or later and check return values.
In the tree after 6.3.3, not yet released, two things change here. Seventy-two COM
classes had no ISupportErrorInfo, so a refusal they made reached the caller as a bare
Exception from HRESULT: 0x800403E9; every class carries it now, and a refusal arrives
with the server's own sentence, which a checker (build/check-com-error-info.py) keeps
true. And authority is a role (schema 6047): serveradmin, domainadmin, helpdesk,
auditor or a custom one, given to an account for named domains. An account with no role
keeps exactly what its AdminLevel gave it; an account with one signs in with its own
address and password and reaches what the role's permissions reach, and COM asks the same
question REST asks (RoleManager::Allows) - Application.CurrentRole and
CurrentPermissions say what the session holds. hmailserver/docs/Roles.md.
flowchart TD
App["Application<br/>hMailServer.Application"]
App --> Auth["Authenticate<br/>AuthenticateWithCode<br/>AdministratorTOTPEnabled"]
App --> Dom["Domains"]
App --> Set["Settings"]
App --> Rul["Rules - global"]
App --> Sta["Status"]
App --> Uti["Utilities"]
App --> Dbs["Database"]
App --> Bak["BackupManager"]
App --> Glo["GlobalObjects"]
App --> Lnk["Links"]
App --> Dia["Diagnostics"]
Dom --> D1["Domain: Name, Active, MaxSize,<br/>DKIM, relay, retention"]
D1 --> Acc["Accounts"]
D1 --> Ali["Aliases"]
D1 --> Dls["DistributionLists"]
D1 --> Dal["DomainAliases"]
Acc --> A1["Account: Address, Password, Active,<br/>MaxSize, Size, AdminLevel"]
A1 --> Msg["Messages"]
A1 --> Fol["IMAPFolders"]
A1 --> Arl["Rules - this account"]
A1 --> Fet["FetchAccounts"]
A1 --> Apw["AppPasswords"]
A1 --> Sie["SieveScript, TOTPEnabled,<br/>vacation, forwarding, signature"]
Set --> S1["AntiSpam - owns Quarantine"]
Set --> S2["AntiVirus"]
Set --> S3["Backup"]
Set --> S4["Directories"]
Set --> S5["Logging"]
Set --> S6["Routes"]
Set --> S7["Scripting"]
Set --> S8["SecurityRanges"]
Set --> S9["SSLCertificates"]
Set --> S10["TCPIPPorts"]
Set --> S11["ServerMessages, Groups,<br/>IncomingRelays, PublicFolders,<br/>MessageIndexing, Cache"]
Glo --> G1["DeliveryQueue"]
Glo --> G2["MessageTrace"]
Glo --> G3["Languages"]
Every collection - Domains, Accounts, Rules, Criterias, Actions, all of
them - offers the same shape: Count, Item(index), ItemByDBID(id), Add(),
Refresh(), Delete(index) and DeleteByDBID(id), with ItemByName or
ItemByAddress where a name exists. Items have Save() and Delete().
Add()does not persist. It returns a new, unsaved object. Set the properties, then callSave(). A script that forgetsSave()looks as though it worked and changes nothing.
Not in a published release. Read from hMailServer.idl at master 540347500: six new
interfaces, which make 100, and 502 more HRESULT declarations than 6.3.3's 1,375. An
early-bound client built against 6.3.3 keeps working, because every member is appended;
recompile it to reach the new ones, and on a hand-copied installation register the server
again, elevated, so the new interfaces are known to COM. Late-bound scripts need nothing.
| Interface | Members added in the tree after 6.3.3 (appended, so the vtable order of 6.3.3's members is kept) |
|---|---|
Application |
Roles, CurrentRole, CurrentPermissions, RoleAssignments, AssignRole, ResetPassword, UnlockAccount (administrator roles) |
Settings |
RemoteDomainPolicies; the breach check (BreachCheckEnabled, BreachCheckUrl, BreachCheckMinimumCount, BreachCheckUnreachableAction, TestBreachCheckEndpoint); the quarantine digest (QuarantineDigestBaseUrl, QuarantineDigestLinkHours); bounces (BounceProcessingEnabled, BounceHardBounceThreshold, BounceMailboxFullIsHard, BounceSuppressionAction, BounceSuppressionExpiryDays, BounceReadDeliveryReports, BounceCountRetentionDays, BounceReportWindowDays); Web Push (WebPushEnabled, WebPushSubject, WebPushVapidPublicKey, GenerateWebPushKeys); QuotaGracePercent, QuotaSendStopPercent; OpenPGP publishing (OpenPGPWebKeyDirectoryEnabled, OpenPGPKeyMaximumBytes, OpenPGPKeysPerAccount, OpenPGPRequireAddressInUserId); webhooks (WebhooksEnabled, WebhookMaxAttempts, WebhookMaxPerMinute, WebhookRetentionDays); country blocking (GeoIpDatabaseFile, GeoIpDatabaseMaxAgeDays, CountryBlockingExemptRanges, CountryPolicy); session policy (SessionIdleSeconds, SessionLifetimeSeconds, SessionOnePerDevice, SessionBindToAddress); complaints (ComplaintProcessingEnabled, ComplaintMailbox, ComplaintSuppressionThreshold, ComplaintRetentionDays); GetChangedFromDefault, GetSettingDefault; the reputation watch (ReputationWatchEnabled, ReputationWatchLists, ReputationWatchAddresses); spam traps (SpamTrapBanMinutes, SpamTrapBlockDays, SpamTrapHitRetentionDays, SpamTrapTeachSpamAssassin); HelpWikiBaseUrl; outbound shaping (OutboundBackoffMinutes, OutboundBackoffMaxMinutes, MaxOutboundConnectionsPerDestination, MaxOutboundConnectionsPerTenant); a save that applies (IniSettingApplies, IniSettingAppliesAll, PendingIniSettings); profiles (GetProfiles, GetProfile, ApplyProfile, RevertProfile, DiscardProfileSnapshot, AppliedProfile, DeclineProfiles); the attachment sandbox (SandboxEnabled, SandboxSubmitUrl, SandboxVerdictUrl, SetSandboxToken, SandboxTimeoutMinutes, SandboxTimeoutAction, SandboxMaxSizeKB, SandboxTypes, SandboxPollSeconds, SandboxScoreThreshold, SandboxCacheHours, SandboxInternalMail, SandboxMaxFiles) |
Domain |
Rules (a domain's own rules); the out-of-office window VacationDays; the quota policy (QuotaWarningPercent, QuotaWarningSubject, QuotaWarningText, QuotaGracePercent, QuotaSendStopPercent); the domain transforms (ExternalTagSubject, ExternalTagHeader, ExternalTagText, FirstContactTip, DisclaimerEnabled, DisclaimerPlainText, DisclaimerHTML); SenderList, QuarantineDigest, QuarantineDigestHour; DKIM rotation (DKIMRotationEnabled, DKIMRotationIntervalDays, DKIMRotationPropagationHours, DKIMRotationRetentionDays, DKIMRotationKeyBits, DKIMRotationStatus, DKIMRotationStart, DKIMRotationCheck, DKIMRotationSwitch, DKIMRotationRetire, DKIMRotationCancel); CountryPolicy; OpenPGPPublishKeys, OpenPGPPublishNewAccounts; WebhooksEnabled, WebhookSubjects; ReportMailbox; RestrictedDomains; impersonation protection (ImpersonationAction, ProtectedPersons, ProtectedPartners); SpamTraps, AddSpamTrap, RemoveSpamTrap; levels (SettingOverride, EffectiveSetting, EffectiveSettingSource) |
Account |
mailbox move (MoveTo, InUseReason, LastMove); Web Push (PushSubscriptionCount, RemovePushSubscriptions); the out-of-office policy (VacationMessageDays, VacationInternalSubject, VacationInternalMessage, VacationMessageInternalOnly); ExportArchive; devices (GetDevices, RevokeDevice); its role (RoleName, RoleDomains, RolePermissions); OpenPGPPublish, OpenPGPKeys; recovery (RecoveryAddress, which answers the address masked, RecoveryAddressVerified, RecoveryCodesUnused, ClearRecoveryAddress, ClearRecoveryCodes); the directory (DirectoryTitle, DirectoryDepartment, DirectoryPhone, DirectoryMobile, DirectoryOffice, DirectoryOptOut, DirectoryNote, DirectoryPhoto, DirectoryPhotoType); ZeroAccess; RestrictedDelivery; levels (SettingOverride, EffectiveSetting, EffectiveSettingSource); ExternalId (the identity provider's id for an account SCIM made) |
Utilities |
RunReputationCheck, GetReputationReport; GetSpamTrapHits, ClearSpamTrapHits; GetJournalRules, SaveJournalRule, DeleteJournalRule; GetSessionPolicy, SetDomainSessionPolicy; LookupCountry, GeoIpDatabaseStatus; RunDkimRotation; GetWebhookSubscriptions, SaveWebhookSubscription; GetSuppressionList, AddSuppression, RemoveSuppression, GetBounceCounts, ClassifyBounce, RunBounceMaintenance; GetComplaints, GetComplaintRates; ImportOpenPGPKey, RemoveOpenPGPKey; GenerateWebPushKeyPair, WebPushDecrypt; GetAuditTrail, VerifyAuditTrail, RevertAuditedChange; EraseAddressLogLines; GetSetupChecklist; GetSampleDomain, CreateSampleDomain, RemoveSampleDomain; MoveMailbox, GetMailboxMoves; BackupArchiveIsEncrypted
|
AntiSpam |
SenderList, SendQuarantineDigests, SendQuarantineDigestsAtHour; greylisting (GreyListingBypassEntries, GreyListingSlideOnEarlyRetry, GreyListingAbuseThreshold, GreyListingAbuseLockMinutes); SMTP policy (UseSPFSoftFailScore, UseSPFTempErrorScore, UseSPFPermErrorScore, SPFTempErrorDefer, RejectMultiRcptNullSender, BanMultiRcptNullSender, BanMultiRcptNullSenderHours, DomainPolicyList, AddDomainPolicyListEntry, RemoveDomainPolicyListEntry) |
Logging |
syslog (SyslogEnabled, SyslogHost, SyslogPort, SyslogTransport, SyslogFacility, SyslogMinimumSeverity, SyslogLogSMTP, SyslogLogPOP3, SyslogLogIMAP, SyslogLogApplication, SyslogLogTCPIP, SyslogLogDebug) |
DistributionList |
Membership (a list that defines itself); the list manager (Managed, SubscribePolicy, Archive, ArchiveDays, Digest, DigestHour, ReplyToList, SubjectPrefix, Footer, Verp, Owner, SendDigest) |
DistributionListRecipient |
Delivery, HardBounces, SoftBounces, Confirmed, ResetBounces
|
BackupManager |
LoadEncryptedBackup; restoring one mailbox or one message (RestoreMailbox, RestoreMessage, DescribeMailbox, MailboxRestoreStatus) |
BackupSettings |
EncryptionPassphrase (write-only), EncryptionPassphraseSet
|
RemoteDomainPolicies, RemoteDomainPolicy (new) |
the collection (Item, Count, Add, ItemByName, ItemByDBID, DeleteByDBID, Refresh, PolicyForDomain, ClearVerificationCache, VerificationCacheSize) and one policy (ID, DomainName, Description, Active, OutboundTls, RequireInboundTls, MaxMessageSizeKB, MaxConnections, MaxMessagesPerMinute, AllowAutomaticReplies, AllowForwarding, the six Callout* properties, Save, Delete, and gateway encryption's Encrypt, EncryptKey, EncryptFallback, EncryptKeyFingerprint, EncryptKeyExpires, EncryptKeySummary) |
SenderListEntries, SenderListEntry (new) |
the collection (Item, Count, Add, ItemByName, ItemByDBID, DeleteByDBID, Refresh, Clear) and one entry (ID, Value, Blocked, Reason, Expires, ExpiresTime, Scope, Save, Delete) |
GreyListingBypassEntries, GreyListingBypassEntry (new) |
the collection (Item, Count, Add, ItemByName, ItemByDBID, DeleteByDBID, Refresh) and one entry (ID, Pattern, MatchOn, RequireSPF, RequireDKIM, RequireDMARC, Note, Save, Delete) |
Enumerations gain hStateDraining (5) in eServerState; the rule criteria eFTAttachmentName (9), eFTSensitiveContent (10), eFTAttachmentType (11), eFTExternalRecipient (12) and eFTContent (13); the rule actions eRAAddDisclaimer (11), eRAReject (12), eRARequireTLS (13), eRAQuarantine (14) and eRAWarnSender (15); and two new enumerations for remote domain policies, the outbound TLS level (eRTNone 0, eRTEncrypted 1, eRTVerified 2, eRTDane 3) and gateway encryption (eREEncryptNone 0, eREEncryptOpenPgp 1, eREEncryptSmime 2). Several features have no COM surface at all and are REST only: SCIM provisioning, SAML and OpenID Connect sign-in, passkeys, the S/MIME authority, the send API, migrations, IP warm-up, the attachment sandbox's held messages, outbound shaping's state and the list of settings with levels; the API key store has never been COM-visible.
$app = New-Object -ComObject 'hMailServer.Application'
$app.Authenticate('Administrator', 'your-admin-password') | Out-Null
# Add a domain
$domain = $app.Domains.Add()
$domain.Name = 'example.com'
$domain.Active = $true
$domain.Save()
# Add an account
$account = $domain.Accounts.Add()
$account.Address = 'alice@example.com'
$account.Password = 'a-strong-password'
$account.Active = $true
$account.MaxSize = 1000 # MB, 0 = unlimited
$account.Save()
# Report on every mailbox
foreach ($d in $app.Domains) {
foreach ($a in $d.Accounts) {
'{0,-40} {1,8} MB used' -f $a.Address, [math]::Round($a.Size, 1) # Size is already in MB
}
}Output shape:
alice@example.com 12.4 MB used
bob@example.com 301.9 MB used
postmaster@example.com 0.0 MB used
Account.Sizeis megabytes (bytes / 1048576, to three decimals). Dividing it by1MBagain - which older examples do - prints0for every mailbox.Message.Size, by contrast, is kilobytes, and theMessage sizerule criterion is bytes. Three units, three objects; check which one you have.
Find one account without walking every domain:
$domain = $app.Domains.ItemByName('example.com')
$account = $domain.Accounts.ItemByAddress('alice@example.com')Send a message from a script, without an SMTP client. Message is a creatable
coclass in its own right, and Save() on a new one queues it for delivery:
$m = New-Object -ComObject 'hMailServer.Message'
$m.FromAddress = 'postmaster@example.com'
$m.From = 'Postmaster <postmaster@example.com>'
$m.AddRecipient('Alice', 'alice@example.com')
$m.Subject = 'Scheduled maintenance tonight'
$m.Body = 'The server will restart at 22:00.'
$m.Save()(Application.SubmitEMail() is a different thing despite the name: it is a
method with no arguments that pushes messages already sitting in the database
into delivery, for the case where a row was inserted by hand.)
Look at the delivery queue and make it try again now:
$app.Status.UndeliveredMessages # tab-separated text, one row per message
$app.GlobalObjects.DeliveryQueue.ResetDeliveryTime()
$app.GlobalObjects.DeliveryQueue.StartDelivery()Check a Sieve script before you store it, and set one:
$err = $app.Utilities.CheckSieveSyntax($script) # empty string = valid
if ($err) { throw $err }
$account.SieveScript = $script # writes the file; no Save() neededProve what a rule criterion really does, with no message involved:
$app.Utilities.CriteriaMatch('*invoice*', 8, 'Your Invoice 42') # 8 = eMTWildcardRun a backup:
$b = $app.Settings.Backup
$b.BackupDomains = $true; $b.BackupSettings = $true; $b.BackupMessages = $true
$b.Destination = 'D:\backups'
$app.BackupManager.StartBackup()Shipped examples live in hmailserver/Addons/Utilities: StartBackup.vbs,
MakeServiceDependent.vbs, EncryptAllPasswords.vbs, LoadAllMessages.vbs,
RemoveUnusedAccounts.vbs, DecryptBlowfish.vbs.
Not everything is a first-class property. The keys that live in
hMailServer.INI are reachable generically:
$app.Settings.IniSettingNames # every key the server knows
$app.Settings.GetIniSetting('ManageSieveServerPort')
$app.Settings.SetIniSetting('ManageSieveServerPort', '4190')
$app.Settings.DeleteIniSetting('HttpProxy')Most INI settings are read at service start, so a change through this route usually needs a restart before it does anything. The Settings Reference marks which are which.
Two Control Panel pages, both under Maintenance: Advanced, whose Scripting tab turns the engine on and chooses the language, and Event scripts, which edits the file.

| Setting | Where | Default | Notes |
|---|---|---|---|
Settings.Scripting.Enabled |
Maintenance → Advanced → Scripting | off | Nothing fires while this is off |
Settings.Scripting.Language |
Maintenance → Advanced → Scripting | VBScript |
Or JScript. An unrecognised value gives an extension that names no file, so the script is empty and nothing fires |
Settings.Scripting.Directory |
[Directories] EventFolder in the INI |
<install>\Events |
|
Settings.Scripting.CurrentScriptFile |
derived |
<EventFolder>\EventHandlers.vbs or .js
|
One file. There is no include mechanism |
ScriptTimeout |
INI [Settings]
|
60 seconds | 0 disables the watchdog |
ScriptAllowedObjects |
INI [Settings]; editor on Maintenance → Advanced → Scripting |
* |
Section 23.9 |
Settings.Scripting.Reload() re-reads the file, CheckSyntax() compiles it and
returns the error text (empty means clean). The Control Panel's Save & reload
button writes a .bak first, then does both.
sequenceDiagram
participant Srv as Server thread
participant SS as ScriptServer
participant Eng as Script engine
participant Fn as Your handler
Srv->>SS: FireEvent OnAcceptMessage, with the caller line and the objects
SS->>SS: Is scripting enabled - if not, return
SS->>SS: Is OnAcceptMessage declared in the loaded script - if not, return
SS->>SS: Take the script text and the language together, under one lock
SS->>Eng: New engine, then publish HMAILSERVER_CLIENT, HMAILSERVER_MESSAGE and Result
SS->>Eng: Add the WHOLE script file, then one more line calling the handler
Note over SS,Eng: Watchdog armed for ScriptTimeout seconds
Eng->>Eng: Run every top-level statement in the file
Eng->>Fn: Then call your handler
Fn-->>Eng: Sets Result.Value and Result.Message
Eng-->>SS: Returns
SS->>SS: Disarm the watchdog and terminate the engine
SS-->>Srv: The server reads Result and acts
Two consequences of that picture, both of which surprise people:
- The whole file is re-parsed and re-executed for every single event. Code outside a function runs on every message, every connection, every error. Put nothing at the top level except constants and function declarations - no database connections, no file opens, no loops.
- Nothing is shared between events. A fresh engine is created and terminated each time, so there are no globals that survive, and no state you can accumulate in memory. Use a file, or the database, or a header on the message.
Every handler is optional. The server checks, when the script is loaded, which of these names the file declares, and fires only those - so an absent handler costs nothing at all.
| Handler | Called as | Fires when |
Result is read? |
|---|---|---|---|
OnClientConnect |
(HMAILSERVER_CLIENT) |
Any TCP connection is accepted, before a greeting | Yes |
OnHELO |
(HMAILSERVER_CLIENT) |
The SMTP client sent HELO or EHLO | Yes |
OnClientLogon |
(HMAILSERVER_CLIENT) |
An SMTP, POP3 or IMAP client finished a logon attempt - successful or not. Client.Authenticated says which, and Client.Username names who was tried. This is the hook to build auditing or a script-side lockout on |
no |
OnClientValidatePassword |
(HMAILSERVER_ACCOUNT, "password") |
A password is being checked, before the built-in check | Yes |
OnSMTPData |
(HMAILSERVER_CLIENT, HMAILSERVER_MESSAGE) |
The DATA command arrived, before the body | Yes |
OnAcceptMessage |
(HMAILSERVER_CLIENT, HMAILSERVER_MESSAGE) |
The body has been received and is about to be queued | Yes |
OnRecipientUnknown |
(HMAILSERVER_CLIENT, HMAILSERVER_MESSAGE) |
A RCPT TO named an address this server does not host | no |
OnTooManyInvalidCommands |
(HMAILSERVER_CLIENT, HMAILSERVER_MESSAGE) |
An SMTP client sent too many invalid commands | no |
OnDeliveryStart |
(HMAILSERVER_MESSAGE) |
First step of a delivery attempt, before virus scanning | Yes |
OnDeliverMessage |
(HMAILSERVER_MESSAGE) |
After virus scanning and global rules, before the recipient split | Yes |
OnDeliveryFailed |
(HMAILSERVER_MESSAGE, "recipient", "error") |
Once per failed recipient; may fire several times per message | no |
OnExternalAccountDownload |
(HMAILSERVER_FETCHACCOUNT, HMAILSERVER_MESSAGE, "uid") |
A message was fetched from an external POP3/IMAP account. The message argument is Nothing/null when there is no message |
Yes |
OnError |
(severity, id, "source", "description") |
Anything is written to the ERROR log | no |
OnBackupCompleted |
() |
A backup finished | no |
OnBackupFailed |
("reason") |
A backup failed | no |
OnError's severity is 1 Critical, 2 High, 3 Medium, 4 Low, and its id
is the HM<nnnn> number without the prefix. Reporting an error from inside
OnError would fire it again, so the server sets a per-thread guard for the
duration of the dispatch - but a handler that raises a script error inside
OnError is still the one place a runaway is easy to write.
Result is published into the script for the events marked above. Set
Result.Value, and for the refusal cases Result.Message.
| Event | Result.Value |
Effect |
|---|---|---|
OnClientConnect |
1 |
Disconnect immediately, before the greeting |
OnHELO |
1 |
554 5.7.1 Rejected |
2 |
554 5.7.1 <Result.Message> |
|
3 |
453 4.7.0 <Result.Message> - a temporary refusal |
|
OnSMTPData |
1 / 2 / 3
|
Exactly as OnHELO
|
OnAcceptMessage |
1 / 2 / 3
|
Exactly as OnHELO. This is the usual place to reject a message |
OnClientValidatePassword |
0 |
Let the user in without checking the password |
1 |
Refuse the logon | |
anything else (default 2) |
Fall through to the normal check | |
OnDeliveryStart |
1 |
Delete the message; logged as "Action triggered by script subscribing to OnDeliveryStart" |
OnDeliverMessage |
1 |
Delete the message; the same log line, naming OnDeliverMessage
|
OnExternalAccountDownload |
1 |
Delete from the remote server immediately |
2 |
Delete after Result.Parameter days |
|
3 |
Never delete from the remote server |
Everything else - OnClientLogon, OnRecipientUnknown,
OnTooManyInvalidCommands, OnDeliveryFailed, OnError, OnBackupCompleted,
OnBackupFailed - is notification only. Setting Result there does nothing.
OnClientValidatePasswordreturning0is a full authentication bypass for that account, on every protocol. It is the single most dangerous line you can write in an event script. If you use it to bridge to another directory, fail closed: return1when your lookup errors, not0.
HMAILSERVER_CLIENT - Port, IPAddress, Username, HELO,
Authenticated, EncryptedConnection, CipherVersion, CipherName,
CipherBits, SessionID. All read-only. The cipher properties are populated
only on a TLS connection.
HMAILSERVER_MESSAGE - the full Message interface: Subject, From,
FromAddress, To, CC, Date, Body, HTMLBody, Charset, Attachments,
Headers, HeaderValue(name) (read and write), Recipients,
AddRecipient, ClearRecipients, HasBodyType, Flag(eMessageFlag), Size
(KB), State, DeliveryAttempt, Filename, UID, InternalDate, Copy,
RefreshContent, Save.
HMAILSERVER_ACCOUNT (OnClientValidatePassword only) - a freshly loaded
Account, deliberately not the cached one.
HMAILSERVER_FETCHACCOUNT (OnExternalAccountDownload only) - the external
account the message came from.
Result - Value (long), Parameter (int), Message (string).
EventLog - published into every event, whether or not the event has a
Result. EventLog.Write("text") appends a line to
<log directory>\hmailserver_events.log. This is the debugger for event scripts:
there is no console, no breakpoint and no state that survives the call, so a
Write at the top of a handler is how you find out whether it ran at all.
Sub OnClientLogon(oClient)
Call EventLog.Write("logon " & oClient.Username & " from " & oClient.IPAddress & _
" ok=" & CStr(oClient.Authenticated) & _
" tls=" & CStr(oClient.EncryptedConnection))
End Subproduces, one tab-separated line per call:
4812 "2026-09-08 11:04:22.117" "logon alice@example.com from 203.0.113.9 ok=True tls=True"
Sub OnAcceptMessage(oClient, oMessage)
' Tag likely invoice fraud, but do not refuse it - a false positive here
' would lose real mail, and the tag lets a rule or a Sieve script file it.
If InStr(LCase(oMessage.Subject), "urgent invoice") > 0 Then
oMessage.HeaderValue("X-Suspicious") = "possible-invoice-fraud"
oMessage.Save
End If
' Refuse anything from one sender, with a reason the sender can read.
If LCase(oMessage.FromAddress) = "spammer@example.org" Then
Result.Value = 2
Result.Message = "This server does not accept mail from that address."
End If
End SuboMessage.Save is what writes the header back to the file on disk. Without it,
the change is lost when the engine is torn down.
The Control Panel's Insert template... button on the Event scripts page
supplies three ready-made OnAcceptMessage starters: an external antivirus or
DLP call, a webhook, and an HTTP API verdict.
A rule's Run script function action calls any function in the same file:
Function StampReceivedDate(oMessage)
oMessage.HeaderValue("X-Received-Date") = Now()
oMessage.Save
End FunctionIt is invoked as StampReceivedDate(HMAILSERVER_MESSAGE), with Result
published but ignored, and the message is reloaded from disk afterwards, so
anything you save is visible to the rest of the rule set. See
Rules and Sieve.
Event scripts run inside the mail flow, on the thread that is doing the work.
| Situation | What happens |
|---|---|
| The script takes a long time | Everything behind it waits. An OnAcceptMessage that takes two seconds caps this server at one message every two seconds on that connection |
The script exceeds ScriptTimeout (default 60 s) |
A watchdog interrupts the engine. HM5019 at High severity: "The script <event> did not complete within N seconds and was interrupted. An interrupt aborts script execution, but cannot release a handler which is blocked inside a COM call." When it is OnError itself that was killed, the message goes to the log without firing OnError again |
| The script raises an error | The engine reports it; the event's Result keeps whatever it had, which for most events means "carry on" |
| The script file does not compile on reload | HM5710: the previous script stays in force, whole - contents, language and handler flags together. On a cold start it is HM5016 instead: nothing is registered until it is fixed |
| Top-level code in the file hangs | HM5021, and the load is abandoned: "Code outside a function runs when the file is loaded, so it must return promptly. The script has not been loaded." Handler discovery would otherwise run the file another fifteen times |
| An exception escapes the load | HM5017; whatever was loaded before is untouched |
A load is deliberately all or nothing. The handler flags gate
security-relevant events - OnClientLogon and OnClientValidatePassword among
them - so the server must never advertise a handler it cannot run. Keeping the
last known-good script is the conservative choice: a typo in a hot reload must
not silently switch off an anti-spam or a logon hook on a running server.
The advice is old and it is still the advice, for a reason this page can now name: an event script is code running as LocalSystem on the thread that delivers your mail, re-executed from the top for every event, with the power to accept a logon and to delete a message. Try it somewhere else first.
Until this setting existed, CreateObject (VBScript) and new ActiveXObject
(JScript) could instantiate any COM class on the machine:
WScript.Shell runs programs, Scripting.FileSystemObject reads and writes the
disk, ADODB.Connection opens databases. A writable script file was therefore
the same thing as a shell as LocalSystem.
The script engines ask their host before creating an object - the
IInternetHostSecurityManager mechanism Internet Explorer used to sandbox pages -
and ScriptAllowedObjects is the answer the host gives.
[Settings]
; Any class at all - the behaviour before the setting existed, and still the default
ScriptAllowedObjects=*
; Nothing beyond hMailServer's own objects
ScriptAllowedObjects=
; Exactly these, by ProgID or by CLSID in braces
ScriptAllowedObjects=WScript.Shell,MSXML2.ServerXMLHTTP,{0D43FE01-F093-11CF-8940-00A0C9054228}| Value | Meaning |
|---|---|
| absent from the file | Treated as *. A setting introduced by an upgrade must not break the scripts an installation already runs |
* |
Every class |
| empty | No class. The objects the server hands the script - HMAILSERVER_MESSAGE and the rest - are unaffected; those are published, not created |
| a comma-separated list | Only those, matched on the registered ProgID (case-insensitively) or on the CLSID |
Matching is on the ProgID the class registers, so listing the version-independent name also covers the versioned one - both register the same class.
What a refusal looks like. The class fails inside the script with the engine's own can't create object error, 429, which the script can trap like any other failure, and one application-log line names the class and the setting. It is not a crash and not a silent no-op.
The editor is on the Scripting tab of Maintenance → Advanced, pre-filled
with * so that saving the page cannot turn "any object, as shipped" into "none"
behind a running script's back. It applies after a service restart.
The three templates the Control Panel inserts use
WScript.ShellandMSXML2.ServerXMLHTTP. If you tighten this setting, those two are the names to start your list with.
| Symptom | Cause | Fix |
|---|---|---|
| Every property raises "You do not have access to this property / method" | Not authenticated, or authenticated as a domain administrator reaching outside the domain | Check Authenticate returned an object, not $null
|
Authenticate returns $null for the administrator and the password is right |
A second factor is enrolled | Read AdministratorTOTPEnabled, then AuthenticateWithCode
|
| A script that "worked" changes nothing |
Add() without Save(), or a property set on an object that was released before Save()
|
Add Save(). In PowerShell, keep the object in a variable rather than chaining |
| Early-bound C# breaks after an upgrade | The interop assembly is stale | Regenerate it. Members are appended, never reordered, so a regenerate is all that is needed |
| No event ever fires |
Settings.Scripting.Enabled is off; or the language does not match the file extension; or the file declares no handler by an exact name |
Check Settings.Scripting.CurrentScriptFile names the file you are editing, then Check syntax
|
| One event fires and another does not | Handler names are matched exactly. OnErrorLog is not a handler; the name is OnError
|
|
| Events stopped firing after an edit | The reload failed and the previous script is still in force - HM5710 | Read the error log; it quotes the compile error |
| Mail is slow, and the slowness scales with volume | Top-level code in the script file, executed once per event | Move it inside a function |
| HM5019 in the log | A handler hit ScriptTimeout
|
Make the handler faster, or raise ScriptTimeout. An interrupt cannot release a handler blocked inside a COM call |
CreateObject fails with 429 for a class that exists |
ScriptAllowedObjects does not list it |
Add its ProgID, or set *, then restart the service |
| Subject | File |
|---|---|
| Interfaces, coclasses, enumerations, help strings | hmailserver/source/Server/hMailServer/hMailServer.idl |
| Authentication, anonymous administrator, the refusal text |
Server/COM/COMAuthentication.cpp, COMAuthenticator.h
|
The LoadSettings / GetAccessDenied pattern |
any Server/COM/Interface*.cpp
|
| Event dispatch, the per-event re-execution, the watchdog, the all-or-nothing load |
Server/Common/Scripting/ScriptServer.cpp and .h
|
Which events exist and what Result means |
Server/Common/Scripting/Events.cpp, Server/SMTP/SMTPConnection.cpp, Server/Common/TCPIP/TCPServer.cpp, Server/Common/Util/PasswordValidator.cpp, Server/ExternalFetcher/ExternalFetchClientBase.cpp, Server/Common/Application/ErrorManager.cpp, BackupManager.cpp
|
| The object-creation policy and how a refusal is produced |
Server/Common/Scripting/ScriptObjectPolicy.cpp, ScriptSite.h
|
ScriptTimeout, ScriptAllowedObjects, EventFolder
|
Server/Common/Application/IniFileSettings.cpp |
| Shipped example scripts | hmailserver/Addons/Utilities |
hMailServer 6.3.8, the latest release (2 November 2026, schema 6108) · AGPL-3.0-or-later · Repository · Mirror on GitHub · Report a documentation error (a GitLab issue naming the page)
Home — full index
Start here
1. Install and run
- Before You Install
- Installing hMailServer
- Installing on Linux
- Running in a Container
- Profiles and Environment
- The Control Panel
- The Control Deck
- Your First Domain and Mailbox
- Connecting a Mail Client
- The Webmail
- DNS for Your Domain
- Migration
2. Secure it
- Encryption and Certificates
- Database TLS
- Encryption for People
- Stopping Spam
- Stopping Viruses
- Security Hardening
- Secure Defaults
- The Service Account
- Secrets at Rest
- Sign-in and Identity
- Security Policy
3. Operate it
- Monitoring and Health
- The SNMP Agent
- Backup and Restore
- Message Storage
- Compliance and eDiscovery
- Troubleshooting
- Diagnosing Stalled Mail
- Relocating an Installation
- Upgrading hMailServer
- Upgrading Guide
- The Update Feed
- Migrating the Database Backend
- High Availability Runbook
- Warm Standby
- Runbooks Digest
4. Extend it
- Rules and Sieve
- Aliases Lists and Public Folders
- Rooms and Equipment
- Mailing Lists
- Routes and Relays
- Mail In to a Webhook
- Outbound Shaping
- The COM API and Scripting
- Event Scripts
- The REST API
- Command Line
- APIs Reference
5. Contribute to it
- Project Handbook
- Architecture
- Contributing
- Release Process
- Governance
- Assurance Case
- Regression Test Environment
- Fuzzing
- Regulatory Scope
- Third-Party Binaries
Look it up — from any journey
- Settings Reference
- Settings Overview
- Capabilities and Configuration
- Control Panel Reference
- Control Panel Pages
- Ports Reference
- Changes-Since-6210
- Version-History-to-6210
- Glossary
About this wiki
- hMailServer 6.3.8 is the latest release (2 November 2026, schema 6108)
- Pages say since 6.3.4 where a feature is new in it
- AGPL-3.0-or-later
- Repository, the one place the project is worked on
- Its mirror on GitHub, since 18 October 2026, with no Issues
- Questions: a GitLab issue with the Question template, GitHub Discussions Q&A, or the forum
- Report a documentation error: a GitLab issue naming the page