{"record":{"id":"1710619a6e3da0a3","repo":"koala73/worldmonitor","slug":"free-email-not-allowed","errorCode":"FREE_EMAIL_NOT_ALLOWED","errorMessage":"Please use a corporate email address.","messagePattern":"Please use a corporate email address\\.","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/contactMessages.ts","lineNumber":72,"sourceCode":"    source: v.string(),\n  },\n  handler: async (ctx, args) => {\n    // Length / shape validation. Reject obviously-bogus input before\n    // it reaches the table — also a defence against prompt-injection\n    // payloads enormous enough to trip downstream LLM cost.\n    const name = clip(args.name, MAX_NAME);\n    const email = clip(args.email, MAX_EMAIL);\n    const organization = clip(args.organization, MAX_ORG);\n    const phone = clip(args.phone, MAX_PHONE);\n    const message = clip(args.message, MAX_MESSAGE, { preserveNewlines: true });\n    const source = clip(args.source, MAX_SOURCE) ?? \"unknown\";\n\n    if (!name) throw new ConvexError(\"Name is required\");\n    if (!email || !EMAIL_RE.test(email)) {\n      throw new ConvexError(\"Valid email is required\");\n    }\n    if (!isCorporateDomain(email)) {\n      throw new ConvexError({\n        kind: \"FREE_EMAIL_NOT_ALLOWED\",\n        message: \"Please use a corporate email address.\",\n      });\n    }\n\n    const normalizedEmail = email.toLowerCase();\n\n    // Throttle: cap recent submissions per email. Index lookup keeps this O(matches),\n    // which the limit caps at PER_EMAIL_LIMIT + 1.\n    const windowStart = Date.now() - PER_EMAIL_WINDOW_MS;\n    const recent = await ctx.db\n      .query(\"contactMessages\")\n      .withIndex(\"by_normalized_email_received\", (q) =>\n        q.eq(\"normalizedEmail\", normalizedEmail).gte(\"receivedAt\", windowStart),\n      )\n      .take(PER_EMAIL_LIMIT + 1);\n\n    if (recent.length >= PER_EMAIL_LIMIT) {","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/contactMessages.ts#L54-L90","documentation":"Thrown by `submit` after the email passes shape validation but `isCorporateDomain(email)` returns false. A domain is corporate only if it has a dot (TLD), is NOT in the `FREE_EMAIL_DOMAINS` set (gmail/yahoo/outlook/icloud/proton/etc.), and passes `mailchecker.isValid()` (rejecting disposable/temporary providers like mailinator, 10minutemail). Carries object data `{ kind: \"FREE_EMAIL_NOT_ALLOWED\", message: \"...\" }`, so the client branches on `err.data.kind`. This is an intentional B2B gate — the contact form is for enterprise/business inquiries, not consumer mail.","triggerScenarios":"Calling `submit` with a free-provider email (`user@gmail.com`, `user@outlook.com`, `user@yahoo.co.uk`, `user@proton.me`, `user@icloud.com`); a disposable/temporary email (`user@mailinator.com`, `user@10minutemail.com` — caught by mailchecker); or a malformed/bare-hostname domain with no TLD (`user@intranet`). The `FREE_EMAIL_DOMAINS` set and mailchecker list are the two rejection sources.","commonSituations":"A user fills the enterprise contact form with their personal Gmail; QA/testing with a throwaway inbox; a prospect whose company uses Google Workspace but types their personal address instead of the corporate one (note: a custom domain on Google Workspace like `user@acme.com` passes because the domain isn't in the free list). Locale-specific providers (qq.com, yandex.ru, web.de, orange.fr) are also blocked.","solutions":["Use a corporate/work email address with a non-free domain (e.g. `you@company.com`).","If the user's company runs on Google Workspace/Microsoft 365, use the custom domain address, not the @gmail/@outlook one.","Client-side: call the same `isCorporateDomain` check (or a mirror of `FREE_EMAIL_DOMAINS`) before submit to give instant feedback.","If the block is wrong for a legitimate domain, verify it isn't in `mailchecker`'s disposable list — `mailchecker.isValid(\"user@domain\")` must return true."],"exampleFix":"// before\nawait convex.mutation(api.contactMessages.submit, { name, email: \"jane.doe@gmail.com\", source });\n// -> ConvexError { kind: \"FREE_EMAIL_NOT_ALLOWED\" }\n\n// after — use the corporate address\nawait convex.mutation(api.contactMessages.submit, { name, email: \"jane.doe@acme.com\", source });","handlingStrategy":"validation","validationCode":"const FREE = new Set([\"gmail.com\",\"googlemail.com\",\"yahoo.com\",\"outlook.com\",\"hotmail.com\",\"icloud.com\",\"protonmail.com\",\"proton.me\",\"aol.com\",/* ... full server list */]);\nfunction isCorporate(email: string): boolean {\n  const at = email.lastIndexOf(\"@\");\n  if (at < 0) return false;\n  const domain = email.slice(at + 1).toLowerCase();\n  return domain.includes(\".\") && !FREE.has(domain);\n}\nif (!isCorporate(email)) { showFieldError(\"Use a corporate email.\"); return; }","typeGuard":"function isCorporateEmail(email: string): boolean {\n  const domain = email.split(\"@\")[1]?.toLowerCase();\n  return !!domain && domain.includes(\".\") && !FREE_EMAIL_DOMAINS.has(domain);\n}","tryCatchPattern":"try {\n  await convex.mutation(api.contactMessages.submit, { ... });\n} catch (err) {\n  if (err.data?.kind === \"FREE_EMAIL_NOT_ALLOWED\") setFieldError(\"email\", \"Please use a corporate email.\");\n  else throw err;\n}","preventionTips":["Mirror the server's FREE_EMAIL_DOMAINS set client-side.","Validate the corporate-domain rule before submit.","For Google Workspace users, prompt for the custom-domain address, not @gmail.","Run mailchecker locally to also catch disposable domains."],"tags":["validation","email","business-logic","b2b"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}