2023-06-29 18:26:45 +02:00
|
|
|
import o from "@tutao/otest"
|
2024-07-01 17:56:41 +02:00
|
|
|
import { Notifications } from "../../../src/common/gui/Notifications.js"
|
2025-10-15 15:49:14 +02:00
|
|
|
import { mock, Spy, spy, verify } from "@tutao/tutanota-test-utils"
|
2024-08-07 08:38:58 +02:00
|
|
|
import { MailSetKind, OperationType } from "../../../src/common/api/common/TutanotaConstants.js"
|
2025-10-14 17:54:14 +02:00
|
|
|
import {
|
|
|
|
BodyTypeRef,
|
|
|
|
Mail,
|
2025-10-15 15:49:14 +02:00
|
|
|
MailDetails,
|
|
|
|
MailDetailsBlobTypeRef,
|
2025-10-14 17:54:14 +02:00
|
|
|
MailDetailsTypeRef,
|
|
|
|
MailFolderTypeRef,
|
|
|
|
MailSetEntryTypeRef,
|
|
|
|
MailTypeRef,
|
|
|
|
} from "../../../src/common/api/entities/tutanota/TypeRefs.js"
|
2024-07-01 17:56:41 +02:00
|
|
|
import { EntityClient } from "../../../src/common/api/common/EntityClient.js"
|
2022-12-27 15:37:40 +01:00
|
|
|
import { EntityRestClientMock } from "../api/worker/rest/EntityRestClientMock.js"
|
|
|
|
import { downcast } from "@tutao/tutanota-utils"
|
2024-07-01 17:56:41 +02:00
|
|
|
import { LoginController } from "../../../src/common/api/main/LoginController.js"
|
2024-08-20 18:03:03 +02:00
|
|
|
import { instance, matchers, object, when } from "testdouble"
|
2024-07-01 17:56:41 +02:00
|
|
|
import { UserController } from "../../../src/common/api/main/UserController.js"
|
2023-11-10 16:59:39 +01:00
|
|
|
import { createTestEntity } from "../TestUtils.js"
|
2025-07-04 13:37:36 +02:00
|
|
|
import { EntityUpdateData, PrefetchStatus } from "../../../src/common/api/common/utils/EntityUpdateUtils.js"
|
2025-10-15 15:49:14 +02:00
|
|
|
import { MailboxDetail, MailboxModel } from "../../../src/common/mailFunctionality/MailboxModel.js"
|
2024-08-07 08:38:58 +02:00
|
|
|
import { getElementId, getListId } from "../../../src/common/api/common/utils/EntityUtils.js"
|
2024-08-20 18:03:03 +02:00
|
|
|
import { MailModel } from "../../../src/mail-app/mail/model/MailModel.js"
|
|
|
|
import { EventController } from "../../../src/common/api/main/EventController.js"
|
|
|
|
import { MailFacade } from "../../../src/common/api/worker/facades/lazy/MailFacade.js"
|
2025-05-16 16:03:24 +02:00
|
|
|
import { ClientModelInfo } from "../../../src/common/api/common/EntityFunctions"
|
2025-10-14 15:53:36 +02:00
|
|
|
import { InboxRuleHandler } from "../../../src/mail-app/mail/model/InboxRuleHandler"
|
|
|
|
import { SpamClassificationHandler } from "../../../src/mail-app/mail/model/SpamClassificationHandler"
|
|
|
|
import { SpamClassifier } from "../../../src/mail-app/workerUtils/spamClassification/SpamClassifier"
|
2025-10-14 18:23:57 +02:00
|
|
|
import { WebsocketConnectivityModel } from "../../../src/common/misc/WebsocketConnectivityModel"
|
2025-10-15 15:49:14 +02:00
|
|
|
import { BulkMailLoader } from "../../../src/mail-app/workerUtils/index/BulkMailLoader"
|
|
|
|
import { FolderSystem } from "../../../src/common/api/common/mail/FolderSystem"
|
2025-10-14 15:53:36 +02:00
|
|
|
|
|
|
|
const { anything } = matchers
|
2018-10-22 12:02:13 +02:00
|
|
|
|
|
|
|
o.spec("MailModelTest", function () {
|
2021-12-23 14:03:23 +01:00
|
|
|
let notifications: Partial<Notifications>
|
2018-10-22 12:02:13 +02:00
|
|
|
let showSpy: Spy
|
2024-08-20 18:03:03 +02:00
|
|
|
let model: MailModel
|
2025-10-15 15:49:14 +02:00
|
|
|
const inboxFolder = createTestEntity(MailFolderTypeRef, { _id: ["folderListId", "inboxId"], folderType: MailSetKind.INBOX })
|
|
|
|
const spamFolder = createTestEntity(MailFolderTypeRef, { _id: ["folderListId", "spamId"], folderType: MailSetKind.SPAM })
|
|
|
|
const anotherFolder = createTestEntity(MailFolderTypeRef, { _id: ["folderListId", "archiveId"], folderType: MailSetKind.ARCHIVE })
|
2022-11-08 17:06:42 +01:00
|
|
|
let logins: LoginController
|
2025-02-10 15:59:28 +01:00
|
|
|
let mailFacade: MailFacade
|
2025-10-14 18:23:57 +02:00
|
|
|
let connectivityModel: WebsocketConnectivityModel
|
2024-08-07 08:38:58 +02:00
|
|
|
const restClient: EntityRestClientMock = new EntityRestClientMock()
|
2023-05-17 16:49:56 +02:00
|
|
|
|
2018-10-22 12:02:13 +02:00
|
|
|
o.beforeEach(function () {
|
|
|
|
notifications = {}
|
2024-08-20 18:03:03 +02:00
|
|
|
const mailboxModel = instance(MailboxModel)
|
|
|
|
const eventController = instance(EventController)
|
2025-02-10 15:59:28 +01:00
|
|
|
mailFacade = instance(MailFacade)
|
2018-10-22 12:02:13 +02:00
|
|
|
showSpy = notifications.showNotification = spy()
|
2022-11-08 17:06:42 +01:00
|
|
|
logins = object()
|
2023-08-31 16:31:00 +02:00
|
|
|
let userController = object<UserController>()
|
|
|
|
when(userController.isUpdateForLoggedInUserInstance(matchers.anything(), matchers.anything())).thenReturn(false)
|
|
|
|
when(logins.getUserController()).thenReturn(userController)
|
|
|
|
|
2025-10-14 18:23:57 +02:00
|
|
|
connectivityModel = object<WebsocketConnectivityModel>()
|
|
|
|
when(connectivityModel.isLeader()).thenReturn(true)
|
|
|
|
|
2025-05-16 16:03:24 +02:00
|
|
|
model = new MailModel(
|
|
|
|
downcast({}),
|
|
|
|
mailboxModel,
|
|
|
|
eventController,
|
|
|
|
new EntityClient(restClient, ClientModelInfo.getNewInstanceForTestsOnly()),
|
|
|
|
logins,
|
|
|
|
mailFacade,
|
2025-10-14 18:23:57 +02:00
|
|
|
connectivityModel,
|
2025-10-14 15:53:36 +02:00
|
|
|
() => object(),
|
2025-06-10 18:17:24 +02:00
|
|
|
() => null,
|
2025-05-16 16:03:24 +02:00
|
|
|
)
|
2018-10-22 12:02:13 +02:00
|
|
|
})
|
2025-10-14 15:53:36 +02:00
|
|
|
|
2020-09-16 14:36:28 +02:00
|
|
|
o("doesn't send notification for another folder", async function () {
|
2025-02-06 10:42:22 +01:00
|
|
|
const mailSetEntry = createTestEntity(MailSetEntryTypeRef, { _id: [anotherFolder.entries, "mailSetEntryId"] })
|
|
|
|
restClient.addListInstances(mailSetEntry)
|
2024-08-20 18:03:03 +02:00
|
|
|
await model.entityEventsReceived([
|
|
|
|
makeUpdate({
|
2025-08-08 13:46:06 +02:00
|
|
|
instanceListId: getListId(mailSetEntry) as NonEmptyString,
|
2025-02-06 10:42:22 +01:00
|
|
|
instanceId: getElementId(mailSetEntry),
|
2024-08-20 18:03:03 +02:00
|
|
|
operation: OperationType.CREATE,
|
|
|
|
}),
|
|
|
|
])
|
2018-10-22 12:02:13 +02:00
|
|
|
o(showSpy.invocations.length).equals(0)
|
|
|
|
})
|
2020-09-16 14:36:28 +02:00
|
|
|
o("doesn't send notification for move operation", async function () {
|
2025-02-06 10:42:22 +01:00
|
|
|
const mailSetEntry = createTestEntity(MailSetEntryTypeRef, { _id: [inboxFolder.entries, "mailSetEntryId"] })
|
|
|
|
restClient.addListInstances(mailSetEntry)
|
2024-08-20 18:03:03 +02:00
|
|
|
await model.entityEventsReceived([
|
|
|
|
makeUpdate({
|
2025-08-08 13:46:06 +02:00
|
|
|
instanceListId: getListId(mailSetEntry) as NonEmptyString,
|
2025-02-06 10:42:22 +01:00
|
|
|
instanceId: getElementId(mailSetEntry),
|
2024-08-20 18:03:03 +02:00
|
|
|
operation: OperationType.DELETE,
|
|
|
|
}),
|
|
|
|
makeUpdate({
|
2025-08-08 13:46:06 +02:00
|
|
|
instanceListId: getListId(mailSetEntry) as NonEmptyString,
|
2025-02-06 10:42:22 +01:00
|
|
|
instanceId: getElementId(mailSetEntry),
|
2024-08-20 18:03:03 +02:00
|
|
|
operation: OperationType.CREATE,
|
|
|
|
}),
|
|
|
|
])
|
2018-10-22 12:02:13 +02:00
|
|
|
o(showSpy.invocations.length).equals(0)
|
|
|
|
})
|
2025-02-10 15:59:28 +01:00
|
|
|
o("markMails", async function () {
|
2025-02-10 16:43:42 +01:00
|
|
|
const mailId1: IdTuple = ["mailbag id1", "mail id1"]
|
|
|
|
const mailId2: IdTuple = ["mailbag id2", "mail id2"]
|
|
|
|
const mailId3: IdTuple = ["mailbag id3", "mail id3"]
|
|
|
|
await model.markMails([mailId1, mailId2, mailId3], true)
|
|
|
|
verify(mailFacade.markMails([mailId1, mailId2, mailId3], true))
|
2025-02-10 15:59:28 +01:00
|
|
|
})
|
|
|
|
|
2025-10-14 15:53:36 +02:00
|
|
|
o.spec("Inbox rule and spam prediction", () => {
|
|
|
|
let inboxRuleHandler: InboxRuleHandler
|
|
|
|
let spamClassificationHandler: SpamClassificationHandler
|
|
|
|
let spamClassifier: SpamClassifier
|
|
|
|
let mailboxModel: MailboxModel
|
2025-10-15 15:49:14 +02:00
|
|
|
let modelWithSpamAndInboxRule: MailModel
|
|
|
|
let mail: Mail
|
|
|
|
let mailDetails: MailDetails
|
2025-10-14 15:53:36 +02:00
|
|
|
|
|
|
|
o.beforeEach(async () => {
|
|
|
|
const entityClient = new EntityClient(restClient, ClientModelInfo.getNewInstanceForTestsOnly())
|
2025-10-15 15:49:14 +02:00
|
|
|
const bulkMailLoader = new BulkMailLoader(entityClient, entityClient, mailFacade)
|
2025-10-14 15:53:36 +02:00
|
|
|
mailboxModel = instance(MailboxModel)
|
|
|
|
inboxRuleHandler = object<InboxRuleHandler>()
|
|
|
|
spamClassifier = object<SpamClassifier>()
|
2025-10-15 15:49:14 +02:00
|
|
|
spamClassificationHandler = new SpamClassificationHandler(mailFacade, spamClassifier, entityClient, bulkMailLoader, connectivityModel)
|
|
|
|
|
|
|
|
mailDetails = createTestEntity(MailDetailsTypeRef, {
|
|
|
|
_id: "mailDetail",
|
|
|
|
body: createTestEntity(BodyTypeRef, { text: "some text" }),
|
|
|
|
})
|
|
|
|
mail = createTestEntity(MailTypeRef, {
|
|
|
|
_id: ["mailListId", "mailId"],
|
|
|
|
_ownerGroup: "mailGroup",
|
|
|
|
mailDetails: ["detailsList", mailDetails._id],
|
|
|
|
sets: [inboxFolder._id],
|
|
|
|
})
|
|
|
|
const mailDetailsBlob = createTestEntity(MailDetailsBlobTypeRef, { _id: mail.mailDetails!, details: mailDetails })
|
|
|
|
restClient.addListInstances(mail)
|
|
|
|
restClient.addBlobInstances(mailDetailsBlob)
|
|
|
|
|
|
|
|
modelWithSpamAndInboxRule = mock(
|
|
|
|
new MailModel(
|
|
|
|
downcast({}),
|
|
|
|
mailboxModel,
|
|
|
|
instance(EventController),
|
|
|
|
entityClient,
|
|
|
|
logins,
|
|
|
|
mailFacade,
|
|
|
|
connectivityModel,
|
|
|
|
() => spamClassificationHandler,
|
|
|
|
() => inboxRuleHandler,
|
|
|
|
),
|
|
|
|
(m: MailModel) => {
|
|
|
|
m.getFolderSystemByGroupId = (groupId) => {
|
|
|
|
o(groupId).equals("mailGroup")
|
|
|
|
return new FolderSystem([inboxFolder, spamFolder, anotherFolder])
|
|
|
|
}
|
|
|
|
|
|
|
|
m.getMailboxDetailsForMail = async (_: Mail) => object<MailboxDetail>()
|
|
|
|
},
|
2025-10-14 15:53:36 +02:00
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
o("does not try to apply inbox rule when downloading of mail fails on create mail event", async function () {
|
2025-10-15 15:49:14 +02:00
|
|
|
restClient.setListElementException(mail._id, new Error("Mail not found"))
|
2025-10-14 15:53:36 +02:00
|
|
|
|
|
|
|
const mailCreateEvent = makeUpdate({ instanceListId: "mailListId", instanceId: "mailId", operation: OperationType.CREATE })
|
2025-10-15 15:49:14 +02:00
|
|
|
await modelWithSpamAndInboxRule.entityEventsReceived([mailCreateEvent])
|
2025-10-14 15:53:36 +02:00
|
|
|
|
2025-10-14 16:35:33 +02:00
|
|
|
verify(inboxRuleHandler.findAndApplyMatchingRule(anything(), anything(), anything()), { times: 0 })
|
2025-10-14 15:53:36 +02:00
|
|
|
})
|
|
|
|
|
2025-10-15 15:49:14 +02:00
|
|
|
o("spam prediction depends on result of inbox rule", async () => {
|
|
|
|
when(spamClassifier.predict(anything())).thenResolve(null)
|
2025-10-14 15:53:36 +02:00
|
|
|
|
|
|
|
const mailCreateEvent = makeUpdate({ instanceListId: "mailListId", instanceId: "mailId", operation: OperationType.CREATE })
|
|
|
|
|
2025-10-15 15:49:14 +02:00
|
|
|
// when inbox rule is applied
|
|
|
|
if (false) {
|
|
|
|
when(inboxRuleHandler.findAndApplyMatchingRule(anything(), anything(), anything())).thenResolve(inboxFolder)
|
|
|
|
await modelWithSpamAndInboxRule.entityEventsReceived([mailCreateEvent])
|
|
|
|
// verify(spamClassifier.predict(anything()), { times: 0 })
|
|
|
|
}
|
|
|
|
|
|
|
|
// when inbox rule is not applied
|
|
|
|
{
|
|
|
|
when(inboxRuleHandler.findAndApplyMatchingRule(anything(), anything(), anything())).thenResolve(null)
|
|
|
|
await modelWithSpamAndInboxRule.entityEventsReceived([mailCreateEvent])
|
|
|
|
verify(spamClassifier.predict(anything()), { times: 1 })
|
|
|
|
}
|
|
|
|
|
|
|
|
// When inbox rule throws an error
|
|
|
|
if (false) {
|
|
|
|
when(inboxRuleHandler.findAndApplyMatchingRule(anything(), anything(), anything())).thenReject(new Error("Some error for inbox rule"))
|
|
|
|
await modelWithSpamAndInboxRule.entityEventsReceived([mailCreateEvent])
|
|
|
|
verify(spamClassifier.predict(anything()), { times: 1 })
|
|
|
|
}
|
2025-10-14 15:53:36 +02:00
|
|
|
})
|
|
|
|
|
2025-10-15 15:49:14 +02:00
|
|
|
o("does not try to do spam classification when downloading of mail fails on create mail event", async function () {
|
|
|
|
when(inboxRuleHandler.findAndApplyMatchingRule(anything(), anything(), anything())).thenResolve(null)
|
2025-10-14 15:53:36 +02:00
|
|
|
const mailCreateEvent = makeUpdate({ instanceListId: "mailListId", instanceId: "mailId", operation: OperationType.CREATE })
|
2025-10-15 15:49:14 +02:00
|
|
|
await modelWithSpamAndInboxRule.entityEventsReceived([mailCreateEvent])
|
2025-10-14 15:53:36 +02:00
|
|
|
|
2025-10-15 15:49:14 +02:00
|
|
|
// mail not being there
|
|
|
|
restClient.setListElementException(mail._id, new Error("Mail not found"))
|
2025-10-14 15:53:36 +02:00
|
|
|
verify(spamClassifier.predict(anything()), { times: 0 })
|
|
|
|
|
2025-10-15 15:49:14 +02:00
|
|
|
// mail being there
|
|
|
|
restClient.addListInstances(mail)
|
2025-10-14 15:53:36 +02:00
|
|
|
verify(spamClassifier.predict(anything()), { times: 1 })
|
|
|
|
})
|
|
|
|
|
2025-10-15 15:49:14 +02:00
|
|
|
o("no spam prediction for draft mails", async () => {
|
|
|
|
mail.mailDetails = null
|
|
|
|
mail.mailDetailsDraft = ["draftListId", "draftId"]
|
|
|
|
restClient.addListInstances(mail)
|
2025-10-14 15:53:36 +02:00
|
|
|
|
2025-10-15 15:49:14 +02:00
|
|
|
when(inboxRuleHandler.findAndApplyMatchingRule(anything(), mail, anything())).thenResolve(inboxFolder)
|
2025-10-14 15:53:36 +02:00
|
|
|
|
|
|
|
const mailCreateEvent = makeUpdate({ instanceListId: "mailListId", instanceId: "mailId", operation: OperationType.CREATE })
|
2025-10-15 15:49:14 +02:00
|
|
|
await modelWithSpamAndInboxRule.entityEventsReceived([mailCreateEvent])
|
2025-10-14 15:53:36 +02:00
|
|
|
|
|
|
|
verify(spamClassificationHandler.predictSpamForNewMail(anything(), mail, anything()), { times: 1 })
|
|
|
|
verify(spamClassifier.predict(anything()), { times: 0 })
|
|
|
|
})
|
2025-10-14 18:11:25 +02:00
|
|
|
|
|
|
|
o("deletes a training datum for deleted mail event", async () => {
|
2025-10-15 15:49:14 +02:00
|
|
|
const mailDeleteEvent = makeUpdate({ instanceListId: "mailListId", instanceId: "mailId", operation: OperationType.DELETE })
|
|
|
|
await modelWithSpamAndInboxRule.entityEventsReceived([mailDeleteEvent])
|
|
|
|
|
|
|
|
verify(spamClassifier.deleteSpamClassification("mailGroup", mail._id), { times: 1 })
|
|
|
|
})
|
|
|
|
|
|
|
|
o("do spam processing if inbox rule handling failed", async () => {
|
|
|
|
const mailDetails = createTestEntity(MailDetailsTypeRef, {
|
|
|
|
_id: "mailDetail",
|
|
|
|
body: createTestEntity(BodyTypeRef, { text: "some text" }),
|
|
|
|
})
|
2025-10-14 18:11:25 +02:00
|
|
|
const mail = createTestEntity(MailTypeRef, {
|
|
|
|
_id: ["mailListId", "mailId"],
|
2025-10-15 15:49:14 +02:00
|
|
|
_ownerGroup: "mailGroup",
|
|
|
|
mailDetails: ["detailsList", mailDetails._id],
|
2025-10-14 18:11:25 +02:00
|
|
|
})
|
2025-10-15 15:49:14 +02:00
|
|
|
restClient.addListInstances(mail)
|
|
|
|
restClient.addElementInstances(mailDetails)
|
|
|
|
when(inboxRuleHandler.findAndApplyMatchingRule(anything(), anything(), anything())).thenReject(new Error("Some failure of inbox rule"))
|
2025-10-14 18:11:25 +02:00
|
|
|
|
2025-10-15 15:49:14 +02:00
|
|
|
const mailCreateEvent = makeUpdate({ instanceListId: "mailListId", instanceId: "mailId", operation: OperationType.CREATE })
|
|
|
|
await modelWithSpamAndInboxRule.entityEventsReceived([mailCreateEvent])
|
2025-10-14 18:11:25 +02:00
|
|
|
|
2025-10-15 15:49:14 +02:00
|
|
|
verify(spamClassifier.predict(anything()), { times: 1 })
|
2025-10-14 18:11:25 +02:00
|
|
|
})
|
2025-10-14 15:53:36 +02:00
|
|
|
})
|
|
|
|
|
2025-08-08 13:46:06 +02:00
|
|
|
function makeUpdate({
|
|
|
|
instanceId,
|
|
|
|
instanceListId,
|
|
|
|
operation,
|
|
|
|
}: {
|
|
|
|
instanceListId: NonEmptyString
|
|
|
|
instanceId: Id
|
|
|
|
operation: OperationType
|
|
|
|
}): EntityUpdateData<Mail> {
|
2025-05-16 16:03:24 +02:00
|
|
|
return {
|
|
|
|
typeRef: MailTypeRef,
|
|
|
|
operation,
|
|
|
|
instanceListId,
|
|
|
|
instanceId,
|
2025-06-13 17:27:15 +02:00
|
|
|
instance: null,
|
|
|
|
patches: null,
|
2025-07-04 13:37:36 +02:00
|
|
|
prefetchStatus: PrefetchStatus.NotPrefetched,
|
2025-05-16 16:03:24 +02:00
|
|
|
}
|
2018-10-22 12:02:13 +02:00
|
|
|
}
|
2022-12-27 15:37:40 +01:00
|
|
|
})
|