@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 +1 -1
- package/dist/index.cjs +2 -1
- package/dist/index.d.cts +2 -0
- package/dist/template.cjs +13 -6
- package/package.json +1 -1
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -2
- package/src/template.js +12 -6
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
else out += `<
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
else out += `<
|
|
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
|
});
|