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 +14 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/delete.js +2 -2
- package/lib/commands/fetch.js +4 -2
- package/lib/commands/idle.js +120 -91
- package/lib/commands/list.js +44 -60
- package/lib/commands/quota.js +2 -2
- package/lib/commands/rename.js +3 -3
- package/lib/commands/select.js +93 -89
- package/lib/commands/status.js +2 -2
- package/lib/commands/subscribe.js +2 -2
- package/lib/commands/unsubscribe.js +2 -2
- package/lib/handler/imap-formal-syntax.js +13 -13
- package/lib/handler/token-parser.js +5 -1
- package/lib/imap-flow.js +41 -4
- package/lib/tools.js +29 -11
- package/lib/types.d.ts +11 -3
- package/package.json +13 -11
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
|
package/lib/commands/create.js
CHANGED
|
@@ -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 &&
|
package/lib/commands/delete.js
CHANGED
|
@@ -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) {
|
package/lib/commands/fetch.js
CHANGED
|
@@ -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:
|
|
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:
|
|
66
|
+
queryStructure.push({ type: 'ATOM', value: `${commandKey}.PEEK`, section: [], partial });
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
// if possible, always request for unique email id
|
package/lib/commands/idle.js
CHANGED
|
@@ -2,112 +2,139 @@
|
|
|
2
2
|
|
|
3
3
|
const NOOP_INTERVAL = 2 * 60 * 1000;
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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 {
|
|
107
|
-
|
|
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,
|
|
163
|
+
idleTimer = setTimeout(runLoop, noopInterval);
|
|
135
164
|
})
|
|
136
165
|
.catch(err => {
|
|
137
166
|
clearTimeout(idleTimer);
|
package/lib/commands/list.js
CHANGED
|
@@ -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.
|
|
49
|
-
|
|
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(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
90
|
+
if (entry.path.toUpperCase() === 'INBOX') {
|
|
91
|
+
entry.specialUse = '\\Inbox';
|
|
92
|
+
}
|
|
104
93
|
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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');
|
package/lib/commands/quota.js
CHANGED
|
@@ -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 =
|
package/lib/commands/rename.js
CHANGED
|
@@ -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;
|
package/lib/commands/select.js
CHANGED
|
@@ -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(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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') {
|
package/lib/commands/status.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
295
|
-
|
|
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.
|
|
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.
|
|
33
|
-
"@types/node": "17.0.
|
|
33
|
+
"@babel/preset-env": "7.17.10",
|
|
34
|
+
"@types/node": "17.0.31",
|
|
34
35
|
"braintree-jsdoc-template": "3.3.0",
|
|
35
|
-
"eslint": "8.
|
|
36
|
+
"eslint": "8.14.0",
|
|
36
37
|
"eslint-config-nodemailer": "1.2.0",
|
|
37
38
|
"eslint-config-prettier": "8.5.0",
|
|
38
|
-
"grunt": "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": "
|
|
48
|
+
"encoding-japanese": "2.0.0",
|
|
47
49
|
"iconv-lite": "0.6.3",
|
|
48
50
|
"libbase64": "1.2.1",
|
|
49
|
-
"libmime": "5.
|
|
51
|
+
"libmime": "5.1.0",
|
|
50
52
|
"libqp": "1.1.0",
|
|
51
|
-
"mailsplit": "5.3.
|
|
52
|
-
"nodemailer": "6.7.
|
|
53
|
-
"pino": "7.
|
|
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
|
}
|