Skip to content

Commit

Permalink
Use UnreadMailStateService for marking mails as read/unread
Browse files Browse the repository at this point in the history
This commit moves the service call to MailFacade and adds tests, too.

Closes #8507
  • Loading branch information
paw-hub committed Feb 10, 2025
1 parent 9b03405 commit b1a9f79
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 18 deletions.
24 changes: 24 additions & 0 deletions src/common/api/worker/facades/lazy/MailFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MoveMailService,
ReportMailService,
SendDraftService,
UnreadMailStateService,
} from "../../../entities/tutanota/Services.js"
import {
ArchiveDataType,
Expand All @@ -23,6 +24,7 @@ import {
MailAuthenticationStatus,
MailMethod,
MailReportType,
MAX_NBR_MOVE_DELETE_MAIL_SERVICE,
OperationType,
PhishingMarkerStatus,
PublicKeyIdentifierType,
Expand Down Expand Up @@ -53,6 +55,7 @@ import {
createReportMailPostData,
createSecureExternalRecipientKeyData,
createSendDraftData,
createUnreadMailStatePostIn,
createUpdateMailFolderData,
DraftAttachment,
DraftRecipient,
Expand Down Expand Up @@ -100,6 +103,7 @@ import {
ofClass,
promiseFilter,
promiseMap,
splitInChunks,
} from "@tutao/tutanota-utils"
import { BlobFacade } from "./BlobFacade.js"
import { assertWorkerOrNode, isApp, isDesktop } from "../../../common/Env.js"
Expand Down Expand Up @@ -1091,6 +1095,26 @@ export class MailFacade {
})
await this.serviceExecutor.post(ApplyLabelService, postIn)
}

/**
* Mark the given mails as read/unread
* @param mails mail ids to mark as unread
* @param unread new unread status (mails that are already this status will not be modified)
*/
async markMails(mails: readonly IdTuple[], unread: boolean) {
await promiseMap(
splitInChunks(MAX_NBR_MOVE_DELETE_MAIL_SERVICE, mails),
async (mails) =>
this.serviceExecutor.post(
UnreadMailStateService,
createUnreadMailStatePostIn({
unread,
mails,
}),
),
{ concurrency: 5 },
)
}
}

export function phishingMarkerValue(type: ReportedMailFieldType, value: string): string {
Expand Down
15 changes: 4 additions & 11 deletions src/mail-app/mail/model/MailModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
noOp,
ofClass,
partition,
promiseMap,
splitInChunks,
} from "@tutao/tutanota-utils"
import {
Expand Down Expand Up @@ -41,7 +40,7 @@ import { WebsocketCounterData } from "../../../common/api/entities/sys/TypeRefs.
import { Notifications, NotificationType } from "../../../common/gui/Notifications.js"
import { lang } from "../../../common/misc/LanguageViewModel.js"
import { ProgrammingError } from "../../../common/api/common/error/ProgrammingError.js"
import { LockedError, NotFoundError, PreconditionFailedError } from "../../../common/api/common/error/RestError.js"
import { NotFoundError, PreconditionFailedError } from "../../../common/api/common/error/RestError.js"
import { UserError } from "../../../common/api/main/UserError.js"
import { EventController } from "../../../common/api/main/EventController.js"
import { InboxRuleHandler } from "./InboxRuleHandler.js"
Expand Down Expand Up @@ -433,15 +432,9 @@ export class MailModel {
}

async markMails(mails: readonly Mail[], unread: boolean): Promise<void> {
await promiseMap(
mails,
async (mail) => {
if (mail.unread !== unread) {
mail.unread = unread
return this.entityClient.update(mail).catch(ofClass(NotFoundError, noOp)).catch(ofClass(LockedError, noOp))
}
},
{ concurrency: 5 },
await this.mailFacade.markMails(
mails.map(({ _id }) => _id),
unread,
)
}

Expand Down
83 changes: 79 additions & 4 deletions test/tests/api/worker/facades/MailFacadeTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ import {
SendDraftDataTypeRef,
SymEncInternalRecipientKeyDataTypeRef,
} from "../../../../../src/common/api/entities/tutanota/TypeRefs.js"
import { CryptoProtocolVersion, MailAuthenticationStatus, ReportedMailFieldType } from "../../../../../src/common/api/common/TutanotaConstants.js"
import { object } from "testdouble"
import {
CryptoProtocolVersion,
MailAuthenticationStatus,
MAX_NBR_MOVE_DELETE_MAIL_SERVICE,
ReportedMailFieldType,
} from "../../../../../src/common/api/common/TutanotaConstants.js"
import { matchers, object } from "testdouble"
import { CryptoFacade } from "../../../../../src/common/api/worker/crypto/CryptoFacade.js"
import { IServiceExecutor } from "../../../../../src/common/api/common/ServiceRequest.js"
import { EntityClient } from "../../../../../src/common/api/common/EntityClient.js"
Expand All @@ -23,6 +28,8 @@ import { downcast } from "@tutao/tutanota-utils"
import { ProgrammingError } from "../../../../../src/common/api/common/error/ProgrammingError.js"
import { createTestEntity } from "../../../TestUtils.js"
import { KeyLoaderFacade } from "../../../../../src/common/api/worker/facades/KeyLoaderFacade.js"
import { verify } from "@tutao/tutanota-test-utils"
import { UnreadMailStateService } from "../../../../../src/common/api/entities/tutanota/Services"

o.spec("MailFacade test", function () {
let facade: MailFacade
Expand Down Expand Up @@ -380,7 +387,14 @@ o.spec("MailFacade test", function () {
}),
])

o(await facade.checkMailForPhishing(mail, [{ href: "https://example.com", innerHTML: "https://evil-domain.com" }])).equals(true)
o(
await facade.checkMailForPhishing(mail, [
{
href: "https://example.com",
innerHTML: "https://evil-domain.com",
},
]),
).equals(true)
})

o("link is not suspicious if on the same domain", async function () {
Expand All @@ -398,7 +412,14 @@ o.spec("MailFacade test", function () {
}),
])

o(await facade.checkMailForPhishing(mail, [{ href: "https://example.com", innerHTML: "https://example.com/test" }])).equals(false)
o(
await facade.checkMailForPhishing(mail, [
{
href: "https://example.com",
innerHTML: "https://example.com/test",
},
]),
).equals(false)
})
})

Expand Down Expand Up @@ -514,4 +535,58 @@ o.spec("MailFacade test", function () {
).equals(false)
})
})
o.spec("markMails", () => {
o.test("test with single mail", async () => {
const testIds: IdTuple[] = [["a", "b"]]
await facade.markMails(testIds, true)
verify(
serviceExecutor.post(
UnreadMailStateService,
matchers.contains({
mails: testIds,
unread: true,
}),
),
)
})

o.test("test with a few mails", async () => {
const testIds: IdTuple[] = [
["a", "b"],
["c", "d"],
]
await facade.markMails(testIds, true)
verify(
serviceExecutor.post(
UnreadMailStateService,
matchers.contains({
mails: testIds,
unread: true,
}),
),
)
})

o.test("batches large amounts of mails", async () => {
const expectedBatches = 4
const testIds: IdTuple[] = []
for (let i = 0; i < MAX_NBR_MOVE_DELETE_MAIL_SERVICE * expectedBatches; i++) {
testIds.push([`${i}`, `${i}`])
}
await facade.markMails(testIds, true)
for (let i = 0; i < expectedBatches; i++) {
verify(
serviceExecutor.post(
UnreadMailStateService,
matchers.contains({
mails: testIds.slice(i * MAX_NBR_MOVE_DELETE_MAIL_SERVICE, (i + 1) * MAX_NBR_MOVE_DELETE_MAIL_SERVICE),
unread: true,
}),
),
)
}

verify(serviceExecutor.post(UnreadMailStateService, matchers.anything()), { times: expectedBatches })
})
})
})
31 changes: 28 additions & 3 deletions test/tests/mail/MailModelTest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import o from "@tutao/otest"
import { Notifications } from "../../../src/common/gui/Notifications.js"
import type { Spy } from "@tutao/tutanota-test-utils"
import { spy } from "@tutao/tutanota-test-utils"
import { Spy, spy, verify } from "@tutao/tutanota-test-utils"
import { MailSetKind, OperationType } from "../../../src/common/api/common/TutanotaConstants.js"
import { MailFolderTypeRef, MailTypeRef } from "../../../src/common/api/entities/tutanota/TypeRefs.js"
import { EntityClient } from "../../../src/common/api/common/EntityClient.js"
Expand Down Expand Up @@ -32,13 +31,14 @@ o.spec("MailModelTest", function () {
let mailboxDetails: Partial<MailboxDetail>[]
let logins: LoginController
let inboxRuleHandler: InboxRuleHandler
let mailFacade: MailFacade
const restClient: EntityRestClientMock = new EntityRestClientMock()

o.beforeEach(function () {
notifications = {}
const mailboxModel = instance(MailboxModel)
const eventController = instance(EventController)
const mailFacade = instance(MailFacade)
mailFacade = instance(MailFacade)
showSpy = notifications.showNotification = spy()
logins = object()
let userController = object<UserController>()
Expand Down Expand Up @@ -80,6 +80,31 @@ o.spec("MailModelTest", function () {
o(showSpy.invocations.length).equals(0)
})

o("markMails", async function () {
const mails = [
createTestEntity(MailTypeRef, {
_id: ["mailbag id1", "mail id1"],
}),
createTestEntity(MailTypeRef, {
_id: ["mailbag id2", "mail id2"],
}),
createTestEntity(MailTypeRef, {
_id: ["mailbag id3", "mail id3"],
}),
]
await model.markMails(mails, true)
verify(
mailFacade.markMails(
[
["mailbag id1", "mail id1"],
["mailbag id2", "mail id2"],
["mailbag id3", "mail id3"],
],
true,
),
)
})

function makeUpdate(arg: { instanceListId: string; instanceId: Id; operation: OperationType }): EntityUpdateData {
return Object.assign(
{},
Expand Down

0 comments on commit b1a9f79

Please sign in to comment.