imapflow 1.0.89 → 1.0.92

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/.gitattributes ADDED
@@ -0,0 +1 @@
1
+ *.js text eol=lf
@@ -0,0 +1,21 @@
1
+ name: Run tests
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ strategy:
10
+ matrix:
11
+ node: [16.x, 18.x]
12
+ os: [ubuntu-latest]
13
+ runs-on: ${{ matrix.os }}
14
+ steps:
15
+ - uses: actions/checkout@v2
16
+ - name: Use Node.js ${{ matrix.node }}
17
+ uses: actions/setup-node@v1
18
+ with:
19
+ node-version: ${{ matrix.node }}
20
+ - run: npm install
21
+ - run: npm test
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # CHANGELOG
2
2
 
3
+ - 1.0.92 - 2022-05-03
4
+
5
+ - Added support for IMAP BINARY extension (rfc3516)
6
+ - Logging improvements
7
+
8
+ - 1.0.91 - 2022-05-02
9
+
10
+ - Do not throw if literal includes a null byte
11
+ - Use UTF8 path names by default
12
+ - Overrides '\*' range query with the EXISTS value
13
+
14
+ - 1.0.90 - 2022-04-04
15
+
16
+ - Added new configuration option `maxIdleTime`
17
+
3
18
  - 1.0.89 - 2022-04-04
4
19
 
5
20
  - Added new event type `response` that emits all command responses
@@ -38,7 +38,17 @@ module.exports = async (connection, destination, content, flags, idate) => {
38
38
  attributes.push({ type: 'STRING', value: idate }); // force quotes as required by date-time
39
39
  }
40
40
 
41
- attributes.push({ type: 'LITERAL', value: content });
41
+ let isLiteral8 = false;
42
+ if (connection.capabilities.has('BINARY') && !connection.disableBinary) {
43
+ if (typeof content === 'string') {
44
+ content = Buffer.from(content);
45
+ }
46
+ // Value is literal8 if it contains NULL bytes. The server must support the BINARY extension
47
+ // and if it does not then send the value as a regular literal and hope for the best
48
+ isLiteral8 = content.indexOf(Buffer.from([0])) >= 0;
49
+ }
50
+
51
+ attributes.push({ type: 'LITERAL', value: content, isLiteral8 });
42
52
 
43
53
  let map = { destination };
44
54
  if (connection.mailbox && connection.mailbox.path) {
@@ -39,7 +39,7 @@ module.exports = async (connection, username, accessToken) => {
39
39
  try {
40
40
  errorResponse = JSON.parse(Buffer.from(resp.attributes[0].value, 'base64').toString());
41
41
  } catch (err) {
42
- connection.log.debug({ data: resp.attributes[0].value, err });
42
+ connection.log.debug({ errorResponse: resp.attributes[0].value, err });
43
43
  }
44
44
  }
45
45
 
@@ -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 = connection.capabilities.has('BINARY') && !connection.disableBinary ? 'BINARY' : '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;