์ด๋ฉ์ผ ๊ฒ์ฆ์ ๋ชจ๋ ๊ฐ๋ฐ์๊ฐ ์ดํดํ๊ณ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํํด์ผ ํ๋ ํ๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ค์ํ ๊ตฌ์ฑ ์์์ ๋๋ค. ์ฌ์ฉ์ ๋ฑ๋ก ์์คํ , ๋ด์ค๋ ํฐ ํ๋ซํผ ๋๋ ์ ์์๊ฑฐ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ๋ , ๊ฐ๋ ฅํ ์ด๋ฉ์ผ ๊ฒ์ฆ ๊ตฌํ์ ์๋ชป๋ ๋ฐ์ดํฐ๋ก๋ถํฐ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ณดํธํ๊ณ , ๋ฐ์ก๋ฅ ์ ์ค์ด๋ฉฐ, ์ ๋ฐ์ ์ธ ์ ๋ฌ์ฑ์ ํฅ์์ํต๋๋ค. ์ด ํฌ๊ด์ ์ธ ๊ฐ์ด๋๋ ๊ฐ๋ฐ์๊ฐ ์ฒ์๋ถํฐ ์ ๋ฌธ๊ฐ ์์ค์ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ๊ตฌํํ๋ ๋ฐ ํ์ํ ๋ชจ๋ ๊ฒ์ ์ ๊ณตํฉ๋๋ค.
๊ฐ๋ฐ์์๊ฒ ์ด๋ฉ์ผ ๊ฒ์ฆ์ด ํ์ํ ์ด์
์ด๋ฉ์ผ ๊ฒ์ฆ์ ์ค์์ฑ์ ์ดํดํ๋ฉด ๊ฐ๋ฐ์๊ฐ ๊ตฌํ ์ ๋ต๊ณผ ๋ฆฌ์์ค ํ ๋น์ ๋ํด ์ ๋ณด์ ์ ๊ฐํ ๊ฒฐ์ ์ ๋ด๋ฆฌ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
์ด๋ฉ์ผ ๊ฒ์ฆ์ ๋น์ฆ๋์ค ์ฌ๋ก
์๋ชป๋ ์ด๋ฉ์ผ ์ฃผ์๋ ๋ง์ผํ ๋น์ฉ ๋ญ๋น, ๋ฐ์ ์ ํํ ์์ ๋ฐ ๊ณ ๊ฐ ์ฐธ์ฌ ๊ธฐํ ์์ค์ ํตํด ๋งค๋ ๊ธฐ์ ์ ์๋ฐฑ๋ง ๋ฌ๋ฌ์ ๋น์ฉ์ ๋ฐ์์ํต๋๋ค. ์ฌ์ฉ์๊ฐ ์คํ๋ ์๋์ ์ธ ๊ฐ์ง ์ฃผ์๋ก ์ธํด ๋ฑ๋ก ์ค์ ์๋ชป๋ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์ ๋ ฅํ๋ฉด ๊ทธ ๊ฒฐ๊ณผ๊ฐ ์ ์ฒด ์์คํ ์ ํ๊ธ๋ฉ๋๋ค.
Gmail, Outlook, Yahoo์ ๊ฐ์ ์ด๋ฉ์ผ ์๋น์ค ์ ๊ณต์ ์ฒด๋ ๋ฐ์ ์ ํํ ์งํ๋ฅผ ๋ฉด๋ฐํ ๋ชจ๋ํฐ๋งํฉ๋๋ค. ์ ํ๋ฆฌ์ผ์ด์ ์ด ์๋ชป๋ ์ฃผ์๋ก ์ด๋ฉ์ผ์ ๋ณด๋ด๋ฉด ์ด๋ฌํ ์ด๋ฉ์ผ์ด ๋ฐ์ก๋์ด ๋ฐ์ ์ ์ ์์ ๋ถ์ ์ ์ธ ์ํฅ์ ๋ฏธ์นฉ๋๋ค. ๋ฐ์ ์ ํํ์ด ์ข์ง ์์ผ๋ฉด ํฉ๋ฒ์ ์ธ ์ด๋ฉ์ผ์ด ์ ์ ๋ ์คํธ ํด๋์ ๋ค์ด๊ฐ๊ฒ ๋์ด ๋ชจ๋ ์ด๋ฉ์ผ ์ปค๋ฎค๋์ผ์ด์ ์ ํจ๊ณผ๊ฐ ๊ฐ์ํฉ๋๋ค.
๊ฐ๋ฐ์์ ๊ฒฝ์ฐ ์ง์ ์์ ์ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ๊ตฌํํ๋ฉด ์ด๋ฌํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ธฐ ์ ์ ๋ฐฉ์งํ ์ ์์ต๋๋ค. ์ฌ์ฉ์ ๊ฐ์ ์ค์ ์ค์๊ฐ์ผ๋ก ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ๊ฒ์ฆํจ์ผ๋ก์จ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฒ์๋ถํฐ ํฉ๋ฒ์ ์ด๊ณ ์ ๋ฌ ๊ฐ๋ฅํ ์ฃผ์๋ง ํฌํจ๋๋๋ก ๋ณด์ฅํ ์ ์์ต๋๋ค.
์ด๋ฉ์ผ ๊ฒ์ฆ์ ๊ธฐ์ ์ ์ด์
๋น์ฆ๋์ค ์งํ ์ธ์๋ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ์ ํ๋ฆฌ์ผ์ด์ ํ์ง๊ณผ ์ ๋ขฐ์ฑ์ ํฅ์์ํค๋ ์ค์ํ ๊ธฐ์ ์ ์ด์ ์ ์ ๊ณตํฉ๋๋ค. ๊นจ๋ํ ์ด๋ฉ์ผ ๋ฐ์ดํฐ๋ ๊ฐ์ง ๊ณ์ ์ผ๋ก ์ธํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํฝ์ฐฝ์ ์ค์ด๊ณ , ์ฟผ๋ฆฌ ์ฑ๋ฅ์ ํฅ์์ํค๋ฉฐ, ์ฌ์ฉ์ ๊ด๋ฆฌ๋ฅผ ๋จ์ํํฉ๋๋ค.
์ด๋ฉ์ผ ๊ฒ์ฆ์ ๋ํ ๊ณ์ ์ด๊ฑฐ ๊ณต๊ฒฉ์ ๋ฐฉ์งํ๊ณ ๋ด ๋ฑ๋ก์ ํจ๊ณผ๋ฅผ ๊ฐ์์์ผ ๋ณด์์ ๊ฐํํฉ๋๋ค. ์๋ ์ ํ ๋ฐ CAPTCHA์ ๊ฐ์ ๋ค๋ฅธ ๋ณด์ ์กฐ์น์ ๊ฒฐํฉํ๋ฉด ์ด๋ฉ์ผ ๊ฒ์ฆ์ ์๋ํ๋ ๋จ์ฉ์ ๋ํ ๊ฐ๋ ฅํ ๋ฐฉ์ด๋ฅผ ์์ฑํฉ๋๋ค.
์ด๋ฉ์ผ ๊ฒ์ฆ ์ํคํ ์ฒ ๊ฐ์
๊ตฌํ ์ธ๋ถ ์ฌํญ์ ๋ค๋ฃจ๊ธฐ ์ ์ ๊ฐ๋ฐ์๋ ์์ ํ ์ด๋ฉ์ผ ๊ฒ์ฆ ์ํคํ ์ฒ์ ๋ค์ํ ๊ตฌ์ฑ ์์๊ฐ ํจ๊ป ์๋ํ๋ ๋ฐฉ์์ ์ดํดํด์ผ ํฉ๋๋ค.
๋ค์ธต ๊ฒ์ฆ ์ ๊ทผ ๋ฐฉ์
์ ๋ฌธ์ ์ธ ์ด๋ฉ์ผ ๊ฒ์ฆ ์์คํ ์ ๊ฐ๊ฐ ๋ค๋ฅธ ์ ํ์ ์๋ชป๋ ์ฃผ์๋ฅผ ํฌ์ฐฉํ๋ ์ฌ๋ฌ ๊ฒ์ฆ ๋ ์ด์ด๋ฅผ ๊ตฌํํฉ๋๋ค. ์ด ๋ ์ด์ด๋ ์ ๊ทผ ๋ฐฉ์์ ์ฑ๋ฅ์ ์ต์ ํํ๋ฉด์ ์ ํ๋๋ฅผ ๊ทน๋ํํฉ๋๋ค.
์ฒซ ๋ฒ์งธ ๋ ์ด์ด๋ ๊ตฌ๋ฌธ ๊ฒ์ฆ์ ์ํํ์ฌ ์ด๋ฉ์ผ ์ฃผ์๊ฐ RFC 5321 ๋ฐ RFC 5322 ํ์ค์ ์ค์ํ๋์ง ํ์ธํฉ๋๋ค. ์ด ๋น ๋ฅธ ๋ก์ปฌ ๊ฒ์ฆ์ ๋คํธ์ํฌ ์์ฒญ ์์ด ๋ช ๋ฐฑํ ํ์ ์ค๋ฅ๋ฅผ ํฌ์ฐฉํฉ๋๋ค.
๋ ๋ฒ์งธ ๋ ์ด์ด๋ DNS ๊ฒ์ฆ์ ์ํํ์ฌ MX ๋ ์ฝ๋๋ฅผ ์ฟผ๋ฆฌํ์ฌ ์ด๋ฉ์ผ ๋๋ฉ์ธ์ด ๋ฉ์ผ์ ์์ ํ ์ ์๋์ง ํ์ธํฉ๋๋ค. ์ด ๋คํธ์ํฌ ๊ธฐ๋ฐ ๊ฒ์ฆ์ ์กด์ฌํ์ง ์๊ฑฐ๋ ์ ์ ํ ์ด๋ฉ์ผ ๊ตฌ์ฑ์ด ์๋ ๋๋ฉ์ธ์ ํฌ์ฐฉํฉ๋๋ค.
์ธ ๋ฒ์งธ ๋ ์ด์ด๋ SMTP ๊ฒ์ฆ์ ์ํํ์ฌ ์์ ์์ ๋ฉ์ผ ์๋ฒ์ ์ฐ๊ฒฐํ์ฌ ํน์ ๋ฉ์ผํจ์ด ์กด์ฌํ๋์ง ํ์ธํฉ๋๋ค. ์ด๋ ๊ฐ์ฅ ๋์ ์ ํ๋๋ฅผ ์ ๊ณตํ์ง๋ง ์ฐจ๋จ๋์ง ์๋๋ก ์ ์คํ ๊ตฌํ์ด ํ์ํฉ๋๋ค.
๋๊ธฐ vs ๋น๋๊ธฐ ๊ฒ์ฆ
๊ฐ๋ฐ์๋ ์์ ์ ์ถ ์ค ๋๊ธฐ ๊ฒ์ฆ๊ณผ ์ ์ถ ํ ๋น๋๊ธฐ ๊ฒ์ฆ ์ค ํ๋๋ฅผ ๊ฒฐ์ ํด์ผ ํฉ๋๋ค. ๊ฐ ์ ๊ทผ ๋ฐฉ์์๋ ๋๋ ทํ ์ฅ์ ๊ณผ ์ ์ถฉ์ ์ด ์์ต๋๋ค.
๋๊ธฐ ๊ฒ์ฆ์ ์ฌ์ฉ์์๊ฒ ์ฆ๊ฐ์ ์ธ ํผ๋๋ฐฑ์ ์ ๊ณตํ์ฌ ์๋ชป๋ ์ฃผ์๊ฐ ์์คํ ์ ์ ๋ ฅ๋๋ ๊ฒ์ ๋ฐฉ์งํฉ๋๋ค. ๊ทธ๋ฌ๋ SMTP ๊ฒ์ฆ์ ๋ช ์ด๊ฐ ๊ฑธ๋ฆด ์ ์์ด ๋ฑ๋ก ์ค์ ์ฌ์ฉ์๋ฅผ ์ข์ ์ํฌ ์ ์์ต๋๋ค.
๋น๋๊ธฐ ๊ฒ์ฆ์ ์ฃผ์๋ฅผ ์ฆ์ ์๋ฝํ๊ณ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๊ฒ์ฆํฉ๋๋ค. ์ด๋ ๋ ๋์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ์ง๋ง ์ ์ถ ํ ๊ฒ์ฆ์ ์คํจํ ์ฃผ์๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํ ์ถ๊ฐ ๋ ผ๋ฆฌ๊ฐ ํ์ํฉ๋๋ค.
๋ง์ ํ๋ก๋์ ์์คํ ์ ํ์ด๋ธ๋ฆฌ๋ ์ ๊ทผ ๋ฐฉ์์ ์ฌ์ฉํ์ฌ ๋น ๋ฅธ ๊ตฌ๋ฌธ ๋ฐ DNS ๊ฒ์ฆ์ ๋๊ธฐ์ ์ผ๋ก ์ํํ๋ฉด์ SMTP ๊ฒ์ฆ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฒ๋ฆฌ๋ก ์ฐ๊ธฐํฉ๋๋ค.
๊ตฌ๋ฌธ ๊ฒ์ฆ ๊ตฌํ
๊ตฌ๋ฌธ ๊ฒ์ฆ์ ๋น์ฉ์ด ๋ง์ด ๋๋ ๋คํธ์ํฌ ์์ ์ ์ํํ๊ธฐ ์ ์ ์๋ชป๋ ํ์์ ์ฃผ์๋ฅผ ํฌ์ฐฉํ๋ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ๊ธฐ์ด์ ๋๋ค.
์ด๋ฉ์ผ ์ฃผ์ ๊ตฌ์กฐ ์ดํด
์ ํจํ ์ด๋ฉ์ผ ์ฃผ์๋ ๋ก์ปฌ ๋ถ๋ถ, @ ๊ธฐํธ ๋ฐ ๋๋ฉ์ธ ๋ถ๋ถ์ผ๋ก ๊ตฌ์ฑ๋ฉ๋๋ค. ์ ์ฒด RFC ์ฌ์์ ๋ณต์กํ ํ์์ ํ์ฉํ์ง๋ง ์ค์ฉ์ ์ธ ๊ฒ์ฆ์ ์ผ๋ฐ์ ์ผ๋ก ํ์ฉ๋๋ ํจํด์ ์ด์ ์ ๋ง์ถฐ์ผ ํฉ๋๋ค.
๋ก์ปฌ ๋ถ๋ถ์ ์์ซ์ ๋ฌธ์, ์ , ํ์ดํ, ๋ฐ์ค ๋ฐ ๋ํ๊ธฐ ๊ธฐํธ๋ฅผ ํฌํจํ ์ ์์ต๋๋ค. ๋๋ฉ์ธ ๋ถ๋ถ์ ๋๋ฉ์ธ๊ณผ ์ต์์ ๋๋ฉ์ธ์ ๊ตฌ๋ถํ๋ ์ต์ํ ํ๋์ ์ ์ด ์๋ ์ ํจํ ๋๋ฉ์ธ ์ด๋ฆ์ด์ด์ผ ํฉ๋๋ค.
์ ๊ท์ ๊ธฐ๋ฐ ๊ฒ์ฆ
์ ๊ท์์ ๋น ๋ฅด๊ณ ์ ์ฐํ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ์ ๊ณตํฉ๋๋ค. ๊ทธ๋ฌ๋ ๋ชจ๋ ์ ํจํ ์ฃผ์๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ๊ฒ์ฆํ๋ฉด์ ์๋ชป๋ ์ฃผ์๋ฅผ ๊ฑฐ๋ถํ๋ ์ ๊ท์์ ๋ง๋๋ ๊ฒ์ ๋๋ผ์ธ ์ ๋๋ก ๋ณต์กํฉ๋๋ค.
// JavaScript๋ฅผ ์ํ ์ค์ฉ์ ์ธ ์ด๋ฉ์ผ ๊ฒ์ฆ ์ ๊ท์
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
function validateEmailSyntax(email) {
if (!email || typeof email !== 'string') {
return { valid: false, error: 'Email is required' };
}
const trimmedEmail = email.trim().toLowerCase();
if (trimmedEmail.length > 254) {
return { valid: false, error: 'Email address too long' };
}
if (!emailRegex.test(trimmedEmail)) {
return { valid: false, error: 'Invalid email format' };
}
const [localPart, domain] = trimmedEmail.split('@');
if (localPart.length > 64) {
return { valid: false, error: 'Local part too long' };
}
return { valid: true, email: trimmedEmail };
}
๊ธฐ๋ณธ ์ ๊ท์ ๊ฒ์ฆ์ ๋์ด์
์ ๊ท์์ ๋ช ๋ฐฑํ ํ์ ์ค๋ฅ๋ฅผ ํฌ์ฐฉํ์ง๋ง ์ถ๊ฐ ๊ฒ์ฌ๋ ๊ฒ์ฆ ์ ํ๋๋ฅผ ํฅ์์ํต๋๋ค. ์ฌ๊ธฐ์๋ ์ฐ์ ์ ํ์ธ, ์ต์์ ๋๋ฉ์ธ ๊ธธ์ด ๊ฒ์ฆ ๋ฐ ์ผ๋ฐ์ ์ธ ์คํ ํจํด ๊ฐ์ง๊ฐ ํฌํจ๋ฉ๋๋ค.
function enhancedSyntaxValidation(email) {
const basicResult = validateEmailSyntax(email);
if (!basicResult.valid) return basicResult;
const normalizedEmail = basicResult.email;
const [localPart, domain] = normalizedEmail.split('@');
// ์ฐ์ ์ ํ์ธ
if (localPart.includes('..') || domain.includes('..')) {
return { valid: false, error: 'Consecutive dots not allowed' };
}
// ์์/๋ ์ ํ์ธ
if (localPart.startsWith('.') || localPart.endsWith('.')) {
return { valid: false, error: 'Local part cannot start or end with dot' };
}
// TLD ๊ฒ์ฆ
const tld = domain.split('.').pop();
if (tld.length < 2 || tld.length > 63) {
return { valid: false, error: 'Invalid top-level domain' };
}
// ์ซ์๋ง ์๋ TLD ํ์ธ (์ ํจํ์ง ์์)
if (/^\d+$/.test(tld)) {
return { valid: false, error: 'TLD cannot be numeric only' };
}
return { valid: true, email: normalizedEmail };
}
DNS ๋ฐ MX ๋ ์ฝ๋ ๊ฒ์ฆ
๊ตฌ๋ฌธ ๊ฒ์ฆ ํ DNS ๊ฒ์ฆ์ ์ ํจํ MX ๋ ์ฝ๋๋ฅผ ํ์ธํ์ฌ ์ด๋ฉ์ผ ๋๋ฉ์ธ์ด ๋ฉ์ผ์ ์์ ํ ์ ์๋์ง ํ์ธํฉ๋๋ค.
MX ๋ ์ฝ๋ ์ดํด
๋ฉ์ผ ๊ตํ(MX) ๋ ์ฝ๋๋ ๋๋ฉ์ธ์ ๋ํ ์ด๋ฉ์ผ ์์ ์ ๋ด๋นํ๋ ๋ฉ์ผ ์๋ฒ๋ฅผ ์ง์ ํ๋ DNS ๋ ์ฝ๋์ ๋๋ค. ๊ฐ MX ๋ ์ฝ๋์๋ ์ฐ์ ์์ ๊ฐ๊ณผ ํธ์คํธ ์ด๋ฆ์ด ํฌํจ๋์ด ์์ด ๋๋ฉ์ธ์ด ์ฅ์ ์กฐ์น์ ํจ๊ป ์ฌ๋ฌ ๋ฉ์ผ ์๋ฒ๋ฅผ ๊ตฌ์ฑํ ์ ์์ต๋๋ค.
user@example.com์ผ๋ก ์ด๋ฉ์ผ์ ๋ณด๋ผ ๋ ๋ฐ์ ์๋ฒ๋ example.com์ MX ๋ ์ฝ๋์ ๋ํด DNS๋ฅผ ์ฟผ๋ฆฌํ ๋ค์ ์๋ตํ๋ ๊ฐ์ฅ ๋์ ์ฐ์ ์์(๊ฐ์ฅ ๋ฎ์ ๋ฒํธ) ๋ฉ์ผ ์๋ฒ์ ์ฐ๊ฒฐํฉ๋๋ค.
Node.js์์ MX ์กฐํ ๊ตฌํ
Node.js๋ dns ๋ชจ๋์ ํตํด ๋ด์ฅ DNS ํด์๋๋ฅผ ์ ๊ณตํ์ฌ MX ๊ฒ์ฆ์ ๊ฐ๋จํ๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค.
const dns = require('dns').promises;
async function validateMXRecords(domain) {
try {
const mxRecords = await dns.resolveMx(domain);
if (!mxRecords || mxRecords.length === 0) {
return {
valid: false,
error: 'No MX records found',
domain
};
}
// ์ฐ์ ์์๋ณ๋ก ์ ๋ ฌ (๋ฎ์์๋ก ์ฐ์ ์์๊ฐ ๋์)
const sortedRecords = mxRecords.sort((a, b) => a.priority - b.priority);
return {
valid: true,
domain,
mxRecords: sortedRecords,
primaryMX: sortedRecords[0].exchange
};
} catch (error) {
if (error.code === 'ENOTFOUND' || error.code === 'ENODATA') {
return {
valid: false,
error: 'Domain does not exist or has no MX records',
domain
};
}
return {
valid: false,
error: `DNS lookup failed: ${error.message}`,
domain
};
}
}
async function validateEmailDomain(email) {
const domain = email.split('@')[1];
// ๋จผ์ MX ๋ ์ฝ๋ ์๋
const mxResult = await validateMXRecords(domain);
if (mxResult.valid) return mxResult;
// A ๋ ์ฝ๋ ํ์ธ์ผ๋ก ํด๋ฐฑ (์ผ๋ถ ๋๋ฉ์ธ์ MX ์์ด ๋ฉ์ผ ์์ )
try {
const aRecords = await dns.resolve4(domain);
if (aRecords && aRecords.length > 0) {
return {
valid: true,
domain,
mxRecords: [],
fallbackToA: true,
aRecords
};
}
} catch (error) {
// A ๋ ์ฝ๋ ์กฐํ๋ ์คํจ
}
return mxResult;
}
DNS ์ฃ์ง ์ผ์ด์ค ์ฒ๋ฆฌ
ํ๋ก๋์ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ์๊ฐ ์ด๊ณผ, ์์ ์คํจ ๋ฐ ๋น์ ์์ ์ธ ๊ตฌ์ฑ์ด ์๋ ๋๋ฉ์ธ์ ํฌํจํ ๋ค์ํ DNS ์ฃ์ง ์ผ์ด์ค๋ฅผ ์ฒ๋ฆฌํด์ผ ํฉ๋๋ค.
async function robustDNSValidation(email, options = {}) {
const { timeout = 5000, retries = 2 } = options;
const domain = email.split('@')[1];
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const result = await validateEmailDomain(email);
clearTimeout(timeoutId);
return result;
} catch (error) {
if (attempt === retries) {
return {
valid: false,
error: 'DNS validation failed after retries',
domain,
temporary: true
};
}
// ์ง์ ๋ฐฑ์คํ
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 100)
);
}
}
}
SMTP ๊ฒ์ฆ ๊ตฌํ
SMTP ๊ฒ์ฆ์ ์์ ์์ ๋ฉ์ผ ์๋ฒ์ ์ง์ ์ฟผ๋ฆฌํ์ฌ ๋ฉ์ผํจ์ด ์กด์ฌํ๋์ง ํ์ธํจ์ผ๋ก์จ ๊ฐ์ฅ ๋์ ์ ํ๋๋ฅผ ์ ๊ณตํฉ๋๋ค.
SMTP ๊ฒ์ฆ ์๋ ๋ฐฉ์
SMTP ๊ฒ์ฆ์ ์ค์ ๋ก ๋ฉ์์ง๋ฅผ ์ ๋ฌํ์ง ์๊ณ ์ด๋ฉ์ผ ์ ์ก์ ์ด๊ธฐ ๋จ๊ณ๋ฅผ ์๋ฎฌ๋ ์ด์ ํฉ๋๋ค. ๊ฒ์ฆ ํ๋ก์ธ์ค๋ ๋ฉ์ผ ์๋ฒ์ ๋ํ ์ฐ๊ฒฐ์ ์ค์ ํ๊ณ , EHLO/HELO๋ก ์์ ์ ์๊ฐํ๊ณ , MAIL FROM์ผ๋ก ๋ฐ์ ์ ์ฃผ์๋ฅผ ์ ๊ณตํ ๋ค์ RCPT TO๋ก ๋์ ์ฃผ์๋ก ์ ์ก์ ์์ฒญํฉ๋๋ค.
RCPT TO์ ๋ํ ๋ฉ์ผ ์๋ฒ์ ์๋ต์ ๋ฉ์ผํจ์ด ์กด์ฌํ๋์ง ์ฌ๋ถ๋ฅผ ๋ํ๋ ๋๋ค. 250 ์๋ต์ ์ฃผ์๊ฐ ์ ํจํจ์ ํ์ธํ๊ณ 550์ ์ฌ์ฉ์๊ฐ ์กด์ฌํ์ง ์์์ ๋ํ๋ ๋๋ค. ๊ทธ๋ฌ๋ ๋ง์ ์๋ฒ๊ฐ ์ด์ ์ด ํ๋ก์ธ์ค๋ฅผ ๋ณต์กํ๊ฒ ๋ง๋๋ ์บ์น์ฌ ๊ตฌ์ฑ ๋๋ ๊ทธ๋ ์ด๋ฆฌ์คํ ์ ์ฌ์ฉํฉ๋๋ค.
Node.js์์ ๊ธฐ๋ณธ SMTP ๊ฒ์ฆ
const net = require('net');
class SMTPVerifier {
constructor(options = {}) {
this.timeout = options.timeout || 10000;
this.fromEmail = options.fromEmail || 'verify@example.com';
this.fromDomain = options.fromDomain || 'example.com';
}
async verify(email, mxHost) {
return new Promise((resolve) => {
const socket = new net.Socket();
let step = 0;
let response = '';
const cleanup = () => {
socket.destroy();
};
socket.setTimeout(this.timeout);
socket.on('timeout', () => {
cleanup();
resolve({ valid: false, error: 'Connection timeout' });
});
socket.on('error', (error) => {
cleanup();
resolve({ valid: false, error: error.message });
});
socket.on('data', (data) => {
response = data.toString();
const code = parseInt(response.substring(0, 3));
switch (step) {
case 0: // ์ฐ๊ฒฐ๋จ, ์ธ์ฌ๋ง ์์
if (code === 220) {
socket.write(`EHLO ${this.fromDomain}\r\n`);
step = 1;
} else {
cleanup();
resolve({ valid: false, error: 'Invalid greeting' });
}
break;
case 1: // EHLO ์๋ต
if (code === 250) {
socket.write(`MAIL FROM:<${this.fromEmail}>\r\n`);
step = 2;
} else {
cleanup();
resolve({ valid: false, error: 'EHLO rejected' });
}
break;
case 2: // MAIL FROM ์๋ต
if (code === 250) {
socket.write(`RCPT TO:<${email}>\r\n`);
step = 3;
} else {
cleanup();
resolve({ valid: false, error: 'MAIL FROM rejected' });
}
break;
case 3: // RCPT TO ์๋ต - ๊ฒ์ฆ ๊ฒฐ๊ณผ
socket.write('QUIT\r\n');
cleanup();
if (code === 250) {
resolve({ valid: true, email });
} else if (code === 550 || code === 551 || code === 552 || code === 553) {
resolve({ valid: false, error: 'Mailbox does not exist', code });
} else if (code === 450 || code === 451 || code === 452) {
resolve({ valid: false, error: 'Temporary failure', temporary: true, code });
} else {
resolve({ valid: false, error: `Unknown response: ${code}`, code });
}
break;
}
});
socket.connect(25, mxHost);
});
}
}
SMTP ๊ณผ์ ์ฒ๋ฆฌ
์ค์ SMTP ๊ฒ์ฆ์ ๊ทธ๋ ์ด๋ฆฌ์คํ , ์๋ ์ ํ ๋ฐ ์บ์น์ฌ ๋๋ฉ์ธ์ ํฌํจํ ์๋ง์ ๊ณผ์ ์ ์ง๋ฉดํฉ๋๋ค. ๊ฐ๋ฐ์๋ ์ด๋ฌํ ์ํฉ์ ์ฒ๋ฆฌํ๊ธฐ ์ํ ์ ๋ต์ ๊ตฌํํด์ผ ํฉ๋๋ค.
async function comprehensiveSMTPVerification(email, mxRecords) {
const verifier = new SMTPVerifier({
fromEmail: 'verify@yourdomain.com',
fromDomain: 'yourdomain.com',
timeout: 15000
});
// ์ฐ์ ์์๋๋ก ๊ฐ MX ์๋ฒ ์๋
for (const mx of mxRecords) {
const result = await verifier.verify(email, mx.exchange);
// ํ์คํ ๋ต๋ณ์ ์ป์ผ๋ฉด ๋ฐํ
if (result.valid || (!result.temporary && result.code === 550)) {
return result;
}
// ์์ ์คํจ ๋๋ ์ฐ๊ฒฐ ๋ฌธ์ ์ ๊ฒฝ์ฐ ๋ค์ ์๋ฒ ์๋
if (result.temporary || result.error.includes('timeout')) {
continue;
}
// ๋ค๋ฅธ ์ค๋ฅ์ ๊ฒฝ์ฐ ๊ฒฐ๊ณผ ๋ฐํ
return result;
}
return {
valid: false,
error: 'All MX servers failed',
temporary: true
};
}
์ด๋ฉ์ผ ๊ฒ์ฆ API ์ฌ์ฉ
์ฌ์ฉ์ ์ง์ ๊ฒ์ฆ ๊ตฌ์ถ์ ๊ต์ก์ ์ด์ง๋ง ํ๋ก๋์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ข ์ข BillionVerify์ ๊ฐ์ ์ ๋ฌธ ์ด๋ฉ์ผ ๊ฒ์ฆ API๋ฅผ ์ฌ์ฉํ๋ฉด ๋์์ด ๋ฉ๋๋ค.
์ด๋ฉ์ผ ๊ฒ์ฆ API๋ฅผ ์ฌ์ฉํ๋ ์ด์
์ ๋ฌธ ์ด๋ฉ์ผ ๊ฒ์ฆ ์๋น์ค๋ ์ฌ์ฉ์ ์ง์ ๊ตฌํ๋ณด๋ค ์ฌ๋ฌ ๊ฐ์ง ์ด์ ์ ์ ๊ณตํฉ๋๋ค. ์๋ ค์ง ์ผํ์ฉ ์ด๋ฉ์ผ ์ ๊ณต์ ์ฒด, ์บ์น์ฌ ๋๋ฉ์ธ ๋ฐ ์คํธ ํธ๋ฉ์ ๊ด๋ฒ์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ ์ง ๊ด๋ฆฌํฉ๋๋ค. ๋ํ ์ฐจ๋จ๋์ง ์๊ณ ๋๋ SMTP ๊ฒ์ฆ์ ํ์ํ ์ธํ๋ผ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
BillionVerify์ ์ด๋ฉ์ผ ๊ฒ์ฆ API๋ ๊ฐ๋จํ REST API๋ฅผ ํตํด ๊ตฌ๋ฌธ ๊ฒ์ฌ, DNS ๊ฒ์ฆ, SMTP ๊ฒ์ฆ, ์ผํ์ฉ ์ด๋ฉ์ผ ๊ฐ์ง ๋ฐ ์ ๋ฌ์ฑ ์ ์๋ฅผ ํฌํจํ ํฌ๊ด์ ์ธ ๊ฒ์ฆ์ ์ ๊ณตํฉ๋๋ค.
BillionVerify API ํตํฉ
const axios = require('axios');
class BillionVerifyClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseURL = 'https://api.billionverify.com/v1';
}
async verifySingle(email) {
try {
const response = await axios.get(`${this.baseURL}/verify`, {
params: { email },
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
});
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: error.response?.data?.message || error.message
};
}
}
async verifyBatch(emails) {
try {
const response = await axios.post(`${this.baseURL}/verify/batch`, {
emails
}, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
});
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: error.response?.data?.message || error.message
};
}
}
}
// ์ฌ์ฉ ์์
async function validateUserEmail(email) {
const client = new BillionVerifyClient(process.env.BILLIONVERIFY_API_KEY);
const result = await client.verifySingle(email);
if (!result.success) {
console.error('Verification failed:', result.error);
return { valid: false, error: 'Verification service unavailable' };
}
const { data } = result;
return {
valid: data.deliverable,
email: data.email,
status: data.status,
isDisposable: data.is_disposable,
isCatchAll: data.is_catch_all,
score: data.quality_score
};
}
์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ค์๊ฐ ๊ฒ์ฆ
์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ค์๊ฐ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ๊ตฌํํ๋ ค๋ฉด ์ฌ์ฉ์ ๊ฒฝํ๊ณผ ์ฑ๋ฅ์ ์ ์คํ๊ฒ ๊ณ ๋ คํด์ผ ํฉ๋๋ค.
ํ๋ก ํธ์๋ ๊ฒ์ฆ ์ ๋ต
ํ๋ก ํธ์๋ ๊ฒ์ฆ์ ๋ช ๋ฐฑํ ์ค๋ฅ์ ๋ํ ์ฆ๊ฐ์ ์ธ ํผ๋๋ฐฑ์ ์ ๊ณตํ๋ฉด์ ํฌ๊ด์ ์ธ ๊ฒ์ฆ์ ๋ฐฑ์๋๋ก ์ฐ๊ธฐํด์ผ ํฉ๋๋ค. ์ด ์ ๊ทผ ๋ฐฉ์์ ๋ณด์๊ณผ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ท ํ์ ๋ง์ถฅ๋๋ค.
// ๋๋ฐ์ด์ฑ์ ์ฌ์ฉํ ํ๋ก ํธ์๋ ์ด๋ฉ์ผ ๊ฒ์ฆ
class EmailValidator {
constructor(options = {}) {
this.debounceMs = options.debounceMs || 500;
this.onValidating = options.onValidating || (() => {});
this.onResult = options.onResult || (() => {});
this.pendingRequest = null;
this.debounceTimer = null;
}
validateSyntax(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
async validate(email) {
// ๋๊ธฐ ์ค์ธ ์์ฒญ ์ทจ์
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
// ์ฆ๊ฐ์ ์ธ ๊ตฌ๋ฌธ ๊ฒ์ฌ
if (!this.validateSyntax(email)) {
this.onResult({
valid: false,
error: 'Please enter a valid email address'
});
return;
}
// API ํธ์ถ ๋๋ฐ์ด์ค
this.debounceTimer = setTimeout(async () => {
this.onValidating(true);
try {
const response = await fetch('/api/verify-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
const result = await response.json();
this.onResult(result);
} catch (error) {
this.onResult({
valid: false,
error: 'Unable to verify email'
});
} finally {
this.onValidating(false);
}
}, this.debounceMs);
}
}
// React ์ปดํฌ๋ํธ ์์
function EmailInput() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState({ checking: false, result: null });
const validator = useMemo(() => new EmailValidator({
onValidating: (checking) => setStatus(s => ({ ...s, checking })),
onResult: (result) => setStatus(s => ({ ...s, result }))
}), []);
const handleChange = (e) => {
const value = e.target.value;
setEmail(value);
if (value) validator.validate(value);
};
return (
<div className="email-input">
<input
type="email"
value={email}
onChange={handleChange}
placeholder="Enter your email"
/>
{status.checking && <span className="loading">Verifying...</span>}
{status.result && (
<span className={status.result.valid ? 'valid' : 'invalid'}>
{status.result.valid ? 'โ Valid email' : status.result.error}
</span>
)}
</div>
);
}
๋ฐฑ์๋ API ์๋ํฌ์ธํธ
๋ฐฑ์๋ API ์๋ํฌ์ธํธ๋ ์๋ ์ ํ์ ํตํด ๋จ์ฉ์ ๋ฐฉ์งํ๋ฉด์ ํฌ๊ด์ ์ธ ๊ฒ์ฆ์ ๊ตฌํํด์ผ ํฉ๋๋ค.
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// ๊ฒ์ฆ ์๋ํฌ์ธํธ์ ๋ํ ์๋ ์ ํ
const verifyLimiter = rateLimit({
windowMs: 60 * 1000, // 1๋ถ
max: 10, // IP๋น ๋ถ๋น 10๊ฐ ์์ฒญ
message: { error: 'Too many verification requests' }
});
app.post('/api/verify-email', verifyLimiter, async (req, res) => {
const { email } = req.body;
if (!email) {
return res.status(400).json({ valid: false, error: 'Email required' });
}
try {
// ๋ ์ด์ด 1: ๊ตฌ๋ฌธ ๊ฒ์ฆ
const syntaxResult = enhancedSyntaxValidation(email);
if (!syntaxResult.valid) {
return res.json(syntaxResult);
}
// ๋ ์ด์ด 2: DNS ๊ฒ์ฆ
const dnsResult = await robustDNSValidation(syntaxResult.email);
if (!dnsResult.valid) {
return res.json(dnsResult);
}
// ๋ ์ด์ด 3: API ๊ธฐ๋ฐ ํฌ๊ด์ ๊ฒ์ฆ
const apiResult = await validateUserEmail(syntaxResult.email);
res.json(apiResult);
} catch (error) {
console.error('Verification error:', error);
res.status(500).json({ valid: false, error: 'Verification failed' });
}
});
์ผํ์ฉ ๋ฐ ์์ ์ด๋ฉ์ผ ๊ฐ์ง
์ผํ์ฉ ์ด๋ฉ์ผ ์ฃผ์๋ ์ง์ ํ ์ฌ์ฉ์ ์ฐธ์ฌ๊ฐ ํ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ค์ํ ๊ณผ์ ๋ฅผ ์ ๊ธฐํฉ๋๋ค. ์ด๋ฌํ ์ฃผ์๋ฅผ ๊ฐ์งํ๊ณ ์ฐจ๋จํ๋ ๊ฒ์ ๋ชฉ๋ก ํ์ง์ ์ ์งํ๋ ๋ฐ ํ์์ ์ ๋๋ค.
์ผํ์ฉ ์ด๋ฉ์ผ ์ดํด
Guerrilla Mail, 10MinuteMail ๋ฐ Mailinator์ ๊ฐ์ ์ผํ์ฉ ์ด๋ฉ์ผ ์๋น์ค๋ ์ฌ์ฉ์๊ฐ ๋ฑ๋ก ์์ด ์ฆ์ ์์ฑํ ์ ์๋ ์์ ์ฃผ์๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ด๋ฌํ ์๋น์ค์๋ ํฉ๋ฒ์ ์ธ ์ฉ๋๊ฐ ์์ง๋ง ๋ฑ๋ก ์๊ตฌ ์ฌํญ์ ์ฐํํ๊ฑฐ๋ ๊ฐ์ง ๊ณ์ ์ ๋ง๋๋ ๋ฐ ์์ฃผ ์ฌ์ฉ๋ฉ๋๋ค.
์ผํ์ฉ ์ด๋ฉ์ผ ๊ฐ์ง๊ธฐ ๊ตฌ์ถ
class DisposableEmailDetector {
constructor() {
// ์ผ๋ฐ์ ์ธ ์ผํ์ฉ ์ด๋ฉ์ผ ๋๋ฉ์ธ
this.knownDisposable = new Set([
'guerrillamail.com', 'guerrillamail.org',
'10minutemail.com', '10minutemail.net',
'mailinator.com', 'mailinator.net',
'tempmail.com', 'tempmail.net',
'throwaway.email', 'throwawaymail.com',
'fakeinbox.com', 'trashmail.com',
'getnada.com', 'temp-mail.org',
'mohmal.com', 'emailondeck.com'
// ๋ ๋ง์ ์๋ ค์ง ์ผํ์ฉ ๋๋ฉ์ธ ์ถ๊ฐ
]);
// ์ผํ์ฉ ์๋น์ค๋ฅผ ๋ํ๋ด๋ ํจํด
this.suspiciousPatterns = [
/^temp/i,
/^trash/i,
/^throw/i,
/^fake/i,
/^disposable/i,
/\d{2,}mail/i,
/minutemail/i
];
}
isDisposable(email) {
const domain = email.split('@')[1].toLowerCase();
// ์๋ ค์ง ์ผํ์ฉ ๋๋ฉ์ธ ํ์ธ
if (this.knownDisposable.has(domain)) {
return { isDisposable: true, reason: 'Known disposable domain' };
}
// ์์ฌ์ค๋ฌ์ด ํจํด ํ์ธ
for (const pattern of this.suspiciousPatterns) {
if (pattern.test(domain)) {
return { isDisposable: true, reason: 'Suspicious domain pattern' };
}
}
return { isDisposable: false };
}
async updateDisposableList() {
// ์ ์ง ๊ด๋ฆฌ๋๋ ์์ค์์ ์
๋ฐ์ดํธ๋ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
try {
const response = await fetch(
'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf'
);
const text = await response.text();
const domains = text.split('\n').filter(d => d.trim());
domains.forEach(domain => this.knownDisposable.add(domain.toLowerCase()));
return { success: true, count: this.knownDisposable.size };
} catch (error) {
return { success: false, error: error.message };
}
}
}
์ฑ๋ฅ ์ต์ ํ ์ ๋ต
์ด๋ฉ์ผ ๊ฒ์ฆ์ ์ ์คํ๊ฒ ๊ตฌํํ์ง ์์ผ๋ฉด ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค. ์ด๋ฌํ ์ต์ ํ ์ ๋ต์ ๋น ๋ฅธ ์๋ต ์๊ฐ์ ์ ์งํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
๊ฒ์ฆ ๊ฒฐ๊ณผ ์บ์ฑ
์บ์ฑ์ ์ค๋ณต ๊ฒ์ฆ ์์ฒญ์ ์ค์ด๊ณ ๋ฐ๋ณต ๊ฒ์ฆ์ ๋ํ ์๋ต ์๊ฐ์ ํฅ์์ํต๋๋ค.
const NodeCache = require('node-cache');
class CachedEmailVerifier {
constructor(options = {}) {
this.cache = new NodeCache({
stdTTL: options.ttl || 3600, // ๊ธฐ๋ณธ๊ฐ 1์๊ฐ
checkperiod: options.checkperiod || 600
});
this.verifier = options.verifier;
}
async verify(email) {
const normalizedEmail = email.toLowerCase().trim();
const cacheKey = `email:${normalizedEmail}`;
// ๋จผ์ ์บ์ ํ์ธ
const cached = this.cache.get(cacheKey);
if (cached) {
return { ...cached, fromCache: true };
}
// ๊ฒ์ฆ ์ํ
const result = await this.verifier.verify(normalizedEmail);
// ๊ฒฐ๊ณผ ์บ์ (์์ ์คํจ๋ ์บ์ํ์ง ์์)
if (!result.temporary) {
this.cache.set(cacheKey, result);
}
return result;
}
invalidate(email) {
const normalizedEmail = email.toLowerCase().trim();
this.cache.del(`email:${normalizedEmail}`);
}
getStats() {
return this.cache.getStats();
}
}
์์ฒญ ๋๊ธฐ์ด ๊ตฌํ
๋๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฒฝ์ฐ ์์ฒญ ๋๊ธฐ์ด์ ๊ฒ์ฆ ์๋น์ค๋ฅผ ์๋ํ๋ ๊ฒ์ ๋ฐฉ์งํ๊ณ ๊ณต์ ํ ๋ฆฌ์์ค ๋ถ๋ฐฐ๋ฅผ ๋ณด์ฅํฉ๋๋ค.
const Queue = require('bull');
const verificationQueue = new Queue('email-verification', {
redis: { host: 'localhost', port: 6379 },
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000
}
}
});
// ๊ฒ์ฆ ์์
์ฒ๋ฆฌ
verificationQueue.process(async (job) => {
const { email, userId } = job.data;
const result = await comprehensiveEmailVerification(email);
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ฒฐ๊ณผ ์ ์ฅ
await updateUserEmailStatus(userId, result);
return result;
});
// ๊ฒ์ฆ ์์ฒญ ๋๊ธฐ์ด์ ์ถ๊ฐ
async function queueEmailVerification(email, userId) {
const job = await verificationQueue.add({
email,
userId
}, {
priority: 1,
delay: 0
});
return job.id;
}
์ค๋ฅ ์ฒ๋ฆฌ ๋ฐ ๋ก๊น
๊ฐ๋ ฅํ ์ค๋ฅ ์ฒ๋ฆฌ ๋ฐ ํฌ๊ด์ ์ธ ๋ก๊น ์ ์์ ์ ์ธ ์ด๋ฉ์ผ ๊ฒ์ฆ ์์คํ ์ ์ ์งํ๋ ๋ฐ ํ์์ ์ ๋๋ค.
ํฌ๊ด์ ์ธ ์ค๋ฅ ์ฒ๋ฆฌ ๊ตฌํ
class EmailVerificationError extends Error {
constructor(message, code, details = {}) {
super(message);
this.name = 'EmailVerificationError';
this.code = code;
this.details = details;
this.timestamp = new Date().toISOString();
}
}
async function safeEmailVerification(email) {
const startTime = Date.now();
try {
// ์
๋ ฅ ๊ฒ์ฆ
if (!email || typeof email !== 'string') {
throw new EmailVerificationError(
'Invalid email input',
'INVALID_INPUT',
{ received: typeof email }
);
}
const result = await comprehensiveEmailVerification(email);
// ์ฑ๊ณต์ ์ธ ๊ฒ์ฆ ๋ก๊น
logger.info('Email verification completed', {
email: maskEmail(email),
valid: result.valid,
duration: Date.now() - startTime
});
return result;
} catch (error) {
// ์ปจํ
์คํธ์ ํจ๊ป ์ค๋ฅ ๋ก๊น
logger.error('Email verification failed', {
email: maskEmail(email),
error: error.message,
code: error.code,
duration: Date.now() - startTime,
stack: error.stack
});
// ์์ ํ ์ค๋ฅ ์๋ต ๋ฐํ
return {
valid: false,
error: 'Verification failed',
errorCode: error.code || 'UNKNOWN_ERROR',
temporary: true
};
}
}
function maskEmail(email) {
const [local, domain] = email.split('@');
const maskedLocal = local.charAt(0) + '***' + local.charAt(local.length - 1);
return `${maskedLocal}@${domain}`;
}
๋ณด์ ๊ณ ๋ ค ์ฌํญ
์ด๋ฉ์ผ ๊ฒ์ฆ ์์คํ ์ ๋จ์ฉ์ ๋ฐฉ์งํ๊ณ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ๋ณดํธํ๊ธฐ ์ํด ๋ณด์์ ์ผ๋์ ๋๊ณ ์ค๊ณ๋์ด์ผ ํฉ๋๋ค.
์ด๊ฑฐ ๊ณต๊ฒฉ ๋ฐฉ์ง
๊ณต๊ฒฉ์๋ ์ด๋ฉ์ผ ๊ฒ์ฆ ์๋ํฌ์ธํธ๋ฅผ ์ฌ์ฉํ์ฌ ์ ํจํ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์ด๊ฑฐํ ์ ์์ต๋๋ค. ์ด ๊ณต๊ฒฉ ๋ฒกํฐ์ ๋ํ ๋ฐฉ์ด๋ฅผ ๊ตฌํํ์ธ์.
const crypto = require('crypto');
function secureVerificationResponse(result, options = {}) {
const { hideDetails = true } = options;
// ํ์ด๋ฐ ๊ณต๊ฒฉ์ ๋ฐฉ์งํ๊ธฐ ์ํด ์ผ๊ด๋ ์๋ต ์๊ฐ ์ถ๊ฐ
const minResponseTime = 200;
const elapsed = Date.now() - result.startTime;
const delay = Math.max(0, minResponseTime - elapsed);
return new Promise(resolve => {
setTimeout(() => {
if (hideDetails && !result.valid) {
// ์ด๋ฉ์ผ์ด ์กด์ฌํ๋์ง ๋๋ ๋๋ฉ์ธ์ด ์ ํจํ์ง ์์์ง ๊ณต๊ฐํ์ง ์์
resolve({
valid: false,
message: 'Unable to verify email address'
});
} else {
resolve(result);
}
}, delay);
});
}
์๋ ์ ํ ๋ฐ ๋จ์ฉ ๋ฐฉ์ง
๊ฒ์ฆ ์๋ํฌ์ธํธ์ ๋จ์ฉ์ ๋ฐฉ์งํ๊ธฐ ์ํด ํฌ๊ด์ ์ธ ์๋ ์ ํ์ ๊ตฌํํ์ธ์.
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const verificationRateLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:verify:'
}),
windowMs: 60 * 1000, // 1๋ถ
max: 5, // ๋ถ๋น 5๊ฐ ์์ฒญ
keyGenerator: (req) => {
// ์ธ์ฆ๋ ๊ฒฝ์ฐ IP์ ์ฌ์ฉ์ ID ๊ฒฐํฉ
const userId = req.user?.id || 'anonymous';
return `${req.ip}:${userId}`;
},
handler: (req, res) => {
res.status(429).json({
error: 'Too many verification requests',
retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
});
}
});
์ด๋ฉ์ผ ๊ฒ์ฆ ์์คํ ํ ์คํธ
ํฌ๊ด์ ์ธ ํ ์คํธ๋ ์ด๋ฉ์ผ ๊ฒ์ฆ ์์คํ ์ด ๋ชจ๋ ์๋๋ฆฌ์ค์์ ์ฌ๋ฐ๋ฅด๊ฒ ์๋ํ๋๋ก ๋ณด์ฅํฉ๋๋ค.
๊ฒ์ฆ ํจ์ ๋จ์ ํ ์คํธ
const { expect } = require('chai');
describe('Email Syntax Validation', () => {
it('should accept valid email addresses', () => {
const validEmails = [
'user@example.com',
'user.name@example.com',
'user+tag@example.com',
'user@subdomain.example.com'
];
validEmails.forEach(email => {
const result = validateEmailSyntax(email);
expect(result.valid).to.be.true;
});
});
it('should reject invalid email addresses', () => {
const invalidEmails = [
'invalid',
'@example.com',
'user@',
'user@@example.com',
'user@example',
'user@.com'
];
invalidEmails.forEach(email => {
const result = validateEmailSyntax(email);
expect(result.valid).to.be.false;
});
});
it('should handle edge cases', () => {
expect(validateEmailSyntax('')).to.have.property('valid', false);
expect(validateEmailSyntax(null)).to.have.property('valid', false);
expect(validateEmailSyntax(undefined)).to.have.property('valid', false);
});
});
๊ฒฐ๋ก
๊ฐ๋ฐ์๋ก์ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ๊ตฌํํ๋ ค๋ฉด ๊ธฐ๋ณธ ๊ตฌ๋ฌธ ๊ฒ์ฌ๋ถํฐ ๊ณ ๊ธ SMTP ๊ฒ์ฆ๊น์ง ์ฌ๋ฌ ๊ฒ์ฆ ๋ ์ด์ด๋ฅผ ์ดํดํด์ผ ํฉ๋๋ค. ๋ก์ปฌ ๊ฒ์ฆ, DNS ์กฐํ ๋ฐ BillionVerify์ ๊ฐ์ ์ ๋ฌธ ๊ฒ์ฆ API๋ฅผ ๊ฒฐํฉํจ์ผ๋ก์จ ๊ฐ๋ฐ์๋ ์ฐ์ํ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ๋ฉด์ ๋์ ๋ฐ์ดํฐ ํ์ง์ ์ ์งํ๋ ๊ฐ๋ ฅํ ์์คํ ์ ๊ตฌ์ถํ ์ ์์ต๋๋ค.
์ฑ๊ณต์ ์ธ ์ด๋ฉ์ผ ๊ฒ์ฆ ๊ตฌํ์ ํต์ฌ ์์น์๋ ํฌ๊ด์ ์ธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ํ ๋ค์ค ๊ฒ์ฆ ๋ ์ด์ด ์ฌ์ฉ, ์ฑ๋ฅ ๋ฐ ๋ณด์์ ์ํ ์ ์ ํ ์บ์ฑ ๋ฐ ์๋ ์ ํ ๊ตฌํ, ์ฃ์ง ์ผ์ด์ค ๋ฐ ์ค๋ฅ์ ์ฐ์ํ ์ฒ๋ฆฌ, ๊ฒ์ฆ ์ ํ๋์ ์ง์์ ์ธ ๋ชจ๋ํฐ๋ง ๋ฐ ๊ฐ์ ์ด ํฌํจ๋ฉ๋๋ค.
์ฌ์ฉ์ ์ง์ ๊ฒ์ฆ ๋ ผ๋ฆฌ๋ฅผ ๊ตฌํํ๋ ์ ๋ฌธ API๋ฅผ ํ์ฉํ๋ ์ด ๊ฐ์ด๋์์ ๋ค๋ฃฌ ๊ธฐ์ ์ ์ ํ๋ฆฌ์ผ์ด์ ๊ณผ ์ฌ์ฉ์๋ฅผ ๋ณดํธํ๋ฉด์ ์ต๊ณ ์์ค์ ์ ๋ฌ์ฑ ๋ฐ ์ฐธ์ฌ๋ฅผ ์ ์งํ๋ ์ด๋ฉ์ผ ๊ฒ์ฆ ์์คํ ์ ๊ตฌ์ถํ๊ธฐ ์ํ ๊ธฐ์ด๋ฅผ ์ ๊ณตํฉ๋๋ค. ๊ฐ๋ ฅํ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ๋ชจ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ํ์ ์์์ ๋๋ค.
BillionVerify์ ๊ฐ๋ฐ์ ์นํ์ ์ธ API๋ก ์ค๋ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ด๋ฉ์ผ ๊ฒ์ฆ ๊ตฌํ์ ์์ํ์ธ์. BillionVerify์ ์ด๋ฉ์ผ ๊ฒ์ฆ API ๊ฐ์ด๋๋ฅผ ์ฐธ๊ณ ํ๋ฉด์ ๊ฐ์ ํ์ฌ ๋ฌด๋ฃ ๊ฒ์ฆ ํฌ๋ ๋ง๊ณผ ํฌ๊ด์ ์ธ ๋ฌธ์๋ฅผ ๋ฐ์ผ์ธ์. ๋ํ ์ด๋ฉ์ผ ๋ง์ผํ ๋ชจ๋ฒ ์ฌ๋ก, ์ ๋ฌ์ฑ ๊ฐ์ ๋ฅผ ์ฐธ๊ณ ํ์ฌ ๊ฒ์ฆ์ด ์ ์ฒด ์์คํ ์ ๋ฏธ์น๋ ์ํฅ์ ๊ทน๋ํํ์ธ์.