{
  "data": {
    "id": "3Q4ZEsXfpGej1x96y",
    "actId": "9RiemV8vYMg3CcH3S",
    "userId": "IYgBDiXtHOijAaUel",
    "startedAt": "2026-10-31T05:57:27.997Z",
    "finishedAt": "2026-10-31T05:57:35.970Z",
    "status": "SUCCEEDED",
    "meta": {
      "origin": "CLI"
    },
    "stats": {
      "durationMillis": 7904,
      "runTimeSecs": 7.904,
      "computeUnits": 0.008782222222222223,
      "imageSizeBytes": 317698048
    },
    "options": {
      "useCache": true,
      "betaPackages": false,
      "memoryMbytes": 4096,
      "diskMbytes": 8192
    },
    "createdByOrganizationMemberUserId": "QYo53EZkJk0prePWY",
    "exitCode": 0,
    "inputSchema": "{\n  \"title\": \"Email Verifier Input\",\n  \"type\": \"object\",\n  \"schemaVersion\": 1,\n  \"properties\": {\n    \"emails\": {\n      \"title\": \"Email Addresses\",\n      \"type\": \"array\",\n      \"description\": \"One email address per item. You can paste a list here or use the text box below; if both are filled, the actor verifies both. Duplicates are removed case-insensitively.\",\n      \"editor\": \"stringList\",\n      \"prefill\": [\n        \"test@gmail.com\"\n      ],\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"emailsText\": {\n      \"title\": \"Email Addresses (text)\",\n      \"type\": \"string\",\n      \"description\": \"Paste a list separated by new lines, commas, semicolons or spaces. Useful for CSV exports and email lists.\",\n      \"editor\": \"textarea\"\n    },\n    \"datasetId\": {\n      \"title\": \"Read emails from an existing dataset\",\n      \"type\": \"string\",\n      \"description\": \"Optional Apify dataset ID. The actor reads one email per item from datasetEmailField, verifies those addresses, and can carry the original columns into each record.\",\n      \"editor\": \"textfield\"\n    },\n    \"datasetEmailField\": {\n      \"title\": \"Dataset email field\",\n      \"type\": \"string\",\n      \"description\": \"Column name that holds the email address when datasetId is set.\",\n      \"default\": \"email\",\n      \"prefill\": \"email\",\n      \"editor\": \"textfield\"\n    },\n    \"includeSourceFields\": {\n      \"title\": \"Carry source dataset columns\",\n      \"type\": \"boolean\",\n      \"description\": \"When reading from a dataset, include the original row in sourceData so you can join the verification result back to your CRM export.\",\n      \"default\": false,\n      \"prefill\": false\n    },\n    \"verificationDepth\": {\n      \"title\": \"Verification Depth\",\n      \"type\": \"string\",\n      \"description\": \"Basic checks syntax, MX records and the A/AAAA fallback. Standard adds disposable, role-based, free-provider and domain-health checks. Deep adds a best-effort SMTP mailbox probe and catch-all detection.\",\n      \"editor\": \"select\",\n      \"default\": \"standard\",\n      \"prefill\": \"basic\",\n      \"enum\": [\n        \"basic\",\n        \"standard\",\n        \"deep\"\n      ],\n      \"enumTitles\": [\n        \"Basic (syntax + MX)\",\n        \"Standard (+ disposable, role, free provider, domain health)\",\n        \"Deep (+ SMTP mailbox probe and catch-all)\"\n      ]\n    },\n    \"concurrency\": {\n      \"title\": \"Concurrency\",\n      \"type\": \"integer\",\n      \"description\": \"How many addresses are checked in parallel. Higher is faster, but mail servers may rate-limit SMTP probes.\",\n      \"editor\": \"number\",\n      \"default\": 10,\n      \"prefill\": 10,\n      \"minimum\": 1,\n      \"maximum\": 50\n    },\n    \"maxEmails\": {\n      \"title\": \"Maximum Emails\",\n      \"type\": \"integer\",\n      \"description\": \"Upper bound on unique addresses to verify in one run. Anything beyond this is skipped and reported in the run summary.\",\n      \"editor\": \"number\",\n      \"default\": 10000,\n      \"prefill\": 10000,\n      \"minimum\": 1,\n      \"maximum\": 100000\n    },\n    \"maxEmailsPerDomain\": {\n      \"title\": \"Maximum Emails per Domain\",\n      \"type\": \"integer\",\n      \"description\": \"Cap on addresses verified for the same domain. Extra addresses get an 'unknown' record explaining the cap instead of hammering one mail server.\",\n      \"editor\": \"number\",\n      \"default\": 100,\n      \"prefill\": 100,\n      \"minimum\": 1,\n      \"maximum\": 10000\n    },\n    \"smtpTimeout\": {\n      \"title\": \"SMTP Timeout (seconds)\",\n      \"type\": \"number\",\n      \"description\": \"Connect and response timeout for each SMTP probe. Use a lower value for fast scans and a higher value for slow mail servers.\",\n      \"editor\": \"number\",\n      \"default\": 10,\n      \"prefill\": 10,\n      \"minimum\": 1,\n      \"maximum\": 60\n    },\n    \"maxRetries\": {\n      \"title\": \"Max Retries\",\n      \"type\": \"integer\",\n      \"description\": \"Extra attempts for temporary DNS, connection and SMTP 4xx failures, using exponential backoff.\",\n      \"editor\": \"number\",\n      \"default\": 2,\n      \"prefill\": 2,\n      \"minimum\": 0,\n      \"maximum\": 5\n    },\n    \"checkSmtp\": {\n      \"title\": \"Check SMTP Mailbox\",\n      \"type\": \"boolean\",\n      \"description\": \"Run the best-effort SMTP mailbox probe even at basic or standard depth. Deep depth already includes it.\",\n      \"editor\": \"checkbox\",\n      \"default\": false,\n      \"prefill\": false\n    },\n    \"checkCatchAll\": {\n      \"title\": \"Detect Catch-all Domains\",\n      \"type\": \"boolean\",\n      \"description\": \"Probe a random address to detect catch-all domains. Implies SMTP probing. Deep depth already includes it.\",\n      \"editor\": \"checkbox\",\n      \"default\": false,\n      \"prefill\": false\n    },\n    \"includeDomainHealth\": {\n      \"title\": \"Include Domain Health\",\n      \"type\": \"boolean\",\n      \"description\": \"Add SPF, DMARC policy and common DKIM selector findings for each domain at standard or deep depth.\",\n      \"editor\": \"checkbox\",\n      \"default\": true,\n      \"prefill\": true\n    }\n  }\n}",
    "readme": "# Bulk Email Deliverability & SMTP Verification\n\nPaste an email list and get one clean record per address: syntax, MX and A/AAAA routing, disposable and role-based detection, typo suggestions, SPF/DMARC/DKIM findings, a best-effort SMTP mailbox probe, catch-all detection, a risk level and a recommended next action.\n\nUse it to clean a CRM export, screen sign-up addresses, scrub a lead list before outreach or periodically re-check stored contacts. No API keys, cookies or third-party verification service are required.\n\n## What it checks\n\n- **Syntax and normalization** - catches malformed addresses and returns the normalized form plus the local part and domain.\n- **Domain routing** - MX records sorted by priority, the primary mail host, the mail provider, and an A/AAAA fallback when a domain publishes no MX record. A null MX record (`0 .`) is treated as \"does not accept email\", not as a deliverability win.\n- **Typo suggestions** - flags likely mistakes such as `gmial.com`, `hotmail.con` or `outlok.com` with a corrected address in `didYouMean`.\n- **Disposable, role and free providers** - separate flags for throwaway domains, shared addresses such as `admin@` or `support@`, and consumer providers such as Gmail or Outlook.\n- **Alias signals** - `plusTag` for `+tag` subaddressing and `isAlias` for plus tags and Gmail dot aliases.\n- **Domain health** - whether SPF and DMARC records exist, the DMARC policy, and which common DKIM selectors publish a usable key.\n- **SMTP mailbox probe** - a best-effort `RCPT TO` check against the domain's mail server with the raw code and message.\n- **Catch-all detection** - tests a random address at the domain and reports how confident the result is.\n- **Risk and action** - a `riskLevel` (`low`, `medium`, `high`) and a `recommendedAction` (`send`, `send_with_caution`, `verify_manually`, `remove`).\n- **Batch summary** - a key-value store record named `SUMMARY` with totals, truncation and per-domain cap counts. It is separate from the dataset, so one dataset row still means one unique email address.\n\n## Output per email\n\n| Field | Type | Description |\n|---|---|---|\n| `email` | string | The normalized address, or the trimmed input when syntax fails. |\n| `normalizedEmail` | string | Normalized address (domain lowercased, IDN converted) when syntax passes. |\n| `localPart` | string | The part before `@`. |\n| `domain` | string | The domain that was checked. |\n| `verdict` | string | `valid`, `invalid`, `risky`, `disposable` or `unknown`. |\n| `confidence` | number | 0-100 confidence score for the verdict. |\n| `checks` | object | Only checks that actually ran: `syntax`, `mxRecord`, `aRecord`, `smtp`, `disposable`, `roleBased`, `freeProvider`, `catchAll`. |\n| `mxHost` | string | Highest-priority mail host. |\n| `mxRecords` | array | MX records as `{host, priority}` objects, sorted by priority. |\n| `mxProvider` | string | Recognized provider such as Google, Microsoft, Proton, Zoho or Fastmail. |\n| `aRecordFallback` | boolean | `true` when there is no MX record and mail routing falls back to A/AAAA. |\n| `domainExists` | boolean | Whether DNS shows the domain exists. |\n| `didYouMean` | string | Suggested correction for a likely typo in the domain. |\n| `isAlias` | boolean | `true` for plus-tag subaddressing or a Gmail dot alias. |\n| `plusTag` | boolean | `true` when the local part contains `+`. |\n| `domainHealth` | object | `spf` and `dmarc` booleans, `dmarcPolicy` when published, and `dkimSelectors` for common selectors with a usable key. |\n| `smtpAttempted` | boolean | `true` when an SMTP probe was attempted. |\n| `smtpVerifiable` | boolean | `true` when the server returned a mailbox-level RCPT response. |\n| `smtpCode` | number | Raw SMTP response code from the mailbox probe. |\n| `smtpMessage` | string | Raw SMTP response text (trimmed). |\n| `checks.catchAll` | boolean | Whether the domain accepted a random address. |\n| `catchAllConfidence` | number | 0-100 confidence in the catch-all result. A random-address acceptance scores 60 because some servers accept first and bounce later. |\n| `catchAllMethod` | string | Method used for catch-all detection (`smtp-random-recipient`). |\n| `riskLevel` | string | `low`, `medium` or `high`. |\n| `recommendedAction` | string | `send`, `send_with_caution`, `verify_manually` or `remove`. |\n| `reason` | string | Plain-language explanation when the verdict is not a clean `valid`. |\n| `verifiedAt` | string | ISO 8601 UTC timestamp. |\n\nFields that were not measured are omitted, not set to `null`. A false boolean is kept because it is a real answer.\n\n## Input\n\nAt least one of `emails`, `emailsText`, or `datasetId` is required. Apify's input-schema validator does not support `anyOf`, so this is enforced by the actor with a clear failure message.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `emails` | array of strings | - | Email addresses, one per item. |\n| `emailsText` | string | - | The same list as text, separated by new lines, commas, semicolons or spaces. |\n| `datasetId` | string | - | Optional existing dataset ID to read emails from. The dataset must be readable by the run. |\n| `datasetEmailField` | string | `email` | Column name that holds the address when `datasetId` is used. |\n| `includeSourceFields` | boolean | `false` | Carry the original dataset row in `sourceData`. |\n| `verificationDepth` | select | `standard` | `basic`, `standard` or `deep`. |\n| `concurrency` | integer | `10` | Parallel checks (1-50). |\n| `maxEmails` | integer | `10000` | Upper bound on unique addresses per run (1-100000). |\n| `maxEmailsPerDomain` | integer | `100` | Cap per domain; extra addresses get an explanatory `unknown` record. |\n| `smtpTimeout` | number | `10` | Per-probe SMTP timeout in seconds (1-60). |\n| `maxRetries` | integer | `2` | Extra attempts for temporary DNS, connection and SMTP 4xx failures (0-5). |\n| `checkSmtp` | boolean | `false` | Run SMTP probing at basic or standard depth. |\n| `checkCatchAll` | boolean | `false` | Detect catch-all domains; implies SMTP probing. |\n| `includeDomainHealth` | boolean | `true` | Add SPF, DMARC and common DKIM selector findings at standard or deep depth. |\n\n`emails` and `emailsText` are combined, then duplicates are removed case-insensitively. If both are empty the run fails with a clear message.\n\n### Example: clean a pasted list\n\n```json\n{\n  \"emailsText\": \"alice@company.com, bob@gmial.com\\ncarol@mailinator.com\",\n  \"verificationDepth\": \"standard\"\n}\n```\n\n### Example: deep check for a short list\n\n```json\n{\n  \"emails\": [\"prospect@target-company.com\"],\n  \"verificationDepth\": \"deep\",\n  \"checkSmtp\": true,\n  \"checkCatchAll\": true,\n  \"smtpTimeout\": 15\n}\n```\n\n### Example: large list with a per-domain cap\n\n```json\n{\n  \"emailsText\": \"...\",\n  \"verificationDepth\": \"basic\",\n  \"maxEmails\": 50000,\n  \"maxEmailsPerDomain\": 50,\n  \"concurrency\": 30\n}\n```\n\n## How to read the verdict\n\n- `valid` - syntax and domain routing are sound. If SMTP was enabled and answered, the mailbox was accepted. Without SMTP, this means the domain can receive mail, not that the individual mailbox exists.\n- `invalid` - broken syntax, a non-existent domain, a null MX record, no MX/A routing, or a definitive mailbox rejection such as SMTP 550.\n- `disposable` - the domain is on a bundled disposable-domain list.\n- `risky` - the domain accepts random addresses (catch-all), so an accepted mailbox proves little.\n- `unknown` - the run could not decide. The `reason` field says why, and no guess is made in either direction.\n\n## SMTP is best-effort, by design\n\nPort 25 is not guaranteed to be open from every network, and many large providers block probe traffic based on the sender IP's reputation, missing reverse DNS or greylisting. When the probe cannot reach the server or the server refuses the probe itself:\n\n- `smtpAttempted` stays `true`.\n- `smtpVerifiable` is `false`.\n- `checks.smtp` is omitted.\n- The verdict is `unknown` when SMTP was the deciding check.\n\nAn unreachable server is never turned into `valid` or `invalid`. This is deliberate: a blocked probe says nothing about the mailbox. If you need mailbox-level certainty for a list, run `deep` from a network with a reputable sending IP and expect some providers to remain unverifiable.\n\nCatch-all detection has the same honest limit: a server that accepts every address at SMTP time may still bounce later. That is why `catchAllConfidence` is 60 for an accepted random address, and the verdict becomes `risky` rather than a confident catch-all claim.\n\n## Domain health coverage\n\nSPF and DMARC are read from public DNS TXT records. DKIM is checked only on a small set of common selectors (`google`, `selector1`, `selector2`, `default`, `k1`, `s1`, `dkim`, `mail`). A missing `dkimSelectors` field means none of those selectors published a usable key; it does not prove the domain has no DKIM at all, because many domains use custom selectors. Domains with a wildcard TXT record are skipped for DKIM rather than reported as a false match.\n\n## Batch behavior and limits\n\n- Records are pushed in batches while the run is still working, so a long list does not sit entirely in memory.\n- A per-domain semaphore limits how many SMTP connections hit one mail server at once.\n- `maxEmails` truncates the input and the count is reported in the `SUMMARY` record and status message.\n- `maxEmailsPerDomain` does not silently drop addresses: each skipped address gets an `unknown` record with a reason.\n- No summary row is added to the dataset, so one row still means one unique email address.\n\n## FAQ\n\n**Is a `valid` verdict a guarantee that the mailbox exists?** No. At `standard` depth it means the address is well-formed and the domain can receive mail. Only a completed SMTP probe can speak to the mailbox, and even then a provider may accept and bounce later.\n\n**Why did a Gmail address come back `unknown` in deep mode?** Gmail and several other large providers reject probe traffic from many IP ranges. The actor reports that honestly instead of calling the address valid or invalid.\n\n**Why is `checks.smtp` missing instead of `false`?** `false` means the server explicitly rejected the mailbox. A missing value means the probe did not produce a mailbox-level answer (blocked port, policy rejection, timeout or temporary response).\n\n**What does `catchAllConfidence: 60` mean?** The domain accepted a random address. That is evidence of catch-all or accept-then-bounce behavior, but it is not proof that every address is deliverable.\n\n**Can I use this for very large lists?** Yes. Keep `concurrency` moderate, set `maxEmailsPerDomain` to protect individual mail servers, and use `basic` or `standard` depth unless mailbox-level probing is required.\n\n**Can I use a CSV or an existing dataset?** Paste the email column into `emailsText`, or set `datasetId` to a dataset that the actor can read (attach it to the run or make it readable by ID) and choose `datasetEmailField`. Turn on `includeSourceFields` to carry the original row into `sourceData`.\n\n**What are the remaining limits?** Outbound port 25 is unavailable from Apify cloud workers, so deep SMTP probes usually return `unknown`; 587/465 require authentication and are not a workaround. Provider reputation blocks, catch-all versus accept-then-bounce, catch-all mailbox resolution, arbitrary DKIM selectors, and A/AAAA parking domains cannot be resolved definitively without sending mail. Domain age is not shipped because RDAP has no service for several TLDs. These limits are reported honestly rather than converted into false verdicts.\n\n**Where is the batch summary?** Open the run's key-value store and read the record named `SUMMARY`. It contains input counts, truncation, per-domain cap counts, verdict totals and SMTP counters. No extra row is added to the dataset.\n\n**Does it need credentials?** No. MX, A/AAAA, SPF, DMARC, DKIM and SMTP checks all use public DNS and the recipient's own mail server. No account, API key or cookie is used.\n",
    "actorDefinition": {
      "actorSpecification": 1,
      "name": "bulk-email-deliverability-and-smtp-verification",
      "title": "Bulk Email Deliverability & SMTP Verification",
      "description": "Verify email lists with syntax, MX and A/AAAA fallback, disposable and role-based detection, typo suggestions, SPF/DMARC/DKIM domain health, best-effort SMTP mailbox probes, catch-all detection, risk scoring and a recommended action per address.",
      "version": "1.2",
      "meta": {
        "templateId": "python-start"
      },
      "input": {
        "title": "Email Verifier Input",
        "type": "object",
        "schemaVersion": 1,
        "properties": {
          "emails": {
            "title": "Email Addresses",
            "type": "array",
            "description": "One email address per item. You can paste a list here or use the text box below; if both are filled, the actor verifies both. Duplicates are removed case-insensitively.",
            "editor": "stringList",
            "prefill": [
              "test@gmail.com"
            ],
            "items": {
              "type": "string"
            }
          },
          "emailsText": {
            "title": "Email Addresses (text)",
            "type": "string",
            "description": "Paste a list separated by new lines, commas, semicolons or spaces. Useful for CSV exports and email lists.",
            "editor": "textarea"
          },
          "datasetId": {
            "title": "Read emails from an existing dataset",
            "type": "string",
            "description": "Optional Apify dataset ID. The actor reads one email per item from datasetEmailField, verifies those addresses, and can carry the original columns into each record.",
            "editor": "textfield"
          },
          "datasetEmailField": {
            "title": "Dataset email field",
            "type": "string",
            "description": "Column name that holds the email address when datasetId is set.",
            "default": "email",
            "prefill": "email",
            "editor": "textfield"
          },
          "includeSourceFields": {
            "title": "Carry source dataset columns",
            "type": "boolean",
            "description": "When reading from a dataset, include the original row in sourceData so you can join the verification result back to your CRM export.",
            "default": false,
            "prefill": false
          },
          "verificationDepth": {
            "title": "Verification Depth",
            "type": "string",
            "description": "Basic checks syntax, MX records and the A/AAAA fallback. Standard adds disposable, role-based, free-provider and domain-health checks. Deep adds a best-effort SMTP mailbox probe and catch-all detection.",
            "editor": "select",
            "default": "standard",
            "prefill": "basic",
            "enum": [
              "basic",
              "standard",
              "deep"
            ],
            "enumTitles": [
              "Basic (syntax + MX)",
              "Standard (+ disposable, role, free provider, domain health)",
              "Deep (+ SMTP mailbox probe and catch-all)"
            ]
          },
          "concurrency": {
            "title": "Concurrency",
            "type": "integer",
            "description": "How many addresses are checked in parallel. Higher is faster, but mail servers may rate-limit SMTP probes.",
            "editor": "number",
            "default": 10,
            "prefill": 10,
            "minimum": 1,
            "maximum": 50
          },
          "maxEmails": {
            "title": "Maximum Emails",
            "type": "integer",
            "description": "Upper bound on unique addresses to verify in one run. Anything beyond this is skipped and reported in the run summary.",
            "editor": "number",
            "default": 10000,
            "prefill": 10000,
            "minimum": 1,
            "maximum": 100000
          },
          "maxEmailsPerDomain": {
            "title": "Maximum Emails per Domain",
            "type": "integer",
            "description": "Cap on addresses verified for the same domain. Extra addresses get an 'unknown' record explaining the cap instead of hammering one mail server.",
            "editor": "number",
            "default": 100,
            "prefill": 100,
            "minimum": 1,
            "maximum": 10000
          },
          "smtpTimeout": {
            "title": "SMTP Timeout (seconds)",
            "type": "number",
            "description": "Connect and response timeout for each SMTP probe. Use a lower value for fast scans and a higher value for slow mail servers.",
            "editor": "number",
            "default": 10,
            "prefill": 10,
            "minimum": 1,
            "maximum": 60
          },
          "maxRetries": {
            "title": "Max Retries",
            "type": "integer",
            "description": "Extra attempts for temporary DNS, connection and SMTP 4xx failures, using exponential backoff.",
            "editor": "number",
            "default": 2,
            "prefill": 2,
            "minimum": 0,
            "maximum": 5
          },
          "checkSmtp": {
            "title": "Check SMTP Mailbox",
            "type": "boolean",
            "description": "Run the best-effort SMTP mailbox probe even at basic or standard depth. Deep depth already includes it.",
            "editor": "checkbox",
            "default": false,
            "prefill": false
          },
          "checkCatchAll": {
            "title": "Detect Catch-all Domains",
            "type": "boolean",
            "description": "Probe a random address to detect catch-all domains. Implies SMTP probing. Deep depth already includes it.",
            "editor": "checkbox",
            "default": false,
            "prefill": false
          },
          "includeDomainHealth": {
            "title": "Include Domain Health",
            "type": "boolean",
            "description": "Add SPF, DMARC policy and common DKIM selector findings for each domain at standard or deep depth.",
            "editor": "checkbox",
            "default": true,
            "prefill": true
          }
        }
      },
      "output": {
        "actorOutputSchemaVersion": 1,
        "title": "Bulk Email Deliverability & SMTP Verification Output",
        "description": "One record per unique email address with verdict, confidence, checks that actually ran, MX and provider details, typo suggestion, alias flags, SMTP response details, catch-all confidence, domain health and a recommended next action.",
        "type": "object",
        "properties": {
          "results": {
            "type": "string",
            "title": "Email Verification Results",
            "description": "Dataset containing one verification record per unique email address, with only the fields that were actually measured.",
            "template": "{{links.apiDefaultDatasetUrl}}/items"
          },
          "summary": {
            "type": "string",
            "title": "Batch Summary",
            "description": "Key-value store record named SUMMARY with input, truncation, per-domain cap and verdict totals for the run.",
            "template": "{{links.apiDefaultKeyValueStoreUrl}}/records/SUMMARY"
          }
        }
      },
      "dockerfile": "../Dockerfile",
      "storages": {
        "dataset": {
          "actorSpecification": 1,
          "fields": {},
          "views": {
            "overview": {
              "title": "Overview",
              "transformation": {
                "fields": [
                  "email",
                  "verdict",
                  "confidence",
                  "riskLevel",
                  "recommendedAction",
                  "mxProvider",
                  "didYouMean",
                  "reason"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "email": {
                    "label": "Email",
                    "format": "text"
                  },
                  "verdict": {
                    "label": "Verdict",
                    "format": "text"
                  },
                  "confidence": {
                    "label": "Confidence",
                    "format": "number"
                  },
                  "riskLevel": {
                    "label": "Risk",
                    "format": "text"
                  },
                  "recommendedAction": {
                    "label": "Action",
                    "format": "text"
                  },
                  "mxProvider": {
                    "label": "MX Provider",
                    "format": "text"
                  },
                  "didYouMean": {
                    "label": "Did You Mean",
                    "format": "text"
                  },
                  "reason": {
                    "label": "Reason",
                    "format": "text"
                  }
                }
              }
            },
            "detailed": {
              "title": "Detailed",
              "transformation": {
                "fields": [
                  "email",
                  "normalizedEmail",
                  "sourceData",
                  "verdict",
                  "confidence",
                  "riskLevel",
                  "recommendedAction",
                  "checks",
                  "mxHost",
                  "mxRecords",
                  "mxProvider",
                  "aRecordFallback",
                  "domainExists",
                  "isAlias",
                  "plusTag",
                  "didYouMean",
                  "smtpAttempted",
                  "smtpVerifiable",
                  "smtpCode",
                  "smtpMessage",
                  "catchAllConfidence",
                  "catchAllMethod",
                  "domainHealth",
                  "reason",
                  "verifiedAt"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "email": {
                    "label": "Email",
                    "format": "text"
                  },
                  "normalizedEmail": {
                    "label": "Normalized Email",
                    "format": "text"
                  },
                  "verdict": {
                    "label": "Verdict",
                    "format": "text"
                  },
                  "confidence": {
                    "label": "Confidence",
                    "format": "number"
                  },
                  "riskLevel": {
                    "label": "Risk",
                    "format": "text"
                  },
                  "recommendedAction": {
                    "label": "Action",
                    "format": "text"
                  },
                  "checks": {
                    "label": "Checks",
                    "format": "object"
                  },
                  "mxHost": {
                    "label": "MX Host",
                    "format": "text"
                  },
                  "mxRecords": {
                    "label": "MX Records",
                    "format": "array"
                  },
                  "mxProvider": {
                    "label": "MX Provider",
                    "format": "text"
                  },
                  "aRecordFallback": {
                    "label": "A/AAAA Fallback",
                    "format": "boolean"
                  },
                  "domainExists": {
                    "label": "Domain Exists",
                    "format": "boolean"
                  },
                  "isAlias": {
                    "label": "Alias",
                    "format": "boolean"
                  },
                  "plusTag": {
                    "label": "Plus Tag",
                    "format": "boolean"
                  },
                  "didYouMean": {
                    "label": "Did You Mean",
                    "format": "text"
                  },
                  "smtpAttempted": {
                    "label": "SMTP Attempted",
                    "format": "boolean"
                  },
                  "smtpVerifiable": {
                    "label": "SMTP Verifiable",
                    "format": "boolean"
                  },
                  "smtpCode": {
                    "label": "SMTP Code",
                    "format": "number"
                  },
                  "smtpMessage": {
                    "label": "SMTP Message",
                    "format": "text"
                  },
                  "catchAllConfidence": {
                    "label": "Catch-all Confidence",
                    "format": "number"
                  },
                  "catchAllMethod": {
                    "label": "Catch-all Method",
                    "format": "text"
                  },
                  "domainHealth": {
                    "label": "Domain Health",
                    "format": "object"
                  },
                  "reason": {
                    "label": "Reason",
                    "format": "text"
                  },
                  "verifiedAt": {
                    "label": "Verified At",
                    "format": "text"
                  }
                }
              }
            }
          }
        }
      },
      "readme": "# Bulk Email Deliverability & SMTP Verification\n\nPaste an email list and get one clean record per address: syntax, MX and A/AAAA routing, disposable and role-based detection, typo suggestions, SPF/DMARC/DKIM findings, a best-effort SMTP mailbox probe, catch-all detection, a risk level and a recommended next action.\n\nUse it to clean a CRM export, screen sign-up addresses, scrub a lead list before outreach or periodically re-check stored contacts. No API keys, cookies or third-party verification service are required.\n\n## What it checks\n\n- **Syntax and normalization** - catches malformed addresses and returns the normalized form plus the local part and domain.\n- **Domain routing** - MX records sorted by priority, the primary mail host, the mail provider, and an A/AAAA fallback when a domain publishes no MX record. A null MX record (`0 .`) is treated as \"does not accept email\", not as a deliverability win.\n- **Typo suggestions** - flags likely mistakes such as `gmial.com`, `hotmail.con` or `outlok.com` with a corrected address in `didYouMean`.\n- **Disposable, role and free providers** - separate flags for throwaway domains, shared addresses such as `admin@` or `support@`, and consumer providers such as Gmail or Outlook.\n- **Alias signals** - `plusTag` for `+tag` subaddressing and `isAlias` for plus tags and Gmail dot aliases.\n- **Domain health** - whether SPF and DMARC records exist, the DMARC policy, and which common DKIM selectors publish a usable key.\n- **SMTP mailbox probe** - a best-effort `RCPT TO` check against the domain's mail server with the raw code and message.\n- **Catch-all detection** - tests a random address at the domain and reports how confident the result is.\n- **Risk and action** - a `riskLevel` (`low`, `medium`, `high`) and a `recommendedAction` (`send`, `send_with_caution`, `verify_manually`, `remove`).\n- **Batch summary** - a key-value store record named `SUMMARY` with totals, truncation and per-domain cap counts. It is separate from the dataset, so one dataset row still means one unique email address.\n\n## Output per email\n\n| Field | Type | Description |\n|---|---|---|\n| `email` | string | The normalized address, or the trimmed input when syntax fails. |\n| `normalizedEmail` | string | Normalized address (domain lowercased, IDN converted) when syntax passes. |\n| `localPart` | string | The part before `@`. |\n| `domain` | string | The domain that was checked. |\n| `verdict` | string | `valid`, `invalid`, `risky`, `disposable` or `unknown`. |\n| `confidence` | number | 0-100 confidence score for the verdict. |\n| `checks` | object | Only checks that actually ran: `syntax`, `mxRecord`, `aRecord`, `smtp`, `disposable`, `roleBased`, `freeProvider`, `catchAll`. |\n| `mxHost` | string | Highest-priority mail host. |\n| `mxRecords` | array | MX records as `{host, priority}` objects, sorted by priority. |\n| `mxProvider` | string | Recognized provider such as Google, Microsoft, Proton, Zoho or Fastmail. |\n| `aRecordFallback` | boolean | `true` when there is no MX record and mail routing falls back to A/AAAA. |\n| `domainExists` | boolean | Whether DNS shows the domain exists. |\n| `didYouMean` | string | Suggested correction for a likely typo in the domain. |\n| `isAlias` | boolean | `true` for plus-tag subaddressing or a Gmail dot alias. |\n| `plusTag` | boolean | `true` when the local part contains `+`. |\n| `domainHealth` | object | `spf` and `dmarc` booleans, `dmarcPolicy` when published, and `dkimSelectors` for common selectors with a usable key. |\n| `smtpAttempted` | boolean | `true` when an SMTP probe was attempted. |\n| `smtpVerifiable` | boolean | `true` when the server returned a mailbox-level RCPT response. |\n| `smtpCode` | number | Raw SMTP response code from the mailbox probe. |\n| `smtpMessage` | string | Raw SMTP response text (trimmed). |\n| `checks.catchAll` | boolean | Whether the domain accepted a random address. |\n| `catchAllConfidence` | number | 0-100 confidence in the catch-all result. A random-address acceptance scores 60 because some servers accept first and bounce later. |\n| `catchAllMethod` | string | Method used for catch-all detection (`smtp-random-recipient`). |\n| `riskLevel` | string | `low`, `medium` or `high`. |\n| `recommendedAction` | string | `send`, `send_with_caution`, `verify_manually` or `remove`. |\n| `reason` | string | Plain-language explanation when the verdict is not a clean `valid`. |\n| `verifiedAt` | string | ISO 8601 UTC timestamp. |\n\nFields that were not measured are omitted, not set to `null`. A false boolean is kept because it is a real answer.\n\n## Input\n\nAt least one of `emails`, `emailsText`, or `datasetId` is required. Apify's input-schema validator does not support `anyOf`, so this is enforced by the actor with a clear failure message.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `emails` | array of strings | - | Email addresses, one per item. |\n| `emailsText` | string | - | The same list as text, separated by new lines, commas, semicolons or spaces. |\n| `datasetId` | string | - | Optional existing dataset ID to read emails from. The dataset must be readable by the run. |\n| `datasetEmailField` | string | `email` | Column name that holds the address when `datasetId` is used. |\n| `includeSourceFields` | boolean | `false` | Carry the original dataset row in `sourceData`. |\n| `verificationDepth` | select | `standard` | `basic`, `standard` or `deep`. |\n| `concurrency` | integer | `10` | Parallel checks (1-50). |\n| `maxEmails` | integer | `10000` | Upper bound on unique addresses per run (1-100000). |\n| `maxEmailsPerDomain` | integer | `100` | Cap per domain; extra addresses get an explanatory `unknown` record. |\n| `smtpTimeout` | number | `10` | Per-probe SMTP timeout in seconds (1-60). |\n| `maxRetries` | integer | `2` | Extra attempts for temporary DNS, connection and SMTP 4xx failures (0-5). |\n| `checkSmtp` | boolean | `false` | Run SMTP probing at basic or standard depth. |\n| `checkCatchAll` | boolean | `false` | Detect catch-all domains; implies SMTP probing. |\n| `includeDomainHealth` | boolean | `true` | Add SPF, DMARC and common DKIM selector findings at standard or deep depth. |\n\n`emails` and `emailsText` are combined, then duplicates are removed case-insensitively. If both are empty the run fails with a clear message.\n\n### Example: clean a pasted list\n\n```json\n{\n  \"emailsText\": \"alice@company.com, bob@gmial.com\\ncarol@mailinator.com\",\n  \"verificationDepth\": \"standard\"\n}\n```\n\n### Example: deep check for a short list\n\n```json\n{\n  \"emails\": [\"prospect@target-company.com\"],\n  \"verificationDepth\": \"deep\",\n  \"checkSmtp\": true,\n  \"checkCatchAll\": true,\n  \"smtpTimeout\": 15\n}\n```\n\n### Example: large list with a per-domain cap\n\n```json\n{\n  \"emailsText\": \"...\",\n  \"verificationDepth\": \"basic\",\n  \"maxEmails\": 50000,\n  \"maxEmailsPerDomain\": 50,\n  \"concurrency\": 30\n}\n```\n\n## How to read the verdict\n\n- `valid` - syntax and domain routing are sound. If SMTP was enabled and answered, the mailbox was accepted. Without SMTP, this means the domain can receive mail, not that the individual mailbox exists.\n- `invalid` - broken syntax, a non-existent domain, a null MX record, no MX/A routing, or a definitive mailbox rejection such as SMTP 550.\n- `disposable` - the domain is on a bundled disposable-domain list.\n- `risky` - the domain accepts random addresses (catch-all), so an accepted mailbox proves little.\n- `unknown` - the run could not decide. The `reason` field says why, and no guess is made in either direction.\n\n## SMTP is best-effort, by design\n\nPort 25 is not guaranteed to be open from every network, and many large providers block probe traffic based on the sender IP's reputation, missing reverse DNS or greylisting. When the probe cannot reach the server or the server refuses the probe itself:\n\n- `smtpAttempted` stays `true`.\n- `smtpVerifiable` is `false`.\n- `checks.smtp` is omitted.\n- The verdict is `unknown` when SMTP was the deciding check.\n\nAn unreachable server is never turned into `valid` or `invalid`. This is deliberate: a blocked probe says nothing about the mailbox. If you need mailbox-level certainty for a list, run `deep` from a network with a reputable sending IP and expect some providers to remain unverifiable.\n\nCatch-all detection has the same honest limit: a server that accepts every address at SMTP time may still bounce later. That is why `catchAllConfidence` is 60 for an accepted random address, and the verdict becomes `risky` rather than a confident catch-all claim.\n\n## Domain health coverage\n\nSPF and DMARC are read from public DNS TXT records. DKIM is checked only on a small set of common selectors (`google`, `selector1`, `selector2`, `default`, `k1`, `s1`, `dkim`, `mail`). A missing `dkimSelectors` field means none of those selectors published a usable key; it does not prove the domain has no DKIM at all, because many domains use custom selectors. Domains with a wildcard TXT record are skipped for DKIM rather than reported as a false match.\n\n## Batch behavior and limits\n\n- Records are pushed in batches while the run is still working, so a long list does not sit entirely in memory.\n- A per-domain semaphore limits how many SMTP connections hit one mail server at once.\n- `maxEmails` truncates the input and the count is reported in the `SUMMARY` record and status message.\n- `maxEmailsPerDomain` does not silently drop addresses: each skipped address gets an `unknown` record with a reason.\n- No summary row is added to the dataset, so one row still means one unique email address.\n\n## FAQ\n\n**Is a `valid` verdict a guarantee that the mailbox exists?** No. At `standard` depth it means the address is well-formed and the domain can receive mail. Only a completed SMTP probe can speak to the mailbox, and even then a provider may accept and bounce later.\n\n**Why did a Gmail address come back `unknown` in deep mode?** Gmail and several other large providers reject probe traffic from many IP ranges. The actor reports that honestly instead of calling the address valid or invalid.\n\n**Why is `checks.smtp` missing instead of `false`?** `false` means the server explicitly rejected the mailbox. A missing value means the probe did not produce a mailbox-level answer (blocked port, policy rejection, timeout or temporary response).\n\n**What does `catchAllConfidence: 60` mean?** The domain accepted a random address. That is evidence of catch-all or accept-then-bounce behavior, but it is not proof that every address is deliverable.\n\n**Can I use this for very large lists?** Yes. Keep `concurrency` moderate, set `maxEmailsPerDomain` to protect individual mail servers, and use `basic` or `standard` depth unless mailbox-level probing is required.\n\n**Can I use a CSV or an existing dataset?** Paste the email column into `emailsText`, or set `datasetId` to a dataset that the actor can read (attach it to the run or make it readable by ID) and choose `datasetEmailField`. Turn on `includeSourceFields` to carry the original row into `sourceData`.\n\n**What are the remaining limits?** Outbound port 25 is unavailable from Apify cloud workers, so deep SMTP probes usually return `unknown`; 587/465 require authentication and are not a workaround. Provider reputation blocks, catch-all versus accept-then-bounce, catch-all mailbox resolution, arbitrary DKIM selectors, and A/AAAA parking domains cannot be resolved definitively without sending mail. Domain age is not shipped because RDAP has no service for several TLDs. These limits are reported honestly rather than converted into false verdicts.\n\n**Where is the batch summary?** Open the run's key-value store and read the record named `SUMMARY`. It contains input counts, truncation, per-domain cap counts, verdict totals and SMTP counters. No extra row is added to the dataset.\n\n**Does it need credentials?** No. MX, A/AAAA, SPF, DMARC, DKIM and SMTP checks all use public DNS and the recipient's own mail server. No account, API key or cookie is used.\n"
    },
    "buildNumber": "1.2.1",
    "imageDigest": "c64185f417353e9366bbd25f09e02391fbbb52d1b547a312318e4abba6bcec82",
    "actVersion": {
      "sourceType": "SOURCE_FILES",
      "buildTag": "latest",
      "versionNumber": "1.2"
    },
    "usage": {
      "ACTOR_COMPUTE_UNITS": 0.008782222222222223
    }
  }
}