mirror of
https://github.com/tutao/tutanota.git
synced 2025-10-19 16:03:43 +00:00
MailSet support (static mail listIds)
In order to allow importing of mails we replace legacy MailFolders (non-static mail listIds) with new MailSets (static mail listIds). From now on, mails have static mail listIds and static mail elementIds. To move mails between new MailSets we introduce MailSetEntries ("entries" property on a MailSet), which are index entries sorted by the received date of the referenced mails (customId). This commit adds support for new MailSets, while still supporting legacy MailFolders (mail lists) to support migrating gradually. * TutanotaModelV74 adds: * MailSet support * and defaultAlarmList on GroupSettings * SystemModelV107 adds model changes for counter (unread mails) updates * Adapt mail list to show MailSet and legacy mails The list model is now largely unaware about listIds since it can display mails from multiple MailBags. MailBags are static mailLists from which a mail is only removed from when the mail is permanently deleted. * Adapt offline storage for mail sets Offline storage gained the ability to provide cached entities from a list of ids.
This commit is contained in:
parent
b803773b4d
commit
2d24bab6f9
97 changed files with 33829 additions and 1275 deletions
|
@ -560,3 +560,26 @@ export function zeroOut(...arrays: (Uint8Array | Int8Array)[]) {
|
|||
a.fill(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 1 if first is bigger than second, -1 if second is bigger than first and 0 otherwise
|
||||
*/
|
||||
export function compare(first: Uint8Array, second: Uint8Array): number {
|
||||
if (first.length > second.length) {
|
||||
return 1
|
||||
} else if (first.length < second.length) {
|
||||
return -1
|
||||
}
|
||||
|
||||
for (let i = 0; i < first.length; i++) {
|
||||
const a = first[i]
|
||||
const b = second[i]
|
||||
if (a > b) {
|
||||
return 1
|
||||
} else if (a < b) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ export {
|
|||
arrayOf,
|
||||
count,
|
||||
zeroOut,
|
||||
compare,
|
||||
} from "./ArrayUtils.js"
|
||||
export { AsyncResult } from "./AsyncResult.js"
|
||||
export { intersection, trisectingDiff, setAddAll, max, maxBy, findBy, min, minBy, mapWith, mapWithout, setEquals, setMap } from "./CollectionUtils.js"
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
splitInChunks,
|
||||
symmetricDifference,
|
||||
} from "../lib/index.js"
|
||||
import { compare } from "../lib/ArrayUtils.js"
|
||||
|
||||
type ObjectWithId = {
|
||||
v: number
|
||||
|
@ -875,4 +876,18 @@ o.spec("array utils", function () {
|
|||
|
||||
o(arrayOf(2, (idx) => idx + 1 + " one thousand")).deepEquals(["1 one thousand", "2 one thousand"])
|
||||
})
|
||||
|
||||
o("customId comparision", function () {
|
||||
o(compare(new Uint8Array([]), new Uint8Array([]))).equals(0)
|
||||
|
||||
o(compare(new Uint8Array([1]), new Uint8Array([]))).equals(1)
|
||||
|
||||
o(compare(new Uint8Array([]), new Uint8Array([1]))).equals(-1)
|
||||
|
||||
o(compare(new Uint8Array([1, 1]), new Uint8Array([1, 1]))).equals(0)
|
||||
|
||||
o(compare(new Uint8Array([1, 1, 3]), new Uint8Array([1, 1, 2]))).equals(1)
|
||||
|
||||
o(compare(new Uint8Array([1, 1, 2]), new Uint8Array([1, 1, 3]))).equals(-1)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -380,6 +380,16 @@
|
|||
"info": "AddAssociation GroupKeyRotationData/groupMembershipUpdateData/AGGREGATION/2432."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": 107,
|
||||
"changes": [
|
||||
{
|
||||
"name": "RenameAttribute",
|
||||
"sourceType": "WebsocketCounterValue",
|
||||
"info": "RenameAttribute WebsocketCounterValue: mailListId -> counterId."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -450,6 +450,46 @@
|
|||
"info": "RemoveValue Contact/autoTransmitPassword/78."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": 74,
|
||||
"changes": [
|
||||
{
|
||||
"name": "AddAssociation",
|
||||
"sourceType": "GroupSettings",
|
||||
"info": "AddAssociation GroupSettings/defaultAlarmsList/AGGREGATION/1449."
|
||||
},
|
||||
{
|
||||
"name": "AddValue",
|
||||
"sourceType": "MailFolder",
|
||||
"info": "AddValue MailFolder/isLabel/1457."
|
||||
},
|
||||
{
|
||||
"name": "AddValue",
|
||||
"sourceType": "MailFolder",
|
||||
"info": "AddValue MailFolder/isMailSet/1458."
|
||||
},
|
||||
{
|
||||
"name": "AddAssociation",
|
||||
"sourceType": "MailFolder",
|
||||
"info": "AddAssociation MailFolder/entries/LIST_ASSOCIATION/1459."
|
||||
},
|
||||
{
|
||||
"name": "AddAssociation",
|
||||
"sourceType": "MailBox",
|
||||
"info": "AddAssociation MailBox/archivedMailBags/AGGREGATION/1463."
|
||||
},
|
||||
{
|
||||
"name": "AddAssociation",
|
||||
"sourceType": "MailBox",
|
||||
"info": "AddAssociation MailBox/currentMailBag/AGGREGATION/1464."
|
||||
},
|
||||
{
|
||||
"name": "AddAssociation",
|
||||
"sourceType": "Mail",
|
||||
"info": "AddAssociation Mail/sets/LIST_ELEMENT_ASSOCIATION/1465."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -220,6 +220,7 @@ export class CalendarModel {
|
|||
group: group._id,
|
||||
color: color,
|
||||
name: null,
|
||||
defaultAlarmsList: [],
|
||||
})
|
||||
userSettingsGroupRoot.groupSettings.push(newGroupSettings)
|
||||
await this.entityClient.update(userSettingsGroupRoot)
|
||||
|
|
|
@ -133,7 +133,7 @@ export class CalendarSearchModel {
|
|||
continue
|
||||
}
|
||||
|
||||
if (restriction.listIds.length > 0 && !restriction.listIds.includes(listIdPart(event._id))) {
|
||||
if (restriction.folderIds.length > 0 && !restriction.folderIds.includes(listIdPart(event._id))) {
|
||||
// check that the event is in the searched calendar.
|
||||
continue
|
||||
}
|
||||
|
@ -221,7 +221,7 @@ export function isSameSearchRestriction(a: SearchRestriction, b: SearchRestricti
|
|||
a.end === b.end &&
|
||||
isSameAttributeIds &&
|
||||
(a.eventSeries === b.eventSeries || (a.eventSeries === null && b.eventSeries === true) || (a.eventSeries === true && b.eventSeries === null)) &&
|
||||
arrayEquals(a.listIds, b.listIds)
|
||||
arrayEquals(a.folderIds, b.folderIds)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -50,8 +50,8 @@ export function getSearchUrl(
|
|||
if (restriction.end) {
|
||||
params.end = restriction.end
|
||||
}
|
||||
if (restriction.listIds.length > 0) {
|
||||
params.list = restriction.listIds
|
||||
if (restriction.folderIds.length > 0) {
|
||||
params.folder = restriction.folderIds
|
||||
}
|
||||
|
||||
if (restriction.eventSeries != null) {
|
||||
|
@ -67,14 +67,14 @@ export function getSearchUrl(
|
|||
/**
|
||||
* Adjusts the restriction according to the account type if necessary
|
||||
*/
|
||||
export function createRestriction(start: number | null, end: number | null, listIds: Array<string>, eventSeries: boolean): SearchRestriction {
|
||||
export function createRestriction(start: number | null, end: number | null, folderIds: Array<string>, eventSeries: boolean): SearchRestriction {
|
||||
return {
|
||||
type: CalendarEventTypeRef,
|
||||
start: start,
|
||||
end: end,
|
||||
field: null,
|
||||
attributeIds: null,
|
||||
listIds,
|
||||
folderIds,
|
||||
eventSeries,
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ export function createRestriction(start: number | null, end: number | null, list
|
|||
export function getRestriction(route: string): SearchRestriction {
|
||||
let start: number | null = null
|
||||
let end: number | null = null
|
||||
let listIds: Array<string> = []
|
||||
let folderIds: Array<string> = []
|
||||
let eventSeries: boolean = true
|
||||
|
||||
if (route.startsWith("/calendar") || route.startsWith("/search/calendar")) {
|
||||
|
@ -104,9 +104,9 @@ export function getRestriction(route: string): SearchRestriction {
|
|||
end = filterInt(params["end"])
|
||||
}
|
||||
|
||||
const list = params["list"]
|
||||
if (Array.isArray(list)) {
|
||||
listIds = list
|
||||
const folder = params["folder"]
|
||||
if (Array.isArray(folder)) {
|
||||
folderIds = folder
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("invalid query: " + route, e)
|
||||
|
@ -127,7 +127,7 @@ export function getRestriction(route: string): SearchRestriction {
|
|||
throw new Error("invalid type " + route)
|
||||
}
|
||||
|
||||
return createRestriction(start, end, listIds, eventSeries)
|
||||
return createRestriction(start, end, folderIds, eventSeries)
|
||||
}
|
||||
|
||||
export function decodeCalendarSearchKey(searchKey: string): { id: Id; start: number } {
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
ofClass,
|
||||
TypeRef,
|
||||
} from "@tutao/tutanota-utils"
|
||||
import { areResultsForTheSameQuery, hasMoreResults, isSameSearchRestriction, CalendarSearchModel } from "../model/CalendarSearchModel.js"
|
||||
import { areResultsForTheSameQuery, CalendarSearchModel, hasMoreResults, isSameSearchRestriction } from "../model/CalendarSearchModel.js"
|
||||
import { NotFoundError } from "../../../../common/api/common/error/RestError.js"
|
||||
import { createRestriction, decodeCalendarSearchKey, encodeCalendarSearchKey, getRestriction } from "../model/SearchUtils.js"
|
||||
import Stream from "mithril/stream"
|
||||
|
@ -159,7 +159,7 @@ export class CalendarSearchViewModel {
|
|||
}
|
||||
|
||||
private listIdMatchesRestriction(listId: string, restriction: SearchRestriction): boolean {
|
||||
return restriction.listIds.length === 0 || restriction.listIds.includes(listId)
|
||||
return restriction.folderIds.length === 0 || restriction.folderIds.includes(listId)
|
||||
}
|
||||
|
||||
onNewUrl(args: Record<string, any>, requestedPath: string) {
|
||||
|
@ -217,7 +217,7 @@ export class CalendarSearchViewModel {
|
|||
|
||||
this.startDate = restriction.start ? new Date(restriction.start) : null
|
||||
this.endDate = restriction.end ? new Date(restriction.end) : null
|
||||
this.selectedCalendar = this.extractCalendarListIds(restriction.listIds)
|
||||
this.selectedCalendar = this.extractCalendarListIds(restriction.folderIds)
|
||||
this.includeRepeatingEvents = restriction.eventSeries ?? true
|
||||
this.lazyCalendarInfos.load()
|
||||
this.latestCalendarRestriction = restriction
|
||||
|
@ -415,12 +415,12 @@ export class CalendarSearchViewModel {
|
|||
|
||||
return { items: entries, complete }
|
||||
},
|
||||
loadSingle: async (elementId: Id) => {
|
||||
loadSingle: async (_listId: Id, elementId: Id) => {
|
||||
const lastResult = this._searchResult
|
||||
if (!lastResult) {
|
||||
return null
|
||||
}
|
||||
const id = lastResult.results.find((r) => r[1] === elementId)
|
||||
const id = lastResult.results.find((resultId) => elementIdPart(resultId) === elementId)
|
||||
if (id) {
|
||||
return this.entityClient
|
||||
.load(lastResult.restriction.type, id)
|
||||
|
@ -446,7 +446,7 @@ export class CalendarSearchViewModel {
|
|||
if (result && isSameTypeRef(typeRef, result.restriction.type)) {
|
||||
// The list id must be null/empty, otherwise the user is filtering by list, and it shouldn't be ignored
|
||||
|
||||
const ignoreList = isSameTypeRef(typeRef, MailTypeRef) && result.restriction.listIds.length === 0
|
||||
const ignoreList = isSameTypeRef(typeRef, MailTypeRef) && result.restriction.folderIds.length === 0
|
||||
|
||||
return result.results.some((r) => this.compareItemId(r, id, ignoreList))
|
||||
}
|
||||
|
|
|
@ -802,6 +802,7 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
|
|||
group: groupInfo.group,
|
||||
color: properties.color,
|
||||
name: shared && properties.name !== groupInfo.name ? properties.name : null,
|
||||
defaultAlarmsList: [],
|
||||
})
|
||||
userSettingsGroupRoot.groupSettings.push(newGroupSettings)
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
import type { FolderSystem } from "./mail/FolderSystem.js"
|
||||
import { Body, Mail, MailFolder } from "../entities/tutanota/TypeRefs.js"
|
||||
import { MailFolderType } from "./TutanotaConstants.js"
|
||||
import { MailSetKind } from "./TutanotaConstants.js"
|
||||
|
||||
export function isSubfolderOfType(system: FolderSystem, folder: MailFolder, type: MailFolderType): boolean {
|
||||
export function isSubfolderOfType(system: FolderSystem, folder: MailFolder, type: MailSetKind): boolean {
|
||||
const systemFolder = system.getSystemFolderByType(type)
|
||||
return systemFolder != null && system.checkFolderForAncestor(folder, systemFolder._id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if given folder is the {@link MailFolderType.SPAM} or {@link MailFolderType.TRASH} folder, or a descendant of those folders.
|
||||
* Returns true if given folder is the {@link MailSetKind.SPAM} or {@link MailSetKind.TRASH} folder, or a descendant of those folders.
|
||||
*/
|
||||
export function isSpamOrTrashFolder(system: FolderSystem, folder: MailFolder): boolean {
|
||||
// not using isOfTypeOrSubfolderOf because checking the type first is cheaper
|
||||
return (
|
||||
folder.folderType === MailFolderType.TRASH ||
|
||||
folder.folderType === MailFolderType.SPAM ||
|
||||
isSubfolderOfType(system, folder, MailFolderType.TRASH) ||
|
||||
isSubfolderOfType(system, folder, MailFolderType.SPAM)
|
||||
folder.folderType === MailSetKind.TRASH ||
|
||||
folder.folderType === MailSetKind.SPAM ||
|
||||
isSubfolderOfType(system, folder, MailSetKind.TRASH) ||
|
||||
isSubfolderOfType(system, folder, MailSetKind.SPAM)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ export const REQUEST_SIZE_LIMIT_MAP: Map<string, number> = new Map([
|
|||
|
||||
export const SYSTEM_GROUP_MAIL_ADDRESS = "system@tutanota.de"
|
||||
|
||||
export const getMailFolderType = (folder: MailFolder): MailFolderType => downcast(folder.folderType)
|
||||
export const getMailFolderType = (folder: MailFolder): MailSetKind => downcast(folder.folderType)
|
||||
|
||||
type ObjectPropertyKey = string | number | symbol
|
||||
export const reverse = <K extends ObjectPropertyKey, V extends ObjectPropertyKey>(objectMap: Record<K, V>): Record<V, K> =>
|
||||
|
@ -84,7 +84,7 @@ export const enum BucketPermissionType {
|
|||
External = "3",
|
||||
}
|
||||
|
||||
export enum MailFolderType {
|
||||
export enum MailSetKind {
|
||||
CUSTOM = "0",
|
||||
INBOX = "1",
|
||||
SENT = "2",
|
||||
|
@ -92,6 +92,7 @@ export enum MailFolderType {
|
|||
ARCHIVE = "4",
|
||||
SPAM = "5",
|
||||
DRAFT = "6",
|
||||
ALL = "7",
|
||||
}
|
||||
|
||||
export const enum ReplyType {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { groupBy, partition } from "@tutao/tutanota-utils"
|
||||
import { MailFolder } from "../../entities/tutanota/TypeRefs.js"
|
||||
import { MailFolderType } from "../TutanotaConstants.js"
|
||||
import { elementIdPart, getElementId, isSameId } from "../utils/EntityUtils.js"
|
||||
import { groupBy, isNotEmpty, partition } from "@tutao/tutanota-utils"
|
||||
import { Mail, MailFolder } from "../../entities/tutanota/TypeRefs.js"
|
||||
import { MailSetKind } from "../TutanotaConstants.js"
|
||||
import { elementIdPart, getElementId, getListId, isSameId } from "../utils/EntityUtils.js"
|
||||
|
||||
export interface IndentedFolder {
|
||||
level: number
|
||||
|
@ -17,7 +17,7 @@ export class FolderSystem {
|
|||
const folderByParent = groupBy(folders, (folder) => (folder.parentFolder ? elementIdPart(folder.parentFolder) : null))
|
||||
const topLevelFolders = folders.filter((f) => f.parentFolder == null)
|
||||
|
||||
const [systemFolders, customFolders] = partition(topLevelFolders, (f) => f.folderType !== MailFolderType.CUSTOM)
|
||||
const [systemFolders, customFolders] = partition(topLevelFolders, (f) => f.folderType !== MailSetKind.CUSTOM)
|
||||
|
||||
this.systemSubtrees = systemFolders.sort(compareSystem).map((f) => this.makeSubtree(folderByParent, f, compareCustom))
|
||||
this.customSubtrees = customFolders.sort(compareCustom).map((f) => this.makeSubtree(folderByParent, f, compareCustom))
|
||||
|
@ -28,16 +28,25 @@ export class FolderSystem {
|
|||
}
|
||||
|
||||
/** Search for a specific folder type. Some mailboxes might not have some system folders! */
|
||||
getSystemFolderByType(type: Omit<MailFolderType, MailFolderType.CUSTOM>): MailFolder | null {
|
||||
getSystemFolderByType(type: Omit<MailSetKind, MailSetKind.CUSTOM>): MailFolder | null {
|
||||
return this.systemSubtrees.find((f) => f.folder.folderType === type)?.folder ?? null
|
||||
}
|
||||
|
||||
getFolderById(folderId: IdTuple): MailFolder | null {
|
||||
getFolderById(folderId: Id): MailFolder | null {
|
||||
const subtree = this.getFolderByIdInSubtrees(this.systemSubtrees, folderId) ?? this.getFolderByIdInSubtrees(this.customSubtrees, folderId)
|
||||
return subtree?.folder ?? null
|
||||
}
|
||||
|
||||
getFolderByMailListId(mailListId: Id): MailFolder | null {
|
||||
getFolderByMail(mail: Mail): MailFolder | null {
|
||||
const sets = mail.sets
|
||||
if (isNotEmpty(sets)) {
|
||||
return this.getFolderById(elementIdPart(sets[0]))
|
||||
} else {
|
||||
return this.getFolderByMailListIdLegacy(getListId(mail))
|
||||
}
|
||||
}
|
||||
|
||||
private getFolderByMailListIdLegacy(mailListId: Id): MailFolder | null {
|
||||
const subtree =
|
||||
this.getFolderByMailListIdInSubtrees(this.systemSubtrees, mailListId) ?? this.getFolderByMailListIdInSubtrees(this.customSubtrees, mailListId)
|
||||
return subtree?.folder ?? null
|
||||
|
@ -49,7 +58,7 @@ export class FolderSystem {
|
|||
*/
|
||||
getCustomFoldersOfParent(parent: IdTuple | null): MailFolder[] {
|
||||
if (parent) {
|
||||
const parentFolder = this.getFolderByIdInSubtrees([...this.customSubtrees, ...this.systemSubtrees], parent)
|
||||
const parentFolder = this.getFolderByIdInSubtrees([...this.customSubtrees, ...this.systemSubtrees], elementIdPart(parent))
|
||||
return parentFolder ? parentFolder.children.map((child) => child.folder) : []
|
||||
} else {
|
||||
return this.customSubtrees.map((subtree) => subtree.folder)
|
||||
|
@ -57,7 +66,7 @@ export class FolderSystem {
|
|||
}
|
||||
|
||||
getDescendantFoldersOfParent(parent: IdTuple): IndentedFolder[] {
|
||||
const parentFolder = this.getFolderByIdInSubtrees([...this.customSubtrees, ...this.systemSubtrees], parent)
|
||||
const parentFolder = this.getFolderByIdInSubtrees([...this.customSubtrees, ...this.systemSubtrees], elementIdPart(parent))
|
||||
if (parentFolder) {
|
||||
return this.getIndentedFolderList([parentFolder]).slice(1)
|
||||
} else {
|
||||
|
@ -78,7 +87,7 @@ export class FolderSystem {
|
|||
} else if (isSameId(currentFolderPointer.parentFolder, potentialAncestorId)) {
|
||||
return true
|
||||
}
|
||||
currentFolderPointer = this.getFolderById(currentFolderPointer.parentFolder)
|
||||
currentFolderPointer = this.getFolderById(elementIdPart(currentFolderPointer.parentFolder))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,8 +108,8 @@ export class FolderSystem {
|
|||
})
|
||||
}
|
||||
|
||||
private getFolderByIdInSubtrees(systems: ReadonlyArray<FolderSubtree>, folderId: IdTuple): FolderSubtree | null {
|
||||
return this.getFolderBy(systems, (system) => isSameId(system.folder._id, folderId))
|
||||
private getFolderByIdInSubtrees(systems: ReadonlyArray<FolderSubtree>, folderId: Id): FolderSubtree | null {
|
||||
return this.getFolderBy(systems, (system) => isSameId(getElementId(system.folder), folderId))
|
||||
}
|
||||
|
||||
private getFolderByMailListIdInSubtrees(systems: ReadonlyArray<FolderSubtree>, mailListId: Id): FolderSubtree | null {
|
||||
|
@ -155,15 +164,16 @@ function compareCustom(folder1: MailFolder, folder2: MailFolder): number {
|
|||
return folder1.name.localeCompare(folder2.name)
|
||||
}
|
||||
|
||||
type SystemMailFolderTypes = Exclude<MailFolderType, MailFolderType.CUSTOM>
|
||||
type SystemMailFolderTypes = Exclude<MailSetKind, MailSetKind.CUSTOM>
|
||||
|
||||
const folderTypeToOrder: Record<SystemMailFolderTypes, number> = {
|
||||
[MailFolderType.INBOX]: 0,
|
||||
[MailFolderType.DRAFT]: 1,
|
||||
[MailFolderType.SENT]: 2,
|
||||
[MailFolderType.TRASH]: 4,
|
||||
[MailFolderType.ARCHIVE]: 5,
|
||||
[MailFolderType.SPAM]: 6,
|
||||
[MailSetKind.INBOX]: 0,
|
||||
[MailSetKind.DRAFT]: 1,
|
||||
[MailSetKind.SENT]: 2,
|
||||
[MailSetKind.TRASH]: 4,
|
||||
[MailSetKind.ARCHIVE]: 5,
|
||||
[MailSetKind.SPAM]: 6,
|
||||
[MailSetKind.ALL]: 7,
|
||||
}
|
||||
|
||||
function compareSystem(folder1: MailFolder, folder2: MailFolder): number {
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
base64ToUint8Array,
|
||||
base64UrlToBase64,
|
||||
clone,
|
||||
compare,
|
||||
hexToBase64,
|
||||
isSameTypeRef,
|
||||
pad,
|
||||
|
@ -108,7 +109,7 @@ export type StrippedEntity<T extends Entity> =
|
|||
*/
|
||||
export function firstBiggerThanSecond(firstId: Id, secondId: Id, typeModel?: TypeModel): boolean {
|
||||
if (typeModel?.values._id.type === ValueType.CustomId) {
|
||||
return firstBiggerThanSecond(customIdToString(firstId), customIdToString(secondId))
|
||||
return firstBiggerThanSecondCustomId(firstId, secondId)
|
||||
} else {
|
||||
// if the number of digits is bigger, then the id is bigger, otherwise we can use the lexicographical comparison
|
||||
if (firstId.length > secondId.length) {
|
||||
|
@ -121,6 +122,17 @@ export function firstBiggerThanSecond(firstId: Id, secondId: Id, typeModel?: Typ
|
|||
}
|
||||
}
|
||||
|
||||
export function firstBiggerThanSecondCustomId(firstId: Id, secondId: Id): boolean {
|
||||
return compare(customIdToUint8array(firstId), customIdToUint8array(secondId)) === 1
|
||||
}
|
||||
|
||||
function customIdToUint8array(id: Id): Uint8Array {
|
||||
if (id === "") {
|
||||
return new Uint8Array()
|
||||
}
|
||||
return base64ToUint8Array(base64UrlToBase64(id))
|
||||
}
|
||||
|
||||
export function compareNewestFirst(id1: Id | IdTuple, id2: Id | IdTuple): number {
|
||||
let firstId = id1 instanceof Array ? id1[1] : id1
|
||||
let secondId = id2 instanceof Array ? id2[1] : id2
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const modelInfo = {
|
||||
version: 106,
|
||||
compatibleSince: 106,
|
||||
version: 107,
|
||||
compatibleSince: 107,
|
||||
}
|
||||
|
||||
export default modelInfo
|
File diff suppressed because it is too large
Load diff
|
@ -3455,7 +3455,7 @@ export type WebsocketCounterValue = {
|
|||
|
||||
_id: Id;
|
||||
count: NumberString;
|
||||
mailListId: Id;
|
||||
counterId: Id;
|
||||
}
|
||||
export const WebsocketEntityDataTypeRef: TypeRef<WebsocketEntityData> = new TypeRef("sys", "WebsocketEntityData")
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const modelInfo = {
|
||||
version: 73,
|
||||
compatibleSince: 73,
|
||||
version: 74,
|
||||
compatibleSince: 74,
|
||||
}
|
||||
|
||||
export default modelInfo
|
File diff suppressed because it is too large
Load diff
|
@ -560,6 +560,18 @@ export type CustomerAccountCreateData = {
|
|||
userData: UserAccountUserData;
|
||||
userGroupData: InternalGroupData;
|
||||
}
|
||||
export const DefaultAlarmInfoTypeRef: TypeRef<DefaultAlarmInfo> = new TypeRef("tutanota", "DefaultAlarmInfo")
|
||||
|
||||
export function createDefaultAlarmInfo(values: StrippedEntity<DefaultAlarmInfo>): DefaultAlarmInfo {
|
||||
return Object.assign(create(typeModels.DefaultAlarmInfo, DefaultAlarmInfoTypeRef), values)
|
||||
}
|
||||
|
||||
export type DefaultAlarmInfo = {
|
||||
_type: TypeRef<DefaultAlarmInfo>;
|
||||
|
||||
_id: Id;
|
||||
trigger: string;
|
||||
}
|
||||
export const DeleteGroupDataTypeRef: TypeRef<DeleteGroupData> = new TypeRef("tutanota", "DeleteGroupData")
|
||||
|
||||
export function createDeleteGroupData(values: StrippedEntity<DeleteGroupData>): DeleteGroupData {
|
||||
|
@ -931,6 +943,7 @@ export type GroupSettings = {
|
|||
color: string;
|
||||
name: null | string;
|
||||
|
||||
defaultAlarmsList: DefaultAlarmInfo[];
|
||||
group: Id;
|
||||
}
|
||||
export const HeaderTypeRef: TypeRef<Header> = new TypeRef("tutanota", "Header")
|
||||
|
@ -1135,6 +1148,7 @@ export type Mail = {
|
|||
mailDetails: null | IdTuple;
|
||||
mailDetailsDraft: null | IdTuple;
|
||||
sender: MailAddress;
|
||||
sets: IdTuple[];
|
||||
}
|
||||
export const MailAddressTypeRef: TypeRef<MailAddress> = new TypeRef("tutanota", "MailAddress")
|
||||
|
||||
|
@ -1164,6 +1178,19 @@ export type MailAddressProperties = {
|
|||
mailAddress: string;
|
||||
senderName: string;
|
||||
}
|
||||
export const MailBagTypeRef: TypeRef<MailBag> = new TypeRef("tutanota", "MailBag")
|
||||
|
||||
export function createMailBag(values: StrippedEntity<MailBag>): MailBag {
|
||||
return Object.assign(create(typeModels.MailBag, MailBagTypeRef), values)
|
||||
}
|
||||
|
||||
export type MailBag = {
|
||||
_type: TypeRef<MailBag>;
|
||||
|
||||
_id: Id;
|
||||
|
||||
mails: Id;
|
||||
}
|
||||
export const MailBoxTypeRef: TypeRef<MailBox> = new TypeRef("tutanota", "MailBox")
|
||||
|
||||
export function createMailBox(values: StrippedEntity<MailBox>): MailBox {
|
||||
|
@ -1182,6 +1209,8 @@ export type MailBox = {
|
|||
_permissions: Id;
|
||||
lastInfoDate: Date;
|
||||
|
||||
archivedMailBags: MailBag[];
|
||||
currentMailBag: null | MailBag;
|
||||
folders: null | MailFolderRef;
|
||||
mailDetailsDrafts: null | MailDetailsDraftsRef;
|
||||
receivedAttachments: Id;
|
||||
|
@ -1274,8 +1303,11 @@ export type MailFolder = {
|
|||
_ownerKeyVersion: null | NumberString;
|
||||
_permissions: Id;
|
||||
folderType: NumberString;
|
||||
isLabel: boolean;
|
||||
isMailSet: boolean;
|
||||
name: string;
|
||||
|
||||
entries: Id;
|
||||
mails: Id;
|
||||
parentFolder: null | IdTuple;
|
||||
}
|
||||
|
@ -1292,6 +1324,22 @@ export type MailFolderRef = {
|
|||
|
||||
folders: Id;
|
||||
}
|
||||
export const MailSetEntryTypeRef: TypeRef<MailSetEntry> = new TypeRef("tutanota", "MailSetEntry")
|
||||
|
||||
export function createMailSetEntry(values: StrippedEntity<MailSetEntry>): MailSetEntry {
|
||||
return Object.assign(create(typeModels.MailSetEntry, MailSetEntryTypeRef), values)
|
||||
}
|
||||
|
||||
export type MailSetEntry = {
|
||||
_type: TypeRef<MailSetEntry>;
|
||||
|
||||
_format: NumberString;
|
||||
_id: IdTuple;
|
||||
_ownerGroup: null | Id;
|
||||
_permissions: Id;
|
||||
|
||||
mail: IdTuple;
|
||||
}
|
||||
export const MailboxGroupRootTypeRef: TypeRef<MailboxGroupRoot> = new TypeRef("tutanota", "MailboxGroupRoot")
|
||||
|
||||
export function createMailboxGroupRoot(values: StrippedEntity<MailboxGroupRoot>): MailboxGroupRoot {
|
||||
|
@ -1361,6 +1409,7 @@ export type MoveMailData = {
|
|||
_format: NumberString;
|
||||
|
||||
mails: IdTuple[];
|
||||
sourceFolder: null | IdTuple;
|
||||
targetFolder: IdTuple;
|
||||
}
|
||||
export const NewDraftAttachmentTypeRef: TypeRef<NewDraftAttachment> = new TypeRef("tutanota", "NewDraftAttachment")
|
||||
|
|
|
@ -101,7 +101,7 @@ import { BlobFacade } from "./BlobFacade.js"
|
|||
import { assertWorkerOrNode, isApp, isDesktop } from "../../../common/Env.js"
|
||||
import { EntityClient } from "../../../common/EntityClient.js"
|
||||
import { getEnabledMailAddressesForGroupInfo, getUserGroupMemberships } from "../../../common/utils/GroupUtils.js"
|
||||
import { containsId, elementIdPart, getLetId, isSameId, listIdPart, stringToCustomId } from "../../../common/utils/EntityUtils.js"
|
||||
import { containsId, elementIdPart, getElementId, getLetId, isSameId, listIdPart, stringToCustomId } from "../../../common/utils/EntityUtils.js"
|
||||
import { htmlToText } from "../../search/IndexUtils.js"
|
||||
import { MailBodyTooLargeError } from "../../../common/error/MailBodyTooLargeError.js"
|
||||
import { UNCOMPRESSED_MAX_SIZE } from "../../Compression.js"
|
||||
|
@ -349,8 +349,8 @@ export class MailFacade {
|
|||
return deferredUpdatePromiseWrapper.promise
|
||||
}
|
||||
|
||||
async moveMails(mails: IdTuple[], targetFolder: IdTuple): Promise<void> {
|
||||
await this.serviceExecutor.post(MoveMailService, createMoveMailData({ mails, targetFolder }))
|
||||
async moveMails(mails: IdTuple[], sourceFolder: IdTuple, targetFolder: IdTuple): Promise<void> {
|
||||
await this.serviceExecutor.post(MoveMailService, createMoveMailData({ mails, sourceFolder, targetFolder }))
|
||||
}
|
||||
|
||||
async reportMail(mail: Mail, reportType: MailReportType): Promise<void> {
|
||||
|
@ -644,11 +644,12 @@ export class MailFacade {
|
|||
await this.serviceExecutor.delete(MailFolderService, deleteMailFolderData, { sessionKey: "dummy" as any })
|
||||
}
|
||||
|
||||
async fixupCounterForMailList(groupId: Id, listId: Id, unreadMails: number): Promise<void> {
|
||||
async fixupCounterForFolder(groupId: Id, folder: MailFolder, unreadMails: number): Promise<void> {
|
||||
const counterId = folder.isMailSet ? getElementId(folder) : folder.mails
|
||||
const data = createWriteCounterData({
|
||||
counterType: CounterType.UnreadMails,
|
||||
row: groupId,
|
||||
column: listId,
|
||||
column: counterId,
|
||||
value: String(unreadMails),
|
||||
})
|
||||
await this.serviceExecutor.post(CounterService, data)
|
||||
|
|
|
@ -30,6 +30,7 @@ import { TokenOrNestedTokens } from "cborg/interface"
|
|||
import {
|
||||
CalendarEventTypeRef,
|
||||
FileTypeRef,
|
||||
MailBoxTypeRef,
|
||||
MailDetailsBlobTypeRef,
|
||||
MailDetailsDraftTypeRef,
|
||||
MailFolderTypeRef,
|
||||
|
@ -248,6 +249,20 @@ export class OfflineStorage implements CacheStorage, ExposedCacheStorage {
|
|||
return result?.entity ? this.deserialize(typeRef, result.entity.value as Uint8Array) : null
|
||||
}
|
||||
|
||||
async provideMultiple<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, elementIds: Id[]): Promise<Array<T>> {
|
||||
if (elementIds.length === 0) return []
|
||||
const type = getTypeId(typeRef)
|
||||
const serializedList: ReadonlyArray<Record<string, TaggedSqlValue>> = await this.allChunked(
|
||||
MAX_SAFE_SQL_VARS - 2,
|
||||
elementIds,
|
||||
(c) => sql`SELECT entity FROM list_entities WHERE type = ${type} AND listId = ${listId} AND elementId IN ${paramList(c)}`,
|
||||
)
|
||||
return this.deserializeList(
|
||||
typeRef,
|
||||
serializedList.map((r) => r.entity.value as Uint8Array),
|
||||
)
|
||||
}
|
||||
|
||||
async getIdsInRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<Array<Id>> {
|
||||
const type = getTypeId(typeRef)
|
||||
const range = await this.getRange(type, listId)
|
||||
|
@ -487,14 +502,25 @@ AND NOT(${firstIdBigger("elementId", upper)})`
|
|||
// lead to an overflow in our 42 bit timestamp in the id.
|
||||
const cutoffTimestamp = now - timeRangeMillisSafe
|
||||
const cutoffId = timestampToGeneratedId(cutoffTimestamp)
|
||||
const folders = await this.getListElementsOfType(MailFolderTypeRef)
|
||||
const folderSystem = new FolderSystem(folders)
|
||||
|
||||
for (const folder of folders) {
|
||||
if (isSpamOrTrashFolder(folderSystem, folder)) {
|
||||
await this.deleteMailList(folder.mails, GENERATED_MAX_ID)
|
||||
const mailBoxes = await this.getElementsOfType(MailBoxTypeRef)
|
||||
for (const mailBox of mailBoxes) {
|
||||
const isMailsetMigrated = mailBox.currentMailBag != null
|
||||
if (isMailsetMigrated) {
|
||||
var mailListIds = [mailBox.currentMailBag!, ...mailBox.archivedMailBags].map((mailbag) => mailbag.mails)
|
||||
for (const mailListId of mailListIds) {
|
||||
await this.deleteMailList(mailListId, cutoffId)
|
||||
}
|
||||
} else {
|
||||
await this.deleteMailList(folder.mails, cutoffId)
|
||||
const folders = await this.getWholeList(MailFolderTypeRef, mailBox.folders!.folders)
|
||||
|
||||
const folderSystem = new FolderSystem(folders)
|
||||
for (const folder of folders) {
|
||||
if (isSpamOrTrashFolder(folderSystem, folder)) {
|
||||
await this.deleteMailList(folder.mails, GENERATED_MAX_ID)
|
||||
} else {
|
||||
await this.deleteMailList(folder.mails, cutoffId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -684,6 +710,23 @@ AND NOT(${firstIdBigger("elementId", upper)})`
|
|||
await this.sqlCipherFacade.run(formattedQuery.query, formattedQuery.params)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* convenience method to execute a potentially too large query over several chunks.
|
||||
* chunkSize must be chosen such that the total number of SQL variables in the final query does not exceed MAX_SAFE_SQL_VARS
|
||||
* */
|
||||
private async allChunked(
|
||||
chunkSize: number,
|
||||
originalList: SqlValue[],
|
||||
formatter: (chunk: SqlValue[]) => FormattedQuery,
|
||||
): Promise<Array<Record<string, TaggedSqlValue>>> {
|
||||
const result: Array<Record<string, TaggedSqlValue>> = []
|
||||
for (const chunk of splitInChunks(chunkSize, originalList)) {
|
||||
const formattedQuery = formatter(chunk)
|
||||
result.push(...(await this.sqlCipherFacade.all(formattedQuery.query, formattedQuery.params)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -25,6 +25,8 @@ import { tutanota73 } from "./migrations/tutanota-v73.js"
|
|||
import { sys104 } from "./migrations/sys-v104.js"
|
||||
import { sys105 } from "./migrations/sys-v105.js"
|
||||
import { sys106 } from "./migrations/sys-v106.js"
|
||||
import { tutanota74 } from "./migrations/tutanota-v74.js"
|
||||
import { sys107 } from "./migrations/sys-v107.js"
|
||||
|
||||
export interface OfflineMigration {
|
||||
readonly app: VersionMetadataBaseKey
|
||||
|
@ -61,6 +63,8 @@ export const OFFLINE_STORAGE_MIGRATIONS: ReadonlyArray<OfflineMigration> = [
|
|||
sys104,
|
||||
sys105,
|
||||
sys106,
|
||||
tutanota74,
|
||||
sys107,
|
||||
]
|
||||
|
||||
const CURRENT_OFFLINE_VERSION = 1
|
||||
|
|
10
src/common/api/worker/offline/migrations/sys-v107.ts
Normal file
10
src/common/api/worker/offline/migrations/sys-v107.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { OfflineMigration } from "../OfflineStorageMigrator.js"
|
||||
import { OfflineStorage } from "../OfflineStorage.js"
|
||||
|
||||
export const sys107: OfflineMigration = {
|
||||
app: "sys",
|
||||
version: 107,
|
||||
async migrate(storage: OfflineStorage) {
|
||||
// only changes data transfer type
|
||||
},
|
||||
}
|
16
src/common/api/worker/offline/migrations/tutanota-v74.ts
Normal file
16
src/common/api/worker/offline/migrations/tutanota-v74.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { OfflineMigration } from "../OfflineStorageMigrator.js"
|
||||
import { OfflineStorage } from "../OfflineStorage.js"
|
||||
import { addValue, migrateAllElements, migrateAllListElements } from "../StandardMigrations.js"
|
||||
import { createMail, createMailBox, MailBoxTypeRef, MailFolderTypeRef, MailTypeRef } from "../../../entities/tutanota/TypeRefs.js"
|
||||
import { GENERATED_MIN_ID } from "../../../common/utils/EntityUtils.js"
|
||||
|
||||
export const tutanota74: OfflineMigration = {
|
||||
app: "tutanota",
|
||||
version: 74,
|
||||
async migrate(storage: OfflineStorage) {
|
||||
// the TutanotaModelV75 introduces MailSets to support import and labels
|
||||
await migrateAllListElements(MailFolderTypeRef, storage, [addValue("isLabel", "0"), addValue("isMailSet", "0"), addValue("entries", GENERATED_MIN_ID)])
|
||||
await migrateAllElements(MailBoxTypeRef, storage, [createMailBox]) // initialize mailbags
|
||||
await migrateAllListElements(MailTypeRef, storage, [createMail]) // initialize sets
|
||||
},
|
||||
}
|
|
@ -133,6 +133,10 @@ export class LateInitializedCacheStorageImpl implements CacheStorageLateInitiali
|
|||
return this.inner.provideFromRange(typeRef, listId, start, count, reverse)
|
||||
}
|
||||
|
||||
provideMultiple<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: string, elementIds: string[]): Promise<T[]> {
|
||||
return this.inner.provideMultiple(typeRef, listId, elementIds)
|
||||
}
|
||||
|
||||
getWholeList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<Array<T>> {
|
||||
return this.inner.getWholeList(typeRef, listId)
|
||||
}
|
||||
|
|
|
@ -115,6 +115,11 @@ export interface ExposedCacheStorage {
|
|||
*/
|
||||
provideFromRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, start: Id, count: number, reverse: boolean): Promise<T[]>
|
||||
|
||||
/**
|
||||
* Load a set of list element entities by id. Missing elements are not returned, no error is thrown.
|
||||
*/
|
||||
provideMultiple<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, elementIds: Id[]): Promise<Array<T>>
|
||||
|
||||
/**
|
||||
* retrieve all list elements that are in the cache
|
||||
* @param typeRef
|
||||
|
|
|
@ -222,6 +222,19 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
return result
|
||||
}
|
||||
|
||||
async provideMultiple<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, elementIds: Id[]): Promise<Array<T>> {
|
||||
const listCache = this.lists.get(typeRefToPath(typeRef))?.get(listId)
|
||||
|
||||
if (listCache == null) {
|
||||
return []
|
||||
}
|
||||
let result: T[] = []
|
||||
for (let a = 0; a < elementIds.length; a++) {
|
||||
result.push(clone(listCache.elements.get(elementIds[a]) as T))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async getRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<{ lower: Id; upper: Id } | null> {
|
||||
const listCache = this.lists.get(typeRefToPath(typeRef))?.get(listId)
|
||||
|
||||
|
|
|
@ -323,7 +323,6 @@ export class Indexer {
|
|||
const transaction = await this.db.dbFacade.createTransaction(false, [MetaDataOS, GroupDataOS])
|
||||
await transaction.put(MetaDataOS, Metadata.userEncDbKey, userEncDbKey.key)
|
||||
await transaction.put(MetaDataOS, Metadata.mailIndexingEnabled, this._mail.mailIndexingEnabled)
|
||||
await transaction.put(MetaDataOS, Metadata.excludedListIds, this._mail._excludedListIds)
|
||||
await transaction.put(MetaDataOS, Metadata.encDbIv, aes256EncryptSearchIndexEntry(this.db.key, this.db.iv))
|
||||
await transaction.put(MetaDataOS, Metadata.userGroupKeyVersion, userEncDbKey.encryptingKeyVersion)
|
||||
await transaction.put(MetaDataOS, Metadata.lastEventIndexTimeMs, this._entityRestClient.getRestClient().getServerTimestampMs())
|
||||
|
@ -336,7 +335,6 @@ export class Indexer {
|
|||
this.db.key = decryptKey(userGroupKey, metaData.userEncDbKey)
|
||||
this.db.iv = unauthenticatedAesDecrypt(this.db.key, neverNull(metaData.encDbIv), true)
|
||||
this._mail.mailIndexingEnabled = metaData.mailIndexingEnabled
|
||||
this._mail._excludedListIds = metaData.excludedListIds
|
||||
const groupDiff = await this._loadGroupDiff(user)
|
||||
await this._updateGroups(user, groupDiff)
|
||||
await this._mail.updateCurrentIndexTimestamp(user)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FULL_INDEXED_TIMESTAMP, MailFolderType, MailState, NOTHING_INDEXED_TIMESTAMP, OperationType } from "../../common/TutanotaConstants"
|
||||
import { FULL_INDEXED_TIMESTAMP, MailSetKind, MailState, NOTHING_INDEXED_TIMESTAMP, OperationType } from "../../common/TutanotaConstants"
|
||||
import type { File as TutanotaFile, Mail, MailBox, MailDetails, MailFolder } from "../../entities/tutanota/TypeRefs.js"
|
||||
import {
|
||||
FileTypeRef,
|
||||
|
@ -55,7 +55,6 @@ export class MailIndexer {
|
|||
mailboxIndexingPromise: Promise<void>
|
||||
isIndexing: boolean = false
|
||||
_indexingCancelled: boolean
|
||||
_excludedListIds: Id[]
|
||||
_core: IndexerCore
|
||||
_db: Db
|
||||
_entityRestClient: EntityRestClient
|
||||
|
@ -82,7 +81,6 @@ export class MailIndexer {
|
|||
this.mailIndexingEnabled = false
|
||||
this.mailboxIndexingPromise = Promise.resolve()
|
||||
this._indexingCancelled = false
|
||||
this._excludedListIds = []
|
||||
this._entityRestClient = entityRestClient
|
||||
this._dateProvider = dateProvider
|
||||
}
|
||||
|
@ -146,10 +144,6 @@ export class MailIndexer {
|
|||
mail: Mail
|
||||
keyToIndexEntries: Map<string, SearchIndexEntry[]>
|
||||
} | null> {
|
||||
if (this._isExcluded(event)) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
return this._defaultCachingEntity
|
||||
.load(MailTypeRef, [event.instanceListId, event.instanceId])
|
||||
.then(async (mail) => {
|
||||
|
@ -213,14 +207,10 @@ export class MailIndexer {
|
|||
return this._db.dbFacade.createTransaction(true, [ElementDataOS]).then((transaction) => {
|
||||
return transaction.get(ElementDataOS, encInstanceId).then((elementData) => {
|
||||
if (elementData) {
|
||||
if (this._isExcluded(event)) {
|
||||
return this._core._processDeleted(event, indexUpdate) // move to spam folder
|
||||
} else {
|
||||
indexUpdate.move.push({
|
||||
encInstanceId,
|
||||
newListId: event.instanceListId,
|
||||
})
|
||||
}
|
||||
indexUpdate.move.push({
|
||||
encInstanceId,
|
||||
newListId: event.instanceListId,
|
||||
})
|
||||
} else {
|
||||
// instance is moved but not yet indexed: handle as new for example moving a mail from non indexed folder like spam to indexed folder
|
||||
return this.processNewMail(event).then((result) => {
|
||||
|
@ -233,42 +223,34 @@ export class MailIndexer {
|
|||
})
|
||||
}
|
||||
|
||||
enableMailIndexing(user: User): Promise<void> {
|
||||
return this._db.dbFacade.createTransaction(true, [MetaDataOS]).then((t) => {
|
||||
return t.get(MetaDataOS, Metadata.mailIndexingEnabled).then((enabled) => {
|
||||
if (!enabled) {
|
||||
return promiseMap(filterMailMemberships(user), (mailGroupMembership) => this._getSpamFolder(mailGroupMembership)).then((spamFolders) => {
|
||||
this._excludedListIds = spamFolders.map((folder) => folder.mails)
|
||||
this.mailIndexingEnabled = true
|
||||
return this._db.dbFacade.createTransaction(false, [MetaDataOS]).then((t2) => {
|
||||
t2.put(MetaDataOS, Metadata.mailIndexingEnabled, true)
|
||||
t2.put(MetaDataOS, Metadata.excludedListIds, this._excludedListIds)
|
||||
async enableMailIndexing(user: User): Promise<void> {
|
||||
const t = await this._db.dbFacade.createTransaction(true, [MetaDataOS])
|
||||
const enabled = await t.get(MetaDataOS, Metadata.mailIndexingEnabled)
|
||||
if (!enabled) {
|
||||
this.mailIndexingEnabled = true
|
||||
const t2 = await this._db.dbFacade.createTransaction(false, [MetaDataOS])
|
||||
t2.put(MetaDataOS, Metadata.mailIndexingEnabled, true)
|
||||
t2.put(MetaDataOS, Metadata.excludedListIds, [])
|
||||
|
||||
// create index in background, termination is handled in Indexer.enableMailIndexing
|
||||
const oldestTimestamp = this._dateProvider.getStartOfDayShiftedBy(-INITIAL_MAIL_INDEX_INTERVAL_DAYS).getTime()
|
||||
// create index in background, termination is handled in Indexer.enableMailIndexing
|
||||
const oldestTimestamp = this._dateProvider.getStartOfDayShiftedBy(-INITIAL_MAIL_INDEX_INTERVAL_DAYS).getTime()
|
||||
|
||||
this.indexMailboxes(user, oldestTimestamp).catch(
|
||||
ofClass(CancelledError, (e) => {
|
||||
console.log("cancelled initial indexing", e)
|
||||
}),
|
||||
)
|
||||
return t2.wait()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return t.get(MetaDataOS, Metadata.excludedListIds).then((excludedListIds) => {
|
||||
this.mailIndexingEnabled = true
|
||||
this._excludedListIds = excludedListIds || []
|
||||
})
|
||||
}
|
||||
this.indexMailboxes(user, oldestTimestamp).catch(
|
||||
ofClass(CancelledError, (e) => {
|
||||
console.log("cancelled initial indexing", e)
|
||||
}),
|
||||
)
|
||||
return t2.wait()
|
||||
} else {
|
||||
return t.get(MetaDataOS, Metadata.excludedListIds).then((excludedListIds) => {
|
||||
this.mailIndexingEnabled = true
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
disableMailIndexing(userId: Id): Promise<void> {
|
||||
this.mailIndexingEnabled = false
|
||||
this._indexingCancelled = true
|
||||
this._excludedListIds = []
|
||||
return this._db.dbFacade.deleteDatabase(b64UserIdHash(userId))
|
||||
}
|
||||
|
||||
|
@ -580,24 +562,17 @@ export class MailIndexer {
|
|||
})
|
||||
}
|
||||
|
||||
_isExcluded(event: EntityUpdate): boolean {
|
||||
return this._excludedListIds.indexOf(event.instanceListId) !== -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides all non-excluded mail list ids of the given mailbox
|
||||
* Provides all mail list ids of the given mailbox
|
||||
*/
|
||||
async _loadMailListIds(mailbox: MailBox): Promise<Id[]> {
|
||||
const folders = await this._defaultCachingEntity.loadAll(MailFolderTypeRef, neverNull(mailbox.folders).folders)
|
||||
const mailListIds: Id[] = []
|
||||
|
||||
for (const folder of folders) {
|
||||
if (!this._excludedListIds.includes(folder.mails)) {
|
||||
mailListIds.push(folder.mails)
|
||||
}
|
||||
const isMailsetMigrated = mailbox.currentMailBag != null
|
||||
if (isMailsetMigrated) {
|
||||
return [mailbox.currentMailBag!, ...mailbox.archivedMailBags].map((mailbag) => mailbag.mails)
|
||||
} else {
|
||||
const folders = await this._defaultCachingEntity.loadAll(MailFolderTypeRef, neverNull(mailbox.folders).folders)
|
||||
return folders.map((f) => f.mails)
|
||||
}
|
||||
|
||||
return mailListIds
|
||||
}
|
||||
|
||||
_getSpamFolder(mailGroup: GroupMembership): Promise<MailFolder> {
|
||||
|
@ -607,7 +582,7 @@ export class MailIndexer {
|
|||
.then((mbox) => {
|
||||
return this._defaultCachingEntity
|
||||
.loadAll(MailFolderTypeRef, neverNull(mbox.folders).folders)
|
||||
.then((folders) => neverNull(folders.find((folder) => folder.folderType === MailFolderType.SPAM)))
|
||||
.then((folders) => neverNull(folders.find((folder) => folder.folderType === MailSetKind.SPAM)))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { MailTypeRef } from "../../entities/tutanota/TypeRefs.js"
|
||||
import { DbTransaction } from "./DbFacade"
|
||||
import { resolveTypeReference } from "../../common/EntityFunctions"
|
||||
import type { PromiseMapFn } from "@tutao/tutanota-utils"
|
||||
import {
|
||||
arrayHash,
|
||||
asyncFind,
|
||||
|
@ -9,11 +8,13 @@ import {
|
|||
downcast,
|
||||
getDayShifted,
|
||||
getStartOfDay,
|
||||
isNotEmpty,
|
||||
isNotNull,
|
||||
isSameTypeRef,
|
||||
neverNull,
|
||||
promiseMap,
|
||||
promiseMapCompat,
|
||||
PromiseMapFn,
|
||||
tokenize,
|
||||
TypeRef,
|
||||
uint8ArrayToBase64,
|
||||
|
@ -47,7 +48,7 @@ import {
|
|||
typeRefToTypeInfo,
|
||||
} from "./IndexUtils"
|
||||
import { FULL_INDEXED_TIMESTAMP, NOTHING_INDEXED_TIMESTAMP } from "../../common/TutanotaConstants"
|
||||
import { compareNewestFirst, firstBiggerThanSecond, timestampToGeneratedId } from "../../common/utils/EntityUtils"
|
||||
import { compareNewestFirst, elementIdPart, firstBiggerThanSecond, getListId, timestampToGeneratedId } from "../../common/utils/EntityUtils"
|
||||
import { INITIAL_MAIL_INDEX_INTERVAL_DAYS, MailIndexer } from "./MailIndexer"
|
||||
import { SuggestionFacade } from "./SuggestionFacade"
|
||||
import { AssociationType, Cardinality, ValueType } from "../../common/EntityConstants.js"
|
||||
|
@ -490,7 +491,6 @@ export class SearchFacade {
|
|||
* Reduces the search result by filtering out all mailIds that don't match all search tokens
|
||||
*/
|
||||
_filterByEncryptedId(results: KeyToEncryptedIndexEntries[]): KeyToEncryptedIndexEntries[] {
|
||||
// let matchingEncIds = null
|
||||
let matchingEncIds: Set<number> | null = null
|
||||
for (const keyToEncryptedIndexEntry of results) {
|
||||
if (matchingEncIds == null) {
|
||||
|
@ -626,37 +626,62 @@ export class SearchFacade {
|
|||
// Use separate array to only sort new results and not all of them.
|
||||
return this._db.dbFacade
|
||||
.createTransaction(true, [ElementDataOS])
|
||||
.then(
|
||||
(
|
||||
transaction, // As an attempt to optimize search we look for items in parallel. Promise.map iterates in arbitrary order!
|
||||
) =>
|
||||
// BUT! we have to look at all of them! Otherwise we may return them in the wrong order. We cannot return elements 10, 15, 20 if we didn't
|
||||
// return element 5 first, no one will ask for it later.
|
||||
// The best thing performance-wise would be to split into chunks of certain length and process them in parallel and stop after certain chunk.
|
||||
promiseMap(
|
||||
indexEntries.slice(0, maxResults || indexEntries.length + 1),
|
||||
(entry, index) => {
|
||||
return transaction.get(ElementDataOS, uint8ArrayToBase64(entry.encId)).then((elementData: ElementDataDbRow | null) => {
|
||||
// mark result index id as processed to not query result in next load more operation
|
||||
entriesCopy[index] = null
|
||||
|
||||
if (
|
||||
elementData &&
|
||||
(!(searchResult.restriction.listIds.length > 0) || searchResult.restriction.listIds.includes(elementData[0]))
|
||||
) {
|
||||
return [elementData[0], entry.id] as IdTuple
|
||||
}
|
||||
.then((transaction) =>
|
||||
// As an attempt to optimize search we look for items in parallel. Promise.map iterates in arbitrary order!
|
||||
// BUT! we have to look at all of them! Otherwise, we may return them in the wrong order.
|
||||
// We cannot return elements 10, 15, 20 if we didn't return element 5 first, no one will ask for it later.
|
||||
// The best thing performance-wise would be to split into chunks of certain length and process them in parallel and stop after certain chunk.
|
||||
promiseMap(
|
||||
indexEntries.slice(0, maxResults || indexEntries.length + 1),
|
||||
async (entry, index) => {
|
||||
return transaction.get(ElementDataOS, uint8ArrayToBase64(entry.encId)).then((elementData: ElementDataDbRow | null) => {
|
||||
// mark result index id as processed to not query result in next load more operation
|
||||
entriesCopy[index] = null
|
||||
|
||||
if (elementData) {
|
||||
return [elementData[0], entry.id] as IdTuple
|
||||
} else {
|
||||
return null
|
||||
})
|
||||
},
|
||||
{
|
||||
concurrency: 5,
|
||||
},
|
||||
),
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
concurrency: 5,
|
||||
},
|
||||
),
|
||||
)
|
||||
.then((intermediateResults) => intermediateResults.filter(isNotNull))
|
||||
.then(async (intermediateResults) => {
|
||||
// apply folder restrictions to intermediateResults
|
||||
|
||||
if (!(searchResult.restriction.folderIds.length > 0)) {
|
||||
// no folder restrictions (ALL)
|
||||
return intermediateResults
|
||||
} else {
|
||||
// some folder restrictions (e.g. INBOX)
|
||||
|
||||
// With the new mailSet architecture (static mail lists) we need to load every mail
|
||||
// in order to check in which mailSet (folder) a mail is included in.
|
||||
const mails = await Promise.all(
|
||||
intermediateResults.map((intermediateResultId) => this._entityClient.load(MailTypeRef, intermediateResultId)),
|
||||
)
|
||||
return mails
|
||||
.filter((mail) => {
|
||||
let folderIds: Array<Id>
|
||||
if (isNotEmpty(mail.sets)) {
|
||||
// new mailSet folders
|
||||
folderIds = mail.sets.map((setId) => elementIdPart(setId))
|
||||
} else {
|
||||
// legacy mail folder (mail list)
|
||||
folderIds = [getListId(mail)]
|
||||
}
|
||||
return folderIds.some((folderId) => searchResult.restriction.folderIds.includes(folderId))
|
||||
})
|
||||
.map((mail) => mail._id)
|
||||
}
|
||||
})
|
||||
.then((newResults) => {
|
||||
searchResult.results.push(...(newResults.filter(isNotNull) as IdTuple[]))
|
||||
searchResult.results.push(...(newResults as IdTuple[]))
|
||||
searchResult.moreResults = entriesCopy.filter(isNotNull)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -129,8 +129,8 @@ export type SearchRestriction = {
|
|||
field: string | null
|
||||
// must be kept in sync with attributeIds
|
||||
attributeIds: number[] | null
|
||||
// if empty, match anything. otherwise it's an OR-match.
|
||||
listIds: Array<Id>
|
||||
// list of locations (calendars, folders, labels to search). if empty, match anything. otherwise it's an OR-match.
|
||||
folderIds: Array<Id>
|
||||
// if true, include repeating events in the search
|
||||
eventSeries: boolean | null
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
MailBoxTypeRef,
|
||||
MailFolder,
|
||||
MailFolderTypeRef,
|
||||
MailSetEntryTypeRef,
|
||||
MailTypeRef,
|
||||
} from "../api/entities/tutanota/TypeRefs.js"
|
||||
import { Group, GroupInfo, GroupInfoTypeRef, GroupMembership, GroupTypeRef, WebsocketCounterData } from "../api/entities/sys/TypeRefs.js"
|
||||
|
@ -23,18 +24,18 @@ import { EntityClient } from "../api/common/EntityClient.js"
|
|||
import { LoginController } from "../api/main/LoginController.js"
|
||||
import { WebsocketConnectivityModel } from "../misc/WebsocketConnectivityModel.js"
|
||||
import { InboxRuleHandler } from "../../mail-app/mail/model/InboxRuleHandler.js"
|
||||
import { assertNotNull, groupBy, lazyMemoized, neverNull, noOp, ofClass, promiseMap, splitInChunks } from "@tutao/tutanota-utils"
|
||||
import { assertNotNull, first, groupBy, isNotEmpty, lazyMemoized, neverNull, noOp, ofClass, promiseMap, splitInChunks } from "@tutao/tutanota-utils"
|
||||
import {
|
||||
FeatureType,
|
||||
MailFolderType,
|
||||
MailReportType,
|
||||
MailSetKind,
|
||||
MAX_NBR_MOVE_DELETE_MAIL_SERVICE,
|
||||
OperationType,
|
||||
ReportMovedMailsType,
|
||||
} from "../api/common/TutanotaConstants.js"
|
||||
import { assertSystemFolderOfType, getEnabledMailAddressesWithUser } from "./SharedMailUtils.js"
|
||||
import { LockedError, NotFoundError, PreconditionFailedError } from "../api/common/error/RestError.js"
|
||||
import { elementIdPart, GENERATED_MAX_ID, getElementId, getListId, isSameId, listIdPart } from "../api/common/utils/EntityUtils.js"
|
||||
import { CUSTOM_MIN_ID, elementIdPart, GENERATED_MAX_ID, getElementId, getListId, isSameId } from "../api/common/utils/EntityUtils.js"
|
||||
import { containsEventOfType, EntityUpdateData, isUpdateForTypeRef } from "../api/common/utils/EntityUpdateUtils.js"
|
||||
import m from "mithril"
|
||||
import { lang } from "../misc/LanguageViewModel.js"
|
||||
|
@ -130,12 +131,10 @@ export class MailModel {
|
|||
return this.entityClient.loadAll(MailFolderTypeRef, folderListId).then((folders) => {
|
||||
return folders.filter((f) => {
|
||||
// We do not show spam or archive for external users
|
||||
if (!this.logins.isInternalUserLoggedIn() && (f.folderType === MailFolderType.SPAM || f.folderType === MailFolderType.ARCHIVE)) {
|
||||
return false
|
||||
} else if (this.logins.isEnabled(FeatureType.InternalCommunication) && f.folderType === MailFolderType.SPAM) {
|
||||
if (!this.logins.isInternalUserLoggedIn() && (f.folderType === MailSetKind.SPAM || f.folderType === MailSetKind.ARCHIVE)) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
return !(this.logins.isEnabled(FeatureType.InternalCommunication) && f.folderType === MailSetKind.SPAM)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -163,15 +162,20 @@ export class MailModel {
|
|||
}
|
||||
}
|
||||
|
||||
getMailboxDetailsForMail(mail: Mail): Promise<MailboxDetail | null> {
|
||||
return this.getMailboxDetailsForMailListId(mail._id[0])
|
||||
async getMailboxDetailsForMail(mail: Mail): Promise<MailboxDetail | null> {
|
||||
const mailboxDetails = await this.getMailboxDetails()
|
||||
const detail = mailboxDetails.find((md) => md.folders.getFolderByMail(mail)) ?? null
|
||||
if (detail == null) {
|
||||
console.warn("Mailbox detail for mail does not exist", mail)
|
||||
}
|
||||
return detail
|
||||
}
|
||||
|
||||
async getMailboxDetailsForMailListId(mailListId: Id): Promise<MailboxDetail | null> {
|
||||
async getMailboxDetailsForMailFolder(mailFolder: MailFolder): Promise<MailboxDetail | null> {
|
||||
const mailboxDetails = await this.getMailboxDetails()
|
||||
const detail = mailboxDetails.find((md) => md.folders.getFolderByMailListId(mailListId)) ?? null
|
||||
const detail = mailboxDetails.find((md) => md.folders.getFolderById(getElementId(mailFolder))) ?? null
|
||||
if (detail == null) {
|
||||
console.warn("Mailbox detail for mail list does not exist", mailListId)
|
||||
console.warn("Mailbox detail for mail folder does not exist", mailFolder)
|
||||
}
|
||||
return detail
|
||||
}
|
||||
|
@ -180,30 +184,36 @@ export class MailModel {
|
|||
const mailboxDetails = await this.getMailboxDetails()
|
||||
return assertNotNull(
|
||||
mailboxDetails.find((md) => mailGroupId === md.mailGroup._id),
|
||||
"No mailbox details for mail group",
|
||||
"Mailbox detail for mail group does not exist",
|
||||
)
|
||||
}
|
||||
|
||||
async getUserMailboxDetails(): Promise<MailboxDetail> {
|
||||
const userMailGroupMembership = this.logins.getUserController().getUserMailGroupMembership()
|
||||
const mailboxDetails = await this.getMailboxDetails()
|
||||
return assertNotNull(mailboxDetails.find((md) => md.mailGroup._id === userMailGroupMembership.group))
|
||||
return assertNotNull(
|
||||
mailboxDetails.find((md) => md.mailGroup._id === userMailGroupMembership.group),
|
||||
"Mailbox detail for user does not exist",
|
||||
)
|
||||
}
|
||||
|
||||
getMailboxFolders(mail: Mail): Promise<FolderSystem | null> {
|
||||
async getMailboxFolders(mail: Mail): Promise<FolderSystem | null> {
|
||||
return this.getMailboxDetailsForMail(mail).then((md) => md && md.folders)
|
||||
}
|
||||
|
||||
getMailFolder(mailListId: Id): MailFolder | null {
|
||||
getMailFolderForMail(mail: Mail): MailFolder | null {
|
||||
const mailboxDetails = this.mailboxDetails() || []
|
||||
|
||||
let foundFolder: MailFolder | null = null
|
||||
for (let detail of mailboxDetails) {
|
||||
const f = detail.folders.getFolderByMailListId(mailListId)
|
||||
if (f) {
|
||||
return f
|
||||
if (isNotEmpty(mail.sets)) {
|
||||
foundFolder = detail.folders.getFolderById(elementIdPart(mail.sets[0]))
|
||||
} else {
|
||||
foundFolder = detail.folders.getFolderByMail(mail)
|
||||
}
|
||||
}
|
||||
|
||||
if (foundFolder != null) return foundFolder
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -211,14 +221,14 @@ export class MailModel {
|
|||
* Sends the given folder and all its descendants to the spam folder, reporting mails (if applicable) and removes any empty folders
|
||||
*/
|
||||
async sendFolderToSpam(folder: MailFolder): Promise<void> {
|
||||
const mailboxDetail = await this.getMailboxDetailsForMailListId(folder.mails)
|
||||
const mailboxDetail = await this.getMailboxDetailsForMailFolder(folder)
|
||||
if (mailboxDetail == null) {
|
||||
return
|
||||
}
|
||||
|
||||
let deletedFolder = await this.removeAllEmpty(mailboxDetail, folder)
|
||||
if (!deletedFolder) {
|
||||
return this.mailFacade.updateMailFolderParent(folder, assertSystemFolderOfType(mailboxDetail.folders, MailFolderType.SPAM)._id)
|
||||
return this.mailFacade.updateMailFolderParent(folder, assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.SPAM)._id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,13 +239,14 @@ export class MailModel {
|
|||
}
|
||||
|
||||
/**
|
||||
* Finally deletes all given mails. Caller must ensure that mails are only from one folder
|
||||
* Finally move all given mails. Caller must ensure that mails are only from
|
||||
* * 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> {
|
||||
let moveMails = mails.filter((m) => m._id[0] !== targetMailFolder.mails && targetMailFolder._ownerGroup === m._ownerGroup) // prevent moving mails between mail boxes.
|
||||
|
||||
// Do not move if target is the same as the current mailFolder
|
||||
const sourceMailFolder = this.getMailFolder(getListId(mails[0]))
|
||||
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(
|
||||
|
@ -244,7 +255,7 @@ export class MailModel {
|
|||
)
|
||||
|
||||
for (const mailChunk of mailChunks) {
|
||||
await this.mailFacade.moveMails(mailChunk, targetMailFolder._id)
|
||||
await this.mailFacade.moveMails(mailChunk, sourceMailFolder._id, targetMailFolder._id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -255,16 +266,20 @@ export class MailModel {
|
|||
*/
|
||||
async moveMails(mails: ReadonlyArray<Mail>, targetMailFolder: MailFolder): Promise<void> {
|
||||
const mailsPerFolder = groupBy(mails, (mail) => {
|
||||
return getListId(mail)
|
||||
return isNotEmpty(mail.sets) ? elementIdPart(mail.sets[0]) : getListId(mail)
|
||||
})
|
||||
|
||||
for (const [listId, mails] of mailsPerFolder) {
|
||||
const sourceMailFolder = this.getMailFolder(listId)
|
||||
for (const [folderId, mailsInFolder] of mailsPerFolder) {
|
||||
const sourceMailFolder = this.getMailFolderForMail(mailsInFolder[0])
|
||||
|
||||
if (sourceMailFolder) {
|
||||
await this._moveMails(mails, targetMailFolder)
|
||||
// group another time because mails in the same Set can be from different mail bags.
|
||||
const mailsPerList = groupBy(mailsInFolder, (mail) => getListId(mail))
|
||||
for (const [listId, mailsInList] of mailsPerList) {
|
||||
await this._moveMails(mailsInList, targetMailFolder)
|
||||
}
|
||||
} else {
|
||||
console.log("Move mail: no mail folder for list id", listId)
|
||||
console.log("Move mail: no mail folder for folder id", folderId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -296,30 +311,34 @@ export class MailModel {
|
|||
* A deletion confirmation must have been show before.
|
||||
*/
|
||||
async deleteMails(mails: ReadonlyArray<Mail>): Promise<void> {
|
||||
const mailsPerFolder = groupBy(mails, (mail) => {
|
||||
return getListId(mail)
|
||||
})
|
||||
|
||||
if (mails.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const mailsPerFolder = groupBy(mails, (mail) => {
|
||||
return isNotEmpty(mail.sets) ? elementIdPart(mail.sets[0]) : getListId(mail)
|
||||
})
|
||||
|
||||
const folders = await this.getMailboxFolders(mails[0])
|
||||
if (folders == null) {
|
||||
return
|
||||
}
|
||||
const trashFolder = assertNotNull(folders.getSystemFolderByType(MailFolderType.TRASH))
|
||||
const trashFolder = assertNotNull(folders.getSystemFolderByType(MailSetKind.TRASH))
|
||||
|
||||
for (const [listId, mails] of mailsPerFolder) {
|
||||
const sourceMailFolder = this.getMailFolder(listId)
|
||||
for (const [folder, mailsInFolder] of mailsPerFolder) {
|
||||
const sourceMailFolder = this.getMailFolderForMail(mailsInFolder[0])
|
||||
|
||||
if (sourceMailFolder) {
|
||||
if (isSpamOrTrashFolder(folders, sourceMailFolder)) {
|
||||
await this._finallyDeleteMails(mails)
|
||||
const mailsPerList = groupBy(mailsInFolder, (mail) => getListId(mail))
|
||||
for (const [listId, mailsInList] of mailsPerList) {
|
||||
if (sourceMailFolder) {
|
||||
if (isSpamOrTrashFolder(folders, sourceMailFolder)) {
|
||||
await this._finallyDeleteMails(mailsInList)
|
||||
} else {
|
||||
await this._moveMails(mailsInList, trashFolder)
|
||||
}
|
||||
} else {
|
||||
await this._moveMails(mails, trashFolder)
|
||||
console.log("Delete mail: no mail folder for list id", folder)
|
||||
}
|
||||
} else {
|
||||
console.log("Delete mail: no mail folder for list id", listId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -329,7 +348,7 @@ export class MailModel {
|
|||
*/
|
||||
async _finallyDeleteMails(mails: Mail[]): Promise<void> {
|
||||
if (!mails.length) return Promise.resolve()
|
||||
const mailFolder = neverNull(this.getMailFolder(getListId(mails[0])))
|
||||
const mailFolder = neverNull(this.getMailFolderForMail(mails[0]))
|
||||
const mailIds = mails.map((m) => m._id)
|
||||
const mailChunks = splitInChunks(MAX_NBR_MOVE_DELETE_MAIL_SERVICE, mailIds)
|
||||
|
||||
|
@ -356,16 +375,20 @@ export class MailModel {
|
|||
await this._init()
|
||||
m.redraw()
|
||||
}
|
||||
} else if (isUpdateForTypeRef(MailTypeRef, update) && update.operation === OperationType.CREATE) {
|
||||
} else if (
|
||||
isUpdateForTypeRef(MailTypeRef, update) &&
|
||||
update.operation === OperationType.CREATE &&
|
||||
!containsEventOfType(updates, OperationType.DELETE, update.instanceId)
|
||||
) {
|
||||
if (this.inboxRuleHandler && this.connectivityModel) {
|
||||
const folder = this.getMailFolder(update.instanceListId)
|
||||
const mailId: IdTuple = [update.instanceListId, update.instanceId]
|
||||
const mail = await this.entityClient.load(MailTypeRef, mailId)
|
||||
const folder = this.getMailFolderForMail(mail)
|
||||
|
||||
if (folder && folder.folderType === MailFolderType.INBOX && !containsEventOfType(updates, OperationType.DELETE, update.instanceId)) {
|
||||
if (folder && folder.folderType === MailSetKind.INBOX) {
|
||||
// If we don't find another delete operation on this email in the batch, then it should be a create operation,
|
||||
// otherwise it's a move
|
||||
const mailId: IdTuple = [update.instanceListId, update.instanceId]
|
||||
const mail = await this.entityClient.load(MailTypeRef, mailId)
|
||||
await this.getMailboxDetailsForMailListId(update.instanceListId)
|
||||
await this.getMailboxDetailsForMail(mail)
|
||||
.then((mailboxDetail) => {
|
||||
// We only apply rules on server if we are the leader in case of incoming messages
|
||||
return (
|
||||
|
@ -377,7 +400,13 @@ export class MailModel {
|
|||
)
|
||||
)
|
||||
})
|
||||
.then((newId) => this._showNotification(newId || mailId))
|
||||
.then((newFolderAndMail) => {
|
||||
if (newFolderAndMail) {
|
||||
this._showNotification(newFolderAndMail.folder, newFolderAndMail.mail)
|
||||
} else {
|
||||
this._showNotification(folder, mail)
|
||||
}
|
||||
})
|
||||
.catch(noOp)
|
||||
}
|
||||
}
|
||||
|
@ -389,13 +418,13 @@ export class MailModel {
|
|||
const normalized = this.mailboxCounters() || {}
|
||||
const group = normalized[counters.mailGroup] || {}
|
||||
for (const value of counters.counterValues) {
|
||||
group[value.mailListId] = Number(value.count) || 0
|
||||
group[value.counterId] = Number(value.count) || 0
|
||||
}
|
||||
normalized[counters.mailGroup] = group
|
||||
this.mailboxCounters(normalized)
|
||||
}
|
||||
|
||||
_showNotification(mailId: IdTuple) {
|
||||
_showNotification(folder: MailFolder, mail: Mail) {
|
||||
this.notifications.showNotification(
|
||||
NotificationType.Mail,
|
||||
lang.get("newMails_msg"),
|
||||
|
@ -403,21 +432,25 @@ export class MailModel {
|
|||
actions: [],
|
||||
},
|
||||
(_) => {
|
||||
m.route.set(`/mail/${listIdPart(mailId)}/${elementIdPart(mailId)}`)
|
||||
m.route.set(`/mail/${getElementId(folder)}/${getElementId(mail)}`)
|
||||
window.focus()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
getCounterValue(listId: Id): Promise<number | null> {
|
||||
return this.getMailboxDetailsForMailListId(listId)
|
||||
getCounterValue(folder: MailFolder): Promise<number | null> {
|
||||
return this.getMailboxDetailsForMailFolder(folder)
|
||||
.then((mailboxDetails) => {
|
||||
if (mailboxDetails == null) {
|
||||
return null
|
||||
} else {
|
||||
const counters = this.mailboxCounters()
|
||||
const mailGroupCounter = counters[mailboxDetails.mailGroup._id]
|
||||
return mailGroupCounter && mailGroupCounter[listId]
|
||||
const mailGroupCounter = this.mailboxCounters()[mailboxDetails.mailGroup._id]
|
||||
if (mailGroupCounter) {
|
||||
const counterId = folder.isMailSet ? getElementId(folder) : folder.mails
|
||||
return mailGroupCounter[counterId]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => null)
|
||||
|
@ -437,13 +470,13 @@ export class MailModel {
|
|||
* Sends the given folder and all its descendants to the trash folder, removes any empty folders
|
||||
*/
|
||||
async trashFolderAndSubfolders(folder: MailFolder): Promise<void> {
|
||||
const mailboxDetail = await this.getMailboxDetailsForMailListId(folder.mails)
|
||||
const mailboxDetail = await this.getMailboxDetailsForMailFolder(folder)
|
||||
if (mailboxDetail == null) {
|
||||
return
|
||||
}
|
||||
let deletedFolder = await this.removeAllEmpty(mailboxDetail, folder)
|
||||
if (!deletedFolder) {
|
||||
const trash = assertSystemFolderOfType(mailboxDetail.folders, MailFolderType.TRASH)
|
||||
const trash = assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.TRASH)
|
||||
return this.mailFacade.updateMailFolderParent(folder, trash._id)
|
||||
}
|
||||
}
|
||||
|
@ -460,9 +493,8 @@ export class MailModel {
|
|||
// we don't update folder system quickly enough so we keep track of deleted folders here and consider them "empty" when all their children are here
|
||||
const deleted = new Set<Id>()
|
||||
for (const descendant of descendants) {
|
||||
// Only load one mail, if there is even one we won't remove
|
||||
if (
|
||||
(await this.entityClient.loadRange(MailTypeRef, descendant.folder.mails, GENERATED_MAX_ID, 1, true)).length === 0 &&
|
||||
(await this.isEmptyFolder(descendant.folder)) &&
|
||||
mailboxDetail.folders.getCustomFoldersOfParent(descendant.folder._id).every((f) => deleted.has(getElementId(f)))
|
||||
) {
|
||||
deleted.add(getElementId(descendant.folder))
|
||||
|
@ -471,9 +503,8 @@ export class MailModel {
|
|||
someNonEmpty = true
|
||||
}
|
||||
}
|
||||
// Only load one mail, if there is even one we won't remove
|
||||
if (
|
||||
(await this.entityClient.loadRange(MailTypeRef, folder.mails, GENERATED_MAX_ID, 1, true)).length === 0 &&
|
||||
(await this.isEmptyFolder(folder)) &&
|
||||
mailboxDetail.folders.getCustomFoldersOfParent(folder._id).every((f) => deleted.has(getElementId(f))) &&
|
||||
!someNonEmpty
|
||||
) {
|
||||
|
@ -484,8 +515,17 @@ export class MailModel {
|
|||
}
|
||||
}
|
||||
|
||||
// Only load one mail, if there is even one we won't remove
|
||||
private async isEmptyFolder(descendant: MailFolder) {
|
||||
if (descendant.isMailSet) {
|
||||
return (await this.entityClient.loadRange(MailSetEntryTypeRef, descendant.entries, CUSTOM_MIN_ID, 1, false)).length === 0
|
||||
} else {
|
||||
return (await this.entityClient.loadRange(MailTypeRef, descendant.mails, GENERATED_MAX_ID, 1, true)).length === 0
|
||||
}
|
||||
}
|
||||
|
||||
public async finallyDeleteCustomMailFolder(folder: MailFolder): Promise<void> {
|
||||
if (folder.folderType !== MailFolderType.CUSTOM) {
|
||||
if (folder.folderType !== MailSetKind.CUSTOM) {
|
||||
throw new ProgrammingError("Cannot delete non-custom folder: " + String(folder._id))
|
||||
}
|
||||
|
||||
|
@ -499,9 +539,11 @@ export class MailModel {
|
|||
)
|
||||
}
|
||||
|
||||
async fixupCounterForMailList(listId: Id, unreadMails: number) {
|
||||
const mailboxDetails = await this.getMailboxDetailsForMailListId(listId)
|
||||
mailboxDetails && (await this.mailFacade.fixupCounterForMailList(mailboxDetails.mailGroup._id, listId, unreadMails))
|
||||
async fixupCounterForFolder(folder: MailFolder, unreadMails: number) {
|
||||
const mailboxDetails = await this.getMailboxDetailsForMailFolder(folder)
|
||||
if (mailboxDetails) {
|
||||
await this.mailFacade.fixupCounterForFolder(mailboxDetails.mailGroup._id, folder, unreadMails)
|
||||
}
|
||||
}
|
||||
|
||||
async clearFolder(folder: MailFolder): Promise<void> {
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
MailDetails,
|
||||
MailTypeRef,
|
||||
} from "../api/entities/tutanota/TypeRefs.js"
|
||||
import { ApprovalStatus, ConversationType, MailFolderType, MailMethod, MAX_ATTACHMENT_SIZE, OperationType, ReplyType } from "../api/common/TutanotaConstants.js"
|
||||
import { ApprovalStatus, ConversationType, MailSetKind, MailMethod, MAX_ATTACHMENT_SIZE, OperationType, ReplyType } from "../api/common/TutanotaConstants.js"
|
||||
import { PartialRecipient, Recipient, RecipientList, Recipients, RecipientType } from "../api/common/recipients/Recipient.js"
|
||||
import {
|
||||
assertNotNull,
|
||||
|
@ -917,8 +917,8 @@ export class SendMailModel {
|
|||
|
||||
private async isMailInTrashOrSpam(draft: Mail): Promise<boolean> {
|
||||
const folders = await this.mailModel.getMailboxFolders(draft)
|
||||
const mailFolder = folders?.getFolderByMailListId(getListId(draft))
|
||||
return !!mailFolder && (mailFolder.folderType === MailFolderType.TRASH || mailFolder.folderType === MailFolderType.SPAM)
|
||||
const mailFolder = folders?.getFolderByMail(draft)
|
||||
return !!mailFolder && (mailFolder.folderType === MailSetKind.TRASH || mailFolder.folderType === MailSetKind.SPAM)
|
||||
}
|
||||
|
||||
private sendApprovalMail(body: string): Promise<unknown> {
|
||||
|
|
|
@ -14,14 +14,14 @@ import {
|
|||
TutanotaProperties,
|
||||
} from "../api/entities/tutanota/TypeRefs.js"
|
||||
import { fullNameToFirstAndLastName, mailAddressToFirstAndLastName } from "../misc/parsing/MailAddressParser.js"
|
||||
import { assertNotNull, contains, endsWith, first, neverNull } from "@tutao/tutanota-utils"
|
||||
import { assertNotNull, contains, endsWith, first, isNotEmpty, neverNull } from "@tutao/tutanota-utils"
|
||||
import {
|
||||
ContactAddressType,
|
||||
ConversationType,
|
||||
EncryptionAuthStatus,
|
||||
getMailFolderType,
|
||||
GroupType,
|
||||
MailFolderType,
|
||||
MailSetKind,
|
||||
MailState,
|
||||
MAX_ATTACHMENT_SIZE,
|
||||
ReplyType,
|
||||
|
@ -36,7 +36,7 @@ import { Icons } from "../gui/base/icons/Icons.js"
|
|||
import { MailboxDetail, MailModel } from "./MailModel.js"
|
||||
import { LoginController } from "../api/main/LoginController.js"
|
||||
import { EntityClient } from "../api/common/EntityClient.js"
|
||||
import { getListId } from "../api/common/utils/EntityUtils.js"
|
||||
import { getListId, isSameId } from "../api/common/utils/EntityUtils.js"
|
||||
import type { FolderSystem, IndentedFolder } from "../api/common/mail/FolderSystem.js"
|
||||
import { MailFacade } from "../api/worker/facades/lazy/MailFacade.js"
|
||||
import { ListFilter } from "../misc/ListModel.js"
|
||||
|
@ -170,27 +170,27 @@ export function getFolderName(folder: MailFolder): string {
|
|||
}
|
||||
}
|
||||
|
||||
export function getFolderIconByType(folderType: MailFolderType): AllIcons {
|
||||
export function getFolderIconByType(folderType: MailSetKind): AllIcons {
|
||||
switch (folderType) {
|
||||
case MailFolderType.CUSTOM:
|
||||
case MailSetKind.CUSTOM:
|
||||
return Icons.Folder
|
||||
|
||||
case MailFolderType.INBOX:
|
||||
case MailSetKind.INBOX:
|
||||
return Icons.Inbox
|
||||
|
||||
case MailFolderType.SENT:
|
||||
case MailSetKind.SENT:
|
||||
return Icons.Send
|
||||
|
||||
case MailFolderType.TRASH:
|
||||
case MailSetKind.TRASH:
|
||||
return Icons.TrashBin
|
||||
|
||||
case MailFolderType.ARCHIVE:
|
||||
case MailSetKind.ARCHIVE:
|
||||
return Icons.Archive
|
||||
|
||||
case MailFolderType.SPAM:
|
||||
case MailSetKind.SPAM:
|
||||
return Icons.Spam
|
||||
|
||||
case MailFolderType.DRAFT:
|
||||
case MailSetKind.DRAFT:
|
||||
return Icons.Draft
|
||||
|
||||
default:
|
||||
|
@ -368,7 +368,15 @@ export async function getMoveTargetFolderSystems(model: MailModel, mails: readon
|
|||
return []
|
||||
}
|
||||
const folderSystem = mailboxDetails.folders
|
||||
return folderSystem.getIndentedList().filter((f: IndentedFolder) => f.folder.mails !== getListId(firstMail))
|
||||
|
||||
return folderSystem.getIndentedList().filter((f: IndentedFolder) => {
|
||||
if (f.folder.isMailSet && isNotEmpty(firstMail.sets)) {
|
||||
const folderId = firstMail.sets[0]
|
||||
return !isSameId(f.folder._id, folderId)
|
||||
} else {
|
||||
return f.folder.mails !== getListId(firstMail)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const MAX_FOLDER_INDENT_LEVEL = 10
|
||||
|
@ -459,11 +467,11 @@ export function isTutanotaMailAddress(mailAddress: string): boolean {
|
|||
*
|
||||
* Use with caution.
|
||||
*/
|
||||
export function assertSystemFolderOfType(system: FolderSystem, type: Omit<MailFolderType, MailFolderType.CUSTOM>): MailFolder {
|
||||
export function assertSystemFolderOfType(system: FolderSystem, type: Omit<MailSetKind, MailSetKind.CUSTOM>): MailFolder {
|
||||
return assertNotNull(system.getSystemFolderByType(type), "System folder of type does not exist!")
|
||||
}
|
||||
|
||||
export function isOfTypeOrSubfolderOf(system: FolderSystem, folder: MailFolder, type: MailFolderType): boolean {
|
||||
export function isOfTypeOrSubfolderOf(system: FolderSystem, folder: MailFolder, type: MailSetKind): boolean {
|
||||
return folder.folderType === type || isSubfolderOfType(system, folder, type)
|
||||
}
|
||||
|
||||
|
|
|
@ -25,20 +25,20 @@ import { ListFetchResult, PageSize } from "../gui/base/ListUtils.js"
|
|||
import { isOfflineError } from "../api/common/utils/ErrorUtils.js"
|
||||
import { ListAutoSelectBehavior } from "./DeviceConfig.js"
|
||||
|
||||
export interface ListModelConfig<ElementType> {
|
||||
export interface ListModelConfig<ListElementType> {
|
||||
topId: Id
|
||||
|
||||
/**
|
||||
* Get the given number of entities starting after the given id. May return more elements than requested, e.g. if all elements are available on first fetch.
|
||||
*/
|
||||
fetch(startId: Id, count: number): Promise<ListFetchResult<ElementType>>
|
||||
fetch(startId: Id, count: number): Promise<ListFetchResult<ListElementType>>
|
||||
|
||||
/**
|
||||
* Returns null if the given element could not be loaded
|
||||
*/
|
||||
loadSingle(elementId: Id): Promise<ElementType | null>
|
||||
loadSingle(listId: Id, elementId: Id): Promise<ListElementType | null>
|
||||
|
||||
sortCompare(entity1: ElementType, entity2: ElementType): number
|
||||
sortCompare(entity1: ListElementType, entity2: ListElementType): number
|
||||
|
||||
autoSelectBehavior: () => ListAutoSelectBehavior
|
||||
}
|
||||
|
@ -197,10 +197,10 @@ export class ListModel<ElementType extends ListElement> {
|
|||
return this.filter != null
|
||||
}
|
||||
|
||||
async entityEventReceived(elementId: Id, operation: OperationType): Promise<void> {
|
||||
async entityEventReceived(listId: Id, elementId: Id, operation: OperationType): Promise<void> {
|
||||
if (operation === OperationType.CREATE || operation === OperationType.UPDATE) {
|
||||
// load the element without range checks for now
|
||||
const entity = await this.config.loadSingle(elementId)
|
||||
const entity = await this.config.loadSingle(listId, elementId)
|
||||
if (!entity) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import m from "mithril"
|
||||
import { locator } from "../../api/main/CommonLocator"
|
||||
import { MailFolderType } from "../../api/common/TutanotaConstants.js"
|
||||
import { MailSetKind } from "../../api/common/TutanotaConstants.js"
|
||||
|
||||
import { assertSystemFolderOfType } from "../../mailFunctionality/SharedMailUtils.js"
|
||||
import { getElementId } from "../../api/common/utils/EntityUtils.js"
|
||||
|
||||
export async function openMailbox(userId: Id, mailAddress: string, requestedPath: string | null) {
|
||||
if (locator.logins.isUserLoggedIn() && locator.logins.getUserController().user._id === userId) {
|
||||
if (!requestedPath) {
|
||||
const [mailboxDetail] = await locator.mailModel.getMailboxDetails()
|
||||
const inbox = assertSystemFolderOfType(mailboxDetail.folders, MailFolderType.INBOX)
|
||||
m.route.set("/mail/" + inbox.mails)
|
||||
const inbox = assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.INBOX)
|
||||
m.route.set("/mail/" + getElementId(inbox))
|
||||
} else {
|
||||
m.route.set("/mail" + requestedPath)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { assertMainOrNode } from "../../api/common/Env"
|
|||
import { modal } from "../../gui/base/Modal"
|
||||
import { CALENDAR_PREFIX, CONTACTS_PREFIX, MAIL_PREFIX, SEARCH_PREFIX, SETTINGS_PREFIX } from "../../misc/RouteChange"
|
||||
import { last } from "@tutao/tutanota-utils"
|
||||
import { CloseEventBusOption, MailFolderType, SECOND_MS } from "../../api/common/TutanotaConstants.js"
|
||||
import { CloseEventBusOption, MailSetKind, SECOND_MS } from "../../api/common/TutanotaConstants.js"
|
||||
import { MobileFacade } from "../common/generatedipc/MobileFacade.js"
|
||||
import { styles } from "../../gui/styles"
|
||||
import { WebsocketConnectivityModel } from "../../misc/WebsocketConnectivityModel.js"
|
||||
|
@ -12,6 +12,7 @@ import { TopLevelView } from "../../../TopLevelView.js"
|
|||
import stream from "mithril/stream"
|
||||
|
||||
import { assertSystemFolderOfType } from "../../mailFunctionality/SharedMailUtils.js"
|
||||
import { getElementId } from "../../api/common/utils/EntityUtils.js"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
|
@ -85,12 +86,12 @@ export class WebMobileFacade implements MobileFacade {
|
|||
.filter((part) => part !== "")
|
||||
|
||||
if (parts.length > 1) {
|
||||
const selectedMailListId = parts[1]
|
||||
const selectedMailFolderId = parts[1]
|
||||
const [mailboxDetail] = await this.mailModel.getMailboxDetails()
|
||||
const inboxMailListId = assertSystemFolderOfType(mailboxDetail.folders, MailFolderType.INBOX).mails
|
||||
const inboxMailFolderId = getElementId(assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.INBOX))
|
||||
|
||||
if (inboxMailListId !== selectedMailListId) {
|
||||
m.route.set(MAIL_PREFIX + "/" + inboxMailListId)
|
||||
if (inboxMailFolderId !== selectedMailFolderId) {
|
||||
m.route.set(MAIL_PREFIX + "/" + inboxMailFolderId)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
|
|
@ -198,7 +198,7 @@ export class UserListView implements UpdatableSettingsViewer {
|
|||
const { instanceListId, instanceId, operation } = update
|
||||
|
||||
if (isUpdateForTypeRef(GroupInfoTypeRef, update) && this.listId.getSync() === instanceListId) {
|
||||
await this.listModel.entityEventReceived(instanceId, operation)
|
||||
await this.listModel.entityEventReceived(instanceListId, instanceId, operation)
|
||||
} else if (isUpdateFor(locator.logins.getUserController().user, update)) {
|
||||
await this.loadAdmins()
|
||||
this.listModel.reapplyFilter()
|
||||
|
@ -228,7 +228,7 @@ export class UserListView implements UpdatableSettingsViewer {
|
|||
|
||||
return { items: allUserGroupInfos, complete: true }
|
||||
},
|
||||
loadSingle: async (elementId) => {
|
||||
loadSingle: async (_listId: Id, elementId: Id) => {
|
||||
const listId = await this.listId.getAsync()
|
||||
try {
|
||||
return await locator.entityClient.load<GroupInfo>(GroupInfoTypeRef, [listId, elementId])
|
||||
|
|
|
@ -91,6 +91,7 @@ export class CustomColorEditorPreview implements Component {
|
|||
movedTime: null,
|
||||
phishingStatus: "0",
|
||||
recipientCount: "0",
|
||||
sets: [],
|
||||
} satisfies Partial<Mail>
|
||||
const mail = createMail({
|
||||
sender: createMailAddress({
|
||||
|
|
|
@ -51,6 +51,7 @@ export function showGroupInvitationDialog(invitation: ReceivedGroupInvitation) {
|
|||
group: invitation.sharedGroup,
|
||||
color: newColor,
|
||||
name: newName,
|
||||
defaultAlarmsList: [],
|
||||
})
|
||||
userSettingsGroupRoot.groupSettings.push(groupSettings)
|
||||
}
|
||||
|
|
|
@ -45,8 +45,8 @@ export function applicationPaths({
|
|||
"/recover": recover,
|
||||
"/mailto": mail,
|
||||
"/mail": mail,
|
||||
"/mail/:listId": mail,
|
||||
"/mail/:listId/:mailId": mail,
|
||||
"/mail/:folderId": mail,
|
||||
"/mail/:folderId/:mailId": mail,
|
||||
"/ext": externalLogin,
|
||||
"/contact": contact,
|
||||
"/contact/:listId": contact,
|
||||
|
|
|
@ -88,7 +88,7 @@ export class ContactListViewModel {
|
|||
const items = await this.getRecipientsForList(listId)
|
||||
return { items, complete: true }
|
||||
},
|
||||
loadSingle: async (elementId: Id) => {
|
||||
loadSingle: async (_listId: Id, elementId: Id) => {
|
||||
return this.entityClient.load(ContactListEntryTypeRef, [listId, elementId])
|
||||
},
|
||||
sortCompare: (rl1, rl2) => rl1.emailAddress.localeCompare(rl2.emailAddress),
|
||||
|
@ -194,8 +194,9 @@ export class ContactListViewModel {
|
|||
private readonly entityEventsReceived: EntityEventsListener = async (updates: ReadonlyArray<EntityUpdateData>): Promise<void> => {
|
||||
for (const update of updates) {
|
||||
if (this.selectedContactList) {
|
||||
if (isUpdateForTypeRef(ContactListEntryTypeRef, update) && isSameId(this.selectedContactList, update.instanceListId)) {
|
||||
await this.listModel?.entityEventReceived(update.instanceId, update.operation)
|
||||
const { instanceListId, instanceId, operation } = update
|
||||
if (isUpdateForTypeRef(ContactListEntryTypeRef, update) && isSameId(this.selectedContactList, instanceListId)) {
|
||||
await this.listModel?.entityEventReceived(instanceListId, instanceId, operation)
|
||||
} else if (isUpdateForTypeRef(ContactTypeRef, update)) {
|
||||
this.getContactsForSelectedContactListEntry()
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export class ContactViewModel {
|
|||
const items = await this.entityClient.loadAll(ContactTypeRef, this.contactListId)
|
||||
return { items, complete: true }
|
||||
},
|
||||
loadSingle: async (elementId: Id) => {
|
||||
loadSingle: async (_listId: Id, elementId: Id) => {
|
||||
const listId = await this.contactModel.getContactListId()
|
||||
if (listId == null) return null
|
||||
return this.entityClient.load(ContactTypeRef, [listId, elementId])
|
||||
|
@ -82,8 +82,9 @@ export class ContactViewModel {
|
|||
|
||||
private readonly entityListener: EntityEventsListener = async (updates) => {
|
||||
for (const update of updates) {
|
||||
if (isUpdateForTypeRef(ContactTypeRef, update) && update.instanceListId === this.contactListId) {
|
||||
await this.listModel.entityEventReceived(update.instanceId, update.operation)
|
||||
const { instanceListId, instanceId, operation } = update
|
||||
if (isUpdateForTypeRef(ContactTypeRef, update) && instanceListId === this.contactListId) {
|
||||
await this.listModel.entityEventReceived(instanceListId, instanceId, operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import type { InboxRule, Mail, MoveMailData } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import type { InboxRule, Mail, MailFolder, MoveMailData } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { createMoveMailData } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { InboxRuleType, MailFolderType, MAX_NBR_MOVE_DELETE_MAIL_SERVICE } from "../../../common/api/common/TutanotaConstants"
|
||||
import { InboxRuleType, MailSetKind, MAX_NBR_MOVE_DELETE_MAIL_SERVICE } from "../../../common/api/common/TutanotaConstants"
|
||||
import { isDomainName, isRegularExpression } from "../../../common/misc/FormatValidator"
|
||||
import { assertNotNull, asyncFind, debounce, ofClass, promiseMap, splitInChunks } from "@tutao/tutanota-utils"
|
||||
import { lang } from "../../../common/misc/LanguageViewModel"
|
||||
import type { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { LockedError, PreconditionFailedError } from "../../../common/api/common/error/RestError"
|
||||
import type { SelectorItemList } from "../../../common/gui/base/DropDownSelector.js"
|
||||
import { getElementId, getListId, isSameId } from "../../../common/api/common/utils/EntityUtils"
|
||||
import { elementIdPart, isSameId } from "../../../common/api/common/utils/EntityUtils"
|
||||
import { assertMainOrNode } from "../../../common/api/common/Env"
|
||||
import { MailFacade } from "../../../common/api/worker/facades/lazy/MailFacade.js"
|
||||
import { LoginController } from "../../../common/api/main/LoginController.js"
|
||||
import { assertSystemFolderOfType, getMailHeaders } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { getMailHeaders } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
|
||||
assertMainOrNode()
|
||||
const moveMailDataPerFolder: MoveMailData[] = []
|
||||
|
@ -24,7 +24,8 @@ async function sendMoveMailRequest(mailFacade: MailFacade): Promise<void> {
|
|||
const mailChunks = splitInChunks(MAX_NBR_MOVE_DELETE_MAIL_SERVICE, moveToTargetFolder.mails)
|
||||
await promiseMap(mailChunks, (mailChunk) => {
|
||||
moveToTargetFolder.mails = mailChunk
|
||||
return mailFacade.moveMails(mailChunk, moveToTargetFolder.targetFolder)
|
||||
const sourceFolder = assertNotNull(moveToTargetFolder.sourceFolder) // old clients don't send sourceFolder. assertNotNull can be removed once sourceFolder cardinality is ONE
|
||||
return mailFacade.moveMails(mailChunk, sourceFolder, moveToTargetFolder.targetFolder)
|
||||
})
|
||||
.catch(
|
||||
ofClass(LockedError, (e) => {
|
||||
|
@ -96,16 +97,17 @@ export class InboxRuleHandler {
|
|||
* 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
|
||||
*/
|
||||
async findAndApplyMatchingRule(mailboxDetail: MailboxDetail, mail: Mail, applyRulesOnServer: boolean): Promise<IdTuple | null> {
|
||||
if (mail._errors || !mail.unread || !isInboxList(mailboxDetail, getListId(mail)) || !this.logins.getUserController().isPremiumAccount()) {
|
||||
async findAndApplyMatchingRule(mailboxDetail: MailboxDetail, mail: Mail, applyRulesOnServer: boolean): Promise<{ folder: MailFolder; mail: Mail } | null> {
|
||||
if (mail._errors || !mail.unread || !isInboxFolder(mailboxDetail, mail) || !this.logins.getUserController().isPremiumAccount()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const inboxRule = await _findMatchingRule(this.mailFacade, mail, this.logins.getUserController().props.inboxRules)
|
||||
if (inboxRule) {
|
||||
let targetFolder = mailboxDetail.folders.getFolderById(inboxRule.targetFolder)
|
||||
let inboxFolder = assertNotNull(mailboxDetail.folders.getSystemFolderByType(MailSetKind.INBOX))
|
||||
let targetFolder = mailboxDetail.folders.getFolderById(elementIdPart(inboxRule.targetFolder))
|
||||
|
||||
if (targetFolder && targetFolder.folderType !== MailFolderType.INBOX) {
|
||||
if (targetFolder && targetFolder.folderType !== MailSetKind.INBOX) {
|
||||
if (applyRulesOnServer) {
|
||||
let moveMailData = moveMailDataPerFolder.find((folderMoveMailData) => isSameId(folderMoveMailData.targetFolder, inboxRule.targetFolder))
|
||||
|
||||
|
@ -113,6 +115,7 @@ export class InboxRuleHandler {
|
|||
moveMailData.mails.push(mail._id)
|
||||
} else {
|
||||
moveMailData = createMoveMailData({
|
||||
sourceFolder: inboxFolder._id,
|
||||
targetFolder: inboxRule.targetFolder,
|
||||
mails: [mail._id],
|
||||
})
|
||||
|
@ -122,7 +125,7 @@ export class InboxRuleHandler {
|
|||
applyMatchingRules(this.mailFacade)
|
||||
}
|
||||
|
||||
return [targetFolder.mails, getElementId(mail)]
|
||||
return { folder: targetFolder, mail }
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
@ -220,6 +223,7 @@ function _checkEmailAddresses(mailAddresses: string[], inboxRule: InboxRule): bo
|
|||
return mailAddress != null
|
||||
}
|
||||
|
||||
export function isInboxList(mailboxDetail: MailboxDetail, listId: Id): boolean {
|
||||
return isSameId(listId, assertSystemFolderOfType(mailboxDetail.folders, MailFolderType.INBOX).mails)
|
||||
export function isInboxFolder(mailboxDetail: MailboxDetail, mail: Mail): boolean {
|
||||
const mailFolder = mailboxDetail.folders.getFolderByMail(mail)
|
||||
return mailFolder?.folderType === MailSetKind.INBOX
|
||||
}
|
||||
|
|
|
@ -1,20 +1,12 @@
|
|||
import { ConversationEntry, ConversationEntryTypeRef, Mail, MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { MailViewerViewModel } from "./MailViewerViewModel.js"
|
||||
import { CreateMailViewerOptions } from "./MailViewer.js"
|
||||
import {
|
||||
elementIdPart,
|
||||
firstBiggerThanSecond,
|
||||
getElementId,
|
||||
getListId,
|
||||
haveSameId,
|
||||
isSameId,
|
||||
listIdPart,
|
||||
} from "../../../common/api/common/utils/EntityUtils.js"
|
||||
import { elementIdPart, firstBiggerThanSecond, getElementId, haveSameId, isSameId, listIdPart } from "../../../common/api/common/utils/EntityUtils.js"
|
||||
import { assertNotNull, findLastIndex, groupBy, makeSingleUse } from "@tutao/tutanota-utils"
|
||||
import { EntityClient } from "../../../common/api/common/EntityClient.js"
|
||||
import { LoadingStateTracker } from "../../../common/offline/LoadingState.js"
|
||||
import { EntityEventsListener, EventController } from "../../../common/api/main/EventController.js"
|
||||
import { ConversationType, MailFolderType, MailState, OperationType } from "../../../common/api/common/TutanotaConstants.js"
|
||||
import { ConversationType, MailSetKind, MailState, OperationType } from "../../../common/api/common/TutanotaConstants.js"
|
||||
import { NotAuthorizedError, NotFoundError } from "../../../common/api/common/error/RestError.js"
|
||||
import { MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
|
||||
|
@ -254,8 +246,8 @@ export class ConversationViewModel {
|
|||
|
||||
private async isInTrash(mail: Mail) {
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMail(mail)
|
||||
const mailFolder = this.mailModel.getMailFolder(getListId(mail))
|
||||
return mailFolder && mailboxDetail && isOfTypeOrSubfolderOf(mailboxDetail.folders, mailFolder, MailFolderType.TRASH)
|
||||
const mailFolder = this.mailModel.getMailFolderForMail(mail)
|
||||
return mailFolder && mailboxDetail && isOfTypeOrSubfolderOf(mailboxDetail.folders, mailFolder, MailSetKind.TRASH)
|
||||
}
|
||||
|
||||
conversationItems(): ReadonlyArray<ConversationItem> {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { MailFolder, MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { Mail, MailFolder, MailSetEntryTypeRef, MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { DropDownSelector, SelectorItemList } from "../../../common/gui/base/DropDownSelector.js"
|
||||
import m from "mithril"
|
||||
import { TextField } from "../../../common/gui/base/TextField.js"
|
||||
|
@ -7,25 +7,26 @@ import { locator } from "../../../common/api/main/CommonLocator.js"
|
|||
import { LockedError } from "../../../common/api/common/error/RestError.js"
|
||||
import { lang, TranslationKey } from "../../../common/misc/LanguageViewModel.js"
|
||||
import { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { MailFolderType, MailReportType } from "../../../common/api/common/TutanotaConstants.js"
|
||||
import { isSameId } from "../../../common/api/common/utils/EntityUtils.js"
|
||||
import { MailReportType, MailSetKind } from "../../../common/api/common/TutanotaConstants.js"
|
||||
import { elementIdPart, isSameId, listIdPart } from "../../../common/api/common/utils/EntityUtils.js"
|
||||
import { reportMailsAutomatically } from "./MailReportDialog.js"
|
||||
import { isOfflineError } from "../../../common/api/common/utils/ErrorUtils.js"
|
||||
import { getFolderName, getIndentedFolderNameForDropdown, getPathToFolderString } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.js"
|
||||
import { groupByAndMap } from "@tutao/tutanota-utils"
|
||||
|
||||
/**
|
||||
* Dialog for Edit and Add folder are the same.
|
||||
* @param editFolder if this is null, a folder is being added, otherwise a folder is being edited
|
||||
* @param editedFolder if this is null, a folder is being added, otherwise a folder is being edited
|
||||
*/
|
||||
export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editFolder: MailFolder | null = null, parentFolder: MailFolder | null = null) {
|
||||
export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedFolder: MailFolder | null = null, parentFolder: MailFolder | null = null) {
|
||||
const noParentFolderOption = lang.get("comboBoxSelectionNone_msg")
|
||||
const mailGroupId = mailBoxDetail.mailGroup._id
|
||||
let folderNameValue = editFolder?.name ?? ""
|
||||
let folderNameValue = editedFolder?.name ?? ""
|
||||
let targetFolders: SelectorItemList<MailFolder | null> = mailBoxDetail.folders
|
||||
.getIndentedList(editFolder)
|
||||
.getIndentedList(editedFolder)
|
||||
// filter: SPAM and TRASH and descendants are only shown if editing (folders can only be moved there, not created there)
|
||||
.filter((folderInfo) => !(editFolder === null && isSpamOrTrashFolder(mailBoxDetail.folders, folderInfo.folder)))
|
||||
.filter((folderInfo) => !(editedFolder === null && isSpamOrTrashFolder(mailBoxDetail.folders, folderInfo.folder)))
|
||||
.map((folderInfo) => {
|
||||
return {
|
||||
name: getIndentedFolderNameForDropdown(folderInfo),
|
||||
|
@ -36,7 +37,7 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editFol
|
|||
let selectedParentFolder = parentFolder
|
||||
let form = () => [
|
||||
m(TextField, {
|
||||
label: editFolder ? "rename_action" : "folderName_label",
|
||||
label: editedFolder ? "rename_action" : "folderName_label",
|
||||
value: folderNameValue,
|
||||
oninput: (newInput) => {
|
||||
folderNameValue = newInput
|
||||
|
@ -51,47 +52,69 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editFol
|
|||
helpLabel: () => (selectedParentFolder ? getPathToFolderString(mailBoxDetail.folders, selectedParentFolder) : ""),
|
||||
}),
|
||||
]
|
||||
|
||||
async function getMailIdsGroupedByListId(folder: MailFolder): Promise<Map<Id, Id[]>> {
|
||||
const mailSetEntries = await locator.entityClient.loadAll(MailSetEntryTypeRef, folder.entries)
|
||||
return groupByAndMap(
|
||||
mailSetEntries,
|
||||
(mse) => listIdPart(mse.mail),
|
||||
(mse) => elementIdPart(mse.mail),
|
||||
)
|
||||
}
|
||||
|
||||
async function loadAllMailsOfFolder(folder: MailFolder, reportableMails: Array<Mail>) {
|
||||
if (folder.isMailSet) {
|
||||
const mailIdsPerBag = await getMailIdsGroupedByListId(folder)
|
||||
for (const [mailListId, mailIds] of mailIdsPerBag) {
|
||||
reportableMails.push(...(await locator.entityClient.loadMultiple(MailTypeRef, mailListId, mailIds)))
|
||||
}
|
||||
} else {
|
||||
reportableMails.push(...(await locator.entityClient.loadAll(MailTypeRef, folder.mails)))
|
||||
}
|
||||
}
|
||||
|
||||
const okAction = async (dialog: Dialog) => {
|
||||
// closing right away to prevent duplicate actions
|
||||
dialog.close()
|
||||
try {
|
||||
// if folder is null, create new folder
|
||||
if (editFolder === null) {
|
||||
if (editedFolder === null) {
|
||||
await locator.mailFacade.createMailFolder(folderNameValue, selectedParentFolder?._id ?? null, mailGroupId)
|
||||
} else {
|
||||
// if it is being moved to trash (and not already in trash), ask about trashing
|
||||
if (selectedParentFolder?.folderType === MailFolderType.TRASH && !isSameId(selectedParentFolder._id, editFolder.parentFolder)) {
|
||||
if (selectedParentFolder?.folderType === MailSetKind.TRASH && !isSameId(selectedParentFolder._id, editedFolder.parentFolder)) {
|
||||
const confirmed = await Dialog.confirm(() =>
|
||||
lang.get("confirmDeleteCustomFolder_msg", {
|
||||
"{1}": getFolderName(editFolder),
|
||||
"{1}": getFolderName(editedFolder),
|
||||
}),
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
await locator.mailFacade.updateMailFolderName(editFolder, folderNameValue)
|
||||
await locator.mailModel.trashFolderAndSubfolders(editFolder)
|
||||
} else if (selectedParentFolder?.folderType === MailFolderType.SPAM && !isSameId(selectedParentFolder._id, editFolder.parentFolder)) {
|
||||
await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue)
|
||||
await locator.mailModel.trashFolderAndSubfolders(editedFolder)
|
||||
} else if (selectedParentFolder?.folderType === MailSetKind.SPAM && !isSameId(selectedParentFolder._id, editedFolder.parentFolder)) {
|
||||
// if it is being moved to spam (and not already in spam), ask about reporting containing emails
|
||||
const confirmed = await Dialog.confirm(() =>
|
||||
lang.get("confirmSpamCustomFolder_msg", {
|
||||
"{1}": getFolderName(editFolder),
|
||||
"{1}": getFolderName(editedFolder),
|
||||
}),
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
// get mails to report before moving to mail model
|
||||
const descendants = mailBoxDetail.folders.getDescendantFoldersOfParent(editFolder._id).sort((l, r) => r.level - l.level)
|
||||
let reportableMails = await locator.entityClient.loadAll(MailTypeRef, editFolder.mails)
|
||||
const descendants = mailBoxDetail.folders.getDescendantFoldersOfParent(editedFolder._id).sort((l, r) => r.level - l.level)
|
||||
let reportableMails: Array<Mail> = []
|
||||
await loadAllMailsOfFolder(editedFolder, reportableMails)
|
||||
for (const descendant of descendants) {
|
||||
reportableMails.push(...(await locator.entityClient.loadAll(MailTypeRef, descendant.folder.mails)))
|
||||
await loadAllMailsOfFolder(descendant.folder, reportableMails)
|
||||
}
|
||||
await reportMailsAutomatically(MailReportType.SPAM, locator.mailModel, mailBoxDetail, reportableMails)
|
||||
|
||||
await locator.mailFacade.updateMailFolderName(editFolder, folderNameValue)
|
||||
await locator.mailModel.sendFolderToSpam(editFolder)
|
||||
await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue)
|
||||
await locator.mailModel.sendFolderToSpam(editedFolder)
|
||||
} else {
|
||||
await locator.mailFacade.updateMailFolderName(editFolder, folderNameValue)
|
||||
await locator.mailFacade.updateMailFolderParent(editFolder, selectedParentFolder?._id || null)
|
||||
await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue)
|
||||
await locator.mailFacade.updateMailFolderParent(editedFolder, selectedParentFolder?._id || null)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -102,7 +125,7 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editFol
|
|||
}
|
||||
|
||||
Dialog.showActionDialog({
|
||||
title: editFolder ? lang.get("editFolder_action") : lang.get("addFolder_action"),
|
||||
title: editedFolder ? lang.get("editFolder_action") : lang.get("addFolder_action"),
|
||||
child: form,
|
||||
validator: () => checkFolderName(mailBoxDetail, folderNameValue, mailGroupId, selectedParentFolder?._id ?? null),
|
||||
allowOkWithReturn: true,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { locator } from "../../../common/api/main/CommonLocator.js"
|
|||
import { SidebarSection } from "../../../common/gui/SidebarSection.js"
|
||||
import { IconButton, IconButtonAttrs } from "../../../common/gui/base/IconButton.js"
|
||||
import { FolderSubtree } from "../../../common/api/common/mail/FolderSystem.js"
|
||||
import { getElementId } from "../../../common/api/common/utils/EntityUtils.js"
|
||||
import { elementIdPart, getElementId } from "../../../common/api/common/utils/EntityUtils.js"
|
||||
import { isSelectedPrefix, NavButtonAttrs, NavButtonColor } from "../../../common/gui/base/NavButton.js"
|
||||
import { MAIL_PREFIX } from "../../../common/misc/RouteChange.js"
|
||||
import { MailFolderRow } from "./MailFolderRow.js"
|
||||
|
@ -14,7 +14,7 @@ import { attachDropdown, DropdownButtonAttrs } from "../../../common/gui/base/Dr
|
|||
import { Icons } from "../../../common/gui/base/icons/Icons.js"
|
||||
import { ButtonColor } from "../../../common/gui/base/Button.js"
|
||||
import { ButtonSize } from "../../../common/gui/base/ButtonSize.js"
|
||||
import { MailFolderType } from "../../../common/api/common/TutanotaConstants.js"
|
||||
import { MailSetKind } from "../../../common/api/common/TutanotaConstants.js"
|
||||
import { px, size } from "../../../common/gui/size.js"
|
||||
import { RowButton } from "../../../common/gui/base/buttons/RowButton.js"
|
||||
import { getFolderIcon, getFolderName, MAX_FOLDER_INDENT_LEVEL } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
|
@ -22,7 +22,7 @@ import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.
|
|||
|
||||
export interface MailFolderViewAttrs {
|
||||
mailboxDetail: MailboxDetail
|
||||
mailListToSelectedMail: ReadonlyMap<Id, Id>
|
||||
mailFolderToSelectedMail: ReadonlyMap<MailFolder, Id>
|
||||
onFolderClick: (folder: MailFolder) => unknown
|
||||
onFolderDrop: (mailId: string, folder: MailFolder) => unknown
|
||||
expandedFolders: ReadonlySet<Id>
|
||||
|
@ -51,7 +51,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
const selectedFolder = mailboxDetail.folders
|
||||
.getIndentedList()
|
||||
.map((f) => f.folder)
|
||||
.find((f) => isSelectedPrefix(MAIL_PREFIX + "/" + f.mails))
|
||||
.find((f) => isSelectedPrefix(MAIL_PREFIX + "/" + getElementId(f)))
|
||||
const path = selectedFolder ? mailboxDetail.folders.getPathToFolder(selectedFolder._id) : []
|
||||
const isInternalUser = locator.logins.isInternalUserLoggedIn()
|
||||
const systemChildren = this.renderFolderTree(systemSystems, groupCounters, attrs, path, isInternalUser)
|
||||
|
@ -93,15 +93,16 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
if (attrs.inEditMode) {
|
||||
return m.route.get()
|
||||
} else {
|
||||
const mailId = attrs.mailListToSelectedMail.get(system.folder.mails)
|
||||
const mailId = attrs.mailFolderToSelectedMail.get(system.folder)
|
||||
const folderId = getElementId(system.folder)
|
||||
if (mailId) {
|
||||
return `/mail/${system.folder.mails}/${mailId}`
|
||||
return `${MAIL_PREFIX}/${folderId}/${mailId}`
|
||||
} else {
|
||||
return `/mail/${system.folder.mails}`
|
||||
return `${MAIL_PREFIX}/${folderId}`
|
||||
}
|
||||
}
|
||||
},
|
||||
isSelectedPrefix: attrs.inEditMode ? false : MAIL_PREFIX + "/" + system.folder.mails,
|
||||
isSelectedPrefix: attrs.inEditMode ? false : MAIL_PREFIX + "/" + getElementId(system.folder),
|
||||
colors: NavButtonColor.Nav,
|
||||
click: () => attrs.onFolderClick(system.folder),
|
||||
dropHandler: (droppedMailId) => attrs.onFolderDrop(droppedMailId, system.folder),
|
||||
|
@ -110,12 +111,13 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
}
|
||||
const currentExpansionState = attrs.inEditMode ? true : attrs.expandedFolders.has(getElementId(system.folder)) ?? false //default is false
|
||||
const hasChildren = system.children.length > 0
|
||||
const summedCount = !currentExpansionState && hasChildren ? this.getTotalFolderCounter(groupCounters, system) : groupCounters[system.folder.mails]
|
||||
const counterId = system.folder.isMailSet ? getElementId(system.folder) : system.folder.mails
|
||||
const summedCount = !currentExpansionState && hasChildren ? this.getTotalFolderCounter(groupCounters, system) : groupCounters[counterId]
|
||||
const childResult =
|
||||
hasChildren && currentExpansionState
|
||||
? this.renderFolderTree(system.children, groupCounters, attrs, path, isInternalUser, indentationLevel + 1)
|
||||
: { children: null, numRows: 0 }
|
||||
const isTrashOrSpam = system.folder.folderType === MailFolderType.TRASH || system.folder.folderType === MailFolderType.SPAM
|
||||
const isTrashOrSpam = system.folder.folderType === MailSetKind.TRASH || system.folder.folderType === MailSetKind.SPAM
|
||||
const isRightButtonVisible = this.visibleRow === id
|
||||
const rightButton =
|
||||
isInternalUser && !isTrashOrSpam && (isRightButtonVisible || attrs.inEditMode)
|
||||
|
@ -171,7 +173,8 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
}
|
||||
|
||||
private getTotalFolderCounter(counters: Counters, system: FolderSubtree): number {
|
||||
return (counters[system.folder.mails] ?? 0) + system.children.reduce((acc, child) => acc + this.getTotalFolderCounter(counters, child), 0)
|
||||
const counterId = system.folder.isMailSet ? getElementId(system.folder) : system.folder.mails
|
||||
return (counters[counterId] ?? 0) + system.children.reduce((acc, child) => acc + this.getTotalFolderCounter(counters, child), 0)
|
||||
}
|
||||
|
||||
private createFolderMoreButton(folder: MailFolder, attrs: MailFolderViewAttrs, onClose: Thunk): IconButtonAttrs {
|
||||
|
@ -183,7 +186,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
size: ButtonSize.Compact,
|
||||
},
|
||||
childAttrs: () => {
|
||||
return folder.folderType === MailFolderType.CUSTOM
|
||||
return folder.folderType === MailSetKind.CUSTOM
|
||||
? // cannot add new folder to custom folder in spam or trash folder
|
||||
isSpamOrTrashFolder(attrs.mailboxDetail.folders, folder)
|
||||
? [this.editButtonAttrs(attrs, folder), this.deleteButtonAttrs(attrs, folder)]
|
||||
|
@ -222,7 +225,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
attrs.onShowFolderAddEditDialog(
|
||||
attrs.mailboxDetail.mailGroup._id,
|
||||
folder,
|
||||
folder.parentFolder ? attrs.mailboxDetail.folders.getFolderById(folder.parentFolder) : null,
|
||||
folder.parentFolder ? attrs.mailboxDetail.folders.getFolderById(elementIdPart(folder.parentFolder)) : null,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
|
|
@ -8,8 +8,7 @@ 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, neverNull, noOp, promiseMap } from "@tutao/tutanota-utils"
|
||||
import { MailFolderType, MailReportType } from "../../../common/api/common/TutanotaConstants"
|
||||
import { getElementId, getListId } from "../../../common/api/common/utils/EntityUtils"
|
||||
import { MailReportType, MailSetKind } from "../../../common/api/common/TutanotaConstants"
|
||||
import { reportMailsAutomatically } from "./MailReportDialog"
|
||||
import { DataFile } from "../../../common/api/common/DataFile"
|
||||
import { lang, TranslationKey } from "../../../common/misc/LanguageViewModel"
|
||||
|
@ -20,22 +19,23 @@ import { ConversationViewModel } from "./ConversationViewModel.js"
|
|||
import { size } from "../../../common/gui/size.js"
|
||||
import { PinchZoom } from "../../../common/gui/PinchZoom.js"
|
||||
import { InlineImageReference, InlineImages } from "../../../common/mailFunctionality/inlineImagesUtils.js"
|
||||
import { isOfTypeOrSubfolderOf } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import {
|
||||
assertSystemFolderOfType,
|
||||
getFolderIcon,
|
||||
getFolderName,
|
||||
getIndentedFolderNameForDropdown,
|
||||
getMoveTargetFolderSystems,
|
||||
isOfTypeOrSubfolderOf,
|
||||
} from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.js"
|
||||
import { getElementId } from "../../../common/api/common/utils/EntityUtils.js"
|
||||
|
||||
export async function showDeleteConfirmationDialog(mails: ReadonlyArray<Mail>): Promise<boolean> {
|
||||
let trashMails: Mail[] = []
|
||||
let moveMails: Mail[] = []
|
||||
for (let mail of mails) {
|
||||
const folder = locator.mailModel.getMailFolder(mail._id[0])
|
||||
const mailboxDetail = await locator.mailModel.getMailboxDetailsForMailListId(getListId(mail))
|
||||
const folder = locator.mailModel.getMailFolderForMail(mail)
|
||||
const mailboxDetail = await locator.mailModel.getMailboxDetailsForMail(mail)
|
||||
if (mailboxDetail == null) {
|
||||
continue
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ interface MoveMailsParams {
|
|||
* @return whether mails were actually moved
|
||||
*/
|
||||
export async function moveMails({ mailModel, mails, targetMailFolder, isReportable = true }: MoveMailsParams): Promise<boolean> {
|
||||
const details = await mailModel.getMailboxDetailsForMailListId(targetMailFolder.mails)
|
||||
const details = await mailModel.getMailboxDetailsForMailFolder(targetMailFolder)
|
||||
if (details == null) {
|
||||
return false
|
||||
}
|
||||
|
@ -104,11 +104,11 @@ export async function moveMails({ mailModel, mails, targetMailFolder, isReportab
|
|||
return mailModel
|
||||
.moveMails(mails, targetMailFolder)
|
||||
.then(async () => {
|
||||
if (isOfTypeOrSubfolderOf(system, targetMailFolder, MailFolderType.SPAM) && isReportable) {
|
||||
if (isOfTypeOrSubfolderOf(system, targetMailFolder, MailSetKind.SPAM) && isReportable) {
|
||||
const reportableMails = mails.map((mail) => {
|
||||
// mails have just been moved
|
||||
const reportableMail = createMail(mail)
|
||||
reportableMail._id = [targetMailFolder.mails, getElementId(mail)]
|
||||
reportableMail._id = targetMailFolder.isMailSet ? mail._id : [targetMailFolder.mails, getElementId(mail)]
|
||||
return reportableMail
|
||||
})
|
||||
const mailboxDetails = await mailModel.getMailboxDetailsForMailGroup(assertNotNull(targetMailFolder._ownerGroup))
|
||||
|
@ -135,7 +135,7 @@ export function archiveMails(mails: Mail[]): Promise<void> {
|
|||
moveMails({
|
||||
mailModel: locator.mailModel,
|
||||
mails: mails,
|
||||
targetMailFolder: assertSystemFolderOfType(folders, MailFolderType.ARCHIVE),
|
||||
targetMailFolder: assertSystemFolderOfType(folders, MailSetKind.ARCHIVE),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
|
@ -151,7 +151,7 @@ export function moveToInbox(mails: Mail[]): Promise<any> {
|
|||
moveMails({
|
||||
mailModel: locator.mailModel,
|
||||
mails: mails,
|
||||
targetMailFolder: assertSystemFolderOfType(folders, MailFolderType.INBOX),
|
||||
targetMailFolder: assertSystemFolderOfType(folders, MailSetKind.INBOX),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
|
@ -160,7 +160,7 @@ export function moveToInbox(mails: Mail[]): Promise<any> {
|
|||
}
|
||||
|
||||
export function getMailFolderIcon(mail: Mail): AllIcons {
|
||||
let folder = locator.mailModel.getMailFolder(mail._id[0])
|
||||
let folder = locator.mailModel.getMailFolderForMail(mail)
|
||||
|
||||
if (folder) {
|
||||
return getFolderIcon(folder)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import m, { Children, Component, Vnode } from "mithril"
|
||||
import { lang } from "../../../common/misc/LanguageViewModel"
|
||||
|
||||
import { Keys, MailFolderType, MailState } from "../../../common/api/common/TutanotaConstants"
|
||||
import { Keys, MailSetKind, MailState } from "../../../common/api/common/TutanotaConstants"
|
||||
import type { Mail, MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { size } from "../../../common/gui/size"
|
||||
import { styles } from "../../../common/gui/styles"
|
||||
|
@ -39,12 +39,10 @@ export interface MailListViewAttrs {
|
|||
// but for that we need to rewrite the List
|
||||
onClearFolder: () => unknown
|
||||
mailViewModel: MailViewModel
|
||||
listId: Id
|
||||
onSingleSelection: (mail: Mail) => unknown
|
||||
}
|
||||
|
||||
export class MailListView implements Component<MailListViewAttrs> {
|
||||
listId: Id
|
||||
// Mails that are currently being or have already been downloaded/bundled/saved
|
||||
// Map of (Mail._id ++ MailExportMode) -> Promise<Filepath>
|
||||
// TODO this currently grows bigger and bigger and bigger if the user goes on an exporting spree.
|
||||
|
@ -84,7 +82,6 @@ export class MailListView implements Component<MailListViewAttrs> {
|
|||
|
||||
constructor({ attrs }: Vnode<MailListViewAttrs>) {
|
||||
this.mailViewModel = attrs.mailViewModel
|
||||
this.listId = attrs.listId
|
||||
this.exportedMails = new Map()
|
||||
this._listDom = null
|
||||
this.mailViewModel.showingTrashOrSpamFolder().then((result) => {
|
||||
|
@ -106,9 +103,9 @@ export class MailListView implements Component<MailListViewAttrs> {
|
|||
|
||||
private getRecoverFolder(mail: Mail, folders: FolderSystem): MailFolder {
|
||||
if (mail.state === MailState.DRAFT) {
|
||||
return assertSystemFolderOfType(folders, MailFolderType.DRAFT)
|
||||
return assertSystemFolderOfType(folders, MailSetKind.DRAFT)
|
||||
} else {
|
||||
return assertSystemFolderOfType(folders, MailFolderType.INBOX)
|
||||
return assertSystemFolderOfType(folders, MailSetKind.INBOX)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -301,7 +298,7 @@ export class MailListView implements Component<MailListViewAttrs> {
|
|||
this.mailViewModel = vnode.attrs.mailViewModel
|
||||
|
||||
// Save the folder before showing the dialog so that there's no chance that it will change
|
||||
const folder = this.mailViewModel.getSelectedFolder()
|
||||
const folder = this.mailViewModel.getFolder()
|
||||
const purgeButtonAttrs: ButtonAttrs = {
|
||||
label: "clearFolder_action",
|
||||
type: ButtonType.Primary,
|
||||
|
@ -396,10 +393,10 @@ export class MailListView implements Component<MailListViewAttrs> {
|
|||
}
|
||||
|
||||
private async targetInbox(): Promise<boolean> {
|
||||
const selectedFolder = this.mailViewModel.getSelectedFolder()
|
||||
const selectedFolder = this.mailViewModel.getFolder()
|
||||
if (selectedFolder) {
|
||||
const mailDetails = await this.mailViewModel.getMailboxDetails()
|
||||
return isOfTypeOrSubfolderOf(mailDetails.folders, selectedFolder, MailFolderType.ARCHIVE) || selectedFolder.folderType === MailFolderType.TRASH
|
||||
return isOfTypeOrSubfolderOf(mailDetails.folders, selectedFolder, MailSetKind.ARCHIVE) || selectedFolder.folderType === MailSetKind.TRASH
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
@ -422,7 +419,7 @@ export class MailListView implements Component<MailListViewAttrs> {
|
|||
//to determinate the target folder
|
||||
const targetMailFolder = this.showingSpamOrTrash
|
||||
? this.getRecoverFolder(listElement, folders)
|
||||
: assertNotNull(folders.getSystemFolderByType(this.showingArchive ? MailFolderType.INBOX : MailFolderType.ARCHIVE))
|
||||
: assertNotNull(folders.getSystemFolderByType(this.showingArchive ? MailSetKind.INBOX : MailSetKind.ARCHIVE))
|
||||
const wereMoved = await moveMails({
|
||||
mailModel: locator.mailModel,
|
||||
mails: [listElement],
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { EncryptionAuthStatus, getMailFolderType, MailFolderType, MailState, ReplyType } from "../../../common/api/common/TutanotaConstants"
|
||||
import { getMailFolderType, MailSetKind, MailState, ReplyType } from "../../../common/api/common/TutanotaConstants"
|
||||
import { FontIcons } from "../../../common/gui/base/icons/FontIcons"
|
||||
import type { Mail } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { formatTimeOrDateOrYesterday } from "../../../common/misc/Formatter.js"
|
||||
|
@ -20,17 +20,17 @@ import { px, size } from "../../../common/gui/size.js"
|
|||
import { NBSP, noOp } from "@tutao/tutanota-utils"
|
||||
import { VirtualRow } from "../../../common/gui/base/ListUtils.js"
|
||||
import { companyTeamLabel } from "../../../common/misc/ClientConstants.js"
|
||||
import { getConfidentialFontIcon, getSenderOrRecipientHeading } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isTutanotaTeamMail } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { getConfidentialFontIcon, getSenderOrRecipientHeading, isTutanotaTeamMail } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
|
||||
const iconMap: Record<MailFolderType, string> = {
|
||||
[MailFolderType.CUSTOM]: FontIcons.Folder,
|
||||
[MailFolderType.INBOX]: FontIcons.Inbox,
|
||||
[MailFolderType.SENT]: FontIcons.Sent,
|
||||
[MailFolderType.TRASH]: FontIcons.Trash,
|
||||
[MailFolderType.ARCHIVE]: FontIcons.Archive,
|
||||
[MailFolderType.SPAM]: FontIcons.Spam,
|
||||
[MailFolderType.DRAFT]: FontIcons.Draft,
|
||||
const iconMap: Record<MailSetKind, string> = {
|
||||
[MailSetKind.CUSTOM]: FontIcons.Folder,
|
||||
[MailSetKind.INBOX]: FontIcons.Inbox,
|
||||
[MailSetKind.SENT]: FontIcons.Sent,
|
||||
[MailSetKind.TRASH]: FontIcons.Trash,
|
||||
[MailSetKind.ARCHIVE]: FontIcons.Archive,
|
||||
[MailSetKind.SPAM]: FontIcons.Spam,
|
||||
[MailSetKind.DRAFT]: FontIcons.Draft,
|
||||
[MailSetKind.ALL]: FontIcons.Folder,
|
||||
}
|
||||
|
||||
export const MAIL_ROW_V_MARGIN = 3
|
||||
|
@ -49,7 +49,7 @@ export class MailRow implements VirtualRow<Mail> {
|
|||
private dateDom!: HTMLElement
|
||||
private iconsDom!: HTMLElement
|
||||
private unreadDom!: HTMLElement
|
||||
private folderIconsDom: Record<MailFolderType, HTMLElement>
|
||||
private folderIconsDom: Record<MailSetKind, HTMLElement>
|
||||
private teamLabelDom!: HTMLElement
|
||||
private checkboxDom!: HTMLInputElement
|
||||
private checkboxWasVisible = shouldAlwaysShowMultiselectCheckbox()
|
||||
|
@ -58,7 +58,7 @@ export class MailRow implements VirtualRow<Mail> {
|
|||
constructor(private readonly showFolderIcon: boolean, private readonly onSelected: (mail: Mail, selected: boolean) => unknown) {
|
||||
this.top = 0
|
||||
this.entity = null
|
||||
this.folderIconsDom = {} as Record<MailFolderType, HTMLElement>
|
||||
this.folderIconsDom = {} as Record<MailSetKind, HTMLElement>
|
||||
}
|
||||
|
||||
update(mail: Mail, selected: boolean, isInMultiSelect: boolean): void {
|
||||
|
@ -255,7 +255,7 @@ export class MailRow implements VirtualRow<Mail> {
|
|||
let iconText = ""
|
||||
|
||||
if (this.showFolderIcon) {
|
||||
let folder = locator.mailModel.getMailFolder(mail._id[0])
|
||||
let folder = locator.mailModel.getMailFolderForMail(mail)
|
||||
iconText += folder ? this.folderIcon(getMailFolderType(folder)) : ""
|
||||
}
|
||||
|
||||
|
@ -291,7 +291,7 @@ export class MailRow implements VirtualRow<Mail> {
|
|||
return iconText
|
||||
}
|
||||
|
||||
private folderIcon(type: MailFolderType): string {
|
||||
private folderIcon(type: MailSetKind): string {
|
||||
return iconMap[type]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ViewSlider } from "../../../common/gui/nav/ViewSlider.js"
|
|||
import { ColumnType, ViewColumn } from "../../../common/gui/base/ViewColumn"
|
||||
import { lang } from "../../../common/misc/LanguageViewModel"
|
||||
import { Dialog } from "../../../common/gui/base/Dialog"
|
||||
import { FeatureType, Keys, MailFolderType } from "../../../common/api/common/TutanotaConstants"
|
||||
import { FeatureType, Keys, MailSetKind } from "../../../common/api/common/TutanotaConstants"
|
||||
import { AppHeaderAttrs, Header } from "../../../common/gui/Header.js"
|
||||
import type { Mail, MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { noOp, ofClass } from "@tutao/tutanota-utils"
|
||||
|
@ -109,12 +109,12 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
this.listColumn = new ViewColumn(
|
||||
{
|
||||
view: () => {
|
||||
const listId = this.mailViewModel.getListId()
|
||||
const folder = this.mailViewModel.getFolder()
|
||||
return m(BackgroundColumnLayout, {
|
||||
backgroundColor: theme.navigation_bg,
|
||||
desktopToolbar: () =>
|
||||
m(DesktopListToolbar, m(SelectAllCheckbox, selectionAttrsForList(this.mailViewModel.listModel)), this.renderFilterButton()),
|
||||
columnLayout: listId
|
||||
columnLayout: folder
|
||||
? m(
|
||||
"",
|
||||
{
|
||||
|
@ -123,9 +123,8 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
},
|
||||
},
|
||||
m(MailListView, {
|
||||
key: listId,
|
||||
key: getElementId(folder),
|
||||
mailViewModel: this.mailViewModel,
|
||||
listId: listId,
|
||||
onSingleSelection: (mail) => {
|
||||
if (!this.mailViewModel.listModel?.state.inMultiselect) {
|
||||
this.viewSlider.focus(this.mailColumn)
|
||||
|
@ -142,7 +141,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
}
|
||||
},
|
||||
onClearFolder: async () => {
|
||||
const folder = this.mailViewModel.getSelectedFolder()
|
||||
const folder = this.mailViewModel.getFolder()
|
||||
if (folder == null) {
|
||||
console.warn("Cannot delete folder, no folder is selected")
|
||||
return
|
||||
|
@ -186,7 +185,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
minWidth: size.second_col_min_width,
|
||||
maxWidth: size.second_col_max_width,
|
||||
headerCenter: () => {
|
||||
const selectedFolder = this.mailViewModel.getSelectedFolder()
|
||||
const selectedFolder = this.mailViewModel.getFolder()
|
||||
return selectedFolder ? getFolderName(selectedFolder) : ""
|
||||
},
|
||||
},
|
||||
|
@ -406,7 +405,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
exec: () => {
|
||||
this.showNewMailDialog().catch(ofClass(PermissionError, noOp))
|
||||
},
|
||||
enabled: () => !!this.mailViewModel.getSelectedFolder() && isNewMailActionAvailable(),
|
||||
enabled: () => !!this.mailViewModel.getFolder() && isNewMailActionAvailable(),
|
||||
help: "newMail_action",
|
||||
},
|
||||
{
|
||||
|
@ -458,7 +457,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
{
|
||||
key: Keys.ONE,
|
||||
exec: () => {
|
||||
this.mailViewModel.switchToFolder(MailFolderType.INBOX)
|
||||
this.mailViewModel.switchToFolder(MailSetKind.INBOX)
|
||||
return true
|
||||
},
|
||||
help: "switchInbox_action",
|
||||
|
@ -466,7 +465,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
{
|
||||
key: Keys.TWO,
|
||||
exec: () => {
|
||||
this.mailViewModel.switchToFolder(MailFolderType.DRAFT)
|
||||
this.mailViewModel.switchToFolder(MailSetKind.DRAFT)
|
||||
return true
|
||||
},
|
||||
help: "switchDrafts_action",
|
||||
|
@ -474,7 +473,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
{
|
||||
key: Keys.THREE,
|
||||
exec: () => {
|
||||
this.mailViewModel.switchToFolder(MailFolderType.SENT)
|
||||
this.mailViewModel.switchToFolder(MailSetKind.SENT)
|
||||
return true
|
||||
},
|
||||
help: "switchSentFolder_action",
|
||||
|
@ -482,7 +481,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
{
|
||||
key: Keys.FOUR,
|
||||
exec: () => {
|
||||
this.mailViewModel.switchToFolder(MailFolderType.TRASH)
|
||||
this.mailViewModel.switchToFolder(MailSetKind.TRASH)
|
||||
return true
|
||||
},
|
||||
help: "switchTrash_action",
|
||||
|
@ -490,7 +489,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
{
|
||||
key: Keys.FIVE,
|
||||
exec: () => {
|
||||
this.mailViewModel.switchToFolder(MailFolderType.ARCHIVE)
|
||||
this.mailViewModel.switchToFolder(MailSetKind.ARCHIVE)
|
||||
return true
|
||||
},
|
||||
enabled: () => locator.logins.isInternalUserLoggedIn(),
|
||||
|
@ -499,7 +498,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
{
|
||||
key: Keys.SIX,
|
||||
exec: () => {
|
||||
this.mailViewModel.switchToFolder(MailFolderType.SPAM)
|
||||
this.mailViewModel.switchToFolder(MailSetKind.SPAM)
|
||||
return true
|
||||
},
|
||||
enabled: () => locator.logins.isInternalUserLoggedIn() && !locator.logins.isEnabled(FeatureType.InternalCommunication),
|
||||
|
@ -594,7 +593,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
return m(MailFoldersView, {
|
||||
mailboxDetail,
|
||||
expandedFolders: this.expandedState,
|
||||
mailListToSelectedMail: this.mailViewModel.getMailListToSelectedMail(),
|
||||
mailFolderToSelectedMail: this.mailViewModel.getMailFolderToSelectedMail(),
|
||||
onFolderClick: () => {
|
||||
if (!inEditMode) {
|
||||
this.viewSlider.focus(this.listColumn)
|
||||
|
@ -643,7 +642,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
this.viewSlider.focusPreviousColumn()
|
||||
}
|
||||
|
||||
this.mailViewModel.showMail(args.listId, args.mailId)
|
||||
this.mailViewModel.showMailWithFolderId(args.folderId, args.mailId)
|
||||
}
|
||||
|
||||
private async handleFolderDrop(droppedMailId: string, folder: MailFolder) {
|
||||
|
@ -677,7 +676,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
}
|
||||
|
||||
private async deleteCustomMailFolder(mailboxDetail: MailboxDetail, folder: MailFolder): Promise<void> {
|
||||
if (folder.folderType !== MailFolderType.CUSTOM) {
|
||||
if (folder.folderType !== MailSetKind.CUSTOM) {
|
||||
throw new Error("Cannot delete non-custom folder: " + String(folder._id))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,34 @@
|
|||
import { ListModel } from "../../../common/misc/ListModel.js"
|
||||
import { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { EntityClient } from "../../../common/api/common/EntityClient.js"
|
||||
import { Mail, MailFolder, MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { firstBiggerThanSecond, GENERATED_MAX_ID, getElementId, isSameId, sortCompareByReverseId } from "../../../common/api/common/utils/EntityUtils.js"
|
||||
import { assertNotNull, count, debounce, lastThrow, lazyMemoized, mapWith, mapWithout, memoized, ofClass, promiseFilter } from "@tutao/tutanota-utils"
|
||||
import { Mail, MailFolder, MailSetEntry, MailSetEntryTypeRef, MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import {
|
||||
elementIdPart,
|
||||
firstBiggerThanSecond,
|
||||
GENERATED_MAX_ID,
|
||||
getElementId,
|
||||
isSameId,
|
||||
listIdPart,
|
||||
sortCompareByReverseId,
|
||||
} from "../../../common/api/common/utils/EntityUtils.js"
|
||||
import {
|
||||
assertNotNull,
|
||||
count,
|
||||
debounce,
|
||||
groupByAndMap,
|
||||
lastThrow,
|
||||
lazyMemoized,
|
||||
mapWith,
|
||||
mapWithout,
|
||||
memoized,
|
||||
ofClass,
|
||||
promiseFilter,
|
||||
} from "@tutao/tutanota-utils"
|
||||
import { ListState } from "../../../common/gui/base/List.js"
|
||||
import { ConversationPrefProvider, ConversationViewModel, ConversationViewModelFactory } from "./ConversationViewModel.js"
|
||||
import { CreateMailViewerOptions } from "./MailViewer.js"
|
||||
import { isOfflineError } from "../../../common/api/common/utils/ErrorUtils.js"
|
||||
import { MailFolderType } from "../../../common/api/common/TutanotaConstants.js"
|
||||
import { MailSetKind, OperationType } from "../../../common/api/common/TutanotaConstants.js"
|
||||
import { WsConnectionState } from "../../../common/api/main/WorkerClient.js"
|
||||
import { WebsocketConnectivityModel } from "../../../common/misc/WebsocketConnectivityModel.js"
|
||||
import { ExposedCacheStorage } from "../../../common/api/worker/rest/DefaultEntityRestCache.js"
|
||||
|
@ -21,8 +41,7 @@ import { Router } from "../../../common/gui/ScopedRouter.js"
|
|||
import { ListFetchResult } from "../../../common/gui/base/ListUtils.js"
|
||||
import { EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
|
||||
import { EventController } from "../../../common/api/main/EventController.js"
|
||||
import { assertSystemFolderOfType, getMailFilterForType, MailFilterType } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isOfTypeOrSubfolderOf } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { assertSystemFolderOfType, getMailFilterForType, isOfTypeOrSubfolderOf, MailFilterType } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isSpamOrTrashFolder, isSubfolderOfType } from "../../../common/api/common/CommonMailUtils.js"
|
||||
|
||||
export interface MailOpenedListener {
|
||||
|
@ -31,7 +50,7 @@ export interface MailOpenedListener {
|
|||
|
||||
/** ViewModel for the overall mail view. */
|
||||
export class MailViewModel {
|
||||
private _listId: Id | null = null
|
||||
private _folder: MailFolder | null = null
|
||||
/** id of the mail we are trying to load based on the URL */
|
||||
private targetMailId: Id | null = null
|
||||
/** needed to prevent parallel target loads*/
|
||||
|
@ -43,7 +62,7 @@ export class MailViewModel {
|
|||
* We remember the last URL used for each folder so if we switch between folders we can keep the selected mail.
|
||||
* There's a similar (but different) hacky mechanism where we store last URL but per each top-level view: navButtonRoutes. This one is per folder.
|
||||
*/
|
||||
private mailListToSelectedMail: ReadonlyMap<Id, Id> = new Map()
|
||||
private mailFolderToSelectedMail: ReadonlyMap<MailFolder, Id> = new Map()
|
||||
private listStreamSubscription: Stream<unknown> | null = null
|
||||
private conversationPref: boolean = false
|
||||
|
||||
|
@ -70,9 +89,14 @@ export class MailViewModel {
|
|||
this.listModel?.setFilter(getMailFilterForType(filter))
|
||||
}
|
||||
|
||||
async showMail(listId?: Id, mailId?: Id) {
|
||||
async showMailWithFolderId(folderId?: Id, mailId?: Id): Promise<void> {
|
||||
const folder: MailFolder | null = folderId ? (await this.getMailboxDetails()).folders.getFolderById(folderId) : null
|
||||
return this.showMail(folder, mailId)
|
||||
}
|
||||
|
||||
async showMail(folder?: MailFolder | null, mailId?: Id) {
|
||||
// an optimization to not open an email that we already display
|
||||
if (listId != null && mailId != null && this.conversationViewModel && isSameId(this.conversationViewModel.primaryMail._id, [listId, mailId])) {
|
||||
if (folder != null && mailId != null && this.conversationViewModel && isSameId(elementIdPart(this.conversationViewModel.primaryMail._id), mailId)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -81,26 +105,26 @@ export class MailViewModel {
|
|||
// target URL
|
||||
this.targetMailId = typeof mailId === "string" ? mailId : null
|
||||
|
||||
let listIdToUse
|
||||
if (typeof listId === "string") {
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailListId(listId)
|
||||
let folderToUse
|
||||
if (folder) {
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(folder)
|
||||
if (mailboxDetail) {
|
||||
listIdToUse = listId
|
||||
folderToUse = folder
|
||||
} else {
|
||||
listIdToUse = await this.getListIdForUserInbox()
|
||||
folderToUse = await this.getFolderForUserInbox()
|
||||
}
|
||||
} else {
|
||||
listIdToUse = this._listId ?? (await this.getListIdForUserInbox())
|
||||
folderToUse = this._folder ?? (await this.getFolderForUserInbox())
|
||||
}
|
||||
|
||||
await this.setListId(listIdToUse)
|
||||
await this.setListId(folderToUse)
|
||||
|
||||
// if there is a target id and we are not loading for this id already then start loading towards that id
|
||||
if (this.targetMailId && this.targetMailId != this.loadingToTargetId) {
|
||||
this.mailListToSelectedMail = mapWith(this.mailListToSelectedMail, listIdToUse, this.targetMailId)
|
||||
this.mailFolderToSelectedMail = mapWith(this.mailFolderToSelectedMail, folderToUse, this.targetMailId)
|
||||
try {
|
||||
this.loadingToTargetId = this.targetMailId
|
||||
await this.loadAndSelectMail([listIdToUse, this.targetMailId])
|
||||
await this.loadAndSelectMail(folderToUse, this.targetMailId)
|
||||
} finally {
|
||||
this.loadingToTargetId = null
|
||||
this.targetMailId = null
|
||||
|
@ -108,16 +132,16 @@ export class MailViewModel {
|
|||
} else {
|
||||
// update URL if the view was just opened without any url params
|
||||
// setListId might not have done it if the list didn't change for us internally but is changed for the view
|
||||
if (listId == null) this.updateUrl()
|
||||
if (folder == null) this.updateUrl()
|
||||
}
|
||||
}
|
||||
|
||||
private async loadAndSelectMail([listId, mailId]: IdTuple) {
|
||||
private async loadAndSelectMail(folder: MailFolder, mailId: Id) {
|
||||
const foundMail = await this.listModel?.loadAndSelect(
|
||||
mailId,
|
||||
() =>
|
||||
// if we changed the list, stop
|
||||
this.getListId() !== listId ||
|
||||
this.getFolder() !== folder ||
|
||||
// if listModel is gone for some reason, stop
|
||||
!this.listModel ||
|
||||
// if the target mail has changed, stop
|
||||
|
@ -126,13 +150,13 @@ export class MailViewModel {
|
|||
(this.listModel.state.items.length > 0 && firstBiggerThanSecond(mailId, getElementId(lastThrow(this.listModel.state.items)))),
|
||||
)
|
||||
if (foundMail == null) {
|
||||
console.log("did not find mail", listId, mailId)
|
||||
console.log("did not find mail", folder, mailId)
|
||||
}
|
||||
}
|
||||
|
||||
private async getListIdForUserInbox(): Promise<Id> {
|
||||
private async getFolderForUserInbox(): Promise<MailFolder> {
|
||||
const mailboxDetail = await this.mailModel.getUserMailboxDetails()
|
||||
return assertSystemFolderOfType(mailboxDetail.folders, MailFolderType.INBOX).mails
|
||||
return assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.INBOX)
|
||||
}
|
||||
|
||||
init() {
|
||||
|
@ -155,26 +179,26 @@ export class MailViewModel {
|
|||
})
|
||||
|
||||
get listModel(): ListModel<Mail> | null {
|
||||
return this._listId ? this._listModel(this._listId) : null
|
||||
return this._folder ? this.listModelForFolder(getElementId(this._folder)) : null
|
||||
}
|
||||
|
||||
getMailListToSelectedMail(): ReadonlyMap<Id, Id> {
|
||||
return this.mailListToSelectedMail
|
||||
getMailFolderToSelectedMail(): ReadonlyMap<MailFolder, Id> {
|
||||
return this.mailFolderToSelectedMail
|
||||
}
|
||||
|
||||
getListId(): Id | null {
|
||||
return this._listId
|
||||
getFolder(): MailFolder | null {
|
||||
return this._folder
|
||||
}
|
||||
|
||||
private async setListId(id: Id) {
|
||||
if (id === this._listId) {
|
||||
private async setListId(folder: MailFolder) {
|
||||
if (folder === this._folder) {
|
||||
return
|
||||
}
|
||||
// Cancel old load all
|
||||
this.listModel?.cancelLoadAll()
|
||||
this._filterType = null
|
||||
|
||||
this._listId = id
|
||||
this._folder = folder
|
||||
this.listStreamSubscription?.end(true)
|
||||
this.listStreamSubscription = this.listModel!.stateStream.map((state) => this.onListStateChange(state))
|
||||
await this.listModel!.loadInitial()
|
||||
|
@ -184,49 +208,53 @@ export class MailViewModel {
|
|||
return this.conversationViewModel
|
||||
}
|
||||
|
||||
private _listModel = memoized((listId: Id) => {
|
||||
private listModelForFolder = memoized((folderId: Id) => {
|
||||
return new ListModel<Mail>({
|
||||
topId: GENERATED_MAX_ID,
|
||||
fetch: async (startId, count) => {
|
||||
const { complete, items } = await this.loadMailRange(listId, startId, count)
|
||||
const folder = assertNotNull(this._folder)
|
||||
const { complete, items } = await this.loadMailRange(folder, startId, count)
|
||||
if (complete) {
|
||||
this.fixCounterIfNeeded(listId, [])
|
||||
this.fixCounterIfNeeded(folder, [])
|
||||
}
|
||||
return { complete, items }
|
||||
},
|
||||
loadSingle: (elementId: Id): Promise<Mail | null> => this.entityClient.load(MailTypeRef, [listId, elementId]),
|
||||
loadSingle: (listId: Id, elementId: Id): Promise<Mail | null> => {
|
||||
return this.entityClient.load(MailTypeRef, [listId, elementId])
|
||||
},
|
||||
sortCompare: sortCompareByReverseId,
|
||||
autoSelectBehavior: () => this.conversationPrefProvider.getMailAutoSelectBehavior(),
|
||||
})
|
||||
})
|
||||
|
||||
private fixCounterIfNeeded: (listId: Id, itemsWhenCalled: ReadonlyArray<Mail>) => void = debounce(
|
||||
private fixCounterIfNeeded: (folder: MailFolder, itemsWhenCalled: ReadonlyArray<Mail>) => void = debounce(
|
||||
2000,
|
||||
async (listId: Id, itemsWhenCalled: ReadonlyArray<Mail>) => {
|
||||
if (this._filterType != null && this.filterType !== MailFilterType.Unread) {
|
||||
async (folder: MailFolder, itemsWhenCalled: ReadonlyArray<Mail>) => {
|
||||
const ourFolder = this.getFolder()
|
||||
if (ourFolder == null || (this._filterType != null && this.filterType !== MailFilterType.Unread)) {
|
||||
return
|
||||
}
|
||||
|
||||
// If folders are changed, list won't have the data we need.
|
||||
// Do not rely on counters if we are not connected
|
||||
if (this.getListId() !== listId || this.connectivityModel.wsConnection()() !== WsConnectionState.connected) {
|
||||
if (!isSameId(getElementId(ourFolder), getElementId(folder)) || this.connectivityModel.wsConnection()() !== WsConnectionState.connected) {
|
||||
return
|
||||
}
|
||||
|
||||
// If list was modified in the meantime, we cannot be sure that we will fix counters correctly (e.g. because of the inbox rules)
|
||||
if (this.listModel?.state.items !== itemsWhenCalled) {
|
||||
console.log(`list changed, trying again later`)
|
||||
return this.fixCounterIfNeeded(listId, this.listModel?.state.items ?? [])
|
||||
return this.fixCounterIfNeeded(folder, this.listModel?.state.items ?? [])
|
||||
}
|
||||
|
||||
const unreadMailsCount = count(this.listModel.state.items, (e) => e.unread)
|
||||
|
||||
const counterValue = await this.mailModel.getCounterValue(listId)
|
||||
const counterValue = await this.mailModel.getCounterValue(folder)
|
||||
if (counterValue != null && counterValue !== unreadMailsCount) {
|
||||
console.log(`fixing up counter for list ${listId}`)
|
||||
await this.mailModel.fixupCounterForMailList(listId, unreadMailsCount)
|
||||
console.log(`fixing up counter for folder ${folder._id}`)
|
||||
await this.mailModel.fixupCounterForFolder(folder, unreadMailsCount)
|
||||
} else {
|
||||
console.log(`same counter, no fixup on list ${listId}`)
|
||||
console.log(`same counter, no fixup on folder ${folder._id}`)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -235,7 +263,7 @@ export class MailViewModel {
|
|||
if (!newState.inMultiselect && newState.selectedItems.size === 1) {
|
||||
const mail = this.listModel!.getSelectedAsArray()[0]
|
||||
if (!this.conversationViewModel || !isSameId(this.conversationViewModel?.primaryMail._id, mail._id)) {
|
||||
this.mailListToSelectedMail = mapWith(this.mailListToSelectedMail, assertNotNull(this.getListId()), getElementId(mail))
|
||||
this.mailFolderToSelectedMail = mapWith(this.mailFolderToSelectedMail, assertNotNull(this.getFolder()), getElementId(mail))
|
||||
|
||||
this.createConversationViewModel({
|
||||
mail,
|
||||
|
@ -246,19 +274,20 @@ export class MailViewModel {
|
|||
} else {
|
||||
this.conversationViewModel?.dispose()
|
||||
this.conversationViewModel = null
|
||||
this.mailListToSelectedMail = mapWithout(this.mailListToSelectedMail, assertNotNull(this.getListId()))
|
||||
this.mailFolderToSelectedMail = mapWithout(this.mailFolderToSelectedMail, assertNotNull(this.getFolder()))
|
||||
}
|
||||
this.updateUrl()
|
||||
this.updateUi()
|
||||
}
|
||||
|
||||
private updateUrl() {
|
||||
const listId = this._listId
|
||||
const mailId = this.targetMailId ?? (listId ? this.getMailListToSelectedMail().get(listId) : null)
|
||||
const folder = this._folder
|
||||
const folderId = folder ? getElementId(folder) : null
|
||||
const mailId = this.targetMailId ?? (folder ? this.getMailFolderToSelectedMail().get(folder) : null)
|
||||
if (mailId != null) {
|
||||
this.router.routeTo("/mail/:listId/:mailId", { listId, mailId })
|
||||
this.router.routeTo("/mail/:folderId/:mailId", { folderId, mailId })
|
||||
} else {
|
||||
this.router.routeTo("/mail/:listId", { listId: listId ?? "" })
|
||||
this.router.routeTo("/mail/:folderId", { folderId: folderId ?? "" })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,18 +296,120 @@ export class MailViewModel {
|
|||
this.conversationViewModel = this.conversationViewModelFactory(viewModelParams)
|
||||
}
|
||||
|
||||
async entityEventsReceived(updates: ReadonlyArray<EntityUpdateData>) {
|
||||
async entityEventsReceivedForLegacy(updates: ReadonlyArray<EntityUpdateData>) {
|
||||
for (const update of updates) {
|
||||
if (isUpdateForTypeRef(MailTypeRef, update) && update.instanceListId === this._listId) {
|
||||
await this.listModel?.entityEventReceived(update.instanceId, update.operation)
|
||||
if (isUpdateForTypeRef(MailTypeRef, update) && update.instanceListId === this._folder?.mails) {
|
||||
await this.listModel?.entityEventReceived(update.instanceListId, update.instanceId, update.operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadMailRange(listId: Id, start: Id, count: number): Promise<ListFetchResult<Mail>> {
|
||||
async entityEventsReceived(updates: ReadonlyArray<EntityUpdateData>) {
|
||||
const folder = this._folder
|
||||
const listModel = this.listModel
|
||||
|
||||
if (!folder || !listModel) {
|
||||
return
|
||||
}
|
||||
if (!folder.isMailSet) {
|
||||
return this.entityEventsReceivedForLegacy(updates)
|
||||
}
|
||||
|
||||
let [mailEvent, oldEntryEvent, newEntryEvent]: EntityUpdateData[] = []
|
||||
for (const event of updates) {
|
||||
if (isUpdateForTypeRef(MailTypeRef, event)) {
|
||||
mailEvent = event
|
||||
} else if (isUpdateForTypeRef(MailSetEntryTypeRef, event)) {
|
||||
if (event.operation == OperationType.DELETE) {
|
||||
oldEntryEvent = event
|
||||
} else {
|
||||
newEntryEvent = event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSameId(folder.entries, newEntryEvent?.instanceListId)) {
|
||||
await this.listModel?.entityEventReceived(mailEvent.instanceListId, mailEvent.instanceId, OperationType.CREATE)
|
||||
} else if (isSameId(folder.entries, oldEntryEvent?.instanceListId)) {
|
||||
await this.listModel?.entityEventReceived(mailEvent.instanceListId, mailEvent.instanceId, OperationType.DELETE)
|
||||
} else if (mailEvent && !oldEntryEvent && !newEntryEvent) {
|
||||
const mail = await this.entityClient.load(MailTypeRef, [mailEvent.instanceListId, mailEvent.instanceId])
|
||||
if (mail.sets.some((id) => isSameId(elementIdPart(id), getElementId(folder)))) {
|
||||
await this.listModel?.entityEventReceived(mailEvent.instanceListId, mailEvent.instanceId, mailEvent.operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadMailRange(folder: MailFolder, start: Id, count: number): Promise<ListFetchResult<Mail>> {
|
||||
if (folder.isMailSet) {
|
||||
return await this.loadMailSetMailRange(folder, start, count)
|
||||
} else {
|
||||
return await this.loadLegacyMailRange(folder, start, count)
|
||||
}
|
||||
}
|
||||
|
||||
private async loadMailSetMailRange(folder: MailFolder, start: string, count: number) {
|
||||
try {
|
||||
const loadMailSetEntries = () => this.entityClient.loadRange(MailSetEntryTypeRef, folder.entries, start, count, true)
|
||||
const loadMails = (listId: Id, mailIds: Array<Id>) => this.entityClient.loadMultiple(MailTypeRef, listId, mailIds)
|
||||
|
||||
const mails = await this.acquireMails(loadMailSetEntries, loadMails)
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(folder)
|
||||
// For inbox rules there are two points where we might want to apply them. The first one is MailModel which applied inbox rules as they are received
|
||||
// in real time. The second one is here, when we load emails in inbox. If they are unread we want to apply inbox rules to them. If inbox rule
|
||||
// applies, the email is moved out of the inbox and we don't return it here.
|
||||
if (mailboxDetail) {
|
||||
const mailsToKeepInInbox = await promiseFilter(mails, async (mail) => {
|
||||
const wasMatched = await this.inboxRuleHandler.findAndApplyMatchingRule(mailboxDetail, mail, true)
|
||||
return !wasMatched
|
||||
})
|
||||
return { items: mailsToKeepInInbox, complete: mails.length < count }
|
||||
} else {
|
||||
return { items: mails, complete: mails.length < count }
|
||||
}
|
||||
} catch (e) {
|
||||
// The way the cache works is that it tries to fulfill the API contract of returning as many items as requested as long as it can.
|
||||
// This is problematic for offline where we might not have the full page of emails loaded (e.g. we delete part as it's too old or we move emails
|
||||
// around). Because of that cache will try to load additional items from the server in order to return `count` items. If it fails to load them,
|
||||
// it will not return anything and instead will throw an error.
|
||||
// This is generally fine but in case of offline we want to display everything that we have cached. For that we fetch directly from the cache,
|
||||
// give it to the list and let list make another request (and almost certainly fail that request) to show a retry button. This way we both show
|
||||
// the items we have and also show that we couldn't load everything.
|
||||
if (isOfflineError(e)) {
|
||||
const loadMailSetEntries = () => this.cacheStorage.provideFromRange(MailSetEntryTypeRef, folder.entries, start, count, true)
|
||||
const loadMails = (listId: Id, mailIds: Array<Id>) => this.cacheStorage.provideMultiple(MailTypeRef, listId, mailIds)
|
||||
const items = await this.acquireMails(loadMailSetEntries, loadMails)
|
||||
if (items.length === 0) throw e
|
||||
return { items, complete: false }
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load mails either from remote or from offline storage. Loader functions must be implemented for each use case.
|
||||
*/
|
||||
private async acquireMails(loadMailSetEntries: () => Promise<MailSetEntry[]>, loadMails: (listId: Id, mailIds: Array<Id>) => Promise<Mail[]>) {
|
||||
const mailSetEntries = await loadMailSetEntries()
|
||||
const mailListIdToMailIds = groupByAndMap(
|
||||
mailSetEntries,
|
||||
(mse) => listIdPart(mse.mail),
|
||||
(mse) => elementIdPart(mse.mail),
|
||||
)
|
||||
const mails: Array<Mail> = []
|
||||
for (const [listId, mailIds] of mailListIdToMailIds) {
|
||||
mails.push(...(await loadMails(listId, mailIds)))
|
||||
}
|
||||
mails.sort((a, b) => b.receivedDate.getTime() - a.receivedDate.getTime())
|
||||
return mails
|
||||
}
|
||||
|
||||
private async loadLegacyMailRange(folder: MailFolder, start: string, count: number) {
|
||||
const listId = folder.mails
|
||||
try {
|
||||
const items = await this.entityClient.loadRange(MailTypeRef, listId, start, count, true)
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailListId(listId)
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(folder)
|
||||
// For inbox rules there are two points where we might want to apply them. The first one is MailModel which applied inbox rules as they are received
|
||||
// in real time. The second one is here, when we load emails in inbox. If they are unread we want to apply inbox rules to them. If inbox rule
|
||||
// applies, the email is moved out of the inbox and we don't return it here.
|
||||
|
@ -309,46 +440,39 @@ export class MailViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
async switchToFolder(folderType: Omit<MailFolderType, MailFolderType.CUSTOM>): Promise<void> {
|
||||
async switchToFolder(folderType: Omit<MailSetKind, MailSetKind.CUSTOM>): Promise<void> {
|
||||
const mailboxDetail = assertNotNull(await this.getMailboxDetails())
|
||||
const listId = assertSystemFolderOfType(mailboxDetail.folders, folderType).mails
|
||||
await this.showMail(listId, this.mailListToSelectedMail.get(listId))
|
||||
const folder = assertSystemFolderOfType(mailboxDetail.folders, folderType)
|
||||
await this.showMail(folder, this.mailFolderToSelectedMail.get(folder))
|
||||
}
|
||||
|
||||
async getMailboxDetails(): Promise<MailboxDetail> {
|
||||
const listId = this.getListId()
|
||||
return await this.mailboxDetailForListWithFallback(listId)
|
||||
}
|
||||
|
||||
getSelectedFolder(): MailFolder | null {
|
||||
const listId = this.getListId()
|
||||
return listId ? this.mailModel.getMailFolder(listId) : null
|
||||
const folder = this.getFolder()
|
||||
return await this.mailboxDetailForListWithFallback(folder)
|
||||
}
|
||||
|
||||
async showingDraftsFolder(): Promise<boolean> {
|
||||
if (!this._listId) return false
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailListId(this._listId)
|
||||
const selectedFolder = this.getSelectedFolder()
|
||||
if (!this._folder) return false
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(this._folder)
|
||||
const selectedFolder = this.getFolder()
|
||||
if (selectedFolder && mailboxDetail) {
|
||||
return isOfTypeOrSubfolderOf(mailboxDetail.folders, selectedFolder, MailFolderType.DRAFT)
|
||||
return isOfTypeOrSubfolderOf(mailboxDetail.folders, selectedFolder, MailSetKind.DRAFT)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async showingTrashOrSpamFolder(): Promise<boolean> {
|
||||
const listId = this._listId
|
||||
if (!listId) return false
|
||||
const folder = await this.mailModel.getMailFolder(listId)
|
||||
const folder = this.getFolder()
|
||||
if (!folder) {
|
||||
return false
|
||||
}
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailListId(listId)
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(folder)
|
||||
return mailboxDetail != null && isSpamOrTrashFolder(mailboxDetail.folders, folder)
|
||||
}
|
||||
|
||||
private async mailboxDetailForListWithFallback(listId?: string | null) {
|
||||
const mailboxDetailForListId = typeof listId === "string" ? await this.mailModel.getMailboxDetailsForMailListId(listId) : null
|
||||
private async mailboxDetailForListWithFallback(folder?: MailFolder | null) {
|
||||
const mailboxDetailForListId = folder ? await this.mailModel.getMailboxDetailsForMailFolder(folder) : null
|
||||
return mailboxDetailForListId ?? (await this.mailModel.getUserMailboxDetails())
|
||||
}
|
||||
|
||||
|
@ -359,16 +483,13 @@ export class MailViewModel {
|
|||
const mailboxDetail = await this.getMailboxDetails()
|
||||
|
||||
// the request is handled a little differently if it is the system folder vs a subfolder
|
||||
if (folder.folderType === MailFolderType.TRASH || folder.folderType === MailFolderType.SPAM) {
|
||||
if (folder.folderType === MailSetKind.TRASH || folder.folderType === MailSetKind.SPAM) {
|
||||
return this.mailModel.clearFolder(folder).catch(
|
||||
ofClass(PreconditionFailedError, () => {
|
||||
throw new UserError("operationStillActive_msg")
|
||||
}),
|
||||
)
|
||||
} else if (
|
||||
isSubfolderOfType(mailboxDetail.folders, folder, MailFolderType.TRASH) ||
|
||||
isSubfolderOfType(mailboxDetail.folders, folder, MailFolderType.SPAM)
|
||||
) {
|
||||
} else if (isSubfolderOfType(mailboxDetail.folders, folder, MailSetKind.TRASH) || isSubfolderOfType(mailboxDetail.folders, folder, MailSetKind.SPAM)) {
|
||||
return this.mailModel.finallyDeleteCustomMailFolder(folder).catch(
|
||||
ofClass(PreconditionFailedError, () => {
|
||||
throw new UserError("operationStillActive_msg")
|
||||
|
|
|
@ -2,7 +2,7 @@ import { px, size } from "../../../common/gui/size"
|
|||
import m, { Children, Component, Vnode } from "mithril"
|
||||
import stream from "mithril/stream"
|
||||
import { windowFacade, windowSizeListener } from "../../../common/misc/WindowFacade"
|
||||
import { FeatureType, InboxRuleType, Keys, MailFolderType, SpamRuleFieldType, SpamRuleType } from "../../../common/api/common/TutanotaConstants"
|
||||
import { FeatureType, InboxRuleType, Keys, MailSetKind, SpamRuleFieldType, SpamRuleType } from "../../../common/api/common/TutanotaConstants"
|
||||
import { File as TutanotaFile, Mail } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { lang } from "../../../common/misc/LanguageViewModel"
|
||||
import { assertMainOrNode } from "../../../common/api/common/Env"
|
||||
|
@ -20,7 +20,6 @@ import { replaceCidsWithInlineImages } from "./MailGuiUtils"
|
|||
import { getCoordsOfMouseOrTouchEvent } from "../../../common/gui/base/GuiUtils"
|
||||
import { copyToClipboard } from "../../../common/misc/ClipboardUtils"
|
||||
import { ContentBlockingStatus, MailViewerViewModel } from "./MailViewerViewModel"
|
||||
import { getListId } from "../../../common/api/common/utils/EntityUtils"
|
||||
import { createEmailSenderListElement } from "../../../common/api/entities/sys/TypeRefs.js"
|
||||
import { UserError } from "../../../common/api/main/UserError"
|
||||
import { showUserError } from "../../../common/misc/ErrorHandlerImpl"
|
||||
|
@ -33,8 +32,7 @@ import { locator } from "../../../common/api/main/CommonLocator.js"
|
|||
import { PinchZoom } from "../../../common/gui/PinchZoom.js"
|
||||
import { responsiveCardHMargin, responsiveCardHPadding } from "../../../common/gui/cards.js"
|
||||
import { Dialog } from "../../../common/gui/base/Dialog.js"
|
||||
import { createNewContact, getExistingRuleForType } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isTutanotaTeamMail } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { createNewContact, getExistingRuleForType, isTutanotaTeamMail } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
|
@ -655,9 +653,9 @@ export class MailViewer implements Component<MailViewerAttrs> {
|
|||
}
|
||||
|
||||
private addSpamRule(defaultInboxRuleField: InboxRuleType | null, address: string) {
|
||||
const folder = this.viewModel.mailModel.getMailFolder(getListId(this.viewModel.mail))
|
||||
const folder = this.viewModel.mailModel.getMailFolderForMail(this.viewModel.mail)
|
||||
|
||||
const spamRuleType = folder && folder.folderType === MailFolderType.SPAM ? SpamRuleType.WHITELIST : SpamRuleType.BLACKLIST
|
||||
const spamRuleType = folder && folder.folderType === MailSetKind.SPAM ? SpamRuleType.WHITELIST : SpamRuleType.BLACKLIST
|
||||
|
||||
let spamRuleField: SpamRuleFieldType
|
||||
switch (defaultInboxRuleField) {
|
||||
|
|
|
@ -13,10 +13,10 @@ import {
|
|||
ExternalImageRule,
|
||||
FeatureType,
|
||||
MailAuthenticationStatus,
|
||||
MailFolderType,
|
||||
MailMethod,
|
||||
MailPhishingStatus,
|
||||
MailReportType,
|
||||
MailSetKind,
|
||||
MailState,
|
||||
OperationType,
|
||||
} from "../../../common/api/common/TutanotaConstants"
|
||||
|
@ -42,7 +42,7 @@ import { lang } from "../../../common/misc/LanguageViewModel"
|
|||
import { LoginController } from "../../../common/api/main/LoginController"
|
||||
import m from "mithril"
|
||||
import { LockedError, NotAuthorizedError, NotFoundError } from "../../../common/api/common/error/RestError"
|
||||
import { getListId, haveSameId, isSameId } from "../../../common/api/common/utils/EntityUtils"
|
||||
import { haveSameId, isSameId } from "../../../common/api/common/utils/EntityUtils"
|
||||
import { getReferencedAttachments, loadInlineImages, moveMails } from "./MailGuiUtils"
|
||||
import { SanitizedFragment } from "../../../common/misc/HtmlSanitizer"
|
||||
import { CALENDAR_MIME_TYPE, FileController } from "../../../common/file/FileController"
|
||||
|
@ -69,16 +69,18 @@ import { AttachmentType, getAttachmentType } from "../../../common/gui/Attachmen
|
|||
import type { ContactImporter } from "../../contacts/ContactImporter.js"
|
||||
import { InlineImages, revokeInlineImages } from "../../../common/mailFunctionality/inlineImagesUtils.js"
|
||||
import {
|
||||
getPathToFolderString,
|
||||
assertSystemFolderOfType,
|
||||
getDefaultSender,
|
||||
getEnabledMailAddressesWithUser,
|
||||
getMailboxName,
|
||||
getFolderName,
|
||||
getMailboxName,
|
||||
getPathToFolderString,
|
||||
isNoReplyTeamAddress,
|
||||
isSystemNotification,
|
||||
isTutanotaTeamMail,
|
||||
loadMailDetails,
|
||||
loadMailHeaders,
|
||||
getDefaultSender,
|
||||
assertSystemFolderOfType,
|
||||
} from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isSystemNotification, isTutanotaTeamMail, isNoReplyTeamAddress } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { getDisplayedSender, getMailBodyText, MailAddressAndName } from "../../../common/api/common/CommonMailUtils.js"
|
||||
|
||||
export const enum ContentBlockingStatus {
|
||||
|
@ -198,7 +200,7 @@ export class MailViewerViewModel {
|
|||
|
||||
private showFolder() {
|
||||
this.folderMailboxText = null
|
||||
const folder = this.mailModel.getMailFolder(this.mail._id[0])
|
||||
const folder = this.mailModel.getMailFolderForMail(this.mail)
|
||||
|
||||
if (folder) {
|
||||
this.mailModel.getMailboxDetailsForMail(this.mail).then((mailboxDetails) => {
|
||||
|
@ -310,10 +312,10 @@ export class MailViewerViewModel {
|
|||
return this.folderMailboxText
|
||||
}
|
||||
|
||||
getFolderInfo(): { folderType: MailFolderType; name: string } | null {
|
||||
const folder = this.mailModel.getMailFolder(getListId(this.mail))
|
||||
getFolderInfo(): { folderType: MailSetKind; name: string } | null {
|
||||
const folder = this.mailModel.getMailFolderForMail(this.mail)
|
||||
if (!folder) return null
|
||||
return { folderType: folder.folderType as MailFolderType, name: getFolderName(folder) }
|
||||
return { folderType: folder.folderType as MailSetKind, name: getFolderName(folder) }
|
||||
}
|
||||
|
||||
getSubject(): string {
|
||||
|
@ -512,7 +514,7 @@ export class MailViewerViewModel {
|
|||
if (mailboxDetail == null) {
|
||||
return
|
||||
}
|
||||
const spamFolder = assertSystemFolderOfType(mailboxDetail.folders, MailFolderType.SPAM)
|
||||
const spamFolder = assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.SPAM)
|
||||
// do not report moved mails again
|
||||
await moveMails({ mailModel: this.mailModel, mails: [this.mail], targetMailFolder: spamFolder, isReportable: false })
|
||||
} catch (e) {
|
||||
|
|
|
@ -153,7 +153,7 @@ export class SearchModel {
|
|||
continue
|
||||
}
|
||||
|
||||
if (restriction.listIds.length > 0 && !restriction.listIds.includes(listIdPart(event._id))) {
|
||||
if (restriction.folderIds.length > 0 && !restriction.folderIds.includes(listIdPart(event._id))) {
|
||||
// check that the event is in the searched calendar.
|
||||
continue
|
||||
}
|
||||
|
@ -255,7 +255,7 @@ export function isSameSearchRestriction(a: SearchRestriction, b: SearchRestricti
|
|||
a.field === b.field &&
|
||||
isSameAttributeIds &&
|
||||
(a.eventSeries === b.eventSeries || (a.eventSeries === null && b.eventSeries === true) || (a.eventSeries === true && b.eventSeries === null)) &&
|
||||
arrayEquals(a.listIds, b.listIds)
|
||||
arrayEquals(a.folderIds, b.folderIds)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -139,8 +139,8 @@ export function getSearchParameters(
|
|||
if (restriction.end) {
|
||||
params.end = restriction.end
|
||||
}
|
||||
if (restriction.listIds.length > 0) {
|
||||
params.list = restriction.listIds
|
||||
if (restriction.folderIds.length > 0) {
|
||||
params.folder = restriction.folderIds
|
||||
}
|
||||
if (restriction.field) {
|
||||
params.field = restriction.field
|
||||
|
@ -167,14 +167,14 @@ export function createRestriction(
|
|||
start: number | null,
|
||||
end: number | null,
|
||||
field: string | null,
|
||||
listIds: Array<string>,
|
||||
folderIds: Array<string>,
|
||||
eventSeries: boolean | null,
|
||||
): SearchRestriction {
|
||||
if (locator.logins.getUserController().isFreeAccount() && searchCategory === SearchCategoryTypes.mail) {
|
||||
start = null
|
||||
end = getFreeSearchStartDate().getTime()
|
||||
field = null
|
||||
listIds = []
|
||||
folderIds = []
|
||||
eventSeries = null
|
||||
}
|
||||
|
||||
|
@ -184,7 +184,7 @@ export function createRestriction(
|
|||
end: end,
|
||||
field: null,
|
||||
attributeIds: null,
|
||||
listIds,
|
||||
folderIds,
|
||||
eventSeries,
|
||||
}
|
||||
|
||||
|
@ -226,7 +226,7 @@ export function getRestriction(route: string): SearchRestriction {
|
|||
let start: number | null = null
|
||||
let end: number | null = null
|
||||
let field: string | null = null
|
||||
let listIds: Array<string> = []
|
||||
let folderIds: Array<string> = []
|
||||
let eventSeries: boolean | null = null
|
||||
|
||||
if (route.startsWith("/mail") || route.startsWith("/search/mail")) {
|
||||
|
@ -249,8 +249,8 @@ export function getRestriction(route: string): SearchRestriction {
|
|||
field = SEARCH_MAIL_FIELDS.find((f) => f.field === fieldString)?.field ?? null
|
||||
}
|
||||
|
||||
if (Array.isArray(params["list"])) {
|
||||
listIds = params["list"]
|
||||
if (Array.isArray(params["folder"])) {
|
||||
folderIds = params["folder"]
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("invalid query: " + route, e)
|
||||
|
@ -274,9 +274,9 @@ export function getRestriction(route: string): SearchRestriction {
|
|||
end = filterInt(params["end"])
|
||||
}
|
||||
|
||||
const list = params["list"]
|
||||
if (Array.isArray(list)) {
|
||||
listIds = list
|
||||
const folder = params["folder"]
|
||||
if (Array.isArray(folder)) {
|
||||
folderIds = folder
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("invalid query: " + route, e)
|
||||
|
@ -298,7 +298,7 @@ export function getRestriction(route: string): SearchRestriction {
|
|||
throw new Error("invalid type " + route)
|
||||
}
|
||||
|
||||
return createRestriction(category, start, end, field, listIds, eventSeries)
|
||||
return createRestriction(category, start, end, field, folderIds, eventSeries)
|
||||
}
|
||||
|
||||
export function decodeCalendarSearchKey(searchKey: string): { id: Id; start: number } {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ViewSlider } from "../../../common/gui/nav/ViewSlider.js"
|
|||
import { ColumnType, ViewColumn } from "../../../common/gui/base/ViewColumn"
|
||||
import type { TranslationKey } from "../../../common/misc/LanguageViewModel"
|
||||
import { lang } from "../../../common/misc/LanguageViewModel"
|
||||
import { FeatureType, Keys, MailFolderType } from "../../../common/api/common/TutanotaConstants"
|
||||
import { FeatureType, Keys, MailSetKind } from "../../../common/api/common/TutanotaConstants"
|
||||
import { assertMainOrNode } from "../../../common/api/common/Env"
|
||||
import { keyManager, Shortcut } from "../../../common/misc/KeyManager"
|
||||
import { NavButton, NavButtonColor } from "../../../common/gui/base/NavButton.js"
|
||||
|
@ -648,11 +648,12 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
|
|||
const mailboxIndex = mailboxes.indexOf(mailbox)
|
||||
const mailFolders = mailbox.folders.getIndentedList()
|
||||
for (const folderInfo of mailFolders) {
|
||||
if (folderInfo.folder.folderType !== MailFolderType.SPAM) {
|
||||
if (folderInfo.folder.folderType !== MailSetKind.SPAM) {
|
||||
const mailboxLabel = mailboxIndex === 0 ? "" : ` (${getGroupInfoDisplayName(mailbox.mailGroupInfo)})`
|
||||
const folderId = folderInfo.folder.isMailSet ? getElementId(folderInfo.folder) : folderInfo.folder.mails
|
||||
availableMailFolders.push({
|
||||
name: getIndentedFolderNameForDropdown(folderInfo) + mailboxLabel,
|
||||
value: folderInfo.folder.mails,
|
||||
value: folderId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { SearchRestriction, SearchResult } from "../../../common/api/worker/sear
|
|||
import { EntityEventsListener, EventController } from "../../../common/api/main/EventController.js"
|
||||
import { CalendarEvent, CalendarEventTypeRef, Contact, ContactTypeRef, Mail, MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { ListElementEntity, SomeEntity } from "../../../common/api/common/EntityTypes.js"
|
||||
import { FULL_INDEXED_TIMESTAMP, MailFolderType, NOTHING_INDEXED_TIMESTAMP, OperationType } from "../../../common/api/common/TutanotaConstants.js"
|
||||
import { FULL_INDEXED_TIMESTAMP, MailSetKind, NOTHING_INDEXED_TIMESTAMP, OperationType } from "../../../common/api/common/TutanotaConstants.js"
|
||||
import {
|
||||
assertIsEntity,
|
||||
assertIsEntity2,
|
||||
|
@ -216,7 +216,7 @@ export class SearchViewModel {
|
|||
}
|
||||
|
||||
private listIdMatchesRestriction(listId: string, restriction: SearchRestriction): boolean {
|
||||
return restriction.listIds.length === 0 || restriction.listIds.includes(listId)
|
||||
return restriction.folderIds.length === 0 || restriction.folderIds.includes(listId)
|
||||
}
|
||||
|
||||
onNewUrl(args: Record<string, any>, requestedPath: string) {
|
||||
|
@ -279,13 +279,13 @@ export class SearchViewModel {
|
|||
this.selectedMailField = restriction.field
|
||||
this.startDate = restriction.end ? new Date(restriction.end) : null
|
||||
this.endDate = restriction.start ? new Date(restriction.start) : null
|
||||
this.selectedMailFolder = restriction.listIds
|
||||
this.selectedMailFolder = restriction.folderIds
|
||||
this.loadAndSelectIfNeeded(args.id)
|
||||
this.latestMailRestriction = restriction
|
||||
} else if (isSameTypeRef(restriction.type, CalendarEventTypeRef)) {
|
||||
this.startDate = restriction.start ? new Date(restriction.start) : null
|
||||
this.endDate = restriction.end ? new Date(restriction.end) : null
|
||||
this.selectedCalendar = this.extractCalendarListIds(restriction.listIds)
|
||||
this.selectedCalendar = this.extractCalendarListIds(restriction.folderIds)
|
||||
this.includeRepeatingEvents = restriction.eventSeries ?? true
|
||||
this.lazyCalendarInfos.load()
|
||||
this.latestCalendarRestriction = restriction
|
||||
|
@ -555,8 +555,8 @@ export class SearchViewModel {
|
|||
|
||||
// if selected folder no longer exist select another one
|
||||
const selectedMailFolder = this.selectedMailFolder
|
||||
if (selectedMailFolder[0] && mailboxes.every((mailbox) => mailbox.folders.getFolderByMailListId(selectedMailFolder[0]) == null)) {
|
||||
this.selectedMailFolder = [assertNotNull(mailboxes[0].folders.getSystemFolderByType(MailFolderType.INBOX)).mails]
|
||||
if (selectedMailFolder[0] && mailboxes.every((mailbox) => mailbox.folders.getFolderById(selectedMailFolder[0]) == null)) {
|
||||
this.selectedMailFolder = [getElementId(assertNotNull(mailboxes[0].folders.getSystemFolderByType(MailSetKind.INBOX)))]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -578,8 +578,8 @@ export class SearchViewModel {
|
|||
(email) => update.instanceId === elementIdPart(email) && update.instanceListId !== listIdPart(email),
|
||||
)
|
||||
if (index >= 0) {
|
||||
const restrictionLength = this._searchResult.restriction.listIds.length
|
||||
if ((restrictionLength > 0 && this._searchResult.restriction.listIds.includes(update.instanceListId)) || restrictionLength === 0) {
|
||||
const restrictionLength = this._searchResult.restriction.folderIds.length
|
||||
if ((restrictionLength > 0 && this._searchResult.restriction.folderIds.includes(update.instanceListId)) || restrictionLength === 0) {
|
||||
// We need to update the listId of the updated item, since it was moved to another folder.
|
||||
const newIdTuple: IdTuple = [update.instanceListId, update.instanceId]
|
||||
this._searchResult.results[index] = newIdTuple
|
||||
|
@ -608,7 +608,7 @@ export class SearchViewModel {
|
|||
}
|
||||
|
||||
this.listModel.getUnfilteredAsArray()
|
||||
await this.listModel.entityEventReceived(instanceId, operation)
|
||||
await this.listModel.entityEventReceived(instanceListId, instanceId, operation)
|
||||
// run the mail or contact update after the update on the list is finished to avoid parallel loading
|
||||
if (operation === OperationType.UPDATE && this.listModel?.isItemSelected(elementIdPart(id))) {
|
||||
try {
|
||||
|
@ -705,12 +705,12 @@ export class SearchViewModel {
|
|||
|
||||
return { items: entries, complete }
|
||||
},
|
||||
loadSingle: async (elementId: Id) => {
|
||||
loadSingle: async (_listId: Id, elementId: Id) => {
|
||||
const lastResult = this._searchResult
|
||||
if (!lastResult) {
|
||||
return null
|
||||
}
|
||||
const id = lastResult.results.find((r) => r[1] === elementId)
|
||||
const id = lastResult.results.find((resultId) => elementIdPart(resultId) === elementId)
|
||||
if (id) {
|
||||
return this.entityClient
|
||||
.load(lastResult.restriction.type, id)
|
||||
|
@ -743,7 +743,7 @@ export class SearchViewModel {
|
|||
if (result && isSameTypeRef(typeRef, result.restriction.type)) {
|
||||
// The list id must be null/empty, otherwise the user is filtering by list, and it shouldn't be ignored
|
||||
|
||||
const ignoreList = isSameTypeRef(typeRef, MailTypeRef) && result.restriction.listIds.length === 0
|
||||
const ignoreList = isSameTypeRef(typeRef, MailTypeRef) && result.restriction.folderIds.length === 0
|
||||
|
||||
return result.results.some((r) => this.compareItemId(r, id, ignoreList))
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import m from "mithril"
|
||||
import { Dialog } from "../../common/gui/base/Dialog"
|
||||
import { lang, TranslationKey } from "../../common/misc/LanguageViewModel"
|
||||
import { InboxRuleType, MailFolderType } from "../../common/api/common/TutanotaConstants"
|
||||
import { InboxRuleType, MailSetKind } from "../../common/api/common/TutanotaConstants"
|
||||
import { isDomainName, isMailAddress, isRegularExpression } from "../../common/misc/FormatValidator"
|
||||
import { getInboxRuleTypeNameMapping } from "../mail/model/InboxRuleHandler"
|
||||
import type { InboxRule } from "../../common/api/entities/tutanota/TypeRefs.js"
|
||||
|
@ -13,7 +13,7 @@ import { TextField } from "../../common/gui/base/TextField.js"
|
|||
import { neverNull } from "@tutao/tutanota-utils"
|
||||
import { LockedError } from "../../common/api/common/error/RestError"
|
||||
import { showNotAvailableForFreeDialog } from "../../common/misc/SubscriptionDialogs"
|
||||
import { isSameId } from "../../common/api/common/utils/EntityUtils"
|
||||
import { elementIdPart, isSameId } from "../../common/api/common/utils/EntityUtils"
|
||||
import { assertMainOrNode } from "../../common/api/common/Env"
|
||||
import { locator } from "../../common/api/main/CommonLocator"
|
||||
import { isOfflineError } from "../../common/api/common/utils/ErrorUtils.js"
|
||||
|
@ -41,8 +41,8 @@ export function show(mailBoxDetail: MailboxDetail, ruleOrTemplate: InboxRuleTemp
|
|||
})
|
||||
const inboxRuleType = stream(ruleOrTemplate.type)
|
||||
const inboxRuleValue = stream(ruleOrTemplate.value)
|
||||
const selectedFolder = ruleOrTemplate.targetFolder == null ? null : mailBoxDetail.folders.getFolderById(ruleOrTemplate.targetFolder)
|
||||
const inboxRuleTarget = stream(selectedFolder ?? assertSystemFolderOfType(mailBoxDetail.folders, MailFolderType.ARCHIVE))
|
||||
const selectedFolder = ruleOrTemplate.targetFolder == null ? null : mailBoxDetail.folders.getFolderById(elementIdPart(ruleOrTemplate.targetFolder))
|
||||
const inboxRuleTarget = stream(selectedFolder ?? assertSystemFolderOfType(mailBoxDetail.folders, MailSetKind.ARCHIVE))
|
||||
|
||||
let form = () => [
|
||||
m(DropDownSelector, {
|
||||
|
|
|
@ -98,7 +98,7 @@ export class KnowledgeBaseListView implements UpdatableSettingsViewer {
|
|||
throw new Error("fetch knowledgeBase entry called for specific start id")
|
||||
}
|
||||
},
|
||||
loadSingle: (elementId) => {
|
||||
loadSingle: (_listId: Id, elementId: Id) => {
|
||||
return this.entityClient.load<KnowledgeBaseEntry>(KnowledgeBaseEntryTypeRef, [this.getListId(), elementId])
|
||||
},
|
||||
autoSelectBehavior: () => ListAutoSelectBehavior.OLDER,
|
||||
|
@ -172,7 +172,7 @@ export class KnowledgeBaseListView implements UpdatableSettingsViewer {
|
|||
async entityEventsReceived(updates: ReadonlyArray<EntityUpdateData>): Promise<any> {
|
||||
for (const update of updates) {
|
||||
if (isUpdateForTypeRef(KnowledgeBaseEntryTypeRef, update) && isSameId(this.getListId(), update.instanceListId)) {
|
||||
await this.listModel.entityEventReceived(update.instanceId, update.operation)
|
||||
await this.listModel.entityEventReceived(update.instanceListId, update.instanceId, update.operation)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ import { EntityUpdateData, isUpdateForTypeRef } from "../../common/api/common/ut
|
|||
import { getDefaultSenderFromUser, getFolderName, getMailAddressDisplayText } from "../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { UpdatableSettingsViewer } from "../../common/settings/Interfaces.js"
|
||||
import { mailLocator } from "../mailLocator.js"
|
||||
import { elementIdPart } from "../../common/api/common/utils/EntityUtils.js"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
|
@ -479,7 +480,7 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
|
|||
}
|
||||
|
||||
_getTextForTarget(mailboxDetail: MailboxDetail, targetFolderId: IdTuple): string {
|
||||
let folder = mailboxDetail.folders.getFolderById(targetFolderId)
|
||||
let folder = mailboxDetail.folders.getFolderById(elementIdPart(targetFolderId))
|
||||
|
||||
if (folder) {
|
||||
return getFolderName(folder)
|
||||
|
|
|
@ -821,6 +821,7 @@ function showRenameTemplateListDialog(instance: TemplateGroupInstance) {
|
|||
group: getEtId(instance.group),
|
||||
color: "",
|
||||
name: newName,
|
||||
defaultAlarmsList: [],
|
||||
})
|
||||
logins.getUserController().userSettingsGroupRoot.groupSettings.push(newSettings)
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ export class TemplateListView implements UpdatableSettingsViewer {
|
|||
throw new Error("fetch template entry called for specific start id")
|
||||
}
|
||||
},
|
||||
loadSingle: (elementId) => {
|
||||
loadSingle: (_listId: Id, elementId: Id) => {
|
||||
return this.entityClient.load<EmailTemplate>(EmailTemplateTypeRef, [this.templateListId(), elementId])
|
||||
},
|
||||
autoSelectBehavior: () => ListAutoSelectBehavior.OLDER,
|
||||
|
@ -170,8 +170,9 @@ export class TemplateListView implements UpdatableSettingsViewer {
|
|||
|
||||
async entityEventsReceived(updates: ReadonlyArray<EntityUpdateData>): Promise<void> {
|
||||
for (const update of updates) {
|
||||
if (isUpdateForTypeRef(EmailTemplateTypeRef, update) && isSameId(this.templateListId(), update.instanceListId)) {
|
||||
await this.listModel.entityEventReceived(update.instanceId, update.operation)
|
||||
const { instanceListId, instanceId, operation } = update
|
||||
if (isUpdateForTypeRef(EmailTemplateTypeRef, update) && isSameId(this.templateListId(), instanceListId)) {
|
||||
await this.listModel.entityEventReceived(instanceListId, instanceId, operation)
|
||||
}
|
||||
}
|
||||
// we need to make another search in case items have changed
|
||||
|
|
|
@ -147,7 +147,7 @@ export class GroupListView implements UpdatableSettingsViewer {
|
|||
const { instanceListId, instanceId, operation } = update
|
||||
|
||||
if (isUpdateForTypeRef(GroupInfoTypeRef, update) && this.listId.getSync() === instanceListId) {
|
||||
await this.listModel.entityEventReceived(instanceId, operation)
|
||||
await this.listModel.entityEventReceived(instanceListId, instanceId, operation)
|
||||
} else if (isUpdateForTypeRef(GroupMemberTypeRef, update)) {
|
||||
this.localAdminGroupMemberships = locator.logins.getUserController().getLocalAdminGroupMemberships()
|
||||
this.listModel.reapplyFilter()
|
||||
|
@ -171,7 +171,7 @@ export class GroupListView implements UpdatableSettingsViewer {
|
|||
throw new Error("fetch user group infos called for specific start id")
|
||||
}
|
||||
},
|
||||
loadSingle: async (elementId) => {
|
||||
loadSingle: async (_listId: Id, elementId: Id) => {
|
||||
const listId = await this.listId.getAsync()
|
||||
try {
|
||||
return await locator.entityClient.load<GroupInfo>(GroupInfoTypeRef, [listId, elementId])
|
||||
|
|
|
@ -250,7 +250,7 @@ o.spec("EventBusClientTest", function () {
|
|||
})
|
||||
|
||||
o("on counter update it send message to the main thread", async function () {
|
||||
const counterUpdate = createCounterData({ mailGroupId: "group1", counterValue: 4, listId: "list1" })
|
||||
const counterUpdate = createCounterData({ mailGroupId: "group1", counterValue: 4, counterId: "list1" })
|
||||
await ebc.connect(ConnectMode.Initial)
|
||||
|
||||
await socket.onmessage?.({
|
||||
|
@ -315,9 +315,9 @@ o.spec("EventBusClientTest", function () {
|
|||
return "entityUpdate;" + JSON.stringify(event)
|
||||
}
|
||||
|
||||
type CounterMessageParams = { mailGroupId: Id; counterValue: number; listId: Id }
|
||||
type CounterMessageParams = { mailGroupId: Id; counterValue: number; counterId: Id }
|
||||
|
||||
function createCounterData({ mailGroupId, counterValue, listId }: CounterMessageParams): WebsocketCounterData {
|
||||
function createCounterData({ mailGroupId, counterValue, counterId }: CounterMessageParams): WebsocketCounterData {
|
||||
return createTestEntity(WebsocketCounterDataTypeRef, {
|
||||
_format: "0",
|
||||
mailGroup: mailGroupId,
|
||||
|
@ -325,7 +325,7 @@ o.spec("EventBusClientTest", function () {
|
|||
createTestEntity(WebsocketCounterValueTypeRef, {
|
||||
_id: "counterupdateid",
|
||||
count: String(counterValue),
|
||||
mailListId: listId,
|
||||
counterId,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
|
|
@ -8,8 +8,10 @@ import { getDayShifted, getFirstOrThrow, getTypeId, lastThrow, mapNullable, prom
|
|||
import { DateProvider } from "../../../../../src/common/api/common/DateProvider.js"
|
||||
import {
|
||||
BodyTypeRef,
|
||||
createMailFolderRef,
|
||||
FileTypeRef,
|
||||
Mail,
|
||||
MailBoxTypeRef,
|
||||
MailDetailsBlob,
|
||||
MailDetailsBlobTypeRef,
|
||||
MailDetailsTypeRef,
|
||||
|
@ -20,7 +22,7 @@ import { OfflineStorageMigrator } from "../../../../../src/common/api/worker/off
|
|||
import { InterWindowEventFacadeSendDispatcher } from "../../../../../src/common/native/common/generatedipc/InterWindowEventFacadeSendDispatcher.js"
|
||||
import * as fs from "node:fs"
|
||||
import { untagSqlObject } from "../../../../../src/common/api/worker/offline/SqlValue.js"
|
||||
import { MailFolderType } from "../../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { MailSetKind } from "../../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { BlobElementEntity, ElementEntity, ListElementEntity, SomeEntity } from "../../../../../src/common/api/common/EntityTypes.js"
|
||||
import { resolveTypeReference } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { Type as TypeId } from "../../../../../src/common/api/common/EntityConstants.js"
|
||||
|
@ -53,7 +55,7 @@ const nativePath = buildOptions.sqliteNativePath
|
|||
const database = "./testdatabase.sqlite"
|
||||
export const offlineDatabaseTestKey = Uint8Array.from([3957386659, 354339016, 3786337319, 3366334248])
|
||||
|
||||
o.spec("OfflineStorage", function () {
|
||||
o.spec("OfflineStorageDb", function () {
|
||||
const now = new Date("2022-01-01 00:00:00 UTC")
|
||||
const timeRangeDays = 10
|
||||
const userId = "userId"
|
||||
|
@ -189,6 +191,29 @@ o.spec("OfflineStorage", function () {
|
|||
const rangeAfter = await storage.getRangeForList(MailTypeRef, listId)
|
||||
o(rangeAfter).equals(null)
|
||||
})
|
||||
|
||||
o("provideMultiple", async function () {
|
||||
const listId = "listId1"
|
||||
const elementId1 = "id1"
|
||||
const elementId2 = "id2"
|
||||
const storableMail1 = createTestEntity(MailTypeRef, { _id: [listId, elementId1] })
|
||||
const storableMail2 = createTestEntity(MailTypeRef, { _id: [listId, elementId2] })
|
||||
|
||||
await storage.init({ userId: elementId1, databaseKey, timeRangeDays, forceNewDatabase: false })
|
||||
|
||||
let mails = await storage.provideMultiple(MailTypeRef, listId, [elementId1])
|
||||
o(mails).deepEquals([])
|
||||
|
||||
await storage.put(storableMail1)
|
||||
|
||||
mails = await storage.provideMultiple(MailTypeRef, listId, [elementId1, elementId2])
|
||||
o(mails).deepEquals([storableMail1])
|
||||
|
||||
await storage.put(storableMail2)
|
||||
|
||||
mails = await storage.provideMultiple(MailTypeRef, listId, [elementId1, elementId2])
|
||||
o(mails).deepEquals([storableMail1, storableMail2])
|
||||
})
|
||||
})
|
||||
|
||||
o.spec("BlobElementType", function () {
|
||||
|
@ -251,10 +276,13 @@ o.spec("OfflineStorage", function () {
|
|||
await storage.init({ userId, databaseKey, timeRangeDays, forceNewDatabase: false })
|
||||
|
||||
await insertEntity(
|
||||
createTestEntity(MailFolderTypeRef, { _id: ["mailFolderList", spamFolderId], mails: spamListId, folderType: MailFolderType.SPAM }),
|
||||
createTestEntity(MailBoxTypeRef, { _id: "mailboxId", currentMailBag: null, folders: createMailFolderRef({ folders: "mailFolderList" }) }),
|
||||
)
|
||||
await insertEntity(
|
||||
createTestEntity(MailFolderTypeRef, { _id: ["mailFolderList", trashFolderId], mails: trashListId, folderType: MailFolderType.TRASH }),
|
||||
createTestEntity(MailFolderTypeRef, { _id: ["mailFolderList", spamFolderId], mails: spamListId, folderType: MailSetKind.SPAM }),
|
||||
)
|
||||
await insertEntity(
|
||||
createTestEntity(MailFolderTypeRef, { _id: ["mailFolderList", trashFolderId], mails: trashListId, folderType: MailSetKind.TRASH }),
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -281,7 +309,13 @@ o.spec("OfflineStorage", function () {
|
|||
o("modified ranges will be shrunk", async function () {
|
||||
const upper = offsetId(2)
|
||||
const lower = offsetId(-2)
|
||||
await insertEntity(createTestEntity(MailFolderTypeRef, { _id: ["mailFolderListId", "mailFolderId"], mails: listId }))
|
||||
await insertEntity(
|
||||
createTestEntity(MailFolderTypeRef, {
|
||||
_id: ["mailFolderList", "mailFolderId"],
|
||||
folderType: MailSetKind.INBOX,
|
||||
mails: listId,
|
||||
}),
|
||||
)
|
||||
await insertRange(MailTypeRef, listId, lower, upper)
|
||||
|
||||
// Here we clear the excluded data
|
||||
|
@ -349,7 +383,7 @@ o.spec("OfflineStorage", function () {
|
|||
_id: ["mailFolderList", trashSubfolderId],
|
||||
parentFolder: ["mailFolderList", trashFolderId],
|
||||
mails: trashSubfolderListId,
|
||||
folderType: MailFolderType.CUSTOM,
|
||||
folderType: MailSetKind.CUSTOM,
|
||||
}),
|
||||
)
|
||||
await insertEntity(spamMail)
|
||||
|
@ -409,7 +443,7 @@ o.spec("OfflineStorage", function () {
|
|||
const afterMailDetails = createTestEntity(MailDetailsBlobTypeRef, { _id: afterMailDetailsId, details: createTestEntity(MailDetailsTypeRef) })
|
||||
|
||||
await insertEntity(
|
||||
createTestEntity(MailFolderTypeRef, { _id: ["mailFolderList", "folderId"], mails: inboxMailList, folderType: MailFolderType.INBOX }),
|
||||
createTestEntity(MailFolderTypeRef, { _id: ["mailFolderList", "folderId"], mails: inboxMailList, folderType: MailSetKind.INBOX }),
|
||||
)
|
||||
await insertEntity(mailBefore)
|
||||
await insertEntity(mailAfter)
|
||||
|
@ -435,7 +469,7 @@ o.spec("OfflineStorage", function () {
|
|||
const mail2 = createTestEntity(MailTypeRef, { _id: [inboxMailList, offsetId(-3)], mailDetails: mailDetailsId2 })
|
||||
|
||||
await insertEntity(
|
||||
createTestEntity(MailFolderTypeRef, { _id: ["mailFolderList", "folderId"], mails: inboxMailList, folderType: MailFolderType.INBOX }),
|
||||
createTestEntity(MailFolderTypeRef, { _id: ["mailFolderList", "folderId"], mails: inboxMailList, folderType: MailSetKind.INBOX }),
|
||||
)
|
||||
await insertEntity(mail1)
|
||||
await insertEntity(mail2)
|
||||
|
@ -469,7 +503,7 @@ o.spec("OfflineStorage", function () {
|
|||
})
|
||||
|
||||
await insertEntity(
|
||||
createTestEntity(MailFolderTypeRef, { _id: ["mailFolderList", "folderId"], mails: inboxMailList, folderType: MailFolderType.INBOX }),
|
||||
createTestEntity(MailFolderTypeRef, { _id: ["mailFolderList", "folderId"], mails: inboxMailList, folderType: MailSetKind.INBOX }),
|
||||
)
|
||||
await insertEntity(mailBefore)
|
||||
await insertEntity(mailAfter)
|
||||
|
@ -519,11 +553,17 @@ o.spec("OfflineStorage", function () {
|
|||
const oldIds = new IdGenerator(offsetId(-5))
|
||||
const newIds = new IdGenerator(offsetId(5))
|
||||
|
||||
const userMailbox = createTestEntity(MailBoxTypeRef, {
|
||||
_id: "mailboxId",
|
||||
currentMailBag: null,
|
||||
folders: createMailFolderRef({ folders: "mailFolderList" }),
|
||||
})
|
||||
|
||||
const inboxListId = oldIds.getNext()
|
||||
const inboxFolder = createTestEntity(MailFolderTypeRef, {
|
||||
_id: [userId, oldIds.getNext()],
|
||||
_id: ["mailFolderList", oldIds.getNext()],
|
||||
mails: inboxListId,
|
||||
folderType: MailFolderType.INBOX,
|
||||
folderType: MailSetKind.INBOX,
|
||||
})
|
||||
const { mails: oldInboxMails, mailDetailsBlobs: oldInboxMailDetailsBlobs } = createMailList(
|
||||
3,
|
||||
|
@ -543,9 +583,9 @@ o.spec("OfflineStorage", function () {
|
|||
|
||||
const trashListId = oldIds.getNext()
|
||||
const trashFolder = createTestEntity(MailFolderTypeRef, {
|
||||
_id: [userId, oldIds.getNext()],
|
||||
_id: ["mailFolderList", oldIds.getNext()],
|
||||
mails: trashListId,
|
||||
folderType: MailFolderType.TRASH,
|
||||
folderType: MailSetKind.TRASH,
|
||||
})
|
||||
const { mails: trashMails, mailDetailsBlobs: trashMailDetailsBlobs } = createMailList(
|
||||
3,
|
||||
|
@ -557,12 +597,13 @@ o.spec("OfflineStorage", function () {
|
|||
|
||||
const spamListId = oldIds.getNext()
|
||||
const spamFolder = createTestEntity(MailFolderTypeRef, {
|
||||
_id: [userId, oldIds.getNext()],
|
||||
_id: ["mailFolderList", oldIds.getNext()],
|
||||
mails: spamListId,
|
||||
folderType: MailFolderType.SPAM,
|
||||
folderType: MailSetKind.SPAM,
|
||||
})
|
||||
|
||||
const everyEntity = [
|
||||
userMailbox,
|
||||
inboxFolder,
|
||||
trashFolder,
|
||||
spamFolder,
|
||||
|
|
|
@ -388,7 +388,6 @@ o.spec("MailIndexer test", () => {
|
|||
const indexer = mock(new MailIndexer(null as any, db, null as any, null as any, null as any, dateProvider, mailFacade), (mocked) => {
|
||||
mocked.indexMailboxes = spy(() => Promise.resolve())
|
||||
mocked.mailIndexingEnabled = false
|
||||
mocked._excludedListIds = []
|
||||
|
||||
mocked._getSpamFolder = (membership) => {
|
||||
o(membership).deepEquals(user.memberships[0])
|
||||
|
@ -398,11 +397,10 @@ o.spec("MailIndexer test", () => {
|
|||
await indexer.enableMailIndexing(user)
|
||||
o(indexer.indexMailboxes.invocations[0]).deepEquals([user, beforeNowInterval])
|
||||
o(indexer.mailIndexingEnabled).equals(true)
|
||||
o(indexer._excludedListIds).deepEquals([spamFolder.mails])
|
||||
o(JSON.stringify(metadata)).equals(
|
||||
JSON.stringify({
|
||||
[MetaData.mailIndexingEnabled]: true,
|
||||
[MetaData.excludedListIds]: [spamFolder.mails],
|
||||
[MetaData.excludedListIds]: [],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
@ -414,7 +412,7 @@ o.spec("MailIndexer test", () => {
|
|||
if (key == MetaData.mailIndexingEnabled) {
|
||||
return Promise.resolve(true)
|
||||
} else if (key == MetaData.excludedListIds) {
|
||||
return Promise.resolve([1, 2])
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
throw new Error("wrong key / os")
|
||||
|
@ -429,12 +427,10 @@ o.spec("MailIndexer test", () => {
|
|||
const indexer: any = new MailIndexer(null as any, db, null as any, null as any, null as any, dateProvider, mailFacade)
|
||||
indexer.indexMailboxes = spy()
|
||||
indexer.mailIndexingEnabled = false
|
||||
indexer._excludedListIds = []
|
||||
let user = createTestEntity(UserTypeRef)
|
||||
await await indexer.enableMailIndexing(user)
|
||||
o(indexer.indexMailboxes.callCount).equals(0)
|
||||
o(indexer.mailIndexingEnabled).equals(true)
|
||||
o(indexer._excludedListIds).deepEquals([1, 2])
|
||||
})
|
||||
o("disableMailIndexing", function () {
|
||||
let db: Db = {
|
||||
|
@ -445,10 +441,8 @@ o.spec("MailIndexer test", () => {
|
|||
} as any
|
||||
const indexer: any = new MailIndexer(null as any, db, null as any, null as any, null as any, dateProvider, mailFacade)
|
||||
indexer.mailIndexingEnabled = true
|
||||
indexer._excludedListIds = [1]
|
||||
indexer.disableMailIndexing()
|
||||
o(indexer.mailIndexingEnabled).equals(false)
|
||||
o(indexer._excludedListIds).deepEquals([])
|
||||
// @ts-ignore
|
||||
o(db.dbFacade.deleteDatabase.callCount).equals(1)
|
||||
})
|
||||
|
|
|
@ -20,13 +20,15 @@ import {
|
|||
timestampToGeneratedId,
|
||||
} from "../../../../../src/common/api/common/utils/EntityUtils.js"
|
||||
import type { Base64 } from "@tutao/tutanota-utils"
|
||||
import { downcast, groupBy, numberRange, splitInChunks } from "@tutao/tutanota-utils"
|
||||
import { groupBy, numberRange, splitInChunks } from "@tutao/tutanota-utils"
|
||||
import { appendBinaryBlocks } from "../../../../../src/common/api/worker/search/SearchIndexEncoding.js"
|
||||
import { createSearchIndexDbStub, DbStub, DbStubTransaction } from "./DbStub.js"
|
||||
import type { BrowserData } from "../../../../../src/common/misc/ClientConstants.js"
|
||||
import { browserDataStub, createTestEntity } from "../../../TestUtils.js"
|
||||
import { aes256RandomKey, fixedIv } from "@tutao/tutanota-crypto"
|
||||
import { ElementDataOS, SearchIndexMetaDataOS, SearchIndexOS } from "../../../../../src/common/api/worker/search/IndexTables.js"
|
||||
import { object, when } from "testdouble"
|
||||
import { EntityClient } from "../../../../../src/common/api/common/EntityClient.js"
|
||||
|
||||
type SearchIndexEntryWithType = SearchIndexEntry & {
|
||||
typeInfo: TypeInfo
|
||||
|
@ -39,8 +41,9 @@ let dbKey
|
|||
const contactTypeInfo = typeRefToTypeInfo(ContactTypeRef)
|
||||
const mailTypeInfo = typeRefToTypeInfo(MailTypeRef)
|
||||
const browserData: BrowserData = browserDataStub
|
||||
const entityClinet = downcast({})
|
||||
const entityClient: EntityClient = object()
|
||||
o.spec("SearchFacade test", () => {
|
||||
let mail = createTestEntity(MailTypeRef)
|
||||
let user = createTestEntity(UserTypeRef)
|
||||
let id1 = "L0YED5d----1"
|
||||
let id2 = "L0YED5d----2"
|
||||
|
@ -65,7 +68,7 @@ o.spec("SearchFacade test", () => {
|
|||
} as any,
|
||||
[],
|
||||
browserData,
|
||||
entityClinet,
|
||||
entityClient,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -138,7 +141,7 @@ o.spec("SearchFacade test", () => {
|
|||
end: end ?? null,
|
||||
field: null,
|
||||
attributeIds: attributeIds ?? null,
|
||||
listIds: listId != null ? [listId] : [],
|
||||
folderIds: listId != null ? [listId] : [],
|
||||
eventSeries: true,
|
||||
}
|
||||
}
|
||||
|
@ -239,16 +242,38 @@ o.spec("SearchFacade test", () => {
|
|||
[["listId2", id2]],
|
||||
)
|
||||
})
|
||||
o("find listId", () => {
|
||||
o("find folderId legacy MailFolders (non-static mail listIds)", () => {
|
||||
let mail1 = createTestEntity(MailTypeRef, { _id: ["mailListId1", id1] })
|
||||
when(entityClient.load(MailTypeRef, mail1._id)).thenReturn(Promise.resolve(mail1))
|
||||
let mail2 = createTestEntity(MailTypeRef, { _id: ["mailListId2", id2] })
|
||||
when(entityClient.load(MailTypeRef, mail2._id)).thenReturn(Promise.resolve(mail2))
|
||||
|
||||
return testSearch(
|
||||
[createKeyToIndexEntries("test", [createMailEntry(id1, 0, [0]), createMailEntry(id2, 0, [0])])],
|
||||
[
|
||||
["listId1", id1],
|
||||
["listId2", id2],
|
||||
],
|
||||
[mail1._id, mail2._id],
|
||||
"test",
|
||||
createMailRestriction(null, "listId2"),
|
||||
[["listId2", id2]],
|
||||
createMailRestriction(null, listIdPart(mail2._id)),
|
||||
[mail2._id],
|
||||
)
|
||||
})
|
||||
o("find folderId new MailSets (static mail listIds)", () => {
|
||||
const mail1 = createTestEntity(MailTypeRef, {
|
||||
_id: ["mailListId", id1],
|
||||
sets: [["setListId", "folderId1"]],
|
||||
})
|
||||
when(entityClient.load(MailTypeRef, mail1._id)).thenReturn(Promise.resolve(mail1))
|
||||
const mail2 = createTestEntity(MailTypeRef, {
|
||||
_id: ["mailListId", id2],
|
||||
sets: [["setListId", "folderId2"]],
|
||||
})
|
||||
when(entityClient.load(MailTypeRef, mail2._id)).thenReturn(Promise.resolve(mail2))
|
||||
|
||||
return testSearch(
|
||||
[createKeyToIndexEntries("test", [createMailEntry(id1, 0, [0]), createMailEntry(id2, 0, [0])])],
|
||||
[mail1._id, mail2._id],
|
||||
"test",
|
||||
createMailRestriction(null, elementIdPart(mail2.sets[0])),
|
||||
[mail2._id],
|
||||
)
|
||||
})
|
||||
o("find with start time", () => {
|
||||
|
|
|
@ -2,7 +2,7 @@ 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 { MailFolderType, OperationType } from "../../../src/common/api/common/TutanotaConstants.js"
|
||||
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"
|
||||
import { EntityRestClientMock } from "../api/worker/rest/EntityRestClientMock.js"
|
||||
|
@ -18,30 +18,31 @@ import { createTestEntity } from "../TestUtils.js"
|
|||
import { EntityUpdateData } from "../../../src/common/api/common/utils/EntityUpdateUtils.js"
|
||||
import { MailboxDetail, MailModel } from "../../../src/common/mailFunctionality/MailModel.js"
|
||||
import { InboxRuleHandler } from "../../../src/mail-app/mail/model/InboxRuleHandler.js"
|
||||
import { getElementId, getListId } from "../../../src/common/api/common/utils/EntityUtils.js"
|
||||
|
||||
o.spec("MailModelTest", function () {
|
||||
let notifications: Partial<Notifications>
|
||||
let showSpy: Spy
|
||||
let model: MailModel
|
||||
const inboxFolder = createTestEntity(MailFolderTypeRef, { _id: ["folderListId", "inboxId"] })
|
||||
const inboxFolder = createTestEntity(MailFolderTypeRef, { _id: ["folderListId", "inboxId"], isMailSet: false })
|
||||
inboxFolder.mails = "instanceListId"
|
||||
inboxFolder.folderType = MailFolderType.INBOX
|
||||
const anotherFolder = createTestEntity(MailFolderTypeRef, { _id: ["folderListId", "archiveId"] })
|
||||
inboxFolder.folderType = MailSetKind.INBOX
|
||||
const anotherFolder = createTestEntity(MailFolderTypeRef, { _id: ["folderListId", "archiveId"], isMailSet: false })
|
||||
anotherFolder.mails = "anotherListId"
|
||||
anotherFolder.folderType = MailFolderType.ARCHIVE
|
||||
anotherFolder.folderType = MailSetKind.ARCHIVE
|
||||
let mailboxDetails: Partial<MailboxDetail>[]
|
||||
let logins: LoginController
|
||||
let inboxRuleHandler: InboxRuleHandler
|
||||
const restClient: EntityRestClientMock = new EntityRestClientMock()
|
||||
|
||||
o.beforeEach(function () {
|
||||
mailboxDetails = [
|
||||
{
|
||||
folders: new FolderSystem([inboxFolder]),
|
||||
folders: new FolderSystem([inboxFolder, anotherFolder]),
|
||||
},
|
||||
]
|
||||
notifications = {}
|
||||
showSpy = notifications.showNotification = spy()
|
||||
const restClient = new EntityRestClientMock()
|
||||
const connectivityModel = object<WebsocketConnectivityModel>()
|
||||
const mailFacade = nodemocker.mock<MailFacade>("mailFacade", {}).set()
|
||||
logins = object()
|
||||
|
@ -55,10 +56,13 @@ o.spec("MailModelTest", function () {
|
|||
model.mailboxDetails(mailboxDetails as MailboxDetail[])
|
||||
})
|
||||
o("doesn't send notification for another folder", async function () {
|
||||
const mail = createTestEntity(MailTypeRef, { _id: [anotherFolder.mails, "mailId"], sets: [] })
|
||||
restClient.addListInstances(mail)
|
||||
await model.entityEventsReceived(
|
||||
[
|
||||
makeUpdate({
|
||||
instanceListId: anotherFolder.mails,
|
||||
instanceListId: getListId(mail),
|
||||
instanceId: getElementId(mail),
|
||||
operation: OperationType.CREATE,
|
||||
}),
|
||||
],
|
||||
|
@ -67,14 +71,18 @@ o.spec("MailModelTest", function () {
|
|||
o(showSpy.invocations.length).equals(0)
|
||||
})
|
||||
o("doesn't send notification for move operation", async function () {
|
||||
const mail = createTestEntity(MailTypeRef, { _id: [inboxFolder.mails, "mailId"], sets: [] })
|
||||
restClient.addListInstances(mail)
|
||||
await model.entityEventsReceived(
|
||||
[
|
||||
makeUpdate({
|
||||
instanceListId: anotherFolder.mails,
|
||||
instanceListId: getListId(mail),
|
||||
instanceId: getElementId(mail),
|
||||
operation: OperationType.DELETE,
|
||||
}),
|
||||
makeUpdate({
|
||||
instanceListId: inboxFolder.mails,
|
||||
instanceListId: getListId(mail),
|
||||
instanceId: getElementId(mail),
|
||||
operation: OperationType.CREATE,
|
||||
}),
|
||||
],
|
||||
|
@ -83,7 +91,7 @@ o.spec("MailModelTest", function () {
|
|||
o(showSpy.invocations.length).equals(0)
|
||||
})
|
||||
|
||||
function makeUpdate(arg: { instanceListId: string; operation: OperationType }): EntityUpdateData {
|
||||
function makeUpdate(arg: { instanceListId: string; instanceId: Id; operation: OperationType }): EntityUpdateData {
|
||||
return Object.assign(
|
||||
{},
|
||||
{
|
||||
|
|
|
@ -1,38 +1,41 @@
|
|||
import o from "@tutao/otest"
|
||||
import { createMailFolder, MailFolderTypeRef } from "../../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { MailFolderType } from "../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { MailFolderTypeRef, MailTypeRef } from "../../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { MailSetKind } from "../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { FolderSystem } from "../../../../src/common/api/common/mail/FolderSystem.js"
|
||||
import { createTestEntity } from "../../TestUtils.js"
|
||||
import { getElementId } from "../../../../src/common/api/common/utils/EntityUtils.js"
|
||||
|
||||
o.spec("FolderSystem", function () {
|
||||
const listId = "listId"
|
||||
const inbox = createTestEntity(MailFolderTypeRef, { _id: [listId, "inbox"], folderType: MailFolderType.INBOX })
|
||||
const archive = createTestEntity(MailFolderTypeRef, { _id: [listId, "archive"], folderType: MailFolderType.ARCHIVE })
|
||||
const inbox = createTestEntity(MailFolderTypeRef, { _id: [listId, "inbox"], folderType: MailSetKind.INBOX })
|
||||
const archive = createTestEntity(MailFolderTypeRef, { _id: [listId, "archive"], folderType: MailSetKind.ARCHIVE })
|
||||
const customFolder = createTestEntity(MailFolderTypeRef, {
|
||||
_id: [listId, "custom"],
|
||||
folderType: MailFolderType.CUSTOM,
|
||||
folderType: MailSetKind.CUSTOM,
|
||||
name: "X",
|
||||
})
|
||||
const customSubfolder = createTestEntity(MailFolderTypeRef, {
|
||||
_id: [listId, "customSub"],
|
||||
folderType: MailFolderType.CUSTOM,
|
||||
folderType: MailSetKind.CUSTOM,
|
||||
parentFolder: customFolder._id,
|
||||
name: "AA",
|
||||
mails: "customSubMailList",
|
||||
})
|
||||
const customSubSubfolder = createTestEntity(MailFolderTypeRef, {
|
||||
_id: [listId, "customSubSub"],
|
||||
folderType: MailFolderType.CUSTOM,
|
||||
folderType: MailSetKind.CUSTOM,
|
||||
parentFolder: customSubfolder._id,
|
||||
name: "B",
|
||||
})
|
||||
const customSubSubfolderAnother = createTestEntity(MailFolderTypeRef, {
|
||||
_id: [listId, "customSubSubAnother"],
|
||||
folderType: MailFolderType.CUSTOM,
|
||||
folderType: MailSetKind.CUSTOM,
|
||||
parentFolder: customSubfolder._id,
|
||||
name: "A",
|
||||
})
|
||||
|
||||
const mail = createTestEntity(MailTypeRef, { _id: ["mailListId", "inbox"], sets: [customSubfolder._id] })
|
||||
|
||||
const allFolders = [archive, inbox, customFolder, customSubfolder, customSubSubfolder, customSubSubfolderAnother]
|
||||
|
||||
o("correctly builds the subtrees", function () {
|
||||
|
@ -74,12 +77,12 @@ o.spec("FolderSystem", function () {
|
|||
o("indented list sorts stepsiblings correctly", function () {
|
||||
const customFolderAnother = createTestEntity(MailFolderTypeRef, {
|
||||
_id: [listId, "customAnother"],
|
||||
folderType: MailFolderType.CUSTOM,
|
||||
folderType: MailSetKind.CUSTOM,
|
||||
name: "Another top-level custom",
|
||||
})
|
||||
const customFolderAnotherSub = createTestEntity(MailFolderTypeRef, {
|
||||
_id: [listId, "customAnotherSub"],
|
||||
folderType: MailFolderType.CUSTOM,
|
||||
folderType: MailSetKind.CUSTOM,
|
||||
parentFolder: customFolderAnother._id,
|
||||
name: "Y",
|
||||
})
|
||||
|
@ -110,25 +113,24 @@ o.spec("FolderSystem", function () {
|
|||
o("getSystemFolderByType", function () {
|
||||
const system = new FolderSystem(allFolders)
|
||||
|
||||
o(system.getSystemFolderByType(MailFolderType.ARCHIVE)).deepEquals(archive)
|
||||
o(system.getSystemFolderByType(MailSetKind.ARCHIVE)).deepEquals(archive)
|
||||
})
|
||||
|
||||
o("getFolderById", function () {
|
||||
const system = new FolderSystem(allFolders)
|
||||
|
||||
o(system.getFolderById(archive._id)).deepEquals(archive)
|
||||
o(system.getFolderById(getElementId(archive))).deepEquals(archive)
|
||||
})
|
||||
|
||||
o("getFolderById not there returns null", function () {
|
||||
const system = new FolderSystem(allFolders)
|
||||
|
||||
o(system.getFolderById([listId, "randomId"])).equals(null)
|
||||
o(system.getFolderById("randomId")).equals(null)
|
||||
})
|
||||
|
||||
o("getFolderByMailListId", function () {
|
||||
o("getFolderByMail", function () {
|
||||
const system = new FolderSystem(allFolders)
|
||||
|
||||
o(system.getFolderByMailListId(customSubfolder.mails)).equals(customSubfolder)
|
||||
o(system.getFolderByMail(mail)).equals(customSubfolder)
|
||||
})
|
||||
|
||||
o("getCustomFoldersOfParent", function () {
|
||||
|
|
|
@ -16,7 +16,7 @@ import { EntityRestClientMock } from "../../api/worker/rest/EntityRestClientMock
|
|||
import { EntityEventsListener, EventController } from "../../../../src/common/api/main/EventController.js"
|
||||
import { defer, DeferredObject, delay, noOp } from "@tutao/tutanota-utils"
|
||||
import { matchers, object, when } from "testdouble"
|
||||
import { MailFolderType, MailState, OperationType } from "../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { MailSetKind, MailState, OperationType } from "../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { isSameId } from "../../../../src/common/api/common/utils/EntityUtils.js"
|
||||
import { createTestEntity } from "../../TestUtils.js"
|
||||
import { MailboxDetail, MailModel } from "../../../../src/common/mailFunctionality/MailModel.js"
|
||||
|
@ -173,11 +173,11 @@ o.spec("ConversationViewModel", function () {
|
|||
const trashDraftMail = addMail("trashDraftMail")
|
||||
trashDraftMail.state = MailState.DRAFT
|
||||
|
||||
const trash = createTestEntity(MailFolderTypeRef, { _id: [listId, "trashFolder"], folderType: MailFolderType.TRASH })
|
||||
const trash = createTestEntity(MailFolderTypeRef, { _id: [listId, "trashFolder"], folderType: MailSetKind.TRASH })
|
||||
entityRestClientMock.addListInstances(trash)
|
||||
|
||||
when(mailModel.getMailboxDetailsForMail(matchers.anything())).thenResolve(mailboxDetail)
|
||||
when(mailModel.getMailFolder(listId)).thenReturn(trash)
|
||||
when(mailModel.getMailFolderForMail(trashDraftMail)).thenReturn(trash)
|
||||
|
||||
conversation.pop() // since this mail shouldn't actually be a part of the conversation
|
||||
|
||||
|
@ -195,11 +195,11 @@ o.spec("ConversationViewModel", function () {
|
|||
const trashDraftMail = addMail("trashDraftMail")
|
||||
trashDraftMail.state = MailState.DRAFT
|
||||
|
||||
const trash = createTestEntity(MailFolderTypeRef, { _id: [listId, "trashFolder"], folderType: MailFolderType.TRASH })
|
||||
const trash = createTestEntity(MailFolderTypeRef, { _id: [listId, "trashFolder"], folderType: MailSetKind.TRASH })
|
||||
entityRestClientMock.addListInstances(trash)
|
||||
|
||||
when(mailModel.getMailboxDetailsForMail(trashDraftMail)).thenResolve(mailboxDetail)
|
||||
when(mailModel.getMailFolder(listId)).thenReturn(trash)
|
||||
when(mailModel.getMailFolderForMail(trashDraftMail)).thenReturn(trash)
|
||||
|
||||
await makeViewModel(trashDraftMail)
|
||||
|
||||
|
@ -311,7 +311,7 @@ o.spec("ConversationViewModel", function () {
|
|||
await loadingDefer.promise
|
||||
|
||||
conversation.pop()
|
||||
const trash = createTestEntity(MailFolderTypeRef, { _id: ["newListId", "trashFolder"], folderType: MailFolderType.TRASH })
|
||||
const trash = createTestEntity(MailFolderTypeRef, { _id: ["folderListId", "trashFolder"], folderType: MailSetKind.TRASH })
|
||||
entityRestClientMock.addListInstances(trash)
|
||||
// adding new mail (is the same mail, just moved to trash)
|
||||
const newTrashDraftMail = addMail("trashDraftMail")
|
||||
|
@ -320,7 +320,7 @@ o.spec("ConversationViewModel", function () {
|
|||
conversation.pop()
|
||||
|
||||
when(mailModel.getMailboxDetailsForMail(matchers.anything())).thenResolve(mailboxDetail)
|
||||
when(mailModel.getMailFolder("newListId")).thenReturn(trash)
|
||||
when(mailModel.getMailFolderForMail(newTrashDraftMail)).thenReturn(trash)
|
||||
|
||||
await eventCallback(
|
||||
[
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import o from "@tutao/otest"
|
||||
import { ListModel, ListModelConfig } from "../../../src/common/misc/ListModel.js"
|
||||
import { GENERATED_MAX_ID, getElementId, sortCompareById, timestampToGeneratedId } from "../../../src/common/api/common/utils/EntityUtils.js"
|
||||
import { GENERATED_MAX_ID, getElementId, getListId, sortCompareById, timestampToGeneratedId } from "../../../src/common/api/common/utils/EntityUtils.js"
|
||||
import { defer, DeferredObject } from "@tutao/tutanota-utils"
|
||||
import { KnowledgeBaseEntry, KnowledgeBaseEntryTypeRef } from "../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { ListFetchResult } from "../../../src/common/gui/base/ListUtils.js"
|
||||
|
@ -413,7 +413,7 @@ o.spec("ListModel", function () {
|
|||
o("when the active item is deleted selectPrevious single will still select previous item relative to it", async function () {
|
||||
await setItems(items)
|
||||
listModel.onSingleInclusiveSelection(itemB)
|
||||
await listModel.entityEventReceived(getElementId(itemB), OperationType.DELETE)
|
||||
await listModel.entityEventReceived(getListId(itemB), getElementId(itemB), OperationType.DELETE)
|
||||
// start state:
|
||||
//
|
||||
// A
|
||||
|
@ -435,7 +435,7 @@ o.spec("ListModel", function () {
|
|||
o("when the active item is deleted selectPrevious multiselect will still select previous item relative to it", async function () {
|
||||
await setItems(items)
|
||||
listModel.onSingleInclusiveSelection(itemB)
|
||||
await listModel.entityEventReceived(getElementId(itemB), OperationType.DELETE)
|
||||
await listModel.entityEventReceived(getListId(itemB), getElementId(itemB), OperationType.DELETE)
|
||||
// start state:
|
||||
//
|
||||
// A
|
||||
|
@ -457,7 +457,7 @@ o.spec("ListModel", function () {
|
|||
o("when the active item is deleted selectNext single will still select next item relative to it", async function () {
|
||||
await setItems(items)
|
||||
listModel.onSingleInclusiveSelection(itemB)
|
||||
await listModel.entityEventReceived(getElementId(itemB), OperationType.DELETE)
|
||||
await listModel.entityEventReceived(getListId(itemB), getElementId(itemB), OperationType.DELETE)
|
||||
// start state:
|
||||
//
|
||||
// A
|
||||
|
@ -479,7 +479,7 @@ o.spec("ListModel", function () {
|
|||
o("when the active item is deleted selectNext multiselect will still select next item relative to it", async function () {
|
||||
await setItems(items)
|
||||
listModel.onSingleInclusiveSelection(itemB)
|
||||
await listModel.entityEventReceived(getElementId(itemB), OperationType.DELETE)
|
||||
await listModel.entityEventReceived(getListId(itemB), getElementId(itemB), OperationType.DELETE)
|
||||
// start state:
|
||||
//
|
||||
// A
|
||||
|
@ -714,7 +714,7 @@ o.spec("ListModel", function () {
|
|||
o("in single select, the active element is next entity when active element gets deleted", async function () {
|
||||
await setItems(items)
|
||||
listModel.onSingleSelection(itemB)
|
||||
await listModel.entityEventReceived(getElementId(itemB), OperationType.DELETE)
|
||||
await listModel.entityEventReceived(getListId(itemB), getElementId(itemB), OperationType.DELETE)
|
||||
|
||||
o(listModel.state.activeIndex).equals(1)
|
||||
})
|
||||
|
@ -722,7 +722,7 @@ o.spec("ListModel", function () {
|
|||
o("in single select, the active element is not changed when a different entity is deleted", async function () {
|
||||
await setItems(items)
|
||||
listModel.onSingleSelection(itemC)
|
||||
await listModel.entityEventReceived(getElementId(itemA), OperationType.DELETE)
|
||||
await listModel.entityEventReceived(getListId(itemA), getElementId(itemA), OperationType.DELETE)
|
||||
|
||||
o(listModel.state.activeIndex).equals(1)
|
||||
})
|
||||
|
@ -730,7 +730,7 @@ o.spec("ListModel", function () {
|
|||
o("in multiselect, next element is not selected when element is removed", async function () {
|
||||
await setItems(items)
|
||||
listModel.onSingleInclusiveSelection(itemB)
|
||||
await listModel.entityEventReceived(getElementId(itemB), OperationType.DELETE)
|
||||
await listModel.entityEventReceived(getListId(itemB), getElementId(itemB), OperationType.DELETE)
|
||||
|
||||
o(listModel.state.inMultiselect).equals(true)
|
||||
o(listModel.state.activeIndex).equals(null)
|
||||
|
@ -743,7 +743,7 @@ o.spec("ListModel", function () {
|
|||
|
||||
const newConfig: ListModelConfig<KnowledgeBaseEntry> = {
|
||||
...defaultListConfig,
|
||||
async loadSingle(elementId: Id): Promise<KnowledgeBaseEntry | null> {
|
||||
async loadSingle(_listId: Id, elementId: Id): Promise<KnowledgeBaseEntry | null> {
|
||||
if (elementId === getElementId(itemD)) {
|
||||
return updatedItemD
|
||||
} else {
|
||||
|
@ -755,7 +755,7 @@ o.spec("ListModel", function () {
|
|||
listModel = new ListModel<KnowledgeBaseEntry>(newConfig)
|
||||
await setItems(items)
|
||||
|
||||
await listModel.entityEventReceived(getElementId(itemD), OperationType.UPDATE)
|
||||
await listModel.entityEventReceived(getListId(itemD), getElementId(itemD), OperationType.UPDATE)
|
||||
|
||||
o(listModel.state.items).deepEquals([itemA, itemB, itemC, updatedItemD])
|
||||
})
|
||||
|
@ -765,7 +765,7 @@ o.spec("ListModel", function () {
|
|||
|
||||
const newConfig: ListModelConfig<KnowledgeBaseEntry> = {
|
||||
...defaultListConfig,
|
||||
async loadSingle(elementId: Id): Promise<KnowledgeBaseEntry | null> {
|
||||
async loadSingle(_listId: Id, elementId: Id): Promise<KnowledgeBaseEntry | null> {
|
||||
if (elementId === getElementId(itemD)) {
|
||||
return updatedItemD
|
||||
} else {
|
||||
|
@ -780,7 +780,7 @@ o.spec("ListModel", function () {
|
|||
listModel = new ListModel<KnowledgeBaseEntry>(newConfig)
|
||||
await setItems(items)
|
||||
|
||||
await listModel.entityEventReceived(getElementId(itemD), OperationType.UPDATE)
|
||||
await listModel.entityEventReceived(getListId(itemD), getElementId(itemD), OperationType.UPDATE)
|
||||
|
||||
o(listModel.state.items).deepEquals([itemA, updatedItemD, itemB, itemC])
|
||||
})
|
||||
|
|
|
@ -410,6 +410,7 @@ mod tests {
|
|||
),
|
||||
],
|
||||
),
|
||||
"sets"=> JsonElement::Array(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4294,7 +4294,7 @@ impl Entity for WebsocketCounterData {
|
|||
pub struct WebsocketCounterValue {
|
||||
pub _id: CustomId,
|
||||
pub count: i64,
|
||||
pub mailListId: GeneratedId,
|
||||
pub counterId: GeneratedId,
|
||||
}
|
||||
|
||||
impl Entity for WebsocketCounterValue {
|
||||
|
|
|
@ -693,6 +693,22 @@ impl Entity for CustomerAccountCreateData {
|
|||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct DefaultAlarmInfo {
|
||||
pub _id: CustomId,
|
||||
pub trigger: String,
|
||||
}
|
||||
|
||||
impl Entity for DefaultAlarmInfo {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "tutanota",
|
||||
type_: "DefaultAlarmInfo",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct DeleteGroupData {
|
||||
pub _format: i64,
|
||||
|
@ -1144,6 +1160,7 @@ pub struct GroupSettings {
|
|||
pub _id: CustomId,
|
||||
pub color: String,
|
||||
pub name: Option<String>,
|
||||
pub defaultAlarmsList: Vec<DefaultAlarmInfo>,
|
||||
pub group: GeneratedId,
|
||||
}
|
||||
|
||||
|
@ -1395,6 +1412,7 @@ pub struct Mail {
|
|||
pub mailDetails: Option<IdTuple>,
|
||||
pub mailDetailsDraft: Option<IdTuple>,
|
||||
pub sender: MailAddress,
|
||||
pub sets: Vec<IdTuple>,
|
||||
pub errors: Option<Errors>,
|
||||
}
|
||||
|
||||
|
@ -1443,6 +1461,22 @@ impl Entity for MailAddressProperties {
|
|||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct MailBag {
|
||||
pub _id: CustomId,
|
||||
pub mails: GeneratedId,
|
||||
}
|
||||
|
||||
impl Entity for MailBag {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "tutanota",
|
||||
type_: "MailBag",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct MailBox {
|
||||
pub _format: i64,
|
||||
|
@ -1453,6 +1487,8 @@ pub struct MailBox {
|
|||
pub _ownerKeyVersion: Option<i64>,
|
||||
pub _permissions: GeneratedId,
|
||||
pub lastInfoDate: DateTime,
|
||||
pub archivedMailBags: Vec<MailBag>,
|
||||
pub currentMailBag: Option<MailBag>,
|
||||
pub folders: Option<MailFolderRef>,
|
||||
pub mailDetailsDrafts: Option<MailDetailsDraftsRef>,
|
||||
pub receivedAttachments: GeneratedId,
|
||||
|
@ -1564,7 +1600,10 @@ pub struct MailFolder {
|
|||
pub _ownerKeyVersion: Option<i64>,
|
||||
pub _permissions: GeneratedId,
|
||||
pub folderType: i64,
|
||||
pub isLabel: bool,
|
||||
pub isMailSet: bool,
|
||||
pub name: String,
|
||||
pub entries: GeneratedId,
|
||||
pub mails: GeneratedId,
|
||||
pub parentFolder: Option<IdTuple>,
|
||||
pub errors: Option<Errors>,
|
||||
|
@ -1596,6 +1635,25 @@ impl Entity for MailFolderRef {
|
|||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct MailSetEntry {
|
||||
pub _format: i64,
|
||||
pub _id: IdTuple,
|
||||
pub _ownerGroup: Option<GeneratedId>,
|
||||
pub _permissions: GeneratedId,
|
||||
pub mail: IdTuple,
|
||||
}
|
||||
|
||||
impl Entity for MailSetEntry {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "tutanota",
|
||||
type_: "MailSetEntry",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct MailboxGroupRoot {
|
||||
pub _format: i64,
|
||||
|
@ -1668,6 +1726,7 @@ impl Entity for MailboxServerProperties {
|
|||
pub struct MoveMailData {
|
||||
pub _format: i64,
|
||||
pub mails: Vec<IdTuple>,
|
||||
pub sourceFolder: Option<IdTuple>,
|
||||
pub targetFolder: IdTuple,
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -42,5 +42,6 @@
|
|||
"name": "Aa4w50xGAXcmNXIN55Y7Cr/m9n5AXDNHmIQ9Cph7mffUCzqnQtYfrs8hAa53J5iyyliZ1nSwdCDs4hCXc2KwzXc=",
|
||||
"contact": null
|
||||
},
|
||||
"toRecipients": []
|
||||
"toRecipients": [],
|
||||
"sets": []
|
||||
}
|
||||
|
|
|
@ -35,5 +35,6 @@
|
|||
"address": "bed-free@tutanota.de",
|
||||
"name": "Aa9WzeZU8eovYX3G6i+5pAk5odHi3cSFvkzQmdQjeyJIBHzw3rf1xkpWCt0TTpZkhA1bw5aqxaJe/EXSHX5PUHg=",
|
||||
"contact": null
|
||||
}
|
||||
},
|
||||
"sets": []
|
||||
}
|
||||
|
|
|
@ -42,5 +42,6 @@
|
|||
"name": "AbmMklsiI2yKGMdbpQBc0eX8dPc1hGNeL7NNa5Wdypurp60v0uP+/Do7fBQnalJX/4K09/znZKIUcbapkifHqJc=",
|
||||
"contact": null
|
||||
},
|
||||
"toRecipients": []
|
||||
"toRecipients": [],
|
||||
"sets": []
|
||||
}
|
||||
|
|
44
tuta-sdk/rust/src/entities/accounting.rs
Normal file
44
tuta-sdk/rust/src/entities/accounting.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
#![allow(non_snake_case, unused_imports)]
|
||||
use super::*;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct CustomerAccountPosting {
|
||||
pub _id: CustomId,
|
||||
pub amount: i64,
|
||||
pub invoiceNumber: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
pub r#type: i64,
|
||||
pub valueDate: DateTime,
|
||||
}
|
||||
|
||||
impl Entity for CustomerAccountPosting {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "accounting",
|
||||
type_: "CustomerAccountPosting",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct CustomerAccountReturn {
|
||||
pub _format: i64,
|
||||
pub _ownerGroup: Option<GeneratedId>,
|
||||
#[serde(with = "serde_bytes")]
|
||||
pub _ownerPublicEncSessionKey: Option<Vec<u8>>,
|
||||
pub _publicCryptoProtocolVersion: Option<i64>,
|
||||
pub balance: i64,
|
||||
pub outstandingBookingsPrice: i64,
|
||||
pub postings: Vec<CustomerAccountPosting>,
|
||||
}
|
||||
|
||||
impl Entity for CustomerAccountReturn {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "accounting",
|
||||
type_: "CustomerAccountReturn",
|
||||
}
|
||||
}
|
||||
}
|
19
tuta-sdk/rust/src/entities/base.rs
Normal file
19
tuta-sdk/rust/src/entities/base.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
#![allow(non_snake_case, unused_imports)]
|
||||
use super::*;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct PersistenceResourcePostReturn {
|
||||
pub _format: i64,
|
||||
pub generatedId: Option<GeneratedId>,
|
||||
pub permissionListId: GeneratedId,
|
||||
}
|
||||
|
||||
impl Entity for PersistenceResourcePostReturn {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "base",
|
||||
type_: "PersistenceResourcePostReturn",
|
||||
}
|
||||
}
|
||||
}
|
4
tuta-sdk/rust/src/entities/gossip.rs
Normal file
4
tuta-sdk/rust/src/entities/gossip.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
#![allow(non_snake_case, unused_imports)]
|
||||
use super::*;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
153
tuta-sdk/rust/src/entities/monitor.rs
Normal file
153
tuta-sdk/rust/src/entities/monitor.rs
Normal file
|
@ -0,0 +1,153 @@
|
|||
#![allow(non_snake_case, unused_imports)]
|
||||
use super::*;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct ApprovalMail {
|
||||
pub _format: i64,
|
||||
pub _id: IdTuple,
|
||||
pub _ownerGroup: Option<GeneratedId>,
|
||||
pub _permissions: GeneratedId,
|
||||
pub date: Option<DateTime>,
|
||||
pub range: Option<String>,
|
||||
pub text: String,
|
||||
pub customer: Option<GeneratedId>,
|
||||
}
|
||||
|
||||
impl Entity for ApprovalMail {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "monitor",
|
||||
type_: "ApprovalMail",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct CounterValue {
|
||||
pub _id: CustomId,
|
||||
pub counterId: GeneratedId,
|
||||
pub value: i64,
|
||||
}
|
||||
|
||||
impl Entity for CounterValue {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "monitor",
|
||||
type_: "CounterValue",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct ErrorReportData {
|
||||
pub _id: CustomId,
|
||||
pub additionalInfo: String,
|
||||
pub appVersion: String,
|
||||
pub clientType: i64,
|
||||
pub errorClass: String,
|
||||
pub errorMessage: Option<String>,
|
||||
pub stackTrace: String,
|
||||
pub time: DateTime,
|
||||
pub userId: Option<String>,
|
||||
pub userMessage: Option<String>,
|
||||
}
|
||||
|
||||
impl Entity for ErrorReportData {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "monitor",
|
||||
type_: "ErrorReportData",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct ErrorReportFile {
|
||||
pub _id: CustomId,
|
||||
pub content: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Entity for ErrorReportFile {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "monitor",
|
||||
type_: "ErrorReportFile",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct ReadCounterData {
|
||||
pub _format: i64,
|
||||
pub columnName: Option<GeneratedId>,
|
||||
pub counterType: i64,
|
||||
pub rowName: String,
|
||||
}
|
||||
|
||||
impl Entity for ReadCounterData {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "monitor",
|
||||
type_: "ReadCounterData",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct ReadCounterReturn {
|
||||
pub _format: i64,
|
||||
pub value: Option<i64>,
|
||||
pub counterValues: Vec<CounterValue>,
|
||||
}
|
||||
|
||||
impl Entity for ReadCounterReturn {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "monitor",
|
||||
type_: "ReadCounterReturn",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct ReportErrorIn {
|
||||
pub _format: i64,
|
||||
pub data: ErrorReportData,
|
||||
pub files: Vec<ErrorReportFile>,
|
||||
}
|
||||
|
||||
impl Entity for ReportErrorIn {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "monitor",
|
||||
type_: "ReportErrorIn",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct WriteCounterData {
|
||||
pub _format: i64,
|
||||
pub column: GeneratedId,
|
||||
pub counterType: Option<i64>,
|
||||
pub row: String,
|
||||
pub value: i64,
|
||||
}
|
||||
|
||||
impl Entity for WriteCounterData {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "monitor",
|
||||
type_: "WriteCounterData",
|
||||
}
|
||||
}
|
||||
}
|
227
tuta-sdk/rust/src/entities/storage.rs
Normal file
227
tuta-sdk/rust/src/entities/storage.rs
Normal file
|
@ -0,0 +1,227 @@
|
|||
#![allow(non_snake_case, unused_imports)]
|
||||
use super::*;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct BlobAccessTokenPostIn {
|
||||
pub _format: i64,
|
||||
pub archiveDataType: Option<i64>,
|
||||
pub read: Option<BlobReadData>,
|
||||
pub write: Option<BlobWriteData>,
|
||||
}
|
||||
|
||||
impl Entity for BlobAccessTokenPostIn {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "BlobAccessTokenPostIn",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct BlobAccessTokenPostOut {
|
||||
pub _format: i64,
|
||||
pub blobAccessInfo: BlobServerAccessInfo,
|
||||
}
|
||||
|
||||
impl Entity for BlobAccessTokenPostOut {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "BlobAccessTokenPostOut",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct BlobArchiveRef {
|
||||
pub _format: i64,
|
||||
pub _id: IdTuple,
|
||||
pub _ownerGroup: Option<GeneratedId>,
|
||||
pub _permissions: GeneratedId,
|
||||
pub archive: GeneratedId,
|
||||
}
|
||||
|
||||
impl Entity for BlobArchiveRef {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "BlobArchiveRef",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct BlobGetIn {
|
||||
pub _format: i64,
|
||||
pub archiveId: GeneratedId,
|
||||
pub blobId: Option<GeneratedId>,
|
||||
pub blobIds: Vec<BlobId>,
|
||||
}
|
||||
|
||||
impl Entity for BlobGetIn {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "BlobGetIn",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct BlobId {
|
||||
pub _id: CustomId,
|
||||
pub blobId: GeneratedId,
|
||||
}
|
||||
|
||||
impl Entity for BlobId {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "BlobId",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct BlobPostOut {
|
||||
pub _format: i64,
|
||||
pub blobReferenceToken: String,
|
||||
}
|
||||
|
||||
impl Entity for BlobPostOut {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "BlobPostOut",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct BlobReadData {
|
||||
pub _id: CustomId,
|
||||
pub archiveId: GeneratedId,
|
||||
pub instanceListId: Option<GeneratedId>,
|
||||
pub instanceIds: Vec<InstanceId>,
|
||||
}
|
||||
|
||||
impl Entity for BlobReadData {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "BlobReadData",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct BlobReferenceDeleteIn {
|
||||
pub _format: i64,
|
||||
pub archiveDataType: i64,
|
||||
pub instanceId: GeneratedId,
|
||||
pub instanceListId: Option<GeneratedId>,
|
||||
pub blobs: Vec<sys::Blob>,
|
||||
}
|
||||
|
||||
impl Entity for BlobReferenceDeleteIn {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "BlobReferenceDeleteIn",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct BlobReferencePutIn {
|
||||
pub _format: i64,
|
||||
pub archiveDataType: i64,
|
||||
pub instanceId: GeneratedId,
|
||||
pub instanceListId: Option<GeneratedId>,
|
||||
pub referenceTokens: Vec<sys::BlobReferenceTokenWrapper>,
|
||||
}
|
||||
|
||||
impl Entity for BlobReferencePutIn {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "BlobReferencePutIn",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct BlobServerAccessInfo {
|
||||
pub _id: CustomId,
|
||||
pub blobAccessToken: String,
|
||||
pub expires: DateTime,
|
||||
pub servers: Vec<BlobServerUrl>,
|
||||
}
|
||||
|
||||
impl Entity for BlobServerAccessInfo {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "BlobServerAccessInfo",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct BlobServerUrl {
|
||||
pub _id: CustomId,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl Entity for BlobServerUrl {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "BlobServerUrl",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct BlobWriteData {
|
||||
pub _id: CustomId,
|
||||
pub archiveOwnerGroup: GeneratedId,
|
||||
}
|
||||
|
||||
impl Entity for BlobWriteData {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "BlobWriteData",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct InstanceId {
|
||||
pub _id: CustomId,
|
||||
pub instanceId: Option<GeneratedId>,
|
||||
}
|
||||
|
||||
impl Entity for InstanceId {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "storage",
|
||||
type_: "InstanceId",
|
||||
}
|
||||
}
|
||||
}
|
4410
tuta-sdk/rust/src/entities/sys.rs
Normal file
4410
tuta-sdk/rust/src/entities/sys.rs
Normal file
File diff suppressed because it is too large
Load diff
2398
tuta-sdk/rust/src/entities/tutanota.rs
Normal file
2398
tuta-sdk/rust/src/entities/tutanota.rs
Normal file
File diff suppressed because it is too large
Load diff
146
tuta-sdk/rust/src/entities/usage.rs
Normal file
146
tuta-sdk/rust/src/entities/usage.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
#![allow(non_snake_case, unused_imports)]
|
||||
use super::*;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct UsageTestAssignment {
|
||||
pub _id: CustomId,
|
||||
pub name: String,
|
||||
pub sendPings: bool,
|
||||
pub testId: GeneratedId,
|
||||
pub variant: Option<i64>,
|
||||
pub stages: Vec<UsageTestStage>,
|
||||
}
|
||||
|
||||
impl Entity for UsageTestAssignment {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "usage",
|
||||
type_: "UsageTestAssignment",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct UsageTestAssignmentIn {
|
||||
pub _format: i64,
|
||||
pub testDeviceId: Option<GeneratedId>,
|
||||
}
|
||||
|
||||
impl Entity for UsageTestAssignmentIn {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "usage",
|
||||
type_: "UsageTestAssignmentIn",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct UsageTestAssignmentOut {
|
||||
pub _format: i64,
|
||||
pub testDeviceId: GeneratedId,
|
||||
pub assignments: Vec<UsageTestAssignment>,
|
||||
}
|
||||
|
||||
impl Entity for UsageTestAssignmentOut {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "usage",
|
||||
type_: "UsageTestAssignmentOut",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct UsageTestMetricConfig {
|
||||
pub _id: CustomId,
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub r#type: i64,
|
||||
pub configValues: Vec<UsageTestMetricConfigValue>,
|
||||
}
|
||||
|
||||
impl Entity for UsageTestMetricConfig {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "usage",
|
||||
type_: "UsageTestMetricConfig",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct UsageTestMetricConfigValue {
|
||||
pub _id: CustomId,
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl Entity for UsageTestMetricConfigValue {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "usage",
|
||||
type_: "UsageTestMetricConfigValue",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct UsageTestMetricData {
|
||||
pub _id: CustomId,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl Entity for UsageTestMetricData {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "usage",
|
||||
type_: "UsageTestMetricData",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct UsageTestParticipationIn {
|
||||
pub _format: i64,
|
||||
pub stage: i64,
|
||||
pub testDeviceId: GeneratedId,
|
||||
pub testId: GeneratedId,
|
||||
pub metrics: Vec<UsageTestMetricData>,
|
||||
}
|
||||
|
||||
impl Entity for UsageTestParticipationIn {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "usage",
|
||||
type_: "UsageTestParticipationIn",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(uniffi::Record, Clone, Serialize, Deserialize)]
|
||||
pub struct UsageTestStage {
|
||||
pub _id: CustomId,
|
||||
pub maxPings: i64,
|
||||
pub minPings: i64,
|
||||
pub name: String,
|
||||
pub metrics: Vec<UsageTestMetricConfig>,
|
||||
}
|
||||
|
||||
impl Entity for UsageTestStage {
|
||||
fn type_ref() -> TypeRef {
|
||||
TypeRef {
|
||||
app: "usage",
|
||||
type_: "UsageTestStage",
|
||||
}
|
||||
}
|
||||
}
|
140
tuta-sdk/rust/src/type_models/accounting.json
Normal file
140
tuta-sdk/rust/src/type_models/accounting.json
Normal file
|
@ -0,0 +1,140 @@
|
|||
{
|
||||
"CustomerAccountPosting": {
|
||||
"name": "CustomerAccountPosting",
|
||||
"since": 3,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 79,
|
||||
"rootId": "CmFjY291bnRpbmcATw",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 80,
|
||||
"since": 3,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"amount": {
|
||||
"final": true,
|
||||
"name": "amount",
|
||||
"id": 84,
|
||||
"since": 3,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": true
|
||||
},
|
||||
"invoiceNumber": {
|
||||
"final": true,
|
||||
"name": "invoiceNumber",
|
||||
"id": 83,
|
||||
"since": 3,
|
||||
"type": "String",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": true
|
||||
},
|
||||
"type": {
|
||||
"final": true,
|
||||
"name": "type",
|
||||
"id": 81,
|
||||
"since": 3,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": true
|
||||
},
|
||||
"valueDate": {
|
||||
"final": true,
|
||||
"name": "valueDate",
|
||||
"id": 82,
|
||||
"since": 3,
|
||||
"type": "Date",
|
||||
"cardinality": "One",
|
||||
"encrypted": true
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "accounting",
|
||||
"version": "7"
|
||||
},
|
||||
"CustomerAccountReturn": {
|
||||
"name": "CustomerAccountReturn",
|
||||
"since": 3,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 86,
|
||||
"rootId": "CmFjY291bnRpbmcAVg",
|
||||
"versioned": false,
|
||||
"encrypted": true,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 87,
|
||||
"since": 3,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"_ownerGroup": {
|
||||
"final": true,
|
||||
"name": "_ownerGroup",
|
||||
"id": 88,
|
||||
"since": 3,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
},
|
||||
"_ownerPublicEncSessionKey": {
|
||||
"final": true,
|
||||
"name": "_ownerPublicEncSessionKey",
|
||||
"id": 89,
|
||||
"since": 3,
|
||||
"type": "Bytes",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
},
|
||||
"_publicCryptoProtocolVersion": {
|
||||
"final": true,
|
||||
"name": "_publicCryptoProtocolVersion",
|
||||
"id": 96,
|
||||
"since": 7,
|
||||
"type": "Number",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
},
|
||||
"balance": {
|
||||
"final": true,
|
||||
"name": "balance",
|
||||
"id": 94,
|
||||
"since": 5,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": true
|
||||
},
|
||||
"outstandingBookingsPrice": {
|
||||
"final": false,
|
||||
"name": "outstandingBookingsPrice",
|
||||
"id": 92,
|
||||
"since": 4,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"postings": {
|
||||
"final": false,
|
||||
"name": "postings",
|
||||
"id": 90,
|
||||
"since": 3,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "CustomerAccountPosting",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "accounting",
|
||||
"version": "7"
|
||||
}
|
||||
}
|
43
tuta-sdk/rust/src/type_models/base.json
Normal file
43
tuta-sdk/rust/src/type_models/base.json
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"PersistenceResourcePostReturn": {
|
||||
"name": "PersistenceResourcePostReturn",
|
||||
"since": 1,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 0,
|
||||
"rootId": "BGJhc2UAAA",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 1,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"generatedId": {
|
||||
"final": false,
|
||||
"name": "generatedId",
|
||||
"id": 2,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
},
|
||||
"permissionListId": {
|
||||
"final": false,
|
||||
"name": "permissionListId",
|
||||
"id": 3,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "base",
|
||||
"version": "1"
|
||||
}
|
||||
}
|
1
tuta-sdk/rust/src/type_models/gossip.json
Normal file
1
tuta-sdk/rust/src/type_models/gossip.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
472
tuta-sdk/rust/src/type_models/monitor.json
Normal file
472
tuta-sdk/rust/src/type_models/monitor.json
Normal file
|
@ -0,0 +1,472 @@
|
|||
{
|
||||
"ApprovalMail": {
|
||||
"name": "ApprovalMail",
|
||||
"since": 14,
|
||||
"type": "LIST_ELEMENT_TYPE",
|
||||
"id": 221,
|
||||
"rootId": "B21vbml0b3IAAN0",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 225,
|
||||
"since": 14,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 223,
|
||||
"since": 14,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"_ownerGroup": {
|
||||
"final": true,
|
||||
"name": "_ownerGroup",
|
||||
"id": 226,
|
||||
"since": 14,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
},
|
||||
"_permissions": {
|
||||
"final": true,
|
||||
"name": "_permissions",
|
||||
"id": 224,
|
||||
"since": 14,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"date": {
|
||||
"final": true,
|
||||
"name": "date",
|
||||
"id": 228,
|
||||
"since": 14,
|
||||
"type": "Date",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
},
|
||||
"range": {
|
||||
"final": true,
|
||||
"name": "range",
|
||||
"id": 227,
|
||||
"since": 14,
|
||||
"type": "String",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
},
|
||||
"text": {
|
||||
"final": true,
|
||||
"name": "text",
|
||||
"id": 229,
|
||||
"since": 14,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"customer": {
|
||||
"final": true,
|
||||
"name": "customer",
|
||||
"id": 230,
|
||||
"since": 14,
|
||||
"type": "ELEMENT_ASSOCIATION",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"refType": "Customer",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "monitor",
|
||||
"version": "28"
|
||||
},
|
||||
"CounterValue": {
|
||||
"name": "CounterValue",
|
||||
"since": 22,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 300,
|
||||
"rootId": "B21vbml0b3IAASw",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 301,
|
||||
"since": 22,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"counterId": {
|
||||
"final": false,
|
||||
"name": "counterId",
|
||||
"id": 302,
|
||||
"since": 22,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"value": {
|
||||
"final": false,
|
||||
"name": "value",
|
||||
"id": 303,
|
||||
"since": 22,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "monitor",
|
||||
"version": "28"
|
||||
},
|
||||
"ErrorReportData": {
|
||||
"name": "ErrorReportData",
|
||||
"since": 23,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 316,
|
||||
"rootId": "B21vbml0b3IAATw",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 317,
|
||||
"since": 23,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"additionalInfo": {
|
||||
"final": true,
|
||||
"name": "additionalInfo",
|
||||
"id": 326,
|
||||
"since": 23,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"appVersion": {
|
||||
"final": true,
|
||||
"name": "appVersion",
|
||||
"id": 319,
|
||||
"since": 23,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"clientType": {
|
||||
"final": true,
|
||||
"name": "clientType",
|
||||
"id": 320,
|
||||
"since": 23,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"errorClass": {
|
||||
"final": true,
|
||||
"name": "errorClass",
|
||||
"id": 322,
|
||||
"since": 23,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"errorMessage": {
|
||||
"final": true,
|
||||
"name": "errorMessage",
|
||||
"id": 323,
|
||||
"since": 23,
|
||||
"type": "String",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
},
|
||||
"stackTrace": {
|
||||
"final": true,
|
||||
"name": "stackTrace",
|
||||
"id": 324,
|
||||
"since": 23,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"time": {
|
||||
"final": true,
|
||||
"name": "time",
|
||||
"id": 318,
|
||||
"since": 23,
|
||||
"type": "Date",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"userId": {
|
||||
"final": true,
|
||||
"name": "userId",
|
||||
"id": 321,
|
||||
"since": 23,
|
||||
"type": "String",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
},
|
||||
"userMessage": {
|
||||
"final": true,
|
||||
"name": "userMessage",
|
||||
"id": 325,
|
||||
"since": 23,
|
||||
"type": "String",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "monitor",
|
||||
"version": "28"
|
||||
},
|
||||
"ErrorReportFile": {
|
||||
"name": "ErrorReportFile",
|
||||
"since": 23,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 305,
|
||||
"rootId": "B21vbml0b3IAATE",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 306,
|
||||
"since": 23,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"content": {
|
||||
"final": true,
|
||||
"name": "content",
|
||||
"id": 308,
|
||||
"since": 23,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"name": {
|
||||
"final": true,
|
||||
"name": "name",
|
||||
"id": 307,
|
||||
"since": 23,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "monitor",
|
||||
"version": "28"
|
||||
},
|
||||
"ReadCounterData": {
|
||||
"name": "ReadCounterData",
|
||||
"since": 1,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 12,
|
||||
"rootId": "B21vbml0b3IADA",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 13,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"columnName": {
|
||||
"final": false,
|
||||
"name": "columnName",
|
||||
"id": 15,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
},
|
||||
"counterType": {
|
||||
"final": false,
|
||||
"name": "counterType",
|
||||
"id": 299,
|
||||
"since": 22,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"rowName": {
|
||||
"final": false,
|
||||
"name": "rowName",
|
||||
"id": 14,
|
||||
"since": 1,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "monitor",
|
||||
"version": "28"
|
||||
},
|
||||
"ReadCounterReturn": {
|
||||
"name": "ReadCounterReturn",
|
||||
"since": 1,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 16,
|
||||
"rootId": "B21vbml0b3IAEA",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 17,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"value": {
|
||||
"final": false,
|
||||
"name": "value",
|
||||
"id": 18,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"counterValues": {
|
||||
"final": false,
|
||||
"name": "counterValues",
|
||||
"id": 304,
|
||||
"since": 22,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "CounterValue",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "monitor",
|
||||
"version": "28"
|
||||
},
|
||||
"ReportErrorIn": {
|
||||
"name": "ReportErrorIn",
|
||||
"since": 23,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 335,
|
||||
"rootId": "B21vbml0b3IAAU8",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 336,
|
||||
"since": 23,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"data": {
|
||||
"final": false,
|
||||
"name": "data",
|
||||
"id": 337,
|
||||
"since": 23,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "One",
|
||||
"refType": "ErrorReportData",
|
||||
"dependency": null
|
||||
},
|
||||
"files": {
|
||||
"final": false,
|
||||
"name": "files",
|
||||
"id": 338,
|
||||
"since": 23,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "ErrorReportFile",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "monitor",
|
||||
"version": "28"
|
||||
},
|
||||
"WriteCounterData": {
|
||||
"name": "WriteCounterData",
|
||||
"since": 4,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 49,
|
||||
"rootId": "B21vbml0b3IAMQ",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 50,
|
||||
"since": 4,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"column": {
|
||||
"final": false,
|
||||
"name": "column",
|
||||
"id": 52,
|
||||
"since": 4,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"counterType": {
|
||||
"final": false,
|
||||
"name": "counterType",
|
||||
"id": 215,
|
||||
"since": 12,
|
||||
"type": "Number",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
},
|
||||
"row": {
|
||||
"final": false,
|
||||
"name": "row",
|
||||
"id": 51,
|
||||
"since": 4,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"value": {
|
||||
"final": false,
|
||||
"name": "value",
|
||||
"id": 53,
|
||||
"since": 4,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "monitor",
|
||||
"version": "28"
|
||||
}
|
||||
}
|
588
tuta-sdk/rust/src/type_models/storage.json
Normal file
588
tuta-sdk/rust/src/type_models/storage.json
Normal file
|
@ -0,0 +1,588 @@
|
|||
{
|
||||
"BlobAccessTokenPostIn": {
|
||||
"name": "BlobAccessTokenPostIn",
|
||||
"since": 1,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 77,
|
||||
"rootId": "B3N0b3JhZ2UATQ",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 78,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"archiveDataType": {
|
||||
"final": false,
|
||||
"name": "archiveDataType",
|
||||
"id": 180,
|
||||
"since": 4,
|
||||
"type": "Number",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"read": {
|
||||
"final": true,
|
||||
"name": "read",
|
||||
"id": 181,
|
||||
"since": 4,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"refType": "BlobReadData",
|
||||
"dependency": null
|
||||
},
|
||||
"write": {
|
||||
"final": false,
|
||||
"name": "write",
|
||||
"id": 80,
|
||||
"since": 1,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"refType": "BlobWriteData",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
},
|
||||
"BlobAccessTokenPostOut": {
|
||||
"name": "BlobAccessTokenPostOut",
|
||||
"since": 1,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 81,
|
||||
"rootId": "B3N0b3JhZ2UAUQ",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 82,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"blobAccessInfo": {
|
||||
"final": false,
|
||||
"name": "blobAccessInfo",
|
||||
"id": 161,
|
||||
"since": 4,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "One",
|
||||
"refType": "BlobServerAccessInfo",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
},
|
||||
"BlobArchiveRef": {
|
||||
"name": "BlobArchiveRef",
|
||||
"since": 4,
|
||||
"type": "LIST_ELEMENT_TYPE",
|
||||
"id": 129,
|
||||
"rootId": "B3N0b3JhZ2UAAIE",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 133,
|
||||
"since": 4,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 131,
|
||||
"since": 4,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"_ownerGroup": {
|
||||
"final": true,
|
||||
"name": "_ownerGroup",
|
||||
"id": 134,
|
||||
"since": 4,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
},
|
||||
"_permissions": {
|
||||
"final": true,
|
||||
"name": "_permissions",
|
||||
"id": 132,
|
||||
"since": 4,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"archive": {
|
||||
"final": false,
|
||||
"name": "archive",
|
||||
"id": 135,
|
||||
"since": 4,
|
||||
"type": "ELEMENT_ASSOCIATION",
|
||||
"cardinality": "One",
|
||||
"refType": "Archive",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
},
|
||||
"BlobGetIn": {
|
||||
"name": "BlobGetIn",
|
||||
"since": 1,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 50,
|
||||
"rootId": "B3N0b3JhZ2UAMg",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 51,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"archiveId": {
|
||||
"final": false,
|
||||
"name": "archiveId",
|
||||
"id": 52,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"blobId": {
|
||||
"final": false,
|
||||
"name": "blobId",
|
||||
"id": 110,
|
||||
"since": 3,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"blobIds": {
|
||||
"final": true,
|
||||
"name": "blobIds",
|
||||
"id": 193,
|
||||
"since": 8,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "BlobId",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
},
|
||||
"BlobId": {
|
||||
"name": "BlobId",
|
||||
"since": 4,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 144,
|
||||
"rootId": "B3N0b3JhZ2UAAJA",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 145,
|
||||
"since": 4,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"blobId": {
|
||||
"final": false,
|
||||
"name": "blobId",
|
||||
"id": 146,
|
||||
"since": 4,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
},
|
||||
"BlobPostOut": {
|
||||
"name": "BlobPostOut",
|
||||
"since": 4,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 125,
|
||||
"rootId": "B3N0b3JhZ2UAfQ",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 126,
|
||||
"since": 4,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"blobReferenceToken": {
|
||||
"final": false,
|
||||
"name": "blobReferenceToken",
|
||||
"id": 127,
|
||||
"since": 4,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
},
|
||||
"BlobReadData": {
|
||||
"name": "BlobReadData",
|
||||
"since": 4,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 175,
|
||||
"rootId": "B3N0b3JhZ2UAAK8",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 176,
|
||||
"since": 4,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"archiveId": {
|
||||
"final": false,
|
||||
"name": "archiveId",
|
||||
"id": 177,
|
||||
"since": 4,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"instanceListId": {
|
||||
"final": true,
|
||||
"name": "instanceListId",
|
||||
"id": 178,
|
||||
"since": 4,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"instanceIds": {
|
||||
"final": true,
|
||||
"name": "instanceIds",
|
||||
"id": 179,
|
||||
"since": 4,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "InstanceId",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
},
|
||||
"BlobReferenceDeleteIn": {
|
||||
"name": "BlobReferenceDeleteIn",
|
||||
"since": 1,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 100,
|
||||
"rootId": "B3N0b3JhZ2UAZA",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 101,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"archiveDataType": {
|
||||
"final": false,
|
||||
"name": "archiveDataType",
|
||||
"id": 124,
|
||||
"since": 4,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"instanceId": {
|
||||
"final": false,
|
||||
"name": "instanceId",
|
||||
"id": 103,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"instanceListId": {
|
||||
"final": false,
|
||||
"name": "instanceListId",
|
||||
"id": 102,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"blobs": {
|
||||
"final": true,
|
||||
"name": "blobs",
|
||||
"id": 105,
|
||||
"since": 1,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "Blob",
|
||||
"dependency": "sys"
|
||||
}
|
||||
},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
},
|
||||
"BlobReferencePutIn": {
|
||||
"name": "BlobReferencePutIn",
|
||||
"since": 1,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 94,
|
||||
"rootId": "B3N0b3JhZ2UAXg",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 95,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"archiveDataType": {
|
||||
"final": false,
|
||||
"name": "archiveDataType",
|
||||
"id": 123,
|
||||
"since": 4,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"instanceId": {
|
||||
"final": false,
|
||||
"name": "instanceId",
|
||||
"id": 107,
|
||||
"since": 2,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"instanceListId": {
|
||||
"final": false,
|
||||
"name": "instanceListId",
|
||||
"id": 97,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"referenceTokens": {
|
||||
"final": true,
|
||||
"name": "referenceTokens",
|
||||
"id": 122,
|
||||
"since": 4,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "BlobReferenceTokenWrapper",
|
||||
"dependency": "sys"
|
||||
}
|
||||
},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
},
|
||||
"BlobServerAccessInfo": {
|
||||
"name": "BlobServerAccessInfo",
|
||||
"since": 4,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 157,
|
||||
"rootId": "B3N0b3JhZ2UAAJ0",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 158,
|
||||
"since": 4,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"blobAccessToken": {
|
||||
"final": false,
|
||||
"name": "blobAccessToken",
|
||||
"id": 159,
|
||||
"since": 4,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"expires": {
|
||||
"final": false,
|
||||
"name": "expires",
|
||||
"id": 192,
|
||||
"since": 6,
|
||||
"type": "Date",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"servers": {
|
||||
"final": false,
|
||||
"name": "servers",
|
||||
"id": 160,
|
||||
"since": 4,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "BlobServerUrl",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
},
|
||||
"BlobServerUrl": {
|
||||
"name": "BlobServerUrl",
|
||||
"since": 4,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 154,
|
||||
"rootId": "B3N0b3JhZ2UAAJo",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 155,
|
||||
"since": 4,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"url": {
|
||||
"final": false,
|
||||
"name": "url",
|
||||
"id": 156,
|
||||
"since": 4,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
},
|
||||
"BlobWriteData": {
|
||||
"name": "BlobWriteData",
|
||||
"since": 1,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 73,
|
||||
"rootId": "B3N0b3JhZ2UASQ",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 74,
|
||||
"since": 1,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"archiveOwnerGroup": {
|
||||
"final": false,
|
||||
"name": "archiveOwnerGroup",
|
||||
"id": 75,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
},
|
||||
"InstanceId": {
|
||||
"name": "InstanceId",
|
||||
"since": 4,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 172,
|
||||
"rootId": "B3N0b3JhZ2UAAKw",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 173,
|
||||
"since": 4,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"instanceId": {
|
||||
"final": true,
|
||||
"name": "instanceId",
|
||||
"id": 174,
|
||||
"since": 4,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "storage",
|
||||
"version": "9"
|
||||
}
|
||||
}
|
14474
tuta-sdk/rust/src/type_models/sys.json
Normal file
14474
tuta-sdk/rust/src/type_models/sys.json
Normal file
File diff suppressed because it is too large
Load diff
8033
tuta-sdk/rust/src/type_models/tutanota.json
Normal file
8033
tuta-sdk/rust/src/type_models/tutanota.json
Normal file
File diff suppressed because it is too large
Load diff
403
tuta-sdk/rust/src/type_models/usage.json
Normal file
403
tuta-sdk/rust/src/type_models/usage.json
Normal file
|
@ -0,0 +1,403 @@
|
|||
{
|
||||
"UsageTestAssignment": {
|
||||
"name": "UsageTestAssignment",
|
||||
"since": 1,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 56,
|
||||
"rootId": "BXVzYWdlADg",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 57,
|
||||
"since": 1,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"name": {
|
||||
"final": false,
|
||||
"name": "name",
|
||||
"id": 59,
|
||||
"since": 1,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"sendPings": {
|
||||
"final": false,
|
||||
"name": "sendPings",
|
||||
"id": 61,
|
||||
"since": 1,
|
||||
"type": "Boolean",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"testId": {
|
||||
"final": true,
|
||||
"name": "testId",
|
||||
"id": 58,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"variant": {
|
||||
"final": true,
|
||||
"name": "variant",
|
||||
"id": 60,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"stages": {
|
||||
"final": false,
|
||||
"name": "stages",
|
||||
"id": 62,
|
||||
"since": 1,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "UsageTestStage",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "usage",
|
||||
"version": "2"
|
||||
},
|
||||
"UsageTestAssignmentIn": {
|
||||
"name": "UsageTestAssignmentIn",
|
||||
"since": 1,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 53,
|
||||
"rootId": "BXVzYWdlADU",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 54,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"testDeviceId": {
|
||||
"final": false,
|
||||
"name": "testDeviceId",
|
||||
"id": 55,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "ZeroOrOne",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "usage",
|
||||
"version": "2"
|
||||
},
|
||||
"UsageTestAssignmentOut": {
|
||||
"name": "UsageTestAssignmentOut",
|
||||
"since": 1,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 63,
|
||||
"rootId": "BXVzYWdlAD8",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 64,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"testDeviceId": {
|
||||
"final": false,
|
||||
"name": "testDeviceId",
|
||||
"id": 65,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"assignments": {
|
||||
"final": false,
|
||||
"name": "assignments",
|
||||
"id": 66,
|
||||
"since": 1,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "UsageTestAssignment",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "usage",
|
||||
"version": "2"
|
||||
},
|
||||
"UsageTestMetricConfig": {
|
||||
"name": "UsageTestMetricConfig",
|
||||
"since": 1,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 12,
|
||||
"rootId": "BXVzYWdlAAw",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 13,
|
||||
"since": 1,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"name": {
|
||||
"final": true,
|
||||
"name": "name",
|
||||
"id": 14,
|
||||
"since": 1,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"type": {
|
||||
"final": true,
|
||||
"name": "type",
|
||||
"id": 15,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"configValues": {
|
||||
"final": false,
|
||||
"name": "configValues",
|
||||
"id": 16,
|
||||
"since": 1,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "UsageTestMetricConfigValue",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "usage",
|
||||
"version": "2"
|
||||
},
|
||||
"UsageTestMetricConfigValue": {
|
||||
"name": "UsageTestMetricConfigValue",
|
||||
"since": 1,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 8,
|
||||
"rootId": "BXVzYWdlAAg",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 9,
|
||||
"since": 1,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"key": {
|
||||
"final": false,
|
||||
"name": "key",
|
||||
"id": 10,
|
||||
"since": 1,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"value": {
|
||||
"final": false,
|
||||
"name": "value",
|
||||
"id": 11,
|
||||
"since": 1,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "usage",
|
||||
"version": "2"
|
||||
},
|
||||
"UsageTestMetricData": {
|
||||
"name": "UsageTestMetricData",
|
||||
"since": 1,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 17,
|
||||
"rootId": "BXVzYWdlABE",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 18,
|
||||
"since": 1,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"name": {
|
||||
"final": true,
|
||||
"name": "name",
|
||||
"id": 19,
|
||||
"since": 1,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"value": {
|
||||
"final": true,
|
||||
"name": "value",
|
||||
"id": 20,
|
||||
"since": 1,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {},
|
||||
"app": "usage",
|
||||
"version": "2"
|
||||
},
|
||||
"UsageTestParticipationIn": {
|
||||
"name": "UsageTestParticipationIn",
|
||||
"since": 1,
|
||||
"type": "DATA_TRANSFER_TYPE",
|
||||
"id": 80,
|
||||
"rootId": "BXVzYWdlAFA",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_format": {
|
||||
"final": false,
|
||||
"name": "_format",
|
||||
"id": 81,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"stage": {
|
||||
"final": false,
|
||||
"name": "stage",
|
||||
"id": 83,
|
||||
"since": 1,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"testDeviceId": {
|
||||
"final": false,
|
||||
"name": "testDeviceId",
|
||||
"id": 84,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"testId": {
|
||||
"final": false,
|
||||
"name": "testId",
|
||||
"id": 82,
|
||||
"since": 1,
|
||||
"type": "GeneratedId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"metrics": {
|
||||
"final": false,
|
||||
"name": "metrics",
|
||||
"id": 85,
|
||||
"since": 1,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "UsageTestMetricData",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "usage",
|
||||
"version": "2"
|
||||
},
|
||||
"UsageTestStage": {
|
||||
"name": "UsageTestStage",
|
||||
"since": 1,
|
||||
"type": "AGGREGATED_TYPE",
|
||||
"id": 35,
|
||||
"rootId": "BXVzYWdlACM",
|
||||
"versioned": false,
|
||||
"encrypted": false,
|
||||
"values": {
|
||||
"_id": {
|
||||
"final": true,
|
||||
"name": "_id",
|
||||
"id": 36,
|
||||
"since": 1,
|
||||
"type": "CustomId",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"maxPings": {
|
||||
"final": false,
|
||||
"name": "maxPings",
|
||||
"id": 88,
|
||||
"since": 2,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"minPings": {
|
||||
"final": false,
|
||||
"name": "minPings",
|
||||
"id": 87,
|
||||
"since": 2,
|
||||
"type": "Number",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
},
|
||||
"name": {
|
||||
"final": false,
|
||||
"name": "name",
|
||||
"id": 37,
|
||||
"since": 1,
|
||||
"type": "String",
|
||||
"cardinality": "One",
|
||||
"encrypted": false
|
||||
}
|
||||
},
|
||||
"associations": {
|
||||
"metrics": {
|
||||
"final": false,
|
||||
"name": "metrics",
|
||||
"id": 38,
|
||||
"since": 1,
|
||||
"type": "AGGREGATION",
|
||||
"cardinality": "Any",
|
||||
"refType": "UsageTestMetricConfig",
|
||||
"dependency": null
|
||||
}
|
||||
},
|
||||
"app": "usage",
|
||||
"version": "2"
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue