@stacksjs/email 0.74.67 → 0.74.69

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.
@@ -33,6 +33,20 @@ export declare function safe(html: string): SafeHtml;
33
33
  export declare function escapeHtml(input: string): string;
34
34
  /** For tests, and for a dev server that regenerated the registry. */
35
35
  export declare function resetTemplateRegistry(): void;
36
+ /**
37
+ * The plain-text part of an HTML email.
38
+ *
39
+ * A text part is what a client shows when it will not render HTML, and what
40
+ * spam filters compare against the HTML. It used to strip every tag and keep
41
+ * the rest, which lost the one thing a transactional mail is for: a link's
42
+ * URL. The framework's own password-reset text part had no reset link, only
43
+ * the word on the button. It also kept the text of `<title>` and `<style>`
44
+ * and every line's HTML indentation.
45
+ *
46
+ * Links now read `label (url)`, head/style/script contents are dropped, and
47
+ * lines are trimmed.
48
+ */
49
+ export declare function htmlToText(html: string): string;
36
50
  /**
37
51
  * Render an email template with optional layout
38
52
  *
package/dist/template.js CHANGED
@@ -1,5 +1,7 @@
1
- var {require}=import.meta;import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{fs}from"@stacksjs/storage";import{defaultsResourcesPath,resourcesPath,storagePath}from"@stacksjs/path";import{join,resolve as resolvePath}from"node:path";import{inlineCss,shouldInlineByDefault}from"./css-inliner";export class SafeHtml{value;__safeHtml=!0;constructor(value){this.value=value}}export function safe(html){return new SafeHtml(html)}function getDefaultVariables(){const primaryColor=config.app.primaryColor||"#3b82f6";return{appName:config.app.name||"Stacks",appUrl:config.app.url||"https://localhost",primaryColor,primaryColorDark:darkenColor(primaryColor,15),year:new Date().getFullYear()}}function darkenColor(hex,percent){const num=Number.parseInt(hex.replace("#",""),16),amt=Math.round(2.55*percent),R=Math.max(0,(num>>16)-amt),G=Math.max(0,(num>>8&255)-amt),B=Math.max(0,(num&255)-amt);return`#${(16777216+R*65536+G*256+B).toString(16).slice(1)}`}export function escapeHtml(input){return input.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function replaceVariables(html,variables){let result=html;for(const[key,value]of Object.entries(variables)){const escapedKey=key.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),regex=new RegExp(`\\{\\{\\s*${escapedKey}\\s*\\}\\}`,"g");result=result.replace(regex,renderTemplateValue(value))}return result}function renderTemplateValue(value){if(value===null||value===void 0)return"";if(value instanceof SafeHtml)return value.value;return escapeHtml(String(value))}const templateRoots=[(relativePath)=>resourcesPath(join("emails",relativePath)),(relativePath)=>defaultsResourcesPath(join("emails",relativePath))];let templateRegistry=null,templateRegistryLoaded=!1,templateRegistryCwd=null;function loadTemplateRegistry(){const cwd=process.cwd();if(templateRegistryLoaded&&templateRegistryCwd===cwd)return templateRegistry;templateRegistry=null;templateRegistryLoaded=!0;templateRegistryCwd=cwd;try{const dir=storagePath("framework/auto-imports"),module=require(`${dir}/emails.ts`);if(!module.emails)return null;templateRegistry=Object.fromEntries(Object.entries(module.emails).map(([name,file])=>[name,{path:resolvePath(dir,file),type:file.endsWith(".stx")?"stx":"html"}]))}catch{templateRegistry=null}return templateRegistry}export function resetTemplateRegistry(){templateRegistry=null;templateRegistryLoaded=!1;templateRegistryCwd=null}function resolveTemplatePath(templateName){const registry=loadTemplateRegistry();if(registry){const bare=templateName.replace(/\.(?:stx|html)$/,""),hit=registry[bare];if(hit){if(templateName.endsWith(".stx")&&hit.type!=="stx");else if(templateName.endsWith(".html")&&hit.type!=="html");else if(fs.existsSync(hit.path))return hit}}for(const root of templateRoots){if(templateName.endsWith(".stx")){const fullPath=root(templateName);if(fs.existsSync(fullPath))return{path:fullPath,type:"stx"};continue}if(templateName.endsWith(".html")){const fullPath=root(templateName);if(fs.existsSync(fullPath))return{path:fullPath,type:"html"};continue}const stxPath=root(`${templateName}.stx`);if(fs.existsSync(stxPath))return{path:stxPath,type:"stx"};const htmlPath=root(`${templateName}.html`);if(fs.existsSync(htmlPath))return{path:htmlPath,type:"html"}}return null}function loadHtmlTemplate(templatePath){const path=templatePath.endsWith(".html")?templatePath:`${templatePath}.html`;for(const root of templateRoots){const fullPath=root(path);if(fs.existsSync(fullPath))return fs.readFileSync(fullPath,"utf-8")}return null}function loadLayout(layoutName){return loadHtmlTemplate(`layouts/${layoutName}`)}function htmlToText(html){return html.replace(/<br\s*\/?>/gi,`
2
- `).replace(/<\/(p|div|h[1-6]|li|tr)>/gi,`
3
- `).replace(/<\/td>/gi,"\t").replace(/<[^>]*>/g,"").replace(/&nbsp;/g," ").replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/&#39;/g,"'").replace(/&copy;/g,"(c)").replace(/\n\s*\n\s*\n/g,`
1
+ var {require}=import.meta;import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{fs}from"@stacksjs/storage";import{defaultsResourcesPath,resourcesPath,storagePath}from"@stacksjs/path";import{join,resolve as resolvePath}from"node:path";import{inlineCss,shouldInlineByDefault}from"./css-inliner";export class SafeHtml{value;__safeHtml=!0;constructor(value){this.value=value}}export function safe(html){return new SafeHtml(html)}function getDefaultVariables(){const primaryColor=config.app.primaryColor||"#3b82f6";return{appName:config.app.name||"Stacks",appUrl:config.app.url||"https://localhost",primaryColor,primaryColorDark:darkenColor(primaryColor,15),year:new Date().getFullYear()}}function darkenColor(hex,percent){const num=Number.parseInt(hex.replace("#",""),16),amt=Math.round(2.55*percent),R=Math.max(0,(num>>16)-amt),G=Math.max(0,(num>>8&255)-amt),B=Math.max(0,(num&255)-amt);return`#${(16777216+R*65536+G*256+B).toString(16).slice(1)}`}export function escapeHtml(input){return input.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function replaceVariables(html,variables){let result=html;for(const[key,value]of Object.entries(variables)){const escapedKey=key.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),regex=new RegExp(`\\{\\{\\s*${escapedKey}\\s*\\}\\}`,"g");result=result.replace(regex,renderTemplateValue(value))}return result}function renderTemplateValue(value){if(value===null||value===void 0)return"";if(value instanceof SafeHtml)return value.value;return escapeHtml(String(value))}const templateRoots=[(relativePath)=>resourcesPath(join("emails",relativePath)),(relativePath)=>defaultsResourcesPath(join("emails",relativePath))];let templateRegistry=null,templateRegistryLoaded=!1,templateRegistryCwd=null;function loadTemplateRegistry(){const cwd=process.cwd();if(templateRegistryLoaded&&templateRegistryCwd===cwd)return templateRegistry;templateRegistry=null;templateRegistryLoaded=!0;templateRegistryCwd=cwd;try{const dir=storagePath("framework/auto-imports"),module=require(`${dir}/emails.ts`);if(!module.emails)return null;templateRegistry=Object.fromEntries(Object.entries(module.emails).map(([name,file])=>[name,{path:resolvePath(dir,file),type:file.endsWith(".stx")?"stx":"html"}]))}catch{templateRegistry=null}return templateRegistry}export function resetTemplateRegistry(){templateRegistry=null;templateRegistryLoaded=!1;templateRegistryCwd=null}function resolveTemplatePath(templateName){const registry=loadTemplateRegistry();if(registry){const bare=templateName.replace(/\.(?:stx|html)$/,""),hit=registry[bare];if(hit){if(templateName.endsWith(".stx")&&hit.type!=="stx");else if(templateName.endsWith(".html")&&hit.type!=="html");else if(fs.existsSync(hit.path))return hit}}for(const root of templateRoots){if(templateName.endsWith(".stx")){const fullPath=root(templateName);if(fs.existsSync(fullPath))return{path:fullPath,type:"stx"};continue}if(templateName.endsWith(".html")){const fullPath=root(templateName);if(fs.existsSync(fullPath))return{path:fullPath,type:"html"};continue}const stxPath=root(`${templateName}.stx`);if(fs.existsSync(stxPath))return{path:stxPath,type:"stx"};const htmlPath=root(`${templateName}.html`);if(fs.existsSync(htmlPath))return{path:htmlPath,type:"html"}}return null}function loadHtmlTemplate(templatePath){const path=templatePath.endsWith(".html")?templatePath:`${templatePath}.html`;for(const root of templateRoots){const fullPath=root(path);if(fs.existsSync(fullPath))return fs.readFileSync(fullPath,"utf-8")}return null}function loadLayout(layoutName){return loadHtmlTemplate(`layouts/${layoutName}`)}export function htmlToText(html){return((value)=>value.replace(/&nbsp;/g," ").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/&#39;|&apos;/g,"'").replace(/&copy;/g,"(c)").replace(/&#(\d+);/g,(_,code)=>String.fromCodePoint(Number(code))).replace(/&#x([\da-f]+);/gi,(_,code)=>String.fromCodePoint(Number.parseInt(code,16))).replace(/&amp;/g,"&"))(html.replace(/<(head|style|script|title)\b[^>]*>[\s\S]*?<\/\1>/gi,"").replace(/<!--[\s\S]*?-->/g,"").replace(/<a\b[^>]*?\bhref\s*=\s*(["'])(.*?)\1[^>]*>([\s\S]*?)<\/a>/gi,(_,_quote,href,inner)=>{const label=inner.replace(/<[^>]*>/g,"").replace(/\s+/g," ").trim(),url=href.trim();if(!url||url==="#"||url.startsWith("javascript:"))return label;if(`mailto:${label}`===url)return label;if(!label||label===url)return url;return`${label} (${url})`}).replace(/<br\s*\/?>/gi,`
2
+ `).replace(/<\/(p|div|h[1-6]|li|tr|table)>/gi,`
3
+ `).replace(/<\/td>/gi,"\t").replace(/<[^>]*>/g,"")).split(`
4
+ `).map((line)=>line.replace(/[ \t]+/g," ").trim()).join(`
5
+ `).replace(/\n{3,}/g,`
4
6
 
5
7
  `).trim()}export async function template(templateName,options={}){return templateByName(templateName,options)}export async function templateByName(templateName,options={}){const{variables={},layout="base",subject="",inline=shouldInlineByDefault()}=options,allVariables={...getDefaultVariables(),subject,...variables},resolved=resolveTemplatePath(templateName);if(!resolved){console.warn(`[Email Template] Template "${templateName}" not found`);return{html:"",text:""}}if(resolved.type==="stx")try{const{renderEmail}=await import("@stacksjs/stx"),result=await renderEmail(resolved.path,allVariables,{componentsDir:defaultsResourcesPath("components/Email")});return{...result,html:inlineCss(result.html,{inline})}}catch(error){log.warn(`[email] STX template rendering failed for ${templateName}: ${error instanceof Error?error.message:String(error)}`);return{html:"",text:""}}let content=fs.readFileSync(resolved.path,"utf-8");content=replaceVariables(content,allVariables);let html;if(layout!==!1){const layoutHtml=loadLayout(layout);if(!layoutHtml){console.warn(`[Email Template] Layout "${layout}" not found, using content only`);html=content}else{allVariables.content=safe(content);html=replaceVariables(layoutHtml,allVariables)}}else html=content;html=inlineCss(html,{inline});const text=htmlToText(html);return{html,text}}export function renderHtml(htmlContent,variables={}){const allVariables={...getDefaultVariables(),...variables},html=replaceVariables(htmlContent,allVariables),text=htmlToText(html);return{html,text}}export function templateExists(templateName){return resolveTemplatePath(templateName)!==null}export function listTemplates(){const emailsPath=resourcesPath("emails");if(!fs.existsSync(emailsPath))return[];const templates=[];function scanDir(dir,prefix=""){const entries=fs.readdirSync(dir,{withFileTypes:!0});for(const entry of entries)if(entry.isDirectory()&&entry.name!=="layouts")scanDir(join(dir,entry.name),`${prefix}${entry.name}/`);else if(entry.isFile()&&(entry.name.endsWith(".html")||entry.name.endsWith(".stx"))){const name=entry.name.replace(/\.(html|stx)$/,"");if(!templates.includes(`${prefix}${name}`))templates.push(`${prefix}${name}`)}}scanDir(emailsPath);return templates}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/email",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.67",
5
+ "version": "0.74.69",
6
6
  "description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -61,26 +61,26 @@
61
61
  "prepublishOnly": "bun run build"
62
62
  },
63
63
  "dependencies": {
64
- "@stacksjs/config": "0.74.67",
65
- "@stacksjs/database": "0.74.67",
66
- "@stacksjs/events": "0.74.67",
67
- "@stacksjs/logging": "0.74.67",
64
+ "@stacksjs/config": "0.74.69",
65
+ "@stacksjs/database": "0.74.69",
66
+ "@stacksjs/events": "0.74.69",
67
+ "@stacksjs/logging": "0.74.69",
68
68
  "@stacksjs/mail": "^0.3.6",
69
- "@stacksjs/path": "0.74.67",
70
- "@stacksjs/storage": "0.74.67",
71
- "@stacksjs/strings": "0.74.67",
69
+ "@stacksjs/path": "0.74.69",
70
+ "@stacksjs/storage": "0.74.69",
71
+ "@stacksjs/strings": "0.74.69",
72
72
  "@stacksjs/stx": "^0.2.307",
73
73
  "@stacksjs/ts-cloud": "^0.16.0",
74
- "@stacksjs/utils": "0.74.67"
74
+ "@stacksjs/utils": "0.74.69"
75
75
  },
76
76
  "devDependencies": {
77
- "@stacksjs/cli": "0.74.67",
78
- "@stacksjs/error-handling": "0.74.67",
79
- "@stacksjs/types": "0.74.67",
77
+ "@stacksjs/cli": "0.74.69",
78
+ "@stacksjs/error-handling": "0.74.69",
79
+ "@stacksjs/types": "0.74.69",
80
80
  "better-dx": "^0.2.24"
81
81
  },
82
82
  "peerDependencies": {
83
- "@stacksjs/queue": "0.74.67"
83
+ "@stacksjs/queue": "0.74.69"
84
84
  },
85
85
  "peerDependenciesMeta": {
86
86
  "@stacksjs/queue": {