Skip to content

hMailServer 6.3.5

Choose a tag to compare

@RainbowWizard-coder RainbowWizard-coder released this 18 Oct 05:24
· 1928 commits to master since this release

Re-published on GitHub on 18 October 2026 from the GitLab release v6.3.5 of 2026-09-29. Each file below was checked against the SHA-256 that GitLab's package registry records for it before it was uploaded here.

hMailServer 6.3.5

Everything that landed on master after the v6.3.4 tag, from 27 September 2026 (compare v6.3.4 with
v6.3.5
). The schema moves from 6104
to 6105 in one step
, which adds a column to the quarantine's table, hm_quarantine.quarantinehelo. What changes
on upgrade without a switch comes first. The operator documents named below are in hmailserver/docs, which has
five more of them than it had at 6.3.4.

This is the first release built to be made by the project's GitLab CI from its tag (RELEASE.md, The
release on GitLab
and On GitLab, from 6.3.5). The tag's pipeline builds the Windows installer on the project's
Windows runner after the whole Windows suite has passed there; once the owner has started the release job, it
Authenticode-signs the installer through Azure Artifact Signing with an RFC 3161 timestamp, signs every file of the
release with Sigstore as that pipeline -
https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5, issuer https://gitlab.com -
into a <file>.cosign.bundle beside it, and checks every bundle with the server's own --verify-update before
anything is published. That identity is the one a 6.3.4 server trusts (SigstoreVerifier.cpp, unchanged since
6.3.4, and the verifier that check runs), so the live update of a 6.3.4 server is built to install this release, on
Windows and on Linux. A server from 6.2.28 to 6.3.3 trusts only the GitHub workflow that signed the releases up to
6.3.3, and as it stands its live update refuses this one: install 6.3.4 or this release by hand, and keep
UpdateAutoDownload off until then, or on Windows give it the GitLab signer as
UpdateFeed.md describes. What the release carries, and what the new package jobs
leave in CI only, is under Operations.

What changes on upgrade without a switch

  1. The Control Panel's sidebar is in the order a new administrator needs. Welcome and Dashboard come first,
    then a new group, Set up this server: the Setup checklist, which was External setup under Monitoring &
    troubleshooting, then Setting profiles and DNS records. Two groups are renamed - TLS & certificates is
    Certificates & encryption and Access & abuse protection is Sign-in & access - and Connections &
    protocols gains a Webmail page (Administration, below). The palette (Ctrl+K) still finds the checklist as
    External setup. A page's nav-<key> UI Automation id is unchanged; the navgroup- and navrail- ids of the
    new and the renamed groups follow their new headings, so a UI test that finds a group by its old id must be
    changed.
  2. The quarantine shows the HELO name, and groups. The Control Panel's Quarantine page and the Control Deck's
    Quarantine view gain a HELO column and group the held messages by the sender's domain unless told otherwise.
    Messages held before the upgrade have no HELO name recorded and are grouped with those that have no HELO
    domain. The Deck now asks before a delete with a Yes or No of its own whose default is No, as the Control Panel
    already did; it used the browser's confirm(), whose default is OK. It offers whitelist and blacklist only to a
    credential holding manage_settings, and a held message's headers and preview only to one holding
    view_mailbox, as the server requires (below).
  3. The webmail's keyboard. The message list is one Tab stop, where each row put its box, star and marker in
    the Tab order, and the list's keys reach what a row holds. Enter on a button in the mail view presses the
    button; it used to open the message under the list's cursor. A letter typed into the formatted signature is
    text, where c opened a new message. Escape closes every modal dialogue as its Cancel does and gives the focus
    back to where it was, the page under a dialogue cannot be reached until it closes, and menus close on Escape.
    The event and task editors close when the page changes. The compose window's Plain is Remove formatting,
    its tooltip's words. On a phone a row's importance marker is shown and not pressed; the message's More menu
    marks it.
  4. An invitation sent the way Outlook sends one is shown as an invitation. GET /api/v1/me/messages/{id}
    answers "invitation": true for a text/calendar part whose Content-Type carries parameters and which has no
    .ics name, where it answered false, and the webmail shows the invitation's card with Accept, Maybe and
    Decline (the fix is under IMAP, POP3 and the webmail).
  5. What a script sees. GET /api/v1/quarantine gives each message three more fields - helo,
    sender_domain and helo_domain - and four routes are new. Over COM, Quarantine gains Bulk (dispid 9)
    and QuarantinedMessage gains HeloName, SenderDomain, HeloDomain, ReadHeaders and ReadPreview
    (dispids 11 to 15), added to the existing interfaces; no existing member or dispid changes. A program built
    against 6.3.5's type library that calls one of them needs a 6.3.5 server.

Anti-spam, anti-virus and mail flow rules

  • Reviewing the quarantine (schema 6105; GitLab issue #1):
    • The HELO name the client gave is stored with each held message. A message held during the SMTP
      conversation records the session's own; one held later, at delivery - by a rule, a block list, an account's
      own threshold, the sandbox or impersonation protection - takes it from the Received header this server wrote
      when it accepted the message, and only when that header names this server, so a Received line the sender
      wrote is never read. Empty for a client that gave none, for a message that did not arrive over SMTP and for
      every message held before the upgrade.
    • Grouping: by the sender's domain, by the HELO name's domain or not at all, each group folding, the group
      with the newest message first, and every column sorting the messages inside the groups. The domain is the
      organization's - the registrable domain by the Public Suffix List - which the server works out, so that every
      page groups a message the same way.
    • Many at once: a box on every message, every group and the header, and four actions on a message, a group
      or the checked messages - release; delete; whitelist, then release; blacklist, then delete. Whitelist and
      blacklist write the lists that already exist and no third one: the sender's address on the server's allow
      and block list (the default), or the organization's domain as .example.com on the domain white and black
      list, matched on the envelope sender, or on the HELO name when the page is grouped by HELO domain. Several
      messages from one sender or domain make one entry, and an entry already there is changed rather than
      doubled. The dialogue first shows what the server would write (a dry run); a domain that is itself a public
      suffix (co.uk, a HELO of localhost) is written only after a box is ticked, and without it the server
      refuses with 409 and changes nothing. The entries are written before any message is acted on, so a message
      whose entry could not be written - the null sender, no HELO recorded, a full list - is left in the
      quarantine with the reason.
    • The headers and a plain-text preview of a held message, read without releasing it: the headers up to
      65,536 characters, and the preview up to 16,384, the text part or the HTML part with its markup, style sheets
      and scripts taken out. Nothing is rendered as HTML.
    • The Control Deck is level with the Control Panel: the same groups, columns, boxes, icons and dialogues, one
      request for the checked messages, a delete dialogue of its own with No as the default, and Remove expired.
      Against a server from before this release, the Control Panel releases and deletes one message at a time, and
      its whitelist and blacklist say the server needs upgrading.
    • REST: POST /api/v1/quarantine/bulk (1 to 1,000 ids and one action, with dry_run and
      confirm_public_suffix), POST /api/v1/quarantine/expire, and GET /api/v1/quarantine/{id}/headers and
      /preview. The list, the headers and the preview need view_quarantine, and the headers and the preview
      view_mailbox beside it, since they are the mail itself; bulk and expire need manage_quarantine, and a
      bulk that allows or blocks needs manage_settings beside it, since it changes who is let in for everyone.
      Every one of them is refused to a credential confined to named domains, as the quarantine's other routes are.
      COM: Quarantine.Bulk takes and answers the same JSON, and each QuarantinedMessage has HeloName,
      SenderDomain, HeloDomain, ReadHeaders() and ReadPreview(); Bulk, ReadHeaders() and
      ReadPreview() are the server administrator's, as the rest of the quarantine is over COM
      (SenderListsAndDigests.md).
  • A HELO that is an address has no domain. A client that says HELO 192.0.2.7, bracketed or not, and a sender
    someone@192.0.2.7, have no organization's domain: helo_domain and sender_domain over REST, and HeloDomain
    and SenderDomain over COM, are empty, the message is grouped with those that have none, and whitelisting or
    blacklisting it by domain leaves it where it is and says why: "The HELO name is an address, which the domain list
    does not name." for the HELO, and "The sender's address has no domain to list." for the sender. Handed to the
    Public Suffix List, whose implicit rule takes the last label for a top-level domain, 192.0.2.7 would have been
    the domain 2.7, and blacklisting it would have refused at MAIL FROM every client whose HELO is an address
    ending in .2.7.
  • Fixed: a rule with a Readable text or Sensitive content found condition, which 6.3.4 added, read a
    message's HTML part in time that could rise with the square of its size. The part's entities were unwrapped by
    a replacement that moved the rest of the text once per occurrence, so 400,000 &amp; - two million characters -
    held the delivery for about a minute, and on a server with such a rule anybody could send one. Each entity is
    now unwrapped in one pass into a new string, with the same result (ContentPolicy::StripHtml). The quarantine's
    preview ends in the same function, and takes the style sheets, scripts and head out of an HTML part in one pass
    too, so a held message's preview costs time in proportion to its size.

IMAP, POP3 and the webmail

  • The webmail, audited on a phone and a desktop, light and dark, by keyboard and through the accessibility
    tree
    , with a seeded mailbox, and what it found fixed. On a phone: the search box has room to type and its
    options are a sheet the width of the screen; the tour skips a step whose target a phone does not draw and keeps
    its card on the screen; help covers the page instead of squeezing it; the settings tabs are one line that
    scrolls; and everything a finger presses is at least 44 pixels each way. By keyboard: the changes above, a focus
    ring on about twenty kinds of control that had lost it, and the earlier messages of a conversation and the
    Storage page's rows opened by keyboard as well as by mouse. For a screen reader: an unread row says Unread, a
    narrow list's tabs are named with their unread count and are a tab list, the row under the list's cursor is
    said, and the hidden page headings are translated. Contrast is raised where it fell short: the initials on the
    avatar colours, white on the dark theme's pale accent and danger colours (dark text now), the placeholders, and
    the light theme's warning colour (5.5:1, from 4.3:1); and the page declares its colour scheme, so that the
    browser draws its own parts of a control for the dark theme. Also: a long attachment name is cut with an
    ellipsis, and the week and agenda views name their month.
  • The webmail heard through a screen reader. Every view was walked with NVDA 2026.2 in Chromium, in English
    and German, and what it said fixed. The message list is one control that reads each message as one line, its
    state first ("unread, starred, Grace Hopper, Re: The migration plan, Sep 26, has attachments, 3 of 51"), where
    Tab used to read all fifty rows and each row's controls; Home, End and the list's own keys work in it, Enter or
    o opens a message at its subject and Escape comes back to the same row, and Shift+F10 opens a message's menu
    from the keyboard. The star says whether it is pressed. What happens without the focus moving - a toast, once,
    not its countdown; a search's count; an attachment added; a new calendar month; a send that failed after the
    undo delay, which now also reopens the message and says why - is said in one place, and z presses a toast's
    Undo. Address suggestions, the command palette and mentions say the suggestion Enter will take; folders say
    their unread count in words and a subfolder its parent; the current folder, page, tab and view are said;
    dialogs and help open at their headings; the logo, the avatars' initials and a duplicate heading are hidden
    from a screen reader. 82 texts that were English on a translated page are in all 23 languages, and the server
    has a browser check the webmail's language file on each use instead of keeping it for a day, so new texts are
    not left in English after an upgrade. A dialog's name is said twice as the focus enters it, and a colour
    picker's name is not said; both are NVDA's reading of what Chromium exposes. Roadmap2 row 43 is done.
  • Fixed: an invitation whose calendar part carries parameters in its Content-Type and no .ics name - the shape
    Outlook sends - was never marked as an invitation, so the webmail showed no card for it. The part's media type is
    now read without its parameters, as the server's iMIP code already read it. 6.3.4 had it too.

Administration

  • Every database setting that should have an editor has one in the Control Panel. 146 of the 314 settings the
    server keeps in its database had no editor in the Control Panel, and 137 of them needed one: the webmail's
    sign-in (passkeys, OpenID Connect, SAML, account recovery), its sessions and limits, OpenPGP, Web Push, the
    alerts, the SNMP agent, the audit trail, outbound webhooks, the spam traps, the complaint feedback loops, the
    reputation watch, country blocking, compromised-account detection, the object store, the managed lists and the
    migrations. They are now on the pages an administrator looks for them on - new tabs on Delivery of e-mail,
    Anti-spam, Auto-ban, Logging, Performance and Advanced - and a new Webmail page, under Connections &
    protocols, holds the webmail's own: Sign-in, Browser sessions, What people may do, Encryption, Notifications and
    Import. The 34 a COM property carries are edited over COM. The other 103, which the server reaches over its REST
    API alone, are read and saved over that API, which is off as shipped (RestApiPort 0): with it on, the Control
    Panel signs in to it once with the credentials it signed in with, asks for the administrator's one-time code once
    when there is a second factor, and sends only the keys that changed; with it off or out of reach, each such tab
    says so, and why. The domain dialog gains Country blocking, OpenPGP and Webhooks tabs, the account dialog an
    Identity tab (a person, a room or a piece of equipment, and the external ID SCIM matches on), an external account
    the options for a mailbox several people share, and a managed list's member how they receive the list's mail.
    build/check-db-settings-coverage.py now fails any database setting with no editor and no recorded reason for
    having none; the other nine have such a reason.
  • The Control Panel, audited page by page in both themes at two window sizes, with each colour measured. The
    server status at the top right is a filled box with no outline - Connected is green on a soft green fill,
    6.0:1 in the light theme and 5.9:1 in the dark - and every status and notice is at 5.5:1 or better; High
    Contrast keeps a one-pixel outline. The light theme's field hints and captions are at 5.3:1, where they were
    3.3:1. Toasts are on the theme's neutral surface, with their level in the title and a glyph, and a failure - a
    page that could not load, a reinitialise that failed, a page partly saved - has a style of its own and stays up
    for eight seconds. A header or a cell too narrow for its text ends in an ellipsis and shows the whole of it as a
    tooltip. A list's Delete is one style, red words while it can be pressed and grey while it cannot; Incoming
    relays' and IP ranges' Properties and Delete need a selection, as every other list's do. The Domains page's
    alias and list boxes have captions. The Setup checklist numbers its items in the order a new installation
    meets them and marks the first one that needs action as the next step, and each item's button names the page it
    opens; the dashboard's attention card numbers and marks its items the same way
    (SetupChecklist.md,
    ControlPanelDesign.md).
  • The Control Deck's census (DeckParity.md) counts 498 of the 508
    properties the Control Panel writes as writable over REST and reachable from a Deck view; the ten it lists as
    missing are an account's directory fields and its external ID, and the server's country policy. The census had
    missed 16 COM members whose help string holds a ].

Operations

  • Packages for Debian, Ubuntu, Fedora, RHEL and openSUSE, the AppImages and both Windows installers, built on
    GitLab CI
    on the project's own runners, never for a merge request, and with no tool from GitHub:

    • Debian and Ubuntu: Debian 12 and 13 and Ubuntu 24.04 for amd64 and arm64, and Ubuntu 26.04 for arm64, as
      hmailserver_<v>-1~<distro>_<arch>.deb - Ubuntu 26.04's as hmailserver_<v>_arm64.deb; the amd64 package for
      Ubuntu 26.04 is the release's own, below (PackagesDeb.md).
    • RPM: Fedora 44 and 43, RHEL 10 and 9, and openSUSE Leap 16.0 and Tumbleweed, for x86_64 and aarch64, as
      hmailserver-<v>-1.<disttag>.<arch>.rpm, with Boost linked in and every other Requires resolved from the
      release's own repositories; the executables are PIE with full RELRO, the stack protector, stack-clash
      protection and _FORTIFY_SOURCE=2 (PackagesRpm.md).
    • Each .deb and .rpm is built in its distribution release's own container with that release's compiler and
      libraries (the arm64 and aarch64 ones cross-compiled), then installed in a clean container of the same release
      and architecture, run and removed. They are built for every v* tag and batch* branch, and on master when
      started by hand.
    • AppImages for x86_64 and aarch64, built on Ubuntu 24.04, so for a host with glibc 2.39 or later and
      OpenSSL 3, with a runtime of the project's own (platform/packaging/appimage/runtime.c) in place of the
      AppImage project's, which is published only on GitHub, and without linuxdeploy; each is run again on
      Fedora 44. They are built for a v* tag, and elsewhere when started by hand
      (PackagesAppImage.md).
    • Windows: the x64 and the ARM64 installer, unsigned, in every pipeline of master, a batch* branch or a
      v* tag, after the Windows suite. The ARM64 job may fail without failing the pipeline, and in this release it
      does: its toolchain check looks for Visual Studio's ARM64 C++ runtime by a path that finds it in no
      installation, so it stops on the Windows runner, which has the ARM64 tools, ATL and SDK libraries, and prints a
      command that changes nothing. The check is corrected for the next release
      (PackagesWindows.md).
  • What the release carries. The release collects the x64 installer built by its own windows-installer job,
    the amd64 .deb and the x86-64 .rpm that linux-build makes (clang, in the CI's Ubuntu 26.04 image), the
    hmailserver-linux-<v>-SHA256SUMS.txt of those two, the server's SPDX and CycloneDX SBOMs and each compiled
    file's own <file>.cdx.json, every file with its Sigstore bundle (build/ci/release-chain.py). The packages
    above - every other .deb and .rpm, the AArch64 ones, the AppImages and the ARM64 installer - are CI job
    artifacts only, kept for 7 to 30 days: they are not signed, not in the release and not offered by the update
    feed. A server installed from one of the other x86-64 .deb or .rpm packages is offered the release's by its
    live update, whose dependencies its distribution may not meet; apt or dnf then refuses it, and the server stays
    as it was. One installed from an AArch64 package, an AppImage or the ARM64 installer is told that the release
    carries nothing for it to download.

Build and tests

  • Reproducible builds on both platforms (ReproducibleBuilds.md).
    hMailServer.exe builds to the same bytes from a checkout at any path: the Release configuration now trims the
    checkout's root and the libraries' root from every path the compiler writes (/d1trimfile), where it trimmed
    only the project's own directory, so that sixteen of the server's source files and 31 Boost headers left their
    absolute paths in the executable. 6.3.4 and the releases before it reproduce, if at all, only from a checkout and
    libraries at the paths they were built at. On Linux a new linux-repro job builds the server, the .deb and the .rpm a second time - from a copy of
    the tree at another path, without .git and with other file modes, in another build directory, under another
    umask and time zone - and fails unless all three are byte for byte linux-build's; the release waits for it.
    For that, the packages now have the same modes whoever makes them - their directories 0755, the .deb's
    maintainer scripts 0755 and its conffiles 0644 - and the .rpm records the commit's time as its build time
    and reproducible as its build host. Not claimed: the installer, the Updater and Minidump helpers, the .NET
    programs and the signatures. The release's runbook builds the executable once more from a second checkout
    (RELEASE.md, step 8).
  • Coverage on Linux, with branches. build/ci/linux-coverage.sh builds the server and its tester programs with
    clang's source-based coverage (-DHM_COVERAGE=ON, a new CMake option), runs the testers and the whole Linux
    regression suite against that server, and writes llvm-cov's line, region and branch figures; a run of the suite
    that did not reach its end is void. On 26 September 2026 on linix they were 49.01%, 47.23% and 38.87% of the
    Linux build (43.35%, 41.38% and 33.82% before the fixtures below were linked, which were measured in a run of
    their own and the counters merged) - a floor, since the Linux suite runs what its REST shims can drive and
    llvm-cov counts every line of a function. The linux-coverage job runs it weekly once the owner creates the
    schedule described coverage (ContinuousIntegration.md, The
    coverage schedule
    ); it has not run on GitLab yet. The Windows measurement of 15 September is in the tree as
    build/coverage-windows.ps1, not yet run from it; it stores the suite's resolver over COM before the console
    server starts, as the suite asks since the settings moved into the database. Seventeen class constants the
    server passes to Format by reference are defined now (static constexpr): six of them kept the instrumented
    build from linking, and the other eleven had the same defect and linked only because the optimiser folded them.
  • Twenty-nine more fixtures run on Linux, among them DKIM signing, ARC sealing, the OpenPGP keys, message
    recall, shared address books, the IMAP mirror, journaling, per-domain relay and the webmail's footers: each
    already compiled against the REST shims and ran clean on linix, and none was linked. API/RestApiRouteSweep is
    new, in both suites: every administrative operation under /api/v1/ that the server's OpenAPI document
    describes is refused, with 403, to an account without a role, and every read is answered to an auditor without
    a server error. The Linux shims give Domains.ItemByName[...] and DistributionLists.ItemByAddress[...] as
    indexers, and Paths.Combine reads a relative path written the Windows way as one on Linux. The linux-suite
    job, which the fixtures lengthen by about 20 minutes, has the runners' four hours now rather than three, and its
    test filter is a variable of its own, HM_LINUX_SUITE_FILTER, which the job refuses, so a variable meant for
    another job cannot make the gate run part of the suite.
  • The maintainers' tools:
    • build/scan-secrets.py, which the git hooks and the landing script run: the directory its scanner's container
      writes to is inside a private (0700) directory, locally and over ssh. 6.3.4 made the whole directory 0777, so
      that on a POSIX machine another account could, while a scan ran, read the lines being committed, plant a link
      the writes followed, or put an empty report in place of the scanner's (a Semgrep finding accepted for 6.3.4 as
      a real defect of low severity). And a scan that did not run to its end no longer passes: gitleaks 8.30.1
      writes an empty report and exits 0 when the git log it reads fails, so a --range naming a commit the
      repository does not have scanned nothing and said nothing was found. Such a range is now refused before any
      scanner is asked, and an empty report is clean only when gitleaks also exited 0 and logged no error.
    • build/ci/repo-hygiene.py's Token-Permissions reads CI rules as GitLab does - every alternative of a
      condition, a rules list at any indentation, tags and extends in every form, templates and includes, and a
      job's rules from the last of its extends parents, which GitLab merges last over first - and refuses what it
      cannot read; its --self-test holds it to 33 cases. Its note on each PostgreSQL password written in
      .gitlab-ci.yml names the job whose service container it is.
    • New checkers: build/check-idl-case.py fails a COM name that hMailServer.idl spells two ways - a type
      library keeps one spelling of a name whatever its case, so a new member spelled like an existing one would
      rename it for every early-bound client - with the seven that shipped that way listed and why;
      build/check-db-settings-coverage.py (under Administration). build/check-deck-parity.py reads a member
      whose help string holds a ], and build/ci/security-findings.py --reasons writes the reason each finding
      is known rather than fixed into the baseline.
    • The installer smoke test takes an upgrade that writes [Security] AdministratorPassword again as a PBKDF2
      hash, which is the documented upgrade of an older hash, and still requires every other [Security] key as it
      was. And it now makes the checks SecureDefaults.md lists for an
      installation on a clean machine: after a new installation, the certificate made at installation (its record,
      its two files, a key only SYSTEM, the administrators and the service can read, RSA 2048 for 825 days naming
      the host), STARTTLS on ports 25, 587, 110 and 143, authentication refused without TLS from the Internet
      range and not from My computer, the audit rows and the setup checklist; after an upgrade, the ports and
      ranges exactly as they were. Against the signed 6.3.4 installer on a clean Windows Server 2025: a new
      installation passed 59 checks and an upgrade from 6.3.3 73, none failed. The run script follows the VM to the
      address its DHCP lease moved to after a snapshot revert.
  • The CI: secret_detection failed master's first pipeline after 6.3.4 on the 24 vetted test keys and
    fixtures, because a pipeline started by a push scans the checkout as files and names a finding by its path, not
    its commit; .gitleaksignore now names each of them that way too. The static-analysis and semgrep
    baselines are written again from runs over the release's tree in the jobs' own images - 1,165 and 122 known
    findings, 264 and 37 of them with the reason each is known rather than fixed, and every finding no scan reports
    any more removed - and a clang-tidy that did not finish on a file (an abort, a kill) is run again alone and
    fails the job if it still does not finish, where before the file's findings read as fixed and the job passed. The package builds no longer read $HM_PIPELINE, which
    in a pipeline where the variable was defined would have refused every build and left GitLab making no pipeline
    for master, a batch or a tag; build/check-release-chain.py now reads the four package files and fails such a
    rule.
  • The documents, corrected: 6.3.4's own notes - it was published on 27 September, not 26; the security
    exceptions file accepted 455 findings at the release, not 169; and ScriptAllowedObjects first exists in
    6.2.25, so before it an event script could create any COM object. ServiceAccountName and
    ServiceAccountPassword are no longer used, though still loaded
    (ServiceAccount.md); the update feed's
    site has served over HTTPS since 26 September 2026 (UpdateFeed.md); an
    inbound webhook's URL writes an international domain name in its xn-- form
    (InboundWebhooks.md), and before 6.3.4's fix the check let a URL outside
    printable ASCII through on Linux and Windows alike, where 6.3.4's notes said Linux refused it; and
    CONTRIBUTING.md's schema-upgrade gate and ContinuousIntegration.md's list of what is still done by hand say what
    the jobs do. Two of these, ServiceAccount.md's and UpdateFeed.md's, were found by the check of the wiki's copies
    (GitLab issue #25).

Full notes: the 6.3.5 release.

Verification

Every file of this release is signed with Sigstore by this project's GitLab CI pipeline for the tag v6.3.5,
and was checked before publication with hmailserver --verify-update, built from the same commit
(c17c6cb6f917c17ec3af5865306217433a45e2b6). Each file's bundle is beside it, named <file>.cosign.bundle. The certificate:

Identity https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5
Issuer https://gitlab.com
Repository https://gitlab.com/Progressiverobot/hmailserver
Repository id 86730559
File SHA-256 Recorded by the transparency log
hMailServer-6.3.5-x64.exe ebb708e056dfd1ca3bcb56b6356e819511b496965e0a82834bdf6987fec79e97 2026-09-29T09:41:47Z
hMailServer-6.3.5-x64.exe.cdx.json 684e2ae5698667bbb832139f8087fd812e26fe28b4c2ab954006538a370b4ff9 2026-09-29T09:41:48Z
hmailserver-6.3.5-1.x86_64.rpm f79465e6604c8328be7d07a0e851d8409a93f08926dee7d5ee4281af5641f989 2026-09-29T09:41:49Z
hmailserver-6.3.5-1.x86_64.rpm.cdx.json 69400f1feb4b154c2032c87a27a7a27389a2e782318dd5e0e98ea218d2f218c1 2026-09-29T09:41:52Z
hmailserver-linux-6.3.5-SHA256SUMS.txt a6e8d531d8b2ecb748de8115fb304a8a4fea17596e0122b440e6c15b37ee3fcf 2026-09-29T09:41:55Z
hmailserver.cyclonedx.json 95a73bee1ea5a69b54ca9fdfedff56de4cd0e39e5edc07334f6dbdc506f98457 2026-09-29T09:41:56Z
hmailserver.spdx.json 8ea39288a8ec99cd2de1e2f56b0dcbc0da9421a6f24bd72f2813edcfd1e91bac 2026-09-29T09:41:57Z
hmailserver_6.3.5_amd64.deb 4e7af16325871aacee0410950278bd35cdaf116f993a312f6ac371cfe333eeae 2026-09-29T09:41:57Z
hmailserver_6.3.5_amd64.deb.cdx.json 299fdbd573471f28bcc808d4e8db86e770e9092b0c3e2e755252ca70c58007e5 2026-09-29T09:41:58Z

Every compiled file goes out with a software bill of materials of its own, in CycloneDX 1.6, named after it, which
names that file by its SHA-256 and lists what it holds and needs. The repository's build/asset-sbom.py verify
checks that one describes the file beside it (SECURITY.md, "Software bills of materials"); each is signed like the rest.

File Its SBOM Components
hMailServer-6.3.5-x64.exe hMailServer-6.3.5-x64.exe.cdx.json 28
hmailserver_6.3.5_amd64.deb hmailserver_6.3.5_amd64.deb.cdx.json 8
hmailserver-6.3.5-1.x86_64.rpm hmailserver-6.3.5-1.x86_64.rpm.cdx.json 8
the source tree hmailserver.spdx.json, hmailserver.cyclonedx.json

To check a download with cosign:

cosign verify-blob --bundle <file>.cosign.bundle \
  --certificate-identity 'https://gitlab.com/Progressiverobot/hmailserver//.gitlab-ci.yml@refs/tags/v6.3.5' \
  --certificate-oidc-issuer https://gitlab.com <file>

The Windows installer is also signed with Authenticode through Azure Artifact Signing, with an RFC 3161 timestamp.

Unlike the releases up to 6.3.3, this one carries no AArch64 packages, no AppImages and no SLSA provenance. The
pipeline also builds the other .deb and .rpm packages, one for each supported distribution release on x86-64
and AArch64, and the AppImages, but only as CI job artifacts: they are not signed and not in the release. The SLSA
provenance generator is GitHub's.

What a signature shows is which pipeline signed a file, not what the file was built from. Here, the .deb,
the .rpm and the SBOMs were made by this pipeline in the runner's own checkout of c17c6cb6f917c17ec3af5865306217433a45e2b6,
the installer's SBOM from the installer as Authenticode signed it.
The Windows installer was made by this pipeline on the project's Windows runner, in that runner's long-lived
checkout, which the job requires to be at that commit with no change to a tracked file and no file git does not
track where a build would take it in, and from the folders the installer takes whole it removes every file git
does not track before building. The server and the tools in it are that checkout's incremental build of the
commit; its OpenSSL and libpq DLLs come from that runner's library tree and its C++ runtime from that runner's
Visual Studio, both outside the pipeline; and nothing here proves that build reproducible. The Linux one is
checked: linux-repro, which the release waits for, built the server, the .deb and the .rpm a second
time from the same commit in the same image, from a copy of the tree at another path, and both packages were
byte for byte the files above (hmailserver/docs/ReproducibleBuilds.md).