VulpesOS/gaia/apps/email/js/ext/smtp/account.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

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