Lokaal India
Lokaal India Documentation
Documentation

Verify before
you send.

Self-hosted email verification — protocol-level checks (syntax, MX, live SMTP), disposable / role / B2C detection, optional Gravatar and HaveIBeenPwned. One POST /v1/check_email call, one flat JSON answer, four possible verdicts.

01Overview

A privacy-preserving alternative to ZeroBounce / NeverBounce. Runs on your own infrastructure, never sends mail, and exposes one clean HTTP API.

Each address goes through a five-stage pipeline. The result is a single flat JSON object — every field at the top level next to is_reachable, no nested syntax / mx / smtp / misc / debug sub-objects.

01
Syntax
RFC 5321/5322 + Levenshtein typo suggestions
02
Misc
disposable / role / B2C / Gravatar / HIBP
03
MX
DNS lookup, priority sort
04
SMTP
EHLO → MAIL FROM → RCPT TO + catch-all probe
05
Score
safe / risky / invalid / unknown

Verdicts

safe mailbox exists and looks deliverable. risky deliverable but flagged (disposable, role, catch-all, full inbox). invalid syntax / domain / mailbox failure. unknown network or provider issue — try again later.

License

Dual-licensed: AGPL-3.0 for open source use, or a commercial license from Autter for proprietary software. See LICENSE.md.

02Quickstart

Run it locally

git clone https://github.com/sagnik11/email-checker.git
cd email-checker
npm install
npm run dev
# server now listening on http://127.0.0.1:8080

Validate an address

curl -X POST http://127.0.0.1:8080/v1/check_email \
  -H 'content-type: application/json' \
  -d '{"to_email":"someone@gmail.com"}'
const res = await fetch("http://127.0.0.1:8080/v1/check_email", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ to_email: "someone@gmail.com" }),
});
const data = await res.json();
console.log(data.is_reachable); // "safe" | "risky" | "invalid" | "unknown"
import requests

resp = requests.post(
    "http://127.0.0.1:8080/v1/check_email",
    json={"to_email": "someone@gmail.com"},
    timeout=30,
)
print(resp.json()["is_reachable"])

Submit a bulk job

Bulk mode requires worker.enable = true, Postgres, and RabbitMQ. See Self-hosting.

# 1. submit
curl -X POST http://127.0.0.1:8080/v1/bulk \
  -H 'content-type: application/json' \
  -H 'x-api-secret: YOUR_SECRET' \
  -d '{"input":["alice@example.com","bob@example.com"]}'
# → { "job_id": 42 }

# 2. poll progress
curl http://127.0.0.1:8080/v1/bulk/42

# 3. download results when complete
curl 'http://127.0.0.1:8080/v1/bulk/42/results?format=csv' -o results.csv

03API reference

Seven endpoints. /health, /ready, /version, and / are unauthenticated. All /v1/* endpoints require an x-api-secret header when header_secret is configured (see Authentication).

GET /health Process liveness (HTTP server alive)

Response — 200

{ "ok": true }
GET /ready Dependency readiness (Postgres + RabbitMQ)

Response — 200

{
  "ok": true,
  "dependencies": {
    "postgres": { "ok": true },
    "rabbitmq": { "ok": true, "queue": "check_email" }
  }
}

Response — 503

{
  "ok": false,
  "dependencies": {
    "postgres": { "ok": false, "error": "..." },
    "rabbitmq": { "ok": true, "queue": "check_email" }
  }
}
GET /version Package version + effective SMTP port

Response — 200

{ "version": "0.2.0", "smtp_port": 25 }

smtp_port reflects the EMAIL_CHECKER_SMTP_PORT env var (default 25). The bundled web UI uses this to pre-fill the SMTP port input.

POST /v1/check_email Validate one address

Request body

FieldTypeDescription
to_email *stringAddress to validate. Required.
check_gravatarbooleanFetch Gravatar avatar URL.
haveibeenpwned_api_keystringHIBP API key. Falls back to EMAIL_CHECKER_HIBP_API_KEY.
proxyobjectSOCKS5 proxy. See ProxyConfig.
hello_namestringEHLO domain (only honored when proxy is set).
from_emailstringMAIL FROM address (only honored when proxy is set).
smtp_timeoutintegerSMTP timeout in seconds (only honored when proxy is set).
smtp_portintegerSMTP port (only honored when proxy is set; default 25). The EMAIL_CHECKER_SMTP_PORT env var wins over this.
yahoo_verif_methodstringOne of api, headless, smtp. Default smtp.
hotmailb2c_verif_methodstringOne of headless, smtp. Default smtp.
Note: when proxy is unset, the body-level hello_name, from_email, smtp_timeout, and smtp_port are ignored and the server's TOML config is used instead. This preserves the original Rust backend's behavior.

Response — 200

{
  "input": "someone@gmail.com",
  "is_reachable": "safe",

  "email_address": "someone@gmail.com",
  "email_username": "someone",
  "email_domain": "gmail.com",
  "normalized_email": "someone@gmail.com",
  "is_valid_syntax": true,
  "syntax_suggestion": null,

  "is_disposable_email": false,
  "is_role_account": false,
  "is_b2c_provider": true,
  "gravatar_url": null,
  "has_been_pwned": null,

  "mx_accepts_mail": true,
  "mx_records": ["gmail-smtp-in.l.google.com"],
  "mx_preferred_host": "gmail-smtp-in.l.google.com",
  "mx_preferred_priority": 5,
  "mx_lookup_error_type": null,
  "mx_lookup_error_message": null,

  "smtp_can_connect": true,
  "smtp_has_full_inbox": false,
  "smtp_is_catch_all": false,
  "smtp_is_deliverable": true,
  "smtp_is_disabled_account": false,
  "smtp_error_type": null,
  "smtp_error_message": null,
  "smtp_error_description": null,

  "backend_name": "backend-dev",
  "check_started_at": "2026-01-01T00:00:00.000Z",
  "check_completed_at": "2026-01-01T00:00:01.200Z",
  "check_duration_ms": 1200,
  "check_duration_seconds": 1,
  "check_duration_nanos": 200000000,
  "verification_method_type": "smtp",
  "verification_method_host": "gmail-smtp-in.l.google.com",
  "verification_method_smtp_port": 25,
  "verification_method_provider": "gmail",
  "verification_method_chosen": "smtp",
  "verification_method_requested": null,
  "verification_method_fallback": null
}

Field-by-field reference: Schemas.

Errors

StatusBodyWhen
400{ error: "to_email field is required." }Missing field, missing/invalid x-api-secret
429{ error: "Rate limit ... exceeded ..." }Throttled
500{ error: "..." }Internal error during the check
503{ error: "..." }Worker mode enabled but RabbitMQ/Postgres unavailable
GET /v1/check_email/stream Validate one address with live progress (SSE)

Same pipeline as POST /v1/check_email, but each stage is streamed back as a Server-Sent Event the moment it completes. Useful for UIs that want to show progress while the SMTP probe runs.

Query parameters

ParamRequiredDescription
email *yesAddress to validate.
api_secretconditionalSame value as the x-api-secret header. Required when the server is configured with a header secret and the request is from a browser EventSource (which can't send custom headers).

Event sequence

EventWhendata: payload
syntaxLocal syntax check{ is_valid_syntax, domain, username, address }
mxMX lookup completes{ accepts_mail, records, preferred, lookupError }
smtp_connectTCP/TLS to MX is open{ host, port }
smtp_rcptServer replied to RCPT TO{ code, message }
donePipeline finishedFull CheckEmailResponse
errorUnexpected server error{ message }

Stages that get short-circuited (invalid syntax → no mx/smtp_*; MX failure → no smtp_*) are simply omitted, then done is sent with the final result. A : ping heartbeat comment is emitted every 15 seconds.

Example — cURL

curl -N "http://127.0.0.1:8080/v1/check_email/stream?email=foo@example.com"

Example — Browser EventSource

const url = new URL("/v1/check_email/stream", window.location.origin);
url.searchParams.set("email", "foo@example.com");

const es = new EventSource(url);
es.addEventListener("syntax",       (e) => console.log("syntax",       JSON.parse(e.data)));
es.addEventListener("mx",           (e) => console.log("mx",           JSON.parse(e.data)));
es.addEventListener("smtp_connect", (e) => console.log("smtp_connect", JSON.parse(e.data)));
es.addEventListener("smtp_rcpt",    (e) => console.log("smtp_rcpt",    JSON.parse(e.data)));
es.addEventListener("done",  (e) => { console.log("result", JSON.parse(e.data)); es.close(); });
es.addEventListener("error", (e) => { console.error(e); es.close(); });
Worker-mode note: this endpoint always runs the check inline in the API process so it can emit progress events directly. The non-streaming POST /v1/check_email retains worker-mode RPC behavior. Throttling and storage match the inline path.

Errors

StatusBodyWhen
400{ error: "email query parameter is required." }Missing email query parameter or missing/invalid auth
429{ error: "Rate limit ... exceeded ..." }Throttled (returned as JSON before the stream opens)
POST /v1/bulk Submit a bulk job (async)

Requires worker.enable = true, Postgres, and a reachable RabbitMQ broker.

Request body

FieldTypeDescription
input *string[]Addresses to validate. Required, non-empty.
webhookobjectPer-result webhook. See WebhookConfig.

Response — 200

{ "job_id": 42 }
GET /v1/bulk/{id} Poll job progress

Response — 200

{
  "job_id": 42,
  "created_at": "2026-01-01T00:00:00.000Z",
  "finished_at": null,
  "total_records": 1000,
  "total_processed": 532,
  "summary": {
    "total_safe": 410,
    "total_risky": 64,
    "total_invalid": 51,
    "total_unknown": 7
  },
  "job_status": "running"
}

job_status is "completed" once total_processed === total_records; finished_at stays null until then.

GET /v1/bulk/{id}/results Fetch results

Query parameters

ParamDefaultDescription
formatjsonOne of json, csv.
limit50 (json), all (csv)Max rows.
offset0Pagination offset.

Returns 400 if the job is still running. Wait for job_status === "completed".

Response — 200 (json)

{ "results": [ /* CheckEmailResponse, ... */ ] }

04Schemas

Mirrored from openapi.yaml. Use the YAML spec to generate clients (Swagger UI, Redoc, openapi-generator, etc.).

CheckEmailResponse

Verdict

FieldTypeDescription
inputstringTrimmed copy of the submitted to_email.
is_reachableenumsafe · risky · invalid · unknown

Syntax

FieldTypeDescription
email_addressstring · nullTrimmed address. Null when syntax invalid.
email_usernamestringLocal part.
email_domainstringDomain.
normalized_emailstring · nullCanonical form (Gmail dots/aliases stripped).
is_valid_syntaxbooleanRFC-compliant syntax check.
syntax_suggestionstring · nullTypo correction (e.g. gnail.com → gmail.com).

Misc

FieldTypeDescription
is_disposable_emailbooleanThrowaway / temporary domain.
is_role_accountbooleanLocal part is role-based (info, admin, noreply, …).
is_b2c_providerbooleanConsumer mailbox (Gmail, Yahoo, Outlook, …).
gravatar_urlstring · nullAvatar URL when check_gravatar=true.
has_been_pwnedboolean · nullHIBP breach lookup result.

MX

FieldTypeDescription
mx_accepts_mailbooleanDomain has resolvable MX records.
mx_recordsstring[]All MX hostnames (sorted by priority).
mx_preferred_hoststring · nullLowest-priority MX (used for the SMTP probe).
mx_preferred_priorityinteger · nullPriority of the preferred MX.
mx_lookup_error_typestring · nullDNS error code (ENODATA, ENOTFOUND, …).
mx_lookup_error_messagestring · nullHuman-readable DNS error.

SMTP

FieldTypeDescription
smtp_can_connectbooleanReached the SMTP greeting.
smtp_has_full_inboxbooleanMailbox over quota.
smtp_is_catch_allbooleanDomain accepts any local part.
smtp_is_deliverablebooleanRCPT TO accepted.
smtp_is_disabled_accountbooleanMailbox disabled / suspended.
smtp_error_typestring · nullError class (ECONNREFUSED, ETIMEDOUT, SmtpError, …).
smtp_error_messagestring · nullRaw SMTP / socket error message.
smtp_error_descriptionstring · nullClassifier tag: IpBlacklisted, NeedsRDNS, etc.

Debug / metadata

FieldTypeDescription
backend_namestringBackend identifier.
check_started_atISO-8601Pipeline start.
check_completed_atISO-8601Pipeline finish.
check_duration_msintegerTotal ms.
check_duration_secondsintegerWhole-second portion.
check_duration_nanosintegerNanosecond remainder.
verification_method_typeenumsmtp · headless · api · skipped
verification_method_hoststring · nullHost used.
verification_method_smtp_portinteger · nullPort used.
verification_method_providerstring · nullgmail · yahoo · hotmailb2c · hotmailb2b · proofpoint · mimecast · everything_else
verification_method_chosenstring · nullMethod actually run.
verification_method_requestedstring · nullMethod the caller asked for (if different).
verification_method_fallbackstring · nullFallback target when requested method was unavailable.

ProxyConfig

SOCKS5 tunnel for the SMTP connection.

FieldTypeRequiredDescription
hoststringyesProxy host.
portintegeryesProxy port.
usernamestringnoSOCKS5 user id.
passwordstringnoSOCKS5 password.
timeout_msintegernoProxy-hop timeout in ms.

WebhookConfig

Per-result webhook delivered by the worker for each address in a bulk job. POSTs JSON { result: CheckEmailResponse, extra: any } to on_each_email.url with the merged headers.

{
  "webhook": {
    "on_each_email": {
      "url": "https://your-app/hooks/email-result",
      "headers": { "authorization": "Bearer ..." },
      "extra":   { "list_id": "q4-cleanup" }
    }
  }
}

05Authentication

Send the configured secret with every protected request:

x-api-secret: YOUR_SECRET

When header_secret is unset, all routes are open. When it's set, missing or wrong values return 400. /health, /ready, /version, and / are always unauthenticated.

Browser bypass. Set allow_browser_without_secret = true (or EMAIL_CHECKER__ALLOW_BROWSER_WITHOUT_SECRET=true) to let same-origin browser requests skip the secret check. Used by the bundled web UI so the secret never leaks to the page source. The check verifies the request's Origin / Referer host matches the server's Host.

06CORS

The default is * (all origins allowed). Restrict it for production:

EMAIL_CHECKER__CORS__ORIGINS=https://app.example.com,https://www.app.example.com

Comma-separated string or JSON array. The server sends Access-Control-Allow-Headers: content-type, x-api-secret, authorization and a 24-hour preflight cache.

07Configuration

Loaded by src/config.ts in this priority order (later overrides earlier):

  1. Built-in defaults
  2. backend_config.toml (or path passed via --config)
  3. Double-underscore env vars: EMAIL_CHECKER__<SECTION>__<KEY>
  4. PORT env var → http_port fallback (Heroku / Fly / Render)

Common settings

Env varTOML keyDefaultNotes
EMAIL_CHECKER__HTTP_HOSThttp_host127.0.0.1Bind address
EMAIL_CHECKER__HTTP_PORThttp_port8080PORT also accepted
EMAIL_CHECKER__HEADER_SECRETheader_secret—API secret for x-api-secret
EMAIL_CHECKER__ALLOW_BROWSER_WITHOUT_SECRETallow_browser_without_secretfalseSame-origin browser bypass
EMAIL_CHECKER__HELLO_NAMEhello_nameexample.comEHLO domain
EMAIL_CHECKER__FROM_EMAILfrom_emailnoreply@example.comMAIL FROM address
EMAIL_CHECKER__SMTP_TIMEOUTsmtp_timeout15sSMTP timeout (seconds)
EMAIL_CHECKER__BACKEND_NAMEbackend_namebackend-devLabel in debug output
EMAIL_CHECKER__CORS__ORIGINScors.origins["*"]Comma-list or JSON array

Throttling

Env varTOML key
EMAIL_CHECKER__THROTTLE__MAX_REQUESTS_PER_SECONDthrottle.max_requests_per_second
EMAIL_CHECKER__THROTTLE__MAX_REQUESTS_PER_MINUTEthrottle.max_requests_per_minute
EMAIL_CHECKER__THROTTLE__MAX_REQUESTS_PER_HOURthrottle.max_requests_per_hour
EMAIL_CHECKER__THROTTLE__MAX_REQUESTS_PER_DAYthrottle.max_requests_per_day

Worker mode

Env varTOML keyDefault
EMAIL_CHECKER__WORKER__ENABLEworker.enablefalse
EMAIL_CHECKER__WORKER__RABBITMQ__URLworker.rabbitmq.urlamqp://guest:guest@localhost:5672
EMAIL_CHECKER__WORKER__RABBITMQ__CONCURRENCYworker.rabbitmq.concurrency5
EMAIL_CHECKER__STORAGE__POSTGRES__DB_URLstorage.postgres.db_url—

Single-underscore aliases

These bypass the TOML overlay and are read directly by the checker. Useful for cloud providers that pass env vars through a flat namespace.

Env varUsed by
EMAIL_CHECKER_SMTP_PORTSMTP probe port. Wins over body-level smtp_port. Useful when port 25 is blocked.
EMAIL_CHECKER_HIBP_API_KEYHaveIBeenPwned API key fallback.
EMAIL_CHECKER_FROM_EMAILMAIL FROM fallback.
EMAIL_CHECKER_HELLO_NAMEEHLO domain fallback.
EMAIL_CHECKER_BACKEND_NAMEBackend label fallback.
SMTP_DEBUGSet to true for verbose JSON-line logging of the SMTP transaction.
Inactive / stub config fields

Present in defaultConfig() but not wired up in this codebase. Leave them blank.

  • sentry_dsn — config field exists but no Sentry SDK is installed.
  • commercial_license_trial — placeholder for the hosted commercial trial integration.
  • webdriver_addr / webdriver — stubs from a removed headless-browser verification mode.
  • overrides.proxies — currently unused.

08Deployment

docker build -t email-validator .
docker run -p 8080:8080 \
  -e EMAIL_CHECKER__HTTP_HOST=0.0.0.0 \
  -e EMAIL_CHECKER__HEADER_SECRET=YOUR_SECRET \
  email-validator

Multi-stage build, Node 20 Alpine, exposes 8080. See Dockerfile.

fly launch --no-deploy
fly secrets set \
  EMAIL_CHECKER__HEADER_SECRET=YOUR_SECRET \
  EMAIL_CHECKER__ALLOW_BROWSER_WITHOUT_SECRET=true
fly deploy

Full step-by-step: FLY_DEPLOYMENT.md. Default region sin, 512 MB shared CPU, HTTPS forced, min_machines_running = 1.

api/index.ts wraps the Express app as a serverless function. vercel.json routes everything to it. 1 GB / 30 s limit.

  1. Push the repo to GitHub.
  2. Import into Vercel.
  3. Set Node.js runtime to 18+.
  4. Add env vars in Project Settings.
Limitation: serverless cannot run the worker or hold persistent RabbitMQ / Postgres connections, so bulk mode is not supported on Vercel. Single-check API only.
npm install
npm run build
EMAIL_CHECKER__HTTP_HOST=0.0.0.0 \
EMAIL_CHECKER__HEADER_SECRET=YOUR_SECRET \
npm start

Pair with systemd, PM2, or a process supervisor of your choice. Reverse-proxy via Caddy / nginx for HTTPS.

If outbound port 25 is blocked by your provider, use EMAIL_CHECKER_SMTP_PORT=587 or route through a SOCKS5 proxy (below).

09Self-hosting

Worker mode

Async bulk processing requires a worker, RabbitMQ, and Postgres. The API and worker can run as a single inline process or as separate scaled processes.

Inline (single process)

# with worker.enable = true in backend_config.toml
email-validator serve --config ./backend_config.toml

Separate processes (recommended for production)

# API
EMAIL_CHECKER__WORKER__ENABLE=false email-validator serve

# Worker (any number of replicas)
email-validator worker --config ./backend_config.toml

Workers consume from the check_email RabbitMQ queue with prefetch = worker.rabbitmq.concurrency (default 5). Single-shot RPC checks have higher priority than bulk job tasks. Transient SMTP failures (unknown) are requeued once before being persisted.

Postgres schema

src/storage/postgres.ts auto-migrates on startup. Two tables:

  • v1_bulk_job — id, created_at, total_records.
  • v1_task_result — id, job_id, created_at, result (jsonb), extra (jsonb), error.

Aggregate progress is computed on the fly — no separate counters to keep in sync.

RabbitMQ

  • Queue: check_email (durable, priority-aware).
  • Single-shot RPC pattern uses an exclusive reply queue per request.
  • Concurrency knob: worker.rabbitmq.concurrency.

SOCKS5 proxy

When the host blocks port 25 outbound (most clouds do), route SMTP through a SOCKS5 proxy:

{
  "to_email": "someone@example.com",
  "proxy": {
    "host": "proxy.local",
    "port": 1080,
    "username": "user",
    "password": "pass",
    "timeout_ms": 20000
  },
  "smtp_port": 25
}

Or use a different port server-side: EMAIL_CHECKER_SMTP_PORT=587.

Observability

  • Logs are JSON-lines on stdout. Set SMTP_DEBUG=true for verbose SMTP transaction logs.
  • Liveness probe: GET /health; readiness probe: GET /ready.
  • Queue depth: monitor RabbitMQ's management UI / Prometheus exporter.
  • Job status: query v1_task_result directly or poll GET /v1/bulk/{id}.

10Architecture

Verification pipeline

Orchestrated in src/checker/checkEmail.ts. Each stage may short-circuit the rest.

1. Syntax — src/checker/syntax.ts

  • RFC 5321/5322 validation via isemail.
  • Gmail / Googlemail normalization: dots stripped, +alias removed; canonical form goes into normalized_email.
  • Typo suggestions via Levenshtein distance < 3 against gmail.com, yahoo.com, outlook.com, hotmail.com, protonmail.com, icloud.com, yandex.com.

2. Misc — src/checker/misc.ts

  • Disposable: union of mailchecker + disposable-email-domains (~96k entries) + a small hardcoded override set. Domain and parent suffixes both checked.
  • Role: lowercased local part looked up in src/data/roles.txt (944 entries).
  • B2C: domain looked up in src/data/b2c.txt (~96,600 entries).
  • Gravatar: optional. MD5 hash + ?d=404 probe, 6 s timeout.
  • HIBP: optional. v3 API, 10 s timeout. Returns true / false / null on error.

3. MX — src/checker/mx.ts

  • node:dns/promises resolveMx(); sorted by priority ascending.
  • Soft DNS errors (ENODATA, ENOTFOUND, NXDOMAIN, SERVFAIL) yield accepts_mail: false and invalid.
  • Hard DNS errors throw → result becomes unknown.

4. SMTP — src/checker/smtp.ts · smtpParser.ts · provider.ts

  • Provider detected from MX hostname: gmail, yahoo, hotmailb2c, hotmailb2b, proofpoint, mimecast, everything_else.
  • TCP socket (or SOCKS5 tunnel). Port 465 → TLS automatically.
  • Sequence: 220 greeting → EHLO → MAIL FROM → optional random RCPT (catch-all probe) → real RCPT → QUIT.
  • Errors classified by smtpParser.ts: invalid mailbox, full inbox, disabled account, blacklisted IP, needs reverse DNS.
  • Retry policy: configurable retries (default 1) on unclassified errors.

5. Reachability — calculateReachable()

Evaluated in this order:

VerdictCondition
unknownAn SMTP error is present (timeout, blacklist, rDNS, …)
riskyis_disposable · is_role_account · is_catch_all · has_full_inbox
invalid!is_deliverable · !can_connect_smtp · is_disabled
safeNone of the above

Syntax failure short-circuits to invalid before the scoring matrix runs.

SMTP rules engine

src/data/rules.json stores per-domain and per-MX overrides applied in src/checker/rules.ts:

  • SkipCatchAll — skip the random-address probe (Gmail, Hotmail, Yahoo always accept random RCPTs and would otherwise look catch-all).
  • SmtpTimeout45s — bump the SMTP timeout to ≥ 45 s for slow / greylisting servers.

Repository layout

src/
  checker/   pipeline (syntax, misc, mx, smtp, scoring, rules, provider)
  http/      Express routes + auth + CORS
  worker/    RabbitMQ queue, consumer loop, task service
  storage/   storage abstraction + Postgres impl (auto-migration)
  data/      roles.txt, b2c.txt, rules.json (loaded as in-memory Sets)
  config.ts  TOML + env precedence
  runtime.ts wires config + storage + throttle + rabbit
  server.ts  HTTP entrypoint
bin/email-validator.ts   CLI (serve | worker | check)
api/index.ts             Vercel wrapper
public/                  static assets (web UI, docs, openapi.yaml)
test/                    node:test suite

11Contributing

Patches, bug reports, and feature requests welcome. Full guide: CONTRIBUTING.md.

git clone https://github.com/sagnik11/email-checker.git
cd email-checker
npm install
npm run dev      # tsx-based dev server
npm test         # node:test runner

Tests live in test/ and cover syntax, SMTP parser, reachability scoring, config loading, and throttling. New features should land with tests where practical.

12License

AGPL-3.0-or-later for open source use, or a commercial license from Autter Pte. Ltd. for proprietary software. See LICENSE / LICENSE.md.