2025-10-14 12:11:22 +02:00
|
|
|
import o from "@tutao/otest"
|
2025-11-03 18:01:36 +01:00
|
|
|
import { matchers, object, when } from "testdouble"
|
2025-10-14 12:11:22 +02:00
|
|
|
import {
|
|
|
|
|
Body,
|
|
|
|
|
BodyTypeRef,
|
|
|
|
|
ClientSpamClassifierResultTypeRef,
|
|
|
|
|
Mail,
|
|
|
|
|
MailDetails,
|
|
|
|
|
MailDetailsTypeRef,
|
|
|
|
|
MailFolderTypeRef,
|
|
|
|
|
MailTypeRef,
|
|
|
|
|
} from "../../../src/common/api/entities/tutanota/TypeRefs"
|
2025-11-03 18:01:36 +01:00
|
|
|
import { SpamClassifier } from "../../../src/mail-app/workerUtils/spamClassification/SpamClassifier"
|
2025-10-14 12:11:22 +02:00
|
|
|
import { MailSetKind, ProcessingState, SpamDecision } from "../../../src/common/api/common/TutanotaConstants"
|
|
|
|
|
import { ClientClassifierType } from "../../../src/common/api/common/ClientClassifierType"
|
|
|
|
|
import { assert, assertNotNull } from "@tutao/tutanota-utils"
|
|
|
|
|
import { MailFacade } from "../../../src/common/api/worker/facades/lazy/MailFacade"
|
|
|
|
|
import { createTestEntity } from "../TestUtils"
|
2025-10-24 09:57:44 +02:00
|
|
|
import { SpamClassificationHandler } from "../../../src/mail-app/mail/model/SpamClassificationHandler"
|
2025-10-14 12:11:22 +02:00
|
|
|
import { FolderSystem } from "../../../src/common/api/common/mail/FolderSystem"
|
|
|
|
|
import { isSameId } from "../../../src/common/api/common/utils/EntityUtils"
|
2025-11-03 18:01:36 +01:00
|
|
|
import { UnencryptedProcessInboxDatum } from "../../../src/mail-app/mail/model/ProcessInboxHandler"
|
|
|
|
|
import { createSpamMailDatum, SpamMailProcessor } from "../../../src/common/api/common/utils/spamClassificationUtils/SpamMailProcessor"
|
2025-10-14 12:11:22 +02:00
|
|
|
|
|
|
|
|
const { anything } = matchers
|
|
|
|
|
|
|
|
|
|
o.spec("SpamClassificationHandlerTest", function () {
|
|
|
|
|
let mailFacade = object<MailFacade>()
|
|
|
|
|
let body: Body
|
|
|
|
|
let mail: Mail
|
|
|
|
|
let spamClassifier: SpamClassifier
|
|
|
|
|
let spamHandler: SpamClassificationHandler
|
2025-11-03 18:01:36 +01:00
|
|
|
let spamMailProcessor: SpamMailProcessor = new SpamMailProcessor()
|
2025-10-14 12:11:22 +02:00
|
|
|
let folderSystem: FolderSystem
|
|
|
|
|
let mailDetails: MailDetails
|
|
|
|
|
|
|
|
|
|
const inboxFolder = createTestEntity(MailFolderTypeRef, { _id: ["listId", "inbox"], folderType: MailSetKind.INBOX })
|
|
|
|
|
const trashFolder = createTestEntity(MailFolderTypeRef, { _id: ["listId", "trash"], folderType: MailSetKind.TRASH })
|
|
|
|
|
const spamFolder = createTestEntity(MailFolderTypeRef, { _id: ["listId", "spam"], folderType: MailSetKind.SPAM })
|
|
|
|
|
|
2025-11-03 18:01:36 +01:00
|
|
|
o.beforeEach(async function () {
|
2025-10-14 12:11:22 +02:00
|
|
|
spamClassifier = object<SpamClassifier>()
|
|
|
|
|
|
|
|
|
|
body = createTestEntity(BodyTypeRef, { text: "Body Text" })
|
|
|
|
|
mailDetails = createTestEntity(MailDetailsTypeRef, { _id: "mailDetail", body })
|
|
|
|
|
mail = createTestEntity(MailTypeRef, {
|
|
|
|
|
_id: ["listId", "elementId"],
|
|
|
|
|
sets: [spamFolder._id],
|
|
|
|
|
subject: "subject",
|
|
|
|
|
_ownerGroup: "owner",
|
|
|
|
|
mailDetails: ["detailsList", mailDetails._id],
|
|
|
|
|
unread: true,
|
|
|
|
|
processingState: ProcessingState.INBOX_RULE_NOT_PROCESSED,
|
|
|
|
|
clientSpamClassifierResult: createTestEntity(ClientSpamClassifierResultTypeRef, { spamDecision: SpamDecision.NONE }),
|
|
|
|
|
})
|
|
|
|
|
folderSystem = object<FolderSystem>()
|
|
|
|
|
|
2025-11-03 18:01:36 +01:00
|
|
|
when(mailFacade.moveMails(anything(), anything(), anything())).thenResolve([])
|
2025-10-14 12:11:22 +02:00
|
|
|
when(folderSystem.getSystemFolderByType(MailSetKind.SPAM)).thenReturn(spamFolder)
|
|
|
|
|
when(folderSystem.getSystemFolderByType(MailSetKind.INBOX)).thenReturn(inboxFolder)
|
|
|
|
|
when(folderSystem.getSystemFolderByType(MailSetKind.TRASH)).thenReturn(trashFolder)
|
|
|
|
|
when(folderSystem.getFolderByMail(anything())).thenDo((mail: Mail) => {
|
|
|
|
|
assert(mail.sets.length === 1, "Expected exactly one mail set")
|
|
|
|
|
const mailFolderId = assertNotNull(mail.sets[0])
|
|
|
|
|
if (isSameId(mailFolderId, trashFolder._id)) return trashFolder
|
|
|
|
|
else if (isSameId(mailFolderId, spamFolder._id)) return spamFolder
|
|
|
|
|
else if (isSameId(mailFolderId, inboxFolder._id)) return inboxFolder
|
|
|
|
|
else throw new Error("Unknown mail Folder")
|
|
|
|
|
})
|
|
|
|
|
when(
|
|
|
|
|
mailFacade.loadMailDetailsBlob(
|
|
|
|
|
matchers.argThat((requestedMails: Array<Mail>) => {
|
|
|
|
|
assert(requestedMails.length === 1, "exactly one mail is requested at a time")
|
|
|
|
|
return isSameId(requestedMails[0]._id, mail._id)
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
anything(),
|
|
|
|
|
).thenDo(async () => [{ mail, mailDetails }])
|
2025-11-03 18:01:36 +01:00
|
|
|
when(spamClassifier.vectorizeAndCompress(createSpamMailDatum(mail, mailDetails))).thenResolve(
|
|
|
|
|
await spamMailProcessor.vectorizeAndCompress(createSpamMailDatum(mail, mailDetails)),
|
|
|
|
|
)
|
|
|
|
|
spamHandler = new SpamClassificationHandler(spamClassifier)
|
2025-10-14 12:11:22 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
o("predictSpamForNewMail does move mail from inbox to spam folder if mail is spam", async function () {
|
|
|
|
|
mail.sets = [inboxFolder._id]
|
2025-11-03 18:01:36 +01:00
|
|
|
when(spamClassifier.predict(anything(), anything())).thenResolve(true)
|
2025-10-14 12:11:22 +02:00
|
|
|
|
|
|
|
|
const finalResult = await spamHandler.predictSpamForNewMail(mail, mailDetails, inboxFolder, folderSystem)
|
2025-11-03 18:01:36 +01:00
|
|
|
|
|
|
|
|
const expectedProcessInboxDatum: UnencryptedProcessInboxDatum = {
|
|
|
|
|
mailId: mail._id,
|
|
|
|
|
targetMoveFolder: spamFolder._id,
|
|
|
|
|
classifierType: ClientClassifierType.CLIENT_CLASSIFICATION,
|
|
|
|
|
vector: await spamMailProcessor.vectorizeAndCompress(createSpamMailDatum(mail, mailDetails)),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
o(finalResult.targetFolder).deepEquals(spamFolder)
|
|
|
|
|
o(finalResult.processInboxDatum).deepEquals(expectedProcessInboxDatum)
|
2025-10-14 12:11:22 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
o("predictSpamForNewMail does NOT move mail from inbox to spam folder if mail is ham", async function () {
|
|
|
|
|
mail.sets = [inboxFolder._id]
|
2025-11-03 18:01:36 +01:00
|
|
|
when(spamClassifier.predict(anything(), anything())).thenResolve(false)
|
2025-10-14 12:11:22 +02:00
|
|
|
|
|
|
|
|
const finalResult = await spamHandler.predictSpamForNewMail(mail, mailDetails, inboxFolder, folderSystem)
|
2025-11-03 18:01:36 +01:00
|
|
|
|
|
|
|
|
const expectedProcessInboxDatum: UnencryptedProcessInboxDatum = {
|
|
|
|
|
mailId: mail._id,
|
|
|
|
|
targetMoveFolder: inboxFolder._id,
|
|
|
|
|
classifierType: null,
|
|
|
|
|
vector: await spamMailProcessor.vectorizeAndCompress(createSpamMailDatum(mail, mailDetails)),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
o(finalResult.targetFolder).deepEquals(inboxFolder)
|
|
|
|
|
o(finalResult.processInboxDatum).deepEquals(expectedProcessInboxDatum)
|
2025-10-14 12:11:22 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
o("predictSpamForNewMail does NOT move mail from spam to inbox folder if mail is spam", async function () {
|
|
|
|
|
mail.sets = [spamFolder._id]
|
2025-11-03 18:01:36 +01:00
|
|
|
when(spamClassifier.predict(anything(), anything())).thenResolve(true)
|
2025-10-14 12:11:22 +02:00
|
|
|
|
|
|
|
|
const finalResult = await spamHandler.predictSpamForNewMail(mail, mailDetails, spamFolder, folderSystem)
|
|
|
|
|
|
2025-11-03 18:01:36 +01:00
|
|
|
const expectedProcessInboxDatum: UnencryptedProcessInboxDatum = {
|
|
|
|
|
mailId: mail._id,
|
|
|
|
|
targetMoveFolder: spamFolder._id,
|
|
|
|
|
classifierType: null,
|
|
|
|
|
vector: await spamMailProcessor.vectorizeAndCompress(createSpamMailDatum(mail, mailDetails)),
|
|
|
|
|
}
|
2025-10-14 12:11:22 +02:00
|
|
|
|
2025-11-03 18:01:36 +01:00
|
|
|
o(finalResult.targetFolder).deepEquals(spamFolder)
|
|
|
|
|
o(finalResult.processInboxDatum).deepEquals(expectedProcessInboxDatum)
|
2025-10-14 12:11:22 +02:00
|
|
|
})
|
|
|
|
|
|
2025-11-03 18:01:36 +01:00
|
|
|
o("predictSpamForNewMail moves mail from spam to inbox folder if mail is ham", async function () {
|
2025-10-14 12:11:22 +02:00
|
|
|
mail.sets = [spamFolder._id]
|
2025-11-03 18:01:36 +01:00
|
|
|
when(spamClassifier.predict(anything(), anything())).thenResolve(false)
|
2025-10-14 12:11:22 +02:00
|
|
|
|
|
|
|
|
const finalResult = await spamHandler.predictSpamForNewMail(mail, mailDetails, spamFolder, folderSystem)
|
|
|
|
|
|
2025-11-03 18:01:36 +01:00
|
|
|
const expectedProcessInboxDatum: UnencryptedProcessInboxDatum = {
|
|
|
|
|
mailId: mail._id,
|
|
|
|
|
targetMoveFolder: inboxFolder._id,
|
|
|
|
|
classifierType: ClientClassifierType.CLIENT_CLASSIFICATION,
|
|
|
|
|
vector: await spamMailProcessor.vectorizeAndCompress(createSpamMailDatum(mail, mailDetails)),
|
|
|
|
|
}
|
2025-10-14 12:11:22 +02:00
|
|
|
|
2025-11-03 18:01:36 +01:00
|
|
|
o(finalResult.targetFolder).deepEquals(inboxFolder)
|
|
|
|
|
o(finalResult.processInboxDatum).deepEquals(expectedProcessInboxDatum)
|
2025-10-14 12:11:22 +02:00
|
|
|
})
|
|
|
|
|
})
|