emailverifier.dev
Posts

Bulk Email Verification: A Safe CSV Workflow

| 6 min read | Usama Ejaz
A CSV grid passes through an email verification gate while row identifiers stay aligned and four result lanes remain separate.

Bulk email verification should preserve every source row, attach evidence beside the original address, and import the result as a new dataset version. It should not turn a list into one unreliable “valid” column.

I start with a preflight manifest. It records which file entered the job, how many rows need checks, and whether duplicates or blanks explain the expected credit count.

Here is the output from the small fixture I ran for this guide:

{
  "sourceRows": 7,
  "nonEmptyRows": 6,
  "blankRows": 1,
  "uniqueExactAddresses": 5,
  "exactDuplicateRows": 1,
  "sourceSha256": "5f6c11f6b77cef1d2feb0a120fd914f3f7babb16faf2b91c4338fa395125f915",
  "preparedSha256": "af82cd9fb602e4075f29fa7059dbd47ca5259a63e04adba162e166173a1b6de5"
}

The addresses were hypothetical and used reserved example.com, example.net, and example.org domains. No verification request was made. The test covered file preparation and reconciliation only.

Start with a manifest, not the upload

A bulk job is a data migration with an external decision step in the middle. Losing row identity is more dangerous than losing row order, because a correct result attached to the wrong customer is still wrong.

For a simple source, put one address on each line in emails.txt. This Node.js script creates a quoted CSV with three fields:

  • source_row is the stable join key.
  • email_original preserves the submitted text.
  • email removes surrounding whitespace for the verification input.
import { createHash } from 'node:crypto'
import { readFileSync, writeFileSync } from 'node:fs'

const sourcePath = process.argv[2] || 'emails.txt'
const preparedPath = process.argv[3] || 'emails-prepared.csv'
const source = readFileSync(sourcePath)
const text = source.toString('utf8').replace(/^\uFEFF/, '')
const lines = text.split(/\r?\n/)

if (lines.at(-1) === '' && /\r?\n$/.test(text)) lines.pop()

const rows = lines.map((emailOriginal, index) => ({
  sourceRow: index + 1,
  emailOriginal,
  email: emailOriginal.trim()
}))

const counts = new Map()
for (const row of rows) {
  if (!row.email) continue
  counts.set(row.email, (counts.get(row.email) || 0) + 1)
}

const csv = value => `"${String(value).replaceAll('"', '""')}"`
const prepared = [
  'source_row,email_original,email',
  ...rows.map(row =>
    [row.sourceRow, row.emailOriginal, row.email].map(csv).join(',')
  )
].join('\n') + '\n'

writeFileSync(preparedPath, prepared)

const nonEmptyRows = rows.filter(row => row.email).length
const sha256 = bytes =>
  createHash('sha256').update(bytes).digest('hex')

console.log(JSON.stringify({
  sourceRows: rows.length,
  nonEmptyRows,
  blankRows: rows.length - nonEmptyRows,
  uniqueExactAddresses: counts.size,
  exactDuplicateRows: nonEmptyRows - counts.size,
  sourceSha256: sha256(source),
  preparedSha256: sha256(Buffer.from(prepared))
}, null, 2))

Run it with:

node bulk-email-preflight.mjs emails.txt emails-prepared.csv

The CSV quoting follows the field escaping documented in RFC 4180, section 2. The hashes use Node’s documented createHash() interface.

A hash identifies the exact file version. It is not a privacy control and does not replace access restrictions.

Keep address comparison out of preflight

The script counts exact duplicates after trimming surrounding whitespace. It deliberately keeps Ana@example.com and ana@example.com separate. It also preserves dev+trial@example.org instead of stripping the tag.

Lowercasing an entire address or applying provider-specific alias rules might merge distinct accounts. The email case-sensitivity storage model owns the deeper comparison policy. Bulk preparation should not invent a new one.

If your application already has a documented comparison key, export it as a separate column. Keep the displayed address and verification input intact.

Verify the prepared file

As of August 29, 2026, the emailverifier.dev bulk verifier accepts CSV files with up to 100 columns, TXT files, or pasted one-address-per-line lists. A job supports up to 5,000 unique addresses.

  1. Upload emails-prepared.csv.
  2. Select email as the address column.
  3. Compare the displayed unique, duplicate, blank, and credit counts with the manifest.
  4. Start the job only after the counts agree.
  5. Download the completed CSV and keep it beside the manifest.

The product charges one project credit per unique, non-empty address. Duplicate rows reuse the first result, blank rows stay blank, and the original rows remain in the download. The result adds status, action, flagged state, signals, and a typo suggestion where available.

A mismatch before launch usually points to the selected column, unexpected whitespace, an export filter, or a duplicate policy difference. Stop there. Starting anyway turns a visible input problem into a harder reconciliation problem.

Keep four outcomes separate

The download has four verification statuses, not a single clean-or-dirty verdict.

  • deliverable records positive address evidence. It does not promise future inbox placement.
  • undeliverable records a confirmed failure such as invalid structure, unusable routing, or mailbox rejection.
  • risky records a concrete signal such as a disposable provider or likely typo. It is not proof of abuse.
  • unknown means the available mailbox evidence was inconclusive. It is not an invalid address.

Route rows from the returned action and retain the signals behind it. The production result-handling guide explains why transport failures and verification outcomes need separate application states.

For a CRM cleanup, I would import these fields beside the existing contact and let a reviewed policy decide suppression. I would not overwrite the original address with a typo suggestion or delete every unknown result.

The fair objection is simple: a cleanup project only feels useful when it removes rows. Still, removing an unknown address converts missing evidence into a permanent decision. Suppress confirmed failures, review risk signals, and keep network uncertainty visible until later delivery or confirmation evidence resolves it.

Import the result as a new version

Join the completed file back to your source on source_row. Before replacing any active dataset, check four conditions:

  1. The source hash matches the manifest.
  2. The prepared hash matches the uploaded file.
  3. The completed file has the expected source-row count.
  4. Every non-empty source row has one preserved verification outcome.

Import into a staging table first. A uniqueness constraint on the job ID plus source_row blocks an accidental double import. Promote the staged version only after row and status counts reconcile.

This versioned approach leaves the old dataset available for rollback and audit. It also separates “the address result changed” from “someone edited the customer record.”

Verification does not create permission

A deliverable result says nothing about how the address entered the list. M3AAWG’s sender best practices require explicit recipient consent before ongoing bulk or marketing messages and recommend retaining collection evidence such as time, source, and disclosure context.

Keep consent, suppression, provenance, and verification as separate fields. The spam-trap guide explains why address evidence cannot replace permission history.

Remove raw files on a real schedule

As of August 29, 2026, emailverifier.dev removes working bulk records when a job completes or permanently fails. Completed result files expire after seven days, with an earlier delete option in the dashboard. Those details are published in the privacy policy.

Your local copy needs its own retention rule. Keep the small audit record required for the import, restrict access to raw addresses, and remove staging files after the rollback window closes.

The finished job is trustworthy when its result rows still map to the original records, every uncertain outcome stays uncertain, and the source list’s permission history survives beside the verification evidence.

Continue reading