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/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.call(console, '[DEBUG] [mail-time]', `${new Date}`, ...args);
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.call(console, '[ERROR] [mail-time]', `${new Date}`, ...args);
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
- export { debug, logError };
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
+ };