๋ชจ๋ ์ด๋ฉ์ผ์ ์ ๊ตํ๊ฒ ๊ตฌ์ฑ๋ ์๋ฒ ๋คํธ์ํฌ๋ฅผ ํตํด ์ ์ก๋๋ฉฐ, MX(Mail Exchange) ๋ ์ฝ๋๋ ์ด ์ฌ์ ์ ์๋ดํ๋ ์ด์ ํ ์ญํ ์ ํฉ๋๋ค. MX ๋ ์ฝ๋ ๊ฒ์ฆ ๋ฐฉ๋ฒ์ ์ดํดํ๋ ๊ฒ์ ์ด๋ฉ์ผ ๊ฒ์ฆ ์์คํ , ๋ฌธ์ ์์ ๋๋ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์์งํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ๋ ๋ชจ๋ ๊ฐ๋ฐ์์๊ฒ ํ์์ ์ธ ๊ธฐ์ ์ ๋๋ค. ์ด ํฌ๊ด์ ์ธ ๊ฐ์ด๋๋ ๊ธฐ๋ณธ ๊ฐ๋ ๋ถํฐ ๊ณ ๊ธ ๊ตฌํ ์ ๋ต๊น์ง MX ๋ ์ฝ๋ ๊ฒ์ฆ์ ๋ค๋ฃจ๋ฉฐ, ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋ ฅํ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ๊ตฌ์ถํ ์ ์๋ ์ง์์ ์ ๊ณตํฉ๋๋ค.
MX ๋ ์ฝ๋ ์ดํดํ๊ธฐ
MX(Mail Exchange) ๋ ์ฝ๋๋ ๋๋ฉ์ธ์ ๋์ ํ์ฌ ์ด๋ฉ์ผ์ ์์ ํ๋ ๋ฉ์ผ ์๋ฒ๋ฅผ ์ง์ ํ๋ DNS ๋ ์ฝ๋์ ๋๋ค. user@example.com์ผ๋ก ์ด๋ฉ์ผ์ ๋ณด๋ผ ๋, ๋ฉ์ผ ์๋ฒ๋ ์ด๋๋ก ์ ๋ฌํด์ผ ํ๋์ง ์์์ผ ํฉ๋๋ค. MX ๋ ์ฝ๋๋ ๋๋ฉ์ธ์ ๋ฉ์ผ ์๋ฒ๋ฅผ ๊ฐ๋ฆฌํด์ผ๋ก์จ ์ด ์ ๋ณด๋ฅผ ์ ๊ณตํฉ๋๋ค.
MX ๋ ์ฝ๋ ์๋ ์๋ฆฌ
์ด๋ฉ์ผ์ด ์ ์ก๋๋ฉด, ๋ฐ์ ๋ฉ์ผ ์๋ฒ๋ ์์ ์ ๋๋ฉ์ธ์ MX ๋ ์ฝ๋๋ฅผ ์ฐพ๊ธฐ ์ํด DNS ์กฐํ๋ฅผ ์ํํฉ๋๋ค. ์ด ์กฐํ๋ ์ฐ์ ์์ ๊ฐ๊ณผ ํจ๊ป ํ๋ ์ด์์ ๋ฉ์ผ ์๋ฒ ํธ์คํธ๋ช ์ ๋ฐํํ๋ฉฐ, ์ฐ์ ์์ ๊ฐ์ ์ ํธ ์์๋ฅผ ๋ํ๋ ๋๋ค.
gmail.com์ ์ผ๋ฐ์ ์ธ MX ๋ ์ฝ๋ ์กฐํ๋ ๋ค์๊ณผ ๊ฐ์ ์ ์์ต๋๋ค:
gmail.com. MX 5 gmail-smtp-in.l.google.com. gmail.com. MX 10 alt1.gmail-smtp-in.l.google.com. gmail.com. MX 20 alt2.gmail-smtp-in.l.google.com. gmail.com. MX 30 alt3.gmail-smtp-in.l.google.com. gmail.com. MX 40 alt4.gmail-smtp-in.l.google.com.
๋ฐ์ ์๋ฒ๋ ๊ฐ์ฅ ๋ฎ์ ์ฐ์ ์์ ์๋ฒ(์ด ๊ฒฝ์ฐ ์ฐ์ ์์ 5)๋ก ๋จผ์ ์ ๋ฌ์ ์๋ํฉ๋๋ค. ํด๋น ์๋ฒ๋ฅผ ์ฌ์ฉํ ์ ์์ผ๋ฉด ๋ค์ ์ฐ์ ์์ ์์ค์ ์๋ํ๋ ์์ ๋๋ค. ์ด๋ฌํ ์ค๋ณต์ฑ์ ๊ฐ๋ณ ์๋ฒ๊ฐ ๋ค์ด๋์ด๋ ์ด๋ฉ์ผ ์ ๋ฌ์ ๋ณด์ฅํฉ๋๋ค.
MX ๋ ์ฝ๋ ๊ตฌ์ฑ ์์
๊ฐ MX ๋ ์ฝ๋์๋ ๋ ๊ฐ์ง ํ์ ์ ๋ณด๊ฐ ํฌํจ๋ฉ๋๋ค:
์ฐ์ ์์(Preference)
๋ฉ์ผ ์๋ฒ๋ฅผ ์๋ํด์ผ ํ๋ ์์๋ฅผ ๋ํ๋ด๋ ์ซ์ ๊ฐ์ ๋๋ค. ๋ฎ์ ์ซ์๊ฐ ๋์ ์ฐ์ ์์๋ฅผ ๋ํ๋ ๋๋ค. ๋์ผํ ์ฐ์ ์์๋ฅผ ๊ฐ์ง ์๋ฒ๋ ๋ฌด์์ ์์๋ก ์๋๋์ด ๋ก๋ ๋ฐธ๋ฐ์ฑ์ ์ ๊ณตํฉ๋๋ค.
๋ฉ์ผ ์๋ฒ ํธ์คํธ๋ช
๋๋ฉ์ธ์ ์ด๋ฉ์ผ์ ์ฒ๋ฆฌํ๋ ๋ฉ์ผ ์๋ฒ์ ์ ๊ทํ๋ ๋๋ฉ์ธ ์ด๋ฆ(FQDN)์ ๋๋ค. ์ด ํธ์คํธ๋ช ์ A ๋๋ AAAA ๋ ์ฝ๋๋ฅผ ํตํด IP ์ฃผ์๋ก ํ์ธ๋์ด์ผ ํฉ๋๋ค.
์ด๋ฉ์ผ ๊ฒ์ฆ์์ MX ๋ ์ฝ๋๊ฐ ์ค์ํ ์ด์
MX ๋ ์ฝ๋ ๊ฒ์ฆ์ ์ด๋ฉ์ผ ๊ฒ์ฆ ํ๋ก์ธ์ค์์ ์ค์ํ ์ฒดํฌํฌ์ธํธ ์ญํ ์ ํฉ๋๋ค:
๋๋ฉ์ธ ์กด์ฌ ํ์ธ
๋๋ฉ์ธ์ MX ๋ ์ฝ๋๊ฐ ์์ผ๋ฉด ์ผ๋ฐ์ ์ผ๋ก ์ด๋ฉ์ผ์ ์์ ํ ์ ์์ต๋๋ค. ์ผ๋ถ ๋๋ฉ์ธ์ A ๋ ์ฝ๋ ๋์ฒด๋ฅผ ์ฌ์ฉํ ์ ์์ง๋ง, MX ๋ ์ฝ๋๊ฐ ์๋ค๋ ๊ฒ์ ๋๋ฉ์ธ์ด ์ด๋ฉ์ผ์ฉ์ผ๋ก ๊ตฌ์ฑ๋์ง ์์์์ ๋ํ๋ด๋ ๊ฐ๋ ฅํ ์งํ์ ๋๋ค.
์ธํ๋ผ ๊ฒ์ฆ
์๋ํ๋ ๋ฉ์ผ ์๋ฒ๋ก ํ์ธ๋๋ ์ ํจํ MX ๋ ์ฝ๋๋ ๋๋ฉ์ธ์ ์ด๋ฉ์ผ ์ธํ๋ผ๊ฐ ์์์ ๋ํ๋ ๋๋ค. ํน์ ์ฃผ์๊ฐ ์กด์ฌํ๋ค๋ ๊ฒ์ ๋ณด์ฅํ์ง๋ ์์ง๋ง, ๋๋ฉ์ธ์ด ์ด๋ฉ์ผ์ ์์ ํ ์ ์์์ ํ์ธํฉ๋๋ค.
์คํธ ๋ฐ ์ฌ๊ธฐ ํ์ง
ํฉ๋ฒ์ ์ธ ๋น์ฆ๋์ค๋ ์ ์ ํ MX ๋ ์ฝ๋๋ฅผ ์ ์งํฉ๋๋ค. ์คํธ์ด๋ ์ฌ๊ธฐ์ ์ฌ์ฉ๋๋ ์์ฌ์ค๋ฌ์ด ๋๋ฉ์ธ์ MX ๋ ์ฝ๋๊ฐ ์๋ชป ๊ตฌ์ฑ๋์๊ฑฐ๋ ๋๋ฝ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค.
์ฑ๋ฅ ์ต์ ํ
SMTP ๊ฒ์ฆ์ ์๋ํ๊ธฐ ์ ์ MX ๋ ์ฝ๋๋ฅผ ํ์ธํ๋ฉด ์ด๋ฉ์ผ์ ์์ ํ ์ ์๋ ๋๋ฉ์ธ์ ์ฐ๊ฒฐํ๋ ๋ฐ ๋ญ๋น๋๋ ์๊ฐ์ ํผํ ์ ์์ต๋๋ค.
MX ๋ ์ฝ๋ ์กฐํ ๊ตฌํํ๊ธฐ
๋ค์ํ ํ๋ก๊ทธ๋๋ฐ ํ๊ฒฝ์์ MX ๋ ์ฝ๋ ๊ฒ์ฆ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
Node.js ๊ตฌํ
Node.js๋ dns ๋ชจ๋์ ํตํด ๋ด์ฅ DNS ํ์ธ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค:
const dns = require('dns').promises;
async function getMxRecords(domain) {
try {
const records = await dns.resolveMx(domain);
// Sort by priority (lowest first)
records.sort((a, b) => a.priority - b.priority);
return {
success: true,
domain,
records: records.map(r => ({
exchange: r.exchange,
priority: r.priority
}))
};
} catch (error) {
return {
success: false,
domain,
error: error.code,
message: getMxErrorMessage(error.code)
};
}
}
function getMxErrorMessage(code) {
const messages = {
'ENODATA': 'No MX records found for this domain',
'ENOTFOUND': 'Domain does not exist',
'ETIMEOUT': 'DNS lookup timed out',
'ESERVFAIL': 'DNS server failed to respond'
};
return messages[code] || 'Unknown DNS error';
}
// Usage
const result = await getMxRecords('gmail.com');
console.log(result);
Python ๊ตฌํ
Python์ dnspython ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ dns.resolver ๋ชจ๋์ ํฌ๊ด์ ์ธ DNS ์กฐํ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค:
import dns.resolver
import dns.exception
def get_mx_records(domain):
try:
answers = dns.resolver.resolve(domain, 'MX')
records = []
for rdata in answers:
records.append({
'exchange': str(rdata.exchange).rstrip('.'),
'priority': rdata.preference
})
# Sort by priority
records.sort(key=lambda x: x['priority'])
return {
'success': True,
'domain': domain,
'records': records
}
except dns.resolver.NXDOMAIN:
return {
'success': False,
'domain': domain,
'error': 'NXDOMAIN',
'message': 'Domain does not exist'
}
except dns.resolver.NoAnswer:
return {
'success': False,
'domain': domain,
'error': 'NoAnswer',
'message': 'No MX records found for this domain'
}
except dns.exception.Timeout:
return {
'success': False,
'domain': domain,
'error': 'Timeout',
'message': 'DNS lookup timed out'
}
# Usage
result = get_mx_records('gmail.com')
print(result)
Go ๊ตฌํ
Go์ net ํจํค์ง๋ ๊ฐ๋จํ DNS ์กฐํ ํจ์๋ฅผ ์ ๊ณตํฉ๋๋ค:
package main
import (
"fmt"
"net"
"sort"
)
type MxResult struct {
Success bool
Domain string
Records []MxRecord
Error string
}
type MxRecord struct {
Exchange string
Priority uint16
}
func getMxRecords(domain string) MxResult {
records, err := net.LookupMX(domain)
if err != nil {
return MxResult{
Success: false,
Domain: domain,
Error: err.Error(),
}
}
if len(records) == 0 {
return MxResult{
Success: false,
Domain: domain,
Error: "No MX records found",
}
}
// Sort by priority
sort.Slice(records, func(i, j int) bool {
return records[i].Pref < records[j].Pref
})
result := MxResult{
Success: true,
Domain: domain,
Records: make([]MxRecord, len(records)),
}
for i, r := range records {
result.Records[i] = MxRecord{
Exchange: r.Host,
Priority: r.Pref,
}
}
return result
}
func main() {
result := getMxRecords("gmail.com")
fmt.Printf("%+v\n", result)
}
๊ณ ๊ธ MX ๊ฒ์ฆ ๊ธฐ๋ฒ
๊ธฐ๋ณธ MX ์กฐํ๋ ๋ ์ฝ๋๊ฐ ์กด์ฌํ๋์ง ํ์ธํ์ง๋ง, ํฌ๊ด์ ์ธ ์ด๋ฉ์ผ ๊ฒ์ฆ์๋ ๋ ๊น์ ๋ถ์์ด ํ์ํฉ๋๋ค.
๋ฉ์ผ ์๋ฒ ์ฐ๊ฒฐ์ฑ ๊ฒ์ฆ
MX ๋ ์ฝ๋๋ IP ์ฃผ์๋ก ํ์ธ๋์ด์ผ ํ๋ ํธ์คํธ๋ช ์ ๊ฐ๋ฆฌํต๋๋ค. ๋ฉ์ผ ์๋ฒ๊ฐ ์ค์ ๋ก ๋๋ฌ ๊ฐ๋ฅํ์ง ํ์ธํ์ธ์:
const dns = require('dns').promises;
const net = require('net');
async function validateMxConnectivity(domain) {
// Get MX records
const mxResult = await getMxRecords(domain);
if (!mxResult.success) {
return mxResult;
}
// Validate each mail server
const validatedRecords = [];
for (const record of mxResult.records) {
const validation = await validateMailServer(record.exchange);
validatedRecords.push({
...record,
...validation
});
}
return {
success: true,
domain,
records: validatedRecords,
hasReachableServer: validatedRecords.some(r => r.reachable)
};
}
async function validateMailServer(hostname) {
try {
// Resolve hostname to IP
const addresses = await dns.resolve4(hostname);
if (addresses.length === 0) {
return { reachable: false, error: 'No A record' };
}
// Test connection to port 25
const connected = await testConnection(addresses[0], 25);
return {
reachable: connected,
ip: addresses[0],
error: connected ? null : 'Connection refused'
};
} catch (error) {
return {
reachable: false,
error: error.message
};
}
}
function testConnection(host, port, timeout = 5000) {
return new Promise((resolve) => {
const socket = new net.Socket();
socket.setTimeout(timeout);
socket.on('connect', () => {
socket.destroy();
resolve(true);
});
socket.on('timeout', () => {
socket.destroy();
resolve(false);
});
socket.on('error', () => {
resolve(false);
});
socket.connect(port, host);
});
}
A ๋ ์ฝ๋ ๋์ฒด ์ฒ๋ฆฌ
MX ๋ ์ฝ๋๊ฐ ์์ ๋, ์ด๋ฉ์ผ ํ์ค(RFC 5321)์ ๋๋ฉ์ธ์ A ๋ ์ฝ๋๋ฅผ ๋์ฒด๋ก ์ฌ์ฉํด์ผ ํ๋ค๊ณ ์ง์ ํฉ๋๋ค. ๊ฒ์ฆ์์ ์ด ๋์ฒด๋ฅผ ๊ตฌํํ์ธ์:
async function getMailServers(domain) {
// Try MX records first
try {
const mxRecords = await dns.resolveMx(domain);
if (mxRecords.length > 0) {
return {
type: 'MX',
servers: mxRecords.sort((a, b) => a.priority - b.priority)
};
}
} catch (error) {
if (error.code !== 'ENODATA') {
throw error;
}
}
// Fallback to A record
try {
const aRecords = await dns.resolve4(domain);
if (aRecords.length > 0) {
return {
type: 'A_FALLBACK',
servers: [{ exchange: domain, priority: 0 }],
warning: 'Using A record fallback - no MX records found'
};
}
} catch (error) {
if (error.code !== 'ENODATA') {
throw error;
}
}
return {
type: 'NONE',
servers: [],
error: 'No mail servers found for domain'
};
}
Null MX ๋ ์ฝ๋ ๊ฐ์ง
RFC 7505๋ ๋๋ฉ์ธ์ด ์ด๋ฉ์ผ์ ์์ ํ์ง ์์์ ๋ช ์์ ์ผ๋ก ๋ํ๋ด๋ "null MX" ๋ ์ฝ๋๋ฅผ ์ ์ํฉ๋๋ค. ์ด๋ฌํ ๋ ์ฝ๋๋ ์ฐ์ ์์ 0๊ณผ ๋น ํธ์คํธ๋ช (".")์ ๊ฐ์ง ๋จ์ผ MX ํญ๋ชฉ์ ๊ฐ์ง๋๋ค:
function hasNullMx(mxRecords) {
if (mxRecords.length === 1) {
const record = mxRecords[0];
if (record.priority === 0 &&
(record.exchange === '.' || record.exchange === '')) {
return true;
}
}
return false;
}
async function validateDomainMx(domain) {
const mxResult = await getMxRecords(domain);
if (!mxResult.success) {
return mxResult;
}
if (hasNullMx(mxResult.records)) {
return {
success: false,
domain,
error: 'NULL_MX',
message: 'Domain explicitly does not accept email'
};
}
return mxResult;
}
MX ์กฐํ ์บ์ฑ
DNS ์กฐํ๋ ๋ชจ๋ ๊ฒ์ฆ์ ์ง์ฐ์ ์ถ๊ฐํฉ๋๋ค. ์ฑ๋ฅ์ ๊ฐ์ ํ๊ธฐ ์ํด ์บ์ฑ์ ๊ตฌํํ์ธ์:
class MxCache {
constructor(ttlMs = 3600000) { // 1 hour default TTL
this.cache = new Map();
this.ttl = ttlMs;
}
get(domain) {
const entry = this.cache.get(domain.toLowerCase());
if (!entry) return null;
if (Date.now() > entry.expiry) {
this.cache.delete(domain.toLowerCase());
return null;
}
return entry.data;
}
set(domain, data) {
this.cache.set(domain.toLowerCase(), {
data,
expiry: Date.now() + this.ttl
});
}
// Respect DNS TTL values when available
setWithTtl(domain, data, ttlSeconds) {
const ttlMs = Math.min(ttlSeconds * 1000, this.ttl);
this.cache.set(domain.toLowerCase(), {
data,
expiry: Date.now() + ttlMs
});
}
}
const mxCache = new MxCache();
async function getMxRecordsCached(domain) {
const cached = mxCache.get(domain);
if (cached) {
return { ...cached, fromCache: true };
}
const result = await getMxRecords(domain);
if (result.success) {
mxCache.set(domain, result);
}
return { ...result, fromCache: false };
}
์ผ๋ฐ์ ์ธ MX ๋ ์ฝ๋ ํจํด
์ผ๋ฐ์ ์ธ MX ๊ตฌ์ฑ์ ์ดํดํ๋ฉด ๊ฒ์ฆ ๊ฒฐ๊ณผ๋ฅผ ํด์ํ๊ณ ์ ์ฌ์ ์ธ ๋ฌธ์ ๋ฅผ ์๋ณํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
์ฃผ์ ์ด๋ฉ์ผ ์ ๊ณต์ ์ฒด
์ฃผ์ ์ ๊ณต์ ์ฒด์ MX ํจํด์ ์ธ์ํ๋ฉด ๋ฌด๋ฃ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์๋ณํ๋ ๋ฐ ๋์์ด ๋ ์ ์์ต๋๋ค:
const knownProviders = {
'google': [
'gmail-smtp-in.l.google.com',
'googlemail-smtp-in.l.google.com',
'aspmx.l.google.com'
],
'microsoft': [
'outlook-com.olc.protection.outlook.com',
'mail.protection.outlook.com'
],
'yahoo': [
'mta5.am0.yahoodns.net',
'mta6.am0.yahoodns.net',
'mta7.am0.yahoodns.net'
],
'protonmail': [
'mail.protonmail.ch',
'mailsec.protonmail.ch'
]
};
function identifyEmailProvider(mxRecords) {
const exchanges = mxRecords.map(r => r.exchange.toLowerCase());
for (const [provider, patterns] of Object.entries(knownProviders)) {
for (const pattern of patterns) {
if (exchanges.some(ex => ex.includes(pattern.toLowerCase()))) {
return provider;
}
}
}
return 'unknown';
}
Google Workspace ๊ฐ์ง
Google Workspace(์ด์ G Suite) ๋๋ฉ์ธ์ Google์ ๋ฉ์ผ ์๋ฒ๋ฅผ ์ฌ์ฉํ์ง๋ง ๋ฌด๋ฃ ์ด๋ฉ์ผ ๊ณ์ ์ด ์๋๋๋ค:
function isGoogleWorkspace(domain, mxRecords) {
const isGoogleMx = mxRecords.some(r =>
r.exchange.toLowerCase().includes('google') ||
r.exchange.toLowerCase().includes('googlemail')
);
// Check if domain is not a known Google consumer domain
const googleConsumerDomains = ['gmail.com', 'googlemail.com'];
const isConsumerDomain = googleConsumerDomains.includes(domain.toLowerCase());
return isGoogleMx && !isConsumerDomain;
}
์์ฒด ํธ์คํ ์ด๋ฉ์ผ ๊ฐ์ง
์์ฒด ์ด๋ฉ์ผ์ ํธ์คํ ํ๋ ๋๋ฉ์ธ์ ์๋ธ๋๋ฉ์ธ์ ๊ฐ๋ฆฌํค๋ MX ๋ ์ฝ๋๋ฅผ ๊ฐ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค:
function isSelfHosted(domain, mxRecords) {
const domainParts = domain.toLowerCase().split('.');
const baseDomain = domainParts.slice(-2).join('.');
return mxRecords.some(r => {
const exchange = r.exchange.toLowerCase();
return exchange.includes(baseDomain) &&
!isKnownProvider(exchange);
});
}
function isKnownProvider(exchange) {
const providers = ['google', 'microsoft', 'yahoo', 'outlook', 'protonmail'];
return providers.some(p => exchange.includes(p));
}
์ด๋ฉ์ผ ๊ฒ์ฆ ํ์ดํ๋ผ์ธ์์์ MX ๊ฒ์ฆ
MX ๊ฒ์ฆ์ ํฌ๊ด์ ์ธ ์ด๋ฉ์ผ ๊ฒ์ฆ ํ๋ก์ธ์ค์ ํ ๋จ๊ณ์ ๋๋ค. ๊ทธ ์ญํ ์ ์ดํดํ๋ฉด ํจ๊ณผ์ ์ธ ๊ฒ์ฆ ํ์ดํ๋ผ์ธ์ ๊ตฌ์ถํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
๊ฒ์ฆ ์์
MX ๊ฒ์ฆ์ ์ผ๋ฐ์ ์ผ๋ก ๊ฒ์ฆ ํ์ดํ๋ผ์ธ ์ด๊ธฐ์ ๋ฐ์ํฉ๋๋ค:
async function verifyEmail(email) {
// 1. Syntax validation (fastest, no network)
const syntaxResult = validateEmailSyntax(email);
if (!syntaxResult.valid) {
return { valid: false, reason: 'invalid_syntax', details: syntaxResult };
}
const domain = email.split('@')[1];
// 2. MX record validation (fast DNS lookup)
const mxResult = await validateMxRecords(domain);
if (!mxResult.valid) {
return { valid: false, reason: 'no_mx_records', details: mxResult };
}
// 3. Additional checks (disposable, role-based, etc.)
const domainCheck = await checkDomainReputation(domain);
if (domainCheck.isDisposable) {
return { valid: true, risky: true, reason: 'disposable_domain' };
}
// 4. SMTP verification (slowest, most thorough)
const smtpResult = await verifySmtp(email, mxResult.records);
return {
valid: smtpResult.exists,
deliverable: smtpResult.deliverable,
mxRecords: mxResult.records,
provider: mxResult.provider
};
}
๋ณ๋ ฌ MX ์กฐํ
์ฌ๋ฌ ์ด๋ฉ์ผ์ ๊ฒ์ฆํ ๋, ์๋ก ๋ค๋ฅธ ๋๋ฉ์ธ์ MX ์กฐํ๋ฅผ ๋ณ๋ ฌํํ์ธ์:
async function verifyEmailsBatch(emails) {
// Group emails by domain
const emailsByDomain = {};
for (const email of emails) {
const domain = email.split('@')[1];
if (!emailsByDomain[domain]) {
emailsByDomain[domain] = [];
}
emailsByDomain[domain].push(email);
}
// Lookup MX records for all domains in parallel
const domains = Object.keys(emailsByDomain);
const mxResults = await Promise.all(
domains.map(domain => getMxRecordsCached(domain))
);
// Map results back to domains
const mxByDomain = {};
domains.forEach((domain, index) => {
mxByDomain[domain] = mxResults[index];
});
// Process emails with MX data
const results = [];
for (const email of emails) {
const domain = email.split('@')[1];
const mx = mxByDomain[domain];
if (!mx.success) {
results.push({ email, valid: false, reason: 'invalid_domain' });
continue;
}
// Continue with SMTP verification using cached MX data
const smtpResult = await verifySmtp(email, mx.records);
results.push({ email, ...smtpResult });
}
return results;
}
์ค๋ฅ ์ฒ๋ฆฌ ๋ฐ ์ฃ์ง ์ผ์ด์ค
๊ฐ๋ ฅํ MX ๊ฒ์ฆ์ ๋ค์ํ ์ค๋ฅ ์กฐ๊ฑด์ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํฉ๋๋ค.
DNS ํ์์์ ์ฒ๋ฆฌ
๋คํธ์ํฌ ๋ฌธ์ ๋ก ์ธํด DNS ์กฐํ๊ฐ ์ค๋จ๋ ์ ์์ต๋๋ค. ํ์์์ ์ฒ๋ฆฌ๋ฅผ ๊ตฌํํ์ธ์:
async function getMxRecordsWithTimeout(domain, timeoutMs = 10000) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('DNS_TIMEOUT')), timeoutMs);
});
try {
const result = await Promise.race([
getMxRecords(domain),
timeoutPromise
]);
return result;
} catch (error) {
if (error.message === 'DNS_TIMEOUT') {
return {
success: false,
domain,
error: 'TIMEOUT',
message: 'DNS lookup timed out',
retryable: true
};
}
throw error;
}
}
์๋ชป๋ ๋๋ฉ์ธ ์ฒ๋ฆฌ
DNS ์กฐํ๋ฅผ ์๋ํ๊ธฐ ์ ์ ๊ตฌ๋ฌธ์ ์ผ๋ก ์ ํจํ์ง ์์ ๋๋ฉ์ธ์ ์ฒ๋ฆฌํ์ธ์:
function isValidDomain(domain) {
if (!domain || typeof domain !== 'string') {
return false;
}
// Check length
if (domain.length > 253) {
return false;
}
// Check for valid characters and structure
const domainRegex = /^(?!-)[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
if (!domainRegex.test(domain)) {
return false;
}
// Check each label length
const labels = domain.split('.');
for (const label of labels) {
if (label.length > 63) {
return false;
}
}
return true;
}
async function validateDomainMxSafe(domain) {
if (!isValidDomain(domain)) {
return {
success: false,
domain,
error: 'INVALID_DOMAIN',
message: 'Domain format is invalid'
};
}
return await getMxRecordsWithTimeout(domain);
}
์ผ์์ ์ธ DNS ์คํจ ์ฒ๋ฆฌ
DNS ์คํจ๋ ์ผ์์ ์ผ ์ ์์ต๋๋ค. ์ง์ ๋ฐฑ์คํ๋ฅผ ์ฌ์ฉํ ์ฌ์๋ ๋ก์ง์ ๊ตฌํํ์ธ์:
async function getMxRecordsWithRetry(domain, maxRetries = 3) {
const delays = [1000, 2000, 4000];
for (let attempt = 0; attempt < maxRetries; attempt++) {
const result = await getMxRecordsWithTimeout(domain);
// Don't retry for definitive failures
if (result.success ||
result.error === 'NXDOMAIN' ||
result.error === 'ENOTFOUND') {
return result;
}
// Retry for temporary failures
if (result.retryable && attempt < maxRetries - 1) {
await sleep(delays[attempt]);
continue;
}
return result;
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
๋ณด์ ๊ณ ๋ ค์ฌํญ
MX ๊ฒ์ฆ์ ๊ฐ๋ฐ์๊ฐ ํด๊ฒฐํด์ผ ํ๋ ๋ณด์ ๊ณ ๋ ค์ฌํญ์ ๋์ ํฉ๋๋ค.
DNS ์คํธํ ๋ฐฉ์ง
ํ์ค DNS ์ฟผ๋ฆฌ๋ ์ํธํ๋์ง ์์ผ๋ฉฐ ์คํธํ์ ์ทจ์ฝํฉ๋๋ค. ๋ฏผ๊ฐํ ์ ํ๋ฆฌ์ผ์ด์ ์๋ DNS over HTTPS(DoH) ์ฌ์ฉ์ ๊ณ ๋ คํ์ธ์:
const https = require('https');
async function getMxRecordsDoH(domain) {
const url = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=MX`;
return new Promise((resolve, reject) => {
https.get(url, {
headers: { 'Accept': 'application/dns-json' }
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const response = JSON.parse(data);
if (response.Status !== 0) {
resolve({
success: false,
domain,
error: 'DNS_ERROR',
status: response.Status
});
return;
}
const records = (response.Answer || [])
.filter(a => a.type === 15)
.map(a => {
const [priority, exchange] = a.data.split(' ');
return {
priority: parseInt(priority),
exchange: exchange.replace(/\.$/, '')
};
})
.sort((a, b) => a.priority - b.priority);
resolve({
success: records.length > 0,
domain,
records
});
} catch (error) {
reject(error);
}
});
}).on('error', reject);
});
}
DNS ์ฟผ๋ฆฌ ์๋ ์ ํ
DNS ์ฟผ๋ฆฌ์ ์๋๋ฅผ ์ ํํ์ฌ ๋จ์ฉ์ ๋ฐฉ์งํ์ธ์:
class DnsRateLimiter {
constructor(maxQueriesPerSecond = 100) {
this.tokens = maxQueriesPerSecond;
this.maxTokens = maxQueriesPerSecond;
this.lastRefill = Date.now();
}
async acquire() {
this.refillTokens();
if (this.tokens > 0) {
this.tokens--;
return true;
}
// Wait for token availability
await sleep(1000 / this.maxTokens);
return this.acquire();
}
refillTokens() {
const now = Date.now();
const elapsed = now - this.lastRefill;
const tokensToAdd = (elapsed / 1000) * this.maxTokens;
this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
const dnsLimiter = new DnsRateLimiter(50);
async function getMxRecordsRateLimited(domain) {
await dnsLimiter.acquire();
return getMxRecords(domain);
}
BillionVerify๋ฅผ ์ฌ์ฉํ MX ๊ฒ์ฆ
MX ๊ฒ์ฆ์ ์ง์ ๊ตฌํํ๋ ๊ฒ์ ๊ต์ก์ ๊ฐ์น๋ฅผ ์ ๊ณตํ์ง๋ง, BillionVerify์ ๊ฐ์ ์ ๋ฌธ ์ด๋ฉ์ผ ๊ฒ์ฆ ์๋น์ค๋ ํฌ๊ด์ ์ธ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ์ผ๋ถ๋ก MX ๊ฒ์ฆ์ ์ฒ๋ฆฌํฉ๋๋ค.
์ด๋ฉ์ผ ๊ฒ์ฆ API ์ฌ์ฉ์ ์ฅ์
ํฌ๊ด์ ์ธ ๊ฒ์ฌ
BillionVerify์ ์ด๋ฉ์ผ ๊ฒ์ฆ API๋ ๋จ์ผ API ํธ์ถ์์ ๊ตฌ๋ฌธ ๊ฒ์ฌ, SMTP ๊ฒ์ฆ, ์ผํ์ฉ ์ด๋ฉ์ผ ๊ฐ์ง ๋ฑ๊ณผ ํจ๊ป MX ๊ฒ์ฆ์ ๊ฒฐํฉํฉ๋๋ค. ์ด๋ฅผ ํตํด ์ฌ๋ฌ ๊ฒ์ฆ ์์คํ ์ ์ ์ง ๊ด๋ฆฌํ ํ์๊ฐ ์์ต๋๋ค.
์ต์ ํ๋ ์ธํ๋ผ
์ ๋ฌธ ์๋น์ค๋ ์ ์ธ๊ณ์ ๋ถ์ฐ๋ DNS ๋ฆฌ์กธ๋ฒ๋ฅผ ์ ์งํ๊ณ , ๋๊ท๋ชจ ์บ์ฑ์ ์ฒ๋ฆฌํ๋ฉฐ, ์๋ฐฑ๋ง ๊ฑด์ ๊ฒ์ฆ์ ๊ฑธ์ณ ์ฑ๋ฅ์ ์ต์ ํํฉ๋๋ค.
์ง์์ ์ธ ์ ๋ฐ์ดํธ
๋ฉ์ผ ์๋ฒ ๊ตฌ์ฑ์ ๋์์์ด ๋ณ๊ฒฝ๋ฉ๋๋ค. ์ด๋ฉ์ผ ๊ฒ์ฆ ์๋น์ค๋ ์๋ ค์ง ์ ๊ณต์ ์ฒด, ์ผํ์ฉ ๋๋ฉ์ธ ๋ฐ ๋ฉ์ผ ์๋ฒ ํจํด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ง์์ ์ผ๋ก ์ ๋ฐ์ดํธํฉ๋๋ค.
API ํตํฉ ์์
async function verifyEmailWithBillionVerify(email) {
const response = await fetch('https://api.billionverify.com/v1/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.BILLIONVERIFY_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email })
});
const result = await response.json();
// MX information is included in the response
console.log('MX Valid:', result.mx_found);
console.log('Domain Valid:', result.domain_valid);
console.log('Is Deliverable:', result.is_deliverable);
return result;
}
๊ฒฐ๋ก
MX ๋ ์ฝ๋ ๊ฒ์ฆ์ ๋ ๋ง์ ๋ฆฌ์์ค๋ฅผ ์๋ชจํ๋ ๊ฒ์ฌ๋ฅผ ์๋ํ๊ธฐ ์ ์ ๋๋ฉ์ธ์ด ์ด๋ฉ์ผ์ ์์ ํ ์ ์๋์ง ํ์ธํ๋ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ๊ธฐ๋ณธ ๊ตฌ์ฑ ์์์ ๋๋ค. ์ ์ ํ MX ๊ฒ์ฆ์ ๊ตฌํํ๋ฉด ์๋ชป๋ ๋๋ฉ์ธ์ ๋น ๋ฅด๊ฒ ํํฐ๋งํ๊ณ , ๊ฒ์ฆ ์ฑ๋ฅ์ ์ต์ ํํ๋ฉฐ, ๋ ์์ ์ ์ธ ์ด๋ฉ์ผ ์ฒ๋ฆฌ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ ์ ์์ต๋๋ค.
MX ๋ ์ฝ๋ ๊ฒ์ฆ์ ํต์ฌ ์์ :
- SMTP ๊ฒ์ฆ์ ์๋ํ๊ธฐ ์ ์ ํญ์ MX ๋ ์ฝ๋๋ฅผ ํ์ธํ์ฌ ์๊ฐ๊ณผ ๋ฆฌ์์ค๋ฅผ ์ ์ฝํ์ธ์
- A ๋ ์ฝ๋ ๋์ฒด๋ฅผ ์ฒ๋ฆฌํ์ฌ MX ๋ ์ฝ๋๊ฐ ์๋ ๋๋ฉ์ธ์ ๋ํ RFC ํ์ค์ ์ค์ํ์ธ์
- ์บ์ฑ์ ๊ตฌํํ์ฌ ๋ฐ๋ณต๋๋ ๊ฒ์ฆ์ ๋ํ DNS ์กฐํ ์ค๋ฒํค๋๋ฅผ ์ค์ด์ธ์
- ์ผ๋ฐ์ ์ธ ํจํด์ ์ธ์ํ์ฌ ์ด๋ฉ์ผ ์ ๊ณต์ ์ฒด์ ์ ์ฌ์ ์ํ์ ์๋ณํ์ธ์
- ์ค๋ฅ๋ฅผ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํ์ฌ ํ์์์, ์ฌ์๋ ๋ฐ ์ ์ ํ ์ค๋ฅ ๋ฉ์์ง๋ฅผ ์ ๊ณตํ์ธ์
์ฌ์ฉ์ ์ ์ ์ด๋ฉ์ผ ๊ฒ์ฆ ์์คํ ์ ๊ตฌ์ถํ๋ BillionVerify์ ๊ฐ์ ์๋น์ค์ ํตํฉํ๋ , MX ๋ ์ฝ๋๋ฅผ ์ดํดํ๋ฉด ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ ๋์ ์ด๋ฉ์ผ ์ฒ๋ฆฌ๋ฅผ ๊ตฌ์ถํ ์ ์์ต๋๋ค. ์ค๋ MX ๊ฒ์ฆ ๊ตฌํ์ ์์ํ๊ณ ํฌ๊ด์ ์ธ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ํฅํ ์ฒซ๊ฑธ์์ ๋ด๋๋์ธ์.