imapflow 1.0.77 → 1.0.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +7 -3
- package/lib/commands/expunge.js +11 -0
- package/lib/commands/fetch.js +13 -4
- package/lib/commands/select.js +39 -1
- package/lib/commands/store.js +1 -1
- package/lib/handler/imap-compiler.js +6 -4
- package/lib/imap-flow.js +54 -10
- package/lib/types.d.ts +6 -0
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
- 1.0.81 - 2022-02-15
|
|
4
|
+
|
|
5
|
+
- Added support for the QRESYNC extension and untagged VANISHED responses
|
|
6
|
+
|
|
7
|
+
- 1.0.80 - 2022-02-07
|
|
8
|
+
|
|
9
|
+
- Added support for the missing NOMODSEQ mailbox modifier
|
|
10
|
+
|
|
11
|
+
- 1.0.79 - 2021-12-29
|
|
12
|
+
|
|
13
|
+
- Added property `greeting` that contains that first response from the server
|
|
14
|
+
|
|
15
|
+
- 1.0.78 - 2021-11-28
|
|
16
|
+
|
|
17
|
+
- Proxy support. Use configuration options `proxy: "url"` to use proxies
|
|
18
|
+
|
|
3
19
|
- 1.0.77 - 2021-11-25
|
|
4
20
|
|
|
5
21
|
- Testing out proxy connections
|
package/README.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# ImapFlow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> Managing IMAP connections is cool, but if you are only looking for an easy way to integrate email accounts, then ImapFlow was built for [EmailEngine Email API](https://emailengine.app/). It's a self-hosted software that converts all IMAP accounts to easy-to-use REST interfaces.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
The focus for ImapFlow is to provide easy to use API over IMAP. Using ImapFlow does not expect knowledge about specific IMAP details. A general understanding is good enough.
|
|
10
|
+
|
|
11
|
+
IMAP extensions are handled in the background, so for example, you can always request `labels` value from a {@link FetchQueryObject|fetch()} call, but if the IMAP server does not support `X-GM-EXT1` extension, then `labels` value is not included in the response.
|
|
8
12
|
|
|
9
13
|
## Source
|
|
10
14
|
|
package/lib/commands/expunge.js
CHANGED
|
@@ -20,6 +20,17 @@ module.exports = async (connection, range, options) => {
|
|
|
20
20
|
let response;
|
|
21
21
|
try {
|
|
22
22
|
response = await connection.exec(command, attributes);
|
|
23
|
+
|
|
24
|
+
// A OK [HIGHESTMODSEQ 9122] Expunge completed (0.010 + 0.000 + 0.012 secs).
|
|
25
|
+
let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section;
|
|
26
|
+
let responseCode = section && section.length && section[0] && typeof section[0].value === 'string' ? section[0].value : '';
|
|
27
|
+
if (responseCode.toUpperCase() === 'HIGHESTMODSEQ') {
|
|
28
|
+
let highestModseq = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
|
|
29
|
+
if (highestModseq && (!connection.mailbox.highestModseq || highestModseq > connection.mailbox.highestModseq)) {
|
|
30
|
+
connection.mailbox.highestModseq = highestModseq;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
response.next();
|
|
24
35
|
return true;
|
|
25
36
|
} catch (err) {
|
package/lib/commands/fetch.js
CHANGED
|
@@ -86,7 +86,7 @@ module.exports = async (connection, range, query, options) => {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// always ask for modseq if possible
|
|
89
|
-
if (connection.enabled.has('CONDSTORE')) {
|
|
89
|
+
if (connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
|
|
90
90
|
queryStructure.push({ type: 'ATOM', value: 'MODSEQ' });
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -137,8 +137,8 @@ module.exports = async (connection, range, query, options) => {
|
|
|
137
137
|
|
|
138
138
|
attributes.push(queryStructure);
|
|
139
139
|
|
|
140
|
-
if (options.changedSince && connection.enabled.has('CONDSTORE')) {
|
|
141
|
-
|
|
140
|
+
if (options.changedSince && connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
|
|
141
|
+
let changedSinceArgs = [
|
|
142
142
|
{
|
|
143
143
|
type: 'ATOM',
|
|
144
144
|
value: 'CHANGEDSINCE'
|
|
@@ -147,7 +147,16 @@ module.exports = async (connection, range, query, options) => {
|
|
|
147
147
|
type: 'ATOM',
|
|
148
148
|
value: options.changedSince.toString()
|
|
149
149
|
}
|
|
150
|
-
]
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
if (options.uid && connection.enabled.has('QRESYNC')) {
|
|
153
|
+
changedSinceArgs.push({
|
|
154
|
+
type: 'ATOM',
|
|
155
|
+
value: 'VANISHED'
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
attributes.push(changedSinceArgs);
|
|
151
160
|
}
|
|
152
161
|
|
|
153
162
|
response = await connection.exec(options.uid ? 'UID FETCH' : 'FETCH', attributes, {
|
package/lib/commands/select.js
CHANGED
|
@@ -21,6 +21,7 @@ module.exports = async (connection, path, options) => {
|
|
|
21
21
|
connection.folders.set(folder.path, folder);
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
|
+
|
|
24
25
|
let folderListData = connection.folders.has(path) ? connection.folders.get(path) : false;
|
|
25
26
|
|
|
26
27
|
let response;
|
|
@@ -33,7 +34,19 @@ module.exports = async (connection, path, options) => {
|
|
|
33
34
|
}
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
|
|
38
|
+
let extraArgs = [];
|
|
39
|
+
if (connection.enabled.has('QRESYNC') && options.changedSince && options.uidValidity) {
|
|
40
|
+
extraArgs.push([
|
|
41
|
+
{ type: 'ATOM', value: 'QRESYNC' },
|
|
42
|
+
[
|
|
43
|
+
{ type: 'ATOM', value: options.uidValidity },
|
|
44
|
+
{ type: 'ATOM', value: options.changedSince }
|
|
45
|
+
]
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
response = await connection.exec(!options.readOnly ? 'SELECT' : 'EXAMINE', [{ type: 'ATOM', value: path }].concat(extraArgs || []), {
|
|
37
50
|
untagged: {
|
|
38
51
|
OK: async untagged => {
|
|
39
52
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
@@ -85,6 +98,16 @@ module.exports = async (connection, path, options) => {
|
|
|
85
98
|
|
|
86
99
|
map[key] = value;
|
|
87
100
|
}
|
|
101
|
+
|
|
102
|
+
if (section && section.length === 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
|
103
|
+
let key = section[0].value.toLowerCase();
|
|
104
|
+
switch (key) {
|
|
105
|
+
case 'nomodseq':
|
|
106
|
+
key = 'noModseq';
|
|
107
|
+
map[key] = true;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
88
111
|
},
|
|
89
112
|
FLAGS: async untagged => {
|
|
90
113
|
if (!untagged.attributes || (!untagged.attributes.length && Array.isArray(untagged.attributes[0]))) {
|
|
@@ -100,6 +123,21 @@ module.exports = async (connection, path, options) => {
|
|
|
100
123
|
}
|
|
101
124
|
|
|
102
125
|
map.exists = num;
|
|
126
|
+
},
|
|
127
|
+
VANISHED: async untagged => {
|
|
128
|
+
await connection.untaggedVanished(
|
|
129
|
+
untagged,
|
|
130
|
+
// mailbox is not yet open, so use a dummy mailbox object
|
|
131
|
+
{ path, uidNext: false, uidValidity: false }
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
// we should only get an untagged FETCH for a SELECT/EXAMINE if QRESYNC was asked for
|
|
135
|
+
FETCH: async untagged => {
|
|
136
|
+
await connection.untaggedFetch(
|
|
137
|
+
untagged,
|
|
138
|
+
// mailbox is not yet open, so use a dummy mailbox object
|
|
139
|
+
{ path, uidNext: false, uidValidity: false }
|
|
140
|
+
);
|
|
103
141
|
}
|
|
104
142
|
}
|
|
105
143
|
});
|
package/lib/commands/store.js
CHANGED
|
@@ -53,7 +53,7 @@ module.exports = async (connection, range, flags, options) => {
|
|
|
53
53
|
|
|
54
54
|
let attributes = [{ type: 'SEQUENCE', value: range }, { type: 'ATOM', value: operation }, flags.map(flag => ({ type: 'ATOM', value: flag }))];
|
|
55
55
|
|
|
56
|
-
if (options.unchangedSince && connection.enabled.has('CONDSTORE')) {
|
|
56
|
+
if (options.unchangedSince && connection.enabled.has('CONDSTORE') && !connection.mailbox.noModseq) {
|
|
57
57
|
attributes.push([
|
|
58
58
|
{
|
|
59
59
|
type: 'ATOM',
|
|
@@ -117,11 +117,13 @@ module.exports = async (response, options) => {
|
|
|
117
117
|
case 'SECTION':
|
|
118
118
|
val = (node.value || '').toString('binary');
|
|
119
119
|
|
|
120
|
-
if (node.
|
|
121
|
-
val
|
|
122
|
-
|
|
120
|
+
if (!node.section || val) {
|
|
121
|
+
if (node.value === '' || imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) {
|
|
122
|
+
val = JSON.stringify(val);
|
|
123
|
+
}
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
resp += val;
|
|
126
|
+
}
|
|
125
127
|
|
|
126
128
|
if (node.section) {
|
|
127
129
|
resp += '[';
|
package/lib/imap-flow.js
CHANGED
|
@@ -29,7 +29,7 @@ const { PassThrough } = require('stream');
|
|
|
29
29
|
|
|
30
30
|
const { proxyConnection } = require('./proxy-connection');
|
|
31
31
|
|
|
32
|
-
const { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, getDecoder, packMessageRange, normalizePath } = require('./tools');
|
|
32
|
+
const { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, getDecoder, packMessageRange, normalizePath, expandRange } = require('./tools');
|
|
33
33
|
|
|
34
34
|
const imapCommands = require('./imap-commands.js');
|
|
35
35
|
|
|
@@ -58,6 +58,7 @@ const states = {
|
|
|
58
58
|
* @property {Set<string>} permanentFlags A Set of flags available to use in this mailbox. If it is not set or includes special flag "\\\*" then any flag can be used.
|
|
59
59
|
* @property {String} [mailboxId] unique mailbox ID if server has `OBJECTID` extension enabled
|
|
60
60
|
* @property {BigInt} [highestModseq] latest known modseq value if server has CONDSTORE or XYMHIGHESTMODSEQ enabled
|
|
61
|
+
* @property {String} [noModseq] if true then the server doesn't support the persistent storage of mod-sequences for the mailbox
|
|
61
62
|
* @property {BigInt} uidValidity Mailbox `UIDVALIDITY` value
|
|
62
63
|
* @property {Number} uidNext Next predicted UID
|
|
63
64
|
* @property {Number} exists Messages in this folder
|
|
@@ -126,9 +127,11 @@ class ImapFlow extends EventEmitter {
|
|
|
126
127
|
* @param {Boolean} [options.logRaw=false] If true then log data read from and written to socket encoded in base64
|
|
127
128
|
* @param {Boolean} [options.emitLogs=false] If `true` then in addition of sending data to logger, ImapFlow emits 'log' events with the same data
|
|
128
129
|
* @param {Boolean} [options.verifyOnly=false] If `true` then logs out automatically after successful authentication
|
|
130
|
+
* @param {Boolean} [options.proxy] Optional proxy URL. Supports HTTP CONNECT (`http://`, `https://`) and SOCKS (`socks://`, `socks4://`, `socks5://`) proxies
|
|
131
|
+
* @param {Boolean} [options.qresync=false] If true, then enables QRESYNC support. EXPUNGE notifications will include `uid` property instead of `seq`
|
|
129
132
|
*/
|
|
130
133
|
constructor(options) {
|
|
131
|
-
super();
|
|
134
|
+
super({ captureRejections: true });
|
|
132
135
|
|
|
133
136
|
this.options = options || {};
|
|
134
137
|
|
|
@@ -615,7 +618,7 @@ class ImapFlow extends EventEmitter {
|
|
|
615
618
|
}
|
|
616
619
|
|
|
617
620
|
// enable extensions if possible
|
|
618
|
-
await this.run('ENABLE', ['CONDSTORE', 'UTF8=ACCEPT']);
|
|
621
|
+
await this.run('ENABLE', ['CONDSTORE', 'UTF8=ACCEPT'].concat(this.options.qresync ? 'QRESYNC' : []));
|
|
619
622
|
|
|
620
623
|
this.usable = true;
|
|
621
624
|
}
|
|
@@ -807,7 +810,13 @@ class ImapFlow extends EventEmitter {
|
|
|
807
810
|
return false;
|
|
808
811
|
}
|
|
809
812
|
|
|
810
|
-
async initialOK() {
|
|
813
|
+
async initialOK(message) {
|
|
814
|
+
this.greeting = (message.attributes || [])
|
|
815
|
+
.filter(entry => entry.type === 'TEXT')
|
|
816
|
+
.map(entry => entry.value)
|
|
817
|
+
.filter(entry => entry)
|
|
818
|
+
.join('');
|
|
819
|
+
|
|
811
820
|
clearTimeout(this.greetingTimeout);
|
|
812
821
|
this.untaggedHandlers.OK = null;
|
|
813
822
|
this.untaggedHandlers.PREAUTH = null;
|
|
@@ -935,21 +944,53 @@ class ImapFlow extends EventEmitter {
|
|
|
935
944
|
this.mailbox.exists--;
|
|
936
945
|
this.emit('expunge', {
|
|
937
946
|
path: this.mailbox.path,
|
|
938
|
-
seq
|
|
947
|
+
seq,
|
|
948
|
+
vanished: false
|
|
939
949
|
});
|
|
940
950
|
}
|
|
941
951
|
}
|
|
942
952
|
|
|
943
|
-
async
|
|
944
|
-
|
|
953
|
+
async untaggedVanished(untagged, mailbox) {
|
|
954
|
+
mailbox = mailbox || this.mailbox;
|
|
955
|
+
if (!mailbox) {
|
|
956
|
+
// mailbox closed, ignore
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
let tags = [];
|
|
961
|
+
let uids = false;
|
|
962
|
+
|
|
963
|
+
if (untagged.attributes.length > 1 && Array.isArray(untagged.attributes[0])) {
|
|
964
|
+
tags = untagged.attributes[0].map(entry => (typeof entry.value === 'string' ? entry.value.toUpperCase() : false)).filter(value => value);
|
|
965
|
+
untagged.attributes.shift();
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (untagged.attributes[0] && typeof untagged.attributes[0].value === 'string') {
|
|
969
|
+
uids = untagged.attributes[0].value;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
let uidList = expandRange(uids);
|
|
973
|
+
for (let uid of uidList) {
|
|
974
|
+
this.emit('expunge', {
|
|
975
|
+
path: mailbox.path,
|
|
976
|
+
uid,
|
|
977
|
+
vanished: true,
|
|
978
|
+
earlier: tags.includes('EARLIER')
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async untaggedFetch(untagged, mailbox) {
|
|
984
|
+
mailbox = mailbox || this.mailbox;
|
|
985
|
+
if (!mailbox) {
|
|
945
986
|
// mailbox closed, ignore
|
|
946
987
|
return;
|
|
947
988
|
}
|
|
948
989
|
|
|
949
|
-
let message = await formatMessageResponse(untagged,
|
|
990
|
+
let message = await formatMessageResponse(untagged, mailbox);
|
|
950
991
|
if (message.flags) {
|
|
951
992
|
let updateEvent = {
|
|
952
|
-
path:
|
|
993
|
+
path: mailbox.path,
|
|
953
994
|
seq: message.seq
|
|
954
995
|
};
|
|
955
996
|
|
|
@@ -1052,10 +1093,13 @@ class ImapFlow extends EventEmitter {
|
|
|
1052
1093
|
this.untaggedHandlers.CAPABILITY = (...args) => this.untaggedCapability(...args);
|
|
1053
1094
|
this.sectionHandlers.CAPABILITY = (...args) => this.sectionCapability(...args);
|
|
1054
1095
|
|
|
1055
|
-
this.untaggedHandlers.FETCH = (...args) => this.untaggedFetch(...args);
|
|
1056
1096
|
this.untaggedHandlers.EXISTS = (...args) => this.untaggedExists(...args);
|
|
1057
1097
|
this.untaggedHandlers.EXPUNGE = (...args) => this.untaggedExpunge(...args);
|
|
1058
1098
|
|
|
1099
|
+
// these methods take an optional second argument, so make sure that some random IMAP tag is not used as the second argument
|
|
1100
|
+
this.untaggedHandlers.FETCH = untagged => this.untaggedFetch(untagged);
|
|
1101
|
+
this.untaggedHandlers.VANISHED = untagged => this.untaggedVanished(untagged);
|
|
1102
|
+
|
|
1059
1103
|
let socket = false;
|
|
1060
1104
|
if (this.options.proxy) {
|
|
1061
1105
|
try {
|
package/lib/types.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ declare module "imapflow" {
|
|
|
26
26
|
* @param [options.logRaw = false] - If true then log data read from and written to socket encoded in base64
|
|
27
27
|
* @param [options.emitLogs = false] - If `true` then in addition of sending data to logger, ImapFlow emits 'log' events with the same data
|
|
28
28
|
* @param [options.verifyOnly = false] - If `true` then logs out automatically after successful authentication
|
|
29
|
+
* @param [options.proxy] - Optional proxy URL. Supports HTTP CONNECT (`http://`, `https://`) and SOCKS (`socks://`, `socks4://`, `socks5://`) proxies
|
|
30
|
+
* @param [options.qresync = false] - If true, then enables QRESYNC support. EXPUNGE notifications will include `uid` property instead of `seq`
|
|
29
31
|
*/
|
|
30
32
|
class ImapFlow extends EventEmitter {
|
|
31
33
|
constructor(options: {
|
|
@@ -50,6 +52,8 @@ declare module "imapflow" {
|
|
|
50
52
|
logRaw?: boolean;
|
|
51
53
|
emitLogs?: boolean;
|
|
52
54
|
verifyOnly?: boolean;
|
|
55
|
+
proxy?: boolean;
|
|
56
|
+
qresync?: boolean;
|
|
53
57
|
});
|
|
54
58
|
/**
|
|
55
59
|
* Current module version as a static class property
|
|
@@ -481,6 +485,7 @@ declare module "imapflow" {
|
|
|
481
485
|
* @property permanentFlags - A Set of flags available to use in this mailbox. If it is not set or includes special flag "\\\*" then any flag can be used.
|
|
482
486
|
* @property [mailboxId] - unique mailbox ID if server has `OBJECTID` extension enabled
|
|
483
487
|
* @property [highestModseq] - latest known modseq value if server has CONDSTORE or XYMHIGHESTMODSEQ enabled
|
|
488
|
+
* @property [noModseq] - if true then the server doesn't support the persistent storage of mod-sequences for the mailbox
|
|
484
489
|
* @property uidValidity - Mailbox `UIDVALIDITY` value
|
|
485
490
|
* @property uidNext - Next predicted UID
|
|
486
491
|
* @property exists - Messages in this folder
|
|
@@ -495,6 +500,7 @@ declare type MailboxObject = {
|
|
|
495
500
|
permanentFlags: Set<string>;
|
|
496
501
|
mailboxId?: string;
|
|
497
502
|
highestModseq?: bigint;
|
|
503
|
+
noModseq?: string;
|
|
498
504
|
uidValidity: bigint;
|
|
499
505
|
uidNext: number;
|
|
500
506
|
exists: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.81",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "./lib/imap-flow.js",
|
|
6
6
|
"scripts": {
|
|
@@ -26,20 +26,20 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://imapflow.com/",
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@babel/eslint-parser": "7.
|
|
30
|
-
"@babel/eslint-plugin": "7.
|
|
29
|
+
"@babel/eslint-parser": "7.17.0",
|
|
30
|
+
"@babel/eslint-plugin": "7.16.5",
|
|
31
31
|
"@babel/plugin-syntax-class-properties": "7.12.13",
|
|
32
|
-
"@babel/preset-env": "7.16.
|
|
33
|
-
"@types/node": "
|
|
32
|
+
"@babel/preset-env": "7.16.11",
|
|
33
|
+
"@types/node": "17.0.18",
|
|
34
34
|
"braintree-jsdoc-template": "3.3.0",
|
|
35
|
-
"eslint": "8.
|
|
35
|
+
"eslint": "8.9.0",
|
|
36
36
|
"eslint-config-nodemailer": "1.2.0",
|
|
37
37
|
"eslint-config-prettier": "8.3.0",
|
|
38
38
|
"grunt": "1.4.1",
|
|
39
39
|
"grunt-cli": "1.4.3",
|
|
40
40
|
"grunt-contrib-nodeunit": "4.0.0",
|
|
41
41
|
"grunt-eslint": "24.0.0",
|
|
42
|
-
"jsdoc": "3.6.
|
|
42
|
+
"jsdoc": "3.6.10",
|
|
43
43
|
"tsd-jsdoc": "2.5.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"libmime": "5.0.0",
|
|
50
50
|
"libqp": "1.1.0",
|
|
51
51
|
"mailsplit": "5.3.1",
|
|
52
|
-
"nodemailer": "6.7.
|
|
53
|
-
"pino": "7.
|
|
54
|
-
"socks": "2.6.
|
|
52
|
+
"nodemailer": "6.7.2",
|
|
53
|
+
"pino": "7.8.0",
|
|
54
|
+
"socks": "2.6.2"
|
|
55
55
|
}
|
|
56
56
|
}
|