@drawbridge/drawbridge-utils 0.0.163 → 0.0.164
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/connections/index.cjs +6 -0
- package/dist/connections/index.d.cts +6 -0
- package/dist/connections/index.d.ts +6 -0
- package/dist/connections/index.js +6 -0
- package/dist/notification.cjs +84 -20
- package/dist/notification.d.cts +117 -34
- package/dist/notification.d.ts +117 -34
- package/dist/notification.js +82 -20
- package/dist/providers.cjs +6 -0
- package/dist/providers.js +6 -0
- package/package.json +1 -1
|
@@ -5808,6 +5808,12 @@ var publicConnectionKeys = Object.freeze([
|
|
|
5808
5808
|
// excerpt, guide, and any vendor redirect copy.
|
|
5809
5809
|
"content",
|
|
5810
5810
|
"createdAt",
|
|
5811
|
+
// THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
|
|
5812
|
+
// paused — kept, configured, and not run — as distinct from a vendor the
|
|
5813
|
+
// platform switched off (which the api overlays as an error). Absent means
|
|
5814
|
+
// on; the dashboard renders Paused from it and the sync gates refuse a paused
|
|
5815
|
+
// connection the way they refuse a missing one.
|
|
5816
|
+
"enabled",
|
|
5811
5817
|
// The connection DOCUMENT's own errors array — scope-drift entries written by
|
|
5812
5818
|
// drawbridge-sync. NOT the manifest's error copy, which is content.errors:
|
|
5813
5819
|
// the document is spread OVER the resolved manifest downstream, so the two
|
|
@@ -7500,6 +7500,12 @@ const publicConnectionKeys = Object.freeze([
|
|
|
7500
7500
|
// excerpt, guide, and any vendor redirect copy.
|
|
7501
7501
|
'content',
|
|
7502
7502
|
'createdAt',
|
|
7503
|
+
// THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
|
|
7504
|
+
// paused — kept, configured, and not run — as distinct from a vendor the
|
|
7505
|
+
// platform switched off (which the api overlays as an error). Absent means
|
|
7506
|
+
// on; the dashboard renders Paused from it and the sync gates refuse a paused
|
|
7507
|
+
// connection the way they refuse a missing one.
|
|
7508
|
+
'enabled',
|
|
7503
7509
|
// The connection DOCUMENT's own errors array — scope-drift entries written by
|
|
7504
7510
|
// drawbridge-sync. NOT the manifest's error copy, which is content.errors:
|
|
7505
7511
|
// the document is spread OVER the resolved manifest downstream, so the two
|
|
@@ -7500,6 +7500,12 @@ const publicConnectionKeys = Object.freeze([
|
|
|
7500
7500
|
// excerpt, guide, and any vendor redirect copy.
|
|
7501
7501
|
'content',
|
|
7502
7502
|
'createdAt',
|
|
7503
|
+
// THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
|
|
7504
|
+
// paused — kept, configured, and not run — as distinct from a vendor the
|
|
7505
|
+
// platform switched off (which the api overlays as an error). Absent means
|
|
7506
|
+
// on; the dashboard renders Paused from it and the sync gates refuse a paused
|
|
7507
|
+
// connection the way they refuse a missing one.
|
|
7508
|
+
'enabled',
|
|
7503
7509
|
// The connection DOCUMENT's own errors array — scope-drift entries written by
|
|
7504
7510
|
// drawbridge-sync. NOT the manifest's error copy, which is content.errors:
|
|
7505
7511
|
// the document is spread OVER the resolved manifest downstream, so the two
|
|
@@ -5732,6 +5732,12 @@ var publicConnectionKeys = Object.freeze([
|
|
|
5732
5732
|
// excerpt, guide, and any vendor redirect copy.
|
|
5733
5733
|
"content",
|
|
5734
5734
|
"createdAt",
|
|
5735
|
+
// THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
|
|
5736
|
+
// paused — kept, configured, and not run — as distinct from a vendor the
|
|
5737
|
+
// platform switched off (which the api overlays as an error). Absent means
|
|
5738
|
+
// on; the dashboard renders Paused from it and the sync gates refuse a paused
|
|
5739
|
+
// connection the way they refuse a missing one.
|
|
5740
|
+
"enabled",
|
|
5735
5741
|
// The connection DOCUMENT's own errors array — scope-drift entries written by
|
|
5736
5742
|
// drawbridge-sync. NOT the manifest's error copy, which is content.errors:
|
|
5737
5743
|
// the document is spread OVER the resolved manifest downstream, so the two
|
package/dist/notification.cjs
CHANGED
|
@@ -19,6 +19,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
// lib/notification.js
|
|
20
20
|
var notification_exports = {};
|
|
21
21
|
__export(notification_exports, {
|
|
22
|
+
POINTS: () => POINTS,
|
|
23
|
+
notificationRules: () => notificationRules,
|
|
22
24
|
notificationWarnings: () => notificationWarnings
|
|
23
25
|
});
|
|
24
26
|
module.exports = __toCommonJS(notification_exports);
|
|
@@ -49,41 +51,103 @@ var phrasesIn = (value) => {
|
|
|
49
51
|
return PHRASES.filter((phrase) => lower.includes(phrase));
|
|
50
52
|
};
|
|
51
53
|
var list = (values) => values.map((value) => "\u201C" + value + "\u201D").join(", ");
|
|
52
|
-
var
|
|
54
|
+
var POINTS = {
|
|
55
|
+
brand: 0,
|
|
56
|
+
currency: 1,
|
|
57
|
+
emoji: 0.5,
|
|
58
|
+
length: 0.5,
|
|
59
|
+
phrase: 1,
|
|
60
|
+
punctuation: 1,
|
|
61
|
+
shouting: 1.5
|
|
62
|
+
};
|
|
63
|
+
var subjectRules = ({ brand, subject }) => {
|
|
53
64
|
const value = String(subject || "").trim();
|
|
54
65
|
if (!value) return [];
|
|
55
66
|
const found = phrasesIn(value);
|
|
56
67
|
const emojis = emojiCount(value);
|
|
57
68
|
return [
|
|
58
|
-
value.length > SUBJECT_DISPLAY_LIMIT &&
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
value.length > SUBJECT_DISPLAY_LIMIT && {
|
|
70
|
+
message: "Most email apps cut the subject around " + SUBJECT_DISPLAY_LIMIT + " characters \u2014 yours is " + value.length + ".",
|
|
71
|
+
points: POINTS.length,
|
|
72
|
+
rule: "length"
|
|
73
|
+
},
|
|
74
|
+
shouting(value) && {
|
|
75
|
+
message: "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
76
|
+
points: POINTS.shouting,
|
|
77
|
+
rule: "shouting"
|
|
78
|
+
},
|
|
79
|
+
/[!?]{2,}/.test(value) && {
|
|
80
|
+
message: "Repeated exclamation or question marks are a common spam signal.",
|
|
81
|
+
points: POINTS.punctuation,
|
|
82
|
+
rule: "punctuation"
|
|
83
|
+
},
|
|
84
|
+
emojis > 1 && {
|
|
85
|
+
message: "More than one emoji in a subject is a common spam signal.",
|
|
86
|
+
points: POINTS.emoji,
|
|
87
|
+
rule: "emoji"
|
|
88
|
+
},
|
|
89
|
+
/[$£€]\s?\d/.test(value) && {
|
|
90
|
+
message: "A currency amount in the subject is a common spam signal.",
|
|
91
|
+
points: POINTS.currency,
|
|
92
|
+
rule: "currency"
|
|
93
|
+
},
|
|
94
|
+
found.length > 0 && {
|
|
95
|
+
message: "Reads like a scam to filters and to people: " + list(found) + ".",
|
|
96
|
+
points: POINTS.phrase * found.length,
|
|
97
|
+
rule: "phrase"
|
|
98
|
+
},
|
|
99
|
+
brand && !value.toLowerCase().includes(String(brand).toLowerCase()) && {
|
|
100
|
+
message: "Nothing here says the mail is from " + brand + " \u2014 subjects that name the sender get opened more.",
|
|
101
|
+
points: POINTS.brand,
|
|
102
|
+
rule: "brand"
|
|
103
|
+
}
|
|
104
|
+
].filter(Boolean).map((rule) => ({ ...rule, field: "subject" }));
|
|
66
105
|
};
|
|
67
|
-
var
|
|
106
|
+
var messageRules = ({ message }) => {
|
|
68
107
|
const value = String(message || "").trim();
|
|
69
108
|
if (!value) return [];
|
|
70
109
|
const found = phrasesIn(value);
|
|
71
110
|
return [
|
|
72
|
-
value.length < MESSAGE_SHORT_LIMIT &&
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
111
|
+
value.length < MESSAGE_SHORT_LIMIT && {
|
|
112
|
+
message: "Very short messages read as a fragment, and bulk senders send exactly this shape.",
|
|
113
|
+
points: POINTS.length,
|
|
114
|
+
rule: "length"
|
|
115
|
+
},
|
|
116
|
+
shouting(value) && {
|
|
117
|
+
message: "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
118
|
+
points: POINTS.shouting,
|
|
119
|
+
rule: "shouting"
|
|
120
|
+
},
|
|
121
|
+
/[!?]{2,}/.test(value) && {
|
|
122
|
+
message: "Repeated exclamation or question marks are a common spam signal.",
|
|
123
|
+
points: POINTS.punctuation,
|
|
124
|
+
rule: "punctuation"
|
|
125
|
+
},
|
|
126
|
+
found.length > 0 && {
|
|
127
|
+
message: "Reads like a scam to filters and to people: " + list(found) + ".",
|
|
128
|
+
points: POINTS.phrase * found.length,
|
|
129
|
+
rule: "phrase"
|
|
130
|
+
}
|
|
131
|
+
].filter(Boolean).map((rule) => ({ ...rule, field: "message" }));
|
|
77
132
|
};
|
|
78
|
-
var
|
|
133
|
+
var notificationRules = ({
|
|
79
134
|
brand,
|
|
80
135
|
message,
|
|
81
136
|
subject
|
|
82
|
-
} = {}) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
137
|
+
} = {}) => [
|
|
138
|
+
...subjectRules({ brand, subject }),
|
|
139
|
+
...messageRules({ message })
|
|
140
|
+
];
|
|
141
|
+
var notificationWarnings = (input) => {
|
|
142
|
+
const rules = notificationRules(input);
|
|
143
|
+
return {
|
|
144
|
+
message: rules.filter((rule) => rule.field === "message").map((rule) => rule.message),
|
|
145
|
+
subject: rules.filter((rule) => rule.field === "subject").map((rule) => rule.message)
|
|
146
|
+
};
|
|
147
|
+
};
|
|
86
148
|
// Annotate the CommonJS export names for ESM import in node:
|
|
87
149
|
0 && (module.exports = {
|
|
150
|
+
POINTS,
|
|
151
|
+
notificationRules,
|
|
88
152
|
notificationWarnings
|
|
89
153
|
});
|
package/dist/notification.d.cts
CHANGED
|
@@ -75,11 +75,42 @@ const phrasesIn = ( value ) => {
|
|
|
75
75
|
|
|
76
76
|
const list = ( values ) => values.map( ( value ) => '“' + value + '”' ).join( ', ' );
|
|
77
77
|
|
|
78
|
+
// WHAT EACH RULE IS WORTH, on SpamAssassin's scale — its own rules run 0.1 to
|
|
79
|
+
// about 2.0 and five is the line most installs call spam at.
|
|
80
|
+
//
|
|
81
|
+
// THESE CARRY THE NUMBER. Measured 2026-09-10 through Postmark's SpamCheck:
|
|
82
|
+
// stock SpamAssassin scored "Congratulations!!!" at ZERO — its rules are tuned
|
|
83
|
+
// for 2005-era pharma and lottery mail and header forgery, and a two-line
|
|
84
|
+
// giveaway email trips none of them. So for the copy this product sends, the
|
|
85
|
+
// filter's opinion is close to noise and these rules are the actual signal.
|
|
86
|
+
// A flat half-point each (the first version) left "Congratulations!!!" reading
|
|
87
|
+
// as fine, which is the opposite of what a merchant needs to hear.
|
|
88
|
+
//
|
|
89
|
+
// Weighted by how reliably each one gets mail binned, and by how likely it is
|
|
90
|
+
// to be an accident. Shouting and scam phrasing are never accidents. Repeated
|
|
91
|
+
// punctuation and a currency amount are strong. Emoji and length are habits
|
|
92
|
+
// worth a nudge. The brand line is advice about context rather than the
|
|
93
|
+
// words — and the default copy every campaign ships with cannot name a brand
|
|
94
|
+
// it does not know — so it is listed at zero and never marks a default down.
|
|
95
|
+
//
|
|
96
|
+
// PHRASES COUNT PER MATCH. "Congratulations, click here, limited time" is
|
|
97
|
+
// three of them and reads like three of them; one sentence lists them all,
|
|
98
|
+
// the points say how many. A product table, and Darren's to move.
|
|
99
|
+
const POINTS = {
|
|
100
|
+
brand : 0,
|
|
101
|
+
currency : 1,
|
|
102
|
+
emoji : 0.5,
|
|
103
|
+
length : 0.5,
|
|
104
|
+
phrase : 1,
|
|
105
|
+
punctuation : 1,
|
|
106
|
+
shouting : 1.5
|
|
107
|
+
};
|
|
108
|
+
|
|
78
109
|
// `brand` is the organization's own name. A subject that never mentions who is
|
|
79
110
|
// writing is the single most recognisable trait of the mail people delete
|
|
80
111
|
// unread — and the one a merchant can fix in five seconds once it is pointed
|
|
81
112
|
// out. Skipped entirely when no brand is supplied rather than guessed at.
|
|
82
|
-
const
|
|
113
|
+
const subjectRules = ({ brand, subject }) => {
|
|
83
114
|
|
|
84
115
|
const value = String( subject || '' ).trim();
|
|
85
116
|
|
|
@@ -89,25 +120,46 @@ const subjectWarnings = ({ brand, subject }) => {
|
|
|
89
120
|
const emojis = emojiCount( value );
|
|
90
121
|
|
|
91
122
|
return [
|
|
92
|
-
value.length > SUBJECT_DISPLAY_LIMIT
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
value.length > SUBJECT_DISPLAY_LIMIT && {
|
|
124
|
+
message : 'Most email apps cut the subject around ' + SUBJECT_DISPLAY_LIMIT + ' characters — yours is ' + value.length + '.',
|
|
125
|
+
points : POINTS.length,
|
|
126
|
+
rule : 'length'
|
|
127
|
+
},
|
|
128
|
+
shouting( value ) && {
|
|
129
|
+
message : 'Mostly capitals reads as shouting, and filters treat it that way too.',
|
|
130
|
+
points : POINTS.shouting,
|
|
131
|
+
rule : 'shouting'
|
|
132
|
+
},
|
|
133
|
+
/[!?]{2,}/.test( value ) && {
|
|
134
|
+
message : 'Repeated exclamation or question marks are a common spam signal.',
|
|
135
|
+
points : POINTS.punctuation,
|
|
136
|
+
rule : 'punctuation'
|
|
137
|
+
},
|
|
138
|
+
emojis > 1 && {
|
|
139
|
+
message : 'More than one emoji in a subject is a common spam signal.',
|
|
140
|
+
points : POINTS.emoji,
|
|
141
|
+
rule : 'emoji'
|
|
142
|
+
},
|
|
143
|
+
/[$£€]\s?\d/.test( value ) && {
|
|
144
|
+
message : 'A currency amount in the subject is a common spam signal.',
|
|
145
|
+
points : POINTS.currency,
|
|
146
|
+
rule : 'currency'
|
|
147
|
+
},
|
|
148
|
+
found.length > 0 && {
|
|
149
|
+
message : 'Reads like a scam to filters and to people: ' + list( found ) + '.',
|
|
150
|
+
points : POINTS.phrase * found.length,
|
|
151
|
+
rule : 'phrase'
|
|
152
|
+
},
|
|
153
|
+
brand && ! value.toLowerCase().includes( String( brand ).toLowerCase() ) && {
|
|
154
|
+
message : 'Nothing here says the mail is from ' + brand + ' — subjects that name the sender get opened more.',
|
|
155
|
+
points : POINTS.brand,
|
|
156
|
+
rule : 'brand'
|
|
157
|
+
}
|
|
158
|
+
].filter( Boolean ).map( ( rule ) => ({ ...rule, field : 'subject' }) );
|
|
107
159
|
|
|
108
160
|
};
|
|
109
161
|
|
|
110
|
-
const
|
|
162
|
+
const messageRules = ({ message }) => {
|
|
111
163
|
|
|
112
164
|
const value = String( message || '' ).trim();
|
|
113
165
|
|
|
@@ -116,20 +168,38 @@ const messageWarnings = ({ message }) => {
|
|
|
116
168
|
const found = phrasesIn( value );
|
|
117
169
|
|
|
118
170
|
return [
|
|
119
|
-
value.length < MESSAGE_SHORT_LIMIT
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
171
|
+
value.length < MESSAGE_SHORT_LIMIT && {
|
|
172
|
+
message : 'Very short messages read as a fragment, and bulk senders send exactly this shape.',
|
|
173
|
+
points : POINTS.length,
|
|
174
|
+
rule : 'length'
|
|
175
|
+
},
|
|
176
|
+
shouting( value ) && {
|
|
177
|
+
message : 'Mostly capitals reads as shouting, and filters treat it that way too.',
|
|
178
|
+
points : POINTS.shouting,
|
|
179
|
+
rule : 'shouting'
|
|
180
|
+
},
|
|
181
|
+
/[!?]{2,}/.test( value ) && {
|
|
182
|
+
message : 'Repeated exclamation or question marks are a common spam signal.',
|
|
183
|
+
points : POINTS.punctuation,
|
|
184
|
+
rule : 'punctuation'
|
|
185
|
+
},
|
|
186
|
+
found.length > 0 && {
|
|
187
|
+
message : 'Reads like a scam to filters and to people: ' + list( found ) + '.',
|
|
188
|
+
points : POINTS.phrase * found.length,
|
|
189
|
+
rule : 'phrase'
|
|
190
|
+
}
|
|
191
|
+
].filter( Boolean ).map( ( rule ) => ({ ...rule, field : 'message' }) );
|
|
128
192
|
|
|
129
193
|
};
|
|
130
194
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
195
|
+
// THE RULES, WITH THEIR WEIGHT: `[ { field, message, points, rule } ]`, subject
|
|
196
|
+
// first. This is what the api's scorer folds into the spam score beside
|
|
197
|
+
// SpamAssassin's own rules, so the sentence a merchant reads and the points
|
|
198
|
+
// it cost are one object.
|
|
199
|
+
//
|
|
200
|
+
// The same rule tripped in both fields comes back twice, once per field, each
|
|
201
|
+
// with its points. Whether that is one habit or two is the scorer's call to
|
|
202
|
+
// make; here the facts are just reported.
|
|
133
203
|
//
|
|
134
204
|
// NO CHANNEL ARGUMENT, deliberately, and it is worth saying why since one was
|
|
135
205
|
// tried twice.
|
|
@@ -150,13 +220,26 @@ const messageWarnings = ({ message }) => {
|
|
|
150
220
|
// wording, and the segment mechanics where one emoji drops the limit from 160
|
|
151
221
|
// characters to 70. Those belong with the SMS connection work rather than here,
|
|
152
222
|
// and their absence is a reason to write them, not to stay quiet meanwhile.
|
|
153
|
-
const
|
|
223
|
+
const notificationRules = ({
|
|
154
224
|
brand,
|
|
155
225
|
message,
|
|
156
226
|
subject
|
|
157
|
-
} = {}) =>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
227
|
+
} = {}) => [
|
|
228
|
+
...subjectRules({ brand, subject }),
|
|
229
|
+
...messageRules({ message })
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
// The sentences alone, per field — what the rules looked like before they had
|
|
233
|
+
// points. Kept for anything that only wants the words.
|
|
234
|
+
const notificationWarnings = ( input ) => {
|
|
235
|
+
|
|
236
|
+
const rules = notificationRules( input );
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
message : rules.filter( ( rule ) => rule.field === 'message' ).map( ( rule ) => rule.message ),
|
|
240
|
+
subject : rules.filter( ( rule ) => rule.field === 'subject' ).map( ( rule ) => rule.message )
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
};
|
|
161
244
|
|
|
162
|
-
export { notificationWarnings };
|
|
245
|
+
export { POINTS, notificationRules, notificationWarnings };
|
package/dist/notification.d.ts
CHANGED
|
@@ -75,11 +75,42 @@ const phrasesIn = ( value ) => {
|
|
|
75
75
|
|
|
76
76
|
const list = ( values ) => values.map( ( value ) => '“' + value + '”' ).join( ', ' );
|
|
77
77
|
|
|
78
|
+
// WHAT EACH RULE IS WORTH, on SpamAssassin's scale — its own rules run 0.1 to
|
|
79
|
+
// about 2.0 and five is the line most installs call spam at.
|
|
80
|
+
//
|
|
81
|
+
// THESE CARRY THE NUMBER. Measured 2026-09-10 through Postmark's SpamCheck:
|
|
82
|
+
// stock SpamAssassin scored "Congratulations!!!" at ZERO — its rules are tuned
|
|
83
|
+
// for 2005-era pharma and lottery mail and header forgery, and a two-line
|
|
84
|
+
// giveaway email trips none of them. So for the copy this product sends, the
|
|
85
|
+
// filter's opinion is close to noise and these rules are the actual signal.
|
|
86
|
+
// A flat half-point each (the first version) left "Congratulations!!!" reading
|
|
87
|
+
// as fine, which is the opposite of what a merchant needs to hear.
|
|
88
|
+
//
|
|
89
|
+
// Weighted by how reliably each one gets mail binned, and by how likely it is
|
|
90
|
+
// to be an accident. Shouting and scam phrasing are never accidents. Repeated
|
|
91
|
+
// punctuation and a currency amount are strong. Emoji and length are habits
|
|
92
|
+
// worth a nudge. The brand line is advice about context rather than the
|
|
93
|
+
// words — and the default copy every campaign ships with cannot name a brand
|
|
94
|
+
// it does not know — so it is listed at zero and never marks a default down.
|
|
95
|
+
//
|
|
96
|
+
// PHRASES COUNT PER MATCH. "Congratulations, click here, limited time" is
|
|
97
|
+
// three of them and reads like three of them; one sentence lists them all,
|
|
98
|
+
// the points say how many. A product table, and Darren's to move.
|
|
99
|
+
const POINTS = {
|
|
100
|
+
brand : 0,
|
|
101
|
+
currency : 1,
|
|
102
|
+
emoji : 0.5,
|
|
103
|
+
length : 0.5,
|
|
104
|
+
phrase : 1,
|
|
105
|
+
punctuation : 1,
|
|
106
|
+
shouting : 1.5
|
|
107
|
+
};
|
|
108
|
+
|
|
78
109
|
// `brand` is the organization's own name. A subject that never mentions who is
|
|
79
110
|
// writing is the single most recognisable trait of the mail people delete
|
|
80
111
|
// unread — and the one a merchant can fix in five seconds once it is pointed
|
|
81
112
|
// out. Skipped entirely when no brand is supplied rather than guessed at.
|
|
82
|
-
const
|
|
113
|
+
const subjectRules = ({ brand, subject }) => {
|
|
83
114
|
|
|
84
115
|
const value = String( subject || '' ).trim();
|
|
85
116
|
|
|
@@ -89,25 +120,46 @@ const subjectWarnings = ({ brand, subject }) => {
|
|
|
89
120
|
const emojis = emojiCount( value );
|
|
90
121
|
|
|
91
122
|
return [
|
|
92
|
-
value.length > SUBJECT_DISPLAY_LIMIT
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
value.length > SUBJECT_DISPLAY_LIMIT && {
|
|
124
|
+
message : 'Most email apps cut the subject around ' + SUBJECT_DISPLAY_LIMIT + ' characters — yours is ' + value.length + '.',
|
|
125
|
+
points : POINTS.length,
|
|
126
|
+
rule : 'length'
|
|
127
|
+
},
|
|
128
|
+
shouting( value ) && {
|
|
129
|
+
message : 'Mostly capitals reads as shouting, and filters treat it that way too.',
|
|
130
|
+
points : POINTS.shouting,
|
|
131
|
+
rule : 'shouting'
|
|
132
|
+
},
|
|
133
|
+
/[!?]{2,}/.test( value ) && {
|
|
134
|
+
message : 'Repeated exclamation or question marks are a common spam signal.',
|
|
135
|
+
points : POINTS.punctuation,
|
|
136
|
+
rule : 'punctuation'
|
|
137
|
+
},
|
|
138
|
+
emojis > 1 && {
|
|
139
|
+
message : 'More than one emoji in a subject is a common spam signal.',
|
|
140
|
+
points : POINTS.emoji,
|
|
141
|
+
rule : 'emoji'
|
|
142
|
+
},
|
|
143
|
+
/[$£€]\s?\d/.test( value ) && {
|
|
144
|
+
message : 'A currency amount in the subject is a common spam signal.',
|
|
145
|
+
points : POINTS.currency,
|
|
146
|
+
rule : 'currency'
|
|
147
|
+
},
|
|
148
|
+
found.length > 0 && {
|
|
149
|
+
message : 'Reads like a scam to filters and to people: ' + list( found ) + '.',
|
|
150
|
+
points : POINTS.phrase * found.length,
|
|
151
|
+
rule : 'phrase'
|
|
152
|
+
},
|
|
153
|
+
brand && ! value.toLowerCase().includes( String( brand ).toLowerCase() ) && {
|
|
154
|
+
message : 'Nothing here says the mail is from ' + brand + ' — subjects that name the sender get opened more.',
|
|
155
|
+
points : POINTS.brand,
|
|
156
|
+
rule : 'brand'
|
|
157
|
+
}
|
|
158
|
+
].filter( Boolean ).map( ( rule ) => ({ ...rule, field : 'subject' }) );
|
|
107
159
|
|
|
108
160
|
};
|
|
109
161
|
|
|
110
|
-
const
|
|
162
|
+
const messageRules = ({ message }) => {
|
|
111
163
|
|
|
112
164
|
const value = String( message || '' ).trim();
|
|
113
165
|
|
|
@@ -116,20 +168,38 @@ const messageWarnings = ({ message }) => {
|
|
|
116
168
|
const found = phrasesIn( value );
|
|
117
169
|
|
|
118
170
|
return [
|
|
119
|
-
value.length < MESSAGE_SHORT_LIMIT
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
171
|
+
value.length < MESSAGE_SHORT_LIMIT && {
|
|
172
|
+
message : 'Very short messages read as a fragment, and bulk senders send exactly this shape.',
|
|
173
|
+
points : POINTS.length,
|
|
174
|
+
rule : 'length'
|
|
175
|
+
},
|
|
176
|
+
shouting( value ) && {
|
|
177
|
+
message : 'Mostly capitals reads as shouting, and filters treat it that way too.',
|
|
178
|
+
points : POINTS.shouting,
|
|
179
|
+
rule : 'shouting'
|
|
180
|
+
},
|
|
181
|
+
/[!?]{2,}/.test( value ) && {
|
|
182
|
+
message : 'Repeated exclamation or question marks are a common spam signal.',
|
|
183
|
+
points : POINTS.punctuation,
|
|
184
|
+
rule : 'punctuation'
|
|
185
|
+
},
|
|
186
|
+
found.length > 0 && {
|
|
187
|
+
message : 'Reads like a scam to filters and to people: ' + list( found ) + '.',
|
|
188
|
+
points : POINTS.phrase * found.length,
|
|
189
|
+
rule : 'phrase'
|
|
190
|
+
}
|
|
191
|
+
].filter( Boolean ).map( ( rule ) => ({ ...rule, field : 'message' }) );
|
|
128
192
|
|
|
129
193
|
};
|
|
130
194
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
195
|
+
// THE RULES, WITH THEIR WEIGHT: `[ { field, message, points, rule } ]`, subject
|
|
196
|
+
// first. This is what the api's scorer folds into the spam score beside
|
|
197
|
+
// SpamAssassin's own rules, so the sentence a merchant reads and the points
|
|
198
|
+
// it cost are one object.
|
|
199
|
+
//
|
|
200
|
+
// The same rule tripped in both fields comes back twice, once per field, each
|
|
201
|
+
// with its points. Whether that is one habit or two is the scorer's call to
|
|
202
|
+
// make; here the facts are just reported.
|
|
133
203
|
//
|
|
134
204
|
// NO CHANNEL ARGUMENT, deliberately, and it is worth saying why since one was
|
|
135
205
|
// tried twice.
|
|
@@ -150,13 +220,26 @@ const messageWarnings = ({ message }) => {
|
|
|
150
220
|
// wording, and the segment mechanics where one emoji drops the limit from 160
|
|
151
221
|
// characters to 70. Those belong with the SMS connection work rather than here,
|
|
152
222
|
// and their absence is a reason to write them, not to stay quiet meanwhile.
|
|
153
|
-
const
|
|
223
|
+
const notificationRules = ({
|
|
154
224
|
brand,
|
|
155
225
|
message,
|
|
156
226
|
subject
|
|
157
|
-
} = {}) =>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
227
|
+
} = {}) => [
|
|
228
|
+
...subjectRules({ brand, subject }),
|
|
229
|
+
...messageRules({ message })
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
// The sentences alone, per field — what the rules looked like before they had
|
|
233
|
+
// points. Kept for anything that only wants the words.
|
|
234
|
+
const notificationWarnings = ( input ) => {
|
|
235
|
+
|
|
236
|
+
const rules = notificationRules( input );
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
message : rules.filter( ( rule ) => rule.field === 'message' ).map( ( rule ) => rule.message ),
|
|
240
|
+
subject : rules.filter( ( rule ) => rule.field === 'subject' ).map( ( rule ) => rule.message )
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
};
|
|
161
244
|
|
|
162
|
-
export { notificationWarnings };
|
|
245
|
+
export { POINTS, notificationRules, notificationWarnings };
|
package/dist/notification.js
CHANGED
|
@@ -26,40 +26,102 @@ var phrasesIn = (value) => {
|
|
|
26
26
|
return PHRASES.filter((phrase) => lower.includes(phrase));
|
|
27
27
|
};
|
|
28
28
|
var list = (values) => values.map((value) => "\u201C" + value + "\u201D").join(", ");
|
|
29
|
-
var
|
|
29
|
+
var POINTS = {
|
|
30
|
+
brand: 0,
|
|
31
|
+
currency: 1,
|
|
32
|
+
emoji: 0.5,
|
|
33
|
+
length: 0.5,
|
|
34
|
+
phrase: 1,
|
|
35
|
+
punctuation: 1,
|
|
36
|
+
shouting: 1.5
|
|
37
|
+
};
|
|
38
|
+
var subjectRules = ({ brand, subject }) => {
|
|
30
39
|
const value = String(subject || "").trim();
|
|
31
40
|
if (!value) return [];
|
|
32
41
|
const found = phrasesIn(value);
|
|
33
42
|
const emojis = emojiCount(value);
|
|
34
43
|
return [
|
|
35
|
-
value.length > SUBJECT_DISPLAY_LIMIT &&
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
value.length > SUBJECT_DISPLAY_LIMIT && {
|
|
45
|
+
message: "Most email apps cut the subject around " + SUBJECT_DISPLAY_LIMIT + " characters \u2014 yours is " + value.length + ".",
|
|
46
|
+
points: POINTS.length,
|
|
47
|
+
rule: "length"
|
|
48
|
+
},
|
|
49
|
+
shouting(value) && {
|
|
50
|
+
message: "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
51
|
+
points: POINTS.shouting,
|
|
52
|
+
rule: "shouting"
|
|
53
|
+
},
|
|
54
|
+
/[!?]{2,}/.test(value) && {
|
|
55
|
+
message: "Repeated exclamation or question marks are a common spam signal.",
|
|
56
|
+
points: POINTS.punctuation,
|
|
57
|
+
rule: "punctuation"
|
|
58
|
+
},
|
|
59
|
+
emojis > 1 && {
|
|
60
|
+
message: "More than one emoji in a subject is a common spam signal.",
|
|
61
|
+
points: POINTS.emoji,
|
|
62
|
+
rule: "emoji"
|
|
63
|
+
},
|
|
64
|
+
/[$£€]\s?\d/.test(value) && {
|
|
65
|
+
message: "A currency amount in the subject is a common spam signal.",
|
|
66
|
+
points: POINTS.currency,
|
|
67
|
+
rule: "currency"
|
|
68
|
+
},
|
|
69
|
+
found.length > 0 && {
|
|
70
|
+
message: "Reads like a scam to filters and to people: " + list(found) + ".",
|
|
71
|
+
points: POINTS.phrase * found.length,
|
|
72
|
+
rule: "phrase"
|
|
73
|
+
},
|
|
74
|
+
brand && !value.toLowerCase().includes(String(brand).toLowerCase()) && {
|
|
75
|
+
message: "Nothing here says the mail is from " + brand + " \u2014 subjects that name the sender get opened more.",
|
|
76
|
+
points: POINTS.brand,
|
|
77
|
+
rule: "brand"
|
|
78
|
+
}
|
|
79
|
+
].filter(Boolean).map((rule) => ({ ...rule, field: "subject" }));
|
|
43
80
|
};
|
|
44
|
-
var
|
|
81
|
+
var messageRules = ({ message }) => {
|
|
45
82
|
const value = String(message || "").trim();
|
|
46
83
|
if (!value) return [];
|
|
47
84
|
const found = phrasesIn(value);
|
|
48
85
|
return [
|
|
49
|
-
value.length < MESSAGE_SHORT_LIMIT &&
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
value.length < MESSAGE_SHORT_LIMIT && {
|
|
87
|
+
message: "Very short messages read as a fragment, and bulk senders send exactly this shape.",
|
|
88
|
+
points: POINTS.length,
|
|
89
|
+
rule: "length"
|
|
90
|
+
},
|
|
91
|
+
shouting(value) && {
|
|
92
|
+
message: "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
93
|
+
points: POINTS.shouting,
|
|
94
|
+
rule: "shouting"
|
|
95
|
+
},
|
|
96
|
+
/[!?]{2,}/.test(value) && {
|
|
97
|
+
message: "Repeated exclamation or question marks are a common spam signal.",
|
|
98
|
+
points: POINTS.punctuation,
|
|
99
|
+
rule: "punctuation"
|
|
100
|
+
},
|
|
101
|
+
found.length > 0 && {
|
|
102
|
+
message: "Reads like a scam to filters and to people: " + list(found) + ".",
|
|
103
|
+
points: POINTS.phrase * found.length,
|
|
104
|
+
rule: "phrase"
|
|
105
|
+
}
|
|
106
|
+
].filter(Boolean).map((rule) => ({ ...rule, field: "message" }));
|
|
54
107
|
};
|
|
55
|
-
var
|
|
108
|
+
var notificationRules = ({
|
|
56
109
|
brand,
|
|
57
110
|
message,
|
|
58
111
|
subject
|
|
59
|
-
} = {}) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
112
|
+
} = {}) => [
|
|
113
|
+
...subjectRules({ brand, subject }),
|
|
114
|
+
...messageRules({ message })
|
|
115
|
+
];
|
|
116
|
+
var notificationWarnings = (input) => {
|
|
117
|
+
const rules = notificationRules(input);
|
|
118
|
+
return {
|
|
119
|
+
message: rules.filter((rule) => rule.field === "message").map((rule) => rule.message),
|
|
120
|
+
subject: rules.filter((rule) => rule.field === "subject").map((rule) => rule.message)
|
|
121
|
+
};
|
|
122
|
+
};
|
|
63
123
|
export {
|
|
124
|
+
POINTS,
|
|
125
|
+
notificationRules,
|
|
64
126
|
notificationWarnings
|
|
65
127
|
};
|
package/dist/providers.cjs
CHANGED
|
@@ -5653,6 +5653,12 @@ var publicConnectionKeys = Object.freeze([
|
|
|
5653
5653
|
// excerpt, guide, and any vendor redirect copy.
|
|
5654
5654
|
"content",
|
|
5655
5655
|
"createdAt",
|
|
5656
|
+
// THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
|
|
5657
|
+
// paused — kept, configured, and not run — as distinct from a vendor the
|
|
5658
|
+
// platform switched off (which the api overlays as an error). Absent means
|
|
5659
|
+
// on; the dashboard renders Paused from it and the sync gates refuse a paused
|
|
5660
|
+
// connection the way they refuse a missing one.
|
|
5661
|
+
"enabled",
|
|
5656
5662
|
// The connection DOCUMENT's own errors array — scope-drift entries written by
|
|
5657
5663
|
// drawbridge-sync. NOT the manifest's error copy, which is content.errors:
|
|
5658
5664
|
// the document is spread OVER the resolved manifest downstream, so the two
|
package/dist/providers.js
CHANGED
|
@@ -5604,6 +5604,12 @@ var publicConnectionKeys = Object.freeze([
|
|
|
5604
5604
|
// excerpt, guide, and any vendor redirect copy.
|
|
5605
5605
|
"content",
|
|
5606
5606
|
"createdAt",
|
|
5607
|
+
// THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
|
|
5608
|
+
// paused — kept, configured, and not run — as distinct from a vendor the
|
|
5609
|
+
// platform switched off (which the api overlays as an error). Absent means
|
|
5610
|
+
// on; the dashboard renders Paused from it and the sync gates refuse a paused
|
|
5611
|
+
// connection the way they refuse a missing one.
|
|
5612
|
+
"enabled",
|
|
5607
5613
|
// The connection DOCUMENT's own errors array — scope-drift entries written by
|
|
5608
5614
|
// drawbridge-sync. NOT the manifest's error copy, which is content.errors:
|
|
5609
5615
|
// the document is spread OVER the resolved manifest downstream, so the two
|
package/package.json
CHANGED