@live-change/email-service 0.9.33 → 0.9.34
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/browser.js +55 -0
- package/config.js +53 -0
- package/package.json +7 -4
- package/render-experiment.js +134 -0
- package/render-experiment2.js +95 -0
- package/render-experiment3.js +213 -0
- package/render.js +295 -16
- package/send-experiment.js +22 -0
- package/send.js +2 -16
package/browser.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import App from '@live-change/framework'
|
|
2
|
+
const app = App.app()
|
|
3
|
+
|
|
4
|
+
import definition from './definition.js'
|
|
5
|
+
import config from './config.js'
|
|
6
|
+
|
|
7
|
+
import { chromium } from 'playwright'
|
|
8
|
+
|
|
9
|
+
import PQueue from 'p-queue'
|
|
10
|
+
import got from "got"
|
|
11
|
+
|
|
12
|
+
import dns from "dns/promises"
|
|
13
|
+
|
|
14
|
+
const browserQueue = new PQueue({ concurrency: config.browser.concurrency })
|
|
15
|
+
|
|
16
|
+
async function newBrowser() {
|
|
17
|
+
if(config.browser.webSocketDebuggerUrl) {
|
|
18
|
+
const browser = await chromium.connect({ wsEndpoint: config.browser.webSocketDebuggerUrl })
|
|
19
|
+
return browser
|
|
20
|
+
} else if(config.browser.url) {
|
|
21
|
+
const browserInfo = await got.post(config.browser.url + '/json/version').json()
|
|
22
|
+
const browser = await chromium.connect({ wsEndpoint: browserInfo.webSocketDebuggerUrl })
|
|
23
|
+
return browser
|
|
24
|
+
} if(config.browser.host) {
|
|
25
|
+
const ip = await dns.resolve4(config.browser.host)
|
|
26
|
+
const browserInfoUrl = `http://${ip}:${config.browser.port}/json/version`
|
|
27
|
+
console.log("Browser info url", browserInfoUrl)
|
|
28
|
+
const browserInfo = await got.post(browserInfoUrl).json()
|
|
29
|
+
console.log("Browser info", browserInfo)
|
|
30
|
+
try {
|
|
31
|
+
const browser = await chromium.connectOverCDP(browserInfo.webSocketDebuggerUrl)
|
|
32
|
+
return browser
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error("Failed to connect to browser", error)
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
const browser = await chromium.launch({
|
|
39
|
+
headless: true
|
|
40
|
+
})
|
|
41
|
+
return browser
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function runWithBrowser(func) {
|
|
46
|
+
return await browserQueue.add(async () => {
|
|
47
|
+
const browser = await newBrowser()
|
|
48
|
+
try {
|
|
49
|
+
const result = await func(browser)
|
|
50
|
+
return result
|
|
51
|
+
} finally {
|
|
52
|
+
await browser.close()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
package/config.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import definition from './definition.js'
|
|
2
|
+
import crypto from 'crypto'
|
|
3
|
+
|
|
4
|
+
// Helper function to get value with fallback
|
|
5
|
+
const getValue = (configValue, envValue, defaultValue) =>
|
|
6
|
+
configValue ?? envValue ?? defaultValue
|
|
7
|
+
|
|
8
|
+
// Helper function to convert value to number
|
|
9
|
+
const getNumberValue = (configValue, envValue, defaultValue) =>
|
|
10
|
+
+(getValue(configValue, envValue, defaultValue))
|
|
11
|
+
|
|
12
|
+
// Browser configuration
|
|
13
|
+
const browser = {
|
|
14
|
+
url: getValue(definition.config.browser?.url, process.env.BROWSER_URL),
|
|
15
|
+
webSocketDebuggerUrl: getValue(
|
|
16
|
+
definition.config.browser?.webSocketDebuggerUrl,
|
|
17
|
+
process.env.BROWSER_WEBSOCKET_DEBUGGER_URL
|
|
18
|
+
),
|
|
19
|
+
host: getValue(definition.config.browser?.host, process.env.BROWSER_HOST),
|
|
20
|
+
port: getNumberValue(definition.config.browser?.port, process.env.BROWSER_PORT, 9222),
|
|
21
|
+
concurrency: getNumberValue(definition.config.browser?.concurrency, process.env.BROWSER_CONCURRENCY, 1),
|
|
22
|
+
renderAuthenticationKey: getValue(
|
|
23
|
+
definition.config.browser?.renderAuthenticationKey,
|
|
24
|
+
null,
|
|
25
|
+
crypto.randomBytes(24).toString('hex')
|
|
26
|
+
),
|
|
27
|
+
ssrUrl: getValue(
|
|
28
|
+
definition.config.browser?.ssrUrl,
|
|
29
|
+
process.env.SSR_URL,
|
|
30
|
+
'http://localhost:8001'
|
|
31
|
+
),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// SMTP configuration
|
|
35
|
+
const smtp = {
|
|
36
|
+
host: getValue(definition.config.smtp?.host, process.env.SMTP_HOST),
|
|
37
|
+
port: getNumberValue(definition.config.smtp?.port, process.env.SMTP_PORT, 587),
|
|
38
|
+
auth: {
|
|
39
|
+
user: getValue(definition.config.smtp?.user, process.env.SMTP_USER),
|
|
40
|
+
pass: getValue(definition.config.smtp?.password, process.env.SMTP_PASSWORD),
|
|
41
|
+
},
|
|
42
|
+
// deduce secure from port, include all secure ports
|
|
43
|
+
secure: getValue(definition.config.smtp?.secure, [465, 587, 2525].includes(definition.config.smtp?.port)),
|
|
44
|
+
ignoreTLS: getValue(definition.config.smtp?.ignoreTLS, undefined),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const renderMethod = getValue(definition.config.renderMethod, process.env.RENDER_METHOD, 'juice')
|
|
48
|
+
|
|
49
|
+
definition.clientConfig = {}
|
|
50
|
+
|
|
51
|
+
const config = { browser, smtp, renderMethod }
|
|
52
|
+
|
|
53
|
+
export default config
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/email-service",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.34",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -21,14 +21,17 @@
|
|
|
21
21
|
"url": "https://www.viamage.com/"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@live-change/framework": "^0.9.
|
|
24
|
+
"@live-change/framework": "^0.9.34",
|
|
25
25
|
"got": "^11.8.6",
|
|
26
26
|
"html-to-text": "8.1.0",
|
|
27
27
|
"inline-css": "4.0.2",
|
|
28
28
|
"jsdom": "^24.1.0",
|
|
29
29
|
"juice": "11.0.0",
|
|
30
|
-
"nodemailer": "^6.7.2"
|
|
30
|
+
"nodemailer": "^6.7.2",
|
|
31
|
+
"postcss": "8.5.3",
|
|
32
|
+
"postcss-calc": "10.1.1",
|
|
33
|
+
"postcss-custom-properties": "14.0.4"
|
|
31
34
|
},
|
|
32
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "e5f22de77fe6a3c0a0e1b91d17593e195aaae3b3",
|
|
33
36
|
"type": "module"
|
|
34
37
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const url = process.argv[2]
|
|
2
|
+
|
|
3
|
+
import got from 'got'
|
|
4
|
+
import juice from 'juice'
|
|
5
|
+
|
|
6
|
+
import fs from 'fs'
|
|
7
|
+
|
|
8
|
+
import { JSDOM } from 'jsdom'
|
|
9
|
+
|
|
10
|
+
import postcss from 'postcss';
|
|
11
|
+
import postcssCustomProperties from 'postcss-custom-properties';
|
|
12
|
+
import postcssCalc from 'postcss-calc';
|
|
13
|
+
|
|
14
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|
15
|
+
|
|
16
|
+
console.log('url', url)
|
|
17
|
+
const response = await got(url)
|
|
18
|
+
let body = response.body
|
|
19
|
+
|
|
20
|
+
fs.writeFileSync('body.html', body)
|
|
21
|
+
|
|
22
|
+
const juiceOptions = {
|
|
23
|
+
webResources: {
|
|
24
|
+
scripts: false,
|
|
25
|
+
links: true,
|
|
26
|
+
images: false,
|
|
27
|
+
resolveCSSVariables: true,
|
|
28
|
+
relativeTo: url
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const processCSS = async (css) => {
|
|
33
|
+
const result = await postcss([
|
|
34
|
+
postcssCustomProperties(),
|
|
35
|
+
postcssCalc()
|
|
36
|
+
]).process(css, { from: undefined });
|
|
37
|
+
|
|
38
|
+
return result.css;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const fetchAndProcessCSS = async (html, baseUrl) => {
|
|
42
|
+
const dom = new JSDOM(html);
|
|
43
|
+
const document = dom.window.document;
|
|
44
|
+
let collectedCSS = "";
|
|
45
|
+
|
|
46
|
+
// Pobieramy CSS z <style> w dokumencie
|
|
47
|
+
const styleTags = document.querySelectorAll('style');
|
|
48
|
+
for (const styleTag of styleTags) {
|
|
49
|
+
collectedCSS += styleTag.textContent + "\n";
|
|
50
|
+
styleTag.remove(); // Usuwamy, bo później wstawimy jeden poprawiony <style>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Pobieramy CSS z <link rel="stylesheet" href="...">
|
|
54
|
+
const linkTags = document.querySelectorAll('link[rel="stylesheet"]');
|
|
55
|
+
for (const linkTag of linkTags) {
|
|
56
|
+
const href = linkTag.href;
|
|
57
|
+
if (href) {
|
|
58
|
+
try {
|
|
59
|
+
const absoluteUrl = new URL(href, baseUrl).href;
|
|
60
|
+
console.log(`Fetching CSS from: ${absoluteUrl}`);
|
|
61
|
+
const response = await got(absoluteUrl);
|
|
62
|
+
collectedCSS += response.body + "\n";
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn(`Failed to fetch CSS: ${href}`, error.message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
linkTag.remove(); // Usuwamy, bo CSS już inline'ujemy
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fs.writeFileSync('collectedCSS.css', collectedCSS)
|
|
71
|
+
// Przetwarzamy zebrany CSS
|
|
72
|
+
const processedCSS = await processCSS(collectedCSS);
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync('processedCSS.css', processedCSS)
|
|
75
|
+
// Dodajemy nowy przetworzony CSS do dokumentu
|
|
76
|
+
const newStyleTag = document.createElement('style');
|
|
77
|
+
newStyleTag.textContent = processedCSS;
|
|
78
|
+
document.head.appendChild(newStyleTag);
|
|
79
|
+
|
|
80
|
+
return dom.serialize();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
body = await fetchAndProcessCSS(body, url)
|
|
84
|
+
|
|
85
|
+
fs.writeFileSync('body-css.html', body)
|
|
86
|
+
|
|
87
|
+
/*body = await juice(body, juiceOptions)*/
|
|
88
|
+
body = await new Promise((resolve, reject) => {
|
|
89
|
+
juice.juiceResources(body, juiceOptions, (err, html) => {
|
|
90
|
+
if(err) reject(err)
|
|
91
|
+
else resolve(html)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const dom = new JSDOM(body)
|
|
96
|
+
fs.writeFileSync('body-juced.html', body)
|
|
97
|
+
|
|
98
|
+
const htmlElement = dom.window.document.querySelector('[data-html]')
|
|
99
|
+
|
|
100
|
+
// traverse all elements and clear all data attributes
|
|
101
|
+
const cleanDataAttributes = (element) => {
|
|
102
|
+
// Remove all data attributes
|
|
103
|
+
for(const attr of element.attributes) {
|
|
104
|
+
if(attr.name.startsWith('data-')) {
|
|
105
|
+
element.removeAttribute(attr.name)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for(const child of element.children) {
|
|
109
|
+
cleanDataAttributes(child)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
cleanDataAttributes(htmlElement)
|
|
113
|
+
|
|
114
|
+
fs.writeFileSync('body-cleaned.html', htmlElement.outerHTML)
|
|
115
|
+
//console.log(htmlElement.outerHTML)
|
|
116
|
+
|
|
117
|
+
// const juiceOptions = {
|
|
118
|
+
// webResources: {
|
|
119
|
+
// scripts: false,
|
|
120
|
+
// links: true,
|
|
121
|
+
// images: false,
|
|
122
|
+
// resolveCSSVariables: true,
|
|
123
|
+
// relativeTo: url
|
|
124
|
+
// }
|
|
125
|
+
// }
|
|
126
|
+
|
|
127
|
+
// const html = await new Promise((resolve, reject) => {
|
|
128
|
+
// juice.juiceResources(response.body, juiceOptions, (err, html) => {
|
|
129
|
+
// if(err) reject(err)
|
|
130
|
+
// else resolve(html)
|
|
131
|
+
// })
|
|
132
|
+
// })
|
|
133
|
+
|
|
134
|
+
// console.log(html)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
|
|
4
|
+
const url = process.argv[2]
|
|
5
|
+
|
|
6
|
+
const browser = await chromium.launch();
|
|
7
|
+
const page = await browser.newPage();
|
|
8
|
+
|
|
9
|
+
// Załaduj stronę
|
|
10
|
+
await page.goto(url); // Podmień na swoją stronę
|
|
11
|
+
|
|
12
|
+
const inlineHtml = await page.evaluate(() => {
|
|
13
|
+
function getFilteredComputedStyles(element) {
|
|
14
|
+
const computed = window.getComputedStyle(element);
|
|
15
|
+
const defaultElement = document.createElement(element.tagName);
|
|
16
|
+
document.body.appendChild(defaultElement);
|
|
17
|
+
const defaultStyles = window.getComputedStyle(defaultElement);
|
|
18
|
+
|
|
19
|
+
const filteredStyles = {};
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < computed.length; i++) {
|
|
22
|
+
const prop = computed[i];
|
|
23
|
+
const computedValue = computed.getPropertyValue(prop);
|
|
24
|
+
const defaultValue = defaultStyles.getPropertyValue(prop);
|
|
25
|
+
|
|
26
|
+
if (computedValue !== defaultValue) {
|
|
27
|
+
filteredStyles[prop] = computedValue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
document.body.removeChild(defaultElement);
|
|
32
|
+
return filteredStyles;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cssStyleDeclarationToString(styles) {
|
|
36
|
+
return Object.entries(styles)
|
|
37
|
+
.map(([prop, value]) => `${prop}: ${value};`)
|
|
38
|
+
.join(' ');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function inlineImportantStyles(root) {
|
|
42
|
+
const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
43
|
+
|
|
44
|
+
while (treeWalker.nextNode()) {
|
|
45
|
+
const element = treeWalker.currentNode;
|
|
46
|
+
const filteredStyles = getFilteredComputedStyles(element);
|
|
47
|
+
const inheritedStyles = getInheritedStyles(element);
|
|
48
|
+
|
|
49
|
+
// Usuwamy style, które są takie same jak u rodzica
|
|
50
|
+
for (const prop in inheritedStyles) {
|
|
51
|
+
if (filteredStyles[prop] === inheritedStyles[prop]) {
|
|
52
|
+
delete filteredStyles[prop];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const inlineStyle = cssStyleDeclarationToString(filteredStyles);
|
|
57
|
+
if (inlineStyle) {
|
|
58
|
+
element.setAttribute('style', inlineStyle);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Usuwanie klas
|
|
62
|
+
element.removeAttribute('class');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getInheritedStyles(element) {
|
|
67
|
+
const inheritedStyles = {};
|
|
68
|
+
let parent = element.parentElement;
|
|
69
|
+
|
|
70
|
+
while (parent) {
|
|
71
|
+
const parentStyles = window.getComputedStyle(parent);
|
|
72
|
+
for (let i = 0; i < parentStyles.length; i++) {
|
|
73
|
+
const prop = parentStyles[i];
|
|
74
|
+
if (!inheritedStyles[prop]) {
|
|
75
|
+
inheritedStyles[prop] = parentStyles.getPropertyValue(prop);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
parent = parent.parentElement;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return inheritedStyles;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
const clonedDoc = document.documentElement.cloneNode(true);
|
|
86
|
+
inlineImportantStyles(clonedDoc);
|
|
87
|
+
return `<!DOCTYPE html>\n${clonedDoc.outerHTML}`;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Zapisz do pliku
|
|
91
|
+
await fs.writeFile('output.html', inlineHtml);
|
|
92
|
+
|
|
93
|
+
console.log('Zapisano HTML z inline CSS jako output.html');
|
|
94
|
+
|
|
95
|
+
await browser.close();
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { chromium } from 'playwright'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
|
|
4
|
+
const url = process.argv[2]
|
|
5
|
+
|
|
6
|
+
const browser = await chromium.launch()
|
|
7
|
+
const page = await browser.newPage()
|
|
8
|
+
|
|
9
|
+
// Załaduj stronę
|
|
10
|
+
await page.goto(url) // Podmień na swoją stronę
|
|
11
|
+
|
|
12
|
+
const inlineHtml = await page.evaluate(() => {
|
|
13
|
+
const ALLOWED_CSS_PROPERTIES = new Set([
|
|
14
|
+
'background',
|
|
15
|
+
'background-blend-mode',
|
|
16
|
+
'background-clip',
|
|
17
|
+
'background-color',
|
|
18
|
+
'background-image',
|
|
19
|
+
'background-origin',
|
|
20
|
+
'background-position',
|
|
21
|
+
'background-repeat',
|
|
22
|
+
'background-size',
|
|
23
|
+
'border',
|
|
24
|
+
'border-bottom',
|
|
25
|
+
'border-bottom-color',
|
|
26
|
+
'border-bottom-left-radius',
|
|
27
|
+
'border-bottom-right-radius',
|
|
28
|
+
'border-bottom-style',
|
|
29
|
+
'border-bottom-width',
|
|
30
|
+
'border-collapse',
|
|
31
|
+
'border-color',
|
|
32
|
+
'border-left',
|
|
33
|
+
'border-left-color',
|
|
34
|
+
'border-left-style',
|
|
35
|
+
'border-left-width',
|
|
36
|
+
'border-radius',
|
|
37
|
+
'border-right',
|
|
38
|
+
'border-right-color',
|
|
39
|
+
'border-right-style',
|
|
40
|
+
'border-right-width',
|
|
41
|
+
'border-spacing',
|
|
42
|
+
'border-style',
|
|
43
|
+
'border-top',
|
|
44
|
+
'border-top-color',
|
|
45
|
+
'border-top-left-radius',
|
|
46
|
+
'border-top-right-radius',
|
|
47
|
+
'border-top-style',
|
|
48
|
+
'border-top-width',
|
|
49
|
+
'border-width',
|
|
50
|
+
'box-sizing',
|
|
51
|
+
'color',
|
|
52
|
+
'display',
|
|
53
|
+
'float',
|
|
54
|
+
'font',
|
|
55
|
+
'font-family',
|
|
56
|
+
'font-feature-settings',
|
|
57
|
+
'font-size',
|
|
58
|
+
'font-size-adjust',
|
|
59
|
+
'font-stretch',
|
|
60
|
+
'font-style',
|
|
61
|
+
'font-weight',
|
|
62
|
+
'height',
|
|
63
|
+
'letter-spacing',
|
|
64
|
+
'line-height',
|
|
65
|
+
'list-style',
|
|
66
|
+
'list-style-position',
|
|
67
|
+
'list-style-type',
|
|
68
|
+
'margin',
|
|
69
|
+
'margin-bottom',
|
|
70
|
+
'margin-left',
|
|
71
|
+
'margin-right',
|
|
72
|
+
'margin-top',
|
|
73
|
+
'max-height',
|
|
74
|
+
'max-width',
|
|
75
|
+
'min-height',
|
|
76
|
+
'min-width',
|
|
77
|
+
'opacity',
|
|
78
|
+
'outline',
|
|
79
|
+
'outline-color',
|
|
80
|
+
'outline-style',
|
|
81
|
+
'outline-width',
|
|
82
|
+
'overflow',
|
|
83
|
+
'overflow-x',
|
|
84
|
+
'overflow-y',
|
|
85
|
+
'padding',
|
|
86
|
+
'padding-bottom',
|
|
87
|
+
'padding-left',
|
|
88
|
+
'padding-right',
|
|
89
|
+
'padding-top',
|
|
90
|
+
'table-layout',
|
|
91
|
+
'text-align',
|
|
92
|
+
'text-decoration',
|
|
93
|
+
'text-decoration-color',
|
|
94
|
+
'text-decoration-line',
|
|
95
|
+
'text-decoration-style',
|
|
96
|
+
'text-indent',
|
|
97
|
+
'text-overflow',
|
|
98
|
+
'text-transform',
|
|
99
|
+
'vertical-align',
|
|
100
|
+
'white-space',
|
|
101
|
+
'width',
|
|
102
|
+
'word-break',
|
|
103
|
+
'word-spacing',
|
|
104
|
+
'word-wrap'
|
|
105
|
+
])
|
|
106
|
+
|
|
107
|
+
function getCSSDefinedStylesWithLayers(element) {
|
|
108
|
+
const computedStyles = window.getComputedStyle(element)
|
|
109
|
+
const styles = {}
|
|
110
|
+
const layerRules = []
|
|
111
|
+
|
|
112
|
+
for (const sheet of document.styleSheets) {
|
|
113
|
+
try {
|
|
114
|
+
for (const rule of sheet.cssRules) {
|
|
115
|
+
if (rule instanceof CSSLayerBlockRule) {
|
|
116
|
+
// Przechodzi przez zdefiniowane warstwy
|
|
117
|
+
for (const subRule of rule.cssRules) {
|
|
118
|
+
if (subRule instanceof CSSStyleRule && element.matches(subRule.selectorText)) {
|
|
119
|
+
layerRules.push(subRule)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else if (rule instanceof CSSStyleRule && element.matches(rule.selectorText)) {
|
|
123
|
+
layerRules.push(rule)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.warn('Nie można odczytać stylów z:', sheet.href)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Zastosowanie reguł w kolejności ich priorytetu
|
|
132
|
+
for (const rule of layerRules) {
|
|
133
|
+
for (const prop of rule.style) {
|
|
134
|
+
styles[prop] = computedStyles.getPropertyValue(prop)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return styles
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getFilteredComputedStyles(element) {
|
|
142
|
+
const definedStyles = getCSSDefinedStylesWithLayers(element)
|
|
143
|
+
const filteredStyles = {}
|
|
144
|
+
|
|
145
|
+
for (const prop in definedStyles) {
|
|
146
|
+
if (ALLOWED_CSS_PROPERTIES.has(prop)) {
|
|
147
|
+
filteredStyles[prop] = definedStyles[prop]
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return filteredStyles
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function cssStyleDeclarationToString(styles) {
|
|
155
|
+
return Object.entries(styles)
|
|
156
|
+
.map(([prop, value]) => `${prop}: ${value};`)
|
|
157
|
+
.join(' ')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function inlineImportantStyles(root) {
|
|
161
|
+
const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT)
|
|
162
|
+
|
|
163
|
+
while (treeWalker.nextNode()) {
|
|
164
|
+
const element = treeWalker.currentNode
|
|
165
|
+
const filteredStyles = getFilteredComputedStyles(element)
|
|
166
|
+
const inheritedStyles = getInheritedStyles(element)
|
|
167
|
+
|
|
168
|
+
// Usuwamy style, które są takie same jak u rodzica
|
|
169
|
+
for (const prop in inheritedStyles) {
|
|
170
|
+
if (filteredStyles[prop] === inheritedStyles[prop]) {
|
|
171
|
+
delete filteredStyles[prop]
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const inlineStyle = cssStyleDeclarationToString(filteredStyles)
|
|
176
|
+
if (inlineStyle) {
|
|
177
|
+
element.setAttribute('style', inlineStyle)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Usuwanie klas
|
|
181
|
+
element.removeAttribute('class')
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getInheritedStyles(element) {
|
|
186
|
+
const inheritedStyles = {}
|
|
187
|
+
let parent = element.parentElement
|
|
188
|
+
|
|
189
|
+
while (parent) {
|
|
190
|
+
const parentStyles = window.getComputedStyle(parent)
|
|
191
|
+
for (let i = 0; i < parentStyles.length; i++) {
|
|
192
|
+
const prop = parentStyles[i]
|
|
193
|
+
if (!inheritedStyles[prop]) {
|
|
194
|
+
inheritedStyles[prop] = parentStyles.getPropertyValue(prop)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
parent = parent.parentElement
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return inheritedStyles
|
|
201
|
+
}
|
|
202
|
+
const htmlElement = document.querySelector('[data-html]')
|
|
203
|
+
// Uruchomienie dla całego dokumentu
|
|
204
|
+
inlineImportantStyles(htmlElement)
|
|
205
|
+
return `<!DOCTYPE html>\n${htmlElement.outerHTML}`
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Zapisz do pliku
|
|
209
|
+
await fs.writeFile('output.html', inlineHtml)
|
|
210
|
+
|
|
211
|
+
console.log('Zapisano HTML z inline CSS jako output.html')
|
|
212
|
+
|
|
213
|
+
await browser.close()
|
package/render.js
CHANGED
|
@@ -11,6 +11,8 @@ import { ObservableValue } from '@live-change/dao'
|
|
|
11
11
|
import definition from './definition.js'
|
|
12
12
|
const config = definition.config
|
|
13
13
|
|
|
14
|
+
import { runWithBrowser } from './browser.js'
|
|
15
|
+
|
|
14
16
|
const publicDir = config.publicDir || 'front/public/'
|
|
15
17
|
|
|
16
18
|
const authenticationKey = new ObservableValue(
|
|
@@ -40,6 +42,7 @@ definition.authenticator({
|
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
})
|
|
45
|
+
|
|
43
46
|
function processElement(element, images) {
|
|
44
47
|
for(let i = 0; i < element.attributes.length; i++) {
|
|
45
48
|
const attribute = element.attributes[i]
|
|
@@ -73,14 +76,7 @@ function processElement(element, images) {
|
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
async function
|
|
77
|
-
const baseUrl = `http://${config.ssrHost||process.env.SSR_HOST||'127.0.0.1'}`+
|
|
78
|
-
`:${config.ssrPort||process.env.SSR_PORT||'8001'}`
|
|
79
|
-
|
|
80
|
-
const encodedData = encodeURIComponent(JSON.stringify(data))
|
|
81
|
-
const url = `${baseUrl}/_email/${data.action}/${data.contact}/${encodedData}`
|
|
82
|
-
+`?sessionKey=${await authenticationKey.getValue()}`
|
|
83
|
-
console.log("RENDER EMAIL", data, "URL", url)
|
|
79
|
+
async function renderEmailWithJuice(url) {
|
|
84
80
|
const response = await got(url)
|
|
85
81
|
let body = response.body
|
|
86
82
|
console.log("BASE URL", baseUrl)
|
|
@@ -131,14 +127,38 @@ async function renderEmail(data) {
|
|
|
131
127
|
}
|
|
132
128
|
}
|
|
133
129
|
console.log("IMAGES", Array.from(images.entries()))
|
|
134
|
-
const imageAttachments =
|
|
130
|
+
const imageAttachments = await processImageAttachments(images, url)
|
|
131
|
+
|
|
132
|
+
email.attachments = email.attachments || []
|
|
133
|
+
email.attachments.push(...imageAttachments)
|
|
134
|
+
|
|
135
|
+
console.log("EMAIL", email)
|
|
136
|
+
|
|
137
|
+
return email
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
import fs from 'fs/promises'
|
|
141
|
+
|
|
142
|
+
async function processImageAttachments(images, url) {
|
|
143
|
+
return Promise.all(Array.from(images.entries()).map(async ([imagePath, contentId]) => {
|
|
144
|
+
if (imagePath.startsWith('http')) {
|
|
145
|
+
console.log("EXTERNAL IMAGE", imagePath)
|
|
146
|
+
return {
|
|
147
|
+
path: imagePath,
|
|
148
|
+
cid: contentId,
|
|
149
|
+
filename
|
|
150
|
+
}
|
|
151
|
+
}
|
|
135
152
|
const imageUrl = new URL(imagePath, url)
|
|
136
153
|
const file = path.resolve(publicDir, imageUrl.pathname.slice(1))
|
|
137
154
|
const filename = path.basename(file)
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
console.log("LOCAL IMAGE", filename)
|
|
156
|
+
// check if file exists
|
|
157
|
+
if(fs.access(file).catch(() => false)) {
|
|
158
|
+
console.log("LOCAL IMAGE DOES NOT EXIST", file) // paths must be wrong, return external image
|
|
159
|
+
const imageUrl = new URL(imagePath, url)
|
|
140
160
|
return {
|
|
141
|
-
path:
|
|
161
|
+
path: imageUrl.toString(),
|
|
142
162
|
cid: contentId,
|
|
143
163
|
filename
|
|
144
164
|
}
|
|
@@ -148,14 +168,273 @@ async function renderEmail(data) {
|
|
|
148
168
|
path: file,
|
|
149
169
|
cid: contentId
|
|
150
170
|
}
|
|
171
|
+
}))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function renderEmailWithBrowser(url) {
|
|
175
|
+
|
|
176
|
+
return await runWithBrowser(async (browser) => {
|
|
177
|
+
const page = await browser.newPage()
|
|
178
|
+
console.log("RENDER EMAIL WITH BROWSER", url)
|
|
179
|
+
await page.goto(url)
|
|
180
|
+
|
|
181
|
+
const email = await page.evaluate(() => {
|
|
182
|
+
const ALLOWED_CSS_PROPERTIES = new Set([
|
|
183
|
+
'background',
|
|
184
|
+
'background-blend-mode',
|
|
185
|
+
'background-clip',
|
|
186
|
+
'background-color',
|
|
187
|
+
'background-image',
|
|
188
|
+
'background-origin',
|
|
189
|
+
'background-position',
|
|
190
|
+
'background-repeat',
|
|
191
|
+
'background-size',
|
|
192
|
+
'border',
|
|
193
|
+
'border-bottom',
|
|
194
|
+
'border-bottom-color',
|
|
195
|
+
'border-bottom-left-radius',
|
|
196
|
+
'border-bottom-right-radius',
|
|
197
|
+
'border-bottom-style',
|
|
198
|
+
'border-bottom-width',
|
|
199
|
+
'border-collapse',
|
|
200
|
+
'border-color',
|
|
201
|
+
'border-left',
|
|
202
|
+
'border-left-color',
|
|
203
|
+
'border-left-style',
|
|
204
|
+
'border-left-width',
|
|
205
|
+
'border-radius',
|
|
206
|
+
'border-right',
|
|
207
|
+
'border-right-color',
|
|
208
|
+
'border-right-style',
|
|
209
|
+
'border-right-width',
|
|
210
|
+
'border-spacing',
|
|
211
|
+
'border-style',
|
|
212
|
+
'border-top',
|
|
213
|
+
'border-top-color',
|
|
214
|
+
'border-top-left-radius',
|
|
215
|
+
'border-top-right-radius',
|
|
216
|
+
'border-top-style',
|
|
217
|
+
'border-top-width',
|
|
218
|
+
'border-width',
|
|
219
|
+
'box-sizing',
|
|
220
|
+
'color',
|
|
221
|
+
'display',
|
|
222
|
+
'float',
|
|
223
|
+
'font',
|
|
224
|
+
'font-family',
|
|
225
|
+
'font-feature-settings',
|
|
226
|
+
'font-size',
|
|
227
|
+
'font-size-adjust',
|
|
228
|
+
'font-stretch',
|
|
229
|
+
'font-style',
|
|
230
|
+
'font-weight',
|
|
231
|
+
'height',
|
|
232
|
+
'letter-spacing',
|
|
233
|
+
'line-height',
|
|
234
|
+
'list-style',
|
|
235
|
+
'list-style-position',
|
|
236
|
+
'list-style-type',
|
|
237
|
+
'margin',
|
|
238
|
+
'margin-bottom',
|
|
239
|
+
'margin-left',
|
|
240
|
+
'margin-right',
|
|
241
|
+
'margin-top',
|
|
242
|
+
'max-height',
|
|
243
|
+
'max-width',
|
|
244
|
+
'min-height',
|
|
245
|
+
'min-width',
|
|
246
|
+
'opacity',
|
|
247
|
+
'outline',
|
|
248
|
+
'outline-color',
|
|
249
|
+
'outline-style',
|
|
250
|
+
'outline-width',
|
|
251
|
+
'overflow',
|
|
252
|
+
'overflow-x',
|
|
253
|
+
'overflow-y',
|
|
254
|
+
'padding',
|
|
255
|
+
'padding-bottom',
|
|
256
|
+
'padding-left',
|
|
257
|
+
'padding-right',
|
|
258
|
+
'padding-top',
|
|
259
|
+
'table-layout',
|
|
260
|
+
'text-align',
|
|
261
|
+
'text-decoration',
|
|
262
|
+
'text-decoration-color',
|
|
263
|
+
'text-decoration-line',
|
|
264
|
+
'text-decoration-style',
|
|
265
|
+
'text-indent',
|
|
266
|
+
'text-overflow',
|
|
267
|
+
'text-transform',
|
|
268
|
+
'vertical-align',
|
|
269
|
+
'white-space',
|
|
270
|
+
'width',
|
|
271
|
+
'word-break',
|
|
272
|
+
'word-spacing',
|
|
273
|
+
'word-wrap'
|
|
274
|
+
])
|
|
275
|
+
|
|
276
|
+
function getCSSDefinedStylesWithLayers(element) {
|
|
277
|
+
const computedStyles = window.getComputedStyle(element)
|
|
278
|
+
const styles = {}
|
|
279
|
+
const layerRules = []
|
|
280
|
+
|
|
281
|
+
for (const sheet of document.styleSheets) {
|
|
282
|
+
try {
|
|
283
|
+
for (const rule of sheet.cssRules) {
|
|
284
|
+
if (rule instanceof CSSLayerBlockRule) {
|
|
285
|
+
// Przechodzi przez zdefiniowane warstwy
|
|
286
|
+
for (const subRule of rule.cssRules) {
|
|
287
|
+
if (subRule instanceof CSSStyleRule && element.matches(subRule.selectorText)) {
|
|
288
|
+
layerRules.push(subRule)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} else if (rule instanceof CSSStyleRule && element.matches(rule.selectorText)) {
|
|
292
|
+
layerRules.push(rule)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch (e) {
|
|
296
|
+
console.warn('Nie można odczytać stylów z:', sheet.href)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Zastosowanie reguł w kolejności ich priorytetu
|
|
301
|
+
for (const rule of layerRules) {
|
|
302
|
+
for (const prop of rule.style) {
|
|
303
|
+
styles[prop] = computedStyles.getPropertyValue(prop)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return styles
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getFilteredComputedStyles(element) {
|
|
311
|
+
const definedStyles = getCSSDefinedStylesWithLayers(element)
|
|
312
|
+
const filteredStyles = {}
|
|
313
|
+
|
|
314
|
+
for (const prop in definedStyles) {
|
|
315
|
+
if (ALLOWED_CSS_PROPERTIES.has(prop)) {
|
|
316
|
+
filteredStyles[prop] = definedStyles[prop]
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return filteredStyles
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function cssStyleDeclarationToString(styles) {
|
|
324
|
+
return Object.entries(styles)
|
|
325
|
+
.map(([prop, value]) => `${prop}: ${value};`)
|
|
326
|
+
.join(' ')
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function inlineImportantStyles(root) {
|
|
330
|
+
const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT)
|
|
331
|
+
|
|
332
|
+
while (treeWalker.nextNode()) {
|
|
333
|
+
const element = treeWalker.currentNode
|
|
334
|
+
const filteredStyles = getFilteredComputedStyles(element)
|
|
335
|
+
const inheritedStyles = getInheritedStyles(element)
|
|
336
|
+
|
|
337
|
+
// Usuwamy style, które są takie same jak u rodzica
|
|
338
|
+
for (const prop in inheritedStyles) {
|
|
339
|
+
if (filteredStyles[prop] === inheritedStyles[prop]) {
|
|
340
|
+
delete filteredStyles[prop]
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const inlineStyle = cssStyleDeclarationToString(filteredStyles)
|
|
345
|
+
if (inlineStyle) {
|
|
346
|
+
element.setAttribute('style', inlineStyle)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Usuwanie klas
|
|
350
|
+
element.removeAttribute('class')
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function getInheritedStyles(element) {
|
|
355
|
+
const inheritedStyles = {}
|
|
356
|
+
let parent = element.parentElement
|
|
357
|
+
|
|
358
|
+
while (parent) {
|
|
359
|
+
const parentStyles = window.getComputedStyle(parent)
|
|
360
|
+
for (let i = 0; i < parentStyles.length; i++) {
|
|
361
|
+
const prop = parentStyles[i]
|
|
362
|
+
if (!inheritedStyles[prop]) {
|
|
363
|
+
inheritedStyles[prop] = parentStyles.getPropertyValue(prop)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
parent = parent.parentElement
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return inheritedStyles
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Get headers
|
|
373
|
+
const headersJson = document.querySelector('[data-headers]').textContent
|
|
374
|
+
const headers = JSON.parse(headersJson)
|
|
375
|
+
|
|
376
|
+
// Process HTML and text versions
|
|
377
|
+
const messageElements = document.querySelectorAll("[data-html],[data-text]")
|
|
378
|
+
const email = { ...headers }
|
|
379
|
+
|
|
380
|
+
for(let messageElement of messageElements) {
|
|
381
|
+
const toHtml = messageElement.getAttribute('data-html')
|
|
382
|
+
const toText = messageElement.getAttribute('data-text')
|
|
383
|
+
|
|
384
|
+
if(toHtml !== null) {
|
|
385
|
+
inlineImportantStyles(messageElement)
|
|
386
|
+
email.html = messageElement.outerHTML
|
|
387
|
+
}
|
|
388
|
+
if(toText !== null) {
|
|
389
|
+
email.text = messageElement.innerText
|
|
390
|
+
if(messageElement.tagName === 'PRE') {
|
|
391
|
+
const indentation = email.text.match(/^ */)[0]
|
|
392
|
+
const indentationRegex = new RegExp('\n' + indentation, 'g')
|
|
393
|
+
email.text = email.text.slice(indentation.length).replace(indentationRegex, '\n')
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return email
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
// process element to remove unwanted attributes and extract images using jsdom
|
|
402
|
+
const dom = new JSDOM(email.html)
|
|
403
|
+
const images = new Map()
|
|
404
|
+
const messageElements = dom.window.document.querySelectorAll("[data-html]")
|
|
405
|
+
for(let messageElement of messageElements) {
|
|
406
|
+
processElement(messageElement, images)
|
|
407
|
+
}
|
|
408
|
+
email.html = dom.serialize()
|
|
409
|
+
dom.window.close()
|
|
410
|
+
|
|
411
|
+
console.log("IMAGES", Array.from(images.entries()))
|
|
412
|
+
const imageAttachments = await processImageAttachments(images, url)
|
|
413
|
+
|
|
414
|
+
email.attachments = email.attachments || []
|
|
415
|
+
email.attachments.push(...imageAttachments)
|
|
416
|
+
|
|
417
|
+
console.log("EMAIL", email)
|
|
418
|
+
|
|
419
|
+
return email
|
|
151
420
|
})
|
|
421
|
+
}
|
|
152
422
|
|
|
153
|
-
|
|
154
|
-
email.attachments.push(...imageAttachments)
|
|
423
|
+
export { renderEmailWithJuice, renderEmailWithBrowser }
|
|
155
424
|
|
|
156
|
-
|
|
425
|
+
async function renderEmail(data) {
|
|
426
|
+
const baseUrl = `http://${config.ssrHost||process.env.SSR_HOST||'127.0.0.1'}`+
|
|
427
|
+
`:${config.ssrPort||process.env.SSR_PORT||'8001'}`
|
|
157
428
|
|
|
158
|
-
|
|
429
|
+
const encodedData = encodeURIComponent(JSON.stringify(data))
|
|
430
|
+
const url = `${baseUrl}/_email/${data.action}/${data.contact}/${encodedData}`
|
|
431
|
+
+`?sessionKey=${await authenticationKey.getValue()}`
|
|
432
|
+
console.log("RENDER EMAIL", data, "URL", url)
|
|
433
|
+
if(config.renderMethod === 'juice') {
|
|
434
|
+
return renderEmailWithJuice(url)
|
|
435
|
+
} else if(config.renderMethod === 'browser') {
|
|
436
|
+
return renderEmailWithBrowser(url)
|
|
437
|
+
}
|
|
159
438
|
}
|
|
160
439
|
|
|
161
440
|
export default renderEmail
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
|
|
3
|
+
import "dotenv/config"
|
|
4
|
+
import nodemailer from 'nodemailer'
|
|
5
|
+
|
|
6
|
+
import { renderEmailWithJuice, renderEmailWithBrowser } from './render.js'
|
|
7
|
+
import config from './config.js'
|
|
8
|
+
|
|
9
|
+
const url = process.argv[2]
|
|
10
|
+
|
|
11
|
+
const email = await renderEmailWithBrowser(url)
|
|
12
|
+
|
|
13
|
+
//await fs.writeFile('email.html', email.html); process.exit(0)
|
|
14
|
+
console.log("SMTP CONFIG", config.smtp)
|
|
15
|
+
|
|
16
|
+
/// send email
|
|
17
|
+
const smtp = nodemailer.createTransport(config.smtp)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const result = await smtp.sendMail(email)
|
|
21
|
+
|
|
22
|
+
console.log(result)
|
package/send.js
CHANGED
|
@@ -6,23 +6,9 @@ const app = App.app()
|
|
|
6
6
|
|
|
7
7
|
import renderEmail from './render.js'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import config from './config.js'
|
|
10
10
|
|
|
11
|
-
const smtp = nodemailer.createTransport(config.
|
|
12
|
-
host: config.host || process.env.SMTP_HOST,
|
|
13
|
-
port: +(config.port || process.env.SMTP_PORT),
|
|
14
|
-
auth: {
|
|
15
|
-
user: (config.user || process.env.SMTP_USER),
|
|
16
|
-
pass: (config.password || process.env.SMTP_PASSWORD)
|
|
17
|
-
},
|
|
18
|
-
secure: (config.secure || process.env.SMTP_SECURE) !== undefined
|
|
19
|
-
? !!((config.secure || process.env.SMTP_SECURE))
|
|
20
|
-
: undefined, // secure:true for port 465, secure:false for port 587
|
|
21
|
-
tls: {
|
|
22
|
-
// do not fail on invalid certs
|
|
23
|
-
rejectUnauthorized: !(config.ignoreTLS || process.env.SMTP_IGNORE_TLS)
|
|
24
|
-
}
|
|
25
|
-
})
|
|
11
|
+
const smtp = nodemailer.createTransport(config.smtp)
|
|
26
12
|
|
|
27
13
|
const SentEmail = definition.model({
|
|
28
14
|
name: "SentEmail",
|