Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis change adds optional custom email headers across the system. Public APIs (OpenAPI, SDK typings, zod schema, Python and Node docs) now accept Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate 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. Comment |
Deploying usesend with
|
| Latest commit: |
4f237f7
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://58d50daa.usesend.pages.dev |
| Branch Preview URL: | https://codex-add-support-for-custom.usesend.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (7)
apps/docs/get-started/nodejs.mdx (1)
66-67: Clarify which system headers useSend may addThe note understates which headers are system-managed. SES integration also sets List-Unsubscribe (when unsubUrl is present), In-Reply-To/References (when replying), Precedence (bulk), and an internal reference header. Suggest clarifying to avoid surprises.
Apply this wording tweak:
- > Custom headers are forwarded as-is. useSend only manages the `X-Usesend-Email-ID` and `References` headers. + > Custom headers are forwarded as-is and merged with system headers. useSend reserves `X-Usesend-Email-ID` and `References`, and may also add `List-Unsubscribe`, `In-Reply-To`, `Precedence`, or an internal reference header when applicable.apps/web/src/server/service/email-queue-service.ts (1)
401-407: Avoid any: narrow headers without casting to anyMinor type hygiene: you can avoid (email as any) by narrowing through unknown.
Apply:
- const headers = (email as any)?.headers; + const headers = (email?.headers ?? undefined) as unknown;Or, if you want stricter typing, leverage Prisma.JsonValue in a follow-up.
apps/web/src/server/public-api/schemas/email-schema.ts (1)
22-28: Harden schema against header injection and abuseGood addition. Consider aligning validation with sanitizer and capping payload size.
Proposed refinement:
- headers: z - .record(z.string().min(1)) - .optional() - .openapi({ - description: - "Custom headers to include with the message. All headers are forwarded except `X-Usesend-Email-ID` and `References`, which useSend manages.", - }), + headers: z + .record( + z + .string() + .min(1) + .refine((v) => !/[\r\n]/.test(v), "Header values must not contain CR or LF") + .max(1024) + ) + .optional() + .superRefine((h, ctx) => { + const count = Object.keys(h ?? {}).length; + if (count > 50) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Up to 50 headers allowed" }); + } + }) + .openapi({ + description: + "Custom headers to include with the message. All headers are forwarded except `X-Usesend-Email-ID` and `References`, which useSend manages.", + }),Optionally also restrict header names with a token regex in the sanitizer (see utils/email-headers.ts comment).
apps/web/src/server/utils/email-headers.ts (2)
27-35: Validate header-name token formatPrevent invalid names (e.g., containing colon or spaces) by enforcing RFC-like token characters.
Apply:
+const VALID_HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/; ... - if (!name || RESERVED_EMAIL_HEADERS.has(name.toLowerCase())) { + if ( + !name || + RESERVED_EMAIL_HEADERS.has(name.toLowerCase()) || + !VALID_HEADER_NAME.test(name) + ) { return undefined; }
52-55: Deduplicate headers case-insensitivelyPrevent duplicates like "X-Id" and "x-id".
Apply:
- return sanitizedEntries.reduce((acc, { name, value }) => { - acc[name] = value; - return acc; - }, {} as Record<string, string>); + const seen = new Set<string>(); + return sanitizedEntries.reduce((acc, { name, value }) => { + const key = name.toLowerCase(); + if (seen.has(key)) return acc; + seen.add(key); + acc[name] = value; + return acc; + }, {} as Record<string, string>);apps/web/src/server/service/email-service.ts (1)
718-744: Reduce duplication and strengthen typing for emailCreateDataYou repeat the create payload assembly (suppressed/normal/bulk). Extract a small helper to build the object once and reuse. Also prefer Prisma.EmailCreateInput over Record<string, unknown> + any to catch schema drift at compile time.
I can draft a helper like buildEmailCreateData(...) returning Prisma.EmailCreateInput if helpful.
apps/web/src/server/aws/ses.ts (1)
232-239: Align override behavior for X-Usesend-Email-ID.You always set X-Usesend-Email-ID, but conditionally set X-Unsend-Email-ID. For consistency (and clearer intent), either:
- Treat X-Usesend-Email-ID as reserved (always set, cannot be overridden), or
- Mirror the guard and only set it when not supplied by the caller.
Currently sanitized headers will overwrite defaults anyway due to spread order, but adding the guard clarifies intent and saves work.
Apply this diff if you choose the “don’t override user value” route:
if (emailId) { - defaultHeaders["X-Usesend-Email-ID"] = emailId; + if (!sanitizedHeaderNames.has("x-usesend-email-id")) { + defaultHeaders["X-Usesend-Email-ID"] = emailId; + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
apps/docs/get-started/nodejs.mdx(1 hunks)apps/docs/get-started/python.mdx(1 hunks)apps/web/prisma/migrations/20250207121500_add_email_headers/migration.sql(1 hunks)apps/web/prisma/schema.prisma(1 hunks)apps/web/src/server/aws/ses.ts(4 hunks)apps/web/src/server/public-api/schemas/email-schema.ts(1 hunks)apps/web/src/server/service/email-queue-service.ts(4 hunks)apps/web/src/server/service/email-service.ts(8 hunks)apps/web/src/server/utils/email-headers.ts(1 hunks)apps/web/src/types/index.ts(1 hunks)packages/python-sdk/pyproject.toml(1 hunks)packages/python-sdk/usesend/types.py(2 hunks)packages/sdk/package.json(1 hunks)packages/sdk/types/schema.d.ts(2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/general.mdc)
Include all required imports, and ensure proper naming of key components.
Files:
apps/web/src/types/index.tsapps/web/src/server/public-api/schemas/email-schema.tsapps/web/src/server/utils/email-headers.tsapps/web/src/server/service/email-service.tsapps/web/src/server/service/email-queue-service.tspackages/sdk/types/schema.d.tsapps/web/src/server/aws/ses.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use TypeScript with 2-space indentation and semicolons (enforced by Prettier)
ESLint must pass with zero warnings using @usesend/eslint-config
Do not use dynamic imports (avoid import() and dynamic loading)
Files:
apps/web/src/types/index.tsapps/web/src/server/public-api/schemas/email-schema.tsapps/web/src/server/utils/email-headers.tsapps/web/src/server/service/email-service.tsapps/web/src/server/service/email-queue-service.tspackages/sdk/types/schema.d.tsapps/web/src/server/aws/ses.ts
**/*.{ts,tsx,md}
📄 CodeRabbit inference engine (AGENTS.md)
Format code and docs with Prettier 3
Files:
apps/web/src/types/index.tsapps/web/src/server/public-api/schemas/email-schema.tsapps/web/src/server/utils/email-headers.tsapps/web/src/server/service/email-service.tsapps/web/src/server/service/email-queue-service.tspackages/sdk/types/schema.d.tsapps/web/src/server/aws/ses.ts
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
apps/web/**/*.{ts,tsx}: In apps/web, use the "/" alias for src imports (e.g., import { x } from "/utils/x")
Prefer using tRPC for API calls unless explicitly instructed otherwise
Files:
apps/web/src/types/index.tsapps/web/src/server/public-api/schemas/email-schema.tsapps/web/src/server/utils/email-headers.tsapps/web/src/server/service/email-service.tsapps/web/src/server/service/email-queue-service.tsapps/web/src/server/aws/ses.ts
🧬 Code graph analysis (3)
apps/web/src/server/service/email-service.ts (1)
apps/web/src/server/utils/email-headers.ts (1)
sanitizeCustomHeaders(37-56)
apps/web/src/server/service/email-queue-service.ts (1)
apps/web/src/server/utils/email-headers.ts (1)
sanitizeCustomHeaders(37-56)
apps/web/src/server/aws/ses.ts (2)
apps/web/src/server/utils/email-headers.ts (1)
sanitizeCustomHeaders(37-56)apps/web/src/server/nanoid.ts (1)
nanoid(8-11)
🔇 Additional comments (25)
packages/python-sdk/pyproject.toml (1)
3-3: Version bump aligns Python SDK with new headers feature0.2.4 cleanly captures the newly added headers support in the types and docs.
packages/sdk/package.json (1)
3-3: JS SDK patch release makes sense1.5.3 is an appropriate patch increment for exposing the headers field downstream.
apps/web/prisma/migrations/20250207121500_add_email_headers/migration.sql (1)
1-2: Migration cleanly adds nullable JSONB columnAdding
headersas nullable keeps existing rows valid while enabling persistence of sanitized maps.apps/web/src/types/index.ts (1)
13-13: Optional headers record fits EmailContentTyping this as
Record<string, string>matches the sanitization contract and keeps the field opt-in.apps/docs/get-started/python.mdx (1)
44-52: Docs accurately reflect Python SDK headers usageExample payload and explanatory note clearly communicate the new capability.
apps/web/prisma/schema.prisma (1)
262-262: Schema change mirrors migrationAdding
headers Json?keeps Prisma in sync with the database and matches the optional semantics.packages/python-sdk/usesend/types.py (2)
197-198: TypedDict update captures optional headers
NotRequired[Dict[str, str]]slots neatly alongside the other optional fields and reflects sanitized inputs.
222-223: Batch item typing stays consistentMirroring the single-send shape ensures batch requests can include headers without special casing.
packages/sdk/types/schema.d.ts (2)
305-307: OpenAPI schema now advertises headersThe optional string map mirrors backend validation, so generated clients stay accurate.
372-374: Batch definition gets the same headers mapEnsures parity between single and batch payloads for client generators.
apps/docs/get-started/nodejs.mdx (1)
60-63: Example with headers looks goodThe example payload correctly demonstrates the new headers field.
apps/web/src/server/service/email-queue-service.ts (3)
14-14: LGTM: sanitizeCustomHeaders is correctly importedImport placement and alias usage fit the project conventions.
409-410: LGTM: re-sanitizing persisted headers before sendIdempotent and safe; protects against legacy rows or manual data edits.
428-428: Verified sendRawEmail forwards headers
Signature now includesheadersand all call sites (including email‐queue‐service) passheaders: customHeaders.apps/web/src/server/utils/email-headers.ts (1)
7-7: LGTM: CR/LF check blocks header injectionThe injection guard is appropriate.
apps/web/src/server/service/email-service.ts (5)
134-152: LGTM: persist sanitized headers for suppressed emailsHeaders are sanitized and saved when all TO recipients are suppressed.
252-277: LGTM: persist sanitized headers for normal pathConditional inclusion avoids empty JSONB writes.
553-580: LGTM: sanitize and persist headers for suppressed emails in bulkConsistent with single-send path.
655-665: LGTM: sanitize per-email in bulkSanitization is done before record creation, per item.
70-75: Confirm TS SDK EmailContent headersApps/web (
apps/web/src/types/index.ts) and the Python SDK (EmailCreate/EmailBatchIteminpackages/python-sdk/usesend/types.py) already declare an optionalheaders?: Record<string,string>; ensure the TS SDK’sEmailContenttype likewise definesheaders?: Record<string,string>.apps/web/src/server/aws/ses.ts (5)
221-225: LGTM: defensive set for presence checks.Computing a lowercase set of sanitized header names is a clean way to avoid unintended overrides.
226-231: LGTM: generate X-Entity-Ref-ID when not provided.Good defaulting without overriding user headers.
250-253: LGTM: bulk precedence header.Reasonable default when isBulk is true.
283-285: LGTM: merge order prioritizes sanitized headers over defaults.Spreading sanitized after defaults ensures user-provided headers win. Good.
205-218: API surface update approved.headersis passed separately asRecord<string, string | null | undefined>, matchingsanitizeCustomHeaders. No changes toEmailContentneeded.
6623409 to
dc7ec13
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/server/service/email-service.ts (1)
545-568: Apply the same header sanitization in bulk flowsBoth the suppressed-email records and the main bulk create path still stringify
headersdirectly. That bypasses the sanitizer and means bulk sends can persist or emit forbidden headers.@@ const { ... apiKeyId, inReplyToId, + headers, } = originalContent; + const sanitizedHeaders = headers + ? sanitizeCustomHeaders(headers) + : undefined; @@ apiId: apiKeyId, inReplyToId, - headers: originalContent.headers - ? JSON.stringify(originalContent.headers) - : undefined, + headers: sanitizedHeaders + ? JSON.stringify(sanitizedHeaders) + : undefined, @@ const { ... apiKeyId, headers, } = content; + const sanitizedHeaders = headers + ? sanitizeCustomHeaders(headers) + : undefined; @@ latestStatus: scheduledAtDate ? "SCHEDULED" : "QUEUED", apiId: apiKeyId, - headers: headers ? JSON.stringify(headers) : undefined, + headers: sanitizedHeaders + ? JSON.stringify(sanitizedHeaders) + : undefined, }, });Also applies to: 704-726
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/src/server/service/email-service.ts(6 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/general.mdc)
Include all required imports, and ensure proper naming of key components.
Files:
apps/web/src/server/service/email-service.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use TypeScript with 2-space indentation and semicolons (enforced by Prettier)
ESLint must pass with zero warnings using @usesend/eslint-config
Do not use dynamic imports (avoid import() and dynamic loading)
Files:
apps/web/src/server/service/email-service.ts
**/*.{ts,tsx,md}
📄 CodeRabbit inference engine (AGENTS.md)
Format code and docs with Prettier 3
Files:
apps/web/src/server/service/email-service.ts
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
apps/web/**/*.{ts,tsx}: In apps/web, use the "/" alias for src imports (e.g., import { x } from "/utils/x")
Prefer using tRPC for API calls unless explicitly instructed otherwise
Files:
apps/web/src/server/service/email-service.ts
🧠 Learnings (2)
📚 Learning: 2025-09-10T12:33:42.667Z
Learnt from: KMKoushik
PR: usesend/useSend#224
File: apps/web/src/server/public-api/api/emails/get-email.ts:63-74
Timestamp: 2025-09-10T12:33:42.667Z
Learning: In the useSend project, Prisma's findUnique method works with composite where clauses including id, teamId, and optional domainId fields in apps/web/src/server/public-api/api/emails/get-email.ts
Applied to files:
apps/web/src/server/service/email-service.ts
📚 Learning: 2025-09-10T12:33:42.667Z
Learnt from: KMKoushik
PR: usesend/useSend#224
File: apps/web/src/server/public-api/api/emails/get-email.ts:63-74
Timestamp: 2025-09-10T12:33:42.667Z
Learning: In the useSend project using Prisma Client 6.6.0, findUnique works with composite where clauses containing id, teamId, and optional domainId fields in apps/web/src/server/public-api/api/emails/get-email.ts, as confirmed by the project maintainer KMKoushik.
Applied to files:
apps/web/src/server/service/email-service.ts
🧬 Code graph analysis (1)
apps/web/src/server/service/email-service.ts (1)
apps/web/src/server/service/domain-service.ts (1)
validateDomainFromEmail(85-128)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cloudflare Pages
| headers, | ||
| } = emailContent; | ||
| let subject = subjectFromApiCall; |
There was a problem hiding this comment.
Sanitize headers before persisting single email records
We never call sanitizeCustomHeaders here, so both the SUPPRESSED branch and the main create path are storing raw, unsanitized header maps. That lets disallowed/unsafe headers slip into persistence and SES, undoing the protections this PR is supposed to provide. Please sanitize once and reuse the sanitized payload wherever we persist headers.
@@
const {
...
inReplyToId,
headers,
} = emailContent;
+ const sanitizedHeaders = headers
+ ? sanitizeCustomHeaders(headers)
+ : undefined;
@@
const email = await db.email.create({
data: {
...
apiId: apiKeyId,
inReplyToId,
+ headers: sanitizedHeaders
+ ? JSON.stringify(sanitizedHeaders)
+ : undefined,
},
});
@@
const email = await db.email.create({
data: {
...
apiId: apiKeyId,
inReplyToId,
- headers: headers ? JSON.stringify(headers) : undefined,
+ headers: sanitizedHeaders
+ ? JSON.stringify(sanitizedHeaders)
+ : undefined,
},
});Also applies to: 137-151, 249-272
🤖 Prompt for AI Agents
In apps/web/src/server/service/email-service.ts around lines 74-76 (and also
apply same change to 137-151 and 249-272), the code assigns headers from
emailContent and proceeds to persist/send without calling sanitizeCustomHeaders;
call sanitizeCustomHeaders once immediately after extracting headers to produce
sanitizedHeaders, replace all uses of the raw headers in both the SUPPRESSED
branch and the main create/send paths with sanitizedHeaders, and ensure that the
sanitizedHeaders object is what gets persisted to the database and passed to SES
so disallowed/unsafe headers cannot be stored or sent.
Summary
Testing
https://chatgpt.com/codex/tasks/task_e_68d056e6c4848329b99dbf5b5562800a
Summary by CodeRabbit
New Features
Documentation
SDKs
Chores