forked from mozilla-b2g/gaia-email-libs-and-more
/
accountmixins.js
271 lines (240 loc) · 8.65 KB
/
accountmixins.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/**
*
**/
define(
[
'exports'
],
function(
exports
) {
/**
* The no-op operation for job operations that are not implemented.
* Returns successs in a future turn of the event loop.
*/
function unimplementedJobOperation(op, callback) {
window.setZeroTimeout(function() {
callback(null, null);
});
}
/**
* Account Mixins:
*
* This mixin function is executed from the constructor of the
* CompositeAccount and ActiveSyncAccount, with 'this' being bound to
* the main account instance. If the account has separate receive/send
* parts, they are passed as arguments. (ActiveSync's receive and send
* pieces merely reference the root account.)
*/
exports.accountConstructorMixin = function(receivePiece, sendPiece) {
// The following flags are set on the receivePiece, because the
// receiving side is what manages the job operations (and sending
// messages from the outbox is a job).
// On startup, we need to ignore any stale sendStatus information
// from messages in the outbox. See `sendOutboxMessages` in
// jobmixins.js.
receivePiece.outboxNeedsFreshSync = true;
// This is a runtime flag, used to temporarily prevent
// `sendOutboxMessages` from executing, such as when the user is
// actively trying to edit the list of messages in the Outbox.
receivePiece.outboxSyncEnabled = true;
};
/**
* @args[
* @param[op MailOp]
* @param[mode @oneof[
* @case['local_do']{
* Apply the mutation locally to our database rep.
* }
* @case['check']{
* Check if the manipulation has been performed on the server. There
* is no need to perform a local check because there is no way our
* database can be inconsistent in its view of this.
* }
* @case['do']{
* Perform the manipulation on the server.
* }
* @case['local_undo']{
* Undo the mutation locally.
* }
* @case['undo']{
* Undo the mutation on the server.
* }
* ]]
* @param[callback @func[
* @args[
* @param[error @oneof[String null]]
* ]
* ]]
* }
* ]
*/
exports.runOp = function runOp(op, mode, callback) {
console.log('runOp(' + mode + ': ' + JSON.stringify(op).substring(0, 160) +
')');
var methodName = mode + '_' + op.type, self = this;
// If the job driver doesn't support the operation, assume that it
// is a moot operation that will succeed. Assign it a no-op callback
// that completes in the next tick, so as to maintain job ordering.
var method = this._jobDriver[methodName];
if (!method) {
console.warn('Unsupported op:', op.type, 'mode:', mode);
method = unimplementedJobOperation;
}
this._LOG.runOp_begin(mode, op.type, null, op);
// _LOG supports wrapping calls, but we want to be able to strip out all
// logging, and that wouldn't work.
try {
method.call(this._jobDriver, op,
function(error, resultIfAny, accountSaveSuggested) {
self._jobDriver.postJobCleanup(!error);
console.log('runOp_end(' + mode + ': ' +
JSON.stringify(op).substring(0, 160) + ')\n');
self._LOG.runOp_end(mode, op.type, error, op);
// defer the callback to the next tick to avoid deep recursion
window.setZeroTimeout(function() {
callback(error, resultIfAny, accountSaveSuggested);
});
});
}
catch (ex) {
this._LOG.opError(mode, op.type, ex);
}
};
/**
* Return the folder metadata for the first folder with the given type, or null
* if no such folder exists.
*/
exports.getFirstFolderWithType = function(type) {
var folders = this.folders;
for (var iFolder = 0; iFolder < folders.length; iFolder++) {
if (folders[iFolder].type === type)
return folders[iFolder];
}
return null;
};
exports.getFolderByPath = function(folderPath) {
var folders = this.folders;
for (var iFolder = 0; iFolder < folders.length; iFolder++) {
if (folders[iFolder].path === folderPath)
return folders[iFolder];
}
return null;
};
/**
* 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.
*/
exports.normalizeFolderHierarchy = function() {
// Find a folder for which we'd like to become a sibling.
var sibling =
this.getFirstFolderWithType('drafts') ||
this.getFirstFolderWithType('sent');
// If for some reason we can't find those folders yet, that's
// okay, we will try this again after the next folder sync.
if (!sibling) {
return;
}
var parent = this.getFolderMetaForFolderId(sibling.parentId);
// NOTE: `parent` may be null if `sibling` is a top-level folder.
var foldersToMove = [this.getFirstFolderWithType('localdrafts'),
this.getFirstFolderWithType('outbox')];
foldersToMove.forEach(function(folder) {
// These folders should always exist, but we double-check here
// for safety. Also, if the folder is already in the right
// place, we're done.
if (!folder || folder.parentId === sibling.parentId) {
return;
}
console.log('Moving folder', folder.name,
'underneath', parent && parent.name || '(root)');
this.universe.__notifyRemovedFolder(this, folder);
// On `delim`: IMAP specifies `account.meta.rootDelim` based on
// server-specific settings. ActiveSync hard-codes "/". POP3
// doesn't even go that far. An empty delimiter would be
// incorrect, as it could cause folder paths to smush into one
// another. Thus, it should be safe to fall back to "/" when
// `account.meta.rootDelim` is undefined.
if (parent) {
folder.path = parent.path + (parent.delim || '/') + folder.name;
folder.delim = parent.delim || this.meta.rootDelim || '/';
folder.parentId = parent.id;
folder.depth = parent.depth + 1;
} else {
folder.path = folder.name;
folder.delim = this.meta.rootDelim || '/';
folder.parentId = null;
folder.depth = 0;
}
this.universe.__notifyAddedFolder(this, folder);
}, this);
};
/**
* Save the state of this account to the database. This entails updating all
* of our highly-volatile state (folderInfos which contains counters, accuracy
* structures, and our block info structures) as well as any dirty blocks.
*
* This should be entirely coherent because the structured clone should occur
* synchronously during this call, but it's important to keep in mind that if
* that ever ends up not being the case that we need to cause mutating
* operations to defer until after that snapshot has occurred.
*/
exports.saveAccountState = function(reuseTrans, callback, reason) {
if (!this._alive) {
this._LOG.accountDeleted('saveAccountState');
return null;
}
this._LOG.saveAccountState_begin(reason, null);
// Indicate save is active, in case something, like
// signaling the end of a sync, needs to run after
// a save, via runAfterSaves.
this._saveAccountStateActive = true;
if (!this._deferredSaveAccountCalls) {
this._deferredSaveAccountCalls = [];
}
if (callback)
this.runAfterSaves(callback);
var perFolderStuff = [], self = this;
for (var iFolder = 0; iFolder < this.folders.length; iFolder++) {
var folderPub = this.folders[iFolder],
folderStorage = this._folderStorages[folderPub.id],
folderStuff = folderStorage.generatePersistenceInfo();
if (folderStuff)
perFolderStuff.push(folderStuff);
}
var folderSaveCount = perFolderStuff.length;
var trans = this._db.saveAccountFolderStates(
this.id, this._folderInfos, perFolderStuff,
this._deadFolderIds,
function stateSaved() {
this._saveAccountStateActive = false;
this._LOG.saveAccountState_end(reason, folderSaveCount);
// NB: we used to log when the save completed, but it ended up being
// annoying to the unit tests since we don't block our actions on
// the completion of the save at this time.
var callbacks = this._deferredSaveAccountCalls;
this._deferredSaveAccountCalls = [];
callbacks.forEach(function(callback) {
callback();
});
}.bind(this),
reuseTrans);
// Reduce the length of time perFolderStuff and its contents are kept alive.
perFolderStuff = null;
this._deadFolderIds = null;
return trans;
};
exports.runAfterSaves = function(callback) {
if (this._saveAccountStateActive || this._saveAccountIsImminent) {
this._deferredSaveAccountCalls.push(callback);
} else {
callback();
}
};
}); // end define