/** * Keencrest: find business emails for a list of websites (Google Sheets). * Uses the Website Contact Details Scraper by Data Gleaner on Apify. * https://apify.com/datagleaner/website-contact-details-scraper * * SETUP * 1. Sheet layout: row 1 is a header. Websites in column A from row 2 down. * Columns B (email), C (status) and D (reason) receive the results. * 2. Extensions > Apps Script > paste this file > Project Settings > Script properties: * add APIFY_TOKEN = your own Apify API token (Apify Console > Settings > Integrations). * 3. Reload the sheet. A "Keencrest" menu appears. Run "Find emails (selected rows or all)". * * COST: $0.002 per site where an email is found, nothing for sites without one. * MAX_USD below is a hard cap per batch. Batches of BATCH sites keep each request short. */ const BATCH = 100; // sites per request const MAX_USD = 1; // maxTotalChargeUsd per request const ACTOR = "datagleaner~website-contact-details-scraper"; function onOpen() { SpreadsheetApp.getUi().createMenu("Keencrest") .addItem("Find emails (selected rows or all)", "enrichEmails") .addToUi(); } function toDomain_(s) { return String(s).trim().replace(/^https?:\/\//i, "").replace(/^www\./i, "").split("/")[0].toLowerCase(); } function enrichEmails() { const token = PropertiesService.getScriptProperties().getProperty("APIFY_TOKEN"); if (!token) throw new Error("Add the script property APIFY_TOKEN first (see the setup notes at the top)."); const sheet = SpreadsheetApp.getActiveSheet(); const last = sheet.getLastRow(); if (last < 2) return; const sel = sheet.getActiveRange(); // Selected rows if the selection is inside the data, otherwise every row. let first = 2, count = last - 1; if (sel && sel.getRow() >= 2 && sel.getRow() <= last && sel.getNumRows() > 0 && sel.getNumRows() < last - 1) { first = sel.getRow(); count = Math.min(sel.getNumRows(), last - first + 1); } const raw = sheet.getRange(first, 1, count, 1).getValues().map(r => String(r[0]).trim()); const rows = []; // {idx, domain} raw.forEach((v, i) => { if (v) rows.push({ idx: i, domain: toDomain_(v) }); }); const out = raw.map(() => ["", "", ""]); for (let s = 0; s < rows.length; s += BATCH) { const chunk = rows.slice(s, s + BATCH); const domains = Array.from(new Set(chunk.map(r => r.domain))); const url = "https://api.apify.com/v2/acts/" + ACTOR + "/run-sync-get-dataset-items" + "?maxTotalChargeUsd=" + MAX_USD + "&fields=domain,bestEmail,status,failureReason"; const res = UrlFetchApp.fetch(url, { method: "post", contentType: "application/json", headers: { Authorization: "Bearer " + token }, payload: JSON.stringify({ websites: domains, maxPagesPerSite: 4 }), muteHttpExceptions: true }); if (res.getResponseCode() !== 201) throw new Error(res.getResponseCode() + " " + res.getContentText().slice(0, 300)); // Output order is not the input order: join on domain. const byDomain = {}; JSON.parse(res.getContentText()).forEach(i => { byDomain[i.domain] = i; }); chunk.forEach(r => { const i = byDomain[r.domain] || {}; out[r.idx] = [i.bestEmail || "", i.status || "missing", i.failureReason || ""]; }); } sheet.getRange(first, 2, count, 3).setValues(out); }