← Files

hMailServer + Kaspersky Cleanup#

<p align="center"><b>Audit an hMailServer message store with the local Kaspersky console client in report-only mode, review the findings, and delete only confirmed infected messages through the hMailServer COM API.</b></p>

Repository and developer#

Item Value
Repository https://github.com/paulmann/hmailserver-kaspersky-cleanup
Purpose Kaspersky (avp.com) based audit and reviewed remediation of infected messages in an hMailServer store
Companion project hmailserver-clamav-cleanup — the same workflow built on ClamAV/clamd
Author Mikhail Deynekin (@paulmann)
E-mail git@deynekin.com
Web https://deynekin.com/
License PolyForm Noncommercial 1.0.0
Language PowerShell 7.2+ (pwsh), Windows only
Status Field-tested against Kaspersky 21.25 on a live mail server

Table of contents#


Why this exists#

hMailServer can hand incoming mail to an external virus scanner, but that only protects what arrives after the integration works. A store that has been running for years usually contains messages that were accepted before the scanner was configured, or while its databases were stale, or while the integration silently failed.

A plain avp.com SCAN C:\mail\Data /i2 is not an acceptable answer for a mail store:

This toolkit therefore separates detection from deletion: Kaspersky always runs with /i0 (report only), the findings are correlated with real hMailServer messages over COM, written into a reviewable plan, and only a second, explicit run consumes that plan.


Repository contents#

File Role
Invoke-HMailKasperskyCleanup.ps1 Main tool. Four modes: Preflight, Scan, Plan, Delete. Resolves avp.com and the hMailServer data directory, scans in batches, parses verdicts, matches them to messages over COM, writes a plan, deletes on demand with an optional quarantine copy.
Test-AvpCli.ps1 Behaviour harness for avp.com. Builds a disposable EICAR corpus and drives the CLI through a full matrix of invocation forms, recording exit codes, reports, verdicts and survival of the target. Also serves as a reusable avp.com locator via -ResolvePathOnly.
README.md This document.

How it works#

flowchart TD
    A([pwsh 7.2+, elevated, interactive session]) --> B{Mode}

    B -->|Preflight| P1[Resolve avp.com<br/>multi-stage discovery + HELP health probe]
    P1 --> P2[Capability matrix:<br/>Cmd vs Direct runner, ANSI/OEM list vs direct args, with/without /RA]
    P2 --> P3[(preflight-matrix.json)]
    P3 --> STOP([stop])

    B -->|Scan / Plan| S1[Resolve avp.com]
    S1 --> S2[Resolve hMailServer Data directory<br/>COM to INI to registry to install path]
    S2 --> S3[Enumerate *.eml, optional -SinceDays filter]
    S3 --> S4[(manifest-before.json<br/>SHA-256 per file)]
    S4 --> S5[Batched SCAN with /i0<br/>report only, nothing is touched]
    S5 --> S6[Parse report body and statistics block<br/>verdict families: EICAR, HEUR:, UDS:, VHO:, PDM:, Trojan, Exploit, Worm, ...]
    S6 --> S7[Coverage check:<br/>intended vs scanned vs vanished vs failed batches]
    S7 --> S8[Correlate detections with messages over COM<br/>domains to accounts to INBOX and IMAP folders]
    S8 --> S9[(remediation-plan.json<br/>infected-messages.csv)]
    S9 --> REVIEW([human review])

    B -->|Delete| D1[Load plan, verify schema 5<br/>and that DataDirectory still matches]
    D1 --> D2[Re-enumerate over COM<br/>match by MessageId + file path]
    D2 --> D3{Quarantine enabled?}
    D3 -->|yes| D4[Copy .eml to quarantine\<stamp><br/>failure aborts that message]
    D3 -->|no| D5
    D4 --> D5[DeleteByDBID via COM<br/>ShouldProcess / -WhatIf honoured]
    D5 --> D6[Optional -DeleteOrphan for unmatched .eml<br/>only inside the store, only *.eml]
    D6 --> D7([run.log, then re-run Scan to verify])

Three design decisions carry the whole tool:

  1. Verdicts come from the report, not from the exit code. A scan that finds an object it cannot process returns exit code 3 while the task itself completes and writes a full report. 0, 3, 101 and 102 are treated as non-fatal; the decision is made from the report body and its ; Total detected: statistics block.

  2. avp.com is discovered, then proven alive. A pending or failed upgrade leaves a second product folder behind, and that stale build accepts invocations and answers nothing. Every candidate is asked for HELP; only a build that answers is used. Newest-folder-wins is not sufficient.

  3. Deletion goes through hMailServer, never through the antivirus. DeleteByDBID keeps the database and the file system consistent; the scanner never has permission to touch the store.


Requirements#

Warning

Back up the hMailServer database and the Data directory before using Delete mode. Deletion is irreversible from the tool's point of view; the quarantine copy is a convenience, not a backup.


Quick start#

# 0. Prove that the console client is usable on this host and see which invocation forms work
.\Invoke-HMailKasperskyCleanup.ps1 -Mode Preflight

# 1. Optional: full behaviour matrix of avp.com on this build (EICAR based, disposable lab)
.\Test-AvpCli.ps1 -Runner Both

# 2. Audit the last 24 hours, report only, nothing is modified
.\Invoke-HMailKasperskyCleanup.ps1 -Mode Scan -SinceDays 1 -BatchSize 40

# 3. Audit the whole store, keeping every batch artefact for inspection
.\Invoke-HMailKasperskyCleanup.ps1 -Mode Scan -UpdateDatabases -KeepBatchArtifacts

# 4. Review, then remediate from the reviewed plan (dry run first)
$plan = 'C:\mail\reports\kaspersky\run-20260811-221500\remediation-plan.json'
.\Invoke-HMailKasperskyCleanup.ps1 -Mode Delete -PlanPath $plan -WhatIf
.\Invoke-HMailKasperskyCleanup.ps1 -Mode Delete -PlanPath $plan

Use Test-AvpCli.ps1 as a locator from your own scripts:

$avp = .\Test-AvpCli.ps1 -ResolvePathOnly -Quiet
& $avp.Path SCAN 'C:\mail\spool' /i0

# or cache the result for another process
.\Test-AvpCli.ps1 -ResolvePathOnly -PathFormat Json -Quiet | Set-Content .\avp-location.json

Operating modes#

Mode Touches the store What it does
Preflight no Discovers and health-probes avp.com, then runs a capability matrix against a disposable clean probe message and writes preflight-matrix.json. Stops there.
Scan read only Manifest, batched report-only scan, verdict parsing, coverage accounting, COM correlation, plan and CSV.
Plan read only Identical to Scan; the run is marked as review-only in the plan metadata.
Delete yes Consumes a plan, re-verifies every entry over COM, optionally quarantines, then deletes through DeleteByDBID. Honours -WhatIf / -Confirm.

Parameters of Invoke-HMailKasperskyCleanup.ps1#

Parameter Default Meaning
-Mode Scan Preflight, Scan, Plan or Delete.
-AvpPath auto Explicit path to avp.com; skips discovery but still health-probed.
-KasperskyRoot C:\Program Files (x86)\Kaspersky Lab Extra root that contains product folders.
-DataDirectory auto hMailServer Data directory. Resolved over COM, then hMailServer.INI, then the registry, then the install path.
-AdminPassword prompt hMailServer Administrator password as a SecureString.
-ReportDirectory %SystemDrive%\mail\reports\kaspersky Root for run directories. Must be outside the store.
-QuarantineDirectory %SystemDrive%\mail\quarantine Root for quarantine copies. Must be outside the store.
-PlanPath — Required in Delete mode: the remediation-plan.json to consume.
-SinceDays 0 (all) Only messages whose LastWriteTime is newer than N days.
-TimeoutMinutes 60 Timeout for a single avp.com call.
-BatchSize 40 Messages per scan call; automatically lowered for direct-argument scope.
-Runner Cmd Cmd (cmd.exe with redirection), Direct (.NET pipes) or Auto.
-MaxCommandLine 7000 Command-line budget used to bound the batch size.
-IncludeQueue off Also scan loose .eml files in the root of the store (the queue).
-UpdateDatabases off Run avp.com UPDATE before scanning.
-SkipQuarantine off Delete without keeping a copy.
-DeleteOrphan off In Delete mode also remove infected files that no message references.
-KeepBatchArtifacts off Keep per-batch reports even when nothing was found.
-AllowPartialCoverage off Accept partial coverage without the loud warning.

A single instance is enforced through a global mutex (Global\Invoke-HMailKasperskyCleanup).


Artefacts of a run#

Everything lands in <ReportDirectory>\run-yyyyMMdd-HHmmss\:

Artefact Content
run.log Timestamped INFO / WARN transcript of the whole run.
avp-health\health-NN.out Raw output of every HELP probe used to pick the live build.
preflight-matrix.json Every runner/scope/report combination with exit code, byte counts and usability.
manifest-before.json Path, size, UTC timestamp and SHA-256 of every selected message (hashing is skipped above 50 000 files).
batches\batch-NNNNN.{lst,report,out} Scope list, Kaspersky report and captured output per batch.
kaspersky-aggregated.txt All batch reports concatenated — the single source of truth for parsing.
unscanned-files.json Written when coverage is partial: the files that were not scanned.
remediation-plan.json The reviewable plan, schema 5.
infected-messages.csv Flat view for review: account, folder, message id, date, from, subject, size, verdict, inner object, file.

Quarantine copies go to <QuarantineDirectory>\<stamp>\<account>_<id>_<file>.bad.


The remediation plan#

{
  "Schema": 5,
  "CreatedUtc": "2026-08-11T19:15:00.000Z",
  "Mode": "Scan",
  "DataDirectory": "C:\\mail\\Data",
  "AvpPath": "C:\\Program Files (x86)\\Kaspersky Lab\\Kaspersky 21.25\\avp.com",
  "Capability":  { "Runner": "Cmd", "Scope": "ListAnsi", "UseReport": true },
  "Statistics":  { "Processed": 18422, "TotalOk": 18420, "Detected": 1, "Suspicions": 1, "Skipped": 1, "Corrupted": 0, "Errors": 0 },
  "Coverage":    { "Intended": 18422, "Scanned": 18422, "Unscanned": 0, "VanishedAfter": 0, "FailedBatches": 0, "Complete": true },
  "Detections":  [ { "Path": "...\\{ID}.eml", "Verdict": "HEUR:Exploit.MSOffice.Generic", "InnerObject": "//attachment.rtf//equation", "Detected": true, "Skipped": true } ],
  "MatchedMessages": [ { "Account": "user@example.com", "Folder": "INBOX", "MessageId": 40213, "Subject": "Official Purchase Order", "Verdict": "HEUR:Exploit.MSOffice.Generic" } ],
  "Orphans": []
}

Delete mode refuses a plan whose Schema is not 5, or whose DataDirectory no longer matches the resolved store, or that references any path outside the store. Entries that changed since the scan are skipped and counted rather than deleted blindly.


Test-AvpCli.ps1: the behaviour harness#

The harness answers a single question: what does this particular Kaspersky build actually accept? It builds a disposable corpus from the EICAR test string — plain file, file in a Cyrillic directory, .eml with a base64 attachment, mbox folder file, ZIP archive — and then records, per case, the exit code, elapsed time, the first line of output, whether /RA and /R produced a report, how many verdicts appeared, and whether the target survived.

Group What is probed
help, state HELP, HELP SCAN, STATUS, STATISTICS
error unknown command, scan of a non-existent path
target directory, Cyrillic directory, single file, no action switch
container .eml with attachment, mbox folder file
list scope lists in ANSI, OEM, Unicode and UTF-8, plus an empty list
report /RA: and /R:
depth, filter /fa, /fe, /fi, /e:A, /e:BM
auth EXPORT with and without credentials
action /i1, /i2, /i8 — only with -IncludeDestructive, only on throwaway copies

Key switches: -Runner Cmd|Direct|Both, -SkipEicar (clean corpus only, for a pure syntax matrix), -IncludeDestructive, -SkipAuthProbe, -SkipLivenessProbe, -ResolvePathOnly with -PathFormat Object|Path|Json, -Quiet.

Outputs land in <LabRoot>\output\: avp-cli-matrix.csv, avp-cli-matrix.json, summary.md and harness.log.

Note

Real-time protection deletes EICAR objects while the matrix runs. The harness never asks you to disable protection or add exclusions: it rebuilds the corpus per runner and records a case whose target vanished as corpus lost instead of pretending a missing file produced a result.

Credentials are handled carefully. Endpoint Security accepts /login=<user> together with /password=<secret>; the consumer line accepts /password only and has no account name. Credentials are appended solely to commands documented as protected (ACTIVATE, ADDKEY, DISABLE, EXIT, EXPORT, IMPORT, LICENSE, MDRLICENSE, PAUSE, RESTORE, RESUME, SELFPROTECTION, STOP). Invalid credentials are never probed, because repeated failures lock the Password-protection account.


Exit codes of avp.com#

Code Meaning Treated as fatal?
0 operation completed successfully no
1 invalid parameter value yes
2 unknown error yes
3 task completion error — in practice objects were detected and left unprocessed no
4 task cancelled yes
101 all dangerous objects processed no
102 dangerous objects detected no
-10 undocumented; observed with the /@: scope-list form yes
-1 the script could not start or wait for the process yes

Field-measured behaviour#

Everything below was measured on a live server, not assumed:


Safety guarantees#


Scheduling#

Because the console client needs the resident process in an interactive session, a fully unattended full-store scan is not always possible. A practical compromise is a daily audit-only run that alerts a human:

$action  = New-ScheduledTaskAction -Execute 'pwsh.exe' `
  -Argument '-NoProfile -File "C:\tools\Invoke-HMailKasperskyCleanup.ps1" -Mode Plan -SinceDays 2'
$trigger = New-ScheduledTaskTrigger -Daily -At 03:30
Register-ScheduledTask -TaskName 'hMail Kaspersky audit' -Action $action -Trigger $trigger -RunLevel Highest

Then review infected-messages.csv and run Delete by hand.


Troubleshooting#

Symptom Cause and fix
No avp.com build answered the HELP command The resident process is not running, the session is not interactive or not elevated, or every candidate belongs to a stale installation. Verify manually: avp.com HELP SCAN.
No avp.com invocation form produced parseable output Same checklist; inspect the captured output under run-*\preflight\.
Cannot create hMailServer.Application (0x80040154) COM is not registered, or the process bitness does not match. Re-register hMailServer and run the matching pwsh.
hMailServer authentication failed Wrong Administrator password; pass -AdminPassword as a SecureString.
Statistics report detections but no path was parsed Inspect kaspersky-aggregated.txt; the report layout of that build differs. Open an issue with the relevant lines.
Coverage is partial Re-run for the paths listed in unscanned-files.json, or accept it with -AllowPartialCoverage. Detections already found remain valid.
Every scan batch failed Run -Mode Preflight and read the matrix; usually a dead CLI or a non-interactive session.
Messages still visible after deletion Compact the folders in the mail client, then re-run Scan to verify.

Security notes#

This toolkit removes mail from a production store. Treat it as a privileged operation: keep the report directory on a volume with an audit trail, keep quarantine copies until you are certain, and never point -DataDirectory at anything you have not backed up. Findings such as HEUR:Exploit.MSOffice.* on an //equation object mean the message reached a mailbox — investigate whether it was opened, not only whether it is gone.


License#

PolyForm Noncommercial 1.0.0 — see LICENSE.

Author#

Mikhail Deynekin — git@deynekin.com · https://deynekin.com/ · @paulmann

If this saved you an incident, a star is appreciated. Issues and pull requests with real report excerpts from other Kaspersky builds are especially welcome.