-
Notifications
You must be signed in to change notification settings - Fork 78
feat: forward emails using resend #293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
|
This Pull-Request has not been updated for a while and will be closed soon. |
There was a problem hiding this 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 ofRESEND_API_KEY. While defensive, this creates redundancy. Consider removing the check fromgetResend()since the POST handler validates it early, or remove the early check and letgetResend()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
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.json,!**/*.jsonpackage.jsonis 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.
| 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]"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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=tsxLength 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=typescriptLength 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.tsusesRESEND_API_KEYsrc/app/api/add-contact/route.tsxusesRESEND_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.
| const buildLogContext = (payload: InboundEmailPayload) => { | ||
| return { | ||
| messageId: payload.data?.id ?? "unknown", | ||
| recipient: payload.data?.to ?? "unknown", | ||
| subject: payload.data?.subject ?? "missing subject" | ||
| }; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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 | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
🧩 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
- Get the webhook signing secret from your Webhooks page or API response.
- Read the raw request body (do NOT let your framework reparse/ stringify it).
- Extract Svix headers: svix-id, svix-timestamp, svix-signature.
- 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);
- Using Resend SDK helper:
- 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.
| console.info("forward-emails: email forwarded successfully", { | ||
| ...logContext, | ||
| forwardAddress, | ||
| resendId: sendResult?.id ?? "unknown" | ||
| }); | ||
|
|
||
| return NextResponse.json({ | ||
| message: "Email forwarded", | ||
| forwardAddress, | ||
| resendId: sendResult?.id ?? null | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| } 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 } | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
No description provided.