@holo-js/mail 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +32 -5
  2. package/package.json +4 -4
package/dist/index.mjs CHANGED
@@ -17,8 +17,9 @@ import {
17
17
 
18
18
  // src/runtime.ts
19
19
  import { randomUUID } from "crypto";
20
- import { mkdir, writeFile } from "fs/promises";
21
- import { join } from "path";
20
+ import { mkdir, realpath, writeFile } from "fs/promises";
21
+ import { tmpdir } from "os";
22
+ import { join, resolve, sep } from "path";
22
23
  import {
23
24
  holoMailDefaults,
24
25
  normalizeAppEnv
@@ -67,6 +68,10 @@ var mailRegistryInternals = {
67
68
 
68
69
  // src/runtime.ts
69
70
  var HOLO_MAIL_DELIVER_JOB = "holo.mail.deliver";
71
+ var LOCAL_ATTACHMENT_ROOTS = Object.freeze([
72
+ tmpdir(),
73
+ resolve(process.cwd(), "storage")
74
+ ]);
70
75
  var MailError = class extends Error {
71
76
  code;
72
77
  constructor(message, code = "MAIL_ERROR", options) {
@@ -267,7 +272,7 @@ function createPreviewHtml(preview) {
267
272
  preview.cc.length > 0 ? `<p><strong>Cc:</strong> ${preview.cc.map(formatAddress).join(", ")}</p>` : "",
268
273
  preview.bcc.length > 0 ? `<p><strong>Bcc:</strong> ${preview.bcc.map(formatAddress).join(", ")}</p>` : ""
269
274
  ].filter(Boolean).join("");
270
- const body = preview.html ? preview.html : preview.text ? `<pre>${escapeHtml(preview.text)}</pre>` : "<p>No rendered content is available.</p>";
275
+ const body = preview.html ? `<pre>${escapeHtml(preview.html)}</pre>` : preview.text ? `<pre>${escapeHtml(preview.text)}</pre>` : "<p>No rendered content is available.</p>";
271
276
  return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(preview.subject)}</title></head><body>${header}${body}</body></html>`;
272
277
  }
273
278
  function escapeHtml(value) {
@@ -477,7 +482,7 @@ async function createSmtpAttachment(attachment) {
477
482
  if (typeof attachment.path === "string") {
478
483
  return {
479
484
  ...base,
480
- path: attachment.path
485
+ path: await resolveLocalAttachmentPath(attachment.path)
481
486
  };
482
487
  }
483
488
  if (attachment.storage) {
@@ -816,7 +821,29 @@ function stripMarkdownSyntax(markdown) {
816
821
  return markdown.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/`([^`]+)`/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^[-*]\s+/gm, "").trim();
817
822
  }
818
823
  function renderMarkdownInline(markdown) {
819
- return escapeHtml(markdown).replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>').replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
824
+ return escapeHtml(markdown).replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, href) => {
825
+ return isSafeMailHref(href) ? `<a href="${escapeHtml(href)}">${label}</a>` : label;
826
+ }).replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
827
+ }
828
+ async function resolveLocalAttachmentPath(path) {
829
+ const resolvedPath = await realpath(path);
830
+ const allowedRoots = await Promise.all(LOCAL_ATTACHMENT_ROOTS.map(async (root) => await realpath(root).catch(() => null)));
831
+ if (allowedRoots.some((root) => root && (resolvedPath === root || resolvedPath.startsWith(`${root}${sep}`)))) {
832
+ return resolvedPath;
833
+ }
834
+ throw new MailError("[@holo-js/mail] Path attachments must resolve inside an allowed attachment directory.", "MAIL_ATTACHMENT_UNSAFE_PATH");
835
+ }
836
+ function isSafeMailHref(href) {
837
+ const trimmed = href.trim();
838
+ if (trimmed.startsWith("#") || trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("../")) {
839
+ return true;
840
+ }
841
+ try {
842
+ const url = new URL(trimmed);
843
+ return url.protocol === "https:" || url.protocol === "http:" || url.protocol === "mailto:";
844
+ } catch {
845
+ return false;
846
+ }
820
847
  }
821
848
  function renderMarkdown(markdown) {
822
849
  const blocks = markdown.trim().split(/\n\s*\n/g).map((block) => block.trim()).filter(Boolean);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holo-js/mail",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Holo-JS Framework - mail contracts, preview helpers, and fluent send helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,12 +28,12 @@
28
28
  "test": "vitest --run"
29
29
  },
30
30
  "dependencies": {
31
- "@holo-js/config": "^0.1.5",
31
+ "@holo-js/config": "^0.1.7",
32
32
  "nodemailer": "^6.10.1"
33
33
  },
34
34
  "peerDependencies": {
35
- "@holo-js/queue": "^0.1.5",
36
- "@holo-js/storage": "^0.1.5"
35
+ "@holo-js/queue": "^0.1.7",
36
+ "@holo-js/storage": "^0.1.7"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@holo-js/queue": {