Testing virus scanning with EICAR

Send the harmless EICAR test file to one of your addresses and watch EmailConnect exclude it from the webhook payload. Then make your handler skip infected files and notice when a scan did not run.

By Xander Groesbeek, founder · Published February 2026 · Updated October 2026 · 4 min read

In brief

What you'll build. An end-to-end test that proves infected attachments never reach your endpoint, and a webhook handler that skips them and flags files that were not scanned.

What you need. A Business or Platform plan with Scan attachments for viruses turned on in Settings → Storage (it is off by default), an alias on your domain with a webhook, and the EICAR test file from eicar.org.

How it works. ClamAV scans each attachment before the webhook fires. An infected file stays in the payload as an entry with excluded: true and excludeReason: "virus-detected", but without its content or download link; security.virusScan sums up the scan for the whole email.

EICAR is a harmless file every scanner flags

The EICAR test file is a 68-character text string, published by the European Institute for Computer Antivirus Research, that antivirus engines agree to detect as if it were a virus. It is not malware, so it is the standard way to check that a scanner is working without putting anything at risk. Download it from eicar.org.

Run the test in three steps

  1. 1

    Download the EICAR file as a .zip

    Archives are scanned. Avoid eicar.com.txt: plain-text files are skipped, so it would arrive unscanned.

  2. 2

    Email it to an alias on your domain

    For example test@in.yourdomain.com. Your own mail provider may refuse to send it; if so, send it from a script or an SMTP tool straight to your domain.

  3. 3

    Check the payload at your endpoint

    The attachment is marked excluded with excludeReason virus-detected, and security.virusScan.threatsFound is 1.

If the payload has no security object, check that your plan is Business or Platform and that scanning is turned on in Settings → Storage.

What the payload shows: clean and infected

Every scanned attachment carries a virusScan object. A clean file is delivered as usual:

{
  "filename": "invoice-1042.pdf",
  "contentType": "application/pdf",
  "size": 48210,
  "downloadUrl": "https://app.emailconnect.eu/attachments/.../download",
  "virusScan": { "status": "clean", "duration": 42 }
}

The EICAR file is excluded. Its entry stays, so you know the email carried it, but there is no content and no downloadUrl: your application never receives the file.

{
  "filename": "eicar_com.zip",
  "contentType": "application/x-zip-compressed",
  "size": 184,
  "excluded": true,
  "excludeReason": "virus-detected",
  "status": "rejected",
  "virusScan": { "status": "infected", "threat": "Eicar-Test-Signature" }
}

virusScan.status has two more values to handle: skipped (the file type is not scanned, such as an image or a plain-text file) and error (the scan itself failed). In both cases the file is delivered without a verdict.

The security summary covers the whole email

With scanning on, each payload also carries security.virusScan. Check scanned first:

"security": {
  "virusScan": {
    "scanned": true,
    "engine": "clamav",
    "engineVersion": "1.5.1",
    "attachmentsScanned": 1,
    "attachmentsSkipped": 0,
    "threatsFound": 1
  }
}
FieldMeaning
scannedWhether the scan ran. When false, the object holds only a reason (scanner-unavailable or disabled)
engine, engineVersionThe engine (clamav) and its version
definitionsUpdatedWhen the virus definitions were last updated (optional)
attachmentsScannedHow many files were scanned
attachmentsSkippedHow many were skipped as not scannable
threatsFoundHow many files were infected

Handle rejected and unscanned files in your webhook

Skip excluded files, log the threat, and decide what to do with files that have no clean verdict. Match excludeReason on its prefix: it is not a closed set of values.

// Express webhook handler (verify the signature first)
app.post('/webhook', (req, res) => {
  const { message, security } = req.body
  const scan = security?.virusScan

  if (scan && !scan.scanned) {
    console.warn('Attachments not scanned:', scan.reason)
  }

  for (const attachment of message.attachments ?? []) {
    if (attachment.excluded) {
      if (attachment.excludeReason?.startsWith('virus-detected')) {
        console.warn(`Rejected: ${attachment.filename} (${attachment.virusScan?.threat})`)
      }
      continue
    }
    if (attachment.virusScan?.status !== 'clean') {
      // skipped, error or no scan: treat as untrusted
      quarantine(attachment)
      continue
    }
    processFile(attachment)
  }

  res.sendStatus(200)
})
# Flask webhook handler (verify the signature first)
@app.route('/webhook', methods=['POST'])
def handle_webhook():
    data = request.get_json()
    scan = (data.get('security') or {}).get('virusScan')

    if scan and not scan.get('scanned'):
        logger.warning(f"Attachments not scanned: {scan.get('reason')}")

    for attachment in data['message'].get('attachments') or []:
        if attachment.get('excluded'):
            if (attachment.get('excludeReason') or '').startswith('virus-detected'):
                threat = (attachment.get('virusScan') or {}).get('threat')
                logger.warning(f"Rejected: {attachment['filename']} ({threat})")
            continue
        if (attachment.get('virusScan') or {}).get('status') != 'clean':
            # skipped, error or no scan: treat as untrusted
            quarantine(attachment)
            continue
        process_file(attachment)

    return '', 200

Whether to quarantine or process unscanned files is your policy: images, audio, video and plain-text files are always skipped, so quarantining every skipped file may be stricter than you need. Scanning finds malware; it does not vouch for content, so validate anything you decode before you use it.

Next: put scanning in your security review

Once the test passes, you have evidence that infected attachments stop at EmailConnect. For how attachments are delivered and stored, see Attachment processing →; for every field, the webhook payload reference →. Where your email is processed, who can access it and how long it is kept are answered in the trust center →

Related