ãŠãŒã¶ãŒç»é²ã¯ã«ã¹ã¿ããŒãžã£ãŒããŒã«ãããŠæãéèŠãªç¬éã®äžã€ã§ãããã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã¯ãã®äœéšãå®å šãã€ã·ãŒã ã¬ã¹ã«ããäžã§æ¥µããŠéèŠãªåœ¹å²ãæãããŸããæ£ããå®è£ ãããã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã¯ãåœã¢ã«ãŠã³ããé²ããããŠã³ã¹çãäœæžããæ¬ç©ã®ãŠãŒã¶ãŒãšã®ä¿¡é Œé¢ä¿ãæ§ç¯ããŸããããããå®è£ ãäžååã ãšããŠãŒã¶ãŒãã€ã©ã€ã©ãããé¢è±çãé«ãããã©ã³ãã®è©å€ãæãªãå¯èœæ§ããããŸãããã®å æ¬çãªã¬ã€ãã§ã¯ãã»ãã¥ãªãã£èŠä»¶ãšæé©ãªãŠãŒã¶ãŒäœéšã®ãã©ã³ã¹ãåããªããããŠãŒã¶ãŒç»é²æã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒãå®è£ ããããã®ãã¹ããã©ã¯ãã£ã¹ãæ¢ããŸããåºç€çãªæŠå¿µã«ã€ããŠã¯ãã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã®å®å šã¬ã€ããã芧ãã ããã
ç»é²æã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã®éèŠãªåœ¹å²
ç»é²æã«ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒãéèŠã§ããçç±ãçè§£ããããšã§ãããŒã ãå®è£ ã®åªå é äœã決å®ããé©åãªãªãœãŒã¹ãå²ãåœãŠãããšãã§ããŸãã
ç»é²æã«ã¡ãŒã«ã¢ãã¬ã¹ãæ€èšŒããçç±
ç»é²æã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã¯ãããžãã¹ãšãŠãŒã¶ãŒã®äž¡æ¹ãä¿è·ããè€æ°ã®éèŠãªæ©èœãæãããŸããäž»ãªç®çã¯ããŠãŒã¶ãŒãå®éã«ææããã¢ã¯ã»ã¹ã§ããæå¹ã§é ä¿¡å¯èœãªã¡ãŒã«ã¢ãã¬ã¹ãæäŸããããšãä¿èšŒããããšã§ãã
ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒããªããã°ããŠãŒã¶ãŒããŒã¿ããŒã¹ã¯ããã«ã¿ã€ããã¹ãåœã®ã¢ãã¬ã¹ãæŸæ£ãããã¢ã«ãŠã³ãã§åãå°œããããŸããç»é²æã«ã¡ãŒã«ã¢ãã¬ã¹ã誀ã£ãŠå ¥åãããŠãŒã¶ãŒã¯ããã¹ã¯ãŒããªã»ããæ©èœãéèŠãªéç¥ãžã®ã¢ã¯ã»ã¹ã倱ããŸããããããæªæã®ããè¡çºè ã«ããåœã®ã¡ãŒã«ã¢ãã¬ã¹ã¯ãã»ãã¥ãªãã£äžã®è匱æ§ãçã¿åºããåæããŒã¿ãæªããŸãã
ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã¯ãæåã®ã€ã³ã¿ã©ã¯ã·ã§ã³ããã¢ããªã±ãŒã·ã§ã³ãšãŠãŒã¶ãŒéã®ã³ãã¥ãã±ãŒã·ã§ã³ãã£ãã«ã確ç«ããŸãããŠãŒã¶ãŒãã¡ãŒã«ã¢ãã¬ã¹ã確èªãããšãæå³ãšãšã³ã²ãŒãžã¡ã³ãã瀺ããã¢ã¯ãã£ãã§äŸ¡å€ã®ãã顧客ã«ãªãå¯èœæ§ãé«ããªããŸãã
ããžãã¹ææšãžã®åœ±é¿
ç»é²æã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã®è³ªã¯ãã³ã³ããŒãžã§ã³çãé¡§å®¢çæ¶¯äŸ¡å€ãããŒã±ãã£ã³ã°å¹æãªã©ãäž»èŠãªããžãã¹ææšã«çŽæ¥åœ±é¿ããŸãã
調æ»ã«ãããšãç»é²æã«å ¥åãããã¡ãŒã«ã¢ãã¬ã¹ã® 20-30% ã«ãšã©ãŒãå«ãŸããŠããããæå³çã«åœã®ã¢ãã¬ã¹ã§ããããšã瀺ãããŠããŸããæ€èšŒããªããã°ããããã®ç¡å¹ãªã¢ãã¬ã¹ã¯ãŠãŒã¶ãŒæ°ãæ°Žå¢ãããŸãããå®éã®äŸ¡å€ã¯æäŸããŸããããããã®ã¢ãã¬ã¹ã«éä¿¡ãããããŒã±ãã£ã³ã°ãã£ã³ããŒã³ã¯ããŠã³ã¹ããéä¿¡è ã®è©å€ãæãªããæ£åœãªãŠãŒã¶ãŒãžã®é ä¿¡çãäœäžãããŸãã
ç»é²æã«é©åãªã¡ãŒã«ã¢ãã¬ã¹æ€èšŒãå®è£ ããäŒæ¥ã¯ãããŠã³ã¹çã® 40-60% åæžãã¡ãŒã«ãšã³ã²ãŒãžã¡ã³ãææšã® 25-35% æ¹åãã¢ã«ãŠã³ãã¢ã¯ã»ã¹ã«é¢é£ããã«ã¹ã¿ããŒãµããŒããã±ããã®å€§å¹ ãªæžå°ãå ±åããŠããŸãã
ã»ãã¥ãªãã£ãšãŠãŒã¶ãŒäœéšã®ãã©ã³ã¹
ç»é²æã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã®èª²é¡ã¯ã培åºçãªæ€èšŒã𿩿Šã®ãªããŠãŒã¶ãŒäœéšã®ãã©ã³ã¹ãåãããšã«ãããŸããé床ã«ç©æ¥µçãªæ€èšŒã¯æ£åœãªãŠãŒã¶ãŒãã€ã©ã€ã©ãããé¢è±ãå¢å ãããŸãããäžååãªæ€èšŒã¯ç¡å¹ãªã¢ãã¬ã¹ãã·ã¹ãã ã«å ¥ãããšãèš±ããŠããŸããŸãã
æè¯ã®å®è£ ã¯ãæãããªãšã©ãŒãå³åº§ã«ãã£ããããªãããããæ·±ãæ€èšŒãéåæã«å®è¡ããã€ã³ããªãžã§ã³ããªå€å±€æ€èšŒã䜿çšããããšã§ããã®ãã©ã³ã¹ãèŠã€ããŸãããã®ã¢ãããŒãã¯ãäžè¬çãªãã¹ã«å¯ŸããŠå³åº§ã«ãã£ãŒãããã¯ãæäŸããªãããç»é²ããã»ã¹äžã«ãŠãŒã¶ãŒããããã¯ããŸããã
ç»é²æã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã®çš®é¡
ç°ãªãæ€èšŒã¢ãããŒãã¯ç°ãªãç®çãæãããã¡ãŒã«ã¢ãã¬ã¹ã®æå¹æ§ã«ã€ããŠããŸããŸãªã¬ãã«ã®ä¿èšŒãæäŸããŸãã
æ§ææ€èšŒ
æ§ææ€èšŒã¯ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã®æåã§æéã®å±€ã§ãããå ¥åãããã¢ãã¬ã¹ãã¡ãŒã«ã¢ãã¬ã¹ã®åºæ¬çãªåœ¢åŒèŠä»¶ã«æºæ ããŠãããããã§ãã¯ããŸãããã®æ€èšŒã¯å®å šã«ãã©ãŠã¶ã§è¡ãããå³åº§ã«ãã£ãŒãããã¯ãæäŸããŸãã
广çãªæ§ææ€èšŒã¯ã@ èšå·ã®æ¬ èœãç¡å¹ãªæåãäžå®å šãªãã¡ã€ã³åããã®ä»ã®æãããªãã©ãŒããããšã©ãŒããã£ããããŸããæ§ææ€èšŒã¯ã¢ãã¬ã¹ãå®éã«ååšãããã©ããã確èªã§ããŸããããæããã«ç¡å¹ãªã¢ãã¬ã¹ã®éä¿¡ãé²ããŸãã
ãã¡ã€ã³æ€èšŒ
ãã¡ã€ã³æ€èšŒã¯æ§æãè¶ ããŠãã¡ãŒã«ãã¡ã€ã³ãååšããã¡ãŒã«ãåä¿¡ã§ãããã©ããããã§ãã¯ããŸããããã«ã¯ãMX ã¬ã³ãŒãã確èªããããã® DNS ã«ãã¯ã¢ãããå«ãŸãããã¡ã€ã³ã«åä¿¡ã¡ãŒã«ãåãå ¥ããã¡ãŒã«ãµãŒããŒãæ§æãããŠããããšã確èªããŸãã
ãã¡ã€ã³æ€èšŒã¯ã"gmail.com" ã®ä»£ããã« "gmial.com" ãªã©ã®äžè¬çãªã¡ãŒã«ãããã€ããŒåã®ã¿ã€ããã¹ããååšããªããã¡ã€ã³ãèå¥ããŸãããã®æ€èšŒå±€ã¯ãµãŒããŒåŽã®åŠçãå¿ èŠã§ãããæ¯èŒçé«éãªãã£ãŒãããã¯ãæäŸã§ããŸãã
ã¡ãŒã«ããã¯ã¹æ€èšŒ
ã¡ãŒã«ããã¯ã¹æ€èšŒã¯æã培åºçãªåœ¢åŒã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã§ãããã¡ãŒã«ãµãŒããŒäžã«ç¹å®ã®ã¡ãŒã«ããã¯ã¹ãååšãããã©ããããã§ãã¯ããŸããããã«ã¯ãåä¿¡è ã®ã¡ãŒã«ãµãŒããŒãšã® SMTP éä¿¡ãè¡ããã¢ãã¬ã¹ãé ä¿¡å¯èœã§ããããšã確èªããããšãå«ãŸããŸãã
ã¡ãŒã«ããã¯ã¹æ€èšŒã¯æé«ã®ç²ŸåºŠãæäŸããŸãããå®äºã«æãæéãããããã°ã¬ãŒãªã¹ãã£ã³ã°ããã£ãããªãŒã«æ§æãªã©ã®èª²é¡ã«çŽé¢ããŸããã»ãšãã©ã®ç»é²ãããŒã¯ããŠãŒã¶ãŒããã©ãŒã ãéä¿¡ããåŸã«éåæã§ãã®æ€èšŒãå®è¡ããŸãã
ã¡ãŒã«ç¢ºèª
ã¡ãŒã«ç¢ºèªã¯ããŠãŒã¶ãŒãæææš©ã確èªããããã«ã¯ãªãã¯ããå¿ èŠãããæ€èšŒãªã³ã¯ãå«ãã¡ãŒã«ãåä¿¡ããåŸæ¥ã®ã¢ãããŒãã§ããããã¯ã¢ã¯ã»ã¹ã®æ±ºå®çãªèšŒæãæäŸããŸãããç»é²ããã»ã¹ã«æ©æŠãå ããã¢ã«ãŠã³ãã®ã¢ã¯ãã£ããŒã·ã§ã³ãé ãããŸãã
çŸä»£ã®ãã¹ããã©ã¯ãã£ã¹ã¯ãç»é²æã®ãªã¢ã«ã¿ã€ã æ€èšŒãšãé«ã»ãã¥ãªãã£ã¢ããªã±ãŒã·ã§ã³åãã®ãªãã·ã§ã³ã®ã¡ãŒã«ç¢ºèªãçµã¿åãããå³åº§ã®æ€èšŒãšç¢ºèªãããæææš©ã®äž¡æ¹ãæäŸããŸãã
ç»é²æã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã® UX ãã¹ããã©ã¯ãã£ã¹
ãŠãŒã¶ãŒäœéšã®èæ ®äºé ã¯ãã¡ãŒã«ã¢ãã¬ã¹æ€èšŒå®è£ ã«ããããã¹ãŠã®æ±ºå®ãå°ãã¹ãã§ãã
ãªã¢ã«ã¿ã€ã³ã©ã€ã³æ€èšŒ
ãªã¢ã«ã¿ã€ã ã€ã³ã©ã€ã³æ€èšŒã¯ããŠãŒã¶ãŒãå ¥åããŠããéã«å³åº§ã«ãã£ãŒãããã¯ãæäŸãããã©ãŒã éä¿¡åã«ãšã©ãŒããã£ããããŸãããã®ã¢ãããŒãã¯ããã©ãŒã å šäœãå®äºããåŸã®ã€ã©ã€ã©ãããšã©ãŒã¡ãã»ãŒãžãé²ãããšã§ããŠãŒã¶ãŒäœéšãåçã«æ¹åããŸãã
广çãªã€ã³ã©ã€ã³æ€èšŒã¯ãã¡ãŒã«ãã£ãŒã«ãã®çŽæ¥æšªã«æ€èšŒç¶æ ã衚瀺ããæå¹ãç¡å¹ãæ€èšŒäžã®ç¶æ ã«æç¢ºãªèŠèŠçã€ã³ãžã±ãŒã¿ãŒã䜿çšãããŠãŒã¶ãŒããã¹ãä¿®æ£ããã®ã«åœ¹ç«ã€å ·äœçã§å®è¡å¯èœãªãšã©ãŒã¡ãã»ãŒãžãæäŸããŸãã
// React component with real-time email validation
import { useState, useCallback, useEffect } from 'react';
import debounce from 'lodash/debounce';
function SignupEmailInput({ onEmailValidated }) {
const [email, setEmail] = useState('');
const [status, setStatus] = useState({
state: 'idle', // idle, validating, valid, invalid
message: ''
});
// Debounced validation function
const validateEmail = useCallback(
debounce(async (emailValue) => {
if (!emailValue) {
setStatus({ state: 'idle', message: '' });
return;
}
// Quick syntax check
const syntaxValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailValue);
if (!syntaxValid) {
setStatus({
state: 'invalid',
message: 'Please enter a valid email address'
});
return;
}
setStatus({ state: 'validating', message: 'Checking email...' });
try {
const response = await fetch('/api/validate-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: emailValue })
});
const result = await response.json();
if (result.valid) {
setStatus({ state: 'valid', message: 'Email looks good!' });
onEmailValidated(emailValue);
} else {
setStatus({
state: 'invalid',
message: result.suggestion
? `Did you mean ${result.suggestion}?`
: result.message || 'This email address is not valid'
});
}
} catch (error) {
// On error, allow submission but log the issue
setStatus({ state: 'valid', message: '' });
console.error('Email validation error:', error);
}
}, 500),
[onEmailValidated]
);
useEffect(() => {
validateEmail(email);
return () => validateEmail.cancel();
}, [email, validateEmail]);
const getStatusIcon = () => {
switch (status.state) {
case 'validating':
return <span className="spinner" aria-label="Validating" />;
case 'valid':
return <span className="check-icon" aria-label="Valid">â</span>;
case 'invalid':
return <span className="error-icon" aria-label="Invalid">â</span>;
default:
return null;
}
};
return (
<div className="email-input-container">
<label htmlFor="email">Email Address</label>
<div className="input-wrapper">
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
aria-describedby="email-status"
className={`email-input ${status.state}`}
/>
<span className="status-icon">{getStatusIcon()}</span>
</div>
{status.message && (
<p
id="email-status"
className={`status-message ${status.state}`}
role={status.state === 'invalid' ? 'alert' : 'status'}
>
{status.message}
</p>
)}
</div>
);
}
ã¿ã€ããã¹ææ¡ãšèªåä¿®æ£
æããŠãŒã¶ãŒãã¬ã³ããªãŒãªã¡ãŒã«ã¢ãã¬ã¹æ€èšŒæ©èœã®äžã€ã¯ãäžè¬çãªã¿ã€ããã¹ãæ€åºããä¿®æ£ãææ¡ããããšã§ãããŠãŒã¶ãŒã "user@gmial.com" ãšå ¥åããå Žåã代ããã« "gmail.com" ãææ¡ããããšã§ãã€ã©ã€ã©ãé²ããã¢ã«ãŠã³ãã®åªå€±ãé²ãããšãã§ããŸãã
ã¿ã€ããã¹æ€åºã¢ã«ãŽãªãºã ã¯ãå ¥åããããã¡ã€ã³ãäžè¬çãªã¡ãŒã«ãããã€ããŒã®ããŒã¿ããŒã¹ãšæ¯èŒããç·šéè·é¢ã®èšç®ã䜿çšããŠå¯èœæ§ã®é«ããã¹ãèå¥ããŸãã
// Common email domain typo suggestions
const commonDomains = {
'gmail.com': ['gmial.com', 'gmal.com', 'gamil.com', 'gmail.co', 'gmail.om'],
'yahoo.com': ['yaho.com', 'yahooo.com', 'yahoo.co', 'yhoo.com'],
'hotmail.com': ['hotmal.com', 'hotmial.com', 'hotmail.co', 'hotmai.com'],
'outlook.com': ['outlok.com', 'outloo.com', 'outlook.co'],
'icloud.com': ['iclod.com', 'icloud.co', 'icoud.com']
};
function suggestEmailCorrection(email) {
const [localPart, domain] = email.toLowerCase().split('@');
if (!domain) return null;
// Check for exact typo matches
for (const [correctDomain, typos] of Object.entries(commonDomains)) {
if (typos.includes(domain)) {
return {
suggestion: `${localPart}@${correctDomain}`,
reason: 'typo'
};
}
}
// Check edit distance for close matches
for (const correctDomain of Object.keys(commonDomains)) {
if (levenshteinDistance(domain, correctDomain) <= 2) {
return {
suggestion: `${localPart}@${correctDomain}`,
reason: 'similar'
};
}
}
return null;
}
function levenshteinDistance(str1, str2) {
const matrix = Array(str2.length + 1).fill(null)
.map(() => Array(str1.length + 1).fill(null));
for (let i = 0; i <= str1.length; i++) matrix[0][i] = i;
for (let j = 0; j <= str2.length; j++) matrix[j][0] = j;
for (let j = 1; j <= str2.length; j++) {
for (let i = 1; i <= str1.length; i++) {
const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1;
matrix[j][i] = Math.min(
matrix[j][i - 1] + 1,
matrix[j - 1][i] + 1,
matrix[j - 1][i - 1] + indicator
);
}
}
return matrix[str2.length][str1.length];
}
æç¢ºãªãšã©ãŒã¡ãã»ãŒãž
ãšã©ãŒã¡ãã»ãŒãžã¯å ·äœçã§ã圹ç«ã¡ãå®è¡å¯èœã§ããã¹ãã§ãããç¡å¹ãªã¡ãŒã«ã¢ãã¬ã¹ãã®ãããªææ§ãªã¡ãã»ãŒãžã¯ãäœãééã£ãŠããã®ãçè§£ã§ããªããŠãŒã¶ãŒãã€ã©ã€ã©ãããŸãã代ããã«ãåé¡ãä¿®æ£ããæ¹æ³ã«ã€ããŠæç¢ºãªã¬ã€ãã³ã¹ãæäŸããŠãã ããã
广çãªãšã©ãŒã¡ãã»ãŒãžã¯ãå ·äœçãªåé¡ã説æããä¿®æ£æ¹æ³ãææ¡ããŸããããšãã°ããç¡å¹ãªã¡ãŒã«åœ¢åŒãã®ä»£ããã«ããã¡ãŒã«ã¢ãã¬ã¹ã«ã¯ example.com ã®ãããªãã¡ã€ã³ãç¶ã @ èšå·ãå¿ èŠã§ããã䜿çšããŸãã
function getHelpfulErrorMessage(validationResult) {
const { error, code } = validationResult;
const errorMessages = {
'MISSING_AT': 'Please include an @ symbol in your email address',
'MISSING_DOMAIN': 'Please add a domain after the @ symbol (like gmail.com)',
'INVALID_DOMAIN': 'This email domain doesn\'t appear to exist. Please check for typos',
'DISPOSABLE_EMAIL': 'Please use a permanent email address, not a temporary one', // åç
§: /blog/disposable-email-detection
'ROLE_BASED': 'Please use a personal email address instead of a role-based one (like info@ or admin@)',
'SYNTAX_ERROR': 'Please check your email address for any typos',
'MAILBOX_NOT_FOUND': 'We couldn\'t verify this email address. Please double-check it\'s correct',
'DOMAIN_NO_MX': 'This domain cannot receive emails. Please use a different email address'
};
return errorMessages[code] || 'Please enter a valid email address';
}
èŠä»¶ã®æ®µéçãªé瀺
ãã¹ãŠã®æ€èšŒã«ãŒã«ãæåãã衚瀺ããŠãŠãŒã¶ãŒãå§åããªãã§ãã ããã代ããã«ãé¢é£æ§ãé«ããªãã«ã€ããŠèŠä»¶ã段éçã«æããã«ããŸãããŠãŒã¶ãŒãå ¥åãéå§ãããšãã«ã®ã¿åœ¢åŒã®ãã³ãã衚瀺ããæ€èšŒã倱æãããšãã«ã®ã¿å ·äœçãªãšã©ãŒã¡ãã»ãŒãžã衚瀺ããŸãã
ãã®ã¢ãããŒãã¯ãåæãã©ãŒã ãã¯ãªãŒã³ã§ã·ã³ãã«ã«ä¿ã¡ãªããããŠãŒã¶ãŒãå¿ èŠãšãããšãã«ãã¹ãŠã®å¿ èŠãªã¬ã€ãã³ã¹ãæäŸããŸãã
ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒ API ã®å®è£
BillionVerify ã®ãããªãããã§ãã·ã§ãã«ãªã¡ãŒã«ã¢ãã¬ã¹æ€èšŒ API ã¯ãã«ã¹ã¿ã æ€èšŒã€ã³ãã©ã¹ãã©ã¯ãã£ãæ§ç¯ããè€éããªãã«å æ¬çãªæ€èšŒãæäŸããŸãã
é©å㪠API ã®éžæ
ç»é²ãããŒçšã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒ API ãéžæããéã¯ãé床ã粟床ãã«ãã¬ããžãã³ã¹ããèæ ®ããŠãã ãããç»é²æ€èšŒã«ã¯ãåªãããŠãŒã¶ãŒäœéšãç¶æããããã«é«éãªå¿çæéãå¿ èŠã§ãããéåžžã¯ã€ã³ã©ã€ã³æ€èšŒã§ 500 ããªç§æªæºã§ãã
BillionVerify ã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒ API ã¯ãæ§ææ€èšŒããã¡ã€ã³æ€èšŒãã¡ãŒã«ããã¯ã¹æ€èšŒãäœ¿ãæšãŠã¡ãŒã«æ€åºãé ä¿¡å¯èœæ§ã¹ã³ã¢ãªã³ã°ãå«ãå æ¬çãªãã§ãã¯ãåãããç»é²ãããŒã«æé©åããããªã¢ã«ã¿ã€ã æ€èšŒãæäŸããŸãã
çµ±åã®ãã¹ããã©ã¯ãã£ã¹
ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒ API ããç»é²äœéšã劚ããã®ã§ã¯ãªã匷åããæ¹æ³ã§çµ±åããŸããAPI ãšã©ãŒãåªé ã«åŠçããã¿ã€ã ã¢ãŠããå®è£ ãããµãŒãã¹ãå©çšã§ããªãå Žåã®ãã©ãŒã«ããã¯æŠç¥ãæã£ãŠãã ããã
// Express.js email validation endpoint
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// Rate limiting for signup validation
const signupLimiter = rateLimit({
windowMs: 60 * 1000,
max: 20,
message: { error: 'Too many requests, please try again later' }
});
app.post('/api/validate-email', signupLimiter, async (req, res) => {
const { email } = req.body;
if (!email) {
return res.status(400).json({
valid: false,
message: 'Email is required'
});
}
// Quick local validation first
const localValidation = validateEmailLocally(email);
if (!localValidation.valid) {
return res.json(localValidation);
}
// Check for typo suggestions
const typoSuggestion = suggestEmailCorrection(email);
try {
// Call BillionVerify API with timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
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 }),
signal: controller.signal
});
clearTimeout(timeout);
const result = await response.json();
return res.json({
valid: result.deliverable,
message: result.deliverable ? '' : getHelpfulErrorMessage(result),
suggestion: typoSuggestion?.suggestion,
details: {
isDisposable: result.is_disposable,
isCatchAll: result.is_catch_all,
score: result.quality_score
}
});
} catch (error) {
// On timeout or error, allow submission with warning
console.error('Email validation API error:', error);
return res.json({
valid: true,
warning: 'Unable to fully verify email',
suggestion: typoSuggestion?.suggestion
});
}
});
function validateEmailLocally(email) {
if (!email || typeof email !== 'string') {
return { valid: false, message: 'Email is required' };
}
const trimmed = email.trim();
if (trimmed.length > 254) {
return { valid: false, message: 'Email address is too long' };
}
if (!trimmed.includes('@')) {
return { valid: false, message: 'Please include an @ symbol', code: 'MISSING_AT' };
}
const [localPart, domain] = trimmed.split('@');
if (!domain || domain.length === 0) {
return { valid: false, message: 'Please add a domain after @', code: 'MISSING_DOMAIN' };
}
if (!domain.includes('.')) {
return { valid: false, message: 'Domain should include a dot (like .com)', code: 'INVALID_DOMAIN' };
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(trimmed)) {
return { valid: false, message: 'Please check the email format', code: 'SYNTAX_ERROR' };
}
return { valid: true };
}
ãšããžã±ãŒã¹ã®åŠç
å®éã®ç»é²ãããŒã§ã¯ãæ éãªåŠçãå¿ èŠãšããå€ãã®ãšããžã±ãŒã¹ã«ééããŸãã
ãã©ã¹ã¢ãã¬ãã·ã³ã°ãšãµãã¢ãã¬ãã·ã³ã°
å€ãã®ã¡ãŒã«ãããã€ããŒã¯ãã©ã¹ã¢ãã¬ãã·ã³ã°ããµããŒãããŠããããŠãŒã¶ãŒã¯ã¡ãŒã«ã¢ãã¬ã¹ã«ãã©ã¹èšå·ãšè¿œå ã®ããã¹ãã远å ã§ããŸãïŒuser+signup@gmail.comïŒãããã¯ãäžéšã®ãŠãŒã¶ãŒããã£ã«ã¿ãªã³ã°ã®ããã«äŸåããŠããæ£åœãªæ©èœã§ãããããæ€èšŒã¯ãããã®ã¢ãã¬ã¹ãåãå ¥ããå¿ èŠããããŸãã
ãã ããäžéšã®ãŠãŒã¶ãŒããã©ã¹ã¢ãã¬ãã·ã³ã°ãæªçšããŠãå®è³ªçã«åãã¡ãŒã«ã¢ãã¬ã¹ã§è€æ°ã®ã¢ã«ãŠã³ããäœæããå¯èœæ§ãããããšã«æ³šæããŠãã ãããéè€ã¢ã«ãŠã³ãããã§ãã¯ããéã¯ããã©ã¹ã¢ãã¬ãã·ã³ã°ãåé€ããŠã¢ãã¬ã¹ãæ£èŠåããããšãæ€èšããŠãã ããã
function normalizeEmailForDuplicateCheck(email) {
const [localPart, domain] = email.toLowerCase().split('@');
// Remove plus addressing
const normalizedLocal = localPart.split('+')[0];
// Handle Gmail dot trick (dots are ignored in Gmail addresses)
let finalLocal = normalizedLocal;
if (domain === 'gmail.com' || domain === 'googlemail.com') {
finalLocal = normalizedLocal.replace(/\./g, '');
}
return `${finalLocal}@${domain}`;
}
åœéã¡ãŒã«ã¢ãã¬ã¹
ã¡ãŒã«ã¢ãã¬ã¹ã¯ãããŒã«ã«éšåãšãã¡ã€ã³åã®äž¡æ¹ã«åœéæåãå«ãããšãã§ããŸãïŒIDN - åœéåãã¡ã€ã³åïŒãæ€èšŒã¯äžçäžã®ãŠãŒã¶ãŒããµããŒãããããã«ããããã®ã¢ãã¬ã¹ãé©åã«åŠçããå¿ èŠããããŸãã
function validateInternationalEmail(email) {
// Convert IDN to ASCII for validation
const { toASCII } = require('punycode/');
try {
const [localPart, domain] = email.split('@');
const asciiDomain = toASCII(domain);
// Validate the ASCII version
const asciiEmail = `${localPart}@${asciiDomain}`;
return validateEmailLocally(asciiEmail);
} catch (error) {
return { valid: false, message: 'Invalid domain format' };
}
}
äŒæ¥ããã³ã«ã¹ã¿ã ãã¡ã€ã³
äŒæ¥ã¡ãŒã«ã¢ãã¬ã¹ã§ç»é²ãããŠãŒã¶ãŒã¯ãæ€èšŒã§èª€æ€ç¥ãåŒãèµ·ããå¯èœæ§ã®ããç°åžžãªãã¡ã€ã³æ§æãæã£ãŠããå ŽåããããŸãããã©ãŒã«ããã¯æŠç¥ãå®è£ ããæ€èšŒãæ±ºå®çã§ãªãå Žåã¯éä¿¡ãèš±å¯ããããšãæ€èšããŠãã ããã
ã¡ãŒã«ç¢ºèªãããŒã®èšèš
確èªãããã¡ãŒã«ã¢ãã¬ã¹ã®æææš©ãå¿ èŠãšããã¢ããªã±ãŒã·ã§ã³ã®å Žåã確èªãããŒã®èšèšã¯ãŠãŒã¶ãŒã®ã¢ã¯ãã£ããŒã·ã§ã³çã«å€§ããªåœ±é¿ãäžããŸãã
確èªã¡ãŒã«ã®é ä¿¡ã®æé©å
確èªã¡ãŒã«ã¯è¿ éã«å°çããç°¡åã«èªèã§ããå¿ èŠããããŸããæç¢ºã§èªèå¯èœãªéä¿¡è åãšä»¶åã䜿çšããŸããã¡ãŒã«æ¬æã¯ãç®ç«ã€è¡ååèµ·ãã¿ã³ã§ã·ã³ãã«ã«ä¿ã¡ãŸãã
async function sendConfirmationEmail(user) {
const token = generateSecureToken();
const confirmationUrl = `${process.env.APP_URL}/confirm-email?token=${token}`;
// Store token with expiration
await storeConfirmationToken(user.id, token, {
expiresIn: '24h'
});
await sendEmail({
to: user.email,
from: {
name: 'Your App',
email: 'noreply@yourapp.com'
},
subject: 'Confirm your email address',
html: `
<div style="max-width: 600px; margin: 0 auto; font-family: sans-serif;">
<h1>Welcome to Your App!</h1>
<p>Please confirm your email address to complete your registration.</p>
<a href="${confirmationUrl}"
style="display: inline-block; padding: 12px 24px;
background-color: #007bff; color: white;
text-decoration: none; border-radius: 4px;">
Confirm Email Address
</a>
<p style="margin-top: 20px; color: #666; font-size: 14px;">
This link expires in 24 hours. If you didn't create an account,
you can safely ignore this email.
</p>
</div>
`,
text: `Welcome! Please confirm your email by visiting: ${confirmationUrl}`
});
}
function generateSecureToken() {
const crypto = require('crypto');
return crypto.randomBytes(32).toString('hex');
}
æªç¢ºèªã¢ã«ãŠã³ãã®åŠç
æªç¢ºèªã¢ã«ãŠã³ãã®æç¢ºãªããªã·ãŒãå®çŸ©ããŸãããŠãŒã¶ãŒã«ç¢ºèªã®å®äºãä¿ãããã«éå®çãªã¢ã¯ã»ã¹ãèš±å¯ããªãããæ©å¯æ©èœãä¿è·ããŸããæŠç¥çãªééã§ãªãã€ã³ããŒã¡ãŒã«ãéä¿¡ããŸãã
// Middleware to check email confirmation status
function requireConfirmedEmail(options = {}) {
const { allowGracePeriod = true, gracePeriodHours = 24 } = options;
return async (req, res, next) => {
const user = req.user;
if (user.emailConfirmed) {
return next();
}
// Allow grace period for new signups
if (allowGracePeriod) {
const signupTime = new Date(user.createdAt);
const gracePeriodEnd = new Date(signupTime.getTime() + gracePeriodHours * 60 * 60 * 1000);
if (new Date() < gracePeriodEnd) {
req.emailPendingConfirmation = true;
return next();
}
}
return res.status(403).json({
error: 'Email confirmation required',
message: 'Please check your email and click the confirmation link',
canResend: true
});
};
}
åéä¿¡æ©èœ
確èªã¡ãŒã«ãåéä¿¡ããããã®æç¢ºãªãªãã·ã§ã³ãæäŸããŸãããæªçšãé²ãããã«ã¬ãŒãå¶éãå®è£ ããŸãã
app.post('/api/resend-confirmation', async (req, res) => {
const user = req.user;
if (user.emailConfirmed) {
return res.json({ message: 'Email already confirmed' });
}
// Check rate limit
const lastSent = await getLastConfirmationEmailTime(user.id);
const minInterval = 60 * 1000; // 1 minute
if (lastSent && Date.now() - lastSent < minInterval) {
const waitSeconds = Math.ceil((minInterval - (Date.now() - lastSent)) / 1000);
return res.status(429).json({
error: 'Please wait before requesting another email',
retryAfter: waitSeconds
});
}
await sendConfirmationEmail(user);
await updateLastConfirmationEmailTime(user.id);
res.json({ message: 'Confirmation email sent' });
});
ã¢ãã€ã«ç»é²ã®èæ ®äºé
ã¢ãã€ã«ç»é²ãããŒã¯ãç»é¢ãå°ããã¿ããã€ã³ã¿ãŒãã§ãŒã¹ã§ãããããã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã«ç¹å¥ãªæ³šæãå¿ èŠã§ãã
ã¢ãã€ã«æé©åãããå ¥åãã£ãŒã«ã
ã¢ãã€ã«ããŒããŒããšèªåè£å®ã®äœéšãæé©åããããã«ãé©åãªå ¥åã¿ã€ããšå±æ§ã䜿çšããŸãã
<input type="email" inputmode="email" autocomplete="email" autocapitalize="none" autocorrect="off" spellcheck="false" placeholder="your@email.com" />
ã¿ãããã¬ã³ããªãŒãªãšã©ãŒè¡šç€º
ã¢ãã€ã«ã®ãšã©ãŒã¡ãã»ãŒãžã¯æç¢ºã«è¡šç€ºãããããŒããŒãã§é ãããªãããã«ããå¿ èŠããããŸããå ¥åãã£ãŒã«ãã®äžã«ãšã©ãŒãé 眮ããããããŒã¹ãéç¥ã䜿çšããããšãæ€èšããŠãã ããã
確èªã®ããã®ãã£ãŒããªã³ã¯
ã¢ãã€ã«ç¢ºèªã¡ãŒã«ã¯ãã€ã³ã¹ããŒã«ãããŠããå Žåã¯ã¢ããªå ã§çŽæ¥éãããã«ãã£ãŒããªã³ã¯ãŸãã¯ãŠãããŒãµã«ãªã³ã¯ã䜿çšããã·ãŒã ã¬ã¹ãªäœéšãæäŸããå¿ èŠããããŸãã
function generateConfirmationUrl(token, platform) {
const webUrl = `${process.env.WEB_URL}/confirm-email?token=${token}`;
if (platform === 'ios') {
return `yourapp://confirm-email?token=${token}&fallback=${encodeURIComponent(webUrl)}`;
}
if (platform === 'android') {
return `intent://confirm-email?token=${token}#Intent;scheme=yourapp;package=com.yourapp;S.browser_fallback_url=${encodeURIComponent(webUrl)};end`;
}
return webUrl;
}
åæãšç£èŠ
ç»é²ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒãããŒãç¶ç¶çã«æ¹åããããã«äž»èŠãªææšã远跡ããŸãã
远跡ãã¹ãäž»èŠãªææš
æ€èšŒã®ããã©ãŒãã³ã¹ãçè§£ããæ¹åãã¹ãé åãç¹å®ããããã«ããããã®ææšãç£èŠããŸãã
// Analytics tracking for email verification
const analytics = {
trackValidationAttempt(email, result) {
track('email_validation_attempt', {
domain: email.split('@')[1],
result: result.valid ? 'valid' : 'invalid',
errorCode: result.code,
responseTime: result.duration,
hadSuggestion: !!result.suggestion
});
},
trackSuggestionAccepted(original, suggested) {
track('email_suggestion_accepted', {
originalDomain: original.split('@')[1],
suggestedDomain: suggested.split('@')[1]
});
},
trackSignupCompletion(user, validationHistory) {
track('signup_completed', {
emailDomain: user.email.split('@')[1],
validationAttempts: validationHistory.length,
usedSuggestion: validationHistory.some(v => v.usedSuggestion),
totalValidationTime: validationHistory.reduce((sum, v) => sum + v.duration, 0)
});
},
trackConfirmationStatus(user, status) {
track('email_confirmation', {
status, // sent, clicked, expired, resent
timeSinceSignup: Date.now() - new Date(user.createdAt).getTime(),
resendCount: user.confirmationResendCount
});
}
};
æ€èšŒãããŒã® A/B ãã¹ã
ã³ã³ããŒãžã§ã³çãæé©åããããã«ãç°ãªãæ€èšŒã¢ãããŒãããã¹ãããŸãããªã¢ã«ã¿ã€ã æ€èšŒãšéä¿¡ææ€èšŒãç°ãªããšã©ãŒã¡ãã»ãŒãžã¹ã¿ã€ã«ãããŸããŸãªç¢ºèªãããŒèšèšãæ¯èŒããŸãã
ã»ãã¥ãªãã£ã®èæ ®äºé
ç»é²æã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã¯ãæ éãªå®è£ ãå¿ èŠãšããã»ãã¥ãªãã£ã«ææãªæäœã§ãã
åææ»æã®é²æ¢
æ»æè ã¯ç»é²ãããŒã䜿çšããŠãã©ã®ã¡ãŒã«ã¢ãã¬ã¹ããã§ã«ç»é²ãããŠãããã倿ããå¯èœæ§ããããŸããåæãé²ãããã«ãäžè²«ããå¿çæéãšã¡ãã»ãŒãžãå®è£ ããŸãã
async function handleSignup(email, password) {
const startTime = Date.now();
const minResponseTime = 500;
try {
const existingUser = await findUserByEmail(email);
if (existingUser) {
// Don't reveal that user exists
// Instead, send a "password reset" email to the existing user
await sendExistingAccountNotification(existingUser);
} else {
const user = await createUser(email, password);
await sendConfirmationEmail(user);
}
// Consistent response regardless of whether user existed
const elapsed = Date.now() - startTime;
const delay = Math.max(0, minResponseTime - elapsed);
await new Promise(resolve => setTimeout(resolve, delay));
return {
success: true,
message: 'Please check your email to complete registration'
};
} catch (error) {
// Log error but return generic message
console.error('Signup error:', error);
return {
success: false,
message: 'Unable to complete registration. Please try again.'
};
}
}
ããŒã¯ã³ã®ã»ãã¥ãªãã£
確èªããŒã¯ã³ã¯æå·åŠçã«å®å šã§ãããé©åã«ç®¡çãããå¿ èŠããããŸãã
const crypto = require('crypto');
async function createConfirmationToken(userId) {
// Generate secure random token
const token = crypto.randomBytes(32).toString('hex');
// Hash token for storage (don't store plaintext)
const hashedToken = crypto
.createHash('sha256')
.update(token)
.digest('hex');
// Store with expiration
await db.confirmationTokens.create({
userId,
tokenHash: hashedToken,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000)
});
return token;
}
async function verifyConfirmationToken(token) {
const hashedToken = crypto
.createHash('sha256')
.update(token)
.digest('hex');
const record = await db.confirmationTokens.findOne({
where: {
tokenHash: hashedToken,
expiresAt: { $gt: new Date() },
usedAt: null
}
});
if (!record) {
return { valid: false, error: 'Invalid or expired token' };
}
// Mark token as used
await record.update({ usedAt: new Date() });
return { valid: true, userId: record.userId };
}
å®è£ ã®ãã¹ã
å æ¬çãªãã¹ãã«ãããã¡ãŒã«ã¢ãã¬ã¹æ€èšŒããã¹ãŠã®ã·ããªãªã§æ£ããæ©èœããããšãä¿èšŒããŸãã
ç»é²æ€èšŒã®ãã¹ãã±ãŒã¹
describe('Signup Email Verification', () => {
describe('Syntax Validation', () => {
it('accepts valid email formats', () => {
const validEmails = [
'user@example.com',
'user.name@example.com',
'user+tag@example.com',
'user@subdomain.example.com',
'user@example.co.uk'
];
validEmails.forEach(email => {
expect(validateEmailLocally(email).valid).toBe(true);
});
});
it('rejects invalid email formats', () => {
const invalidEmails = [
'invalid',
'@example.com',
'user@',
'user@@example.com',
'user@.com'
];
invalidEmails.forEach(email => {
expect(validateEmailLocally(email).valid).toBe(false);
});
});
});
describe('Typo Suggestions', () => {
it('suggests corrections for common typos', () => {
const typos = [
{ input: 'user@gmial.com', expected: 'user@gmail.com' },
{ input: 'user@yaho.com', expected: 'user@yahoo.com' },
{ input: 'user@hotmal.com', expected: 'user@hotmail.com' }
];
typos.forEach(({ input, expected }) => {
const suggestion = suggestEmailCorrection(input);
expect(suggestion?.suggestion).toBe(expected);
});
});
});
describe('API Integration', () => {
it('handles API timeouts gracefully', async () => {
// Mock a timeout
jest.spyOn(global, 'fetch').mockImplementation(() =>
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 100)
)
);
const result = await validateEmailWithAPI('user@example.com');
// Should allow submission on timeout
expect(result.valid).toBe(true);
expect(result.warning).toBeTruthy();
});
});
});
çµè«
ãŠãŒã¶ãŒç»é²æã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã®å®è£ ã«ã¯ããŠãŒã¶ãŒäœéšãã»ãã¥ãªãã£ã粟床ãããã©ãŒãã³ã¹ãå«ãè€æ°ã®æžå¿µäºé ã®ãã©ã³ã¹ãåãããšãå¿ èŠã§ãããã®ã¬ã€ãã§æŠèª¬ãããŠãããã¹ããã©ã¯ãã£ã¹ã«åŸãããšã§ãç¡å¹ãªããŒã¿ããã¢ããªã±ãŒã·ã§ã³ãä¿è·ããªãããæ£åœãªãŠãŒã¶ãŒã«ã¹ã ãŒãºã§ã¹ãã¬ã¹ã®ãªãäœéšãæäŸããç»é²ãããŒãäœæã§ããŸãã
ç»é²æã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒãæåãããããã®äž»èŠãªååã«ã¯ã圹ç«ã€ãã£ãŒãããã¯ã䌎ããªã¢ã«ã¿ã€ã ã€ã³ã©ã€ã³æ€èšŒã®æäŸãäžè¬çãªã¿ã€ããã¹ã®ä¿®æ£ææ¡ããŠãŒã¶ãŒãå§åããªãããã®æ®µéçé瀺ã®äœ¿çšãAPI é害ã«å¯Ÿããå ç¢ãªãšã©ãŒåŠçã®å®è£ ãäœéšãç¶ç¶çã«æ¹åããããã®ææšã®è¿œè·¡ãå«ãŸããŸãã
ã«ã¹ã¿ã æ€èšŒããžãã¯ãæ§ç¯ããããBillionVerify ã®ãããªãããã§ãã·ã§ãã«ãµãŒãã¹ãçµ±åãããã«ããããããããã§åãäžããæè¡ãšãã¿ãŒã³ã¯ãããŒã¿å質ãç¶æããªãã蚪åè ããšã³ã²ãŒãžã¡ã³ãã®é«ããŠãŒã¶ãŒã«å€æããç»é²æã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã®åŒ·åºãªåºç€ãæäŸããŸãã
仿¥ããç»é²ãããŒã§ããè¯ãã¡ãŒã«ã¢ãã¬ã¹æ€èšŒã®å®è£ ãéå§ããŸããããBillionVerify ã®ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒ API ã¯ããªã¢ã«ã¿ã€ã ç»é²æ€èšŒã«å¿ èŠãªé床ãšç²ŸåºŠãæäŸããŸããç¡æã¯ã¬ãžããã§å§ããããŠãé«å質ãªã¡ãŒã«ã¢ãã¬ã¹æ€èšŒãããããéãã宿ããŠãã ãããé©åãªãœãªã¥ãŒã·ã§ã³ã®éžæã«ã€ããŠã¯ãã¡ãŒã«ã¢ãã¬ã¹æ€èšŒãµãŒãã¹ã®æ¯èŒãã芧ãã ããã