1
2
3
4
5
6
7
8
9import http from 'node:http';
10import { Actor, log } from 'apify';
11import { validateEmail } from './validator.js';
12
13const PPE_EVENT = 'email-clean';
14
15
16
17
18
19
20function normalizeItems(items) {
21 return items.map((item) => {
22 if (typeof item === 'string') {
23 return item;
24 }
25 if (item && typeof item === 'object' && 'input' in item) {
26 return String(item.input ?? '');
27 }
28 return String(item ?? '');
29 });
30}
31
32await Actor.init();
33
34if (Actor.config.get('metaOrigin') === 'STANDBY') {
35 startStandbyServer();
36} else {
37 await runBatchMode();
38 await Actor.exit();
39}
40
41
42
43
44
45function startStandbyServer() {
46 const port = Actor.config.get('containerPort');
47 const mxCache = new Map();
48
49 const server = http.createServer(async (req, res) => {
50
51 if (req.headers['x-apify-container-server-readiness-probe']) {
52 res.writeHead(200, { 'Content-Type': 'application/json' });
53 res.end(JSON.stringify({ status: 'ready' }));
54 return;
55 }
56
57
58 if (req.method !== 'GET') {
59 res.writeHead(405, { 'Content-Type': 'application/json' });
60 res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
61 return;
62 }
63
64
65 const reqUrl = new URL(req.url, `http://${req.headers.host}`);
66 if (!reqUrl.searchParams.get('input')) {
67 res.writeHead(200, { 'Content-Type': 'text/html' });
68 res.end(getLandingPageHtml());
69 return;
70 }
71
72 try {
73 const result = await handleStandbyRequest(reqUrl, mxCache);
74 await Actor.pushData(result);
75
76
77 try {
78 await Actor.charge({ eventName: PPE_EVENT, count: 1 });
79 } catch (chargeErr) {
80 console.warn(`PPE charge failed: ${chargeErr.message}`);
81 }
82
83 res.writeHead(200, { 'Content-Type': 'application/json' });
84 res.end(JSON.stringify(result));
85 } catch (err) {
86 const statusCode = err.statusCode || 500;
87 res.writeHead(statusCode, { 'Content-Type': 'application/json' });
88 res.end(JSON.stringify({ error: err.message }));
89 }
90 });
91
92 server.listen(port, () => {
93 console.log(`Superclean Emails Standby server listening on port ${port}`);
94 });
95}
96
97async function handleStandbyRequest(reqUrl, mxCache) {
98 const input = reqUrl.searchParams.get('input');
99
100 if (!input) {
101 const err = new Error("Provide an 'input' query parameter with the email address to validate.");
102 err.statusCode = 400;
103 throw err;
104 }
105
106 const result = await validateEmail(input, mxCache);
107
108 return {
109 id: 1,
110 input,
111 output: result.output,
112 isValid: result.isValid,
113 domain: result.domain,
114 hasMx: result.hasMx,
115 isDisposable: result.isDisposable,
116 isFreeProvider: result.isFreeProvider,
117 suggestedFix: result.suggestedFix,
118 confidence: Math.round(result.confidence * 100) / 100,
119 };
120}
121
122
123
124
125
126async function runBatchMode() {
127 const input = await Actor.getInput();
128
129 if (!input) {
130 await Actor.fail('No input provided. Example: {"items": ["user@example.com"]}');
131 return;
132 }
133
134 let { items: rawItems = [], item } = input;
135
136
137 if (typeof item === 'string' && item.trim()) {
138 if (!Array.isArray(rawItems)) rawItems = [];
139 rawItems = [item.trim(), ...rawItems];
140 }
141
142
143 if (typeof rawItems === 'string') rawItems = [rawItems];
144
145 if (!Array.isArray(rawItems) || rawItems.length === 0) {
146 await Actor.fail('Input required: provide "items" (array) or "item" (single string). Example: {"items": ["user@example.com"]}');
147 return;
148 }
149
150
151 const items = normalizeItems(rawItems);
152
153 log.info(`Validating ${items.length} email(s)`);
154
155 const mxCache = new Map();
156 const results = [];
157 let processed = 0;
158
159 for (let i = 0; i < items.length; i++) {
160 const original = items[i];
161 const id = i + 1;
162
163 try {
164 const result = await validateEmail(original, mxCache);
165
166 results.push({
167 id,
168 input: original,
169 output: result.output,
170 isValid: result.isValid,
171 domain: result.domain,
172 hasMx: result.hasMx,
173 isDisposable: result.isDisposable,
174 isFreeProvider: result.isFreeProvider,
175 suggestedFix: result.suggestedFix,
176 confidence: Math.round(result.confidence * 100) / 100,
177 });
178
179 processed++;
180
181 if (processed % 100 === 0) {
182 log.info(`Validated ${processed}/${items.length} emails`);
183 }
184 } catch (err) {
185 log.warning(`Failed to validate item ${id}: ${err.message}`);
186 results.push({
187 id,
188 input: original,
189 output: original,
190 isValid: false,
191 domain: null,
192 hasMx: null,
193 isDisposable: false,
194 isFreeProvider: false,
195 suggestedFix: null,
196 confidence: 0,
197 error: err.message,
198 });
199 }
200 }
201
202
203 let chargedCount = 0;
204 let chargeLimitReached = false;
205 for (const result of results) {
206 if (!chargeLimitReached) {
207 try {
208 const chargeResult = await Actor.charge({ eventName: PPE_EVENT, count: 1 });
209 chargedCount++;
210 if (chargeResult?.eventChargeLimitReached) {
211 chargeLimitReached = true;
212 console.log(`User spending limit reached after charging ${chargedCount}/${results.length} items`);
213 }
214 } catch (chargeErr) {
215 console.warn(`PPE charge failed: ${chargeErr.message}`);
216 }
217 }
218 }
219
220 await Actor.pushData(results);
221
222 log.info(`Completed validating ${results.length} email(s)`, { charged: chargedCount });
223
224
225 const kvStore = await Actor.openKeyValueStore();
226 await kvStore.setValue('summary', {
227 totalItems: results.length,
228 processedAt: new Date().toISOString(),
229 sampleResults: results.slice(0, 5),
230 });
231}
232
233
234
235
236
237function getLandingPageHtml() {
238 return `<!DOCTYPE html>
239<html lang="en">
240<head>
241<meta charset="utf-8">
242<meta name="viewport" content="width=device-width, initial-scale=1">
243<title>Superclean Emails — Superlative</title>
244<link rel="preconnect" href="https://fonts.googleapis.com">
245<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
246<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
247<style>
248:root{--color-bg:#0a0e14;--color-surface:#12171f;--color-border:#1e2632;--color-text:#e8eaed;--color-text-muted:#8b95a5;--color-accent:#4ade80;--color-accent-dim:rgba(74,222,128,0.15);--font-display:'Space Mono',monospace;--font-body:'DM Sans',sans-serif}
249*{margin:0;padding:0;box-sizing:border-box}
250body{font-family:var(--font-body);background:var(--color-bg);color:var(--color-text);line-height:1.6;min-height:100vh;overflow-x:hidden}
251a{color:var(--color-accent);text-decoration:none;transition:color .2s ease}
252a:hover{color:#6ee7a0}
253.grid-bg{position:fixed;top:0;left:0;width:100%;height:100%;background-image:linear-gradient(var(--color-border) 1px,transparent 1px),linear-gradient(90deg,var(--color-border) 1px,transparent 1px);background-size:60px 60px;opacity:.3;pointer-events:none;z-index:0}
254.accent-line{position:fixed;top:0;left:0;width:100%;height:2px;background:linear-gradient(90deg,transparent 0%,var(--color-accent) 50%,transparent 100%);animation:shimmer 3s ease-in-out infinite;z-index:10}
255@keyframes shimmer{0%,100%{transform:translateX(-100%)}50%{transform:translateX(100%)}}
256@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
257.container{position:relative;z-index:1;max-width:900px;margin:0 auto;padding:0 24px}
258header{padding:32px 0}
259.logo{font-family:var(--font-display);font-size:14px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text);display:flex;align-items:center;gap:10px;text-decoration:none}
260.logo:hover{color:var(--color-text)}
261.logo-mark{width:28px;height:28px;background:var(--color-accent);display:flex;align-items:center;justify-content:center}
262.logo-mark svg{width:16px;height:16px}
263.hero{padding:80px 0 60px;animation:fadeUp .8s ease-out}
264.hero-label{font-family:var(--font-display);font-size:12px;font-weight:400;letter-spacing:.15em;text-transform:uppercase;color:var(--color-accent);margin-bottom:24px;display:flex;align-items:center;gap:12px}
265.hero-label::before{content:'';width:24px;height:1px;background:var(--color-accent)}
266h1{font-family:var(--font-display);font-size:clamp(28px,5vw,44px);font-weight:700;line-height:1.15;margin-bottom:20px;letter-spacing:-.02em}
267h1 .highlight{color:var(--color-accent)}
268.hero-desc{font-size:18px;color:var(--color-text-muted);max-width:540px;margin-bottom:28px;line-height:1.7}
269.hero-link{font-family:var(--font-display);font-size:13px;letter-spacing:.05em;color:var(--color-accent);display:inline-flex;align-items:center;gap:6px}
270.section{padding:60px 0;border-top:1px solid var(--color-border);animation:fadeUp .8s ease-out .2s backwards}
271.section-label{font-family:var(--font-display);font-size:11px;font-weight:400;letter-spacing:.2em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:24px}
272h2{font-family:var(--font-display);font-size:20px;font-weight:700;margin-bottom:16px;letter-spacing:-.01em}
273p,li{font-size:15px;color:var(--color-text-muted);line-height:1.7}
274li{margin-bottom:6px}
275ul{margin:.5rem 0 1rem 1.25rem}
276strong{color:var(--color-text);font-weight:600}
277pre{background:var(--color-surface);color:var(--color-text);padding:16px 20px;overflow-x:auto;margin:12px 0 16px;font-family:var(--font-display);font-size:13px;line-height:1.6;border:1px solid var(--color-border)}
278code{font-family:var(--font-display);font-size:13px}
279p code,li code,td code{background:var(--color-accent-dim);color:var(--color-accent);padding:2px 6px}
280table{width:100%;border-collapse:collapse;margin:12px 0 16px;font-size:14px}
281th,td{border:1px solid var(--color-border);padding:10px 14px;text-align:left}
282th{background:var(--color-surface);color:var(--color-text-muted);font-family:var(--font-display);font-size:12px;font-weight:400;letter-spacing:.05em;text-transform:uppercase}
283td{color:var(--color-text-muted)}
284.badge{display:inline-block;background:var(--color-accent-dim);color:var(--color-accent);padding:3px 8px;font-family:var(--font-display);font-size:11px;font-weight:400;letter-spacing:.05em}
285.note{font-size:13px;color:var(--color-text-muted);margin-top:8px}
286.product-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;margin-top:24px}
287.product-card{background:var(--color-surface);border:1px solid var(--color-border);padding:24px;transition:all .3s ease;text-decoration:none;color:inherit;display:block}
288.product-card:hover{border-color:var(--color-accent);transform:translateY(-2px)}
289.product-name{font-family:var(--font-display);font-size:14px;font-weight:700;margin-bottom:8px;color:var(--color-text);display:flex;align-items:center;gap:8px}
290.product-name .status{font-size:9px;font-weight:400;padding:3px 6px;background:var(--color-accent-dim);color:var(--color-accent);letter-spacing:.1em}
291.product-desc{font-size:14px;color:var(--color-text-muted);line-height:1.5}
292footer{padding:48px 0;border-top:1px solid var(--color-border)}
293.footer-content{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:24px}
294.footer-text{font-size:13px;color:var(--color-text-muted)}
295.footer-links{display:flex;gap:24px}
296.footer-links a{font-family:var(--font-display);font-size:12px;color:var(--color-text-muted);letter-spacing:.05em}
297.footer-links a:hover{color:var(--color-accent)}
298@media(max-width:600px){.hero{padding:60px 0 40px}.section{padding:40px 0}.footer-content{flex-direction:column;align-items:flex-start}pre{font-size:12px;padding:12px 14px}}
299</style>
300</head>
301<body>
302<div class="grid-bg"></div>
303<div class="accent-line"></div>
304
305<div class="container">
306 <header>
307 <a href="https://apify.com/superlativetech?fpr=8e9l1" class="logo">
308 <div class="logo-mark">
309 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
310 <polyline points="4 17 10 11 4 5"></polyline>
311 <line x1="12" y1="19" x2="20" y2="19"></line>
312 </svg>
313 </div>
314 Superlative
315 </a>
316 </header>
317
318 <section class="hero">
319 <div class="hero-label">Superclean</div>
320 <h1>Superclean <span class="highlight">Emails</span></h1>
321 <p class="hero-desc">Instant email validation and cleaning. Detect typos, disposable domains, missing MX records, and free providers. Sub-second responses via Standby API.</p>
322 <a href="https://apify.com/superlativetech/superclean-emails?fpr=8e9l1" class="hero-link">View on Apify Store →</a>
323 </section>
324
325 <section class="section">
326 <div class="section-label">Features</div>
327 <h2>What this does</h2>
328 <ul>
329 <li><strong>Validates syntax</strong> — RFC 5322 compliant email format checking</li>
330 <li><strong>Fixes typos</strong> — Corrects common domain misspellings (gmial.com → gmail.com)</li>
331 <li><strong>Detects disposable emails</strong> — Flags temporary/throwaway email providers</li>
332 <li><strong>Flags free providers</strong> — Identifies Gmail, Yahoo, Hotmail, etc.</li>
333 <li><strong>Verifies MX records</strong> — Confirms the domain can actually receive email</li>
334 <li><strong>Batch mode</strong> — Validate hundreds of emails via the standard Actor run</li>
335 </ul>
336 </section>
337
338 <section class="section">
339 <div class="section-label">Getting Started</div>
340 <h2>Quick start</h2>
341 <p>Replace <code>YOUR_TOKEN</code> with your Apify API token.</p>
342
343 <p style="margin-top:16px"><strong>Validate an email:</strong></p>
344 <pre>curl "https://superlativetech--superclean-emails.apify.actor?token=YOUR_TOKEN&input=user@gmail.com"</pre>
345
346 <p><strong>Detect a typo:</strong></p>
347 <pre>curl "https://superlativetech--superclean-emails.apify.actor?token=YOUR_TOKEN&input=user@gmial.com"</pre>
348
349 <p><strong>Check a disposable email:</strong></p>
350 <pre>curl "https://superlativetech--superclean-emails.apify.actor?token=YOUR_TOKEN&input=test@mailinator.com"</pre>
351 </section>
352
353 <section class="section">
354 <div class="section-label">Reference</div>
355 <h2>Query parameters</h2>
356 <table>
357 <tr><th>Parameter</th><th>Required</th><th>Description</th></tr>
358 <tr><td><code>input</code></td><td>Yes</td><td>Email address to validate</td></tr>
359 <tr><td><code>token</code></td><td>Yes</td><td>Your Apify API token</td></tr>
360 </table>
361 </section>
362
363 <section class="section">
364 <div class="section-label">Response</div>
365 <h2>Response format</h2>
366 <pre>{
367 "id": 1,
368 "input": "USER@GMIAL.COM",
369 "output": "user@gmail.com",
370 "isValid": true,
371 "domain": "gmail.com",
372 "hasMx": true,
373 "isDisposable": false,
374 "isFreeProvider": true,
375 "suggestedFix": "gmial.com \\u2192 gmail.com",
376 "confidence": 0.85
377}</pre>
378 </section>
379
380 <section class="section">
381 <div class="section-label">Cost</div>
382 <h2>Pricing</h2>
383 <p>Pay-per-event pricing on the Apify platform:</p>
384 <table>
385 <tr><th>Tier</th><th>Emails</th><th>Per 1,000</th></tr>
386 <tr><td>Base</td><td>Any</td><td>$0.50</td></tr>
387 <tr><td><span class="badge">Bronze</span></td><td>100+</td><td>$0.45</td></tr>
388 <tr><td><span class="badge">Silver</span></td><td>1,000+</td><td>$0.40</td></tr>
389 <tr><td><span class="badge">Gold</span></td><td>10,000+</td><td>$0.35</td></tr>
390 </table>
391 </section>
392
393 <section class="section">
394 <div class="section-label">Auth</div>
395 <h2>Authentication</h2>
396 <p>Authenticate using either method:</p>
397 <ul>
398 <li>Query parameter: <code>?token=YOUR_APIFY_TOKEN</code></li>
399 <li>Header: <code>Authorization: Bearer YOUR_APIFY_TOKEN</code></li>
400 </ul>
401 <p style="margin-top:8px">Get your token from <a href="https://console.apify.com/settings/integrations">Apify Console → Settings → Integrations</a>.</p>
402 </section>
403
404 <section class="section">
405 <div class="section-label">Superlative</div>
406 <h2>More from Superlative</h2>
407 <div class="product-grid">
408 <a href="https://apify.com/superlativetech/superclean-company-names?fpr=8e9l1" class="product-card">
409 <div class="product-name">Company Names <span class="status">Live</span></div>
410 <p class="product-desc">Normalize company names for CRM and cold email outreach.</p>
411 </a>
412 <a href="https://apify.com/superlativetech/superclean-job-titles?fpr=8e9l1" class="product-card">
413 <div class="product-name">Job Titles <span class="status">Live</span></div>
414 <p class="product-desc">Standardize job titles for outreach sequences.</p>
415 </a>
416 <a href="https://apify.com/superlativetech/superclean-person-names?fpr=8e9l1" class="product-card">
417 <div class="product-name">Person Names <span class="status">Live</span></div>
418 <p class="product-desc">Clean and format person names for personalization.</p>
419 </a>
420 <a href="https://apify.com/superlativetech/superclean-product-names?fpr=8e9l1" class="product-card">
421 <div class="product-name">Product Names <span class="status">Live</span></div>
422 <p class="product-desc">Normalize product and brand names from exports.</p>
423 </a>
424 <a href="https://apify.com/superlativetech/superclean-places?fpr=8e9l1" class="product-card">
425 <div class="product-name">Places <span class="status">Live</span></div>
426 <p class="product-desc">Parse and normalize location data from lead exports.</p>
427 </a>
428 <a href="https://apify.com/superlativetech/superclean-urls?fpr=8e9l1" class="product-card">
429 <div class="product-name">URLs <span class="status">Live</span></div>
430 <p class="product-desc">Clean and normalize URLs from lead data.</p>
431 </a>
432 <a href="https://apify.com/superlativetech/superclean-phone-numbers?fpr=8e9l1" class="product-card">
433 <div class="product-name">Phone Numbers <span class="status">Live</span></div>
434 <p class="product-desc">Format and validate phone numbers.</p>
435 </a>
436 <a href="https://apify.com/superlativetech/dns-lookup?fpr=8e9l1" class="product-card">
437 <div class="product-name">Supernet DNS Lookup <span class="status">Live</span></div>
438 <p class="product-desc">Look up DNS records for any domain.</p>
439 </a>
440 </div>
441 </section>
442
443 <footer>
444 <div class="footer-content">
445 <p class="footer-text">© 2026 Superlative</p>
446 <div class="footer-links">
447 <a href="https://apify.com/superlativetech?fpr=8e9l1">Apify Store</a>
448 </div>
449 </div>
450 </footer>
451</div>
452
453</body>
454</html>`;
455}