Domains and accounts
Creating and managing tenants, domains, domain verification and ownership proof, accounts, groups, aliases, resources and passwords.
Everything Nixt Server hosts is kept in its directory: tenants, the domains they hold, and the people, groups, aliases and resources in those domains. You manage the directory with versealx-server admin, which calls the admin API over the node’s local socket. Every change is authorised and written to the audit log.
The examples use the vsx shell function from the Quick start. Without it, run sudo -u versealx versealx-server admin --config /etc/versealx-server/versealx-server.toml followed by the same arguments. The server must be running.
How the directory fits together
| Thing | What it is |
|---|---|
| Tenant | The unit of separation. Nobody but the operator sees across tenants, and each tenant’s messages are encrypted with its own key. Most installations have one. |
| Domain | A mail domain, held by exactly one tenant. |
| Principal | Anything with an address in a domain: a user (a person with a mailbox), a group, an alias or a resource. The API calls them all accounts. |
Every tenant and every principal has a number. Tenant numbers are unique on the server; principal numbers are unique within a tenant. init creates tenant 1, named after the first domain, and the administrator as principal 1 in it.
Tenants
List tenants
vsx admin get tenants
{
"items": [
{
"created_at": 1789387200000,
"id": 1,
"name": "example.com",
"suspended": false
}
]
}
Create a tenant
vsx admin post tenants name=example-org
The name must be 1 to 64 characters and unique on the server, whatever its case. The answer is the new tenant, with its id. Add its domains and accounts under that id.
Only the operator may list or create tenants.
Look at or suspend a tenant
vsx admin get tenants/2
vsx admin patch tenants/2 suspended=true
vsx admin patch tenants/2 suspended=false
While a tenant is suspended, other servers trying to deliver mail to its domains are refused with a temporary failure, 452 4.2.2 Mailbox full; try later, so they keep the mail and try again later.
Suspending a tenant and bringing it back are the operator’s alone. While a tenant is suspended, the admin API and SCIM answer its own administrators and provisioning tokens with 403 Forbidden and this organisation is suspended: nothing of it is answered until its operator resumes it; the operator’s requests go on as before.
Domains
Add a domain
vsx admin post tenants/1/domains name=example.org
{
"alias_of": null,
"catch_all": null,
"created_at": 1789387200000,
"name": "example.org",
"verification": "versealx-verify=<token>",
"verified": false
}
- The name can be written in any form, including Unicode; it is stored in its ASCII (A-label) form, so
münchen.exampleandxn--mnchen-3ya.exampleare the same domain. - A name must have at least two labels.
- A domain can be held by only one tenant. Adding one another tenant holds is refused with
409 Conflictandexample.org is already taken. - Who adds the domain decides when it becomes the tenant’s. Added by the operator — over the local socket, with
initor the command line — it is the tenant’s at once. Added by the tenant’s own administrator, it is a claim: the record, its token and its DNS guidance are there to set it up, but the server does not treat the domain as hosted until its ownership is proved by the ownership token or a DKIM record. Until then, mail to it from other tenants or from outside goes where it would have gone had nobody claimed it, and mail from it reaches only the tenant’s own domains. Several tenants may claim the same name; the first whose claim is proved is given it. - A new domain is not verified and has no DKIM keys. Generate keys for it before sending mail from it; see DKIM signing.
After adding a domain, print its records with versealx-server dns example.org and publish them.
List and look at domains
vsx admin get tenants/1/domains
vsx admin get tenants/1/domains/example.org
The single-domain answer adds ownership, what DNS was found to say:
{
"alias_of": null,
"catch_all": null,
"created_at": 1789387200000,
"name": "example.org",
"ownership": {
"attempts": 0,
"checked_at": 1789387260000,
"domain": "example.org",
"state": {
"Proved": {
"at": 1789387260000,
"method": "Token",
"trust": "Secure"
}
}
},
"verification": "versealx-verify=<token>",
"verified": true
}
| Field | Meaning |
|---|---|
name | The domain, in ASCII form. |
verified | Whether an administrator has marked the domain verified. |
verification | The ownership token to publish at _versealx-verify.<domain>. |
created_at | When the domain was added, in Unix milliseconds. |
ownership | The result of the ownership proof: see below. |
Domain verification
Two separate things decide what the server does with a domain, and they are shown separately.
The verified mark
verified is what an administrator says. Set it once the ownership token is published:
vsx admin post tenants/1/domains/example.org/verify
The server answers autoconfig, Autodiscover and MTA-STS policy requests only for verified domains. Marking a domain verified does not look anything up, but it does make the ownership check below run straight away.
Ownership proof
ownership is what DNS says. A background check looks for any one of three proofs, each something only whoever controls the domain’s DNS can publish:
| Proof | method | What must be published |
|---|---|---|
| The ownership token | Token | _versealx-verify.example.org. IN TXT "versealx-verify=<token>" |
| A DKIM record | {"Dkim": {"selector": "<selector>"}} | A DKIM record carrying the public half of a key the server holds for the domain. |
| The MX | {"Mx": {"host": "mail.example.com"}} | An MX record naming this server’s hostname. |
An MX record proves a domain the operator added, but not a claim: it names this server and not a tenant, so it would prove every tenant’s claim on the name alike. Publish the token or a DKIM record to prove a claim.
trust records whether the answer was validated with DNSSEC (Secure) or not (Insecure).
state | Meaning |
|---|---|
Pending | Nothing has proved the domain yet. |
Proved | A proof was found, with how and when. |
Lost | The domain was proved, and its proof has since been removed. |
How often it looks:
- A new or pending domain is looked at soon, then less often, backing off to once an hour.
- A proved domain is looked at again once a day.
- When a proved domain’s check fails, it is looked at every hour. Three failures in a row make it
Lost; one failure is treated as a resolver having a bad minute. - The
verifycommand makes it look again at once.
The log records each change: domain proved with the method, and domain no longer proves itself; the record it was proved by has gone.
What ownership decides
A domain that is not proved can still receive mail, and its people can still send to each other. What it cannot do is send to the outside world over SMTP submission: a message from an unproved domain to an address this server does not host is refused with:
550 5.7.1 example.org is not proved to be this tenant's, so mail from it is accepted only for recipients this server hosts, and example.net is not one. Publish the ownership token, a DKIM record or an MX naming this host — `versealx-server dns example.org` prints them — and the domain proves itself within the minute. Nothing has proved it yet.
When the domain was proved before, the last sentence is It was proved before, so a record that was published has been removed.
A tenant whose domains have no DNS of their own can turn this off with the runtime setting outbound.unproved_domains = "allow"; see Runtime settings.
Accounts
Create a user
vsx admin post tenants/1/accounts address=alex@example.com "displayName=Alex Morgan"
{
"addresses": ["alex@example.com"],
"created_at": 1789387200000,
"display_name": "Alex Morgan",
"forward_to": [],
"id": 2,
"keep_copy": true,
"kind": "User",
"send_as": [],
"status": "Active"
}
| Body field | Required | Meaning |
|---|---|---|
address | Yes | The primary address. Its domain must already belong to the tenant. |
displayName | No | A name for people. |
kind | No | user (the default), group, alias or resource. |
members | For a group | A list of principal ids. Each must exist. |
restriction | For a group | Who may send to it: anyone (the default), tenant or members. |
target | For an alias | The id of the principal the alias stands for. |
- The part before the
@is stored in lower case; the domain in its ASCII form. - An address already used in the tenant is refused with
409 Conflictandalex@example.com is already taken. - An address in a domain the tenant does not hold is refused with
409 Conflictandalex@elsewhere.example: that domain is not this tenant's.
A new user has no password and cannot sign in until you set one. The account’s mailboxes — Inbox, Drafts, Sent, Junk, Trash and Archive — are created the first time mail is delivered to it or the person signs in over IMAP, POP3 or JMAP. A hidden Quarantine mailbox is created with them.
List and look at accounts
vsx admin get tenants/1/accounts
vsx admin get tenants/1/accounts/2
| Field | Meaning |
|---|---|
id | The principal’s number. For a user, it is also the mailbox account. |
kind | "User", "Resource", {"Group": {"members": [...], "restriction": "Anyone"}} or {"Alias": {"target": 2, "external": null}}. |
display_name | The name for people. |
addresses | Its addresses, primary first. |
send_as | Other addresses it may send as. |
status | "Active", "Disabled" or "Deprovisioned". |
forward_to | Addresses its mail is also sent on to. |
keep_copy | Whether a copy is kept when mail is forwarded. |
created_at | When it was created, in Unix milliseconds. |
Set or reset a password
vsx admin put tenants/1/accounts/2/password "password=<new password>"
The answer is 204 No Content, which prints nothing. The same rules apply as for any password:
| Rule | Refusal |
|---|---|
At least 14 characters, or the organisation’s passwords.min_length | password must be at least 14 characters |
| At most 256 characters | password must be at most 256 characters |
Must not contain the part of the address before the @, when that part is 3 or more characters, in any case | password must not contain the user name |
| Must not have appeared in a data breach, when the organisation refuses leaked passwords | This password has appeared in a data breach, so it would be among the first an attacker tries. Choose another. |
The audit log records that the password changed, never what it changed to.
To have the person choose their own at their next sign-in, let the server make a temporary password:
vsx admin people reset-password ada@example.com
It shows the temporary password once. Give it to them in person or by phone. When they sign in with it on the server’s pages, they are asked to choose a new password, after their second step if they have one. Until then, mail apps cannot sign in with the temporary password. Over the API, add "mustChange": true to the body.
Resetting a password also signs the person out everywhere: each of their devices asks them to sign in again, with the new password. Their app passwords keep working.
Passwords that have leaked
A password that has appeared in a data breach is among the first an attacker tries. The server can refuse one wherever a password is chosen: an administrator’s reset, the console, SCIM, an invitation, and a person changing their own.
It asks Have I Been Pwned’s range service without sending the password or anything that identifies it. The server works out the password’s SHA-1 hash, sends only the first five of its forty hexadecimal characters, and compares the rest itself against the list of hashes that come back. It asks for padding, so even the size of the answer says nothing. Answers are kept for an hour; nothing of the password is kept.
Each organisation chooses with the passwords.refuse_breached runtime setting, or from the command line:
vsx admin org leaked-passwords strict
| Setting | A leaked password | When the service cannot be reached |
|---|---|---|
off | Taken. | — |
on | Refused. | The password is taken, and the audit line says it was not checked. |
strict | Refused. | New passwords are refused until the check can be made: The leaked-password check could not be made; try again in a minute. |
strict is the default, for every organisation that has not chosen otherwise.
The most common passwords are always refused. Whatever the setting, and on a server that cannot reach the range service, a new password is compared, ignoring case, with the 100,000 most common passwords the UK National Cyber Security Centre published from the same breach data, which the server carries with it. One of them is refused with This is one of the most common passwords, so it would be among the first an attacker tries. Choose another.
Passwords already in use are checked too. After a person signs in with their password, the server checks it again, at most once a day, without slowing the sign-in. When a password has appeared in a breach since it was set, the account is marked to change it: the server’s sign-in pages ask the person to choose a new one before signing them in, and mail apps keep working with the old one until it is changed, so nobody is locked out of their mail. Each finding is a line in the audit log. A new password clears it.
See whose password has been found:
vsx admin org breached-passwords
bo@example.com found in a breach on 2026-10-02; must change it at the next sign-in
vsx admin people show says the same of one person. Over the API the list is GET /api/v1/tenants/{tenant}/breached-passwords, and an account’s details carry password_breached_at in Unix milliseconds. An alert of the kind breached-passwords tells administrators when one is found.
The operator decides whether the server asks at all, and whom. For a server with no internet access, set breach_check = false in [passwords]; versealx-server doctor says whether the service answers.
Disable, deprovision or restore an account
vsx admin patch tenants/1/accounts/2 status=disabled
vsx admin patch tenants/1/accounts/2 status=active
vsx admin delete tenants/1/accounts/2
| Status | Mail to the account | Signing in |
|---|---|---|
active | Delivered. | Allowed. |
disabled | Refused with 550 5.2.1 Mailbox disabled. | Refused. |
deprovisioned | Refused with 550 5.2.1 Mailbox disabled. | Refused. |
delete deprovisions the account (204 No Content); it does not erase it or its mail, so the audit trail keeps its meaning. Set the status back to active to restore it.
An app with a connection open to an account that is disabled or deprovisioned, over IMAP or JMAP, is disconnected within a minute.
Sign someone out everywhere
When a phone is lost, or somebody else may be in an account, end every session it has:
vsx admin people sign-out ada@example.com
Every sign-in made through the server’s own sign-in page ends, and each app that used one asks them to sign in again. That includes the Nixt Office apps, the console, the quarantine page, and any mail app that signed in with the page rather than a password. An app stops at its next request, and one that keeps a connection open, over IMAP or JMAP, is disconnected within a minute. A mail app connected over SMTP or POP3 is asked the next time it connects.
An app that signs in with the password itself signs straight back in, because it still has the password. To shut somebody else out, reset the password as well; a reset signs the account out on its own.
App passwords are not sessions and keep working: a device that uses one signs straight back in. Revoke them separately if a device that held one is gone.
An administrator, a domain administrator for their own domains, a helpdesk, or the person themselves may sign an account out. An auditor may not. The account’s details show when it was last signed out everywhere, as signed_out_at in Unix milliseconds.
The same over the API is POST /api/v1/tenants/{tenant}/accounts/{id}/sign-out.
See and end one session
Every sign-in through the server’s own sign-in page is a session: the Nixt Office apps, the console, the quarantine page, and mail apps that signed in with the page. When one device is lost, end its session alone, and the person’s other devices carry on:
vsx admin people sessions ada@example.com
4 Nixt Mail, Chrome on Android, from 198.51.100.4, last used 2026-10-02 00:00 UTC, since 2026-10-01
1 the console, Safari on macOS, from somewhere unknown, last used 2026-10-01 23:40 UTC, since 2026-10-01 (this one)
Ended in the last hour:
3 some-jmap-app, an unknown device, from 203.0.113.9, ended: a copy of its sign-in was used
Each session shows its number, the app it was made for, the browser and system it runs on (never the raw user-agent text), where it was last used from, and when it started and was last used; the last use is accurate to fifteen minutes. The session you are using is marked. Sessions ended in the last hour are listed below, with why: ended by the person or an administrator, signed out by the app, ended because a copy of its sign-in was used by somebody else, or ended by the organisation’s access rules because it had lasted as long as they allow, its browser closed, or the rules no longer let it in.
vsx admin people end-session ada@example.com 4
The session’s next request is refused, and it must sign in again. An app holding a connection open is disconnected: over JMAP at its next message, over IMAP within a minute. The person, or whoever may reset their password, may end a session. Over the API the list is GET /api/v1/tenants/{tenant}/accounts/{id}/sessions and ending one is DELETE /api/v1/tenants/{tenant}/accounts/{id}/sessions/{session}.
A person can see and end their own sessions without the console, with their password (and their second step, when they have one): POST /account/sessions lists them, beside the mail apps signed in with the password or an app password, and POST /account/sessions/end ends one, or revokes an app password. Asking for the password again means a stolen phone cannot end its owner’s other sessions.
In a browser, the same is a page: open https://mail.example.com/account/sessions and sign in with your address and password, your second step if you are asked for one, or your organisation’s sign-in. The page lists your sessions with the app, the device, where each was last used from and when it began, the ones ended in the last hour and why, and the mail apps signed in with your password or an app password. End ends one session and leaves the others. Sign out everywhere ends every session, this page included. The page’s own sign-in lasts fifteen minutes.
Signing an app out with the OAuth revocation endpoint ends its session too.
Where the organisation’s mail is
vsx admin org where
This lists where the organisation’s mail is stored, where it enters and where it leaves, each as a kind and a country, such as cloud DE. If you have a standby site, it also lists where the standby keeps its copy. If you have other premises with a site, it lists those too, because mail for the mailboxes there rests there. It never shows host names or addresses. The console shows the same in the Where your mail is card on the Organisation page. Over the API it is GET /api/v1/tenants/{tenant}/where.
The countries the organisation’s mail may rest in, and pass through, are its residency. Whoever runs the server sets it, at the organisation’s request or as a condition of the service:
vsx admin org residency --rest DE,AT --transit DE,AT,NL
The organisation’s administrators and auditors can read it (GET /api/v1/tenants/{tenant}/residency); only the operator changes it (PUT). “Where your mail is” names anything outside the residency. Moving the organisation to a node whose store or blob store rests outside it, or to a node that doesn’t say where they rest, is refused before anything is copied.
A blob store of the organisation’s own
An organisation’s message bodies and attachments can be kept in a bucket or directory of their own, apart from the rest of the node’s, for example in a country its residency names. On a node, run:
sudo -u versealx versealx-server tenant storage example.com --blobs s3://example-mail --region eu-central-1 --site frankfurt
--blobs takes an s3:// bucket or a directory. A bucket elsewhere than AWS takes --endpoint <url>. --site names the site the place rests in, or site in [organisation_blobs] does. The organisation’s residency is checked against that site first, and the command is refused before anything is copied when the site is outside its countries or none is named while it has a residency.
The command copies the organisation’s mail there, reads every copy back, and only then switches it. If it stops part way, or a copy does not read back, run it again and it carries on. Once it has switched:
- Every node sends the organisation’s new mail to its own place within 30 seconds, and reads anything not there yet from the node’s place.
- Run the same command again after the time it prints. It copies what arrived in the meantime.
- When a run finds nothing left to copy, the organisation’s mail is read from its own place alone, and the command says so.
A bucket’s credentials come from the environment variables [organisation_blobs] names, never from the command line or the store.
To move the organisation on from a place of its own, run the same command with the new place. Its residency is checked against the new place’s site, its mail is copied there and read back, and while the move finishes, anything not there yet is read from the place it left. --blobs node brings it back to the node’s own place, checked against the site [placement] gives the node’s blob store. As before, run the command again after the time it prints until it says the move is done. A move that is still finishing must be done before another starts. Once a move is done, the copies in the place it left are no longer read. Delete them with --clean, naming the place it left (node for the node’s own):
sudo -u versealx versealx-server tenant storage example.com --clean node --dry-run
sudo -u versealx versealx-server tenant storage example.com --clean node
Each copy is deleted only when the organisation’s current place holds the same file, so the only copy is never deleted. A copy with no match is kept and listed. Only this organisation’s files are touched. It is refused while the organisation is still in that place, or still reading from it. --dry-run lists what would go and deletes nothing. A large clean runs in passes of 1,000; run it again until it says nothing is left. For a bucket the organisation left, add its --endpoint and --region.
The organisation’s administrators and auditors see where its files are kept on the Organisation page’s Where your mail’s files are kept card, with any move under way; with versealx-server admin org storage-place; or with GET /api/v1/tenants/{tenant}/storage/place. It shows the place by name, never its credentials or endpoint, and nothing there moves anything.
A sign-in from somewhere new
When an account is signed in to from a network it has not used in ninety days, its person is sent one message. The message is titled “New sign-in to your account” and names the network, the country its address is in, how the account was signed in to, and when. It contains no link, and it asks the person to open their account page the way they always do. At most one message is sent an hour for each account. None is sent for an account’s first sign-ins, when every place is new. A network is an IPv4 /24 or an IPv6 /48, so a phone moving between addresses of one carrier doesn’t count as new each time. To stop these messages, set sign_ins.notify_new_places to off in the runtime settings.
If it wasn’t them, the person says so at POST /account/sessions/not-me, with their password (and their second step, when they have one). This does three things:
- It signs the account out everywhere.
- It requires a new password before the account’s own password opens anything again.
- It tells the organisation’s administrators at once through the
not-mealert.
Administrators see the new networks in the organisation’s sign-ins, and the new-networks alert tells them when many accounts sign in from new networks in the same hour.
Countries come from IP Geolocation by DB-IP, which the server carries with it, so no address is sent anywhere to be looked up. A country is where an address is registered to be used. For a phone or a VPN that isn’t always where the person is, so it helps a person recognise a sign-in and decides nothing on its own.
Where someone signs in from
Before signing somebody out, see where their account is used:
vsx admin people sign-ins ada@example.com
Each line is one way in, from one place, with one kind of credential. It shows the protocol (imap, submission, pop3, managesieve, dav or jmap), where from, and whether with the password, an app password or a token. It also shows when that combination was last and first seen. An IPv6 address is shown as its network of 64 bits, because a device changes its own address within one. The last time is accurate to within fifteen minutes.
Only sign-ins that succeeded are listed: the fifty places seen most recently, and none older than ninety days. Failed attempts are not listed; see Lockout for how repeated failures lock an account. The console’s list, and the API’s answer under refused, also show where the organisation’s access rules refused the person although their credentials were right.
Anybody who may see the account sees the list, the person included. Over the API it is GET /api/v1/tenants/{tenant}/accounts/{id}/sign-ins.
Sign-ins across the organisation
To look for a place nobody expected, see every account’s sign-ins at once, the most recently seen first:
vsx admin org sign-ins
vsx admin org sign-ins --protocol imap
vsx admin org sign-ins --with 'app password' --limit 200
Each line is one account, one way in, one place and one kind of credential, with when it was last and first seen, as in one person’s list. The API gives each line’s country too, as a two-letter ISO 3166 code. A line marked new network is a sign-in from a network the account had not used before: an IPv4 /24 or an IPv6 /48. --new lists only those. The console’s Sign-ins on the People page shows the same when you choose From networks new to the person. --protocol narrows the list to one way in, and --with to one kind of credential: password, app password or token. It shows 50 lines unless --limit asks for more, up to 500. When there are more, the last line gives the command for the next page, with --after.
Accounts being guessed at
See which accounts are locked out now, and which are collecting failed sign-ins before they are:
vsx admin org lockouts
Locked accounts come first, each with when its lock ends. After them come the accounts with failed sign-ins in the last fifteen minutes, the most failures first, each with how many and when the last one was. Lockout explains when an account locks and for how long.
When somebody locked out has remembered their password, let them back in at once rather than waiting for the lock to end:
vsx admin people unlock ada@example.com
Administrators, helpdesks and auditors see both lists, and a domain administrator sees the accounts in their own domains. Over the API they are GET /api/v1/tenants/{tenant}/sign-ins, with protocol, with, limit and cursor, the value an answer with more to come ends with, and GET /api/v1/tenants/{tenant}/lockouts, with limit. Unlocking is POST /api/v1/tenants/{tenant}/accounts/{id}/unlock.
Networks locked
When failed sign-ins from one network, across many accounts, lock the network (see Lockout), the organisation can see it while the lock lasts:
vsx admin sign-ins network-locks
Each line is a network the organisation’s own people’s failed sign-ins locked, and until when. The server’s operator sees every locked network, including those refused mail for guessing at addresses, and only the operator lifts a lock, on the server itself:
sudo -u versealx versealx-server admin sign-ins network-locks --tenant 0
sudo -u versealx versealx-server admin sign-ins unlock-network 203.0.113.0/24
A network is named as it is listed, or by any address in it. Over the API the list is GET /api/v1/tenants/{tenant}/network-locks, and lifting one is DELETE /api/v1/network-locks/{network}.
Legal hold
When a dispute or an investigation means nothing in a mailbox may be lost, put it on hold:
vsx admin hold set ada@example.com --reason 'Acme v. Example'
vsx admin hold set ada@example.com --reason 'Acme v. Example' --until 2027-06-30
vsx admin hold show ada@example.com
vsx admin hold lift ada@example.com
While a mailbox is held, the person uses their mail as before, and nothing in it is deleted for good. A message deleted from the last folder it was in, however that happened, is kept where nobody is shown it: deleted in a mail app, discarded from quarantine, or taken back out of mailboxes by an administrator. hold show says since when the mailbox is held, why, until when, and how much the hold keeps.
When the hold is lifted, or reaches the day given with --until, what it kept is deleted within the next few minutes.
Only an organisation’s administrators may set or lift a hold, and its auditors may see one. Domain administrators, helpdesks and the person held are not shown it. Over the API it is GET, PUT (with reason, and until in Unix milliseconds) and DELETE /api/v1/tenants/{tenant}/accounts/{id}/hold.
How much a mailbox may hold
A mailbox may have a ceiling in size, in messages, or both, and a warning level: 90% of each ceiling unless you set another.
vsx admin mailbox limit ada@example.com 5GB --messages 200000 --warn 85%
vsx admin mailbox usage ada@example.com
vsx admin mailbox limit ada@example.com none
none takes a ceiling away. mailbox usage shows what the mailbox holds against each ceiling, and when its person was last warned.
- At the warning level, the delivery that takes the mailbox there puts one message from the postmaster in its inbox, with a subject such as
Your mailbox is 90% full. It says how full the mailbox is and how to make room. It comes again at most once a week while the mailbox stays over the level. - At a ceiling, new mail is refused with
452 4.2.2 Mailbox full; try later, so the sender’s server tries again later, and IMAP refuses a copy into the mailbox withOVERQUOTA. Mail the person sends still goes, so they can ask for help. - Lowering a ceiling below what the mailbox holds deletes nothing: the mailbox keeps its mail and takes no more until it is under the line.
- Mail kept by a legal hold counts toward the ceiling, so a held mailbox can fill.
Mail apps see the same numbers: IMAP GETQUOTA reports STORAGE and MESSAGE, and JMAP answers Quota/get (RFC 9425, urn:ietf:params:jmap:quota).
In the console the limits are on the person’s Mailbox card. Administrators set them, and domain administrators for their own domains’ people. Over the API it is GET and PUT /api/v1/tenants/{tenant}/accounts/{id}/quota, with bytes, messages and warnPercent.
The organisation’s storage
An organisation may also have a ceiling of its own: the most all its mailboxes may hold together.
vsx admin org storage 2TB --warn 90%
vsx admin usage storage
usage storage shows what the organisation holds against its ceiling, and its fullest mailboxes first. At the warning level, the organisation’s administrators are told by the organisation-storage alert. At the ceiling, mail for everybody in the organisation is refused with 452 4.3.1 The organisation's mail storage is full; try later until there is room.
The total is kept as each message is stored or deleted, and checked against a full recount every day; a difference is corrected and noted in the audit log. In the console it is Settings › Storage, and Usage shows the fullest mailboxes. Administrators of the organisation set it, and auditors read it. Over the API it is GET and PUT /api/v1/tenants/{tenant}/quota.
Sending limits
Each account may send 1,000 messages an hour, 500 recipients a message and 10,000 recipients a day, unless you set other limits for it. A recipient is an envelope recipient, so a group counts once. When a limit is reached, the next message is refused and the refusal says which limit. Each refusal is in the audit log, once an hour for each person.
See what someone has sent in the last hour and the last day, give them limits of their own, or put them back on the defaults:
vsx admin sending show ada@example.com
vsx admin sending set ada@example.com --per-hour 200 --per-day 2000
vsx admin sending reset ada@example.com
A leaked password usually shows first in what it sends. See who has sent the most in the last day, against their limits:
vsx admin sending top
The list gives the accounts that sent to the most recipients in the last day, then the most messages, with each one’s last hour beside it. It covers the last day only. A domain administrator sees the accounts in their own domains. Over the API it is GET /api/v1/tenants/{tenant}/sending, and one account’s is GET, PUT or DELETE /api/v1/tenants/{tenant}/accounts/{id}/sending.
Sending held for review
A stolen password is used to send spam or phishing, and sending limits only count: whoever has it sends up to them. So each account also keeps a picture of its own sending: each of the last four weeks’ days, and the last hour by the minute. These signs are read against it:
| Sign | Holds the account when | Runtime setting |
|---|---|---|
| Volume | It has sent to more recipients today than 10 times its usual day, and to more than 200. Its usual day is the average of its days before today. | sending.hold_multiple (10) |
| New recipients | It has written to more than 100 recipients in an hour that it had never written to before. | sending.hold_new_recipients (100) |
| Spam | The outbound filter has scored its mail as spam 3 times in an hour, counting messages it refused. | sending.hold_spam_scores (3) |
| Complaints | Receivers’ feedback loops have complained about its mail 5 times in an hour. See Complaints from feedback loops. | sending.hold_complaints (5) |
Each is a number the organisation sets in its runtime settings, and 0 turns that sign off. An account younger than a week, or in an organisation watched for less than a week, is held to its sending limits only, so a new person’s first busy day is not held; complaints hold it from its first day. A dormant account that suddenly sends hundreds of messages is held.
When a sign fires, the account’s sending is on hold. What it sends is still accepted, so its app does not keep retrying: SMTP answers 250 2.0.0 Accepted and held for review by your administrator, and a JMAP submission’s delivery status says the same. The messages wait on the queue, and their trace says they were held and by which sign. Mail to colleagues in the organisation is delivered as usual, unless the spam sign fired. The organisation’s administrators are told by the sending-held alert, and the hold is a line in the audit log.
See whose sending is held:
vsx admin sending held
ada@example.com held since 2026-10-02 14:05 UTC: it sent to 2340 recipients today, more than the 400 it is held to (its usual day is 40)
held: 18 messages to 2140 recipients
"Invoice overdue" to a@example.net, b@example.org, c@example.net
signed in over submission from 203.0.113.9 with password, last at 2026-10-02 14:04 UTC
Each account shows the sign that fired, how much is held, the subjects and first recipients of a sample, and where the account signed in from in the last day. Only headers are shown, never what a message says. Then decide:
vsx admin sending release ada@example.com
vsx admin sending discard ada@example.com
release sends everything held and lifts the hold, and the account is watched from there as before. discard deletes what is held, unsent, and reports what it deleted; the account stays on hold until it is released. If the account was taken over, secure it first.
Mail a person scheduled to go later is held here too, when it falls due after their account could no longer sign in: suspended or removed, or signed out everywhere, as a password reset does. It is listed under the person, marked Scheduled, due after sign-in stopped, and release and discard decide it with the rest. Deleted, a message scheduled from a JMAP app goes back to the person’s Drafts.
A sender that is meant to send a lot, such as a newsletter, can be exempted from the volume signs. The spam sign still holds it.
vsx admin sending exempt news@example.com
vsx admin sending watch news@example.com
watch ends the exemption. In the console, Mail flow › Held for review lists every account on hold, with Release and keep watching, Delete what’s held and Secure the account. A person’s page shows their hold, and offers Let them send a lot for the exemption.
The organisation’s administrators, and domain administrators for their own domains, see and decide; auditors see. Helpdesks are not shown what is held, because it shows what people’s mail is about. Over the API the list is GET /api/v1/tenants/{tenant}/sending/held; deciding is POST /api/v1/tenants/{tenant}/accounts/{id}/sending/release or /discard, and the exemption is PUT or DELETE /api/v1/tenants/{tenant}/accounts/{id}/sending/exempt.
Secure an account that was taken over
When an account has been taken over, secure it in one step:
vsx admin people secure ada@example.com
Ada Lovelace (ada@example.com) is secured:
signed out everywhere
1 app password revoked
a new password is required before theirs opens anything
their running Sieve script "main" is turned off, and kept:
"extra", saved at 2026-10-02 13:10 UTC, sends mail on to drop@example.net
forwarding outside the organisation asked for in the last day is refused: inbox@example.net
Forwarding an administrator set stays: to ada.home@example.org.
Give them a new password, or an invitation, where they can be reached another way: whoever took the account may know the one it has.
It does four things at once:
- Signs the account out everywhere, closing open IMAP and JMAP connections.
- Revokes all its app passwords.
- Requires a new password before the account’s password opens anything again.
- Turns off forwarding added in the last day. The person’s running Sieve script is turned off when it, or a script it includes, was saved in the last day and sends mail on; the script is kept for somebody to read and turn back on. Every forward outside the organisation the person asked for in the last day is refused, whether it was waiting for approval or already going. Forwarding an administrator set is the organisation’s own, and is left as it is.
The steps and what each changed are one line in the audit log. Whoever may reset the account’s password may secure it: the organisation’s administrators, a domain administrator for their own domains, and a helpdesk. In the console it is Secure the account, on the person’s page and on Held for review. Over the API it is POST /api/v1/tenants/{tenant}/accounts/{id}/secure.
App passwords
An app password lets one device or app sign in without the person’s own password. It is made for the protocols it may use, and it can be revoked on its own. A person makes their own; nobody makes one for somebody else, because whoever holds one can read the account’s mail.
See someone’s app passwords, and revoke one or all of them:
vsx admin people app-passwords ada@example.com
vsx admin people revoke-app-password ada@example.com 3
vsx admin people revoke-all-app-passwords ada@example.com
The list shows each app password’s number, the name of the device it was made for, the protocols it opens (imap, smtp, pop3, jmap, managesieve, dav), the day it was made and when it was last used, to within a quarter of an hour. One that has never been used says so: it is usually the first to revoke. It never shows the password itself. Once one is revoked, the device that used it can sign in no more, and if it is connected over IMAP it is disconnected within a minute. Apps signed in any other way are left alone.
Anybody who may see the account sees the list. An administrator, a domain administrator for their own domains, a helpdesk, or the person may revoke.
A person makes their own on the server’s page, https://<mail server>/account/app-passwords, which also offers a signed profile for an iPhone, iPad or Mac: see An app password, and an iPhone or Mac profile. An app can make one with the person’s own credentials over the API, naming the device and the protocols it opens:
POST /api/v1/tenants/1/accounts/2/app-passwords
Content-Type: application/json
{"name": "Ada's phone", "scopes": ["imap", "smtp"]}
The answer carries the secret, shown this once. An account keeps at most 20 app passwords.
Forwarding outside the organisation
Mail forwarded on to an address outside the organisation is the first thing somebody who has taken over an account sets up, and it keeps working after the password is changed. So a person’s forwarding outside goes only once the organisation allows it. Every destination outside that somebody’s mail would go to is recorded, however it was asked for:
- an administrator set it (
forward_toon the account, orvsx admin people forward); - the person asked on their own account doors (
POST /account/forwarding/add, with their password); - their Sieve script redirects there. Saving the script over ManageSieve asks for each such destination, and the save is answered
OK (WARNINGS)saying that the forward waits for approval.
Whether a forward goes is decided by the organisation’s policy, the forwarding.external runtime setting:
| Setting | A destination outside that nobody decided |
|---|---|
approval | Waits for an administrator, unless it is on the list of destinations approved for everybody. |
allowed | Goes. |
off (the default) | Does not go. Only what an administrator set goes. |
vsx admin org forwarding approval
vsx admin org forwarding-destinations add partner.example
vsx admin org forwarding-destinations add accounts@supplier.example
The list, forwarding.destinations, holds whole domains (not their subdomains) and single addresses, at most a thousand. What an administrator set needs nobody’s approval.
See and decide what waits:
vsx admin forwarding requests
vsx admin forwarding approve 3
vsx admin forwarding approve-for-everybody 3 --domain
vsx admin forwarding refuse 4 --reason 'Forward to your work address only'
Each request shows whose mail, where to, how it was asked for, and where the person last signed in from, since a request nobody expected is a sign of a taken-over account. Approving for everybody also puts the address, or with --domain its domain, on the list. A refusal stands whatever the policy later says, and the person is told the reason.
See every forward in force, why it goes, and how many messages it took last week, and stop one:
vsx admin forwarding list
vsx admin forwarding revoke 7 --reason 'Account under review'
A forward that does not go is held: the message is kept here, even where the script would have discarded it, and the message trace says which forward was held and why. The person is told in a notice put straight into their inbox, when something changes and weekly while it stays. Forwards outside count towards the person’s sending limits, and one over them is held. A stricter policy revokes nothing silently: forwards return to waiting and are worked out again at the next message.
The organisation’s administrators, and domain administrators for their own domains, decide; auditors can see. The forwarding-requests alert counts what waits. Over the API: GET /api/v1/tenants/{tenant}/forwarding/requests, POST …/requests/{id}/approve and …/refuse, GET /api/v1/tenants/{tenant}/forwarding and DELETE …/forwarding/{id}. A person sees and withdraws their own with POST /account/forwarding and /account/forwarding/remove.
Automatic replies
Set an automatic reply for somebody who is away and could not set their own:
vsx admin autoreply show ada@example.com
vsx admin autoreply set ada@example.com --text 'I am away until 14 October. For anything urgent, write to team@example.com.' --from 2026-10-01 --until 2026-10-14
vsx admin autoreply stop ada@example.com
| Option | Meaning |
|---|---|
--text | The reply, in plain text, at most 10,000 characters. Required. |
--subject | Its subject, one line. Without it, the server uses its own. |
--from | The first day it is sent, YYYY-MM-DD in UTC. Without it, it starts now. |
--until | The last day it is sent, YYYY-MM-DD in UTC. Without it, it runs until it is stopped. |
This is the same setting a person makes in their mail app. A reply goes once to each sender in a period, and never to mailing lists, bounces or other automatic mail. A reply set here is plain text: if their mail app kept a formatted version, it is replaced. stop turns it off and keeps the words for next time.
Anybody who may see the account reads the reply. An administrator, a domain administrator for their own domains, or the person sets and stops it. A helpdesk does not, because the reply goes out in the person’s name.
The same over the API is GET, PUT or DELETE on /api/v1/tenants/{tenant}/accounts/{id}/vacation.
Sending as another address
A person may always send from their own addresses, and from the address of any group they belong to. To let them send from another address too, such as a shared support@ or a colleague’s, give it to them:
vsx admin people send-as bob@example.com support@example.com
vsx admin people send-as bob@example.com --none
The addresses given replace the ones they had, and --none takes them all away. Each must be in one of the organisation’s domains. A message sent this way looks exactly as if the address’s owner had sent it. To show who really sent it, make the person a delegate who sends on the owner’s behalf instead.
A domain administrator gives only addresses in their own domains. An address that belongs to somebody with an administrative role is given only by an administrator who could give that role, because whoever sends from it speaks for them.
Over the API it is PUT /api/v1/tenants/{tenant}/accounts/{id}/send-as with {"addresses": [...]}.
Delegates
A delegate is somebody who may read another person’s mail, send as them, send on their behalf, or manage their calendars: an assistant for a manager, or a colleague answering for somebody who is away or has left.
vsx admin people delegate ada@example.com bob@example.com --read --send-on-behalf
vsx admin people delegates ada@example.com
vsx admin people undelegate ada@example.com bob@example.com
bob@example.com reads their mail, sends on their behalf
| Option | What the delegate may do |
|---|---|
--read | Read the person’s mail, in every folder, without changing any of it. |
--send-as | Send from the person’s address as if the person had sent it. |
--send-on-behalf | Send from the person’s address, with the delegate named as the sender. |
--calendar | See and change every one of the person’s calendars, and answer invitations for them. See Calendars and scheduling. |
delegate gives that one delegate exactly the options named and leaves everybody else’s as they were. Name at least one option; to take every right away, use undelegate. A delegate is another person in the same organisation. A group, an alias, a room, a shared mailbox, somebody in another organisation, a deprovisioned account, or the person themself is refused.
Reading. The person’s folders appear in the delegate’s own mail app:
- over IMAP, under Other Users/ada@example.com/;
- over JMAP, as a second account in the delegate’s session.
The delegate can open, search and read the messages, but cannot move, delete, flag or file anything, and reading a message does not mark it read for its owner. When the right is taken away, the folders are gone at the delegate’s next request, and an app that is connected and waiting is disconnected within a minute.
Sending on their behalf. The message’s From is the person’s address, and its Sender is one of the delegate’s own addresses. Mail apps show it as sent by the delegate on behalf of the person. A message from the person’s address with no Sender is sent as them, and needs --send-as.
Sending as them. --send-as is the same permission as sending as another address: it gives the delegate the person’s primary address. Taking it away takes back every one of the person’s addresses the delegate was given.
While the person’s account is disabled, for instance after they have left, their delegates still read their mail and send on their behalf. Once the account is deprovisioned, that ends.
Anybody who may look at the account sees its delegates, and so does the person. An organisation’s administrator changes them, and so does a domain administrator for the people in their domains, adding as delegates, and giving or taking back --send-as from, only people in those domains. The mail of somebody with an administrative role is delegated only by an administrator who could give that role. Each change is in the audit log with the list before and after.
Over the API it is GET and PUT /api/v1/tenants/{tenant}/accounts/{id}/delegates:
{
"delegates": [
{"account": 3, "address": "bob@example.com", "read": true, "sendAs": false, "sendOnBehalf": true}
],
"version": 4
}
A PUT sends the whole list with the version it read, each delegate named by account or address. If the list changed in between, it is refused with 409 Conflict: read it again and retry.
Groups
A group is an address that delivers to its members.
vsx admin post tenants/1/accounts address=team@example.com kind=group "members=[2,3]" restriction=tenant "displayName=Team"
restriction | Who may send to the group |
|---|---|
anyone | Anyone. |
tenant | Senders whose address is in one of the tenant’s domains. |
members | Senders whose address belongs to a member. |
A sender the restriction does not allow is refused with 550 5.2.1 Mailbox disabled. Members of a group may send mail from the group’s address. Groups may contain groups, up to 8 levels deep.
Aliases
An alias is another address for an existing principal:
vsx admin post tenants/1/accounts address=sales@example.com kind=alias target=2
Mail to sales@example.com is delivered to principal 2 and runs that person’s Sieve script. Vacation replies are sent only for mail that names one of the person’s own addresses in To or Cc, so mail addressed only to the alias gets none.
Resources
A resource is a room or piece of equipment:
vsx admin post tenants/1/accounts address=room-1@example.com kind=resource "displayName=Meeting room 1"
A resource receives mail into its own mailboxes. It cannot sign in.
Reports addresses
Every domain accepts mail at dmarc@<domain> and tlsrpt@<domain> without an account of that name, so the DMARC and TLS reports your records ask for are not refused. The reports are read and filed; see Email authentication. If you create an account with one of these addresses, it receives the messages as well.
Errors
| Status | Detail | Cause |
|---|---|---|
| 400 | "<name>" is not a domain name | A domain name without a dot, or not a valid name. |
| 400 | `address` must be text or `address` must have a domain | The address is missing or has no @. |
| 400 | `kind` is user, group, alias or resource, not "<value>" | An unknown kind. |
| 400 | `status` is active, disabled or deprovisioned, not "<value>" | An unknown status. |
| 400 | `suspended` must be true or false | A missing or non-boolean value. |
| 400 | "<text>" is not an id | A principal id that is not a number. |
| 400 | an alias needs a target | An alias without a target. |
| 404 | account <id> does not exist | No principal with that id in the tenant. |
| 404 | member <id> does not exist or target <id> does not exist | A group member or alias target that does not exist. |
| 404 | <domain> is not this tenant's | The domain belongs to another tenant, or to none. |
| 409 | <name> is already taken | A tenant name, domain or address already in use. |
See Admin API for the full error format.
Something unclear or out of date on this page? Tell us.