VulpesOS/gaia/apps/email/js/ext/imap/client.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

298 lines
11 KiB
JavaScript
Executable file

/**
* imap/client.js: Wraps IMAP connection creation, to avoid redundancy
* between connection-related setup in imap/account.js and
* imap/probe.js.
*/
define(function(require, exports) {
var BrowserBox = require('browserbox');
var ImapClient = require('browserbox-imap');
var imapHandler = require('imap-handler');
var logic = require('logic');
var syncbase = require('../syncbase');
var errorutils = require('../errorutils');
var oauth = require('../oauth');
var setTimeout = window.setTimeout;
var clearTimeout = window.clearTimeout;
exports.setTimeoutFunctions = function(setFn, clearFn) {
setTimeout = setFn;
clearTimeout = clearFn;
};
function noop() {
// nothing
}
var scope = logic.scope('ImapClient');
/**
* Open a connection to an IMAP server.
*
* @param {object} credentials
* keys: username, password, [oauth2]
* @param {object} connInfo
* keys: hostname, port, crypto
* @param {function(credentials)} credsUpdatedCallback
* Callback, called if the credentials have been updated and
* should be stored to disk. Not called if the credentials are
* already up-to-date.
* @return {Promise}
* resolve => {BrowserBox} conn
* reject => {String} normalized String error
*/
exports.createImapConnection = function(credentials, connInfo,
credsUpdatedCallback) {
var conn;
return oauth.ensureUpdatedCredentials(credentials, credsUpdatedCallback)
.then(function() {
return new Promise(function(resolve, reject) {
conn = new BrowserBox(
connInfo.hostname,
connInfo.port, {
auth: {
user: credentials.username,
pass: credentials.password,
xoauth2: credentials.oauth2 ?
credentials.oauth2.accessToken : null
},
id: {
vendor: 'Mozilla',
name: 'GaiaMail',
version: '0.2',
'support-url': 'http://mzl.la/file-gaia-email-bug'
},
useSecureTransport: (connInfo.crypto === 'ssl' ||
connInfo.crypto === true),
requireTLS: connInfo.crypto === 'starttls',
// In the case no encryption is explicitly requested (either for
// testing or because a user regrettably chose to disable it via
// manual config), we want to avoid opportunistic encryption
// since in the latter case the user may have done this because
// the server's certificates are invalid.
ignoreTLS: connInfo.crypto === 'plain'
});
var connectTimeout = setTimeout(function() {
conn.onerror('unresponsive-server');
conn.close();
}, syncbase.CONNECT_TIMEOUT_MS);
conn.onauth = function() {
clearTimeout(connectTimeout);
logic(scope, 'connected', { connInfo: connInfo });
conn.onauth = conn.onerror = noop;
resolve(conn);
};
conn.onerror = function(err) {
clearTimeout(connectTimeout);
// XXX: if error is just expired access token, try to refresh one time
reject(err);
};
conn.connect();
});
}).catch(function(errorObject) {
var errorString = normalizeImapError(conn, errorObject);
if (conn) {
conn.close();
}
// Could hit an oauth reauth case due to date skews, so give a token
// review a shot before really bailing.
if (errorString === 'needs-oauth-reauth' &&
oauth.isRenewPossible(credentials)) {
return oauth.ensureUpdatedCredentials(credentials,
credsUpdatedCallback, true)
.then(function() {
return exports.createImapConnection(credentials, connInfo,
credsUpdatedCallback);
});
} else {
logic(scope, 'connect-error', {
error: errorString
});
throw errorString;
}
});
};
//****************************************************************
// UNFORTUNATE IMAP WORKAROUNDS & SHIMS BEGIN HERE
//----------------------------------------------------------------
// ImapClient (from BrowserBox) doesn't pass along any useful error
// information other than a human-readable string, and even then,
// not always reliably. When we receive an IMAP protocol error
// response form the server, all of the data we care about is in the
// most recent 'NO' or 'BAD' response line. Until we can add
// improved error handling upstream, cache the most recent error
// response from the IMAP server so that we can extract detailed
// error codes when we handle error events from the IMAP library.
var processResponse = ImapClient.prototype._processResponse;
ImapClient.prototype._processResponse = function(response) {
processResponse.apply(this, arguments);
var cmd = (response && response.command || '').toString()
.toUpperCase().trim();
if (['NO', 'BAD'].indexOf(cmd) !== -1) {
logic(scope, 'protocol-error', {
humanReadable: response.humanReadable,
responseCode: response.code,
// Include the command structure
commandData: this._currentCommand && this._currentCommand.request &&
imapHandler.compiler(this._currentCommand.request)
});
this._lastImapError = {
// To most accurately report STARTTLS issues, latch the active command
// at the time of the failure rather than just the response. (An evil
// attacker could say "NO SUCKER" instead of "NO STARTTLS" or
// something.)
command: this._currentCommand,
response: response
};
}
}
// ImapClient passes data directly into the `new Error()`
// constructor, which causes err.message to equal "[Object object]"
// rather than the actual error object with details. This is just a
// copy of that function, with the `new Error` constructor stripped
// out so that the error details pass through to onerror.
ImapClient.prototype._onError = function(evt) {
if (this.isError(evt)) {
this.onerror(evt);
} else if (evt && this.isError(evt.data)) {
this.onerror(evt.data);
} else {
this.onerror(evt && evt.data && evt.data.message ||
evt.data || evt || 'Error');
}
this.close();
};
/**
* Given an error possibly generated by the IMAP client, analyze it
* and convert it to a normalized string if possible. Otherwise,
* return null.
*/
function analyzeLastImapError(lastErrInfo, conn) {
// Make sure it's an error we know how to analyze:
if (!lastErrInfo || !lastErrInfo.response) {
return null;
}
// If the most recent command was to initiate STARTTLS, then this is a
// security error.
if (lastErrInfo.command && lastErrInfo.command.request &&
lastErrInfo.command.request.command === 'STARTTLS') {
return 'bad-security';
}
var wasOauth = conn && !!conn.options.auth.xoauth2;
// Structure of an IMAP error response:
// { "tag":"W2",
// "command": "NO",
// "code": "AUTHENTICATIONFAILED",
// "attributes": [
// {"type":"TEXT","value":"invalid password"}
// ],
// "humanReadable": "invalid password" }
// Dovecot says after a delay and does not terminate the connection:
// NO [AUTHENTICATIONFAILED] Authentication failed.
// Zimbra 7.2.x says after a delay and DOES terminate the connection:
// NO LOGIN failed
// * BYE Zimbra IMAP server terminating connection
// Yahoo says after a delay and does not terminate the connection:
// NO [AUTHENTICATIONFAILED] Incorrect username or password.
var err = lastErrInfo.response;
var str = (err.code || '') + (err.humanReadable || '');
if (/Your account is not enabled for IMAP use/.test(str) ||
/IMAP access is disabled for your domain/.test(str)) {
return 'imap-disabled';
} else if (/UNAVAILABLE/.test(str)) {
return 'server-maintenance';
// If login failed and LOGINDISABLED was claimed, then it's
// server-maintenance. We used to be more aggressive about
// LOGINDISABLED and would just give up if we saw it, but we had a
// bad regression, so the balance has tipped here. Additionally, it
// makes a lot of sense to only report the error if we actually failed
// to login!
} else if (conn.capability.indexOf('LOGINDISABLED') != -1 &&
!conn.authenticated) {
return 'server-maintenance';
// The invalid-credentials case goes last, because we
// optimistically assume that any other not-authenticated failure
// is caused by the user's invalid credentials.
} else if (/AUTHENTICATIONFAILED/.test(str) ||
/Invalid credentials/i.test(str) || // Gmail bad access token
/login failed/i.test(str) ||
/password/.test(str) ||
// We can't trust state since it gets updated on close, but
// authenticated latches to true and stays that way.
!conn.authenticated) {
// If we got a protocol-level error but we weren't authenticated
// yet, it's likely an authentication problem, as authenticating
// is the first thing we do. Any other socket-level connection
// problems (including STARTTLS, since we pass that along as an
// exception) will be surfaced before hitting this conditional.
if (wasOauth) {
// Gmail returns "NO [ALERT] Invalid credentials (Failure)" in
// the case of a failed OAUTH password.
return 'needs-oauth-reauth';
} else {
return 'bad-user-or-pass';
}
} else {
return null;
}
}
/**
* Here's where the cascades come in: This function accepts an error
* of potentially unknown origin. Maybe it's an exception; maybe
* it's an IMAP library error with details; maybe it has no useful
* details, in which case we can see if the IMAP connection reported
* any error responses. No matter what, we take an IMAP-related
* error, analyze it, and convert it to our normalized string error
* representation.
*
* @param {BrowserBox} conn
* The IMAP connection; necessary if you expect us to analyze
* protocol errors
* @param {object} err
* The exception or error that started all the trouble.
*/
var normalizeImapError = exports.normalizeImapError = function(conn, err) {
var socketLevelError = errorutils.analyzeException(err);
var protocolLevelError =
conn && analyzeLastImapError(conn.client._lastImapError, conn);
var reportAs = (socketLevelError ||
protocolLevelError ||
'unknown');
logic(scope, 'normalized-error', {
error: err,
errorName: err && err.name,
errorMessage: err && err.message,
errorStack: err && err.stack,
socketLevelError: socketLevelError,
protocolLevelError: protocolLevelError,
reportAs: reportAs
});
return reportAs;
};
});