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,269 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @help Adds MULTISEARCH [RFC7377] capability, loads ESEARCH as well
|
|
5
|
+
* @help ESEARCH IN (mailboxes "a" subtree "b" personal ...) RETURN (...) searches several mailboxes, also in the authenticated state
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const esearch = require('./esearch');
|
|
9
|
+
const { buildEsearchResponse, selectReturned } = require('../esearch');
|
|
10
|
+
const makeSearch = require('../commands/handlers/search');
|
|
11
|
+
const { badError, criteriaValues, sendSearchError } = makeSearch;
|
|
12
|
+
const { states } = require('../command-states');
|
|
13
|
+
const { isAtom, isAstring } = require('../arguments');
|
|
14
|
+
|
|
15
|
+
// RFC 5465 section 6 filter-mailboxes, as changed by RFC 7377 section 2.2. The value tells if a mailbox name
|
|
16
|
+
// or a list of names follows (one-or-more-mailbox)
|
|
17
|
+
const FILTERS = {
|
|
18
|
+
SELECTED: false,
|
|
19
|
+
INBOXES: false,
|
|
20
|
+
PERSONAL: false,
|
|
21
|
+
SUBSCRIBED: false,
|
|
22
|
+
SUBTREE: true,
|
|
23
|
+
'SUBTREE-ONE': true,
|
|
24
|
+
MAILBOXES: true
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses the source options (RFC 7377 section 4): esearch-source-opts = "IN" SP "(" source-mbox [SP "(" scope-options ")"] ")",
|
|
29
|
+
* source-mbox = filter-mailboxes *(SP filter-mailboxes)
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} connection IMAP connection, converts the mailbox names to storage names
|
|
32
|
+
* @param {Array} list Parsed list after IN
|
|
33
|
+
* @return {Array} list of `{ filter, names }`
|
|
34
|
+
*/
|
|
35
|
+
function parseSources(connection, list) {
|
|
36
|
+
if (!Array.isArray(list) || !list.length) {
|
|
37
|
+
throw badError('IN expects a parenthesized list of mailbox specifiers');
|
|
38
|
+
}
|
|
39
|
+
const sources = [];
|
|
40
|
+
for (let i = 0; i < list.length; i++) {
|
|
41
|
+
const item = list[i];
|
|
42
|
+
if (Array.isArray(item)) {
|
|
43
|
+
// scope-options, RFC 7377 defines none
|
|
44
|
+
throw badError('Unknown ESEARCH scope option');
|
|
45
|
+
}
|
|
46
|
+
const filter = item && item.type === 'ATOM' ? String(item.value).toUpperCase() : '';
|
|
47
|
+
if (!Object.hasOwn(FILTERS, filter)) {
|
|
48
|
+
// "selected-delayed" is not valid here (RFC 7377 section 2.2)
|
|
49
|
+
throw badError('Invalid mailbox specifier' + (filter ? ' ' + item.value : ''));
|
|
50
|
+
}
|
|
51
|
+
const source = { filter, names: [] };
|
|
52
|
+
if (FILTERS[filter]) {
|
|
53
|
+
const value = list[++i];
|
|
54
|
+
const names = Array.isArray(value) ? value : [value];
|
|
55
|
+
// RFC 5465 section 8: one-or-more-mailbox = mailbox / many-mailboxes, many-mailboxes = "(" mailbox *(SP mailbox) ")"
|
|
56
|
+
if (!names.length || !names.every(isAstring)) {
|
|
57
|
+
throw badError(filter.toLowerCase() + ' expects a mailbox name or a list of mailbox names');
|
|
58
|
+
}
|
|
59
|
+
names.forEach(name => source.names.push(connection.importMailboxName(name.value)));
|
|
60
|
+
}
|
|
61
|
+
sources.push(source);
|
|
62
|
+
}
|
|
63
|
+
return sources;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parses the arguments of ESEARCH (RFC 7377 section 4): esearch = "ESEARCH" [SP esearch-source-opts] [SP search-return-opts] SP search-program
|
|
68
|
+
*
|
|
69
|
+
* @return {Object} `{ sources, attributes }`, sources is null when IN was not used, attributes hold the result options and the search program
|
|
70
|
+
*/
|
|
71
|
+
function splitArguments(connection, attributes) {
|
|
72
|
+
attributes = attributes || [];
|
|
73
|
+
if (isAtom(attributes[0], 'IN')) {
|
|
74
|
+
return { sources: parseSources(connection, attributes[1]), attributes: attributes.slice(2) };
|
|
75
|
+
}
|
|
76
|
+
return { sources: null, attributes };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = function (server) {
|
|
80
|
+
// RFC 7377 section 2.1: the ESEARCH command answers with ESEARCH responses (RFC 4731)
|
|
81
|
+
esearch(server);
|
|
82
|
+
|
|
83
|
+
server.registerCapability('MULTISEARCH');
|
|
84
|
+
|
|
85
|
+
const isSelectable = mailbox => !!mailbox && mailbox.flags.indexOf('\\Noselect') < 0;
|
|
86
|
+
|
|
87
|
+
// every mailbox, INBOX first
|
|
88
|
+
const allMailboxes = () => {
|
|
89
|
+
const list = Object.keys(server.folderCache).map(path => server.folderCache[path]);
|
|
90
|
+
return list.sort((a, b) => (a.path === 'INBOX' ? -1 : b.path === 'INBOX' ? 1 : 0));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// a mailbox and the mailboxes below it, `depth` levels down (RFC 7377 section 2.2). Visited
|
|
94
|
+
// mailboxes are not walked again, which also defends against loops in the hierarchy
|
|
95
|
+
const walkSubtree = (mailbox, depth, visit) => {
|
|
96
|
+
if (!visit(mailbox) || !depth || !mailbox.folders) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
Object.keys(mailbox.folders).forEach(name => walkSubtree(mailbox.folders[name], depth - 1, visit));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolves the source options to the mailboxes to search, in the order they were given. Mailboxes
|
|
104
|
+
* that do not exist, are not selectable or that the user may not search (server.searchAccessChecks, ACL)
|
|
105
|
+
* are ignored (RFC 7377 section 2.2)
|
|
106
|
+
*/
|
|
107
|
+
const resolveMailboxes = (connection, sources) => {
|
|
108
|
+
const result = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
// `named` is true for a mailbox that the client gave the name of
|
|
111
|
+
const add = (mailbox, named) => {
|
|
112
|
+
// the client SHOULD NOT name a mailbox twice, the duplicates are removed (RFC 7377 section 2.2)
|
|
113
|
+
if (!mailbox || seen.has(mailbox) || !isSelectable(mailbox)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (server.searchAccessChecks.every(check => check(connection, mailbox, !!named))) {
|
|
117
|
+
seen.add(mailbox);
|
|
118
|
+
result.push(mailbox);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
sources.forEach(source => {
|
|
122
|
+
switch (source.filter) {
|
|
123
|
+
case 'SELECTED':
|
|
124
|
+
add(connection.selectedMailbox, true);
|
|
125
|
+
break;
|
|
126
|
+
case 'INBOXES':
|
|
127
|
+
// RFC 5465 section 6.3: messages are only delivered to INBOX
|
|
128
|
+
add(server.getMailbox('INBOX'), false);
|
|
129
|
+
break;
|
|
130
|
+
case 'PERSONAL':
|
|
131
|
+
allMailboxes()
|
|
132
|
+
.filter(mailbox => server.isPersonal(mailbox))
|
|
133
|
+
.forEach(mailbox => add(mailbox, false));
|
|
134
|
+
break;
|
|
135
|
+
case 'SUBSCRIBED':
|
|
136
|
+
// RFC 5465 section 6.4
|
|
137
|
+
allMailboxes()
|
|
138
|
+
.filter(mailbox => mailbox.subscribed)
|
|
139
|
+
.forEach(mailbox => add(mailbox, false));
|
|
140
|
+
break;
|
|
141
|
+
case 'MAILBOXES':
|
|
142
|
+
source.names.forEach(name => add(server.getMailbox(name), true));
|
|
143
|
+
break;
|
|
144
|
+
default: {
|
|
145
|
+
const depth = source.filter === 'SUBTREE' ? Infinity : 1;
|
|
146
|
+
const visited = new Set();
|
|
147
|
+
source.names.forEach(name => {
|
|
148
|
+
const mailbox = server.getMailbox(name);
|
|
149
|
+
if (mailbox) {
|
|
150
|
+
walkSubtree(mailbox, depth, child => {
|
|
151
|
+
if (visited.has(child)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
visited.add(child);
|
|
155
|
+
// the named mailbox itself, not the ones below it
|
|
156
|
+
add(child, child === mailbox);
|
|
157
|
+
return true;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return result;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const handler = (connection, parsed, data, callback) => {
|
|
168
|
+
const fail = (status, text) => {
|
|
169
|
+
connection.sendStatus(parsed, data, status, text, false, 'ESEARCH FAILED');
|
|
170
|
+
return callback();
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let sources;
|
|
174
|
+
let options = new Map();
|
|
175
|
+
let params;
|
|
176
|
+
try {
|
|
177
|
+
const split = splitArguments(connection, parsed.attributes);
|
|
178
|
+
sources = split.sources || [{ filter: 'SELECTED', names: [] }];
|
|
179
|
+
let attributes = split.attributes;
|
|
180
|
+
if (isAtom(attributes[0], 'RETURN')) {
|
|
181
|
+
options = parsed.searchReturn = server.parseSearchReturn(attributes[1], connection, parsed);
|
|
182
|
+
attributes = attributes.slice(2);
|
|
183
|
+
}
|
|
184
|
+
if (!attributes.length) {
|
|
185
|
+
throw badError('ESEARCH expects search criteria');
|
|
186
|
+
}
|
|
187
|
+
params = criteriaValues(attributes);
|
|
188
|
+
} catch (E) {
|
|
189
|
+
return fail('BAD', E.message);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const onlySelected = sources.every(source => source.filter === 'SELECTED');
|
|
193
|
+
// RFC 7377 section 2.2: "selected" needs the selected state
|
|
194
|
+
if (sources.some(source => source.filter === 'SELECTED') && !connection.selectedMailbox) {
|
|
195
|
+
return fail('BAD', 'No mailbox is selected');
|
|
196
|
+
}
|
|
197
|
+
// RFC 7377 section 2.2: SAVE is valid only if "selected" is the sole mailbox to search
|
|
198
|
+
if (options.has('SAVE') && !onlySelected) {
|
|
199
|
+
return fail('BAD', 'SAVE can only be used when searching the selected mailbox');
|
|
200
|
+
}
|
|
201
|
+
// RFC 7377 section 2.2: UPDATE applies to the selected mailbox only, and needs one
|
|
202
|
+
if (options.has('UPDATE') && !connection.selectedMailbox) {
|
|
203
|
+
return fail('BAD', 'UPDATE needs a selected mailbox');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const mailboxes = resolveMailboxes(connection, sources);
|
|
207
|
+
const results = [];
|
|
208
|
+
try {
|
|
209
|
+
// the criteria are checked even when no mailbox is searched
|
|
210
|
+
const searchMailbox = mailbox => {
|
|
211
|
+
if (mailbox && mailbox === connection.selectedMailbox) {
|
|
212
|
+
return makeSearch(connection, connection.getSessionMessages(), params);
|
|
213
|
+
}
|
|
214
|
+
const messages = mailbox ? mailbox.messages : [];
|
|
215
|
+
return makeSearch(connection, messages, params, (range, isUid) => server.getMessageRange(messages, range, isUid));
|
|
216
|
+
};
|
|
217
|
+
if (!mailboxes.length) {
|
|
218
|
+
searchMailbox(connection.selectedMailbox || null);
|
|
219
|
+
}
|
|
220
|
+
mailboxes.forEach(mailbox => results.push({ mailbox, result: searchMailbox(mailbox) }));
|
|
221
|
+
} catch (E) {
|
|
222
|
+
sendSearchError(connection, parsed, data, E, 'ESEARCH FAILED');
|
|
223
|
+
return callback();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const silent = server.isSilentReturn(options);
|
|
227
|
+
results.forEach(({ mailbox, result }) => {
|
|
228
|
+
// RFC 7377 section 2.1: one ESEARCH response per mailbox with matches, always with UIDs and the
|
|
229
|
+
// MAILBOX, TAG and UIDVALIDITY correlators, none for a mailbox without matches
|
|
230
|
+
if (!result.list.length || silent) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
connection.send(
|
|
234
|
+
buildEsearchResponse(parsed.tag, true, result, options, mailbox),
|
|
235
|
+
'ESEARCH',
|
|
236
|
+
parsed,
|
|
237
|
+
data,
|
|
238
|
+
Object.assign({}, result, { list: selectReturned(result.list, options) })
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const selected = results.find(entry => entry.mailbox === connection.selectedMailbox);
|
|
243
|
+
if (options.has('SAVE')) {
|
|
244
|
+
// SEARCHRES stores it once the tagged response tells if the command succeeded
|
|
245
|
+
parsed.searchresFound = selected ? selectReturned(selected.result.list, options) : [];
|
|
246
|
+
}
|
|
247
|
+
if (options.has('UPDATE') && selected) {
|
|
248
|
+
server.addSearchContext(connection, parsed, data, selected.result, true, selected.mailbox);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
connection.sendStatus(parsed, data, 'OK', 'ESEARCH completed', false, 'ESEARCH');
|
|
252
|
+
return callback();
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// RFC 7377 section 4: command-auth =/ esearch, command-select =/ esearch
|
|
256
|
+
server.setCommandHandler('ESEARCH', handler, { states: states.AUTHENTICATED, searchCriteria: 0 });
|
|
257
|
+
|
|
258
|
+
// RFC 3501 section 5.5: sequence numbers can only be in the search program
|
|
259
|
+
server.connectionHandlers.push(connection => {
|
|
260
|
+
// the source options are taken off here, the ESEARCH plugin takes off the result options
|
|
261
|
+
const usesSequenceNumbers = connection.usesSequenceNumbers;
|
|
262
|
+
connection.usesSequenceNumbers = function (parsed) {
|
|
263
|
+
if ((parsed.command || '').toUpperCase() === 'ESEARCH' && isAtom((parsed.attributes || [])[0], 'IN')) {
|
|
264
|
+
parsed = Object.assign({}, parsed, { attributes: parsed.attributes.slice(2) });
|
|
265
|
+
}
|
|
266
|
+
return usesSequenceNumbers.call(this, parsed);
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { states } = require('../command-states');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @help Adds NAMESPACE [RFC2342] capability
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = function (server) {
|
|
10
|
+
// Register capability, always usable
|
|
11
|
+
server.registerCapability('NAMESPACE');
|
|
12
|
+
|
|
13
|
+
// Add NAMESPACE command
|
|
14
|
+
server.setCommandHandler(
|
|
15
|
+
'NAMESPACE',
|
|
16
|
+
(connection, parsed, data, callback) => {
|
|
17
|
+
const list = {
|
|
18
|
+
personal: [],
|
|
19
|
+
user: [],
|
|
20
|
+
shared: []
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
Object.keys(server.storage).forEach(key => {
|
|
24
|
+
const ns = server.storage[key];
|
|
25
|
+
// INBOX is a mailbox, not a namespace
|
|
26
|
+
if (key === 'INBOX' || !ns) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (Object.hasOwn(list, ns.type)) {
|
|
30
|
+
list[ns.type].push([connection.exportMailboxName(key), ns.separator]);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
connection.send(
|
|
35
|
+
{
|
|
36
|
+
tag: '*',
|
|
37
|
+
command: 'NAMESPACE',
|
|
38
|
+
attributes: [list.personal.length ? list.personal : null, list.user.length ? list.user : null, list.shared.length ? list.shared : null]
|
|
39
|
+
},
|
|
40
|
+
'NAMESPACE',
|
|
41
|
+
parsed,
|
|
42
|
+
data,
|
|
43
|
+
list
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
connection.send(
|
|
47
|
+
{
|
|
48
|
+
tag: parsed.tag,
|
|
49
|
+
command: 'OK',
|
|
50
|
+
attributes: [
|
|
51
|
+
{
|
|
52
|
+
type: 'TEXT',
|
|
53
|
+
value: 'Completed'
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
'NAMESPACE',
|
|
58
|
+
parsed,
|
|
59
|
+
data,
|
|
60
|
+
list
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return callback();
|
|
64
|
+
},
|
|
65
|
+
{ states: states.AUTHENTICATED, noArguments: true }
|
|
66
|
+
);
|
|
67
|
+
};
|