The COM API on Windows: object model, Save() since 6.3.4, roles and the Import Tool
-
Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) confines an account given the
serveradminrole with a list of domains to those domains over COM as over REST, so it is no longer the server's administrator: give the role with no list to an account that is to administer the whole server, a script's or a Control Panel's account among them. Over COM, one givendomainadminwith a list that leaves out its own domain no longer administers that domain's ordinary accounts; 6.3.4 to 6.3.6 allowed both where REST refused them. A role is given or taken away only by a credential that holdsmanage_rolesin the account's domain and takes in what the account holds now and afterwards, and a credential confined to named domains is refused over COM, as over REST, a domain's rename, a domain alias, a relay host changed to an address that is not public and an action on a domain's or an account's rule that reaches one of the server's own addresses, a configured route or a function of the event script.Rule.SaveandRuleAction.Saverefuse, on a domain's or an account's rule, a Set header value of From, Sender, Resent-From, Resent-Sender or DKIM-Signature and a Reply from an address the owner may not send as, and on a rule of every owner a Set header value of the loop count; one stored before the upgrade is taken without that action, and the application log names it. For a rule of every owner they refuse a Run function that is not a plain function, which one stored before still meets when the rule fires, as since 6.3.4.Utilities.CheckSieveSyntaxrefuses a Sieve script that adds or deletes From, Sender, Resent-From, Resent-Sender, DKIM-Signature, Auto-Submitted or the loop count. Once the database has ended aDatabaseobject's transaction, every later statement is refused andDatabase.CommitTransactionfails: callDatabase.RollbackTransactionand run the whole transaction again, or stop. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) signs a name in over COM, as at every other sign-in, only when it is the account's stored address, compared in lower case after a domain alias and the default domain: a mailbox address spelt otherwise, in more than the case of the letters A to Z, or with white space at either end, may no longer sign in; it fails as a name that does not exist fails, and is counted as a failure. See the 6.3.8 announcement.This topic covers the COM API of hMailServer 6.3.4: what a script connects to and how it authenticates, the object model and the shape every collection shares, working PowerShell examples, the change 6.3.4 makes to when a configuration change takes effect, what 6.3.4 adds to the object model, administrator roles over COM and why API keys are not there, the changed-from-stock document, the COM surfaces of distribution lists and the directory, and the Import Tool. The one thing to know first is that the COM API is Windows-only, and that an object you add to a collection, or an item of one you change, is not stored until you call its
Save(); a few writes need none, among them an account'sSieveScriptandSettings.SetIniSetting, and an address saved or deleted throughRoute.Addressesneeds noSave()of the route. Since 6.3.4 the running server's routes, relays and other shared configuration objects also wait for thatSave(); up to 6.3.3 they changed the moment a property was set.What you are connecting to
hMailServer exposes a full COM API. Almost anything the Control Panel can do, a script can do, because the Control Panel does its work through COM. The exceptions are the pages that edit a file on the server itself: REST API keys (described under roles below), the two directory pages'
[LDAP]section, the sending limits on Server limits & expert settings, the[Database]rows of the Performance page, and the event script; and Discovery, which reads the REST listener because eDiscovery has no COM interface. The COM API and the Control Panel are both Windows-only: a Linux server is administered over REST,hmctland the Control Deck (hMailServer on Linux: what you get and what you do not).- One type library,
hMailServer, with 94 coclasses in 6.3.3 and 100 in 6.3.4. 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 depends on vtable order, so new members are always appended, never inserted. An interop assembly built against 6.3.3 keeps working; recompile it to reach the new members, and on a hand-copied installation register the server again, elevated, so that the new interfaces are known to COM. Late-bound scripts need nothing.
- The installer's
admintoolscomponent exists only to register the type library, so that scripts on another machine can administer this one.
# 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'))Authenticating
$app = New-Object -ComObject 'hMailServer.Application' $account = $app.Authenticate('Administrator', 'your-admin-password') if (-not $account) { throw 'Authentication failed.' }Authenticatereturns anAccountobject, or$nullon failure.Sign in as Gets Administratorwith[Security] AdministratorPasswordfromhMailServer.INIserver administrator A mailbox address whose AdminLevelis 2server administrator A mailbox address whose AdminLevelis 1domain administrator: their own domain only A mailbox address that holds a role (since 6.3.4) what the role's permissions reach, in the domains it reaches An empty administrator password is an open door. If the administrator password is empty, and no administrator TOTP secret is enrolled, every COM session starts already authenticated as a server administrator. This is the historical behaviour.
A second factor changes the call you must make. With
Settings.EnrolAdministratorTOTP()used,Authenticaterefuses the administrator outright andAuthenticateWithCodeis the only way in:if ($app.AdministratorTOTPEnabled) { # readable BEFORE authenticating $app.AuthenticateWithCode('Administrator', $password, $code) | Out-Null } else { $app.Authenticate('Administrator', $password) | Out-Null }Three things about that sign-in change in 6.3.4:
Change, since 6.3.4 What a script sees A one-time code is taken once a TOTP code the server has accepted is refused if it comes again, for the administrator and for an account alike; the refusal is not counted as a wrong code A wrong code is counted AuthenticateWithCodechecks the password first, and a wrong code after a right password counts against a budget of five in fifteen minutes, then no code is taken for fifteenAn app password is not a COM logon AuthenticateandAuthenticateWithCodeanswer an app password as they answer a wrong password; it still opens the mailbox over IMAP, POP3 and SMTPHow a refusal reaches you. If you are not entitled to an object, every member returns an error HRESULT: a terminating exception in PowerShell, a trappable error in VBScript, a
COMExceptionin C#. Up to 6.3.3, seventy-two COM classes answered a refusal as a bareException from HRESULT: 0x800403E9. Since 6.3.4 a refusal arrives with the server's own sentence.The object model
From ApplicationWhat is there DomainsDomain, and under eachAccounts,Aliases,DistributionLists,DomainAliasesand, since 6.3.4,RulesAccount(underAccounts)Address,Password,Active,MaxSize,Size,AdminLevel;Messages,IMAPFolders,Rules,FetchAccounts,AppPasswords;SieveScript,TOTPEnabledSettingsthe server-wide properties and the sub-objects AntiSpam(which ownsQuarantine),AntiVirus,Backup,Cache,Directories,Groups,IncomingRelays,Logging,MessageIndexing,PublicFolders,Routes,Scripting,SecurityRanges,ServerMessages,SSLCertificates,TCPIPPorts; since 6.3.4,RemoteDomainPoliciesRulesthe global rules StatusUndeliveredMessages,StartTime,ProcessedMessages,RemovedViruses,RemovedSpamMessages,SessionCountUtilitieshelpers such as CheckSieveSyntax,CriteriaMatch,MakeDependent,PerformMaintenance,ImportMessageFromFileToIMAPFolderDatabaseExecuteSQL,ExecuteSQLWithReturn,RequiresUpgrade,IsConnected,DatabaseTypeBackupManagerStartBackup,LoadBackupGlobalObjectsDeliveryQueue,MessageTrace,LanguagesLinksDomain(DBID),Account(DBID),Alias(DBID),DistributionList(DBID)DiagnosticsDiagnosticResults, each aDiagnosticResultEvery collection offers the same shape:
Count,Item(index),ItemByDBID(id),Add(),Refresh(),Delete(index)andDeleteByDBID(id), withItemByNameorItemByAddresswhere a name exists. Items haveSave()andDelete().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. In PowerShell, keep the object in a variable rather than chaining.Working examples
$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 = '[email protected]' $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 } }Three units, three objects.
Account.Sizeis megabytes. Dividing it by1MBagain, which older examples do, prints0for every mailbox.Message.Sizeis kilobytes, and the Message size rule criterion is bytes.Find one account without walking every domain:
$domain = $app.Domains.ItemByName('example.com') $account = $domain.Accounts.ItemByAddress('[email protected]')Send a message from a script.
Messageis a creatable coclass in its own right, andSave()on a new one queues it for delivery:$m = New-Object -ComObject 'hMailServer.Message' $m.FromAddress = '[email protected]' $m.From = 'Postmaster <[email protected]>' $m.AddRecipient('Alice', '[email protected]') $m.Subject = 'Scheduled maintenance tonight' $m.Body = 'The server will restart at 22:00.' $m.Save()The queue, and a backup:
$app.Status.UndeliveredMessages # tab-separated text, one row per message $app.GlobalObjects.DeliveryQueue.ResetDeliveryTime() $app.GlobalObjects.DeliveryQueue.StartDelivery() $b = $app.Settings.Backup $b.BackupDomains = $true; $b.BackupSettings = $true; $b.BackupMessages = $true $b.Destination = 'D:\backups' $app.BackupManager.StartBackup()The
[Settings]keys are reachable generically:$app.Settings.IniSettingNames # every key the server knows $app.Settings.GetIniSetting('ManageSieveServerPort') $app.Settings.SetIniSetting('ManageSieveServerPort', '4190')Up to 6.3.3 a change this way usually needed a restart. Since 6.3.4 the keys are kept in the database, and
Settings.IniSettingApplies(name)says whether a save applies a key at once, at a reinitialise or at a restart;Settings.PendingIniSettingslists the saved ones the server does not run on yet. Settings: when a save takes effect, and the three levels has the counts and what becomes of the file's copy.What changed in 6.3.4: a shared object takes effect at Save()
Some collections are not loaded for your COM session alone: the running server reads the very same items while it works. They are the routes and their addresses, the incoming relays, the DNS black lists, the SURBL servers, the remote domain policies, the server messages, the blocked attachments, the SSL certificates and the groups.
Up to 6.3.3, setting a property on one of those items wrote the live object every SMTP, IMAP and delivery thread was reading, which let a session read a string while it was being freed. Since 6.3.4:
- The first property you set makes a copy, held by your COM object, and every later set writes the copy. Reading a property back from the same object reads what you set.
- Nothing is in effect until
Save().Save()writes the copy to the database and the server reads the collection again. - A property set and never saved changes nothing at all. Up to 6.3.3 it changed the running server until the next restart while the database kept the old value. A script that relied on that must now call
Save(). - An object read before the save keeps what it read. After a
Save(), read the item again from its collection (ItemByDBID,ItemByName) to see the saved values. - A route's addresses. A change to an existing address waits for that address's
Save(), like every item here. An address saved or deleted throughRoute.Addressesreaches the running route at once, without the route's ownSave(), as it always did. ARouteAddresseslist read before the route's save is the old route's list, though an address saved or deleted through it still reaches the running route. - A save the database refuses leaves the running item as it was, and your object keeps the refused values, to be corrected and saved again. A remote domain policy is the exception: it is read back as the database holds it.
- A save whose re-read fails still answers success, because the save succeeded, and is reported as error
HM7390: the change is in the database and not yet in effect, until a reinitialise, a restart, or another save.
The Control Panel reads each item fresh, sets, saves and reads again, so it is not affected. The other collections were never shared this way and behave as before: TCP/IP ports, security ranges, the white list, the greylisting lists, blocked senders, sender list entries, and everything loaded for the COM object itself (domains, accounts, aliases, lists, rules, fetch accounts, app passwords, folder permissions).
What 6.3.4 adds to the object model
Six new interfaces make 100 coclasses, and the IDL has 1,884
HRESULTdeclarations against 6.3.3's 1,375. The wiki page lists every member; this is the shape of it.Interface Added in 6.3.4 ApplicationRoles,CurrentRole,CurrentPermissions,RoleAssignments,AssignRole,ResetPassword,UnlockAccountSettingsRemoteDomainPolicies;GetChangedFromDefault,GetSettingDefault;IniSettingApplies,IniSettingAppliesAll,PendingIniSettings; the profiles (GetProfiles,ApplyProfile,RevertProfileand the rest)DomainRules; the quota policy; the domain transforms and disclaimer;SenderList; the DKIM rotation members;SpamTrapsAccountMoveTo;GetDevices,RevokeDevice;RoleName,RoleDomains,RolePermissions; the nineDirectoryproperties;ExportArchive;Kind(0 a person, 1 a meeting room, 2 a piece of equipment)Utilitiesamong others GetAuditTrail,VerifyAuditTrail,RevertAuditedChange,GetSetupChecklist,MoveMailbox,RunDkimRotationDistributionListMembership, and the list manager's membersBackupManager,BackupSettingsLoadEncryptedBackup,RestoreMailbox,RestoreMessage;EncryptionPassphrase(write-only),EncryptionPassphraseSetNew RemoteDomainPoliciesandRemoteDomainPolicy;SenderListEntriesandSenderListEntry;GreyListingBypassEntriesandGreyListingBypassEntryeServerStategainshStateDraining(5). The rule criteria gaineFTAttachmentName(9),eFTSensitiveContent(10),eFTAttachmentType(11),eFTExternalRecipient(12) andeFTContent(13), and the rule actionseRAAddDisclaimer(11),eRAReject(12),eRARequireTLS(13),eRAQuarantine(14) andeRAWarnSender(15).Roles over COM, and why API keys are not
Since 6.3.4, authority is a role:
serveradmin,domainadmin,helpdesk,auditoror a custom one, given to an account for named domains. Nothing changes on the day you upgrade: an account with no assignment holds exactly what itsAdminLevelhas always given it. An account with a role signs in with its own address and password, and COM asks the same question REST asks.Application.CurrentRoleandCurrentPermissionssay what the session holds. Administrator roles end to end has the permissions, the domains a role reaches and the Control Panel pages each role sees.Giving a role over COM is
Application.AssignRole("[email protected]", "helpdesk", "example.test"), with an empty role name to take it away.Account.RoleName,RoleDomainsandRolePermissionsread it back. Custom roles are made over REST (POST /api/v1/roles), and COM has no way to delete a role at all.A help desk and an auditor over COM. A help desk's script has
Application.ResetPassword,UnlockAccountandGlobalObjects.MessageTrace; COM's settings objects answer only the server administrator, so an auditor reads the configuration through the Deck or REST.Refusals that do not give anything away.
Application.ResetPasswordandUnlockAccountanswerNo such account.alike for an account that does not exist and for one the role does not reach.Settings.ClearLogonFailureListanswers the server administrator alone.The question is asked where the write is made, not where the object was reached.
Account.Save,Account.Delete,Account.MoveTo,Application.ResetPassword, the app passwords, and every write to what is in an account (FetchAccount.Save,Rule.Save,IMAPFolder.Save,Message.Saveand the rest) each ask that the credential reach the account and take in its authority. An event script's objects carry no credential and are left as they were.API keys are not a COM object. The API key store has never been COM-visible. The key store (
/api/v1/apikeys) answers to the administrator password and to nothing else, and a key is given a role withPOST /api/v1/apikeysand"role":"helpdesk". The Control Panel's REST API keys page does not go through COM either: it editshMailServerApiKeys.inidirectly.What this server has changed from stock
Since 6.3.4,
Settings.GetChangedFromDefaultanswers every setting whose value is not the one this version ships with, with the shipped value beside it: the same document, byte for byte, asGET /api/v1/settings/changed. Only the administrator password may read it; an API key of any scope and an account holding an administrative role are both refused. The settings topic says what each entry carries.Distribution lists and the directory
A list that defines itself. Since 6.3.4,
DistributionList.Membershipis a string property, read and written like the moderator and bounce addresses, holding a membership query whose accounts are added to the list's own recipients. A domain administrator over COM may setdomain, apattern:in a domain it holds, or nothing; the server administrator may set any form, and a query the server cannot answer is refused when it is saved. Aliases, distribution lists and membership queries has the five forms and how a query is answered.The directory. Since 6.3.4 an account has
Account.DirectoryTitle,.DirectoryDepartment,.DirectoryPhone,.DirectoryMobile,.DirectoryOffice,.DirectoryOptOut,.DirectoryNote,.DirectoryPhoto(base64) and.DirectoryPhotoType(read-only). The row is read at the first get and written byAccount.Save()with the rest. A change of the opt-out throughAccount.Save()is recorded on the audit trail asdirectory-opt-out-changed.The Import Tool
The Import Tool is a Windows program (
Start menu > hMailServer > Import Tool) that works through the COM API: it brings mbox files into an account, handing each message to the server throughUtilities.ImportMessageFromFileToIMAPFolder, reads a Maildir with its flags, and creates accounts in bulk from a text file. Bringing mailboxes in covers each of its jobs.The breach check applies to a COM script. It is off by default. Where it has been switched on, it runs whenever a password is set through COM
Account.Password, the Import Tool's included, and a bulk operation makes one request per account: run a large import against a mirror, or with the check off.What it does not do
- The COM API and the Control Panel do not exist on Linux, and there is no Import Tool on Linux.
- The Import Tool does not read PST files. The file goes into the server's import directory, and REST,
hmctlor the Control Deck start the job. - COM has no API keys and cannot delete a role.
- These 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, eDiscovery, mail in to a webhook and mailbox encryption at rest, among others the wiki lists.
Where to read more
- The COM API and Scripting, sections 23.1 to 23.5, and APIs Reference, sections 8 and 9.
- Roles.md, ChangedFromStock.md, DistributionLists.md and Directory.md.
- Migration.md for the Import Tool, and Upgrading.md for what an upgrader's scripts now see.
- The public documentation's COM API chapter and its Import Tool section.
- On this forum: Upgrading to 6.3.4, the 6.3.4 announcement, event scripts, and the REST API for what COM does not reach.
- One type library,
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login