---
title: "Compliance: eDiscovery, legal holds, personal data, erasure and the audit trail"
url: https://www.hmailserver.co.uk/topic/28242/compliance-ediscovery-legal-holds-personal-data-erasure-and-the-audit-trail
author: "Progressiverobot"
published: 2026-09-27T19:24:13.731Z
updated: 2026-09-27T19:24:13.731Z
category: "Guides & tutorials"
tags: ["administration","security","control-deck","rest-api","backup"]
site: hMailServer Community Forum
---

# Compliance: eDiscovery, legal holds, personal data, erasure and the audit trail

*Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) regroups the Control Panel's sidebar: the audit trail, journaling and eDiscovery are a Compliance group of their own rather than the last rows of *Monitoring & troubleshooting*, as the table below has it, and Ctrl+K takes every old title and route to its page. See [the 6.3.7 announcement](/topic/28277).*

This guide covers what an operator is asked for when a lawyer, a regulator or a person with a data request arrives: eDiscovery (cases, searches, legal holds and exports), a person's own data (the export and the erasure), and the audit trail that records who did what. eDiscovery, the person's data as one archive, the erasure of log lines, the erasure's reach into the stores beside the mailbox, and the audit trail are new since 6.3.4. The one thing to know first: **cases, their files and their holds are not in the backup.** A server rebuilt from a backup has no cases, no exports and no holds, so place the holds again before its mail can be deleted.

### Where it is

| Where | What |
|---|---|
| The Control Deck, *Discovery* view | The whole of it: cases, the search form, holds with **Lift the hold**, exports with **Download**, **Open** and **Close** |
| REST, `/api/v1/discovery/cases` | The same ten routes the Deck calls, in the OpenAPI document |
| The Control Panel (Windows only), *Monitoring & troubleshooting → Discovery* (*eDiscovery*, in the *Compliance* group, since 6.3.7) | Read-only: the cases and their lines of the audit trail, read over the REST listener, which must be on (`RestApiPort`). It changes nothing |
| COM, `hmctl` | Neither |

On Linux, then, discovery is worked from the Deck or over REST. Discovery and the erasure do nothing until somebody opens a case or asks for an erasure; the audit trail is on from the first start.

Two neighbouring tools are not this one. The archive has had a legal hold of its own since before 6.3.4 (`/api/v1/archive/{id}/hold`), which keeps one archived copy from the archive's retention sweep ([Quotas, retention and the archive](/topic/28236)). Journaling, a copy of a domain's or an account's mail sent elsewhere as it passes, has its own topic: [Journaling and dual delivery](/topic/28271).

### Cases

A case has a name (up to 255 characters), a description (up to 1,000), who opened it and when, and a state: open or closed. Its searches, holds and exports belong to it. **Closing a case releases every hold of it that is still live**, each release on the audit trail; a closed case runs no search, places no hold and makes no export, and what it made stays readable and downloadable. A case is never deleted, and an address's erasure does not reach it.

The files a case makes are under the data directory, in `Discovery\case-<id>\`: `search-<id>.json` for each search and `export-<id>.zip` for each export. Nothing removes an export; delete its zip by hand when it is no longer needed.

### Searching

`POST /api/v1/discovery/cases/{id}/searches` takes:

| Part | What it means |
|---|---|
| `custodians` (required, at most 100) | An address, a domain, or `*` for every account on the server and the whole archive |
| `from`, `to` (optional, `YYYY-MM-DD`) | The day a message is dated by its `Date` field, read as UTC; else the day the server stored it |
| `words` (optional, at most 50) | Every word must be found, a quoted phrase as one: a case-insensitive substring of what IMAP `SEARCH TEXT` reads |
| `attachment` (optional) | `name`, `type` and `min_kb`; `{}` is any attachment at all |
| `scope` | `mailboxes`, `archive` or `both`, the default |

There is no sender or recipient criterion: a word is looked for in the whole decoded header, and a person is named as a custodian. A query the server cannot take is refused with `400` and the reason; `202` answers with the search's id, and the search is queued.

**Where it looks.** For each custodian account: its folders, its deleted items (what a hold kept, and what `RecoverDeletedDays` keeps), and the archive. A search copies nothing; it lists each hit, and lists separately what it could not read, with the reason.

**What it cannot see.**

- **100,000 hits.** A search stops there and says `truncated`. Narrow it and run it again.
- **Text inside a PDF, a Word document or a spreadsheet.** Only `text/*` attachments are read, up to 1 MB of characters a message. An attachment criterion still finds the PDF by name, type and size.
- **More than 80 MB a message.** It is listed as not searched when the query asks about its body, and never dropped as a non-match.
- **Zero-access mailboxes and messages encrypted end to end** are matched on their headers alone and marked `headers_only`.
- **The stores beside the mailbox**, `<data directory>\Confidential` and `\Protected`, and archived copies with no index row (a bounce, a delivery report).

**The full-text index is a filter, never the answer.** It is off by default (`IndexerFullText`). When it is on, a message the index proves cannot contain a word is ruled out; every other message is read.

**One at a time.** Searches and exports are queued and run by one runner, the server's own. Only one hMailServer service may run against a database at a time ([Warm standby and high availability](/topic/28232)), so a standby started after a failover takes over the queue rather than sharing it.

### Holds

`POST /api/v1/discovery/cases/{id}/holds` takes one of:

| Body | Holds |
|---|---|
| `{"scope":"account","target":"alice@example.com"}` | Every message of the account, and its archived copies |
| `{"scope":"domain","target":"example.com"}` | Every message of every account in the domain, and their archived copies |
| `{"scope":"archive","search":12}` | The archived copies search 12 of this case matched, at most 10,000 at once |

A hold is live until it is released or its case is closed, and what it keeps is kept by any live hold of any case.

| Path | Under a hold |
|---|---|
| The person's delete (IMAP `EXPUNGE`, POP3 `DELE`, the webmail's delete, a folder deleted) and an administrator's delete of a message | The message leaves the folder, and a copy is kept among the account's deleted items whatever `RecoverDeletedDays` says (0 by default). If the copy cannot be made, the delete is refused (HM7333) |
| An IMAP `MOVE` out of the account | The original is kept among the held account's deleted items |
| Every message of the account deleted at once | Refused, `409` over REST |
| The account's own filters at delivery: a rule's Delete or Reject, a Sieve `discard`, `reject` or `redirect`, a forward that keeps no copy | The copy is kept among the deleted items instead of dropped; if it cannot be kept, the message is delivered |
| Retention, the recover-deleted sweep, deleting a deleted item for good | Nothing of the held account is deleted |
| A mailbox restore with `mode` replace, a full restore with the domains option | Refused (the full restore as HM5014) |
| Moving the account to another domain, removing its mailbox key, deleting the account or its domain | Refused, over REST, SCIM and COM alike |
| The archive's retention sweep and an address's erasure | A held copy keeps its file and its index row |

**What a hold does not reach:** the server's and a domain's rules, which decide before a message is split between its recipients; spam and viruses refused or deleted before a message is the account's, and what the quarantine discards; the delivery queue; the confidential and password-protected stores; archived copies with no index row. If `ArchiveDir` was put inside the data directory, a full restore with the messages option replaces held archived copies too, so keep the archive outside it.

**The hold is not hidden from the custodian.** The account's own `GET /api/v1/me/recoverable` answers `held: true` and lists what the hold kept, and the webmail's *Recover deleted items* page offers no delete for good.

**It fails closed.** When the holds cannot be read, every question is answered held, and the error log says so once a minute (HM7332). A hold is a row in the database. The server reads the holds again as soon as one is placed or released, and no more than five seconds after a change made elsewhere.

### Exports

`POST .../searches/{search}/export` queues an export of a finished search's hits. The zip holds:

- **Each hit as EML**, the message's own bytes as the server stores them, under `<custodian>/<folder>/<n>-<message id>.eml`, and an mbox per custodian folder beside them, in parts of at most 256 MB.
- **`SHA256SUMS`**, so that `sha256sum -c SHA256SUMS` checks the unpacked export.
- **`manifest.json`**: the case, the search as it was run, who ran it, who asked for the export, every item with its path, SHA-256 and size, and every item withheld, with the reason.

The zip's own SHA-256 is kept with the export, and the case answers it (`exports[].sha256`).

**Withheld:** a zero-access mailbox's messages, and anything deleted, moved or swept since the search ran. Search again to find where a message is now.

**Limits.** The zip written is the 32-bit format, so no export passes 4 GB: narrow the search and export each part. A download past 500 MB answers `413` with where the file is on the server's disk and its SHA-256. An export that would leave the data directory's volume with less than a gigabyte free fails. A discovery export is never a PST; an examiner who needs one converts the EML files with a third-party converter.

### Who may

| Permission | Allows | Held by |
|---|---|---|
| `view_discovery` | Reading the cases, their searches and hits, holds and exports | The server administrator and the `auditor` role |
| `manage_discovery` | Opening and closing a case, running a search, placing and releasing a hold, making an export | The server administrator; a custom role may be given it |

Downloading an export needs `view_mailbox` as well, since it hands over the mail itself, the custodians' deleted items among it. A search that reads the archive needs `view_archive` as well. A credential confined to named domains names no custodian outside them, never `*`, and is answered `404` for a case that reaches outside them.

### A person's data: the export

One .zip of everything the server holds for an account. The person downloads it from the webmail's *Your data* card (`GET /api/v1/me/export.zip`), with no administrator involved; that download is refused (403) to a request authenticated with an app password. The administrator has the Deck's "Everything (.zip)" on the account row, `GET /api/v1/accounts/{address}/export.zip` (the administrator password's alone), `POST /api/v1/accounts/{address}/export` with `archive`, `hmctl privacy export <address> --archive <file>` and, on Windows, COM `Account.ExportArchive(file)`.

| Entry | What it is |
|---|---|
| `manifest.json` | The address, the moment, and the counts of what follows |
| `mail/<folder path>.mbox` | Every folder with mail, one mbox each |
| `account.xml` | What the backup carries for the account, less the credentials |
| `contacts.vcf` | The address book as vCard 3.0 |
| `calendars/<calendar>/<object>` | Every calendar object as the client sent it |
| `preferences.json`, `filters.sieve` | The webmail's preferences, and the active Sieve script |
| `logs.txt` | The lines of the server's own log files that name the address, up to 50 MB |

A download past 500 MB is refused with 413, and the operator writes the archive to disk instead; the disk form has no ceiling but the format's 4 GB.

### A person's data: the erasure

`POST /api/v1/addresses/erase`, `hmctl privacy erase`, the Deck's "Erase traces" and, on Windows, the Control Panel's "Erase traces..." remove what deleting the account deliberately leaves: quarantine entries, message-trace rows, greylisting triplets, distribution-list memberships, aliases on either side, the bounce and complaint records, and the rows naming the address in other people's address books and first-contact memory. With the archive included, the person's archive tree is swept and every index row naming the address goes. The log lines are asked for by name (`include_logs` on the REST body, `--logs`): every occurrence becomes `[erased]`, and a log past 256 MB is reported instead of rewritten.

**Since 6.3.4 it also reaches the stores beside the mailbox.** A message held for review that the address sent or would receive is discarded. The address is taken out of the recipients of a message that expires, and one it sent is ended. A password-protected message to it is ended. The recipients an account remembers writing to, the out-of-office memory, booking policies and correspondents' S/MIME certificates and OpenPGP keys lose it.

**Every list of addresses is matched whole.** Erasing `ann@example.com` leaves `joann@example.com` and `ann@example.com.au` as they are.

**A legal hold outranks it.** An erasure never deletes a message from a mailbox, so a hold meets it only in the stores it deletes from, and there the hold wins. The REST answer's `held` counts what a hold kept; run the erasure again once the case is closed. When the holds themselves cannot be read, the erasure fails (HM6250; `500` over REST) and says nothing about a legal hold.

**What it leaves, by design,** among others: mail in mailboxes, the person's own and everybody else's, and a mailbox's deleted items; a list's archive of its posts; other people's calendars; configuration somebody made that names the address (a forward, a rule, a list's moderator); the audit trail, and the alert events, which nothing prunes yet; the spam trap record; the delivery queue and the webhook queue; the contacts in DMARC and TLS reports; the archive's Inbound folder; and backups, which are outside the running server's reach. [PersonalData.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/PersonalData.md) has the full list, with each reason.

### The audit trail, for a compliance reader

The audit trail is on by default. It keeps one row in `hm_audit` for each administrative change made over COM, REST or the Deck, a mailbox's move included, with a few exceptions such as an API key made or revoked or a quarantined message released or deleted; it records changes, never reads. A password, key, token or other secret is recorded as `(changed)`, never as a value. Each row carries the SHA-256 of the row before it and its own, so `GET /api/v1/audit/verify` tells an edited row from a removed one: tampering is detectable, not impossible, since anyone who can write the table can recompute the whole chain. `audit_retention_days` is 0 by default, which keeps everything.

Three things on it matter here. Every discovery search is recorded with its whole query; a download of a discovery export is recorded on the trail alone, so with the trail off a download is recorded nowhere; and an erasure is recorded with the address it erased wherever it deletes from recorded tables (aliases, list recipients, the archive index): those rows name the person until `audit_retention_days` ages them out. [Alerts, the audit trail, syslog and the journal](/topic/28273) has the columns, reading and putting a change back, who may read it, and what it does not record.

### Regulatory scope, in one paragraph

The project's own determination, reviewed 12 August 2026, is that the EU Cyber Resilience Act and the revised Product Liability Directive do not place a manufacturer's obligations on it, because it is supplied outside a commercial activity. The document calls itself a self-assessment and not legal advice, and lists the triggers that would re-open it. Under GDPR and UK GDPR the project is neither controller nor processor: the operator of an installation is. The export, the erasure and the log retention limits exist so that operators can meet their own obligations.

### What it does not do

- No PST export, and no text read from attachments but `text/*`.
- No silent hold: the custodian can see what a hold kept.
- No export past 4 GB and no download past 500 MB.
- No saved queries, review tags, redaction or de-duplication across custodians: a message sent to two custodians is two hits.
- No search of backups, and no case carried by a backup.

### Where to read more

- Wiki: [Compliance and eDiscovery](https://gitlab.com/hmailserver/hmailserver/-/wikis/Compliance-and-eDiscovery).
- Documents: [Discovery.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Discovery.md), [PersonalData.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/PersonalData.md), [AlertsAndAuditTrail.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/AlertsAndAuditTrail.md) and [RegulatoryScope.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/RegulatoryScope.md).
- On this forum: [The Control Deck](/topic/28192), [Journaling and dual delivery](/topic/28271), and what a backup carries and what it leaves in [Backup and restore](/topic/28221).
