imapflow 1.0.195 → 1.0.196

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.196](https://github.com/postalsys/imapflow/compare/v1.0.195...v1.0.196) (2025-09-14)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * Added authzid option for AUTH=PLAIN authentication ([541c43a](https://github.com/postalsys/imapflow/commit/541c43ab0f15608c7d28f19c810e1a14bc8519bc))
9
+
3
10
  ## [1.0.195](https://github.com/postalsys/imapflow/compare/v1.0.194...v1.0.195) (2025-08-26)
4
11
 
5
12
 
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
- let encodedResponse = Buffer.from(['', username, password].join('\x00')).toString('base64');
119
- let loggedResponse = Buffer.from(['', username, '(* value hidden *)'].join('\x00')).toString('base64');
120
- connection.log.debug({ src: 'c', msg: loggedResponse, comment: `Encoded response for AUTH=PLAIN` });
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
- return username;
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') {
@@ -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
  *
@@ -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, { password: this.options.auth.pass, loginMethod });
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.195",
3
+ "version": "1.0.196",
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.0",
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.3.0",
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.5",
56
- "pino": "9.9.0",
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
+ };