mailery 0.10.2 → 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/admin/spa/{index-BTww-4AA.js → index-l7m1PyMb.js} +2 -2
- package/dist/admin/spa/{index-BTww-4AA.js.map → index-l7m1PyMb.js.map} +1 -1
- package/dist/admin/spa/index.html +1 -1
- package/dist/admin/spa/{template-editor-CrOKLpSw.js → template-editor-pGcCFnJ7.js} +33 -33
- package/dist/admin/spa/{template-editor-CrOKLpSw.js.map → template-editor-pGcCFnJ7.js.map} +1 -1
- package/dist/index.cjs +114 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +114 -8
- package/dist/index.js.map +1 -1
- package/dist/{null-CU_EApC6.d.cts → null-CfzvqNEp.d.cts} +17 -1
- package/dist/{null-CU_EApC6.d.ts → null-CfzvqNEp.d.ts} +17 -1
- package/dist/testing.cjs +11 -4
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +3 -2
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +11 -4
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as MailTesterFeedback, d as Mailer, T as TemplateDoc, e as SuppressionScope, D as DeliveryWindow, F as FlowStep, f as SegmentFilter, P as Predicate } from './null-
|
|
2
|
-
export { g as AuditLogDoc, B as BroadcastDoc, h as BroadcastStatus, i as CircuitBreakerThresholds, j as Collections, k as ContactTagDoc, E as EventDoc, l as FlowDoc, m as FlowGoal, n as FlowRunDoc, o as FlowRunStatus, p as FlowVersionDoc, H as HealthDoc, q as HealthStatus, L as LeadDoc, r as MailerConfig, s as NullProvider, O as OutboxDoc, t as RESERVED_VAR_KEYS, u as RedisOptions, v as SegmentDefinition, w as SendDoc, x as SendStatus, y as SenderDomainConfig, z as SenderDomainRegistry, G as SenderDomainValidation, I as SubscriptionDoc, J as SubscriptionStatus, K as SuppressionDoc, Q as SuppressionReason, U as TemplateKind, V as TemplateVersionDoc, W as VarsAdapter, X as VarsResolveInfo, Y as WebhookEventDoc, Z as defineVars, _ as ensureIndexes, $ as getCollections, a0 as validateSenderDomain, a1 as varsJsonSchema } from './null-
|
|
1
|
+
import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as MailTesterFeedback, d as Mailer, T as TemplateDoc, e as SuppressionScope, D as DeliveryWindow, F as FlowStep, f as SegmentFilter, P as Predicate } from './null-CfzvqNEp.cjs';
|
|
2
|
+
export { g as AuditLogDoc, B as BroadcastDoc, h as BroadcastStatus, i as CircuitBreakerThresholds, j as Collections, k as ContactTagDoc, E as EventDoc, l as FlowDoc, m as FlowGoal, n as FlowRunDoc, o as FlowRunStatus, p as FlowVersionDoc, H as HealthDoc, q as HealthStatus, L as LeadDoc, r as MailerConfig, s as NullProvider, O as OutboxDoc, t as RESERVED_VAR_KEYS, u as RedisOptions, v as SegmentDefinition, w as SendDoc, x as SendStatus, y as SenderDomainConfig, z as SenderDomainRegistry, G as SenderDomainValidation, I as SubscriptionDoc, J as SubscriptionStatus, K as SuppressionDoc, Q as SuppressionReason, U as TemplateKind, V as TemplateVersionDoc, W as VarsAdapter, X as VarsResolveInfo, Y as WebhookEventDoc, Z as defineVars, _ as ensureIndexes, $ as getCollections, a0 as validateSenderDomain, a1 as varsJsonSchema } from './null-CfzvqNEp.cjs';
|
|
3
3
|
import { ObjectId, Db, Filter } from 'mongodb';
|
|
4
4
|
import { Request, Router } from 'express';
|
|
5
5
|
import Handlebars from 'handlebars';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as MailTesterFeedback, d as Mailer, T as TemplateDoc, e as SuppressionScope, D as DeliveryWindow, F as FlowStep, f as SegmentFilter, P as Predicate } from './null-
|
|
2
|
-
export { g as AuditLogDoc, B as BroadcastDoc, h as BroadcastStatus, i as CircuitBreakerThresholds, j as Collections, k as ContactTagDoc, E as EventDoc, l as FlowDoc, m as FlowGoal, n as FlowRunDoc, o as FlowRunStatus, p as FlowVersionDoc, H as HealthDoc, q as HealthStatus, L as LeadDoc, r as MailerConfig, s as NullProvider, O as OutboxDoc, t as RESERVED_VAR_KEYS, u as RedisOptions, v as SegmentDefinition, w as SendDoc, x as SendStatus, y as SenderDomainConfig, z as SenderDomainRegistry, G as SenderDomainValidation, I as SubscriptionDoc, J as SubscriptionStatus, K as SuppressionDoc, Q as SuppressionReason, U as TemplateKind, V as TemplateVersionDoc, W as VarsAdapter, X as VarsResolveInfo, Y as WebhookEventDoc, Z as defineVars, _ as ensureIndexes, $ as getCollections, a0 as validateSenderDomain, a1 as varsJsonSchema } from './null-
|
|
1
|
+
import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as MailTesterFeedback, d as Mailer, T as TemplateDoc, e as SuppressionScope, D as DeliveryWindow, F as FlowStep, f as SegmentFilter, P as Predicate } from './null-CfzvqNEp.js';
|
|
2
|
+
export { g as AuditLogDoc, B as BroadcastDoc, h as BroadcastStatus, i as CircuitBreakerThresholds, j as Collections, k as ContactTagDoc, E as EventDoc, l as FlowDoc, m as FlowGoal, n as FlowRunDoc, o as FlowRunStatus, p as FlowVersionDoc, H as HealthDoc, q as HealthStatus, L as LeadDoc, r as MailerConfig, s as NullProvider, O as OutboxDoc, t as RESERVED_VAR_KEYS, u as RedisOptions, v as SegmentDefinition, w as SendDoc, x as SendStatus, y as SenderDomainConfig, z as SenderDomainRegistry, G as SenderDomainValidation, I as SubscriptionDoc, J as SubscriptionStatus, K as SuppressionDoc, Q as SuppressionReason, U as TemplateKind, V as TemplateVersionDoc, W as VarsAdapter, X as VarsResolveInfo, Y as WebhookEventDoc, Z as defineVars, _ as ensureIndexes, $ as getCollections, a0 as validateSenderDomain, a1 as varsJsonSchema } from './null-CfzvqNEp.js';
|
|
3
3
|
import { ObjectId, Db, Filter } from 'mongodb';
|
|
4
4
|
import { Request, Router } from 'express';
|
|
5
5
|
import Handlebars from 'handlebars';
|
package/dist/index.js
CHANGED
|
@@ -304,7 +304,10 @@ var init_sendgrid = __esm({
|
|
|
304
304
|
replyTo: args.replyTo,
|
|
305
305
|
subject: args.subject,
|
|
306
306
|
text: args.text,
|
|
307
|
-
html:
|
|
307
|
+
// Omit the key entirely rather than sending `html: undefined` — a
|
|
308
|
+
// text_only template must produce a single-part text/plain message, and
|
|
309
|
+
// an explicit undefined can still serialize into an empty HTML part.
|
|
310
|
+
...args.html ? { html: args.html } : {},
|
|
308
311
|
headers: args.headers,
|
|
309
312
|
customArgs: args.messageMeta,
|
|
310
313
|
trackingSettings: {
|
|
@@ -2143,7 +2146,8 @@ async function dispatchSend(sendId, ctx) {
|
|
|
2143
2146
|
await markFailed(send._id, `render error: ${String(err?.message ?? err)}`, ctx);
|
|
2144
2147
|
throw err;
|
|
2145
2148
|
}
|
|
2146
|
-
const
|
|
2149
|
+
const textOnly = template.bodyFormat === "text_only";
|
|
2150
|
+
const tracking = textOnly ? { html: "", links: [] } : applyTracking(rendered.html, {
|
|
2147
2151
|
sendId: String(send._id),
|
|
2148
2152
|
publicUrl: ctx.config.publicUrl,
|
|
2149
2153
|
trackOpens: template.trackOpens ?? ctx.config.trackOpens,
|
|
@@ -2155,7 +2159,9 @@ async function dispatchSend(sendId, ctx) {
|
|
|
2155
2159
|
{
|
|
2156
2160
|
$set: {
|
|
2157
2161
|
links: tracking.links,
|
|
2158
|
-
|
|
2162
|
+
// Hash what actually goes out, so a text_only send's fingerprint
|
|
2163
|
+
// tracks the text body rather than an HTML part it never had.
|
|
2164
|
+
bodyHash: sha256(textOnly ? rendered.plainText : tracking.html),
|
|
2159
2165
|
status: "sending",
|
|
2160
2166
|
fromName: rendered.fromName,
|
|
2161
2167
|
fromEmail: rendered.fromEmail,
|
|
@@ -2180,7 +2186,7 @@ async function dispatchSend(sendId, ctx) {
|
|
|
2180
2186
|
fromEmail: rendered.fromEmail,
|
|
2181
2187
|
replyTo: rendered.replyTo ?? void 0,
|
|
2182
2188
|
subject: rendered.subject,
|
|
2183
|
-
html: tracking.html,
|
|
2189
|
+
...textOnly ? {} : { html: tracking.html },
|
|
2184
2190
|
text: rendered.plainText,
|
|
2185
2191
|
headers,
|
|
2186
2192
|
messageMeta: { sendId: String(send._id) }
|
|
@@ -4685,6 +4691,40 @@ ${input.plainText}`);
|
|
|
4685
4691
|
});
|
|
4686
4692
|
}
|
|
4687
4693
|
}
|
|
4694
|
+
const offDomain = findOffDomainLinkHosts(input.html, input.fromEmail);
|
|
4695
|
+
if (offDomain.majority && offDomain.hosts.length > 0) {
|
|
4696
|
+
issues.push({
|
|
4697
|
+
rule: "offdomain_links",
|
|
4698
|
+
severity: "warning",
|
|
4699
|
+
message: `Most links point away from the From domain: ${offDomain.hosts.join(", ")}.`,
|
|
4700
|
+
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."
|
|
4701
|
+
});
|
|
4702
|
+
}
|
|
4703
|
+
const insecure = findInsecureLinkHosts(input.html);
|
|
4704
|
+
if (insecure.length > 0) {
|
|
4705
|
+
issues.push({
|
|
4706
|
+
rule: "insecure_link",
|
|
4707
|
+
severity: "warning",
|
|
4708
|
+
message: `Link uses plain http://: ${insecure.join(", ")}.`,
|
|
4709
|
+
hint: "Mixed-content links get rewritten or warned about by some clients, and http:// correlates with stale spam templates. Use https://."
|
|
4710
|
+
});
|
|
4711
|
+
}
|
|
4712
|
+
if (countImagesMissingAlt(input.html) > 0) {
|
|
4713
|
+
issues.push({
|
|
4714
|
+
rule: "image_missing_alt",
|
|
4715
|
+
severity: "warning",
|
|
4716
|
+
message: "One or more images have no alt text.",
|
|
4717
|
+
hint: "Most clients block images by default on first open. Alt text is what the recipient actually sees, and screen readers need it."
|
|
4718
|
+
});
|
|
4719
|
+
}
|
|
4720
|
+
if (hasImageOnlyLink(input.html)) {
|
|
4721
|
+
issues.push({
|
|
4722
|
+
rule: "image_only_link",
|
|
4723
|
+
severity: "warning",
|
|
4724
|
+
message: "A link wraps an image with no accompanying text.",
|
|
4725
|
+
hint: "With images blocked, an image-only call-to-action is invisible and unclickable. Add a text label inside the link."
|
|
4726
|
+
});
|
|
4727
|
+
}
|
|
4688
4728
|
const linkCount = countMatches(input.html, /<a\s[^>]*\bhref\s*=/gi);
|
|
4689
4729
|
if (linkCount > 10) {
|
|
4690
4730
|
issues.push({
|
|
@@ -4739,6 +4779,63 @@ function hasBareUrlInVisibleText(html) {
|
|
|
4739
4779
|
const stripped = html.replace(/<a\b[^>]*>.*?<\/a>/gis, " ").replace(/<style\b[^>]*>.*?<\/style>/gis, " ").replace(/<script\b[^>]*>.*?<\/script>/gis, " ").replace(/<head\b[^>]*>.*?<\/head>/gis, " ");
|
|
4740
4780
|
return /https?:\/\/[^\s<>"']+/i.test(stripped);
|
|
4741
4781
|
}
|
|
4782
|
+
function findOffDomainLinkHosts(html, fromEmail) {
|
|
4783
|
+
const fromDomain = fromEmail.split("@")[1]?.toLowerCase();
|
|
4784
|
+
if (!fromDomain) return { hosts: [], majority: false };
|
|
4785
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
4786
|
+
let total = 0;
|
|
4787
|
+
let off = 0;
|
|
4788
|
+
for (const href of extractHrefs(html)) {
|
|
4789
|
+
if (href.includes("{{")) continue;
|
|
4790
|
+
const host = hostnameOf(href);
|
|
4791
|
+
if (!host) continue;
|
|
4792
|
+
total++;
|
|
4793
|
+
if (sameSite(host, fromDomain)) continue;
|
|
4794
|
+
off++;
|
|
4795
|
+
hosts.add(host);
|
|
4796
|
+
}
|
|
4797
|
+
return { hosts: Array.from(hosts), majority: total > 0 && off * 2 > total };
|
|
4798
|
+
}
|
|
4799
|
+
function sameSite(a, b) {
|
|
4800
|
+
if (a === b) return true;
|
|
4801
|
+
if (a.endsWith(`.${b}`) || b.endsWith(`.${a}`)) return true;
|
|
4802
|
+
return lastLabels(a) === lastLabels(b);
|
|
4803
|
+
}
|
|
4804
|
+
function lastLabels(host) {
|
|
4805
|
+
return host.split(".").slice(-2).join(".");
|
|
4806
|
+
}
|
|
4807
|
+
function findInsecureLinkHosts(html) {
|
|
4808
|
+
const hits = /* @__PURE__ */ new Set();
|
|
4809
|
+
for (const href of extractHrefs(html)) {
|
|
4810
|
+
if (!/^http:\/\//i.test(href)) continue;
|
|
4811
|
+
const host = hostnameOf(href);
|
|
4812
|
+
if (host) hits.add(host);
|
|
4813
|
+
}
|
|
4814
|
+
return Array.from(hits);
|
|
4815
|
+
}
|
|
4816
|
+
function countImagesMissingAlt(html) {
|
|
4817
|
+
let missing = 0;
|
|
4818
|
+
const re = /<img\b[^>]*>/gi;
|
|
4819
|
+
let m;
|
|
4820
|
+
while (m = re.exec(html)) {
|
|
4821
|
+
const tag = m[0];
|
|
4822
|
+
const alt = /\balt\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(tag);
|
|
4823
|
+
const value = alt ? alt[1] ?? alt[2] ?? alt[3] ?? "" : "";
|
|
4824
|
+
if (value.trim().length === 0) missing++;
|
|
4825
|
+
}
|
|
4826
|
+
return missing;
|
|
4827
|
+
}
|
|
4828
|
+
function hasImageOnlyLink(html) {
|
|
4829
|
+
const re = /<a\b[^>]*>(.*?)<\/a>/gis;
|
|
4830
|
+
let m;
|
|
4831
|
+
while (m = re.exec(html)) {
|
|
4832
|
+
const inner = m[1];
|
|
4833
|
+
if (!/<img\b/i.test(inner)) continue;
|
|
4834
|
+
const text = inner.replace(/<[^>]*>/g, "").replace(/ /gi, " ").trim();
|
|
4835
|
+
if (text.length === 0) return true;
|
|
4836
|
+
}
|
|
4837
|
+
return false;
|
|
4838
|
+
}
|
|
4742
4839
|
function findSpamSignals(text) {
|
|
4743
4840
|
const out = /* @__PURE__ */ new Set();
|
|
4744
4841
|
for (const { pattern, phrase } of SPAM_PHRASES) {
|
|
@@ -4811,6 +4908,9 @@ function isAllCaps(subject) {
|
|
|
4811
4908
|
// src/server/api/admin.ts
|
|
4812
4909
|
init_vars();
|
|
4813
4910
|
|
|
4911
|
+
// src/shared/enums.ts
|
|
4912
|
+
var TEMPLATE_BODY_FORMATS = ["multipart", "text_only"];
|
|
4913
|
+
|
|
4814
4914
|
// src/server/api/setup-status.ts
|
|
4815
4915
|
async function runSetupChecks(mailer) {
|
|
4816
4916
|
const checks = [];
|
|
@@ -6470,6 +6570,7 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6470
6570
|
lastModifiedAt: now
|
|
6471
6571
|
},
|
|
6472
6572
|
tags: [],
|
|
6573
|
+
bodyFormat: "multipart",
|
|
6473
6574
|
trackOpens: kind === "marketing",
|
|
6474
6575
|
trackClicks: kind === "marketing",
|
|
6475
6576
|
stats: { sent: 0, delivered: 0, opened: 0, clicked: 0, bounced: 0, complained: 0, unsubscribed: 0, lastSentAt: null },
|
|
@@ -6495,7 +6596,7 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6495
6596
|
asyncHandler(async (req, res) => {
|
|
6496
6597
|
const tpl = await c.templates.findOne({ slug: req.params.slug });
|
|
6497
6598
|
if (!tpl) return res.status(404).json({ error: "not_found" });
|
|
6498
|
-
const { subject, preheader, mjml, editorJson, notes, name, fromName, fromEmail, replyTo, kind, trackOpens, trackClicks } = req.body ?? {};
|
|
6599
|
+
const { subject, preheader, mjml, editorJson, notes, name, fromName, fromEmail, replyTo, kind, bodyFormat, trackOpens, trackClicks } = req.body ?? {};
|
|
6499
6600
|
const set = {
|
|
6500
6601
|
"draft.lastModifiedBy": req.actor,
|
|
6501
6602
|
"draft.lastModifiedAt": /* @__PURE__ */ new Date(),
|
|
@@ -6523,6 +6624,7 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6523
6624
|
});
|
|
6524
6625
|
}
|
|
6525
6626
|
}
|
|
6627
|
+
if (TEMPLATE_BODY_FORMATS.includes(bodyFormat)) set.bodyFormat = bodyFormat;
|
|
6526
6628
|
if (typeof trackOpens === "boolean") set.trackOpens = trackOpens;
|
|
6527
6629
|
if (typeof trackClicks === "boolean") set.trackClicks = trackClicks;
|
|
6528
6630
|
await c.templates.updateOne({ _id: tpl._id }, { $set: set });
|
|
@@ -6608,7 +6710,9 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6608
6710
|
fromEmail: tpl.fromEmail,
|
|
6609
6711
|
replyTo: tpl.replyTo ?? void 0,
|
|
6610
6712
|
subject: draft.subject,
|
|
6611
|
-
|
|
6713
|
+
// Match the real send shape — a text_only template's deliverability
|
|
6714
|
+
// should be scored on the single-part message it actually sends.
|
|
6715
|
+
...tpl.bodyFormat === "text_only" ? {} : { html: compiled.html },
|
|
6612
6716
|
text: compiled.plainText,
|
|
6613
6717
|
headers: {},
|
|
6614
6718
|
messageMeta: { mailTesterCheckId: checkId }
|
|
@@ -6953,7 +7057,9 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6953
7057
|
fromName: rendered.fromName,
|
|
6954
7058
|
fromEmail: rendered.fromEmail,
|
|
6955
7059
|
subject: `[TEST] ${rendered.subject}`,
|
|
6956
|
-
|
|
7060
|
+
// Match the real send shape, or a test of a text_only template would
|
|
7061
|
+
// arrive as HTML and hide exactly what the author is checking.
|
|
7062
|
+
...tpl.bodyFormat === "text_only" ? {} : { html: tracking.html },
|
|
6957
7063
|
text: rendered.plainText
|
|
6958
7064
|
});
|
|
6959
7065
|
await mailer.audit({
|
|
@@ -7593,7 +7699,7 @@ var DEDUPE_POLICIES = [
|
|
|
7593
7699
|
];
|
|
7594
7700
|
|
|
7595
7701
|
// src/server/index.ts
|
|
7596
|
-
var VERSION = "0.
|
|
7702
|
+
var VERSION = "0.12.0" ;
|
|
7597
7703
|
|
|
7598
7704
|
export { DEDUPE_POLICIES, FLOW_STEP_KINDS, Mailer, MongoContactAdapter, NullProvider, PREDICATE_KINDS, RESERVED_VAR_KEYS, SEGMENT_FILTER_KINDS, SendGridProvider, VERSION, applyTracking, applyWebhookEvent, compileMailyTemplate, compileTemplate, computeDeliveryTime, createAdminRouter, createPublicRouter, defaultFlowStep, defaultPredicate, defaultSegmentFilter, defineVars, derivePlaintext, dispatchSend, ensureIndexes, getCollections, predicateKind, processNewlyFiredEventTriggers, processOneRunStep, renderTemplate, runTick, sha256Hex, signUnsubscribeToken, sweepStrandedFlowRuns, validateSenderDomain, varsJsonSchema, verifyUnsubscribeToken };
|
|
7599
7705
|
//# sourceMappingURL=index.js.map
|