imapflow 1.0.195 → 1.0.197
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 +14 -0
- package/README.md +25 -0
- package/lib/commands/authenticate.js +12 -7
- package/lib/imap-flow.d.ts +2 -0
- package/lib/imap-flow.js +12 -2
- package/package.json +5 -5
- package/test/authentication-test.js +25 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.197](https://github.com/postalsys/imapflow/compare/v1.0.196...v1.0.197) (2025-09-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **imap ID:** Re-request ID information if only a single key was returned ([87bc016](https://github.com/postalsys/imapflow/commit/87bc016ae677b4428916d4a2047abcdeb95950bb))
|
|
9
|
+
|
|
10
|
+
## [1.0.196](https://github.com/postalsys/imapflow/compare/v1.0.195...v1.0.196) (2025-09-14)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* Added authzid option for AUTH=PLAIN authentication ([541c43a](https://github.com/postalsys/imapflow/commit/541c43ab0f15608c7d28f19c810e1a14bc8519bc))
|
|
16
|
+
|
|
3
17
|
## [1.0.195](https://github.com/postalsys/imapflow/compare/v1.0.194...v1.0.195) (2025-08-26)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -73,6 +73,31 @@ const main = async () => {
|
|
|
73
73
|
main().catch(err => console.error(err));
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
### Admin Impersonation / Delegation (SASL PLAIN with authzid)
|
|
77
|
+
|
|
78
|
+
ImapFlow supports admin impersonation for mail systems like Zimbra that allow administrators to access user mailboxes. This is done using the SASL PLAIN mechanism with an authorization identity (`authzid`).
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
const { ImapFlow } = require('imapflow');
|
|
82
|
+
const client = new ImapFlow({
|
|
83
|
+
host: 'mail.example.com',
|
|
84
|
+
port: 993,
|
|
85
|
+
secure: true,
|
|
86
|
+
auth: {
|
|
87
|
+
user: 'admin@example.com', // Admin credentials (authentication identity)
|
|
88
|
+
pass: 'adminpassword',
|
|
89
|
+
authzid: 'user@example.com', // User to impersonate (authorization identity)
|
|
90
|
+
loginMethod: 'AUTH=PLAIN' // Must use PLAIN mechanism for authzid
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Connection will authenticate as admin but authorize as the specified user
|
|
95
|
+
await client.connect();
|
|
96
|
+
// Now operating on user@example.com's mailbox as admin
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Note:** The `authzid` parameter only works with the `AUTH=PLAIN` mechanism. The server must support admin delegation/impersonation for this to work.
|
|
100
|
+
|
|
76
101
|
## Documentation
|
|
77
102
|
|
|
78
103
|
[API reference](https://imapflow.com/module-imapflow-ImapFlow.html).
|
|
@@ -110,14 +110,18 @@ async function authLogin(connection, username, password) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
async function authPlain(connection, username, password) {
|
|
113
|
+
async function authPlain(connection, username, password, authzid) {
|
|
114
114
|
let errorResponse = false;
|
|
115
115
|
try {
|
|
116
116
|
let response = await connection.exec('AUTHENTICATE', [{ type: 'ATOM', value: 'PLAIN' }], {
|
|
117
117
|
onPlusTag: async () => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
// SASL PLAIN format: [authzid]\x00authcid\x00password
|
|
119
|
+
// authzid: authorization identity (who to impersonate)
|
|
120
|
+
// authcid: authentication identity (who is authenticating)
|
|
121
|
+
let authzidValue = authzid || '';
|
|
122
|
+
let encodedResponse = Buffer.from([authzidValue, username, password].join('\x00')).toString('base64');
|
|
123
|
+
let loggedResponse = Buffer.from([authzidValue, username, '(* value hidden *)'].join('\x00')).toString('base64');
|
|
124
|
+
connection.log.debug({ src: 'c', msg: loggedResponse, comment: `Encoded response for AUTH=PLAIN${authzid ? ' with authzid' : ''}` });
|
|
121
125
|
connection.write(encodedResponse);
|
|
122
126
|
}
|
|
123
127
|
});
|
|
@@ -126,7 +130,8 @@ async function authPlain(connection, username, password) {
|
|
|
126
130
|
|
|
127
131
|
connection.authCapabilities.set(`AUTH=PLAIN`, true);
|
|
128
132
|
|
|
129
|
-
|
|
133
|
+
// Return the identity we're authorized as (authzid if provided, otherwise username)
|
|
134
|
+
return authzid || username;
|
|
130
135
|
} catch (err) {
|
|
131
136
|
let errorCode = getStatusCode(err.response);
|
|
132
137
|
if (errorCode) {
|
|
@@ -142,7 +147,7 @@ async function authPlain(connection, username, password) {
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
// Authenticates user using LOGIN
|
|
145
|
-
module.exports = async (connection, username, { accessToken, password, loginMethod }) => {
|
|
150
|
+
module.exports = async (connection, username, { accessToken, password, loginMethod, authzid }) => {
|
|
146
151
|
if (connection.state !== connection.states.NOT_AUTHENTICATED) {
|
|
147
152
|
// nothing to do here
|
|
148
153
|
return;
|
|
@@ -157,7 +162,7 @@ module.exports = async (connection, username, { accessToken, password, loginMeth
|
|
|
157
162
|
|
|
158
163
|
if (password) {
|
|
159
164
|
if ((!loginMethod && connection.capabilities.has('AUTH=PLAIN')) || loginMethod === 'AUTH=PLAIN') {
|
|
160
|
-
return await authPlain(connection, username, password);
|
|
165
|
+
return await authPlain(connection, username, password, authzid);
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
if ((!loginMethod && connection.capabilities.has('AUTH=LOGIN')) || loginMethod === 'AUTH=LOGIN') {
|
package/lib/imap-flow.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface ImapFlowOptions {
|
|
|
23
23
|
accessToken?: string;
|
|
24
24
|
/** Optional login method override. Set to 'LOGIN', 'AUTH=LOGIN' or 'AUTH=PLAIN' to use specific method */
|
|
25
25
|
loginMethod?: string;
|
|
26
|
+
/** Authorization identity for SASL PLAIN (used for admin impersonation/delegation). When set, authenticates as `user` but authorizes as `authzid` */
|
|
27
|
+
authzid?: string;
|
|
26
28
|
};
|
|
27
29
|
/** Client identification info sent to the server if server supports ID extension */
|
|
28
30
|
clientInfo?: IdInfoObject;
|
package/lib/imap-flow.js
CHANGED
|
@@ -158,6 +158,12 @@ class ImapFlow extends EventEmitter {
|
|
|
158
158
|
* Optional login method for password-based authentication (e.g., "LOGIN", "AUTH=LOGIN", or "AUTH=PLAIN").
|
|
159
159
|
* If not set, ImapFlow chooses based on available mechanisms.
|
|
160
160
|
*
|
|
161
|
+
* @property {String} [auth.authzid]
|
|
162
|
+
* Authorization identity for SASL PLAIN authentication (used for admin impersonation/delegation).
|
|
163
|
+
* When set, authenticates as `auth.user` but authorizes as `auth.authzid`.
|
|
164
|
+
* This is typically used in mail systems like Zimbra for admin users to access other users' mailboxes.
|
|
165
|
+
* Only works with AUTH=PLAIN mechanism.
|
|
166
|
+
*
|
|
161
167
|
* @property {IdInfoObject} [clientInfo]
|
|
162
168
|
* Client identification info sent to the server (via the ID command).
|
|
163
169
|
*
|
|
@@ -851,7 +857,7 @@ class ImapFlow extends EventEmitter {
|
|
|
851
857
|
|
|
852
858
|
await this.authenticate();
|
|
853
859
|
|
|
854
|
-
if (!this.idRequested && this.capabilities.has('ID')) {
|
|
860
|
+
if ((!this.idRequested || Object.keys(this.idRequested).length < 2) && this.capabilities.has('ID')) {
|
|
855
861
|
// re-request ID after LOGIN
|
|
856
862
|
this.idRequested = await this.run('ID', this.clientInfo);
|
|
857
863
|
}
|
|
@@ -1140,7 +1146,11 @@ class ImapFlow extends EventEmitter {
|
|
|
1140
1146
|
this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, { accessToken: this.options.auth.accessToken });
|
|
1141
1147
|
} else if (this.options.auth.pass) {
|
|
1142
1148
|
if ((this.capabilities.has('AUTH=LOGIN') || this.capabilities.has('AUTH=PLAIN')) && loginMethod !== 'LOGIN') {
|
|
1143
|
-
this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, {
|
|
1149
|
+
this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, {
|
|
1150
|
+
password: this.options.auth.pass,
|
|
1151
|
+
loginMethod,
|
|
1152
|
+
authzid: this.options.auth.authzid
|
|
1153
|
+
});
|
|
1144
1154
|
} else {
|
|
1145
1155
|
if (this.capabilities.has('LOGINDISABLED')) {
|
|
1146
1156
|
throw new AuthenticationFailure('Login is disabled');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.197",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "lib/imap-flow.js",
|
|
6
6
|
"types": "lib/imap-flow.d.ts",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://imapflow.com/",
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@babel/eslint-parser": "7.28.
|
|
31
|
+
"@babel/eslint-parser": "7.28.4",
|
|
32
32
|
"@babel/eslint-plugin": "7.27.1",
|
|
33
33
|
"@babel/plugin-syntax-class-properties": "7.12.13",
|
|
34
34
|
"@babel/preset-env": "7.28.3",
|
|
35
|
-
"@types/node": "24.
|
|
35
|
+
"@types/node": "24.4.0",
|
|
36
36
|
"eslint": "8.57.0",
|
|
37
37
|
"eslint-config-nodemailer": "1.2.0",
|
|
38
38
|
"eslint-config-prettier": "9.1.0",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"libmime": "5.3.7",
|
|
53
53
|
"libqp": "2.1.1",
|
|
54
54
|
"mailsplit": "5.4.6",
|
|
55
|
-
"nodemailer": "7.0.
|
|
56
|
-
"pino": "9.9.
|
|
55
|
+
"nodemailer": "7.0.6",
|
|
56
|
+
"pino": "9.9.5",
|
|
57
57
|
"socks": "2.8.7"
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -11,7 +11,7 @@ module.exports['Authentication: Password auth configuration'] = test => {
|
|
|
11
11
|
pass: 'testpass'
|
|
12
12
|
}
|
|
13
13
|
});
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
test.equal(client.options.auth.user, 'testuser');
|
|
16
16
|
test.equal(client.options.auth.pass, 'testpass');
|
|
17
17
|
test.done();
|
|
@@ -25,7 +25,7 @@ module.exports['Authentication: OAuth2 auth configuration'] = test => {
|
|
|
25
25
|
accessToken: 'oauth2_token_here'
|
|
26
26
|
}
|
|
27
27
|
});
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
test.equal(client.options.auth.user, 'testuser');
|
|
30
30
|
test.equal(client.options.auth.accessToken, 'oauth2_token_here');
|
|
31
31
|
test.done();
|
|
@@ -40,14 +40,32 @@ module.exports['Authentication: Login method specification'] = test => {
|
|
|
40
40
|
loginMethod: 'AUTH=PLAIN'
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
|
-
|
|
43
|
+
|
|
44
|
+
test.equal(client.options.auth.loginMethod, 'AUTH=PLAIN');
|
|
45
|
+
test.done();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
module.exports['Authentication: SASL PLAIN with authzid for impersonation'] = test => {
|
|
49
|
+
let client = new ImapFlow({
|
|
50
|
+
host: 'imap.example.com',
|
|
51
|
+
auth: {
|
|
52
|
+
user: 'admin@example.com',
|
|
53
|
+
pass: 'adminpass',
|
|
54
|
+
authzid: 'user@example.com',
|
|
55
|
+
loginMethod: 'AUTH=PLAIN'
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test.equal(client.options.auth.user, 'admin@example.com');
|
|
60
|
+
test.equal(client.options.auth.pass, 'adminpass');
|
|
61
|
+
test.equal(client.options.auth.authzid, 'user@example.com');
|
|
44
62
|
test.equal(client.options.auth.loginMethod, 'AUTH=PLAIN');
|
|
45
63
|
test.done();
|
|
46
64
|
};
|
|
47
65
|
|
|
48
66
|
module.exports['Authentication: AuthenticationFailure error structure'] = test => {
|
|
49
67
|
let error = new AuthenticationFailure('Invalid credentials');
|
|
50
|
-
|
|
68
|
+
|
|
51
69
|
test.ok(error instanceof Error);
|
|
52
70
|
test.equal(error.constructor.name, 'AuthenticationFailure');
|
|
53
71
|
test.equal(error.message, 'Invalid credentials');
|
|
@@ -63,7 +81,7 @@ module.exports['Authentication: Verify-only mode'] = test => {
|
|
|
63
81
|
},
|
|
64
82
|
verifyOnly: true
|
|
65
83
|
});
|
|
66
|
-
|
|
84
|
+
|
|
67
85
|
test.equal(client.options.verifyOnly, true);
|
|
68
86
|
test.done();
|
|
69
87
|
};
|
|
@@ -77,7 +95,7 @@ module.exports['Authentication: Disable auto IDLE'] = test => {
|
|
|
77
95
|
},
|
|
78
96
|
disableAutoIdle: true
|
|
79
97
|
});
|
|
80
|
-
|
|
98
|
+
|
|
81
99
|
test.equal(client.options.disableAutoIdle, true);
|
|
82
100
|
test.done();
|
|
83
|
-
};
|
|
101
|
+
};
|