{"record":{"id":"9764814e65e9c4fb","repo":"koala73/worldmonitor","slug":"invalid-email","errorCode":null,"errorMessage":"Invalid email","messagePattern":"Invalid email","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/leads/v1/submit-contact.ts","lineNumber":153,"sourceCode":"  }\n\n  const ip = getClientIp(ctx.request);\n  const country = ctx.request.headers.get('cf-ipcountry')\n    || ctx.request.headers.get('x-vercel-ip-country');\n\n  const turnstileOk = await verifyTurnstile({\n    token: req.turnstileToken || '',\n    ip,\n    logPrefix: '[contact]',\n  });\n  if (!turnstileOk) {\n    throw new ApiError(403, 'Bot verification failed', '');\n  }\n\n  const { email, name, organization, phone, message, source } = req;\n\n  if (!email || !EMAIL_RE.test(email)) {\n    throw new ValidationError([{ field: 'email', description: 'Invalid email' }]);\n  }\n\n  const emailDomain = email.split('@')[1]?.toLowerCase();\n  if (emailDomain && FREE_EMAIL_DOMAINS.has(emailDomain)) {\n    throw new ApiError(422, 'Please use your work email address', '');\n  }\n\n  if (!name || name.trim().length === 0) {\n    throw new ValidationError([{ field: 'name', description: 'Name is required' }]);\n  }\n  if (!organization || organization.trim().length === 0) {\n    throw new ValidationError([{ field: 'organization', description: 'Company is required' }]);\n  }\n  if (!phone || !PHONE_RE.test(phone.trim())) {\n    throw new ValidationError([{ field: 'phone', description: 'Valid phone number is required' }]);\n  }\n\n  const safeName = name.slice(0, MAX_FIELD);","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/koala73/worldmonitor/blob/a96956387a927b8cd7aa34b0c41fca357e746be9/server/worldmonitor/leads/v1/submit-contact.ts#L135-L171","documentation":"Contact-form email gate (submit-contact.ts:152): the email must be present and match /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/ or a ValidationError on field 'email' is thrown. Unlike the waitlist endpoint there is no 320-char cap here, and this fires after Turnstile — so a 403 'Bot verification failed' will mask an invalid email until the captcha passes.","triggerScenarios":"Submitting \"user@localhost\", whitespace-containing emails, double-@ addresses, or an empty string; also any submit where the honeypot 'website' field is empty but the widget token was valid and the email was never validated client-side.","commonSituations":"Forms relying only on HTML validation that browsers skip (e.g. custom submit via JS with novalidate); pasting from spreadsheets that appends trailing delimiters; integration tests with placeholder emails.","solutions":["Validate with the same regex client-side before submit (and before spending a Turnstile token)","Trim whitespace and set <input type=\"email\" required>","Order client checks: email syntax first, then captcha, so users never burn single-use tokens on doomed submits"],"exampleFix":"// before\nconst token = await solveCaptcha();\nawait submitContact({ ...form, email: 'not-an-email', turnstileToken: token }); // burns token, then 4xx\n\n// after\nconst email = form.email.trim();\nif (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) throw new Error('invalid email');\nconst token = await solveCaptcha();\nawait submitContact({ ...form, email, turnstileToken: token });","handlingStrategy":"validation","validationCode":"const EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst email = raw.trim();\nif (!EMAIL_RE.test(email)) { showFieldError('email', 'Invalid email'); return; }\nconst turnstileToken = await solveCaptcha(); // spend the token only after syntax passes\nawait submitContact({ ...form, email, website: '', turnstileToken });","typeGuard":"function isSyntacticEmail(v: unknown): v is string {\n  return typeof v === 'string' && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v);\n}","tryCatchPattern":"try { await submitContact(form); }\ncatch (e) {\n  if (isValidationError(e) && e.violations?.some(v => v.field === 'email')) {\n    showFieldError('email', 'Invalid email'); return;\n  }\n  throw e;\n}","preventionTips":["Validate email syntax before solving the captcha — tokens are single-use and wasted on doomed submits","Trim input; use type=\"email\" required","Never populate the honeypot 'website' field in tests — it silently swallows the submission"],"tags":["email","validation","form-input"],"backgroundTag":"invalid-email-format","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"}