imapflow 1.0.166 → 1.0.168

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.168](https://github.com/postalsys/imapflow/compare/v1.0.167...v1.0.168) (2024-11-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **imap:** Fixed issue with a single + response ([51be3a9](https://github.com/postalsys/imapflow/commit/51be3a98205cc900eba6d3cf1962bc42a8de942d))
9
+
10
+ ## [1.0.167](https://github.com/postalsys/imapflow/compare/v1.0.166...v1.0.167) (2024-11-07)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **auth:** Prefer AUTH=LOGIN and AUTH=PLAIN to LOGIN ([3efd98a](https://github.com/postalsys/imapflow/commit/3efd98a5033db29228023302fc4663316b2b28fb))
16
+
3
17
  ## [1.0.166](https://github.com/postalsys/imapflow/compare/v1.0.165...v1.0.166) (2024-11-05)
4
18
 
5
19
 
@@ -2,68 +2,161 @@
2
2
 
3
3
  const { getStatusCode, getErrorText } = require('../tools.js');
4
4
 
5
- // Authenticates user using LOGIN
6
- module.exports = async (connection, username, accessToken) => {
7
- if (connection.state !== connection.states.NOT_AUTHENTICATED) {
8
- // nothing to do here
9
- return;
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
- // AUTH=OAUTHBEARER and AUTH=XOAUTH in the context of OAuth2 or very similar so we can handle these together
13
- if (connection.capabilities.has('AUTH=OAUTHBEARER') || connection.capabilities.has('AUTH=XOAUTH') || connection.capabilities.has('AUTH=XOAUTH2')) {
14
- let oauthbearer;
15
- let command;
16
- let breaker;
17
-
18
- if (connection.capabilities.has('AUTH=OAUTHBEARER')) {
19
- oauthbearer = [`n,a=${username},`, `host=${connection.servername}`, `port=993`, `auth=Bearer ${accessToken}`, '', ''].join('\x01');
20
- command = 'OAUTHBEARER';
21
- breaker = 'AQ==';
22
- } else if (connection.capabilities.has('AUTH=XOAUTH') || connection.capabilities.has('AUTH=XOAUTH2')) {
23
- oauthbearer = [`user=${username}`, `auth=Bearer ${accessToken}`, '', ''].join('\x01');
24
- command = 'XOAUTH2';
25
- breaker = '';
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
- let errorResponse = false;
29
- try {
30
- let response = await connection.exec(
31
- 'AUTHENTICATE',
32
- [
33
- { type: 'ATOM', value: command },
34
- { type: 'ATOM', value: Buffer.from(oauthbearer).toString('base64'), sensitive: true }
35
- ],
36
- {
37
- onPlusTag: async resp => {
38
- if (resp.attributes && resp.attributes[0] && resp.attributes[0].type === 'TEXT') {
39
- try {
40
- errorResponse = JSON.parse(Buffer.from(resp.attributes[0].value, 'base64').toString());
41
- } catch (err) {
42
- connection.log.debug({ errorResponse: resp.attributes[0].value, err });
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
- response.next();
86
+ }
87
+ });
52
88
 
53
- connection.authCapabilities.set(`AUTH=${command}`, true);
89
+ response.next();
54
90
 
55
- return username;
56
- } catch (err) {
57
- let errorCode = getStatusCode(err.response);
58
- if (errorCode) {
59
- err.serverResponseCode = errorCode;
60
- }
61
- err.authenticationFailed = true;
62
- err.response = await getErrorText(err.response);
63
- if (errorResponse) {
64
- err.oauthError = errorResponse;
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
- throw err;
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
 
@@ -28,6 +28,7 @@ module.exports = async (command, options) => {
28
28
  response.tag = await parser.getTag();
29
29
 
30
30
  await parser.getSpace();
31
+
31
32
  response.command = await parser.getCommand();
32
33
 
33
34
  if (nullBytesRemoved) {
@@ -119,6 +119,11 @@ class ParserInstance {
119
119
 
120
120
  async getSpace() {
121
121
  if (!this.remainder.length) {
122
+ if (this.tag === '+' && this.pos === 1) {
123
+ // special case, empty + response
124
+ return;
125
+ }
126
+
122
127
  let error = new Error(`Unexpected end of input at position ${this.pos} [E4]`);
123
128
  error.code = 'ParserError4';
124
129
  error.parserContext = { input: this.input, pos: this.pos };
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
- this.authenticated = await this.run('LOGIN', this.options.auth.user, this.options.auth.pass);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.166",
3
+ "version": "1.0.168",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "./lib/imap-flow.js",
6
6
  "scripts": {