mail-time 3.0.0 → 4.1.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/LICENSE +1 -1
- package/README.md +381 -607
- package/adapters/mongo.d.ts +118 -0
- package/adapters/mongo.js +204 -80
- package/adapters/postgres.d.ts +100 -0
- package/adapters/postgres.js +536 -0
- package/adapters/redis.d.ts +105 -0
- package/adapters/redis.js +342 -79
- package/helpers.js +286 -3
- package/index.cjs +2395 -457
- package/index.d.cts +322 -0
- package/index.d.ts +322 -0
- package/index.js +886 -303
- package/package.json +78 -17
- package/presets.d.ts +87 -0
- package/presets.js +193 -0
- package/adapters/blank-example.js +0 -230
package/helpers.js
CHANGED
|
@@ -1,11 +1,294 @@
|
|
|
1
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @name hasOwnProp - `Object.hasOwn` (ES2022) polyfill.
|
|
5
|
+
* @function
|
|
6
|
+
* @param {object} obj
|
|
7
|
+
* @param {PropertyKey} key
|
|
8
|
+
* @returns {boolean} `true` if the object has the property, `false` otherwise.
|
|
9
|
+
*/
|
|
10
|
+
const hasOwnProp = (obj, key) => hasOwn.call(obj, key);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @name debug - Debug logging.
|
|
14
|
+
* @function
|
|
15
|
+
* @param {boolean} isDebug
|
|
16
|
+
* @param {...any} args
|
|
17
|
+
* @returns {void}
|
|
18
|
+
*/
|
|
1
19
|
const debug = (isDebug, ...args) => {
|
|
2
20
|
if (isDebug) {
|
|
3
|
-
console.info
|
|
21
|
+
console.info('[DEBUG] [mail-time]', `${new Date()}`, ...args);
|
|
4
22
|
}
|
|
5
23
|
};
|
|
6
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @name logError - Error logging.
|
|
27
|
+
* @function
|
|
28
|
+
* @param {...any} args
|
|
29
|
+
* @returns {void}
|
|
30
|
+
*/
|
|
7
31
|
const logError = (...args) => {
|
|
8
|
-
console.error
|
|
32
|
+
console.error('[ERROR] [mail-time]', `${new Date()}`, ...args);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @name isPlainObject - Check whether a value is a plain object (literal or `Object.create(null)`).
|
|
37
|
+
* @function
|
|
38
|
+
* @param {any} value
|
|
39
|
+
* @returns {boolean} `true` for plain objects, `false` for `null`, arrays, class instances, and primitives.
|
|
40
|
+
*/
|
|
41
|
+
const isPlainObject = (value) => {
|
|
42
|
+
if (value === null || typeof value !== 'object') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const proto = Object.getPrototypeOf(value);
|
|
46
|
+
return proto === null || proto === Object.prototype;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @name deepMerge - Minimal deep-merge sufficient for nodemailer-shaped mail options:
|
|
51
|
+
* - plain objects merge key-by-key
|
|
52
|
+
* - arrays concatenate
|
|
53
|
+
* - other values (strings, numbers, Date, Buffer, streams, classes) replace
|
|
54
|
+
* Source values override target values.
|
|
55
|
+
* @function
|
|
56
|
+
* @param {any} target - Base value; merged into a shallow clone when a plain object, otherwise ignored.
|
|
57
|
+
* @param {any} source - Overriding value; returns `target` unchanged when not a plain object.
|
|
58
|
+
* @returns {any} The merged result (a new object), or `target` when `source` is not a plain object.
|
|
59
|
+
*/
|
|
60
|
+
const deepMerge = (target, source) => {
|
|
61
|
+
if (!isPlainObject(source)) {
|
|
62
|
+
return target;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const out = isPlainObject(target) ? { ...target } : {};
|
|
66
|
+
|
|
67
|
+
for (const key of Object.keys(source)) {
|
|
68
|
+
const sVal = source[key];
|
|
69
|
+
const tVal = out[key];
|
|
70
|
+
|
|
71
|
+
if (Array.isArray(sVal)) {
|
|
72
|
+
out[key] = Array.isArray(tVal) ? tVal.concat(sVal) : sVal.slice();
|
|
73
|
+
} else if (isPlainObject(sVal)) {
|
|
74
|
+
out[key] = isPlainObject(tVal) ? deepMerge(tVal, sVal) : deepMerge({}, sVal);
|
|
75
|
+
} else {
|
|
76
|
+
out[key] = sVal;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return out;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @name equals - Order-insensitive deep equality. Treats arrays as multisets and
|
|
85
|
+
* objects as unordered maps. Designed for the small `mailOptions`
|
|
86
|
+
* shape used by MailTime's email concatenation dedup.
|
|
87
|
+
* @function
|
|
88
|
+
* @param {any} a
|
|
89
|
+
* @param {any} b
|
|
90
|
+
* @returns {boolean} `true` when `a` and `b` are deeply equal ignoring array/key order.
|
|
91
|
+
*/
|
|
92
|
+
const equals = (a, b) => {
|
|
93
|
+
if (a === b) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!a || !b || typeof a !== 'object' || typeof b !== 'object') {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (a instanceof Date && b instanceof Date) {
|
|
102
|
+
return a.valueOf() === b.valueOf();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(a)) {
|
|
106
|
+
if (!Array.isArray(b) || a.length !== b.length) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const matched = new Array(b.length).fill(false);
|
|
111
|
+
for (let i = 0; i < a.length; i++) {
|
|
112
|
+
let found = false;
|
|
113
|
+
for (let j = 0; j < b.length; j++) {
|
|
114
|
+
if (!matched[j] && equals(a[i], b[j])) {
|
|
115
|
+
matched[j] = true;
|
|
116
|
+
found = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!found) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (Array.isArray(b)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const aKeys = Object.keys(a);
|
|
132
|
+
const bKeys = Object.keys(b);
|
|
133
|
+
|
|
134
|
+
if (aKeys.length !== bKeys.length) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
139
|
+
const key = aKeys[i];
|
|
140
|
+
if (!hasOwn.call(b, key) || !equals(a[key], b[key])) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return true;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @name extractEmail - Extract the email part of a nodemailer-shaped recipient entry.
|
|
150
|
+
* Accepts `'a@x.com'`, `'Name <a@x.com>'`, or `{ name, address }`.
|
|
151
|
+
* @function
|
|
152
|
+
* @param {string|{name?: string, address?: string}|null|undefined} entry
|
|
153
|
+
* @returns {string|null} The address lowercased and trimmed, or `null` when none can be parsed.
|
|
154
|
+
*/
|
|
155
|
+
const extractEmail = (entry) => {
|
|
156
|
+
if (!entry) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (typeof entry === 'object' && typeof entry.address === 'string') {
|
|
160
|
+
return entry.address.trim().toLowerCase();
|
|
161
|
+
}
|
|
162
|
+
if (typeof entry !== 'string') {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const angled = entry.match(/<([^>]+)>/);
|
|
166
|
+
return (angled ? angled[1] : entry).trim().toLowerCase();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @name toAddressList - Normalize a `to`/`cc`/`bcc` field into a flat list of lowercase addresses.
|
|
171
|
+
* @function
|
|
172
|
+
* @param {string|Array<string|{name?: string, address?: string}>|null|undefined} field
|
|
173
|
+
* @returns {string[]} Flat list of parsed lowercase addresses; empty when `field` is falsy or unparseable.
|
|
174
|
+
*/
|
|
175
|
+
const toAddressList = (field) => {
|
|
176
|
+
if (!field) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
if (Array.isArray(field)) {
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const entry of field) {
|
|
182
|
+
const addr = extractEmail(entry);
|
|
183
|
+
if (addr) {
|
|
184
|
+
out.push(addr);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
const single = extractEmail(field);
|
|
190
|
+
return single ? [single] : [];
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @name filterAddressField - Remove entries whose extracted address is in `acceptedSet` from a
|
|
195
|
+
* nodemailer `to`/`cc`/`bcc` field.
|
|
196
|
+
* @function
|
|
197
|
+
* @param {string|Array<string|{name?: string, address?: string}>|null|undefined} field
|
|
198
|
+
* @param {Set<string>} acceptedSet - Lowercase addresses to drop.
|
|
199
|
+
* @returns {string|Array|undefined} The filtered field, or `void 0` when the filtered array would be
|
|
200
|
+
* empty or the single string is dropped. Returns `field` unchanged when `acceptedSet` is empty.
|
|
201
|
+
*/
|
|
202
|
+
const filterAddressField = (field, acceptedSet) => {
|
|
203
|
+
if (!field || !(acceptedSet instanceof Set) || acceptedSet.size === 0) {
|
|
204
|
+
return field;
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(field)) {
|
|
207
|
+
const filtered = [];
|
|
208
|
+
for (const entry of field) {
|
|
209
|
+
const addr = extractEmail(entry);
|
|
210
|
+
if (!addr || !acceptedSet.has(addr)) {
|
|
211
|
+
filtered.push(entry);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return filtered.length ? filtered : void 0;
|
|
215
|
+
}
|
|
216
|
+
const addr = extractEmail(field);
|
|
217
|
+
if (addr && acceptedSet.has(addr)) {
|
|
218
|
+
return void 0;
|
|
219
|
+
}
|
|
220
|
+
return field;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @name isSendClaimUpdate - Detect an atomic send-claim update (`{ isSending: true, tries: N }`).
|
|
225
|
+
* @function
|
|
226
|
+
* @param {object} updateObj
|
|
227
|
+
* @returns {boolean} `true` when `updateObj` claims a row for sending.
|
|
228
|
+
*/
|
|
229
|
+
const isSendClaimUpdate = (updateObj) => {
|
|
230
|
+
return updateObj && updateObj.isSending === true && typeof updateObj.tries === 'number';
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @name isSendLeaseGuardedUpdate - Detect an update guarded by a send lease
|
|
235
|
+
* (carries `leaseTries` and `leaseSendingAt`).
|
|
236
|
+
* @function
|
|
237
|
+
* @param {object} updateObj
|
|
238
|
+
* @returns {boolean} `true` when both lease guard fields are present numbers.
|
|
239
|
+
*/
|
|
240
|
+
const isSendLeaseGuardedUpdate = (updateObj) => {
|
|
241
|
+
return updateObj && typeof updateObj.leaseTries === 'number' && typeof updateObj.leaseSendingAt === 'number';
|
|
9
242
|
};
|
|
10
243
|
|
|
11
|
-
|
|
244
|
+
/**
|
|
245
|
+
* @name isAppendMailOptionUpdate - Detect an update that appends a mail option (for email concatenation).
|
|
246
|
+
* @function
|
|
247
|
+
* @param {object} updateObj
|
|
248
|
+
* @returns {boolean} `true` when `updateObj.appendMailOption` is set.
|
|
249
|
+
*/
|
|
250
|
+
const isAppendMailOptionUpdate = (updateObj) => {
|
|
251
|
+
return updateObj && updateObj.appendMailOption !== void 0;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @name stripInternalUpdateMeta - Strip MailTime-internal update keys before persisting to storage.
|
|
256
|
+
* @function
|
|
257
|
+
* @param {object} updateObj
|
|
258
|
+
* @returns {object} A shallow clone without `leaseTries`, `leaseSendingAt`, and `appendMailOption`.
|
|
259
|
+
*/
|
|
260
|
+
const stripInternalUpdateMeta = (updateObj) => {
|
|
261
|
+
const out = { ...updateObj };
|
|
262
|
+
delete out.leaseTries;
|
|
263
|
+
delete out.leaseSendingAt;
|
|
264
|
+
delete out.appendMailOption;
|
|
265
|
+
return out;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @name isSendLeaseRemove - Detect a remove guarded by a send lease
|
|
270
|
+
* (carries `leaseTries` and `leaseSendingAt`).
|
|
271
|
+
* @function
|
|
272
|
+
* @param {object} opts
|
|
273
|
+
* @returns {boolean} `true` when both lease guard fields are present numbers.
|
|
274
|
+
*/
|
|
275
|
+
const isSendLeaseRemove = (opts) => {
|
|
276
|
+
return opts && typeof opts.leaseTries === 'number' && typeof opts.leaseSendingAt === 'number';
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export {
|
|
280
|
+
debug,
|
|
281
|
+
logError,
|
|
282
|
+
hasOwnProp,
|
|
283
|
+
isPlainObject,
|
|
284
|
+
deepMerge,
|
|
285
|
+
equals,
|
|
286
|
+
extractEmail,
|
|
287
|
+
toAddressList,
|
|
288
|
+
filterAddressField,
|
|
289
|
+
isSendClaimUpdate,
|
|
290
|
+
isSendLeaseGuardedUpdate,
|
|
291
|
+
isAppendMailOptionUpdate,
|
|
292
|
+
stripInternalUpdateMeta,
|
|
293
|
+
isSendLeaseRemove,
|
|
294
|
+
};
|