éä¿¡ãããã¹ãŠã®ã¡ãŒã«ã¯ãç·»å¯ã«çµç¹åããããµãŒããŒãããã¯ãŒã¯ãçµç±ããŠé ä¿¡ãããŸããMail ExchangeïŒMXïŒã¬ã³ãŒãã¯ããã®çµè·¯ãæ¡å ããéæšã®åœ¹å²ãæãããŸããMXã¬ã³ãŒãã®æ€èšŒæ¹æ³ãçè§£ããããšã¯ãã¡ãŒã«æ€èšŒã·ã¹ãã ãã³ã³ã¿ã¯ããã©ãŒã ããŸãã¯ã¡ãŒã«ã¢ãã¬ã¹ãåéããã¢ããªã±ãŒã·ã§ã³ãæ§ç¯ããéçºè ã«ãšã£ãŠå¿ é ã®ã¹ãã«ã§ãããã®å æ¬çãªã¬ã€ãã§ã¯ãåºæ¬æŠå¿µããé«åºŠãªå®è£ æŠç¥ãŸã§ãMXã¬ã³ãŒãæ€èšŒã«ã€ããŠè©³ãã解説ããã¢ããªã±ãŒã·ã§ã³ã«å ç¢ãªã¡ãŒã«æ€èšŒãçµã¿èŸŒãããã®ç¥èãæäŸããŸãã
MXã¬ã³ãŒãã®çè§£
Mail Exchangeã¬ã³ãŒãã¯ããã¡ã€ã³ã«ä»£ãã£ãŠã¡ãŒã«ãåä¿¡ããã¡ãŒã«ãµãŒããŒãæå®ããDNSã¬ã³ãŒãã§ããuser@example.com ã«ã¡ãŒã«ãéä¿¡ãããšãéä¿¡åŽã®ã¡ãŒã«ãµãŒããŒã¯é ä¿¡å ãç¥ãå¿ èŠããããŸããMXã¬ã³ãŒãã¯ããã¡ã€ã³ã®ã¡ãŒã«ãµãŒããŒãæã瀺ãããšã§ãã®æ å ±ãæäŸããŸãã
MXã¬ã³ãŒãã®ä»çµã¿
ã¡ãŒã«ãéä¿¡ããããšãéä¿¡åŽã®ã¡ãŒã«ãµãŒããŒã¯åä¿¡è ã®ãã¡ã€ã³ã®MXã¬ã³ãŒããååŸããããDNSã«ãã¯ã¢ãããå®è¡ããŸãããã®ã«ãã¯ã¢ããã¯ãåªå é äœã瀺ãå€ãšãšãã«ã1ã€ä»¥äžã®ã¡ãŒã«ãµãŒããŒã®ãã¹ãåãè¿ããŸãã
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ã¬ã³ãŒãã«ã¯2ã€ã®éèŠãªæ å ±ãå«ãŸããŠããŸãïŒ
åªå 床ïŒ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);
// åªå
床ã§ãœãŒãïŒæå°å€ãå
é ã«ïŒ
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': 'ãã®ãã¡ã€ã³ã®MXã¬ã³ãŒããèŠã€ãããŸãã',
'ENOTFOUND': 'ãã¡ã€ã³ãååšããŸãã',
'ETIMEOUT': 'DNSã«ãã¯ã¢ãããã¿ã€ã ã¢ãŠãããŸãã',
'ESERVFAIL': 'DNSãµãŒããŒãå¿çã«å€±æããŸãã'
};
return messages[code] || 'äžæãªDNSãšã©ãŒ';
}
// 䜿çšäŸ
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
})
# åªå
床ã§ãœãŒã
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': 'ãã¡ã€ã³ãååšããŸãã'
}
except dns.resolver.NoAnswer:
return {
'success': False,
'domain': domain,
'error': 'NoAnswer',
'message': 'ãã®ãã¡ã€ã³ã®MXã¬ã³ãŒããèŠã€ãããŸãã'
}
except dns.exception.Timeout:
return {
'success': False,
'domain': domain,
'error': 'Timeout',
'message': 'DNSã«ãã¯ã¢ãããã¿ã€ã ã¢ãŠãããŸãã'
}
# 䜿çšäŸ
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.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) {
// MXã¬ã³ãŒããååŸ
const mxResult = await getMxRecords(domain);
if (!mxResult.success) {
return mxResult;
}
// åã¡ãŒã«ãµãŒããŒãæ€èšŒ
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 {
// ãã¹ãåãIPã«è§£æ±º
const addresses = await dns.resolve4(hostname);
if (addresses.length === 0) {
return { reachable: false, error: 'Aã¬ã³ãŒãããããŸãã' };
}
// ããŒã25ãžã®æ¥ç¶ããã¹ã
const connected = await testConnection(addresses[0], 25);
return {
reachable: connected,
ip: addresses[0],
error: connected ? null : 'æ¥ç¶ãæåŠãããŸãã'
};
} 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) {
// ãŸãMXã¬ã³ãŒãã詊è¡
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;
}
}
// Aã¬ã³ãŒãã«ãã©ãŒã«ããã¯
try {
const aRecords = await dns.resolve4(domain);
if (aRecords.length > 0) {
return {
type: 'A_FALLBACK',
servers: [{ exchange: domain, priority: 0 }],
warning: 'Aã¬ã³ãŒããã©ãŒã«ããã¯ãäœ¿çš - MXã¬ã³ãŒããèŠã€ãããŸãã'
};
}
} catch (error) {
if (error.code !== 'ENODATA') {
throw error;
}
}
return {
type: 'NONE',
servers: [],
error: 'ãã¡ã€ã³ã®ã¡ãŒã«ãµãŒããŒãèŠã€ãããŸãã'
};
}
NullMXã¬ã³ãŒãã®æ€åº
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: 'ãã¡ã€ã³ã¯æç€ºçã«ã¡ãŒã«ãåãä»ããŸãã'
};
}
return mxResult;
}
MXã«ãã¯ã¢ããã®ãã£ãã·ã³ã°
DNSã«ãã¯ã¢ããã¯æ€èšŒã®ãã³ã«ã¬ã€ãã³ã·ã远å ããŸããããã©ãŒãã³ã¹åäžã®ãããã£ãã·ã³ã°ãå®è£ ããŸãïŒ
class MxCache {
constructor(ttlMs = 3600000) { // ããã©ã«ãTTL 1æé
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
});
}
// å©çšå¯èœãªå Žåã¯DNS TTLå€ãå°é
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')
);
// ãã¡ã€ã³ãæ¢ç¥ã®Googleã³ã³ã·ã¥ãŒããŒãã¡ã€ã³ã§ãªãããšã確èª
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æ€èšŒã¯ãå æ¬çãªã¡ãŒã«æ€èšŒããã»ã¹ã®1ã¹ãããã§ãããã®åœ¹å²ãçè§£ããããšã§ã广çãªæ€èšŒãã€ãã©ã€ã³ãæ§ç¯ã§ããŸãã
æ€èšŒã®é åº
MXæ€èšŒã¯éåžžãæ€èšŒãã€ãã©ã€ã³ã®æ©ã段éã§å®è¡ãããŸãïŒ
async function verifyEmail(email) {
// 1. æ§ææ€èšŒïŒæéããããã¯ãŒã¯äžèŠïŒ
const syntaxResult = validateEmailSyntax(email);
if (!syntaxResult.valid) {
return { valid: false, reason: 'invalid_syntax', details: syntaxResult };
}
const domain = email.split('@')[1];
// 2. MXã¬ã³ãŒãæ€èšŒïŒé«éDNSã«ãã¯ã¢ããïŒ
const mxResult = await validateMxRecords(domain);
if (!mxResult.valid) {
return { valid: false, reason: 'no_mx_records', details: mxResult };
}
// 3. 远å ãã§ãã¯ïŒäœ¿ãæšãŠã圹å²ããŒã¹ãªã©ïŒ
const domainCheck = await checkDomainReputation(domain);
if (domainCheck.isDisposable) {
return { valid: true, risky: true, reason: 'disposable_domain' };
}
// 4. SMTPæ€èšŒïŒæãé
ããæã培åºçïŒ
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) {
// ãã¡ã€ã³ããšã«ã¡ãŒã«ãã°ã«ãŒãå
const emailsByDomain = {};
for (const email of emails) {
const domain = email.split('@')[1];
if (!emailsByDomain[domain]) {
emailsByDomain[domain] = [];
}
emailsByDomain[domain].push(email);
}
// ãã¹ãŠã®ãã¡ã€ã³ã®MXã¬ã³ãŒãã䞊åã§ã«ãã¯ã¢ãã
const domains = Object.keys(emailsByDomain);
const mxResults = await Promise.all(
domains.map(domain => getMxRecordsCached(domain))
);
// çµæããã¡ã€ã³ã«ãããã³ã°
const mxByDomain = {};
domains.forEach((domain, index) => {
mxByDomain[domain] = mxResults[index];
});
// MXããŒã¿ã䜿çšããŠã¡ãŒã«ãåŠç
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;
}
// ãã£ãã·ã¥ãããMXããŒã¿ã䜿çšããŠSMTPæ€èšŒãç¶ç¶
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ã«ãã¯ã¢ãããã¿ã€ã ã¢ãŠãããŸãã',
retryable: true
};
}
throw error;
}
}
ç¡å¹ãªãã¡ã€ã³ã®åŠç
DNSã«ãã¯ã¢ããã詊ã¿ãåã«ãæ§æçã«ç¡å¹ãªãã¡ã€ã³ãåŠçããŸãïŒ
function isValidDomain(domain) {
if (!domain || typeof domain !== 'string') {
return false;
}
// é·ãããã§ãã¯
if (domain.length > 253) {
return false;
}
// æå¹ãªæåãšæ§é ããã§ãã¯
const domainRegex = /^(?!-)[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
if (!domainRegex.test(domain)) {
return false;
}
// åã©ãã«ã®é·ãããã§ãã¯
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: 'ãã¡ã€ã³åœ¢åŒãç¡å¹ã§ã'
};
}
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);
// 確å®çãªé害ã«å¯ŸããŠã¯å詊è¡ããªã
if (result.success ||
result.error === 'NXDOMAIN' ||
result.error === 'ENOTFOUND') {
return result;
}
// äžæçãªé害ã«å¯ŸããŠå詊è¡
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;
}
// ããŒã¯ã³ãå©çšå¯èœã«ãªããŸã§åŸ
æ©
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);
}
MXæ€èšŒã«BillionVerifyã䜿çšãã
MXæ€èšŒãèªåã§å®è£ ããããšã¯æè²ç䟡å€ããããŸãããBillionVerifyã®ãããªãããã§ãã·ã§ãã«ãªã¡ãŒã«æ€èšŒãµãŒãã¹ã¯ãå æ¬çãªã¡ãŒã«æ€èšŒã®äžéšãšããŠMXæ€èšŒãåŠçããŸãã
ã¡ãŒã«æ€èšŒAPIã䜿çšããå©ç¹
å æ¬çãªãã§ãã¯
BillionVerifyã®ã¡ãŒã«æ€èšŒAPIã¯ãMXæ€èšŒãæ§æãã§ãã¯ãSMTPæ€èšŒãäœ¿ãæšãŠã¡ãŒã«æ€åºãªã©ãšçµã¿åãããŠãåäžã®APIåŒã³åºãã§æäŸããŸããããã«ãããè€æ°ã®æ€èšŒã·ã¹ãã ãç¶æããå¿ èŠããªããªããŸãã
æé©åãããã€ã³ãã©ã¹ãã©ã¯ãã£
ãããã§ãã·ã§ãã«ãªãµãŒãã¹ã¯ãã°ããŒãã«ã«åæ£ããã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æ
å ±ã¯ã¬ã¹ãã³ã¹ã«å«ãŸããŸã
console.log('MXæå¹:', result.mx_found);
console.log('ãã¡ã€ã³æå¹:', result.domain_valid);
console.log('é
ä¿¡å¯èœ:', result.is_deliverable);
return result;
}
ãŸãšã
MXã¬ã³ãŒãæ€èšŒã¯ãããå€ãã®ãªãœãŒã¹ãæ¶è²»ãããã§ãã¯ã詊ã¿ãåã«ããã¡ã€ã³ãã¡ãŒã«ãåä¿¡ã§ããããšã確èªããã¡ãŒã«æ€èšŒã®åºæ¬ã³ã³ããŒãã³ãã§ããé©åãªMXæ€èšŒãå®è£ ããããšã§ãç¡å¹ãªãã¡ã€ã³ãçŽ æ©ããã£ã«ã¿ãªã³ã°ããæ€èšŒã®ããã©ãŒãã³ã¹ãæé©åããããä¿¡é Œæ§ã®é«ãã¡ãŒã«åŠçã¢ããªã±ãŒã·ã§ã³ãæ§ç¯ã§ããŸãã
MXã¬ã³ãŒãæ€èšŒã®éèŠãªãã€ã³ãïŒ
- åžžã«MXã¬ã³ãŒãããã§ã㯠- SMTPæ€èšŒã詊ã¿ãåã«MXã¬ã³ãŒãããã§ãã¯ããæéãšãªãœãŒã¹ãç¯çŽããŸã
- Aã¬ã³ãŒããã©ãŒã«ããã¯ãåŠç - MXã¬ã³ãŒãã®ãªããã¡ã€ã³ã«å¯ŸããRFCæšæºã«åŸããŸã
- ãã£ãã·ã³ã°ãå®è£ - ç¹°ãè¿ãã®æ€èšŒã«ãããDNSã«ãã¯ã¢ããã®ãªãŒããŒããããåæžããŸã
- äžè¬çãªãã¿ãŒã³ãèªè - ã¡ãŒã«ãããã€ããŒãšæœåšçãªãªã¹ã¯ãèå¥ããŸã
- ãšã©ãŒãé©åã«åŠç - ã¿ã€ã ã¢ãŠããå詊è¡ãé©åãªãšã©ãŒã¡ãã»ãŒãžãå®è£ ããŸã
ã«ã¹ã¿ã ã¡ãŒã«æ€èšŒã·ã¹ãã ãæ§ç¯ããå Žåã§ããBillionVerifyã®ãããªãµãŒãã¹ãšçµ±åããå Žåã§ããMXã¬ã³ãŒããçè§£ããããšã§ãã¢ããªã±ãŒã·ã§ã³ã«åªããã¡ãŒã«åŠçãçµã¿èŸŒãããšãã§ããŸããä»ããMXæ€èšŒã®å®è£ ãéå§ããå æ¬çãªã¡ãŒã«æ€èšŒãžã®ç¬¬äžæ©ãèžã¿åºããŸãããã