{"id":18711828,"url":"https://github.com/martyndavies/legit","last_synced_at":"2026-04-01T20:02:00.859Z","repository":{"id":10205540,"uuid":"12299471","full_name":"martyndavies/legit","owner":"martyndavies","description":"NodeJS library for checking MX records exist on a domain","archived":false,"fork":false,"pushed_at":"2026-03-20T21:26:40.000Z","size":175,"stargazers_count":111,"open_issues_count":0,"forks_count":16,"subscribers_count":4,"default_branch":"master","last_synced_at":"2026-03-21T08:59:03.169Z","etag":null,"topics":["email","mx","mx-record","nodejs","validation"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/martyndavies.png","metadata":{"files":{"readme":"readme.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2013-08-22T14:45:01.000Z","updated_at":"2026-03-20T21:26:37.000Z","dependencies_parsed_at":"2024-11-07T12:44:38.163Z","dependency_job_id":"667f303b-d98e-45f5-82c5-1e68d9529a7b","html_url":"https://github.com/martyndavies/legit","commit_stats":{"total_commits":61,"total_committers":3,"mean_commits":"20.333333333333332","dds":"0.19672131147540983","last_synced_commit":"6d0684aa6ec003019f7370a6c147cf94961bb1d9"},"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/martyndavies/legit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martyndavies%2Flegit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martyndavies%2Flegit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martyndavies%2Flegit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martyndavies%2Flegit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/martyndavies","download_url":"https://codeload.github.com/martyndavies/legit/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martyndavies%2Flegit/sbom","scorecard":{"id":622147,"data":{"date":"2025-08-11","repo":{"name":"github.com/martyndavies/legit","commit":"5146d9a64d9b76b6895c8b49d90239e88513725c"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.3,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Code-Review","score":0,"reason":"Found 0/23 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":2,"reason":"3 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 2","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 12 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-21T05:26:39.757Z","repository_id":10205540,"created_at":"2025-08-21T05:26:39.757Z","updated_at":"2025-08-21T05:26:39.757Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291337,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["email","mx","mx-record","nodejs","validation"],"created_at":"2024-11-07T12:40:41.062Z","updated_at":"2026-04-01T20:02:00.850Z","avatar_url":"https://github.com/martyndavies.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# legit\n\n**DNS-based email validation for Node.js.** Checks whether an email address's domain has active mail server records before you try to send to it.\n\nZero runtime dependencies. Full TypeScript support. Node.js ≥ 18.\n\n---\n\n## Why DNS validation?\n\nFormat validation (`user@domain.com` passes a regex) tells you almost nothing about whether an email is deliverable. DNS validation goes one step further: it queries the domain's MX records to confirm there is actually a mail server waiting to receive messages.\n\nThis catches a large class of bad addresses at the point of collection — typos like `user@gmial.com`, defunct domains, and domains that have explicitly opted out of receiving email — without the cost and complexity of SMTP probing.\n\nIt does **not** tell you whether a specific mailbox exists. See [Limitations](#limitations) for the full picture.\n\n---\n\n## Installation\n\n```bash\nnpm install legit\n```\n\n---\n\n## Quick start\n\n```typescript\nimport legit from 'legit';\n\nconst result = await legit('user@example.com');\n\nif (result.isValid) {\n  // result.mxArray is MxRecord[] — TypeScript knows this\n  console.log('Primary mail server:', result.mxArray[0].exchange);\n} else {\n  // result.mxRecordSetExists is boolean — TypeScript knows this too\n  console.log('Domain cannot receive email');\n}\n```\n\n---\n\n## How it works\n\nEvery call follows this decision tree:\n\n```\nInput\n  │\n  ├─ Structurally invalid? (no @, bad local part, etc.)\n  │     → rejects with TypeError immediately — no DNS query issued\n  │\n  └─ Valid structure\n        │\n        ├─ MX records found?\n        │     ├─ Null MX (RFC 7505)? → { isValid: false, mxRecordSetExists: true }\n        │     └─ Real MX records    → { isValid: true, mxArray: [...sorted by priority] }\n        │\n        └─ No MX records (ENODATA)\n              │\n              ├─ A record exists? (RFC 5321 §5.1 fallback)\n              │     → { isValid: true, mxArray: [{ exchange: domain, priority: 0 }] }\n              │\n              └─ No A record / domain not found (ENOTFOUND)\n                    → { isValid: false, mxRecordSetExists: false }\n```\n\nUnexpected DNS errors (`ESERVFAIL`, `ECONNREFUSED`, timeout) reject the promise so you can distinguish infrastructure problems from a simple invalid domain.\n\n---\n\n## API\n\n### `legit(emailAddress, options?)`\n\n```typescript\nimport legit from 'legit';\nimport type { LegitOptions, LegitResult } from 'legit';\n```\n\n**Parameters**\n\n| Name | Type | Description |\n|---|---|---|\n| `emailAddress` | `string` | The email address to validate |\n| `options` | `LegitOptions` | Optional configuration (see below) |\n\n**Returns** `Promise\u003cLegitResult\u003e`\n\n---\n\n### `LegitOptions`\n\n```typescript\ninterface LegitOptions {\n  timeout?: number; // DNS lookup timeout in ms. Default: 10000\n}\n```\n\n---\n\n### `LegitResult`\n\nA [discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) on the `isValid` field. Checking `result.isValid` narrows the type automatically.\n\n```typescript\ntype LegitResult =\n  | { isValid: true;  mxArray: MxRecord[] }\n  | { isValid: false; mxArray: null; mxRecordSetExists: boolean }\n```\n\n#### When `isValid` is `true`\n\n| Field | Type | Description |\n|---|---|---|\n| `isValid` | `true` | The domain has active mail server records |\n| `mxArray` | `MxRecord[]` | All MX records, sorted by priority ascending (lowest number = highest priority). Each record has `exchange: string` and `priority: number`. |\n\n\u003e **Note:** When the domain has no MX records but has an A record (RFC 5321 §5.1 fallback), `mxArray` contains a single synthetic entry `{ exchange: domain, priority: 0 }`.\n\n#### When `isValid` is `false`\n\n| Field | Type | Description |\n|---|---|---|\n| `isValid` | `false` | The domain cannot receive email |\n| `mxArray` | `null` | Always `null` |\n| `mxRecordSetExists` | `boolean` | `false` — domain does not exist in DNS at all, or exists but has no MX or A records. `true` — domain exists but has a null MX record (RFC 7505), meaning it explicitly opts out of receiving email. |\n\n---\n\n## Examples\n\n### Valid domain\n\n```typescript\nconst result = await legit('hello@gmail.com');\n// {\n//   isValid: true,\n//   mxArray: [\n//     { exchange: 'aspmx.l.google.com',      priority: 1  },\n//     { exchange: 'alt1.aspmx.l.google.com', priority: 5  },\n//     { exchange: 'alt2.aspmx.l.google.com', priority: 5  },\n//     { exchange: 'alt3.aspmx.l.google.com', priority: 10 },\n//     { exchange: 'alt4.aspmx.l.google.com', priority: 10 }\n//   ]\n// }\n```\n\n### Domain that does not exist\n\n```typescript\nconst result = await legit('user@definitelynotarealdomain99.com');\n// {\n//   isValid: false,\n//   mxArray: null,\n//   mxRecordSetExists: false\n// }\n```\n\n### Domain with null MX (explicitly rejects all email per RFC 7505)\n\n```typescript\nconst result = await legit('user@example-null-mx-domain.com');\n// {\n//   isValid: false,\n//   mxArray: null,\n//   mxRecordSetExists: true\n// }\n```\n\n### Structurally invalid address\n\n```typescript\n// These all reject with a TypeError — no DNS query is made\nawait legit('notanemail');          // no @ sign\nawait legit('');                    // empty string\nawait legit('.user@example.com');   // local part starts with dot\nawait legit('user..name@example.com'); // consecutive dots\nawait legit('a'.repeat(65) + '@example.com'); // local part \u003e 64 chars\n```\n\n---\n\n## Error handling\n\nThe promise resolves for all expected DNS outcomes. It only **rejects** for two reasons:\n\n| Rejection type | Cause | What to do |\n|---|---|---|\n| `TypeError` | Email address is structurally invalid | Caught before any DNS query. Treat as a user input error. |\n| `Error` | Unexpected DNS failure (`ESERVFAIL`, `ECONNREFUSED`) or timeout | Indicates an infrastructure problem. Consider retrying or degrading gracefully. |\n\n```typescript\nimport legit from 'legit';\n\ntry {\n  const result = await legit(emailFromUser, { timeout: 5000 });\n\n  if (result.isValid) {\n    await sendWelcomeEmail(emailFromUser);\n  } else {\n    showError('That email address does not appear to accept mail.');\n  }\n} catch (err) {\n  if (err instanceof TypeError) {\n    // Invalid format — tell the user\n    showError('Please enter a valid email address.');\n  } else {\n    // DNS infrastructure problem — fail open or retry\n    console.error('DNS validation failed, proceeding anyway:', err);\n    await sendWelcomeEmail(emailFromUser);\n  }\n}\n```\n\n---\n\n## Common patterns\n\n### Express / Fastify registration endpoint\n\n```typescript\nimport legit from 'legit';\n\napp.post('/register', async (req, res) =\u003e {\n  const { email } = req.body;\n\n  let result;\n  try {\n    result = await legit(email, { timeout: 5000 });\n  } catch (err) {\n    if (err instanceof TypeError) {\n      return res.status(400).json({ error: 'Invalid email address format.' });\n    }\n    // DNS unavailable — fail open so users can still register\n    return next(); // or proceed with registration\n  }\n\n  if (!result.isValid) {\n    return res.status(400).json({\n      error: 'Email domain does not accept mail. Please check the address and try again.',\n    });\n  }\n\n  // Proceed with registration\n});\n```\n\n### Validating a list of addresses\n\n```typescript\nimport legit from 'legit';\n\nasync function filterValidAddresses(emails: string[]): Promise\u003cstring[]\u003e {\n  const results = await Promise.allSettled(\n    emails.map(email =\u003e legit(email, { timeout: 5000 }))\n  );\n\n  return emails.filter((_, i) =\u003e {\n    const outcome = results[i];\n    return outcome.status === 'fulfilled' \u0026\u0026 outcome.value.isValid;\n  });\n}\n```\n\n### Checking which mail server will handle delivery\n\n```typescript\nimport legit from 'legit';\n\nconst result = await legit('user@example.com');\n\nif (result.isValid \u0026\u0026 result.mxArray.length \u003e 0) {\n  const primary = result.mxArray[0]; // always lowest priority number = highest preference\n  console.log(`Primary mail server: ${primary.exchange} (priority ${primary.priority})`);\n}\n```\n\n---\n\n## Internationalised domains (IDN)\n\nUnicode domain names are automatically normalised to their ACE/punycode form before the DNS lookup. You do not need to encode them yourself.\n\n```typescript\n// Both of these work the same way\nawait legit('user@münchen.de');      // unicode\nawait legit('user@xn--mnchen-3ya.de'); // punycode equivalent\n```\n\n---\n\n## Limitations\n\nUnderstanding what `legit` does *not* do is just as important as understanding what it does.\n\n**It does not confirm a specific mailbox exists.**\nMX records tell you the domain has a mail server. They say nothing about whether `alice@` or `bob@` that domain is a real, active account. The only way to confirm that without actually sending is SMTP probing (connecting to port 25 and issuing a `RCPT TO`), which is frequently blocked by firewalls, triggers spam filters, and is unreliable against catch-all configurations.\n\n**It does not validate the full local part.**\nBasic structural checks are applied (length ≤ 64 chars, no leading/trailing/consecutive dots in unquoted addresses), but the full RFC 5322 local-part grammar is not enforced. Addresses like `user name@example.com` (unquoted space) are not rejected.\n\n**It does not detect disposable email domains.**\nDomains like `mailinator.com` or `guerrillamail.com` have perfectly valid MX records. Blocking them requires a regularly-updated blocklist, which is a policy decision outside the scope of DNS validation.\n\n**It does not verify that MX hostnames resolve to IP addresses.**\nA domain can have MX records that point to hostnames with no A record (\"dangling MX\"). These would still return `isValid: true` even though mail delivery would fail.\n\n**Results can become stale.**\nDNS is cached and changes over time. A domain that passes validation today could remove its MX records tomorrow. For critical use cases, re-validate periodically rather than storing results indefinitely.\n\n---\n\n## Requirements\n\n- **Node.js ≥ 18.0.0**\n- No runtime dependencies\n\n---\n\n## License\n\nMIT © 2015–2026 Martyn Davies and contributors\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartyndavies%2Flegit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmartyndavies%2Flegit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartyndavies%2Flegit/lists"}