{"record":{"id":"b781d2ac83becded","repo":"koala73/worldmonitor","slug":"emailcheck-reason","errorCode":null,"errorMessage":"emailCheck.reason","messagePattern":"emailCheck\\.reason","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/leads/v1/register-interest.ts","lineNumber":330,"sourceCode":"  } else {\n    const turnstileOk = await verifyTurnstile({\n      token: req.turnstileToken || '',\n      ip,\n      logPrefix: '[register-interest]',\n    });\n    if (!turnstileOk) {\n      throw new ApiError(403, 'Bot verification failed', '');\n    }\n  }\n\n  const { email, source, appVersion, referredBy } = req;\n  if (!email || email.length > MAX_EMAIL_LENGTH || !EMAIL_RE.test(email)) {\n    throw new ValidationError([{ field: 'email', description: 'Invalid email address' }]);\n  }\n\n  const emailCheck = await validateEmail(email);\n  if (!emailCheck.valid) {\n    throw new ValidationError([{ field: 'email', description: emailCheck.reason }]);\n  }\n\n  const safeSource = source ? source.slice(0, MAX_META_LENGTH) : 'unknown';\n  const safeAppVersion = appVersion ? appVersion.slice(0, MAX_META_LENGTH) : 'unknown';\n  const safeReferredBy = referredBy ? referredBy.slice(0, 20) : undefined;\n\n  const convexUrl = process.env.CONVEX_URL;\n  if (!convexUrl) {\n    throw new ApiError(503, 'Registration service unavailable', '');\n  }\n\n  const client = new ConvexHttpClient(convexUrl);\n  const result = (await client.mutation(api.registerInterest.register, {\n    email,\n    source: safeSource,\n    appVersion: safeAppVersion,\n    referredBy: safeReferredBy,\n  })) as ConvexRegisterResult;","sourceCodeStart":312,"sourceCodeEnd":348,"githubUrl":"https://github.com/koala73/worldmonitor/blob/a96956387a927b8cd7aa34b0c41fca357e746be9/server/worldmonitor/leads/v1/register-interest.ts#L312-L348","documentation":"The email passed the regex but validateEmail (server/_shared/email-validation.ts) rejected it on content, and the violation description carries the specific reason. Four possible reasons: offensive local/domain part, a disposable/temp-mail domain (mailinator, yopmail, guerrillamail, ...), a typo TLD (.con, .coma, .gmai), or a live MX lookup via Cloudflare DoH showing the domain cannot receive mail. The MX check fails OPEN on DNS errors/timeouts, so the mail-rejection reason only fires on a definitive empty MX answer.","triggerScenarios":"Registering with user@mailinator.com or any of ~40 disposable domains; \"user@gmial.con\" (typo TLD); a domain that exists but has no MX records (e.g. a parked domain or a bare A-record-only hostname); offensive words in the local part.","commonSituations":"Users masking with temp-mail services to farm referral codes (the endpoint exists precisely to stop this); typosquat endings like .con; someone using their vanity domain that has no mail routing configured.","solutions":["Use a permanent mailbox on a domain with MX records","If the reason mentions a typo TLD, correct the domain ending and resubmit","If you own the domain, add MX records so it can receive the confirmation mail","Do not retry with random disposable domains — each is checked"],"exampleFix":"// before\nawait registerInterest({ email: 'signup@mailinator.com', ... }); // \"Disposable email addresses are not allowed...\"\n\n// after\nawait registerInterest({ email: 'signup@yourcompany.com', ... }); // real domain with MX","handlingStrategy":"validation","validationCode":"// Mirror the cheap checks client-side (disposable list + typo TLD); MX stays server-side\nconst domain = email.trim().toLowerCase().split('@')[1] ?? '';\nif (DISPOSABLE_DOMAINS.has(domain)) return show('Disposable email addresses are not allowed.');\nif (TYPO_TLDS.has(domain.split('.').pop() ?? '')) return show('This email domain looks like a typo.');\nawait registerInterest({ ...req, email });","typeGuard":"function isPlausiblyDeliverableEmail(v: unknown): v is string {\n  if (!isSyntacticEmail(v)) return false;\n  const domain = v.toLowerCase().split('@')[1] ?? '';\n  return !DISPOSABLE_DOMAINS.has(domain) && !TYPO_TLDS.has(domain.split('.').pop() ?? '');\n}","tryCatchPattern":"try { await registerInterest(req); }\ncatch (e) {\n  if (isValidationError(e) && e.violations?.some(v => v.field === 'email')) {\n    const reason = e.violations.find(v => v.field === 'email')!.description;\n    showFieldError('email', reason); // reason text is user-safe: disposable / typo TLD / no MX\n    return;\n  }\n  throw e;\n}","preventionTips":["Block disposable-domain signup options in the UI before the captcha step","Suggest corrections for .con/.gmai typo endings","If a user's own domain is rejected for no MX, tell them to fix mail routing — retrying changes nothing"],"tags":["email","disposable-email","mx-lookup","validation"],"backgroundTag":"email-deliverability-check-failed","analyzedSha":"a96956387a927b8cd7aa34b0c41fca357e746be9","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}