@drawbridge/drawbridge-utils 0.0.158 → 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/nanoid.d.cts CHANGED
@@ -11,7 +11,47 @@ import { customAlphabet } from 'nanoid';
11
11
  // New code should prefer this subpath, which also exposes `retry`:
12
12
  // const { nanoid, retry } = require( '@drawbridge/drawbridge-utils/nanoid' );
13
13
 
14
- const nanoid = customAlphabet( '0123456789abcdefghijklmnopqrstuvwxyz', 8 );
14
+ const generate = customAlphabet( '0123456789abcdefghijklmnopqrstuvwxyz', 8 );
15
+
16
+ // SpamAssassin's FROM_LOCAL_NOVOWEL scores any From address whose LOCAL PART
17
+ // carries seven consecutive letters from this class:
18
+ //
19
+ // header FROM_LOCAL_NOVOWEL From =~ /[bcdfgjklmnpqrstvwxz]{7}\S*\@/i
20
+ //
21
+ // organization.shortId becomes exactly that: lead mail sends from
22
+ // org+<shortId>@<sending domain> (lib/email.js subAddress). Nineteen of the
23
+ // thirty-six characters in the alphabet above are in the class, so a run of
24
+ // seven inside eight characters is not rare — measured at 1.67% of generated
25
+ // ids, which is roughly one organization in sixty penalised on every lead email
26
+ // it will ever send, decided by nothing but the draw.
27
+ //
28
+ // Rejecting those costs a re-roll on 1.7% of calls and a slice of the keyspace
29
+ // far too small to matter against 36⁸. Applied to EVERY nanoid rather than only
30
+ // the organization's: the rest become slugs in urls where the rule is
31
+ // irrelevant and the cost is nil, and doing it here means no future caller can
32
+ // put an id in an address and quietly reintroduce this.
33
+ //
34
+ // Existing ids are untouched — this only shapes new ones.
35
+ const NOVOWEL = /[bcdfgjklmnpqrstvwxz]{7}/i;
36
+
37
+ // Bounded rather than looping until clean. At a 1.67% rejection rate the odds
38
+ // of ten consecutive rejections are about 1 in 10^18, so the cap is never
39
+ // reached — but a generator that CANNOT hang is worth more than one that is
40
+ // merely very unlikely to, and returning a slightly penalised id beats spinning.
41
+ const ATTEMPTS = 10;
42
+
43
+ const nanoid = () => {
44
+
45
+ let value = generate();
46
+
47
+ for( let attempt = 1; attempt < ATTEMPTS && NOVOWEL.test( value ); attempt++ ){
48
+
49
+ value = generate();
50
+
51
+ }
52
+ return value;
53
+
54
+ };
15
55
 
16
56
  // Retry an async operation when Mongo rejects an insert because of a
17
57
  // duplicate-key error on a specific field. The operation receives a fresh
package/dist/nanoid.d.ts CHANGED
@@ -11,7 +11,47 @@ import { customAlphabet } from 'nanoid';
11
11
  // New code should prefer this subpath, which also exposes `retry`:
12
12
  // const { nanoid, retry } = require( '@drawbridge/drawbridge-utils/nanoid' );
13
13
 
14
- const nanoid = customAlphabet( '0123456789abcdefghijklmnopqrstuvwxyz', 8 );
14
+ const generate = customAlphabet( '0123456789abcdefghijklmnopqrstuvwxyz', 8 );
15
+
16
+ // SpamAssassin's FROM_LOCAL_NOVOWEL scores any From address whose LOCAL PART
17
+ // carries seven consecutive letters from this class:
18
+ //
19
+ // header FROM_LOCAL_NOVOWEL From =~ /[bcdfgjklmnpqrstvwxz]{7}\S*\@/i
20
+ //
21
+ // organization.shortId becomes exactly that: lead mail sends from
22
+ // org+<shortId>@<sending domain> (lib/email.js subAddress). Nineteen of the
23
+ // thirty-six characters in the alphabet above are in the class, so a run of
24
+ // seven inside eight characters is not rare — measured at 1.67% of generated
25
+ // ids, which is roughly one organization in sixty penalised on every lead email
26
+ // it will ever send, decided by nothing but the draw.
27
+ //
28
+ // Rejecting those costs a re-roll on 1.7% of calls and a slice of the keyspace
29
+ // far too small to matter against 36⁸. Applied to EVERY nanoid rather than only
30
+ // the organization's: the rest become slugs in urls where the rule is
31
+ // irrelevant and the cost is nil, and doing it here means no future caller can
32
+ // put an id in an address and quietly reintroduce this.
33
+ //
34
+ // Existing ids are untouched — this only shapes new ones.
35
+ const NOVOWEL = /[bcdfgjklmnpqrstvwxz]{7}/i;
36
+
37
+ // Bounded rather than looping until clean. At a 1.67% rejection rate the odds
38
+ // of ten consecutive rejections are about 1 in 10^18, so the cap is never
39
+ // reached — but a generator that CANNOT hang is worth more than one that is
40
+ // merely very unlikely to, and returning a slightly penalised id beats spinning.
41
+ const ATTEMPTS = 10;
42
+
43
+ const nanoid = () => {
44
+
45
+ let value = generate();
46
+
47
+ for( let attempt = 1; attempt < ATTEMPTS && NOVOWEL.test( value ); attempt++ ){
48
+
49
+ value = generate();
50
+
51
+ }
52
+ return value;
53
+
54
+ };
15
55
 
16
56
  // Retry an async operation when Mongo rejects an insert because of a
17
57
  // duplicate-key error on a specific field. The operation receives a fresh
package/dist/nanoid.js CHANGED
@@ -1,6 +1,16 @@
1
1
  // lib/nanoid.js
2
2
  import { customAlphabet } from "nanoid";
3
- var nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 8);
3
+ var generate = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 8);
4
+ var NOVOWEL = /[bcdfgjklmnpqrstvwxz]{7}/i;
5
+ var ATTEMPTS = 10;
6
+ var nanoid = () => {
7
+ let value = generate();
8
+ for (let attempt = 1; attempt < ATTEMPTS && NOVOWEL.test(value); attempt++) {
9
+ value = generate();
10
+ }
11
+ ;
12
+ return value;
13
+ };
4
14
  var DEFAULT_MAX_RETRIES = 5;
5
15
  var retry = async ({
6
16
  field,
@@ -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;