imapflow 1.0.76 → 1.0.80
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 +17 -0
- package/README.md +7 -3
- package/lib/commands/fetch.js +2 -2
- package/lib/commands/idle.js +1 -1
- package/lib/commands/select.js +10 -0
- package/lib/commands/store.js +1 -1
- package/lib/handler/imap-compiler.js +6 -4
- package/lib/imap-flow.js +45 -3
- package/lib/proxy-connection.js +120 -0
- package/lib/search-compiler.js +5 -0
- package/lib/types.d.ts +4 -0
- package/package.json +10 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
- 1.0.80 - 2022-02-07
|
|
4
|
+
|
|
5
|
+
- Added support for the missing NOMODSEQ mailbox modifier
|
|
6
|
+
|
|
7
|
+
- 1.0.79 - 2021-12-29
|
|
8
|
+
|
|
9
|
+
- Added property `greeting` that contains that first response from the server
|
|
10
|
+
|
|
11
|
+
- 1.0.78 - 2021-11-28
|
|
12
|
+
|
|
13
|
+
- Proxy support. Use configuration options `proxy: "url"` to use proxies
|
|
14
|
+
|
|
15
|
+
- 1.0.77 - 2021-11-25
|
|
16
|
+
|
|
17
|
+
- Testing out proxy connections
|
|
18
|
+
- Add X-GM-RAW support for search (jhi721 #68)
|
|
19
|
+
|
|
3
20
|
- 1.0.75 - 2021-10-15
|
|
4
21
|
|
|
5
22
|
- Fixed an exception with failing IDLE (ramiroaisen #60)
|
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/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,7 +137,7 @@ module.exports = async (connection, range, query, options) => {
|
|
|
137
137
|
|
|
138
138
|
attributes.push(queryStructure);
|
|
139
139
|
|
|
140
|
-
if (options.changedSince && connection.enabled.has('CONDSTORE')) {
|
|
140
|
+
if (options.changedSince && connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
|
|
141
141
|
attributes.push([
|
|
142
142
|
{
|
|
143
143
|
type: 'ATOM',
|
package/lib/commands/idle.js
CHANGED
|
@@ -66,7 +66,7 @@ module.exports = async connection => {
|
|
|
66
66
|
|
|
67
67
|
response = await connection.exec('IDLE', false, {
|
|
68
68
|
onPlusTag: async () => {
|
|
69
|
-
connection.log.debug({ msg: `Initiated IDLE, waiting for server input
|
|
69
|
+
connection.log.debug({ msg: `Initiated IDLE, waiting for server input`, doneRequested });
|
|
70
70
|
canEnd = true;
|
|
71
71
|
if (doneRequested) {
|
|
72
72
|
preCheck();
|
package/lib/commands/select.js
CHANGED
|
@@ -85,6 +85,16 @@ module.exports = async (connection, path, options) => {
|
|
|
85
85
|
|
|
86
86
|
map[key] = value;
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
if (section && section.length === 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
|
90
|
+
let key = section[0].value.toLowerCase();
|
|
91
|
+
switch (key) {
|
|
92
|
+
case 'nomodseq':
|
|
93
|
+
key = 'noModseq';
|
|
94
|
+
map[key] = true;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
88
98
|
},
|
|
89
99
|
FLAGS: async untagged => {
|
|
90
100
|
if (!untagged.attributes || (!untagged.attributes.length && Array.isArray(untagged.attributes[0]))) {
|
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
|
@@ -27,6 +27,8 @@ const libbase64 = require('libbase64');
|
|
|
27
27
|
const FlowedDecoder = require('mailsplit/lib/flowed-decoder');
|
|
28
28
|
const { PassThrough } = require('stream');
|
|
29
29
|
|
|
30
|
+
const { proxyConnection } = require('./proxy-connection');
|
|
31
|
+
|
|
30
32
|
const { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, getDecoder, packMessageRange, normalizePath } = require('./tools');
|
|
31
33
|
|
|
32
34
|
const imapCommands = require('./imap-commands.js');
|
|
@@ -56,6 +58,7 @@ const states = {
|
|
|
56
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.
|
|
57
59
|
* @property {String} [mailboxId] unique mailbox ID if server has `OBJECTID` extension enabled
|
|
58
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
|
|
59
62
|
* @property {BigInt} uidValidity Mailbox `UIDVALIDITY` value
|
|
60
63
|
* @property {Number} uidNext Next predicted UID
|
|
61
64
|
* @property {Number} exists Messages in this folder
|
|
@@ -124,6 +127,7 @@ class ImapFlow extends EventEmitter {
|
|
|
124
127
|
* @param {Boolean} [options.logRaw=false] If true then log data read from and written to socket encoded in base64
|
|
125
128
|
* @param {Boolean} [options.emitLogs=false] If `true` then in addition of sending data to logger, ImapFlow emits 'log' events with the same data
|
|
126
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
|
|
127
131
|
*/
|
|
128
132
|
constructor(options) {
|
|
129
133
|
super();
|
|
@@ -805,7 +809,13 @@ class ImapFlow extends EventEmitter {
|
|
|
805
809
|
return false;
|
|
806
810
|
}
|
|
807
811
|
|
|
808
|
-
async initialOK() {
|
|
812
|
+
async initialOK(message) {
|
|
813
|
+
this.greeting = (message.attributes || [])
|
|
814
|
+
.filter(entry => entry.type === 'TEXT')
|
|
815
|
+
.map(entry => entry.value)
|
|
816
|
+
.filter(entry => entry)
|
|
817
|
+
.join('');
|
|
818
|
+
|
|
809
819
|
clearTimeout(this.greetingTimeout);
|
|
810
820
|
this.untaggedHandlers.OK = null;
|
|
811
821
|
this.untaggedHandlers.PREAUTH = null;
|
|
@@ -1054,6 +1064,22 @@ class ImapFlow extends EventEmitter {
|
|
|
1054
1064
|
this.untaggedHandlers.EXISTS = (...args) => this.untaggedExists(...args);
|
|
1055
1065
|
this.untaggedHandlers.EXPUNGE = (...args) => this.untaggedExpunge(...args);
|
|
1056
1066
|
|
|
1067
|
+
let socket = false;
|
|
1068
|
+
if (this.options.proxy) {
|
|
1069
|
+
try {
|
|
1070
|
+
socket = await proxyConnection(this.log, this.options.proxy, this.host, this.port);
|
|
1071
|
+
if (!socket) {
|
|
1072
|
+
throw new Error('Failed to setup proxy connection');
|
|
1073
|
+
}
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
let error = new Error('Failed to setup proxy connection');
|
|
1076
|
+
error.code = err.code || 'ProxyError';
|
|
1077
|
+
error._err = err;
|
|
1078
|
+
this.log.error({ error, cid: this.id });
|
|
1079
|
+
throw error;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1057
1083
|
await new Promise((resolve, reject) => {
|
|
1058
1084
|
this.connectTimeout = setTimeout(() => {
|
|
1059
1085
|
let err = new Error('Failed to established connection in required time');
|
|
@@ -1063,7 +1089,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1063
1089
|
reject(err);
|
|
1064
1090
|
}, CONNECT_TIMEOUT);
|
|
1065
1091
|
|
|
1066
|
-
|
|
1092
|
+
let onConnect = () => {
|
|
1067
1093
|
clearTimeout(this.connectTimeout);
|
|
1068
1094
|
this.socket.setKeepAlive(true, 5 * 1000);
|
|
1069
1095
|
this.socket.setTimeout(SOCKET_TIMEOUT);
|
|
@@ -1105,7 +1131,23 @@ class ImapFlow extends EventEmitter {
|
|
|
1105
1131
|
// executed by initial "* OK"
|
|
1106
1132
|
this.initialResolve = resolve;
|
|
1107
1133
|
this.initialReject = reject;
|
|
1108
|
-
}
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
if (socket) {
|
|
1137
|
+
// socket is already establised via proxy
|
|
1138
|
+
if (this.secureConnection) {
|
|
1139
|
+
// TLS socket requires a handshake
|
|
1140
|
+
opts.socket = socket;
|
|
1141
|
+
this.socket = connector.connect(opts, onConnect);
|
|
1142
|
+
} else {
|
|
1143
|
+
// cleartext socket is already usable
|
|
1144
|
+
this.socket = socket;
|
|
1145
|
+
setImmediate(onConnect);
|
|
1146
|
+
}
|
|
1147
|
+
} else {
|
|
1148
|
+
this.socket = connector.connect(opts, onConnect);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1109
1151
|
this.writeSocket = this.socket;
|
|
1110
1152
|
|
|
1111
1153
|
this.socket.once('error', err => {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const httpProxyClient = require('nodemailer/lib/smtp-connection/http-proxy-client');
|
|
4
|
+
const { SocksClient } = require('socks');
|
|
5
|
+
const util = require('util');
|
|
6
|
+
const httpProxyClientAsync = util.promisify(httpProxyClient);
|
|
7
|
+
const dns = require('dns').promises;
|
|
8
|
+
const net = require('net');
|
|
9
|
+
|
|
10
|
+
const proxyConnection = async (logger, connectionUrl, host, port) => {
|
|
11
|
+
let proxyUrl = new URL(connectionUrl);
|
|
12
|
+
|
|
13
|
+
let protocol = proxyUrl.protocol.replace(/:$/, '').toLowerCase();
|
|
14
|
+
|
|
15
|
+
if (!net.isIP(host)) {
|
|
16
|
+
let resolveResult = await dns.resolve(host);
|
|
17
|
+
if (resolveResult && resolveResult.length) {
|
|
18
|
+
host = resolveResult[0];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
switch (protocol) {
|
|
23
|
+
// Connect using a HTTP CONNECT method
|
|
24
|
+
case 'http':
|
|
25
|
+
case 'https': {
|
|
26
|
+
try {
|
|
27
|
+
let socket = await httpProxyClientAsync(proxyUrl.href, port, host);
|
|
28
|
+
if (socket) {
|
|
29
|
+
if (proxyUrl.password) {
|
|
30
|
+
proxyUrl.password = '(hidden)';
|
|
31
|
+
}
|
|
32
|
+
logger.info({
|
|
33
|
+
msg: 'Established a socket via HTTP proxy',
|
|
34
|
+
proxyUrl: proxyUrl.href,
|
|
35
|
+
port,
|
|
36
|
+
host
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return socket;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (proxyUrl.password) {
|
|
42
|
+
proxyUrl.password = '(hidden)';
|
|
43
|
+
}
|
|
44
|
+
logger.error({
|
|
45
|
+
msg: 'Failed to establish a socket via HTTP proxy',
|
|
46
|
+
proxyUrl: proxyUrl.href,
|
|
47
|
+
port,
|
|
48
|
+
host,
|
|
49
|
+
err
|
|
50
|
+
});
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// SOCKS proxy
|
|
56
|
+
case 'socks':
|
|
57
|
+
case 'socks5':
|
|
58
|
+
case 'socks4':
|
|
59
|
+
case 'socks4a': {
|
|
60
|
+
let proxyType = Number(protocol.replace(/\D/g, '')) || 5;
|
|
61
|
+
|
|
62
|
+
let targetHost = proxyUrl.hostname;
|
|
63
|
+
if (!net.isIP(targetHost)) {
|
|
64
|
+
let resolveResult = await dns.resolve(targetHost);
|
|
65
|
+
if (resolveResult && resolveResult.length) {
|
|
66
|
+
targetHost = resolveResult[0];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let connectionOpts = {
|
|
71
|
+
proxy: {
|
|
72
|
+
host: targetHost,
|
|
73
|
+
port: Number(proxyUrl.port) || 1080,
|
|
74
|
+
type: proxyType
|
|
75
|
+
},
|
|
76
|
+
destination: {
|
|
77
|
+
host,
|
|
78
|
+
port
|
|
79
|
+
},
|
|
80
|
+
command: 'connect',
|
|
81
|
+
set_tcp_nodelay: true
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (proxyUrl.username || proxyUrl.password) {
|
|
85
|
+
connectionOpts.proxy.userId = proxyUrl.username;
|
|
86
|
+
connectionOpts.proxy.password = proxyUrl.password;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const info = await SocksClient.createConnection(connectionOpts);
|
|
91
|
+
if (info && info.socket) {
|
|
92
|
+
if (proxyUrl.password) {
|
|
93
|
+
proxyUrl.password = '(hidden)';
|
|
94
|
+
}
|
|
95
|
+
logger.info({
|
|
96
|
+
msg: 'Established a socket via SOCKS proxy',
|
|
97
|
+
proxyUrl: proxyUrl.href,
|
|
98
|
+
port,
|
|
99
|
+
host
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return info.socket;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (proxyUrl.password) {
|
|
105
|
+
proxyUrl.password = '(hidden)';
|
|
106
|
+
}
|
|
107
|
+
logger.error({
|
|
108
|
+
msg: 'Failed to establish a socket via SOCKS proxy',
|
|
109
|
+
proxyUrl: proxyUrl.href,
|
|
110
|
+
port,
|
|
111
|
+
host,
|
|
112
|
+
err
|
|
113
|
+
});
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
module.exports = { proxyConnection };
|
package/lib/search-compiler.js
CHANGED
|
@@ -117,6 +117,11 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
117
117
|
setOpt(attributes, 'X-GM-THRID', params[term]);
|
|
118
118
|
}
|
|
119
119
|
break;
|
|
120
|
+
case 'GMRAW':
|
|
121
|
+
if (connection.capabilities.has('X-GM-EXT-1')) {
|
|
122
|
+
setOpt(attributes, 'X-GM-RAW', params[term]);
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
120
125
|
|
|
121
126
|
case 'BEFORE':
|
|
122
127
|
case 'ON':
|
package/lib/types.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ 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
|
|
29
30
|
*/
|
|
30
31
|
class ImapFlow extends EventEmitter {
|
|
31
32
|
constructor(options: {
|
|
@@ -50,6 +51,7 @@ declare module "imapflow" {
|
|
|
50
51
|
logRaw?: boolean;
|
|
51
52
|
emitLogs?: boolean;
|
|
52
53
|
verifyOnly?: boolean;
|
|
54
|
+
proxy?: boolean;
|
|
53
55
|
});
|
|
54
56
|
/**
|
|
55
57
|
* Current module version as a static class property
|
|
@@ -481,6 +483,7 @@ declare module "imapflow" {
|
|
|
481
483
|
* @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
484
|
* @property [mailboxId] - unique mailbox ID if server has `OBJECTID` extension enabled
|
|
483
485
|
* @property [highestModseq] - latest known modseq value if server has CONDSTORE or XYMHIGHESTMODSEQ enabled
|
|
486
|
+
* @property [noModseq] - if true then the server doesn't support the persistent storage of mod-sequences for the mailbox
|
|
484
487
|
* @property uidValidity - Mailbox `UIDVALIDITY` value
|
|
485
488
|
* @property uidNext - Next predicted UID
|
|
486
489
|
* @property exists - Messages in this folder
|
|
@@ -495,6 +498,7 @@ declare type MailboxObject = {
|
|
|
495
498
|
permanentFlags: Set<string>;
|
|
496
499
|
mailboxId?: string;
|
|
497
500
|
highestModseq?: bigint;
|
|
501
|
+
noModseq?: string;
|
|
498
502
|
uidValidity: bigint;
|
|
499
503
|
uidNext: number;
|
|
500
504
|
exists: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.80",
|
|
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.
|
|
33
|
-
"@types/node": "
|
|
32
|
+
"@babel/preset-env": "7.16.11",
|
|
33
|
+
"@types/node": "17.0.15",
|
|
34
34
|
"braintree-jsdoc-template": "3.3.0",
|
|
35
|
-
"eslint": "8.0
|
|
35
|
+
"eslint": "8.8.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,6 +49,8 @@
|
|
|
49
49
|
"libmime": "5.0.0",
|
|
50
50
|
"libqp": "1.1.0",
|
|
51
51
|
"mailsplit": "5.3.1",
|
|
52
|
-
"
|
|
52
|
+
"nodemailer": "6.7.2",
|
|
53
|
+
"pino": "7.6.5",
|
|
54
|
+
"socks": "2.6.2"
|
|
53
55
|
}
|
|
54
56
|
}
|