imapflow 1.0.166 → 1.0.167
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 +7 -0
- package/lib/commands/authenticate.js +145 -52
- package/lib/imap-flow.js +6 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.167](https://github.com/postalsys/imapflow/compare/v1.0.166...v1.0.167) (2024-11-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **auth:** Prefer AUTH=LOGIN and AUTH=PLAIN to LOGIN ([3efd98a](https://github.com/postalsys/imapflow/commit/3efd98a5033db29228023302fc4663316b2b28fb))
|
|
9
|
+
|
|
3
10
|
## [1.0.166](https://github.com/postalsys/imapflow/compare/v1.0.165...v1.0.166) (2024-11-05)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -2,68 +2,161 @@
|
|
|
2
2
|
|
|
3
3
|
const { getStatusCode, getErrorText } = require('../tools.js');
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
async function authOauth(connection, username, accessToken) {
|
|
6
|
+
let oauthbearer;
|
|
7
|
+
let command;
|
|
8
|
+
let breaker;
|
|
9
|
+
|
|
10
|
+
if (connection.capabilities.has('AUTH=OAUTHBEARER')) {
|
|
11
|
+
oauthbearer = [`n,a=${username},`, `host=${connection.servername}`, `port=993`, `auth=Bearer ${accessToken}`, '', ''].join('\x01');
|
|
12
|
+
command = 'OAUTHBEARER';
|
|
13
|
+
breaker = 'AQ==';
|
|
14
|
+
} else if (connection.capabilities.has('AUTH=XOAUTH') || connection.capabilities.has('AUTH=XOAUTH2')) {
|
|
15
|
+
oauthbearer = [`user=${username}`, `auth=Bearer ${accessToken}`, '', ''].join('\x01');
|
|
16
|
+
command = 'XOAUTH2';
|
|
17
|
+
breaker = '';
|
|
10
18
|
}
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
let
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
let errorResponse = false;
|
|
21
|
+
try {
|
|
22
|
+
let response = await connection.exec(
|
|
23
|
+
'AUTHENTICATE',
|
|
24
|
+
[
|
|
25
|
+
{ type: 'ATOM', value: command },
|
|
26
|
+
{ type: 'ATOM', value: Buffer.from(oauthbearer).toString('base64'), sensitive: true }
|
|
27
|
+
],
|
|
28
|
+
{
|
|
29
|
+
onPlusTag: async resp => {
|
|
30
|
+
if (resp.attributes && resp.attributes[0] && resp.attributes[0].type === 'TEXT') {
|
|
31
|
+
try {
|
|
32
|
+
errorResponse = JSON.parse(Buffer.from(resp.attributes[0].value, 'base64').toString());
|
|
33
|
+
} catch (err) {
|
|
34
|
+
connection.log.debug({ errorResponse: resp.attributes[0].value, err });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
connection.log.debug({ src: 'c', msg: breaker, comment: `Error response for ${command}` });
|
|
39
|
+
connection.write(breaker);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
response.next();
|
|
44
|
+
|
|
45
|
+
connection.authCapabilities.set(`AUTH=${command}`, true);
|
|
46
|
+
|
|
47
|
+
return username;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
let errorCode = getStatusCode(err.response);
|
|
50
|
+
if (errorCode) {
|
|
51
|
+
err.serverResponseCode = errorCode;
|
|
52
|
+
}
|
|
53
|
+
err.authenticationFailed = true;
|
|
54
|
+
err.response = await getErrorText(err.response);
|
|
55
|
+
if (errorResponse) {
|
|
56
|
+
err.oauthError = errorResponse;
|
|
26
57
|
}
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
27
61
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
62
|
+
async function authLogin(connection, username, password) {
|
|
63
|
+
let errorResponse = false;
|
|
64
|
+
try {
|
|
65
|
+
let response = await connection.exec('AUTHENTICATE', [{ type: 'ATOM', value: 'LOGIN' }], {
|
|
66
|
+
onPlusTag: async resp => {
|
|
67
|
+
if (resp.attributes && resp.attributes[0] && resp.attributes[0].type === 'TEXT') {
|
|
68
|
+
let question = Buffer.from(resp.attributes[0].value, 'base64').toString();
|
|
69
|
+
switch (question.toLowerCase().replace(/:$/, '')) {
|
|
70
|
+
case 'username': {
|
|
71
|
+
let encodedUsername = Buffer.from(username).toString('base64');
|
|
72
|
+
connection.log.debug({ src: 'c', msg: encodedUsername, comment: `Encoded username for AUTH=LOGIN` });
|
|
73
|
+
connection.write(encodedUsername);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 'password':
|
|
77
|
+
connection.log.debug({ src: 'c', msg: '(* value hidden *)', comment: `Encoded password for AUTH=LOGIN` });
|
|
78
|
+
connection.write(Buffer.from(password).toString('base64'));
|
|
79
|
+
break;
|
|
80
|
+
default: {
|
|
81
|
+
let error = new Error(`Unknown LOGIN question "${question}"`);
|
|
82
|
+
throw error;
|
|
44
83
|
}
|
|
45
|
-
|
|
46
|
-
connection.log.debug({ src: 'c', msg: breaker, comment: `Error response for ${command}` });
|
|
47
|
-
connection.write(breaker);
|
|
48
84
|
}
|
|
49
85
|
}
|
|
50
|
-
|
|
51
|
-
|
|
86
|
+
}
|
|
87
|
+
});
|
|
52
88
|
|
|
53
|
-
|
|
89
|
+
response.next();
|
|
54
90
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
err.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
connection.authCapabilities.set(`AUTH=LOGIN`, true);
|
|
92
|
+
|
|
93
|
+
return username;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
let errorCode = getStatusCode(err.response);
|
|
96
|
+
if (errorCode) {
|
|
97
|
+
err.serverResponseCode = errorCode;
|
|
98
|
+
}
|
|
99
|
+
err.authenticationFailed = true;
|
|
100
|
+
err.response = await getErrorText(err.response);
|
|
101
|
+
if (errorResponse) {
|
|
102
|
+
err.oauthError = errorResponse;
|
|
103
|
+
}
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function authPlain(connection, username, password) {
|
|
109
|
+
let errorResponse = false;
|
|
110
|
+
try {
|
|
111
|
+
let response = await connection.exec('AUTHENTICATE', [{ type: 'ATOM', value: 'PLAIN' }], {
|
|
112
|
+
onPlusTag: async () => {
|
|
113
|
+
let encodedResponse = Buffer.from(['', username, password].join('\x00')).toString('base64');
|
|
114
|
+
let loggedResponse = Buffer.from(['', username, '(* value hidden *)'].join('\x00')).toString('base64');
|
|
115
|
+
connection.log.debug({ src: 'c', msg: loggedResponse, comment: `Encoded response for AUTH=PLAIN` });
|
|
116
|
+
connection.write(encodedResponse);
|
|
65
117
|
}
|
|
66
|
-
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
response.next();
|
|
121
|
+
|
|
122
|
+
connection.authCapabilities.set(`AUTH=PLAIN`, true);
|
|
123
|
+
|
|
124
|
+
return username;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
let errorCode = getStatusCode(err.response);
|
|
127
|
+
if (errorCode) {
|
|
128
|
+
err.serverResponseCode = errorCode;
|
|
129
|
+
}
|
|
130
|
+
err.authenticationFailed = true;
|
|
131
|
+
err.response = await getErrorText(err.response);
|
|
132
|
+
if (errorResponse) {
|
|
133
|
+
err.oauthError = errorResponse;
|
|
134
|
+
}
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Authenticates user using LOGIN
|
|
140
|
+
module.exports = async (connection, username, { accessToken, password }) => {
|
|
141
|
+
if (connection.state !== connection.states.NOT_AUTHENTICATED) {
|
|
142
|
+
// nothing to do here
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (accessToken) {
|
|
147
|
+
// AUTH=OAUTHBEARER and AUTH=XOAUTH in the context of OAuth2 or very similar so we can handle these together
|
|
148
|
+
if (connection.capabilities.has('AUTH=OAUTHBEARER') || connection.capabilities.has('AUTH=XOAUTH') || connection.capabilities.has('AUTH=XOAUTH2')) {
|
|
149
|
+
return await authOauth(connection, username, accessToken);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (password) {
|
|
154
|
+
if (connection.capabilities.has('AUTH=LOGIN')) {
|
|
155
|
+
return await authLogin(connection, username, password);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (connection.capabilities.has('AUTH=PLAIN')) {
|
|
159
|
+
return await authPlain(connection, username, password);
|
|
67
160
|
}
|
|
68
161
|
}
|
|
69
162
|
|
package/lib/imap-flow.js
CHANGED
|
@@ -931,9 +931,13 @@ class ImapFlow extends EventEmitter {
|
|
|
931
931
|
this.expectCapabilityUpdate = true;
|
|
932
932
|
|
|
933
933
|
if (this.options.auth.accessToken) {
|
|
934
|
-
this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, this.options.auth.accessToken);
|
|
934
|
+
this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, { accessToken: this.options.auth.accessToken });
|
|
935
935
|
} else if (this.options.auth.pass) {
|
|
936
|
-
|
|
936
|
+
if (this.capabilities.has('AUTH=LOGIN') || this.capabilities.has('AUTH=PLAIN')) {
|
|
937
|
+
this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, { password: this.options.auth.pass });
|
|
938
|
+
} else {
|
|
939
|
+
this.authenticated = await this.run('LOGIN', this.options.auth.user, this.options.auth.pass);
|
|
940
|
+
}
|
|
937
941
|
}
|
|
938
942
|
|
|
939
943
|
if (this.authenticated) {
|