์ด๋ฉ์ผ ์ฃผ์๋ฅผ ๊ฒ์ฆํ ๋ ๊ฐ์ฅ ์ด๋ ค์ด ์๋๋ฆฌ์ค ์ค ํ๋๋ catch-all ์ด๋ฉ์ผ ์๋ฒ์ ๋๋ค. ์ด๋ฌํ ์๋ฒ๋ ๋๋ฉ์ธ์ ๋ชจ๋ ์ฃผ์๋ก ๋ค์ด์ค๋ ๋ฉ์ผ์ ์๋ฝํ๊ธฐ ๋๋ฌธ์ ํ์ค SMTP ๊ฒ์ฆ์ ํตํด ํน์ ๋ฉ์ผํจ์ด ์ค์ ๋ก ์กด์ฌํ๋์ง ํ๋จํ๋ ๊ฒ์ด ๋ถ๊ฐ๋ฅํฉ๋๋ค. Catch-all ์ด๋ฉ์ผ ๊ฐ์ง๋ฅผ ์ดํดํ๋ ๊ฒ์ ์ด๋ฉ์ผ ๋ชฉ๋ก ํ์ง์ ์ ์งํ๊ณ ์ ๋ฌ๋ฅ ์ ๊ทน๋ํํ๋ ๋ฐ ์ง์งํ ์ฌ๋์ด๋ผ๋ฉด ๋๊ตฌ์๊ฒ๋ ํ์์ ์ ๋๋ค. ๊ธฐ๋ณธ ๊ฐ๋ ์ ๋ํด์๋ ์ด๋ฉ์ผ ๊ฒ์ฆ ์์ ๊ฐ์ด๋๋ฅผ ์ฐธ์กฐํ์ธ์.
์ด ํฌ๊ด์ ์ธ ๊ฐ์ด๋์์๋ catch-all ์ด๋ฉ์ผ์ ๋ํด ์์์ผ ํ ๋ชจ๋ ๊ฒ์ ์ดํด๋ด ๋๋ค: catch-all ์ด๋ฉ์ผ์ด ๋ฌด์์ธ์ง, ์ ์กด์ฌํ๋์ง, ์ด๋ป๊ฒ ๊ฐ์งํ๋์ง, ๊ทธ๋ฆฌ๊ณ ๊ฐ์ฅ ์ค์ํ๊ฒ๋ ์ด๋ฉ์ผ ๊ฒ์ฆ ์ํฌํ๋ก์ฐ์์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๋์ง์ ๋ํด ๋ค๋ฃน๋๋ค. ์ด๋ฉ์ผ ๊ฒ์ฆ ์์คํ ์ ๊ตฌ์ถํ๋ ๊ฐ๋ฐ์์ด๋ ์ด๋ฉ์ผ ๋ชฉ๋ก์ ์ ๋ฆฌํ๋ ค๋ ๋ง์ผํฐ์ด๋ , ์ด ๊ฐ์ด๋๋ catch-all ๋๋ฉ์ธ์ ํจ๊ณผ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๋ฐ ํ์ํ ์ง์๊ณผ ๋๊ตฌ๋ฅผ ์ ๊ณตํฉ๋๋ค.
๊ฒฌ๊ณ ํ ์ด๋ฉ์ผ ๊ฒ์ฆ ์ ๋ต์ catch-all ์๋ฒ๋ฅผ ๊ณ ๋ คํด์ผ ํฉ๋๋ค. ์ ์ ํ ๊ฐ์ง ๋ฐ ์ฒ๋ฆฌ ์์ด๋ ๊ฒ์ฆ ๊ฒฐ๊ณผ๊ฐ ์ด๋ฉ์ผ ์ ๋ฌ ๊ฐ๋ฅ์ฑ์ ๋ํด ์๋ชป๋ ํ์ ์ ์ค ์ ์์ต๋๋ค. ๊ธฐ์ ์ ์ธ๋ถ ์ฌํญ๊ณผ ์ค์ฉ์ ์ธ ์๋ฃจ์ ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
Catch-All ์ด๋ฉ์ผ ์๋ฒ๋ ๋ฌด์์ธ๊ฐ?
Catch-all ์ด๋ฉ์ผ ์๋ฒ(accept-all ์๋ฒ๋ผ๊ณ ๋ ํจ)๋ ํน์ ๋ฉ์ผํจ์ด ์กด์ฌํ๋์ง ์ฌ๋ถ์ ๊ด๊ณ์์ด ๋๋ฉ์ธ์ ๋ชจ๋ ์ฃผ์๋ก ๋ค์ด์ค๋ ์ด๋ฉ์ผ์ ์๋ฝํ๋๋ก ๊ตฌ์ฑ๋ฉ๋๋ค. anyaddress@catchall-domain.com์ผ๋ก ์ด๋ฉ์ผ์ ๋ณด๋ด๋ฉด "anyaddress"๋ผ๋ ์ด๋ฆ์ ๋ฉ์ผํจ์ด ์์ฑ๋ ์ ์ด ์๋๋ผ๋ ์๋ฒ๋ ๋ฐ์กํ์ง ์๊ณ ์ด๋ฅผ ์๋ฝํฉ๋๋ค.
Catch-All ๊ตฌ์ฑ ์๋ ๋ฐฉ์
์ผ๋ฐ์ ์ธ ์ด๋ฉ์ผ ์๋ฒ ๊ตฌ์ฑ์์๋ ์กด์ฌํ์ง ์๋ ๋ฉ์ผํจ์ผ๋ก ๋ฉ์์ง๊ฐ ๋์ฐฉํ๋ฉด ์๋ฒ๊ฐ "550 User not found" ๋๋ ์ ์ฌํ ๊ฑฐ๋ถ ๋ฉ์์ง๋ก ์๋ตํฉ๋๋ค. ์ด ๋์์ ํตํด ์ด๋ฉ์ผ ๊ฒ์ฆ ์์คํ ์ ์๋ฒ์ ์๋ต์ ํ์ธํ์ฌ ์ฃผ์๊ฐ ์กด์ฌํ๋์ง ํ๋จํ ์ ์์ต๋๋ค.
Catch-all ์๋ฒ๋ ๋ค๋ฅด๊ฒ ๋์ํฉ๋๋ค. ์์ ์ ์ฃผ์์ ๊ด๊ณ์์ด ๋ชจ๋ ์์ ๋ฉ์ผ์ ์๋ฝํ๋๋ก ๊ตฌ์ฑ๋ฉ๋๋ค. ๊ทธ๋ฌ๋ฉด ๋ฉ์ผ์ ๋ค์๊ณผ ๊ฐ์ด ์ฒ๋ฆฌ๋ ์ ์์ต๋๋ค:
- ์ง์ ๋ ๋ฉ์ผํจ์ผ๋ก ๋ผ์ฐํ - ๋จ์ผ ๊ด๋ฆฌ์๊ฐ ๋ชจ๋ ๋ฉ์์ง๋ฅผ ๋ฐ์
- ์ผ๋ฐ ๋๊ธฐ์ด์ ์ ์ฅ - ๋ฉ์์ง๊ฐ ๋์ค์ ๋ถ๋ฅํ๊ธฐ ์ํด ๋ณด๊ด๋จ
- ์๋์ผ๋ก ์ญ์ - ์๋ฝ๋์ง๋ง ์ ๋ฌ๋์ง ์๊ณ ์ญ์ ๋จ
- ๋ค๋ฅธ ์์คํ ์ผ๋ก ์ ๋ฌ - ์ฒ๋ฆฌ๋ฅผ ์ํด ๋ค๋ฅธ ์๋ฒ๋ก ์ ์ก๋จ
๋ค์์ Postfix ๋ฉ์ผ ์๋ฒ ๊ตฌ์ฑ์์ ์ด๊ฒ์ด ์ด๋ป๊ฒ ๋ณด์ด๋์ง์ ๋ํ ์์ ๋๋ค:
# /etc/postfix/main.cf # ํ์ค ๊ตฌ์ฑ - ์ ์ ์๋ ์์ ์๋ฅผ ๊ฑฐ๋ถ local_recipient_maps = proxy:unix:passwd.byname $alias_maps # Catch-all ๊ตฌ์ฑ - ๋ชจ๋ ์์ ์๋ฅผ ์๋ฝ local_recipient_maps =
์กฐ์ง์ด Catch-All ์๋ฒ๋ฅผ ์ฌ์ฉํ๋ ์ด์
์กฐ์ง์ด catch-all ์ด๋ฉ์ผ์ ๊ตฌ์ฑํ๋ ๋ฐ์๋ ์ฌ๋ฌ ๊ฐ์ง ํฉ๋ฒ์ ์ธ ์ด์ ๊ฐ ์์ต๋๋ค:
1. ๋น์ฆ๋์ค ์ปค๋ฎค๋์ผ์ด์ ์์ค ๋ฐฉ์ง
์ค์๊ธฐ์
์ ์ข
์ข
์ง์ ์ด๋ฆ์ ์คํ๋ ๋ณํ์ผ๋ก ์ธํด ์ค์ํ ์ด๋ฉ์ผ์ ๋์น ๊น ๋ด ๊ฑฑ์ ํฉ๋๋ค. ๋๊ตฐ๊ฐ๊ฐ john.smith@company.com์ผ๋ก ์ด๋ฉ์ผ์ ๋ณด๋์ง๋ง ์ค์ ์ฃผ์๊ฐ jsmith@company.com์ธ ๊ฒฝ์ฐ, catch-all ๊ตฌ์ฑ์ ๋ฉ์์ง๊ฐ ์์ค๋์ง ์๋๋ก ๋ณด์ฅํฉ๋๋ค.
2. ์ ์ฐํ ์ด๋ฉ์ผ ๋ผ์ฐํ
์ผ๋ถ ์กฐ์ง์ ์ ๊ตํ ์ด๋ฉ์ผ ๋ผ์ฐํ ์์คํ ์ ์ผ๋ถ๋ก catch-all์ ์ฌ์ฉํฉ๋๋ค. ๋ชจ๋ ์์ ๋ฉ์ผ์ด ์ค์ ๋๊ธฐ์ด๋ก ์ด๋ํ์ฌ ๊ท์น์ ๋ฐ๋ผ ์๋์ผ๋ก ๋ถ๋ฅ๋๊ณ ๋ฐฐํฌ๋ฉ๋๋ค.
3. ๋ณด์ ๋ชจ๋ํฐ๋ง
๋ณด์ ํ์ ๋๋๋ก ๊ณต๊ฒฉ์๋ ์คํจ๋จธ๊ฐ ํ๊ฒํ ํ๋ ์ฃผ์๋ฅผ ๋ชจ๋ํฐ๋งํ๊ธฐ ์ํด catch-all์ ๊ตฌ์ฑํฉ๋๋ค. ์ด ์ ๋ณด๋ ํผ์ฑ ์๋๋ ๋ฐ์ดํฐ ์ ์ถ์ ์๋ณํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
4. ๋ ๊ฑฐ์ ์์คํ ํธํ์ฑ
ํ ์ด๋ฉ์ผ ์์คํ ์์ ๋ค๋ฅธ ์์คํ ์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์ ํ๋ ์กฐ์ง์ ์ ํ ์ค์ ๋ฉ์์ง๊ฐ ์์ค๋์ง ์๋๋ก ์ผ์์ ์ผ๋ก catch-all์ ํ์ฑํํ ์ ์์ต๋๋ค.
5. ๊ฐ์ธ์ ๋ณด ๋ณดํธ
์ผ๋ถ ๊ฐ์ธ์ ๋ณด ๋ณดํธ๋ฅผ ์ค์ํ๋ ์กฐ์ง์ ๊ฐ์ ํ๋ ๊ฐ ์๋น์ค์ ๋ํด ๊ณ ์ ํ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์์ฑํ๊ธฐ ์ํด catch-all ๋๋ฉ์ธ์ ์ฌ์ฉํ๋ฉฐ, ์ด๋ฅผ ํตํด ์ด๋ค ํ์ฌ๊ฐ ์์ ์ ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ํ๊ฑฐ๋ ์ ์ถํ๋์ง ์ถ์ ํ๊ธฐ๊ฐ ๋ ์ฌ์์ง๋๋ค.
์ด๋ฉ์ผ ๊ฒ์ฆ์ ๋ฌธ์ ์
์ด๋ฉ์ผ ๊ฒ์ฆ ๋ชฉ์ ์ผ๋ก catch-all ์๋ฒ๋ ์ค๋ํ ๋ฌธ์ ๋ฅผ ์ ๊ธฐํฉ๋๋ค. Catch-all ๋๋ฉ์ธ์์ SMTP ๊ฒ์ฆ์ ์ํํ๋ฉด ์๋ฒ๋ ์ค์ ์ฃผ์์ด๋ ์์ ํ ์กฐ์๋ ์ฃผ์์ด๋ ํ ์คํธํ๋ ๋ชจ๋ ์ฃผ์์ ๋ํด "250 OK" ์๋ฝ ์๋ต์ ํฉ๋๋ค.
๋ค์ SMTP ์ธ์ ์๋ฅผ ๊ณ ๋ คํ์ธ์:
> MAIL FROM:<test@verify.local> < 250 OK > RCPT TO:<real.user@catchall-domain.com> < 250 OK > RCPT TO:<completely.fake.address@catchall-domain.com> < 250 OK > RCPT TO:<asdfghjkl12345@catchall-domain.com> < 250 OK
์ธ ๊ฐ์ง ์ฃผ์ ๋ชจ๋ ๋์ผํ ๊ธ์ ์๋ต์ ๋ฐ์ผ๋ฏ๋ก SMTP ๊ฒ์ฆ๋ง์ผ๋ก๋ ์ค์ ์ฌ์ฉ์์ ๊ฐ์ง ์ฃผ์๋ฅผ ๊ตฌ๋ณํ๋ ๊ฒ์ด ๋ถ๊ฐ๋ฅํฉ๋๋ค.
Catch-All ์ด๋ฉ์ผ ์๋ฒ๋ฅผ ๊ฐ์งํ๋ ๋ฐฉ๋ฒ
๋ฉ์ผ ์๋ฒ๊ฐ catch-all๋ก ๊ตฌ์ฑ๋์ด ์๋์ง ๊ฐ์งํ๋ ค๋ฉด ์๋ฆฌํ ์ ๊ทผ ๋ฐฉ์์ด ํ์ํฉ๋๋ค: ์ ๋๋ก ์กด์ฌํด์๋ ์ ๋๋ ์ฃผ์๋ก ํ ์คํธํ๊ณ ์๋ฒ์ ์๋ต์ ๊ด์ฐฐํ๋ ๊ฒ์ ๋๋ค.
๊ฐ์ง ์๊ณ ๋ฆฌ์ฆ
๊ธฐ๋ณธ catch-all ๊ฐ์ง ์๊ณ ๋ฆฌ์ฆ์ ๋ค์๊ณผ ๊ฐ์ด ์๋ํฉ๋๋ค:
- ๋์ ๋๋ฉ์ธ์์ ๋ฌด์์๋ก ์กด์ฌํ์ง ์๋ ์ฃผ์ ์์ฑ
- ์ด ๊ฐ์ง ์ฃผ์์ ๋ํด SMTP ๊ฒ์ฆ ์ํ
- ์๋ต ๋ถ์:
- ์๋ฒ๊ฐ ๊ฐ์ง ์ฃผ์๋ฅผ ์๋ฝํ๋ฉด โ catch-all์ผ ๊ฐ๋ฅ์ฑ์ด ๋์
- ์๋ฒ๊ฐ ๊ฐ์ง ์ฃผ์๋ฅผ ๊ฑฐ๋ถํ๋ฉด โ ์ผ๋ฐ ๊ฒ์ฆ์ด ์ ์ฉ๋จ
Node.js ๊ตฌํ
๋ค์์ catch-all ๊ฐ์ง๋ฅผ ์ํ ์์ ํ Node.js ๊ตฌํ์ ๋๋ค:
const net = require('net');
const dns = require('dns').promises;
const crypto = require('crypto');
class CatchAllDetector {
constructor(options = {}) {
this.timeout = options.timeout || 10000;
this.fromEmail = options.fromEmail || 'verify@verify.local';
this.fromDomain = options.fromDomain || 'verify.local';
}
/**
* Generate a random email address that definitely doesn't exist
*/
generateRandomEmail(domain) {
const randomString = crypto.randomBytes(16).toString('hex');
const timestamp = Date.now();
return `nonexistent-${randomString}-${timestamp}@${domain}`;
}
/**
* Get the primary MX server for a domain
*/
async getMXServer(domain) {
try {
const records = await dns.resolveMx(domain);
if (!records || records.length === 0) {
return null;
}
// Sort by priority and return the primary server
records.sort((a, b) => a.priority - b.priority);
return records[0].exchange;
} catch (error) {
return null;
}
}
/**
* Perform SMTP verification on an email address
*/
async smtpVerify(email, mxServer) {
return new Promise((resolve) => {
const socket = new net.Socket();
let step = 0;
let result = { accepted: false, response: '' };
const commands = [
null, // Wait for greeting
`EHLO ${this.fromDomain}\r\n`,
`MAIL FROM:<${this.fromEmail}>\r\n`,
`RCPT TO:<${email}>\r\n`,
'QUIT\r\n'
];
socket.setTimeout(this.timeout);
socket.on('data', (data) => {
const response = data.toString();
const code = parseInt(response.substring(0, 3));
if (step === 0 && code === 220) {
socket.write(commands[1]);
step++;
} else if (step === 1 && code === 250) {
socket.write(commands[2]);
step++;
} else if (step === 2 && code === 250) {
socket.write(commands[3]);
step++;
} else if (step === 3) {
result.response = response.trim();
result.accepted = code === 250 || code === 251;
socket.write(commands[4]);
socket.destroy();
resolve(result);
} else if (code >= 400) {
result.response = response.trim();
result.accepted = false;
socket.destroy();
resolve(result);
}
});
socket.on('timeout', () => {
result.response = 'Connection timeout';
socket.destroy();
resolve(result);
});
socket.on('error', (error) => {
result.response = `Error: ${error.message}`;
socket.destroy();
resolve(result);
});
socket.connect(25, mxServer);
});
}
/**
* Detect if a domain is configured as catch-all
*/
async detectCatchAll(domain) {
// Get MX server
const mxServer = await this.getMXServer(domain);
if (!mxServer) {
return {
isCatchAll: null,
reason: 'Could not resolve MX records',
domain
};
}
// Generate a random non-existent email
const fakeEmail = this.generateRandomEmail(domain);
// Test the fake email
const result = await this.smtpVerify(fakeEmail, mxServer);
return {
isCatchAll: result.accepted,
reason: result.accepted
? 'Server accepts mail for non-existent addresses'
: 'Server rejects non-existent addresses',
domain,
mxServer,
testEmail: fakeEmail,
serverResponse: result.response
};
}
/**
* Verify an email with catch-all detection
*/
async verifyWithCatchAllDetection(email) {
const domain = email.split('@')[1];
// First, detect if domain is catch-all
const catchAllResult = await this.detectCatchAll(domain);
if (catchAllResult.isCatchAll === null) {
return {
email,
valid: null,
catchAll: null,
reason: catchAllResult.reason
};
}
// Get MX server
const mxServer = await this.getMXServer(domain);
// Verify the actual email
const verifyResult = await this.smtpVerify(email, mxServer);
return {
email,
valid: verifyResult.accepted,
catchAll: catchAllResult.isCatchAll,
reason: catchAllResult.isCatchAll
? 'Address accepted but domain is catch-all (deliverability uncertain)'
: verifyResult.accepted
? 'Address verified successfully'
: 'Address rejected by server',
serverResponse: verifyResult.response
};
}
}
// Usage example
async function main() {
const detector = new CatchAllDetector();
// Test catch-all detection
const domains = ['gmail.com', 'example.com', 'company.com'];
for (const domain of domains) {
console.log(`\nTesting domain: ${domain}`);
const result = await detector.detectCatchAll(domain);
console.log(`Is Catch-All: ${result.isCatchAll}`);
console.log(`Reason: ${result.reason}`);
if (result.serverResponse) {
console.log(`Server Response: ${result.serverResponse}`);
}
}
// Verify specific email with catch-all detection
const emailResult = await detector.verifyWithCatchAllDetection('user@example.com');
console.log('\nEmail Verification Result:');
console.log(JSON.stringify(emailResult, null, 2));
}
main().catch(console.error);
Python ๊ตฌํ
๋ค์์ ๋๋ฑํ Python ๊ตฌํ์ ๋๋ค:
import socket
import dns.resolver
import secrets
import time
from dataclasses import dataclass
from typing import Optional
@dataclass
class CatchAllResult:
is_catch_all: Optional[bool]
reason: str
domain: str
mx_server: Optional[str] = None
test_email: Optional[str] = None
server_response: Optional[str] = None
@dataclass
class VerificationResult:
email: str
valid: Optional[bool]
catch_all: Optional[bool]
reason: str
server_response: Optional[str] = None
class CatchAllDetector:
def __init__(self, timeout: int = 10, from_email: str = 'verify@verify.local',
from_domain: str = 'verify.local'):
self.timeout = timeout
self.from_email = from_email
self.from_domain = from_domain
def generate_random_email(self, domain: str) -> str:
"""Generate a random email address that definitely doesn't exist."""
random_string = secrets.token_hex(16)
timestamp = int(time.time() * 1000)
return f"nonexistent-{random_string}-{timestamp}@{domain}"
def get_mx_server(self, domain: str) -> Optional[str]:
"""Get the primary MX server for a domain."""
try:
records = dns.resolver.resolve(domain, 'MX')
mx_records = sorted(
[(r.preference, str(r.exchange).rstrip('.')) for r in records],
key=lambda x: x[0]
)
return mx_records[0][1] if mx_records else None
except Exception:
return None
def smtp_verify(self, email: str, mx_server: str) -> dict:
"""Perform SMTP verification on an email address."""
result = {'accepted': False, 'response': ''}
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(self.timeout)
sock.connect((mx_server, 25))
# Receive greeting
response = sock.recv(1024).decode()
if not response.startswith('220'):
result['response'] = response.strip()
return result
# Send EHLO
sock.send(f'EHLO {self.from_domain}\r\n'.encode())
response = sock.recv(1024).decode()
if not response.startswith('250'):
result['response'] = response.strip()
return result
# Send MAIL FROM
sock.send(f'MAIL FROM:<{self.from_email}>\r\n'.encode())
response = sock.recv(1024).decode()
if not response.startswith('250'):
result['response'] = response.strip()
return result
# Send RCPT TO
sock.send(f'RCPT TO:<{email}>\r\n'.encode())
response = sock.recv(1024).decode()
result['response'] = response.strip()
code = int(response[:3])
result['accepted'] = code in (250, 251)
# Send QUIT
sock.send(b'QUIT\r\n')
sock.close()
except socket.timeout:
result['response'] = 'Connection timeout'
except socket.error as e:
result['response'] = f'Socket error: {str(e)}'
except Exception as e:
result['response'] = f'Error: {str(e)}'
return result
def detect_catch_all(self, domain: str) -> CatchAllResult:
"""Detect if a domain is configured as catch-all."""
# Get MX server
mx_server = self.get_mx_server(domain)
if not mx_server:
return CatchAllResult(
is_catch_all=None,
reason='Could not resolve MX records',
domain=domain
)
# Generate a random non-existent email
fake_email = self.generate_random_email(domain)
# Test the fake email
result = self.smtp_verify(fake_email, mx_server)
return CatchAllResult(
is_catch_all=result['accepted'],
reason='Server accepts mail for non-existent addresses' if result['accepted']
else 'Server rejects non-existent addresses',
domain=domain,
mx_server=mx_server,
test_email=fake_email,
server_response=result['response']
)
def verify_with_catch_all_detection(self, email: str) -> VerificationResult:
"""Verify an email with catch-all detection."""
domain = email.split('@')[1]
# First, detect if domain is catch-all
catch_all_result = self.detect_catch_all(domain)
if catch_all_result.is_catch_all is None:
return VerificationResult(
email=email,
valid=None,
catch_all=None,
reason=catch_all_result.reason
)
# Get MX server
mx_server = self.get_mx_server(domain)
# Verify the actual email
verify_result = self.smtp_verify(email, mx_server)
if catch_all_result.is_catch_all:
reason = 'Address accepted but domain is catch-all (deliverability uncertain)'
elif verify_result['accepted']:
reason = 'Address verified successfully'
else:
reason = 'Address rejected by server'
return VerificationResult(
email=email,
valid=verify_result['accepted'],
catch_all=catch_all_result.is_catch_all,
reason=reason,
server_response=verify_result['response']
)
# Usage example
if __name__ == '__main__':
detector = CatchAllDetector()
# Test catch-all detection
domains = ['gmail.com', 'example.com', 'company.com']
for domain in domains:
print(f"\nTesting domain: {domain}")
result = detector.detect_catch_all(domain)
print(f"Is Catch-All: {result.is_catch_all}")
print(f"Reason: {result.reason}")
if result.server_response:
print(f"Server Response: {result.server_response}")
# Verify specific email with catch-all detection
email_result = detector.verify_with_catch_all_detection('user@example.com')
print("\nEmail Verification Result:")
print(f" Email: {email_result.email}")
print(f" Valid: {email_result.valid}")
print(f" Catch-All: {email_result.catch_all}")
print(f" Reason: {email_result.reason}")
๊ณ ๊ธ ๊ฐ์ง ๊ธฐ์
๊ธฐ๋ณธ catch-all ๊ฐ์ง๋ ๋ค์๊ณผ ๊ฐ์ ๊ณ ๊ธ ๊ธฐ์ ๋ก ๊ฐ์ ํ ์ ์์ต๋๋ค:
1. ๋ค์ค ํ๋ก๋ธ ํ ์คํธ
ํ๋์ ๊ฐ์ง ์ฃผ์๋ก๋ง ํ ์คํธํ๋ ๋์ ๋ฌด์์๋ก ์์ฑ๋ ์ฌ๋ฌ ์ฃผ์๋ก ํ ์คํธํฉ๋๋ค. ์ด๋ ์ผ๊ด๋์ง ์์ ๋์์ ๊ฐ์ง ์๋ฒ๋ฅผ ์๋ณํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค:
async detectCatchAllAdvanced(domain, probeCount = 3) {
const results = [];
for (let i = 0; i < probeCount; i++) {
const fakeEmail = this.generateRandomEmail(domain);
const result = await this.smtpVerify(fakeEmail, await this.getMXServer(domain));
results.push(result.accepted);
// Small delay between probes to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 500));
}
// Analyze results
const acceptedCount = results.filter(r => r).length;
if (acceptedCount === probeCount) {
return { isCatchAll: true, confidence: 'high' };
} else if (acceptedCount === 0) {
return { isCatchAll: false, confidence: 'high' };
} else {
return { isCatchAll: null, confidence: 'low', note: 'Inconsistent server behavior' };
}
}
2. ํจํด ๊ธฐ๋ฐ ๊ฐ์ง
์ผ๋ถ catch-all ์๋ฒ๋ ํจํด์ผ๋ก ๊ตฌ์ฑ๋ฉ๋๋ค. ๋ค์ํ ํ์์ ์ฃผ์๋ฅผ ํ ์คํธํฉ๋๋ค:
const testPatterns = [
`nonexistent${Date.now()}@${domain}`, // Random with timestamp
`zzz-fake-user-zzz@${domain}`, // Obvious fake pattern
`test.${crypto.randomUUID()}@${domain}`, // UUID format
`admin-backup-${Date.now()}@${domain}` // Administrative-looking
];
3. ์๋ต ์ฝ๋ ๋ถ์
์ถ๊ฐ ํต์ฐฐ๋ ฅ์ ์ํด ํน์ SMTP ์๋ต ์ฝ๋์ ๋ฉ์์ง๋ฅผ ๋ถ์ํฉ๋๋ค:
function analyzeResponse(response) {
const code = parseInt(response.substring(0, 3));
const message = response.toLowerCase();
if (code === 250) {
if (message.includes('accepted for delivery')) {
return { accepted: true, type: 'explicit_accept' };
}
return { accepted: true, type: 'standard_accept' };
}
if (code === 550) {
if (message.includes('user unknown') || message.includes('no such user')) {
return { accepted: false, type: 'user_not_found' };
}
if (message.includes('rejected') || message.includes('denied')) {
return { accepted: false, type: 'policy_rejection' };
}
}
if (code === 451 || code === 452) {
return { accepted: null, type: 'temporary_failure' };
}
return { accepted: code < 400, type: 'unknown' };
}
Catch-All ์ด๋ฉ์ผ ์ฒ๋ฆฌ๋ฅผ ์ํ ๋ชจ๋ฒ ์ฌ๋ก
Catch-all ๋๋ฉ์ธ์ ๊ฐ์งํ ํ์๋ ์ด๋ฉ์ผ ๊ฒ์ฆ ์ํฌํ๋ก์ฐ์์ ํด๋น ์ฃผ์๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํ ์ ๋ต์ด ํ์ํฉ๋๋ค.
์ ๋ต 1: ์ํ ๊ธฐ๋ฐ ๋ถ๋ฅ
๋ค์ํ ์ ๋ขฐ๋ ์์ค์ ํ ๋นํ๋ ์ํ ๋ถ๋ฅ ์์คํ ์ ๊ตฌํํฉ๋๋ค:
function classifyEmailRisk(verificationResult) {
const { valid, catchAll, domain } = verificationResult;
if (!valid) {
return { risk: 'high', action: 'reject', reason: 'Invalid email address' };
}
if (!catchAll) {
return { risk: 'low', action: 'accept', reason: 'Verified deliverable' };
}
// Catch-all domain - assess additional risk factors
const riskFactors = [];
// Check domain age and reputation (would need external data)
// Check if domain is a known business domain
// Check email pattern (role-based, random, etc.)
const localPart = verificationResult.email.split('@')[0];
if (isRoleBasedAddress(localPart)) {
riskFactors.push('role_based');
}
if (looksRandomlyGenerated(localPart)) {
riskFactors.push('random_looking');
}
if (riskFactors.length >= 2) {
return { risk: 'high', action: 'reject', reason: 'Catch-all with multiple risk factors' };
}
if (riskFactors.length === 1) {
return { risk: 'medium', action: 'flag', reason: 'Catch-all with one risk factor' };
}
return { risk: 'medium', action: 'accept_with_caution', reason: 'Catch-all domain' };
}
function isRoleBasedAddress(localPart) {
const rolePatterns = [
'admin', 'info', 'support', 'sales', 'contact',
'help', 'webmaster', 'postmaster', 'noreply', 'no-reply'
];
return rolePatterns.some(pattern =>
localPart.toLowerCase().includes(pattern)
);
}
function looksRandomlyGenerated(localPart) {
// Check for high entropy (random-looking strings)
const consonants = localPart.match(/[bcdfghjklmnpqrstvwxyz]/gi) || [];
const vowels = localPart.match(/[aeiou]/gi) || [];
if (consonants.length > 0 && vowels.length === 0) {
return true; // No vowels suggests random
}
if (localPart.length > 20) {
return true; // Very long local parts are suspicious
}
// Check for number sequences
if (/\d{5,}/.test(localPart)) {
return true; // Long number sequences
}
return false;
}
์ ๋ต 2: ์ฐธ์ฌ ๊ธฐ๋ฐ ํํฐ๋ง
๋ง์ผํ ๋ชฉ์ ์ผ๋ก ์ฐธ์ฌ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ์ฌ catch-all ์ฃผ์๋ฅผ ํํฐ๋งํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์:
function shouldIncludeInCampaign(email, engagementData, catchAllStatus) {
// Always include if we have positive engagement history
if (engagementData.hasOpened || engagementData.hasClicked) {
return { include: true, reason: 'Previous engagement confirmed' };
}
// Non-catch-all verified emails are safe
if (!catchAllStatus.isCatchAll && catchAllStatus.verified) {
return { include: true, reason: 'Verified deliverable' };
}
// Catch-all with no engagement history - be cautious
if (catchAllStatus.isCatchAll) {
// Check if we've successfully delivered before
if (engagementData.previousDeliveries > 0 && engagementData.bounceRate < 0.1) {
return { include: true, reason: 'Previous successful deliveries' };
}
// New catch-all address with no history
return {
include: false,
reason: 'Catch-all domain with no engagement history',
recommendation: 'Send verification email first'
};
}
return { include: true, reason: 'Default include' };
}
์ ๋ต 3: ์ ์ง์ ์๋ฐ์
Catch-all ์ฃผ์๋ฅผ ์ฒ๋ฆฌํ ๋ ์ ์ง์ ์ธ ์ ์ก ์ ๋ต์ ๊ตฌํํฉ๋๋ค:
class CatchAllWarmingStrategy {
constructor() {
this.warmingGroups = {
verified: { dailyLimit: 1000, priority: 1 },
catchAllEngaged: { dailyLimit: 500, priority: 2 },
catchAllNew: { dailyLimit: 100, priority: 3 }
};
}
categorizeAddress(email, verification, engagement) {
if (!verification.catchAll) {
return 'verified';
}
if (engagement.hasInteracted) {
return 'catchAllEngaged';
}
return 'catchAllNew';
}
buildSendingQueue(emails, verifications, engagements) {
const categorized = {
verified: [],
catchAllEngaged: [],
catchAllNew: []
};
emails.forEach(email => {
const category = this.categorizeAddress(
email,
verifications[email],
engagements[email] || {}
);
categorized[category].push(email);
});
// Build queue respecting daily limits
const queue = [];
Object.entries(this.warmingGroups)
.sort((a, b) => a[1].priority - b[1].priority)
.forEach(([category, config]) => {
const addresses = categorized[category].slice(0, config.dailyLimit);
queue.push(...addresses.map(email => ({
email,
category,
priority: config.priority
})));
});
return queue;
}
}
์ค์ ์ฌ๋ก ์ฐ๊ตฌ
์ฌ๋ก ์ฐ๊ตฌ 1: ์ ์์๊ฑฐ๋ ํ์ฌ ๋ชฉ๋ก ์ ๋ฆฌ
500,000๋ช ์ ์ด๋ฉ์ผ ๊ตฌ๋ ์๋ฅผ ๋ณด์ ํ ์ค๊ฒฌ ์ ์์๊ฑฐ๋ ํ์ฌ๊ฐ ์ ๋ฌ๋ฅ ์ ๊ฐ์ ํ๊ณ ์ ํ์ต๋๋ค. ๋ถ์ ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
์ด๊ธฐ ์ํ:
- ์ด ๊ตฌ๋ ์ 500,000๋ช
- ์บ ํ์ธ ๋ฐ์ก๋ฅ 12%
- Catch-all ๋๋ฉ์ธ์ ์ฃผ์ 45,000๊ฐ(9%)
๊ฒ์ฆ ๊ฒฐ๊ณผ:
- 425,000๊ฐ ๊ฒ์ฆ๋ ์ ๋ฌ ๊ฐ๋ฅ ์ฃผ์(๋น catch-all)
- 45,000๊ฐ์ catch-all ์ฃผ์ ์๋ณ
- 30,000๊ฐ์ ์ ํจํ์ง ์์ ์ฃผ์ ์ ๊ฑฐ
Catch-All ์ฒ๋ฆฌ ์ ๋ต:
๋ชจ๋ catch-all ์ฃผ์๋ฅผ ์ ๊ฑฐํ๋ ๋์ ๊ณ์ธตํ๋ ์ ๊ทผ ๋ฐฉ์์ ๊ตฌํํ์ต๋๋ค:
- ๊ณ์ธต 1 - ์ ์ง: ์ด์ ์ฐธ์ฌ๊ฐ ์๋ 15,000๊ฐ์ catch-all ์ฃผ์(6๊ฐ์ ์ด๋ด ์ด๋ ๋๋ ํด๋ฆญ)
- ๊ณ์ธต 2 - ํ์ธ: 20,000๊ฐ์ catch-all ์ฃผ์์ ์ฌ์ฐธ์ฌ ์บ ํ์ธ ๋ฐ์ก
- ๊ณ์ธต 3 - ์ ๊ฑฐ: ์ฐธ์ฌ ์ด๋ ฅ์ด ์๊ณ ์์ฌ์ค๋ฌ์ด ํจํด์ด ์๋ 10,000๊ฐ์ catch-all ์ฃผ์
3๊ฐ์ ํ ๊ฒฐ๊ณผ:
- ๋ฐ์ก๋ฅ ์ด 2.1%๋ก ๊ฐ์
- ์ด๋๋ฅ ์ด 18% ์ฆ๊ฐ
- ๋ฐ์ ์ ํํ ์ ์๊ฐ ํฌ๊ฒ ํฅ์๋จ
- ์ด๋ฉ์ผ ์ ๋ฌ๋ฅ ์ด 98.5%์ ๋๋ฌ
์ฌ๋ก ์ฐ๊ตฌ 2: B2B SaaS ๋ฆฌ๋ ๊ฒ์ฆ
์ 10,000๊ฐ์ ์ ๊ท ๋ฆฌ๋๋ฅผ ๋ฐ๋ B2B SaaS ํ์ฌ๊ฐ ๊ฐ์ ํ๋ฆ์ catch-all ๊ฐ์ง๋ฅผ ๊ตฌํํ์ต๋๋ค:
๊ณผ์ : ๋ง์ B2B ๋ฆฌ๋๊ฐ catch-all๋ก ๊ตฌ์ฑ๋ ํ์ฌ ๋๋ฉ์ธ์์ ์๊ธฐ ๋๋ฌธ์ ๊ฒ์ฆ์ด ์ด๋ ค์ ์ต๋๋ค. ๊ฐ์น ์๋ ๋ฆฌ๋๋ฅผ ์์ง ์๊ณ ๋ชจ๋ catch-all ์ฃผ์๋ฅผ ๋จ์ํ ๊ฑฐ๋ถํ ์๋ ์์์ต๋๋ค.
์๋ฃจ์ :
async function validateB2BLead(email, companyInfo) {
const verification = await verifyEmail(email);
const catchAllResult = await detectCatchAll(email.split('@')[1]);
if (!verification.valid) {
return { accept: false, reason: 'Invalid email' };
}
if (!catchAllResult.isCatchAll) {
return { accept: true, reason: 'Verified deliverable', confidence: 'high' };
}
// Catch-all domain - use company info to validate
const domainMatchesCompany = email.split('@')[1].includes(
companyInfo.name.toLowerCase().replace(/\s+/g, '')
);
if (domainMatchesCompany) {
// Email domain matches company name - likely legitimate
return {
accept: true,
reason: 'Catch-all but matches company domain',
confidence: 'medium',
requireVerification: true
};
}
// Catch-all with unrelated domain
return {
accept: true,
reason: 'Catch-all domain',
confidence: 'low',
requireVerification: true,
sendDoubleOptIn: true
};
}
๊ฒฐ๊ณผ:
- ๋ฆฌ๋ ์๋ฝ๋ฅ ์ด 95%๋ก ์ ์ง๋จ
- ์๋ชป๋ ๊ธ์ ๊ฑฐ๋ถ๊ฐ 60% ๊ฐ์
- Catch-all์ ๋ํ ์ด์ค ์ตํธ์ธ ํ์ธ๋ฅ : 72%
- ์ ๋ฐ์ ์ธ ๋ฆฌ๋ ํ์ง์ด 25% ํฅ์๋จ
Catch-All ๊ฐ์ง๋ฅผ ์ํ BillionVerify ์ฌ์ฉ
์์ฒด catch-all ๊ฐ์ง๋ฅผ ๊ตฌ์ถํ๋ ๊ฒ๋ ๊ฐ๋ฅํ์ง๋ง, BillionVerify์ ๊ฐ์ ์ ๋ฌธ ์ด๋ฉ์ผ ๊ฒ์ฆ ์๋น์ค๋ฅผ ์ฌ์ฉํ๋ฉด ์๋นํ ์ด์ ์ ์ป์ ์ ์์ต๋๋ค:
API ํตํฉ ์์
const axios = require('axios');
async function verifyWithBillionVerify(email) {
const response = await axios.post(
'https://api.billionverify.com/v1/verify',
{ email },
{
headers: {
'Authorization': `Bearer ${process.env.BILLIONVERIFY_API_KEY}`,
'Content-Type': 'application/json'
}
}
);
const result = response.data;
return {
email: result.email,
deliverable: result.deliverable,
isCatchAll: result.is_catch_all,
isDisposable: result.is_disposable,
isRoleBased: result.is_role_address,
qualityScore: result.quality_score,
recommendation: result.recommendation
};
}
// Bulk verification with catch-all handling
async function bulkVerifyWithStrategy(emails) {
const results = await Promise.all(
emails.map(email => verifyWithBillionVerify(email))
);
return {
safe: results.filter(r => r.deliverable && !r.isCatchAll),
catchAll: results.filter(r => r.deliverable && r.isCatchAll),
invalid: results.filter(r => !r.deliverable),
stats: {
total: results.length,
safeCount: results.filter(r => r.deliverable && !r.isCatchAll).length,
catchAllCount: results.filter(r => r.deliverable && r.isCatchAll).length,
invalidCount: results.filter(r => !r.deliverable).length
}
};
}
BillionVerify ์ฌ์ฉ์ ์ฅ์
๋ ๋์ ์ ํ๋: catch-all ๊ฐ์ง๋ ์ฌ๋ฌ ๊ฒ์ฆ ๊ธฐ์ ์ ์ฌ์ฉํ๊ณ ์๋ ค์ง catch-all ๋๋ฉ์ธ์ ๊ด๋ฒ์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ ์ง ๊ด๋ฆฌํฉ๋๋ค.
์ถ๊ฐ ์ธํ ๋ฆฌ์ ์ค: Catch-all ๊ฐ์ง ์ธ์๋ ์ผํ์ฉ ์ด๋ฉ์ผ ๊ฐ์ง, ์ญํ ๊ธฐ๋ฐ ์ฃผ์ ์๋ณ ๋ฐ ํ์ง ์ ์๋ฅผ ์ป์ ์ ์์ต๋๋ค.
์๋ ์ ํ ๊ด๋ฆฌ: ์๋ ์ ํ ๋ฐ IP ๋กํ ์ด์ ์ ์ฒ๋ฆฌํ์ฌ ์ฐจ๋จ ์์ด ์ผ๊ด๋ ๊ฒ์ฆ์ ๋ณด์ฅํฉ๋๋ค.
๊ธฐ๋ก ๋ฐ์ดํฐ: ๊ธฐ๋ก ๊ฒ์ฆ ๋ฐ์ดํฐ์ ๋ํ ์ก์ธ์ค๋ ํจํด์ ์๋ณํ๊ณ ์์ฌ ๊ฒฐ์ ์ ๊ฐ์ ํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
์ค์๊ฐ ์ ๋ฐ์ดํธ: catch-all ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ๋๋ฉ์ธ ๊ตฌ์ฑ์ด ๋ณ๊ฒฝ๋จ์ ๋ฐ๋ผ ์ง์์ ์ผ๋ก ์ ๋ฐ์ดํธ๋ฉ๋๋ค.
๊ฒฐ๋ก
Catch-all ์ด๋ฉ์ผ ๊ฐ์ง๋ ํฌ๊ด์ ์ธ ์ด๋ฉ์ผ ๊ฒ์ฆ ์ ๋ต์ ์ค์ํ ๊ตฌ์ฑ ์์์ ๋๋ค. ์ด๋ฌํ ์๋ฒ๋ ๊ฒ์ฆ์ ์ด๋ ค์์ ์ ์ํ์ง๋ง, ์๋ ๋ฐฉ์์ ์ดํดํ๊ณ ์ ์ ํ ๊ฐ์ง ๋ฐ ์ฒ๋ฆฌ ์ ๋ต์ ๊ตฌํํ๋ฉด ๊ฐ์น ์๋ ์ฐ๋ฝ์ฒ๋ฅผ ์์ง ์์ผ๋ฉด์ ๋์ ์ ๋ฌ๋ฅ ์ ์ ์งํ ์ ์์ต๋๋ค.
์ด ๊ฐ์ด๋์ ์ฃผ์ ๋ด์ฉ:
- Catch-all ์๋ฒ๋ ๋ชจ๋ ๋ฉ์ผ์ ์๋ฝํฉ๋๋ค - ํน์ ๋ฉ์ผํจ์ด ์กด์ฌํ๋์ง ์ฌ๋ถ์ ๊ด๊ณ์์ด
- ๊ฐ์ง์๋ ํ ์คํธ๊ฐ ํฌํจ๋ฉ๋๋ค - ์ ๋๋ก ์กด์ฌํ์ง ์๋ ์ฃผ์๋ก ํ ์คํธ
- ์๋์ผ๋ก ๊ฑฐ๋ถํ์ง ๋ง์ธ์ - catch-all ์ฃผ์์ ๋ํด ์ํ ๊ธฐ๋ฐ ์ ๋ต์ ๊ตฌํํ์ธ์
- ์ฐธ์ฌ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ์ธ์ - catch-all ์ฐ๋ฝ์ฒ์ ๋ํ ์ ๋ณด์ ์ ๊ฐํ ๊ฒฐ์ ์ ๋ด๋ฆฌ์ธ์
- ์ ๋ฌธ ์๋น์ค๋ฅผ ๊ณ ๋ คํ์ธ์ - ํ๋ก๋์ ์์คํ ์ ์ํด BillionVerify์ ๊ฐ์ ์๋น์ค ์ฌ์ฉ
์ํฌํ๋ก์ฐ์ catch-all ๊ฐ์ง๋ฅผ ๊ตฌํํ ์ค๋น๊ฐ ๋์ จ๋์? ์ด๋ฉ์ผ ๊ฒ์ฌ๊ธฐ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ๋ณ ์ฃผ์๋ฅผ ํ ์คํธํ๊ฑฐ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํํ๊ฒ ํตํฉํ๊ธฐ ์ํ BillionVerify API๋ฅผ ์ดํด๋ณด์ธ์.
Catch-all ๋๋ฉ์ธ์ ์ ์ ํ ์ฒ๋ฆฌํจ์ผ๋ก์จ ์ด๋ฉ์ผ ์ ๋ฌ๋ฅ ์ ๊ฐ์ ํ๊ณ ๋ฐ์ ์ ํํ์ ๋ณดํธํ๋ฉฐ ์ด๋ฉ์ผ ์ฐ๋ฝ์ฒ์ ๋ํด ๋ ๋์ ๊ฒฐ์ ์ ๋ด๋ฆด ์ ์์ต๋๋ค.