Create MailboxProperties for other users as an admin, #516

This commit is contained in:
ivk 2022-11-08 17:06:42 +01:00 committed by Willow
parent caf64a3f50
commit 3cc58ae1c1
31 changed files with 645 additions and 228 deletions

View file

@ -7,6 +7,7 @@ import {last, TypeRef} from "@tutao/tutanota-utils"
import { resolveTypeReference} from "./EntityFunctions"
import type {ElementEntity, ListElementEntity, SomeEntity} from "./EntityTypes"
import {downcast} from "@tutao/tutanota-utils";
import {EntityRestClientSetupOptions} from "../worker/rest/EntityRestClient"
export class EntityClient {
_target: EntityRestInterface
@ -15,8 +16,8 @@ export class EntityClient {
this._target = target
}
load<T extends SomeEntity>(typeRef: TypeRef<T>, id: PropertyType<T, "_id">, query?: Dict, extraHeaders?: Dict): Promise<T> {
return this._target.load(typeRef, id, query, extraHeaders)
load<T extends SomeEntity>(typeRef: TypeRef<T>, id: PropertyType<T, "_id">, query?: Dict, extraHeaders?: Dict, ownerKey?: Aes128Key): Promise<T> {
return this._target.load(typeRef, id, query, extraHeaders, ownerKey)
}
async loadAll<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, start?: Id): Promise<T[]> {
@ -81,16 +82,16 @@ export class EntityClient {
return this._target.loadMultiple(typeRef, listId, elementIds)
}
setup<T extends SomeEntity>(listId: Id | null, instance: T, extraHeaders?: Dict): Promise<Id> {
return this._target.setup(listId, instance, extraHeaders)
setup<T extends SomeEntity>(listId: Id | null, instance: T, extraHeaders?: Dict, options?: EntityRestClientSetupOptions): Promise<Id> {
return this._target.setup(listId, instance, extraHeaders, options)
}
setupMultipleEntities<T extends SomeEntity>(listId: Id | null, instances: Array<T>): Promise<Array<Id>> {
return this._target.setupMultiple(listId, instances)
}
update<T extends SomeEntity>(instance: T): Promise<void> {
return this._target.update(instance)
update<T extends SomeEntity>(instance: T, ownerKey?: Aes128Key): Promise<void> {
return this._target.update(instance, ownerKey)
}
erase<T extends SomeEntity>(instance: T): Promise<void> {

View file

@ -5,6 +5,8 @@ import stream from "mithril/stream"
import Stream from "mithril/stream"
import {assertMainOrNode} from "../common/Env"
import {EntityUpdate, WebsocketCounterData} from "../entities/sys/TypeRefs"
import {SomeEntity} from "../common/EntityTypes.js"
import {isSameId} from "../common/utils/EntityUtils.js"
assertMainOrNode()
export type EntityUpdateData = {
@ -16,6 +18,11 @@ export type EntityUpdateData = {
}
export type EntityEventsListener = (updates: ReadonlyArray<EntityUpdateData>, eventOwnerGroupId: Id) => Promise<any>
export const isUpdateForTypeRef = <T>(typeRef: TypeRef<T>, update: EntityUpdateData): boolean => isSameTypeRefByAttr(typeRef, update.application, update.type)
export function isUpdateFor<T extends SomeEntity>(entity: T, update: EntityUpdateData): boolean {
const typeRef = entity._type as TypeRef<T>
return isUpdateForTypeRef(typeRef, update)
&& (update.instanceListId === "" ? isSameId(update.instanceId, entity._id) : isSameId([update.instanceListId, update.instanceId], entity._id))
}
export class EventController {
private countersStream: Stream<WebsocketCounterData> = stream()

View file

@ -173,6 +173,7 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
locator.rsa,
locator.cachingEntityClient,
locator.serviceExecutor,
new EntityClient(entityRestClient), // without cache
)
locator.customer = new CustomerFacade(
worker,

View file

@ -168,6 +168,16 @@ export class CryptoFacade {
return key == null ? null : bitArrayToUint8Array(key)
}
/** Resolve a session key an {@param instance} using an already known {@param ownerKey}. */
resolveSessionKeyWithOwnerKey(instance: Record<string, any>, ownerKey: Aes128Key): Aes128Key {
let key = instance._ownerEncSessionKey
if (typeof key === "string") {
key = base64ToUint8Array(instance._ownerEncSessionKey)
}
return decryptKey(ownerKey, key)
}
/**
* Returns the session key for the provided type/instance:
* * null, if the instance is unencrypted
@ -189,23 +199,13 @@ export class CryptoFacade {
delete this.mailBodySessionKeyCache[instance._id]
return sessionKey
} else if (instance._ownerEncSessionKey && this.userFacade.isFullyLoggedIn() && this.userFacade.hasGroup(instance._ownerGroup)) {
let gk = this.userFacade.getGroupKey(instance._ownerGroup)
let key = instance._ownerEncSessionKey
if (typeof key === "string") {
key = base64ToUint8Array(instance._ownerEncSessionKey)
}
return decryptKey(gk, key)
const gk = this.userFacade.getGroupKey(instance._ownerGroup)
return this.resolveSessionKeyWithOwnerKey(instance, gk)
} else if (instance.ownerEncSessionKey) {
// TODO this is a service instance: Rename all ownerEncSessionKey attributes to _ownerEncSessionKey and add _ownerGroupId (set ownerEncSessionKey here automatically after resolving the group)
// add to payment data service
const gk = this.userFacade.getGroupKey(this.userFacade.getGroupId(GroupType.Mail))
const key: Uint8Array = (typeof instance.ownerEncSessionKey === "string")
? base64ToUint8Array(instance.ownerEncSessionKey)
: instance.ownerEncSessionKey
return decryptKey(gk, key)
return this.resolveSessionKeyWithOwnerKey(instance, gk)
} else {
// See PermissionType jsdoc for more info on permissions
const permissions = await this.entityClient.loadAll(PermissionTypeRef, instance._permissions)
@ -387,7 +387,7 @@ export class CryptoFacade {
* the entity must already have an _ownerGroup
* @returns the generated key
*/
setNewOwnerEncSessionKey(model: TypeModel, entity: Record<string, any>): Aes128Key | null {
setNewOwnerEncSessionKey(model: TypeModel, entity: Record<string, any>, keyToEncryptSessionKey?: Aes128Key): Aes128Key | null {
if (!entity._ownerGroup) {
throw new Error(`no owner group set ${JSON.stringify(entity)}`)
}
@ -397,8 +397,9 @@ export class CryptoFacade {
throw new Error(`ownerEncSessionKey already set ${JSON.stringify(entity)}`)
}
let sessionKey = aes128RandomKey()
entity._ownerEncSessionKey = encryptKey(this.userFacade.getGroupKey(entity._ownerGroup), sessionKey)
const sessionKey = aes128RandomKey()
const effectiveKeyToEncryptSessionKey = keyToEncryptSessionKey ?? this.userFacade.getGroupKey(entity._ownerGroup)
entity._ownerEncSessionKey = encryptKey(effectiveKeyToEncryptSessionKey, sessionKey)
return sessionKey
} else {
return null

View file

@ -1,20 +1,17 @@
import {Const, GroupType} from "../../common/TutanotaConstants"
import {createCreateMailGroupData} from "../../entities/tutanota/TypeRefs.js"
import type {InternalGroupData} from "../../entities/tutanota/TypeRefs.js"
import {createInternalGroupData} from "../../entities/tutanota/TypeRefs.js"
import {hexToUint8Array, neverNull} from "@tutao/tutanota-utils"
import {LoginFacade} from "./LoginFacade"
import {createCreateLocalAdminGroupData} from "../../entities/tutanota/TypeRefs.js"
import type {Group} from "../../entities/sys/TypeRefs.js"
import {GroupTypeRef} from "../../entities/sys/TypeRefs.js"
import {createMembershipAddData} from "../../entities/sys/TypeRefs.js"
import {createMembershipRemoveData} from "../../entities/sys/TypeRefs.js"
import {createDeleteGroupData} from "../../entities/tutanota/TypeRefs.js"
import type {InternalGroupData, UserAreaGroupData} from "../../entities/tutanota/TypeRefs.js"
import {
createCreateLocalAdminGroupData,
createCreateMailGroupData,
createDeleteGroupData,
createInternalGroupData,
createUserAreaGroupData,
createUserAreaGroupPostData
} from "../../entities/tutanota/TypeRefs.js"
import {assertNotNull, hexToUint8Array, neverNull} from "@tutao/tutanota-utils"
import type {Group, User} from "../../entities/sys/TypeRefs.js"
import {createMembershipAddData, createMembershipRemoveData, GroupTypeRef, UserTypeRef} from "../../entities/sys/TypeRefs.js"
import {CounterFacade} from "./CounterFacade"
import type {User} from "../../entities/sys/TypeRefs.js"
import {createUserAreaGroupPostData} from "../../entities/tutanota/TypeRefs.js"
import type {UserAreaGroupData} from "../../entities/tutanota/TypeRefs.js"
import {createUserAreaGroupData} from "../../entities/tutanota/TypeRefs.js"
import {EntityClient} from "../../common/EntityClient"
import {assertWorkerOrNode} from "../../common/Env"
import {encryptString} from "../crypto/CryptoFacade"
@ -24,6 +21,7 @@ import {IServiceExecutor} from "../../common/ServiceRequest"
import {LocalAdminGroupService, MailGroupService, TemplateGroupService} from "../../entities/tutanota/Services"
import {MembershipService} from "../../entities/sys/Services"
import {UserFacade} from "./UserFacade"
import {ProgrammingError} from "../../common/error/ProgrammingError.js"
assertWorkerOrNode()
@ -35,7 +33,8 @@ export class GroupManagementFacade {
private readonly entityClient: EntityClient,
private readonly rsa: RsaImplementation,
private readonly serviceExecutor: IServiceExecutor,
) {}
) {
}
readUsedGroupStorage(groupId: Id): Promise<number> {
return this.counters.readCounterValue(Const.COUNTER_USED_MEMORY, groupId).then(usedStorage => {
@ -159,8 +158,8 @@ export class GroupManagementFacade {
}
async addUserToGroup(user: User, groupId: Id): Promise<void> {
const userGroupKey = await this.getGroupKeyAsAdmin(user.userGroup.group)
const groupKey = await this.getGroupKeyAsAdmin(groupId)
const userGroupKey = await this.getGroupKeyViaAdminEncGKey(user.userGroup.group)
const groupKey = await this.getGroupKeyViaAdminEncGKey(groupId)
const data = createMembershipAddData({
user: user._id,
group: groupId,
@ -192,26 +191,48 @@ export class GroupManagementFacade {
}
}
getGroupKeyAsAdmin(groupId: Id): Promise<Aes128Key> {
/**
* Get a group key for any group we are admin and know some member of.
*
* Unlike {@link getGroupKeyViaAdminEncGKey} this should work for any group because we will actually go a "long" route of decrypting userGroupKey of the
* member and decrypting group key with that.
*/
async getGroupKeyViaUser(groupId: Id, viaUser: Id): Promise<Aes128Key> {
const user = await this.entityClient.load(UserTypeRef, viaUser)
const userGroupKey = await this.getGroupKeyViaAdminEncGKey(user.userGroup.group)
const ship = assertNotNull(await user.memberships.find((m) => m.group === groupId), "User doesn't have this group membership!")
return decryptKey(userGroupKey, ship.symEncGKey)
}
/**
* Get a group key for certain group types.
*
* Some groups (e.g. user groups or shared mailboxes) have adminGroupEncGKey set on creation. For those groups we can fairly easy get a group key without
* decrypting userGroupKey of some member of that group.
*/
getGroupKeyViaAdminEncGKey(groupId: Id): Promise<Aes128Key> {
if (this.user.hasGroup(groupId)) {
// e.g. I am a global admin and want to add another user to the global admin group
return Promise.resolve(this.user.getGroupKey(neverNull(groupId)))
return Promise.resolve(this.user.getGroupKey(groupId))
} else {
return this.entityClient.load(GroupTypeRef, groupId).then(group => {
if (group.adminGroupEncGKey == null || group.adminGroupEncGKey.length === 0) {
throw new ProgrammingError("Group doesn't have adminGroupEncGKey, you can't get group key this way")
}
return Promise.resolve()
.then(() => {
if (group.admin && this.user.hasGroup(group.admin)) {
// e.g. I am a member of the group that administrates group G and want to add a new member to G
return this.user.getGroupKey(neverNull(group.admin))
return this.user.getGroupKey(assertNotNull(group.admin))
} else {
// e.g. I am a global admin but group G is administrated by a local admin group and want to add a new member to G
let globalAdminGroupId = this.user.getGroupId(GroupType.Admin)
let globalAdminGroupKey = this.user.getGroupKey(globalAdminGroupId)
return this.entityClient.load(GroupTypeRef, neverNull(group.admin)).then(localAdminGroup => {
return this.entityClient.load(GroupTypeRef, assertNotNull(group.admin)).then(localAdminGroup => {
if (localAdminGroup.admin === globalAdminGroupId) {
return decryptKey(globalAdminGroupKey, neverNull(localAdminGroup.adminGroupEncGKey))
return decryptKey(globalAdminGroupKey, assertNotNull(localAdminGroup.adminGroupEncGKey))
} else {
throw new Error(`local admin group ${localAdminGroup._id} is not administrated by global admin group ${globalAdminGroupId}`)
}
@ -219,7 +240,7 @@ export class GroupManagementFacade {
}
})
.then(adminGroupKey => {
return decryptKey(adminGroupKey, neverNull(group.adminGroupEncGKey))
return decryptKey(adminGroupKey, assertNotNull(group.adminGroupEncGKey))
})
})
}

View file

@ -11,8 +11,16 @@ import {
} from "../../entities/sys/TypeRefs.js"
import {encryptBytes, encryptString} from "../crypto/CryptoFacade"
import {assertNotNull, neverNull, uint8ArrayToHex} from "@tutao/tutanota-utils"
import type {ContactFormUserData, UserAccountUserData} from "../../entities/tutanota/TypeRefs.js"
import {createContactFormUserData, createUserAccountCreateData, createUserAccountUserData} from "../../entities/tutanota/TypeRefs.js"
import type {ContactFormUserData, MailboxGroupRoot, MailboxProperties, UserAccountUserData} from "../../entities/tutanota/TypeRefs.js"
import {
createContactFormUserData,
createMailAddressProperties,
createMailboxProperties,
createUserAccountCreateData,
createUserAccountUserData,
MailboxGroupRootTypeRef,
MailboxPropertiesTypeRef
} from "../../entities/tutanota/TypeRefs.js"
import type {GroupManagementFacade} from "./GroupManagementFacade"
import type {RecoverData} from "./LoginFacade"
import type {WorkerImpl} from "../WorkerImpl"
@ -53,10 +61,12 @@ export class UserManagementFacade {
private readonly rsa: RsaImplementation,
private readonly entityClient: EntityClient,
private readonly serviceExecutor: IServiceExecutor,
) {}
private readonly nonCachingEntityClient: EntityClient,
) {
}
async changeUserPassword(user: User, newPassword: string): Promise<void> {
const userGroupKey = await this.groupManagement.getGroupKeyAsAdmin(user.userGroup.group)
const userGroupKey = await this.groupManagement.getGroupKeyViaAdminEncGKey(user.userGroup.group)
const salt = generateRandomSalt()
const passwordKey = generateKeyFromPassphrase(newPassword, salt, KeyLength.b128)
const pwEncUserGroupKey = encryptKey(passwordKey, userGroupKey)
@ -358,4 +368,59 @@ export class UserManagementFacade {
})
.then(() => hexCode)
}
/** Get mailAddress to senderName mappings for mail group that the specified user is a member of. */
async getSenderNames(mailGroupId: Id, viaUser: Id): Promise<Map<string, string>> {
const mailboxProperties = await this.getMailboxProperties(mailGroupId, viaUser)
return this.collectSenderNames(mailboxProperties)
}
/** Set mailAddress to senderName mapping for mail group that the specified user is a member of. */
async setSenderName(mailGroupId: Id, viaUser: Id, mailAddress: string, senderName: string): Promise<Map<string, string>> {
const mailboxProperties = await this.getMailboxProperties(mailGroupId, viaUser)
let mailAddressProperty = mailboxProperties.mailAddressProperties.find((p) => p.mailAddress === mailAddress)
if (mailAddressProperty == null) {
mailAddressProperty = createMailAddressProperties({mailAddress})
mailboxProperties.mailAddressProperties.push(mailAddressProperty)
}
mailAddressProperty.senderName = senderName
const updatedProperties = await this.updateMailboxProperties(mailboxProperties, viaUser)
return this.collectSenderNames(updatedProperties)
}
private async getMailboxProperties(mailGroupId: Id, viaUser: Id): Promise<MailboxProperties> {
// Using non-caching entityClient because we are not a member of the user's mail group and we won't receive updates for it
const key = await this.groupManagement.getGroupKeyViaUser(mailGroupId, viaUser)
const mailboxGroupRoot = await this.nonCachingEntityClient.load(MailboxGroupRootTypeRef, mailGroupId)
if (mailboxGroupRoot.mailboxProperties == null) {
return this.createMailboxProperties(mailboxGroupRoot, key)
}
return await this.nonCachingEntityClient.load(MailboxPropertiesTypeRef, mailboxGroupRoot.mailboxProperties, undefined, undefined, key)
}
private async createMailboxProperties(mailboxGroupRoot: MailboxGroupRoot, groupKey: Aes128Key): Promise<MailboxProperties> {
// Using non-caching entityClient because we are not a member of the user's mail group and we won't receive updates for it
const mailboxProperties = createMailboxProperties({
_ownerGroup: mailboxGroupRoot._ownerGroup,
reportMovedMails: "",
mailAddressProperties: [],
})
const id = await this.nonCachingEntityClient.setup(null, mailboxProperties, undefined, {ownerKey: groupKey})
return this.nonCachingEntityClient.load(MailboxPropertiesTypeRef, id, undefined, undefined, groupKey)
}
private async updateMailboxProperties(mailboxProperties: MailboxProperties, viaUser: Id): Promise<MailboxProperties> {
const key = await this.groupManagement.getGroupKeyViaUser(assertNotNull(mailboxProperties._ownerGroup), viaUser)
await this.nonCachingEntityClient.update(mailboxProperties, key)
return await this.nonCachingEntityClient.load(MailboxPropertiesTypeRef, mailboxProperties._id, undefined, undefined, key)
}
private async collectSenderNames(mailboxProperties: MailboxProperties): Promise<Map<string, string>> {
const result = new Map<string, string>()
for (const data of mailboxProperties.mailAddressProperties) {
result.set(data.mailAddress, data.senderName)
}
return result
}
}

View file

@ -1,5 +1,5 @@
import type {EntityRestInterface} from "./EntityRestClient"
import {EntityRestClient} from "./EntityRestClient"
import {EntityRestClient, EntityRestClientSetupOptions} from "./EntityRestClient"
import {resolveTypeReference} from "../../common/EntityFunctions"
import {OperationType} from "../../common/TutanotaConstants"
import {assertNotNull, difference, firstThrow, flat, groupBy, isSameTypeRef, lastThrow, TypeRef} from "@tutao/tutanota-utils"
@ -226,8 +226,8 @@ export class DefaultEntityRestCache implements EntityRestCache {
return this._loadMultiple(typeRef, listId, elementIds)
}
setup<T extends SomeEntity>(listId: Id | null, instance: T, extraHeaders?: Dict): Promise<Id> {
return this.entityRestClient.setup(listId, instance, extraHeaders)
setup<T extends SomeEntity>(listId: Id | null, instance: T, extraHeaders?: Dict, options?: EntityRestClientSetupOptions): Promise<Id> {
return this.entityRestClient.setup(listId, instance, extraHeaders, options)
}
setupMultiple<T extends SomeEntity>(listId: Id | null, instances: Array<T>): Promise<Array<Id>> {

View file

@ -26,6 +26,8 @@ export function typeRefToPath(typeRef: TypeRef<any>): string {
export interface EntityRestClientSetupOptions {
baseUrl?: string,
/** Use this key to encrypt session key instead of trying to resolve the owner key based on the ownerGroup. */
ownerKey?: Aes128Key,
}
/**
@ -34,8 +36,9 @@ export interface EntityRestClientSetupOptions {
export interface EntityRestInterface {
/**
* Reads a single element from the server (or cache). Entities are decrypted before they are returned.
* @param ownerKey Use this key to decrypt session key instead of trying to resolve the owner key based on the ownerGroup.
*/
load<T extends SomeEntity>(typeRef: TypeRef<T>, id: PropertyType<T, "_id">, queryParameters?: Dict, extraHeaders?: Dict): Promise<T>
load<T extends SomeEntity>(typeRef: TypeRef<T>, id: PropertyType<T, "_id">, queryParameters?: Dict, extraHeaders?: Dict, ownerKey?: Aes128Key): Promise<T>
/**
* Reads a range of elements from the server (or cache). Entities are decrypted before they are returned.
@ -59,8 +62,9 @@ export interface EntityRestInterface {
/**
* Modifies a single element on the server. Entities are encrypted before they are sent.
* @param ownerKey Use this key to decrypt session key instead of trying to resolve the owner key based on the ownerGroup.
*/
update<T extends SomeEntity>(instance: T): Promise<void>
update<T extends SomeEntity>(instance: T, ownerKey?: Aes128Key): Promise<void>
/**
* Deletes a single element on the server.
@ -70,7 +74,7 @@ export interface EntityRestInterface {
/**
* Must be called when entity events are received.
* @param batch The entity events that were received.
* @return Similar to the events in the data parementer, but reduced by the events which are obsolete.
* @return Similar to the events in the data parameter, but reduced by the events which are obsolete.
*/
entityEventsReceived(batch: QueuedBatch): Promise<Array<EntityUpdate>>
}
@ -107,6 +111,7 @@ export class EntityRestClient implements EntityRestInterface {
id: PropertyType<T, "_id">,
queryParameters?: Dict,
extraHeaders?: Dict,
ownerKey?: Aes128Key,
): Promise<T> {
const {listId, elementId} = expandId(id)
const {
@ -114,7 +119,7 @@ export class EntityRestClient implements EntityRestInterface {
queryParams,
headers,
typeModel
} = await this._validateAndPrepareRestRequest(typeRef, listId, elementId, queryParameters, extraHeaders)
} = await this._validateAndPrepareRestRequest(typeRef, listId, elementId, queryParameters, extraHeaders, ownerKey)
const json = await this._restClient.request(path, HttpMethod.GET, {
queryParams,
headers,
@ -122,7 +127,9 @@ export class EntityRestClient implements EntityRestInterface {
})
const entity = JSON.parse(json)
const migratedEntity = await this._crypto.applyMigrations(typeRef, entity)
const sessionKey = await this._crypto.resolveSessionKey(typeModel, migratedEntity)
const sessionKey = ownerKey ?
this._crypto.resolveSessionKeyWithOwnerKey(migratedEntity, ownerKey)
: await this._crypto.resolveSessionKey(typeModel, migratedEntity)
.catch(ofClass(SessionKeyNotFoundError, e => {
console.log("could not resolve session key", e)
return null // will result in _errors being set on the instance
@ -143,7 +150,7 @@ export class EntityRestClient implements EntityRestInterface {
headers,
typeModel,
queryParams
} = await this._validateAndPrepareRestRequest(typeRef, listId, null, rangeRequestParams, undefined)
} = await this._validateAndPrepareRestRequest(typeRef, listId, null, rangeRequestParams, undefined, undefined)
// This should never happen if type checking is not bypassed with any
if (typeModel.type !== Type.ListElement) throw new Error("only ListElement types are permitted")
const json = await this._restClient.request(path, HttpMethod.GET, {
@ -155,7 +162,7 @@ export class EntityRestClient implements EntityRestInterface {
}
async loadMultiple<T extends SomeEntity>(typeRef: TypeRef<T>, listId: Id | null, elementIds: Array<Id>): Promise<Array<T>> {
const {path, headers} = await this._validateAndPrepareRestRequest(typeRef, listId, null, undefined, undefined)
const {path, headers} = await this._validateAndPrepareRestRequest(typeRef, listId, null, undefined, undefined, undefined)
const idChunks = splitInChunks(LOAD_MULTIPLE_LIMIT, elementIds)
const loadedChunks = await promiseMap(idChunks, async idChunk => {
const queryParams = {
@ -208,7 +215,7 @@ export class EntityRestClient implements EntityRestInterface {
path,
headers,
queryParams
} = await this._validateAndPrepareRestRequest(typeRef, listId, null, undefined, extraHeaders)
} = await this._validateAndPrepareRestRequest(typeRef, listId, null, undefined, extraHeaders, options?.ownerKey)
if (typeModel.type === Type.ListElement) {
if (!listId) throw new Error("List id must be defined for LETs")
@ -216,7 +223,7 @@ export class EntityRestClient implements EntityRestInterface {
if (listId) throw new Error("List id must not be defined for ETs")
}
const sk = this._crypto.setNewOwnerEncSessionKey(typeModel, instance)
const sk = this._crypto.setNewOwnerEncSessionKey(typeModel, instance, options?.ownerKey)
const encryptedEntity = await this._instanceMapper.encryptAndMapToLiteral(typeModel, instance, sk)
const persistencePostReturn = await this._restClient.request(
@ -242,7 +249,7 @@ export class EntityRestClient implements EntityRestInterface {
const instanceChunks = splitInChunks(POST_MULTIPLE_LIMIT, instances)
const typeRef = instances[0]._type
const {typeModel, path, headers} = await this._validateAndPrepareRestRequest(typeRef, listId, null, undefined, undefined)
const {typeModel, path, headers} = await this._validateAndPrepareRestRequest(typeRef, listId, null, undefined, undefined, undefined)
if (typeModel.type === Type.ListElement) {
if (!listId) throw new Error("List id must be defined for LETs")
@ -300,7 +307,7 @@ export class EntityRestClient implements EntityRestInterface {
}
}
async update<T extends SomeEntity>(instance: T): Promise<void> {
async update<T extends SomeEntity>(instance: T, ownerKey?: Aes128Key): Promise<void> {
if (!instance._id) throw new Error("Id must be defined")
const {listId, elementId} = expandId(instance._id)
const {
@ -308,8 +315,10 @@ export class EntityRestClient implements EntityRestInterface {
queryParams,
headers,
typeModel
} = await this._validateAndPrepareRestRequest(instance._type, listId, elementId, undefined, undefined)
const sessionKey = await this._crypto.resolveSessionKey(typeModel, instance)
} = await this._validateAndPrepareRestRequest(instance._type, listId, elementId, undefined, undefined, ownerKey)
const sessionKey = (ownerKey)
? this._crypto.resolveSessionKeyWithOwnerKey(instance, ownerKey)
: await this._crypto.resolveSessionKey(typeModel, instance)
const encryptedEntity = await this._instanceMapper.encryptAndMapToLiteral(typeModel, instance, sessionKey)
await this._restClient.request(path, HttpMethod.PUT, {
queryParams,
@ -325,7 +334,7 @@ export class EntityRestClient implements EntityRestInterface {
path,
queryParams,
headers
} = await this._validateAndPrepareRestRequest(instance._type, listId, elementId, undefined, undefined)
} = await this._validateAndPrepareRestRequest(instance._type, listId, elementId, undefined, undefined, undefined)
await this._restClient.request(path, HttpMethod.DELETE, {
queryParams,
headers,
@ -338,6 +347,7 @@ export class EntityRestClient implements EntityRestInterface {
elementId: Id | null,
queryParams: Dict | undefined,
extraHeaders: Dict | undefined,
ownerKey: Aes128Key | undefined
): Promise<{
path: string
queryParams: Dict | undefined
@ -348,7 +358,7 @@ export class EntityRestClient implements EntityRestInterface {
_verifyType(typeModel)
if (!this.authDataProvider.isFullyLoggedIn() && typeModel.encrypted) {
if (ownerKey == undefined && !this.authDataProvider.isFullyLoggedIn() && typeModel.encrypted) {
// Short-circuit before we do an actual request which we can't decrypt
throw new LoginIncompleteError(`Trying to do a network request with encrypted entity but is not fully logged in yet, type: ${typeModel.name}`)
}

View file

@ -1290,7 +1290,7 @@ export async function createCalendarEventViewModel(
): Promise<CalendarEventViewModel> {
const model = await import("../../mail/editor/SendMailModel")
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetail)
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot)
return new CalendarEventViewModel(
logins.getUserController(),
calendarUpdateDistributor,

View file

@ -114,7 +114,7 @@ export async function replyToEventInvitation(
foundAttendee.status = decision
const calendar = await locator.calendarModel.loadOrCreateCalendarInfo(new NoopProgressMonitor()).then(findPrivateCalendar)
const mailboxDetails = await locator.mailModel.getMailboxDetailsForMail(previousMail)
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails)
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const {SendMailModel} = await import("../../mail/editor/SendMailModel")
const sendMailModel = new SendMailModel(
locator.mailFacade,

View file

@ -1028,7 +1028,7 @@ export async function newMailEditorFromTemplate(
senderMailAddress?: string,
initialChangedState?: boolean
): Promise<Dialog> {
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails)
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
return defaultSendMailModel(mailboxDetails, mailboxProperties)
.initWithTemplate(recipients, subject, bodyText, attachments, confidential, senderMailAddress, initialChangedState)
.then(model => createMailEditorDialog(model))
@ -1138,6 +1138,6 @@ async function getMailboxDetailsAndProperties(
mailboxDetails: MailboxDetail | null | undefined,
): Promise<{mailboxDetails: MailboxDetail, mailboxProperties: MailboxProperties}> {
mailboxDetails = mailboxDetails ?? await locator.mailModel.getUserMailboxDetails()
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails)
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
return {mailboxDetails, mailboxProperties}
}

View file

@ -2,7 +2,7 @@ import m from "mithril"
import stream from "mithril/stream"
import Stream from "mithril/stream"
import {containsEventOfType} from "../../api/common/utils/Utils"
import {groupBy, neverNull, noOp, ofClass, promiseMap, splitInChunks} from "@tutao/tutanota-utils"
import {assertNotNull, groupBy, neverNull, noOp, ofClass, promiseMap, splitInChunks} from "@tutao/tutanota-utils"
import type {Mail, MailBox, MailboxGroupRoot, MailboxProperties, MailFolder} from "../../api/entities/tutanota/TypeRefs.js"
import {
createMailboxProperties,
@ -45,8 +45,12 @@ export class MailModel {
mailboxDetails: Stream<MailboxDetail[]>
mailboxCounters: Stream<MailboxCounters>
private initialization: Promise<any> | null
/** A way to avoid race conditions in case we try to create mailbox properties from multiple places. */
private mailboxPropertiesPromise: Promise<MailboxProperties> | null = null
/**
* Map from MailboxGroupRoot id to MailboxProperties
* A way to avoid race conditions in case we try to create mailbox properties from multiple places.
*
*/
private mailboxPropertiesPromises: Map<Id, Promise<MailboxProperties>> = new Map()
constructor(
private readonly notifications: Notifications,
@ -145,13 +149,15 @@ export class MailModel {
return this.getMailboxDetails().then(mailboxDetails => neverNull(mailboxDetails.find(md => md.folders.find(f => f.mails === mailListId) != null)))
}
getMailboxDetailsForMailGroup(mailGroupId: Id): Promise<MailboxDetail> {
return this.getMailboxDetails().then(mailboxDetails => neverNull(mailboxDetails.find(md => mailGroupId === md.mailGroup._id)))
async getMailboxDetailsForMailGroup(mailGroupId: Id): Promise<MailboxDetail> {
const mailboxDetails = await this.getMailboxDetails()
return assertNotNull(mailboxDetails.find(md => mailGroupId === md.mailGroup._id), "No mailbox details for mail group")
}
getUserMailboxDetails(): Promise<MailboxDetail> {
let userMailGroupMembership = this.logins.getUserController().getUserMailGroupMembership()
return this.getMailboxDetails().then(mailboxDetails => neverNull(mailboxDetails.find(md => md.mailGroup._id === userMailGroupMembership.group)))
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))
}
getMailboxFolders(mail: Mail): Promise<MailFolder[]> {
@ -365,7 +371,7 @@ export class MailModel {
await this.mailFacade.unsubscribe(mail._id, recipient, headers)
}
async getMailboxProperties(mailboxDetails: MailboxDetail): Promise<MailboxProperties> {
async getMailboxProperties(mailboxGroupRoot: MailboxGroupRoot): Promise<MailboxProperties> {
// MailboxProperties is an encrypted instance that is created lazily. When we create it the reference is automatically written to the MailboxGroupRoot.
// Unfortunately we will only get updated new MailboxGroupRoot with the next EntityUpdate.
// To prevent parallel creation attempts we do two things:
@ -373,35 +379,35 @@ export class MailModel {
// - we set mailboxProperties reference manually (we could save the id elsewhere but it's easier this way)
// If we are already loading/creating, just return it to avoid races
if (this.mailboxPropertiesPromise) {
return this.mailboxPropertiesPromise
}
if (mailboxDetails.mailboxGroupRoot.mailboxProperties) {
this.mailboxPropertiesPromise = this.entityClient.load(MailboxPropertiesTypeRef, mailboxDetails.mailboxGroupRoot.mailboxProperties)
} else {
this.mailboxPropertiesPromise = this.saveReportMovedMails(mailboxDetails, null, ReportMovedMailsType.ALWAYS_ASK)
}
return this.mailboxPropertiesPromise.finally(() => this.mailboxPropertiesPromise = null)
const existingPromise = this.mailboxPropertiesPromises.get(mailboxGroupRoot._id)
if (existingPromise) {
return existingPromise
}
async saveReportMovedMails(mailboxDetails: MailboxDetail, props: MailboxProperties | null, reportMovedMails: ReportMovedMailsType): Promise<MailboxProperties> {
if (!props) {
props = createMailboxProperties({
_ownerGroup: mailboxDetails.mailGroup._id,
})
const promise: Promise<MailboxProperties> = this.loadOrCreateMailboxProperties(mailboxGroupRoot)
this.mailboxPropertiesPromises.set(
mailboxGroupRoot._id,
promise,
)
return promise.finally(() => this.mailboxPropertiesPromises.delete(mailboxGroupRoot._id))
}
props.reportMovedMails = reportMovedMails
await this.saveMailboxProperties(props)
mailboxDetails.mailboxGroupRoot.mailboxProperties = props._id
return props
private async loadOrCreateMailboxProperties(mailboxGroupRoot: MailboxGroupRoot): Promise<MailboxProperties> {
if (!mailboxGroupRoot.mailboxProperties) {
mailboxGroupRoot.mailboxProperties = await this.entityClient.setup(null, createMailboxProperties({
_ownerGroup: mailboxGroupRoot._ownerGroup,
}))
}
return this.entityClient.load(MailboxPropertiesTypeRef, mailboxGroupRoot.mailboxProperties)
}
private async saveMailboxProperties(props: MailboxProperties) {
if (props._id) {
await this.entityClient.update(props)
} else {
props._id = await this.entityClient.setup(null, props)
}
async saveReportMovedMails(
mailboxGroupRoot: MailboxGroupRoot,
reportMovedMails: ReportMovedMailsType,
): Promise<MailboxProperties> {
const mailboxProperties = await this.loadOrCreateMailboxProperties(mailboxGroupRoot)
mailboxProperties.reportMovedMails = reportMovedMails
await this.entityClient.update(mailboxProperties)
return mailboxProperties
}
}

View file

@ -31,7 +31,7 @@ export function openPressReleaseEditor(mailboxDetails: MailboxDetail): void {
}
async function send() {
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails)
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const body = pressRelease.bodyHtml()
const subject = pressRelease.subject()
let recipients

View file

@ -21,8 +21,7 @@ function confirmMailReportDialog(mailModel: MailModel, mailboxDetails: MailboxDe
async function updateSpamReportSetting(areMailsReported: boolean) {
if (shallRememberDecision) {
const reportMovedMails = areMailsReported ? ReportMovedMailsType.AUTOMATICALLY_ONLY_SPAM : ReportMovedMailsType.NEVER
const mailboxProperties = await mailModel.getMailboxProperties(mailboxDetails)
await mailModel.saveReportMovedMails(mailboxDetails, mailboxProperties, reportMovedMails)
await mailModel.saveReportMovedMails(mailboxDetails.mailboxGroupRoot, reportMovedMails)
}
resolve(areMailsReported)
@ -68,7 +67,7 @@ export async function reportMailsAutomatically(
return
}
const mailboxProperties = await mailModel.getMailboxProperties(mailboxDetails)
const mailboxProperties = await mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
let allowUndoing = true // decides if a snackbar is shown to prevent the server request
let isReportable = false

View file

@ -898,7 +898,7 @@ export class MailViewerViewModel {
const args = await this.createResponseMailArgsForForwarding([recipient], newReplyTos, false)
const mailboxDetails = await this.getMailboxDetails()
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails)
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const {defaultSendMailModel} = await import("../editor/SendMailModel")
// Make sure inline images are loaded
await this.loadAll({notify: false})

View file

@ -48,6 +48,7 @@ import {DropDownSelector} from "../gui/base/DropDownSelector.js"
import {ButtonSize} from "../gui/base/ButtonSize.js"
import {SettingsExpander} from "./SettingsExpander.js"
import {MailAddressTableModel} from "./mailaddress/MailAddressTableModel.js"
import {OwnMailAddressNameChanger} from "./mailaddress/OwnMailAddressNameChanger.js"
assertMainOrNode()
// Number of days for that we load rejected senders
@ -137,8 +138,8 @@ export class GlobalSettingsViewer implements UpdatableSettingsViewer {
locator.mailAddressFacade,
logins,
locator.eventController,
locator.mailModel,
(await locator.mailModel.getUserMailboxDetails()).mailGroup._id
logins.getUserController().userGroupInfo,
new OwnMailAddressNameChanger(locator.mailModel, locator.entityClient),
)
await showAddDomainWizard("", customerInfo, mailAddressTableModel)
this.updateDomains()
@ -594,9 +595,9 @@ export class GlobalSettingsViewer implements UpdatableSettingsViewer {
locator.mailAddressFacade,
logins,
locator.eventController,
locator.mailModel,
logins.getUserController().userGroupInfo,
// Assuming user mailbox for now
(await locator.mailModel.getUserMailboxDetails()).mailGroup._id,
new OwnMailAddressNameChanger(locator.mailModel, locator.entityClient),
)
showAddDomainWizard(domainDnsStatus.domain, customerInfo, mailAddressTableModel).then(() => {
domainDnsStatus.loadCurrentStatus().then(() => m.redraw())

View file

@ -1,7 +1,7 @@
import m, {Children} from "mithril"
import {assertMainOrNode, isApp} from "../api/common/Env"
import {lang} from "../misc/LanguageViewModel"
import type {MailboxProperties, OutOfOfficeNotification, TutanotaProperties} from "../api/entities/tutanota/TypeRefs.js"
import type {MailboxGroupRoot, MailboxProperties, OutOfOfficeNotification, TutanotaProperties} from "../api/entities/tutanota/TypeRefs.js"
import {MailboxPropertiesTypeRef, MailFolderTypeRef, OutOfOfficeNotificationTypeRef, TutanotaPropertiesTypeRef} from "../api/entities/tutanota/TypeRefs.js"
import {FeatureType, InboxRuleType, OperationType, ReportMovedMailsType} from "../api/common/TutanotaConstants"
import {capitalizeFirstLetter, defer, LazyLoaded, noOp, ofClass} from "@tutao/tutanota-utils"
@ -43,6 +43,7 @@ import {IconButton, IconButtonAttrs} from "../gui/base/IconButton.js"
import {ButtonSize} from "../gui/base/ButtonSize.js";
import {getReportMovedMailsType} from "../misc/MailboxPropertiesUtils.js"
import {MailAddressTableModel} from "./mailaddress/MailAddressTableModel.js"
import {OwnMailAddressNameChanger} from "./mailaddress/OwnMailAddressNameChanger.js"
assertMainOrNode()
@ -61,8 +62,7 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
_identifierListViewer: IdentifierListViewer
_outOfOfficeNotification: LazyLoaded<OutOfOfficeNotification | null>
_outOfOfficeStatus: Stream<string> // stores the status label, based on whether the notification is/ or will really be activated (checking start time/ end time)
// null until it's loaded
mailAddressTableModel: MailAddressTableModel | null = null
private mailAddressTableModel: MailAddressTableModel
private offlineStorageSettings = new OfflineStorageSettingsModel(
logins.getUserController(),
@ -83,29 +83,22 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
this._outOfOfficeStatus = stream(lang.get("deactivated_label"))
this._indexStateWatch = null
this._identifierListViewer = new IdentifierListViewer(logins.getUserController().user)
locator.mailModel.getUserMailboxDetails().then((mailboxDetails) => {
// we never dispose it because we live forever! 🧛
this.mailAddressTableModel = new MailAddressTableModel(
locator.entityClient,
locator.mailAddressFacade,
logins, locator.eventController,
locator.mailModel,
mailboxDetails.mailGroup._id,
logins.getUserController().userGroupInfo,
new OwnMailAddressNameChanger(locator.mailModel, locator.entityClient)
)
m.redraw()
})
this._updateInboxRules(logins.getUserController().props)
// if (logins.getUserController().isGlobalAdmin()) {
// updateNbrOfAliases(this._editAliasFormAttrs)
// }
this._mailboxProperties = new LazyLoaded(async () => {
// For now we assume user mailbox, in the future we should specify which mailbox we are configuring
const mailboxDetails = await this.getMailboxDetails()
return locator.mailModel.getMailboxProperties(mailboxDetails)
const mailboxGroupRoot = await this.getMailboxGroupRoot()
return locator.mailModel.getMailboxProperties(mailboxGroupRoot)
})
this._updateMailboxPropertiesSettings()
@ -117,9 +110,10 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
this.offlineStorageSettings.init().then(() => m.redraw())
}
private getMailboxDetails(): Promise<MailboxDetail> {
private async getMailboxGroupRoot(): Promise<MailboxGroupRoot> {
// For now we assume user mailbox, in the future we should specify which mailbox we are configuring
return locator.mailModel.getUserMailboxDetails()
const {mailboxGroupRoot} = await locator.mailModel.getUserMailboxDetails()
return mailboxGroupRoot
}
view(): Children {
@ -304,7 +298,7 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
m(DropDownSelector, reportMovedMailsAttrs),
m(TextField, outOfOfficeAttrs),
this.renderLocalDataSection(),
this.mailAddressTableModel ? m(MailAddressTable, {model: this.mailAddressTableModel}) : null,
m(MailAddressTable, {model: this.mailAddressTableModel}),
logins.isEnabled(FeatureType.InternalCommunication)
? null
: [
@ -474,8 +468,8 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
],
selectedValue: this._reportMovedMails,
selectionChangedHandler: async (reportMovedMails) => {
const mailboxDetails = await this.getMailboxDetails()
this._mailboxProperties.getAsync().then(props => locator.mailModel.saveReportMovedMails(mailboxDetails, props, reportMovedMails))
const mailboxGroupRoot = await this.getMailboxGroupRoot()
await locator.mailModel.saveReportMovedMails(mailboxGroupRoot, reportMovedMails)
},
dropdownWidth: 250,
}

View file

@ -5,7 +5,7 @@ import {formatDateWithMonth, formatStorageSize} from "../misc/Formatter"
import {lang} from "../misc/LanguageViewModel"
import type {Customer, GroupInfo, GroupMembership, User} from "../api/entities/sys/TypeRefs.js"
import {CustomerTypeRef, GroupInfoTypeRef, GroupTypeRef, UserTypeRef} from "../api/entities/sys/TypeRefs.js"
import {firstThrow, LazyLoaded, neverNull, ofClass, promiseMap, remove} from "@tutao/tutanota-utils"
import {asyncFind, firstThrow, LazyLoaded, neverNull, ofClass, promiseMap, remove} from "@tutao/tutanota-utils"
import {BookingItemFeatureType, GroupType, OperationType} from "../api/common/TutanotaConstants"
import {BadRequestError, NotAuthorizedError, PreconditionFailedError} from "../api/common/error/RestError"
import {logins} from "../api/main/LoginController"
@ -21,7 +21,6 @@ import {isUpdateForTypeRef} from "../api/main/EventController"
import {HtmlEditor as Editor, HtmlEditorMode} from "../gui/editor/HtmlEditor"
import {filterContactFormsForLocalAdmin} from "./contactform/ContactFormListView.js"
import {checkAndImportUserData, CSV_USER_FORMAT} from "./ImportUsersViewer"
import type {MailAddressTableAttrs} from "./mailaddress/MailAddressTable.js"
import {MailAddressTable} from "./mailaddress/MailAddressTable.js"
import {compareGroupInfos, getGroupInfoDisplayName} from "../api/common/utils/GroupUtils"
import {CUSTOM_MIN_ID, isSameId} from "../api/common/utils/EntityUtils"
@ -34,21 +33,25 @@ import {UpdatableSettingsDetailsViewer} from "./SettingsView"
import {showChangeOwnPasswordDialog, showChangeUserPasswordAsAdminDialog} from "./ChangePasswordDialogs.js";
import {IconButton, IconButtonAttrs} from "../gui/base/IconButton.js"
import {ButtonSize} from "../gui/base/ButtonSize.js";
import {MailAddressTableModel} from "./mailaddress/MailAddressTableModel.js"
import {progressIcon} from "../gui/base/Icon.js"
import {AnotherUserMailAddressNameChanger} from "./mailaddress/AnotherUserMailAddressNameChanger.js"
import {OwnMailAddressNameChanger} from "./mailaddress/OwnMailAddressNameChanger.js"
assertMainOrNode()
export class UserViewer implements UpdatableSettingsDetailsViewer {
private readonly user = new LazyLoaded(() => this.loadUser())
private readonly user: LazyLoaded<User> = new LazyLoaded(() => this.loadUser())
private readonly customer = new LazyLoaded(() => this.loadCustomer())
private readonly teamGroupInfos = new LazyLoaded(() => this.loadTeamGroupInfos())
private senderName: string
private groupsTableAttrs: TableAttrs | null = null
private contactFormsTableAttrs: TableAttrs | null = null
private readonly secondFactorsForm: EditSecondFactorsForm
private editAliasFormAttrs: MailAddressTableAttrs | null
private usedStorage: number | null = null
private administratedBy: Id | null = null
private availableTeamGroupInfos: Array<GroupInfo> = []
private mailAddressTableModel: MailAddressTableModel | null = null
constructor(
public userGroupInfo: GroupInfo,
@ -97,13 +100,26 @@ export class UserViewer implements UpdatableSettingsDetailsViewer {
}
})
this.editAliasFormAttrs = null
// FIXME
// this.editAliasFormAttrs = createEditAliasFormAttrs(this.userGroupInfo)
//
// if (logins.getUserController().isGlobalAdmin()) {
// updateNbrOfAliases(this.editAliasFormAttrs)
// }
this.user.getAsync().then(async (user) => {
const maybeMailShip = await asyncFind(user.memberships, async (ship) => {
return ship.groupType === GroupType.Mail && (await locator.entityClient.load(GroupTypeRef, ship.group)).user === user._id
})
if (maybeMailShip == null) {
console.error("User doesn't have a mailbox?", user._id)
return
}
// we never dispose it because we live forever! 🧛
this.mailAddressTableModel = new MailAddressTableModel(
locator.entityClient,
locator.mailAddressFacade,
logins,
locator.eventController,
this.userGroupInfo,
this.isItMe()
? new OwnMailAddressNameChanger(locator.mailModel, locator.entityClient)
: new AnotherUserMailAddressNameChanger(locator.userManagementFacade, maybeMailShip.group, user._id),
)
})
this.updateUsedStorageAndAdminFlag()
}
@ -175,7 +191,7 @@ export class UserViewer implements UpdatableSettingsDetailsViewer {
this.groupsTableAttrs ? m(Table, this.groupsTableAttrs) : null,
this.contactFormsTableAttrs ? m(".h4.mt-l.mb-s", lang.get("contactForms_label")) : null,
this.contactFormsTableAttrs ? m(Table, this.contactFormsTableAttrs) : null,
this.editAliasFormAttrs ? m(MailAddressTable, this.editAliasFormAttrs) : null,
this.mailAddressTableModel ? m(MailAddressTable, {model: this.mailAddressTableModel}) : progressIcon(),
])
}

View file

@ -0,0 +1,23 @@
import {AddressToName, MailAddressNameChanger} from "./MailAddressTableModel.js"
import {UserManagementFacade} from "../../api/worker/facades/UserManagementFacade.js"
/**
* A {@link MailAddressNameChanger} intended for admins to set names for aliases bound to user mailboxes.
* We can't normally update instances for the groups we are not member of so we do it via a service.
*/
export class AnotherUserMailAddressNameChanger implements MailAddressNameChanger {
constructor(
private readonly userManagementFacade: UserManagementFacade,
private readonly mailGroupId: Id,
private readonly userId: Id,
) {
}
getSenderNames(): Promise<AddressToName> {
return this.userManagementFacade.getSenderNames(this.mailGroupId, this.userId)
}
setSenderName(address: string, name: string): Promise<AddressToName> {
return this.userManagementFacade.setSenderName(this.mailGroupId, this.userId, address, name)
}
}

View file

@ -49,12 +49,14 @@ export class MailAddressTable implements Component<MailAddressTableAttrs> {
view(vnode: Vnode<MailAddressTableAttrs>): Children {
const a = vnode.attrs
const addAliasButtonAttrs: IconButtonAttrs = {
const addAliasButtonAttrs: IconButtonAttrs | null = a.model.userCanModifyAliases()
? {
title: "addEmailAlias_label",
click: () => this.onAddAlias(a),
icon: Icons.Add,
size: ButtonSize.Compact
}
: null
const aliasesTableAttrs: TableAttrs = {
columnHeading: ["emailAlias_label", "state_label"],
columnWidths: [ColumnWidth.Largest, ColumnWidth.Small],
@ -81,6 +83,9 @@ export class MailAddressTable implements Component<MailAddressTableAttrs> {
}
private renderAliasCount({model}: MailAddressTableAttrs) {
if (!model.userCanModifyAliases()) {
return null
}
const aliasCount = model.aliasCount()
return m(
".small",
@ -94,7 +99,7 @@ export class MailAddressTable implements Component<MailAddressTableAttrs> {
private onAddAlias(attrs: MailAddressTableAttrs) {
const {model} = attrs
switch (model.canAddAlias()) {
switch (model.checkTryingToAddAlias()) {
case "freeaccount":
showNotAvailableForFreeDialog(true)
break
@ -105,6 +110,7 @@ export class MailAddressTable implements Component<MailAddressTableAttrs> {
this.showAddAliasDialog(attrs)
break
case "loading":
case "notanadmin":
break
}
}
@ -190,7 +196,8 @@ export function getAliasLineAttrs(attrs: MailAddressTableAttrs): Array<TableLine
label: () => "Set name",
click: () => showSenderNameChangeDialog(attrs.model, alias)
},
(alias.enabled)
attrs.model.userCanModifyAliases()
? (alias.enabled)
? {
label: isTutanotaMailAddress(alias.address) ? "deactivate_action" : "delete_action",
click: () => {
@ -206,7 +213,8 @@ export function getAliasLineAttrs(attrs: MailAddressTableAttrs): Array<TableLine
switchAliasStatus(alias, attrs)
}
},
},
}
: null,
],
},

View file

@ -1,14 +1,12 @@
import {EntityClient} from "../../api/common/EntityClient.js"
import {createMailAddressProperties, MailboxProperties, MailboxPropertiesTypeRef} from "../../api/entities/tutanota/TypeRefs.js"
import {MailboxPropertiesTypeRef} from "../../api/entities/tutanota/TypeRefs.js"
import {MailAddressFacade} from "../../api/worker/facades/MailAddressFacade.js"
import {LoginController} from "../../api/main/LoginController.js"
import stream from "mithril/stream"
import {EntityUpdateData, EventController, isUpdateForTypeRef} from "../../api/main/EventController.js"
import {EntityUpdateData, EventController, isUpdateFor, isUpdateForTypeRef} from "../../api/main/EventController.js"
import {OperationType} from "../../api/common/TutanotaConstants.js"
import {getSenderName} from "../../misc/MailboxPropertiesUtils.js"
import {getAvailableDomains} from "./MailAddressesUtils.js"
import {MailModel} from "../../mail/model/MailModel.js"
import {CustomerInfoTypeRef} from "../../api/entities/sys/TypeRefs.js"
import {CustomerInfoTypeRef, GroupInfo, GroupInfoTypeRef} from "../../api/entities/sys/TypeRefs.js"
export interface AliasCount {
availableToCreate: number
@ -21,40 +19,58 @@ export interface AddressInfo {
enabled: boolean
}
export type AddressToName = Map<string, string>
/** A strategy to change mail address to sender name mapping. */
export interface MailAddressNameChanger {
getSenderNames(): Promise<AddressToName>
setSenderName(address: string, name: string): Promise<AddressToName>
}
/** Model for showing the list of mail addresses and optionally adding more, enabling/disabling/setting names for them. */
export class MailAddressTableModel {
readonly redraw = stream<void>()
private mailboxProperties: MailboxProperties | null = null
private _aliasCount: AliasCount | null = null
private nameMappings: AddressToName | null = null
constructor(
private readonly entityClient: EntityClient,
private readonly mailAddressFacade: MailAddressFacade,
private readonly logins: LoginController,
private readonly eventController: EventController,
private readonly mailModel: MailModel,
private readonly mailGroupId: Id,
private userGroupInfo: GroupInfo,
private readonly nameChanger: MailAddressNameChanger,
) {
eventController.addEntityListener(this.entityEventsReceived)
}
async init() {
await this.updateAliasCount()
await this.loadMailboxProperties()
await this.loadNames()
this.redraw()
}
dispose() {
this.eventController.removeEntityListener(this.entityEventsReceived)
this.redraw.end(true)
}
userCanModifyAliases(): boolean {
return this.logins.getUserController().isGlobalAdmin()
}
addresses(): AddressInfo[] {
const {mailboxProperties} = this
if (mailboxProperties == null) {
const {nameMappings} = this
if (nameMappings == null) {
return []
}
return this.logins.getUserController().userGroupInfo.mailAddressAliases
return this.userGroupInfo.mailAddressAliases
.slice()
.sort((a, b) => (a.mailAddress > b.mailAddress ? 1 : -1))
.map(({mailAddress, enabled}) => {
return {
name: getSenderName(mailboxProperties, mailAddress) ?? "",
name: nameMappings.get(mailAddress) ?? "",
address: mailAddress,
enabled: enabled,
}
@ -66,14 +82,7 @@ export class MailAddressTableModel {
}
async setAliasName(address: string, senderName: string) {
if (this.mailboxProperties == null) return
let aliasConfig = this.mailboxProperties.mailAddressProperties.find((p) => p.mailAddress === address)
if (aliasConfig == null) {
aliasConfig = createMailAddressProperties({mailAddress: address})
this.mailboxProperties.mailAddressProperties.push(aliasConfig)
}
aliasConfig.senderName = senderName
await this.entityClient.update(this.mailboxProperties)
this.nameMappings = await this.nameChanger.setSenderName(address, senderName)
this.redraw()
}
@ -89,7 +98,7 @@ export class MailAddressTableModel {
}
async addAlias(alias: string): Promise<void> {
await this.mailAddressFacade.addMailAlias(this.logins.getUserController().userGroupInfo.group, alias)
await this.mailAddressFacade.addMailAlias(this.userGroupInfo.group, alias)
await this.updateAliasCount()
}
@ -99,13 +108,15 @@ export class MailAddressTableModel {
async setAliasStatus(address: string, restore: boolean): Promise<void> {
await this.mailAddressFacade
.setMailAliasStatus(this.logins.getUserController().userGroupInfo.group, address, restore)
.setMailAliasStatus(this.userGroupInfo.group, address, restore)
this.redraw()
await this.updateAliasCount()
}
canAddAlias(): "loading" | "freeaccount" | "limitreached" | "ok" {
if (this._aliasCount == null) {
checkTryingToAddAlias(): "loading" | "freeaccount" | "limitreached" | "notanadmin" | "ok" {
if (!this.logins.getUserController().isGlobalAdmin()) {
return "notanadmin"
} else if (this._aliasCount == null) {
return "loading"
} else if (this._aliasCount.availableToCreate === 0) {
if (this.logins.getUserController().isFreeAccount()) {
@ -119,24 +130,19 @@ export class MailAddressTableModel {
}
private entityEventsReceived = async (updates: ReadonlyArray<EntityUpdateData>) => {
for (const update of updates) {
if (isUpdateForTypeRef(MailboxPropertiesTypeRef, update) && update.operation === OperationType.UPDATE) {
await this.loadMailboxProperties()
await this.loadNames()
} else if (isUpdateForTypeRef(CustomerInfoTypeRef, update)) {
await this.updateAliasCount()
} else if (isUpdateFor(this.userGroupInfo, update) && update.operation === OperationType.UPDATE) {
this.userGroupInfo = await this.entityClient.load(GroupInfoTypeRef, this.userGroupInfo._id)
}
}
this.redraw()
}
private async loadMailboxProperties() {
const mailboxDetails = await this.mailModel.getMailboxDetailsForMailGroup(this.mailGroupId)
this.mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails)
}
dispose() {
this.eventController.removeEntityListener(this.entityEventsReceived)
this.redraw.end(true)
private async loadNames() {
this.nameMappings = await this.nameChanger.getSenderNames()
}
}

View file

@ -0,0 +1,44 @@
import {AddressToName, MailAddressNameChanger} from "./MailAddressTableModel.js"
import {MailModel} from "../../mail/model/MailModel.js"
import {createMailAddressProperties, MailboxProperties} from "../../api/entities/tutanota/TypeRefs.js"
import {EntityClient} from "../../api/common/EntityClient.js"
export class OwnMailAddressNameChanger implements MailAddressNameChanger {
constructor(
private readonly mailModel: MailModel,
private readonly entityClient: EntityClient,
) {
}
async getSenderNames(): Promise<AddressToName> {
const mailboxProperties = await this.getMailboxProperties()
return this.collectMap(mailboxProperties)
}
async setSenderName(address: string, name: string): Promise<AddressToName> {
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
const mailboxProperties =
await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
let aliasConfig = mailboxProperties.mailAddressProperties.find((p) => p.mailAddress === address)
if (aliasConfig == null) {
aliasConfig = createMailAddressProperties({mailAddress: address})
mailboxProperties.mailAddressProperties.push(aliasConfig)
}
aliasConfig.senderName = name
await this.entityClient.update(mailboxProperties)
return this.collectMap(mailboxProperties)
}
private collectMap(mailboxProperties: MailboxProperties) {
const result = new Map()
for (const properties of mailboxProperties.mailAddressProperties) {
result.set(properties.mailAddress, properties.senderName)
}
return result
}
private async getMailboxProperties(): Promise<MailboxProperties> {
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
return await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
}
}

View file

@ -81,7 +81,7 @@ async function _sendNotificationEmail(recipients: Recipients, subject: string, b
usePlaceholderForInlineImages: false,
}).html
const mailboxDetails = await locator.mailModel.getUserMailboxDetails()
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails)
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const sender = getEnabledMailAddressesWithUser(mailboxDetails, logins.getUserController().userGroupInfo).includes(senderMailAddress)
? senderMailAddress
: getDefaultSender(logins, mailboxDetails)

View file

@ -96,6 +96,7 @@ import "./misc/NewsModelTest.js"
import "./file/FileControllerTest.js"
import "./api/worker/rest/CustomCacheHandlerTest.js"
import "./misc/RecipientsModelTest.js"
import "./api/worker/facades/UserManagementFacadeTest.js"
import * as td from "testdouble"
import {random} from "@tutao/tutanota-crypto"
import {Mode} from "../../src/api/common/Env.js"

View file

@ -0,0 +1,121 @@
import o from "ospec"
import {UserManagementFacade} from "../../../../../src/api/worker/facades/UserManagementFacade.js"
import {WorkerImpl} from "../../../../../src/api/worker/WorkerImpl.js"
import {UserFacade} from "../../../../../src/api/worker/facades/UserFacade.js"
import {GroupManagementFacade} from "../../../../../src/api/worker/facades/GroupManagementFacade.js"
import {CounterFacade} from "../../../../../src/api/worker/facades/CounterFacade.js"
import {RsaImplementation} from "../../../../../src/api/worker/crypto/RsaImplementation.js"
import {EntityClient} from "../../../../../src/api/common/EntityClient.js"
import {ServiceExecutor} from "../../../../../src/api/worker/rest/ServiceExecutor.js"
import {matchers, object, when} from "testdouble"
import {EntityRestClientMock} from "../rest/EntityRestClientMock.js"
import {
createMailAddressProperties,
createMailboxGroupRoot,
createMailboxProperties,
MailboxGroupRootTypeRef,
MailboxPropertiesTypeRef
} from "../../../../../src/api/entities/tutanota/TypeRefs.js"
import {mapToObject} from "@tutao/tutanota-test-utils"
o.spec("UserManagementFacadeTest", function () {
let worker: WorkerImpl
let userFacade: UserFacade
let groupManagementFacade: GroupManagementFacade
let countersFacade: CounterFacade
let rsa: RsaImplementation
let entityClient: EntityClient
let serviceExecutor: ServiceExecutor
let nonCachingEntityClient: EntityClient
let restClientMock = new EntityRestClientMock()
let facade: UserManagementFacade
o.beforeEach(function () {
worker = object()
userFacade = object()
groupManagementFacade = object()
countersFacade = object()
rsa = object()
entityClient = object()
serviceExecutor = object()
nonCachingEntityClient = object()
facade = new UserManagementFacade(
worker,
userFacade,
groupManagementFacade,
countersFacade,
rsa,
entityClient,
serviceExecutor,
nonCachingEntityClient
)
})
o.spec("getSenderNames", function () {
o("when there is existing MailboxProperties it returns the names", async function () {
const mailGroupId = "mailGroupId"
const viaUser = "viaUser"
const mailboxPropertiesId = "mailboxProeprtiesId"
const mailboxGroupRoot = createMailboxGroupRoot({
_ownerGroup: mailGroupId,
mailboxProperties: mailboxPropertiesId,
})
const mailGroupKey = [1, 2, 3]
const mailboxProperties = createMailboxProperties({
mailAddressProperties: [
createMailAddressProperties({
mailAddress: "a@a.com",
senderName: "a",
}),
createMailAddressProperties({
mailAddress: "b@b.com",
senderName: "b",
})
]
})
when(groupManagementFacade.getGroupKeyViaUser(mailGroupId, viaUser)).thenResolve(mailGroupKey)
when(nonCachingEntityClient.load(MailboxGroupRootTypeRef, mailGroupId)).thenResolve(mailboxGroupRoot)
when(nonCachingEntityClient.load(MailboxPropertiesTypeRef, mailboxPropertiesId, undefined, undefined, mailGroupKey)).thenResolve(mailboxProperties)
const result = await facade.getSenderNames(mailGroupId, viaUser)
o(mapToObject(result)).deepEquals({
"a@a.com": "a",
"b@b.com": "b",
})
})
o("when there's no existing MailboxProperties it creates and returns one", async function () {
const mailGroupId = "mailGroupId"
const viaUser = "viaUser"
const mailboxPropertiesId = "mailboxProeprtiesId"
const mailboxGroupRoot = createMailboxGroupRoot({
_ownerGroup: mailGroupId,
mailboxProperties: null,
})
const mailGroupKey = [1, 2, 3]
const mailboxProperties = createMailboxProperties({
_id: mailboxPropertiesId,
reportMovedMails: "",
mailAddressProperties: []
})
const expectedCreatedProperties = createMailboxProperties({
_ownerGroup: mailGroupId,
reportMovedMails: "",
mailAddressProperties: [],
})
when(groupManagementFacade.getGroupKeyViaUser(mailGroupId, viaUser)).thenResolve(mailGroupKey)
when(nonCachingEntityClient.load(MailboxGroupRootTypeRef, mailGroupId)).thenResolve(mailboxGroupRoot)
when(nonCachingEntityClient.setup(null, matchers.anything(), undefined, {ownerKey: mailGroupKey})).thenResolve(mailboxPropertiesId)
when(nonCachingEntityClient.load(MailboxPropertiesTypeRef, mailboxPropertiesId, undefined, undefined, mailGroupKey)).thenResolve(mailboxProperties)
const result = await facade.getSenderNames(mailGroupId, viaUser)
o(mapToObject(result)).deepEquals({})
})
})
})

View file

@ -156,6 +156,31 @@ o.spec("EntityRestClient", async function () {
await assertThrows(LoginIncompleteError, () => entityRestClient.load(CalendarEventTypeRef, ["listId", "id"]))
assertThatNoRequestsWereMade()
})
o("when ownerKey is passed it is used instead for session key resolution", async function () {
const calendarListId = "calendarListId"
const id1 = "id1"
when(restClient.request(
`${typeRefToPath(CalendarEventTypeRef)}/${calendarListId}/${id1}`,
HttpMethod.GET,
{
headers: {...authHeader, v: String(tutanotaModelInfo.version)},
responseType: MediaType.Json,
queryParams: undefined,
}
)).thenResolve(JSON.stringify({instance: "calendar"}))
const ownerKey = [1, 2, 3]
const sessionKey = [3, 2, 1]
when(cryptoFacadeMock.resolveSessionKeyWithOwnerKey(anything(), ownerKey)).thenReturn(sessionKey)
const result = await entityRestClient.load(CalendarEventTypeRef, [calendarListId, id1], undefined, undefined, ownerKey)
const typeModel = await resolveTypeReference(CalendarEventTypeRef)
verify(instanceMapperMock.decryptAndMapToInstance(typeModel, anything(), sessionKey))
verify(cryptoFacadeMock.resolveSessionKey(anything(), anything()), {times: 0})
o(result as any).deepEquals({instance: "calendar", decrypted: true, migrated: true, migratedForInstance: true})
})
})
o.spec("Load Range", function () {
@ -376,6 +401,35 @@ o.spec("EntityRestClient", async function () {
await entityRestClient.setup("listId", createContact(), undefined, {baseUrl: "some url"})
verify(restClient.request(anything(), HttpMethod.POST, argThat(arg => arg.baseUrl === "some url")))
})
o("when ownerKey is passed it is used instead for session key resolution", async function () {
const typeModel = await resolveTypeReference(CustomerTypeRef)
const v = typeModel.version
const newCustomer = createCustomer()
const resultId = "id"
when(restClient.request(
`/rest/sys/customer`,
HttpMethod.POST,
{
baseUrl: undefined,
headers: {...authHeader, v},
queryParams: undefined,
responseType: MediaType.Json,
body: JSON.stringify({...newCustomer, encrypted: true}),
}
), {times: 1}).thenResolve(JSON.stringify({generatedId: resultId}))
const ownerKey = [1, 2, 3]
const sessionKey = [3, 2, 1]
when(cryptoFacadeMock.setNewOwnerEncSessionKey(typeModel, anything(), ownerKey)).thenReturn(sessionKey)
const result = await entityRestClient.setup(null, newCustomer, undefined, {ownerKey})
verify(instanceMapperMock.encryptAndMapToLiteral(anything(), anything(), sessionKey))
verify(cryptoFacadeMock.resolveSessionKey(anything(), anything()), {times: 0})
o(result).equals(resultId)
})
})
o.spec("Setup multiple", async function () {
@ -644,6 +698,31 @@ o.spec("EntityRestClient", async function () {
const result = await assertThrows(Error, async () => await entityRestClient.update(newCustomer))
o(result.message).equals("Id must be defined")
})
o("when ownerKey is passed it is used instead for session key resolution", async function () {
const typeModel = await resolveTypeReference(CustomerTypeRef)
const version = typeModel.version
const newCustomer = createCustomer({
_id: "id",
})
when(restClient.request(
"/rest/sys/customer/id",
HttpMethod.PUT,
{
headers: {...authHeader, v: version},
body: JSON.stringify({...newCustomer, encrypted: true}),
}
))
const ownerKey = [1, 2, 3]
const sessionKey = [3, 2, 1]
when(cryptoFacadeMock.resolveSessionKeyWithOwnerKey(anything(), ownerKey)).thenReturn(sessionKey)
await entityRestClient.update(newCustomer, ownerKey)
verify(instanceMapperMock.encryptAndMapToLiteral(anything(), anything(), sessionKey))
verify(cryptoFacadeMock.resolveSessionKey(anything(), anything()), {times: 0})
})
})
o.spec("Delete", function () {

View file

@ -16,7 +16,7 @@ import {
createContact,
createContactMailAddress,
createEncryptedMailAddress,
createMail,
createMail, createMailboxProperties,
EncryptedMailAddress
} from "../../../src/api/entities/tutanota/TypeRefs.js"
import {AccountType, AlarmInterval, assertEnumValue, CalendarAttendeeStatus, ShareCapability,} from "../../../src/api/common/TutanotaConstants.js"
@ -142,6 +142,8 @@ o.spec("CalendarEventViewModel", function () {
userController.user
))
const mailboxProperties = createMailboxProperties()
inviteModel = new SendMailModel(
mailFacadeMock,
entityClient,
@ -152,6 +154,7 @@ o.spec("CalendarEventViewModel", function () {
mailboxDetail,
recipientsModel,
new NoZoneDateProvider(),
mailboxProperties
)
updateModel = new SendMailModel(
mailFacadeMock,
@ -163,6 +166,7 @@ o.spec("CalendarEventViewModel", function () {
mailboxDetail,
recipientsModel,
new NoZoneDateProvider(),
mailboxProperties,
)
cancelModel = new SendMailModel(
mailFacadeMock,
@ -174,6 +178,7 @@ o.spec("CalendarEventViewModel", function () {
mailboxDetail,
recipientsModel,
new NoZoneDateProvider(),
mailboxProperties,
)
responseModel = new SendMailModel(
mailFacadeMock,
@ -185,6 +190,7 @@ o.spec("CalendarEventViewModel", function () {
mailboxDetail,
recipientsModel,
new NoZoneDateProvider(),
mailboxProperties,
)
const sendFactory = (_, purpose) => {
@ -202,6 +208,7 @@ o.spec("CalendarEventViewModel", function () {
calendarModel,
entityClient,
mailboxDetail,
mailboxProperties,
sendFactory,
now,
zone,

View file

@ -8,17 +8,15 @@ import {createDnsRecord} from "../../../../src/api/entities/sys/TypeRefs.js";
import {DomainDnsStatus} from "../../../../src/settings/DomainDnsStatus.js";
import {AddDomainData} from "../../../../src/settings/emaildomain/AddDomainWizard.js";
import {createGroupInfo} from "../../../../src/api/entities/sys/TypeRefs.js";
import {MailAddressTableModel} from "../../../../src/settings/mailaddress/MailAddressTableModel.js"
import {object} from "testdouble"
const data: AddDomainData = {
domain: stream("domain"),
customerInfo: createCustomerInfo(),
expectedVerificationRecord: createDnsRecord(),
editAliasFormAttrs: {
userGroupInfo: createGroupInfo(),
aliasCount: {
availableToCreate: 1,
availableToEnable: 1,
},
model: object<MailAddressTableModel>()
},
domainStatus: new DomainDnsStatus("domain"),
}

View file

@ -14,6 +14,8 @@ import nodemocker from "../nodemocker.js"
import {downcast} from "@tutao/tutanota-utils"
import {WorkerClient} from "../../../src/api/main/WorkerClient.js"
import {MailFacade} from "../../../src/api/worker/facades/MailFacade.js"
import {LoginController} from "../../../src/api/main/LoginController.js"
import {object} from "testdouble"
o.spec("MailModelTest", function () {
let notifications: Partial<Notifications>
@ -30,18 +32,21 @@ o.spec("MailModelTest", function () {
folders: [inboxFolder],
},
]
let logins: LoginController
o.beforeEach(function () {
notifications = {}
showSpy = notifications.showNotification = spy()
const restClient = new EntityRestClientMock()
const workerClient = nodemocker.mock<WorkerClient>("worker", {}).set()
const mailFacade = nodemocker.mock<MailFacade>("mailFacade", {}).set()
logins = object()
model = new MailModel(
downcast(notifications),
downcast({}),
workerClient,
mailFacade,
new EntityClient(restClient),
logins,
)
// not pretty, but works
model.mailboxDetails(mailboxDetails as MailboxDetail[])

View file

@ -14,7 +14,7 @@ import {
createMail,
createMailAddress,
createMailBox,
createMailboxGroupRoot,
createMailboxGroupRoot, createMailboxProperties,
createTutanotaProperties,
CustomerAccountCreateDataTypeRef,
MailTypeRef,
@ -178,6 +178,8 @@ o.spec("SendMailModel", function () {
)
})
const mailboxProperties = createMailboxProperties()
model = new SendMailModel(
mailFacade,
entity,
@ -187,7 +189,8 @@ o.spec("SendMailModel", function () {
eventController,
mailboxDetails,
recipientsModel,
new NoZoneDateProvider()
new NoZoneDateProvider(),
mailboxProperties,
)
replace(model, "getDefaultSender", () => DEFAULT_SENDER_FOR_TESTING)