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

1488 lines
47 KiB
JavaScript
Executable file

define(
[
'logic',
'../date',
'../syncbase',
'../allback',
'../db/mail_rep',
'activesync/codepages/AirSync',
'activesync/codepages/AirSyncBase',
'activesync/codepages/ItemEstimate',
'activesync/codepages/Email',
'activesync/codepages/ItemOperations',
'safe-base64',
'mimetypes',
'module',
'require',
'exports'
],
function(
logic,
$date,
$sync,
allback,
mailRep,
$AirSync,
$AirSyncBase,
$ItemEstimate,
$Email,
$ItemOperations,
safeBase64,
mimetypes,
$module,
require,
exports
) {
'use strict';
/**
* The desired number of bytes to fetch when downloading bodies, but the body's
* size exceeds the maximum requested size.
*/
var DESIRED_TEXT_SNIPPET_BYTES = 512;
/**
* This is minimum number of messages we'd like to get for a folder for a given
* sync range. It's not exact, since we estimate from the number of messages in
* the past two weeks, but it's close enough.
*/
var DESIRED_MESSAGE_COUNT = 50;
/**
* Filter types are lazy initialized once the activesync code is loaded.
*/
var FILTER_TYPE, SYNC_RANGE_TO_FILTER_TYPE, FILTER_TYPE_TO_STRING;
function initFilterTypes() {
FILTER_TYPE = $AirSync.Enums.FilterType;
/**
* Map our built-in sync range values to their corresponding ActiveSync
* FilterType values. We exclude 3 and 6 months, since they aren't valid for
* email.
*
* Also see SYNC_RANGE_ENUMS_TO_MS in `syncbase.js`.
*/
SYNC_RANGE_TO_FILTER_TYPE = {
'auto': null,
'1d': FILTER_TYPE.OneDayBack,
'3d': FILTER_TYPE.ThreeDaysBack,
'1w': FILTER_TYPE.OneWeekBack,
'2w': FILTER_TYPE.TwoWeeksBack,
'1m': FILTER_TYPE.OneMonthBack,
'all': FILTER_TYPE.NoFilter,
};
/**
* This mapping is purely for logging purposes.
*/
FILTER_TYPE_TO_STRING = {
0: 'all messages',
1: 'one day',
2: 'three days',
3: 'one week',
4: 'two weeks',
5: 'one month',
};
}
var $wbxml, parseAddresses, $mailchew;
function lazyConnection(cbIndex, fn, failString) {
return function lazyRun() {
var args = Array.slice(arguments),
errback = args[cbIndex],
self = this;
require(['wbxml', 'addressparser', '../mailchew'],
function (wbxml, addressparser, mailchew) {
if (!$wbxml) {
$wbxml = wbxml;
parseAddresses = addressparser.parse.bind(addressparser);
$mailchew = mailchew;
initFilterTypes();
}
self._account.withConnection(errback, function () {
fn.apply(self, args);
}, failString);
});
};
}
function ActiveSyncFolderConn(account, storage) {
this._account = account;
this._storage = storage;
logic.defineScope(this, 'ActiveSyncFolderConn',
{ folderId: storage.folderId,
accountId: account.id });
this.folderMeta = storage.folderMeta;
if (!this.syncKey)
this.syncKey = '0';
}
ActiveSyncFolderConn.prototype = {
get syncKey() {
return this.folderMeta.syncKey;
},
set syncKey(value) {
return this.folderMeta.syncKey = value;
},
get serverId() {
return this.folderMeta.serverId;
},
/**
* Get the filter type for this folder. The account-level syncRange property
* takes precedence here, but if it's set to "auto", we'll look at the
* filterType on a per-folder basis. The per-folder filterType may be
* undefined, in which case, we will attempt to infer a good filter type
* elsewhere (see _inferFilterType()).
* ASSUMES that it is only called after lazy load of activesync code and
* initFilterTypes() has been run.
*/
get filterType() {
var syncRange = this._account.accountDef.syncRange;
if (SYNC_RANGE_TO_FILTER_TYPE.hasOwnProperty(syncRange)) {
var accountFilterType = SYNC_RANGE_TO_FILTER_TYPE[syncRange];
if (accountFilterType)
return accountFilterType;
else
return this.folderMeta.filterType;
}
else {
console.warn('Got an invalid syncRange (' + syncRange +
') using three days back instead');
return $AirSync.Enums.FilterType.ThreeDaysBack;
}
},
/**
* Get the initial sync key for the folder so we can start getting data. We
* assume we have already negotiated a connection in the caller.
*
* @param {string} filterType The filter type for our synchronization
* @param {function} callback A callback to be run when the operation finishes
*/
_getSyncKey: lazyConnection(1, function asfc__getSyncKey(filterType,
callback) {
var folderConn = this;
var account = this._account;
var as = $AirSync.Tags;
var w = new $wbxml.Writer('1.3', 1, 'UTF-8');
w.stag(as.Sync)
.stag(as.Collections)
.stag(as.Collection)
if (account.conn.currentVersion.lt('12.1'))
w.tag(as.Class, 'Email');
w.tag(as.SyncKey, '0')
.tag(as.CollectionId, this.serverId)
.stag(as.Options)
.tag(as.FilterType, filterType)
.etag()
.etag()
.etag()
.etag();
account.conn.postCommand(w, function(aError, aResponse) {
if (aError) {
console.error(aError);
account._reportErrorIfNecessary(aError);
callback('unknown');
return;
}
// Reset the SyncKey, just in case we don't see a sync key in the
// response.
folderConn.syncKey = '0';
var e = new $wbxml.EventParser();
e.addEventListener([as.Sync, as.Collections, as.Collection, as.SyncKey],
function(node) {
folderConn.syncKey = node.children[0].textContent;
});
e.onerror = function() {}; // Ignore errors.
e.run(aResponse);
if (folderConn.syncKey === '0') {
// We should never actually hit this, since it would mean that the
// server is refusing to give us a sync key. On the off chance that we
// do hit it, just bail.
console.error('Unable to get sync key for folder');
callback('unknown');
}
else {
callback();
}
});
}),
/**
* Get an estimate of the number of messages to be synced. We assume we have
* already negotiated a connection in the caller.
*
* @param {string} filterType The filter type for our estimate
* @param {function} callback A callback to be run when the operation finishes
*/
_getItemEstimate: lazyConnection(1, function asfc__getItemEstimate(filterType,
callback) {
var ie = $ItemEstimate.Tags;
var as = $AirSync.Tags;
var account = this._account;
var w = new $wbxml.Writer('1.3', 1, 'UTF-8');
w.stag(ie.GetItemEstimate)
.stag(ie.Collections)
.stag(ie.Collection);
if (this._account.conn.currentVersion.gte('14.0')) {
w.tag(as.SyncKey, this.syncKey)
.tag(ie.CollectionId, this.serverId)
.stag(as.Options)
.tag(as.FilterType, filterType)
.etag();
}
else if (this._account.conn.currentVersion.gte('12.0')) {
w.tag(ie.CollectionId, this.serverId)
.tag(as.FilterType, filterType)
.tag(as.SyncKey, this.syncKey);
}
else {
w.tag(ie.Class, 'Email')
.tag(as.SyncKey, this.syncKey)
.tag(ie.CollectionId, this.serverId)
.tag(as.FilterType, filterType);
}
w.etag(ie.Collection)
.etag(ie.Collections)
.etag(ie.GetItemEstimate);
account.conn.postCommand(w, function(aError, aResponse) {
if (aError) {
console.error(aError);
account._reportErrorIfNecessary(aError);
callback('unknown');
return;
}
var e = new $wbxml.EventParser();
var base = [ie.GetItemEstimate, ie.Response];
var status, estimate;
e.addEventListener(base.concat(ie.Status), function(node) {
status = node.children[0].textContent;
});
e.addEventListener(base.concat(ie.Collection, ie.Estimate),
function(node) {
estimate = parseInt(node.children[0].textContent, 10);
});
try {
e.run(aResponse);
}
catch (ex) {
console.error('Error parsing GetItemEstimate response', ex, '\n',
ex.stack);
callback('unknown');
return;
}
if (status !== $ItemEstimate.Enums.Status.Success) {
console.error('Error getting item estimate:', status);
callback('unknown');
}
else {
callback(null, estimate);
}
});
}),
/**
* Infer the filter type for this folder to get a sane number of messages.
*
* @param {function} callback A callback to be run when the operation
* finishes, taking two arguments: an error (if any), and the filter type we
* picked
*/
_inferFilterType: lazyConnection(0, function asfc__inferFilterType(callback) {
var folderConn = this;
var Type = $AirSync.Enums.FilterType;
var getEstimate = function(filterType, onSuccess) {
folderConn._getSyncKey(filterType, function(error) {
if (error) {
callback('unknown');
return;
}
folderConn._getItemEstimate(filterType, function(error, estimate) {
if (error) {
// If we couldn't get an estimate, just tell the main callback that
// we want three days back.
callback(null, Type.ThreeDaysBack);
return;
}
onSuccess(estimate);
});
});
};
getEstimate(Type.TwoWeeksBack, function(estimate) {
var messagesPerDay = estimate / 14; // Two weeks. Twoooo weeeeeeks.
var filterType;
if (estimate < 0)
filterType = Type.ThreeDaysBack;
else if (messagesPerDay >= DESIRED_MESSAGE_COUNT)
filterType = Type.OneDayBack;
else if (messagesPerDay * 3 >= DESIRED_MESSAGE_COUNT)
filterType = Type.ThreeDaysBack;
else if (messagesPerDay * 7 >= DESIRED_MESSAGE_COUNT)
filterType = Type.OneWeekBack;
else if (messagesPerDay * 14 >= DESIRED_MESSAGE_COUNT)
filterType = Type.TwoWeeksBack;
else if (messagesPerDay * 30 >= DESIRED_MESSAGE_COUNT)
filterType = Type.OneMonthBack;
else {
getEstimate(Type.NoFilter, function(estimate) {
var filterType;
if (estimate > DESIRED_MESSAGE_COUNT) {
filterType = Type.OneMonthBack;
// Reset the sync key since we're changing filter types. This avoids
// a round-trip where we'd normally get a zero syncKey from the
// server.
folderConn.syncKey = '0';
}
else {
filterType = Type.NoFilter;
}
logic(folderConn, 'inferFilterType', { filterType: filterType });
callback(null, filterType);
});
return;
}
if (filterType !== Type.TwoWeeksBack) {
// Reset the sync key since we're changing filter types. This avoids a
// round-trip where we'd normally get a zero syncKey from the server.
folderConn.syncKey = '0';
}
logic(folderConn, 'inferFilterType', { filterType: filterType });
callback(null, filterType);
});
}),
/**
* Sync the folder with the server and enumerate all the changes since the
* last sync.
*
* @param {function} callback A function to be called when the operation has
* completed, taking three arguments: |added|, |changed|, and |deleted|
* @param {function} progress A function to be called as the operation
* progresses that takes a number in the range [0.0, 1.0] to express
* progress.
*/
_enumerateFolderChanges: lazyConnection(0,
function asfc__enumerateFolderChanges(callback, progress) {
var folderConn = this, storage = this._storage;
if (!this.filterType) {
this._inferFilterType(function(error, filterType) {
if (error) {
callback('unknown');
return;
}
console.log('We want a filter of', FILTER_TYPE_TO_STRING[filterType]);
folderConn.folderMeta.filterType = filterType;
folderConn._enumerateFolderChanges(callback, progress);
});
return;
}
if (this.syncKey === '0') {
this._getSyncKey(this.filterType, function(error) {
if (error) {
callback('aborted');
return;
}
folderConn._enumerateFolderChanges(callback, progress);
});
return;
}
var as = $AirSync.Tags;
var asEnum = $AirSync.Enums;
var asb = $AirSyncBase.Tags;
var asbEnum = $AirSyncBase.Enums;
var w;
// If the last sync was ours and we got an empty response back, we can send
// an empty request to repeat our request. This saves a little bandwidth.
if (this._account._syncsInProgress++ === 0 &&
this._account._lastSyncKey === this.syncKey &&
this._account._lastSyncFilterType === this.filterType &&
this._account._lastSyncResponseWasEmpty) {
w = as.Sync;
}
else {
w = new $wbxml.Writer('1.3', 1, 'UTF-8');
w.stag(as.Sync)
.stag(as.Collections)
.stag(as.Collection);
if (this._account.conn.currentVersion.lt('12.1'))
w.tag(as.Class, 'Email');
w.tag(as.SyncKey, this.syncKey)
.tag(as.CollectionId, this.serverId)
.tag(as.GetChanges)
.stag(as.Options)
.tag(as.FilterType, this.filterType)
// Older versions of ActiveSync give us the body by default. Ensure they
// omit it.
if (this._account.conn.currentVersion.lte('12.0')) {
w.tag(as.MIMESupport, asEnum.MIMESupport.Never)
.tag(as.Truncation, asEnum.MIMETruncation.TruncateAll);
}
w.etag()
.etag()
.etag()
.etag();
}
this._account.conn.postCommand(w, function(aError, aResponse) {
var added = [];
var changed = [];
var deleted = [];
var status;
var moreAvailable = false;
folderConn._account._syncsInProgress--;
if (aError) {
console.error('Error syncing folder:', aError);
folderConn._account._reportErrorIfNecessary(aError);
callback('aborted');
return;
}
folderConn._account._lastSyncKey = folderConn.syncKey;
folderConn._account._lastSyncFilterType = folderConn.filterType;
if (!aResponse) {
console.log('Sync completed with empty response');
folderConn._account._lastSyncResponseWasEmpty = true;
callback(null, added, changed, deleted);
return;
}
folderConn._account._lastSyncResponseWasEmpty = false;
var e = new $wbxml.EventParser();
var base = [as.Sync, as.Collections, as.Collection];
e.addEventListener(base.concat(as.SyncKey), function(node) {
folderConn.syncKey = node.children[0].textContent;
});
e.addEventListener(base.concat(as.Status), function(node) {
status = node.children[0].textContent;
});
e.addEventListener(base.concat(as.MoreAvailable), function(node) {
moreAvailable = true;
});
e.addEventListener(base.concat(as.Commands, [[as.Add, as.Change]]),
function(node) {
var id, guid, msg;
for (var iter in Iterator(node.children)) {
var child = iter[1];
switch (child.tag) {
case as.ServerId:
guid = child.children[0].textContent;
break;
case as.ApplicationData:
try {
msg = folderConn._parseMessage(child, node.tag === as.Add,
storage);
}
catch (ex) {
// If we get an error, just log it and skip this message.
console.error('Failed to parse a message:', ex, '\n', ex.stack);
return;
}
break;
}
}
msg.header.srvid = guid;
var collection = node.tag === as.Add ? added : changed;
collection.push(msg);
});
e.addEventListener(base.concat(as.Commands, [[as.Delete, as.SoftDelete]]),
function(node) {
var guid;
for (var iter in Iterator(node.children)) {
var child = iter[1];
switch (child.tag) {
case as.ServerId:
guid = child.children[0].textContent;
break;
}
}
deleted.push(guid);
});
try {
e.run(aResponse);
}
catch (ex) {
console.error('Error parsing Sync response:', ex, '\n', ex.stack);
callback('unknown');
return;
}
if (status === asEnum.Status.Success) {
console.log('Sync completed: added ' + added.length + ', changed ' +
changed.length + ', deleted ' + deleted.length);
callback(null, added, changed, deleted, moreAvailable);
if (moreAvailable)
folderConn._enumerateFolderChanges(callback, progress);
}
else if (status === asEnum.Status.InvalidSyncKey) {
console.warn('ActiveSync had a bad sync key');
callback('badkey');
}
else {
console.error('Something went wrong during ActiveSync syncing and we ' +
'got a status of ' + status);
callback('unknown');
}
}, null, null,
function progressData(bytesSoFar, totalBytes) {
// We get the XHR progress status and convert it into progress in the
// range [0.10, 0.80]. The remaining 20% is processing the specific
// messages, but we don't bother to generate notifications since that
// is done synchronously.
if (!totalBytes)
totalBytes = Math.max(1000000, bytesSoFar);
progress(0.1 + 0.7 * bytesSoFar / totalBytes);
});
}, 'aborted'),
/**
* Parse the DOM of an individual message to build header and body objects for
* it.
* ASSUMES activesync code has already been lazy-loaded.
*
* @param {WBXML.Element} node The fully-parsed node describing the message
* @param {boolean} isAdded True if this is a new message, false if it's a
* changed one
* @return {object} An object containing the header and body for the message
*/
_parseMessage: function asfc__parseMessage(node, isAdded, storage) {
var em = $Email.Tags;
var asb = $AirSyncBase.Tags;
var asbEnum = $AirSyncBase.Enums;
var header, body, flagHeader;
if (isAdded) {
var newId = storage._issueNewHeaderId();
// note: these will be passed through mailRep.make* later
header = {
id: newId,
// This will be fixed up afterwards for control flow paranoia.
srvid: null,
suid: storage.folderId + '/' + newId,
// ActiveSync does not/cannot tell us the Message-ID header unless we
// fetch the entire MIME body
guid: '',
author: null,
to: null,
cc: null,
bcc: null,
replyTo: null,
date: null,
flags: [],
hasAttachments: false,
subject: null,
snippet: null
};
body = {
date: null,
size: 0,
attachments: [],
relatedParts: [],
references: null,
bodyReps: null
};
flagHeader = function(flag, state) {
if (state)
header.flags.push(flag);
}
}
else {
header = {
flags: [],
mergeInto: function(o) {
// Merge flags
for (var iter in Iterator(this.flags)) {
var flagstate = iter[1];
if (flagstate[1]) {
o.flags.push(flagstate[0]);
}
else {
var index = o.flags.indexOf(flagstate[0]);
if (index !== -1)
o.flags.splice(index, 1);
}
}
// Merge everything else
var skip = ['mergeInto', 'suid', 'srvid', 'guid', 'id', 'flags'];
for (var iter in Iterator(this)) {
var key = iter[0], value = iter[1];
if (skip.indexOf(key) !== -1)
continue;
o[key] = value;
}
},
};
body = {
mergeInto: function(o) {
for (var iter in Iterator(this)) {
var key = iter[0], value = iter[1];
if (key === 'mergeInto') continue;
o[key] = value;
}
},
};
flagHeader = function(flag, state) {
header.flags.push([flag, state]);
}
}
var bodyType, bodySize;
for (var iter in Iterator(node.children)) {
var child = iter[1];
var childText = child.children.length ? child.children[0].textContent :
null;
switch (child.tag) {
case em.Subject:
header.subject = childText;
break;
case em.From:
header.author = parseAddresses(childText)[0] || null;
break;
case em.To:
header.to = parseAddresses(childText);
break;
case em.Cc:
header.cc = parseAddresses(childText);
break;
case em.ReplyTo:
header.replyTo = parseAddresses(childText);
break;
case em.DateReceived:
body.date = header.date = new Date(childText).valueOf();
break;
case em.Read:
flagHeader('\\Seen', childText === '1');
break;
case em.Flag:
for (var iter2 in Iterator(child.children)) {
var grandchild = iter2[1];
if (grandchild.tag === em.Status)
flagHeader('\\Flagged', grandchild.children[0].textContent !== '0');
}
break;
case asb.Body: // ActiveSync 12.0+
for (var iter2 in Iterator(child.children)) {
var grandchild = iter2[1];
switch (grandchild.tag) {
case asb.Type:
var type = grandchild.children[0].textContent;
if (type === asbEnum.Type.HTML)
bodyType = 'html';
else {
// I've seen a handful of extra-weird messages with body types
// that aren't plain or html. Let's assume they're plain, though.
if (type !== asbEnum.Type.PlainText)
console.warn('A message had a strange body type:', type)
bodyType = 'plain';
}
break;
case asb.EstimatedDataSize:
bodySize = grandchild.children[0].textContent;
break;
}
}
break;
case em.BodySize: // pre-ActiveSync 12.0
bodyType = 'plain';
bodySize = childText;
break;
case asb.Attachments: // ActiveSync 12.0+
case em.Attachments: // pre-ActiveSync 12.0
for (var iter2 in Iterator(child.children)) {
var attachmentNode = iter2[1];
if (attachmentNode.tag !== asb.Attachment &&
attachmentNode.tag !== em.Attachment)
continue;
var attachment = {
name: null,
contentId: null,
type: null,
part: null,
encoding: null,
sizeEstimate: null,
file: null,
};
var isInline = false;
for (var iter3 in Iterator(attachmentNode.children)) {
var attachData = iter3[1];
var dot, ext;
var attachDataText = attachData.children.length ?
attachData.children[0].textContent : null;
switch (attachData.tag) {
case asb.DisplayName:
case em.DisplayName:
attachment.name = attachDataText;
// Get the file's extension to look up a mimetype, but ignore it
// if the filename is of the form '.bashrc'.
dot = attachment.name.lastIndexOf('.');
ext = dot > 0 ? attachment.name.substring(dot + 1).toLowerCase() :
'';
attachment.type = mimetypes.detectMimeType(ext);
break;
case asb.FileReference:
case em.AttName:
case em.Att0Id:
attachment.part = attachDataText;
break;
case asb.EstimatedDataSize:
case em.AttSize:
attachment.sizeEstimate = parseInt(attachDataText, 10);
break;
case asb.ContentId:
attachment.contentId = attachDataText;
break;
case asb.IsInline:
isInline = (attachDataText === '1');
break;
}
}
if (isInline)
body.relatedParts.push(mailRep.makeAttachmentPart(attachment));
else
body.attachments.push(mailRep.makeAttachmentPart(attachment));
}
header.hasAttachments = body.attachments.length > 0;
break;
}
}
// If this is an add, then these are new structures so we need to normalize
// them.
if (isAdded) {
body.bodyReps = [mailRep.makeBodyPart({
type: bodyType,
sizeEstimate: bodySize,
amountDownloaded: 0,
isDownloaded: false
})];
return {
header: mailRep.makeHeaderInfo(header),
body: mailRep.makeBodyInfo(body)
};
}
// It's not an add, so this is a delta, and header/body have mergeInto
// methods and we should not attempt to normalize them.
else {
return { header: header, body: body };
}
},
/**
* Download the bodies for a set of headers.
*
* XXX This method is a slightly modified version of
* ImapFolderConn._lazyDownloadBodies; we should attempt to remove the
* duplication.
*/
downloadBodies: function(headers, options, callback) {
if (this._account.conn.currentVersion.lt('12.0'))
return this._syncBodies(headers, callback);
var downloadsNeeded = 0,
folderConn = this;
var latch = allback.latch();
for (var i = 0; i < headers.length; i++) {
var header = headers[i];
// We obviously can't do anything with null header references.
// To avoid redundant work, we also don't want to do any fetching if we
// already have a snippet. This could happen because of the extreme
// potential for a caller to spam multiple requests at us before we
// service any of them. (Callers should only have one or two outstanding
// jobs of this and do their own suppression tracking, but bugs happen.)
if (!header || header.snippet !== null) {
continue;
}
// This isn't absolutely guaranteed to be 100% correct, but is good enough
// for indicating to the caller that we did some work.
downloadsNeeded++;
this.downloadBodyReps(header, options, latch.defer(header.suid));
}
latch.then(function(results) {
callback(allback.extractErrFromCallbackArgs(results), downloadsNeeded);
});
},
downloadBodyReps: lazyConnection(1, function(header, options, callback) {
var folderConn = this;
var account = this._account;
if (account.conn.currentVersion.lt('12.0'))
return this._syncBodies([header], callback);
if (typeof(options) === 'function') {
callback = options;
options = null;
}
options = options || {};
var io = $ItemOperations.Tags;
var ioEnum = $ItemOperations.Enums;
var as = $AirSync.Tags;
var asEnum = $AirSync.Enums;
var asb = $AirSyncBase.Tags;
var Type = $AirSyncBase.Enums.Type;
var gotBody = function gotBody(bodyInfo) {
if (!bodyInfo)
return callback('unknown');
// ActiveSync only stores one body rep, no matter how many body parts the
// MIME message actually has.
var bodyRep = bodyInfo.bodyReps[0];
var bodyType = bodyRep.type === 'html' ? Type.HTML : Type.PlainText;
var truncationSize;
// If the body is bigger than the max size, grab a small bit of plain text
// to show as the snippet.
if (options.maximumBytesToFetch < bodyRep.sizeEstimate) {
bodyType = Type.PlainText;
truncationSize = DESIRED_TEXT_SNIPPET_BYTES;
}
var w = new $wbxml.Writer('1.3', 1, 'UTF-8');
w.stag(io.ItemOperations)
.stag(io.Fetch)
.tag(io.Store, 'Mailbox')
.tag(as.CollectionId, folderConn.serverId)
.tag(as.ServerId, header.srvid)
.stag(io.Options)
// Only get the AirSyncBase:Body element to minimize bandwidth.
.stag(io.Schema)
.tag(asb.Body)
.etag()
.stag(asb.BodyPreference)
.tag(asb.Type, bodyType);
if (truncationSize)
w.tag(asb.TruncationSize, truncationSize);
w.etag()
.etag()
.etag()
.etag();
account.conn.postCommand(w, function(aError, aResponse) {
if (aError) {
console.error(aError);
account._reportErrorIfNecessary(aError);
callback('unknown');
return;
}
var status, bodyContent, parseError,
e = new $wbxml.EventParser();
e.addEventListener([io.ItemOperations, io.Status], function(node) {
status = node.children[0].textContent;
});
e.addEventListener([io.ItemOperations, io.Response, io.Fetch,
io.Properties, asb.Body, asb.Data], function(node) {
bodyContent = node.children[0].textContent;
});
try {
e.run(aResponse);
}
catch (ex) {
return callback('unknown');
}
if (status !== ioEnum.Status.Success)
return callback('unknown');
folderConn._updateBody(header, bodyInfo, bodyContent, !!truncationSize,
callback);
});
};
this._storage.getMessageBody(header.suid, header.date, gotBody);
}),
/**
* Sync message bodies. This function should only be used against ActiveSync
* 2.5! XXX: This *always* downloads the bodies for all the messages, even if
* it exceeds the maximum requested size.
*/
_syncBodies: function(headers, callback) {
var as = $AirSync.Tags;
var asEnum = $AirSync.Enums;
var em = $Email.Tags;
var folderConn = this;
var account = this._account;
var w = new $wbxml.Writer('1.3', 1, 'UTF-8');
w.stag(as.Sync)
.stag(as.Collections)
.stag(as.Collection)
.tag(as.Class, 'Email')
.tag(as.SyncKey, this.syncKey)
.tag(as.CollectionId, this.serverId)
.stag(as.Options)
.tag(as.MIMESupport, asEnum.MIMESupport.Never)
.etag()
.stag(as.Commands);
for (var i = 0; i < headers.length; i++) {
w.stag(as.Fetch)
.tag(as.ServerId, headers[i].srvid)
.etag();
}
w.etag()
.etag()
.etag()
.etag();
account.conn.postCommand(w, function(aError, aResponse) {
if (aError) {
console.error(aError);
account._reportErrorIfNecessary(aError);
callback('unknown');
return;
}
var latch = allback.latch();
var iHeader = 0;
var e = new $wbxml.EventParser();
var base = [as.Sync, as.Collections, as.Collection];
e.addEventListener(base.concat(as.SyncKey), function(node) {
folderConn.syncKey = node.children[0].textContent;
});
e.addEventListener(base.concat(as.Status), function(node) {
var status = node.children[0].textContent;
if (status !== asEnum.Status.Success) {
latch.defer('status')('unknown');
}
});
e.addEventListener(base.concat(as.Responses, as.Fetch,
as.ApplicationData, em.Body),
function(node) {
// We assume the response is in the same order as the request!
var header = headers[iHeader++];
var bodyContent = node.children[0].textContent;
var latchCallback = latch.defer(header.suid);
folderConn._storage.getMessageBody(header.suid, header.date,
function(body) {
folderConn._updateBody(header, body, bodyContent, false,
latchCallback);
});
});
e.run(aResponse);
latch.then(function(results) {
callback(allback.extractErrFromCallbackArgs(results));
});
});
},
/**
* Determine whether an activesync header represents a read message.
* ActiveSync has an different header flag formant: ['flag', true/false].
*/
_activeSyncHeaderIsSeen: function(header) {
for (var i = 0; i < header.flags.length; i++) {
if (header.flags[i][0] === '\\Seen' && header.flags[i][1]) {
return true;
}
}
return false;
},
_updateBody: function(header, bodyInfo, bodyContent, snippetOnly, callback) {
var bodyRep = bodyInfo.bodyReps[0];
// We neither need to store or want to deal with \r in the processing of
// the body.
bodyContent = bodyContent.replace(/\r/g, '');
var type = snippetOnly ? 'plain' : bodyRep.type;
var data = $mailchew.processMessageContent(bodyContent, type, !snippetOnly,
true);
header.snippet = data.snippet;
bodyRep.isDownloaded = !snippetOnly;
bodyRep.amountDownloaded = bodyContent.length;
if (!snippetOnly)
bodyRep.content = data.content;
var event = {
changeDetails: {
bodyReps: [0]
}
};
var latch = allback.latch();
this._storage.updateMessageHeader(header.date, header.id, false, header,
bodyInfo, latch.defer('header'));
this._storage.updateMessageBody(header, bodyInfo, {}, event,
latch.defer('body'));
latch.then(callback.bind(null, null, bodyInfo, /* flushed */ false));
},
sync: lazyConnection(1, function asfc_sync(accuracyStamp, doneCallback,
progressCallback) {
var folderConn = this,
addedMessages = 0,
changedMessages = 0,
deletedMessages = 0;
logic(this, 'sync_begin');
var self = this;
this._enumerateFolderChanges(function (error, added, changed, deleted,
moreAvailable) {
var storage = folderConn._storage;
if (error === 'badkey') {
folderConn._account._recreateFolder(storage.folderId, function(s) {
// If we got a bad sync key, we'll end up creating a new connection,
// so just clear out the old storage to make this connection unusable.
folderConn._storage = null;
logic(folderConn, 'sync_end', {
full: null, changed: null, deleted: null
});
});
return;
}
else if (error) {
// Sync is over!
logic(folderConn, 'sync_end', {
full: null, changed: null, deleted: null
});
doneCallback(error);
return;
}
var latch = allback.latch();
for (var iter in Iterator(added)) {
var message = iter[1];
// If we already have this message, it's probably because we moved it as
// part of a local op, so let's assume that the data we already have is
// ok. XXX: We might want to verify this, to be safe.
if (storage.hasMessageWithServerId(message.header.srvid))
continue;
storage.addMessageHeader(message.header, message.body, latch.defer());
storage.addMessageBody(message.header, message.body, latch.defer());
addedMessages++;
}
for (var iter in Iterator(changed)) {
var message = iter[1];
// If we don't know about this message, just bail out.
if (!storage.hasMessageWithServerId(message.header.srvid))
continue;
storage.updateMessageHeaderByServerId(message.header.srvid, true,
function(message, oldHeader) {
if (!self._activeSyncHeaderIsSeen(oldHeader) &&
self._activeSyncHeaderIsSeen(message.header)) {
storage.folderMeta.unreadCount--;
} else if (self._activeSyncHeaderIsSeen(oldHeader) &&
!self._activeSyncHeaderIsSeen(message.header)) {
storage.folderMeta.unreadCount++;
}
message.header.mergeInto(oldHeader);
return true;
// Previously, this callback was called without safeguards in place
// to prevent issues caused by the message variable changing,
// so it is now bound to the function.
}.bind(null, message), /* body hint */ null, latch.defer());
changedMessages++;
// XXX: update bodies
}
for (var iter in Iterator(deleted)) {
var messageGuid = iter[1];
// If we don't know about this message, it's probably because we already
// deleted it.
if (!storage.hasMessageWithServerId(messageGuid))
continue;
storage.deleteMessageByServerId(messageGuid, latch.defer());
deletedMessages++;
}
if (!moreAvailable) {
var messagesSeen = addedMessages + changedMessages + deletedMessages;
// Do not report completion of sync until all of our operations have
// been persisted to our in-memory database. We tell this via their
// callbacks having completed.
latch.then(function() {
// Note: For the second argument here, we report the number of
// messages we saw that *changed*. This differs from IMAP, which
// reports the number of messages it *saw*.
logic(folderConn, 'sync_end', {
full: addedMessages,
changed: changedMessages,
deleted: deletedMessages
});
storage.markSyncRange($sync.OLDEST_SYNC_DATE, accuracyStamp, 'XXX',
accuracyStamp);
doneCallback(null, null, messagesSeen);
});
}
},
progressCallback);
}),
performMutation: lazyConnection(1, function(invokeWithWriter, callWhenDone) {
var folderConn = this;
var as = $AirSync.Tags;
var account = this._account;
var w = new $wbxml.Writer('1.3', 1, 'UTF-8');
w.stag(as.Sync)
.stag(as.Collections)
.stag(as.Collection);
if (account.conn.currentVersion.lt('12.1'))
w.tag(as.Class, 'Email');
w.tag(as.SyncKey, this.syncKey)
.tag(as.CollectionId, this.serverId)
// Use DeletesAsMoves in non-trash folders. Don't use it in trash
// folders because that doesn't make any sense.
.tag(as.DeletesAsMoves, this.folderMeta.type === 'trash' ? '0' : '1')
// GetChanges defaults to true, so we must explicitly disable it to
// avoid hearing about changes.
.tag(as.GetChanges, '0')
.stag(as.Commands);
try {
invokeWithWriter(w);
}
catch (ex) {
console.error('Exception in performMutation callee:', ex,
'\n', ex.stack);
callWhenDone('unknown');
return;
}
w.etag(as.Commands)
.etag(as.Collection)
.etag(as.Collections)
.etag(as.Sync);
account.conn.postCommand(w, function(aError, aResponse) {
if (aError) {
console.error('postCommand error:', aError);
account._reportErrorIfNecessary(aError);
callWhenDone('unknown');
return;
}
var e = new $wbxml.EventParser();
var syncKey, status;
var base = [as.Sync, as.Collections, as.Collection];
e.addEventListener(base.concat(as.SyncKey), function(node) {
syncKey = node.children[0].textContent;
});
e.addEventListener(base.concat(as.Status), function(node) {
status = node.children[0].textContent;
});
try {
e.run(aResponse);
}
catch (ex) {
console.error('Error parsing Sync response:', ex, '\n', ex.stack);
callWhenDone('unknown');
return;
}
if (status === $AirSync.Enums.Status.Success) {
folderConn.syncKey = syncKey;
if (callWhenDone)
callWhenDone(null);
}
else {
console.error('Something went wrong during ActiveSync syncing and we ' +
'got a status of ' + status);
callWhenDone('status:' + status);
}
});
}),
// XXX: take advantage of multipart responses here.
// See http://msdn.microsoft.com/en-us/library/ee159875%28v=exchg.80%29.aspx
downloadMessageAttachments: lazyConnection(2, function(uid,
partInfos,
callback,
progress) {
var folderConn = this;
var io = $ItemOperations.Tags;
var ioStatus = $ItemOperations.Enums.Status;
var asb = $AirSyncBase.Tags;
var w = new $wbxml.Writer('1.3', 1, 'UTF-8');
w.stag(io.ItemOperations);
for (var iter in Iterator(partInfos)) {
var part = iter[1];
w.stag(io.Fetch)
.tag(io.Store, 'Mailbox')
.tag(asb.FileReference, part.part)
.etag();
}
w.etag();
this._account.conn.postCommand(w, function(aError, aResult) {
if (aError) {
console.error('postCommand error:', aError);
folderConn._account._reportErrorIfNecessary(aError);
callback('unknown');
return;
}
var globalStatus;
var attachments = {};
var e = new $wbxml.EventParser();
e.addEventListener([io.ItemOperations, io.Status], function(node) {
globalStatus = node.children[0].textContent;
});
e.addEventListener([io.ItemOperations, io.Response, io.Fetch],
function(node) {
var part = null, attachment = {};
for (var iter in Iterator(node.children)) {
var child = iter[1];
switch (child.tag) {
case io.Status:
attachment.status = child.children[0].textContent;
break;
case asb.FileReference:
part = child.children[0].textContent;
break;
case io.Properties:
var contentType = null, data = null;
for (var iter2 in Iterator(child.children)) {
var grandchild = iter2[1];
var textContent = grandchild.children[0].textContent;
switch (grandchild.tag) {
case asb.ContentType:
contentType = textContent;
break;
case io.Data:
data = safeBase64.decode(textContent);
break;
}
}
if (contentType && data)
attachment.data = new Blob([data], { type: contentType });
break;
}
if (part)
attachments[part] = attachment;
}
});
e.run(aResult);
var error = globalStatus !== ioStatus.Success ? 'unknown' : null;
var bodies = [];
for (var iter in Iterator(partInfos)) {
var part = iter[1];
if (attachments.hasOwnProperty(part.part) &&
attachments[part.part].status === ioStatus.Success) {
bodies.push(attachments[part.part].data);
}
else {
error = 'unknown';
bodies.push(null);
}
}
callback(error, bodies);
});
}),
};
function ActiveSyncFolderSyncer(account, folderStorage) {
this._account = account;
this.folderStorage = folderStorage;
logic.defineScope(this, 'ActiveSyncFolderSyncer',
{ accountId: account.id,
folderId: folderStorage.folderId });
this.folderConn = new ActiveSyncFolderConn(account, folderStorage);
}
exports.ActiveSyncFolderSyncer = ActiveSyncFolderSyncer;
ActiveSyncFolderSyncer.prototype = {
/**
* Can we synchronize? Not if we don't have a server id! (This happens for
* the inbox when it is speculative before our first syncFolderList.)
*/
get syncable() {
return this.folderConn.serverId !== null;
},
/**
* Can we grow this sync range? Not in ActiveSync land!
*/
get canGrowSync() {
return false;
},
initialSync: function(slice, initialDays, syncCallback,
doneCallback, progressCallback) {
syncCallback('sync', true /* Ignore Headers */);
this.folderConn.sync(
$date.NOW(),
this.onSyncCompleted.bind(this, doneCallback, true),
progressCallback);
},
refreshSync: function(slice, dir, startTS, endTS, origStartTS,
doneCallback, progressCallback) {
this.folderConn.sync(
$date.NOW(),
this.onSyncCompleted.bind(this, doneCallback, false),
progressCallback);
},
// Returns false if no sync is necessary.
growSync: function(slice, growthDirection, anchorTS, syncStepDays,
doneCallback, progressCallback) {
// ActiveSync is different, and trying to sync more doesn't work with it.
// Just assume we've got all we need.
// (There is no need to invoke the callbacks; by returning false, we
// indicate that we did no work.)
return false;
},
/**
* Whatever synchronization we last triggered has now completed; we should
* either trigger another sync if we still want more data, or close out the
* current sync.
*/
onSyncCompleted: function ifs_onSyncCompleted(doneCallback, initialSync,
err, bisectInfo, messagesSeen) {
var storage = this.folderStorage;
console.log("Sync Completed!", messagesSeen, "messages synced");
// Expand the accuracy range to cover everybody.
if (!err)
storage.markSyncedToDawnOfTime();
// Always save state, although as an optimization, we could avoid saving
// state if we were sure that our state with the server did not advance.
// Do not call our callback until the save has completed.
this._account.__checkpointSyncCompleted(function() {
if (err) {
doneCallback(err);
}
else if (initialSync) {
storage._curSyncSlice.ignoreHeaders = false;
storage._curSyncSlice.waitingOnData = 'db';
// TODO: We could potentially shave some latency by doing the DB fetch
// but deferring the doneCallback until the checkpoint has notified.
// I'm copping out on this right now because there may be some nuances
// in there that I would like to think about more and this is also not
// a major slowdown concern. We're already slow here and the more
// important thing for us to do would just be to trigger the initial
// sync much earlier in the UI process to save even more time.
storage.getMessagesInImapDateRange(
0, null, $sync.INITIAL_FILL_SIZE, $sync.INITIAL_FILL_SIZE,
// Don't trigger a refresh; we just synced. Accordingly,
// releaseMutex can be null.
storage.onFetchDBHeaders.bind(storage, storage._curSyncSlice, false,
doneCallback, null)
);
}
else {
doneCallback(err);
}
});
},
allConsumersDead: function() {
},
shutdown: function() {
this.folderConn.shutdown();
}
};
}); // end define