Move from xml2js to xmlbuilder2
This commit is contained in:
parent
e8a99001ed
commit
e212793337
|
@ -685,6 +685,74 @@
|
||||||
"chalk": "^3.0.0"
|
"chalk": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@oozcitak/dom": {
|
||||||
|
"version": "1.15.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.5.tgz",
|
||||||
|
"integrity": "sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A==",
|
||||||
|
"requires": {
|
||||||
|
"@oozcitak/infra": "1.0.5",
|
||||||
|
"@oozcitak/url": "1.0.0",
|
||||||
|
"@oozcitak/util": "8.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/util": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@oozcitak/infra": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-o+zZH7M6l5e3FaAWy3ojaPIVN5eusaYPrKm6MZQt0DKNdgXa2wDYExjpP0t/zx+GoQgQKzLu7cfD8rHCLt8JrQ==",
|
||||||
|
"requires": {
|
||||||
|
"@oozcitak/util": "8.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/util": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@oozcitak/url": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-LGrMeSxeLzsdaitxq3ZmBRVOrlRRQIgNNci6L0VRnOKlJFuRIkNm4B+BObXPCJA6JT5bEJtrrwjn30jueHJYZQ==",
|
||||||
|
"requires": {
|
||||||
|
"@oozcitak/infra": "1.0.3",
|
||||||
|
"@oozcitak/util": "1.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/infra": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag==",
|
||||||
|
"requires": {
|
||||||
|
"@oozcitak/util": "1.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/util": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-dFwFqcKrQnJ2SapOmRD1nQWEZUtbtIy9Y6TyJquzsalWNJsKIPxmTI0KG6Ypyl8j7v89L2wixH9fQDNrF78hKg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@oozcitak/util": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-4n8B1cWlJleSOSba5gxsMcN4tO8KkkcvXhNWW+ADqvq9Xj+Lrl9uCa90GRpjekqQJyt84aUX015DG81LFpZYXA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@oozcitak/util": {
|
||||||
|
"version": "8.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.3.tgz",
|
||||||
|
"integrity": "sha512-Ufpab7G5PfnEhQyy5kDg9C8ltWJjsVT1P/IYqacjstaqydG4Q21HAT2HUZQYBrC/a1ZLKCz87pfydlDvv8y97w=="
|
||||||
|
},
|
||||||
"@opencensus/core": {
|
"@opencensus/core": {
|
||||||
"version": "0.0.9",
|
"version": "0.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.9.tgz",
|
||||||
|
@ -1170,15 +1238,6 @@
|
||||||
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==",
|
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/xml2js": {
|
|
||||||
"version": "0.4.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.5.tgz",
|
|
||||||
"integrity": "sha512-yohU3zMn0fkhlape1nxXG2bLEGZRc1FeqF80RoHaYXJN7uibaauXfhzhOJr1Xh36sn+/tx21QAOf07b/xYVk1w==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/yargs": {
|
"@types/yargs": {
|
||||||
"version": "15.0.4",
|
"version": "15.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz",
|
||||||
|
@ -8199,20 +8258,16 @@
|
||||||
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
|
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"xml2js": {
|
"xmlbuilder2": {
|
||||||
"version": "0.4.23",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-2.1.0.tgz",
|
||||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
"integrity": "sha512-hWCfRTKFhhLv1QlSRn+PMwu0knJUQ5PSQanHFoY9Zy1q8cMEm0/37PHbeiBwlplgNafDzDJRdwQkMYVdXt77Nw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"sax": ">=0.6.0",
|
"@oozcitak/dom": "1.15.5",
|
||||||
"xmlbuilder": "~11.0.0"
|
"@oozcitak/infra": "1.0.5",
|
||||||
|
"@oozcitak/util": "8.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"xmlbuilder": {
|
|
||||||
"version": "11.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
|
||||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
|
||||||
},
|
|
||||||
"xmlchars": {
|
"xmlchars": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"react": "^16.13.0",
|
"react": "^16.13.0",
|
||||||
"react-dom": "^16.13.0",
|
"react-dom": "^16.13.0",
|
||||||
"smtp-server": "^3.6.0",
|
"smtp-server": "^3.6.0",
|
||||||
"xml2js": "^0.4.23"
|
"xmlbuilder2": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.3",
|
"@types/express": "^4.17.3",
|
||||||
|
@ -24,7 +24,6 @@
|
||||||
"@types/react": "^16.9.29",
|
"@types/react": "^16.9.29",
|
||||||
"@types/react-dom": "^16.9.5",
|
"@types/react-dom": "^16.9.5",
|
||||||
"@types/smtp-server": "^3.5.4",
|
"@types/smtp-server": "^3.5.4",
|
||||||
"@types/xml2js": "^0.4.5",
|
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"concurrently": "^5.1.0",
|
"concurrently": "^5.1.0",
|
||||||
"jest": "^25.2.4",
|
"jest": "^25.2.4",
|
||||||
|
|
34
src/check.ts
34
src/check.ts
|
@ -1,21 +1,19 @@
|
||||||
import xml2js from "xml2js";
|
import * as xmlbuilder2 from "xmlbuilder2";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
(async () => {
|
for (const feed of fs
|
||||||
for (const feed of fs
|
.readdirSync("static/feeds")
|
||||||
.readdirSync("static/feeds")
|
.filter(file => !file.startsWith("."))) {
|
||||||
.filter(file => !file.startsWith("."))) {
|
try {
|
||||||
try {
|
const xml: any = xmlbuilder2.convert(
|
||||||
const xml = await new xml2js.Parser().parseStringPromise(
|
fs.readFileSync(`static/feeds/${feed}`, "utf8"),
|
||||||
fs.readFileSync(`static/feeds/${feed}`, "utf8")
|
{ format: "object", wellFormed: true }
|
||||||
);
|
);
|
||||||
if (xml?.feed?.updated === undefined)
|
if (xml?.feed?.updated === undefined)
|
||||||
throw new Error("Can’t find xml.feed.updated");
|
throw new Error("Can’t find xml.feed.updated");
|
||||||
new xml2js.Builder().buildObject(xml);
|
console.log(`OK ${feed}`);
|
||||||
console.log(`OK ${feed}`);
|
} catch (error) {
|
||||||
} catch (error) {
|
console.log(`ERROR ${feed}: ${error}`);
|
||||||
console.log(`ERROR ${feed}: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
console.log("FINISHED");
|
}
|
||||||
})();
|
console.log("FINISHED");
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { SMTPServer } from "smtp-server";
|
||||||
import mailparser from "mailparser";
|
import mailparser from "mailparser";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOMServer from "react-dom/server";
|
import ReactDOMServer from "react-dom/server";
|
||||||
import xml2js from "xml2js";
|
import * as xmlbuilder2 from "xmlbuilder2";
|
||||||
import { promises as fs } from "fs";
|
import { promises as fs } from "fs";
|
||||||
import cryptoRandomString from "crypto-random-string";
|
import cryptoRandomString from "crypto-random-string";
|
||||||
|
|
||||||
|
@ -82,9 +82,10 @@ export const emailServer = new SMTPServer({
|
||||||
const path = feedPath(identifier);
|
const path = feedPath(identifier);
|
||||||
const xmlText = await fs.readFile(path, "utf8").catch(() => null);
|
const xmlText = await fs.readFile(path, "utf8").catch(() => null);
|
||||||
if (xmlText === null) continue;
|
if (xmlText === null) continue;
|
||||||
const xml = await new xml2js.Parser().parseStringPromise(xmlText);
|
const xml = parseXML(xmlText);
|
||||||
xml.feed.updated = now();
|
xml.feed.updated = now();
|
||||||
if (xml.feed.entry === undefined) xml.feed.entry = [];
|
if (xml.feed.entry === undefined) xml.feed.entry = [];
|
||||||
|
if (!Array.isArray(xml.feed.entry)) xml.feed.entry = [xml.feed.entry];
|
||||||
xml.feed.entry.unshift(entry);
|
xml.feed.entry.unshift(entry);
|
||||||
while (xml.feed.entry.length > 0 && renderXML(xml).length > 500_000)
|
while (xml.feed.entry.length > 0 && renderXML(xml).length > 500_000)
|
||||||
xml.feed.entry.pop();
|
xml.feed.entry.pop();
|
||||||
|
@ -94,7 +95,7 @@ export const emailServer = new SMTPServer({
|
||||||
})().catch(error => {
|
})().catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
stream.resume();
|
stream.resume();
|
||||||
callback(error);
|
callback(new Error("Failed to receive message. Please try again."));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).listen(process.env.EMAIL_PORT ?? 2525);
|
}).listen(process.env.EMAIL_PORT ?? 2525);
|
||||||
|
@ -207,21 +208,17 @@ function Created({ identifier }: { identifier: string }) {
|
||||||
function Feed({ name, identifier }: { name: string; identifier: string }) {
|
function Feed({ name, identifier }: { name: string; identifier: string }) {
|
||||||
return {
|
return {
|
||||||
feed: {
|
feed: {
|
||||||
$: { xmlns: "http://www.w3.org/2005/Atom" },
|
"@xmlns": "http://www.w3.org/2005/Atom",
|
||||||
link: [
|
link: [
|
||||||
{
|
{
|
||||||
$: {
|
"@rel": "self",
|
||||||
rel: "self",
|
"@type": "application/atom+xml",
|
||||||
type: "application/atom+xml",
|
"@href": feedURL(identifier)
|
||||||
href: feedURL(identifier)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$: {
|
"@rel": "alternate",
|
||||||
rel: "alternate",
|
"@type": "text/html",
|
||||||
type: "text/html",
|
"@href": "https://www.kill-the-newsletter.com/"
|
||||||
href: "https://www.kill-the-newsletter.com/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
id: urn(identifier),
|
id: urn(identifier),
|
||||||
|
@ -258,13 +255,11 @@ function Entry({
|
||||||
author: { name: author },
|
author: { name: author },
|
||||||
updated: now(),
|
updated: now(),
|
||||||
link: {
|
link: {
|
||||||
$: {
|
"@rel": "alternate",
|
||||||
rel: "alternate",
|
"@type": "text/html",
|
||||||
type: "text/html",
|
"@href": "https://www.kill-the-newsletter.com/entry"
|
||||||
href: "https://www.kill-the-newsletter.com/entry"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
content: { $: { type: "html" }, _: content }
|
content: { "@type": "html", "#": content }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -301,5 +296,18 @@ function renderHTML(component: React.ReactElement): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderXML(xml: object): string {
|
function renderXML(xml: object): string {
|
||||||
return new xml2js.Builder().buildObject(xml);
|
return xmlbuilder2.convert({ invalidCharReplacement: "" }, xml, {
|
||||||
|
format: "xml",
|
||||||
|
wellFormed: true,
|
||||||
|
noDoubleEncoding: true,
|
||||||
|
prettyPrint: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseXML(xml: string): any {
|
||||||
|
return xmlbuilder2.convert({ invalidCharReplacement: "" }, xml, {
|
||||||
|
format: "object",
|
||||||
|
wellFormed: true,
|
||||||
|
noDoubleEncoding: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
41
src/test.ts
41
src/test.ts
|
@ -11,6 +11,21 @@ test("create feed", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("receive email", () => {
|
describe("receive email", () => {
|
||||||
|
test("‘updated’ field is updated", async () => {
|
||||||
|
const identifier = await createFeed();
|
||||||
|
const before = await getFeed(identifier);
|
||||||
|
await emailClient.sendMail({
|
||||||
|
from: "publisher@example.com",
|
||||||
|
to: `${identifier}@kill-the-newsletter.com`,
|
||||||
|
subject: "New Message",
|
||||||
|
html: "<p>HTML content</p>"
|
||||||
|
});
|
||||||
|
const after = await getFeed(identifier);
|
||||||
|
expect(after.match(/<updated>(.*)<\/updated>/)![1]).not.toMatch(
|
||||||
|
before.match(/<updated>(.*)<\/updated>/)![1]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("HTML content", async () => {
|
test("HTML content", async () => {
|
||||||
const identifier = await createFeed();
|
const identifier = await createFeed();
|
||||||
await emailClient.sendMail({
|
await emailClient.sendMail({
|
||||||
|
@ -50,6 +65,32 @@ describe("receive email", () => {
|
||||||
expect(feed).toMatch(`href="https://www.kill-the-newsletter.com"`);
|
expect(feed).toMatch(`href="https://www.kill-the-newsletter.com"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("invalid XML character in HTML", async () => {
|
||||||
|
const identifier = await createFeed();
|
||||||
|
await emailClient.sendMail({
|
||||||
|
from: "publisher@example.com",
|
||||||
|
to: `${identifier}@kill-the-newsletter.com`,
|
||||||
|
subject: "New Message",
|
||||||
|
html: "<p>Invalid XML character (backspace): ‘\b’</p>"
|
||||||
|
});
|
||||||
|
const feed = await getFeed(identifier);
|
||||||
|
expect(feed).toMatch("Invalid XML character (backspace): ‘’");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid XML character in text", async () => {
|
||||||
|
const identifier = await createFeed();
|
||||||
|
await emailClient.sendMail({
|
||||||
|
from: "publisher@example.com",
|
||||||
|
to: `${identifier}@kill-the-newsletter.com`,
|
||||||
|
subject: "New Message",
|
||||||
|
text: "Invalid XML character (backspace): ‘\b’"
|
||||||
|
});
|
||||||
|
const feed = await getFeed(identifier);
|
||||||
|
expect(feed).toMatch(
|
||||||
|
"Invalid XML character (backspace): ‘’"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("missing content", async () => {
|
test("missing content", async () => {
|
||||||
const identifier = await createFeed();
|
const identifier = await createFeed();
|
||||||
await emailClient.sendMail({
|
await emailClient.sendMail({
|
||||||
|
|
Loading…
Reference in New Issue