Skip to content

Commit

Permalink
WIP: Switch to using ids for move/trash mail, only resolve if needed
Browse files Browse the repository at this point in the history
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Co-authored-by: ivk <[email protected]>
  • Loading branch information
paw-hub and charlag committed Feb 11, 2025
1 parent 40a4258 commit 445cd72
Show file tree
Hide file tree
Showing 14 changed files with 208 additions and 140 deletions.
2 changes: 1 addition & 1 deletion libs/webassembly/liboqs
Submodule liboqs updated 2639 files
46 changes: 28 additions & 18 deletions src/mail-app/mail/model/MailModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
collectToMap,
getFirstOrThrow,
groupBy,
groupByAndMap,
isEmpty,
isNotNull,
lazyMemoized,
neverNull,
noOp,
ofClass,
partition,
promiseMap,
splitInChunks,
} from "@tutao/tutanota-utils"
import {
Expand Down Expand Up @@ -302,17 +304,9 @@ export class MailModel {
* * one folder (because we send one source folder)
* * from one list (for locking it on the server)
*/
async _moveMails(mails: Mail[], targetMailFolder: MailFolder): Promise<void> {
// Do not move if target is the same as the current mailFolder
const sourceMailFolder = this.getMailFolderForMail(mails[0])
let moveMails = mails.filter((m) => sourceMailFolder !== targetMailFolder && targetMailFolder._ownerGroup === m._ownerGroup) // prevent moving mails between mail boxes.

if (moveMails.length > 0 && sourceMailFolder && !isSameId(targetMailFolder._id, sourceMailFolder._id)) {
const mailChunks = splitInChunks(
MAX_NBR_MOVE_DELETE_MAIL_SERVICE,
mails.map((m) => m._id),
)

async _moveMails(mails: readonly IdTuple[], sourceMailFolder: MailFolder, targetMailFolder: MailFolder): Promise<void> {
if (mails.length > 0 && !isSameId(targetMailFolder._id, sourceMailFolder._id)) {
const mailChunks = splitInChunks(MAX_NBR_MOVE_DELETE_MAIL_SERVICE, mails)
for (const mailChunk of mailChunks) {
await this.mailFacade.moveMails(mailChunk, sourceMailFolder._id, targetMailFolder._id)
}
Expand All @@ -323,7 +317,7 @@ export class MailModel {
* Preferably use moveMails() in MailGuiUtils.js which has built-in error handling
* @throws PreconditionFailedError or LockedError if operation is locked on the server
*/
async moveMails(mails: ReadonlyArray<Mail>, targetMailFolder: MailFolder): Promise<void> {
async moveMailsFromMultipleFolders(mails: ReadonlyArray<Mail>, targetMailFolder: MailFolder): Promise<void> {
const mailsPerFolder = groupBy(mails, (mail) => {
return this.getMailFolderForMail(mail)?._id?.[1]
})
Expand All @@ -333,16 +327,24 @@ export class MailModel {

if (sourceMailFolder) {
// group another time because mails in the same Set can be from different mail bags.
const mailsPerList = groupBy(mailsInFolder, (mail) => getListId(mail))
const mailsPerList = groupByAndMap(mailsInFolder, getListId, (mail) => mail._id)
for (const [listId, mailsInList] of mailsPerList) {
await this._moveMails(mailsInList, targetMailFolder)
await this._moveMails(mailsInList, sourceMailFolder, targetMailFolder)
}
} else {
console.log("Move mail: no mail folder for folder id", folderId)
}
}
}

/**
* Preferably use moveMails() in MailGuiUtils.js which has built-in error handling
* @throws PreconditionFailedError or LockedError if operation is locked on the server
*/
async moveMailsFromFolder(mails: ReadonlyArray<IdTuple>, sourceMailFolder: MailFolder, targetMailFolder: MailFolder): Promise<void> {
await this._moveMails(mails, sourceMailFolder, targetMailFolder)
}

/**
* Moves all mails to the trash if any are not currently in the spam or trash folder.
*
Expand All @@ -366,11 +368,11 @@ export class MailModel {
for (const [folder, mailsInFolder] of mailsPerFolder) {
const sourceMailFolder = this.getMailFolderForMail(mailsInFolder[0])

const mailsPerList = groupBy(mailsInFolder, (mail) => getListId(mail))
const mailsPerList = groupByAndMap(mailsInFolder, getListId, (mail) => mail._id)
for (const [_, mailsInList] of mailsPerList) {
if (sourceMailFolder) {
if (!isSpamOrTrashFolder(folders, sourceMailFolder)) {
await this._moveMails(mailsInList, trashFolder)
await this._moveMails(mailsInList, sourceMailFolder, trashFolder)
}
} else {
console.log("Trash mail: no mail folder for list id", folder)
Expand Down Expand Up @@ -445,8 +447,9 @@ export class MailModel {
}
}

async reportMails(reportType: MailReportType, mails: ReadonlyArray<Mail>): Promise<void> {
for (const mail of mails) {
async reportMails(reportType: MailReportType, mails: () => Promise<ReadonlyArray<Mail>>): Promise<void> {
const mailsToReport = await mails()
for (const mail of mailsToReport) {
await this.mailFacade.reportMail(mail, reportType).catch(ofClass(NotFoundError, (e) => console.log("mail to be reported not found", e)))
}
}
Expand Down Expand Up @@ -709,4 +712,11 @@ export class MailModel {
async resolveConversationsForMails(mails: readonly Mail[]): Promise<IdTuple[]> {
return await this.mailFacade.resolveConversations(mails.map((m) => listIdPart(m.conversationEntry)))
}

async loadAllMails(mailIds: readonly IdTuple[]): Promise<Mail[]> {
const mailIdsPerList = groupByAndMap(mailIds, listIdPart, elementIdPart)
return (
await promiseMap(mailIdsPerList, ([listId, elementIds]) => this.entityClient.loadMultiple(MailTypeRef, listId, elementIds), { concurrency: 2 })
).flat()
}
}
12 changes: 8 additions & 4 deletions src/mail-app/mail/view/EditFolderDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,15 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedF

// get mails to report before moving to mail model
const descendants = folders.getDescendantFoldersOfParent(editedFolder._id).sort((l: IndentedFolder, r: IndentedFolder) => r.level - l.level)
let reportableMails: Array<Mail> = []
await loadAllMailsOfFolder(editedFolder, reportableMails)
for (const descendant of descendants) {
await loadAllMailsOfFolder(descendant.folder, reportableMails)
const reportableMails = async () => {
const reportableMails: Array<Mail> = []
await loadAllMailsOfFolder(editedFolder, reportableMails)
for (const descendant of descendants) {
await loadAllMailsOfFolder(descendant.folder, reportableMails)
}
return reportableMails
}

await reportMailsAutomatically(MailReportType.SPAM, locator.mailboxModel, mailLocator.mailModel, mailBoxDetail, reportableMails)

await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue)
Expand Down
160 changes: 100 additions & 60 deletions src/mail-app/mail/view/MailGuiUtils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { createMail, File as TutanotaFile, Mail, MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { File as TutanotaFile, Mail, MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { LockedError, PreconditionFailedError } from "../../../common/api/common/error/RestError"
import { Dialog } from "../../../common/gui/base/Dialog"
import { locator } from "../../../common/api/main/CommonLocator"
import { AllIcons } from "../../../common/gui/base/Icon"
import { Icons } from "../../../common/gui/base/icons/Icons"
import { isApp, isDesktop } from "../../../common/api/common/Env"
import { assertNotNull, endsWith, getFirstOrThrow, isEmpty, neverNull, noOp, promiseMap } from "@tutao/tutanota-utils"
import { $Promisable, assertNotNull, endsWith, getFirstOrThrow, isEmpty, neverNull, noOp, promiseMap } from "@tutao/tutanota-utils"
import {
EncryptionAuthStatus,
getMailFolderType,
Expand All @@ -15,7 +15,6 @@ import {
MailState,
SYSTEM_GROUP_MAIL_ADDRESS,
} from "../../../common/api/common/TutanotaConstants"
import { getElementId } from "../../../common/api/common/utils/EntityUtils"
import { reportMailsAutomatically } from "./MailReportDialog"
import { DataFile } from "../../../common/api/common/DataFile"
import { lang, Translation } from "../../../common/misc/LanguageViewModel"
Expand All @@ -34,7 +33,6 @@ import {
FolderInfo,
getFolderName,
getIndentedFolderNameForDropdown,
getMoveTargetFolderSystems,
getMoveTargetFolderSystemsForMailsInFolder,
} from "../model/MailUtils.js"
import { FontIcons } from "../../../common/gui/base/icons/FontIcons.js"
Expand All @@ -45,7 +43,7 @@ import type { FolderSystem, IndentedFolder } from "../../../common/api/common/ma
/**
* A function that returns an array of mails, or a promise that eventually returns one.
*/
export type LazyMailResolver = () => readonly Mail[] | Promise<readonly Mail[]>
export type LazyMailIdResolver = () => $Promisable<readonly IdTuple[]>

/**
* Moves all mails to the trash folder if they are not currently in a trash or spam folder.
Expand All @@ -59,11 +57,11 @@ export type LazyMailResolver = () => readonly Mail[] | Promise<readonly Mail[]>
*/
export async function trashOrDeleteMails(
mailModel: MailModel,
mailResolver: LazyMailResolver,
mailResolver: LazyMailIdResolver,
currentFolder: MailFolder | null,
onConfirm: () => void,
): Promise<boolean> {
let mails: readonly Mail[] | null = null
let mails: readonly IdTuple[] | null = null

// Determine if current folder is trash or spam.
let isDeletion
Expand Down Expand Up @@ -135,7 +133,7 @@ export async function trashOrDeleteMails(
export async function trashOrDeleteSingleMail(mailModel: MailModel, mail: Mail, onConfirm: () => void): Promise<boolean> {
const folder = mailModel.getMailFolderForMail(mail)
if (folder != null) {
return trashOrDeleteMails(mailModel, () => [mail], folder, onConfirm)
return trashOrDeleteMails(mailModel, () => [mail._id], folder, onConfirm)
} else {
return false
}
Expand All @@ -149,48 +147,79 @@ interface MoveMailsParams {
isReportable?: boolean
}

async function reportMails(
system: FolderSystem,
targetMailFolder: MailFolder,
isReportable: boolean,
mails: () => Promise<readonly Mail[]>,
mailboxModel: MailboxModel,
mailModel: MailModel,
): Promise<boolean> {
if (isOfTypeOrSubfolderOf(system, targetMailFolder, MailSetKind.SPAM) && isReportable) {
const mailboxDetails = await mailboxModel.getMailboxDetailsForMailGroup(assertNotNull(targetMailFolder._ownerGroup))
await reportMailsAutomatically(MailReportType.SPAM, mailboxModel, mailModel, mailboxDetails, mails)
}
return true
}

/**
* Moves the mails and reports them as spam if the user or settings allow it.
* @return whether mails were actually moved
*/
export async function moveMails({ mailboxModel, mailModel, mails, targetMailFolder, isReportable = true }: MoveMailsParams): Promise<boolean> {
const details = await mailModel.getMailboxDetailsForMailFolder(targetMailFolder)
if (details == null || details.mailbox.folders == null) {
export async function moveResolvedMails({ mailboxModel, mailModel, mails, targetMailFolder, isReportable = true }: MoveMailsParams): Promise<boolean> {
const system = mailModel.getFolderSystemByGroupId(assertNotNull(targetMailFolder._ownerGroup))
if (system == null) {
return false
}
const system = await mailModel.getMailboxFoldersForId(details.mailbox.folders._id)
return mailModel
.moveMails(mails, targetMailFolder)
.then(async () => {
if (isOfTypeOrSubfolderOf(system, targetMailFolder, MailSetKind.SPAM) && isReportable) {
const reportableMails = mails.map((mail) => {
// mails have just been moved
const reportableMail = createMail(mail)
reportableMail._id = targetMailFolder.isMailSet ? mail._id : [targetMailFolder.mails, getElementId(mail)]
return reportableMail
})
const mailboxDetails = await mailboxModel.getMailboxDetailsForMailGroup(assertNotNull(targetMailFolder._ownerGroup))
await reportMailsAutomatically(MailReportType.SPAM, mailboxModel, mailModel, mailboxDetails, reportableMails)
}
try {
await mailModel.moveMailsFromMultipleFolders(mails, targetMailFolder)
return await reportMails(system, targetMailFolder, isReportable, async () => mails, mailboxModel, mailModel)
} catch (e) {
//LockedError should no longer be thrown!?!
if (e instanceof LockedError || e instanceof PreconditionFailedError) {
return Dialog.message("operationStillActive_msg").then(() => false)
} else {
throw e
}
}
}

return true
})
.catch((e) => {
//LockedError should no longer be thrown!?!
if (e instanceof LockedError || e instanceof PreconditionFailedError) {
return Dialog.message("operationStillActive_msg").then(() => false)
} else {
throw e
}
})
/**
* Moves the mails and reports them as spam if the user or settings allow it.
* @return whether mails were actually moved
*/
export async function moveMails(
mailboxModel: MailboxModel,
mailModel: MailModel,
mails: readonly IdTuple[],
sourceMailFolder: MailFolder,
targetMailFolder: MailFolder,
isReportable: boolean = true,
): Promise<boolean> {
const system = mailModel.getFolderSystemByGroupId(assertNotNull(targetMailFolder._ownerGroup))
if (system == null) {
return false
}
try {
await mailModel.moveMailsFromFolder(mails, sourceMailFolder, targetMailFolder)
const resolveMails = () => mailModel.loadAllMails(mails)
return await reportMails(system, targetMailFolder, isReportable, resolveMails, mailboxModel, mailModel)
} catch (e) {
//LockedError should no longer be thrown!?!
if (e instanceof LockedError || e instanceof PreconditionFailedError) {
return Dialog.message("operationStillActive_msg").then(() => false)
} else {
throw e
}
}
}

export function archiveMails(mails: Mail[]): Promise<void> {
if (mails.length > 0) {
// assume all mails in the array belong to the same Mailbox
return mailLocator.mailModel.getMailboxFoldersForMail(mails[0]).then((folders: FolderSystem) => {
if (folders) {
moveMails({
moveResolvedMails({
mailboxModel: locator.mailboxModel,
mailModel: mailLocator.mailModel,
mails: mails,
Expand All @@ -208,7 +237,7 @@ export function moveToInbox(mails: Mail[]): Promise<any> {
// assume all mails in the array belong to the same Mailbox
return mailLocator.mailModel.getMailboxFoldersForMail(mails[0]).then((folders: FolderSystem) => {
if (folders) {
moveMails({
moveResolvedMails({
mailboxModel: locator.mailboxModel,
mailModel: mailLocator.mailModel,
mails: mails,
Expand Down Expand Up @@ -386,40 +415,51 @@ export function getReferencedAttachments(attachments: Array<TutanotaFile>, refer
return attachments.filter((file) => referencedCids.find((rcid) => file.cid === rcid))
}

export async function showMoveMailsDropdownForMailInFolder(
// always has folder and lazy mails
export async function showMoveMailsDropdownForMailsInFolder(
mailboxModel: MailboxModel,
mailModel: MailModel,
origin: PosRect,
mails: LazyMailIdResolver,
currentFolder: MailFolder,
opts?: { width?: number; withBackground?: boolean; onSelected?: () => unknown },
) {
const folders = await getMoveTargetFolderSystemsForMailsInFolder(mailModel, currentFolder)
await showMailFolderDropdown(
origin,
folders,
async (f) => {
const resolvedMails = await mails()
return moveMails(mailboxModel, mailModel, resolvedMails, currentFolder, f.folder)
},
opts,
)
}

// no folder, just mails
export async function showMoveMailsDropdownForMails(
mailboxModel: MailboxModel,
model: MailModel,
origin: PosRect,
mails: LazyMailResolver,
currentFolder: MailFolder | null,
mails: readonly Mail[],
opts?: { width?: number; withBackground?: boolean; onSelected?: () => unknown },
): Promise<void> {
// Determine what folders to show in dropdown.
// If there's a current folder than we are likely in the list and we will show all other folders for that mailbox.
// Otherwise we are likely in search and we just show all folders of the mailbox.
let resolvedMails: readonly Mail[] | null = null
let folders: readonly FolderInfo[]
if (currentFolder != null) {
folders = await getMoveTargetFolderSystemsForMailsInFolder(model, currentFolder)
} else {
resolvedMails = await mails()
if (isEmpty(resolvedMails)) {
return
}
const folderSystem = await model.getMailboxFoldersForMail(getFirstOrThrow(resolvedMails))
if (folderSystem == null) {
return
}
folders = folderSystem.getIndentedList()
) {
if (isEmpty(mails)) {
return
}
const folderSystem = await model.getMailboxFoldersForMail(getFirstOrThrow(mails))
if (folderSystem == null) {
return
}
const folders = folderSystem.getIndentedList()
await showMailFolderDropdown(
origin,
folders,
async (f) =>
moveMails({
moveResolvedMails({
mailboxModel,
mailModel: model,
mails: resolvedMails ?? (await mails()),
mails: mails,
targetMailFolder: f.folder,
}),
opts,
Expand Down
Loading

0 comments on commit 445cd72

Please sign in to comment.