mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-25 01:36:06 +08:00
169 lines
5.5 KiB
JavaScript
169 lines
5.5 KiB
JavaScript
|
const {PromiseUtils} = require('isomorphic-core')
|
||
|
// const _ = require('underscore');
|
||
|
|
||
|
|
||
|
function pickMatchingThread(message, threads) {
|
||
|
return threads.pop();
|
||
|
|
||
|
// This logic is tricky... Used to say that threads with >2 participants in common
|
||
|
// should be treated as the same, plus special cases for when it's a 1<>1
|
||
|
// conversation. Put it back soonish.
|
||
|
|
||
|
// const messageEmails = _.uniq([].concat(message.to, message.cc, message.from).map(p => p.email));
|
||
|
// logger.info({
|
||
|
// num_candidate_threads: threads.length,
|
||
|
// message_subject: message.subject,
|
||
|
// }, `Found candidate threads for message`)
|
||
|
//
|
||
|
// for (const thread of threads) {
|
||
|
// const threadEmails = _.uniq([].concat(thread.participants).map(p => p.email));
|
||
|
// logger.info(`Intersection: ${_.intersection(threadEmails, messageEmails).join(',')}`)
|
||
|
//
|
||
|
// if (_.intersection(threadEmails, messageEmails) >= threadEmails.length * 0.9) {
|
||
|
// return thread;
|
||
|
// }
|
||
|
// }
|
||
|
//
|
||
|
// return null;
|
||
|
}
|
||
|
|
||
|
function cleanSubject(subject = "") {
|
||
|
const regex = new RegExp(/^((re|fw|fwd|aw|wg|undeliverable|undelivered):\s*)+/ig);
|
||
|
return subject.replace(regex, () => "");
|
||
|
}
|
||
|
|
||
|
function emptyThread({Thread, accountId}, options = {}) {
|
||
|
const t = Thread.build(Object.assign({accountId}, options))
|
||
|
t.folders = [];
|
||
|
t.labels = [];
|
||
|
return Promise.resolve(t)
|
||
|
}
|
||
|
|
||
|
function findOrBuildByMatching(db, message) {
|
||
|
const {Thread, Label, Folder} = db
|
||
|
|
||
|
// in the future, we should look at In-reply-to. Problem is it's a single-
|
||
|
// directional linked list, and we don't scan the mailbox from oldest=>newest,
|
||
|
// but from newest->oldest, so when we ingest a message it's very unlikely
|
||
|
// we have the "In-reply-to" message yet.
|
||
|
|
||
|
return Thread.findAll({
|
||
|
where: {
|
||
|
subject: cleanSubject(message.subject),
|
||
|
},
|
||
|
order: [
|
||
|
['id', 'DESC'],
|
||
|
],
|
||
|
limit: 10,
|
||
|
include: [{model: Label}, {model: Folder}],
|
||
|
}).then((threads) =>
|
||
|
pickMatchingThread(message, threads) || emptyThread(db, {})
|
||
|
)
|
||
|
}
|
||
|
|
||
|
function findOrBuildByRemoteThreadId(db, remoteThreadId) {
|
||
|
const {Thread, Label, Folder} = db;
|
||
|
return Thread.find({
|
||
|
where: {remoteThreadId},
|
||
|
include: [{model: Label}, {model: Folder}],
|
||
|
}).then((thread) => {
|
||
|
return thread || emptyThread(db, {remoteThreadId})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function detectThread({db, message}) {
|
||
|
if (!(message.labels instanceof Array)) {
|
||
|
throw new Error("Threading processMessage expects labels to be an inflated array.");
|
||
|
}
|
||
|
if (!message.folder) {
|
||
|
throw new Error("Threading processMessage expects folder value to be present.");
|
||
|
}
|
||
|
|
||
|
|
||
|
const {Folder, Label} = db;
|
||
|
let findOrBuildThread = null;
|
||
|
if (message.headers['x-gm-thrid']) {
|
||
|
findOrBuildThread = findOrBuildByRemoteThreadId(db, message.headers['x-gm-thrid'])
|
||
|
} else {
|
||
|
findOrBuildThread = findOrBuildByMatching(db, message)
|
||
|
}
|
||
|
|
||
|
return PromiseUtils.props({
|
||
|
thread: findOrBuildThread,
|
||
|
sentFolder: Folder.find({where: {role: 'sent'}}),
|
||
|
sentLabel: Label.find({where: {role: 'sent'}}),
|
||
|
})
|
||
|
.then(({thread, sentFolder, sentLabel}) => {
|
||
|
if (!(thread.labels instanceof Array)) {
|
||
|
throw new Error("Threading processMessage expects thread.labels to be an inflated array.");
|
||
|
}
|
||
|
if (!(thread.folders instanceof Array)) {
|
||
|
throw new Error("Threading processMessage expects thread.folders to be an inflated array.");
|
||
|
}
|
||
|
|
||
|
// update the basic properties of the thread
|
||
|
thread.accountId = message.accountId;
|
||
|
|
||
|
// update the participants on the thread
|
||
|
const threadParticipants = [].concat(thread.participants);
|
||
|
const threadEmails = thread.participants.map(p => p.email);
|
||
|
|
||
|
for (const p of [].concat(message.to, message.cc, message.from)) {
|
||
|
if (!threadEmails.includes(p.email)) {
|
||
|
threadParticipants.push(p);
|
||
|
threadEmails.push(p.email);
|
||
|
}
|
||
|
}
|
||
|
thread.participants = threadParticipants;
|
||
|
|
||
|
// update starred and unread
|
||
|
if (thread.starredCount == null) { thread.starredCount = 0; }
|
||
|
thread.starredCount += message.starred ? 1 : 0;
|
||
|
if (thread.unreadCount == null) { thread.unreadCount = 0; }
|
||
|
thread.unreadCount += message.unread ? 1 : 0;
|
||
|
|
||
|
// update dates
|
||
|
if (!thread.lastMessageDate || (message.date > thread.lastMessageDate)) {
|
||
|
thread.lastMessageDate = message.date;
|
||
|
thread.snippet = message.snippet;
|
||
|
thread.subject = cleanSubject(message.subject);
|
||
|
}
|
||
|
if (!thread.firstMessageDate || (message.date < thread.firstMessageDate)) {
|
||
|
thread.firstMessageDate = message.date;
|
||
|
}
|
||
|
|
||
|
let isSent = false;
|
||
|
if (sentFolder) {
|
||
|
isSent = message.folderId === sentFolder.id
|
||
|
} else if (sentLabel) {
|
||
|
isSent = !!message.labels.find(l => l.id === sentLabel.id)
|
||
|
}
|
||
|
|
||
|
if (isSent && ((message.date > thread.lastMessageSentDate) || !thread.lastMessageSentDate)) {
|
||
|
thread.lastMessageSentDate = message.date;
|
||
|
}
|
||
|
if (!isSent && ((message.date > thread.lastMessageReceivedDate) || !thread.lastMessageReceivedDate)) {
|
||
|
thread.lastMessageReceivedDate = message.date;
|
||
|
}
|
||
|
if (!thread.folders.find(f => f.id === message.folderId)) {
|
||
|
thread.folders.push(message.folder)
|
||
|
}
|
||
|
return thread.save()
|
||
|
.then((saved) => {
|
||
|
const promises = []
|
||
|
// update folders and labels
|
||
|
if (!saved.folders.find(f => f.id === message.folderId)) {
|
||
|
promises.push(saved.addFolder(message.folder))
|
||
|
}
|
||
|
for (const label of message.labels) {
|
||
|
if (!saved.labels.find(l => l.id === label)) {
|
||
|
promises.push(saved.addLabel(label))
|
||
|
}
|
||
|
}
|
||
|
return Promise.all(promises).thenReturn(saved)
|
||
|
})
|
||
|
});
|
||
|
}
|
||
|
|
||
|
module.exports = detectThread
|