Skip to content

Commit

Permalink
fix(password-auth): Added option auth.loginMethod to set specific aut…
Browse files Browse the repository at this point in the history
…hentication method ('LOGIN', 'AUTH=PLAIN', 'AUTH=LOGIN')
  • Loading branch information
andris9 committed Jan 3, 2025
1 parent 214d195 commit ce3c339
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 100 deletions.
7 changes: 4 additions & 3 deletions lib/commands/authenticate.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ async function authPlain(connection, username, password) {
}

// Authenticates user using LOGIN
module.exports = async (connection, username, { accessToken, password }) => {
module.exports = async (connection, username, { accessToken, password, loginMethod }) => {
if (connection.state !== connection.states.NOT_AUTHENTICATED) {
// nothing to do here
return;
Expand All @@ -151,10 +151,11 @@ module.exports = async (connection, username, { accessToken, password }) => {
}

if (password) {
if (connection.capabilities.has('AUTH=PLAIN')) {
if ((!loginMethod && connection.capabilities.has('AUTH=PLAIN')) || loginMethod === 'AUTH=PLAIN') {
return await authPlain(connection, username, password);
}
if (connection.capabilities.has('AUTH=LOGIN')) {

if ((!loginMethod && connection.capabilities.has('AUTH=LOGIN')) || loginMethod === 'AUTH=LOGIN') {
return await authLogin(connection, username, password);
}
}
Expand Down
154 changes: 107 additions & 47 deletions lib/imap-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,48 +116,110 @@ class ImapFlow extends EventEmitter {
static version = packageInfo.version;

/**
* @param {Object} options IMAP connection options
* @param {String} options.host Hostname of the IMAP server
* @param {Number} options.port Port number for the IMAP server
* @param {Boolean} [options.secure=false] Should the connection be established immediately and directly over TLS? Typically on port 993.
* @param {Boolean} [options.doSTARTTLS=undefined] Should the connection be established using STARTTLS?
* * If `true`, the connection is first established as unencrypted and then upgraded to TLS using STARTTLS, before authentication.
* If the server does not advertize the `STARTTLS` `CAPABILITY`, or the upgrade fails for other reasons, then the connection fails.
* Note: The combination `secure=true` (direct TLS) and `doSTARTTLS=true` is invalid.
* * If `false`, then STARTTLS will not be used, even if the server advertizes it in IMAP `CAPABILITY`.
* This helps with servers that have a broken TLS configuration.
* If `doSTARTTLS=false` and `secure=false`, then a plain unencrypted socket is used.
* Be sure to clearly warn the user about the consequences.
* * If `undefined` (default) and `secure=false` (default), the connection is upgraded using STARTTLS before authentication, /only if possible/ .
* If not possible, the connection will use an unencrypted plain socket.
* This can mean TLS is used under normal circumstances, but a serious attacker can force an unencrypted connection and steal passwords,
* called "downgrade attack". This can lead to a false sense of security. Be sure to warn the user.
* @param {String} [options.servername] Servername for SNI (or when host is set to an IP address)
* @param {Boolean} [options.disableCompression=false] if `true` then client does not try to use COMPRESS=DEFLATE extension
* @param {Object} options.auth Authentication options. Authentication is requested automatically during <code>connect()</code>
* @param {String} options.auth.user Usename
* @param {String} [options.auth.pass] Password, if using regular authentication
* @param {String} [options.auth.accessToken] OAuth2 Access Token, if using OAuth2 authentication
* @param {IdInfoObject} [options.clientInfo] Client identification info
* @param {Boolean} [options.disableAutoIdle=false] if `true` then IDLE is not started automatically. Useful if you only need to perform specific tasks over the connection
* @param {Object} [options.tls] Additional TLS options (see [Node.js TLS connect](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) for all available options)
* @param {Boolean} [options.tls.rejectUnauthorized=true] if `false` then client accepts self-signed and expired certificates from the server
* @param {String} [options.tls.minVersion=TLSv1.2] To improvde security you might need to use something newer, eg *'TLSv1.2'*
* @param {Number} [options.tls.minDHSize=1024] Minimum size of the DH parameter in bits to accept a TLS connection
* @param {Object} [options.logger] Custom logger instance with `debug(obj)`, `info(obj)`, `warn(obj)` and `error(obj)` methods. If not provided then ImapFlow logs to console using pino format. Can be disabled by setting to `false`
* @param {Boolean} [options.logRaw=false] If true then log data read from and written to socket encoded in base64
* @param {Boolean} [options.emitLogs=false] If `true` then in addition of sending data to logger, ImapFlow emits 'log' events with the same data
* @param {Boolean} [options.verifyOnly=false] If `true` then logs out automatically after successful authentication
* @param {String} [options.proxy] Optional proxy URL. Supports HTTP CONNECT (`http://`, `https://`) and SOCKS (`socks://`, `socks4://`, `socks5://`) proxies
* @param {Boolean} [options.qresync=false] If true, then enables QRESYNC support. EXPUNGE notifications will include `uid` property instead of `seq`
* @param {Number} [options.maxIdleTime] If set, then breaks and restarts IDLE every maxIdleTime ms
* @param {String} [options.missingIdleCommand="NOOP"] Which command to use if server does not support IDLE
* @param {Boolean} [options.disableBinary=false] If true, then ignores the BINARY extension when making FETCH and APPEND calls
* @param {Boolean} [options.disableAutoEnable] Do not enable supported extensions by default
* @param {Number} [options.connectionTimeout=90000] how many milliseconds to wait for the connection to establish (default is 90 seconds)
* @param {Number} [options.greetingTimeout=16000] how many milliseconds to wait for the greeting after connection is established (default is 16 seconds)
* @param {Number} [options.socketTimeout=300000] how many milliseconds of inactivity to allow (default is 5 minutes)
* IMAP connection options
*
* @property {String} host
* Hostname of the IMAP server.
*
* @property {Number} port
* Port number for the IMAP server.
*
* @property {Boolean} [secure=false]
* If `true`, establishes the connection directly over TLS (commonly on port 993).
* If `false`, a plain (unencrypted) connection is used first and, if possible, the connection is upgraded to STARTTLS.
*
* @property {Boolean} [doSTARTTLS=undefined]
* Determines whether to upgrade the connection to TLS via STARTTLS:
* - **true**: Start unencrypted and upgrade to TLS using STARTTLS before authentication.
* The connection fails if the server does not support STARTTLS or the upgrade fails.
* Note that `secure=true` combined with `doSTARTTLS=true` is invalid.
* - **false**: Never use STARTTLS, even if the server advertises support.
* This is useful if the server has a broken TLS setup.
* Combined with `secure=false`, this results in a fully unencrypted connection.
* Make sure you warn users about the security risks.
* - **undefined** (default): If `secure=false` (default), attempt to upgrade to TLS via STARTTLS before authentication if the server supports it. If not supported, continue unencrypted. This may expose the connection to a downgrade attack.
*
* @property {String} [servername]
* Server name for SNI or when using an IP address as `host`.
*
* @property {Boolean} [disableCompression=false]
* If `true`, the client does not attempt to use the COMPRESS=DEFLATE extension.
*
* @property {Object} auth
* Authentication options. Authentication occurs automatically during {@link connect}.
*
* @property {String} auth.user
* Username for authentication.
*
* @property {String} [auth.pass]
* Password for regular authentication.
*
* @property {String} [auth.accessToken]
* OAuth2 access token, if using OAuth2 authentication.
*
* @property {String} [auth.loginMethod]
* Optional login method for password-based authentication (e.g., "LOGIN", "AUTH=LOGIN", or "AUTH=PLAIN").
* If not set, ImapFlow chooses based on available mechanisms.
*
* @property {IdInfoObject} [clientInfo]
* Client identification info sent to the server (via the ID command).
*
* @property {Boolean} [disableAutoIdle=false]
* If `true`, do not start IDLE automatically. Useful when only specific operations are needed.
*
* @property {Object} [tls]
* Additional TLS options. For details, see [Node.js TLS connect](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback).
*
* @property {Boolean} [tls.rejectUnauthorized=true]
* If `false`, allows self-signed or expired certificates.
*
* @property {String} [tls.minVersion='TLSv1.2']
* Minimum accepted TLS version (e.g., `'TLSv1.2'`).
*
* @property {Number} [tls.minDHSize=1024]
* Minimum size (in bits) of the DH parameter for TLS connections.
*
* @property {Object|Boolean} [logger]
* Custom logger instance with `debug(obj)`, `info(obj)`, `warn(obj)`, and `error(obj)` methods.
* If `false`, logging is disabled. If not provided, ImapFlow logs to console in [pino format](https://getpino.io/).
*
* @property {Boolean} [logRaw=false]
* If `true`, logs all raw data (read and written) in base64 encoding. You can pipe such logs to [eerawlog](https://github.com/postalsys/eerawlog) command for readable output.
*
* @property {Boolean} [emitLogs=false]
* If `true`, emits `'log'` events with the same data passed to the logger.
*
* @property {Boolean} [verifyOnly=false]
* If `true`, disconnects after successful authentication without performing other actions.
*
* @property {String} [proxy]
* Proxy URL. Supports HTTP CONNECT (`http://`, `https://`) and SOCKS (`socks://`, `socks4://`, `socks5://`).
*
* @property {Boolean} [qresync=false]
* If `true`, enables QRESYNC support so that EXPUNGE notifications include `uid` instead of `seq`.
*
* @property {Number} [maxIdleTime]
* If set, breaks and restarts IDLE every `maxIdleTime` milliseconds.
*
* @property {String} [missingIdleCommand="NOOP"]
* Command to use if the server does not support IDLE.
*
* @property {Boolean} [disableBinary=false]
* If `true`, ignores the BINARY extension for FETCH and APPEND operations.
*
* @property {Boolean} [disableAutoEnable=false]
* If `true`, do not automatically enable supported IMAP extensions.
*
* @property {Number} [connectionTimeout=90000]
* Maximum time (in milliseconds) to wait for the connection to establish. Defaults to 90 seconds.
*
* @property {Number} [greetingTimeout=16000]
* Maximum time (in milliseconds) to wait for the server greeting after a connection is established. Defaults to 16 seconds.
*
* @property {Number} [socketTimeout=300000]
* Maximum period of inactivity (in milliseconds) before terminating the connection. Defaults to 5 minutes.
*/

constructor(options) {
super({ captureRejections: true });

Expand Down Expand Up @@ -277,10 +339,6 @@ class ImapFlow extends EventEmitter {
*/
this.idling = false;

/**
* If `true` then in addition of sending data to logger, ImapFlow emits 'log' events with the same data
* @type {Boolean}
*/
this.emitLogs = !!this.options.emitLogs;
// ordering number for emitted logs
this.lo = 0;
Expand Down Expand Up @@ -986,11 +1044,13 @@ class ImapFlow extends EventEmitter {

this.expectCapabilityUpdate = true;

let loginMethod = (this.options.auth.loginMethod || '').toString().trim().toUpperCase();

if (this.options.auth.accessToken) {
this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, { accessToken: this.options.auth.accessToken });
} else if (this.options.auth.pass) {
if (this.capabilities.has('AUTH=LOGIN') || this.capabilities.has('AUTH=PLAIN')) {
this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, { password: this.options.auth.pass });
if ((this.capabilities.has('AUTH=LOGIN') || this.capabilities.has('AUTH=PLAIN')) && loginMethod !== 'LOGIN') {
this.authenticated = await this.run('AUTHENTICATE', this.options.auth.user, { password: this.options.auth.pass, loginMethod });
} else {
this.authenticated = await this.run('LOGIN', this.options.auth.user, this.options.auth.pass);
}
Expand Down
Loading

0 comments on commit ce3c339

Please sign in to comment.