---
title: "The COM API on Windows: object model, Save() since 6.3.4, roles and the Import Tool"
url: https://www.hmailserver.co.uk/topic/28262/the-com-api-on-windows-object-model-save-since-6.3.4-roles-and-the-import-tool
author: "Progressiverobot"
published: 2026-09-27T19:44:13.731Z
updated: 2026-09-27T19:44:13.731Z
category: "Scripting & the COM API"
tags: ["windows","api","administration","upgrade","migration"]
site: hMailServer Community Forum
---

# 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 `serveradmin` role 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 given `domainadmin` with 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 holds `manage_roles` in 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.Save` and `RuleAction.Save` refuse, 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.CheckSieveSyntax` refuses 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 a `Database` object's transaction, every later statement is refused and `Database.CommitTransaction` fails: call `Database.RollbackTransaction` and run the whole transaction again, or stop. See [the 6.3.7 announcement](/topic/28277). 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](/topic/28278).*



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's `SieveScript` and `Settings.SetIniSetting`, and an address saved or deleted through `Route.Addresses` needs no `Save()` of the route. Since 6.3.4 the running server's routes, relays and other shared configuration objects also wait for that `Save()`; 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, `hmctl` and the Control Deck ([hMailServer on Linux: what you get and what you do not](/topic/28189)).

- **One type library**, `hMailServer`, with 94 coclasses in 6.3.3 and 100 in 6.3.4. The root is `Application`, ProgID `hMailServer.Application`.
- **Every interface is a dual `IDispatch` interface**, so late binding works without registering the type library. `CreateObject`, `New-Object -ComObject` and C# `dynamic` all 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 `admintools` component** exists only to register the type library, so that scripts on another machine can administer this one.

```powershell
# 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

```powershell
$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.

| 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 |
| 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, `Authenticate` refuses the administrator outright and `AuthenticateWithCode` is the only way in:

```powershell
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 | `AuthenticateWithCode` checks 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 fifteen |
| An app password is not a COM logon | `Authenticate` and `AuthenticateWithCode` answer an app password as they answer a wrong password; it still opens the mailbox over IMAP, POP3 and SMTP |

**How 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 `COMException` in C#. Up to 6.3.3, seventy-two COM classes answered a refusal as a bare `Exception from HRESULT: 0x800403E9`. Since 6.3.4 a refusal arrives with the server's own sentence.

### The object model

| From `Application` | What is there |
|---|---|
| `Domains` | `Domain`, and under each `Accounts`, `Aliases`, `DistributionLists`, `DomainAliases` and, since 6.3.4, `Rules` |
| `Account` (under `Accounts`) | `Address`, `Password`, `Active`, `MaxSize`, `Size`, `AdminLevel`; `Messages`, `IMAPFolders`, `Rules`, `FetchAccounts`, `AppPasswords`; `SieveScript`, `TOTPEnabled` |
| `Settings` | the server-wide properties and the sub-objects `AntiSpam` (which owns `Quarantine`), `AntiVirus`, `Backup`, `Cache`, `Directories`, `Groups`, `IncomingRelays`, `Logging`, `MessageIndexing`, `PublicFolders`, `Routes`, `Scripting`, `SecurityRanges`, `ServerMessages`, `SSLCertificates`, `TCPIPPorts`; since 6.3.4, `RemoteDomainPolicies` |
| `Rules` | the global rules |
| `Status` | `UndeliveredMessages`, `StartTime`, `ProcessedMessages`, `RemovedViruses`, `RemovedSpamMessages`, `SessionCount` |
| `Utilities` | helpers such as `CheckSieveSyntax`, `CriteriaMatch`, `MakeDependent`, `PerformMaintenance`, `ImportMessageFromFileToIMAPFolder` |
| `Database` | `ExecuteSQL`, `ExecuteSQLWithReturn`, `RequiresUpgrade`, `IsConnected`, `DatabaseType` |
| `BackupManager` | `StartBackup`, `LoadBackup` |
| `GlobalObjects` | `DeliveryQueue`, `MessageTrace`, `Languages` |
| `Links` | `Domain(DBID)`, `Account(DBID)`, `Alias(DBID)`, `DistributionList(DBID)` |
| `Diagnostics` | `DiagnosticResults`, each a `DiagnosticResult` |

**Every collection 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 call `Save()`. A script that forgets `Save()` looks as though it worked and changes nothing. In PowerShell, keep the object in a variable rather than chaining.

### Working examples

```powershell
$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
   }
}
```

**Three units, three objects.** `Account.Size` is megabytes. Dividing it by `1MB` again, which older examples do, prints `0` for every mailbox. `Message.Size` is kilobytes, and the *Message size* rule criterion is bytes.

**Find one account without walking every domain:**

```powershell
$domain  = $app.Domains.ItemByName('example.com')
$account = $domain.Accounts.ItemByAddress('alice@example.com')
```

**Send a message from a script.** `Message` is a creatable coclass in its own right, and `Save()` on a new one queues it for delivery:

```powershell
$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()
```

**The queue, and a backup:**

```powershell
$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:

```powershell
$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.PendingIniSettings` lists the saved ones the server does not run on yet. [Settings: when a save takes effect, and the three levels](/topic/28222) 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 through `Route.Addresses` reaches the running route at once, without the route's own `Save()`, as it always did. A `RouteAddresses` list 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 `HRESULT` declarations 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 |
|---|---|
| `Application` | `Roles`, `CurrentRole`, `CurrentPermissions`, `RoleAssignments`, `AssignRole`, `ResetPassword`, `UnlockAccount` |
| `Settings` | `RemoteDomainPolicies`; `GetChangedFromDefault`, `GetSettingDefault`; `IniSettingApplies`, `IniSettingAppliesAll`, `PendingIniSettings`; the profiles (`GetProfiles`, `ApplyProfile`, `RevertProfile` and the rest) |
| `Domain` | `Rules`; the quota policy; the domain transforms and disclaimer; `SenderList`; the DKIM rotation members; `SpamTraps` |
| `Account` | `MoveTo`; `GetDevices`, `RevokeDevice`; `RoleName`, `RoleDomains`, `RolePermissions`; the nine `Directory` properties; `ExportArchive`; `Kind` (0 a person, 1 a meeting room, 2 a piece of equipment) |
| `Utilities` | among others `GetAuditTrail`, `VerifyAuditTrail`, `RevertAuditedChange`, `GetSetupChecklist`, `MoveMailbox`, `RunDkimRotation` |
| `DistributionList` | `Membership`, and the list manager's members |
| `BackupManager`, `BackupSettings` | `LoadEncryptedBackup`, `RestoreMailbox`, `RestoreMessage`; `EncryptionPassphrase` (write-only), `EncryptionPassphraseSet` |
| New | `RemoteDomainPolicies` and `RemoteDomainPolicy`; `SenderListEntries` and `SenderListEntry`; `GreyListingBypassEntries` and `GreyListingBypassEntry` |

`eServerState` gains `hStateDraining` (5). The rule criteria gain `eFTAttachmentName` (9), `eFTSensitiveContent` (10), `eFTAttachmentType` (11), `eFTExternalRecipient` (12) and `eFTContent` (13), and the rule actions `eRAAddDisclaimer` (11), `eRAReject` (12), `eRARequireTLS` (13), `eRAQuarantine` (14) and `eRAWarnSender` (15).

### Roles over COM, and why API keys are not

Since 6.3.4, authority is a **role**: `serveradmin`, `domainadmin`, `helpdesk`, `auditor` or 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 its `AdminLevel` has 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.CurrentRole` and `CurrentPermissions` say what the session holds. [Administrator roles end to end](/topic/28237) has the permissions, the domains a role reaches and the Control Panel pages each role sees.

**Giving a role over COM** is `Application.AssignRole("helen@example.test", "helpdesk", "example.test")`, with an empty role name to take it away. `Account.RoleName`, `RoleDomains` and `RolePermissions` read 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`, `UnlockAccount` and `GlobalObjects.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.ResetPassword` and `UnlockAccount` answer `No such account.` alike for an account that does not exist and for one the role does not reach. `Settings.ClearLogonFailureList` answers 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.Save` and 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 with `POST /api/v1/apikeys` and `"role":"helpdesk"`. The Control Panel's REST API keys page does not go through COM either: it edits `hMailServerApiKeys.ini` directly.

### What this server has changed from stock

Since 6.3.4, `Settings.GetChangedFromDefault` answers every setting whose value is not the one this version ships with, with the shipped value beside it: the same document, byte for byte, as `GET /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](/topic/28222) says what each entry carries.

### Distribution lists and the directory

**A list that defines itself.** Since 6.3.4, `DistributionList.Membership` is 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 set `domain`, a `pattern:` 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](/topic/28270) 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 by `Account.Save()` with the rest. A change of the opt-out through `Account.Save()` is recorded on the audit trail as `directory-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 through `Utilities.ImportMessageFromFileToIMAPFolder`, reads a Maildir with its flags, and creates accounts in bulk from a text file. [Bringing mailboxes in](/topic/28230) 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, `hmctl` or 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](https://gitlab.com/hmailserver/hmailserver/-/wikis/The-COM-API-and-Scripting), sections 23.1 to 23.5, and [APIs Reference](https://gitlab.com/hmailserver/hmailserver/-/wikis/APIs-Reference), sections 8 and 9.
- [Roles.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Roles.md), [ChangedFromStock.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/ChangedFromStock.md), [DistributionLists.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/DistributionLists.md) and [Directory.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Directory.md).
- [Migration.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Migration.md) for the Import Tool, and [Upgrading.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Upgrading.md) for what an upgrader's scripts now see.
- The public documentation's [COM API chapter](https://www.progressiverobot.com/hmailserver-documentation/#23-the-com-api-and-scripting) and its [Import Tool section](https://www.progressiverobot.com/hmailserver-documentation/#338-importing-accounts-and-mail-the-import-tool).
- On this forum: [Upgrading to 6.3.4](/topic/28193), [the 6.3.4 announcement](/topic/28218), [event scripts](/topic/28261), and [the REST API](/topic/28264) for what COM does not reach.
