imapkit 0.0.0-stage → 4.0.1
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/LICENSE +16 -0
- package/README.md +608 -2
- package/bin/help.txt +98 -0
- package/bin/imapkit.js +108 -0
- package/cert/server.crt +20 -0
- package/cert/server.key +28 -0
- package/lib/addressparser.js +283 -0
- package/lib/arguments.js +112 -0
- package/lib/bodystructure.js +149 -0
- package/lib/command-states.js +109 -0
- package/lib/commands/append.js +313 -0
- package/lib/commands/capability.js +47 -0
- package/lib/commands/check.js +21 -0
- package/lib/commands/close.js +30 -0
- package/lib/commands/copy.js +115 -0
- package/lib/commands/create.js +52 -0
- package/lib/commands/delete.js +64 -0
- package/lib/commands/examine.js +7 -0
- package/lib/commands/expunge.js +27 -0
- package/lib/commands/fetch.js +229 -0
- package/lib/commands/handlers/fetch.js +209 -0
- package/lib/commands/handlers/flags.js +42 -0
- package/lib/commands/handlers/search.js +519 -0
- package/lib/commands/handlers/status.js +85 -0
- package/lib/commands/handlers/store.js +127 -0
- package/lib/commands/list.js +100 -0
- package/lib/commands/login.js +67 -0
- package/lib/commands/logout.js +41 -0
- package/lib/commands/lsub.js +87 -0
- package/lib/commands/noop.js +21 -0
- package/lib/commands/rename.js +102 -0
- package/lib/commands/search.js +76 -0
- package/lib/commands/select.js +289 -0
- package/lib/commands/status.js +63 -0
- package/lib/commands/store.js +151 -0
- package/lib/commands/subscribe.js +53 -0
- package/lib/commands/uid copy.js +7 -0
- package/lib/commands/uid fetch.js +5 -0
- package/lib/commands/uid search.js +5 -0
- package/lib/commands/uid store.js +5 -0
- package/lib/commands/unsubscribe.js +50 -0
- package/lib/dates.js +123 -0
- package/lib/deflate-layer.js +232 -0
- package/lib/envelope.js +82 -0
- package/lib/esearch.js +208 -0
- package/lib/framing.js +102 -0
- package/lib/list-extensions.js +36 -0
- package/lib/load-plugins.js +109 -0
- package/lib/mailbox-name.js +133 -0
- package/lib/mimeparser.js +778 -0
- package/lib/mock-client.js +233 -0
- package/lib/numbers.js +52 -0
- package/lib/plugins/acl.js +964 -0
- package/lib/plugins/appendlimit.js +83 -0
- package/lib/plugins/auth-plain.js +94 -0
- package/lib/plugins/binary.js +256 -0
- package/lib/plugins/catenate.js +253 -0
- package/lib/plugins/compress.js +76 -0
- package/lib/plugins/condstore.js +563 -0
- package/lib/plugins/context-search.js +321 -0
- package/lib/plugins/context-sort.js +19 -0
- package/lib/plugins/create-special-use.js +108 -0
- package/lib/plugins/enable.js +155 -0
- package/lib/plugins/esearch.js +156 -0
- package/lib/plugins/esort.js +60 -0
- package/lib/plugins/id.js +138 -0
- package/lib/plugins/idle.js +105 -0
- package/lib/plugins/imap4rev2.js +202 -0
- package/lib/plugins/list-extended.js +258 -0
- package/lib/plugins/list-status.js +31 -0
- package/lib/plugins/literalminus.js +20 -0
- package/lib/plugins/literalplus.js +18 -0
- package/lib/plugins/logindisabled.js +50 -0
- package/lib/plugins/messagelimit.js +234 -0
- package/lib/plugins/metadata-server.js +13 -0
- package/lib/plugins/metadata.js +475 -0
- package/lib/plugins/move.js +110 -0
- package/lib/plugins/multiappend.js +26 -0
- package/lib/plugins/multisearch.js +269 -0
- package/lib/plugins/namespace.js +67 -0
- package/lib/plugins/notify.js +654 -0
- package/lib/plugins/oauthbearer.js +217 -0
- package/lib/plugins/objectid.js +243 -0
- package/lib/plugins/partial.js +68 -0
- package/lib/plugins/preview.js +400 -0
- package/lib/plugins/qresync.js +525 -0
- package/lib/plugins/quota.js +285 -0
- package/lib/plugins/replace.js +145 -0
- package/lib/plugins/sasl-ir.js +12 -0
- package/lib/plugins/savedate.js +59 -0
- package/lib/plugins/savelimit.js +18 -0
- package/lib/plugins/searchres.js +82 -0
- package/lib/plugins/sort-display.js +23 -0
- package/lib/plugins/sort.js +132 -0
- package/lib/plugins/special-use.js +95 -0
- package/lib/plugins/starttls.js +57 -0
- package/lib/plugins/status-size.js +19 -0
- package/lib/plugins/thread-orderedsubject.js +16 -0
- package/lib/plugins/thread-references.js +16 -0
- package/lib/plugins/uidonly.js +135 -0
- package/lib/plugins/uidplus.js +124 -0
- package/lib/plugins/unauthenticate.js +28 -0
- package/lib/plugins/unselect.js +36 -0
- package/lib/plugins/utf8-accept.js +68 -0
- package/lib/plugins/x-gm-ext-1.js +456 -0
- package/lib/plugins/xoauth2.js +188 -0
- package/lib/plugins/xtoybird.js +282 -0
- package/lib/server.js +2880 -0
- package/lib/smtp-listener.js +51 -0
- package/lib/sorting.js +373 -0
- package/lib/threading.js +357 -0
- package/lib/utf8-session.js +123 -0
- package/lib/vanished.js +57 -0
- package/package.json +61 -5
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { updateSession } = require('../utf8-session');
|
|
4
|
+
const { registerEnable } = require('./enable');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @help Adds UTF8=ACCEPT [RFC9755] capability, loads ENABLE as well
|
|
8
|
+
* @help After ENABLE UTF8=ACCEPT mailbox names are UTF-8 instead of
|
|
9
|
+
* @help modified UTF-7 and strings are sent quoted as UTF-8
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks if the header of a message holds 8-bit octets
|
|
14
|
+
*
|
|
15
|
+
* @param {String} raw Message source as a binary string
|
|
16
|
+
* @return {Boolean} true if the header has 8-bit octets
|
|
17
|
+
*/
|
|
18
|
+
function has8bitHeader(raw) {
|
|
19
|
+
const match = raw.match(/\r?\n\r?\n/);
|
|
20
|
+
return /[\x80-\xff]/.test(match ? raw.substr(0, match.index) : raw);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = function (server) {
|
|
24
|
+
// RFC 9755 section 3: the client turns UTF-8 support on with ENABLE
|
|
25
|
+
server.registerCapability('UTF8=ACCEPT');
|
|
26
|
+
|
|
27
|
+
registerEnable(server, 'UTF8=ACCEPT');
|
|
28
|
+
|
|
29
|
+
// RFC 9755 section 3: a server that supports UTF8=ACCEPT accepts UTF-8 in quoted strings. Before
|
|
30
|
+
// ENABLE, mailbox names must still be modified UTF-7 and SEARCH needs CHARSET UTF-8 for 8-bit strings.
|
|
31
|
+
// See lib/utf8-session.js, the session state is shared with IMAP4rev2
|
|
32
|
+
server.utf8Accept = true;
|
|
33
|
+
server.connectionHandlers.push(updateSession);
|
|
34
|
+
// RFC 8437 section 3: UNAUTHENTICATE resets the ENABLEd extensions
|
|
35
|
+
server.resetHandlers.push(updateSession);
|
|
36
|
+
|
|
37
|
+
server.outputHandlers.push((connection, response, description, parsed, data, extra) => {
|
|
38
|
+
if (description === 'ENABLED' && Array.isArray(extra) && extra.indexOf('UTF8=ACCEPT') >= 0) {
|
|
39
|
+
// strings that are valid UTF-8 are sent quoted, mailbox names are UTF-8 in both directions
|
|
40
|
+
// ("&" is an ordinary character), SEARCH strings are UTF-8 and CHARSET is refused
|
|
41
|
+
updateSession(connection);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// RFC 9755 section 4: without ENABLE UTF8=ACCEPT, APPEND MUST be refused with NO
|
|
46
|
+
// if the message header has 8-bit characters
|
|
47
|
+
const appendHandler = server.getCommandHandler('APPEND');
|
|
48
|
+
server.setCommandHandler('APPEND', (connection, parsed, data, callback) => {
|
|
49
|
+
const args = parsed.attributes || [];
|
|
50
|
+
const raw = args[args.length - 1];
|
|
51
|
+
if (
|
|
52
|
+
// UTF-8 is on after ENABLE UTF8=ACCEPT, or IMAP4rev2, which permits 8-bit characters in the message
|
|
53
|
+
// (RFC 9051 section 6.3.12) and which RFC 9755 does not extend
|
|
54
|
+
!connection.utf8Enabled &&
|
|
55
|
+
args.length >= 2 &&
|
|
56
|
+
raw &&
|
|
57
|
+
['LITERAL', 'LITERAL8'].indexOf(raw.type) >= 0 &&
|
|
58
|
+
has8bitHeader(raw.value || '')
|
|
59
|
+
) {
|
|
60
|
+
connection.sendStatus(parsed, data, 'NO', 'Message header has 8-bit characters, ENABLE UTF8=ACCEPT first (RFC 9755 section 4)');
|
|
61
|
+
return callback();
|
|
62
|
+
}
|
|
63
|
+
return appendHandler(connection, parsed, data, callback);
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ENABLE UTF8=ACCEPT is the only way to use this extension
|
|
68
|
+
module.exports.requires = ['ENABLE'];
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const search = require('../commands/handlers/search');
|
|
4
|
+
const { MONTHS, isRealDate } = require('../dates');
|
|
5
|
+
|
|
6
|
+
const { badError } = search;
|
|
7
|
+
|
|
8
|
+
// X-GM-MSGID is a 64 bit unsigned number, so it is tracked as a BigInt
|
|
9
|
+
|
|
10
|
+
// Sample value from Gmail IMAP extensions API page
|
|
11
|
+
// https://developers.google.com/workspace/gmail/imap/imap-extensions
|
|
12
|
+
// Used as default, if server.options["HIGHESTX-GM-MSGID"]
|
|
13
|
+
// is missing
|
|
14
|
+
const SEED = '1278455344230334865';
|
|
15
|
+
|
|
16
|
+
const MAX_UINT64 = 2n ** 64n - 1n;
|
|
17
|
+
|
|
18
|
+
// X-GM-RAW mailbox shortcuts of in: and is: (Gmail search operators), as ImapKit criteria
|
|
19
|
+
const RAW_SHORTCUTS = {
|
|
20
|
+
'in:inbox': ['X-GM-LABELS', '\\Inbox'],
|
|
21
|
+
'in:sent': ['X-GM-LABELS', '\\Sent'],
|
|
22
|
+
'in:drafts': ['X-GM-LABELS', '\\Drafts'],
|
|
23
|
+
'in:trash': ['X-GM-LABELS', '\\Trash'],
|
|
24
|
+
'in:spam': ['X-GM-LABELS', '\\Junk'],
|
|
25
|
+
'in:anywhere': ['ALL'],
|
|
26
|
+
'is:unread': ['UNSEEN'],
|
|
27
|
+
'is:read': ['SEEN'],
|
|
28
|
+
'is:starred': ['FLAGGED'],
|
|
29
|
+
'is:important': ['X-GM-LABELS', '\\Important']
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// X-GM-RAW operators that take a value, as the search key the value is given to
|
|
33
|
+
const RAW_OPERATORS = {
|
|
34
|
+
from: 'FROM',
|
|
35
|
+
to: 'TO',
|
|
36
|
+
cc: 'CC',
|
|
37
|
+
bcc: 'BCC',
|
|
38
|
+
subject: 'SUBJECT',
|
|
39
|
+
label: 'X-GM-LABELS',
|
|
40
|
+
larger: 'LARGER',
|
|
41
|
+
smaller: 'SMALLER',
|
|
42
|
+
after: 'SINCE',
|
|
43
|
+
before: 'BEFORE',
|
|
44
|
+
rfc822msgid: 'HEADER'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Gmail operators that ImapKit does not implement, answered with NO instead of a wrong result
|
|
48
|
+
const RAW_UNSUPPORTED = ['is', 'has', 'list', 'filename', 'around', 'size', 'older', 'newer', 'older_than', 'newer_than', 'deliveredto', 'category'];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @help Adds Gmail specific X-GM-EXT-1 capability
|
|
52
|
+
* @help (https://developers.google.com/workspace/gmail/imap/imap-extensions)
|
|
53
|
+
* @help X-GM-MSGID and X-GM-THRID: FETCH and SEARCH. X-GM-THRID is the
|
|
54
|
+
* @help X-GM-MSGID of the message unless storage sets an X-GM-THRID
|
|
55
|
+
* @help value, so every message is its own thread unless the
|
|
56
|
+
* @help fixture groups messages. With OBJECTID, the messages of a
|
|
57
|
+
* @help THREADID share the X-GM-THRID of its first message
|
|
58
|
+
* @help X-GM-LABELS: FETCH, STORE (+/-, .SILENT) and SEARCH. System
|
|
59
|
+
* @help labels are atoms that start with "\" (\Inbox for INBOX, the
|
|
60
|
+
* @help special-use attribute for special-use mailboxes), other labels
|
|
61
|
+
* @help are mailbox names, sent like mailbox names (modified UTF-7, or
|
|
62
|
+
* @help UTF-8 after ENABLE UTF8=ACCEPT, quoted when not atoms). Labels
|
|
63
|
+
* @help have no side effects: the message is not copied to or removed
|
|
64
|
+
* @help from other mailboxes. In SEARCH a label that starts with "\"
|
|
65
|
+
* @help is a system label
|
|
66
|
+
* @help X-GM-RAW: a subset of the Gmail search syntax. Words and "quoted
|
|
67
|
+
* @help phrases" (TEXT), -term, OR, (groups), {any of}, from: to: cc:
|
|
68
|
+
* @help bcc: subject: label: in: (inbox sent drafts trash spam
|
|
69
|
+
* @help anywhere, or a label) is: (read unread starred important)
|
|
70
|
+
* @help larger: smaller: (k and m suffixes) after: before:
|
|
71
|
+
* @help (YYYY/MM/DD) and rfc822msgid:. Other Gmail operators are
|
|
72
|
+
* @help answered with NO
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
module.exports = function (server) {
|
|
76
|
+
server.registerCapability('X-GM-EXT-1');
|
|
77
|
+
|
|
78
|
+
server['HIGHESTX-GM-MSGID'] = BigInt(server.options['HIGHESTX-GM-MSGID'] || SEED);
|
|
79
|
+
|
|
80
|
+
// set X-GM-MSGID values when message is created / initialized
|
|
81
|
+
server.messageHandlers.push((connection, message, mailbox) => {
|
|
82
|
+
let labels;
|
|
83
|
+
|
|
84
|
+
if (!message['X-GM-MSGID']) {
|
|
85
|
+
server['HIGHESTX-GM-MSGID'] += 1n;
|
|
86
|
+
message['X-GM-MSGID'] = server['HIGHESTX-GM-MSGID'].toString();
|
|
87
|
+
} else if (/^\d+$/.test(message['X-GM-MSGID']) && BigInt(message['X-GM-MSGID']) > server['HIGHESTX-GM-MSGID']) {
|
|
88
|
+
// Storage might be shared with another server instance, do not reuse existing values
|
|
89
|
+
server['HIGHESTX-GM-MSGID'] = BigInt(message['X-GM-MSGID']);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Ensure message has an array of labels
|
|
93
|
+
message['X-GM-LABELS'] = [].concat(message['X-GM-LABELS'] || []);
|
|
94
|
+
|
|
95
|
+
if (mailbox.path.toUpperCase() === 'INBOX') {
|
|
96
|
+
labels = ['\\Inbox'];
|
|
97
|
+
} else if (mailbox['special-use'] && mailbox['special-use'].length) {
|
|
98
|
+
labels = [].concat(mailbox['special-use']);
|
|
99
|
+
} else {
|
|
100
|
+
labels = [userLabel(mailbox.path)];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
labels.forEach(label => {
|
|
104
|
+
server.ensureFlag(message['X-GM-LABELS'], label);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// A message starts its own thread unless storage puts it into one. With OBJECTID loaded, the messages of a THREADID
|
|
109
|
+
// (RFC 8474 section 5.2) share an X-GM-THRID: the one of the first message of that thread, like Gmail uses the
|
|
110
|
+
// X-GM-MSGID of the first message. This runs after every other message handler, so OBJECTID set the THREADID
|
|
111
|
+
// already, whatever the load order is
|
|
112
|
+
const gmThreads = new Map();
|
|
113
|
+
server.once('pluginsLoaded', () => {
|
|
114
|
+
server.messageHandlers.push((server, message) => {
|
|
115
|
+
const known = message.THREADID && gmThreads.get(message.THREADID);
|
|
116
|
+
if (!message['X-GM-THRID']) {
|
|
117
|
+
message['X-GM-THRID'] = known || message['X-GM-MSGID'];
|
|
118
|
+
}
|
|
119
|
+
if (message.THREADID && !known) {
|
|
120
|
+
gmThreads.set(message.THREADID, message['X-GM-THRID']);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Retrieve X-GM-MSGID values with FETCH
|
|
126
|
+
server.fetchHandlers['X-GM-MSGID'] = function (connection, message) {
|
|
127
|
+
return {
|
|
128
|
+
type: 'ATOM',
|
|
129
|
+
value: message['X-GM-MSGID']
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Retrieve X-GM-LABELS values with FETCH
|
|
134
|
+
server.fetchHandlers['X-GM-LABELS'] = function (connection, message) {
|
|
135
|
+
return formatLabels(message);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// X-GM-MSGID and X-GM-THRID are 64 bit unsigned numbers
|
|
139
|
+
const idArgument = () => [
|
|
140
|
+
value => {
|
|
141
|
+
if (!/^\d{1,20}$/.test(value) || BigInt(value) > MAX_UINT64) {
|
|
142
|
+
throw badError('Expected a 64 bit unsigned number');
|
|
143
|
+
}
|
|
144
|
+
return BigInt(value).toString();
|
|
145
|
+
}
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
server.searchHandlers['X-GM-MSGID'] = function (connection, message, sequence, xGmMsgid) {
|
|
149
|
+
return message['X-GM-MSGID'] === xGmMsgid;
|
|
150
|
+
};
|
|
151
|
+
server.searchHandlers['X-GM-MSGID'].argumentTypes = idArgument;
|
|
152
|
+
|
|
153
|
+
// Retrieve X-GM-THRID values with FETCH
|
|
154
|
+
server.fetchHandlers['X-GM-THRID'] = function (connection, message) {
|
|
155
|
+
return {
|
|
156
|
+
type: 'ATOM',
|
|
157
|
+
value: message['X-GM-THRID']
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
server.searchHandlers['X-GM-THRID'] = function (connection, message, sequence, xGmThrid) {
|
|
162
|
+
return message['X-GM-THRID'] === xGmThrid;
|
|
163
|
+
};
|
|
164
|
+
server.searchHandlers['X-GM-THRID'].argumentTypes = idArgument;
|
|
165
|
+
|
|
166
|
+
// SEARCH X-GM-LABELS label, the label is given in the form the session uses for mailbox names
|
|
167
|
+
server.searchHandlers['X-GM-LABELS'] = function (connection, message, sequence, label) {
|
|
168
|
+
return message['X-GM-LABELS'].some(stored => {
|
|
169
|
+
if (isSystemLabel(stored)) {
|
|
170
|
+
// system labels are matched without case, like flags
|
|
171
|
+
return stored.toLowerCase() === label.toLowerCase();
|
|
172
|
+
}
|
|
173
|
+
return connection.exportMailboxName(labelName(stored)) === label;
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// SEARCH X-GM-RAW query, a subset of the Gmail search syntax
|
|
178
|
+
// the translated criteria are compiled once per SEARCH, they are the same array for every message
|
|
179
|
+
const rawMatchers = new WeakMap();
|
|
180
|
+
server.searchHandlers['X-GM-RAW'] = function (connection, message, sequence, criteria) {
|
|
181
|
+
if (!rawMatchers.has(criteria)) {
|
|
182
|
+
// without UTF8=ACCEPT the translated criteria take a CHARSET, Gmail queries are UTF-8
|
|
183
|
+
const params = connection.searchCharset ? criteria : ['CHARSET', 'UTF-8'].concat(criteria);
|
|
184
|
+
rawMatchers.set(criteria, search(connection, [], params, () => []).matches);
|
|
185
|
+
}
|
|
186
|
+
return rawMatchers.get(criteria)(message, sequence);
|
|
187
|
+
};
|
|
188
|
+
server.searchHandlers['X-GM-RAW'].argumentTypes = () => [parseRawQuery];
|
|
189
|
+
|
|
190
|
+
const setLabels = (message, labels) => {
|
|
191
|
+
message['X-GM-LABELS'] = [];
|
|
192
|
+
addLabels(message, labels);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const addLabels = (message, labels) => {
|
|
196
|
+
labels.forEach(label => server.ensureFlag(message['X-GM-LABELS'], label));
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const removeLabels = (message, labels) => {
|
|
200
|
+
labels.forEach(label => server.removeFlag(message['X-GM-LABELS'], label));
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
[
|
|
204
|
+
['X-GM-LABELS', setLabels],
|
|
205
|
+
['+X-GM-LABELS', addLabels],
|
|
206
|
+
['-X-GM-LABELS', removeLabels]
|
|
207
|
+
].forEach(([name, update]) => {
|
|
208
|
+
// all labels are parsed before the message changes, an invalid one fails the STORE
|
|
209
|
+
const apply = (connection, message, flags) =>
|
|
210
|
+
update(
|
|
211
|
+
message,
|
|
212
|
+
flags.map(flag => parseLabel(connection, flag))
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
server.storeHandlers[name] = function (connection, message, flags, index, parsed, data) {
|
|
216
|
+
apply(connection, message, flags);
|
|
217
|
+
sendLabelUpdate(connection, parsed, data, index, message);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
server.storeHandlers[name + '.SILENT'] = apply;
|
|
221
|
+
|
|
222
|
+
// labels are astrings (Gmail IMAP extensions): literals are accepted, NIL is a label name
|
|
223
|
+
server.storeHandlers[name].astringValues = server.storeHandlers[name + '.SILENT'].astringValues = true;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Gmail keeps the same X-GM-MSGID for a message in every mailbox, so copies get the value of the source message
|
|
227
|
+
server.copyHandlers.push((server, source, copy) => {
|
|
228
|
+
if (source['X-GM-MSGID']) {
|
|
229
|
+
copy['X-GM-MSGID'] = source['X-GM-MSGID'];
|
|
230
|
+
copy['X-GM-THRID'] = source['X-GM-THRID'];
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Labels are kept as strings in message["X-GM-LABELS"]. A system label like \Inbox starts with "\",
|
|
237
|
+
* any other label is a mailbox name in storage form. A mailbox name that itself starts with "\" is
|
|
238
|
+
* kept with one more "\" in front, so it can not be mistaken for a system label
|
|
239
|
+
*/
|
|
240
|
+
const isSystemLabel = label => label.charAt(0) === '\\' && label.charAt(1) !== '\\';
|
|
241
|
+
const userLabel = name => (name.charAt(0) === '\\' ? '\\' + name : name);
|
|
242
|
+
const labelName = label => (label.charAt(0) === '\\' ? label.substr(1) : label);
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Converts a STORE X-GM-LABELS value to a stored label. An atom that starts with "\" is a system
|
|
246
|
+
* label, anything else is a label name: an astring in the form the session uses for mailbox names
|
|
247
|
+
*
|
|
248
|
+
* @param {Object} connection IMAP connection
|
|
249
|
+
* @param {Object} attr Parsed value
|
|
250
|
+
* @return {String} Stored label
|
|
251
|
+
*/
|
|
252
|
+
function parseLabel(connection, attr) {
|
|
253
|
+
const value = String((attr && attr.value) || '');
|
|
254
|
+
if (attr.type === 'ATOM' && value.charAt(0) === '\\') {
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
return userLabel(connection.importMailboxName(value));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Formats the labels of a message for a FETCH response. Label names are MAILBOX attributes, so the
|
|
262
|
+
* session gets them in its own form and quoted when they are not atoms
|
|
263
|
+
*/
|
|
264
|
+
function formatLabels(message) {
|
|
265
|
+
return message['X-GM-LABELS'].map(label => (isSystemLabel(label) ? { type: 'ATOM', value: label } : { type: 'MAILBOX', value: labelName(label) }));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Translates an X-GM-RAW query (Gmail search syntax) to ImapKit search criteria. Terms are
|
|
270
|
+
* ANDed, OR binds tighter than that ("a b OR c" is a AND (b OR c)), "-" negates a term, ( ) groups
|
|
271
|
+
* terms and { } matches any of its terms.
|
|
272
|
+
*
|
|
273
|
+
* @param {String} query Query as a binary string, UTF-8
|
|
274
|
+
* @return {Array} Search criteria
|
|
275
|
+
* @throws {Error} BAD for a query that can not be parsed, NO for an unsupported operator
|
|
276
|
+
*/
|
|
277
|
+
function parseRawQuery(query) {
|
|
278
|
+
const tokens = [];
|
|
279
|
+
const re = /\s*(?:([(){}])|"([^"]*)"|([^\s(){}"]+?:)"([^"]*)"|([^\s(){}"]+))/gy;
|
|
280
|
+
let match;
|
|
281
|
+
// end of the last token, a failed sticky match resets lastIndex
|
|
282
|
+
let end = 0;
|
|
283
|
+
while (end < query.length && (match = re.exec(query))) {
|
|
284
|
+
end = re.lastIndex;
|
|
285
|
+
if (match[1]) {
|
|
286
|
+
tokens.push({ type: match[1] });
|
|
287
|
+
} else if (typeof match[2] === 'string') {
|
|
288
|
+
tokens.push({ type: 'word', value: match[2] });
|
|
289
|
+
} else if (match[3]) {
|
|
290
|
+
tokens.push({ type: 'word', value: match[3] + match[4], operator: match[3].slice(0, -1), operand: match[4] });
|
|
291
|
+
} else {
|
|
292
|
+
tokens.push({ type: 'word', value: match[5] });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (query.slice(end).trim()) {
|
|
296
|
+
throw badError('X-GM-RAW query has an unterminated quoted string');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let pos = 0;
|
|
300
|
+
const peek = () => tokens[pos];
|
|
301
|
+
|
|
302
|
+
// expression = 1*term, until the end or a closing bracket
|
|
303
|
+
const expression = end => {
|
|
304
|
+
const terms = [];
|
|
305
|
+
while (pos < tokens.length && (!peek() || peek().type !== end)) {
|
|
306
|
+
terms.push(term());
|
|
307
|
+
}
|
|
308
|
+
if (end) {
|
|
309
|
+
if (!peek() || peek().type !== end) {
|
|
310
|
+
throw badError('X-GM-RAW query is missing "' + end + '"');
|
|
311
|
+
}
|
|
312
|
+
pos++;
|
|
313
|
+
}
|
|
314
|
+
if (!terms.length) {
|
|
315
|
+
throw badError('X-GM-RAW query is empty');
|
|
316
|
+
}
|
|
317
|
+
return terms.length === 1 ? terms[0] : terms;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// term = unary *("OR" unary)
|
|
321
|
+
const term = () => {
|
|
322
|
+
let result = unary();
|
|
323
|
+
while (peek() && peek().type === 'word' && peek().value === 'OR' && tokens[pos + 1]) {
|
|
324
|
+
pos++;
|
|
325
|
+
result = ['OR', result, unary()];
|
|
326
|
+
}
|
|
327
|
+
return result;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const unary = () => {
|
|
331
|
+
const token = tokens[pos++];
|
|
332
|
+
if (!token || token.type === ')' || token.type === '}') {
|
|
333
|
+
throw badError('Unexpected end of X-GM-RAW query');
|
|
334
|
+
}
|
|
335
|
+
if (token.type === '(') {
|
|
336
|
+
return expression(')');
|
|
337
|
+
}
|
|
338
|
+
if (token.type === '{') {
|
|
339
|
+
const options = [];
|
|
340
|
+
while (peek() && peek().type !== '}') {
|
|
341
|
+
options.push(unary());
|
|
342
|
+
}
|
|
343
|
+
if (!peek() || !options.length) {
|
|
344
|
+
throw badError('X-GM-RAW query has an empty or unterminated { } group');
|
|
345
|
+
}
|
|
346
|
+
pos++;
|
|
347
|
+
return options.reduce((any, option) => ['OR', any, option]);
|
|
348
|
+
}
|
|
349
|
+
if (token.value === '-' && peek() && (peek().type === '(' || peek().type === '{')) {
|
|
350
|
+
return ['NOT', unary()];
|
|
351
|
+
}
|
|
352
|
+
if (token.value.charAt(0) === '-' && token.value.length > 1) {
|
|
353
|
+
return ['NOT', rawTerm(rawWord(token.value.substr(1)))];
|
|
354
|
+
}
|
|
355
|
+
return rawTerm(token);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
return [expression()];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// splits "name:value" into an operator and an operand
|
|
362
|
+
function rawWord(value) {
|
|
363
|
+
const match = value.match(/^([a-z_0-9]+):(.+)$/i);
|
|
364
|
+
return match ? { value, operator: match[1], operand: match[2] } : { value, operator: false, operand: false };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function rawTerm(token) {
|
|
368
|
+
const { operator, operand } = token.operator ? token : rawWord(token.value);
|
|
369
|
+
if (!operator) {
|
|
370
|
+
return ['TEXT', token.value];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const name = operator.toLowerCase();
|
|
374
|
+
// own keys only, names like "constructor:" are plain text
|
|
375
|
+
const shortcutKey = name + ':' + operand.toLowerCase();
|
|
376
|
+
const shortcut = Object.hasOwn(RAW_SHORTCUTS, shortcutKey) && RAW_SHORTCUTS[shortcutKey];
|
|
377
|
+
if (shortcut) {
|
|
378
|
+
return shortcut;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const key = Object.hasOwn(RAW_OPERATORS, name) && RAW_OPERATORS[name];
|
|
382
|
+
if (key) {
|
|
383
|
+
switch (key) {
|
|
384
|
+
case 'LARGER':
|
|
385
|
+
case 'SMALLER': {
|
|
386
|
+
const size = operand.match(/^(\d+)([km]?)$/i);
|
|
387
|
+
if (!size) {
|
|
388
|
+
throw badError('X-GM-RAW ' + name + ': expects a size');
|
|
389
|
+
}
|
|
390
|
+
return [key, String(Number(size[1]) * { '': 1, k: 1024, m: 1024 * 1024 }[size[2].toLowerCase()])];
|
|
391
|
+
}
|
|
392
|
+
case 'SINCE':
|
|
393
|
+
case 'BEFORE': {
|
|
394
|
+
const date = operand.match(/^(\d{4})[/-](\d{1,2})[/-](\d{1,2})$/);
|
|
395
|
+
if (!date || !isRealDate(Number(date[3]), Number(date[2]) - 1, Number(date[1]))) {
|
|
396
|
+
throw badError('X-GM-RAW ' + name + ': expects a date as YYYY/MM/DD');
|
|
397
|
+
}
|
|
398
|
+
return [key, Number(date[3]) + '-' + MONTHS[Number(date[2]) - 1] + '-' + date[1]];
|
|
399
|
+
}
|
|
400
|
+
case 'HEADER':
|
|
401
|
+
return [key, 'Message-ID', operand];
|
|
402
|
+
default:
|
|
403
|
+
return [key, operand];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (name === 'in') {
|
|
408
|
+
// any other mailbox is a label
|
|
409
|
+
return ['X-GM-LABELS', operand];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (RAW_UNSUPPORTED.indexOf(name) >= 0) {
|
|
413
|
+
const err = new Error('X-GM-RAW operator ' + name + ':' + operand + ' is not supported by imapkit');
|
|
414
|
+
err.imapResponse = 'NO';
|
|
415
|
+
throw err;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// not an operator, like "http://example.com"
|
|
419
|
+
return ['TEXT', token.value];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function sendLabelUpdate(connection, parsed, data, index, message) {
|
|
423
|
+
const resp = [
|
|
424
|
+
{
|
|
425
|
+
type: 'ATOM',
|
|
426
|
+
value: 'X-GM-LABELS'
|
|
427
|
+
},
|
|
428
|
+
formatLabels(message)
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
if ((parsed.command || '').toUpperCase() === 'UID STORE') {
|
|
432
|
+
resp.push({
|
|
433
|
+
type: 'ATOM',
|
|
434
|
+
value: 'UID'
|
|
435
|
+
});
|
|
436
|
+
resp.push(message.uid);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
connection.send(
|
|
440
|
+
{
|
|
441
|
+
tag: '*',
|
|
442
|
+
attributes: [
|
|
443
|
+
index,
|
|
444
|
+
{
|
|
445
|
+
type: 'ATOM',
|
|
446
|
+
value: 'FETCH'
|
|
447
|
+
},
|
|
448
|
+
resp
|
|
449
|
+
]
|
|
450
|
+
},
|
|
451
|
+
'FLAG UPDATE',
|
|
452
|
+
parsed,
|
|
453
|
+
data,
|
|
454
|
+
message
|
|
455
|
+
);
|
|
456
|
+
}
|