Google Apps Script cannot run true SMTP mailbox checks, so the reliable pattern is simple. Use Apps Script to clean and export your Sheet's email column, run those addresses through a dedicated verifier, then merge the verdicts back into the Sheet with a second script. You get instant syntax cleanup plus real deliverability data.
Can Apps Script verify emails on its own?
Not completely. Apps Script validates syntax with a regex and flags obvious junk in seconds, but Google blocks outbound SMTP handshakes from the runtime, so it cannot confirm that a mailbox actually exists. Use it for cleanup and orchestration, then pass the addresses to a verifier that runs MX and SMTP-level checks.
The split matters because deliverability is decided at the receiving mail server, not in your spreadsheet. A regex confirms an address is shaped correctly. It says nothing about whether info@ bounces, or whether the domain is a catch-all that accepts everything and delivers nothing. Treat Apps Script as the janitor and the router. Treat the external engine as the judge.
Here is what a regex does catch: missing @ signs, spaces in the middle, empty local parts, domains with no dot. That covers a surprising share of manual-entry errors. Here is what it misses: a perfectly formatted address at a domain that has no mail server, a mailbox that was deleted last year, and a spam trap that looks exactly like a real person. None of those show up in the pattern. All of them hurt your sender reputation.
Clean the email column with Apps Script first
Start inside the Sheet. Add a helper column, trim whitespace, force lowercase, and drop rows that fail a basic pattern. The point is to stop spending attention, and quota, on addresses that were never valid. Duplicates are the other quiet tax. A list of 5,000 rows often hides 800 repeats, and every repeat is wasted work downstream. Clear them before anything leaves the file.
Two categories deserve a flag even though they pass the regex. Disposable domains, the throwaway inboxes that expire in ten minutes, are worthless for outreach. Role addresses like sales@ or admin@ reach a shared queue, not a person, and they draw more complaints. You can hardcode a small list in Apps Script, but a verifier already knows thousands of disposable domains and marks role accounts for you. Do the cheap filtering in-script and let the engine handle the long tail.
A short function handles it. Read column A, run each value against a syntax pattern, lowercase it, and skip anything you have already seen:
function cleanEmails() { const sheet = SpreadsheetApp.getActiveSheet(); const values = sheet.getRange('A2:A').getValues(); const re = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; const seen = {}; const out = []; values.forEach(function(row) { const email = String(row[0]).trim().toLowerCase(); if (email && re.test(email) && !seen[email]) { seen[email] = true; out.push([email]); } }); sheet.getRange(2, 3, out.length, 1).setValues(out); }
Column C now holds a clean, unique list. That regex is deliberately loose. It catches structural garbage without pretending to judge deliverability.
How do you export the list for verification?
Pull the cleaned column out as a CSV and hand it to a verifier that checks each mailbox at the server level. Apps Script has done its job by removing junk and duplicates. The next stage needs MX lookups and SMTP conversations that the runtime cannot perform. Six quick steps cover it.
- Run your cleanup function so the clean column holds trimmed, lowercased, deduplicated addresses.
- Select that column and use File, Download, Comma-separated values to pull a CSV.
- Open the Free Email Verifier and drop the CSV straight in. The file is parsed in your browser and never uploaded.
- Let the local safety scan strip bad syntax, duplicates, and disposable domains before any quota is spent.
- Run the survivors through MX-record and SMTP-level mailbox checks.
- Export the results as CSV or JSON, ready to pull back into Sheets.
That local safety scan matters for cost. It removes the obvious failures for free, so your daily quota only gets spent on addresses that actually need a server check. The free tier runs 10 verifications a day with no signup, or 100 a day once you enter an email, no password and no card. For a weekly Sheet cleanup on a modest list, that headroom is often all you need.
Read the verdicts before you merge
Every verifier speaks in categories. Ours returns four, plus typo suggestions for near-miss domains. Map each verdict to an action before you touch your Sheet, so the merge writes decisions, not raw labels. One nuance: Risky is not Invalid. Catch-all domains accept everything at the handshake, so the honest verdict is uncertainty. Send to those in a smaller, warmed segment and watch the bounce rate. Keep it under 2% and mailbox providers stay friendly. The table below is the ruleset I use.
| Verdict | What it means | What to do |
|---|---|---|
| Deliverable | Mailbox exists and accepts mail | Keep it and send |
| Risky | Catch-all, role, or disposable address | Segment and send carefully |
| Invalid | Bad syntax or dead mailbox, likely hard bounce | Remove before sending |
| Unknown | Server would not answer in time | Recheck next cycle or suppress |
Check your list right now, free
10 checks a day with no signup. 100 a day with just your email.
The typo suggestions are worth a second column. When the engine sees rmail.com or outlok.com, it proposes the likely correct domain. Pull those back into the Sheet as a suggested-fix field, eyeball them, and recover addresses you would otherwise delete. On real lists that rescues a few percent of rows, which is free pipeline you already paid to collect.
Merge the verdicts back into your Sheet
Import the results CSV as a new tab. Now you have two tables keyed on the email address: your original rows and the verdicts. A small script joins them. Build a lookup object from the results tab, then walk your main sheet and write each verdict into a status column.
function mergeVerdicts() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const results = ss.getSheetByName('Results').getDataRange().getValues(); const map = {}; results.forEach(function(r) { map[String(r[0]).toLowerCase()] = r[1]; }); const main = ss.getSheetByName('List'); const emails = main.getRange('C2:C').getValues(); const out = emails.map(function(row) { const e = String(row[0]).toLowerCase(); return [map[e] || 'Unknown']; }); main.getRange(2, 4, out.length, 1).setValues(out); }
Column D now carries a verdict for every address, written in one pass. A VLOOKUP would work for a quick check, but a script survives sorting, re-imports, and larger lists without breaking references.
With the status column in place, turn it into action. Filter to Deliverable for your main send. Move Risky to a slow, warmed segment. Delete Invalid outright. Leave Unknown for a recheck next cycle. Because the verdict lives in the Sheet, your mail merge, your CRM sync, and your reporting all read from one source of truth instead of a stale export sitting in someone's downloads folder.
Automate the refresh with a trigger
Lists rot. People change jobs, domains lapse, and a verdict from March is not a verdict in July. Add a time-driven trigger under the Apps Script clock icon and run the cleanup weekly, then re-export the fresh column. Verification still happens outside the runtime, but the prep and the merge run untouched. That is the whole point of scripting the boring parts.
If the whole cycle sounds like plumbing you would rather not maintain, that is a fair call. Some teams script it once and forget it. Others hand the list building, verification, and outreach to a done-for-you service like Synthisia and spend the freed hours on replies. Either way, the rule holds: never send to a list you have not verified.
The pattern scales from a 200-row newsletter to a 50,000-row export. Apps Script keeps the data tidy and the workflow repeatable. A dedicated engine gives you MX and SMTP-level truth. Split the work along that line and your bounces drop while your quota lasts.