@drawbridge/drawbridge-utils 0.0.159 → 0.0.160
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 +14 -12
- package/dist/connections/index.d.cts +9 -8
- package/dist/connections/index.d.ts +9 -8
- package/dist/connections/index.js +12 -10
- package/dist/index.cjs +30 -4
- package/dist/index.d.cts +19 -6
- package/dist/index.d.ts +19 -6
- package/dist/index.js +30 -4
- package/dist/nanoid.cjs +11 -1
- package/dist/nanoid.d.cts +41 -1
- package/dist/nanoid.d.ts +41 -1
- package/dist/nanoid.js +11 -1
- package/dist/notification.cjs +93 -0
- package/dist/notification.d.cts +155 -0
- package/dist/notification.d.ts +155 -0
- package/dist/notification.js +69 -0
- package/dist/plans.cjs +4 -2
- package/dist/plans.d.cts +2 -1
- package/dist/plans.d.ts +2 -1
- package/dist/plans.js +4 -2
- package/dist/pricing.cjs +4 -2
- package/dist/pricing.d.cts +2 -1
- package/dist/pricing.d.ts +2 -1
- package/dist/pricing.js +4 -2
- package/dist/providers.cjs +16 -14
- package/dist/providers.d.cts +2 -1
- package/dist/providers.d.ts +2 -1
- package/dist/providers.js +14 -12
- package/package.json +6 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// lib/notification.js
|
|
20
|
+
var notification_exports = {};
|
|
21
|
+
__export(notification_exports, {
|
|
22
|
+
notificationWarnings: () => notificationWarnings
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(notification_exports);
|
|
25
|
+
var PHRASES = [
|
|
26
|
+
"act now",
|
|
27
|
+
"apply now",
|
|
28
|
+
"click here",
|
|
29
|
+
"congratulations",
|
|
30
|
+
"limited time",
|
|
31
|
+
"no obligation",
|
|
32
|
+
"once in a lifetime",
|
|
33
|
+
"risk free",
|
|
34
|
+
"this is not a scam",
|
|
35
|
+
"urgent",
|
|
36
|
+
"you have been selected"
|
|
37
|
+
];
|
|
38
|
+
var SUBJECT_DISPLAY_LIMIT = 60;
|
|
39
|
+
var MESSAGE_SHORT_LIMIT = 20;
|
|
40
|
+
var emojiCount = (value) => (value.match(new RegExp("\\p{Extended_Pictographic}", "gu")) || []).length;
|
|
41
|
+
var shouting = (value) => {
|
|
42
|
+
const letters = value.replace(/[^A-Za-z]/g, "");
|
|
43
|
+
if (letters.length < 4) return false;
|
|
44
|
+
const upper = letters.replace(/[^A-Z]/g, "");
|
|
45
|
+
return upper.length / letters.length > 0.7;
|
|
46
|
+
};
|
|
47
|
+
var phrasesIn = (value) => {
|
|
48
|
+
const lower = value.toLowerCase();
|
|
49
|
+
return PHRASES.filter((phrase) => lower.includes(phrase));
|
|
50
|
+
};
|
|
51
|
+
var list = (values) => values.map((value) => "\u201C" + value + "\u201D").join(", ");
|
|
52
|
+
var subjectWarnings = ({ brand, subject }) => {
|
|
53
|
+
const value = String(subject || "").trim();
|
|
54
|
+
if (!value) return [];
|
|
55
|
+
const found = phrasesIn(value);
|
|
56
|
+
const emojis = emojiCount(value);
|
|
57
|
+
return [
|
|
58
|
+
value.length > SUBJECT_DISPLAY_LIMIT && "Most email apps cut the subject around " + SUBJECT_DISPLAY_LIMIT + " characters \u2014 yours is " + value.length + ".",
|
|
59
|
+
shouting(value) && "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
60
|
+
/[!?]{2,}/.test(value) && "Repeated exclamation or question marks are a common spam signal.",
|
|
61
|
+
emojis > 1 && "More than one emoji in a subject is a common spam signal.",
|
|
62
|
+
/[$£€]\s?\d/.test(value) && "A currency amount in the subject is a common spam signal.",
|
|
63
|
+
found.length > 0 && "Reads like a scam to filters and to people: " + list(found) + ".",
|
|
64
|
+
brand && !value.toLowerCase().includes(String(brand).toLowerCase()) && "Nothing here says the mail is from " + brand + " \u2014 subjects that name the sender get opened more."
|
|
65
|
+
].filter(Boolean);
|
|
66
|
+
};
|
|
67
|
+
var messageWarnings = ({ message }) => {
|
|
68
|
+
const value = String(message || "").trim();
|
|
69
|
+
if (!value) return [];
|
|
70
|
+
const found = phrasesIn(value);
|
|
71
|
+
return [
|
|
72
|
+
value.length < MESSAGE_SHORT_LIMIT && "Very short messages read as a fragment, and bulk senders send exactly this shape.",
|
|
73
|
+
shouting(value) && "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
74
|
+
/[!?]{2,}/.test(value) && "Repeated exclamation or question marks are a common spam signal.",
|
|
75
|
+
found.length > 0 && "Reads like a scam to filters and to people: " + list(found) + "."
|
|
76
|
+
].filter(Boolean);
|
|
77
|
+
};
|
|
78
|
+
var notificationWarnings = ({
|
|
79
|
+
brand,
|
|
80
|
+
channel = "email",
|
|
81
|
+
message,
|
|
82
|
+
subject
|
|
83
|
+
} = {}) => {
|
|
84
|
+
if (channel !== "email") return { message: [], subject: [] };
|
|
85
|
+
return {
|
|
86
|
+
message: messageWarnings({ message }),
|
|
87
|
+
subject: subjectWarnings({ brand, subject })
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
91
|
+
0 && (module.exports = {
|
|
92
|
+
notificationWarnings
|
|
93
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Warnings about the copy a merchant writes into a notification — the subject
|
|
2
|
+
// and message pair behind every lead-facing send.
|
|
3
|
+
//
|
|
4
|
+
// WHAT THIS IS NOT: a deliverability prediction. Gmail accepts mail and then
|
|
5
|
+
// decides where to file it using reputation and engagement signals no local
|
|
6
|
+
// check can see — a send can score perfectly here and still land in Promotions.
|
|
7
|
+
// Anyone reading a clean result as "this will reach the inbox" has been misled,
|
|
8
|
+
// so nothing here returns a score or a pass. It returns sentences a person can
|
|
9
|
+
// act on, or nothing at all.
|
|
10
|
+
//
|
|
11
|
+
// WHAT IT IS FOR: the own-goals. A merchant swapping one weak default for a
|
|
12
|
+
// worse one, in ALL CAPS, with three exclamation marks. That matters more here
|
|
13
|
+
// than on most platforms because lead mail sends from a SHARED domain
|
|
14
|
+
// (org+<shortId>@send.…), so one merchant's copy is spent out of every other
|
|
15
|
+
// tenant's reputation. A merchant on their own verified domain is spending only
|
|
16
|
+
// their own.
|
|
17
|
+
//
|
|
18
|
+
// Every rule below is a WARNING, never an error. False positives are certain —
|
|
19
|
+
// a giveaway platform's mail legitimately says "you won" — and blocking a
|
|
20
|
+
// merchant's own words over a heuristic is worse than the heuristic is good.
|
|
21
|
+
|
|
22
|
+
// Deliberately short, and deliberately NOT the usual spam-word list. This
|
|
23
|
+
// product's mail is about prizes: "winner", "free" and "claim" are the domain's
|
|
24
|
+
// own vocabulary and flagging them would fire on every campaign, which teaches
|
|
25
|
+
// people to ignore the warnings entirely. What is left is the scam-SHAPED
|
|
26
|
+
// phrasing — vague, urgent, and naming no brand — which is exactly what a
|
|
27
|
+
// merchant reaches for when they have nothing better to say.
|
|
28
|
+
const PHRASES = [
|
|
29
|
+
'act now',
|
|
30
|
+
'apply now',
|
|
31
|
+
'click here',
|
|
32
|
+
'congratulations',
|
|
33
|
+
'limited time',
|
|
34
|
+
'no obligation',
|
|
35
|
+
'once in a lifetime',
|
|
36
|
+
'risk free',
|
|
37
|
+
'this is not a scam',
|
|
38
|
+
'urgent',
|
|
39
|
+
'you have been selected'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Most clients cut the subject around here, and a mobile list cuts it sooner.
|
|
43
|
+
// Not a spam rule at all — a legibility one — but it belongs in the same place
|
|
44
|
+
// because it is the same question a merchant is asking when they type.
|
|
45
|
+
const SUBJECT_DISPLAY_LIMIT = 60;
|
|
46
|
+
|
|
47
|
+
// Under this, a body reads as a fragment rather than a message. Short bodies
|
|
48
|
+
// with a link are also the shape bulk senders use, so it is worth a nudge.
|
|
49
|
+
const MESSAGE_SHORT_LIMIT = 20;
|
|
50
|
+
|
|
51
|
+
const emojiCount = ( value ) => ( value.match( /\p{Extended_Pictographic}/gu ) || [] ).length;
|
|
52
|
+
|
|
53
|
+
// Shouting is measured over LETTERS, not characters: "WIN A $500 GIFT CARD" is
|
|
54
|
+
// mostly non-letters, and counting those would let a genuinely shouted subject
|
|
55
|
+
// slip under any sensible ratio.
|
|
56
|
+
const shouting = ( value ) => {
|
|
57
|
+
|
|
58
|
+
const letters = value.replace( /[^A-Za-z]/g, '' );
|
|
59
|
+
|
|
60
|
+
if( letters.length < 4 ) return false;
|
|
61
|
+
|
|
62
|
+
const upper = letters.replace( /[^A-Z]/g, '' );
|
|
63
|
+
|
|
64
|
+
return ( upper.length / letters.length ) > 0.7;
|
|
65
|
+
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const phrasesIn = ( value ) => {
|
|
69
|
+
|
|
70
|
+
const lower = value.toLowerCase();
|
|
71
|
+
|
|
72
|
+
return PHRASES.filter( ( phrase ) => lower.includes( phrase ) );
|
|
73
|
+
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const list = ( values ) => values.map( ( value ) => '“' + value + '”' ).join( ', ' );
|
|
77
|
+
|
|
78
|
+
// `brand` is the organization's own name. A subject that never mentions who is
|
|
79
|
+
// writing is the single most recognisable trait of the mail people delete
|
|
80
|
+
// unread — and the one a merchant can fix in five seconds once it is pointed
|
|
81
|
+
// out. Skipped entirely when no brand is supplied rather than guessed at.
|
|
82
|
+
const subjectWarnings = ({ brand, subject }) => {
|
|
83
|
+
|
|
84
|
+
const value = String( subject || '' ).trim();
|
|
85
|
+
|
|
86
|
+
if( ! value ) return [];
|
|
87
|
+
|
|
88
|
+
const found = phrasesIn( value );
|
|
89
|
+
const emojis = emojiCount( value );
|
|
90
|
+
|
|
91
|
+
return [
|
|
92
|
+
value.length > SUBJECT_DISPLAY_LIMIT
|
|
93
|
+
&& 'Most email apps cut the subject around ' + SUBJECT_DISPLAY_LIMIT + ' characters — yours is ' + value.length + '.',
|
|
94
|
+
shouting( value )
|
|
95
|
+
&& 'Mostly capitals reads as shouting, and filters treat it that way too.',
|
|
96
|
+
/[!?]{2,}/.test( value )
|
|
97
|
+
&& 'Repeated exclamation or question marks are a common spam signal.',
|
|
98
|
+
emojis > 1
|
|
99
|
+
&& 'More than one emoji in a subject is a common spam signal.',
|
|
100
|
+
/[$£€]\s?\d/.test( value )
|
|
101
|
+
&& 'A currency amount in the subject is a common spam signal.',
|
|
102
|
+
found.length > 0
|
|
103
|
+
&& 'Reads like a scam to filters and to people: ' + list( found ) + '.',
|
|
104
|
+
brand && ! value.toLowerCase().includes( String( brand ).toLowerCase() )
|
|
105
|
+
&& 'Nothing here says the mail is from ' + brand + ' — subjects that name the sender get opened more.'
|
|
106
|
+
].filter( Boolean );
|
|
107
|
+
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const messageWarnings = ({ message }) => {
|
|
111
|
+
|
|
112
|
+
const value = String( message || '' ).trim();
|
|
113
|
+
|
|
114
|
+
if( ! value ) return [];
|
|
115
|
+
|
|
116
|
+
const found = phrasesIn( value );
|
|
117
|
+
|
|
118
|
+
return [
|
|
119
|
+
value.length < MESSAGE_SHORT_LIMIT
|
|
120
|
+
&& 'Very short messages read as a fragment, and bulk senders send exactly this shape.',
|
|
121
|
+
shouting( value )
|
|
122
|
+
&& 'Mostly capitals reads as shouting, and filters treat it that way too.',
|
|
123
|
+
/[!?]{2,}/.test( value )
|
|
124
|
+
&& 'Repeated exclamation or question marks are a common spam signal.',
|
|
125
|
+
found.length > 0
|
|
126
|
+
&& 'Reads like a scam to filters and to people: ' + list( found ) + '.'
|
|
127
|
+
].filter( Boolean );
|
|
128
|
+
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ONE ENTRY POINT, answering per field so each warning renders against the
|
|
132
|
+
// input it is about rather than as a pile at the bottom of the form.
|
|
133
|
+
//
|
|
134
|
+
// `channel` is accepted and only 'email' is implemented. SMS is a genuinely
|
|
135
|
+
// different problem — carriers filter on SHAFT categories, link shorteners and
|
|
136
|
+
// opt-out wording, none of which these rules look at — and returning email
|
|
137
|
+
// advice for an SMS would be confidently wrong. It returns nothing instead,
|
|
138
|
+
// until someone writes the carrier rules properly.
|
|
139
|
+
const notificationWarnings = ({
|
|
140
|
+
brand,
|
|
141
|
+
channel = 'email',
|
|
142
|
+
message,
|
|
143
|
+
subject
|
|
144
|
+
} = {}) => {
|
|
145
|
+
|
|
146
|
+
if( channel !== 'email' ) return { message : [], subject : [] };
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
message : messageWarnings({ message }),
|
|
150
|
+
subject : subjectWarnings({ brand, subject })
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export { notificationWarnings };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Warnings about the copy a merchant writes into a notification — the subject
|
|
2
|
+
// and message pair behind every lead-facing send.
|
|
3
|
+
//
|
|
4
|
+
// WHAT THIS IS NOT: a deliverability prediction. Gmail accepts mail and then
|
|
5
|
+
// decides where to file it using reputation and engagement signals no local
|
|
6
|
+
// check can see — a send can score perfectly here and still land in Promotions.
|
|
7
|
+
// Anyone reading a clean result as "this will reach the inbox" has been misled,
|
|
8
|
+
// so nothing here returns a score or a pass. It returns sentences a person can
|
|
9
|
+
// act on, or nothing at all.
|
|
10
|
+
//
|
|
11
|
+
// WHAT IT IS FOR: the own-goals. A merchant swapping one weak default for a
|
|
12
|
+
// worse one, in ALL CAPS, with three exclamation marks. That matters more here
|
|
13
|
+
// than on most platforms because lead mail sends from a SHARED domain
|
|
14
|
+
// (org+<shortId>@send.…), so one merchant's copy is spent out of every other
|
|
15
|
+
// tenant's reputation. A merchant on their own verified domain is spending only
|
|
16
|
+
// their own.
|
|
17
|
+
//
|
|
18
|
+
// Every rule below is a WARNING, never an error. False positives are certain —
|
|
19
|
+
// a giveaway platform's mail legitimately says "you won" — and blocking a
|
|
20
|
+
// merchant's own words over a heuristic is worse than the heuristic is good.
|
|
21
|
+
|
|
22
|
+
// Deliberately short, and deliberately NOT the usual spam-word list. This
|
|
23
|
+
// product's mail is about prizes: "winner", "free" and "claim" are the domain's
|
|
24
|
+
// own vocabulary and flagging them would fire on every campaign, which teaches
|
|
25
|
+
// people to ignore the warnings entirely. What is left is the scam-SHAPED
|
|
26
|
+
// phrasing — vague, urgent, and naming no brand — which is exactly what a
|
|
27
|
+
// merchant reaches for when they have nothing better to say.
|
|
28
|
+
const PHRASES = [
|
|
29
|
+
'act now',
|
|
30
|
+
'apply now',
|
|
31
|
+
'click here',
|
|
32
|
+
'congratulations',
|
|
33
|
+
'limited time',
|
|
34
|
+
'no obligation',
|
|
35
|
+
'once in a lifetime',
|
|
36
|
+
'risk free',
|
|
37
|
+
'this is not a scam',
|
|
38
|
+
'urgent',
|
|
39
|
+
'you have been selected'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Most clients cut the subject around here, and a mobile list cuts it sooner.
|
|
43
|
+
// Not a spam rule at all — a legibility one — but it belongs in the same place
|
|
44
|
+
// because it is the same question a merchant is asking when they type.
|
|
45
|
+
const SUBJECT_DISPLAY_LIMIT = 60;
|
|
46
|
+
|
|
47
|
+
// Under this, a body reads as a fragment rather than a message. Short bodies
|
|
48
|
+
// with a link are also the shape bulk senders use, so it is worth a nudge.
|
|
49
|
+
const MESSAGE_SHORT_LIMIT = 20;
|
|
50
|
+
|
|
51
|
+
const emojiCount = ( value ) => ( value.match( /\p{Extended_Pictographic}/gu ) || [] ).length;
|
|
52
|
+
|
|
53
|
+
// Shouting is measured over LETTERS, not characters: "WIN A $500 GIFT CARD" is
|
|
54
|
+
// mostly non-letters, and counting those would let a genuinely shouted subject
|
|
55
|
+
// slip under any sensible ratio.
|
|
56
|
+
const shouting = ( value ) => {
|
|
57
|
+
|
|
58
|
+
const letters = value.replace( /[^A-Za-z]/g, '' );
|
|
59
|
+
|
|
60
|
+
if( letters.length < 4 ) return false;
|
|
61
|
+
|
|
62
|
+
const upper = letters.replace( /[^A-Z]/g, '' );
|
|
63
|
+
|
|
64
|
+
return ( upper.length / letters.length ) > 0.7;
|
|
65
|
+
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const phrasesIn = ( value ) => {
|
|
69
|
+
|
|
70
|
+
const lower = value.toLowerCase();
|
|
71
|
+
|
|
72
|
+
return PHRASES.filter( ( phrase ) => lower.includes( phrase ) );
|
|
73
|
+
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const list = ( values ) => values.map( ( value ) => '“' + value + '”' ).join( ', ' );
|
|
77
|
+
|
|
78
|
+
// `brand` is the organization's own name. A subject that never mentions who is
|
|
79
|
+
// writing is the single most recognisable trait of the mail people delete
|
|
80
|
+
// unread — and the one a merchant can fix in five seconds once it is pointed
|
|
81
|
+
// out. Skipped entirely when no brand is supplied rather than guessed at.
|
|
82
|
+
const subjectWarnings = ({ brand, subject }) => {
|
|
83
|
+
|
|
84
|
+
const value = String( subject || '' ).trim();
|
|
85
|
+
|
|
86
|
+
if( ! value ) return [];
|
|
87
|
+
|
|
88
|
+
const found = phrasesIn( value );
|
|
89
|
+
const emojis = emojiCount( value );
|
|
90
|
+
|
|
91
|
+
return [
|
|
92
|
+
value.length > SUBJECT_DISPLAY_LIMIT
|
|
93
|
+
&& 'Most email apps cut the subject around ' + SUBJECT_DISPLAY_LIMIT + ' characters — yours is ' + value.length + '.',
|
|
94
|
+
shouting( value )
|
|
95
|
+
&& 'Mostly capitals reads as shouting, and filters treat it that way too.',
|
|
96
|
+
/[!?]{2,}/.test( value )
|
|
97
|
+
&& 'Repeated exclamation or question marks are a common spam signal.',
|
|
98
|
+
emojis > 1
|
|
99
|
+
&& 'More than one emoji in a subject is a common spam signal.',
|
|
100
|
+
/[$£€]\s?\d/.test( value )
|
|
101
|
+
&& 'A currency amount in the subject is a common spam signal.',
|
|
102
|
+
found.length > 0
|
|
103
|
+
&& 'Reads like a scam to filters and to people: ' + list( found ) + '.',
|
|
104
|
+
brand && ! value.toLowerCase().includes( String( brand ).toLowerCase() )
|
|
105
|
+
&& 'Nothing here says the mail is from ' + brand + ' — subjects that name the sender get opened more.'
|
|
106
|
+
].filter( Boolean );
|
|
107
|
+
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const messageWarnings = ({ message }) => {
|
|
111
|
+
|
|
112
|
+
const value = String( message || '' ).trim();
|
|
113
|
+
|
|
114
|
+
if( ! value ) return [];
|
|
115
|
+
|
|
116
|
+
const found = phrasesIn( value );
|
|
117
|
+
|
|
118
|
+
return [
|
|
119
|
+
value.length < MESSAGE_SHORT_LIMIT
|
|
120
|
+
&& 'Very short messages read as a fragment, and bulk senders send exactly this shape.',
|
|
121
|
+
shouting( value )
|
|
122
|
+
&& 'Mostly capitals reads as shouting, and filters treat it that way too.',
|
|
123
|
+
/[!?]{2,}/.test( value )
|
|
124
|
+
&& 'Repeated exclamation or question marks are a common spam signal.',
|
|
125
|
+
found.length > 0
|
|
126
|
+
&& 'Reads like a scam to filters and to people: ' + list( found ) + '.'
|
|
127
|
+
].filter( Boolean );
|
|
128
|
+
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ONE ENTRY POINT, answering per field so each warning renders against the
|
|
132
|
+
// input it is about rather than as a pile at the bottom of the form.
|
|
133
|
+
//
|
|
134
|
+
// `channel` is accepted and only 'email' is implemented. SMS is a genuinely
|
|
135
|
+
// different problem — carriers filter on SHAFT categories, link shorteners and
|
|
136
|
+
// opt-out wording, none of which these rules look at — and returning email
|
|
137
|
+
// advice for an SMS would be confidently wrong. It returns nothing instead,
|
|
138
|
+
// until someone writes the carrier rules properly.
|
|
139
|
+
const notificationWarnings = ({
|
|
140
|
+
brand,
|
|
141
|
+
channel = 'email',
|
|
142
|
+
message,
|
|
143
|
+
subject
|
|
144
|
+
} = {}) => {
|
|
145
|
+
|
|
146
|
+
if( channel !== 'email' ) return { message : [], subject : [] };
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
message : messageWarnings({ message }),
|
|
150
|
+
subject : subjectWarnings({ brand, subject })
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export { notificationWarnings };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// lib/notification.js
|
|
2
|
+
var PHRASES = [
|
|
3
|
+
"act now",
|
|
4
|
+
"apply now",
|
|
5
|
+
"click here",
|
|
6
|
+
"congratulations",
|
|
7
|
+
"limited time",
|
|
8
|
+
"no obligation",
|
|
9
|
+
"once in a lifetime",
|
|
10
|
+
"risk free",
|
|
11
|
+
"this is not a scam",
|
|
12
|
+
"urgent",
|
|
13
|
+
"you have been selected"
|
|
14
|
+
];
|
|
15
|
+
var SUBJECT_DISPLAY_LIMIT = 60;
|
|
16
|
+
var MESSAGE_SHORT_LIMIT = 20;
|
|
17
|
+
var emojiCount = (value) => (value.match(new RegExp("\\p{Extended_Pictographic}", "gu")) || []).length;
|
|
18
|
+
var shouting = (value) => {
|
|
19
|
+
const letters = value.replace(/[^A-Za-z]/g, "");
|
|
20
|
+
if (letters.length < 4) return false;
|
|
21
|
+
const upper = letters.replace(/[^A-Z]/g, "");
|
|
22
|
+
return upper.length / letters.length > 0.7;
|
|
23
|
+
};
|
|
24
|
+
var phrasesIn = (value) => {
|
|
25
|
+
const lower = value.toLowerCase();
|
|
26
|
+
return PHRASES.filter((phrase) => lower.includes(phrase));
|
|
27
|
+
};
|
|
28
|
+
var list = (values) => values.map((value) => "\u201C" + value + "\u201D").join(", ");
|
|
29
|
+
var subjectWarnings = ({ brand, subject }) => {
|
|
30
|
+
const value = String(subject || "").trim();
|
|
31
|
+
if (!value) return [];
|
|
32
|
+
const found = phrasesIn(value);
|
|
33
|
+
const emojis = emojiCount(value);
|
|
34
|
+
return [
|
|
35
|
+
value.length > SUBJECT_DISPLAY_LIMIT && "Most email apps cut the subject around " + SUBJECT_DISPLAY_LIMIT + " characters \u2014 yours is " + value.length + ".",
|
|
36
|
+
shouting(value) && "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
37
|
+
/[!?]{2,}/.test(value) && "Repeated exclamation or question marks are a common spam signal.",
|
|
38
|
+
emojis > 1 && "More than one emoji in a subject is a common spam signal.",
|
|
39
|
+
/[$£€]\s?\d/.test(value) && "A currency amount in the subject is a common spam signal.",
|
|
40
|
+
found.length > 0 && "Reads like a scam to filters and to people: " + list(found) + ".",
|
|
41
|
+
brand && !value.toLowerCase().includes(String(brand).toLowerCase()) && "Nothing here says the mail is from " + brand + " \u2014 subjects that name the sender get opened more."
|
|
42
|
+
].filter(Boolean);
|
|
43
|
+
};
|
|
44
|
+
var messageWarnings = ({ message }) => {
|
|
45
|
+
const value = String(message || "").trim();
|
|
46
|
+
if (!value) return [];
|
|
47
|
+
const found = phrasesIn(value);
|
|
48
|
+
return [
|
|
49
|
+
value.length < MESSAGE_SHORT_LIMIT && "Very short messages read as a fragment, and bulk senders send exactly this shape.",
|
|
50
|
+
shouting(value) && "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
51
|
+
/[!?]{2,}/.test(value) && "Repeated exclamation or question marks are a common spam signal.",
|
|
52
|
+
found.length > 0 && "Reads like a scam to filters and to people: " + list(found) + "."
|
|
53
|
+
].filter(Boolean);
|
|
54
|
+
};
|
|
55
|
+
var notificationWarnings = ({
|
|
56
|
+
brand,
|
|
57
|
+
channel = "email",
|
|
58
|
+
message,
|
|
59
|
+
subject
|
|
60
|
+
} = {}) => {
|
|
61
|
+
if (channel !== "email") return { message: [], subject: [] };
|
|
62
|
+
return {
|
|
63
|
+
message: messageWarnings({ message }),
|
|
64
|
+
subject: subjectWarnings({ brand, subject })
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
export {
|
|
68
|
+
notificationWarnings
|
|
69
|
+
};
|
package/dist/plans.cjs
CHANGED
|
@@ -199,7 +199,6 @@ var organization = {
|
|
|
199
199
|
|
|
200
200
|
// index.js
|
|
201
201
|
var import_currency_codes = require("currency-codes");
|
|
202
|
-
var import_nanoid = require("nanoid");
|
|
203
202
|
|
|
204
203
|
// lib/color.js
|
|
205
204
|
var import_tinycolor2 = __toESM(require("tinycolor2"), 1);
|
|
@@ -262,8 +261,11 @@ var style = {
|
|
|
262
261
|
}
|
|
263
262
|
};
|
|
264
263
|
|
|
264
|
+
// lib/nanoid.js
|
|
265
|
+
var import_nanoid = require("nanoid");
|
|
266
|
+
var generate = (0, import_nanoid.customAlphabet)("0123456789abcdefghijklmnopqrstuvwxyz", 8);
|
|
267
|
+
|
|
265
268
|
// index.js
|
|
266
|
-
var nanoid = (0, import_nanoid.customAlphabet)("0123456789abcdefghijklmnopqrstuvwxyz", 8);
|
|
267
269
|
var infinite = 1e300;
|
|
268
270
|
var megabyte = 1024 * 1024;
|
|
269
271
|
var gigabyte = megabyte * 1024;
|
package/dist/plans.d.cts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { organization, page, connection, fields, field } from './features.cjs';
|
|
2
2
|
import { infinite, gigabyte } from './index.cjs';
|
|
3
3
|
import 'currency-codes';
|
|
4
|
-
import 'nanoid';
|
|
5
4
|
import './color.cjs';
|
|
6
5
|
import 'tinycolor2';
|
|
6
|
+
import './nanoid.cjs';
|
|
7
|
+
import 'nanoid';
|
|
7
8
|
import './usage.cjs';
|
|
8
9
|
|
|
9
10
|
const featuresFor = ( array = [] ) => Object.values({
|
package/dist/plans.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { organization, page, connection, fields, field } from './features.js';
|
|
2
2
|
import { infinite, gigabyte } from './index.js';
|
|
3
3
|
import 'currency-codes';
|
|
4
|
-
import 'nanoid';
|
|
5
4
|
import './color.js';
|
|
6
5
|
import 'tinycolor2';
|
|
6
|
+
import './nanoid.js';
|
|
7
|
+
import 'nanoid';
|
|
7
8
|
import './usage.js';
|
|
8
9
|
|
|
9
10
|
const featuresFor = ( array = [] ) => Object.values({
|
package/dist/plans.js
CHANGED
|
@@ -160,7 +160,6 @@ var organization = {
|
|
|
160
160
|
|
|
161
161
|
// index.js
|
|
162
162
|
import { code, data } from "currency-codes";
|
|
163
|
-
import { customAlphabet } from "nanoid";
|
|
164
163
|
|
|
165
164
|
// lib/color.js
|
|
166
165
|
import tinycolor from "tinycolor2";
|
|
@@ -223,8 +222,11 @@ var style = {
|
|
|
223
222
|
}
|
|
224
223
|
};
|
|
225
224
|
|
|
225
|
+
// lib/nanoid.js
|
|
226
|
+
import { customAlphabet } from "nanoid";
|
|
227
|
+
var generate = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 8);
|
|
228
|
+
|
|
226
229
|
// index.js
|
|
227
|
-
var nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 8);
|
|
228
230
|
var infinite = 1e300;
|
|
229
231
|
var megabyte = 1024 * 1024;
|
|
230
232
|
var gigabyte = megabyte * 1024;
|
package/dist/pricing.cjs
CHANGED
|
@@ -490,7 +490,6 @@ var organization = {
|
|
|
490
490
|
|
|
491
491
|
// index.js
|
|
492
492
|
var import_currency_codes = require("currency-codes");
|
|
493
|
-
var import_nanoid = require("nanoid");
|
|
494
493
|
|
|
495
494
|
// lib/color.js
|
|
496
495
|
var import_tinycolor2 = __toESM(require("tinycolor2"), 1);
|
|
@@ -553,8 +552,11 @@ var style = {
|
|
|
553
552
|
}
|
|
554
553
|
};
|
|
555
554
|
|
|
555
|
+
// lib/nanoid.js
|
|
556
|
+
var import_nanoid = require("nanoid");
|
|
557
|
+
var generate = (0, import_nanoid.customAlphabet)("0123456789abcdefghijklmnopqrstuvwxyz", 8);
|
|
558
|
+
|
|
556
559
|
// index.js
|
|
557
|
-
var nanoid = (0, import_nanoid.customAlphabet)("0123456789abcdefghijklmnopqrstuvwxyz", 8);
|
|
558
560
|
var infinite = 1e300;
|
|
559
561
|
var megabyte = 1024 * 1024;
|
|
560
562
|
var gigabyte = megabyte * 1024;
|
package/dist/pricing.d.cts
CHANGED
|
@@ -7,9 +7,10 @@ import './usage.cjs';
|
|
|
7
7
|
import './features.cjs';
|
|
8
8
|
import './index.cjs';
|
|
9
9
|
import 'currency-codes';
|
|
10
|
-
import 'nanoid';
|
|
11
10
|
import './color.cjs';
|
|
12
11
|
import 'tinycolor2';
|
|
12
|
+
import './nanoid.cjs';
|
|
13
|
+
import 'nanoid';
|
|
13
14
|
|
|
14
15
|
// Pricing — the one file you open to see every customer-facing number.
|
|
15
16
|
//
|
package/dist/pricing.d.ts
CHANGED
|
@@ -7,9 +7,10 @@ import './usage.js';
|
|
|
7
7
|
import './features.js';
|
|
8
8
|
import './index.js';
|
|
9
9
|
import 'currency-codes';
|
|
10
|
-
import 'nanoid';
|
|
11
10
|
import './color.js';
|
|
12
11
|
import 'tinycolor2';
|
|
12
|
+
import './nanoid.js';
|
|
13
|
+
import 'nanoid';
|
|
13
14
|
|
|
14
15
|
// Pricing — the one file you open to see every customer-facing number.
|
|
15
16
|
//
|
package/dist/pricing.js
CHANGED
|
@@ -438,7 +438,6 @@ var organization = {
|
|
|
438
438
|
|
|
439
439
|
// index.js
|
|
440
440
|
import { code, data } from "currency-codes";
|
|
441
|
-
import { customAlphabet } from "nanoid";
|
|
442
441
|
|
|
443
442
|
// lib/color.js
|
|
444
443
|
import tinycolor from "tinycolor2";
|
|
@@ -501,8 +500,11 @@ var style = {
|
|
|
501
500
|
}
|
|
502
501
|
};
|
|
503
502
|
|
|
503
|
+
// lib/nanoid.js
|
|
504
|
+
import { customAlphabet } from "nanoid";
|
|
505
|
+
var generate = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 8);
|
|
506
|
+
|
|
504
507
|
// index.js
|
|
505
|
-
var nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 8);
|
|
506
508
|
var infinite = 1e300;
|
|
507
509
|
var megabyte = 1024 * 1024;
|
|
508
510
|
var gigabyte = megabyte * 1024;
|