2017-08-15 13:54:22 +02:00
|
|
|
//@flow
|
2021-02-03 17:13:38 +01:00
|
|
|
import type {MoveMailData} from "../../api/entities/tutanota/MoveMailData"
|
|
|
|
|
import {createMoveMailData} from "../../api/entities/tutanota/MoveMailData"
|
|
|
|
|
import {TutanotaService} from "../../api/entities/tutanota/Services"
|
|
|
|
|
import {InboxRuleType, MAX_NBR_MOVE_DELETE_MAIL_SERVICE} from "../../api/common/TutanotaConstants"
|
|
|
|
|
import {isDomainName, isRegularExpression} from "../../misc/FormatValidator"
|
|
|
|
|
import {HttpMethod} from "../../api/common/EntityFunctions"
|
|
|
|
|
import {debounce, getMailHeaders, noOp} from "../../api/common/utils/Utils"
|
|
|
|
|
import {assertMainOrNode} from "../../api/common/Env"
|
|
|
|
|
import {lang} from "../../misc/LanguageViewModel"
|
|
|
|
|
import {MailHeadersTypeRef} from "../../api/entities/tutanota/MailHeaders"
|
|
|
|
|
import {logins} from "../../api/main/LoginController"
|
2017-11-24 15:14:56 +01:00
|
|
|
import type {MailboxDetail} from "./MailModel"
|
2021-02-03 17:13:38 +01:00
|
|
|
import {LockedError, NotFoundError, PreconditionFailedError} from "../../api/common/error/RestError"
|
|
|
|
|
import type {Mail} from "../../api/entities/tutanota/Mail"
|
|
|
|
|
import type {InboxRule} from "../../api/entities/tutanota/InboxRule"
|
|
|
|
|
import type {SelectorItemList} from "../../gui/base/DropDownSelectorN"
|
|
|
|
|
import {splitInChunks} from "../../api/common/utils/ArrayUtils"
|
|
|
|
|
import {EntityClient} from "../../api/common/EntityClient"
|
|
|
|
|
import type {WorkerClient} from "../../api/main/WorkerClient"
|
|
|
|
|
import {getElementId, getListId, isSameId} from "../../api/common/utils/EntityUtils";
|
2019-09-13 13:49:11 +02:00
|
|
|
import {getInboxFolder} from "./MailUtils"
|
2021-06-22 15:24:10 +02:00
|
|
|
import {ofClass, promiseMap} from "../../api/common/utils/PromiseUtils"
|
2017-08-15 13:54:22 +02:00
|
|
|
|
|
|
|
|
assertMainOrNode()
|
|
|
|
|
|
2020-09-28 14:50:34 +02:00
|
|
|
const moveMailDataPerFolder: MoveMailData[] = []
|
|
|
|
|
const DEBOUNCE_FIRST_MOVE_MAIL_REQUEST_MS = 200
|
|
|
|
|
let applyingRules = false // used to avoid concurrent application of rules (-> requests to locked service)
|
|
|
|
|
|
2019-09-13 13:49:11 +02:00
|
|
|
function sendMoveMailRequest(worker: WorkerClient): Promise<void> {
|
2020-09-28 14:50:34 +02:00
|
|
|
if (moveMailDataPerFolder.length) {
|
|
|
|
|
const moveToTargetFolder = moveMailDataPerFolder.shift()
|
|
|
|
|
const mailChunks = splitInChunks(MAX_NBR_MOVE_DELETE_MAIL_SERVICE, moveToTargetFolder.mails)
|
2021-06-22 15:24:10 +02:00
|
|
|
return promiseMap(mailChunks, mailChunk => {
|
2020-09-28 14:50:34 +02:00
|
|
|
moveToTargetFolder.mails = mailChunk
|
2019-09-13 13:49:11 +02:00
|
|
|
return worker.serviceRequest(TutanotaService.MoveMailService, HttpMethod.POST, moveToTargetFolder)
|
2021-06-21 17:47:54 +02:00
|
|
|
}).catch(ofClass(LockedError, e => { //LockedError should no longer be thrown!?!
|
2020-09-28 14:50:34 +02:00
|
|
|
console.log("moving mail failed", e, moveToTargetFolder)
|
2021-06-21 17:47:54 +02:00
|
|
|
})).catch(ofClass(PreconditionFailedError, e => {
|
2020-09-28 14:50:34 +02:00
|
|
|
// move mail operation may have been locked by other process
|
|
|
|
|
console.log("moving mail failed", e, moveToTargetFolder)
|
2021-06-21 17:47:54 +02:00
|
|
|
})).finally(() => {
|
2019-09-13 13:49:11 +02:00
|
|
|
return sendMoveMailRequest(worker)
|
2020-09-28 14:50:34 +02:00
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
//We are done and unlock for future requests
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We throttle the moveMail requests to a rate of 50ms
|
|
|
|
|
// Each target folder requires one request
|
2019-09-13 13:49:11 +02:00
|
|
|
const applyMatchingRules = debounce(DEBOUNCE_FIRST_MOVE_MAIL_REQUEST_MS, (worker: WorkerClient) => {
|
2020-09-28 14:50:34 +02:00
|
|
|
if (applyingRules) return
|
|
|
|
|
// We lock to avoid concurrent requests
|
|
|
|
|
applyingRules = true
|
2019-09-13 13:49:11 +02:00
|
|
|
sendMoveMailRequest(worker).finally(() => {
|
2020-09-28 14:50:34 +02:00
|
|
|
applyingRules = false
|
|
|
|
|
})
|
|
|
|
|
})
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
export function getInboxRuleTypeNameMapping(): SelectorItemList<string> {
|
2017-08-15 13:54:22 +02:00
|
|
|
return [
|
|
|
|
|
{value: InboxRuleType.FROM_EQUALS, name: lang.get("inboxRuleSenderEquals_action")},
|
|
|
|
|
{value: InboxRuleType.RECIPIENT_TO_EQUALS, name: lang.get("inboxRuleToRecipientEquals_action")},
|
|
|
|
|
{value: InboxRuleType.RECIPIENT_CC_EQUALS, name: lang.get("inboxRuleCCRecipientEquals_action")},
|
|
|
|
|
{value: InboxRuleType.RECIPIENT_BCC_EQUALS, name: lang.get("inboxRuleBCCRecipientEquals_action")},
|
|
|
|
|
{value: InboxRuleType.SUBJECT_CONTAINS, name: lang.get("inboxRuleSubjectContains_action")},
|
|
|
|
|
{value: InboxRuleType.MAIL_HEADER_CONTAINS, name: lang.get("inboxRuleMailHeaderContains_action")}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getInboxRuleTypeName(type: string): string {
|
2018-07-26 14:25:29 +02:00
|
|
|
let typeNameMapping = getInboxRuleTypeNameMapping().find(t => t.value === type)
|
2017-12-21 12:12:05 +01:00
|
|
|
return typeNameMapping != null ? typeNameMapping.name : ""
|
2017-08-15 13:54:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Checks the mail for an existing inbox rule and moves the mail to the target folder of the rule.
|
|
|
|
|
* @returns true if a rule matches otherwise false
|
|
|
|
|
*/
|
2019-09-13 13:49:11 +02:00
|
|
|
export function findAndApplyMatchingRule(worker: WorkerClient, entityClient: EntityClient, mailboxDetail: MailboxDetail, mail: Mail,
|
|
|
|
|
applyRulesOnServer: boolean): Promise<?IdTuple> {
|
2020-09-28 14:50:34 +02:00
|
|
|
if (mail._errors || !mail.unread || !isInboxList(mailboxDetail, getListId(mail))
|
2018-07-18 17:22:54 +02:00
|
|
|
|| !logins.getUserController().isPremiumAccount()) {
|
2019-04-09 16:32:57 +02:00
|
|
|
return Promise.resolve(null)
|
2017-08-15 13:54:22 +02:00
|
|
|
}
|
2021-07-15 11:34:55 +02:00
|
|
|
return _findMatchingRule(entityClient, mail, logins.getUserController().props.inboxRules).then(inboxRule => {
|
2017-08-15 13:54:22 +02:00
|
|
|
if (inboxRule) {
|
2020-07-08 10:23:41 +02:00
|
|
|
let targetFolder = mailboxDetail.folders.filter(folder => folder !== getInboxFolder(mailboxDetail.folders))
|
2020-09-28 14:50:34 +02:00
|
|
|
.find(folder => isSameId(folder._id, inboxRule.targetFolder))
|
2017-08-15 13:54:22 +02:00
|
|
|
if (targetFolder) {
|
2020-10-13 17:15:52 +02:00
|
|
|
if (applyRulesOnServer) {
|
|
|
|
|
let moveMailData = moveMailDataPerFolder.find(folderMoveMailData => isSameId(folderMoveMailData.targetFolder, inboxRule.targetFolder))
|
|
|
|
|
if (moveMailData) {
|
|
|
|
|
moveMailData.mails.push(mail._id)
|
|
|
|
|
} else {
|
|
|
|
|
moveMailData = createMoveMailData()
|
|
|
|
|
moveMailData.targetFolder = inboxRule.targetFolder
|
|
|
|
|
moveMailData.mails.push(mail._id)
|
|
|
|
|
moveMailDataPerFolder.push(moveMailData)
|
|
|
|
|
}
|
2019-09-13 13:49:11 +02:00
|
|
|
applyMatchingRules(worker)
|
2020-09-28 14:50:34 +02:00
|
|
|
}
|
2019-04-09 16:32:57 +02:00
|
|
|
return [targetFolder.mails, getElementId(mail)]
|
2017-08-15 13:54:22 +02:00
|
|
|
} else {
|
2019-04-09 16:32:57 +02:00
|
|
|
return null
|
2017-08-15 13:54:22 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
2019-04-09 16:32:57 +02:00
|
|
|
return null
|
2017-08-15 13:54:22 +02:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Finds the first matching inbox rule for the mail and returns it.
|
|
|
|
|
* export only for testing
|
|
|
|
|
*/
|
2021-07-15 11:34:55 +02:00
|
|
|
export function _findMatchingRule(entityClient: EntityClient, mail: Mail, rules: InboxRule []): Promise<?InboxRule> {
|
|
|
|
|
return Promise.reduce(rules, (resultInboxRule, inboxRule) => {
|
2017-08-15 13:54:22 +02:00
|
|
|
|
|
|
|
|
if (resultInboxRule) {
|
|
|
|
|
//console.log("rule matches", resultInboxRule)
|
|
|
|
|
return resultInboxRule
|
|
|
|
|
}
|
2020-10-09 15:49:04 +02:00
|
|
|
// console.log("find matching rule", inboxRule.value)
|
2017-08-15 13:54:22 +02:00
|
|
|
let ruleType = inboxRule.type;
|
2020-10-09 15:49:04 +02:00
|
|
|
try {
|
|
|
|
|
if (ruleType === InboxRuleType.FROM_EQUALS) {
|
2021-07-15 11:34:55 +02:00
|
|
|
let mailAddresses = [mail.sender.address]
|
|
|
|
|
if (mail.differentEnvelopeSender) {
|
|
|
|
|
mailAddresses.push(mail.differentEnvelopeSender)
|
|
|
|
|
}
|
|
|
|
|
return _checkEmailAddresses(mailAddresses, inboxRule)
|
2020-10-09 15:49:04 +02:00
|
|
|
} else if (ruleType === InboxRuleType.RECIPIENT_TO_EQUALS) {
|
2021-07-15 11:34:55 +02:00
|
|
|
return _checkEmailAddresses(mail.toRecipients.map(m => m.address), inboxRule)
|
2020-10-09 15:49:04 +02:00
|
|
|
} else if (ruleType === InboxRuleType.RECIPIENT_CC_EQUALS) {
|
2021-07-15 11:34:55 +02:00
|
|
|
return _checkEmailAddresses(mail.ccRecipients.map(m => m.address), inboxRule)
|
2020-10-09 15:49:04 +02:00
|
|
|
} else if (ruleType === InboxRuleType.RECIPIENT_BCC_EQUALS) {
|
2021-07-15 11:34:55 +02:00
|
|
|
return _checkEmailAddresses(mail.bccRecipients.map(m => m.address), inboxRule)
|
2020-10-09 15:49:04 +02:00
|
|
|
} else if (ruleType === InboxRuleType.SUBJECT_CONTAINS) {
|
|
|
|
|
return _checkContainsRule(mail.subject, inboxRule)
|
|
|
|
|
} else if (ruleType === InboxRuleType.MAIL_HEADER_CONTAINS) {
|
|
|
|
|
if (mail.headers) {
|
2019-09-13 13:49:11 +02:00
|
|
|
return entityClient.load(MailHeadersTypeRef, mail.headers)
|
2020-10-09 15:49:04 +02:00
|
|
|
.then(mailHeaders => {
|
|
|
|
|
return _checkContainsRule(getMailHeaders(mailHeaders), inboxRule)
|
|
|
|
|
})
|
|
|
|
|
.catch(e => {
|
2021-06-21 17:47:54 +02:00
|
|
|
if (!(e instanceof NotFoundError)) {
|
|
|
|
|
// Does the outer catch already handle this case?
|
|
|
|
|
console.error("Error processing inbox rule:", e.message)
|
|
|
|
|
}
|
2020-10-09 15:49:04 +02:00
|
|
|
return null
|
|
|
|
|
})
|
|
|
|
|
}
|
2017-08-15 13:54:22 +02:00
|
|
|
}
|
2020-10-09 15:49:04 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
console.error("Error processing inbox rule:", e.message)
|
2017-08-15 13:54:22 +02:00
|
|
|
}
|
2020-10-09 15:49:04 +02:00
|
|
|
|
|
|
|
|
return null
|
2017-08-15 13:54:22 +02:00
|
|
|
}, null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function _checkContainsRule(value: string, inboxRule: InboxRule): ?InboxRule {
|
|
|
|
|
if (isRegularExpression(inboxRule.value) && _matchesRegularExpression(value, inboxRule)) {
|
|
|
|
|
return inboxRule
|
|
|
|
|
} else if (value.indexOf(inboxRule.value) >= 0) {
|
|
|
|
|
return inboxRule
|
|
|
|
|
} else {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-07-18 17:22:54 +02:00
|
|
|
|
2017-08-15 13:54:22 +02:00
|
|
|
/** export for test. */
|
|
|
|
|
export function _matchesRegularExpression(value: string, inboxRule: InboxRule): boolean {
|
|
|
|
|
if (isRegularExpression(inboxRule.value)) {
|
2020-10-15 03:59:30 -04:00
|
|
|
let flags = inboxRule.value.replace(/.*\/([gimsuy]*)$/, '$1');
|
|
|
|
|
let pattern = inboxRule.value.replace(new RegExp('^/(.*?)/' + flags + '$'), '$1');
|
|
|
|
|
let regExp = new RegExp(pattern, flags);
|
2017-08-15 13:54:22 +02:00
|
|
|
return regExp.test(value)
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-15 11:34:55 +02:00
|
|
|
function _checkEmailAddresses(mailAddresses: string[], inboxRule: InboxRule): ?InboxRule {
|
2017-08-15 13:54:22 +02:00
|
|
|
let mailAddress = mailAddresses.find(mailAddress => {
|
2021-07-15 11:34:55 +02:00
|
|
|
let cleanMailAddress = mailAddress.toLowerCase().trim();
|
2017-08-15 13:54:22 +02:00
|
|
|
if (isRegularExpression(inboxRule.value)) {
|
|
|
|
|
return _matchesRegularExpression(cleanMailAddress, inboxRule)
|
|
|
|
|
} else if (isDomainName(inboxRule.value)) {
|
|
|
|
|
let domain = cleanMailAddress.split("@")[1];
|
2018-07-18 17:22:54 +02:00
|
|
|
return domain === inboxRule.value
|
2017-08-15 13:54:22 +02:00
|
|
|
} else {
|
2018-07-18 17:22:54 +02:00
|
|
|
return cleanMailAddress === inboxRule.value
|
2017-08-15 13:54:22 +02:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
if (mailAddress) {
|
|
|
|
|
return inboxRule
|
|
|
|
|
} else {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
export function isInboxList(mailboxDetail: MailboxDetail, listId: Id): boolean {
|
2017-11-24 15:14:56 +01:00
|
|
|
return isSameId(listId, getInboxFolder(mailboxDetail.folders).mails)
|
2017-08-15 13:54:22 +02:00
|
|
|
}
|