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

280 lines
9.2 KiB
JavaScript
Executable file

'use strict';
/**
* This module exposes a single helper method,
* `sendNextAvailableOutboxMessage`, which is used by the
* sendOutboxMessages job in jobmixins.js.
*/
define(function(require) {
/**
* Send the next available outbox message. Returns a promise that
* resolves to the following:
*
* {
* moreExpected: (Boolean),
* messageNamer: { date, suid }
* }
*
* If there might be more messages left to send after this one,
* moreExpected will be `true`.
*
* If we attempted to send a message, messageNamer will point to it.
* This can then be passed to a subsequent invocation of this, to
* send the next available message after the given messageNamer.
*
* @param {CompositeAccount|ActiveSyncAccount} account
* @param {FolderStorage} storage
* @param {MessageNamer|null} beforeMessage
* Send the first message chronologically preceding `beforeMessage`.
* @param {Boolean} emitNotifications
* If true, we will emit backgroundSendStatus notifications
* for this message.
* @param {Boolean} outboxNeedsFreshSync
* If true, ignore any potentially stale "sending" state,
* as in when we restore the app from a crash.
* @param {SmartWakeLock} wakeLock
* A SmartWakeLock to be held open during the sending process.
* @return {Promise}
* @public
*/
function sendNextAvailableOutboxMessage(
account, storage, beforeMessage, emitNotifications,
outboxNeedsFreshSync, wakeLock) {
return getNextHeader(storage, beforeMessage).then(function(header) {
// If there are no more messages to send, resolve `null`. This
// should ordinarily not happen, because clients should pay
// attention to the `moreExpected` results from earlier sends;
// but job scheduling might introduce edge cases where this
// happens, so better to be safe.
if (!header) {
return {
moreExpected: false,
messageNamer: null
};
}
// Figure out if this is the last message to consider sending in the
// outbox. (We are moving from newest to oldest, so this is the last one
// if it is the oldest. We need to figure this out before the send
// process completes since we will delete the header once it's all sent.)
var moreExpected = !storage.headerIsOldestKnown(header.date,
header.id);
if (!header.sendStatus) {
header.sendStatus = {};
}
// If the header has not been sent, or we've been instructed to
// ignore any existing sendStatus, clear it out.
if (header.sendStatus.state !== 'sending' || outboxNeedsFreshSync) {
// If this message is not already being sent, send it.
return constructComposer(account, storage, header, wakeLock)
.then(sendMessage.bind(null, account, storage, emitNotifications))
.then(function(header) {
return {
moreExpected: moreExpected,
messageNamer: {
suid: header.suid,
date: header.date
}
};
});
} else {
// If this message is currently being sent, advance to the
// next header.
return sendNextAvailableOutboxMessage(account, storage, {
suid: header.suid,
date: header.date
}, emitNotifications, outboxNeedsFreshSync, wakeLock);
}
});
}
////////////////////////////////////////////////////////////////
// The following functions are internal helpers.
/**
* Resolve to the header immediately preceding `beforeMessage` in
* time. If beforeMessage is null, resolve the most recent message.
* If no message could be found, resolve `null`.
*
* @param {FolderStorage} storage
* @param {MessageNamer} beforeMessage
* @return {Promise(MailHeader)}
*/
function getNextHeader(storage, /* optional */ beforeMessage) {
return new Promise(function(resolve) {
if (beforeMessage) {
// getMessagesBeforeMessage expects an 'id', not a 'suid'.
var id = parseInt(beforeMessage.suid.substring(
beforeMessage.suid.lastIndexOf('/') + 1));
storage.getMessagesBeforeMessage(
beforeMessage.date,
id,
/* limit = */ 1,
function(headers, moreExpected) {
// There may be no headers, and that's okay.
resolve(headers[0] || null);
});
} else {
storage.getMessagesInImapDateRange(
0,
null,
/* min */ 1,
/* max */ 1,
function(headers, moreExpected) {
resolve(headers[0]);
});
}
});
}
/**
* Build a Composer instance pointing to the given header.
*
* @param {MailAccount} account
* @param {FolderStorage} storage
* @param {MailHeader} header
* @param {SmartWakeLock} wakeLock
* @return {Promise(Composer)}
*/
function constructComposer(account, storage, header, wakeLock) {
return new Promise(function(resolve, reject) {
storage.getMessage(header.suid, header.date, function(msg) {
// If for some reason the message doesn't have a body, we
// can't construct a composer for this header.
if (!msg || !msg.body) {
console.error('Failed to create composer; no body available.');
reject();
return;
}
require(['../drafts/composer'], function(cmp) {
var composer = new cmp.Composer(msg, account, account.identities[0]);
composer.setSmartWakeLock(wakeLock);
resolve(composer);
});
});
});
}
/**
* Attempt to send the given message from the outbox.
*
* During the sending process, post status updates to the universe,
* so that the frontend can display status notifications if it
* desires.
*
* If the message successfully sends, remove it from the outbox;
* otherwise, its `sendStatus.state` will equal 'error', with
* details about the failure.
*
* Resolves to the header; you can check `header.sendStatus` to see
* the result of this send attempt.
*
* @param {MailAccount} account
* @param {FolderStorage} storage
* @param {Composer} composer
* @return {Promise(MailHeader)}
*/
function sendMessage(account, storage, emitNotifications, composer) {
var header = composer.header;
var progress = publishStatus.bind(
null, account, storage, composer, header, emitNotifications);
// As part of the progress notification, the client would like to
// know whether or not they can expect us to immediately send more
// messages after this one. If there are messages in the outbox
// older than this one, the answer is yes.
var oldestDate = storage.getOldestMessageTimestamp();
var willSendMore = oldestDate > 0 && oldestDate < header.date.valueOf();
// Send the initial progress information.
progress({
state: 'sending',
err: null,
badAddresses: null,
sendFailures: header.sendStatus && header.sendStatus.sendFailures || 0
});
return new Promise(function(resolve) {
account.sendMessage(composer, function(err, badAddresses) {
if (err) {
console.log('Message failed to send (' + err + ')');
progress({
state: 'error',
err: err,
badAddresses: badAddresses,
sendFailures: (header.sendStatus.sendFailures || 0) + 1
});
resolve(composer.header);
} else {
console.log('Message sent; deleting from outbox.');
progress({
state: 'success',
err: null,
badAddresses: null
});
storage.deleteMessageHeaderAndBodyUsingHeader(header, function() {
resolve(composer.header);
});
}
});
});
}
/**
* Publish a universe notification with the message's current send
* status, and queue it for persistence in the database.
*
* NOTE: Currently, we do not checkpoint our state, so the
* intermediary "sending" steps will not actually get written to
* disk. That is generally fine, since sendStatus is invalid upon a
* restart. However, when we address bug 1032451 (sendMessage is not
* actually atomic), we will want to checkpoint state during the
* sending process.
*/
function publishStatus(account, storage, composer,
header, emitNotifications, status) {
header.sendStatus = {
state: status.state,
err: status.err,
badAddresses: status.badAddresses,
sendFailures: status.sendFailures
};
account.universe.__notifyBackgroundSendStatus({
// Status information (also stored on the header):
state: status.state,
err: status.err,
badAddresses: status.badAddresses,
sendFailures: status.sendFailures,
// Message/Account Information (for notifications):
accountId: account.id,
suid: header.suid,
emitNotifications: emitNotifications,
// Unit test support:
messageId: composer.messageId,
sentDate: composer.sentDate
});
storage.updateMessageHeader(
header.date,
header.id,
/* partOfSync */ false,
header,
/* body hint */ null);
}
return {
sendNextAvailableOutboxMessage: sendNextAvailableOutboxMessage
};
});