Skip to content

Conversation

@zenorocha
Copy link
Contributor

No description provided.

@zenorocha zenorocha marked this pull request as draft September 8, 2025 19:08
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 8, 2025

Walkthrough

Adds a new Next.js API route for forwarding inbound emails via the Resend service. The route validates incoming payloads, applies forwarding rules to determine recipient addresses, sends emails through Resend, and returns success or error responses with structured logging.

Changes

Cohort / File(s) Summary
Email forwarding API route
src/app/api/forward-emails/route.ts
New POST endpoint handling inbound email forwarding. Validates RESEND_API_KEY and payload structure, applies forwarding rules to determine recipient, sends email via Resend client, returns success with forward address and Resend message ID, or error responses. Includes helpers: getResend() for client initialization, isInboundEmailPayload() type guard, buildLogContext() for structured logging, and forwardEmail() for email transmission.

Sequence Diagram

sequenceDiagram
    participant Client
    participant API as forward-emails<br/>POST handler
    participant Validation as Validation &<br/>Type Guards
    participant Rules as Forwarding<br/>Rules Engine
    participant Resend as Resend<br/>Service

    Client->>API: POST with email payload
    API->>Validation: Validate RESEND_API_KEY
    alt Missing API Key
        Validation-->>API: Error 500
        API-->>Client: Error: missing key
    else Valid Key
        API->>Validation: Check payload type &<br/>required fields
        alt Invalid Payload
            Validation-->>API: Error
            API-->>Client: Error: invalid payload
        else Valid Payload
            API->>Rules: Determine forward address<br/>from rules or default
            Rules-->>API: Forward address
            API->>Resend: Send email to<br/>forward address
            alt Send Success
                Resend-->>API: Message ID
                API-->>Client: Success + messageId
            else Send Failure
                Resend-->>API: Error
                API-->>Client: Error: send failed
            end
        end
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

  • Validation logic for payload type guard and required fields
  • Forwarding rules engine correctness and default fallback behavior
  • Resend API client initialization and error propagation
  • Error handling coverage for missing API key and API failures

Poem

🐰 Bouncing through your inbox with glee,
Emails forward swift and free,
Resend's service, rules so bright,
Validates and sends just right,
No more manual hops today! ✉️

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to evaluate whether the description relates to the changeset. Add a pull request description explaining the purpose of the email forwarding feature, implementation details, and any relevant context.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: forward emails using resend' clearly and concisely describes the main change: adding email forwarding functionality via the Resend service.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/forward-emails-using-resend

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.

@github-actions
Copy link

github-actions bot commented Nov 8, 2025

This Pull-Request has not been updated for a while and will be closed soon.

@github-actions github-actions bot added the stale label Nov 8, 2025
@luxonauta luxonauta self-assigned this Nov 12, 2025
@luxonauta luxonauta marked this pull request as ready for review November 12, 2025 17:14
Copy link
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: 7

🧹 Nitpick comments (3)
src/app/api/forward-emails/route.ts (3)

56-66: Consider removing redundant API key check.

Both the POST handler (line 57) and getResend() (line 28) check for the presence of RESEND_API_KEY. While defensive, this creates redundancy. Consider removing the check from getResend() since the POST handler validates it early, or remove the early check and let getResend() handle it.


124-139: Make the from address configurable.

The from address is hardcoded as "[email protected]". Consider making this configurable via an environment variable to improve flexibility and maintainability.

 async function forwardEmail(to: string, subject: string, html: string) {
   const resend = getResend();
+  const fromAddress = process.env.EMAIL_FROM_ADDRESS ?? "[email protected]";

   const { data, error } = await resend.emails.send({
-    from: "[email protected]",
+    from: fromAddress,
     to: [to],
     subject,
     html
   });

56-122: Add rate limiting to prevent abuse.

This endpoint has no rate limiting, which could allow attackers to abuse it for sending large volumes of emails. Consider implementing rate limiting using Next.js middleware, a service like Vercel's rate limiting, or a dedicated rate limiting library.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a482a8d and 70d6d3f.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json, !**/*.json
  • package.json is excluded by !**/*.json
📒 Files selected for processing (1)
  • src/app/api/forward-emails/route.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/api/forward-emails/route.ts (1)
src/app/api/add-contact/route.tsx (1)
  • request (3-47)
🔇 Additional comments (4)
src/app/api/forward-emails/route.ts (4)

1-2: LGTM!

Imports are appropriate for the Next.js API route and Resend email service.


4-12: LGTM!

Type definition appropriately models the inbound webhook payload structure with optional fields that are validated by the type guard later.


35-46: LGTM!

The type guard properly validates the required fields and correctly narrows the type for downstream usage.


68-79: LGTM!

Payload parsing and validation logic is correct with appropriate error handling.

Comment on lines +14 to +23
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move forwarding rules to environment variables or external configuration.

Hardcoding email addresses (especially personal email addresses) in source code poses security and maintainability risks. If this repository is or becomes public, these addresses are exposed. Additionally, updating forwarding rules requires code changes and redeployment.

Consider storing forwarding rules in environment variables or a configuration file that can be updated without code changes.

Example using environment variables:

-const forwardingRules: Record<string, string> = {
-  "[email protected]": "[email protected]",
-  "[email protected]": "[email protected]",
-  "[email protected]": "[email protected]",
-  "[email protected]": "[email protected]",
-  "[email protected]": "[email protected]",
-  "[email protected]": "[email protected]"
-};
-
-const defaultForwardAddress = "[email protected]";
+const getForwardingRules = (): Record<string, string> => {
+  const rulesJson = process.env.EMAIL_FORWARDING_RULES;
+  if (!rulesJson) {
+    return {};
+  }
+  try {
+    return JSON.parse(rulesJson);
+  } catch {
+    console.error("forward-emails: failed to parse EMAIL_FORWARDING_RULES");
+    return {};
+  }
+};
+
+const getDefaultForwardAddress = () => {
+  return process.env.DEFAULT_FORWARD_ADDRESS ?? "[email protected]";
+};

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/api/forward-emails/route.ts around lines 14 to 23, the
forwardingRules and defaultForwardAddress are hardcoded; move them to
environment/config to avoid exposing personal emails and allow updates without
redeploy. Replace the inline object with loading logic that reads a serialized
mapping (e.g., JSON or key=value list) from an environment variable or a config
file, parse and validate entries at startup, provide a configurable default
forward address via env (with a safe fallback), and add runtime validation and
clear error logging if the config is missing or malformed; ensure no personal
addresses remain in source.

Comment on lines +25 to +33
const getResend = () => {
const apiKey = process.env.RESEND_API_KEY;

if (!apiKey) {
throw new Error("RESEND_API_KEY environment variable is not set");
}

return new Resend(apiKey);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify environment variable naming consistency.

This route uses RESEND_API_KEY, but the add-contact route uses RESEND_API_TOKEN. Inconsistent environment variable names can lead to configuration errors and confusion during deployment.

Ensure all routes use the same environment variable name for the Resend API key.


🏁 Script executed:

#!/bin/bash
# Description: Check which Resend environment variable names are used across the codebase

rg -n "RESEND_API_(KEY|TOKEN)" --type=ts --type=tsx

Length of output: 27


I need to correct the script. Let me search for the environment variables without the unrecognized file type flag:


🏁 Script executed:

#!/bin/bash
# Description: Check which Resend environment variable names are used across the codebase

rg -n "RESEND_API_(KEY|TOKEN)" --type=typescript

Length of output: 34


Let me try a simpler approach without the type flag:


🏁 Script executed:

#!/bin/bash
# Description: Check which Resend environment variable names are used across the codebase

rg -n "RESEND_API_(KEY|TOKEN)"

Length of output: 759


Standardize environment variable naming for Resend API key across all routes.

The codebase uses inconsistent environment variable names:

  • src/app/api/forward-emails/route.ts uses RESEND_API_KEY
  • src/app/api/add-contact/route.tsx uses RESEND_API_TOKEN

Both routes authenticate with the same API. Update add-contact/route.tsx to use RESEND_API_KEY to match forward-emails/route.ts and avoid deployment configuration errors.

🤖 Prompt for AI Agents
In src/app/api/add-contact/route.tsx (where RESEND_API_TOKEN is currently read),
replace usage of process.env.RESEND_API_TOKEN with process.env.RESEND_API_KEY so
it matches src/app/api/forward-emails/route.ts; update any variable names or
constants in that file to reflect RESEND_API_KEY and adjust .env/.env.example or
deployment config if present to ensure the single standardized key name is used
everywhere.

Comment on lines +48 to +54
const buildLogContext = (payload: InboundEmailPayload) => {
return {
messageId: payload.data?.id ?? "unknown",
recipient: payload.data?.to ?? "unknown",
subject: payload.data?.subject ?? "missing subject"
};
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove or mask PII from logging context.

Logging email addresses and subjects can violate privacy regulations (GDPR, CCPA). Email addresses are PII, and subjects may contain sensitive information. Consider masking or hashing these values, or omitting them entirely from logs.

Example with masked emails:

 const buildLogContext = (payload: InboundEmailPayload) => {
+  const maskEmail = (email: string) => {
+    const [local, domain] = email.split('@');
+    return `${local.substring(0, 2)}***@${domain}`;
+  };
+  
   return {
     messageId: payload.data?.id ?? "unknown",
-    recipient: payload.data?.to ?? "unknown",
-    subject: payload.data?.subject ?? "missing subject"
+    recipientMasked: payload.data?.to ? maskEmail(payload.data.to) : "unknown",
+    subjectLength: payload.data?.subject?.length ?? 0
   };
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const buildLogContext = (payload: InboundEmailPayload) => {
return {
messageId: payload.data?.id ?? "unknown",
recipient: payload.data?.to ?? "unknown",
subject: payload.data?.subject ?? "missing subject"
};
};
const buildLogContext = (payload: InboundEmailPayload) => {
const maskEmail = (email: string) => {
const [local, domain] = email.split('@');
return `${local.substring(0, 2)}***@${domain}`;
};
return {
messageId: payload.data?.id ?? "unknown",
recipientMasked: payload.data?.to ? maskEmail(payload.data.to) : "unknown",
subjectLength: payload.data?.subject?.length ?? 0
};
};
🤖 Prompt for AI Agents
In src/app/api/forward-emails/route.ts around lines 48 to 54, the logging
context currently includes raw recipient email and subject (PII); update it to
avoid logging raw PII by keeping messageId unchanged and replacing recipient and
subject with masked or hashed values (e.g., mask the email local-part leaving
the domain or compute a stable hash/truncated hash of the full email, and redact
or truncate/replace the subject with a fixed token or its hash). Implement a
small helper (maskOrHash) to produce consistent masked strings for recipient and
subject and use that helper when building the log context so logs no longer
contain raw email addresses or full subjects.

Comment on lines +81 to +96
const forwardAddress =
forwardingRules[payload.data.to] ?? defaultForwardAddress;
const htmlLength = payload.data.html.length;

console.info("forward-emails: forwarding email", {
...logContext,
to: payload.data.to,
forwardAddress,
htmlLength
});

const sendResult = await forwardEmail(
forwardAddress,
payload.data.subject,
payload.data.html
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consider sanitizing HTML content before forwarding.

The HTML content from inbound emails is forwarded as-is without sanitization. While Resend likely handles some security concerns, forwarding unsanitized HTML could pose risks if the forwarded emails are displayed in contexts that don't properly sanitize content.

Consider using a library like DOMPurify or similar to sanitize HTML before forwarding, or at minimum document the security assumptions being made.

🤖 Prompt for AI Agents
In src/app/api/forward-emails/route.ts around lines 81 to 96, the code forwards
payload.data.html verbatim which can transmit unsafe HTML; sanitize the HTML
before forwarding by integrating a server-side sanitizer (e.g., DOMPurify with
jsdom or the sanitize-html package), sanitize payload.data.html into a new
variable (e.g., sanitizedHtml) using a safe allowlist/config, and pass that
sanitizedHtml to forwardEmail; alternatively, if you intentionally rely on
Resend to handle sanitization, add a clear comment documenting that assumption
and why forwarding raw HTML is acceptable.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Critical: Add webhook signature verification.

This endpoint has no authentication or authorization. Anyone can POST to it, creating an open relay vulnerability that attackers could exploit to send spam or phishing emails. Resend webhooks should include a signature that you must verify to ensure requests are legitimate.

Consult the Resend documentation for webhook signature verification and implement it before deploying this route.


🌐 Web query:

How to verify webhook signatures in Resend?

💡 Result:

Short answer — use Resend's webhook secret plus Svix headers and the raw request body to verify the signature (either via the Resend SDK helper resend.webhooks.verify or directly with the Svix library).

Steps

  1. Get the webhook signing secret from your Webhooks page or API response.
  2. Read the raw request body (do NOT let your framework reparse/ stringify it).
  3. Extract Svix headers: svix-id, svix-timestamp, svix-signature.
  4. Verify:
    • Using Resend SDK helper:
      • resend.webhooks.verify({ payload: rawBody, headers: { id: req.headers['svix-id'], timestamp: req.headers['svix-timestamp'], signature: req.headers['svix-signature'] }, webhookSecret: process.env.RESEND_WEBHOOK_SECRET })
    • Or using svix library (Node example):
      • npm install svix
      • const { Webhook } = require('svix'); const wh = new Webhook(secret); wh.verify(payload, headers);
  5. On verification failure, reject the request (HTTP 400). On success, process the parsed payload.

Notes

  • Always use the raw request text — even minor changes (JSON re-serialization) will break the signature.
  • The headers used are svix-id, svix-timestamp, svix-signature and the signing secret is returned when creating/listing webhooks.

Sources: Resend docs (Verify Webhooks Requests), Resend changelog (webhook API & verify examples). [1][2]

References
[1] Resend — Verify Webhooks Requests.
[2] Resend — Managing Webhooks via API / verify example.



---

🏁 Script executed:

```shell
#!/bin/bash
# View the complete route handler to check for authentication/webhook verification
cat src/app/api/forward-emails/route.ts

Length of output: 3500


Implement webhook signature verification before processing requests.

The POST handler has no webhook authentication. This is a critical open relay vulnerability—anyone can forge requests to forward arbitrary emails. Resend webhooks use Svix headers (svix-id, svix-timestamp, svix-signature) that must be verified with the webhook secret. Use the Resend SDK helper resend.webhooks.verify() or the Svix library, passing the raw request body, headers, and RESEND_WEBHOOK_SECRET. Reject requests with verification failures (HTTP 400). Add this check at the start of the POST handler before calling request.json().

🤖 Prompt for AI Agents
In src/app/api/forward-emails/route.ts around lines 81 to 96, add webhook
signature verification at the very start of the POST handler before calling
request.json(): obtain the raw request body (text or ArrayBuffer/Uint8Array) and
the incoming headers (svix-id, svix-timestamp, svix-signature), then call the
Resend helper (resend.webhooks.verify(rawBody, headers,
process.env.RESEND_WEBHOOK_SECRET)) or Svix equivalent to verify the signature;
if verification fails return HTTP 400 and stop processing, otherwise continue to
call request.json() and the existing forwarding logic.

Comment on lines +98 to +108
console.info("forward-emails: email forwarded successfully", {
...logContext,
forwardAddress,
resendId: sendResult?.id ?? "unknown"
});

return NextResponse.json({
message: "Email forwarded",
forwardAddress,
resendId: sendResult?.id ?? null
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider removing forwardAddress from response.

Returning the forwardAddress in the response body exposes internal routing information that the webhook caller doesn't need. This could be considered information disclosure. Consider returning only the resendId and a generic success message.

 return NextResponse.json({
   message: "Email forwarded",
-  forwardAddress,
   resendId: sendResult?.id ?? null
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.info("forward-emails: email forwarded successfully", {
...logContext,
forwardAddress,
resendId: sendResult?.id ?? "unknown"
});
return NextResponse.json({
message: "Email forwarded",
forwardAddress,
resendId: sendResult?.id ?? null
});
return NextResponse.json({
message: "Email forwarded",
resendId: sendResult?.id ?? null
});
🤖 Prompt for AI Agents
In src/app/api/forward-emails/route.ts around lines 98 to 108, the response
includes forwardAddress which exposes internal routing info; remove
forwardAddress from the JSON response and only return a generic success message
and the resendId (or null). Update the console.info log can keep forwardAddress
for internal logs but ensure the HTTP response omits forwardAddress and returns
{ message: "Email forwarded", resendId: sendResult?.id ?? null } instead.

Comment on lines +109 to +121
} catch (error) {
const message =
error instanceof Error ? error.message : "Unknown error encountered";

console.error("forward-emails: failed to forward email", {
message
});

return NextResponse.json(
{ error: "Failed to forward email" },
{ status: 500 }
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Improve error logging for debugging.

The error handler only logs error.message, which loses important debugging context like stack traces. Consider logging the full error object and including request context to aid in troubleshooting production issues.

   } catch (error) {
     const message =
       error instanceof Error ? error.message : "Unknown error encountered";

-    console.error("forward-emails: failed to forward email", {
-      message
-    });
+    console.error("forward-emails: failed to forward email", {
+      error: error instanceof Error ? { message: error.message, stack: error.stack } : error,
+      timestamp: new Date().toISOString()
+    });
🤖 Prompt for AI Agents
In src/app/api/forward-emails/route.ts around lines 109 to 121, the catch block
only logs error.message which loses stack and context; update the handler to log
the full error object (including error.stack when available) and include
relevant request context (e.g., request ID, headers or identifying fields from
the body) in the log entry so you can trace the failing request; ensure you pass
the actual error object to console.error or your structured logger (not just a
string) and keep the response to the client unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

3 participants