Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
298 lines
11 KiB
JavaScript
Executable file
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;
|
|
};
|
|
|
|
});
|