mailery 0.11.0 → 0.12.0
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/dist/index.cjs +92 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +92 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4709,6 +4709,40 @@ ${input.plainText}`);
|
|
|
4709
4709
|
});
|
|
4710
4710
|
}
|
|
4711
4711
|
}
|
|
4712
|
+
const offDomain = findOffDomainLinkHosts(input.html, input.fromEmail);
|
|
4713
|
+
if (offDomain.majority && offDomain.hosts.length > 0) {
|
|
4714
|
+
issues.push({
|
|
4715
|
+
rule: "offdomain_links",
|
|
4716
|
+
severity: "warning",
|
|
4717
|
+
message: `Most links point away from the From domain: ${offDomain.hosts.join(", ")}.`,
|
|
4718
|
+
hint: "Link domains that match the sending domain build reputation. Route links through your own domain (or a tracking subdomain of it) where you can."
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
const insecure = findInsecureLinkHosts(input.html);
|
|
4722
|
+
if (insecure.length > 0) {
|
|
4723
|
+
issues.push({
|
|
4724
|
+
rule: "insecure_link",
|
|
4725
|
+
severity: "warning",
|
|
4726
|
+
message: `Link uses plain http://: ${insecure.join(", ")}.`,
|
|
4727
|
+
hint: "Mixed-content links get rewritten or warned about by some clients, and http:// correlates with stale spam templates. Use https://."
|
|
4728
|
+
});
|
|
4729
|
+
}
|
|
4730
|
+
if (countImagesMissingAlt(input.html) > 0) {
|
|
4731
|
+
issues.push({
|
|
4732
|
+
rule: "image_missing_alt",
|
|
4733
|
+
severity: "warning",
|
|
4734
|
+
message: "One or more images have no alt text.",
|
|
4735
|
+
hint: "Most clients block images by default on first open. Alt text is what the recipient actually sees, and screen readers need it."
|
|
4736
|
+
});
|
|
4737
|
+
}
|
|
4738
|
+
if (hasImageOnlyLink(input.html)) {
|
|
4739
|
+
issues.push({
|
|
4740
|
+
rule: "image_only_link",
|
|
4741
|
+
severity: "warning",
|
|
4742
|
+
message: "A link wraps an image with no accompanying text.",
|
|
4743
|
+
hint: "With images blocked, an image-only call-to-action is invisible and unclickable. Add a text label inside the link."
|
|
4744
|
+
});
|
|
4745
|
+
}
|
|
4712
4746
|
const linkCount = countMatches(input.html, /<a\s[^>]*\bhref\s*=/gi);
|
|
4713
4747
|
if (linkCount > 10) {
|
|
4714
4748
|
issues.push({
|
|
@@ -4763,6 +4797,63 @@ function hasBareUrlInVisibleText(html) {
|
|
|
4763
4797
|
const stripped = html.replace(/<a\b[^>]*>.*?<\/a>/gis, " ").replace(/<style\b[^>]*>.*?<\/style>/gis, " ").replace(/<script\b[^>]*>.*?<\/script>/gis, " ").replace(/<head\b[^>]*>.*?<\/head>/gis, " ");
|
|
4764
4798
|
return /https?:\/\/[^\s<>"']+/i.test(stripped);
|
|
4765
4799
|
}
|
|
4800
|
+
function findOffDomainLinkHosts(html, fromEmail) {
|
|
4801
|
+
const fromDomain = fromEmail.split("@")[1]?.toLowerCase();
|
|
4802
|
+
if (!fromDomain) return { hosts: [], majority: false };
|
|
4803
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
4804
|
+
let total = 0;
|
|
4805
|
+
let off = 0;
|
|
4806
|
+
for (const href of extractHrefs(html)) {
|
|
4807
|
+
if (href.includes("{{")) continue;
|
|
4808
|
+
const host = hostnameOf(href);
|
|
4809
|
+
if (!host) continue;
|
|
4810
|
+
total++;
|
|
4811
|
+
if (sameSite(host, fromDomain)) continue;
|
|
4812
|
+
off++;
|
|
4813
|
+
hosts.add(host);
|
|
4814
|
+
}
|
|
4815
|
+
return { hosts: Array.from(hosts), majority: total > 0 && off * 2 > total };
|
|
4816
|
+
}
|
|
4817
|
+
function sameSite(a, b) {
|
|
4818
|
+
if (a === b) return true;
|
|
4819
|
+
if (a.endsWith(`.${b}`) || b.endsWith(`.${a}`)) return true;
|
|
4820
|
+
return lastLabels(a) === lastLabels(b);
|
|
4821
|
+
}
|
|
4822
|
+
function lastLabels(host) {
|
|
4823
|
+
return host.split(".").slice(-2).join(".");
|
|
4824
|
+
}
|
|
4825
|
+
function findInsecureLinkHosts(html) {
|
|
4826
|
+
const hits = /* @__PURE__ */ new Set();
|
|
4827
|
+
for (const href of extractHrefs(html)) {
|
|
4828
|
+
if (!/^http:\/\//i.test(href)) continue;
|
|
4829
|
+
const host = hostnameOf(href);
|
|
4830
|
+
if (host) hits.add(host);
|
|
4831
|
+
}
|
|
4832
|
+
return Array.from(hits);
|
|
4833
|
+
}
|
|
4834
|
+
function countImagesMissingAlt(html) {
|
|
4835
|
+
let missing = 0;
|
|
4836
|
+
const re = /<img\b[^>]*>/gi;
|
|
4837
|
+
let m;
|
|
4838
|
+
while (m = re.exec(html)) {
|
|
4839
|
+
const tag = m[0];
|
|
4840
|
+
const alt = /\balt\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(tag);
|
|
4841
|
+
const value = alt ? alt[1] ?? alt[2] ?? alt[3] ?? "" : "";
|
|
4842
|
+
if (value.trim().length === 0) missing++;
|
|
4843
|
+
}
|
|
4844
|
+
return missing;
|
|
4845
|
+
}
|
|
4846
|
+
function hasImageOnlyLink(html) {
|
|
4847
|
+
const re = /<a\b[^>]*>(.*?)<\/a>/gis;
|
|
4848
|
+
let m;
|
|
4849
|
+
while (m = re.exec(html)) {
|
|
4850
|
+
const inner = m[1];
|
|
4851
|
+
if (!/<img\b/i.test(inner)) continue;
|
|
4852
|
+
const text = inner.replace(/<[^>]*>/g, "").replace(/ /gi, " ").trim();
|
|
4853
|
+
if (text.length === 0) return true;
|
|
4854
|
+
}
|
|
4855
|
+
return false;
|
|
4856
|
+
}
|
|
4766
4857
|
function findSpamSignals(text) {
|
|
4767
4858
|
const out = /* @__PURE__ */ new Set();
|
|
4768
4859
|
for (const { pattern, phrase } of SPAM_PHRASES) {
|
|
@@ -7626,7 +7717,7 @@ var DEDUPE_POLICIES = [
|
|
|
7626
7717
|
];
|
|
7627
7718
|
|
|
7628
7719
|
// src/server/index.ts
|
|
7629
|
-
var VERSION = "0.
|
|
7720
|
+
var VERSION = "0.12.0" ;
|
|
7630
7721
|
|
|
7631
7722
|
exports.DEDUPE_POLICIES = DEDUPE_POLICIES;
|
|
7632
7723
|
exports.FLOW_STEP_KINDS = FLOW_STEP_KINDS;
|