fix: Add better validation for sender email

This commit is contained in:
Dries Augustyns
2026-01-08 09:04:50 +01:00
parent 940a4d225b
commit e75e07f73f
2 changed files with 52 additions and 27 deletions
+2 -2
View File
@@ -186,7 +186,7 @@
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object", "type": "object",
"required": ["to"], "required": ["to", "from"],
"properties": { "properties": {
"to": { "to": {
"oneOf": [ "oneOf": [
@@ -277,7 +277,7 @@
"description": "Sender with name and email" "description": "Sender with name and email"
} }
], ],
"description": "Custom from address (requires verified domain). Can be a string or an object with {name, email}." "description": "Sender email address (requires verified domain). Required unless using a template that has a 'from' address configured. Can be a string (e.g., '[email protected]') or an object with {name, email} (e.g., {name: 'My App', email: '[email protected]'})."
}, },
"name": { "name": {
"type": "string", "type": "string",
+41 -16
View File
@@ -68,7 +68,11 @@ export const ProjectSchemas = {
update: z.object({ update: z.object({
name: z.string().min(1).max(100).optional(), name: z.string().min(1).max(100).optional(),
tracking: z.nativeEnum(TrackingMode).optional(), tracking: z.nativeEnum(TrackingMode).optional(),
language: z.string().length(2).regex(/^[a-z]{2}$/).optional(), language: z
.string()
.length(2)
.regex(/^[a-z]{2}$/)
.optional(),
}), }),
} as const; } as const;
@@ -342,14 +346,29 @@ export const ActionSchemas = {
template: uuid.optional(), template: uuid.optional(),
subscribed: z.boolean().optional(), subscribed: z.boolean().optional(),
name: z.string().optional(), name: z.string().optional(),
from: z.union([ from: z
.union(
[
email, // Simple email string (backward compatible) email, // Simple email string (backward compatible)
z.object({ z.object({
// Object with name and email // Object with name and email
name: z.string().optional(), name: z.string().optional(),
email: email, email: email,
}), }),
]), ],
{
errorMap: (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_union) {
return {
message:
'Invalid "from" field. Expected a valid email string (e.g., "[email protected]") or an object with an email field and optional name (e.g., {email: "[email protected]", name: "My App"})',
};
}
return {message: ctx.defaultError};
},
},
)
.optional(),
reply: email.optional(), reply: email.optional(),
headers: z.record(z.string().max(998)).optional(), headers: z.record(z.string().max(998)).optional(),
data: jsonSchema.optional(), data: jsonSchema.optional(),
@@ -364,23 +383,29 @@ export const ActionSchemas = {
.max(10) // Maximum 10 attachments per email .max(10) // Maximum 10 attachments per email
.optional(), .optional(),
}) })
.refine(data => data.template ?? (data.subject && data.body), { .superRefine((data, ctx) => {
// Validate that either template or subject+body are provided
if (!data.template && !(data.subject && data.body)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Either template ID or both subject and body are required', message: 'Either template ID or both subject and body are required',
}) path: ['template'],
.refine( });
data => {
// Validate total attachment size (sum of base64 strings should be reasonable)
if (!data.attachments || data.attachments.length === 0) {
return true;
} }
// Each base64 char = ~0.75 bytes, so 13.3M base64 chars ≈ 10MB actual data
// Validate total attachment size
if (data.attachments && data.attachments.length > 0) {
const totalBase64Length = data.attachments.reduce((sum, att) => sum + att.content.length, 0); const totalBase64Length = data.attachments.reduce((sum, att) => sum + att.content.length, 0);
return totalBase64Length <= 13333333; // ~10MB limit if (totalBase64Length > 13333333) {
}, // ~10MB limit
{ ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Total attachment size must not exceed 10MB', message: 'Total attachment size must not exceed 10MB',
}, path: ['attachments'],
), });
}
}
}),
verify: z.object({ verify: z.object({
email, email,
}), }),