From 27622c1281cd18c751b7df147791ce6dca1b084e Mon Sep 17 00:00:00 2001 From: Mallya Date: Fri, 26 Jun 2026 23:50:13 +0530 Subject: [PATCH 1/2] fix: validate mailto links in job alerts --- frontend/src/pages/Enhance.jsx | 1 - frontend/src/pages/JobAlerts.jsx | 83 ++++++++++++++++++++++++++++---- frontend/src/utils/email.js | 12 +++++ 3 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 frontend/src/utils/email.js diff --git a/frontend/src/pages/Enhance.jsx b/frontend/src/pages/Enhance.jsx index 4f5ecd064..cc7959ded 100644 --- a/frontend/src/pages/Enhance.jsx +++ b/frontend/src/pages/Enhance.jsx @@ -644,7 +644,6 @@ for (const part of parts) { } finally { setEnhancing(false) } - } } const handleGeneratePortfolio = async () => { diff --git a/frontend/src/pages/JobAlerts.jsx b/frontend/src/pages/JobAlerts.jsx index 460ed5de9..1955bf57f 100644 --- a/frontend/src/pages/JobAlerts.jsx +++ b/frontend/src/pages/JobAlerts.jsx @@ -16,8 +16,8 @@ import { import toast from 'react-hot-toast'; import { jobAlertsApi, jobsApi } from '../services/api'; import { JobAlertModal, JobAlertsList } from '../components'; -import { SkeletonStatCards, SkeletonJobList } from '../components/ui/Skeleton' - +import { SkeletonStatCards, SkeletonJobList } from '../components/ui/Skeleton'; +import { isValidEmail } from '../utils/email'; export default function JobAlerts() { const [activeTab, setActiveTab] = useState('alerts'); // 'alerts' | 'search' const [isModalOpen, setIsModalOpen] = useState(false); @@ -238,7 +238,7 @@ export default function JobAlerts() { animate="animate" > {searchResults.map((job, index) => ( - + ))} @@ -268,23 +268,49 @@ export default function JobAlerts() { onClose={() => setIsModalOpen(false)} onSuccess={fetchStats} /> + {outreachJob && ( + setOutreachJob(null)} + /> + )} ); } // Job Card Component -function JobCard({ job, index }) { +function JobCard({ job, index , onEmail }) { const handleApply = () => { if (job.applyLink) { window.open(job.applyLink, '_blank'); } }; - const handleEmail = () => { - if (job.recruiterEmail) { - window.location.href = `mailto:${job.recruiterEmail}?subject=Application for ${job.title}`; - } - }; +const [showMailtoConfirm, setShowMailtoConfirm] = useState(false); +const recruiterEmailIsValid = isValidEmail(job.recruiterEmail); + +const handleEmail = () => { + onEmail?.(job); +}; + +const handleMailtoFallback = () => { + if (!recruiterEmailIsValid) { + toast.error("That recruiter email doesn't look valid, so we can't open your mail client."); + return; + } + setShowMailtoConfirm(true); +}; + +const confirmMailtoFallback = () => { + setShowMailtoConfirm(false); + if (!isValidEmail(job.recruiterEmail)) { + toast.error("That recruiter email doesn't look valid, so we can't open your mail client."); + return; + } + const safeEmail = job.recruiterEmail.trim(); + const safeSubject = encodeURIComponent(`Application for ${job.title || 'this role'}`); + window.location.href = `mailto:${safeEmail}?subject=${safeSubject}`; +}; return ( )} + + {recruiterEmailIsValid && ( + + )} + + {showMailtoConfirm && ( +
+
+

+ Open your mail client to apply? +

+

+ This will leave Career Pilot and open your default email app with the recruiter's address pre-filled. +

+
+ + +
+
+
+ )}
); } \ No newline at end of file diff --git a/frontend/src/utils/email.js b/frontend/src/utils/email.js new file mode 100644 index 000000000..e148877d3 --- /dev/null +++ b/frontend/src/utils/email.js @@ -0,0 +1,12 @@ + +const MAX_EMAIL_LENGTH = 254; // RFC 5321 max length for a mailbox + +const EMAIL_REGEX = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/; + +export function isValidEmail(email) { + if (typeof email !== 'string') return false; + const trimmed = email.trim(); + if (trimmed.length === 0 || trimmed.length > MAX_EMAIL_LENGTH) return false; + return EMAIL_REGEX.test(trimmed); +} \ No newline at end of file From 295859218366ff88631fd905d8dda3e871740d00 Mon Sep 17 00:00:00 2001 From: Mallya Date: Sat, 27 Jun 2026 00:20:33 +0530 Subject: [PATCH 2/2] fix: tighten recruiter email validation --- frontend/src/pages/JobAlerts.jsx | 12 +++--------- frontend/src/utils/email.js | 13 ++++++++----- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/frontend/src/pages/JobAlerts.jsx b/frontend/src/pages/JobAlerts.jsx index 1955bf57f..e7d24d72a 100644 --- a/frontend/src/pages/JobAlerts.jsx +++ b/frontend/src/pages/JobAlerts.jsx @@ -238,7 +238,7 @@ export default function JobAlerts() { animate="animate" > {searchResults.map((job, index) => ( - + ))} @@ -268,18 +268,12 @@ export default function JobAlerts() { onClose={() => setIsModalOpen(false)} onSuccess={fetchStats} /> - {outreachJob && ( - setOutreachJob(null)} - /> - )} ); } // Job Card Component -function JobCard({ job, index , onEmail }) { +function JobCard({ job, index }) { const handleApply = () => { if (job.applyLink) { window.open(job.applyLink, '_blank'); @@ -290,7 +284,7 @@ const [showMailtoConfirm, setShowMailtoConfirm] = useState(false); const recruiterEmailIsValid = isValidEmail(job.recruiterEmail); const handleEmail = () => { - onEmail?.(job); + handleMailtoFallback(); }; const handleMailtoFallback = () => { diff --git a/frontend/src/utils/email.js b/frontend/src/utils/email.js index e148877d3..37338f57f 100644 --- a/frontend/src/utils/email.js +++ b/frontend/src/utils/email.js @@ -1,12 +1,15 @@ - -const MAX_EMAIL_LENGTH = 254; // RFC 5321 max length for a mailbox - +const MAX_EMAIL_LENGTH = 254; const EMAIL_REGEX = - /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/; + /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; export function isValidEmail(email) { if (typeof email !== 'string') return false; + const trimmed = email.trim(); - if (trimmed.length === 0 || trimmed.length > MAX_EMAIL_LENGTH) return false; + + if (trimmed.length === 0 || trimmed.length > MAX_EMAIL_LENGTH) { + return false; + } + return EMAIL_REGEX.test(trimmed); } \ No newline at end of file