@sandsoftwaresolutions/mail-kit 0.1.0 → 0.1.1

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.
package/README.md CHANGED
@@ -40,7 +40,7 @@ Node 20 or newer. Works with `import` and `require()`, with TypeScript types for
40
40
 
41
41
  ## What you get
42
42
 
43
- **Templates that work.** `email()` makes table-based HTML with inline styles (Outlook, Gmail and Apple Mail), a phone layout, a dark-mode variant, a hidden preheader, and a **hand-written plain-text version from the same blocks** (not stripped HTML). Blocks: `heading`, paragraph (a plain string), `button`, `code` (verification codes), `list`, `table` (invoice rows), `note`, `divider`, `image`. Text blocks understand `**bold**`, `` `code` `` and `[label](https://url)`. Everything is escaped; unsafe links and invalid colors are refused, not passed through.
43
+ **Templates that work.** `email()` makes table-based HTML with inline styles (Outlook, Gmail and Apple Mail), a phone layout, a dark-mode variant, a hidden preheader, and a **hand-written plain-text version from the same blocks** (not stripped HTML). Blocks: `heading`, paragraph (a plain string), `button`, `code` (verification codes), `list`, `table` (invoice rows), `note`, `divider`, `image`. Text blocks understand `**bold**`, `` `code` `` and `[label](https://url)`. **Text you did not write yourself** (a customer's name) goes through `escapeMarkup(name)` first, otherwise a name like `[click](https://evil)` would become a real link in your mail. Everything is HTML-escaped either way; unsafe links and invalid colors are refused, not passed through.
44
44
 
45
45
  **Transports.**
46
46
 
package/dist/index.cjs CHANGED
@@ -3,7 +3,7 @@ const { MailError } = require("./errors.cjs");
3
3
  const { encodeWords, formatAddress, isEmail, normalizeAddress, parseAddress, parseAddressList } = require("./address.cjs");
4
4
  const { prepareMessage, quotedPrintable } = require("./message.cjs");
5
5
  const { escapeHtml, extractLinks, htmlToText, wrapText } = require("./text.cjs");
6
- const { block, email } = require("./template.cjs");
6
+ const { block, email, escapeMarkup } = require("./template.cjs");
7
7
  const { smtpTransport } = require("./smtp.cjs");
8
8
  const { failoverTransport, memoryTransport, resendTransport } = require("./transports.cjs");
9
9
  const { createMailer } = require("./mailer.cjs");
@@ -25,6 +25,7 @@ exports.htmlToText = htmlToText;
25
25
  exports.wrapText = wrapText;
26
26
  exports.block = block;
27
27
  exports.email = email;
28
+ exports.escapeMarkup = escapeMarkup;
28
29
  exports.smtpTransport = smtpTransport;
29
30
  exports.failoverTransport = failoverTransport;
30
31
  exports.memoryTransport = memoryTransport;
package/dist/index.d.cts CHANGED
@@ -119,6 +119,8 @@ export interface EmailOptions {
119
119
  }
120
120
  export interface Mail { subject: string; html: string; text: string }
121
121
  export function email(options: EmailOptions): Mail;
122
+ /** Escape a name or any outside text before putting it in a block, so it cannot turn into a link, bold text or code. */
123
+ export function escapeMarkup(value: unknown): string;
122
124
  export function preview(mail: Mail): string;
123
125
  export function lintEmail(mail: MessageInput | PreparedMessage): { level: "warning" | "info"; message: string }[];
124
126
 
package/dist/template.cjs CHANGED
@@ -15,7 +15,11 @@ const color = (value, fallback, name) => {
15
15
  };
16
16
 
17
17
  // Inline markup used in every text block: **bold**, `code`, [label](https://url). Everything else is plain text.
18
- const INLINE = /\[([^\]\n]+)\]\(([^)\s]+)\)|\*\*([^*\n]+)\*\*|`([^`\n]+)`/g;
18
+ // A backslash makes the next markup character literal; escapeMarkup() does that for text you did not write yourself.
19
+ const INLINE = /\\([\\*`[\]()])|\[([^\]\n]+)\]\(([^)\s]+)\)|\*\*([^*\n]+)\*\*|`([^`\n]+)`/g;
20
+
21
+ /** Makes a name, address or any other outside text safe to put inside a block: it can no longer become a link or bold text. */
22
+ const escapeMarkup = (value) => String(value).replace(/([\\*`[\]()])/g, "\\$1");
19
23
 
20
24
  function inlineHtml(text, theme) {
21
25
  let out = "";
@@ -23,16 +27,18 @@ function inlineHtml(text, theme) {
23
27
  for (const m of String(text).matchAll(INLINE)) {
24
28
  out += escapeHtml(text.slice(last, m.index));
25
29
  last = m.index + m[0].length;
26
- if (m[1] !== undefined) {
27
- out += LINKABLE.test(m[2]) ? `<a href="${escapeHtml(m[2])}" style="color:${theme.accent};text-decoration:underline;">${escapeHtml(m[1])}</a>` : escapeHtml(m[1]);
28
- } else if (m[3] !== undefined) out += `<strong>${escapeHtml(m[3])}</strong>`;
29
- else out += `<code class="ss-soft" style="font-family:Menlo,Consolas,monospace;font-size:0.95em;background:${theme.soft};padding:1px 5px;border-radius:4px;">${escapeHtml(m[4])}</code>`;
30
+ if (m[1] !== undefined) out += escapeHtml(m[1]);
31
+ else if (m[2] !== undefined) {
32
+ out += LINKABLE.test(m[3]) ? `<a href="${escapeHtml(m[3])}" style="color:${theme.accent};text-decoration:underline;">${escapeHtml(m[2])}</a>` : escapeHtml(m[2]);
33
+ } else if (m[4] !== undefined) out += `<strong>${escapeHtml(m[4])}</strong>`;
34
+ else out += `<code class="ss-soft" style="font-family:Menlo,Consolas,monospace;font-size:0.95em;background:${theme.soft};padding:1px 5px;border-radius:4px;">${escapeHtml(m[5])}</code>`;
30
35
  }
31
36
  return (out + escapeHtml(text.slice(last))).replace(/\n/g, "<br>");
32
37
  }
33
38
 
34
39
  function inlineText(text) {
35
- return String(text).replace(INLINE, (_, label, url, bold, code) => {
40
+ return String(text).replace(INLINE, (_, escaped, label, url, bold, code) => {
41
+ if (escaped !== undefined) return escaped;
36
42
  if (label !== undefined) return LINKABLE.test(url) && label !== url ? `${label} (${url})` : label;
37
43
  return bold ?? code;
38
44
  });
@@ -195,5 +201,6 @@ ${footerLines.length ? `<tr><td class="ss-pad" style="padding:0 36px 28px;border
195
201
  return { subject: subject.trim(), html, text: parts.join("\n\n") + "\n" };
196
202
  }
197
203
 
204
+ exports.escapeMarkup = escapeMarkup;
198
205
  exports.block = block;
199
206
  exports.email = email;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandsoftwaresolutions/mail-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Send good-looking email from Node with no dependencies: SMTP (Zoho, Gmail, ...) and Resend, templates that work in Outlook and dark mode, automatic text version, safe by default, retries without double-sending.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
package/src/index.d.ts CHANGED
@@ -119,6 +119,8 @@ export interface EmailOptions {
119
119
  }
120
120
  export interface Mail { subject: string; html: string; text: string }
121
121
  export function email(options: EmailOptions): Mail;
122
+ /** Escape a name or any outside text before putting it in a block, so it cannot turn into a link, bold text or code. */
123
+ export function escapeMarkup(value: unknown): string;
122
124
  export function preview(mail: Mail): string;
123
125
  export function lintEmail(mail: MessageInput | PreparedMessage): { level: "warning" | "info"; message: string }[];
124
126
 
package/src/index.js CHANGED
@@ -2,7 +2,7 @@ import { MailError } from "./errors.js";
2
2
  import { encodeWords, formatAddress, isEmail, normalizeAddress, parseAddress, parseAddressList } from "./address.js";
3
3
  import { prepareMessage, quotedPrintable } from "./message.js";
4
4
  import { escapeHtml, extractLinks, htmlToText, wrapText } from "./text.js";
5
- import { block, email } from "./template.js";
5
+ import { block, email, escapeMarkup } from "./template.js";
6
6
  import { smtpTransport } from "./smtp.js";
7
7
  import { failoverTransport, memoryTransport, resendTransport } from "./transports.js";
8
8
  import { createMailer } from "./mailer.js";
@@ -11,4 +11,4 @@ import { preview } from "./preview.js";
11
11
 
12
12
  export { MailError, encodeWords, formatAddress, isEmail, normalizeAddress, parseAddress, parseAddressList };
13
13
  export { prepareMessage, quotedPrintable, escapeHtml, extractLinks, htmlToText, wrapText };
14
- export { block, email, smtpTransport, failoverTransport, memoryTransport, resendTransport, createMailer, lintEmail, preview };
14
+ export { block, email, escapeMarkup, smtpTransport, failoverTransport, memoryTransport, resendTransport, createMailer, lintEmail, preview };
package/src/template.js CHANGED
@@ -14,7 +14,11 @@ const color = (value, fallback, name) => {
14
14
  };
15
15
 
16
16
  // Inline markup used in every text block: **bold**, `code`, [label](https://url). Everything else is plain text.
17
- const INLINE = /\[([^\]\n]+)\]\(([^)\s]+)\)|\*\*([^*\n]+)\*\*|`([^`\n]+)`/g;
17
+ // A backslash makes the next markup character literal; escapeMarkup() does that for text you did not write yourself.
18
+ const INLINE = /\\([\\*`[\]()])|\[([^\]\n]+)\]\(([^)\s]+)\)|\*\*([^*\n]+)\*\*|`([^`\n]+)`/g;
19
+
20
+ /** Makes a name, address or any other outside text safe to put inside a block: it can no longer become a link or bold text. */
21
+ export const escapeMarkup = (value) => String(value).replace(/([\\*`[\]()])/g, "\\$1");
18
22
 
19
23
  function inlineHtml(text, theme) {
20
24
  let out = "";
@@ -22,16 +26,18 @@ function inlineHtml(text, theme) {
22
26
  for (const m of String(text).matchAll(INLINE)) {
23
27
  out += escapeHtml(text.slice(last, m.index));
24
28
  last = m.index + m[0].length;
25
- if (m[1] !== undefined) {
26
- out += LINKABLE.test(m[2]) ? `<a href="${escapeHtml(m[2])}" style="color:${theme.accent};text-decoration:underline;">${escapeHtml(m[1])}</a>` : escapeHtml(m[1]);
27
- } else if (m[3] !== undefined) out += `<strong>${escapeHtml(m[3])}</strong>`;
28
- else out += `<code class="ss-soft" style="font-family:Menlo,Consolas,monospace;font-size:0.95em;background:${theme.soft};padding:1px 5px;border-radius:4px;">${escapeHtml(m[4])}</code>`;
29
+ if (m[1] !== undefined) out += escapeHtml(m[1]);
30
+ else if (m[2] !== undefined) {
31
+ out += LINKABLE.test(m[3]) ? `<a href="${escapeHtml(m[3])}" style="color:${theme.accent};text-decoration:underline;">${escapeHtml(m[2])}</a>` : escapeHtml(m[2]);
32
+ } else if (m[4] !== undefined) out += `<strong>${escapeHtml(m[4])}</strong>`;
33
+ else out += `<code class="ss-soft" style="font-family:Menlo,Consolas,monospace;font-size:0.95em;background:${theme.soft};padding:1px 5px;border-radius:4px;">${escapeHtml(m[5])}</code>`;
29
34
  }
30
35
  return (out + escapeHtml(text.slice(last))).replace(/\n/g, "<br>");
31
36
  }
32
37
 
33
38
  function inlineText(text) {
34
- return String(text).replace(INLINE, (_, label, url, bold, code) => {
39
+ return String(text).replace(INLINE, (_, escaped, label, url, bold, code) => {
40
+ if (escaped !== undefined) return escaped;
35
41
  if (label !== undefined) return LINKABLE.test(url) && label !== url ? `${label} (${url})` : label;
36
42
  return bold ?? code;
37
43
  });