imapflow 1.0.88 → 1.0.91

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # CHANGELOG
2
2
 
3
+ - 1.0.91 - 2022-05-02
4
+
5
+ - Do not throw if literal includes a null byte
6
+ - Use UTF8 path names by default
7
+ - Overrides '\*' range query with then EXISTIS value
8
+
9
+ - 1.0.90 - 2022-04-04
10
+
11
+ - Added new configuration option `maxIdleTime`
12
+
13
+ - 1.0.89 - 2022-04-04
14
+
15
+ - Added new event type `response` that emits all command responses
16
+
3
17
  - 1.0.88 - 2022-03-24
4
18
 
5
19
  - Fixed folder name match where not all non-ascii characters triggered UTF encoding
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { normalizePath, getStatusCode, getErrorText } = require('../tools.js');
3
+ const { encodePath, normalizePath, getStatusCode, getErrorText } = require('../tools.js');
4
4
 
5
5
  // Creates a new mailbox
6
6
  module.exports = async (connection, path) => {
@@ -16,7 +16,7 @@ module.exports = async (connection, path) => {
16
16
  let map = {
17
17
  path
18
18
  };
19
- response = await connection.exec('CREATE', [{ type: 'ATOM', value: path }]);
19
+ response = await connection.exec('CREATE', [{ type: 'ATOM', value: encodePath(connection, path) }]);
20
20
 
21
21
  let section =
22
22
  response.response.attributes &&
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { normalizePath, getStatusCode, getErrorText } = require('../tools.js');
3
+ const { encodePath, normalizePath, getStatusCode, getErrorText } = require('../tools.js');
4
4
 
5
5
  // Deletes an existing mailbox
6
6
  module.exports = async (connection, path) => {
@@ -20,7 +20,7 @@ module.exports = async (connection, path) => {
20
20
  let map = {
21
21
  path
22
22
  };
23
- response = await connection.exec('DELETE', [{ type: 'ATOM', value: path }]);
23
+ response = await connection.exec('DELETE', [{ type: 'ATOM', value: encodePath(connection, path) }]);
24
24
  response.next();
25
25
  return map;
26
26
  } catch (err) {
@@ -18,6 +18,8 @@ module.exports = async (connection, range, query, options) => {
18
18
  list: []
19
19
  };
20
20
 
21
+ const commandKey = 'BODY';
22
+
21
23
  let response;
22
24
  try {
23
25
  let attributes = [{ type: 'SEQUENCE', value: (range || '*').toString() }];
@@ -27,7 +29,7 @@ module.exports = async (connection, range, query, options) => {
27
29
  let setBodyPeek = (attributes, partial) => {
28
30
  let bodyPeek = {
29
31
  type: 'ATOM',
30
- value: 'BODY.PEEK',
32
+ value: `${commandKey}.PEEK`,
31
33
  section: [],
32
34
  partial
33
35
  };
@@ -61,7 +63,7 @@ module.exports = async (connection, range, query, options) => {
61
63
  partial.push(Number(query.source.maxLength));
62
64
  }
63
65
  }
64
- queryStructure.push({ type: 'ATOM', value: 'BODY.PEEK', section: [], partial });
66
+ queryStructure.push({ type: 'ATOM', value: `${commandKey}.PEEK`, section: [], partial });
65
67
  }
66
68
 
67
69
  // if possible, always request for unique email id
@@ -2,112 +2,139 @@
2
2
 
3
3
  const NOOP_INTERVAL = 2 * 60 * 1000;
4
4
 
5
- // Listes for changes in mailbox
6
- module.exports = async connection => {
7
- if (connection.state !== connection.states.SELECTED) {
8
- // nothing to do here
9
- return;
10
- }
5
+ async function runIdle(connection) {
6
+ let response;
11
7
 
12
- if (connection.capabilities.has('IDLE')) {
13
- let response;
14
-
15
- let preCheckWaitQueue = [];
16
- try {
17
- connection.idling = true;
18
-
19
- //let idleSent = false;
20
- let doneRequested = false;
21
- let doneSent = false;
22
- let canEnd = false;
23
-
24
- let preCheck = () => {
25
- doneRequested = true;
26
- if (canEnd && !doneSent) {
27
- connection.log.debug({
28
- src: 'c',
29
- msg: `DONE`,
30
- comment: `breaking IDLE`,
31
- lockId: connection.currentLockId,
32
- path: connection.mailbox && connection.mailbox.path
33
- });
34
- connection.write('DONE');
35
- doneSent = true;
36
-
37
- connection.idling = false;
38
- connection.preCheck = false; // unset itself
39
-
40
- while (preCheckWaitQueue.length) {
41
- let { resolve } = preCheckWaitQueue.shift();
42
- resolve();
43
- }
44
- }
45
- };
46
-
47
- connection.preCheck = () => {
48
- let handler = new Promise((resolve, reject) => {
49
- preCheckWaitQueue.push({ resolve, reject });
50
- });
8
+ let preCheckWaitQueue = [];
9
+ try {
10
+ connection.idling = true;
51
11
 
52
- connection.log.trace({
53
- msg: 'Requesting IDLE break',
12
+ //let idleSent = false;
13
+ let doneRequested = false;
14
+ let doneSent = false;
15
+ let canEnd = false;
16
+
17
+ let preCheck = () => {
18
+ doneRequested = true;
19
+ if (canEnd && !doneSent) {
20
+ connection.log.debug({
21
+ src: 'c',
22
+ msg: `DONE`,
23
+ comment: `breaking IDLE`,
54
24
  lockId: connection.currentLockId,
55
- path: connection.mailbox && connection.mailbox.path,
56
- queued: preCheckWaitQueue.length,
57
- doneRequested,
58
- canEnd,
59
- doneSent
25
+ path: connection.mailbox && connection.mailbox.path
60
26
  });
27
+ connection.write('DONE');
28
+ doneSent = true;
61
29
 
62
- preCheck();
63
-
64
- return handler;
65
- };
30
+ connection.idling = false;
31
+ connection.preCheck = false; // unset itself
66
32
 
67
- response = await connection.exec('IDLE', false, {
68
- onPlusTag: async () => {
69
- connection.log.debug({ msg: `Initiated IDLE, waiting for server input`, doneRequested });
70
- canEnd = true;
71
- if (doneRequested) {
72
- preCheck();
73
- }
74
- },
75
- onSend: () => {
76
- //idleSent = true;
77
- }
78
- });
79
-
80
- // unset before response.next()
81
- if (typeof connection.preCheck === 'function') {
82
- connection.log.trace({
83
- msg: 'Clearing pre-check function',
84
- lockId: connection.currentLockId,
85
- path: connection.mailbox && connection.mailbox.path,
86
- queued: preCheckWaitQueue.length,
87
- doneRequested,
88
- canEnd,
89
- doneSent
90
- });
91
- connection.preCheck = false;
92
33
  while (preCheckWaitQueue.length) {
93
34
  let { resolve } = preCheckWaitQueue.shift();
94
35
  resolve();
95
36
  }
96
37
  }
38
+ };
97
39
 
98
- response.next();
99
- return;
100
- } catch (err) {
101
- connection.preCheck = false;
102
- connection.idling = false;
40
+ connection.preCheck = () => {
41
+ let handler = new Promise((resolve, reject) => {
42
+ preCheckWaitQueue.push({ resolve, reject });
43
+ });
44
+
45
+ connection.log.trace({
46
+ msg: 'Requesting IDLE break',
47
+ lockId: connection.currentLockId,
48
+ path: connection.mailbox && connection.mailbox.path,
49
+ queued: preCheckWaitQueue.length,
50
+ doneRequested,
51
+ canEnd,
52
+ doneSent
53
+ });
103
54
 
104
- connection.log.warn({ err, cid: connection.id });
55
+ preCheck();
56
+
57
+ return handler;
58
+ };
59
+
60
+ response = await connection.exec('IDLE', false, {
61
+ onPlusTag: async () => {
62
+ connection.log.debug({ msg: `Initiated IDLE, waiting for server input`, doneRequested });
63
+ canEnd = true;
64
+ if (doneRequested) {
65
+ preCheck();
66
+ }
67
+ },
68
+ onSend: () => {
69
+ //idleSent = true;
70
+ }
71
+ });
72
+
73
+ // unset before response.next()
74
+ if (typeof connection.preCheck === 'function') {
75
+ connection.log.trace({
76
+ msg: 'Clearing pre-check function',
77
+ lockId: connection.currentLockId,
78
+ path: connection.mailbox && connection.mailbox.path,
79
+ queued: preCheckWaitQueue.length,
80
+ doneRequested,
81
+ canEnd,
82
+ doneSent
83
+ });
84
+ connection.preCheck = false;
105
85
  while (preCheckWaitQueue.length) {
106
- let { reject } = preCheckWaitQueue.shift();
107
- reject(err);
86
+ let { resolve } = preCheckWaitQueue.shift();
87
+ resolve();
108
88
  }
109
- return false;
110
89
  }
90
+
91
+ response.next();
92
+ return;
93
+ } catch (err) {
94
+ connection.preCheck = false;
95
+ connection.idling = false;
96
+
97
+ connection.log.warn({ err, cid: connection.id });
98
+ while (preCheckWaitQueue.length) {
99
+ let { reject } = preCheckWaitQueue.shift();
100
+ reject(err);
101
+ }
102
+ return false;
103
+ }
104
+ }
105
+
106
+ // Listes for changes in mailbox
107
+ module.exports = async (connection, maxIdleTime) => {
108
+ if (connection.state !== connection.states.SELECTED) {
109
+ // nothing to do here
110
+ return;
111
+ }
112
+
113
+ if (connection.capabilities.has('IDLE')) {
114
+ let idleTimer;
115
+ let stillIdling = false;
116
+ let runIdleLoop = async () => {
117
+ if (maxIdleTime) {
118
+ idleTimer = setTimeout(() => {
119
+ if (connection.idling) {
120
+ if (typeof connection.preCheck === 'function') {
121
+ stillIdling = true;
122
+ // request IDLE break if IDLE has been running for allowed time
123
+ connection.log.trace({ msg: 'Max allowed IDLE time reached', cid: connection.id });
124
+ connection.preCheck().catch(err => connection.log.warn({ err, cid: connection.id }));
125
+ }
126
+ }
127
+ }, maxIdleTime);
128
+ }
129
+ let resp = await runIdle(connection);
130
+ clearTimeout(idleTimer);
131
+ if (stillIdling) {
132
+ stillIdling = false;
133
+ return runIdleLoop();
134
+ }
135
+ return resp;
136
+ };
137
+ return runIdleLoop();
111
138
  }
112
139
 
113
140
  let idleTimer;
@@ -127,11 +154,13 @@ module.exports = async connection => {
127
154
  response.next();
128
155
  };
129
156
 
157
+ let noopInterval = maxIdleTime ? Math.min(NOOP_INTERVAL, maxIdleTime) : NOOP_INTERVAL;
158
+
130
159
  let runLoop = () => {
131
160
  idleCheck()
132
161
  .then(() => {
133
162
  clearTimeout(idleTimer);
134
- idleTimer = setTimeout(runLoop, NOOP_INTERVAL);
163
+ idleTimer = setTimeout(runLoop, noopInterval);
135
164
  })
136
165
  .catch(err => {
137
166
  clearTimeout(idleTimer);
@@ -1,8 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { normalizePath } = require('../tools.js');
3
+ const { decodePath, encodePath, normalizePath } = require('../tools.js');
4
4
  const { specialUse } = require('../special-use');
5
- const iconv = require('iconv-lite');
6
5
 
7
6
  // Lists mailboxes from server
8
7
  module.exports = async (connection, reference, mailbox) => {
@@ -14,7 +13,7 @@ module.exports = async (connection, reference, mailbox) => {
14
13
  let entries = [];
15
14
 
16
15
  let runList = async (reference, mailbox) => {
17
- response = await connection.exec(listCommand, [reference, mailbox], {
16
+ response = await connection.exec(listCommand, [encodePath(connection, reference), encodePath(connection, mailbox)], {
18
17
  untagged: {
19
18
  [listCommand]: async untagged => {
20
19
  if (!untagged.attributes || !untagged.attributes.length) {
@@ -22,7 +21,8 @@ module.exports = async (connection, reference, mailbox) => {
22
21
  }
23
22
 
24
23
  let entry = {
25
- path: (untagged.attributes[2] && untagged.attributes[2].value) || '',
24
+ path: normalizePath(connection, decodePath(connection, (untagged.attributes[2] && untagged.attributes[2].value) || '')),
25
+ pathAsListed: (untagged.attributes[2] && untagged.attributes[2].value) || '',
26
26
  flags: new Set(untagged.attributes[0].map(entry => entry.value)),
27
27
  delimiter: untagged.attributes[1] && untagged.attributes[1].value,
28
28
  listed: true
@@ -45,19 +45,8 @@ module.exports = async (connection, reference, mailbox) => {
45
45
  entry.path = entry.path.slice(1);
46
46
  }
47
47
 
48
- entry.parent = (entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path]).map(folder => {
49
- if (!connection.enabled.has('UTF8=ACCEPT')) {
50
- try {
51
- return iconv.decode(Buffer.from(folder), 'utf-7-imap').toString();
52
- } catch (err) {
53
- return folder; // keep as is
54
- }
55
- } else {
56
- return folder; // keep as is
57
- }
58
- });
59
-
60
- entry.path = normalizePath(connection, entry.path);
48
+ entry.parentPath = entry.delimiter && entry.path ? entry.path.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
49
+ entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
61
50
  entry.name = entry.parent.pop();
62
51
 
63
52
  let specialUseFlag = specialUse(connection.capabilities.has('XLIST') || connection.capabilities.has('SPECIAL-USE'), entry);
@@ -80,59 +69,54 @@ module.exports = async (connection, reference, mailbox) => {
80
69
  await runList('', 'INBOX');
81
70
  }
82
71
 
83
- response = await connection.exec('LSUB', [normalizePath(connection, reference || ''), normalizePath(connection, mailbox || '', true)], {
84
- untagged: {
85
- LSUB: async untagged => {
86
- if (!untagged.attributes || !untagged.attributes.length) {
87
- return;
88
- }
89
-
90
- let entry = {
91
- flags: new Set(untagged.attributes[0].map(entry => entry.value)),
92
- delimiter: untagged.attributes[1] && untagged.attributes[1].value,
93
- path: (untagged.attributes[2] && untagged.attributes[2].value) || '',
94
- subscribed: true
95
- };
72
+ response = await connection.exec(
73
+ 'LSUB',
74
+ [encodePath(connection, normalizePath(connection, reference || '')), encodePath(connection, normalizePath(connection, mailbox || '', true))],
75
+ {
76
+ untagged: {
77
+ LSUB: async untagged => {
78
+ if (!untagged.attributes || !untagged.attributes.length) {
79
+ return;
80
+ }
96
81
 
97
- if (entry.path.toUpperCase() === 'INBOX') {
98
- entry.specialUse = '\\Inbox';
99
- }
82
+ let entry = {
83
+ path: normalizePath(connection, decodePath(connection, (untagged.attributes[2] && untagged.attributes[2].value) || '')),
84
+ pathAsListed: (untagged.attributes[2] && untagged.attributes[2].value) || '',
85
+ flags: new Set(untagged.attributes[0].map(entry => entry.value)),
86
+ delimiter: untagged.attributes[1] && untagged.attributes[1].value,
87
+ subscribed: true
88
+ };
100
89
 
101
- if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
102
- entry.path = entry.path.slice(1);
103
- }
90
+ if (entry.path.toUpperCase() === 'INBOX') {
91
+ entry.specialUse = '\\Inbox';
92
+ }
104
93
 
105
- entry.parent = (entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path]).map(folder => {
106
- if (!connection.enabled.has('UTF8=ACCEPT')) {
107
- try {
108
- return iconv.decode(Buffer.from(folder), 'utf-7-imap').toString();
109
- } catch (err) {
110
- return folder; // keep as is
111
- }
112
- } else {
113
- return folder; // keep as is
94
+ if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
95
+ entry.path = entry.path.slice(1);
114
96
  }
115
- });
116
97
 
117
- entry.name = entry.parent.pop();
98
+ entry.parentPath = entry.delimiter && entry.path ? entry.path.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
99
+ entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
100
+ entry.name = entry.parent.pop();
118
101
 
119
- let existing = entries.find(existing => existing.path === entry.path);
120
- if (existing) {
121
- existing.subscribed = true;
122
- entry.flags.forEach(flag => existing.flags.add(flag));
123
- } else {
124
- // ignore non-listed folders
125
- /*
126
- let specialUseFlag = specialUse(connection.capabilities.has('XLIST') || connection.capabilities.has('SPECIAL-USE'), entry);
127
- if (specialUseFlag && !flagsSeen.has(specialUseFlag)) {
128
- entry.specialUse = specialUseFlag;
102
+ let existing = entries.find(existing => existing.path === entry.path);
103
+ if (existing) {
104
+ existing.subscribed = true;
105
+ entry.flags.forEach(flag => existing.flags.add(flag));
106
+ } else {
107
+ // ignore non-listed folders
108
+ /*
109
+ let specialUseFlag = specialUse(connection.capabilities.has('XLIST') || connection.capabilities.has('SPECIAL-USE'), entry);
110
+ if (specialUseFlag && !flagsSeen.has(specialUseFlag)) {
111
+ entry.specialUse = specialUseFlag;
112
+ }
113
+ entries.push(entry);
114
+ */
129
115
  }
130
- entries.push(entry);
131
- */
132
116
  }
133
117
  }
134
118
  }
135
- });
119
+ );
136
120
  response.next();
137
121
 
138
122
  let inboxEntry = entries.find(entry => entry.specialUse === '\\Inbox');
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { getStatusCode, normalizePath, getErrorText } = require('../tools.js');
3
+ const { encodePath, getStatusCode, normalizePath, getErrorText } = require('../tools.js');
4
4
 
5
5
  // Requests quota information for a mailbox
6
6
  module.exports = async (connection, path) => {
@@ -63,7 +63,7 @@ module.exports = async (connection, path) => {
63
63
  let quotaFound = false;
64
64
  let response;
65
65
  try {
66
- response = await connection.exec('GETQUOTAROOT', [{ type: 'ATOM', value: path }], {
66
+ response = await connection.exec('GETQUOTAROOT', [{ type: 'ATOM', value: encodePath(connection, path) }], {
67
67
  untagged: {
68
68
  QUOTAROOT: async untagged => {
69
69
  let quotaRoot =
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { normalizePath, getStatusCode, getErrorText } = require('../tools.js');
3
+ const { encodePath, normalizePath, getStatusCode, getErrorText } = require('../tools.js');
4
4
 
5
5
  // Renames existing mailbox
6
6
  module.exports = async (connection, path, newPath) => {
@@ -23,8 +23,8 @@ module.exports = async (connection, path, newPath) => {
23
23
  newPath
24
24
  };
25
25
  response = await connection.exec('RENAME', [
26
- { type: 'ATOM', value: path },
27
- { type: 'ATOM', value: newPath }
26
+ { type: 'ATOM', value: encodePath(connection, path) },
27
+ { type: 'ATOM', value: encodePath(connection, newPath) }
28
28
  ]);
29
29
  response.next();
30
30
  return map;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { normalizePath, getStatusCode, getErrorText } = require('../tools.js');
3
+ const { encodePath, normalizePath, getStatusCode, getErrorText } = require('../tools.js');
4
4
 
5
5
  // Selects a mailbox
6
6
  module.exports = async (connection, path, options) => {
@@ -47,101 +47,105 @@ module.exports = async (connection, path, options) => {
47
47
  map.qresync = true;
48
48
  }
49
49
 
50
- response = await connection.exec(!options.readOnly ? 'SELECT' : 'EXAMINE', [{ type: 'ATOM', value: path }].concat(extraArgs || []), {
51
- untagged: {
52
- OK: async untagged => {
53
- if (!untagged.attributes || !untagged.attributes.length) {
54
- return;
55
- }
56
- let section = !untagged.attributes[0].value && untagged.attributes[0].section;
57
- if (section && section.length > 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
58
- let key = section[0].value.toLowerCase();
59
- let value;
60
-
61
- if (typeof section[1].value === 'string') {
62
- value = section[1].value;
63
- } else if (Array.isArray(section[1])) {
64
- value = section[1].map(entry => (typeof entry.value === 'string' ? entry.value : false)).filter(entry => entry);
50
+ response = await connection.exec(
51
+ !options.readOnly ? 'SELECT' : 'EXAMINE',
52
+ [{ type: 'ATOM', value: encodePath(connection, path) }].concat(extraArgs || []),
53
+ {
54
+ untagged: {
55
+ OK: async untagged => {
56
+ if (!untagged.attributes || !untagged.attributes.length) {
57
+ return;
65
58
  }
66
-
67
- switch (key) {
68
- case 'highestmodseq':
69
- key = 'highestModseq';
70
- if (/^[0-9]+$/.test(value)) {
71
- value = BigInt(value);
72
- }
73
- break;
74
-
75
- case 'mailboxid':
76
- key = 'mailboxId';
77
- if (Array.isArray(value) && value.length) {
78
- value = value[0];
79
- }
80
- break;
81
-
82
- case 'permanentflags':
83
- key = 'permanentFlags';
84
- value = new Set(value);
85
- break;
86
-
87
- case 'uidnext':
88
- key = 'uidNext';
89
- value = Number(value);
90
- break;
91
-
92
- case 'uidvalidity':
93
- key = 'uidValidity';
94
- if (/^[0-9]+$/.test(value)) {
95
- value = BigInt(value);
96
- }
97
- break;
59
+ let section = !untagged.attributes[0].value && untagged.attributes[0].section;
60
+ if (section && section.length > 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
61
+ let key = section[0].value.toLowerCase();
62
+ let value;
63
+
64
+ if (typeof section[1].value === 'string') {
65
+ value = section[1].value;
66
+ } else if (Array.isArray(section[1])) {
67
+ value = section[1].map(entry => (typeof entry.value === 'string' ? entry.value : false)).filter(entry => entry);
68
+ }
69
+
70
+ switch (key) {
71
+ case 'highestmodseq':
72
+ key = 'highestModseq';
73
+ if (/^[0-9]+$/.test(value)) {
74
+ value = BigInt(value);
75
+ }
76
+ break;
77
+
78
+ case 'mailboxid':
79
+ key = 'mailboxId';
80
+ if (Array.isArray(value) && value.length) {
81
+ value = value[0];
82
+ }
83
+ break;
84
+
85
+ case 'permanentflags':
86
+ key = 'permanentFlags';
87
+ value = new Set(value);
88
+ break;
89
+
90
+ case 'uidnext':
91
+ key = 'uidNext';
92
+ value = Number(value);
93
+ break;
94
+
95
+ case 'uidvalidity':
96
+ key = 'uidValidity';
97
+ if (/^[0-9]+$/.test(value)) {
98
+ value = BigInt(value);
99
+ }
100
+ break;
101
+ }
102
+
103
+ map[key] = value;
98
104
  }
99
105
 
100
- map[key] = value;
101
- }
102
-
103
- if (section && section.length === 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
104
- let key = section[0].value.toLowerCase();
105
- switch (key) {
106
- case 'nomodseq':
107
- key = 'noModseq';
108
- map[key] = true;
109
- break;
106
+ if (section && section.length === 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
107
+ let key = section[0].value.toLowerCase();
108
+ switch (key) {
109
+ case 'nomodseq':
110
+ key = 'noModseq';
111
+ map[key] = true;
112
+ break;
113
+ }
114
+ }
115
+ },
116
+ FLAGS: async untagged => {
117
+ if (!untagged.attributes || (!untagged.attributes.length && Array.isArray(untagged.attributes[0]))) {
118
+ return;
119
+ }
120
+ let flags = untagged.attributes[0].map(flag => (typeof flag.value === 'string' ? flag.value : false)).filter(flag => flag);
121
+ map.flags = new Set(flags);
122
+ },
123
+ EXISTS: async untagged => {
124
+ let num = Number(untagged.command);
125
+ if (isNaN(num)) {
126
+ return false;
110
127
  }
111
- }
112
- },
113
- FLAGS: async untagged => {
114
- if (!untagged.attributes || (!untagged.attributes.length && Array.isArray(untagged.attributes[0]))) {
115
- return;
116
- }
117
- let flags = untagged.attributes[0].map(flag => (typeof flag.value === 'string' ? flag.value : false)).filter(flag => flag);
118
- map.flags = new Set(flags);
119
- },
120
- EXISTS: async untagged => {
121
- let num = Number(untagged.command);
122
- if (isNaN(num)) {
123
- return false;
124
- }
125
128
 
126
- map.exists = num;
127
- },
128
- VANISHED: async untagged => {
129
- await connection.untaggedVanished(
130
- untagged,
131
- // mailbox is not yet open, so use a dummy mailbox object
132
- { path, uidNext: false, uidValidity: false }
133
- );
134
- },
135
- // we should only get an untagged FETCH for a SELECT/EXAMINE if QRESYNC was asked for
136
- FETCH: async untagged => {
137
- await connection.untaggedFetch(
138
- untagged,
139
- // mailbox is not yet open, so use a dummy mailbox object
140
- { path, uidNext: false, uidValidity: false }
141
- );
129
+ map.exists = num;
130
+ },
131
+ VANISHED: async untagged => {
132
+ await connection.untaggedVanished(
133
+ untagged,
134
+ // mailbox is not yet open, so use a dummy mailbox object
135
+ { path, uidNext: false, uidValidity: false }
136
+ );
137
+ },
138
+ // we should only get an untagged FETCH for a SELECT/EXAMINE if QRESYNC was asked for
139
+ FETCH: async untagged => {
140
+ await connection.untaggedFetch(
141
+ untagged,
142
+ // mailbox is not yet open, so use a dummy mailbox object
143
+ { path, uidNext: false, uidValidity: false }
144
+ );
145
+ }
142
146
  }
143
147
  }
144
- });
148
+ );
145
149
 
146
150
  let section = !response.response.attributes[0].value && response.response.attributes[0].section;
147
151
  if (section && section.length && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { normalizePath } = require('../tools.js');
3
+ const { encodePath, normalizePath } = require('../tools.js');
4
4
 
5
5
  // Requests info about a mailbox
6
6
  module.exports = async (connection, path, query) => {
@@ -11,7 +11,7 @@ module.exports = async (connection, path, query) => {
11
11
 
12
12
  path = normalizePath(connection, path);
13
13
 
14
- let attributes = [{ type: 'ATOM', value: path }];
14
+ let attributes = [{ type: 'ATOM', value: encodePath(connection, path) }];
15
15
 
16
16
  let queryAttributes = [];
17
17
  Object.keys(query || {}).forEach(key => {
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { normalizePath, getStatusCode, getErrorText } = require('../tools.js');
3
+ const { encodePath, normalizePath, getStatusCode, getErrorText } = require('../tools.js');
4
4
 
5
5
  // Subscribes to a mailbox
6
6
  module.exports = async (connection, path) => {
@@ -13,7 +13,7 @@ module.exports = async (connection, path) => {
13
13
 
14
14
  let response;
15
15
  try {
16
- response = await connection.exec('SUBSCRIBE', [{ type: 'ATOM', value: path }]);
16
+ response = await connection.exec('SUBSCRIBE', [{ type: 'ATOM', value: encodePath(connection, path) }]);
17
17
  response.next();
18
18
  return true;
19
19
  } catch (err) {
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { normalizePath, getStatusCode, getErrorText } = require('../tools.js');
3
+ const { encodePath, normalizePath, getStatusCode, getErrorText } = require('../tools.js');
4
4
 
5
5
  // Unsubscribes from a mailbox
6
6
  module.exports = async (connection, path) => {
@@ -13,7 +13,7 @@ module.exports = async (connection, path) => {
13
13
 
14
14
  let response;
15
15
  try {
16
- response = await connection.exec('UNSUBSCRIBE', [{ type: 'ATOM', value: path }]);
16
+ response = await connection.exec('UNSUBSCRIBE', [{ type: 'ATOM', value: encodePath(connection, path) }]);
17
17
  response.next();
18
18
  return true;
19
19
  } catch (err) {
@@ -26,7 +26,7 @@ function excludeChars(source, exclude) {
26
26
  module.exports = {
27
27
  CHAR() {
28
28
  let value = expandRange(0x01, 0x7f);
29
- this.CHAR = function() {
29
+ this.CHAR = function () {
30
30
  return value;
31
31
  };
32
32
  return value;
@@ -34,7 +34,7 @@ module.exports = {
34
34
 
35
35
  CHAR8() {
36
36
  let value = expandRange(0x01, 0xff);
37
- this.CHAR8 = function() {
37
+ this.CHAR8 = function () {
38
38
  return value;
39
39
  };
40
40
  return value;
@@ -46,7 +46,7 @@ module.exports = {
46
46
 
47
47
  CTL() {
48
48
  let value = expandRange(0x00, 0x1f) + '\x7F';
49
- this.CTL = function() {
49
+ this.CTL = function () {
50
50
  return value;
51
51
  };
52
52
  return value;
@@ -58,7 +58,7 @@ module.exports = {
58
58
 
59
59
  ALPHA() {
60
60
  let value = expandRange(0x41, 0x5a) + expandRange(0x61, 0x7a);
61
- this.ALPHA = function() {
61
+ this.ALPHA = function () {
62
62
  return value;
63
63
  };
64
64
  return value;
@@ -66,7 +66,7 @@ module.exports = {
66
66
 
67
67
  DIGIT() {
68
68
  let value = expandRange(0x30, 0x39) + expandRange(0x61, 0x7a);
69
- this.DIGIT = function() {
69
+ this.DIGIT = function () {
70
70
  return value;
71
71
  };
72
72
  return value;
@@ -74,7 +74,7 @@ module.exports = {
74
74
 
75
75
  'ATOM-CHAR'() {
76
76
  let value = excludeChars(this.CHAR(), this['atom-specials']());
77
- this['ATOM-CHAR'] = function() {
77
+ this['ATOM-CHAR'] = function () {
78
78
  return value;
79
79
  };
80
80
  return value;
@@ -82,7 +82,7 @@ module.exports = {
82
82
 
83
83
  'ASTRING-CHAR'() {
84
84
  let value = this['ATOM-CHAR']() + this['resp-specials']();
85
- this['ASTRING-CHAR'] = function() {
85
+ this['ASTRING-CHAR'] = function () {
86
86
  return value;
87
87
  };
88
88
  return value;
@@ -90,7 +90,7 @@ module.exports = {
90
90
 
91
91
  'TEXT-CHAR'() {
92
92
  let value = excludeChars(this.CHAR(), '\r\n');
93
- this['TEXT-CHAR'] = function() {
93
+ this['TEXT-CHAR'] = function () {
94
94
  return value;
95
95
  };
96
96
  return value;
@@ -98,7 +98,7 @@ module.exports = {
98
98
 
99
99
  'atom-specials'() {
100
100
  let value = '(' + ')' + '{' + this.SP() + this.CTL() + this['list-wildcards']() + this['quoted-specials']() + this['resp-specials']();
101
- this['atom-specials'] = function() {
101
+ this['atom-specials'] = function () {
102
102
  return value;
103
103
  };
104
104
  return value;
@@ -109,8 +109,8 @@ module.exports = {
109
109
  },
110
110
 
111
111
  'quoted-specials'() {
112
- let value = this.DQUOTE() + '\\';
113
- this['quoted-specials'] = function() {
112
+ let value = this.DQUOTE() + "'\\";
113
+ this['quoted-specials'] = function () {
114
114
  return value;
115
115
  };
116
116
  return value;
@@ -122,7 +122,7 @@ module.exports = {
122
122
 
123
123
  tag() {
124
124
  let value = excludeChars(this['ASTRING-CHAR'](), '+');
125
- this.tag = function() {
125
+ this.tag = function () {
126
126
  return value;
127
127
  };
128
128
  return value;
@@ -130,7 +130,7 @@ module.exports = {
130
130
 
131
131
  command() {
132
132
  let value = this.ALPHA() + this.DIGIT() + '-';
133
- this.command = function() {
133
+ this.command = function () {
134
134
  return value;
135
135
  };
136
136
  return value;
@@ -189,6 +189,7 @@ class TokenParser {
189
189
  case '{':
190
190
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
191
191
  this.currentNode.type = 'LITERAL';
192
+ this.currentNode.literalType = 'literal';
192
193
  this.state = 'LITERAL';
193
194
  this.currentNode.closed = false;
194
195
  break;
@@ -385,10 +386,13 @@ class TokenParser {
385
386
  if (this.currentNode.started) {
386
387
  // only relevant if literals are not already parsed out from input
387
388
 
388
- //if(imapFormalSyntax['CHAR8']().indexOf(chr) < 0){
389
+ // Disabled NUL check
390
+ // See https://github.com/emailjs/emailjs-imap-handler/commit/f11b2822bedabe492236e8263afc630134a3c41c
391
+ /*
389
392
  if (chr === '\u0000') {
390
393
  throw new Error('Unexpected \\x00 at position ' + (this.pos + i));
391
394
  }
395
+ */
392
396
 
393
397
  this.currentNode.chBuffer[this.currentNode.chPos++] = chr.charCodeAt(0);
394
398
 
package/lib/imap-flow.js CHANGED
@@ -129,6 +129,7 @@ class ImapFlow extends EventEmitter {
129
129
  * @param {Boolean} [options.verifyOnly=false] If `true` then logs out automatically after successful authentication
130
130
  * @param {Boolean} [options.proxy] Optional proxy URL. Supports HTTP CONNECT (`http://`, `https://`) and SOCKS (`socks://`, `socks4://`, `socks5://`) proxies
131
131
  * @param {Boolean} [options.qresync=false] If true, then enables QRESYNC support. EXPUNGE notifications will include `uid` property instead of `seq`
132
+ * @param {Number} [options.maxIdleTime] If set, then breaks and restarts IDLE every maxIdleTime ms
132
133
  */
133
134
  constructor(options) {
134
135
  super({ captureRejections: true });
@@ -260,6 +261,8 @@ class ImapFlow extends EventEmitter {
260
261
  this.currentLock = false;
261
262
  this.locks = [];
262
263
 
264
+ this.maxIdleTime = this.options.maxIdleTime || false;
265
+
263
266
  this.streamer.on('error', err => {
264
267
  if (['Z_BUF_ERROR', 'ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(err.code)) {
265
268
  // just close the connection, usually nothing but noise
@@ -411,6 +414,20 @@ class ImapFlow extends EventEmitter {
411
414
 
412
415
  try {
413
416
  parsed = await parser(data.payload, { literals: data.literals });
417
+ if (parsed.tag && !['*', '+'].includes(parsed.tag) && parsed.command) {
418
+ let payload = { response: parsed.command };
419
+
420
+ if (
421
+ parsed.attributes &&
422
+ parsed.attributes[0] &&
423
+ parsed.attributes[0].section &&
424
+ parsed.attributes[0].section[0] &&
425
+ parsed.attributes[0].section[0].type === 'ATOM'
426
+ ) {
427
+ payload.code = parsed.attributes[0].section[0].value;
428
+ }
429
+ this.emit('response', payload);
430
+ }
414
431
  } catch (err) {
415
432
  // can not make sense of this
416
433
  this.log.error({ src: 's', msg: data.payload.toString(), err, cid: this.id });
@@ -1046,6 +1063,15 @@ class ImapFlow extends EventEmitter {
1046
1063
  range = range.toString();
1047
1064
  }
1048
1065
 
1066
+ // special case, some servers allow this, some do not, so replace it with the last known EXISTS value
1067
+ if (range === '*') {
1068
+ if (!this.mailbox.exists) {
1069
+ return false;
1070
+ }
1071
+ range = this.mailbox.exists.toString();
1072
+ options.uid = false; // sequence query
1073
+ }
1074
+
1049
1075
  if (range && typeof range === 'object' && !Array.isArray(range)) {
1050
1076
  if (range.all && Object.keys(range) === 1) {
1051
1077
  range = '1:*';
@@ -1340,9 +1366,12 @@ class ImapFlow extends EventEmitter {
1340
1366
  /**
1341
1367
  * @typedef {Object} ListResponse
1342
1368
  * @global
1343
- * @property {String} path mailbox path
1369
+ * @property {String} path mailbox path (unicode string)
1370
+ * @property {String} pathAsListed mailbox path as listed in the LIST/LSUB response
1344
1371
  * @property {String} name mailbox name (last part of path after delimiter)
1345
1372
  * @property {String} delimiter mailbox path delimiter, usually "." or "/"
1373
+ * @property {Array<string>} parent An array of parent folder names. All names are in unicode
1374
+ * @property {String} parentPath Same as `parent`, but as a complete string path (unicode string)
1346
1375
  * @property {Set<string>} flags a set of flags for this mailbox
1347
1376
  * @property {String} specialUse one of special-use flags (if applicable): "\All", "\Archive", "\Drafts", "\Flagged", "\Junk", "\Sent", "\Trash". Additionally INBOX has non-standard "\Inbox" flag set
1348
1377
  * @property {Boolean} listed `true` if mailbox was found from the output of LIST command
@@ -1546,7 +1575,7 @@ class ImapFlow extends EventEmitter {
1546
1575
  /**
1547
1576
  * Requests the status of the indicated mailbox. Only requested status values will be returned.
1548
1577
  *
1549
- * @param {String} path mailbox path to check for
1578
+ * @param {String} path mailbox path to check for (unicode string)
1550
1579
  * @param {Object} query defines requested status items
1551
1580
  * @param {Boolean} query.messages if `true` request count of messages
1552
1581
  * @param {Boolean} query.recent if `true` request count of messages with \\Recent tag
@@ -1579,7 +1608,7 @@ class ImapFlow extends EventEmitter {
1579
1608
  */
1580
1609
  async idle() {
1581
1610
  if (!this.idling) {
1582
- return await this.run('IDLE');
1611
+ return await this.run('IDLE', this.maxIdleTime);
1583
1612
  }
1584
1613
  }
1585
1614
 
@@ -1769,7 +1798,7 @@ class ImapFlow extends EventEmitter {
1769
1798
  /**
1770
1799
  * Appends a new message to a mailbox
1771
1800
  *
1772
- * @param {String} path Mailbox path to upload the message to
1801
+ * @param {String} path Mailbox path to upload the message to (unicode string)
1773
1802
  * @param {string|Buffer} content RFC822 formatted email message
1774
1803
  * @param {string[]} [flags] an array of flags to be set for the uploaded message
1775
1804
  * @param {Date|string} [idate=now] internal date to be set for the message
@@ -2079,6 +2108,14 @@ class ImapFlow extends EventEmitter {
2079
2108
  return;
2080
2109
  }
2081
2110
 
2111
+ if (seq === '*') {
2112
+ if (!this.mailbox.exists) {
2113
+ return false;
2114
+ }
2115
+ seq = this.mailbox.exists.toString();
2116
+ options = Object.assign({}, options || {}, { uid: false }); // force into a sequence query
2117
+ }
2118
+
2082
2119
  let response = await this.run('FETCH', (seq || '').toString(), query, options);
2083
2120
 
2084
2121
  if (!response || !response.list || !response.list.length) {
package/lib/tools.js CHANGED
@@ -9,6 +9,30 @@ const { JPDecoder } = require('./jp-decoder');
9
9
  const iconv = require('iconv-lite');
10
10
 
11
11
  module.exports = {
12
+ encodePath(connection, path) {
13
+ path = (path || '').toString();
14
+ if (!connection.enabled.has('UTF8=ACCEPT') && /[&\x00-\x08\x0b-\x0c\x0e-\x1f\u0080-\uffff]/.test(path)) {
15
+ try {
16
+ path = iconv.encode(path, 'utf-7-imap').toString();
17
+ } catch (err) {
18
+ // ignore, keep name as is
19
+ }
20
+ }
21
+ return path;
22
+ },
23
+
24
+ decodePath(connection, path) {
25
+ path = (path || '').toString();
26
+ if (!connection.enabled.has('UTF8=ACCEPT') && /[&]/.test(path)) {
27
+ try {
28
+ path = iconv.decode(Buffer.from(path), 'utf-7-imap').toString();
29
+ } catch (err) {
30
+ // ignore, keep name as is
31
+ }
32
+ }
33
+ return path;
34
+ },
35
+
12
36
  normalizePath(connection, path, skipNamespace) {
13
37
  if (Array.isArray(path)) {
14
38
  path = path.join((connection.namespace && connection.namespace.delimiter) || '');
@@ -19,14 +43,6 @@ module.exports = {
19
43
  return 'INBOX';
20
44
  }
21
45
 
22
- if (!connection.enabled.has('UTF8=ACCEPT') && /[\x00-\x08\x0b-\x0c\x0e-\x1f\u0080-\uffff]/.test(path)) {
23
- try {
24
- path = iconv.encode(path, 'utf-7-imap').toString();
25
- } catch (err) {
26
- // ignore, keep name as is
27
- }
28
- }
29
-
30
46
  // ensure namespace prefix if needed
31
47
  if (!skipNamespace && connection.namespace && connection.namespace.prefix && path.indexOf(connection.namespace.prefix) !== 0) {
32
48
  path = connection.namespace.prefix + path;
@@ -290,9 +306,10 @@ module.exports = {
290
306
  break;
291
307
  }
292
308
 
293
- default:
294
- if (key.indexOf('body[') >= 0) {
295
- let partKey = key.replace(/^body\[|]$/g, '');
309
+ default: {
310
+ let match = key.match(/(body)\[/i);
311
+ if (match) {
312
+ let partKey = key.replace(/^(body)\[|]$/gi, '');
296
313
  partKey = partKey.replace(/\.fields.*$/g, '');
297
314
 
298
315
  let value = getBuffer(attribute);
@@ -308,6 +325,7 @@ module.exports = {
308
325
  break;
309
326
  }
310
327
  break;
328
+ }
311
329
  }
312
330
  }
313
331
 
package/lib/types.d.ts CHANGED
@@ -28,6 +28,7 @@ declare module "imapflow" {
28
28
  * @param [options.verifyOnly = false] - If `true` then logs out automatically after successful authentication
29
29
  * @param [options.proxy] - Optional proxy URL. Supports HTTP CONNECT (`http://`, `https://`) and SOCKS (`socks://`, `socks4://`, `socks5://`) proxies
30
30
  * @param [options.qresync = false] - If true, then enables QRESYNC support. EXPUNGE notifications will include `uid` property instead of `seq`
31
+ * @param [options.maxIdleTime] - If set, then breaks and restarts IDLE every maxIdleTime ms
31
32
  */
32
33
  class ImapFlow extends EventEmitter {
33
34
  constructor(options: {
@@ -54,6 +55,7 @@ declare module "imapflow" {
54
55
  verifyOnly?: boolean;
55
56
  proxy?: boolean;
56
57
  qresync?: boolean;
58
+ maxIdleTime?: number;
57
59
  });
58
60
  /**
59
61
  * Current module version as a static class property
@@ -239,7 +241,7 @@ declare module "imapflow" {
239
241
  * let status = await client.status('INBOX', {unseen: true});
240
242
  * console.log(status.unseen);
241
243
  * // 123
242
- * @param path - mailbox path to check for
244
+ * @param path - mailbox path to check for (unicode string)
243
245
  * @param query - defines requested status items
244
246
  * @param query.messages - if `true` request count of messages
245
247
  * @param query.recent - if `true` request count of messages with \\Recent tag
@@ -340,7 +342,7 @@ declare module "imapflow" {
340
342
  * Appends a new message to a mailbox
341
343
  * @example
342
344
  * await client.append('INBOX', rawMessageBuffer, ['\\Seen'], new Date(2000, 1, 1));
343
- * @param path - Mailbox path to upload the message to
345
+ * @param path - Mailbox path to upload the message to (unicode string)
344
346
  * @param content - RFC822 formatted email message
345
347
  * @param [flags] - an array of flags to be set for the uploaded message
346
348
  * @param [idate = now] - internal date to be set for the message
@@ -563,9 +565,12 @@ declare type QuotaResponse = {
563
565
  };
564
566
 
565
567
  /**
566
- * @property path - mailbox path
568
+ * @property path - mailbox path (unicode string)
569
+ * @property pathAsListed - mailbox path as listed in the LIST/LSUB response
567
570
  * @property name - mailbox name (last part of path after delimiter)
568
571
  * @property delimiter - mailbox path delimiter, usually "." or "/"
572
+ * @property parent - An array of parent folder names. All names are in unicode
573
+ * @property parentPath - Same as `parent`, but as a complete string path (unicode string)
569
574
  * @property flags - a set of flags for this mailbox
570
575
  * @property specialUse - one of special-use flags (if applicable): "\All", "\Archive", "\Drafts", "\Flagged", "\Junk", "\Sent", "\Trash". Additionally INBOX has non-standard "\Inbox" flag set
571
576
  * @property listed - `true` if mailbox was found from the output of LIST command
@@ -573,8 +578,11 @@ declare type QuotaResponse = {
573
578
  */
574
579
  declare type ListResponse = {
575
580
  path: string;
581
+ pathAsListed: string;
576
582
  name: string;
577
583
  delimiter: string;
584
+ parent: string[];
585
+ parentPath: string;
578
586
  flags: Set<string>;
579
587
  specialUse: string;
580
588
  listed: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.88",
3
+ "version": "1.0.91",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "./lib/imap-flow.js",
6
6
  "scripts": {
@@ -8,7 +8,8 @@
8
8
  "prepare": "npm run build",
9
9
  "docs": "rm -rf docs && mkdir -p docs && jsdoc lib/imap-flow.js -c jsdoc.json -R README.md --destination docs/ && cp assets/favicon.ico docs",
10
10
  "dst": "node types.js",
11
- "build": "npm run docs && npm run dst"
11
+ "build": "npm run docs && npm run dst",
12
+ "st": "npm run docs && st -d docs -i index.html"
12
13
  },
13
14
  "repository": {
14
15
  "type": "git",
@@ -29,28 +30,29 @@
29
30
  "@babel/eslint-parser": "7.17.0",
30
31
  "@babel/eslint-plugin": "7.17.7",
31
32
  "@babel/plugin-syntax-class-properties": "7.12.13",
32
- "@babel/preset-env": "7.16.11",
33
- "@types/node": "17.0.23",
33
+ "@babel/preset-env": "7.17.10",
34
+ "@types/node": "17.0.31",
34
35
  "braintree-jsdoc-template": "3.3.0",
35
- "eslint": "8.11.0",
36
+ "eslint": "8.14.0",
36
37
  "eslint-config-nodemailer": "1.2.0",
37
38
  "eslint-config-prettier": "8.5.0",
38
- "grunt": "1.4.1",
39
+ "grunt": "1.5.2",
39
40
  "grunt-cli": "1.4.3",
40
41
  "grunt-contrib-nodeunit": "4.0.0",
41
42
  "grunt-eslint": "24.0.0",
42
43
  "jsdoc": "3.6.10",
44
+ "st": "3.0.0",
43
45
  "tsd-jsdoc": "2.5.0"
44
46
  },
45
47
  "dependencies": {
46
- "encoding-japanese": "1.0.30",
48
+ "encoding-japanese": "2.0.0",
47
49
  "iconv-lite": "0.6.3",
48
50
  "libbase64": "1.2.1",
49
- "libmime": "5.0.0",
51
+ "libmime": "5.1.0",
50
52
  "libqp": "1.1.0",
51
- "mailsplit": "5.3.1",
52
- "nodemailer": "6.7.3",
53
- "pino": "7.9.2",
53
+ "mailsplit": "5.3.2",
54
+ "nodemailer": "6.7.4",
55
+ "pino": "7.11.0",
54
56
  "socks": "2.6.2"
55
57
  }
56
58
  }