Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
311 lines
10 KiB
JavaScript
Executable file
311 lines
10 KiB
JavaScript
Executable file
define(function(require) {
|
|
|
|
var logic = require('logic');
|
|
var client = require('./client');
|
|
var DisasterRecovery = require('../disaster-recovery');
|
|
|
|
function SmtpAccount(universe, compositeAccount, accountId, credentials,
|
|
connInfo) {
|
|
this.universe = universe;
|
|
logic.defineScope(this, 'Account', { accountId: accountId,
|
|
accountType: 'smtp' });
|
|
this.compositeAccount = compositeAccount;
|
|
this.accountId = accountId;
|
|
this.credentials = credentials;
|
|
this.connInfo = connInfo;
|
|
this._activeConnections = [];
|
|
}
|
|
|
|
SmtpAccount.prototype = {
|
|
type: 'smtp',
|
|
toString: function() {
|
|
return '[SmtpAccount: ' + this.id + ']';
|
|
},
|
|
|
|
get numActiveConns() {
|
|
return this._activeConnections.length;
|
|
},
|
|
|
|
shutdown: function() {
|
|
// Nothing to do.
|
|
},
|
|
|
|
accountDeleted: function() {
|
|
this.shutdown();
|
|
},
|
|
|
|
/**
|
|
* Asynchronously send an e-mail message. Does not provide retries, offline
|
|
* remembering of the command, or any follow-on logic like appending the
|
|
* message to the sent folder.
|
|
*
|
|
* @args[
|
|
* @param[composedMessage MailComposer]{
|
|
* A mailcomposer instance that has already generated its message payload
|
|
* to its _outputBuffer field. We previously used streaming generation,
|
|
* but have abandoned this for now for IMAP Sent folder saving purposes.
|
|
* Namely, our IMAP implementation doesn't support taking a stream for
|
|
* APPEND right now, and there's no benefit to doing double the work and
|
|
* generating extra garbage.
|
|
* }
|
|
* @param[callback @func[
|
|
* @args[
|
|
* @param[error @oneof[
|
|
* @case[null]{
|
|
* No error, message sent successfully.
|
|
* }
|
|
* @case['bad-user-or-pass']{
|
|
* Authentication problem. This should probably be escalated to
|
|
* the user so they can fix their password.
|
|
* }
|
|
* @case['bad-sender']{
|
|
* We logged in, but it didn't like our sender e-mail.
|
|
* }
|
|
* @case['bad-recipient']{
|
|
* There were one or more bad recipients; they are listed in the
|
|
* next argument.
|
|
* }
|
|
* @case['bad-message']{
|
|
* It failed during the sending of the message.
|
|
* }
|
|
* @case['server-maybe-offline']{
|
|
* The server won't let us login, maybe because of a bizarre offline
|
|
* for service strategy? (We've seen this with IMAP before...)
|
|
*
|
|
* This should be considered a fatal problem during probing or if
|
|
* it happens consistently.
|
|
* }
|
|
* @case['insecure']{
|
|
* We couldn't establish a secure connection.
|
|
* }
|
|
* @case['connection-lost']{
|
|
* The connection went away, we don't know why. Could be a
|
|
* transient thing, could be a jerky server, who knows.
|
|
* }
|
|
* @case['unknown']{
|
|
* Some other error. Internal error reporting/support should
|
|
* ideally be logging this somehow.
|
|
* }
|
|
* ]]
|
|
* @param[badAddresses @listof[String]]
|
|
* ]
|
|
* ]
|
|
* ]
|
|
*/
|
|
sendMessage: function(composer, callback) {
|
|
var scope = this;
|
|
this.establishConnection({
|
|
/**
|
|
* Send the envelope.
|
|
* @param conn
|
|
* @param {function()} bail Abort the connection. Used for when
|
|
* we must gracefully cancel without sending a message.
|
|
*/
|
|
sendEnvelope: function(conn) {
|
|
var envelope = composer.getEnvelope();
|
|
logic(scope, 'sendEnvelope', { _envelope: envelope });
|
|
conn.useEnvelope(envelope);
|
|
},
|
|
|
|
onProgress: function() {
|
|
// Keep the wake lock open as long as it looks like we're
|
|
// still communicating with the server.
|
|
composer.renewSmartWakeLock('SMTP XHR Progress');
|
|
},
|
|
/**
|
|
* Send the message body.
|
|
*/
|
|
sendMessage: function(conn) {
|
|
// Then send the actual message if everything was cool
|
|
logic(scope, 'building-blob');
|
|
composer.withMessageBlob({ includeBcc: false, smtp: true },
|
|
function(blob) {
|
|
logic(scope, 'sending-blob', { size: blob.size });
|
|
// simplesmtp's SMTPClient does not understand Blobs, so we
|
|
// issue the write directly. All that it cares about is
|
|
// knowing whether our data payload included a trailing
|
|
// \r\n. We had hoped to avoid this silliness in bug 885110,
|
|
// but SMTPClient still does not support blobs yet, so we
|
|
// still need this.
|
|
conn.socket.send(blob);
|
|
// SMTPClient tracks the last bytes it has written in _lastDataBytes
|
|
// to this end and writes the \r\n if they aren't the last bytes
|
|
// written. Since we know that mailcomposer always ends the buffer
|
|
// with \r\n we just set that state directly ourselves.
|
|
conn._lastDataBytes = '\r\n';
|
|
|
|
// this does not actually terminate the connection; just tells the
|
|
// client to flush stuff, etc.
|
|
conn.end();
|
|
});
|
|
},
|
|
/**
|
|
* The send succeeded.
|
|
*/
|
|
onSendComplete: function(conn) {
|
|
logic(scope, 'smtp:sent');
|
|
callback(null);
|
|
},
|
|
/**
|
|
* The send failed.
|
|
*/
|
|
onError: function(err, badAddresses) {
|
|
logic(scope, 'smtp:error', {
|
|
error: err,
|
|
badAddresses: badAddresses
|
|
});
|
|
callback(err, badAddresses);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Check the account credentials by connecting to the server. Calls
|
|
* back with an error if we had a problem (see sendMessage for
|
|
* details), or no arguments if we succeeded.
|
|
*
|
|
* @param {function(err)} callback
|
|
*/
|
|
checkAccount: function(callback) {
|
|
var success = false;
|
|
this.establishConnection({
|
|
sendEnvelope: function(conn, bail) {
|
|
// If we get here, we've successfully connected. Sorry, SMTP
|
|
// server friend, we aren't actually going to send a message
|
|
// now. Psych
|
|
success = true;
|
|
bail();
|
|
callback();
|
|
},
|
|
sendMessage: function(conn) {
|
|
// We're not sending a message, so this won't be called.
|
|
},
|
|
onSendComplete: function(conn) {
|
|
// Ibid.
|
|
},
|
|
onError: function(err, badAddresses) {
|
|
// Aha, here we have an error -- we might have bad credentials
|
|
// or something else. This error is normalized per the
|
|
// documentation for sendMessage, so we can just pass it along.
|
|
|
|
// We only report auth errors. When checking the account,
|
|
// transient server connection errors don't matter; and we're
|
|
// not trying to send a message.
|
|
if (err === 'bad-user-or-pass') {
|
|
this.universe.__reportAccountProblem(
|
|
this.compositeAccount, err, 'outgoing');
|
|
}
|
|
callback(err);
|
|
}.bind(this)
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Abstract out connection management so that we can do different
|
|
* things with the connection (i.e. just test login credentials, or
|
|
* actually send a message).
|
|
*
|
|
* Callbacks is an object with the following functions, all required:
|
|
*
|
|
* sendEnvelope(conn) -- you should send the envelope
|
|
* sendMessage(conn) -- you should send the message body
|
|
* onSendComplete(conn) -- the message was successfully sent
|
|
* onError(err, badAddresses) -- send failed (or connection error)
|
|
*/
|
|
establishConnection: function(callbacks) {
|
|
var scope = this;
|
|
var conn;
|
|
var sendingMessage = false;
|
|
client.createSmtpConnection(
|
|
this.credentials,
|
|
this.connInfo,
|
|
function onCredentialsUpdated() {
|
|
return new Promise(function(resolve) {
|
|
// Note: Since we update the credentials object in-place,
|
|
// there's no need to explicitly assign the changes here;
|
|
// just save the account information.
|
|
this.universe.saveAccountDef(
|
|
this.compositeAccount.accountDef,
|
|
/* folderInfo: */ null,
|
|
/* callback: */ resolve);
|
|
}.bind(this));
|
|
}.bind(this)
|
|
).then(function(newConn) {
|
|
conn = newConn;
|
|
DisasterRecovery.associateSocketWithAccount(conn.socket, this);
|
|
this._activeConnections.push(conn);
|
|
|
|
// Intercept the 'ondrain' event, which is as close as we can
|
|
// get to knowing that we are still sending data to the
|
|
// server. We use this to hold a wakelock open.
|
|
var oldOnDrain = conn.socket.ondrain;
|
|
conn.socket.ondrain = function() {
|
|
oldOnDrain && oldOnDrain.call(conn.socket);
|
|
callbacks.onProgress && callbacks.onProgress();
|
|
};
|
|
|
|
callbacks.sendEnvelope(conn, conn.close.bind(conn));
|
|
|
|
// We sent the envelope; see if we can now send the message.
|
|
conn.onready = function(badRecipients) {
|
|
logic(scope, 'onready');
|
|
|
|
if (badRecipients.length) {
|
|
conn.close();
|
|
logic(scope, 'bad-recipients', { badRecipients: badRecipients });
|
|
callbacks.onError('bad-recipient', badRecipients);
|
|
} else {
|
|
sendingMessage = true;
|
|
callbacks.sendMessage(conn);
|
|
}
|
|
};
|
|
|
|
// Done sending the message, ideally successfully.
|
|
conn.ondone = function(success) {
|
|
conn.close();
|
|
|
|
if (success) {
|
|
logic(scope, 'sent');
|
|
callbacks.onSendComplete(conn);
|
|
} else {
|
|
logic(scope, 'send-failed');
|
|
// We don't have an error to reference here, but we stored
|
|
// the most recent SMTP error, which should tell us why the
|
|
// server rejected the message.
|
|
var err = client.analyzeSmtpError(conn, null, sendingMessage);
|
|
callbacks.onError(err, /* badAddresses: */ null);
|
|
}
|
|
};
|
|
|
|
conn.onerror = function(err) {
|
|
// Some sort of error occurred; analyze and report.
|
|
conn.close();
|
|
err = client.analyzeSmtpError(conn, err, sendingMessage);
|
|
callbacks.onError(err, /* badAddresses: */ null);
|
|
};
|
|
|
|
conn.onclose = function() {
|
|
logic(scope, 'onclose');
|
|
|
|
var idx = this._activeConnections.indexOf(conn);
|
|
if (idx !== -1) {
|
|
this._activeConnections.splice(idx, 1);
|
|
} else {
|
|
logic(scope, 'dead-unknown-connection');
|
|
}
|
|
}.bind(this);
|
|
}.bind(this))
|
|
.catch(function(err) {
|
|
err = client.analyzeSmtpError(conn, err, sendingMessage);
|
|
callbacks.onError(err);
|
|
});
|
|
}
|
|
|
|
};
|
|
|
|
return {
|
|
Account: SmtpAccount,
|
|
SmtpAccount: SmtpAccount
|
|
};
|
|
|
|
}); // end define
|