imapkit 0.0.0-stage → 4.0.0
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,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { SMTPServer } = require('smtp-server');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Starts an SMTP server that appends every received message to the INBOX
|
|
7
|
+
* of the given ImapKit IMAP server. Any sender, recipient and credentials
|
|
8
|
+
* are accepted.
|
|
9
|
+
*
|
|
10
|
+
* @param {Number} smtpPort - port to listen on for SMTP commands
|
|
11
|
+
* @param {Object} imapServer - the ImapKit IMAP server
|
|
12
|
+
* @param {Function} [callback] - function executed when SMTP server is listening
|
|
13
|
+
* @return {SMTPServer} the SMTP server instance, use `close()` to stop it
|
|
14
|
+
*/
|
|
15
|
+
exports.startSMTPServer = function startSMTPServer(smtpPort, imapServer, callback) {
|
|
16
|
+
const credentials = imapServer.getCredentials();
|
|
17
|
+
|
|
18
|
+
const server = new SMTPServer({
|
|
19
|
+
banner: 'ImapKit',
|
|
20
|
+
key: credentials.key,
|
|
21
|
+
cert: credentials.cert,
|
|
22
|
+
authOptional: true,
|
|
23
|
+
logger: false,
|
|
24
|
+
onAuth(auth, session, done) {
|
|
25
|
+
done(null, { user: auth.username });
|
|
26
|
+
},
|
|
27
|
+
onData(stream, session, done) {
|
|
28
|
+
const chunks = [];
|
|
29
|
+
stream.on('data', chunk => chunks.push(chunk));
|
|
30
|
+
stream.on('error', done);
|
|
31
|
+
stream.on('end', () => {
|
|
32
|
+
// smtp-server has already removed the dot-stuffing (RFC 5321 section 4.5.2)
|
|
33
|
+
imapServer.appendMessage('INBOX', [], false, Buffer.concat(chunks).toString('binary'));
|
|
34
|
+
done();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
server.on('error', err => {
|
|
40
|
+
if (imapServer.options.debug) {
|
|
41
|
+
console.error('SMTP server error: %s', err.message);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
server.listen(smtpPort, () => {
|
|
46
|
+
console.log('Incoming SMTP server up and running on port %s', server.server.address().port);
|
|
47
|
+
callback?.();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return server;
|
|
51
|
+
};
|
package/lib/sorting.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Message values for SORT and THREAD (RFC 5256): string collation, base subject, sent date,
|
|
4
|
+
// addresses and Message IDs, and the search step that both commands start with
|
|
5
|
+
|
|
6
|
+
const { getMessageData } = require('./mimeparser');
|
|
7
|
+
const { processAddress } = require('./envelope');
|
|
8
|
+
const { parseDateTime, parseHeaderDate, toTimestamp } = require('./dates');
|
|
9
|
+
const makeSearch = require('./commands/handlers/search');
|
|
10
|
+
const { badError, criteriaValues, sendSearchError } = makeSearch;
|
|
11
|
+
|
|
12
|
+
// RFC 2047 section 2: encoded-word = "=?" charset "?" encoding "?" encoded-text "?=", RFC 2231 section 5
|
|
13
|
+
// adds an optional "*" language suffix to the charset
|
|
14
|
+
const ENCODED_WORD = /=\?([^?\s*]+)(?:\*[^?\s]*)?\?([BbQq])\?([^?\s]*)\?=/g;
|
|
15
|
+
|
|
16
|
+
const utf8Decoder = new TextDecoder('utf-8');
|
|
17
|
+
|
|
18
|
+
// decoders by lower case charset name, false for a charset that TextDecoder does not know
|
|
19
|
+
const decoders = new Map();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Decodes the octets of an encoded word, or returns false if the charset is unknown
|
|
23
|
+
*/
|
|
24
|
+
function decodeCharset(charset, octets) {
|
|
25
|
+
charset = charset.toLowerCase();
|
|
26
|
+
if (!decoders.has(charset)) {
|
|
27
|
+
let decoder = false;
|
|
28
|
+
try {
|
|
29
|
+
decoder = new TextDecoder(charset);
|
|
30
|
+
} catch {
|
|
31
|
+
// unknown charset
|
|
32
|
+
}
|
|
33
|
+
decoders.set(charset, decoder);
|
|
34
|
+
}
|
|
35
|
+
const decoder = decoders.get(charset);
|
|
36
|
+
return decoder && decoder.decode(octets);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decodes an RFC 2047 header value to a Unicode string. Text outside encoded words is read as UTF-8
|
|
41
|
+
* (invalid sequences become U+FFFD). Adjacent encoded words in the same charset are decoded together,
|
|
42
|
+
* so a multi-octet character may span them, and the white space between them is dropped (RFC 2047
|
|
43
|
+
* section 6.2). Encoded words in an unknown charset are kept as they are.
|
|
44
|
+
*
|
|
45
|
+
* @param {String} value Header value as a binary string
|
|
46
|
+
* @return {String} Decoded value
|
|
47
|
+
*/
|
|
48
|
+
function decodeHeader(value) {
|
|
49
|
+
value = (value || '').toString();
|
|
50
|
+
let result = '';
|
|
51
|
+
let pending = null;
|
|
52
|
+
let lastIndex = 0;
|
|
53
|
+
|
|
54
|
+
const flush = () => {
|
|
55
|
+
if (pending) {
|
|
56
|
+
const decoded = decodeCharset(pending.charset, Buffer.concat(pending.octets));
|
|
57
|
+
result += decoded === false ? pending.source : decoded;
|
|
58
|
+
pending = null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
ENCODED_WORD.lastIndex = 0;
|
|
63
|
+
let match;
|
|
64
|
+
while ((match = ENCODED_WORD.exec(value))) {
|
|
65
|
+
const between = value.substring(lastIndex, match.index);
|
|
66
|
+
const adjacent = pending && /^\s*$/.test(between);
|
|
67
|
+
if (!adjacent) {
|
|
68
|
+
flush();
|
|
69
|
+
result += utf8Decoder.decode(Buffer.from(between, 'binary'));
|
|
70
|
+
}
|
|
71
|
+
lastIndex = ENCODED_WORD.lastIndex;
|
|
72
|
+
|
|
73
|
+
const octets =
|
|
74
|
+
match[2].toUpperCase() === 'B'
|
|
75
|
+
? Buffer.from(match[3], 'base64')
|
|
76
|
+
: Buffer.from(
|
|
77
|
+
match[3].replace(/_/g, ' ').replace(/=([0-9a-fA-F]{2})/g, (m, hex) => String.fromCharCode(parseInt(hex, 16))),
|
|
78
|
+
'binary'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (pending && pending.charset.toLowerCase() !== match[1].toLowerCase()) {
|
|
82
|
+
flush();
|
|
83
|
+
}
|
|
84
|
+
if (!pending) {
|
|
85
|
+
pending = { charset: match[1], octets: [], source: '' };
|
|
86
|
+
}
|
|
87
|
+
pending.octets.push(octets);
|
|
88
|
+
pending.source += (pending.source ? between : '') + match[0];
|
|
89
|
+
}
|
|
90
|
+
flush();
|
|
91
|
+
return result + utf8Decoder.decode(Buffer.from(value.substr(lastIndex), 'binary'));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// UnicodeData.txt titlecase mappings that differ from the single code point uppercase mapping
|
|
95
|
+
// that String#toUpperCase gives: the Latin digraphs and the Greek letters with ypogegrammeni
|
|
96
|
+
const TITLECASE = new Map([
|
|
97
|
+
[0x01c4, 0x01c5],
|
|
98
|
+
[0x01c6, 0x01c5],
|
|
99
|
+
[0x01c7, 0x01c8],
|
|
100
|
+
[0x01c9, 0x01c8],
|
|
101
|
+
[0x01ca, 0x01cb],
|
|
102
|
+
[0x01cc, 0x01cb],
|
|
103
|
+
[0x01f1, 0x01f2],
|
|
104
|
+
[0x01f3, 0x01f2],
|
|
105
|
+
[0x1fb3, 0x1fbc],
|
|
106
|
+
[0x1fc3, 0x1fcc],
|
|
107
|
+
[0x1ff3, 0x1ffc]
|
|
108
|
+
]);
|
|
109
|
+
for (const start of [0x1f80, 0x1f90, 0x1fa0]) {
|
|
110
|
+
for (let i = 0; i < 8; i++) {
|
|
111
|
+
TITLECASE.set(start + i, start + i + 8);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns the i;unicode-casemap form of a string (RFC 5051 section 2): every character is titlecased
|
|
117
|
+
* and then decomposed (NFKD). Comparing the UTF-8 octets of the results is the collation that
|
|
118
|
+
* RFC 5256 section 7 requires for SORT and THREAD (I18NLEVEL=1, RFC 5255 section 4.5)
|
|
119
|
+
*
|
|
120
|
+
* @param {String} str Unicode string
|
|
121
|
+
* @return {Buffer} UTF-8 octets to compare with Buffer.compare
|
|
122
|
+
*/
|
|
123
|
+
function collationKey(str) {
|
|
124
|
+
let result = '';
|
|
125
|
+
for (const char of str) {
|
|
126
|
+
const code = char.codePointAt(0);
|
|
127
|
+
if (TITLECASE.has(code)) {
|
|
128
|
+
result += String.fromCodePoint(TITLECASE.get(code));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// full case mappings (e.g. "ß" to "SS") are not the simple mapping of UnicodeData.txt
|
|
132
|
+
const upper = char.toUpperCase();
|
|
133
|
+
result += [...upper].length === 1 ? upper : char;
|
|
134
|
+
}
|
|
135
|
+
return Buffer.from(result.normalize('NFKD'), 'utf-8');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// RFC 5256 section 5: subj-refwd = ("re" / ("fw" ["d"])) *WSP [subj-blob] ":", subj-blob = "[" *BLOBCHAR "]" *WSP
|
|
139
|
+
const SUBJ_BLOB = '\\[[^[\\]]*\\] *';
|
|
140
|
+
const SUBJ_LEADER = new RegExp('^(?:' + SUBJ_BLOB + ')*(?:re|fwd?) *(?:' + SUBJ_BLOB + ')?:', 'i');
|
|
141
|
+
const SUBJ_BLOB_PREFIX = new RegExp('^' + SUBJ_BLOB);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extracts the base subject (RFC 5256 section 2.1) and tells if the subject marks a reply or a
|
|
145
|
+
* forward (RFC 5256 section 3, REFERENCES): the extraction removed a subj-refwd, a "(fwd)" trailer
|
|
146
|
+
* or a subj-fwd-hdr and subj-fwd-trl pair.
|
|
147
|
+
*
|
|
148
|
+
* @param {String} subject Subject header value as a binary string
|
|
149
|
+
* @return {Object} `{ subject, isReply }`, subject is the base subject as a Unicode string
|
|
150
|
+
*/
|
|
151
|
+
function baseSubject(subject) {
|
|
152
|
+
let isReply = false;
|
|
153
|
+
|
|
154
|
+
// (1) decode encoded words, tabs and continuations become a space, runs of spaces one space
|
|
155
|
+
let text = decodeHeader(subject)
|
|
156
|
+
.replace(/\r?\n(?=[ \t])/g, '')
|
|
157
|
+
.replace(/[\t\r\n]/g, ' ')
|
|
158
|
+
.replace(/ {2,}/g, ' ');
|
|
159
|
+
|
|
160
|
+
for (;;) {
|
|
161
|
+
// (2) remove subj-trailer text until there is none
|
|
162
|
+
let end = text.length;
|
|
163
|
+
for (;;) {
|
|
164
|
+
if (text.charAt(end - 1) === ' ') {
|
|
165
|
+
end--;
|
|
166
|
+
} else if (end >= 5 && text.substring(end - 5, end).toLowerCase() === '(fwd)') {
|
|
167
|
+
isReply = true;
|
|
168
|
+
end -= 5;
|
|
169
|
+
} else {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
text = text.substring(0, end);
|
|
174
|
+
|
|
175
|
+
// (3) remove subj-leader text, (4) remove a subj-blob prefix if a subj-base remains, (5) repeat
|
|
176
|
+
for (;;) {
|
|
177
|
+
let match;
|
|
178
|
+
if (text.charAt(0) === ' ') {
|
|
179
|
+
text = text.substr(1);
|
|
180
|
+
} else if ((match = text.match(SUBJ_LEADER))) {
|
|
181
|
+
isReply = true;
|
|
182
|
+
text = text.substr(match[0].length);
|
|
183
|
+
} else if ((match = text.match(SUBJ_BLOB_PREFIX)) && match[0].length < text.length) {
|
|
184
|
+
text = text.substr(match[0].length);
|
|
185
|
+
} else {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// (6) unwrap "[fwd: ... ]" and start over from step (2)
|
|
191
|
+
if (/^\[fwd:/i.test(text) && /\]$/.test(text)) {
|
|
192
|
+
isReply = true;
|
|
193
|
+
text = text.slice(5, -1);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// (7) the remaining text is the base subject
|
|
198
|
+
return { subject: text, isReply };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Internal date and time of a message in milliseconds (RFC 5256 ARRIVAL)
|
|
204
|
+
*
|
|
205
|
+
* @param {Object} message Message object
|
|
206
|
+
* @return {Number} timestamp, 0 if the internal date can not be parsed
|
|
207
|
+
*/
|
|
208
|
+
function arrivalTime(message) {
|
|
209
|
+
const date = parseDateTime(message.internaldate);
|
|
210
|
+
return date ? toTimestamp(date) : 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Sent date of a message in milliseconds (RFC 5256 section 2.2): the Date header normalized to UTC.
|
|
215
|
+
* An invalid zone is read as UTC and an invalid time as 00:00:00, and if the header is missing or
|
|
216
|
+
* has no valid date, the internal date is used instead.
|
|
217
|
+
*
|
|
218
|
+
* @param {Object} message Message object
|
|
219
|
+
* @return {Number} timestamp
|
|
220
|
+
*/
|
|
221
|
+
function sentTime(message) {
|
|
222
|
+
const date = parseHeaderDate(getMessageData(message).tree.parsedHeader.date);
|
|
223
|
+
if (!date) {
|
|
224
|
+
return arrivalTime(message);
|
|
225
|
+
}
|
|
226
|
+
if (date.hours === undefined || date.hours > 23 || date.minutes > 59 || date.seconds > 60) {
|
|
227
|
+
Object.assign(date, { hours: 0, minutes: 0, seconds: 0 });
|
|
228
|
+
}
|
|
229
|
+
return toTimestamp(date);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Returns the first address of an envelope address list, as [name, adl, mailbox, host]
|
|
234
|
+
*/
|
|
235
|
+
function firstAddress(message, header) {
|
|
236
|
+
const list = processAddress(getMessageData(message).tree.parsedHeader[header]);
|
|
237
|
+
return (list && list[0]) || null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* addr-mailbox of the first address in a header (RFC 5256 CC, FROM and TO), empty if there is none
|
|
242
|
+
*
|
|
243
|
+
* @param {Object} message Message object
|
|
244
|
+
* @param {String} header "from", "to" or "cc"
|
|
245
|
+
* @return {String} Unicode string
|
|
246
|
+
*/
|
|
247
|
+
function addressMailbox(message, header) {
|
|
248
|
+
const address = firstAddress(message, header);
|
|
249
|
+
return address && address[2] ? decodeHeader(address[2]) : '';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* DISPLAY sort value of the first address in a header (RFC 5957 sections 3 and 4): the decoded
|
|
254
|
+
* addr-name, or addr-mailbox@addr-host, or addr-mailbox, or the empty string
|
|
255
|
+
*
|
|
256
|
+
* @param {Object} message Message object
|
|
257
|
+
* @param {String} header "from" or "to"
|
|
258
|
+
* @return {String} Unicode string
|
|
259
|
+
*/
|
|
260
|
+
function displayAddress(message, header) {
|
|
261
|
+
const address = firstAddress(message, header);
|
|
262
|
+
const name = address && address[0] ? decodeHeader(address[0]) : '';
|
|
263
|
+
if (name || !address || !address[2]) {
|
|
264
|
+
return name;
|
|
265
|
+
}
|
|
266
|
+
return decodeHeader(address[3] ? address[2] + '@' + address[3] : address[2]);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Lists the Message IDs in a header value, normalized so that quoting does not matter (RFC 5256
|
|
271
|
+
* section 3, REFERENCES): comments and white space are dropped and a quoted id-left is unquoted.
|
|
272
|
+
* Only ids of the form "<" id-left "@" id-right ">" (RFC 5322 section 3.6.4) are valid.
|
|
273
|
+
*
|
|
274
|
+
* @param {String|Array} value Header value
|
|
275
|
+
* @return {Array} Message IDs
|
|
276
|
+
*/
|
|
277
|
+
function parseMessageIds(value) {
|
|
278
|
+
const str = [].concat(value || []).join(' ');
|
|
279
|
+
const ids = [];
|
|
280
|
+
let depth = 0;
|
|
281
|
+
let quoted = false;
|
|
282
|
+
let current = null;
|
|
283
|
+
|
|
284
|
+
for (let i = 0; i < str.length; i++) {
|
|
285
|
+
const char = str.charAt(i);
|
|
286
|
+
if (quoted) {
|
|
287
|
+
if (char === '\\') {
|
|
288
|
+
if (current !== null) {
|
|
289
|
+
current += str.charAt(i + 1);
|
|
290
|
+
}
|
|
291
|
+
i++;
|
|
292
|
+
} else if (char === '"') {
|
|
293
|
+
quoted = false;
|
|
294
|
+
} else if (current !== null) {
|
|
295
|
+
current += char;
|
|
296
|
+
}
|
|
297
|
+
} else if (depth) {
|
|
298
|
+
if (char === '\\') {
|
|
299
|
+
i++;
|
|
300
|
+
} else if (char === '(') {
|
|
301
|
+
depth++;
|
|
302
|
+
} else if (char === ')') {
|
|
303
|
+
depth--;
|
|
304
|
+
}
|
|
305
|
+
} else if (char === '"') {
|
|
306
|
+
quoted = true;
|
|
307
|
+
} else if (char === '(') {
|
|
308
|
+
depth++;
|
|
309
|
+
} else if (char === '<') {
|
|
310
|
+
current = '';
|
|
311
|
+
} else if (char === '>') {
|
|
312
|
+
if (current !== null) {
|
|
313
|
+
const at = current.lastIndexOf('@');
|
|
314
|
+
if (at > 0 && at < current.length - 1) {
|
|
315
|
+
ids.push(current);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
current = null;
|
|
319
|
+
} else if (current !== null && !/\s/.test(char)) {
|
|
320
|
+
current += char;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return ids;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Runs the search part of SORT and THREAD (RFC 5256 section 3): the charset is mandatory and the
|
|
328
|
+
* criteria follow it, both as in SEARCH. Sends a tagged BAD or NO response if the search fails.
|
|
329
|
+
*
|
|
330
|
+
* @param {Object} connection IMAP connection
|
|
331
|
+
* @param {Object} parsed Parsed command
|
|
332
|
+
* @param {String} data Raw command
|
|
333
|
+
* @param {Array} attributes Command arguments starting with the charset
|
|
334
|
+
* @return {Object|Boolean} `{ list, numbers }` like the SEARCH handler returns, or false after an error
|
|
335
|
+
*/
|
|
336
|
+
function searchMessages(connection, parsed, data, attributes) {
|
|
337
|
+
const command = parsed.command.toUpperCase();
|
|
338
|
+
const charset = attributes[0];
|
|
339
|
+
try {
|
|
340
|
+
// RFC 5256 section 5: charset = atom / quoted
|
|
341
|
+
if (attributes.length < 2) {
|
|
342
|
+
throw badError(command + ' expects a charset and search criteria');
|
|
343
|
+
}
|
|
344
|
+
if (!charset || ['ATOM', 'STRING'].indexOf(charset.type) < 0) {
|
|
345
|
+
throw badError('Charset must be an atom or a quoted string');
|
|
346
|
+
}
|
|
347
|
+
const criteria = criteriaValues(attributes.slice(1));
|
|
348
|
+
if (connection.searchCharset) {
|
|
349
|
+
// RFC 9755 section 3: with a fixed session charset (UTF8=ACCEPT) other charsets are BAD
|
|
350
|
+
if (charset.value.toUpperCase() !== connection.searchCharset) {
|
|
351
|
+
throw badError('Charset must be ' + connection.searchCharset + ' in this session');
|
|
352
|
+
}
|
|
353
|
+
return makeSearch(connection, connection.getSessionMessages(), criteria);
|
|
354
|
+
}
|
|
355
|
+
const params = ['CHARSET', charset.value].concat(criteria);
|
|
356
|
+
return makeSearch(connection, connection.getSessionMessages(), params);
|
|
357
|
+
} catch (E) {
|
|
358
|
+
sendSearchError(connection, parsed, data, E, command + ' FAILED');
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
module.exports = {
|
|
364
|
+
decodeHeader,
|
|
365
|
+
collationKey,
|
|
366
|
+
baseSubject,
|
|
367
|
+
arrivalTime,
|
|
368
|
+
sentTime,
|
|
369
|
+
addressMailbox,
|
|
370
|
+
displayAddress,
|
|
371
|
+
parseMessageIds,
|
|
372
|
+
searchMessages
|
|
373
|
+
};
|