Skip to content

feature: Job-agent email delivery with Resend test sender, rate limits, and UI cooldowns#327

Merged
Sachinchaurasiya360 merged 1 commit into
Sachinchaurasiya360:mainfrom
ojasdhargave-iiitv:feature/email-job-agent
May 21, 2026
Merged

feature: Job-agent email delivery with Resend test sender, rate limits, and UI cooldowns#327
Sachinchaurasiya360 merged 1 commit into
Sachinchaurasiya360:mainfrom
ojasdhargave-iiitv:feature/email-job-agent

Conversation

@ojasdhargave-iiitv
Copy link
Copy Markdown

@ojasdhargave-iiitv ojasdhargave-iiitv commented May 18, 2026

Summary

Implements “Email me these jobs” for the AI job-agent so students can receive matched jobs by email, with server-side throttling and client cooldown UX.

What & why

Adds a student-only email flow for job-agent results so users can receive their matches in their inbox and revisit them later.
Implements server-side rate limiting (60s cooldown + 5/day) with durable logging to prevent abuse and avoid repeated sends.

Backend changes

[POST /api/job-agent/email-jobs] added behind auth and student role.
Fetches the student’s email from DB (never from request body).
Re-queries [jobIndex] for only active, non-expired job IDs, preserves requested order, and drops invalid jobs.
Generates HTML + text email payloads with job details and internal job links.
Logs sends after successful delivery for accurate cooldown enforcement.

Frontend changes

Adds an “Email me these jobs” action under assistant messages that include jobs.
Shows a success state briefly, then starts a 60s local cooldown.
Handles 429 with server-provided [retryAfter] countdown; shows inline error for other failures.
Passes the preceding user prompt as optional context for the email header.

Email delivery behavior

Currently uses Resend’s test sender (onboarding@resend.dev), so emails land in the Resend test inbox.
For real student inbox delivery, verify a domain in Resend and set [EMAIL_FROM] to a verified sender on that domain.

Files touched

Server: [job-agent.service.ts], [email.utils.ts], [email-templates.ts]
Client: client/src/module/student/ai-agent/AgentMessage.tsx and related UI component(s)

Testing

Manual: trigger “Email me these jobs” in the job agent UI and verify 200 response.
Confirm cooldown behavior on repeat sends.
(Note: Actual inbox delivery requires verified domain in Resend.)

Closes #41.

Screenshots

image image

Summary by CodeRabbit

Release Notes

  • New Features
    • Users can now email job recommendations from the job agent directly to their account
    • Added rate limiting with cooldown periods and daily sending caps
    • Enhanced error messages with helpful retry guidance for failed email submissions

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

Implements "Email me these jobs" feature for the job-agent chat. Students can click a button below job cards to email matched jobs to their inbox. Client collects job IDs and preceding chat context, calls a new POST /job-agent/email-jobs endpoint which validates input, enforces per-user rate limits (60s cooldown, 5/day cap), sends a formatted HTML+text email via the existing mailer, and logs the send attempt to track usage and enforce limits.

Changes

Email Jobs Feature

Layer / File(s) Summary
Client-side email UI and integration
client/src/module/student/job-agent/AgentMessage.tsx, client/src/module/student/job-agent/EmailJobsButton.tsx, client/src/module/student/job-agent/JobAgentPage.tsx
AgentMessage imports and renders EmailJobsButton when jobs are present and not streaming. JobAgentPage computes and passes precedingUserPrompt (the prior user message) for context. EmailJobsButton manages cooldown state via local interval, calls POST /job-agent/email-jobs via React Query mutation, and displays UI feedback for sent/pending/error states with countdown timer and retry-after handling.
Server endpoint, validation, and routing
server/src/module/job-agent/job-agent.validation.ts, server/src/module/job-agent/job-agent.routes.ts, server/src/module/job-agent/job-agent.controller.ts
emailJobsSchema validates jobIds (1–20 positive integers) and optional context (max 2000 chars). New POST /email-jobs route wires to controller. emailJobs handler extracts authenticated userId, validates payload, calls service.emailJobs, and catches JobAgentEmailError to return custom-shaped JSON with optional retryAfter.
Email service implementation and database persistence
server/src/module/job-agent/job-agent.service.ts, server/src/database/prisma/schema/base.prisma, server/src/database/prisma/migrations/20260519000000_add_job_agent_email_log/migration.sql
JobAgentEmailError class carries statusCode and retryAfter. Helper functions build job URLs by sourceType (INTERNAL/ADMIN/SCRAPED), truncate context, and compute time ranges. emailJobs method enforces per-user cooldown (60s) and daily limit (5 sends) via jobAgentEmailLog queries, deduplicates and filters eligible jobs, resolves ADMIN URLs via admin slug, builds email payloads, sends via sendEmail, and persists log record before returning { sent, count }. Prisma schema adds user.jobAgentEmailLogs relation and jobAgentEmailLog model. Migration creates jobAgentEmailLog table with userId FK (cascade delete), jobIds array, optional context, sentCount, createdAt timestamp, and composite index on (userId, createdAt).
Email templates and utility updates
server/src/utils/email.utils.ts, server/src/utils/email-templates.ts
sendEmail updated to accept optional text field and return Promise. Adds test sender/recipient constants and recipient sandbox logic (redirects to test address when not in production). Throws on error; returns true on success. jobAgentJobsEmailHtml generates HTML email with student first name, optional truncated chat context, per-job cards (company, title, location, salary, deadline, description snippet, view-job link), and footer with settings link. jobAgentJobsEmailText renders plaintext equivalent. Helper utilities sanitize HTML, truncate long text, and format US locale deadlines.

Sequence Diagram

sequenceDiagram
  participant Student
  participant Client as EmailJobsButton
  participant APIEndpoint as POST /email-jobs
  participant Service as JobAgentService
  participant Mailer as sendEmail
  participant Database as jobAgentEmailLog
  
  Student->>Client: click "Email me these jobs"
  Client->>Client: build { jobIds, context }
  Client->>APIEndpoint: POST with payload
  APIEndpoint->>Service: emailJobs(userId, input)
  Service->>Service: check cooldown & daily limit
  Service->>Service: fetch & filter eligible jobs
  Service->>Service: build email payload (HTML+text)
  Service->>Mailer: sendEmail(to, subject, html, text)
  Mailer-->>Service: true (success) or throw
  Service->>Database: persist jobAgentEmailLog row
  Service-->>APIEndpoint: { sent: true, count }
  APIEndpoint-->>Client: response
  Client->>Client: set 60s cooldown, show "Sent"
  Client-->>Student: feedback + countdown
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Sachinchaurasiya360/InternHack#226: Refactors message state/rendering logic in JobAgentPage.tsx, which overlaps with this PR's addition of precedingUserPrompt derivation in the same file.
  • Sachinchaurasiya360/InternHack#14: Establishes the initial job-agent chat UI scaffold (AgentMessage.tsx, JobAgentPage.tsx), which this PR extends with email functionality.

Suggested labels

enhancement, level:intermediate

Poem

🐰 With whiskers and care, we've added new cheer—
Now students can email their jobs held so dear!
One click sends the cards to their inbox with grace,
Rate limits protect us; no spam in this place! 📧

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: adding job-agent email delivery with test sender support, rate limits, and UI cooldowns.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #41: email button UI, server endpoint with rate limiting, email templates, Zod validation, and client cooldown logic.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the email delivery feature. No unrelated modifications or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
client/src/module/student/job-agent/EmailJobsButton.tsx (1)

114-114: ⚡ Quick win

Replace arbitrary text size class with a scale token.

Line 114 uses text-[10px], which violates the Tailwind sizing rule in this repo.

♻️ Proposed fix
-        <span className="text-[10px] font-mono uppercase tracking-widest text-red-500 dark:text-red-400">
+        <span className="text-xs font-mono uppercase tracking-widest text-red-500 dark:text-red-400">

As per coding guidelines, "Do not use arbitrary bracket sizes like text-[17px], use standard scale classes instead".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/module/student/job-agent/EmailJobsButton.tsx` at line 114, In
EmailJobsButton.tsx replace the arbitrary Tailwind class text-[10px] on the
<span> (the span in the EmailJobsButton component) with the repo's standard
scale token—e.g., use text-xs (or the closest approved token) instead of
text-[10px]; if 10px is required, update the design token/scale instead of using
an arbitrary bracket size.
server/src/utils/email-templates.ts (1)

1337-1348: 💤 Low value

Redundant date parsing in plain text template.

formatJobDeadline(job.deadline) is called twice for the same value—once to check truthiness and again to interpolate. The HTML version correctly stores the result in a variable first (line 1271).

♻️ Suggested fix
   const jobLines = args.jobs.map((job, index) => {
+    const deadline = formatJobDeadline(job.deadline);
     const parts = [
       `${index + 1}. ${job.title} - ${job.company}`,
       job.location ? `Location: ${job.location}` : null,
       job.salary ? `Salary: ${job.salary}` : null,
-      formatJobDeadline(job.deadline) ? `Apply by: ${formatJobDeadline(job.deadline)}` : null,
+      deadline ? `Apply by: ${deadline}` : null,
       snippet(job.description, 200) || null,
       `View job: ${job.url}`,
     ].filter(Boolean);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/utils/email-templates.ts` around lines 1337 - 1348, In the
jobLines plain-text template mapping, avoid calling
formatJobDeadline(job.deadline) twice; compute const formattedDeadline =
formatJobDeadline(job.deadline) inside the args.jobs.map callback and then use
formattedDeadline for the truthiness check and the interpolated "Apply by: ..."
string (similar to the HTML version), so only one parse/format occurs per job in
the jobLines construction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/src/module/job-agent/job-agent.service.ts`:
- Around line 478-483: The job eligibility query in prisma.jobIndex.findMany
(the block filtering by uniqueJobIds, isActive, and deadline) is missing the
publish-state predicate used by the job feed search; update the where clause to
include the same publish predicate used in your feed search logic (the predicate
that enforces published visibility) so only published jobs are returned—locate
the publish predicate implementation used by the feed search query and add it
into the where object alongside isActive and deadline in the
JobAgentService/job-agent.service.ts eligibility query.
- Around line 440-453: The current sequence reads rate-limit state
(prisma.jobAgentEmailLog.findFirst, .count and startOfToday()) then sends and
logs later, allowing concurrent requests to bypass the 60s/5-per-day limits;
change this to an atomic reservation inside a transaction or per-user lock:
within a single DB transaction use prisma.jobAgentEmailLog.create (or an
upsert/pending marker) plus a count/query to check last sent timestamp and
today's count and fail the operation if limits would be exceeded, then commit so
only one request can reserve/send at a time; update the code paths that call
prisma.jobAgentEmailLog.findFirst, prisma.jobAgentEmailLog.count, and the later
logging/creation to use this transactional reservation pattern (or SELECT ...
FOR UPDATE equivalent) around userId/startOfToday() checks to enforce the limits
atomically.

---

Nitpick comments:
In `@client/src/module/student/job-agent/EmailJobsButton.tsx`:
- Line 114: In EmailJobsButton.tsx replace the arbitrary Tailwind class
text-[10px] on the <span> (the span in the EmailJobsButton component) with the
repo's standard scale token—e.g., use text-xs (or the closest approved token)
instead of text-[10px]; if 10px is required, update the design token/scale
instead of using an arbitrary bracket size.

In `@server/src/utils/email-templates.ts`:
- Around line 1337-1348: In the jobLines plain-text template mapping, avoid
calling formatJobDeadline(job.deadline) twice; compute const formattedDeadline =
formatJobDeadline(job.deadline) inside the args.jobs.map callback and then use
formattedDeadline for the truthiness check and the interpolated "Apply by: ..."
string (similar to the HTML version), so only one parse/format occurs per job in
the jobLines construction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0233540a-b109-4d9f-9289-9bbefe27f882

📥 Commits

Reviewing files that changed from the base of the PR and between 3552efa and 6eda633.

📒 Files selected for processing (11)
  • client/src/module/student/job-agent/AgentMessage.tsx
  • client/src/module/student/job-agent/EmailJobsButton.tsx
  • client/src/module/student/job-agent/JobAgentPage.tsx
  • server/src/database/prisma/migrations/20260519000000_add_job_agent_email_log/migration.sql
  • server/src/database/prisma/schema/base.prisma
  • server/src/module/job-agent/job-agent.controller.ts
  • server/src/module/job-agent/job-agent.routes.ts
  • server/src/module/job-agent/job-agent.service.ts
  • server/src/module/job-agent/job-agent.validation.ts
  • server/src/utils/email-templates.ts
  • server/src/utils/email.utils.ts

Comment on lines +440 to +453
const [user, lastEmail, sentToday] = await Promise.all([
prisma.user.findUnique({
where: { id: userId },
select: { name: true, email: true },
}),
prisma.jobAgentEmailLog.findFirst({
where: { userId },
orderBy: { createdAt: "desc" },
select: { createdAt: true },
}),
prisma.jobAgentEmailLog.count({
where: { userId, createdAt: { gte: startOfToday() } },
}),
]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make rate-limit enforcement atomic to avoid concurrent bypass.

Lines 440-475 read cooldown/daily state before send, while Line 552 logs afterward. Concurrent requests can both pass checks and both send, violating the 60s and 5/day guarantees.

Use an atomic reservation strategy (e.g., transactional per-user lock + pending log row/state) so only one request can pass the gate at a time.

Also applies to: 459-475, 552-559

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/module/job-agent/job-agent.service.ts` around lines 440 - 453, The
current sequence reads rate-limit state (prisma.jobAgentEmailLog.findFirst,
.count and startOfToday()) then sends and logs later, allowing concurrent
requests to bypass the 60s/5-per-day limits; change this to an atomic
reservation inside a transaction or per-user lock: within a single DB
transaction use prisma.jobAgentEmailLog.create (or an upsert/pending marker)
plus a count/query to check last sent timestamp and today's count and fail the
operation if limits would be exceeded, then commit so only one request can
reserve/send at a time; update the code paths that call
prisma.jobAgentEmailLog.findFirst, prisma.jobAgentEmailLog.count, and the later
logging/creation to use this transactional reservation pattern (or SELECT ...
FOR UPDATE equivalent) around userId/startOfToday() checks to enforce the limits
atomically.

Comment on lines +478 to +483
const jobs = await prisma.jobIndex.findMany({
where: {
id: { in: uniqueJobIds },
isActive: true,
OR: [{ deadline: null }, { deadline: { gte: now } }],
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add publish-state filtering in job eligibility query.

Line 478 currently filters only by isActive and deadline; it does not enforce the “published” constraint from the endpoint requirements. This can email jobs that shouldn’t be user-visible yet.

Use the same publish predicate used by your job feed search query in this where clause.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/module/job-agent/job-agent.service.ts` around lines 478 - 483, The
job eligibility query in prisma.jobIndex.findMany (the block filtering by
uniqueJobIds, isActive, and deadline) is missing the publish-state predicate
used by the job feed search; update the where clause to include the same publish
predicate used in your feed search logic (the predicate that enforces published
visibility) so only published jobs are returned—locate the publish predicate
implementation used by the feed search query and add it into the where object
alongside isActive and deadline in the JobAgentService/job-agent.service.ts
eligibility query.

@Sachinchaurasiya360 Sachinchaurasiya360 added enhancement New feature or request level:intermediate Requires moderate project understanding gssoc:approved Approved for GSSoC scoring labels May 21, 2026
@Sachinchaurasiya360 Sachinchaurasiya360 merged commit 4219b4b into Sachinchaurasiya360:main May 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request gssoc:approved Approved for GSSoC scoring level:intermediate Requires moderate project understanding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: "Email me these jobs" button — send current chat's job cards to student's inbox

2 participants