UNPKG

@lucidcms/core

Version:

The core of the Lucid CMS. It's responsible for spinning up the API and serving the CMS.

1 lines 6.23 kB
{"version":3,"file":"attachments.mjs","names":[],"sources":["../../../src/libs/email/attachments.ts"],"sourcesContent":["import z from \"zod\";\nimport type { ServiceResponse } from \"../../utils/services/types.js\";\nimport { copy, translate } from \"../i18n/index.js\";\nimport type { EmailAttachment } from \"./types.js\";\n\nconst MAX_ATTACHMENTS = 10;\nconst MAX_FILENAME_LENGTH = 255;\nconst CONTENT_TYPE_REGEX = /^[A-Za-z0-9!#$&^_.+-]+\\/[A-Za-z0-9!#$&^_.+-]+$/;\n\n/**\n * Keeps attachment sources on remote HTTP/S URLs only.\n */\nconst isHttpUrl = (value: string) => {\n\ttry {\n\t\treturn [\"http:\", \"https:\"].includes(new URL(value).protocol);\n\t} catch {\n\t\treturn false;\n\t}\n};\n\n/**\n * Validates CIDs for safe use in MIME headers and `cid:` HTML references.\n */\nconst isValidContentId = (value: string) => {\n\tif (value.length < 1 || value.length > 127 || /\\s/.test(value)) return false;\n\n\treturn [...value].every((char) => {\n\t\tconst code = char.charCodeAt(0);\n\t\treturn code > 31 && code !== 127;\n\t});\n};\n\n/**\n * Keeps filenames safe for MIME attachment headers.\n */\nconst isValidFilename = (value: string) => {\n\tif (\n\t\tvalue.length < 1 ||\n\t\tvalue.length > MAX_FILENAME_LENGTH ||\n\t\tvalue === \".\" ||\n\t\tvalue === \"..\" ||\n\t\t/[\\\\/]/.test(value)\n\t) {\n\t\treturn false;\n\t}\n\n\treturn [...value].every((char) => {\n\t\tconst code = char.charCodeAt(0);\n\t\treturn code > 31 && code !== 127;\n\t});\n};\n\n/**\n * Shared URL attachment schema used by all send paths before persistence.\n */\nconst emailAttachmentBaseSchema = z.object({\n\ttype: z.literal(\"url\"),\n\turl: z.url().refine((value) => isHttpUrl(value), {\n\t\tmessage: translate(\"server:core.email.attachment.url.must.use.http\"),\n\t}),\n\tfilename: z\n\t\t.string()\n\t\t.trim()\n\t\t.refine(\n\t\t\tisValidFilename,\n\t\t\ttranslate(\"server:core.email.attachment.filename.invalid\"),\n\t\t),\n\tcontentType: z\n\t\t.string()\n\t\t.trim()\n\t\t.regex(\n\t\t\tCONTENT_TYPE_REGEX,\n\t\t\ttranslate(\"server:core.email.attachment.content.type.invalid\"),\n\t\t)\n\t\t.optional(),\n});\n\n/**\n * Enforces the public attachment union, including inline CID requirements.\n */\nconst emailAttachmentSchema = z.union([\n\temailAttachmentBaseSchema.extend({\n\t\tdisposition: z.literal(\"attachment\").optional().default(\"attachment\"),\n\t\tcontentId: z.never().optional(),\n\t}),\n\temailAttachmentBaseSchema.extend({\n\t\tdisposition: z.literal(\"inline\"),\n\t\tcontentId: z\n\t\t\t.string()\n\t\t\t.trim()\n\t\t\t.refine(\n\t\t\t\tisValidContentId,\n\t\t\t\ttranslate(\"server:core.email.attachment.content.id.invalid\"),\n\t\t\t),\n\t}),\n]);\n\n/**\n * Applies per-email limits and cross-attachment checks.\n */\nconst emailAttachmentsSchema = z\n\t.array(emailAttachmentSchema)\n\t.max(\n\t\tMAX_ATTACHMENTS,\n\t\ttranslate(\"server:core.email.attachment.max.attachments\", {\n\t\t\tdata: {\n\t\t\t\tmax: MAX_ATTACHMENTS,\n\t\t\t},\n\t\t}),\n\t)\n\t.superRefine((attachments, ctx) => {\n\t\tconst contentIds = new Set<string>();\n\n\t\tfor (const [index, attachment] of attachments.entries()) {\n\t\t\tif (attachment.disposition !== \"inline\") continue;\n\n\t\t\tif (contentIds.has(attachment.contentId)) {\n\t\t\t\tctx.addIssue({\n\t\t\t\t\tcode: \"custom\",\n\t\t\t\t\tpath: [index, \"contentId\"],\n\t\t\t\t\tmessage: translate(\n\t\t\t\t\t\t\"server:core.email.attachment.content.id.must.be.unique\",\n\t\t\t\t\t),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tcontentIds.add(attachment.contentId);\n\t\t}\n\t});\n\n/**\n * Normalizes untrusted send input into the internal attachment shape.\n *\n * Returns service errors instead of throwing so send flows can fail before any\n * email or attachment rows are inserted.\n */\nexport const normalizeEmailAttachments = (\n\tattachments?: unknown[] | null,\n): Awaited<ServiceResponse<EmailAttachment[]>> => {\n\tif (!attachments || attachments.length === 0) {\n\t\treturn {\n\t\t\terror: undefined,\n\t\t\tdata: [],\n\t\t};\n\t}\n\n\tconst result = emailAttachmentsSchema.safeParse(attachments);\n\tif (!result.success) {\n\t\treturn {\n\t\t\terror: {\n\t\t\t\ttype: \"validation\",\n\t\t\t\tname: copy(\"server:core.errors.validation.name\"),\n\t\t\t\tmessage: copy(\"server:core.errors.validation.message\"),\n\t\t\t\tstatus: 400,\n\t\t\t\tzod: result.error,\n\t\t\t},\n\t\t\tdata: undefined,\n\t\t};\n\t}\n\n\treturn {\n\t\terror: undefined,\n\t\tdata: result.data,\n\t};\n};\n"],"mappings":"6GAKA,MAEM,EAAqB,iDAKrB,EAAa,GAAkB,CACpC,GAAI,CACH,MAAO,CAAC,QAAS,QAAQ,CAAC,CAAC,SAAS,IAAI,IAAI,CAAK,CAAC,CAAC,QAAQ,CAC5D,MAAQ,CACP,MAAO,EACR,CACD,EAKM,EAAoB,GACrB,EAAM,OAAS,GAAK,EAAM,OAAS,KAAO,KAAK,KAAK,CAAK,EAAU,GAEhE,CAAC,GAAG,CAAK,CAAC,CAAC,MAAO,GAAS,CACjC,IAAM,EAAO,EAAK,WAAW,CAAC,EAC9B,OAAO,EAAO,IAAM,IAAS,GAC9B,CAAC,EA0BI,EAA4B,EAAE,OAAO,CAC1C,KAAM,EAAE,QAAQ,KAAK,EACrB,IAAK,EAAE,IAAI,CAAC,CAAC,OAAQ,GAAU,EAAU,CAAK,EAAG,CAChD,QAAS,EAAU,gDAAgD,CACpE,CAAC,EACD,SAAU,EACR,OAAO,CAAC,CACR,KAAK,CAAC,CACN,OA5BsB,GAEvB,EAAM,OAAS,GACf,EAAM,OAAS,KACf,IAAU,KACV,IAAU,MACV,QAAQ,KAAK,CAAK,EAEX,GAGD,CAAC,GAAG,CAAK,CAAC,CAAC,MAAO,GAAS,CACjC,IAAM,EAAO,EAAK,WAAW,CAAC,EAC9B,OAAO,EAAO,IAAM,IAAS,GAC9B,CAAC,EAgBC,EAAU,+CAA+C,CAC1D,EACD,YAAa,EACX,OAAO,CAAC,CACR,KAAK,CAAC,CACN,MACA,EACA,EAAU,mDAAmD,CAC9D,CAAC,CACA,SAAS,CACZ,CAAC,EAKK,EAAwB,EAAE,MAAM,CACrC,EAA0B,OAAO,CAChC,YAAa,EAAE,QAAQ,YAAY,CAAC,CAAC,SAAS,CAAC,CAAC,QAAQ,YAAY,EACpE,UAAW,EAAE,MAAM,CAAC,CAAC,SAAS,CAC/B,CAAC,EACD,EAA0B,OAAO,CAChC,YAAa,EAAE,QAAQ,QAAQ,EAC/B,UAAW,EACT,OAAO,CAAC,CACR,KAAK,CAAC,CACN,OACA,EACA,EAAU,iDAAiD,CAC5D,CACF,CAAC,CACF,CAAC,EAKK,EAAyB,EAC7B,MAAM,CAAqB,CAAC,CAC5B,IACA,GACA,EAAU,+CAAgD,CACzD,KAAM,CACL,IAAK,EACN,CACD,CAAC,CACF,CAAC,CACA,aAAa,EAAa,IAAQ,CAClC,IAAM,EAAa,IAAI,IAEvB,IAAK,GAAM,CAAC,EAAO,KAAe,EAAY,QAAQ,EACjD,EAAW,cAAgB,WAE3B,EAAW,IAAI,EAAW,SAAS,GACtC,EAAI,SAAS,CACZ,KAAM,SACN,KAAM,CAAC,EAAO,WAAW,EACzB,QAAS,EACR,wDACD,CACD,CAAC,EAGF,EAAW,IAAI,EAAW,SAAS,EAErC,CAAC,EAQW,EACZ,GACiD,CACjD,GAAI,CAAC,GAAe,EAAY,SAAW,EAC1C,MAAO,CACN,MAAO,IAAA,GACP,KAAM,CAAC,CACR,EAGD,IAAM,EAAS,EAAuB,UAAU,CAAW,EAc3D,OAbK,EAAO,QAaL,CACN,MAAO,IAAA,GACP,KAAM,EAAO,IACd,EAfQ,CACN,MAAO,CACN,KAAM,aACN,KAAM,EAAK,oCAAoC,EAC/C,QAAS,EAAK,uCAAuC,EACrD,OAAQ,IACR,IAAK,EAAO,KACb,EACA,KAAM,IAAA,EACP,CAOF"}