Skip to content
hMailServer 6.3.8 is the current release, signed 2 November 2026. Windows and Linux, and an in-place upgrade from 5.3 or later, but for the original hMailServer's 5.7.Download 6.3.8 Documentation FAQ
  • 0 Votes
    1 Posts
    134 Views
    ProgressiverobotP
    An organisation that keeps its people in an identity provider wants two things of its mail server: mailboxes that follow the directory, and a sign-in that goes through it. Since 6.3.4 hMailServer has both. SCIM 2.0 at /scim/v2 lets Microsoft Entra ID or Okta make, rename, suspend and remove accounts, with their aliases and distribution lists. Single sign-on, by OpenID Connect or SAML 2.0, opens the webmail at /portal and the Control Deck at /. The one thing to know first: they answer different questions and take different credentials. SCIM answers to an API key whose role holds provision and to nothing else. Single sign-on opens the two pages only: IMAP, POP3 and SMTP still take the account's password and its application passwords. Passkeys and account recovery are covered in the topic on signing in. How the pieces fit Piece Direction What it does LDAP directory authentication This server asks the directory Checks a password when somebody signs in SCIM provisioning The identity provider calls this server Says what changed, as it changes Single sign-on The browser goes to the identity provider and back Signs a person in to the webmail or the Control Deck oidc_auto_provision, saml_auto_provision In passing, at sign-in Makes a mailbox for whoever arrives with a verified address, and never touches it again SCIM is the deliberate version of auto-provisioning. The two work together: SCIM keeps the mailboxes, and single sign-on is how the people it made sign in to them. Where the settings are. The single sign-on settings are the REST API's and the Control Deck's. There is no COM property and no Control Panel page for them, on Windows or on Linux. SCIM: the key and the permission A SCIM request is a request of the REST listener. RestApiPort has to be on and the listener reachable from the identity provider's service over TLS, with a certificate it trusts (RestApiCertificateFile, or the ACME certificate). A body is at most 64 KB, and the key's requests count against the listener's budget of 200 in ten seconds per credential, beyond which the answer is 429 with Retry-After. The key's role must hold provision. The built-in serveradmin role holds it, so every key issued with "scope":"full" provisions; no other built-in role does, so a read-only key is answered 403. The way to issue a key that provisions and does nothing else is a custom role: curl -u Administrator:your-admin-password -H "Content-Type: application/json" \ -d '{"name":"entra-provisioning","description":"Microsoft Entra ID","permissions":"provision"}' \ https://mail.example.com:8443/api/v1/roles curl -u Administrator:your-admin-password -H "Content-Type: application/json" \ -d '{"label":"Entra provisioning","role":"entra-provisioning"}' \ https://mail.example.com:8443/api/v1/apikeys The second request, POST /api/v1/apikeys, answers the hmapi_... token once, and that token is the identity provider's secret. provision is needed for SCIM's reads as well as its writes. Both requests present the administrator password, which works while no second factor is enrolled on it. With one enrolled, sign in once at POST /api/v1/session with the code in X-hMailServer-OTP, and send both requests with the hmailsession cookie and X-Requested-With: hMailServer: since 6.3.4 a one-time code is taken once, so a second request carrying the same code within the same half minute is refused with 401. Every other credential is refused. The administrator password, an account's password and a browser session are answered 401 on /scim/, in SCIM's shape, with WWW-Authenticate: Bearer. A key confined to named domains ("domains":"example.com") provisions those domains and learns nothing about the rest: listings are filtered, an account elsewhere is the same 404 as one that is not there, and an address elsewhere is refused 400 invalidValue. A key may not change or remove an account whose authority it does not take in, and is answered 403. A key whose role holds provision alone takes in no administrator at all: not a domain administrator, not a help desk, not an auditor. A full key confined to named domains takes in an administrator of those domains and nobody else. A full key issued for every domain takes in every account. SCIM: the routes and the mapping Route What it does GET /scim/v2/ServiceProviderConfig What this server supports: patch, filter (200 at most), changePassword and etag; not bulk and not sort GET /scim/v2/Schemas, GET /scim/v2/ResourceTypes The User and Group schemas, listing only what this server keeps; the two resource types GET, POST /scim/v2/Users The accounts: listed and filtered, or one made GET, PUT, PATCH, DELETE /scim/v2/Users/{id} One account: read, replaced, changed, removed GET, POST /scim/v2/Groups The distribution lists: listed and filtered, or one made GET, PUT, PATCH, DELETE /scim/v2/Groups/{id} One list: read, replaced, changed, removed Every answer under /scim/ is application/scim+json, the refusals included. A User is an account. SCIM This server id, userName The account's address. The id is the address, so a rename changes the id. name.givenName, name.familyName The first and last names, sixty characters each displayName The two names composed. Written alone to an account that already has a name, it is accepted and not kept. active The account's switch. false suspends it and deletes nothing. emails The address, type work and primary, and every alias whose value is the address, type other password Write-only. Judged by the password policy, the reuse history and, when it is on, the breach check. externalId The identity provider's own id for the person, at most 255 characters (schema 6096) meta.created and meta.lastModified are not answered, because the tables keep no such times. A Group is a distribution list: id and displayName are the list's address, and members are its members. The sharing groups at /api/v1/groups are folder-permission principals and are not SCIM groups. Attributes the schemas have and this server does not keep, such as title, phoneNumbers and the enterprise extension's department and manager, are accepted and not kept. A misspelt attribute or path is refused by name. SCIM: what each operation does Making a user. POST /scim/v2/Users needs a userName at a domain this server hosts and the key reaches. A body with no password, which is what Entra sends, makes the account with thirty-two random bytes for one, which nobody is ever told. An address something already has is 409 uniqueness. Filters. These and no others: userName eq, userName co, userName sw, externalId eq, emails.value eq and active eq. Any other filter is 400 invalidFilter. count is at most 200, and sortBy and sortOrder are ignored. Groups filter on displayName eq only. Changing. PUT takes the body a POST takes, and an attribute it leaves out is left as it is. PATCH takes a PatchOp, with op in any case, as Entra capitalises it. If-Match is honoured on PUT, PATCH and DELETE; a version other than the current one is 412. A change is judged whole before anything is written. Suspending. active false refuses every sign-in from the next attempt, refuses mail to the account, ends its browser sessions and drops its live IMAP, POP3 and SMTP connections. The mailbox, the aliases and the lists are untouched. Renaming. A new userName renames the account through the mailbox move. It is refused 409 while the mailbox is in use. The id changes, the old id is 404 from then on, and the old address is kept as an alias until a later request that sends emails leaves it out. Every alias of the account is part of emails, including one an administrator made. An identity provider that replaces emails removes the aliases it does not list. Where aliases are managed on this server and not in the directory, do not map emails beyond the work address. Removing. DELETE /scim/v2/Users/{id} removes the account and its mailbox, every alias emails answered for it, and its row in every distribution list. It answers 204. Nothing of it can be put back but from a backup. Entra sends active false straight away and the DELETE thirty days after a user is soft-deleted, so the suspension is the safe first step. Five diagnostics in the error log, HM7220 to HM7224, name what a SCIM write left behind when it failed part way. Every SCIM write is on the audit trail, each row naming key:<id>. Setting up Microsoft Entra ID for SCIM Issue the key as above and note the token. In the Entra admin centre, Enterprise applications > New application > Create your own application > Integrate any other application you don't find in the gallery (Non-gallery). Provisioning > Automatic. Tenant URL: https://<host>:<port>/scim/v2, where the port is RestApiPort. Secret token: the hmapi_... token. Test connection asks for a user that does not exist and expects an empty list, which is what this server answers. Mappings > Provision Microsoft Entra ID Users: map userName from userPrincipalName where that is the mailbox address, or from mail where it is not. emails[type eq "work"].value must be the same address as userName. Keep active, name.givenName, name.familyName, displayName and externalId, and delete the mappings this server does not keep. Mappings > Provision Microsoft Entra ID Groups, for mail-enabled groups that should be distribution lists: map displayName from mail, keep members, and delete the mapping of externalId. Assign the users and groups, and switch Provisioning Status on. Setting up Okta for SCIM Issue the key as above. Applications > Create App Integration > SAML 2.0 or SWA, then on General enable SCIM provisioning. Provisioning > Integration: SCIM connector base URL https://<host>:<port>/scim/v2; Unique identifier field for users userName; Supported provisioning actions: Import New Users and Profile Updates, Push New Users, Push Profile Updates, Push Groups; Authentication Mode HTTP Header, and the token in Authorization. Provisioning > To App: enable Create Users, Update User Attributes and Deactivate Users. Push Groups sends a group under its Okta name as displayName, and here that is the list's address, so a push of Engineering is 400 invalidValue. Name each pushed group with the list's address, or leave Push Groups off. Okta sends emails with the primary address alone, so an alias made on this server is removed by Okta's next update. Scim.md gives no steps for Google Workspace: at the time it was written, a custom application there could not be pointed at a SCIM endpoint. Single sign-on with OpenID Connect Register this server at the issuer as an authorization-code client with PKCE, with the redirect URI <oidc_redirect_base_url>/portal/oidc/callback. Setting What it is oidc_issuer For example https://login.example.com. The discovery document is read from <issuer>/.well-known/openid-configuration and cached for an hour. Empty offers no single sign-on at all. oidc_client_id The client id this server is registered under oidc_client_secret Its secret, stored protected and never emitted. Empty for a public client. oidc_scopes openid email by default oidc_redirect_base_url The address the webmail is reached at from outside. Empty falls back to import_oauth_redirect_base_url. oidc_auto_provision Whether a verified address with no mailbox here gets one. Off. oidc_administrators The addresses that open the Control Deck as the administrator. Empty means single sign-on reaches the webmail only. The flow is GET /portal/oidc/start[?deck=1] to the issuer and GET /portal/oidc/callback back. The callback verifies the id_token: its signature against the issuer's published keys (RS256 and ES256 only), iss, aud, exp, iat and nbf with a minute of skew, and the nonce. It then takes email, and only when email_verified says the issuer verified it. With oidc_issuer set, both sign-in pages draw a Sign in with button naming the issuer's host. What they draw is decided by GET /api/v1/signin-methods, which needs no credential and says nothing about who exists here. With SAML set up it answers saml: true and saml_label, and both pages draw a second button. The callback does not make the session. It mints a one-use ticket, good for two minutes, and the page hands it to POST /api/v1/session, which is where every browser session is made. The ticket is bound to the browser by an hmsso cookie, and POST /api/v1/session refuses a ticket that arrives without it. Mapping. The verified address is looked up as a mailbox. With auto-provisioning off, the default, a sign-in with no mailbox is refused. With it on, a mailbox is made, but only at a domain this server already has, and with a random password nobody is told. For the Control Deck, the address must be in oidc_administrators. An issuer that says somebody is an administrator is not obeyed. Single sign-on with SAML 2.0 For identity providers that speak only SAML 2.0: ADFS above all, and an Entra ID or Okta application set up the SAML way. It ends in the same ticket, the same POST /api/v1/session and the same mapping rules. Setting What it is saml_idp_entity_id The identity provider's entity ID. Empty, the default, offers no SAML sign-in at all. saml_idp_sso_url Its sign-in URL for the HTTP-Redirect binding saml_idp_certificate Its signing certificate in PEM, or two while it rolls its key over. Write-only. saml_sp_entity_id This server's entity ID. Empty means <base URL>/saml. saml_email_attribute The attribute the address is read from when the NameID is not an email address saml_require_signed_assertion Whether the Assertion must carry its own signature. On by default. saml_auto_provision, saml_administrators As the OpenID Connect settings of the same names saml_button_label The words on the button: Sign in with SAML by default The base URL is OpenID Connect's: oidc_redirect_base_url, falling back to import_oauth_redirect_base_url. From it follow the entity ID <base URL>/saml, the ACS URL <base URL>/portal/saml/acs and the metadata at <base URL>/saml/metadata. Identity provider What to enter there What to copy back Microsoft Entra ID Identifier (Entity ID) <base URL>/saml; Reply URL <base URL>/portal/saml/acs; token encryption off Login URL to saml_idp_sso_url; Microsoft Entra Identifier to saml_idp_entity_id; Certificate (Base64) to saml_idp_certificate Okta Single sign-on URL <base URL>/portal/saml/acs; Audience URI (SP Entity ID) <base URL>/saml; Name ID format EmailAddress; Assertion Encryption Unencrypted Identity Provider Single Sign-On URL to saml_idp_sso_url; Identity Provider Issuer to saml_idp_entity_id; X.509 Certificate to saml_idp_certificate ADFS A claims-aware relying party trust, imported from <base URL>/saml/metadata or entered by hand; E-Mail-Addresses sent as E-Mail Address https://<adfs host>/adfs/ls/ as saml_idp_sso_url; the Federation Service Identifier as saml_idp_entity_id; the Token-signing certificate as saml_idp_certificate The flow. GET /portal/saml/start[?deck=1] to the identity provider, with an AuthnRequest, and the hmsaml cookie POST /portal/saml/acs back from it, with the Response GET /portal/saml/finish on from the ACS, where the cookie is asked for GET /saml/metadata this service provider, for the identity provider The ACS takes a form of at most 256 KB; a larger body is answered 413. The signature is verified against the certificates an administrator stored and never a key the message carries. The signature method must be rsa-sha256 or ecdsa-sha256, and SHA-1 is refused. A Response is taken only when it answers a request this server issued, not yet answered, with the same RelayState. The checks refuse signature wrapping, a replayed Response or assertion, a Response nobody asked for and an encrypted assertion; comment injection in the NameID gains nothing, because the address read is the whole of what was signed. A saml_idp_certificate that cannot be read stops every SAML sign-in. The settings refuse one, so it comes only from a database edited by hand; HM7320 in the error log then names the setting to paste again. A SAML identity provider says nothing like email_verified. The attribute the address is read from must be one the directory controls, such as mail or the user principal name, and never one a person can set themselves. This server cannot check that. What it does not do SCIM: no bulk, no sorting and no POST .search (/Bulk, /Me and POST .search answer 501); no enterprise user extension; no groups but distribution lists; not a SCIM client. externalId is shown by nothing but SCIM and COM's Account.ExternalId, which is Windows-only. Single sign-on, both kinds: no logout in either direction, so an account disabled at the identity provider keeps a session it already started until that session's own ceiling ends it. One OpenID Connect issuer and one SAML identity provider. Nothing is enforced at IMAP, POP3 and SMTP. OpenID Connect: no claims mapping. The email claim and nothing else. SAML: no encrypted assertions, no signed requests, no metadata fetch, no artifact binding and no sign-in started by the identity provider. Where to read more The operator documents: Scim.md, PasskeysAndSingleSignOn.md and Roles.md. The wiki: The REST API and APIs Reference. The public documentation: chapter 24, The REST API and chapter 36, The Control Deck and the webmail portal. On this forum: passkeys, recovery and the rest of sign-in are in Signing in: passwords, second factor, passkeys, single sign-on; LDAP sign-in and directory synchronisation are in Active Directory and LDAP sign-in; The Control Deck; the 6.3.4 announcement; and the REST API guide.
  • 0 Votes
    1 Posts
    140 Views
    ProgressiverobotP
    Since 6.3.4 the delivery queue shapes what it sends. It pauses a destination that asked it to slow down, caps how many sessions it holds open to one destination and how many deliveries one tenant has in flight, and holds a new outbound address to a daily ceiling at each large provider. Everything in this topic is new in 6.3.4 except MaxOutboundPerDestinationPerMinute, a fixed per-destination rate that 6.3.3 already has. What to know first: of what this topic covers, two things change behaviour on upgrade without a switch. The back-off is on by default, and a deferral this server chose does not spend a message's retries until the message has been queued for longer than its retry schedule spans; only a failure at a remote does. The second covers every hold in this topic, and so changes an upgraded server that already sets MaxOutboundPerDestinationPerMinute. The connection ceilings, the tenant share and the warm-up do nothing until you set them. The order of the checks For each batch of recipients on one target, the server judges in this order, before any connection is opened: The remote domain policy's largest message: 5.3.4, permanent, measured on the queued message. The destination's back-off window. Held until the window ends; then one delivery goes through as the probe, and any other arriving meanwhile waits a minute. The policy's messages a minute (max_messages_per_minute). The connection ceiling: the policy's max_connections where the policy sets one, otherwise the server-wide MaxOutboundConnectionsPerDestination. The tenant share, MaxOutboundConnectionsPerTenant. The server-wide MaxOutboundPerDestinationPerMinute. DNS: the MX lookup (or the route's host) and MTA-STS; then the IP warm-up's daily ceiling; then, where DANE applies, whether the MX set is DNSSEC-validated. Gateway encryption, for a domain whose policy encrypts. The copy is made here, so a batch that is only going to be deferred is never encrypted. The connection attempts. Every one of 2 to 6 is a deferral with 4.4.5 ("mail system congestion") and a sentence naming the rule. The target each check is keyed by is what the batch is delivered to: the recipient domain of an MX delivery, or the host of a route or a relay, or its address when the route names an IP literal, lower-cased. The back-off: a destination said slow down When a destination answers a delivery with a request to slow down, it gets a back-off window: nothing is sent to it until the window ends, one delivery then probes it, and the same answer doubles the window. Deliveries to every other destination are untouched. What is read as a signal. Reply Read as Code 421, unless an exclusion below applies A signal, whatever the text, at the banner or anywhere after it A 4xx whose enhanced status is 4.7.0, 4.7.28, 4.7.500, 4.7.650, 4.7.652 or 4.3.2 A signal A 4xx whose text, lower-cased, contains one of the phrases below A signal The phrases: rate limit, rate-limit, ratelimit, too many messages, too many connections, too much, unusual rate, unexpected volume, temporarily deferred, temporarily rate, throttl, try again later, server busy, back off, slow down. They are English, and a remote that says the same in another language is read by its codes alone. The table is fixed and cannot be edited. What is deliberately not a signal, each of which wins over the text and over the 421 rule: any 5xx: a refusal is not a request to wait; 4.7.1, which greylisting and policy refusals answer, and any reply whose text says greylist or graylist; 4.2.x: one mailbox, never the destination; 4.5.3: too many recipients for one message; 4.4.x: 421 4.4.2 timeout exceeded is the remote's idle timeout on a slow DATA; a connection that was refused or timed out, a DNS failure, and this server's own deferrals. The window and the probe. The first slow-down reply opens a window of OutboundBackoffMinutes. While it is open a batch for that destination is held with 4.4.5, and the application log says: Delivery to <target> deferred: the destination asked this server to slow down at <time> (<reply>); next attempt after <time>. When the window has passed, the first delivery goes through as the probe. If it is delivered, or fails with anything that is not a slow-down reply, the window is cleared. If it is answered with a slow-down reply, the window doubles, up to OutboundBackoffMaxMinutes. A probe nobody ended - a delivery thread that died with it - goes stale after fifteen minutes and the next delivery takes it over. Setting Default Range Meaning OutboundBackoffMinutes 2 0 to 10080 The first window. 0 turns the back-off off OutboundBackoffMaxMinutes 60 1 to 10080 The ceiling a doubling window stops at The two refuse to cross each other on every surface: to move both, move the ceiling first when raising and the base first when lowering. A relay is one destination. With SMTPRelayer set, or a route through a smart host, one slow-down reply from it holds every message that goes through it for the window. One miss, stated in the document. The delivery client keeps a recipient's first non-fatal reply, so a genuine 421 from a second MX host after a plain 4xx from the first is not seen for that recipient. Connection ceilings and the tenant share Both use the same counting semaphore, and it never blocks: a delivery thread that waited for a slot would be a thread not delivering anybody else's mail. Over the ceiling the batch is deferred one to five minutes. Setting Default Range What it bounds MaxOutboundConnectionsPerDestination 0, off 0 to 1000 Sessions open at once to one target that no remote domain policy sets max_connections for MaxOutboundConnectionsPerTenant 0, off 0 to 1000 Deliveries one tenant may have in flight at once The per-domain ceilings are on the remote domain policy, whose TLS half is in Requiring TLS: Policy field 0 means Beyond it Largest message (KB) no limit of ours Not attempted: no connection is opened, and the message is bounced at once with 5.3.4, naming the policy and both sizes Simultaneous connections unlimited Deferred with 4.4.5 for one to five minutes. The count covers every domain the policy's pattern matches Messages a minute unlimited Deferred with 4.4.5 for one to five minutes. Independent of MaxOutboundPerDestinationPerMinute; both apply A policy's connection ceiling wins over the server-wide one wherever the policy sets it. Who a tenant is. The hosted domain of the envelope sender, with an alias domain resolved to its domain. A sender this server does not host - mail relayed for an IP range or a route, forwarded external mail, the null sender - is the one tenant (external), so relayed mail cannot starve the hosted domains either. The tenant share is not a volume control. It is a share of connections, bounded by MaxDeliveryThreads: a share larger than the thread count is no share at all. The per-account sending limits are the volume control. This server's own delivery reports have the null sender, so they share (external) with relayed mail. Where the settings are. The four new settings are on the Control Panel's Delivery of e-mail page beside MaxOutboundPerDestinationPerMinute (Windows), on the Deck's Settings view, over COM as Settings.OutboundBackoffMinutes, OutboundBackoffMaxMinutes, MaxOutboundConnectionsPerDestination and MaxOutboundConnectionsPerTenant (Windows), and over REST on /api/v1/settings as outbound_backoff_minutes, outbound_backoff_max_minutes, max_outbound_connections_per_destination and max_outbound_connections_per_tenant. In hm_settings the two connection rows are named MaxOutboundConnsPerDestination and MaxOutboundConnsPerTenant, because the column holds thirty characters. A deferral this server chose is not counted When every non-fatal failure of a delivery pass was one of this server's own deferrals, the message's next try is set to the longest deferral of the pass with the tries counter untouched, and the log says so: Delivery deferred by this server's own shaping, not by a remote server. Scheduling it for later delivery in N minute(s); the attempt is not counted against the retry limit. When any recipient of the pass failed at a remote, the counted path runs as it always has. Before this, with a small retry count, a message could be bounced having never been offered to anybody. The hold is bounded. A message queued for longer than the span its counted schedule would have allowed - (retries + 1) times the minutes between tries, from the time it was accepted - takes the counted path from then on, so that a destination that says "slow down" for ever still ends in a bounce: Deferred by this server's own shaping, but it has been queued for N minute(s), longer than the M minute(s) its retry schedule spans. This deferral is counted, so that the message is not held for ever. A held pass is a first pass only once. DKIM and ARC signing, the mirror copy and a rule's policy notice happen once, however many passes a message is held through. Nor does a held pass forward, copy, reply or call a rule's script function again: those four actions run once per counted attempt. IP warm-up Schema 6093. A schedule holds one outbound address to a daily ceiling of recipients per provider, rising day by day along a fixed plan; the queue sends up to the ceiling and holds the rest, usually until the next UTC day. Nothing is held until an administrator makes a schedule. A schedule is about one address, and the address has to be bound. It is the local address a delivery leaves from: a global rule's bind to address action when one applies, otherwise SMTPDeliveryBindToIP (Bind outbound connections to IP, smtp_delivery_bind_to_ip). A delivery that binds no address leaves from whatever the operating system chooses, and no schedule is consulted for it. So a schedule does nothing, even enabled, until the address is configured on the host and one of those binds it. The server can check the setting and nothing else: the listing says in_bind_setting, and the Deck warns when it is false. The provider is decided from the mail host, never from the recipient's domain. A company on Google Workspace is reached through Google's MX hosts, and it is Google that judges the address. The first host the batch will try decides. Provider Key Mail host names Google google google.com, googlemail.com, aspmx.l.google.com Microsoft microsoft outlook.com, hotmail.com, protection.outlook.com, olc.protection.outlook.com Yahoo yahoo yahoodns.net, yahoo.com, aol.com Apple apple icloud.com, apple.com Proton proton protonmail.ch, proton.me Every other provider other everything else, counted together at five times the named providers' ceiling Mail relayed through a smart host that is not one of those names is other, whoever the smart host delivers it to. The plans, in recipients per named provider per day. After a plan's last day there is no ceiling, and the schedule holds and counts nothing. Day of standard Ceiling other 1 50 250 2 100 500 3 200 1,000 4 500 2,500 5 1,000 5,000 6 2,000 10,000 7 5,000 25,000 8 to 10 10,000 50,000 11 to 14 20,000 100,000 15 to 21 50,000 250,000 22 to 30 100,000 500,000 The plans side by side: Plan Length Day 1 Last day Rule standard 30 days 50 100,000 the table above gentle 60 days 25 50,000 half of each ceiling, twice the days fast 15 days 100 200,000 twice each ceiling, half the days Days are UTC days, whatever the server's time zone, and a start later than today is refused. Recipients, not messages. A message to forty people at Gmail is forty to Google. Before a delivery connects, its recipients reserve room out of the day's allowance; when the attempts are over, every recipient that was delivered is added to sent and the reservation is let go. A recipient that was not delivered is not counted. So a provider that refuses the warming address is offered as many recipients a day as the queue has; its slow-down replies are answered by the back-off. The counts are written to hm_warmupcounts at every change, so a restart resumes the day. Local delivery is not counted. The hold. Recipients that do not fit - the last of the batch, not chosen by any priority - are marked 4.4.5. When the day's ceiling was reached, the next try is one to five minutes after the next midnight UTC: SMTPDeliverer - Message 1234: 1 recipient(s) held by the IP warm-up of 192.0.2.10: day 1 of its standard schedule allows 50 recipient(s) a day at Google, and today's have been sent. Next attempt after midnight UTC, in 723 minute(s); the attempt is not counted against the retry limit. How long a backlog is carried. With the default schedule - four retries, sixty minutes apart, a five-hour span - a recipient queued in the morning comes back on each of the next five UTC days and is returned if the fifth still has no room. Send no more from a warming address than the sum of those days' ceilings, or raise the retry count for the duration of the warm-up. Managing schedules. On the Control Deck's Delivery queue page, in the Warm-up box, or over REST. The desktop Control Panel has nothing for it, and there is no COM interface. Route What it does GET /api/v1/warmup Today's UTC date and the schedules, each with its day, in_bind_setting, and per provider sent, deferred and ceiling POST /api/v1/warmup Adds one: address (required), plan (standard by default), started (YYYY-MM-DD, today by default), enabled PUT /api/v1/warmup/{id} Changes plan, started or enabled. The address cannot be changed DELETE /api/v1/warmup/{id} Removes a schedule and its counts GET /api/v1/warmup/{id}/history The counts, newest first, with the day and ceiling in force then Reading takes view_queue and changing takes manage_queue; a credential confined to named domains is refused. A second schedule for one address is 409. Back up settings carries the schedules but not the counts. When the database does not answer, the queue does not stop: schedules that cannot be read (7210) and a day's count that cannot be read or started (7211, 7212) leave the delivery going as though the address had no schedule. A count that cannot be saved (7213) is kept in memory; if the service restarts first, the day can go past its ceiling by that much. Where to see it GET /api/v1/queue/shaping (view_queue, server-wide): the open back-off windows - destination, since, until, the window's minutes, the reply and the host that gave it, how many deliveries it has held, whether a probe is out - and the connections open under each ceiling and each tenant. DELETE /api/v1/queue/shaping/{destination} (manage_queue) clears one window once you have fixed the cause: 200 with {"cleared":true}, 404 when no window is open for that name, and an event on the audit trail. The Deck's Delivery queue shows the same as Slowed destinations, with a Resume button, and the connection counts as they were when the page was drawn. The application log says, for each batch held, which rule held it and until when. What is per process. The windows and the connection counts live in memory. A restart forgets every window, which costs at most one more slow-down reply per destination. Nothing is shared between the nodes of a warm-standby pair. What it does not do The signal phrases, the providers and the plans are fixed. A sixth provider, or a plan of your own, is not possible. No per-tenant volume quota beyond the per-account one: this shares connections, not messages a day. No pool of addresses. A schedule is about one address, and mail that did not fit waits. No reputation feedback. The ceilings rise by the calendar, not by how the providers are answering. No delayed delivery report. A sender learns of a long hold only when it ends in a bounce. A deferral by a rate or a ceiling costs one delivery pass per message every one to five minutes for as long as the backlog lasts. The warm-up counts are not exported as metrics. Not proved by any test, according to IpWarmup.md: room held by deliveries in progress; a later host of the other address family skipped for being reached from another warming address; what a database failure does (7210 to 7213); the audit trail's entry; the configuration backup's round trip of the schedules; and an MX lookup's own host names reaching the classification, since every delivery in the fixture goes through a route. Where to read more Operator documents at the v6.3.4 tag: OutboundShaping.md, IpWarmup.md and RemoteDomains.md. Wiki: Outbound Shaping and Routes and Relays. Public documentation: 26.7 The delivery queue and the retry schedule and 14.1 Sending through a smart host. On this forum: The Control Deck, Upgrading to 6.3.4, Requiring TLS for the remote domain policy's TLS half, and DKIM rotation, DMARC and TLS reports, complaints, bounces and the reputation watch for bounces, complaints and block lists.
  • 0 Votes
    1 Posts
    138 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) refuses a state-changing REST request that carries a credential - HTTP Basic included, which a browser keeps once it has answered the sign-in box - when the browser reports it as cross-site (Sec-Fetch-Site, or an Origin that is not this server's), and holds the password-reset request to the same rule, so another site cannot have a current browser drive it; the webmail and the Control Deck act on their own origin, and scripts, hmctl and the PowerShell module send no such header, so none of them is affected. SMTP, POP3 and ManageSieve now refuse a SASL PLAIN sign-in that asks to act as another account. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) signs a name in to an account only when it is the account's stored address, compared in lower case after a domain alias and the default domain, on every protocol, the webmail, the REST API and COM, and for an OpenID Connect or SAML sign-in, so that the per-name lockout and the compromised-account hold can no longer be got round with another spelling of the name: a client that signs in with another spelling, in more than the case of the letters A to Z, or with white space at either end, may no longer sign in, since such a name fails as a name that does not exist fails. A failed account sign-in at the REST listener through a reverse proxy on the same machine, like a failed CalDAV, CardDAV or Autodiscover sign-in from loopback or an address in WebServicesTrustedProxies, is counted against the name alone (AccountLockoutThreshold, off by default), never towards a ban of the proxy's address. See the 6.3.8 announcement. This topic covers how people prove who they are to hMailServer 6.3.4: what 6.3.3 already had, and what 6.3.4 adds. The one thing to know first is that there are two front doors. The mail protocols - IMAP, POP3, SMTP submission, CalDAV and CardDAV - take a password: the account's own, an app password, or an OAuth2 bearer token where that is switched on. The two pages - the webmail at /portal and the Control Deck at / - take the password as well and, since 6.3.4, a passkey or a sign-in at the organisation's identity provider. Operators upgrading should read the note on passwords with characters above U+00FF first. What 6.3.3 already had Feature What it is A second factor for an account A TOTP secret on the mailbox. Once one exists, the account's password stops being a credential wherever a code cannot be presented alongside it - every mail client, and the webmail's sign-in - and app passwords are what those use App passwords One per device, shown once, revocable one at a time; they work for directory-linked accounts too and are untouched by password expiry A second factor for the administrator Enforced by the server on every surface - COM, REST, the Control Panel The password policy Length, mixed case, digits, symbols, a common-password list, history and maximum age LDAP and Active Directory Passwords checked against the directory; provisioning from it on a preview-then-apply OAuth2 bearer tokens on the protocols SASL XOAUTH2 and OAUTHBEARER validated against an identity provider's published keys There is no self-service enrolment of an authenticator app in the webmail: an administrator sets and clears the second factor, over COM and the Windows Control Panel. Create the app passwords first and enrol the secret second, or every configured client stops working with no way back in. The breach check (since 6.3.4). A password being chosen, never at logon, can be refused because it is in the published corpus of breached passwords; the check is off by default, because it sends five characters of the password's SHA-1 hash off the server, and its settings are in Hardening a public mail server. Four more changes in 6.3.4. The administrator password, which the Windows installer wrote as an unsalted MD5 digest, is written again as PBKDF2 at the first successful administrator sign-in (a file the service cannot write keeps the old value and says so once, HM7494). An account's TOTP secret is sealed at rest once the installation has a stored-secrets key. A mailbox encrypted at rest takes its own password only: no TOTP secret can be enrolled on it, and its app passwords are refused. And a room or a piece of equipment cannot be signed into by any credential. Upgrading to 6.3.4: passwords with characters above U+00FF Before 6.3.4 every password hash was made over a conversion that gave NUL bytes on Windows from the first character above U+00FF on, and ? for each such character on Linux, so a password of Cyrillic, Greek or CJK letters could be opened by any other password of its length. Hashes are now made over UTF-8, which leaves every ASCII password's hash as it was. The consequence, item 24 of the CHANGELOG's upgrade list: a password hash stored before 6.3.4 of a password with a character above U+00FF is refused, to its own password too, and the application log names the account. An administrator sets such a password again, and soon: until then SCRAM-SHA-256, whose client derives its key itself, still opens such a PBKDF2 hash to the NUL bytes (Windows) or question marks (Linux) it was made over, which the server cannot tell from the password; and on Linux such a hash also still opens to the password written with ? for those characters. Every ASCII and Latin-1 password signs in as before. The same release fixed SCRAM-SHA-256 having accepted the account's own password for an account with a second factor enrolled, and a password past PasswordPolicyMaximumAgeDays; each is now a forced failure of the exchange. SCRAM is always the account's own password, since an app password's hash is not one SCRAM can derive from, so such an account cannot sign in over SCRAM at all. Passkeys A passkey (schema 6077) is a WebAuthn credential held by the person's own device, such as a phone or a security key. The private half never leaves it; the server keeps the public half. Passkeys are offered on a stock installation, and nobody has one until they enrol it. Enrolling. The webmail's Security page has a Passkeys card, and the Control Deck has a Passkeys view for the administrator's own. Both ask for the owner's password again, with the one-time code in X-hMailServer-OTP where a second factor is enrolled, or for an account one of its recovery codes in X-hMailServer-Recovery-Code; a request that was itself authenticated with an app password is refused outright. At most ten per owner. POST /api/v1/passkeys/options what the browser needs to make one POST /api/v1/passkeys the credential it made GET /api/v1/passkeys the ones this credential holds DELETE /api/v1/passkeys/{id} remove one Listing and removing are answered while passkeys_enabled is off as well. Signing in. POST /api/v1/passkeys/challenge hands out thirty-two random bytes, kept in memory for five minutes and spent on first use; the assertion goes to POST /api/v1/session, the one place this server mints a browser session, so the sign-in gets the same ceilings and a device-list entry marked passkey. Nobody types an address first. A passkey stands in for the one-time code: user verification - a PIN, a fingerprint or a face - is required at registration and on every assertion. It carries nobody past an inactive account or domain, the compromised-account hold or the domain's country policy. Behind several names. By default the relying party id is the Host header the page was served on. passkey_relying_party set to a registrable parent (example.com) lets a passkey made at mail.example.com work at webmail.example.com. With it set, passkey_allowed_origins must be set too, comma separated as the browser writes them (https://mail.example.com, https://webmail.example.com); until it is, no passkey is registered or taken. Not verified: the attestation statement, so an installation cannot say "only hardware keys of this make". Not in the backup: enrol the device again after a restore elsewhere. Registering and removing a passkey is on the audit trail. Single sign-on Since 6.3.4 a person can sign in at the organisation's identity provider instead, by OpenID Connect or by SAML 2.0 (for a provider such as ADFS that speaks only SAML). Once it is set up, both sign-in pages draw a button: Sign in with and the issuer's host name, or Sign in with SAML. It opens the webmail and the Control Deck and nothing else: IMAP, POP3 and SMTP still take the password or an app password. An identity provider that says somebody is an administrator is not obeyed; the Deck opens as the administrator only for an address listed in oidc_administrators or saml_administrators. There is no logout in either direction, so a person disabled at the provider keeps a session already started here until its own ceiling ends it, which is what the session policies below are for. The settings, the Entra ID, Okta and ADFS entries and the flows are in SCIM provisioning and single sign-on. Account recovery Three independent parts (schema 6076): a recovery address and ten one-time codes, which the person sets on the webmail's Security page, and the forgotten-password flow on the sign-in card. Setting either asks for the account's own password again - never an app password - and the second factor where there is one. What it needs. account_recovery_enabled is on by default and inert on a stock installation: links are written against the server's public base URL - confidential_base_url, falling back to quarantine_digest_base_url and then the import sign-in's import_oauth_redirect_base_url - and with none set POST /api/v1/password-reset answers 503. recovery_reset_link_minutes (30; 5 to 1440) is how long a reset link lives, and recovery_verify_link_hours (24; 1 to 168) how long the confirmation link lives. The recovery address counts for nothing until the link mailed to it is followed, on a page with a button rather than on the GET, which a mail scanner would follow first. It is never answered in full again, to anybody: [email protected] reads as [email protected]. The confirmation is metered at one a minute and six an hour for the address, and again for the asking account, answering 429 with retry_after. The ten one-time codes are made together, shown once and stored hashed. A code goes in X-hMailServer-Recovery-Code wherever an authenticator's code is asked for: POST /api/v1/session, POST /api/v1/me/password, POST /api/v1/me/app-passwords, PUT /api/v1/me/recovery and POST /api/v1/me/recovery/codes. The password is verified before the code is looked at, and the code is spent only once the whole attempt has been accepted. Every owner has a budget that needs no setting: five wrong codes in fifteen minutes, and for the next fifteen every code for that owner is refused - the right one too - with 429 and code second_factor_locked. And a code is taken once (RFC 6238, section 5.2): an accepted TOTP code is refused if it comes again within its window, without being counted as wrong. A script that sent the administrator's password and a code with every request signs in once instead, or uses an API key. Forgotten your password? on the sign-in card posts the address to POST /api/v1/password-reset, which takes no credentials and answers always the same - 202 and one sentence, in the same time - whether or not the address is a mailbox with a confirmed recovery address. Since 6.3.7 a request a browser reports as cross-site (Sec-Fetch-Site, or an Origin that is not this server's) is refused, so another site cannot have a current browser drive it. The limits are one request a minute and six an hour per address, and twenty an hour per client address; a loopback peer, such as a reverse proxy on the same machine, is held to thirty in any sixty seconds instead. The link goes to the confirmed recovery address and nowhere else, is good once, and dies after recovery_reset_link_minutes. Completing it applies the password policy, the reuse history and the breach check, ends every browser session of the account, tells the mailbox and the recovery address, and raises the account.password-reset alert. A used, expired or unknown token answers 410. The administrator can see whether an address is set and confirmed, its masked spelling and how many codes are unused - over REST, on the Deck's Recovery card and the Windows Control Panel's Recovery tab - and can clear either: DELETE /api/v1/accounts/{address}/recovery and DELETE /api/v1/accounts/{address}/recovery/codes. The administrator cannot read the address in full, read a code, set an address on somebody's behalf or generate codes for them. A recovery address is exactly as strong as the mailbox that receives it: a convenience, not a second factor, and an installation that cannot accept that sets account_recovery_enabled to 0. Recovery does not enrol a new authenticator either. Nor does a reset link open encrypted mail: for a mailbox encrypted at rest the link is refused before it is spent and the person is sent to their administrator, and for a zero-access mailbox the password it sets does not open the key, so at the next sign-in the page asks for the previous password or the recovery key. Devices and sessions Every client that has authenticated to an account is one entry of a list: the protocol (imap, pop3, smtp, web, dav, autodiscover or managesieve), the address it came from, the user agent where the protocol carries one, the credential it used, the app password's name where one was used, and when it was first and last seen. IMAP's ID command is deliberately not used, so two mail programs on one machine over IMAP are one entry. At most fifty entries are kept per account. Surface Read Revoke Webmail Settings → Security, the Devices card Revoke beside the entry; the box under the table adds the app password REST, the account GET /api/v1/me/devices DELETE /api/v1/me/devices/{id}, with ?credential=revoke and ?forget=true REST, an administrator GET /api/v1/accounts/{address}/devices DELETE /api/v1/accounts/{address}/devices/{id} COM (Windows) Account.GetDevices() Account.RevokeDevice(id, revokeCredential) Control Deck The Devices view Revoke, with the app password box hmctl hmctl device [email protected] list hmctl device [email protected] revoke ID [--credential] [--forget] PowerShell Get-HmDevice -Address [email protected] Revoke-HmDevice -Address [email protected] -DeviceId ID [-Credential] [-Forget] What revoking does, exactly. A browser session's token is invalidated, so the next request on that cookie is 401; a live IMAP, POP3 or SMTP connection is dropped where it stands; and with credential=revoke the app password is deactivated - not deleted, so the person can put it back if the phone turns up. CalDAV, CardDAV and ManageSieve clients have no connection to drop, so revoking one means revoking its app password. A revoke is not a ban: a client with a credential that still works signs in again and clears the mark. The account's own revoke is refused (403) to a request that came in on an app password, and an administrator's revoke is recorded in the audit trail. The list, hm_signins, is not in the backup. The session policies govern every signed-in browser - the webmail, the portal and the Deck - server-wide and per domain, and are re-read at every request. A mail program's connection is not a session in this sense. Policy Setting Unset means Idle timeout session_idle_seconds 1800 seconds; at most 2592000 (thirty days) Absolute lifetime session_lifetime_seconds 43200 seconds, twelve hours; at most 2592000 One session per device session_one_per_device (a domain's: 0 the server's answer, 1 on, 2 off) Off Bound to the address it began on session_bind_to_address (0, 1 or 2 as above) Off - it ends a phone's session when it moves from wi-fi to mobile hmctl settings set --set session_idle_seconds=900 --set session_bind_to_address=true hmctl domain update example.com --set session_lifetime_seconds=28800 --set session_one_per_device=1 A domain's value wins over the server's where it is set. Nothing changes on the day you upgrade: every policy reads 0, which means the ceilings the server has always had. The cookie, hmailsession, is HttpOnly and SameSite=Strict, and Secure whenever the browser came over HTTPS; a proxy that terminates HTTPS in front of the loopback listener should set X-Forwarded-Proto: https. Browser sessions live in the server's memory, so a restart ends every one, and a password change ends every session and drops every connection of the account. What none of this does It does not reach the mail protocols. A phone's IMAP and SMTP still take the password or an app password, and an account that only ever signs in through its identity provider needs a password set before its person can mint an app password. No password-less accounts: a passkey sits beside the password, and no policy forbids the password. Where to read more Wiki: Sign-in and Identity, Connecting a Mail Client, Security Hardening. Documents: PasskeysAndSingleSignOn.md, AccountRecovery.md, DevicesAndSessions.md, PasswordBreachCheck.md, and item 24 of the 6.3.4 section of the CHANGELOG. The public documentation: 17.2 Modern authentication. On this forum: Connecting a mail client for app passwords on a phone, Active Directory and LDAP sign-in, encryption at rest, Upgrading to 6.3.4, The Control Deck, The webmail portal at /portal and the 6.3.4 announcement.
  • 0 Votes
    1 Posts
    132 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.8 (2 November 2026) has the web services listener, which serves CalDAV and CardDAV, refuse an address an auto-ban or an IP range bans: it closes the connection with no answer, before the TLS handshake, when the IP range that decides for the address allows none of SMTP, POP3 and IMAP, where up to 6.3.7 a banned address could go on guessing passwords there. So an IP range of your own that allows some addresses nothing now closes CalDAV and CardDAV to them as well: check that these services' clients do not come from one. Loopback and the addresses in WebServicesTrustedProxies are never refused by a range, and a failed sign-in from one of them is counted against the name alone (AccountLockoutThreshold, off by default), never towards a ban of that address. A client that signs in with another spelling of the account's address than the stored one, in more than the case of the letters A to Z, or with white space at either end, may no longer sign in. See the 6.3.8 announcement. This topic covers what hMailServer 6.3.4 serves to a phone or a desktop client over CalDAV and CardDAV, and what the webmail's Calendar, Tasks, Contacts and Directory pages do with the same data. The one thing to know first: CalDAV and CardDAV are served on the web-services listener, they take HTTP Basic as the account - its password or an app password - over HTTPS only, and a passkey or single sign-on does not reach them. CardDAV has been there since 6.3.3 with one address book; calendars, several and shared address books, the directory and rooms are all since 6.3.4. Setting up a phone or a desktop client A client given the mail address finds the server by discovery: /.well-known/carddav and /.well-known/caldav redirect to /dav/, a PROPFIND there answers the principal, and the principal answers the address-book home and the calendar home. iOS, macOS Contacts and Calendar, DAVx5 on Android and Thunderbird all sync what is there. The IMAP logon's lockout and auto-ban apply to the sign-in. Which password. An account with a TOTP second factor enrolled uses an app password, because once a secret exists the account password stops being a mailbox credential. Minting an app password on the webmail's Security page asks for the account's own password again, so an account that single sign-on or SCIM made, whose random password nobody was told, needs a password set by an administrator before its person can make an app password for a phone. The listener. The web services are off as shipped: WebServicesHttpPort and WebServicesHttpsPort default to 0, and the HTTPS port takes WebServicesCertificateFile and WebServicesPrivateKeyFile. Since 6.3.4 these keys are kept in the database: change them in the Control Panel (Windows), over REST, or on Linux with hmailserver --set-setting NAME=VALUE, not by editing hMailServer.ini. A saved port or certificate waits for a reinitialise or a restart, since the listener binds when it starts. On the Devices list a DAV client appears with the protocol dav and its user agent. It has no connection to drop - each request authenticates afresh - so revoking it means revoking the app password behind it. Collection What it is /dav/calendars/<address>/calendar/ The default calendar, Calendar, made the first time it is asked for /dav/calendars/<address>/<name>/ Another calendar of the account's own, made in the webmail or with MKCALENDAR /dav/calendars/<address>/shared-<id>/ A calendar another account, or a group, shares with this one /dav/addressbooks/<address>/contacts/ The default address book, where a client that has synced since 6.3.3 still finds it /dav/addressbooks/<address>/book-<id>/ Another book of the account's own (schema 6079) /dav/addressbooks/<address>/shared-<id>/ A book shared with this account, or with the whole domain /dav/addressbooks/<address>/directory/ The organisation's directory, read-only (schema 6078) Each calendar collection advertises VEVENT and VTODO, so a phone's calendar and its reminders list are the same collection. Calendars, events and tasks An account has one calendar until it makes more. The Calendars box beside the grid adds one with a name and a colour (POST /api/v1/me/calendars), renames or recolours it and deletes it with its events; the default cannot be deleted. Over CalDAV the colour travels as Apple's calendar-color, so a phone shows the same one, and PROPPATCH sets the name and colour from the phone's side. Events. An event takes a title, start and end as wall-clock times in tz (an IANA zone the server knows), all_day, a location and notes, rrule (daily, weekly on chosen days, monthly, yearly, with an interval and an end by count or by date), alarm_minutes and guests. The server writes DTSTART;TZID=<zone> so a weekly 09:00 stays 09:00 across a clock change, and a VALARM for the reminder, which is what CalDAV clients ring for. A repeating event is expanded across daylight saving in any of the 592 zone names the server knows; calendar-query with a time range, calendar-multiget and sync-collection are all answered, with ETags and If-Match. Changing one occurrence alone writes an override; deleting one writes an EXDATE on the master. Tasks. The Tasks page lists every VTODO of every calendar the account can see, soonest due first. Ticking one off writes STATUS:COMPLETED, COMPLETED and PERCENT-COMPLETE:100 as a client does. A second calendar is a task list of its own. Sharing a calendar, and delegates The owner names another account of this server and whether it may write (PUT /api/v1/me/calendars/{id}/shares/{address} with write). The other account sees the calendar in its own Calendars box and in its CalDAV home as shared-{id}/, with owner pointing at the sharer's principal and a current-user-privilege-set of read, or read and write. A read-only share takes no PUT, no event from the page and no PROPPATCH. The sharee leaves with a CalDAV DELETE of shared-{id}/ or from the page. Shared with a group. Since schema 6086 the name may be one of the server's groups (hm_groups) instead of an address; a name that is both is the account. The membership is read at the moment a member asks, so somebody added later sees the calendar and somebody removed no longer does. A member cannot leave a group share (403): they leave the group, or the owner takes the share back by the group's name. A delegate. A share may be a delegation (delegate: true, which implies write the sharee may answer the owner's invitations on the owner's behalf. The answer is POST /api/v1/me/calendar/events/{id}/reply with partstat ACCEPTED, TENTATIVE or DECLINED, allowed to the owner and to a delegate and refused (403) to a plain writer or a reader. The organiser receives a METHOD:REPLY whose ATTENDEE carries SENT-BY="mailto:<delegate>", From: the owner with Sender: the delegate. Free-busy An account of the domain can see when a colleague is busy, and nothing else about them. GET /api/v1/me/freebusy?address=<account>&from=<when>&to=<when> answers the busy periods of that account's own calendars - not the ones shared with it - over a window of at most 62 days (400 beyond). An event that is TRANSP:TRANSPARENT or STATUS:CANCELLED, or that the account declined, is left out; a period is tentative when the account's PARTSTAT is TENTATIVE or NEEDS-ACTION, else busy. No title, location or description is answered. Another hosted domain's account, an address nobody has, an inactive account, and an account whose freebusy preference is nobody are one and the same 404, so the route enumerates nothing. The preference is the person's own, in Settings: "Who may see when you are busy". Over CalDAV the same computation answers the free-busy-query REPORT on a calendar the caller may read, as one bare VFREEBUSY, over a time-range that needs both bounds and spans at most 62 days. The webmail's event editor draws an availability strip for the guests of a meeting from the same computation. Invitations by mail (iMIP) An event with guests, saved with "Send invitations by e-mail", mails each guest the event as a text/calendar part with method=REQUEST (RFC 6047) - what Outlook, Gmail and Apple Mail show as an invitation card. Saving a change mails an update with the sequence stepped; deleting the event mails METHOD:CANCEL. A message that carries a text/calendar part or an .ics attachment shows a card with Accept, Maybe, Decline and "Propose a new time" (METHOD:COUNTER). Filed on delivery. A METHOD:REQUEST delivered to an account is filed into its default calendar as it arrived (NEEDS-ACTION as a rule) when, and only when, it names the account as an ATTENDEE and not as the organiser. A message flagged as spam files nothing; a REQUEST naming somebody else is not kept; and an account that has never opened a calendar gets nothing filed. A stored copy under the same UID is replaced only by the same or a later SEQUENCE, and never when its ORGANIZER is not the invitation's. A REPLY delivered to the organiser sets that attendee's PARTSTAT in the stored event, so every phone syncing the calendar shows who accepted without anyone opening the mail; a CANCEL delivered to a guest marks the stored copy cancelled. Rooms and equipment (since 6.3.4) A room or a projector is an account whose kind says so: person (the default), room or equipment, a column of hm_accounts (schema 6101). It is invited like any guest and answers by itself, from its own address, by its booking policy; what it does not decide it leaves to its delegates. Where What The Control Deck The account editor's General group has a select of the three kinds; a room or equipment gets a Booking policy card REST kind on GET and PUT /api/v1/accounts/{address}; the policy at GET and PUT /api/v1/accounts/{address}/booking-policy COM (Windows) Account.Kind (0 person, 1 room, 2 equipment), saved by Save(); the booking policy is not on COM The Windows Control Panel Does not show it: a room is an ordinary account to it From the moment its kind is saved every sign-in as the room is refused, whatever the credential, with the ordinary wrong-password answer. A room with no policy row answers by the defaults: Field Default What it decides auto_accept true Answer invitations by this policy; false leaves every one for the delegates window_days 180 How many days ahead a booking may start, 1 to 1080 max_duration_minutes 1440 The longest booking; 0 is no limit working_hours_only false Only inside the working hours, on the working days work_days, work_start, work_end Monday to Friday, 08:00 to 17:00 The working days and hours time_zone UTC The zone the working hours are in, such as Europe/London allow_conflicts false Accept a booking over another one allow_recurring true Accept a recurring meeting bookers empty Empty is everyone in this server's own domains; a list is an address, @domain, a distribution list's address or a group's name. An organiser from outside must be named here capacity 0 How many people the room seats, shown in the directory; it decides nothing delegates none Addresses and group names, each given a delegate share of the room's calendar Who is asking. The room first decides whether the invitation's ORGANIZER is who sent it, from what the server recorded when the message arrived and never from a From: header or a MAIL FROM. An organiser at this server's own domains is believed only when the message was submitted by a signed-in session as that account, or as one allowed to send as it; a message sent through an API key is not believed. An organiser from outside is believed only when bookers names the address or its @domain and the message passed DMARC for that domain. SPF on its own is not enough, and with DMARC off no outside organiser is ever believed. Anything else is neither filed nor answered, and the delegates are told, within limits. The answer is decided just after delivery, on a work queue of the rooms' own; no delivery and no SMTP answer waits for it. In order: who is asking; may they book; sent automatically (an empty reverse path or Auto-Submitted, never answered); auto_accept; at most 20 answers an hour to one organiser; recurrence; the window; the longest booking; working hours; conflicts, counting every booking the room holds that is not cancelled as busy. Accepted, the booking is filed and a METHOD:REPLY goes from the room's address, marked Auto-Submitted: auto-replied. Declined, the reply carries the reason alone; a conflict says when, never whose. A request left for the delegates is never put into the room's calendar on the sender's word. A room with no delegates keeps what it leaves and tells nobody: name at least one. Two servers sharing one database are not serialised for rooms: run rooms on one node. Finding a room. The directory lists a room with its kind and capacity, and its CardDAV card says KIND:location (KIND:device for equipment). Every CalDAV principal says its calendar-user-type - INDIVIDUAL, ROOM or RESOURCE - and the principal-property-search REPORT on /dav/principals/, which is what a client's room finder sends, answers the domain's rooms and equipment. Apple Calendar searches for locations this way; Thunderbird has no room finder. Every request stays in the room's mailbox, and a full mailbox answers nothing more, so give a room a quota and a MessageRetentionDays that suit it. Contacts, groups and several address books Kept, and other contacts. A contact has a source: manual for what the person added, imported or kept, and collected for every address the person has written to - from the webmail's send and, since 6.3.4, from a mail client over an authenticated SMTP submission. A collected address is offered under Other contacts and by the To, Cc and Bcc completion, and nowhere else: CardDAV serves only what the person kept, so a phone's address book is not filled with everyone ever written to. Keep makes it a contact. What a card carries. A card a CardDAV client sends is kept as sent and served back byte for byte. The page reads birthday (BDAY), note, phones (every TEL) and the embedded PHOTO; a photo that is only a URL is not the server's to fetch. Groups. A group is a card of kind group - KIND:group in vCard 4.0, X-ADDRESSBOOKSERVER-KIND:group in 3.0 - so a group made on the phone appears in the webmail and the reverse. A group's name in To becomes that group's recipients, so the reader sees the people and not the group. The ceilings hold on both surfaces: 500 members in one group (a card past it is refused 403 CARDDAV:valid-address-data) and 200 groups in one address book (400 over REST, 507 Insufficient Storage over CardDAV). A group is not a distribution list: nothing about it reaches SMTP. Several address books, and shared ones. Since schema 6079 the account starts with one book, Contacts, which cannot be deleted, and may have at most 50. The owner shares one from the Contacts page with an address - one account of this server - or with its own domain's name; Can edit makes the share writable, and a book takes at most 200 shares. A domain-wide share aimed at another hosted domain is 403, and a person cannot leave a domain share: only the owner or an administrator removes it. One account holds one address once across all its books, so a contact is moved between books, never copied. Over CardDAV a read-only collection grants read and read-current-user-privilege-set and nothing else, a PUT into it is 403, and DELETE on the collection gives the share back and deletes nobody's contacts. MKCOL is refused: a book is made from the Contacts page or /api/v1/me/address-books. No group is shared, in either direction. The backup carries the books and their contacts; hm_addressbookshares is not carried, so the sharing is set again. The Control Panel and the Control Deck have no page for this. An administrator has GET /api/v1/accounts/{address}/address-books, which shows the shape of the sharing and never the contacts, and a route that removes one share, recorded as address-book-share-removed. Import and export. The Contacts page imports a vCard file or a CSV, including Google's and Outlook's exports. Export as vCard downloads contacts.vcf; Export as CSV downloads contacts.csv and leaves groups out. The organisation's directory It is one domain at a time, and always the reader's own: every active account of the domain whose owner has not opted out, and every enabled distribution list, with what is set on top in hm_directory (schema 6078) - job title, department, work telephone, mobile, office, a photo (JPEG, PNG or GIF, at most 100 KB decoded, judged by its bytes), and a note that is the administrator's alone. Two switches, both rows of hm_settings and both on by default. DirectoryEnabled is whether the directory is answered at all; turn it off on a server whose one domain holds people who are not one organisation. With it off the webmail hides its Directory page and the CardDAV collection stays, empty, so a subscribed phone syncs to nothing rather than failing every poll. DirectoryUserEditable is whether a person may set their own fields. The opt-out is never what a switch takes away, and it is not invisibility: mail still arrives and the address still works. An administrator changing somebody's opt-out is on the audit trail as directory-opt-out-changed. The surfaces. The webmail's Directory page (#/directory); the To completion, which offers the reader's own contacts first and the directory hits after; GET /api/v1/me/directory?q=&limit= for the person and GET and PUT /api/v1/accounts/{address}/directory for the administrator; a Directory entry tab in the Windows Control Panel's account dialog, and the same card in the Control Deck; and the read-only CardDAV collection, where PUT, DELETE and PROPPATCH are refused 403. Nothing is learned from anywhere else: there is no import from Active Directory or LDAP, even on a server that authenticates against one, and no LDAP server for a mail client to search. What it does not do No RFC 6638 scheduling (no schedule-inbox or schedule-outbox), no free-busy of a calendar on another server, no per-calendar time zone (calendar-timezone is not kept), no partial calendar-data, and no time-range filter on a VALARM. No ActiveSync and no Exchange Web Services: a phone uses IMAP and SMTP for mail, and CalDAV and CardDAV for the rest. Calendar shares and address-book shares are not in the backup, so set them again after a restore; the booking policy, the books, their contacts and the directory rows are. Where to read more Wiki: Connecting a Mail Client, Rooms and Equipment, The Webmail, Ports Reference. Documents: Calendar.md, Contacts.md, SharedAddressBooks.md, Directory.md, RoomsAndEquipment.md. On this forum: The webmail portal at /portal, The Control Deck, Signing in, Connecting a mail client, and the 6.3.4 announcement.
  • 0 Votes
    1 Posts
    130 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) regroups the Control Panel's sidebar: the audit trail, journaling and eDiscovery are a Compliance group of their own rather than the last rows of Monitoring & troubleshooting, as the table below has it, and Ctrl+K takes every old title and route to its page. See the 6.3.7 announcement. This guide covers what an operator is asked for when a lawyer, a regulator or a person with a data request arrives: eDiscovery (cases, searches, legal holds and exports), a person's own data (the export and the erasure), and the audit trail that records who did what. eDiscovery, the person's data as one archive, the erasure of log lines, the erasure's reach into the stores beside the mailbox, and the audit trail are new since 6.3.4. The one thing to know first: cases, their files and their holds are not in the backup. A server rebuilt from a backup has no cases, no exports and no holds, so place the holds again before its mail can be deleted. Where it is Where What The Control Deck, Discovery view The whole of it: cases, the search form, holds with Lift the hold, exports with Download, Open and Close REST, /api/v1/discovery/cases The same ten routes the Deck calls, in the OpenAPI document The Control Panel (Windows only), Monitoring & troubleshooting → Discovery (eDiscovery, in the Compliance group, since 6.3.7) Read-only: the cases and their lines of the audit trail, read over the REST listener, which must be on (RestApiPort). It changes nothing COM, hmctl Neither On Linux, then, discovery is worked from the Deck or over REST. Discovery and the erasure do nothing until somebody opens a case or asks for an erasure; the audit trail is on from the first start. Two neighbouring tools are not this one. The archive has had a legal hold of its own since before 6.3.4 (/api/v1/archive/{id}/hold), which keeps one archived copy from the archive's retention sweep (Quotas, retention and the archive). Journaling, a copy of a domain's or an account's mail sent elsewhere as it passes, has its own topic: Journaling and dual delivery. Cases A case has a name (up to 255 characters), a description (up to 1,000), who opened it and when, and a state: open or closed. Its searches, holds and exports belong to it. Closing a case releases every hold of it that is still live, each release on the audit trail; a closed case runs no search, places no hold and makes no export, and what it made stays readable and downloadable. A case is never deleted, and an address's erasure does not reach it. The files a case makes are under the data directory, in Discovery\case-<id>\: search-<id>.json for each search and export-<id>.zip for each export. Nothing removes an export; delete its zip by hand when it is no longer needed. Searching POST /api/v1/discovery/cases/{id}/searches takes: Part What it means custodians (required, at most 100) An address, a domain, or * for every account on the server and the whole archive from, to (optional, YYYY-MM-DD) The day a message is dated by its Date field, read as UTC; else the day the server stored it words (optional, at most 50) Every word must be found, a quoted phrase as one: a case-insensitive substring of what IMAP SEARCH TEXT reads attachment (optional) name, type and min_kb; {} is any attachment at all scope mailboxes, archive or both, the default There is no sender or recipient criterion: a word is looked for in the whole decoded header, and a person is named as a custodian. A query the server cannot take is refused with 400 and the reason; 202 answers with the search's id, and the search is queued. Where it looks. For each custodian account: its folders, its deleted items (what a hold kept, and what RecoverDeletedDays keeps), and the archive. A search copies nothing; it lists each hit, and lists separately what it could not read, with the reason. What it cannot see. 100,000 hits. A search stops there and says truncated. Narrow it and run it again. Text inside a PDF, a Word document or a spreadsheet. Only text/* attachments are read, up to 1 MB of characters a message. An attachment criterion still finds the PDF by name, type and size. More than 80 MB a message. It is listed as not searched when the query asks about its body, and never dropped as a non-match. Zero-access mailboxes and messages encrypted end to end are matched on their headers alone and marked headers_only. The stores beside the mailbox, <data directory>\Confidential and \Protected, and archived copies with no index row (a bounce, a delivery report). The full-text index is a filter, never the answer. It is off by default (IndexerFullText). When it is on, a message the index proves cannot contain a word is ruled out; every other message is read. One at a time. Searches and exports are queued and run by one runner, the server's own. Only one hMailServer service may run against a database at a time (Warm standby and high availability), so a standby started after a failover takes over the queue rather than sharing it. Holds POST /api/v1/discovery/cases/{id}/holds takes one of: Body Holds {"scope":"account","target":"[email protected]"} Every message of the account, and its archived copies {"scope":"domain","target":"example.com"} Every message of every account in the domain, and their archived copies {"scope":"archive","search":12} The archived copies search 12 of this case matched, at most 10,000 at once A hold is live until it is released or its case is closed, and what it keeps is kept by any live hold of any case. Path Under a hold The person's delete (IMAP EXPUNGE, POP3 DELE, the webmail's delete, a folder deleted) and an administrator's delete of a message The message leaves the folder, and a copy is kept among the account's deleted items whatever RecoverDeletedDays says (0 by default). If the copy cannot be made, the delete is refused (HM7333) An IMAP MOVE out of the account The original is kept among the held account's deleted items Every message of the account deleted at once Refused, 409 over REST The account's own filters at delivery: a rule's Delete or Reject, a Sieve discard, reject or redirect, a forward that keeps no copy The copy is kept among the deleted items instead of dropped; if it cannot be kept, the message is delivered Retention, the recover-deleted sweep, deleting a deleted item for good Nothing of the held account is deleted A mailbox restore with mode replace, a full restore with the domains option Refused (the full restore as HM5014) Moving the account to another domain, removing its mailbox key, deleting the account or its domain Refused, over REST, SCIM and COM alike The archive's retention sweep and an address's erasure A held copy keeps its file and its index row What a hold does not reach: the server's and a domain's rules, which decide before a message is split between its recipients; spam and viruses refused or deleted before a message is the account's, and what the quarantine discards; the delivery queue; the confidential and password-protected stores; archived copies with no index row. If ArchiveDir was put inside the data directory, a full restore with the messages option replaces held archived copies too, so keep the archive outside it. The hold is not hidden from the custodian. The account's own GET /api/v1/me/recoverable answers held: true and lists what the hold kept, and the webmail's Recover deleted items page offers no delete for good. It fails closed. When the holds cannot be read, every question is answered held, and the error log says so once a minute (HM7332). A hold is a row in the database. The server reads the holds again as soon as one is placed or released, and no more than five seconds after a change made elsewhere. Exports POST .../searches/{search}/export queues an export of a finished search's hits. The zip holds: Each hit as EML, the message's own bytes as the server stores them, under <custodian>/<folder>/<n>-<message id>.eml, and an mbox per custodian folder beside them, in parts of at most 256 MB. SHA256SUMS, so that sha256sum -c SHA256SUMS checks the unpacked export. manifest.json: the case, the search as it was run, who ran it, who asked for the export, every item with its path, SHA-256 and size, and every item withheld, with the reason. The zip's own SHA-256 is kept with the export, and the case answers it (exports[].sha256). Withheld: a zero-access mailbox's messages, and anything deleted, moved or swept since the search ran. Search again to find where a message is now. Limits. The zip written is the 32-bit format, so no export passes 4 GB: narrow the search and export each part. A download past 500 MB answers 413 with where the file is on the server's disk and its SHA-256. An export that would leave the data directory's volume with less than a gigabyte free fails. A discovery export is never a PST; an examiner who needs one converts the EML files with a third-party converter. Who may Permission Allows Held by view_discovery Reading the cases, their searches and hits, holds and exports The server administrator and the auditor role manage_discovery Opening and closing a case, running a search, placing and releasing a hold, making an export The server administrator; a custom role may be given it Downloading an export needs view_mailbox as well, since it hands over the mail itself, the custodians' deleted items among it. A search that reads the archive needs view_archive as well. A credential confined to named domains names no custodian outside them, never *, and is answered 404 for a case that reaches outside them. A person's data: the export One .zip of everything the server holds for an account. The person downloads it from the webmail's Your data card (GET /api/v1/me/export.zip), with no administrator involved; that download is refused (403) to a request authenticated with an app password. The administrator has the Deck's "Everything (.zip)" on the account row, GET /api/v1/accounts/{address}/export.zip (the administrator password's alone), POST /api/v1/accounts/{address}/export with archive, hmctl privacy export <address> --archive <file> and, on Windows, COM Account.ExportArchive(file). Entry What it is manifest.json The address, the moment, and the counts of what follows mail/<folder path>.mbox Every folder with mail, one mbox each account.xml What the backup carries for the account, less the credentials contacts.vcf The address book as vCard 3.0 calendars/<calendar>/<object> Every calendar object as the client sent it preferences.json, filters.sieve The webmail's preferences, and the active Sieve script logs.txt The lines of the server's own log files that name the address, up to 50 MB A download past 500 MB is refused with 413, and the operator writes the archive to disk instead; the disk form has no ceiling but the format's 4 GB. A person's data: the erasure POST /api/v1/addresses/erase, hmctl privacy erase, the Deck's "Erase traces" and, on Windows, the Control Panel's "Erase traces..." remove what deleting the account deliberately leaves: quarantine entries, message-trace rows, greylisting triplets, distribution-list memberships, aliases on either side, the bounce and complaint records, and the rows naming the address in other people's address books and first-contact memory. With the archive included, the person's archive tree is swept and every index row naming the address goes. The log lines are asked for by name (include_logs on the REST body, --logs every occurrence becomes [erased], and a log past 256 MB is reported instead of rewritten. Since 6.3.4 it also reaches the stores beside the mailbox. A message held for review that the address sent or would receive is discarded. The address is taken out of the recipients of a message that expires, and one it sent is ended. A password-protected message to it is ended. The recipients an account remembers writing to, the out-of-office memory, booking policies and correspondents' S/MIME certificates and OpenPGP keys lose it. Every list of addresses is matched whole. Erasing [email protected] leaves [email protected] and [email protected] as they are. A legal hold outranks it. An erasure never deletes a message from a mailbox, so a hold meets it only in the stores it deletes from, and there the hold wins. The REST answer's held counts what a hold kept; run the erasure again once the case is closed. When the holds themselves cannot be read, the erasure fails (HM6250; 500 over REST) and says nothing about a legal hold. What it leaves, by design, among others: mail in mailboxes, the person's own and everybody else's, and a mailbox's deleted items; a list's archive of its posts; other people's calendars; configuration somebody made that names the address (a forward, a rule, a list's moderator); the audit trail, and the alert events, which nothing prunes yet; the spam trap record; the delivery queue and the webhook queue; the contacts in DMARC and TLS reports; the archive's Inbound folder; and backups, which are outside the running server's reach. PersonalData.md has the full list, with each reason. The audit trail, for a compliance reader The audit trail is on by default. It keeps one row in hm_audit for each administrative change made over COM, REST or the Deck, a mailbox's move included, with a few exceptions such as an API key made or revoked or a quarantined message released or deleted; it records changes, never reads. A password, key, token or other secret is recorded as (changed), never as a value. Each row carries the SHA-256 of the row before it and its own, so GET /api/v1/audit/verify tells an edited row from a removed one: tampering is detectable, not impossible, since anyone who can write the table can recompute the whole chain. audit_retention_days is 0 by default, which keeps everything. Three things on it matter here. Every discovery search is recorded with its whole query; a download of a discovery export is recorded on the trail alone, so with the trail off a download is recorded nowhere; and an erasure is recorded with the address it erased wherever it deletes from recorded tables (aliases, list recipients, the archive index): those rows name the person until audit_retention_days ages them out. Alerts, the audit trail, syslog and the journal has the columns, reading and putting a change back, who may read it, and what it does not record. Regulatory scope, in one paragraph The project's own determination, reviewed 12 August 2026, is that the EU Cyber Resilience Act and the revised Product Liability Directive do not place a manufacturer's obligations on it, because it is supplied outside a commercial activity. The document calls itself a self-assessment and not legal advice, and lists the triggers that would re-open it. Under GDPR and UK GDPR the project is neither controller nor processor: the operator of an installation is. The export, the erasure and the log retention limits exist so that operators can meet their own obligations. What it does not do No PST export, and no text read from attachments but text/*. No silent hold: the custodian can see what a hold kept. No export past 4 GB and no download past 500 MB. No saved queries, review tags, redaction or de-duplication across custodians: a message sent to two custodians is two hits. No search of backups, and no case carried by a backup. Where to read more Wiki: Compliance and eDiscovery. Documents: Discovery.md, PersonalData.md, AlertsAndAuditTrail.md and RegulatoryScope.md. On this forum: The Control Deck, Journaling and dual delivery, and what a backup carries and what it leaves in Backup and restore.
  • 0 Votes
    1 Posts
    156 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) is a security release that closes gaps in what this guide describes. Over COM an account given serveradmin with a list of domains was the server's administrator, its list aside, and one given domainadmin with a list that left out its own domain administered that domain's ordinary accounts; both are now confined as over REST, so before upgrading give serveradmin with no list to any account that is to administer the whole server over COM, a script's or a Control Panel's account among them. A credential confined to named domains is refused a domain's rename, a domain alias, a custom role's creation, change or deletion, a rule action that reaches the server's own addresses, a route's stored credentials or the event script, a relay host that is not public, and the server-wide metric history, over REST and COM alike; and a role is given or taken away only by a credential that takes in what the account holds now and afterwards, so manage_roles in named domains can no longer take a role from an account whose admin level is the server administrator's. A state-changing REST request that a browser reports as cross-site is refused. In the Control Panel, API & monitoring is Monitoring & alerts and REST API keys is under Sign-in & access; Ctrl+K takes every old title to its page. See the 6.3.7 announcement. This guide puts administrator roles in one place. Roles are new since 6.3.4 (schema 6047). The one thing to know first: an upgrade changes nobody's authority. The upgrade writes no assignment, and an account with no assignment holds exactly what its admin level has always given it. Nothing narrows until you give somebody a role. Permissions, roles and assignments A permission is one thing a credential may do, a role is a named set of permissions, and an assignment gives one account one role in named domains. An account holds at most one role. Role For Reaches when no domain is named The same as serveradmin Everything, including the secrets no other role reads Every domain The administrator password, admin level Server, an API key with Scope=full and no role domainadmin The accounts, aliases, lists and mailboxes of a domain The account's own domain Admin level Domain helpdesk Finding an account, resetting its password, unlocking it, and reading where its mail went, and nothing else The account's own domain Nothing before 6.3.4 auditor Reading everything the administration exposes, the audit trail included, and changing nothing Every domain An API key with Scope=readonly and no role The built-in roles cannot be edited or deleted. Their permissions are in the server's code, not the hm_roles table, so a change to the table cannot hand anybody a permission. The 27 permissions and who holds them A permission marked server-wide is reached only by a credential with no list of domains. The queue, the quarantine, the archive, the logs and the settings carry every domain's mail, so a role confined to named domains is refused them whatever it holds. Permission What it reaches serveradmin domainadmin helpdesk auditor administer_server Everything, secrets such as password hashes and private keys included; COM's "is this the server administrator?" yes administer_domain COM's "is this a domain administrator?" yes yes view_configuration Status, settings, listeners, certificates, rules, routes, anti-spam lists, the roles and who holds them, alert rules and events, bounce and complaint readings; never a secret yes yes yes view_accounts Domains, accounts, aliases, lists, groups (server-wide), fetch accounts yes yes yes yes view_mailbox Another account's folders and messages yes yes yes view_queue The delivery queue (server-wide) yes yes view_quarantine The quarantine (server-wide) yes yes yes view_archive The archive (server-wide) yes yes view_logs The logs (server-wide) yes yes view_message_trace The message trace; with view_configuration, the reports and a domain's suppression list yes yes yes yes view_audit The audit trail (server-wide) yes yes manage_accounts Creating, changing and deleting accounts, aliases, lists, groups, fetch accounts and app passwords; a domain's spam traps, DKIM rotation and suppression list; moving a mailbox (needs both domains) yes yes reset_password POST /api/v1/accounts/{address}/password, Application.ResetPassword yes yes yes unlock_account POST /api/v1/accounts/{address}/unlock, Application.UnlockAccount, Account.UnlockMailbox; clearing the logon-failure list (server-wide) yes yes yes manage_mailbox Another account's folders, messages and folder rights, a support session, a PST migration job (with manage_accounts) yes yes manage_queue Retrying or deleting a queued message (server-wide) yes manage_quarantine Releasing or deleting held mail (server-wide) yes yes manage_archive Holding and releasing archived messages (server-wide) yes manage_settings Every setting, listener, certificate, route, rule and anti-spam list, the alert rules (server-wide); the webmail's branding and file-link policy, a confined credential only its own domains' yes manage_domains Which domains exist (server-wide) yes manage_server Reinitialise, back up, reload scripts, clear caches, install an update, run a pass now (server-wide) yes manage_roles Roles and who holds them (server-wide) yes send_through_api Sending through POST /api/v1/send; only an API key reaches it yes provision SCIM 2.0 at /scim/v2, reads and writes; only an API key reaches it yes view_discovery eDiscovery's cases, searches, holds and exports yes yes manage_discovery Opening and closing a case, searching, legal holds, exports yes manage_inbound_webhooks Routing an address or a domain to a URL, as much as reading every message the route takes yes Four rules sit beside the table: The auditor reads mail. It holds view_mailbox; the help desk does not. The reports need two permissions. GET /api/v1/reports needs view_configuration and view_message_trace, because it says who mails whom. domainadmin and auditor read them; helpdesk is refused, and the Deck leaves the Reports view out for any role without both. The trace follows the role's reach. A confined help desk traces one whole address in a domain it reaches and sees only events naming that address exactly: [email protected] does not also show [email protected]. Clearing the logon-failure list is server-wide. hm_logon_failures records only the address a failure came from, with no account or domain, so a confined help desk is refused POST /api/v1/settings/logon-failures/clear, and Settings.ClearLogonFailureList answers the server administrator alone. The help desk unlocks one account with POST /api/v1/accounts/{address}/unlock instead; a password reset clears the lockout too. A ban already placed is an IP range under /api/v1/ipranges, which needs manage_settings. What only serveradmin has Reading a secret. No view_ permission covers a password hash or a private key: an auditor that could read one would be an offline attack away from being every role. Handing out authority. No custom role may hold administer_server, administer_domain or manage_roles, and a credential may never give a role holding a permission, or reaching a domain, that it does not hold itself. Taking over a wider account. A credential may not reset the password of, or change, an account whose whole authority it does not take in. A help desk is refused a domain administrator's or an auditor's password, even in its own domain, because otherwise it could sign in as them; a domain administrator is refused an auditor's. Where the server administrator's own mailbox is in a domain somebody else administers, that domain's administrator is refused all of these on it, over REST and COM. Over COM this 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) 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. Roles.md lists every route and COM member that asks this. Which domains a role reaches Credential No domains named * A list An assignment of serveradmin or auditor Every domain Every domain Only those An assignment of any other role, custom ones included The account's own domain Every domain Only those An API key's Domains= line, whatever its role Every domain Matches no domain Only those A custom role holding a server-wide permission needs * before an account can use it at all. The listings answer every_domain over REST, and a fourth field over COM. A credential confined to named domains sees only the assignments of accounts in those domains. Giving a role, surface by surface Control Panel (Windows only): Accounts & domains → Administrator roles lists the roles and who holds them. Type an address, choose a role, optionally name the domains, and press Give the role; select a row and press Take the role away. It applies at once. Control Deck (Windows and Linux): the Roles view does the same. REST: giving or taking away a role needs manage_roles, which only serveradmin holds and which is server-wide: the administrator password, or a serveradmin credential reaching every domain. PUT /api/v1/role-assignments/[email protected] {"role":"helpdesk","domains":"example.test,example.org"} DELETE /api/v1/role-assignments/[email protected] GET /api/v1/role-assignments GET /api/v1/roles COM (Windows only): an empty role name takes the role away, and Account.RoleName, RoleDomains and RolePermissions read it back: $app = New-Object -ComObject 'hMailServer.Application' $account = $app.Authenticate('Administrator', 'your-admin-password') if (-not $account) { throw 'Authentication failed.' } $app.AssignRole('[email protected]', 'helpdesk', 'example.test') $helen = $app.Domains.ItemByName('example.test').Accounts.ItemByAddress('[email protected]') $helen.RoleName; $helen.RoleDomains; $helen.RolePermissions With a second factor on the administrator, Authenticate refuses and the script uses AuthenticateWithCode. Application.CurrentRole and CurrentPermissions say what a session holds. An API key: POST /api/v1/apikeys with "role":"helpdesk" writes a Role= line to hMailServerApiKeys.ini. The key store answers to the administrator password alone. The Deck's API keys view, signed in with that password, also makes a key with a role and its domains, showing the token once. curl -u Administrator:your-admin-password \ -H "Content-Type: application/json" \ -d '{"label":"help desk tool","role":"helpdesk","domains":"example.test"}' \ https://<host>:8045/api/v1/apikeys Use the address and port your listener answers on; bound to loopback with no certificate, it is http://127.0.0.1:8045 (https:// once a certificate, or an ACME one, is in place). A key's role decides what it reaches. The Control Panel's REST API keys page, as its reference describes it, names no role; give a key its role over REST or in the Deck. No API key signs in to the Deck. Custom roles are made over REST from a name, a description and a comma-separated permission list; both editors list them and give them out. A role that releases held mail holds server-wide permissions, so it is given with * and reaches every domain's held mail: POST /api/v1/roles {"name":"quarantine-desk","description":"releases held mail","permissions":"view_quarantine,manage_quarantine"} PUT /api/v1/role-assignments/[email protected] {"role":"quarantine-desk","domains":"*"} A permission the server does not know is dropped, not refused, so a custom role ends with less authority than it asked for, never more. A custom role somebody holds cannot be deleted: DELETE /api/v1/roles/{name} answers 409 saying how many accounts hold it, and the Deck's Delete shows that sentence. Give each holder another role or take this one away first; assignments are never removed for you, because an account with none falls back to its admin level. A built-in role answers 400, and COM cannot delete a role. An assignment decides even when its role holds nothing: holders of a custom role whose permissions name nothing this version knows are ordinary accounts, not their admin level. Where each role works Role Control Deck REST API Control Panel (COM) serveradmin Every view Every route Every page domainadmin Dashboard, DANE / TLSA, domains, fetch accounts, lists, roles and reports for its domains Its domains' accounts, aliases, lists and mailboxes, the status, who holds a role there Domains and Administrator roles helpdesk Domains, fetch accounts and lists, with Reset password and Unlock on every account The reset, the unlock, the account listings, the trace of an address in its domains Welcome and About only; Application.ResetPassword, UnlockAccount and GlobalObjects.MessageTrace answer a script auditor Every view that reads, the audit trail and reports included Every read, the audit trail and alerts included; every write refused Welcome, About and Administrator roles The help desk and the auditor work in the Deck. COM's settings objects answer only the server administrator, so an auditor reads the configuration through the Deck or REST. A domain administrator is not shown the settings, rules, routes, remote domains, certificates, ports, IP ranges, groups, backup, server messages, queue, logs or audit trail, which are server-wide. The quarantine is server-wide as well, so a domain administrator confined to its domain is refused it. Hiding a page is a courtesy, not the boundary. The server refuses the request whatever a page draws, and a Control Panel connected to a server that predates roles shows every page. A role holder signs in with its own address and password, and its second factor where it has one. An app password never carries the role: over REST it is an ordinary account's request, and COM's Application.Authenticate and AuthenticateWithCode treat it as a wrong password. A support session into a mailbox does not carry the person's role either. A role holder's own mail and settings under /api/v1/me stay its own. Setting up a help desk Turn the REST listener on if it is off. It needs RestApiPort set, RestApiBindAddress set to an address staff can reach (the default, 127.0.0.1, serves this machine alone), a certificate unless it is bound to loopback, and a name staff can reach. On Windows set them on the Control Panel's API & monitoring page (Monitoring & alerts since 6.3.7) and press Apply now, since they wait for a reinitialise. On Linux run sudo hmailserver --set-setting RestApiPort=8045 and the same for RestApiBindAddress and, for any address but loopback, RestApiCertificateFile and RestApiPrivateKeyFile (or let an ACME certificate stand in), then sudo systemctl restart hmailserver. The Control Deck topic has the details. Give each person a mailbox of their own; the role goes to that account. Decide the domains. With none named, helpdesk reaches only the account's own domain; name several, comma separated, for a desk serving more. Give the role on any surface above and check it with GET /api/v1/role-assignments or the Roles view. The person signs in to the Deck at https://<host>:8045/ as themselves, then uses Reset password or Unlock on an account. Tell them the limits: no mail reading, no reports, no domain administrator's or auditor's password, no removing a ban, and, when confined, no clearing of the logon-failure list. Each change the help desk makes is on the audit trail as its own, over COM and REST, with actor type account and a detail ending [role: helpdesk]. What a refusal says A permission the role lacks: 403 naming both, for example the role 'helpdesk' does not hold the permission 'manage_settings'. A domain it does not reach: the role 'helpdesk' does not reach that domain, whether or not the account exists, so nobody can list another domain's mailboxes one request at a time. Over COM, Application.ResetPassword and UnlockAccount answer No such account. for both. An account wider than the credential: that account holds authority this credential does not. Over COM, since 6.3.4 a refusal carries the server's sentence rather than a bare Exception from HRESULT: 0x800403E9. An API key on the key store, the [Settings] routes, the audit trail or the alerts: 401 with {"error":"authentication failed"}, the same as "not a key". The two stores only the administrator password opens The key store (/api/v1/apikeys) and the [Settings] section of hMailServer.ini (/api/v1/settings/ini) answer to the administrator password and nothing else: not an account holding serveradmin, not a key of any role or scope. A key that could mint keys could issue itself an unrestricted replacement, and the file holds the server's secrets. An account is refused 403 with the reason, a key 401. The audit trail (/api/v1/audit) and the alerts (/api/v1/alerts/...) are refused to every API key with the same 401, so a stolen key cannot read what it did or silence the alerting. An account whose role reaches every domain reaches them through view_audit, view_configuration, manage_settings or manage_server; a confined role is refused them. Upgrades, the audit trail and backups The upgrade. Schema step 6047 adds hm_roles, seeded with the four built-in rows, and hm_roleassignments, and writes no assignment. Roles are data, not settings: nothing goes into hm_settings or hMailServer.ini. The audit trail. Every role created, changed or deleted, and every role given or taken away, is a row naming the credential that did it; an assignment's row reads like role: helpdesk -> auditor; domains: the account's own domain -> every domain. The actor type follows the role: the administrator password and holders of serveradmin or domainadmin are administrator, every other account is account. Backup and restore. An account's assignment travels with it, by role name, and the custom roles travel whenever the domains or the settings do, restored first. A role the restoring server already has is kept as it is, with any difference named in the backup log. The admin level waits until the assignment is written, so a restore that stops part-way leaves no account wider than its role. An assignment naming a role neither side has is dropped, the level stays withheld, and HM6129 names the account. No archive carries the API key store. What it does not do No mapping from an identity provider. There is no claims mapping; an identity provider saying somebody is an administrator is not obeyed. The server's roles and oidc_administrators or saml_administrators decide. No role on an app password or a support session. No Control Panel or COM on Linux: roles are given there in the Deck and over REST. Where to read more Roles.md, the authority on all of this. Sign-in and Identity §7, The Control Deck, The REST API §2 and The COM API and Scripting §23.2. AlertsAndAuditTrail.md, Backup.md and Reports.md. On this forum: The Control Deck: browser administration at /, The REST API: turning it on, credentials and roles, The COM API on Windows, Hardening a public mail server, Alerts and the audit trail and IP ranges, auto-ban and lockout.
  • 0 Votes
    1 Posts
    143 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) has the migrations as a card on the Control Panel's Backup, restore & migration page, where this topic says there is no Control Panel page; and a message collected from an external account is sent to SpamAssassin with the Return-Path the other server recorded, where it was the guess from the From field, so its SPF and envelope rules may score such mail differently. See the 6.3.7 announcement. The Windows installer of hMailServer 6.3.8 (2 November 2026) refuses an installation of the original hMailServer's 5.7 (5.7.0, its pre-release builds 2788 and 2843, 5.7.1 and 5.7.2), which this fork's upgrade chain has no step from, before it changes anything, and the installers of 6.3.7 and earlier must never be run over one: such a server's mail comes in by a migration of kind imap, as Migration.md's From the original hMailServer says. See the 6.3.8 announcement. This topic covers every route 6.3.4 offers for bringing existing mail into this server: the webmail's Other accounts page for one person, an external account with the mirror on for a few mailboxes that must stay in step, migration jobs for hundreds of mailboxes from an IMAP server, Google Workspace or Microsoft 365, the Import Tool for mbox files and a Maildir, and the PST import for Outlook data files. It ends with the mailbox move, for a mailbox that is already here and needs a new address or a new domain. The one thing to know first: every route needs the local account to exist. Create the accounts with the Import Tool's text-file import, POST /api/v1/domains/{domain}/accounts, a spreadsheet over REST and hmctl, or SCIM, and only then bring the mail. Moving an installation is a different subject: Upgrading to 6.3.4 for an upgrade and Moving to new hardware for a new machine. Which route You are moving Use Keeps Does not keep One person's own mailbox, by that person The webmail's Other accounts (since 6.3.4) Folders as copies, then new mail collected Rules, signatures A few mailboxes that must stay in step until the cut-over An external account with Mirror every folder on (6.3.3 and later) Folders, every message byte for byte, the five standard flags, the internal date Subscriptions, custom keywords, ACLs Many mailboxes from an IMAP server, Google Workspace or Microsoft 365 A migration, a job per mailbox (since 6.3.4, schema 6090) The above, with keywords, special-use folders, contacts and calendars Rules, signatures, shared mailboxes, ACLs, Gmail's labels as labels mbox files The Import Tool, one folder per file (Windows only) Every message, the date from the headers Flags A Maildir The Import Tool's Select Maildir (Windows only) The Maildir++ hierarchy, the flags in the file names Custom keywords Outlook PST files PST import, a migration of kind pst (since 6.3.4) Folders, messages rebuilt as Internet mail, contacts, calendar items, tasks Attachments by reference, OLE objects, distribution lists What PST import refuses Outlook itself, over IMAP Whatever Outlook uploads Anything Outlook does not put on the wire One person, from the webmail: Other accounts Since 6.3.4. Settings, Other accounts lets a person do the IMAP route for their own mailbox without an administrator. They pick the old provider (Gmail, Outlook.com, Yahoo Mail and iCloud Mail fill in the server, port and encryption), give the user name and password, choose whether to leave the mail there, and tick Bring the folders over too. It is an external account made through /api/v1/me/fetch-accounts, with these differences from one an administrator makes: The spam and virus filters are on for new mail; folders brought over arrive as copies, without the filters. The host must resolve to public addresses only, on port 110 or 995 (POP3) or 143 or 993 (IMAP), unless the administrator turns on reader_fetch_accounts_private_addresses. Schema 6104 marks such an account reader_created, and the host is judged again at every collection: a name that answers a private address later is not collected, and the error log says why (HM5909). The interval is at least 15 minutes, and a person has at most reader_fetch_accounts_max of them: 3 by default, and 0 takes the page away. A host on FetchOAuth2Hosts (outlook.office365.com by default) is refused from the page; only a server administrator adds one. Signing in rather than giving a password. Outlook.com no longer accepts a password from another mail server, and Gmail takes only an app password. Where an administrator has registered the server with Google and Microsoft, the page offers Sign in to Gmail and Sign in to Outlook.com: the person grants IMAP access there and the account logs in with XOAUTH2. No password is stored, and the permission can be taken back at the provider. The first collection then lists the folders to tick, with Gmail's All Mail, Starred and Important unticked so that nothing arrives twice. The registration is set on PUT /api/v1/settings: import_oauth_redirect_base_url (the redirect URI to register is that plus /portal/oauth/callback), import_oauth_google_client_id and import_oauth_google_client_secret, import_oauth_microsoft_client_id and import_oauth_microsoft_client_secret. The Google client needs the Gmail API and the https://mail.google.com/ scope; the Microsoft Entra registration needs the delegated IMAP.AccessAsUser.All and offline_access permissions. Until Google verifies the app, only the test users named on its consent screen can sign in. A few mailboxes that must stay in step: the mirror With Mirror every folder on, the server's own IMAP client does what imapsync does. The switch is on the account dialog's External tab in the Control Panel (Windows) and on the Deck's fetch-accounts view; over COM (Windows) it is FetchAccount.ServerType = 1 and FetchAccount.MirrorFolders = true. Create the local account. Add an external account on it: server type IMAP, the remote host, port and security (implicit TLS on 993, STARTTLS on 143), the remote credentials, and the mirror on. Decide what happens on the far side. Days to keep messages at 0 deletes each message from the remote folder once it is filed here; any other value leaves it there, so the two servers stay in step for as long as the account polls. A migration usually leaves the messages in place until the cut-over, then one last poll, and the account is disabled. Poll. Download now starts a collection at once. A large mailbox is collected over several polls, and a message is recorded as collected only after it has been filed. A mirrored message is stored byte for byte: no X-hMailServer-ExternalAccount header, no delivery, no rules, no anti-spam or anti-virus, because it is a copy of mail the other server had already accepted. The remote flags and internal date become the local ones, and Archive/2025 becomes Archive.2025. Use a new external account for the mirror. One that has been collecting the INBOX without the mirror collects it once more when the mirror is switched on. A \Noselect mailbox is skipped with a line in the application log, OnExternalAccountDownload does not fire for a mirrored message, and subscriptions, ACLs, quotas, Sieve and vacation settings on the far side are not read. Many mailboxes: migration jobs Since 6.3.4, schema 6090. A migration is a batch the server runs itself: a job per mailbox, throttled, resumed after a failure or a restart, with a report per mailbox and the contacts and calendars beside the mail. It is administered under /api/v1/migrations and in the Control Deck's Migrations view; there is no COM interface, and in 6.3.4 no Control Panel page (in 6.3.7's Control Panel the migrations are a card on Backup, restore & migration). A migration names its source: kind imap (a password per mailbox), google or microsoft365 (each mailbox signed in to by its owner), or pst; concurrency (1 to 32, 4 by default); and messages_per_minute (0 for no limit). A Google or Microsoft 365 migration reads imap.gmail.com or outlook.office365.com on port 993 over TLS, and any other host is refused. A job is queued, running, paused, done, failed or cancelled, through the phases mail, contacts, calendars and report. POST /api/v1/migrations {"name": "Old server", "host": "imap.old.example"} POST /api/v1/migrations/{id}/jobs the CSV below, or one job as JSON POST /api/v1/migrations/{id}/start GET /api/v1/migrations/{id}/jobs/{job}/report GET /api/v1/migrations/{id}/summary.csv sourceuser,secret,target [email protected],Anna's old password,[email protected] [email protected],,[email protected] One request takes up to 5,000 lines. Every line is checked before any job is made, and a bad line refuses the whole request and names itself, so nothing is half added. A job into an account another migration is copying into now is refused (409). Google Workspace and Microsoft 365. A job added with an empty secret waits for its owner: the answer carries signin_url, a one-use link for that person to open, sign in as that mailbox and grant access. A sign-in as anybody else attaches nothing, and POST .../jobs/{job}/signin makes a new link. The scopes asked for are Google's https://mail.google.com/, https://www.googleapis.com/auth/carddav and https://www.googleapis.com/auth/calendar, and Microsoft's https://outlook.office.com/IMAP.AccessAsUser.All and offline_access with Microsoft Graph Contacts.Read and Calendars.Read. A work or school tenant may need an administrator's consent. What a job does. It maps the INBOX to the INBOX, a special-use folder (\Sent, \Drafts, \Junk, \Trash, \Archive) to the account's own folder of that use whatever it is called, and everything else to the same path. It opens each folder with EXAMINE, so nothing on the source is marked read, and stores each message byte for byte with its flags, keywords and internal date. A message too large is skipped as too_large; an account over its quota pauses the job. No rule, filter or scan runs over a copied message. Contacts and calendars follow: from an IMAP server by CardDAV and CalDAV found by RFC 6764 discovery (or dav_url), from Google by its CardDAV and CalDAV, from Microsoft 365 by Microsoft Graph. A phase that cannot run is skipped with its reason and never fails a job whose mail is copied. A job is one copy, not a synchronisation: move the MX records first and migrate after, or keep a mirror for the mailboxes that must stay in step. It survives being interrupted. After every 50 messages and at the end of each folder the job writes its resume marks, and a job that has run before reads back what it wrote, so each message arrives once. A failure time may mend is retried after 1, 5 and 15 minutes and then hourly; after 24 attempts in a row without a message copied the job is failed. Wrong credentials fail it at once, and POST .../jobs/{job}/retry with {"secret": "..."} takes a mended password. Throttling is threefold: concurrency per migration; MigrationConnectionsPerHost (4 by default, 1 to 64) across every migration from one source host, because a provider counts sessions per client address; and at most 64 jobs on the server at once. Who may. Reading needs view_accounts and writing manage_accounts in the target's domain. The server administrator creates a migration, and a domain administrator then sees and adds to it. The audit trail records migrations and jobs, never a secret's value. Jobs are not carried by a backup. HM7240 (a resume mark could not be written) and HM7241 (a message could not be filed) are the two codes that reach the error log for a job from an IMAP server, Google or Microsoft 365. A PST job says the same two as HM7370 and HM7371, and its contacts and calendars phases add HM7410 and HM7411. The report says, folder by folder, how many messages the source held, how many were copied and why any were skipped: duplicate, another_folder, too_large, refused, gone, empty. mbox files, a Maildir and accounts from a text file: the Import Tool The Import Tool is a Windows program (Start menu > hMailServer > Import Tool, or Addons\ImportTool\ImportTool.exe) that works through COM, each message going to the server through Utilities.ImportMessageFromFileToIMAPFolder; there is no Import Tool on Linux. It imports a folder of mbox files into one account, one IMAP folder per file; Inbox in any case is the INBOX, and a subfolder's files are a second run. Each message is dated from its Received or Date header. mbox carries no flags the tool could read, so every imported message is unread. Select Maildir points at the directory holding cur, new and tmp, and lists the INBOX and every Maildir++ folder beside it. The flags after :2, in a file name are set on the copy: S seen, F flagged, R answered, D draft, T deleted. A colon is not allowed in a Windows file name, so the tool also reads ;2, and the same marker written with an exclamation mark in place of the colon; a copy whose names lost the info part imports unread. Accounts from a text file. The tool's other button creates or updates the accounts of one domain from a comma-separated text file, three fields a line: the account's name without the domain, its password and its size limit in MB (0 for none). Where the breach check is on, it runs for each password the tool sets through COM's Account.Password, one request per account: run a large import against your own mirror of the endpoint, or with the check off. Outlook data files: PST import Since 6.3.4. The server reads a PST itself, with a reader of this project's own written from Microsoft's published specifications and no third-party PST code. It is a migration of kind pst: one job per file, each into one account. Off until a directory is named. A job names a file, never a path, and the file is looked for only in the subdirectory named for the target account's domain inside the directory pst_import_directory names (PstImportDirectory in hm_settings, set with PUT /api/v1/settings or on the Deck's Settings page). It is empty as shipped, and while it is empty every PST job is refused with No PST import directory is set on this server. D:\PstImport\ <- pst_import_directory D:\PstImport\example.com\archive.pst <- a job for [email protected] names archive.pst Copy the file into the domain's subdirectory, where the service's account can read it. A share on another machine is refused, and so is a name that climbs out of the domain's directory. Make the migration, POST /api/v1/migrations with {"name": "Archives", "kind": "pst"}, then jobs as CSV lines of file,password,target over REST or in the Deck, or one job as JSON with hmctl api POST /api/v1/migrations/{id}/jobs. There is no Import Tool button for it and no COM route. Start it. It needs manage_accounts and manage_mailbox in the target's domain. What comes in. The folders a person sees in Outlook, with Sent Items, Deleted Items, Drafts and Junk E-mail found by what the store says they are, never by their names. Every message is rebuilt as Internet mail, deterministically, so a resumed job knows what it filed. Read, unsent, flagged and replied state become \Seen, \Draft, \Flagged and \Answered, forwarded the keyword $Forwarded, categories keywords. Contacts go into the address book, calendar items and tasks into the calendars, notes and journal entries into mail folders as messages. ANSI files (Outlook 97 to 2002) are read. What it refuses, with a sentence saying what to do: the 4K-page layout Outlook 2013 and later write for an OST, an OST caching an Exchange mailbox, a file under Windows Information Protection, and a missing or wrong password. A file Outlook still has open pauses the job. A message over 128 MB of text and data together is left out and named. Migration.md is candid about its evidence: no password-protected PST and no OST from Outlook were at hand. The reader runs in a process of its own (hMailServer.exe --pst-read on Windows, hmailserver --pst-read on Linux, not a command for an administrator), killed on a pause, a cancel or a stop, or when it has not answered in ten minutes. A file that crashes it costs one message, not the service. This is crash containment, not a sandbox: the reader runs as the service's own account, bounded in memory (2 GB committed on Windows, 4 GB of address space on Linux). The fixture API.PstImport runs on Windows; nothing runs it on Linux yet. What is not migrated Rules and filters, signatures, out-of-office replies and forwarding: make them again here. Shared mailboxes and permissions; a Microsoft 365 shared mailbox, which nobody signs in as, cannot be a job. Subscriptions: every folder is created subscribed. Gmail's labels as labels. A google migration copies each message once, and the star arrives as \Flagged. Contact groups; from Microsoft 365, the default contacts folder and calendar only. A mailbox already here: renaming it, or moving it to another domain Do not copy it into a second account: that loses everything hanging off the account id, which is its rules, folders, application passwords, second factor, contacts, calendars and role. Move or rename (since 6.3.4) keeps the account's row, and leaves the old address as an alias unless you say otherwise. Relocating an installation, and moving or renaming a mailbox has where it is done, what moves and what is refused. Where to read more The wiki page: Migration The document, with every field, limit and refusal: Migration.md Renaming and moving a mailbox: MailboxMove.md The documentation page: Importing accounts and mail, the Import Tool On this forum: The Control Deck, where the Migrations view is, and The webmail portal at /portal, where Other accounts is.
  • 0 Votes
    1 Posts
    137 Views
    ProgressiverobotP
    This topic is about the settings themselves, whichever interface you use: where a setting is stored, which of yours are not what 6.3.4 ships with, when a saved value is in effect, and how the twenty-seven settings a domain or an account may hold its own value of combine with the server's. The one thing to know first: since 6.3.4 the database is the settings store. A [Settings] line edited by hand in hMailServer.ini changes nothing; at the next start the server uses the stored value, names the key in the error log as HM5804, and, where the service may write the file, puts the line back. Everything below describes 6.3.4 unless it says otherwise; a server on 6.3.3 still reads the file and applies a change at the next start. Two stores, and how a setting is changed The table hm_settings. Everything the COM Settings object has always carried, such as the anti-spam thresholds and the delivery options. Since 6.3.4 every new setting goes here, and no key is added to [Settings]. 6.3.4 added 193 rows for its features. The [Settings] keys. 241 keys read by the settings loader, plus UseLanguage, read per call. On 6.3.3 they live in the file, mirrored into hm_inisettings. Since 6.3.4 (schema 6042) hm_inisettings is the store and the file's section is a copy, which the server keeps current where the service may write the file. Where the service cannot write the file, the copy goes stale. That is a new Windows installation, whose service runs as NT SERVICE\hMailServer, and a Linux package installation, where /etc/hmailserver/hMailServer.ini is 0640 root:hmailserver. There the copy is not refreshed and an edited line is not put back; the database's values are still the ones used. The server reports this once a start in the application log, not as an error, and ServiceAccount.md says the same of the Linux packages; the wiki's Installing on Linux page names HM5806 at a save and HM5803 at every start for them instead. On such a server read a setting from the store, not from the file. The migration is the first start. A key the table has never seen is adopted with the file's value, and there is nothing to run by hand. A line edited after the old server last started, including one edited to prepare the upgrade, is taken for an edit and put back, so make such a change after upgrading. A 6.3.3 server refuses a database at schema 6042 or later with HM5011; see Upgrading to 6.3.4. What stays in the file. [Database], [Directories], [Security] and [GUILanguages] are the bootstrap, read before the database is open and never in the store. [LDAP], [SendingLimits] and [SendingLimitsOverrides] stay in the file. PasswordPepper is kept in the file only, never in the database and never in a backup. [SettingsOverride] is the one way to make the file win, for a database that is unreachable or holds a value the server will not start on: a key written there is applied over whatever is stored and named in the error log as HM5807 at every start, because no page shows it and no backup carries it. Storing a value for a key an override shadows is accepted and reported as HM5808. Surface Set a [Settings] key Return it to the default Control Panel (Windows only) the key's editor, written over COM even on the server itself No. Clearing a box stores an empty value, which a number reads as 0 REST PUT /api/v1/settings/ini/{name} with {"value": "..."} DELETE /api/v1/settings/ini/{name} hmctl, over REST hmctl ini set <name> <value> hmctl ini delete <name> PowerShell module Set-HmIniSetting none COM (Windows only) Settings.SetIniSetting(name, value) Settings.DeleteIniSetting(name) Linux command line, as root hmailserver --set-setting NAME=VALUE, with no listener running. A secret's value is refused on the command line: give the name alone and type the value at the prompt none Control Deck only SmtpTarpitCount, SmtpTarpitDelaySeconds, UseLanguage and RewriteEnvelopeFromWhenForwarding none The REST ini routes take the administrator password only; an API key of any scope is refused. Since 6.3.4 a setting the environment names, as HM_SETTING_<key> or HM_PROPERTY_<name> (configuration from the environment), refuses an administrator's change, 409 over REST, until a start without the variable. What this server has changed from stock Since 6.3.4. One list of every setting whose value is not the one this version ships with, with the shipped value beside it. The example in ChangedFromStock.md: STORE NAME VALUE DEFAULT ini LogLevel 5 9 ini MaxLogLineLen 2000 500 ini SMTPDMaxTimeout 600 1800 ini OAuth2HmacSecret (set) ini DnsServer 10.0.0.1 (forced from [SettingsOverride]) database smtpnoofretries 6 4 database maxsmtpconnections 200 0 7 of 391 settings are not what this version ships with. Where What to ask Control Deck Changed from stock, in the navigation beside Settings REST GET /api/v1/settings/changed Command line hmctl config changed PowerShell Get-HmChangedSetting COM Settings.GetChangedFromDefault The REST route and the COM call answer the same document. store is ini (hm_inisettings) or database (hm_settings). What "default" means. The value the server behaves as though the setting has when nothing has been stored for it, not what a fresh installation's files contain. hMailServer.ini ships with lines the packaging wrote, such as RestApiBindAddress=127.0.0.1, and a line holding the shipped value is not a change; one holding another value is listed. An upgraded installation lists more than a new one. database defaults are what a database created by this version holds. An installation upgraded from an old schema got its values from the upgrade scripts of the time, and where those differ (maxmessagesize, usecache, protocolimap, smtpallowincorrectlineendings and half a dozen more) the older installation is genuinely different and is listed. A row nothing reads any more (useordb, usespamhaus, tarpitcount, filters, imapport, relaymode) is not reported at all. The marks on an entry. secret: a setting whose name says its value is a secret is listed by name, with (set) or empty, and never shown. A key file, such as RestApiPrivateKeyFile, is a path and is shown. known false: the server holds a value and ships no default for it, such as a key stored for a script of your own. hmctl prints (none: this server does not read it). overridden: [SettingsOverride] is forcing the value. Such a key is listed even when neither store holds anything for it; hmctl prints (forced from [SettingsOverride]). environment is variable or file, and hmctl prints (set by the environment). profile names the setting profile whose apply wrote the value, while the setting still holds it. Which values these are. The value stored, not the value the server settled on where the two differ: a PasswordHashIterations outside the range the server accepts is reported as HM5561 and the built-in work factor used; an unusable PreferredHashAlgorithm is reported as HM5528 (or HM5607, for Argon2id on a build whose OpenSSL has none). Who may see it. The administrator password, and nothing else. An API key of any scope is refused with a 401, and an account holding an administrative role, an auditor included, with a 403, because the list includes commands the server runs: AutoBanCommand runs as the service account. The feature changes nothing. It does not compare two servers (two hmctl config changed --json answers and diff do), and it does not say when or by whom a setting was changed; the audit trail does (GET /api/v1/audit?object_type=setting). When a saved setting takes effect Since 6.3.4. Until then every [Settings] key was latched when the service started. Now a save publishes: the store takes the value, the server rebuilds its settings from the rows, and every key whose readers can take a new value is in effect when the save returns, whichever door saved it. Every key carries one of three marks: Mark Keys What it means now 211 A save applies it reinitialize 28 Bound into something the servers build when they start, so it waits for a reinitialise: Apply now in the Control Panel or the Deck, POST /api/v1/server/reinitialize, or Application.Reinitialize over COM restart 31 Read once per process: 2 [Settings] keys and the 29 bootstrap keys (20 [Database], 5 [Directories], 3 [Security], 1 [GUILanguages]) The 28 that wait for a reinitialise. What is rebuilt Keys The REST listener RestApiBindAddress, RestApiPort, RestApiCertificateFile, RestApiPrivateKeyFile The metrics listener MetricsServerBindAddress, MetricsServerPort, MetricsServerCertificateFile, MetricsServerPrivateKeyFile, MetricsServerAuthPassword, MetricsServerAuthToken, MetricsServerAuthUsername The web-services listener WebServicesBindAddress, WebServicesHttpPort, WebServicesHttpsPort, WebServicesCertificateFile, WebServicesPrivateKeyFile The ManageSieve listener ManageSieveServerBindAddress, ManageSieveServerPort The SSL contexts of the SMTP, IMAP, POP3 and ManageSieve listeners and the outbound client TlsCipherSuites13, TlsKeyExchangeGroups, TlsSessionCacheSize, TlsSessionTicketsEnabled, TlsSessionTimeoutSeconds, TlsTicketKeyRotationSeconds ACME, the database pool, the fetch queue AcmeEnabled, AcmeCertificateDirectory, DatabaseStatementTimeout, MaxNumberOfExternalFetchThreads The 2 that wait for a restart are ServiceAccountName and ServiceAccountPassword, which are retired: since 6.3.4 hMailServer.exe /Register takes the account from its command line alone, and a stored value is shown as pending until the next start. The bootstrap keys are never in the store, so none is ever pending. Some keys are now because a subscriber re-latches them. MaxSubmissionsPerIPPerMinute and MaxOutboundPerDestinationPerMinute start each limit's one-minute window again, so a saved limit counts from when it was saved. A cache of data is not a setting: a changed DNSServer is used for the next lookup, and an answer already cached stays until its TTL runs out. Pending. A reinitialize or restart key whose stored value is not the value the server is running on is pending. The list is recomputed at every request and never remembered, so a value put back before anything applied it is not pending. Where What REST GET /api/v1/server/settings/pending answers [{name, stored, running, applies, secret}], and applies and pending come on every /api/v1/settings/ini/{name} answer. The administrator password's route COM Settings.IniSettingApplies(name), Settings.IniSettingAppliesAll, Settings.PendingIniSettings Control Panel Applies on save, After reinitialize or After a restart under each field kept in the store, and a pending bar with Apply now Control Deck The same words as a badge, and Apply now above its Settings view A name the loader does not read (UseLanguage, a key a script of your own reads, a misspelling) has no mark: COM's IniSettingApplies refuses it and REST answers applies: null. Apply now is a reinitialise. Every service stops and starts again in place, which takes every reinitialize key and not a restart key. It asks first. A reinitialise ends maintenance mode, and POST /api/v1/server/reinitialize answers {"reinitializing": true, "drained": true} when it ended a drain. The Deck's session ends with the REST listener it was using; the Control Panel's COM session survives. What a save does not do. It does not re-read the bootstrap sections or [SettingsOverride]; a reinitialise or a start re-reads the override section and announces it as HM5807. It does not reconcile the file against the table; that is a start's and a reinitialise's only. And a value stored by another process (another node on the same database, or hmailserver --set-setting while the service runs) reaches this server at the next save here, a reinitialise or a restart, because nothing watches the table. hmctl has no pending verb. On Linux the regression fixture alone proves the marks, with the three COM members answered over REST; the internal tester that swaps settings under load does not run there. One value, three levels Since 6.3.4 (schema 6088). Twenty-seven settings may be set for the server, for a domain and for an account, and every place that shows one shows what is in effect and where it came from. For an account: its own value, else its domain's, else the server's. Every setting on the list is exactly one kind: Kind How many The rule Who sets a level below the server Limit 15 A level below may tighten the level above and never loosen it. A looser value is refused when it is written; a value the level above has since tightened past is capped when it is read Anybody who administers that level: a domain administrator, a key restricted to the domain Gate 4 The deepest level that holds a value wins, either way The server administrator alone; a domain-restricted key is refused with 403 Preference 8 The deepest level that holds a value wins The domain's administrators, and a domain-restricted key The last column is the rule for the levels held in hm_settingoverrides. A field the domain already had keeps the authority it had, whatever its kind: the domain's maximum message size and the domain's retention are the server administrator's. Kind Settings Limits max_message_size_kb, session_idle_seconds, session_lifetime_seconds, session_one_per_device, session_bind_to_address, quota_grace_percent, quota_send_stop_percent, recall_days, recover_deleted_days, sending_max_messages, sending_max_recipients, reader_fetch_accounts_max, reader_sender_addresses_max, reader_masked_addresses_max, reader_offline_days_max Gates plus_addressing_enabled, openpgp_portal_enabled, zero_access_allowed, directory_user_editable Preferences quarantine_digest, quarantine_digest_hour, external_tag_subject, external_tag_header, impersonation_action, quota_warning_percent, country_policy, message_retention_days A server setting not on the list is the server's alone: its name is refused on the domain and account routes (400, unknown field) and by COM's SettingOverride. Of the [Settings] keys only QuotaWarningPercent has levels. Where each level is held. A field the domain or the account already had, such as the domain's MaxMessageSize or MessageRetentionDays, stays where it was, with the meaning it always had; its 0 or -1 is the level holding nothing. The rest are rows of hm_settingoverrides, where no row means "take the level above". An owner's rows are cached for sixty seconds; every write through the interfaces drops the cache at once, and so does POST /api/v1/settings/cache/clear or COM Cache.Clear. A row put in by hand that the interfaces would refuse is treated as holding nothing and reported as HM7311. Nothing changes for a domain or an account that holds no value of its own. Where What REST GET /api/v1/settings/levels lists the twenty-seven. GET /api/v1/domains/{domain} and GET /api/v1/accounts/{address} answer <name>_effective and <name>_source, and <name> where the level is held in the overlay. For a level held in the overlay a PUT takes a value of the setting's own JSON type, and null makes it inherit; for a level held in a field of its own null leaves it unchanged, and the field's own 0 or -1 is "take the level above" COM (Windows only) Domain.SettingOverride(name) and Account.SettingOverride(name), an empty string for "take the level above", written at once and not by Save; EffectiveSetting(name) and EffectiveSettingSource(name) hmctl hmctl domain set <domain> <name> <value\|inherit> and hmctl account set <address> <name> <value\|inherit> Control Panel (Windows only) A Levels tab on the domain and account dialogs, with "In effect: ..., from the ..." beside each setting and Save levels now Control Deck The same card in its domain and account editors A saved level applies at once, at every level and from every door, because the resolver is asked when the value is used (at MAIL FROM, when a session is checked) and never latched. Deleting, backing up and moving. Deleting an account or a domain deletes its rows; a delete that cannot remove them is reported as HM7310 for a domain and HM6109 for an account. The XML backup carries the rows, and on restore a row the build refuses is left out and reported as HM7312. A mailbox moved to another domain keeps its own values. What is thinner than it sounds. Attachment blocking, web push, passkeys and account recovery have no levels. The domain's session seconds and switches and its quota grace and send-stop shares could always be set looser than the server's and still can; the ceiling binds the account below them. The maximum message size is resolved at MAIL FROM and at IMAP APPEND; the EHLO SIZE advertisement, the webmail's own send and the send API stay the server's. The session policies are the browser sessions' (the webmail, the portal, the Deck); IMAP, POP3 and SMTP sessions have none. country_policy cannot be set to empty over COM, where an empty string means "take the level above"; REST's "" does it. Twelve of the twenty-seven are proved by what the account and its domain answer rather than by driving the behaviour per account, and the levels' own regression fixture is Windows-only. When a setting appears to do nothing Is it pending? A reinitialize key waits for Apply now and a restart key for a service restart. Was it typed into hMailServer.ini? The stored value wins and HM5804 names the key. Store it instead. Is the key spelled exactly as the code reads it, and is a boolean literally 1? An unknown key is the default, and only 1 is true: 2, yes and true all read as false. Does it need a companion? SRSEnabled needs SRSSecret, ACME needs AcmeDomains, autoconfig needs WebServicesHttpPort or WebServicesHttpsPort, and MTA-STS hosting needs the HTTPS one. Does the error log show HM5807 or HM5808? A [SettingsOverride] section is forcing a value. Where to read more The tour: https://gitlab.com/hmailserver/hmailserver/-/wikis/Settings-Overview. Every key with its default, validation and mark: https://gitlab.com/hmailserver/hmailserver/-/wikis/Settings-Reference. Where the two disagree, the reference is generated from the code and is right. The three documents behind this topic: https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/ChangedFromStock.md, https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SettingsApply.md and https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SettingLevels.md The public documentation's settings chapter: https://www.progressiverobot.com/hmailserver-documentation/#21-settings-reference-hmailserverini On this forum: the Control Panel on Windows, hmctl and the PowerShell module, the hmailserver command line, the Control Deck, Upgrading to 6.3.4 and hMailServer on Linux.
  • 0 Votes
    1 Posts
    126 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) refuses a state-changing REST request that carries a credential - HTTP Basic included - when the browser reports it as cross-site (Sec-Fetch-Site, or an Origin that is not this server's); the Deck and the webmail act on their own origin, and scripts, hmctl and the PowerShell module send no such header, so none of them is affected. A credential confined to named domains, a domain administrator among them, is refused a domain's rename, a domain alias, a custom role's creation, change or delete, a domain's or an account's rule's bind_to_address, send_using_route or script_function action, its domain's relay host changed to an address that is not public and the server-wide metric history, over REST as over COM, and so from the Deck. 6.3.7 also carries the first step of the Control Deck's and the webmail's rebuild planned for 6.4 - the framework decision (ControlDeckFramework.md), the threat model of the web surface and the authorisation matrix with its check - with no product code. See the 6.3.7 announcement. The Control Deck is the administration page hMailServer serves at /, on the same listener as the API and the portal. On Linux it is the only administration front end there is, because that build has no Control Panel and no COM. On Windows it does not replace the Control Panel and it does less. Turning it on. One setting, RestApiPort, turns on the API, the Deck and the portal together. The default is 0, which leaves all three off, so a server upgraded without touching its settings serves none of them. The listener refuses to start rather than come up without a credential or without TLS, and logs which. Four messages begin RestApi: Refusing to start. The two you will meet are an unset administrator password and a non-loopback bind with no certificate. The TLS exemption applies only to a bind address that is exactly 127.0.0.1, localhost or ::1. 36.2 Signing in. The password goes once to POST /api/v1/session and is exchanged for an hmailsession cookie that is HttpOnly and SameSite=Strict. Two ceilings apply, 30 minutes idle and 12 hours absolute. Since 6.3.4 those are the defaults: SessionIdleSeconds and SessionLifetimeSeconds, server-wide or per domain, change them, and neither is set as shipped, after an upgrade or in a new database (DevicesAndSessions.md). Any cookie-authenticated request whose method is not GET or HEAD must carry X-Requested-With: hMailServer or it answers 403; HTTP Basic and bearer keys are exempt. Since 6.3.7 a state-changing request that carries any credential is refused when the browser reports it as cross-site (Sec-Fetch-Site, or an Origin that is not this server's); scripts, hmctl and the PowerShell module send no such header and are unaffected, as are the Deck and the webmail, which act on their own origin. An API key cannot mint a session. Since 6.3.4 an account that holds an administrative role signs in with its own address and password, and a passkey can start the same session, as can the organisation's single sign-on for the addresses named in oidc_administrators or saml_administrators (both empty as shipped). Sessions are process-local, so a service restart ends every one. 36.3 What it does. Dashboard, Domains, Delivery queue, DANE/TLSA, Settings, Rules, Routes, Certificates, Ports and Logs. 6.3.3 added full domain editing, the account editor in full, distribution lists and aliases, an IP-ranges view, fetch-account and backup views, the scripting, cache and indexing groups, and a CI harness of 292 checks. At 6.3.4 it has 40 views, in twenty languages besides English. The settings forms are worth understanding. The Deck fetches GET /api/v1/openapi.json once per session and draws each form from the schema of that group's PUT. Nothing in the page lists a settings key by name, so a key added to the API appears on its own, with its type, its permitted words, and a badge when it is read-only, write-only, required, or takes effect only on restart; since 6.3.4 a key kept in the settings store says instead whether a save applies it at once, after a reinitialise or after a restart. The Ports view carries a Restart the services now button, which posts to /api/v1/server/reinitialize. It drops connections in progress and ends your session with everyone else's. 36.4 What the Deck cannot write is measured, not asserted. build/check-deck-parity.py counts every field the desktop Control Panel writes against the REST API and the Deck, and reports in hmailserver/docs/DeckParity.md. Of 330 properties, 240 were writable over REST and 153 reachable from a Deck view when that work started; 328 and 322 at 6.3.3. At 6.3.4 the census counts 466, finds 458 of them writable over REST and every one of those reachable from a Deck view, and lists eight as missing: an account's directory fields (title, department, phone, mobile, office, note, photo and the opt-out). Those are written too, by PUT /api/v1/accounts/{address}/directory and the directory card of the Deck's account editor, under names the census does not match. 36.5 now opens with the 6.3.4 census and keeps its description of the 6.3.1 gaps, which were far wider, below it for the record; the 6.3.4 section of CHANGELOG.md is the current statement. Check DeckParity.md in the source tree before assuming a route does not exist.
  • 0 Votes
    1 Posts
    134 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.8 (2 November 2026) is the current release, a security release: its Linux packages are, as 6.3.7's (17 October 2026) were, the generic .deb and .rpm for x86-64 and AArch64, a package for each of Debian 12 and 13, Ubuntu 24.04, Fedora 43 and 44, Enterprise Linux 9 and 10 and openSUSE Leap 16.0 and Tumbleweed, and both AppImages, and its container image is registry.gitlab.com/hmailserver/hmailserver:6.3.8; the AArch64 files are cross-compiled and tested under emulation, and the Ubuntu PPA and Fedora Copr, which have published 6.3.7 since 21 October 2026, are sent it by a job started by hand. From 6.3.6 or 6.3.7 the live update installs it through the root helper, for the generic packages and the AppImages; a server from one of 6.3.6's or 6.3.7's per-distribution packages upgrades with its package manager and its release's own package, and from 6.3.4 or 6.3.5 install it with apt, dnf or zypper. 6.3.8 names files in UTF-8 whatever LANG says, so on a server that ran under a LANG that is not UTF-8, rename each file and folder named outside ASCII to its UTF-8 name before upgrading. See the 6.3.8 announcement. Since 6.3.0 the same source tree builds, installs and delivers mail on Linux, as a systemd service, from a .deb, an .rpm or an Arch PKGBUILD: on x86-64 and AArch64 up to 6.3.3, and on x86-64 only in 6.3.4's packages. That is why the release is numbered 6.3 and not 6.2.29. Read §35.1 before you install anything. Finding out about a missing capability after the mailboxes are populated is the wrong order. What crossed The SMTP, POP3 and IMAP engines, delivery, routing, rules, anti-spam, anti-virus, ACME, the backup manager, the REST API, the Control Deck and the self-service portal. The on-disk message format is unchanged: one directory per domain and per mailbox, one file per message. The database schema is the same version the Windows build of the same release needs. The packages 6.3.4's .deb installs with apt on Ubuntu 26.04, where it is built, and on Ubuntu 24.04. It needs glibc 2.38 or later, so it does not install on Debian 12. Its .rpm installs with dnf on Fedora 43 and 44 and Enterprise Linux 10, not on Enterprise Linux 9, whose glibc and libstdc++ are older than it asks for, and nothing has been tried with openSUSE's zypper. There is no apt or dnf repository yet. Arch is a PKGBUILD in the tree, built with makepkg -si from the git tag, which it clones from https://gitlab.com/hmailserver/hmailserver. 6.3.4's packages are x86-64 only: the AArch64 packages were last published with 6.3.3, built natively on the CI the project lost with its GitHub account. Nothing else is packaged. The AppImage is not for running mail. It runs as whoever started it, with no hmailserver user, no unit and none of the systemd hardening, it puts its store under the invoking user's home directory, and it cannot bind port 25. Use it to see the thing work on a laptop. 6.3.4 has none; the last AppImage is 6.3.3's. §35.2. Where things live Configuration is /etc/hmailserver/hMailServer.ini at 0640 root:hmailserver. The store is /var/lib/hmailserver, the logs /var/log/hmailserver, the unit /usr/lib/systemd/system/hmailserver.service. The service runs as hmailserver:hmailserver and never as root. Full table: §35.6. The backends are PostgreSQL through libpq, a link-time dependency, and MySQL or MariaDB through a client loaded with dlopen at run time, which nothing pulls in for you. SQL Server and SQL Server Compact are refused by name, so an INI carried across from Windows is repointed or it does not start. The gaps, stated plainly No Control Panel and no COM API. Both are Windows-only and are not compiled here. Every third-party COM script stops at that boundary. Event scripts in JavaScript only, since 6.3.4. EventHandlers.js runs in an embedded QuickJS engine, for the same events under the same function names; the scripting language must be set to JavaScript (hmctl settings set scripting --set-string language=JavaScript), and VBScript and JScript are refused. Scripting is off until you turn it on. A script has no file, process or network access and cannot create COM objects, so a Windows script is ported, not copied (EventScripts.md). Domain properties were not writable when Linux arrived, and are now. At 6.3.0 per-domain DKIM, per-domain size limits and the domain signature were readable over REST and not writable. 6.3.3 made PUT /api/v1/domains/{domain} take the whole domain - its DKIM signing, its limits, its signature and its relay host among the rest - and gave the Control Deck full domain editing. At 6.3.4 the project's census finds 458 of the 466 properties the Windows Control Panel writes writable over REST and reachable from a Deck view, and the eight it lists as missing, an account's directory fields, are written on a route of their own, PUT /api/v1/accounts/{address}/directory, which the Deck's account editor uses (DeckParity.md). Self-update from 6.3.4, but not to it. The packages now install hmailserver-update, a root helper outside the server's unit, and the live update, when an administrator asks it to install, applies a .deb, an .rpm or an AppImage through it, rolling back when the server does not come back. The update check is off until you turn it on. 6.3.4 itself carries no Sigstore bundle, so no live update installs it: every server reaches 6.3.4 with apt or dnf, and the helper has nothing to apply before a release signed on the project's GitLab CI. Arch upgrades with pacman. No tested path from Windows. The schema and the store format are shared, so the pieces are there. Nobody has run a Windows installation onto Linux and verified the result. §35.14 says what is known. Administration is therefore the Control Deck at / on the REST listener, and the REST API behind it. §35.10 covers the daily jobs.