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.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,287 @@
define(
[
'mailbuild',
'../mailchew',
'../util'
],
function(
MimeNode,
$mailchew,
$imaputil
) {
var formatAddresses = $imaputil.formatAddresses;
// MimeNode doesn't have a `removeHeader` method, but it's so helpful.
// Upstream this when possible.
MimeNode.prototype.removeHeader = function(key) {
for (var i = 0, len = this._headers.length; i < len; i++) {
if (this._headers[i].key === key) {
this._headers.splice(i, 1);
break;
}
}
};
/**
* Ensure that all newlines are of the form \r\n. Our database representation
* for composed messages uses just \n at the current time.
*
* Test coverage is currently provided by end-to-end tests like test_compose
* since the SMTP fake server knows to generate a 451 if it sees incorrect
* newlines. (Thanks to qmail!)
*/
function normalizeNewlines(body) {
// If regexps supported look-behinds we could avoid the wasted identity
// transform on \r\n but that's the only way to find an \n not preceded by
// an \r. We don't really need the lone \r but if we're normalizing why
// not normalize 100%?
return body.replace(/\r?\n|\r/g, '\r\n');
}
/**
* Abstraction around the mailbuild helper library and our efforts to avoid
* filling up the device's memory and crashing when we send large messages. We
* produce a Blob that is made up of some combination of strings and Blobs.
*
* ## Composer Endpoints ##
*
* There are 3 main endpoints for our data:
*
* 1) SMTP transmission. SMTP does not require us to know the size of what we
* are sending up front. It usually does NOT want the BCC header included in
* the message.
*
* 2) IMAP APPEND. APPEND needs to know the size of the message we are
* appending up front. It DOES want the BCC header included for archival
* purposes, so this needs to be a different body.
*
* 3) ActiveSync sending. There are actually two paths here; ActiveSync <= 12.x
* does a more traditional POST of just the MIME data. ActiveSync >= 14.0
* generates a WBXML command, but it's basically just a wrapper around the MIME
* data. Because we use XHR's to communicate with the server, the size
* effectively needs to be known ahead of time.
*
* ## The Data ##
*
* The actual data that goes into our message is either finite-ish (headers),
* variable in size but arguably manageable (the contents of the message), or
* potentially very large in size (attachments). Besides being potentially
* huge, attachments are also notable because they are immutable once attached.
* Also, they arguably need to be redundantly stored once attached. That is,
* if a user attaches an image from DeviceStorage and then later deletes it
* before the message is sent, you can make a convincing case that the
* attachment should still be sent with the message. Previously, this would
* happen as a side-effect of IndexedDB's need to duplicate the contents of
* Blobs passed into it so it could persist them and manage its life-cycle.
*
* @param newRecords
* The HeaderInfo and BodyInfo for the most recent saved copy of the draft.
* Used to construct the outgoing message.
* @param account
* @param identity
*/
function Composer(newRecords, account, identity) {
var header = this.header = newRecords.header;
var body = this.body = newRecords.body;
this.account = account;
this.identity = identity;
this.sentDate = new Date(this.header.date);
this._smartWakeLock = null;
// Snapshot data we create for consistency.
// We're copying nodemailer here; we might want to include some more.
this.messageId =
'<' + Date.now() + Math.random().toString(16).substr(1) + '@mozgaia>';
var messageNode;
// text/plain and text/html
var textContent = body.bodyReps[0].content[1];
if (body.bodyReps.length === 2) {
var htmlContent = body.bodyReps[1].content;
messageNode = new MimeNode('text/html');
messageNode.setContent(
normalizeNewlines(
$mailchew.mergeUserTextWithHTML(textContent, htmlContent)));
} else {
messageNode = new MimeNode('text/plain');
messageNode.setContent(normalizeNewlines(textContent));
}
var root;
if (body.attachments.length) {
root = this._rootNode = new MimeNode('multipart/mixed');
root.appendChild(messageNode);
} else {
root = this._rootNode = messageNode;
}
root.setHeader('From', formatAddresses([this.identity]));
root.setHeader('Subject', header.subject);
if (this.identity.replyTo) {
root.setHeader('Reply-To', this.identity.replyTo);
}
if (header.to && header.to.length) {
root.setHeader('To', formatAddresses(header.to));
}
if (header.cc && header.cc.length) {
root.setHeader('Cc', formatAddresses(header.cc));
}
// Note: We include the BCC header here, even though we also do some
// BCC trickery below, so that the getEnvelope() function includes
// the proper "to" addresses in the envelope.
if (header.bcc && header.bcc.length) {
root.setHeader('Bcc', formatAddresses(header.bcc));
}
root.setHeader('User-Agent', 'GaiaMail/0.2');
root.setHeader('Date', this.sentDate.toUTCString());
root.setHeader('Message-Id', this.messageId);
if (body.references) {
root.setHeader('References', body.references);
}
// Set the transfer-encoding to quoted-printable so that mailbuild
// doesn't attempt to insert linebreaks in HTML mail.
root.setHeader('Content-Transfer-Encoding', 'quoted-printable');
// Mailbuild doesn't currently support blobs. As a workaround,
// insert a unique placeholder separator, which we will replace with
// the real contents of the blobs during the sending process.
this._blobReplacements = [];
this._uniqueBlobBoundary = '{{blob!' + Math.random() + Date.now() + '}}';
body.attachments.forEach(function(attachment) {
try {
var attachmentNode = new MimeNode(
attachment.type,
{
// This implies Content-Disposition: attachment
filename: attachment.name
});
// Explicitly indicate that the attachment is base64 encoded. mailbuild
// only picks base64 for non-text/* MIME parts, but our attachment logic
// encodes *all* attachments in base64, so base64 is the only correct
// answer. (Also, failure to base64 encode our _uniqueBlobBoundary breaks
// the replace logic in withMessageBlob. So base64 all the things!)
attachmentNode.setHeader('Content-Transfer-Encoding', 'base64');
attachmentNode.setContent(this._uniqueBlobBoundary);
root.appendChild(attachmentNode);
this._blobReplacements.push(new Blob(attachment.file));
} catch (ex) {
console.error('Problem attaching attachment:', ex, '\n', ex.stack);
}
}.bind(this));
}
Composer.prototype = {
/**
* Return the currently-built envelope, in a format compatible with
* the SMTP library.
*
* @return {Object}
* { from: "me@example.com", to: [] }
*/
getEnvelope: function() {
return this._rootNode.getEnvelope();
},
/**
* Request that a body be produced as a single Blob with the given options.
* Multiple calls to this method may overlap concurrently.
*
* @param {Object} opts
* @param {Boolean} [opts.includeBcc=true]
* @param {Boolean} [opts.smtp=false]
* Is this for an SMTP server? Matters for dot-stuffing. Our use of
* Blobs currently has the side-effect of making it impossible for
* smtpclient's dot-stuffing to work, which is somewhat of a problem.
* @param {function(blob)} callback
*/
withMessageBlob: function(opts, callback) {
// Another horrible workaround: Mailbuild _never_ includes the
// 'Bcc' header in the generated output, and there isn't a simple
// monkeypatch that could address that behavior. Instead, insert a
// temporary header in place of BCC, and rename that header in the
// final output. Alternately, we could maintain a forked version
// of mailbuild, but we would need to be very careful not to
// inadvertently clobber our changes during updates.
var TEMP_BCC = 'Bcc-Temp';
var TEMP_BCC_REGEX = /^Bcc-Temp: /m;
var hasBcc = opts.includeBcc && this.header.bcc && this.header.bcc.length;
if (hasBcc) {
this._rootNode.setHeader(TEMP_BCC, formatAddresses(this.header.bcc));
} else {
this._rootNode.removeHeader(TEMP_BCC);
}
var str = this._rootNode.build();
// smtpclient knows how to do dot-stuffing, but we bypass its dot-stuffing
// logic because smtpclient doesn't understand Blobs.
if (opts.smtp) {
str = str.replace(/\n\./g, '\n..');
}
if (hasBcc) {
str = str.replace(TEMP_BCC_REGEX, 'Bcc: ');
}
// Ensure that the message always ends with a trailing CRLF for
// SMTP transmission (currently, our SMTP sending logic assumes
// that this will always be the case):
if (str.slice(-2) !== '\r\n') {
str += '\r\n';
}
// Split the message into an array, delimited by our unique blob
// boundary, interleaving the attachment blobs. Note that we must
// search for the base64-encoded blob boundary, as at this point
// it has been encoded for transport.
var splits = str.split(btoa(this._uniqueBlobBoundary) + '\r\n');
this._blobReplacements.forEach(function(blob, i) {
// blob 0 => index 1
// blob 1 => index 3
// blob 2 => index 5 ...
splits.splice((i * 2) + 1, 0, blob);
});
// Build a super-blob from any subparts.
callback(new Blob(splits, {
type: this._rootNode.getHeader('content-type')
}));
},
/**
* Call setSmartWakeLock to assign a SmartWakeLock instance.
* Accounts will call renewSmartWakeLock() during the sending
* process, so that the wake lock can continually be renewed during
* the sending process. This prevents the lock from expiring.
*
* @param {SmartWakeLock} wakeLock
*/
setSmartWakeLock: function(wakeLock) {
this._smartWakeLock = wakeLock;
},
/**
* Renew the SmartWakeLock associated with this composer, if one has
* been set. Provide an optional reason, which will be logged.
*
* @param {String} reason
*/
renewSmartWakeLock: function(reason) {
if (this._smartWakeLock) {
this._smartWakeLock.renew(reason);
}
}
};
return {
Composer: Composer
};
}); // end define

View file

@ -0,0 +1,192 @@
/**
* Back-end draft abstraction.
*
* Drafts are saved to folder storage and look almost exactly like received
* messages. The primary difference is that attachments that are in the
* process of being attached are stored in an `attaching` field on the
* `BodyInfo` instance and that they are discarded on load if still present
* (indicating a crash/something like a crash during the save process).
*
**/
define(function(require) {
var mailRep = require('../db/mail_rep');
/**
* Create a new header and body for a draft by extracting any useful state
* from the previous draft's persisted header/body and the revised draft.
*
* @method mergeDraftStates
* @param oldHeader {HeaderInfo}
* @param oldBody {BodyInfo}
* @param newDraftRep {DraftRep}
* @param newDraftInfo {Object}
* @param newDraftInfo.id {Number}
* @param newDraftInfo.suid {SUID}
* @param newDraftInfo.date {Number}
*/
function mergeDraftStates(oldHeader, oldBody,
newDraftRep, newDraftInfo,
universe) {
var identity = universe.getIdentityForSenderIdentityId(newDraftRep.senderId);
// -- convert from compose rep to header/body rep
var newHeader = mailRep.makeHeaderInfo({
id: newDraftInfo.id,
srvid: null, // stays null
suid: newDraftInfo.suid, // filled in by the job
// we currently don't generate a message-id for drafts, but we'll need to
// do this when we start appending to the server.
guid: oldHeader ? oldHeader.guid : null,
author: { name: identity.name, address: identity.address},
to: newDraftRep.to,
cc: newDraftRep.cc,
bcc: newDraftRep.bcc,
replyTo: identity.replyTo,
date: newDraftInfo.date,
flags: [],
hasAttachments: oldBody ? oldBody.attachments.length > 0 : false,
subject: newDraftRep.subject,
snippet: newDraftRep.body.text.substring(0, 100),
});
var newBody = mailRep.makeBodyInfo({
date: newDraftInfo.date,
size: 0,
attachments: oldBody ? oldBody.attachments.concat() : [],
relatedParts: oldBody ? oldBody.relatedParts.concat() : [],
references: newDraftRep.referencesStr,
bodyReps: []
});
newBody.bodyReps.push(mailRep.makeBodyPart({
type: 'plain',
part: null,
sizeEstimate: newDraftRep.body.text.length,
amountDownloaded: newDraftRep.body.text.length,
isDownloaded: true,
_partInfo: {},
content: [0x1, newDraftRep.body.text]
}));
if (newDraftRep.body.html) {
newBody.bodyReps.push(mailRep.makeBodyPart({
type: 'html',
part: null,
sizeEstimate: newDraftRep.body.html.length,
amountDownloaded: newDraftRep.body.html.length,
isDownloaded: true,
_partInfo: {},
content: newDraftRep.body.html
}));
}
return {
header: newHeader,
body: newBody
};
}
function convertHeaderAndBodyToDraftRep(account, header, body) {
var composeBody = {
text: '',
html: null,
};
// Body structure should be guaranteed, but add some checks.
if (body.bodyReps.length >= 1 &&
body.bodyReps[0].type === 'plain' &&
body.bodyReps[0].content.length === 2 &&
body.bodyReps[0].content[0] === 0x1) {
composeBody.text = body.bodyReps[0].content[1];
}
// HTML is optional, but if present, should satisfy our guard
if (body.bodyReps.length == 2 &&
body.bodyReps[1].type === 'html') {
composeBody.html = body.bodyReps[1].content;
}
var attachments = [];
body.attachments.forEach(function(att) {
attachments.push({
name: att.name,
blob: att.file
});
});
var draftRep = {
identity: account.identities[0],
subject: header.subject,
body: composeBody,
to: header.to,
cc: header.cc,
bcc: header.bcc,
referencesStr: body.references,
attachments: attachments
};
}
/**
* Given the HeaderInfo and BodyInfo for a draft, create a new header and body
* suitable for saving to the sent folder for a POP3 account. Specifically:
* - mark the message as read
* - make sure body.attaching does not make it through
* - strip the Blob references so we don't accidentally keep the base64
* encoded attachment parts around forever and clog up the disk.
* - avoid accidental use of the same instances between the drafts folder and
* the sent folder
*
* @param header {HeaderInfo}
* @param body {BodyInfo}
* @param newInfo
* @param newInfo.id
* @param newInfo.suid {SUID}
* @return { header, body }
*/
function cloneDraftMessageForSentFolderWithoutAttachments(header, body,
newInfo) {
// clone the header (drops excess fields)
var newHeader = mailRep.makeHeaderInfo(header);
// clobber the id/suid
newHeader.id = newInfo.id;
newHeader.suid = newInfo.suid;
// Mark the message as read. We are clobbering other flags, but we don't
// currently support a way for them to exist.
newHeader.flags = ['\\Seen'];
// clone the body, dropping excess fields like "attaching".
var newBody = mailRep.makeBodyInfo(body);
// transform attachments
if (newBody.attachments) {
newBody.attachments = newBody.attachments.map(function(oldAtt) {
var newAtt = mailRep.makeAttachmentPart(oldAtt);
// mark the attachment as non-downloadable
newAtt.type = 'application/x-gelam-no-download';
// get rid of the blobs!
newAtt.file = null;
// we will keep around the sizeEstimate so they know how much they sent
// and we keep around encoding/charset/textFormat because we don't care
return newAtt;
});
}
// We currently can't generate related parts, but just in case.
if (newBody.relatedParts) {
newBody.relatedParts = [];
}
// Body parts can be transferred verbatim.
newBody.bodyReps = newBody.bodyReps.map(function(oldRep) {
return mailRep.makeBodyPart(oldRep);
});
return { header: newHeader, body: newBody };
}
return {
mergeDraftStates: mergeDraftStates,
convertHeaderAndBodyToDraftRep: convertHeaderAndBodyToDraftRep,
cloneDraftMessageForSentFolderWithoutAttachments:
cloneDraftMessageForSentFolderWithoutAttachments
};
}); // end define

View file

@ -0,0 +1,379 @@
/**
* Draft jobs: save/delete drafts, attach/remove attachments. These gets mixed
* into the specific JobDriver implementations.
**/
define(function(require, exports) {
var mailRep = require('../db/mail_rep');
var draftRep = require('../drafts/draft_rep');
var base64 = require('safe-base64');
var asyncFetchBlobAsUint8Array =
require('../async_blob_fetcher').asyncFetchBlobAsUint8Array;
var draftsMixins = exports.draftsMixins = {};
////////////////////////////////////////////////////////////////////////////////
// attachBlobToDraft
/**
* How big a chunk of an attachment should we encode in a single read? Because
* we want our base64-encoded lines to be 76 bytes long (before newlines) and
* there's a 4/3 expansion factor, we want to read a multiple of 57 bytes.
*
* I initially chose the largest value just under 1MiB. This appeared too
* chunky on the ZTE open, so I'm halving to just under 512KiB. Calculated via
* Math.floor(512 * 1024 / 57) = 9198. The encoded size of this ends up to be
* 9198 * 78 which is ~700 KiB. So together that's ~1.2 megs if we don't
* generate a ton of garbage by creating a lot of intermediary strings.
*
* This seems reasonable given goals of not requiring the GC to run after every
* block and not having us tie up the CPU too long during our encoding.
*/
draftsMixins.BLOB_BASE64_BATCH_CONVERT_SIZE = 9198 * 57;
/**
* Incrementally convert an attachment into its base64 encoded attachment form
* which we save in chunks to IndexedDB to avoid using too much memory now or
* during the sending process.
*
* - Retrieve the body the draft is persisted to,
* - Repeat until the attachment is fully attached:
* - take a chunk of the source attachment
* - base64 encode it into a Blob by creating a Uint8Array and manually
* encoding into that. (We need to put a \r\n after every 76 bytes, and
* doing that using window.btoa is going to create a lot of garbage. And
* addressing that is no longer premature optimization.)
* - update the body with that Blob
* - trigger a save of the account so that IndexedDB writes the account to
* disk.
* - force the body block to be discarded from the cache and then re-get the
* body. We won't be saving any memory until the Blob has been written to
* disk and we have forgotten all references to the in-memory Blob we wrote
* to the database. (The Blob does not magically get turned into a
* reference to the database.)
* - Be done. Note that we leave the "small" Blobs independent; we do not
* create a super Blob.
*
* ## Logging ##
*
* We log at:
* - The start of the process.
* - For each block.
* - The end of the process.
*/
draftsMixins.local_do_attachBlobToDraft = function(op, callback) {
var localDraftsFolder = this.account.getFirstFolderWithType('localdrafts');
if (!localDraftsFolder) {
callback('moot');
return;
}
var self = this;
this._accessFolderForMutation(
localDraftsFolder.id, /* needConn*/ false,
function(nullFolderConn, folderStorage) {
var wholeBlob = op.attachmentDef.blob;
// - Retrieve the message
var header, body;
console.log('attachBlobToDraft: retrieving message');
folderStorage.getMessage(
op.existingNamer.suid, op.existingNamer.date, {}, gotMessage);
function gotMessage(records) {
header = records.header;
body = records.body;
if (!header || !body) {
// No header/body suggests either some major invariant is busted or
// one or more UIs issued attach commands after the draft was mooted.
callback('failure-give-up');
return;
}
body.attaching = mailRep.makeAttachmentPart({
name: op.attachmentDef.name,
type: wholeBlob.type,
sizeEstimate: wholeBlob.size,
// this is where we put the Blob segments...
file: [],
});
convertNextChunk(body);
}
var blobOffset = 0;
function convertNextChunk(refreshedBody) {
body = refreshedBody;
var nextOffset =
Math.min(wholeBlob.size,
blobOffset + self.BLOB_BASE64_BATCH_CONVERT_SIZE);
console.log('attachBlobToDraft: fetching', blobOffset, 'to',
nextOffset, 'of', wholeBlob.size);
var slicedBlob = wholeBlob.slice(blobOffset, nextOffset);
blobOffset = nextOffset;
asyncFetchBlobAsUint8Array(slicedBlob, gotChunk);
}
function gotChunk(err, binaryDataU8) {
console.log('attachBlobToDraft: fetched');
// The Blob really should not be disappear out from under us, but it
// could happen.
if (err) {
callback('failure-give-up');
return;
}
var lastChunk = (blobOffset >= wholeBlob.size);
var encodedU8 = base64.mimeStyleBase64Encode(binaryDataU8);
body.attaching.file.push(new Blob([encodedU8],
{ type: wholeBlob.type }));
var eventDetails;
if (lastChunk) {
var attachmentIndex = body.attachments.length;
body.attachments.push(body.attaching);
delete body.attaching; // bad news for shapes, but drafts are rare.
eventDetails = {
changeDetails: {
attachments: [attachmentIndex]
}
};
}
else {
// Do not generate an event for intermediary states; there is nothing
// to observe.
eventDetails = null;
}
console.log('attachBlobToDraft: flushing');
folderStorage.updateMessageBody(
header, body, { flushBecause: 'blobs' }, eventDetails,
lastChunk ? bodyUpdatedAllDone : convertNextChunk);
body = null;
}
function bodyUpdatedAllDone(newBodyInfo) {
console.log('attachBlobToDraft: blob fully attached');
callback(null);
}
},
/* no conn => no deathback required */ null,
'attachBlobToDraft');
};
draftsMixins.do_attachBlobToDraft = function(op, callback) {
// there is no server component for this
callback(null);
};
draftsMixins.check_attachBlobToDraft = function(op, callback) {
callback(null, 'moot');
};
draftsMixins.local_undo_attachBlobToDraft = function(op, callback) {
callback(null);
};
draftsMixins.undo_attachBlobToDraft = function(op, callback) {
callback(null);
};
////////////////////////////////////////////////////////////////////////////////
// detachAttachmentFromDraft
draftsMixins.local_do_detachAttachmentFromDraft = function(op, callback) {
var localDraftsFolder = this.account.getFirstFolderWithType('localdrafts');
if (!localDraftsFolder) {
callback('moot');
return;
}
var self = this;
this._accessFolderForMutation(
localDraftsFolder.id, /* needConn*/ false,
function(nullFolderConn, folderStorage) {
// - Retrieve the message
var header, body;
console.log('detachAttachmentFromDraft: retrieving message');
folderStorage.getMessage(
op.existingNamer.suid, op.existingNamer.date, {}, gotMessage);
function gotMessage(records) {
header = records.header;
body = records.body;
if (!header || !body) {
// No header/body suggests either some major invariant is busted or
// one or more UIs issued attach commands after the draft was mooted.
callback('failure-give-up');
return;
}
// Just forget about the attachment. Splice handles insane indices.
body.attachments.splice(op.attachmentIndex, 1);
console.log('detachAttachmentFromDraft: flushing');
folderStorage.updateMessageBody(
header, body,
{ flushBecause: 'blobs' },
{
changeDetails: {
detachedAttachments: [op.attachmentIndex]
}
},
bodyUpdatedAllDone);
}
function bodyUpdatedAllDone(newBodyInfo) {
console.log('detachAttachmentFromDraft: blob fully detached');
callback(null);
}
},
/* no conn => no deathback required */ null,
'detachAttachmentFromDraft');
};
draftsMixins.do_detachAttachmentFromDraft = function(op, callback) {
// there is no server component for this at this time.
callback(null);
};
draftsMixins.check_detachAttachmentFromDraft = function(op, callback) {
callback(null);
};
draftsMixins.local_undo_detachAttachmentFromDraft = function(op, callback) {
callback(null);
};
draftsMixins.undo_detachAttachmentFromDraft = function(op, callback) {
callback(null);
};
////////////////////////////////////////////////////////////////////////////////
// saveDraft
/**
* Save a draft; if there already was a draft, it gets replaced. The new
* draft gets a new date and id/SUID so it is logically distinct. However,
* we will propagate attachment and on-server information between drafts.
*/
draftsMixins.local_do_saveDraft = function(op, callback) {
var localDraftsFolder = this.account.getFirstFolderWithType('localdrafts');
if (!localDraftsFolder) {
callback('moot');
return;
}
var self = this;
this._accessFolderForMutation(
localDraftsFolder.id, /* needConn*/ false,
function(nullFolderConn, folderStorage) {
// there's always a header add and a body add
var waitingForDbMods = 2;
function gotMessage(oldRecords) {
var newRecords = draftRep.mergeDraftStates(
oldRecords.header, oldRecords.body,
op.draftRep,
op.newDraftInfo,
self.account.universe);
// If there already was a draft saved, delete it.
// Note that ordering of the removal and the addition doesn't really
// matter here because of our use of transactions.
if (op.existingNamer) {
waitingForDbMods++;
folderStorage.deleteMessageHeaderAndBody(
op.existingNamer.suid, op.existingNamer.date, dbModCompleted);
}
folderStorage.addMessageHeader(newRecords.header, newRecords.body,
dbModCompleted);
folderStorage.addMessageBody(newRecords.header, newRecords.body,
dbModCompleted);
function dbModCompleted() {
if (--waitingForDbMods === 0) {
callback(
/* no error */ null,
/* result */ newRecords,
/* save account */ true);
}
}
}
if (op.existingNamer) {
folderStorage.getMessage(
op.existingNamer.suid, op.existingNamer.date, null, gotMessage);
}
else {
gotMessage({ header: null, body: null });
}
},
/* no conn => no deathback required */ null,
'saveDraft');
};
/**
* FUTURE WORK: Save a draft to the server; this is inherently IMAP only.
* Tracked on: https://bugzilla.mozilla.org/show_bug.cgi?id=799822
*
* It is very possible that we will save local drafts faster / more frequently
* than we can update our server state. It only makes sense to upload the
* latest draft state to the server. Because we delete our old local drafts,
* it's obvious when we should skip out on updating the server draft for
* something.
*
* Because IMAP drafts have to replace the prior drafts, we use our old 'srvid'
* to know what message to delete as well as what message to pull attachments
* from when we're in a mode where we upload attachments to drafts and CATENATE
* is available.
*/
draftsMixins.do_saveDraft = function(op, callback) {
callback(null);
};
draftsMixins.check_saveDraft = function(op, callback) {
callback(null, 'moot');
};
draftsMixins.local_undo_saveDraft = function(op, callback) {
callback(null);
};
draftsMixins.undo_saveDraft = function(op, callback) {
callback(null);
};
////////////////////////////////////////////////////////////////////////////////
// deleteDraft
draftsMixins.local_do_deleteDraft = function(op, callback) {
var localDraftsFolder = this.account.getFirstFolderWithType('localdrafts');
if (!localDraftsFolder) {
callback('moot');
return;
}
var self = this;
this._accessFolderForMutation(
localDraftsFolder.id, /* needConn*/ false,
function(nullFolderConn, folderStorage) {
folderStorage.deleteMessageHeaderAndBody(
op.messageNamer.suid, op.messageNamer.date,
function() {
callback(null, null, /* save account */ true);
});
},
/* no conn => no deathback required */ null,
'deleteDraft');
};
draftsMixins.do_deleteDraft = function(op, callback) {
// there is no server component for this
callback(null);
};
draftsMixins.check_deleteDraft = function(op, callback) {
callback(null, 'moot');
};
draftsMixins.local_undo_deleteDraft = function(op, callback) {
callback(null);
};
draftsMixins.undo_deleteDraft = function(op, callback) {
callback(null);
};
////////////////////////////////////////////////////////////////////////////////
}); // end define