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