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

1032 lines
34 KiB
JavaScript
Executable file

/**
* Implements the ActiveSync protocol for Hotmail and Exchange.
**/
define(
[
'logic',
'../a64',
'../accountmixins',
'../mailslice',
'../searchfilter',
// We potentially create the synthetic inbox while offline, so this can't be
// lazy-loaded.
'activesync/codepages/FolderHierarchy',
'./folder',
'./jobs',
'../util',
'../db/folder_info_rep',
'module',
'require',
'exports'
],
function(
logic,
$a64,
$acctmixins,
$mailslice,
$searchfilter,
$FolderHierarchy,
$asfolder,
$asjobs,
$util,
$folder_info,
$module,
require,
exports
) {
'use strict';
// Lazy loaded vars.
var $wbxml, $asproto, ASCP;
var bsearchForInsert = $util.bsearchForInsert;
var $FolderTypes = $FolderHierarchy.Enums.Type;
var DEFAULT_TIMEOUT_MS = exports.DEFAULT_TIMEOUT_MS = 30 * 1000;
/**
* Randomly create a unique device id so that multiple devices can independently
* synchronize without interfering with each other. Our only goals are to avoid
* needlessly providing fingerprintable data and avoid collisions with other
* instances of ourself. We're using Math.random over crypto.getRandomValues
* since node does not have the latter right now and predictable values aren't
* a concern.
*
* @return {String} An multi-character ASCII alphanumeric sequence. (Probably
10 or 11 digits.)
*/
exports.makeUniqueDeviceId = function() {
return Math.random().toString(36).substr(2);
};
/**
* Prototype-helper to wrap a method in a call to withConnection. This exists
* largely for historical reasons. All actual lazy-loading happens within
* withConnection.
*/
function lazyConnection(cbIndex, fn, failString) {
return function lazyRun() {
var args = Array.slice(arguments),
errback = args[cbIndex],
self = this;
this.withConnection(errback, function () {
fn.apply(self, args);
}, failString);
};
}
function ActiveSyncAccount(universe, accountDef, folderInfos, dbConn,
receiveProtoConn) {
this.universe = universe;
this.id = accountDef.id;
this.accountDef = accountDef;
// Transparent upgrade; allocate a device-id if we don't have one. By doing
// this we avoid forcing the user to manually re-create the account. And the
// current migration system would throw away any saved drafts, which is not
// desirable. The common thing in all cases is that we will need to re-sync
// the folders.
// XXX remove this upgrade logic when we next compel a version upgrade (and do
// so safely.)
if (!accountDef.connInfo.deviceId) {
accountDef.connInfo.deviceId = exports.makeUniqueDeviceId();
}
this._db = dbConn;
logic.defineScope(this, 'Account', { accountId: this.id,
accountType: 'activesync' });
if (receiveProtoConn) {
this.conn = receiveProtoConn;
this._attachLoggerToConnection(this.conn);
}
else {
this.conn = null;
}
this.enabled = true;
this.problems = [];
this._alive = true;
this.identities = accountDef.identities;
this.folders = [];
this._folderStorages = {};
this._folderInfos = folderInfos;
this._serverIdToFolderId = {};
this._deadFolderIds = null;
this._syncsInProgress = 0;
this._lastSyncKey = null;
this._lastSyncResponseWasEmpty = false;
this.meta = folderInfos.$meta;
this.mutations = folderInfos.$mutations;
// Sync existing folders
for (var folderId in folderInfos) {
if (folderId[0] === '$')
continue;
var folderInfo = folderInfos[folderId];
this._folderStorages[folderId] =
new $mailslice.FolderStorage(this, folderId, folderInfo, this._db,
$asfolder.ActiveSyncFolderSyncer);
this._serverIdToFolderId[folderInfo.$meta.serverId] = folderId;
this.folders.push(folderInfo.$meta);
}
this.folders.sort(function(a, b) { return a.path.localeCompare(b.path); });
this._jobDriver = new $asjobs.ActiveSyncJobDriver(
this,
this._folderInfos.$mutationState);
// Immediately ensure that we have any required local-only folders,
// as those can be created even while offline.
this.ensureEssentialOfflineFolders();
// Mix in any fields common to all accounts.
$acctmixins.accountConstructorMixin.call(
this, /* receivePiece = */ this, /* sendPiece = */ this);
}
exports.Account = exports.ActiveSyncAccount = ActiveSyncAccount;
ActiveSyncAccount.prototype = {
type: 'activesync',
supportsServerFolders: true,
toString: function asa_toString() {
return '[ActiveSyncAccount: ' + this.id + ']';
},
/**
* Manages connecting, and wiring up initial connection if it is not
* initialized yet.
*/
withConnection: function (errback, callback, failString) {
// lazy load our dependencies if they haven't already been fetched. This
// occurs regardless of whether we have a connection already or not. We
// do this because the connection may have been passed-in to us as a
// leftover of the account creation process.
if (!$wbxml) {
require(['wbxml', 'activesync/protocol', 'activesync/codepages'],
function (_wbxml, _asproto, _ASCP) {
$wbxml = _wbxml;
$asproto = _asproto;
ASCP = _ASCP;
this.withConnection(errback, callback, failString);
}.bind(this));
return;
}
if (!this.conn) {
var accountDef = this.accountDef;
this.conn = new $asproto.Connection(accountDef.connInfo.deviceId);
this._attachLoggerToConnection(this.conn);
this.conn.open(accountDef.connInfo.server,
accountDef.credentials.username,
accountDef.credentials.password);
this.conn.timeout = DEFAULT_TIMEOUT_MS;
}
if (!this.conn.connected) {
this.conn.connect(function(error) {
if (error) {
this._reportErrorIfNecessary(error);
// If the error was HTTP 401 (bad user/pass), report it as
// bad-user-or-pass so that account logic like
// _cmd_clearAccountProblems knows whether or not to report
// the error as user-serviceable.
if (this._isBadUserOrPassError(error) && !failString) {
failString = 'bad-user-or-pass';
}
errback(failString || 'unknown');
return;
}
callback();
}.bind(this));
} else {
callback();
}
},
_isBadUserOrPassError: function(error) {
return (error &&
error instanceof $asproto.HttpError &&
error.status === 401);
},
/**
* Reports the error to the user if necessary.
*/
_reportErrorIfNecessary: function(error) {
if (!error) {
return;
}
if (this._isBadUserOrPassError(error)) {
// prompt the user to try a different password
this.universe.__reportAccountProblem(
this, 'bad-user-or-pass', 'incoming');
}
},
_attachLoggerToConnection: function(conn) {
logic.defineScope(conn, 'ActiveSyncConnection',
{ connectionId: logic.uniqueId() });
if (!logic.isCensored) {
conn.onmessage = this._onmessage_dangerous.bind(this, conn);
} else {
conn.onmessage = this._onmessage_safe.bind(this, conn);
}
},
/**
* Basic onmessage ActiveSync protocol logging function. This does not
* include user data and is intended for safe circular logging purposes.
*/
_onmessage_safe: function onmessage(conn,
type, special, xhr, params, extraHeaders, sentData, response) {
if (type === 'options') {
logic(conn, 'options', { special: special,
status: xhr.status,
response: response });
}
else {
logic(conn, 'command', { type: type,
special: special,
status: xhr.status });
}
},
/**
* Dangerous onmessage ActiveSync protocol logging function. This is
* intended to log user data for unit testing purposes or very specialized
* debugging only.
*/
_onmessage_dangerous: function onmessage(conn,
type, special, xhr, params, extraHeaders, sentData, response) {
if (type === 'options') {
logic(conn, 'options', { special: special,
status: xhr.status,
response: response });
}
else {
var sentXML, receivedXML;
if (sentData) {
try {
var sentReader = new $wbxml.Reader(new Uint8Array(sentData), ASCP);
sentXML = sentReader.dump();
}
catch (ex) {
sentXML = 'parse problem';
}
}
if (response) {
try {
receivedXML = response.dump();
response.rewind();
}
catch (ex) {
receivedXML = 'parse problem';
}
}
logic(conn, 'command', { type: type,
special: special,
status: xhr.status,
params: params,
extraHeaders: extraHeaders,
sentXML: sentXML,
receivedXML: receivedXML });
}
},
toBridgeWire: function asa_toBridgeWire() {
return {
id: this.accountDef.id,
name: this.accountDef.name,
path: this.accountDef.name,
type: this.accountDef.type,
defaultPriority: this.accountDef.defaultPriority,
enabled: this.enabled,
problems: this.problems,
syncRange: this.accountDef.syncRange,
syncInterval: this.accountDef.syncInterval,
notifyOnNew: this.accountDef.notifyOnNew,
playSoundOnSend: this.accountDef.playSoundOnSend,
identities: this.identities,
credentials: {
username: this.accountDef.credentials.username,
},
servers: [
{
type: this.accountDef.type,
connInfo: this.accountDef.connInfo
},
]
};
},
toBridgeFolder: function asa_toBridgeFolder() {
return {
id: this.accountDef.id,
name: this.accountDef.name,
path: this.accountDef.name,
type: 'account',
};
},
get numActiveConns() {
return 0;
},
/**
* Check that the account is healthy in that we can login at all.
*/
checkAccount: function(callback) {
// disconnect first so as to properly check credentials
if (this.conn != null) {
if (this.conn.connected) {
this.conn.disconnect();
}
this.conn = null;
}
this.withConnection(function(err) {
callback(err);
}, function() {
callback();
});
},
/**
* We are being told that a synchronization pass completed, and that we may
* want to consider persisting our state.
*/
__checkpointSyncCompleted: function(callback, betterReason) {
this.saveAccountState(null, callback, betterReason || 'checkpointSync');
},
shutdown: function asa_shutdown(callback) {
if (callback)
callback();
},
accountDeleted: function asa_accountDeleted() {
this._alive = false;
this.shutdown();
},
sliceFolderMessages: function asa_sliceFolderMessages(folderId,
bridgeHandle) {
var storage = this._folderStorages[folderId],
slice = new $mailslice.MailSlice(bridgeHandle, storage);
storage.sliceOpenMostRecent(slice);
},
searchFolderMessages: function(folderId, bridgeHandle, phrase, whatToSearch) {
var storage = this._folderStorages[folderId],
slice = new $searchfilter.SearchSlice(bridgeHandle, storage, phrase,
whatToSearch);
storage.sliceOpenSearch(slice);
return slice;
},
syncFolderList: lazyConnection(0, function asa_syncFolderList(callback) {
var account = this;
var fh = ASCP.FolderHierarchy.Tags;
var w = new $wbxml.Writer('1.3', 1, 'UTF-8');
w.stag(fh.FolderSync)
.tag(fh.SyncKey, this.meta.syncKey)
.etag();
this.conn.postCommand(w, function(aError, aResponse) {
if (aError) {
account._reportErrorIfNecessary(aError);
callback(aError);
return;
}
var e = new $wbxml.EventParser();
var deferredAddedFolders = [];
e.addEventListener([fh.FolderSync, fh.SyncKey], function(node) {
account.meta.syncKey = node.children[0].textContent;
});
e.addEventListener([fh.FolderSync, fh.Changes, [fh.Add, fh.Delete]],
function(node) {
var folder = {};
for (var iter in Iterator(node.children)) {
var child = iter[1];
folder[child.localTagName] = child.children[0].textContent;
}
if (node.tag === fh.Add) {
if (!account._addedFolder(folder.ServerId, folder.ParentId,
folder.DisplayName, folder.Type))
deferredAddedFolders.push(folder);
}
else {
account._deletedFolder(folder.ServerId);
}
});
try {
e.run(aResponse);
}
catch (ex) {
console.error('Error parsing FolderSync response:', ex, '\n',
ex.stack);
callback('unknown');
return;
}
// It's possible we got some folders in an inconvenient order (i.e. child
// folders before their parents). Keep trying to add folders until we're
// done.
while (deferredAddedFolders.length) {
var moreDeferredAddedFolders = [];
for (var iter in Iterator(deferredAddedFolders)) {
var folder = iter[1];
if (!account._addedFolder(folder.ServerId, folder.ParentId,
folder.DisplayName, folder.Type))
moreDeferredAddedFolders.push(folder);
}
if (moreDeferredAddedFolders.length === deferredAddedFolders.length)
throw new Error('got some orphaned folders');
deferredAddedFolders = moreDeferredAddedFolders;
}
// Once we've synchonized the folder list, kick off another job
// to check that we have all essential online folders. Once that
// completes, we'll check to make sure our offline-only folders
// (localdrafts, outbox) are in the right place according to
// where this server stores other built-in folders.
account.ensureEssentialOnlineFolders();
account.normalizeFolderHierarchy();
console.log('Synced folder list');
callback && callback(null);
});
}),
// Map folder type numbers from ActiveSync to Gaia's types
_folderTypes: {
1: 'normal', // Generic
2: 'inbox', // DefaultInbox
3: 'drafts', // DefaultDrafts
4: 'trash', // DefaultDeleted
5: 'sent', // DefaultSent
6: 'normal', // DefaultOutbox
12: 'normal', // Mail
},
/**
* List of known junk folder names, taken from browserbox.js, and used to
* infer folders that are junk folders based on their name since there is
* no enumerated type representing junk folders.
*/
_junkFolderNames: [
'bulk mail', 'correo no deseado', 'courrier indésirable', 'istenmeyen',
'istenmeyen e-posta', 'junk', 'levélszemét', 'nevyžiadaná pošta',
'nevyžádaná pošta', 'no deseado', 'posta indesiderata', 'pourriel',
'roskaposti', 'skräppost', 'spam', 'spamowanie', 'søppelpost',
'thư rác', 'спам', 'דואר זבל', 'الرسائل العشوائية', 'هرزنامه', 'สแปม',
'垃圾郵件', '垃圾邮件', '垃圾電郵'],
/**
* Update the internal database and notify the appropriate listeners when we
* discover a new folder.
*
* @param {string} serverId A GUID representing the new folder
* @param {string} parentServerId A GUID representing the parent folder, or
* '0' if this is a root-level folder
* @param {string} displayName The display name for the new folder
* @param {string} typeNum A numeric value representing the new folder's type,
* corresponding to the mapping in _folderTypes above
* @param {string} forceType Force a string folder type for this folder.
* Used for synthetic folders like localdrafts.
* @param {boolean} suppressNotification (optional) if true, don't notify any
* listeners of this addition
* @return {object} the folderMeta if we added the folder, true if we don't
* care about this kind of folder, or null if we need to wait until later
* (e.g. if we haven't added the folder's parent yet)
*/
_addedFolder: function asa__addedFolder(serverId, parentServerId, displayName,
typeNum, forceType,
suppressNotification) {
if (!forceType && !(typeNum in this._folderTypes))
return true; // Not a folder type we care about.
var path = displayName;
var parentFolderId = null;
var depth = 0;
if (parentServerId !== '0') {
parentFolderId = this._serverIdToFolderId[parentServerId];
// We haven't learned about the parent folder. Just return, and wait until
// we do.
if (parentFolderId === undefined)
return null;
var parent = this._folderInfos[parentFolderId];
path = parent.$meta.path + '/' + path;
depth = parent.$meta.depth + 1;
}
var useFolderType = this._folderTypes[typeNum];
// Check if this looks like a junk folder based on its name/path. (There
// is no type for junk/spam folders, so this regrettably must be inferred
// from the name. At least for hotmail.com/outlook.com, it appears that
// the name is "Junk" regardless of the locale in which the account is
// created, but our current datapoint is one account created using the
// Spanish locale.
//
// In order to avoid bad detections, we assume that the junk folder is
// at the top-level or is only nested one level deep.
if (depth < 2) {
var normalizedName = displayName.toLowerCase();
if (this._junkFolderNames.indexOf(normalizedName) !== -1) {
useFolderType = 'junk';
}
}
if (forceType) {
useFolderType = forceType;
}
// Handle sentinel Inbox.
if (typeNum === $FolderTypes.DefaultInbox) {
var existingInboxMeta = this.getFirstFolderWithType('inbox');
if (existingInboxMeta) {
// Update the server ID to folder ID mapping.
delete this._serverIdToFolderId[existingInboxMeta.serverId];
this._serverIdToFolderId[serverId] = existingInboxMeta.id;
// Update everything about the folder meta.
existingInboxMeta.serverId = serverId;
existingInboxMeta.name = displayName;
existingInboxMeta.path = path;
existingInboxMeta.depth = depth;
return existingInboxMeta;
}
}
var folderId = this.id + '/' + $a64.encodeInt(this.meta.nextFolderNum++);
var folderInfo = this._folderInfos[folderId] = {
$meta: $folder_info.makeFolderMeta({
id: folderId,
serverId: serverId,
name: displayName,
type: useFolderType,
path: path,
parentId: parentFolderId,
depth: depth,
lastSyncedAt: 0,
syncKey: '0',
version: $mailslice.FOLDER_DB_VERSION
}),
// any changes to the structure here must be reflected in _recreateFolder!
$impl: {
nextId: 0,
nextHeaderBlock: 0,
nextBodyBlock: 0,
},
accuracy: [],
headerBlocks: [],
bodyBlocks: [],
serverIdHeaderBlockMapping: {},
};
console.log('Added folder ' + displayName + ' (' + folderId + ')');
this._folderStorages[folderId] =
new $mailslice.FolderStorage(this, folderId, folderInfo, this._db,
$asfolder.ActiveSyncFolderSyncer);
this._serverIdToFolderId[serverId] = folderId;
var folderMeta = folderInfo.$meta;
var idx = bsearchForInsert(this.folders, folderMeta, function(a, b) {
return a.path.localeCompare(b.path);
});
this.folders.splice(idx, 0, folderMeta);
if (!suppressNotification)
this.universe.__notifyAddedFolder(this, folderMeta);
return folderMeta;
},
/**
* Update the internal database and notify the appropriate listeners when we
* find out a folder has been removed.
*
* @param {string} serverId A GUID representing the deleted folder
* @param {boolean} suppressNotification (optional) if true, don't notify any
* listeners of this addition
*/
_deletedFolder: function asa__deletedFolder(serverId, suppressNotification) {
var folderId = this._serverIdToFolderId[serverId],
folderInfo = this._folderInfos[folderId],
folderMeta = folderInfo.$meta;
console.log('Deleted folder ' + folderMeta.name + ' (' + folderId + ')');
delete this._serverIdToFolderId[serverId];
delete this._folderInfos[folderId];
delete this._folderStorages[folderId];
var idx = this.folders.indexOf(folderMeta);
this.folders.splice(idx, 1);
if (this._deadFolderIds === null)
this._deadFolderIds = [];
this._deadFolderIds.push(folderId);
if (!suppressNotification)
this.universe.__notifyRemovedFolder(this, folderMeta);
},
/**
* Recreate the folder storage for a particular folder; useful when we end up
* desyncing with the server and need to start fresh. No notification is
* generated, although slices are repopulated.
*
* FYI: There is a nearly identical method in IMAP's account implementation.
*
* @param {string} folderId the local ID of the folder
* @param {function} callback a function to be called when the operation is
* complete, taking the new folder storage
*/
_recreateFolder: function asa__recreateFolder(folderId, callback) {
logic(this, 'recreateFolder', { folderId: folderId });
var folderInfo = this._folderInfos[folderId];
folderInfo.$impl = {
nextId: 0,
nextHeaderBlock: 0,
nextBodyBlock: 0,
};
folderInfo.accuracy = [];
folderInfo.headerBlocks = [];
folderInfo.bodyBlocks = [];
folderInfo.serverIdHeaderBlockMapping = {};
if (this._deadFolderIds === null)
this._deadFolderIds = [];
this._deadFolderIds.push(folderId);
var self = this;
this.saveAccountState(null, function() {
var newStorage =
new $mailslice.FolderStorage(self, folderId, folderInfo, self._db,
$asfolder.ActiveSyncFolderSyncer);
for (var iter in Iterator(self._folderStorages[folderId]._slices)) {
var slice = iter[1];
slice._storage = newStorage;
slice.reset();
newStorage.sliceOpenMostRecent(slice);
}
self._folderStorages[folderId]._slices = [];
self._folderStorages[folderId] = newStorage;
callback(newStorage);
}, 'recreateFolder');
},
/**
* Create a folder that is the child/descendant of the given parent folder.
* If no parent folder id is provided, we attempt to create a root folder.
*
* NOTE: This function is currently unused. It might have been used for
* testing at some point. It will be used again someday but should not be
* assumed to actually work when that day comes.
*
* @args[
* @param[parentFolderId String]
* @param[folderName]
* @param[containOnlyOtherFolders Boolean]{
* Should this folder only contain other folders (and no messages)?
* On some servers/backends, mail-bearing folders may not be able to
* create sub-folders, in which case one would have to pass this.
* }
* @param[callback @func[
* @args[
* @param[error @oneof[
* @case[null]{
* No error, the folder got created and everything is awesome.
* }
* @case['offline']{
* We are offline and can't create the folder.
* }
* @case['already-exists']{
* The folder appears to already exist.
* }
* @case['unknown']{
* It didn't work and we don't have a better reason.
* }
* ]]
* @param[folderMeta ImapFolderMeta]{
* The meta-information for the folder.
* }
* ]
* ]]{
* }
* ]
*/
createFolder: lazyConnection(3, function asa_createFolder(parentFolderId,
folderName,
containOnlyOtherFolders,
callback) {
var account = this;
var parentFolderServerId = parentFolderId ?
this._folderInfos[parentFolderId] : '0';
var fh = ASCP.FolderHierarchy.Tags;
var fhStatus = ASCP.FolderHierarchy.Enums.Status;
var folderType = ASCP.FolderHierarchy.Enums.Type.Mail;
var w = new $wbxml.Writer('1.3', 1, 'UTF-8');
w.stag(fh.FolderCreate)
.tag(fh.SyncKey, this.meta.syncKey)
.tag(fh.ParentId, parentFolderServerId)
.tag(fh.DisplayName, folderName)
.tag(fh.Type, folderType)
.etag();
this.conn.postCommand(w, function(aError, aResponse) {
account._reportErrorIfNecessary(aError);
var e = new $wbxml.EventParser();
var status, serverId;
e.addEventListener([fh.FolderCreate, fh.Status], function(node) {
status = node.children[0].textContent;
});
e.addEventListener([fh.FolderCreate, fh.SyncKey], function(node) {
account.meta.syncKey = node.children[0].textContent;
});
e.addEventListener([fh.FolderCreate, fh.ServerId], function(node) {
serverId = node.children[0].textContent;
});
try {
e.run(aResponse);
}
catch (ex) {
console.error('Error parsing FolderCreate response:', ex, '\n',
ex.stack);
callback('unknown');
return;
}
if (status === fhStatus.Success) {
var folderMeta = account._addedFolder(serverId, parentFolderServerId,
folderName, folderType);
callback(null, folderMeta);
}
else if (status === fhStatus.FolderExists) {
callback('already-exists');
}
else {
callback('unknown');
}
});
}),
/**
* Delete an existing folder WITHOUT ANY ABILITY TO UNDO IT. Current UX
* does not desire this, but the unit tests do.
*
* Callback is like the createFolder one, why not.
*/
deleteFolder: lazyConnection(1, function asa_deleteFolder(folderId,
callback) {
var account = this;
var folderMeta = this._folderInfos[folderId].$meta;
var fh = ASCP.FolderHierarchy.Tags;
var fhStatus = ASCP.FolderHierarchy.Enums.Status;
var folderType = ASCP.FolderHierarchy.Enums.Type.Mail;
var w = new $wbxml.Writer('1.3', 1, 'UTF-8');
w.stag(fh.FolderDelete)
.tag(fh.SyncKey, this.meta.syncKey)
.tag(fh.ServerId, folderMeta.serverId)
.etag();
this.conn.postCommand(w, function(aError, aResponse) {
account._reportErrorIfNecessary(aError);
var e = new $wbxml.EventParser();
var status;
e.addEventListener([fh.FolderDelete, fh.Status], function(node) {
status = node.children[0].textContent;
});
e.addEventListener([fh.FolderDelete, fh.SyncKey], function(node) {
account.meta.syncKey = node.children[0].textContent;
});
try {
e.run(aResponse);
}
catch (ex) {
console.error('Error parsing FolderDelete response:', ex, '\n',
ex.stack);
callback('unknown');
return;
}
if (status === fhStatus.Success) {
account._deletedFolder(folderMeta.serverId);
callback(null, folderMeta);
}
else {
callback('unknown');
}
});
}),
sendMessage: lazyConnection(1, function asa_sendMessage(composer, callback) {
var account = this;
// we want the bcc included because that's how we tell the server the bcc
// results.
composer.withMessageBlob({ includeBcc: true }, function(mimeBlob) {
// ActiveSync 14.0 has a completely different API for sending email. Make
// sure we format things the right way.
if (this.conn.currentVersion.gte('14.0')) {
var cm = ASCP.ComposeMail.Tags;
var w = new $wbxml.Writer('1.3', 1, 'UTF-8', null, 'blob');
w.stag(cm.SendMail)
// The ClientId is defined to be for duplicate messages suppression
// and does not need to have any uniqueness constraints apart from
// not being similar to (recently sent) messages by this client.
.tag(cm.ClientId, Date.now().toString()+'@mozgaia')
.tag(cm.SaveInSentItems)
.stag(cm.Mime)
.opaque(mimeBlob)
.etag()
.etag();
this.conn.postCommand(w, function(aError, aResponse) {
if (aError) {
account._reportErrorIfNecessary(aError);
console.error(aError);
callback('unknown');
return;
}
if (aResponse === null) {
console.log('Sent message successfully!');
callback(null);
}
else {
console.error('Error sending message. XML dump follows:\n' +
aResponse.dump());
callback('unknown');
}
}, /* aExtraParams = */ null, /* aExtraHeaders = */ null,
/* aProgressCallback = */ function() {
// Keep holding the wakelock as we continue sending.
composer.renewSmartWakeLock('ActiveSync XHR Progress');
});
}
else { // ActiveSync 12.x and lower
this.conn.postData('SendMail', 'message/rfc822', mimeBlob,
function(aError, aResponse) {
if (aError) {
account._reportErrorIfNecessary(aError);
console.error(aError);
callback('unknown');
return;
}
console.log('Sent message successfully!');
callback(null);
}, { SaveInSent: 'T' }, /* aExtraHeaders = */ null,
/* aProgressCallback = */ function() {
// Keep holding the wakelock as we continue sending.
composer.renewSmartWakeLock('ActiveSync XHR Progress');
});
}
}.bind(this));
}),
getFolderStorageForFolderId: function asa_getFolderStorageForFolderId(
folderId) {
return this._folderStorages[folderId];
},
getFolderStorageForServerId: function asa_getFolderStorageForServerId(
serverId) {
return this._folderStorages[this._serverIdToFolderId[serverId]];
},
getFolderMetaForFolderId: function(folderId) {
if (this._folderInfos.hasOwnProperty(folderId))
return this._folderInfos[folderId].$meta;
return null;
},
/**
* Ensure that local-only folders exist. This runs synchronously
* before we sync the folder list with the server. Ideally, these
* folders should reside in a proper place in the folder hierarchy,
* which may differ between servers depending on whether the
* account's other folders live underneath the inbox or as
* top-level-folders. But since moving folders is easy and doesn't
* really affect the backend, we'll just ensure they exist here, and
* fix up their hierarchical location when syncing the folder list.
*/
ensureEssentialOfflineFolders: function() {
// On folder type numbers: While there are enum values for outbox
// and drafts, they represent server-side default folders, not the
// local folders we create for ourselves, so they must be created
// with an unknown typeNum.
[{
type: 'inbox',
displayName: 'Inbox', // Intentionally title-case.
typeNum: $FolderTypes.DefaultInbox,
}, {
type: 'outbox',
displayName: 'outbox',
typeNum: $FolderTypes.Unknown, // There is no "local outbox" typeNum.
}, {
type: 'localdrafts',
displayName: 'localdrafts',
typeNum: $FolderTypes.Unknown, // There is no "localdrafts" typeNum.
}].forEach(function(data) {
if (!this.getFirstFolderWithType(data.type)) {
this._addedFolder(
/* serverId: */ null,
/* parentServerId: */ '0',
/* displayName: */ data.displayName,
/* typeNum: */ data.typeNum,
/* forceType: */ data.type,
/* suppressNotification: */ true);
}
}, this);
},
/**
* Kick off jobs to create essential folders (sent, trash) if
* necessary. These folders should be created on both the client and
* the server; contrast with `ensureEssentialOfflineFolders`.
*
* TODO: Support localizing all automatically named e-mail folders
* regardless of the origin locale.
* Relevant bugs: <https://bugzil.la/905869>, <https://bugzil.la/905878>.
*
* @param {function} callback
* Called when all ops have run.
*/
ensureEssentialOnlineFolders: function(callback) {
// Our ActiveSync implementation currently assumes that all
// ActiveSync servers always come with Sent and Trash folders. If
// that assumption proves false, we'd add them here like IMAP.
callback && callback();
},
/**
* Ensure that local-only folders live in a reasonable place in the
* folder hierarchy by moving them if necessary.
*
* We proactively create local-only folders at the root level before
* we synchronize with the server; if possible, we want these
* folders to reside as siblings to other system-level folders on
* the account. This is called at the end of syncFolderList, after
* we have learned about all existing server folders.
*/
normalizeFolderHierarchy: $acctmixins.normalizeFolderHierarchy,
scheduleMessagePurge: function(folderId, callback) {
// ActiveSync servers have no incremental folder growth, so message purging
// makes no sense for them.
if (callback)
callback();
},
upgradeFolderStoragesIfNeeded: $acctmixins.upgradeFolderStoragesIfNeeded,
runOp: $acctmixins.runOp,
getFirstFolderWithType: $acctmixins.getFirstFolderWithType,
getFolderByPath: $acctmixins.getFolderByPath,
saveAccountState: $acctmixins.saveAccountState,
runAfterSaves: $acctmixins.runAfterSaves,
allOperationsCompleted: function() {
}
};
}); // end define