mail-time 1.4.0 → 3.0.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.
@@ -0,0 +1,230 @@
1
+ import { logError } from '../helpers.js';
2
+
3
+ /** Class representing Example Queue for MailTime */
4
+ class BlankQueue {
5
+ /**
6
+ * Create a BlankQueue instance
7
+ * @param {object} opts - configuration object
8
+ * @param {object} opts.requiredOption - Required option description
9
+ * @param {string} [opts.prefix] - Optional prefix for scope isolation; use when creating multiple MailTime instances within the single application
10
+ */
11
+ constructor (opts) {
12
+ this.name = 'queue-name';
13
+ if (!opts || typeof opts !== 'object' || opts === null) {
14
+ throw new TypeError('[mail-time] Configuration object must be passed into BlankQueue constructor');
15
+ }
16
+
17
+ if (!opts.requiredOption) {
18
+ throw new Error('[mail-time] [BlankQueue] required {requiredOption} option is missing');
19
+ }
20
+
21
+ this.prefix = (typeof opts.prefix === 'string') ? opts.prefix : 'default';
22
+ this.uniqueName = `mailtime:${this.prefix}`;
23
+ this.requiredOption = opts.requiredOption;
24
+
25
+ // Stored JSON structure:
26
+ // uuid {string}
27
+ // to {string|[string]}
28
+ // tries {number} - qty of send attempts
29
+ // sendAt {number} - When letter should be sent
30
+ // isSent {boolean} - Email status
31
+ // isCancelled {boolean} - `true` if email was cancelled before it was sent
32
+ // isFailed {boolean} - `true` if email has failed to send
33
+ // template {string} - Template for this email
34
+ // transport {number} - Last used transport
35
+ // concatSubject {string|boolean} - Email concatenation subject
36
+ // ---
37
+ // mailOptions {[object]} - Array of nodeMailer's `mailOptions`
38
+ // mailOptions.to {string|[string]} - [REQUIRED]
39
+ // mailOptions.from {string}
40
+ // mailOptions.text {string|boolean}
41
+ // mailOptions.html {string}
42
+ // mailOptions.subject {string}
43
+ // mailOptions.Other nodeMailer `sendMail` options...
44
+ }
45
+
46
+ /**
47
+ * @async
48
+ * @memberOf BlankQueue
49
+ * @name ping
50
+ * @description Check connection to Storage
51
+ * @returns {Promise<object>}
52
+ */
53
+ async ping() {
54
+ // LEAVE THIS if BLOCK UNCHANGED
55
+ if (!this.mailTimeInstance) {
56
+ return {
57
+ status: 'Service Unavailable',
58
+ code: 503,
59
+ statusCode: 503,
60
+ error: new Error('MailTime instance not yet assigned to {mailTimeInstance} of Queue Adapter context'),
61
+ };
62
+ }
63
+
64
+ try {
65
+ const ping = await this.requiredOption.ping();
66
+ if (ping === 'PONG') {
67
+ return {
68
+ status: 'OK',
69
+ code: 200,
70
+ statusCode: 200,
71
+ };
72
+ }
73
+
74
+ throw new Error(`Unexpected response from Storage#ping received: ${ping}`);
75
+ } catch (pingError) {
76
+ return {
77
+ status: 'Internal Server Error',
78
+ code: 500,
79
+ statusCode: 500,
80
+ error: pingError
81
+ };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * @memberOf BlankQueue
87
+ * @name iterate
88
+ * @description iterate over queued emails passing to `mailTimeInstance.___send` method
89
+ * @returns {void 0}
90
+ */
91
+ async iterate() {
92
+ // GET EMAILS WITHIN this.uniqueName SCOPE!
93
+ // AND ONLY WHERE sendAt <= now
94
+ // RUN ONE BY ONE VIA this.mailTimeInstance.___send() METHOD
95
+ const cursorWithEmails = this.requiredOption.getEmails({
96
+ scope: this.uniqueName,
97
+ isSent: false,
98
+ isFailed: false,
99
+ isCancelled: false,
100
+ sendAt: {
101
+ $lte: Date.now() // Number (timestamp)
102
+ }
103
+ });
104
+
105
+ for await (const emailObj of cursorWithEmails) {
106
+ await this.mailTimeInstance.___send(emailObj);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * @async
112
+ * @memberOf BlankQueue
113
+ * @name getPendingTo
114
+ * @description get queued email by `to` field (addressee)
115
+ * @param to {string} - email address
116
+ * @param sendAt {number} - timestamp
117
+ * @returns {Promise<object|null>}
118
+ */
119
+ async getPendingTo(to, sendAt) {
120
+ if (typeof to !== 'string' || typeof sendAt !== 'number') {
121
+ return null;
122
+ }
123
+
124
+ const email = await this.requiredOption.get({
125
+ to: to,
126
+ isSent: false,
127
+ isFailed: false,
128
+ isCancelled: false,
129
+ sendAt: {
130
+ $lte: sendAt // Number (timestamp)
131
+ }
132
+ });
133
+
134
+ if (!email || email.isSent === true || email.isCancelled === true || email.isFailed === true) {
135
+ return null;
136
+ }
137
+
138
+ return email;
139
+ }
140
+
141
+ /**
142
+ * @async
143
+ * @memberOf BlankQueue
144
+ * @name push
145
+ * @description push email to the queue/storage
146
+ * @param email {object} - email's object
147
+ * @returns {Promise<void 0>}
148
+ */
149
+ async push(email) {
150
+ if (typeof email !== 'object') {
151
+ return;
152
+ }
153
+
154
+ if (email.sendAt instanceof Date) {
155
+ email.sendAt = +email.sendAt;
156
+ }
157
+
158
+ await this.requiredOption.save(email);
159
+ }
160
+
161
+ /**
162
+ * @async
163
+ * @memberOf BlankQueue
164
+ * @name cancel
165
+ * @description cancel scheduled email
166
+ * @param uuid {string} - email's uuid
167
+ * @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
168
+ */
169
+ async cancel(uuid) {
170
+ if (typeof uuid !== 'string') {
171
+ return false;
172
+ }
173
+
174
+ const email = JSON.parse(await this.requiredOption.get({
175
+ uuid: uuid
176
+ }));
177
+
178
+ if (!email || email.isSent === true || email.isCancelled === true) {
179
+ return false;
180
+ }
181
+
182
+ if (!this.mailTimeInstance.keepHistory) {
183
+ return await this.remove(email);
184
+ }
185
+
186
+ return await this.update(email, {
187
+ isCancelled: true,
188
+ });
189
+ }
190
+
191
+ /**
192
+ * @async
193
+ * @memberOf BlankQueue
194
+ * @name remove
195
+ * @description remove email from queue
196
+ * @param email {object} - email's object
197
+ * @returns {Promise<boolean>} returns `true` if removed or `false` if not found
198
+ */
199
+ async remove(email) {
200
+ if (typeof email !== 'object' || typeof email.uuid !== 'string') {
201
+ return false;
202
+ }
203
+
204
+ return await this.requiredOption.remove({
205
+ uuid: email.uuid
206
+ });
207
+ }
208
+
209
+ /**
210
+ * @async
211
+ * @memberOf BlankQueue
212
+ * @name update
213
+ * @description remove email from queue
214
+ * @param email {object} - email's object
215
+ * @param updateObj {object} - fields with new values to update
216
+ * @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
217
+ */
218
+ async update(email, updateObj) {
219
+ if (typeof email !== 'object' || typeof email.uuid !== 'string' || typeof updateObj !== 'object') {
220
+ return false;
221
+ }
222
+
223
+ const updatedEmail = { ...email, ...updateObj };
224
+ return await this.requiredOption.update({
225
+ uuid: email.uuid
226
+ }, updatedEmail);
227
+ }
228
+ }
229
+
230
+ export { BlankQueue };
@@ -0,0 +1,310 @@
1
+ import { logError } from '../helpers.js';
2
+
3
+ /**
4
+ * Ensure (create) index on MongoDB collection, catch and log exception if thrown
5
+ * @function ensureIndex
6
+ * @param {Collection} collection - Mongo's driver Collection instance
7
+ * @param {object} keys - Field and value pairs where the field is the index key and the value describes the type of index for that field
8
+ * @param {object} opts - Set of options that controls the creation of the index
9
+ * @returns {void 0}
10
+ */
11
+ const ensureIndex = async (collection, keys, opts) => {
12
+ try {
13
+ await collection.createIndex(keys, opts);
14
+ } catch (e) {
15
+ if (e.code === 85) {
16
+ let indexName;
17
+ const indexes = await collection.indexes();
18
+ for (const index of indexes) {
19
+ let drop = true;
20
+ for (const indexKey of Object.keys(keys)) {
21
+ if (typeof index.key[indexKey] === 'undefined') {
22
+ drop = false;
23
+ break;
24
+ }
25
+ }
26
+
27
+ for (const indexKey of Object.keys(index.key)) {
28
+ if (typeof keys[indexKey] === 'undefined') {
29
+ drop = false;
30
+ break;
31
+ }
32
+ }
33
+
34
+ if (drop) {
35
+ indexName = index.name;
36
+ break;
37
+ }
38
+ }
39
+
40
+ if (indexName) {
41
+ await collection.dropIndex(indexName);
42
+ await collection.createIndex(keys, opts);
43
+ }
44
+ } else {
45
+ logError(`[ensureIndex] Can not set ${Object.keys(keys).join(' + ')} index on "${collection._name || 'MongoDB'}" collection`, { keys, opts, details: e });
46
+ }
47
+ }
48
+ };
49
+
50
+ /** Class representing MongoDB Queue for MailTime */
51
+ class MongoQueue {
52
+ /**
53
+ * Create a MongoQueue instance
54
+ * @param {object} opts - configuration object
55
+ * @param {Db} opts.db - Required, Mongo's `Db` instance, like one returned from `MongoClient#db()` method
56
+ * @param {string} [opts.prefix] - Optional prefix for scope isolation; use when creating multiple MailTime instances within the single application
57
+ */
58
+ constructor (opts) {
59
+ this.name = 'mongo-queue';
60
+ if (!opts || typeof opts !== 'object' || opts === null) {
61
+ throw new TypeError('[mail-time] Configuration object must be passed into MongoQueue constructor');
62
+ }
63
+
64
+ if (!opts.db) {
65
+ throw new Error('[mail-time] [MongoQueue] requires MongoDB database {db} option, like returned from `MongoClient.connect`');
66
+ }
67
+
68
+ this.prefix = (typeof opts.prefix === 'string') ? opts.prefix : '';
69
+ this.db = opts.db;
70
+ this.collection = opts.db.collection(`__mailTimeQueue__${this.prefix}`);
71
+ ensureIndex(this.collection, { uuid: 1 }, { background: false });
72
+ ensureIndex(this.collection, { isSent: 1, isFailed: 1, isCancelled: 1, to: 1, sendAt: 1 }, { background: false });
73
+ ensureIndex(this.collection, { isSent: 1, isFailed: 1, isCancelled: 1, sendAt: 1, tries: 1 }, { background: false });
74
+
75
+ // MongoDB Collection Schema:
76
+ // _id
77
+ // uuid {string}
78
+ // to {string|[string]}
79
+ // tries {number} - qty of send attempts
80
+ // sendAt {number} - When letter should be sent
81
+ // isSent {boolean} - Email status
82
+ // isCancelled {boolean} - `true` if email was cancelled before it was sent
83
+ // isFailed {boolean} - `true` if email has failed to send
84
+ // template {string} - Template for this email
85
+ // transport {number} - Last used transport
86
+ // concatSubject {string|boolean} - Email concatenation subject
87
+ // ---
88
+ // mailOptions {[object]} - Array of nodeMailer's `mailOptions`
89
+ // mailOptions.to {string|[string]} - [REQUIRED]
90
+ // mailOptions.from {string}
91
+ // mailOptions.text {string|boolean}
92
+ // mailOptions.html {string}
93
+ // mailOptions.subject {string}
94
+ // mailOptions.Other nodeMailer `sendMail` options...
95
+ }
96
+
97
+ /**
98
+ * @async
99
+ * @memberOf MongoQueue
100
+ * @name ping
101
+ * @description Check connection to Storage
102
+ * @returns {Promise<object>}
103
+ */
104
+ async ping() {
105
+ if (!this.mailTimeInstance) {
106
+ return {
107
+ status: 'Service Unavailable',
108
+ code: 503,
109
+ statusCode: 503,
110
+ error: new Error('MailTime instance not yet assigned to {mailTimeInstance} of Queue Adapter context'),
111
+ };
112
+ }
113
+
114
+ try {
115
+ const ping = await this.db.command({ ping: 1 });
116
+ if (ping?.ok === 1) {
117
+ return {
118
+ status: 'OK',
119
+ code: 200,
120
+ statusCode: 200,
121
+ };
122
+ }
123
+ } catch (pingError) {
124
+ return {
125
+ status: 'Internal Server Error',
126
+ code: 500,
127
+ statusCode: 500,
128
+ error: pingError
129
+ };
130
+ }
131
+
132
+ return {
133
+ status: 'Service Unavailable',
134
+ code: 503,
135
+ statusCode: 503,
136
+ error: new Error('Service Unavailable')
137
+ };
138
+ }
139
+
140
+ /**
141
+ * @memberOf MongoQueue
142
+ * @name iterate
143
+ * @description iterate over queued tasks passing to `mailTimeInstance.___send` method
144
+ * @returns {void 0}
145
+ */
146
+ async iterate() {
147
+ try {
148
+ const cursor = this.collection.find({
149
+ isSent: false,
150
+ isFailed: false,
151
+ isCancelled: false,
152
+ sendAt: {
153
+ $lte: Date.now()
154
+ },
155
+ tries: {
156
+ $lt: this.mailTimeInstance.maxTries
157
+ }
158
+ }, {
159
+ projection: {
160
+ _id: 1,
161
+ uuid: 1,
162
+ tries: 1,
163
+ template: 1,
164
+ transport: 1,
165
+ isSent: 1,
166
+ isFailed: 1,
167
+ isCancelled: 1,
168
+ mailOptions: 1,
169
+ concatSubject: 1,
170
+ }
171
+ });
172
+
173
+ while (await cursor.hasNext()) {
174
+ await this.mailTimeInstance.___send(await cursor.next());
175
+ }
176
+ await cursor.close();
177
+ } catch (iterateError) {
178
+ logError('[iterate] [while/await] [iterateError]', iterateError);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * @async
184
+ * @memberOf MongoQueue
185
+ * @name getPendingTo
186
+ * @description get queued task by `to` field (addressee)
187
+ * @param to {string} - email address
188
+ * @param sendAt {number} - timestamp
189
+ * @returns {Promise<object|null>}
190
+ */
191
+ async getPendingTo(to, sendAt) {
192
+ if (typeof to !== 'string' || typeof sendAt !== 'number') {
193
+ return null;
194
+ }
195
+
196
+ return await this.collection.findOne({
197
+ to: to,
198
+ isSent: false,
199
+ isFailed: false,
200
+ isCancelled: false,
201
+ sendAt: {
202
+ $lte: sendAt
203
+ }
204
+ }, {
205
+ projection: {
206
+ _id: 1,
207
+ to: 1,
208
+ uuid: 1,
209
+ tries: 1,
210
+ isSent: 1,
211
+ isFailed: 1,
212
+ isCancelled: 1,
213
+ mailOptions: 1,
214
+ }
215
+ });
216
+ }
217
+
218
+ /**
219
+ * @async
220
+ * @memberOf MongoQueue
221
+ * @name push
222
+ * @description push task to the queue/storage
223
+ * @param task {object} - task's object
224
+ * @returns {Promise<void 0>}
225
+ */
226
+ async push(task) {
227
+ if (typeof task !== 'object') {
228
+ return;
229
+ }
230
+
231
+ if (task.sendAt instanceof Date) {
232
+ task.sendAt = +task.sendAt;
233
+ }
234
+ await this.collection.insertOne(task);
235
+ }
236
+
237
+ /**
238
+ * @async
239
+ * @memberOf MongoQueue
240
+ * @name cancel
241
+ * @description cancel scheduled email
242
+ * @param uuid {string} - email's uuid
243
+ * @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
244
+ */
245
+ async cancel(uuid) {
246
+ if (typeof uuid !== 'string') {
247
+ return false;
248
+ }
249
+
250
+ const task = await this.collection.findOne({ uuid }, {
251
+ projection: {
252
+ _id: 1,
253
+ uuid: 1,
254
+ isSent: 1,
255
+ isCancelled: 1,
256
+ }
257
+ });
258
+
259
+ if (!task || task.isSent === true || task.isCancelled === true) {
260
+ return false;
261
+ }
262
+
263
+ if (!this.mailTimeInstance.keepHistory) {
264
+ return await this.remove(task);
265
+ }
266
+
267
+ return await this.update(task, {
268
+ isCancelled: true,
269
+ });
270
+ }
271
+
272
+ /**
273
+ * @async
274
+ * @memberOf MongoQueue
275
+ * @name remove
276
+ * @description remove task from queue
277
+ * @param task {object} - task's object
278
+ * @returns {Promise<boolean>} returns `true` if removed or `false` if not found
279
+ */
280
+ async remove(task) {
281
+ if (typeof task !== 'object') {
282
+ return false;
283
+ }
284
+
285
+ return (await this.collection.deleteOne({ _id: task._id }))?.deletedCount >= 1;
286
+ }
287
+
288
+ /**
289
+ * @async
290
+ * @memberOf MongoQueue
291
+ * @name update
292
+ * @description remove task from queue
293
+ * @param task {object} - task's object
294
+ * @param updateObj {object} - fields with new values to update
295
+ * @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
296
+ */
297
+ async update(task, updateObj) {
298
+ if (typeof task !== 'object' || typeof updateObj !== 'object') {
299
+ return false;
300
+ }
301
+
302
+ return (await this.collection.updateOne({
303
+ _id: task._id
304
+ }, {
305
+ $set: updateObj
306
+ }))?.modifiedCount >= 1;
307
+ }
308
+ }
309
+
310
+ export { MongoQueue };