imapflow 1.2.8 → 1.2.9
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/.ncurc.js +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +3 -3
- package/lib/charsets.js +15 -0
- package/lib/commands/append.js +62 -54
- package/lib/commands/authenticate.js +99 -52
- package/lib/commands/capability.js +12 -2
- package/lib/commands/close.js +11 -1
- package/lib/commands/compress.js +10 -1
- package/lib/commands/copy.js +18 -1
- package/lib/commands/create.js +15 -2
- package/lib/commands/delete.js +10 -1
- package/lib/commands/enable.js +12 -1
- package/lib/commands/expunge.js +18 -2
- package/lib/commands/fetch.js +39 -4
- package/lib/commands/id.js +22 -3
- package/lib/commands/idle.js +39 -4
- package/lib/commands/list.js +86 -48
- package/lib/commands/login.js +12 -1
- package/lib/commands/logout.js +11 -2
- package/lib/commands/move.js +17 -1
- package/lib/commands/namespace.js +32 -2
- package/lib/commands/noop.js +6 -1
- package/lib/commands/quota.js +33 -14
- package/lib/commands/rename.js +13 -1
- package/lib/commands/search.js +16 -1
- package/lib/commands/select.js +76 -33
- package/lib/commands/starttls.js +6 -1
- package/lib/commands/status.js +64 -52
- package/lib/commands/store.js +27 -4
- package/lib/commands/subscribe.js +7 -1
- package/lib/commands/unsubscribe.js +7 -1
- package/lib/handler/imap-compiler.js +44 -2
- package/lib/handler/imap-formal-syntax.js +51 -3
- package/lib/handler/imap-handler.js +8 -0
- package/lib/handler/imap-parser.js +23 -2
- package/lib/handler/imap-stream.js +84 -31
- package/lib/handler/parser-instance.js +61 -1
- package/lib/handler/token-parser.js +66 -9
- package/lib/imap-commands.js +11 -0
- package/lib/imap-flow.d.ts +6 -0
- package/lib/imap-flow.js +164 -42
- package/lib/jp-decoder.js +10 -0
- package/lib/limited-passthrough.js +12 -5
- package/lib/proxy-connection.js +18 -12
- package/lib/search-compiler.js +3 -11
- package/lib/special-use.js +23 -16
- package/lib/tools.js +218 -13
- package/package.json +4 -11
- package/test/commands-integration-test.js +33 -0
- package/test/special-use-test.js +32 -0
- package/assets/favicon.ico +0 -0
- package/jsdoc.json +0 -28
package/lib/commands/search.js
CHANGED
|
@@ -3,7 +3,15 @@
|
|
|
3
3
|
const { enhanceCommandError } = require('../tools.js');
|
|
4
4
|
const { searchCompiler } = require('../search-compiler.js');
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Searches for messages matching the specified criteria.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} connection - IMAP connection instance
|
|
10
|
+
* @param {Object|boolean} query - Search query object, or true/empty object to match all messages
|
|
11
|
+
* @param {Object} [options] - Search options
|
|
12
|
+
* @param {boolean} [options.uid] - If true, use UID SEARCH instead of SEARCH
|
|
13
|
+
* @returns {Promise<number[]|boolean>} Sorted array of matching sequence numbers or UIDs, or false on failure
|
|
14
|
+
*/
|
|
7
15
|
module.exports = async (connection, query, options) => {
|
|
8
16
|
if (connection.state !== connection.states.SELECTED) {
|
|
9
17
|
// nothing to do here
|
|
@@ -14,6 +22,10 @@ module.exports = async (connection, query, options) => {
|
|
|
14
22
|
|
|
15
23
|
let attributes;
|
|
16
24
|
|
|
25
|
+
// Three query branches:
|
|
26
|
+
// 1. Empty/truthy/all-only query -> use IMAP "SEARCH ALL" to match every message
|
|
27
|
+
// 2. Non-empty object -> compile into IMAP SEARCH criteria via searchCompiler
|
|
28
|
+
// 3. Anything else (unexpected type) -> bail out with false
|
|
17
29
|
if (!query || query === true || (typeof query === 'object' && (!Object.keys(query).length || (Object.keys(query).length === 1 && query.all)))) {
|
|
18
30
|
// search for all messages
|
|
19
31
|
attributes = [{ type: 'ATOM', value: 'ALL' }];
|
|
@@ -24,6 +36,8 @@ module.exports = async (connection, query, options) => {
|
|
|
24
36
|
return false;
|
|
25
37
|
}
|
|
26
38
|
|
|
39
|
+
// Use a Set to deduplicate sequence numbers/UIDs -- servers may return
|
|
40
|
+
// duplicates across multiple untagged SEARCH responses.
|
|
27
41
|
let results = new Set();
|
|
28
42
|
let response;
|
|
29
43
|
try {
|
|
@@ -41,6 +55,7 @@ module.exports = async (connection, query, options) => {
|
|
|
41
55
|
}
|
|
42
56
|
});
|
|
43
57
|
response.next();
|
|
58
|
+
// Sort numerically for consistent, predictable output order
|
|
44
59
|
return Array.from(results).sort((a, b) => a - b);
|
|
45
60
|
} catch (err) {
|
|
46
61
|
await enhanceCommandError(err);
|
package/lib/commands/select.js
CHANGED
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Selects or examines a mailbox, making it the current mailbox for subsequent operations.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} connection - IMAP connection instance
|
|
9
|
+
* @param {string} path - Mailbox path to select
|
|
10
|
+
* @param {Object} [options] - Select options
|
|
11
|
+
* @param {boolean} [options.readOnly] - If true, use EXAMINE instead of SELECT (read-only access)
|
|
12
|
+
* @param {string} [options.changedSince] - QRESYNC modseq value to fetch changes since
|
|
13
|
+
* @param {BigInt} [options.uidValidity] - QRESYNC UID validity value
|
|
14
|
+
* @returns {Promise<Object|undefined>} Mailbox info object with path, flags, exists, uidNext, uidValidity, highestModseq, etc., or undefined if preconditions not met
|
|
15
|
+
* @throws {Error} If the SELECT/EXAMINE command fails
|
|
16
|
+
*/
|
|
6
17
|
module.exports = async (connection, path, options) => {
|
|
7
18
|
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
|
8
19
|
// nothing to do here
|
|
@@ -12,6 +23,8 @@ module.exports = async (connection, path, options) => {
|
|
|
12
23
|
|
|
13
24
|
path = normalizePath(connection, path);
|
|
14
25
|
|
|
26
|
+
// Ensure we have folder metadata (delimiter, flags, specialUse) by running LIST if needed.
|
|
27
|
+
// This is cached in connection.folders to avoid repeated LIST calls.
|
|
15
28
|
if (!connection.folders.has(path)) {
|
|
16
29
|
let folders = await connection.run('LIST', '', path);
|
|
17
30
|
if (!folders) {
|
|
@@ -35,6 +48,9 @@ module.exports = async (connection, path, options) => {
|
|
|
35
48
|
});
|
|
36
49
|
}
|
|
37
50
|
|
|
51
|
+
// QRESYNC (RFC 7162): allows efficient mailbox resynchronization by sending
|
|
52
|
+
// the last known UIDVALIDITY and HIGHESTMODSEQ. Server responds with only
|
|
53
|
+
// the changes (new flags, expunged UIDs) since that point.
|
|
38
54
|
let extraArgs = [];
|
|
39
55
|
if (connection.enabled.has('QRESYNC') && options.changedSince && options.uidValidity) {
|
|
40
56
|
extraArgs.push([
|
|
@@ -49,6 +65,9 @@ module.exports = async (connection, path, options) => {
|
|
|
49
65
|
|
|
50
66
|
let encodedPath = encodePath(connection, path);
|
|
51
67
|
|
|
68
|
+
// SELECT opens the mailbox read-write; EXAMINE opens it read-only.
|
|
69
|
+
// Path encoding: if the encoded path contains '&' (UTF-7 encoding marker),
|
|
70
|
+
// send as quoted STRING to avoid parser issues with the ampersand.
|
|
52
71
|
let selectCommand = {
|
|
53
72
|
command: !options.readOnly ? 'SELECT' : 'EXAMINE',
|
|
54
73
|
arguments: [{ type: encodedPath.indexOf('&') >= 0 ? 'STRING' : 'ATOM', value: encodedPath }].concat(extraArgs || [])
|
|
@@ -56,15 +75,22 @@ module.exports = async (connection, path, options) => {
|
|
|
56
75
|
|
|
57
76
|
response = await connection.exec(selectCommand.command, selectCommand.arguments, {
|
|
58
77
|
untagged: {
|
|
78
|
+
// Untagged OK responses carry response codes in brackets, e.g.:
|
|
79
|
+
// * OK [UIDVALIDITY 1234] UIDs valid
|
|
80
|
+
// * OK [PERMANENTFLAGS (\Seen \Answered \*)] Flags permitted
|
|
81
|
+
// The section array holds the parsed bracket contents: section[0] is the
|
|
82
|
+
// key (e.g., "UIDVALIDITY"), section[1] is the value or list.
|
|
59
83
|
OK: async untagged => {
|
|
60
84
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
61
85
|
return;
|
|
62
86
|
}
|
|
63
87
|
let section = !untagged.attributes[0].value && untagged.attributes[0].section;
|
|
88
|
+
// Handle response codes with a key-value pair (section has 2+ elements)
|
|
64
89
|
if (section && section.length > 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
|
65
90
|
let key = section[0].value.toLowerCase();
|
|
66
91
|
let value;
|
|
67
92
|
|
|
93
|
+
// Value can be a single string or a list of strings (e.g., PERMANENTFLAGS)
|
|
68
94
|
if (typeof section[1].value === 'string') {
|
|
69
95
|
value = section[1].value;
|
|
70
96
|
} else if (Array.isArray(section[1])) {
|
|
@@ -72,6 +98,10 @@ module.exports = async (connection, path, options) => {
|
|
|
72
98
|
}
|
|
73
99
|
|
|
74
100
|
switch (key) {
|
|
101
|
+
// CONDSTORE (RFC 7162): highest mod-sequence value for the mailbox.
|
|
102
|
+
// Used for incremental sync -- clients compare against their cached
|
|
103
|
+
// value to detect changes. Stored as BigInt since modseq values
|
|
104
|
+
// can exceed Number.MAX_SAFE_INTEGER.
|
|
75
105
|
case 'highestmodseq':
|
|
76
106
|
key = 'highestModseq';
|
|
77
107
|
if (/^[0-9]+$/.test(value)) {
|
|
@@ -79,6 +109,9 @@ module.exports = async (connection, path, options) => {
|
|
|
79
109
|
}
|
|
80
110
|
break;
|
|
81
111
|
|
|
112
|
+
// OBJECTID (RFC 8474): server-assigned unique mailbox identifier.
|
|
113
|
+
// Unlike path, this ID survives renames. Value comes as a
|
|
114
|
+
// parenthesized list, so extract the first (only) element.
|
|
82
115
|
case 'mailboxid':
|
|
83
116
|
key = 'mailboxId';
|
|
84
117
|
if (Array.isArray(value) && value.length) {
|
|
@@ -86,16 +119,23 @@ module.exports = async (connection, path, options) => {
|
|
|
86
119
|
}
|
|
87
120
|
break;
|
|
88
121
|
|
|
122
|
+
// Flags that the client can change permanently on messages in
|
|
123
|
+
// this mailbox. Includes \* if the server allows custom flags.
|
|
89
124
|
case 'permanentflags':
|
|
90
125
|
key = 'permanentFlags';
|
|
91
126
|
value = new Set(value);
|
|
92
127
|
break;
|
|
93
128
|
|
|
129
|
+
// The next UID that will be assigned to a new message in this
|
|
130
|
+
// mailbox. Useful for detecting new arrivals.
|
|
94
131
|
case 'uidnext':
|
|
95
132
|
key = 'uidNext';
|
|
96
133
|
value = Number(value);
|
|
97
134
|
break;
|
|
98
135
|
|
|
136
|
+
// Unique identifier validity value. If this changes between
|
|
137
|
+
// sessions, all previously cached UIDs are invalid and the
|
|
138
|
+
// client must re-sync from scratch.
|
|
99
139
|
case 'uidvalidity':
|
|
100
140
|
key = 'uidValidity';
|
|
101
141
|
if (/^[0-9]+$/.test(value)) {
|
|
@@ -107,9 +147,12 @@ module.exports = async (connection, path, options) => {
|
|
|
107
147
|
map[key] = value;
|
|
108
148
|
}
|
|
109
149
|
|
|
150
|
+
// Handle response codes with only a keyword (no value), e.g., [NOMODSEQ]
|
|
110
151
|
if (section && section.length === 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
|
111
152
|
let key = section[0].value.toLowerCase();
|
|
112
153
|
switch (key) {
|
|
154
|
+
// NOMODSEQ means the mailbox does not support mod-sequences.
|
|
155
|
+
// CONDSTORE/QRESYNC features are unavailable for this mailbox.
|
|
113
156
|
case 'nomodseq':
|
|
114
157
|
key = 'noModseq';
|
|
115
158
|
map[key] = true;
|
|
@@ -117,6 +160,9 @@ module.exports = async (connection, path, options) => {
|
|
|
117
160
|
}
|
|
118
161
|
}
|
|
119
162
|
},
|
|
163
|
+
|
|
164
|
+
// Untagged FLAGS response lists all flags defined for this mailbox
|
|
165
|
+
// (both system flags and custom flags). Example: * FLAGS (\Seen \Answered \Flagged)
|
|
120
166
|
FLAGS: async untagged => {
|
|
121
167
|
if (!untagged.attributes || (!untagged.attributes.length && Array.isArray(untagged.attributes[0]))) {
|
|
122
168
|
return;
|
|
@@ -124,6 +170,9 @@ module.exports = async (connection, path, options) => {
|
|
|
124
170
|
let flags = untagged.attributes[0].map(flag => (typeof flag.value === 'string' ? flag.value : false)).filter(flag => flag);
|
|
125
171
|
map.flags = new Set(flags);
|
|
126
172
|
},
|
|
173
|
+
|
|
174
|
+
// Untagged EXISTS response: "* <count> EXISTS" tells us the total number
|
|
175
|
+
// of messages in the mailbox. The count is in the command field (numeric prefix).
|
|
127
176
|
EXISTS: async untagged => {
|
|
128
177
|
let num = Number(untagged.command);
|
|
129
178
|
if (isNaN(num)) {
|
|
@@ -132,58 +181,51 @@ module.exports = async (connection, path, options) => {
|
|
|
132
181
|
|
|
133
182
|
map.exists = num;
|
|
134
183
|
},
|
|
184
|
+
|
|
185
|
+
// VANISHED responses (QRESYNC): server reports UIDs that have been expunged
|
|
186
|
+
// since the client's last known state. Only received when QRESYNC was requested.
|
|
187
|
+
// A dummy mailbox object is passed because the mailbox isn't officially open yet.
|
|
135
188
|
VANISHED: async untagged => {
|
|
136
|
-
await connection.untaggedVanished(
|
|
137
|
-
untagged,
|
|
138
|
-
// mailbox is not yet open, so use a dummy mailbox object
|
|
139
|
-
{ path, uidNext: false, uidValidity: false }
|
|
140
|
-
);
|
|
189
|
+
await connection.untaggedVanished(untagged, { path, uidNext: false, uidValidity: false });
|
|
141
190
|
},
|
|
142
|
-
|
|
191
|
+
|
|
192
|
+
// Untagged FETCH during SELECT/EXAMINE: only occurs with QRESYNC, delivering
|
|
193
|
+
// updated flags for messages that changed since the client's last modseq.
|
|
143
194
|
FETCH: async untagged => {
|
|
144
|
-
await connection.untaggedFetch(
|
|
145
|
-
untagged,
|
|
146
|
-
// mailbox is not yet open, so use a dummy mailbox object
|
|
147
|
-
{ path, uidNext: false, uidValidity: false }
|
|
148
|
-
);
|
|
195
|
+
await connection.untaggedFetch(untagged, { path, uidNext: false, uidValidity: false });
|
|
149
196
|
}
|
|
150
197
|
}
|
|
151
198
|
});
|
|
152
199
|
|
|
200
|
+
// The tagged OK response to SELECT/EXAMINE includes [READ-ONLY] or [READ-WRITE]
|
|
201
|
+
// in its response code, indicating the access mode the server granted.
|
|
153
202
|
let section = !response.response.attributes[0].value && response.response.attributes[0].section;
|
|
154
203
|
if (section && section.length && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
|
155
|
-
|
|
156
|
-
case 'READ-ONLY':
|
|
157
|
-
map.readOnly = true;
|
|
158
|
-
break;
|
|
159
|
-
case 'READ-WRITE':
|
|
160
|
-
default:
|
|
161
|
-
map.readOnly = false;
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
204
|
+
map.readOnly = section[0].value.toUpperCase() === 'READ-ONLY';
|
|
164
205
|
}
|
|
165
206
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
!map.highestModseq ||
|
|
172
|
-
// NOMODSEQ is not allowed
|
|
173
|
-
map.noModseq)
|
|
174
|
-
) {
|
|
175
|
-
// QRESYNC does not apply here, so unset it
|
|
207
|
+
// Validate QRESYNC preconditions (RFC 7162 Section 3.2.5):
|
|
208
|
+
// QRESYNC results are only valid if UIDVALIDITY matches, HIGHESTMODSEQ is
|
|
209
|
+
// present, and the mailbox supports mod-sequences. If any condition fails,
|
|
210
|
+
// the client cannot trust the incremental updates and must do a full resync.
|
|
211
|
+
if (map.qresync && (options.uidValidity !== map.uidValidity || !map.highestModseq || map.noModseq)) {
|
|
176
212
|
map.qresync = false;
|
|
177
213
|
}
|
|
178
214
|
|
|
215
|
+
// Transition mailbox state: save previous mailbox reference, temporarily
|
|
216
|
+
// clear it, then emit events and set the new mailbox.
|
|
179
217
|
let currentMailbox = connection.mailbox;
|
|
180
218
|
connection.mailbox = false;
|
|
181
219
|
|
|
220
|
+
// Emit mailboxClose if we're switching from a different mailbox.
|
|
221
|
+
// Re-selecting the same mailbox (e.g., for resync) does not trigger close/open.
|
|
182
222
|
if (currentMailbox && currentMailbox.path !== path) {
|
|
183
223
|
connection.emit('mailboxClose', currentMailbox);
|
|
184
224
|
}
|
|
185
225
|
|
|
186
226
|
connection.mailbox = map;
|
|
227
|
+
// Save the SELECT command for potential re-use (e.g., NOOP fallback polling
|
|
228
|
+
// re-issues the SELECT to detect changes on servers without IDLE support).
|
|
187
229
|
connection.currentSelectCommand = selectCommand;
|
|
188
230
|
connection.state = connection.states.SELECTED;
|
|
189
231
|
|
|
@@ -196,9 +238,10 @@ module.exports = async (connection, path, options) => {
|
|
|
196
238
|
} catch (err) {
|
|
197
239
|
await enhanceCommandError(err);
|
|
198
240
|
|
|
241
|
+
// If SELECT/EXAMINE fails while a mailbox was already selected, we must
|
|
242
|
+
// reset to AUTHENTICATED state since the server has implicitly deselected
|
|
243
|
+
// the previous mailbox on failure (RFC 3501 Section 6.3.1).
|
|
199
244
|
if (connection.state === connection.states.SELECTED) {
|
|
200
|
-
// reset selected state
|
|
201
|
-
|
|
202
245
|
let currentMailbox = connection.mailbox;
|
|
203
246
|
|
|
204
247
|
connection.mailbox = false;
|
package/lib/commands/starttls.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Initiates STARTTLS connection upgrade.
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} connection - IMAP connection instance
|
|
7
|
+
* @returns {Promise<boolean>} True if STARTTLS was initiated, false if not supported or already secure
|
|
8
|
+
*/
|
|
4
9
|
module.exports = async connection => {
|
|
5
10
|
if (!connection.capabilities.has('STARTTLS') || connection.secureConnection) {
|
|
6
11
|
// nothing to do here
|
package/lib/commands/status.js
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const { encodePath, normalizePath } = require('../tools.js');
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Requests status information about a mailbox.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} connection - IMAP connection instance
|
|
9
|
+
* @param {string} path - Mailbox path to query
|
|
10
|
+
* @param {Object} query - Status data items to request (e.g., {messages: true, uidNext: true, unseen: true})
|
|
11
|
+
* @returns {Promise<{path: string, messages?: number, recent?: number, uidNext?: number, uidValidity?: BigInt, unseen?: number, highestModseq?: BigInt}|boolean>} Status information object, or false if preconditions not met or on failure
|
|
12
|
+
* @throws {Error} If the mailbox does not exist
|
|
13
|
+
*/
|
|
6
14
|
module.exports = async (connection, path, query) => {
|
|
7
15
|
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state) || !path) {
|
|
8
16
|
// nothing to do here
|
|
@@ -12,8 +20,12 @@ module.exports = async (connection, path, query) => {
|
|
|
12
20
|
path = normalizePath(connection, path);
|
|
13
21
|
let encodedPath = encodePath(connection, path);
|
|
14
22
|
|
|
23
|
+
// Use quoted STRING if the encoded path contains '&' (modified UTF-7 marker),
|
|
24
|
+
// otherwise use unquoted ATOM. Same approach as in SELECT.
|
|
15
25
|
let attributes = [{ type: encodedPath.indexOf('&') >= 0 ? 'STRING' : 'ATOM', value: encodedPath }];
|
|
16
26
|
|
|
27
|
+
// Build the list of STATUS data items the caller wants.
|
|
28
|
+
// HIGHESTMODSEQ requires the CONDSTORE extension to be available.
|
|
17
29
|
let queryAttributes = [];
|
|
18
30
|
Object.keys(query || {}).forEach(key => {
|
|
19
31
|
if (!query[key]) {
|
|
@@ -48,14 +60,50 @@ module.exports = async (connection, path, query) => {
|
|
|
48
60
|
let map = { path };
|
|
49
61
|
response = await connection.exec('STATUS', attributes, {
|
|
50
62
|
untagged: {
|
|
63
|
+
// STATUS response: * STATUS <mailbox> (<key> <value> <key> <value> ...)
|
|
64
|
+
// Parsed as alternating key-value pairs (i % 2 pattern).
|
|
51
65
|
STATUS: async untagged => {
|
|
52
|
-
// If
|
|
66
|
+
// If querying the currently selected mailbox, also update the
|
|
67
|
+
// connection's live mailbox state and emit events for changes.
|
|
53
68
|
let updateCurrent = connection.state === connection.states.SELECTED && path === connection.mailbox.path;
|
|
54
69
|
|
|
55
70
|
let list = untagged.attributes && Array.isArray(untagged.attributes[1]) ? untagged.attributes[1] : false;
|
|
56
71
|
if (!list) {
|
|
57
72
|
return;
|
|
58
73
|
}
|
|
74
|
+
// Maps IMAP STATUS field names to their output key names, type parsers,
|
|
75
|
+
// and optional callbacks to update the live mailbox state.
|
|
76
|
+
const STATUS_FIELD_MAP = {
|
|
77
|
+
MESSAGES: {
|
|
78
|
+
key: 'messages',
|
|
79
|
+
parser: Number,
|
|
80
|
+
updateMailbox: (val, conn) => {
|
|
81
|
+
let prevCount = conn.mailbox.exists;
|
|
82
|
+
if (prevCount !== val) {
|
|
83
|
+
conn.mailbox.exists = val;
|
|
84
|
+
conn.emit('exists', { path, count: val, prevCount });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
RECENT: { key: 'recent', parser: Number },
|
|
89
|
+
UIDNEXT: {
|
|
90
|
+
key: 'uidNext',
|
|
91
|
+
parser: Number,
|
|
92
|
+
updateMailbox: (val, conn) => {
|
|
93
|
+
conn.mailbox.uidNext = val;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
UIDVALIDITY: { key: 'uidValidity', parser: BigInt },
|
|
97
|
+
UNSEEN: { key: 'unseen', parser: Number },
|
|
98
|
+
HIGHESTMODSEQ: {
|
|
99
|
+
key: 'highestModseq',
|
|
100
|
+
parser: BigInt,
|
|
101
|
+
updateMailbox: (val, conn) => {
|
|
102
|
+
conn.mailbox.highestModseq = val;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
59
107
|
let key;
|
|
60
108
|
list.forEach((entry, i) => {
|
|
61
109
|
if (i % 2 === 0) {
|
|
@@ -65,61 +113,22 @@ module.exports = async (connection, path, query) => {
|
|
|
65
113
|
if (!key || !entry || typeof entry.value !== 'string') {
|
|
66
114
|
return;
|
|
67
115
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
|
73
|
-
if (updateCurrent) {
|
|
74
|
-
let prevCount = connection.mailbox.exists;
|
|
75
|
-
if (prevCount !== value) {
|
|
76
|
-
// somehow message count in current folder has changed?
|
|
77
|
-
connection.mailbox.exists = value;
|
|
78
|
-
connection.emit('exists', {
|
|
79
|
-
path,
|
|
80
|
-
count: value,
|
|
81
|
-
prevCount
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
break;
|
|
86
|
-
|
|
87
|
-
case 'RECENT':
|
|
88
|
-
key = 'recent';
|
|
89
|
-
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
|
90
|
-
break;
|
|
91
|
-
|
|
92
|
-
case 'UIDNEXT':
|
|
93
|
-
key = 'uidNext';
|
|
94
|
-
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
|
95
|
-
if (updateCurrent) {
|
|
96
|
-
connection.mailbox.uidNext = value;
|
|
97
|
-
}
|
|
98
|
-
break;
|
|
99
|
-
|
|
100
|
-
case 'UIDVALIDITY':
|
|
101
|
-
key = 'uidValidity';
|
|
102
|
-
value = !isNaN(entry.value) ? BigInt(entry.value) : false;
|
|
103
|
-
break;
|
|
104
|
-
|
|
105
|
-
case 'UNSEEN':
|
|
106
|
-
key = 'unseen';
|
|
107
|
-
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
|
108
|
-
break;
|
|
109
|
-
|
|
110
|
-
case 'HIGHESTMODSEQ':
|
|
111
|
-
key = 'highestModseq';
|
|
112
|
-
value = !isNaN(entry.value) ? BigInt(entry.value) : false;
|
|
113
|
-
if (updateCurrent) {
|
|
114
|
-
connection.mailbox.highestModseq = value;
|
|
115
|
-
}
|
|
116
|
-
break;
|
|
116
|
+
|
|
117
|
+
const fieldConfig = STATUS_FIELD_MAP[key.toUpperCase()];
|
|
118
|
+
if (!fieldConfig) {
|
|
119
|
+
return;
|
|
117
120
|
}
|
|
121
|
+
|
|
122
|
+
const value = !isNaN(entry.value) ? fieldConfig.parser(entry.value) : false;
|
|
118
123
|
if (value === false) {
|
|
119
124
|
return;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
map[key] = value;
|
|
127
|
+
map[fieldConfig.key] = value;
|
|
128
|
+
|
|
129
|
+
if (updateCurrent && fieldConfig.updateMailbox) {
|
|
130
|
+
fieldConfig.updateMailbox(value, connection);
|
|
131
|
+
}
|
|
123
132
|
});
|
|
124
133
|
}
|
|
125
134
|
}
|
|
@@ -127,6 +136,9 @@ module.exports = async (connection, path, query) => {
|
|
|
127
136
|
response.next();
|
|
128
137
|
return map;
|
|
129
138
|
} catch (err) {
|
|
139
|
+
// A NO response usually means the mailbox doesn't exist. Verify by
|
|
140
|
+
// running LIST -- if no results, throw a clear NotFound error instead
|
|
141
|
+
// of the generic IMAP error.
|
|
130
142
|
if (err.responseStatus === 'NO') {
|
|
131
143
|
let folders = await connection.run('LIST', '', path, { listOnly: true });
|
|
132
144
|
if (folders && !folders.length) {
|
package/lib/commands/store.js
CHANGED
|
@@ -2,7 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
const { formatFlag, canUseFlag, enhanceCommandError } = require('../tools.js');
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Updates flags or labels for messages in the selected mailbox.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} connection - IMAP connection instance
|
|
9
|
+
* @param {string} range - Message sequence number or UID range
|
|
10
|
+
* @param {string|string[]} flags - Flag(s) to set, add, or remove
|
|
11
|
+
* @param {Object} options - Store options
|
|
12
|
+
* @param {boolean} [options.uid] - If true, use UID STORE instead of STORE
|
|
13
|
+
* @param {boolean} [options.useLabels] - If true, operate on Gmail labels instead of flags
|
|
14
|
+
* @param {boolean} [options.silent] - If true, use .SILENT variant to suppress server response
|
|
15
|
+
* @param {string} [options.operation] - Operation type: 'set', 'add', or 'remove'
|
|
16
|
+
* @param {string} [options.unchangedSince] - Only update messages not changed since this modseq value
|
|
17
|
+
* @returns {Promise<boolean>} True on success, false on failure or if nothing to do
|
|
18
|
+
*/
|
|
6
19
|
module.exports = async (connection, range, flags, options) => {
|
|
7
20
|
if (connection.state !== connection.states.SELECTED || !range || (options.useLabels && !connection.capabilities.has('X-GM-EXT-1'))) {
|
|
8
21
|
// nothing to do here
|
|
@@ -10,19 +23,25 @@ module.exports = async (connection, range, flags, options) => {
|
|
|
10
23
|
}
|
|
11
24
|
|
|
12
25
|
options = options || {};
|
|
26
|
+
|
|
27
|
+
// Build the IMAP STORE operation name. The format is:
|
|
28
|
+
// [+|-]FLAGS[.SILENT] or [+|-]X-GM-LABELS
|
|
29
|
+
// Where: no prefix = replace all, + = add, - = remove
|
|
30
|
+
// .SILENT suppresses the server from sending back updated flags (saves bandwidth).
|
|
13
31
|
let operation;
|
|
14
32
|
|
|
15
33
|
operation = 'FLAGS';
|
|
16
34
|
|
|
17
35
|
if (options.useLabels) {
|
|
36
|
+
// Gmail labels (X-GM-EXT-1 extension): operates on labels instead of IMAP flags
|
|
18
37
|
operation = 'X-GM-LABELS';
|
|
19
38
|
} else if (options.silent) {
|
|
20
39
|
operation = `${operation}.SILENT`;
|
|
21
40
|
}
|
|
22
41
|
|
|
42
|
+
// Prefix determines the operation: none = set (replace), + = add, - = remove
|
|
23
43
|
switch ((options.operation || '').toLowerCase()) {
|
|
24
44
|
case 'set':
|
|
25
|
-
// do nothing, keep operation value as is
|
|
26
45
|
break;
|
|
27
46
|
case 'remove':
|
|
28
47
|
operation = `-${operation}`;
|
|
@@ -33,12 +52,14 @@ module.exports = async (connection, range, flags, options) => {
|
|
|
33
52
|
break;
|
|
34
53
|
}
|
|
35
54
|
|
|
55
|
+
// Validate each flag: format it (normalize backslash prefix for system flags),
|
|
56
|
+
// then check if the mailbox's permanentFlags allow it. Removal is always allowed
|
|
57
|
+
// since it doesn't require the flag to be in permanentFlags.
|
|
36
58
|
flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
|
|
37
59
|
.map(flag => {
|
|
38
60
|
flag = formatFlag(flag);
|
|
39
61
|
|
|
40
62
|
if (!canUseFlag(connection.mailbox, flag) && operation !== 'remove') {
|
|
41
|
-
// it does not seem that we can set this flag
|
|
42
63
|
return false;
|
|
43
64
|
}
|
|
44
65
|
|
|
@@ -46,13 +67,15 @@ module.exports = async (connection, range, flags, options) => {
|
|
|
46
67
|
})
|
|
47
68
|
.filter(flag => flag);
|
|
48
69
|
|
|
70
|
+
// Allow empty flags only for 'set' operation (which clears all flags)
|
|
49
71
|
if (!flags.length && options.operation !== 'set') {
|
|
50
|
-
// nothing to do here
|
|
51
72
|
return false;
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
let attributes = [{ type: 'SEQUENCE', value: range }, { type: 'ATOM', value: operation }, flags.map(flag => ({ type: 'ATOM', value: flag }))];
|
|
55
76
|
|
|
77
|
+
// CONDSTORE (RFC 7162): UNCHANGEDSINCE modifier prevents updating messages whose
|
|
78
|
+
// mod-sequence is higher than the specified value, avoiding overwriting concurrent changes.
|
|
56
79
|
if (options.unchangedSince && connection.enabled.has('CONDSTORE') && !connection.mailbox.noModseq) {
|
|
57
80
|
attributes.push([
|
|
58
81
|
{
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Subscribes to a mailbox.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} connection - IMAP connection instance
|
|
9
|
+
* @param {string} path - Mailbox path to subscribe to
|
|
10
|
+
* @returns {Promise<boolean|undefined>} True on success, false on failure, or undefined if preconditions not met
|
|
11
|
+
*/
|
|
6
12
|
module.exports = async (connection, path) => {
|
|
7
13
|
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
|
8
14
|
// nothing to do here
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Unsubscribes from a mailbox.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} connection - IMAP connection instance
|
|
9
|
+
* @param {string} path - Mailbox path to unsubscribe from
|
|
10
|
+
* @returns {Promise<boolean|undefined>} True on success, false on failure, or undefined if preconditions not met
|
|
11
|
+
*/
|
|
6
12
|
module.exports = async (connection, path) => {
|
|
7
13
|
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
|
8
14
|
// nothing to do here
|