From b9d430e3e6b4d883e8004d274a6d4eafdc671db4 Mon Sep 17 00:00:00 2001 From: muskan2320 Date: Sun, 28 Jun 2026 00:17:42 +0530 Subject: [PATCH 1/4] fix(job-alerts): validate recruiter email and replace direct mailto with in-app outreach --- frontend/src/__tests__/email.test.js | 57 +++++++++++++ frontend/src/pages/JobAlerts.jsx | 120 +++++++++++++++++++++++---- frontend/src/utils/emailCheck.js | 37 +++++++++ 3 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 frontend/src/__tests__/email.test.js create mode 100644 frontend/src/utils/emailCheck.js diff --git a/frontend/src/__tests__/email.test.js b/frontend/src/__tests__/email.test.js new file mode 100644 index 000000000..acdf9a03d --- /dev/null +++ b/frontend/src/__tests__/email.test.js @@ -0,0 +1,57 @@ +import { describe, expect, test } from 'vitest' +import { + MAX_EMAIL_LENGTH, + sanitizeRecruiterEmail, + isValidRecruiterEmail, + buildSafeMailtoUrl, +} from '../utils/emailCheck' + +describe('sanitizeRecruiterEmail', () => { + test('returns a trimmed email', () => { + expect(sanitizeRecruiterEmail(' jobs@startup.com ')).toBe('jobs@startup.com') + }) + + test('strips injected query parameters', () => { + expect(sanitizeRecruiterEmail('recruiter@co.com?cc=attacker@evil.com')).toBe('recruiter@co.com') + expect(sanitizeRecruiterEmail('recruiter@co.com&body=phish')).toBe('recruiter@co.com') + }) + + test('caps length at MAX_EMAIL_LENGTH', () => { + const longLocal = 'a'.repeat(MAX_EMAIL_LENGTH) + expect(sanitizeRecruiterEmail(`${longLocal}@example.com`).length).toBe(MAX_EMAIL_LENGTH) + }) +}) + +describe('isValidRecruiterEmail', () => { + test('accepts valid addresses', () => { + expect(isValidRecruiterEmail('jobs@startup.com')).toBe(true) + }) + + test('rejects invalid addresses', () => { + expect(isValidRecruiterEmail('not-an-email')).toBe(false) + expect(isValidRecruiterEmail('')).toBe(false) + expect(isValidRecruiterEmail(null)).toBe(false) + }) + + test('accepts address after stripping injected query parameters', () => { + expect(isValidRecruiterEmail('recruiter@co.com?cc=attacker@evil.com')).toBe(true) + expect(sanitizeRecruiterEmail('recruiter@co.com?cc=attacker@evil.com')).toBe('recruiter@co.com') + }) +}) + +describe('buildSafeMailtoUrl', () => { + test('builds a mailto link with encoded subject', () => { + expect(buildSafeMailtoUrl('jobs@startup.com', { subject: 'Application for Engineer' })) + .toBe('mailto:jobs@startup.com?subject=Application+for+Engineer') + }) + + test('returns null for invalid email', () => { + expect(buildSafeMailtoUrl('bad-address', { subject: 'Hi' })).toBeNull() + }) + + test('ignores injected cc parameters from raw input', () => { + const url = buildSafeMailtoUrl('recruiter@co.com?cc=attacker@evil.com', { subject: 'Hello' }) + expect(url).toBe('mailto:recruiter@co.com?subject=Hello') + expect(url).not.toContain('attacker@evil.com') + }) +}) diff --git a/frontend/src/pages/JobAlerts.jsx b/frontend/src/pages/JobAlerts.jsx index 460ed5de9..530beca93 100644 --- a/frontend/src/pages/JobAlerts.jsx +++ b/frontend/src/pages/JobAlerts.jsx @@ -9,14 +9,21 @@ import { Mail, ExternalLink, Loader2, - AlertCircle, Sparkles, - Zap + Zap, + Send } from 'lucide-react'; 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 OutreachPanel from '../components/OutreachPanel'; +import Modal from '../components/Modal'; +import { SkeletonStatCards, SkeletonJobList } from '../components/ui/Skeleton'; +import { + buildSafeMailtoUrl, + isValidRecruiterEmail, + sanitizeRecruiterEmail, +} from '../utils/emailCheck'; export default function JobAlerts() { const [activeTab, setActiveTab] = useState('alerts'); // 'alerts' | 'search' @@ -26,6 +33,8 @@ export default function JobAlerts() { const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [searchLoading, setSearchLoading] = useState(false); + const [outreachJob, setOutreachJob] = useState(null); + const [mailtoConfirm, setMailtoConfirm] = useState(null); const hoverBorderClassMap = { indigo: 'hover:border-primary/30', @@ -72,6 +81,33 @@ export default function JobAlerts() { setIsModalOpen(true); }; + const handleApplyOutreach = (job) => { + setOutreachJob(job); + }; + + const handleRequestMailto = (job) => { + const email = sanitizeRecruiterEmail(job.recruiterEmail); + if (!isValidRecruiterEmail(email)) { + toast.error('Invalid recruiter email address'); + return; + } + setMailtoConfirm({ email, title: job.title }); + }; + + const handleConfirmMailto = () => { + if (!mailtoConfirm) return; + const url = buildSafeMailtoUrl(mailtoConfirm.email, { + subject: `Application for ${mailtoConfirm.title}`, + }); + if (!url) { + toast.error('Invalid recruiter email address'); + setMailtoConfirm(null); + return; + } + window.location.href = url; + setMailtoConfirm(null); + }; + return (
@@ -238,7 +274,13 @@ export default function JobAlerts() { animate="animate" > {searchResults.map((job, index) => ( - + ))}
@@ -268,21 +310,56 @@ export default function JobAlerts() { onClose={() => setIsModalOpen(false)} onSuccess={fetchStats} /> + + {outreachJob && ( + setOutreachJob(null)} + /> + )} + + setMailtoConfirm(null)} + title="Open your mail client to apply?" + size="sm" + > +

+ You'll leave CareerPilot and open your email app to send an application + {mailtoConfirm?.email ? ` to ${mailtoConfirm.email}` : ''}. +

+
+ + +
+
); } // Job Card Component -function JobCard({ job, index }) { - const handleApply = () => { - if (job.applyLink) { - window.open(job.applyLink, '_blank'); - } - }; +function JobCard({ job, index, onApplyOutreach, onRequestMailto }) { + const recruiterEmail = sanitizeRecruiterEmail(job.recruiterEmail); + const hasValidRecruiterEmail = isValidRecruiterEmail(recruiterEmail); - const handleEmail = () => { - if (job.recruiterEmail) { - window.location.href = `mailto:${job.recruiterEmail}?subject=Application for ${job.title}`; + const handleApplyOnSite = () => { + if (job.applyLink) { + window.open(job.applyLink, '_blank', 'noopener,noreferrer'); } }; @@ -310,18 +387,27 @@ function JobCard({ job, index }) { {/* Action Buttons */}
+ {job.recruiterEmail && ( + + )} {job.applyLink && ( )} - {job.recruiterEmail && ( + {hasValidRecruiterEmail && (