2016-06-23 15:49:22 +08:00
|
|
|
const {
|
2016-06-24 07:28:48 +08:00
|
|
|
SchedulerUtils,
|
2016-06-23 15:49:22 +08:00
|
|
|
IMAPConnection,
|
|
|
|
PubsubConnector,
|
|
|
|
DatabaseConnector,
|
2016-06-29 06:35:35 +08:00
|
|
|
MessageTypes,
|
2016-06-23 15:49:22 +08:00
|
|
|
} = require('nylas-core');
|
2016-07-02 06:46:15 +08:00
|
|
|
const {
|
|
|
|
jsonError,
|
|
|
|
} = require('./sync-utils')
|
2016-06-24 02:45:24 +08:00
|
|
|
|
2016-07-02 06:41:22 +08:00
|
|
|
const {CLAIM_DURATION} = SchedulerUtils;
|
|
|
|
|
2016-07-09 08:13:30 +08:00
|
|
|
const FetchFolderList = require('./imap/fetch-folder-list')
|
|
|
|
const FetchMessagesInFolder = require('./imap/fetch-messages-in-folder')
|
2016-06-24 06:46:52 +08:00
|
|
|
const SyncbackTaskFactory = require('./syncback-task-factory')
|
2016-06-19 18:02:32 +08:00
|
|
|
|
2016-06-28 01:27:38 +08:00
|
|
|
|
2016-06-20 15:19:16 +08:00
|
|
|
class SyncWorker {
|
2016-07-02 06:41:22 +08:00
|
|
|
constructor(account, db, onExpired) {
|
2016-06-21 08:33:23 +08:00
|
|
|
this._db = db;
|
|
|
|
this._conn = null;
|
|
|
|
this._account = account;
|
2016-07-02 06:41:22 +08:00
|
|
|
this._startTime = Date.now();
|
2016-06-22 05:58:20 +08:00
|
|
|
this._lastSyncTime = null;
|
2016-07-02 06:41:22 +08:00
|
|
|
this._onExpired = onExpired;
|
2016-07-09 08:13:30 +08:00
|
|
|
this._logger = global.Logger.forAccount(account)
|
2016-06-19 18:02:32 +08:00
|
|
|
|
2016-06-21 08:33:23 +08:00
|
|
|
this._syncTimer = null;
|
2016-06-24 02:45:24 +08:00
|
|
|
this._destroyed = false;
|
2016-06-19 18:02:32 +08:00
|
|
|
|
2016-07-09 06:36:50 +08:00
|
|
|
this.syncNow({reason: 'Initial'});
|
2016-06-23 15:49:22 +08:00
|
|
|
|
2016-06-30 02:44:30 +08:00
|
|
|
this._onMessage = this._onMessage.bind(this);
|
2016-07-01 04:25:13 +08:00
|
|
|
this._listener = PubsubConnector.observeAccount(account.id).subscribe(this._onMessage)
|
2016-06-23 15:49:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
cleanup() {
|
2016-07-12 07:56:18 +08:00
|
|
|
clearTimeout(this._syncTimer);
|
|
|
|
this._syncTimer = null;
|
2016-06-24 02:45:24 +08:00
|
|
|
this._destroyed = true;
|
2016-06-23 15:49:22 +08:00
|
|
|
this._listener.dispose();
|
2016-06-28 07:01:21 +08:00
|
|
|
this.closeConnection()
|
|
|
|
}
|
|
|
|
|
|
|
|
closeConnection() {
|
2016-07-02 02:36:02 +08:00
|
|
|
if (this._conn) {
|
|
|
|
this._conn.end();
|
|
|
|
}
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
2016-06-19 18:02:32 +08:00
|
|
|
|
2016-06-29 08:12:45 +08:00
|
|
|
_onMessage(msg) {
|
2016-06-30 02:44:30 +08:00
|
|
|
const {type} = JSON.parse(msg);
|
2016-06-29 09:01:43 +08:00
|
|
|
switch (type) {
|
2016-07-12 05:12:03 +08:00
|
|
|
case MessageTypes.ACCOUNT_CREATED:
|
|
|
|
// No other processing currently required for account creation
|
|
|
|
break;
|
2016-07-12 07:56:18 +08:00
|
|
|
case MessageTypes.ACCOUNT_UPDATED:
|
|
|
|
this._onAccountUpdated();
|
|
|
|
break;
|
|
|
|
case MessageTypes.ACCOUNT_DELETED:
|
|
|
|
this.cleanup();
|
|
|
|
this._onExpired();
|
|
|
|
break;
|
|
|
|
case MessageTypes.SYNCBACK_REQUESTED:
|
|
|
|
this.syncNow({reason: 'Syncback Action Queued'});
|
|
|
|
break;
|
2016-06-29 06:35:35 +08:00
|
|
|
default:
|
2016-07-12 05:10:30 +08:00
|
|
|
this._logger.error({message: msg}, 'SyncWorker: Invalid message')
|
2016-06-29 06:35:35 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_onAccountUpdated() {
|
2016-07-09 07:59:00 +08:00
|
|
|
if (!this.isWaitingForNextSync()) {
|
|
|
|
return;
|
2016-07-09 06:36:50 +08:00
|
|
|
}
|
2016-07-12 05:10:30 +08:00
|
|
|
this._getAccount()
|
|
|
|
.then((account) => {
|
2016-07-09 07:59:00 +08:00
|
|
|
this._account = account;
|
|
|
|
this.syncNow({reason: 'Account Modification'});
|
2016-07-12 05:10:30 +08:00
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
this._logger.error(err, 'SyncWorker: Error getting account for update')
|
|
|
|
})
|
2016-06-29 06:44:29 +08:00
|
|
|
}
|
|
|
|
|
2016-07-01 03:33:08 +08:00
|
|
|
_onConnectionIdleUpdate() {
|
2016-07-09 07:59:00 +08:00
|
|
|
if (!this.isWaitingForNextSync()) {
|
|
|
|
return;
|
|
|
|
}
|
2016-07-09 06:36:50 +08:00
|
|
|
this.syncNow({reason: 'IMAP IDLE Fired'});
|
2016-07-01 03:33:08 +08:00
|
|
|
}
|
|
|
|
|
2016-06-29 06:44:29 +08:00
|
|
|
_getAccount() {
|
|
|
|
return DatabaseConnector.forShared().then(({Account}) =>
|
|
|
|
Account.find({where: {id: this._account.id}})
|
|
|
|
);
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
|
|
|
|
2016-07-01 03:33:08 +08:00
|
|
|
_getIdleFolder() {
|
2016-07-01 00:29:21 +08:00
|
|
|
return this._db.Folder.find({where: {role: ['all', 'inbox']}})
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
ensureConnection() {
|
2016-06-22 05:58:20 +08:00
|
|
|
if (this._conn) {
|
|
|
|
return this._conn.connect();
|
|
|
|
}
|
2016-06-28 07:01:21 +08:00
|
|
|
const settings = this._account.connectionSettings;
|
|
|
|
const credentials = this._account.decryptedCredentials();
|
2016-06-22 09:29:58 +08:00
|
|
|
|
2016-06-28 07:01:21 +08:00
|
|
|
if (!settings || !settings.imap_host) {
|
2016-07-02 06:46:15 +08:00
|
|
|
return Promise.reject(new Error("ensureConnection: There are no IMAP connection settings for this account."))
|
2016-06-28 07:01:21 +08:00
|
|
|
}
|
|
|
|
if (!credentials) {
|
2016-07-02 06:46:15 +08:00
|
|
|
return Promise.reject(new Error("ensureConnection: There are no IMAP connection credentials for this account."))
|
2016-06-28 07:01:21 +08:00
|
|
|
}
|
2016-06-21 08:33:23 +08:00
|
|
|
|
2016-07-12 07:01:18 +08:00
|
|
|
const conn = new IMAPConnection({
|
|
|
|
db: this._db,
|
|
|
|
settings: Object.assign({}, settings, credentials),
|
|
|
|
logger: this._logger,
|
|
|
|
});
|
|
|
|
|
2016-06-28 07:01:21 +08:00
|
|
|
conn.on('mail', () => {
|
2016-07-01 03:33:08 +08:00
|
|
|
this._onConnectionIdleUpdate();
|
2016-06-28 07:01:21 +08:00
|
|
|
})
|
|
|
|
conn.on('update', () => {
|
2016-07-01 03:33:08 +08:00
|
|
|
this._onConnectionIdleUpdate();
|
2016-06-28 07:01:21 +08:00
|
|
|
})
|
|
|
|
conn.on('queue-empty', () => {
|
2016-06-22 05:58:20 +08:00
|
|
|
});
|
2016-06-28 07:01:21 +08:00
|
|
|
|
|
|
|
this._conn = conn;
|
|
|
|
return this._conn.connect();
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
|
|
|
|
2016-06-24 04:15:30 +08:00
|
|
|
syncbackMessageActions() {
|
2016-06-29 08:12:45 +08:00
|
|
|
const where = {where: {status: "NEW"}, limit: 100};
|
|
|
|
return this._db.SyncbackRequest.findAll(where)
|
|
|
|
.map((req) => SyncbackTaskFactory.create(this._account, req))
|
2016-06-29 10:02:12 +08:00
|
|
|
.each(this.runSyncbackTask.bind(this))
|
|
|
|
}
|
|
|
|
|
|
|
|
runSyncbackTask(task) {
|
2016-06-30 01:11:53 +08:00
|
|
|
const syncbackRequest = task.syncbackRequestObject()
|
2016-06-29 10:02:12 +08:00
|
|
|
return this._conn.runOperation(task)
|
2016-06-30 02:44:30 +08:00
|
|
|
.then(() => {
|
|
|
|
syncbackRequest.status = "SUCCEEDED"
|
|
|
|
})
|
2016-06-29 10:07:49 +08:00
|
|
|
.catch((error) => {
|
2016-06-30 01:11:53 +08:00
|
|
|
syncbackRequest.error = error
|
|
|
|
syncbackRequest.status = "FAILED"
|
2016-07-09 07:59:00 +08:00
|
|
|
})
|
|
|
|
.finally(() => syncbackRequest.save())
|
2016-06-24 04:15:30 +08:00
|
|
|
}
|
|
|
|
|
2016-06-28 01:27:38 +08:00
|
|
|
syncAllCategories() {
|
2016-07-01 00:29:21 +08:00
|
|
|
const {Folder} = this._db;
|
2016-06-22 05:58:20 +08:00
|
|
|
const {folderSyncOptions} = this._account.syncPolicy;
|
2016-06-21 08:33:23 +08:00
|
|
|
|
2016-07-01 00:29:21 +08:00
|
|
|
return Folder.findAll().then((categories) => {
|
2016-06-28 07:05:31 +08:00
|
|
|
const priority = ['inbox', 'all', 'drafts', 'sent', 'spam', 'trash'].reverse();
|
2016-06-29 09:31:36 +08:00
|
|
|
const categoriesToSync = categories.sort((a, b) =>
|
2016-06-22 05:58:20 +08:00
|
|
|
(priority.indexOf(a.role) - priority.indexOf(b.role)) * -1
|
2016-06-21 08:33:23 +08:00
|
|
|
)
|
2016-06-22 05:58:20 +08:00
|
|
|
|
|
|
|
return Promise.all(categoriesToSync.map((cat) =>
|
2016-07-09 08:13:30 +08:00
|
|
|
this._conn.runOperation(new FetchMessagesInFolder(cat, folderSyncOptions, this._logger))
|
2016-06-24 02:20:47 +08:00
|
|
|
))
|
2016-06-20 15:19:16 +08:00
|
|
|
});
|
2016-06-19 18:02:32 +08:00
|
|
|
}
|
2016-06-21 08:33:23 +08:00
|
|
|
|
2016-07-09 06:36:50 +08:00
|
|
|
syncNow({reason} = {}) {
|
2016-06-21 08:33:23 +08:00
|
|
|
clearTimeout(this._syncTimer);
|
2016-07-09 06:36:50 +08:00
|
|
|
this._syncTimer = null;
|
2016-06-28 07:01:21 +08:00
|
|
|
|
2016-07-13 07:46:14 +08:00
|
|
|
this._account.reload().then(() => {
|
|
|
|
if (!process.env.SYNC_AFTER_ERRORS && this._account.errored()) {
|
|
|
|
this._logger.info(`SyncWorker: Account is in error state - Skipping sync`)
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
this._logger.info({reason}, `SyncWorker: Account sync started`)
|
|
|
|
|
|
|
|
return this._account.update({syncError: null})
|
|
|
|
.then(() => this.ensureConnection())
|
|
|
|
.then(() => this.syncbackMessageActions())
|
|
|
|
.then(() => this._conn.runOperation(new FetchFolderList(this._account.provider)))
|
|
|
|
.then(() => this.syncAllCategories())
|
|
|
|
.then(() => this.onSyncDidComplete())
|
|
|
|
.catch((error) => this.onSyncError(error))
|
|
|
|
})
|
2016-06-24 02:20:47 +08:00
|
|
|
.finally(() => {
|
2016-06-24 06:52:45 +08:00
|
|
|
this._lastSyncTime = Date.now()
|
2016-06-28 07:01:21 +08:00
|
|
|
this.scheduleNextSync()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
onSyncError(error) {
|
2016-07-09 08:13:30 +08:00
|
|
|
this._logger.error(error, `SyncWorker: Error while syncing account`)
|
2016-06-28 07:01:21 +08:00
|
|
|
this.closeConnection()
|
2016-07-01 03:33:08 +08:00
|
|
|
|
2016-07-12 15:59:41 +08:00
|
|
|
// Continue to retry if it was a network error
|
|
|
|
if (error.source && (error.source.includes('socket') || error.source.includes('timeout'))) {
|
2016-06-28 07:01:21 +08:00
|
|
|
return Promise.resolve()
|
|
|
|
}
|
2016-07-09 07:59:00 +08:00
|
|
|
|
2016-07-02 06:46:15 +08:00
|
|
|
this._account.syncError = jsonError(error)
|
2016-06-28 07:01:21 +08:00
|
|
|
return this._account.save()
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
|
|
|
|
2016-07-01 03:33:08 +08:00
|
|
|
onSyncDidComplete() {
|
|
|
|
const {afterSync} = this._account.syncPolicy;
|
2016-07-13 06:04:12 +08:00
|
|
|
const now = Date.now();
|
2016-07-01 03:33:08 +08:00
|
|
|
|
2016-07-12 05:16:39 +08:00
|
|
|
if (!this._account.firstSyncCompletion) {
|
2016-07-13 06:04:12 +08:00
|
|
|
this._account.firstSyncCompletion = now;
|
2016-07-02 04:15:49 +08:00
|
|
|
}
|
|
|
|
|
2016-07-08 05:30:51 +08:00
|
|
|
const syncGraphTimeLength = 60 * 30; // 30 minutes, should be the same as SyncGraph.config.timeLength
|
2016-07-12 07:56:18 +08:00
|
|
|
let lastSyncCompletions = [].concat(this._account.lastSyncCompletions)
|
2016-07-08 05:30:51 +08:00
|
|
|
lastSyncCompletions = [now, ...lastSyncCompletions]
|
|
|
|
while (now - lastSyncCompletions[lastSyncCompletions.length - 1] > 1000 * syncGraphTimeLength) {
|
|
|
|
lastSyncCompletions.pop();
|
2016-07-01 08:04:13 +08:00
|
|
|
}
|
2016-07-08 05:30:51 +08:00
|
|
|
|
2016-07-01 08:04:13 +08:00
|
|
|
this._account.lastSyncCompletions = lastSyncCompletions
|
|
|
|
this._account.save()
|
2016-07-13 09:31:55 +08:00
|
|
|
|
2016-07-09 08:13:30 +08:00
|
|
|
this._logger.info('Syncworker: Completed sync cycle')
|
2016-07-01 08:04:13 +08:00
|
|
|
|
2016-07-01 03:33:08 +08:00
|
|
|
if (afterSync === 'idle') {
|
|
|
|
return this._getIdleFolder()
|
|
|
|
.then((idleFolder) => this._conn.openBox(idleFolder.name))
|
2016-07-12 05:10:30 +08:00
|
|
|
.then(() => this._logger.info('SyncWorker: Idling on inbox category'))
|
2016-07-01 03:33:08 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (afterSync === 'close') {
|
2016-07-12 05:10:30 +08:00
|
|
|
this._logger.info('SyncWorker: Closing connection');
|
2016-07-01 03:33:08 +08:00
|
|
|
this.closeConnection()
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
|
|
|
|
2016-07-12 05:10:30 +08:00
|
|
|
this._logger.error({after_sync: afterSync}, `SyncWorker.onSyncDidComplete: Unknown afterSync behavior`)
|
|
|
|
throw new Error('SyncWorker.onSyncDidComplete: Unknown afterSync behavior')
|
2016-07-01 03:33:08 +08:00
|
|
|
}
|
|
|
|
|
2016-07-09 07:59:00 +08:00
|
|
|
isWaitingForNextSync() {
|
2016-07-09 06:36:50 +08:00
|
|
|
return this._syncTimer != null;
|
|
|
|
}
|
|
|
|
|
2016-06-21 08:33:23 +08:00
|
|
|
scheduleNextSync() {
|
2016-07-02 06:41:22 +08:00
|
|
|
if (Date.now() - this._startTime > CLAIM_DURATION) {
|
2016-07-09 08:13:30 +08:00
|
|
|
this._logger.info("SyncWorker: - Has held account for more than CLAIM_DURATION, returning to pool.");
|
2016-07-02 06:41:22 +08:00
|
|
|
this.cleanup();
|
|
|
|
this._onExpired();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-06-24 07:28:48 +08:00
|
|
|
SchedulerUtils.checkIfAccountIsActive(this._account.id).then((active) => {
|
|
|
|
const {intervals} = this._account.syncPolicy;
|
|
|
|
const interval = active ? intervals.active : intervals.inactive;
|
|
|
|
|
|
|
|
if (interval) {
|
|
|
|
const target = this._lastSyncTime + interval;
|
2016-07-09 08:13:30 +08:00
|
|
|
this._logger.info({
|
|
|
|
is_active: active,
|
|
|
|
next_sync: new Date(target).toLocaleString(),
|
|
|
|
}, `SyncWorker: Next sync scheduled`);
|
2016-06-24 07:28:48 +08:00
|
|
|
this._syncTimer = setTimeout(() => {
|
2016-07-09 06:36:50 +08:00
|
|
|
this.syncNow({reason: 'Scheduled'});
|
2016-06-24 07:28:48 +08:00
|
|
|
}, target - Date.now());
|
|
|
|
}
|
|
|
|
});
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
2016-06-19 18:02:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = SyncWorker;
|