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
    107 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) changes the anti-virus action that removes attachments (Remove all attachments and deliver the text in 6.3.7's Control Panel, Delete infected attachments only in 6.3.6's): an S/MIME message signed as one part, which is opaque but not encrypted, is now replaced by the action's note when the scanner finds something in it, where every earlier version delivered it as it arrived, and a message the server cannot read is deleted, its recipients told; an encrypted message is still delivered as it is (work item 92). See the 6.3.7 announcement. In hMailServer 6.3.8 (2 November 2026) the web services listener refuses an address an auto-ban or an IP range bans (work item 105), so an IP range of your own that allows some addresses none of SMTP, POP3 and IMAP now closes that listener to them, and with it the Web Key Directory: check that the clients that look keys up there do not come from such a range. See the 6.3.8 announcement. This topic is about encrypting the message rather than the connection, so that the person it is for can read it and nobody on the way can, this server included. S/MIME in the webmail has been there since 6.3.2; everything else here is new in 6.3.4. The design rule running through all of it is that this server holds nothing that decrypts a person's mail: keys are made in the browser and kept wrapped under a passphrase or password the server never sees. The one thing to know before you allow any of it is the cost of that rule: nothing on this server can recover mail encrypted to a key that was lost. There is no escrow. Which one you want You want Use S/MIME inside the organisation without buying a certificate per person A domain's S/MIME authority Correspondents who use GnuPG, Thunderbird, Proton or Mailvelope OpenPGP in the webmail, and the Web Key Directory To write privately to somebody who has no key at all A password-protected message A mailbox the administrator cannot read Zero access Everything to one partner organisation encrypted, whatever the sender's client did Gateway encryption, on a remote domain policy A stolen disk or backup to hold nothing readable, while the mailbox works as before Mailbox encryption at rest, which the server can read and so is not in this topic All of the browser cryptography needs a page opened over HTTPS: a browser withholds crypto.subtle from any other, and the page says so instead of offering Sign and Encrypt. S/MIME with your own authority Since 6.3.4 (schema 6095). A domain may have its own S/MIME authority: a root certificate the domain owns, which certifies the keys its accounts make in the webmail, publishes them for colleagues to look up, and says what it has revoked in a signed CRL. Nothing is issued until an administrator creates one. Who holds what. The person's private key never leaves the browser. The webmail makes the key pair, builds a PKCS#10 request, wraps the private key under the account password, and sends only the request. The authority's own key is on the server, because it signs there, and it is protected by the platform's secret store: Where the authority's key opens Windows DPAPI with the machine's key. It opens on this machine and nowhere else, and on this machine to any process that asks Linux The stored-secret key file, <DataFolder>/.hmailserver-secret-key (AES-256-GCM, mode 0600). It opens wherever that file is It never falls back to the legacy scheme. When the store is unavailable the authority is not created (503). Creating it. In the Control Deck, open the domain and find S/MIME authority, or POST /api/v1/domains/{domain}/smime-ca with {"key": "p256"} or "rsa3072". The root is self-signed, CA:TRUE with pathlen:0, valid ten years, with a critical name constraint permitting the domain's own addresses and nothing else. P-256 is the default. The Control Panel shows nothing of it, and there is no COM surface. Enrolment. A person finds Get a certificate from {domain} under Settings → Security → S/MIME. They pick the key type. RSA 2048 is the default and the one to choose. A P-256 certificate from this page signs, and neither this webmail nor a colleague's can encrypt to it. The page makes the key pair and the request, and asks for the account password that wraps the private key before anything is sent. POST /api/v1/me/smime/enroll is made from the account's own sign-in. An app password and an administrator's support session are refused with 403. The server checks the request. It must name exactly one address, and that address must be the account's own or an active alias of its own domain that points at it. The certificate is issued pending. When it arrives in the account's own store it is confirmed, and only then is it answered to colleagues and the address's earlier certificates revoked as superseded. The limits are five enrolments a day (429) and ten valid certificates at once (409). A person with two devices ticks Keep my other certificates for this address on the second. What is issued. Valid one year. With smime_authority_base_url empty, which is the default, a certificate carries no CRL distribution point. What that costs: Outlook and Thunderbird cannot fetch the CRL from the certificate, so a revocation reaches them only when they have been given the CRL another way. This webmail does not use the CRL at all; it reads the authority's own record. Publication and trust. The root and the CRL are served over plain HTTP by the web services listener (WebServicesHttpPort), at /smime-ca/{domain}.pem, .cer and .crl. A signature verifies in Outlook, Thunderbird, Apple Mail or a phone only once the root is installed on the clients, by group policy, a device-management profile or by hand. This webmail trusts the domain's authority by itself. A colleague's certificate is looked up with GET /api/v1/me/directory/{address}/smime, which the webmail asks on its own when Encrypt is ticked. Moving and restoring. The authority is in the configuration backup, but its key opens only where its envelope opens. Restored anywhere else, the Deck shows a warning, enrolment is refused with 503, its CRL is not served, and the authority must be rotated there. Rotation makes a new root and retires the old one without deleting it, and clients must install the new root beside the old. What it is not. Not Active Directory Certificate Services. No OCSP. No key escrow: a forgotten password that the key was wrapped under leaves the mail encrypted to that key unreadable, by anybody, for ever. No elliptic-curve encryption in the webmail. One address per certificate. No automatic renewal: a person enrols again before the year is out. OpenPGP: the half in the directory and the half in the browser The directory The page Where it runs On the server In the person's browser What it holds Public keys only Own keys, the secret half encrypted under a passphrase, and correspondents' public keys What cryptography it does None at all All of it: signing, encrypting, decrypting, verifying, in PGP/MIME Table and schema hm_pgpkeys, schema 6050 hm_pgpring, schema 6080; the trust columns 6081 State as shipped Three switches, all off portal_enabled on In neither half does the server sign, verify, encrypt or decrypt a message. It makes an OpenPGP message itself in only two places further down, a delivery to a zero-access mailbox and gateway encryption to a partner's key; there it encrypts, never signs or decrypts, and verifies only the binding of the subkey it encrypts to. The directory publishes keys at a URL derived from the address, so that anybody who has the address can find the key. <hash> is the local part, lower-cased, SHA-1 hashed and z-base-32 encoded. Both methods are served: Method The key advanced https://openpgpkey.<domain>/.well-known/openpgpkey/<domain>/hu/<hash> direct https://<domain>/.well-known/openpgpkey/hu/<hash> Three switches have to be on before one key is served: The server: OpenPGPWebKeyDirectoryEnabled, set with hmctl settings set openpgp --set web_key_directory_enabled=true or PUT /api/v1/settings/openpgp. The domain: openpgp_publish on the domain. The account: openpgp_publish on the account. This one is the person's own, through PUT /api/v1/me/openpgp/publish. The web services listener has to exist (WebServicesHttpsPort), and the advanced method, which clients try first, needs an address record for openpgpkey.<domain> and a certificate valid for it. Keys are served over HTTPS only. Every refusal is the same 404, whether the domain is unknown, a switch is off, or the account has no key, so that the directory cannot be used to ask whether somebody has an account. Put a key in with hmctl openpgp store [email protected] ann.asc --publish, and check it as a correspondent's client will: gpg --locate-keys --auto-key-locate clear,wkd [email protected] An upload carrying a secret-key packet is refused; so are a version 3 key, a key with no user id or no self-signature, a key past OpenPGPKeyMaximumBytes (65536), and a key whose user ids do not carry the account's own address. An account may hold OpenPGPKeysPerAccount keys (8). A signature on an uploaded key is not verified cryptographically: what makes a key in this directory the right one is that its owner or an administrator put it there. The desktop Control Panel has nothing for the directory yet. The page is /portal-openpgp.js, written on the Web Crypto API with no library vendored. Keys. Curve25519 (the default) or RSA 3072 or 4096, made in the browser. The secret key is stored as OpenPGP writes one, under the passphrase, so the bytes the server holds are bytes it cannot open, and the same bytes import into GnuPG. A revocation certificate is made at the same moment and saved to the device. Autocrypt Level 1, both ways. Finding a key. GET /api/v1/me/openpgp/lookup?address= asks the correspondent's own Web Key Directory (lookup_wkd, on) and then a keyserver (lookup_keyserver, off by default; keyserver_url is https://keys.openpgp.org). Nothing is stored by the lookup. Trust made visible. A different key arriving for an address the ring already holds a key for is stored as a change. Until the person accepts it, the page does not encrypt to it. A key can be marked verified in person, and the mark is never inherited by a key that replaces it. Who may write. Every write to the ring is refused to an app password and to an administrator's support session. Not implemented, and refused by name: version 5 and 6 keys and AEAD; DSA and ElGamal keys and the NIST and Brainpool curves; smartcards and hardware tokens; the web of trust; a draft or a scheduled send that is to be sealed. Curve25519 needs Chrome 137, Firefox 130 or Safari 17 or later. Both directions are proved against GnuPG 2.4 by build/check-portal-openpgp.js, which GitLab CI runs on every push and every merge request. A password-protected message (schema 6082) is for the correspondent with no key. The page encrypts the text and attachments to a password as one OpenPGP message, and what goes out by SMTP is a short notice with a link to /portal/protected/<token>, where the recipient's browser decrypts it. The link is the whole access control. It needs portal_enabled on and a public address for the links (the confidential_base_url chain the messages that expire use), and is offered only over HTTPS. A message lives protected_message_days (30 by default). The ceilings are 25 MB a message and 250 MB an account. It does not promise that the recipient cannot copy what they read. Zero-access mailboxes Since 6.3.4 (schema 6083). A mailbox this server holds and cannot read. An administrator allows it server-wide once (zero_access_allowed, under /api/v1/settings/openpgp, off by default); the person then switches it on for their own account on the webmail's Security page. From then on every message delivered to the account is encrypted to the account's own OpenPGP key - after the spam and virus filters, the rules and the Sieve script have had the text - and stored as PGP/MIME with the headers in the clear. An administrator sees the switch read-only everywhere. What it costs. The webmail refuses to switch until the recovery key has been typed back and the sentence about a lost password has been ticked. Cost What it means The administrator cannot read the mail The message source and the account export answer 403 Neither the administrator nor the server can search it The index and IMAP SEARCH read the headers, never the text. A word in the subject is found; a word in the body is not Nobody can recover it A forgotten password without the recovery key is the mail lost, for ever IMAP and POP see ciphertext Valid PGP/MIME that a client holding the key opens. Until then the account is a webmail account The plaintext was on the disk From acceptance until the seal. The account's copy is overwritten with zeros; whether the bytes are gone is the filesystem's decision Only what is delivered is sealed, and the Sent copy A draft, and anything a client uploads with IMAP APPEND, is stored as it is A journal rule sees the plaintext Journaling runs at acceptance, before delivery The filters have read it A forward goes on in the clear. A rule that files into a public folder files the plaintext there. Quarantined mail is held in the clear Switching off decrypts nothing Mail that arrived while it was on stays encrypted to the key The headers the server kept. From, To, Cc, Date, Subject, Message-ID, the Received trace and the rest stay in the clear, so that the listing, the threading and the spam folder work without the key. The server adds X-hMailServer-Zero-Access: sealed on delivery. The key, in two sealed copies. hm_zeroaccess holds two copies of the secret key, both sealed in the browser: one under a passphrase the page derives from the account password with Argon2id, three passes over 64 MiB in four lanes. It was measured at 1.6 seconds in Node 24 on the development machine; a phone is expected to take several seconds, and it was not measured on one; one under the recovery key: 30 random octets shown once as eight groups of six base32 characters. A key that carries an expiry is refused at the switch-on. When a seal fails, the delivery is refused rather than stored in the clear: the sender is told with 4.3.0 and the error log carries HM7070. Passwords. A change through the webmail carries the key sealed again under the new password, and the server refuses a change without it (409, zero_access_rewrap). A password an administrator sets cannot re-wrap; at the next sign-in the page asks for the previous password or the recovery key. It is not encryption at rest. Mailbox encryption at rest stores mail under a key the server holds while the person is signed in. The two are never on one mailbox: each switch is refused where the other is on. Not built: a local bridge for IMAP or POP clients; search over the decrypted text in the browser; encryption of mail already in the mailbox when the switch is thrown. What the server does with a sealed message A sealed message is one signed or encrypted end to end, which must leave this server byte for byte as it arrived. An opaque one is a sealed message the server cannot read at all. A multipart/signed message is sealed but not opaque: its text is in the clear. Opaque is decided from the body, never from the label, or every check that stands aside for an encrypted message would be one header away from being switched off by anyone sending mail. The body must be S/MIME whose one part begins a CMS object (enveloped-data, authenticated-enveloped-data or signed-data), or PGP/MIME of exactly two parts: Version: 1, then one armoured OpenPGP message. None of what follows is a setting. Part of the server What it does with an opaque message Blocked-attachment list Never rewrites it. One application log line says the list was not applied. In 6.3.3 and earlier, blocking *.asc or *.p7m destroyed the ciphertext without a log line Virus scanner Scans it, and logs that its content COULD NOT BE EXAMINED for viruses. It is not treated as a scanner failure, so AVFailAction=1 does not hold it. If mail the server cannot scan must not be delivered, use a rule or a Sieve script on body :raw Delete attachments action (Remove all attachments and deliver the text since 6.3.7) Never strips an encrypted message. Since 6.3.7 an S/MIME message signed as one part - application/pkcs7-mime holding signed-data, opaque but not encrypted - is replaced by the action's note when the scanner finds something in it, where it was delivered as it arrived (work item 92). Set the action to delete the message if such mail must not be delivered Rules A body criterion does not match, whichever way it is phrased, with one log line per rule per message: under AND the rule does not fire; under OR its other criteria still count. To catch encrypted mail, test Content-Type for multipart/encrypted or application/pkcs7-mime with a custom-header criterion Sieve The evaluation is unchanged, as RFC 5173 requires; a log line is new. Use body :raw to recognise such a message Search and the index Only the header is indexed Content spam scoring Sees nothing in it, so encrypted spam is scored more leniently. SPF, DKIM, DMARC and DNSBL are unaffected A message only labelled encrypted, whose parts are readable, is blocked, stripped, filtered and searched like any other. In SealedMail.md's words: a message that arrives encrypted is not scanned, and your mail client, and you, are the last check. Gateway encryption, briefly Since 6.3.4 (schema 6094). On a remote domain policy, Encrypt mail to this domain at this server encrypts every message to one partner, here, to the partner's own gateway key. It encrypts at the server, as a delivery to a zero-access mailbox does, and the server holds no private key for it. The Subject is not hidden. When a message cannot be encrypted the default is to refuse it with 5.7.10. It does not sign, look keys up, or decrypt replies. Where to read more Operator documents at the v6.3.4 tag: SmimeAuthority.md, OpenPGP.md, ZeroAccess.md, SealedMail.md and RemoteDomains.md. Wiki: Encryption for People and Encryption and Certificates. Public documentation: 36.6 The portal: what a user gets. On this forum: The webmail portal at /portal, The Control Deck, Moving to new hardware before you carry an authority's key to another machine, and the rest of this category.
  • 0 Votes
    1 Posts
    132 Views
    ProgressiverobotP
    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) signs an authenticated SMTP submission as another domain hosted here only when its account may send as the address its From names - the list the From picker below draws on: its own address, an alias pointing at it, a mailbox that granted it the post right, an address it proved or a masked address of its own; otherwise the mail is still accepted and sent, unsigned as that domain. The webmail's own send already holds From to the account, so mail sent from the page is signed as it was. 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, the threat model of the web surface and the authorisation matrix with its check - with no product code. See the 6.3.7 announcement. This topic goes deeper into the webmail that hMailServer serves at /portal on the REST listener; what the page is and how it is turned on is in The webmail portal at /portal. The one thing to know first: most of what follows is the reader's own choice, bounded by a setting the administrator holds. Search with operators, the keyboard map, the rich-text editor, the From picker over aliases and granted mailboxes, plain signatures and the offline inbox listing were in 6.3.3; the three stars, the importance marker and its keys, and the rest of what follows are since 6.3.4. What the administrator bounds Each is a setting on PUT /api/v1/settings and the Control Deck's Settings view; since 6.3.4 many can also be set per domain or per account. Setting What it bounds Default recover_deleted_days How long deleted mail can be put back 0, off; at most 365 reader_offline_days_max The most days a reader may keep offline 30; at most 365; 0 offers none reader_masked_addresses_max Masked addresses per person 20; at most 200; 0 takes the page away reader_sender_addresses_max Send-as addresses per person, proved or not 5; at most 20; 0 takes the section away brand_logos_enabled Brand logos at all On; needs AuthenticationResultsEnabled and DMARC confidential_base_url The address the webmail is reached at from outside, which links are written against; falls back to quarantine_digest_base_url, then import_oauth_redirect_base_url Empty Search operators and keyboard shortcuts The search box searches the folder shown; the options panel beside it builds the same syntax from fields and can search every folder the account may read. Operator What it matches word, "quoted phrase" The word in the subject or the sender, failing those anywhere in the message; the words together, in that order -term, a OR b What follows the minus must not match; one of the two must match. OR in capitals - or is a word from:, to:, cc:, bcc:, subject: The sender's address or name; a recipient's, in To or Cc; an address or name in Cc, or in Bcc (only in the sender's own copy); the subject has:attachment, has:link, filename: An attachment; an http or https link; an attachment whose file name contains the text larger:, smaller: By size, as in larger:10M before:YYYY-MM-DD, after:YYYY-MM-DD By date older_than:, newer_than: By age: 7d, 2w, 3m, 1y in:folder Messages in that folder; a whole path may be given is:unread, is:read, is:flagged, is:unflagged, is:answered, is:muted, is:pinned By the flag or keyword label:name An IMAP keyword; label:$Important is the sidebar's Important view category: The inbox tab: primary, social, promotions, updates or forums in_reply_to: Messages whose In-Reply-To or References names the message-id Since 6.3.4 a search request opens at most 2,000 message files and stops after about two and a half seconds, saying where to go on from; Search older messages carries on. The keyboard map (? lists it) acts on the message list and an open message, and nothing is captured while the cursor is in a text field. j and k move, Enter or o opens, u or Esc goes back, x ticks a row, e archives, ! files as junk (or, in Junk, as not junk), # deletes to Trash, l labels, m mutes, c writes, r a f reply, reply to all and forward, 1 to 9 run a quick step, / searches and Ctrl+K opens the palette. s gives the next of three stars; the second and third are the $Star2 and $Star3 keywords on top of the IMAP flag, so another client sees a flagged message. Offline mail The webmail has always kept the inbox listing and the last opened messages - thirty-one of them according to OfflineMail.md and EvidencePack.md, thirty according to README.md and the wiki - and an outbox of sends that failed for want of a connection. Since 6.3.4 it can also keep the text, formatting and attachments of the last days in the browser's own storage, and nothing is kept until the reader chooses. Settings, Offline in this browser, offers Nothing beyond the inbox listing and then the last 7, 14, 30, 60, 90, 180 or 365 days, up to the server's ceiling. The choice is the account's offline_days preference, so it follows the person between browsers; what is kept does not. How it fills. The store is IndexedDB, database hmPortalOffline. A message goes in when it is opened and the window covers it, and in the background, newest first. An attachment above 8 MB is not kept, and a message's parts stop at 32 MB together. When the window is bigger than the browser allows the site, the card says so and the newest are kept. Without a connection a message opens from the store, a folder lists what the store holds, and a search runs over the kept words with the operators the store can answer. has:link and in_reply_to: let a message through rather than hide it, OR is ignored, and a date operator is measured on the browser's clock. What it is not. It is not a backup and it is not synchronised back: a flag set offline is not sent when the connection returns. It is not encrypted beyond what the browser does with its own storage, so anyone who can open this browser profile can read what is kept. It does not survive sign-out: the database is deleted. Clear what is kept deletes everything now and sets the choice to nothing. Important, and sections of the inbox Gmail's importance marker, decided in the browser: no model is trained anywhere, and nothing about whom this person reads or writes to is sent to the server to decide it. The page counts in localStorage and forgets the whole of it at sign-out. What it decides is one IMAP keyword: $Important on a message judged important, $NotImportant on one the reader says is not, so Thunderbird, a phone and hmctl all see them. Each inbox message is judged once, the first time this session lists it; two points make it important. Reason Points You marked mail from this sender important, or not important +4, or -4 You write to this sender +3 You answered this sender +2 You open mail from this sender (two or more opened here) +1 You starred mail from this sender +1 It was sent to you by name (a To of five or fewer) +1 It is in one of the other tabs: Social, Promotions, Updates or Forums -2 A message that already carries either keyword is never judged again. The marker is changed from the arrow on the row, with an Undo, or with the keys + or = and -. Sent and Drafts have no marker. In Settings, Importance marker offers the page deciding (the default), marking by hand only, or no marker; Inbox sections can draw Important, Starred, Everything else (off by default). It will be wrong, which is why every mark can be undone in one press. It does not sort, hide or delay anything, and what it has learned stays in this browser; the keywords on the mail travel. A message that expires Gmail's confidential mode: the message is kept on this server, and what crosses the internet is a short notice with a link. It is not end-to-end encryption. A message this server can delete on a timer is a message this server can read, and anyone who administers the server, or a backup of it, can read a kept message while it exists. Nor does it stop a recipient photographing the screen. Sending. On the compose form, Expires (never, in an hour, a day, a week, a month) and an optional Passcode. Over the API, expires_in (seconds, 1 to 7,776,000 - ninety days) and passcode (4 to 64 characters) on POST /api/v1/me/messages. The send is refused when the server has no public address to write the link against (403, confidential_unavailable), and for a message the page signed or encrypted. The kept copy is a file under <data directory>/Confidential/<account id>/ with a row in hm_confidential (schema 6069); it counts against the mailbox's quota (413, quota_exceeded), and an account has at most 1,000 outstanding at once (400, confidential_ceiling). Each recipient is sent the notice; the sender's Sent folder gets the full message. Reading. The page at /confidential/<token> takes no credentials. The reader proves they are a recipient in one of three ways: signed in to this server's webmail as a recipient address; a six-digit code mailed to a recipient address, good for ten minutes and five tries; or the passcode, where ten wrong answers pause it for a quarter of an hour. After thirty wrong answers in all, codes stop opening the message. The page answers the same way whether or not the address typed is a recipient's. It shows the text, names other attachments without offering them, and runs no script. After the expiry or a withdrawal it answers 410. The sender's side. Messages that expire, on the Sent folder's menu, lists what was sent and whether and by whom it was opened, with a Withdraw button. The kept file is deleted when the expiry passes. The copies are not in the backup, but an archive taken by a 6.3.4 build made before the release holds them, in the clear: treat it as the mail itself, or delete it. Send-as and masked addresses The webmail's From picker offers four kinds of address: Kind Given by The account's own address Always An alias that points at it The administrator Another mailbox on this server That mailbox's owner, with the post (p) right on their INBOX An address at another provider The person, proved with a code mailed to it With SmtpAuthenticatedSenderCheck on, an authenticated SMTP session's MAIL FROM is held to the same list: see who may send as whom. Since 6.3.7 the DKIM signature follows the list too: an authenticated submission whose From names another domain hosted here is signed as that domain only when the account may send as that address, and is otherwise accepted and sent unsigned as that domain; the webmail's own send already holds From to the account. Proving an address elsewhere (since 6.3.4). Under Settings, Other accounts, Send mail as the person types the address and presses Send the code. The server mails a six-digit code to it from the postmaster of the account's domain, and the person types it back. The code lives in the server's memory for an hour and is never stored; five wrong tries end it. An address of a domain on this server is refused. What this cannot change: mail this server sends as [email protected] is not signed by Gmail's DKIM key and not sent from an address Gmail's SPF record names, so a receiver that honours the domain's DMARC policy will quarantine or refuse it when that policy says so. The page says so beside an address whose domain publishes quarantine or reject. For such an address, send from its own provider. Masked addresses. Under Settings, Masked addresses the person says what the address is for and presses Make an address; the server makes one in the account's own domain, such as [email protected]. While it is on, mail to it is delivered to the account, it is in the From picker (kind: masked), and a reply to mail that came through it goes out from it. Switched off, mail to it is refused at RCPT exactly as mail to an address that never existed is - not handed to the domain's catch-all. An account, alias or list made later with the same address takes it over. Both kinds of address are carried by the backup with the account. Recovering deleted mail It is off by default: recover_deleted_days (0 to 365) turns it on for the whole server, and a domain or an account may keep its copies for fewer days than the level above, never for more. What is kept is a message a person deletes: IMAP EXPUNGE, UID EXPUNGE and CLOSE on a folder with messages marked \Deleted; POP3 DELE; the webmail's delete and its emptying of a folder; and the retention the person chose, or the administrator's retention policy. What is removed for any other reason is not kept: an IMAP MOVE, a scheduled draft that has been sent, an account being deleted. Putting it back. Recover deleted items, on the Trash folder's menu and in the command palette, lists what was deleted within the window. Put back returns a message to its folder with its flags - or to a folder of the same path, or otherwise the INBOX - as a new message with a new UID. Delete for good removes it now. The copies are files under <data directory>/Recoverable/<account id>/; they count against no quota and are not in the backup. The retention task, six-hourly, ends the copies older than the window. Signatures and formatted messages A logo in the signature. Add image in the signature's toolbar (Settings > Name and signature) takes a PNG, JPEG, GIF or WebP file, judged by its first bytes. The whole formatted signature is at most 64 KB - 64,000 bytes, its images included, which leaves room for a logo of up to about 45 KB. A file that does not fit is drawn smaller, starting at 600 by 200 pixels. The limit is the account's signature through the webmail: the Control Panel, COM and the administrator's REST route do not check it. What the recipient receives. Gmail and Outlook do not display a data: image, so the server moves each one into a part of its own with a Content-ID and points the HTML at it with a cid: URL - multipart/related inside the alternative. At most 64 different images in one message are moved. Not every mail client displays WebP; a logo that has to show everywhere is safer as a PNG. The formatted editor rebuilds what is written from an allowed subset of HTML and drops everything else while keeping the words: a style, a colspan, a <script>, a remote <img src="https://...">. Since 6.3.4 it takes three more things: Emoji: a picker of about a thousand characters in eight groups, kept in the page itself. The names are English; there are no skin tones and no flags. Pictures, pasted, dropped or chosen: PNG, JPEG, GIF and WebP by their first bytes, with SVG refused; drawn down to 1,200 pixels on the longer side; at most 64 in a message. Tables, up to 50 by 20, written with border="1" cellpadding="6" cellspacing="0" as attributes, because Outlook renders mail with Word's engine and ignores most of a stylesheet. Tab moves to the next cell and, in the last cell, adds a row. There are no merged cells, no column widths or colours of the reader's choosing, and no resizing a picture once it is in. A second account in the same page Two mailboxes side by side: the account the person signed in as, and one more, added under Settings, Other accounts with its address and password. One press switches the whole page to the other. Nothing is collected or copied; this is not the Other accounts feature beside it, which collects mail from another mailbox into this one. The second account's session lives in the page, not in the browser. A cookie is one per origin, so its session is a token the page holds in memory (POST /api/v1/session?bearer=1), written nowhere - so a reload forgets it, and the person adds the account again. Everything the browser keeps between visits stays the first account's: the offline store, the outbox, push, the recent searches and the Important tally. What is thinner. One second account, not several. Remote images are not loaded in its messages, its S/MIME keys are unlocked by hand, and the one-session-per-device policy counts it. The form is never minted for the administrator (403), and a bearer session is the mailbox and nothing more: the server drops any administrative role from every request made on one. Brand logos The list shows a sender's BIMI logo as its avatar only when the claim is checked. Three things have to be on: AuthenticationResultsEnabled=1, the DMARC check, and BrandLogosEnabled (on by default). GET /api/v1/me/brand-logo?domain= answers the SVG only when all of these hold: default._bimi.<domain>, or the same name at its organizational domain, is a v=BIMI1 record with an l= location. The domain's DMARC policy is quarantine or reject with pct=100. A domain at p=none gets no logo, even for mail that passed. The location is https, reached only at a public address, within ten seconds and 32 KB, with no redirect followed. The file is SVG Tiny Portable/Secure. Every answer is kept a day per domain. A refusal is 404 with an outcome - no_record, declined, weak_policy, not_https, fetch_failed, not_tiny_ps or switched_off - and the webmail shows the initials. A Verified Mark Certificate (a=) is not validated. What it does not do No ActiveSync and no Exchange Web Services: a phone uses IMAP and SMTP, and CalDAV and CardDAV. Recall reaches this server's own mailboxes only, and only unread copies. A folder of a hundred thousand messages is paged, searched and selected whole without the page copying it: built, and not yet measured. Where to read more Wiki: The Webmail. Documents: WebmailShortcuts.md, OfflineMail.md, ImportantMail.md, ConfidentialMessages.md, SendAs.md, MaskedAddresses.md, RecoverDeleted.md, Signatures.md, FormattedMessages.md, SecondAccount.md, BrandLogos.md. The public documentation: 36.6 The portal: what a user gets. On this forum: The webmail portal at /portal, The Control Deck, Signing in for passkeys and account recovery, and the 6.3.4 announcement.
  • 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.