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-28 01:27:38 +08:00
|
|
|
ExtendableError,
|
2016-06-23 15:49:22 +08:00
|
|
|
} = require('nylas-core');
|
2016-06-24 02:45:24 +08:00
|
|
|
|
2016-06-24 02:20:47 +08:00
|
|
|
const FetchCategoryList = require('./imap/fetch-category-list')
|
|
|
|
const FetchMessagesInCategory = require('./imap/fetch-messages-in-category')
|
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
|
|
|
class SyncAllCategoriesError extends ExtendableError {
|
|
|
|
constructor(message, failures) {
|
|
|
|
super(message)
|
|
|
|
this.failures = failures
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-20 15:19:16 +08:00
|
|
|
class SyncWorker {
|
|
|
|
constructor(account, db) {
|
2016-06-21 08:33:23 +08:00
|
|
|
this._db = db;
|
|
|
|
this._conn = null;
|
|
|
|
this._account = account;
|
2016-06-22 05:58:20 +08:00
|
|
|
this._lastSyncTime = null;
|
2016-06-19 18:02:32 +08:00
|
|
|
|
2016-06-21 08:33:23 +08:00
|
|
|
this._syncTimer = null;
|
|
|
|
this._expirationTimer = null;
|
2016-06-24 02:45:24 +08:00
|
|
|
this._destroyed = false;
|
2016-06-19 18:02:32 +08:00
|
|
|
|
2016-06-21 08:33:23 +08:00
|
|
|
this.syncNow();
|
2016-06-23 15:49:22 +08:00
|
|
|
|
2016-06-27 00:52:03 +08:00
|
|
|
this._listener = PubsubConnector.observableForAccountChanges(account.id)
|
|
|
|
.subscribe(() => this.onAccountChanged())
|
2016-06-23 15:49:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
cleanup() {
|
2016-06-24 02:45:24 +08:00
|
|
|
this._destroyed = true;
|
2016-06-23 15:49:22 +08:00
|
|
|
this._listener.dispose();
|
2016-06-24 02:45:24 +08:00
|
|
|
this._conn.end();
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
2016-06-19 18:02:32 +08:00
|
|
|
|
2016-06-21 08:33:23 +08:00
|
|
|
onAccountChanged() {
|
2016-06-24 02:45:24 +08:00
|
|
|
console.log("SyncWorker: Detected change to account. Reloading and syncing now.")
|
2016-06-23 15:49:22 +08:00
|
|
|
DatabaseConnector.forShared().then(({Account}) => {
|
|
|
|
Account.find({where: {id: this._account.id}}).then((account) => {
|
|
|
|
this._account = account;
|
|
|
|
this.syncNow();
|
|
|
|
})
|
|
|
|
});
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
onSyncDidComplete() {
|
|
|
|
const {afterSync} = this._account.syncPolicy;
|
|
|
|
|
|
|
|
if (afterSync === 'idle') {
|
2016-06-27 00:52:03 +08:00
|
|
|
return this.getInboxCategory()
|
2016-06-28 01:27:38 +08:00
|
|
|
.then((inboxCategory) => this._conn.openBox(inboxCategory.name))
|
|
|
|
.then(() => console.log("SyncWorker: - Idling on inbox category"))
|
2016-06-21 08:33:23 +08:00
|
|
|
} else if (afterSync === 'close') {
|
2016-06-24 02:45:24 +08:00
|
|
|
console.log("SyncWorker: - Closing connection");
|
2016-06-21 08:33:23 +08:00
|
|
|
this._conn.end();
|
|
|
|
this._conn = null;
|
2016-06-27 00:52:03 +08:00
|
|
|
return Promise.resolve()
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
2016-06-27 00:52:03 +08:00
|
|
|
return Promise.reject(new Error(`onSyncDidComplete: Unknown afterSync behavior: ${afterSync}`))
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
onConnectionIdleUpdate() {
|
2016-06-22 05:58:20 +08:00
|
|
|
this.syncNow();
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
2016-06-19 18:02:32 +08:00
|
|
|
|
2016-06-21 08:33:23 +08:00
|
|
|
getInboxCategory() {
|
|
|
|
return this._db.Category.find({where: {role: 'inbox'}})
|
|
|
|
}
|
|
|
|
|
|
|
|
ensureConnection() {
|
2016-06-22 05:58:20 +08:00
|
|
|
if (this._conn) {
|
|
|
|
return this._conn.connect();
|
|
|
|
}
|
2016-06-27 00:52:03 +08:00
|
|
|
return new Promise((resolve, reject) => {
|
2016-06-22 09:29:58 +08:00
|
|
|
const settings = this._account.connectionSettings;
|
|
|
|
const credentials = this._account.decryptedCredentials();
|
|
|
|
|
2016-06-23 08:19:48 +08:00
|
|
|
if (!settings || !settings.imap_host) {
|
2016-06-27 00:52:03 +08:00
|
|
|
return reject(new Error("ensureConnection: There are no IMAP connection settings for this account."))
|
2016-06-22 09:29:58 +08:00
|
|
|
}
|
2016-06-25 07:46:38 +08:00
|
|
|
if (!credentials) {
|
2016-06-27 00:52:03 +08:00
|
|
|
return reject(new Error("ensureConnection: There are no IMAP connection credentials for this account."))
|
2016-06-22 09:29:58 +08:00
|
|
|
}
|
|
|
|
|
2016-06-23 08:19:48 +08:00
|
|
|
const conn = new IMAPConnection(this._db, Object.assign({}, settings, credentials));
|
2016-06-21 08:33:23 +08:00
|
|
|
conn.on('mail', () => {
|
|
|
|
this.onConnectionIdleUpdate();
|
|
|
|
})
|
|
|
|
conn.on('update', () => {
|
|
|
|
this.onConnectionIdleUpdate();
|
|
|
|
})
|
|
|
|
conn.on('queue-empty', () => {
|
|
|
|
});
|
|
|
|
|
|
|
|
this._conn = conn;
|
2016-06-28 01:27:38 +08:00
|
|
|
return resolve(this._conn.connect());
|
2016-06-22 05:58:20 +08:00
|
|
|
});
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
|
|
|
|
2016-06-24 02:20:47 +08:00
|
|
|
fetchCategoryList() {
|
|
|
|
return this._conn.runOperation(new FetchCategoryList())
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
|
|
|
|
2016-06-24 04:15:30 +08:00
|
|
|
syncbackMessageActions() {
|
2016-06-24 06:46:52 +08:00
|
|
|
return Promise.resolve();
|
|
|
|
// TODO
|
|
|
|
const {SyncbackRequest, accountId, Account} = this._db;
|
|
|
|
|
|
|
|
return Account.find({where: {id: accountId}}).then((account) => {
|
|
|
|
return Promise.each(SyncbackRequest.findAll().then((reqs = []) =>
|
|
|
|
reqs.map((request) => {
|
|
|
|
const task = SyncbackTaskFactory.create(account, request);
|
|
|
|
return this._conn.runOperation(task)
|
|
|
|
})
|
|
|
|
));
|
|
|
|
});
|
2016-06-24 04:15:30 +08:00
|
|
|
}
|
|
|
|
|
2016-06-28 01:27:38 +08:00
|
|
|
syncAllCategories() {
|
2016-06-20 15:19:16 +08:00
|
|
|
const {Category} = this._db;
|
2016-06-22 05:58:20 +08:00
|
|
|
const {folderSyncOptions} = this._account.syncPolicy;
|
2016-06-21 08:33:23 +08:00
|
|
|
|
|
|
|
return Category.findAll().then((categories) => {
|
2016-06-22 05:58:20 +08:00
|
|
|
const priority = ['inbox', 'drafts', 'sent'].reverse();
|
|
|
|
const categoriesToSync = categories.sort((a, b) =>
|
|
|
|
(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
|
|
|
|
|
|
|
// const filtered = sorted.filter(cat =>
|
|
|
|
// ['[Gmail]/All Mail', '[Gmail]/Trash', '[Gmail]/Spam'].includes(cat.name)
|
|
|
|
// )
|
|
|
|
|
2016-06-28 01:27:38 +08:00
|
|
|
// TODO Don't accumulate errors, just bail on the first error and clear
|
|
|
|
// the queue and the connection
|
|
|
|
const failures = []
|
2016-06-22 05:58:20 +08:00
|
|
|
return Promise.all(categoriesToSync.map((cat) =>
|
2016-06-24 02:20:47 +08:00
|
|
|
this._conn.runOperation(new FetchMessagesInCategory(cat, folderSyncOptions))
|
2016-06-28 01:27:38 +08:00
|
|
|
.catch((error) => failures.push({error, category: cat.name}))
|
2016-06-24 02:20:47 +08:00
|
|
|
))
|
2016-06-28 01:27:38 +08:00
|
|
|
.then(() => {
|
|
|
|
if (failures.length > 0) {
|
|
|
|
const error = new SyncAllCategoriesError(
|
|
|
|
`Failed to sync all categories for ${this._account.emailAddress}`, failures
|
|
|
|
)
|
|
|
|
return Promise.reject(error)
|
|
|
|
}
|
|
|
|
return Promise.resolve()
|
|
|
|
})
|
2016-06-20 15:19:16 +08:00
|
|
|
});
|
2016-06-19 18:02:32 +08:00
|
|
|
}
|
2016-06-21 08:33:23 +08:00
|
|
|
|
|
|
|
syncNow() {
|
|
|
|
clearTimeout(this._syncTimer);
|
2016-06-24 02:20:47 +08:00
|
|
|
this.ensureConnection()
|
|
|
|
.then(this.fetchCategoryList.bind(this))
|
|
|
|
.then(this.syncbackMessageActions.bind(this))
|
2016-06-28 01:27:38 +08:00
|
|
|
.then(this.syncAllCategories.bind(this))
|
|
|
|
.catch((error) => {
|
|
|
|
// TODO
|
|
|
|
// Distinguish between temporary and critical errors
|
|
|
|
// Update account sync state for critical errors
|
|
|
|
// Handle connection errors separately
|
|
|
|
console.log('----------------------------------')
|
|
|
|
console.log('Erroring where you are supposed to')
|
|
|
|
console.log(error)
|
|
|
|
console.log('----------------------------------')
|
|
|
|
})
|
2016-06-24 02:20:47 +08:00
|
|
|
.finally(() => {
|
2016-06-24 06:52:45 +08:00
|
|
|
this._lastSyncTime = Date.now()
|
2016-06-27 00:52:03 +08:00
|
|
|
this.onSyncDidComplete()
|
2016-06-28 01:27:38 +08:00
|
|
|
.catch((error) => (
|
|
|
|
console.error('SyncWorker.syncNow: Unhandled error while cleaning up after sync: ', error)
|
|
|
|
))
|
2016-06-27 00:52:03 +08:00
|
|
|
.finally(() => this.scheduleNextSync())
|
2016-06-21 08:33:23 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
scheduleNextSync() {
|
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;
|
|
|
|
console.log(`SyncWorker: Account ${active ? 'active' : 'inactive'}. Next sync scheduled for ${new Date(target).toLocaleString()}`);
|
|
|
|
this._syncTimer = setTimeout(() => {
|
|
|
|
this.syncNow();
|
|
|
|
}, target - Date.now());
|
|
|
|
}
|
|
|
|
});
|
2016-06-21 08:33:23 +08:00
|
|
|
}
|
2016-06-19 18:02:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = SyncWorker;
|