mirror of
https://github.com/tutao/tutanota.git
synced 2025-10-19 16:03:43 +00:00
Create MailboxProperties for other users as an admin, #516
This commit is contained in:
parent
caf64a3f50
commit
3cc58ae1c1
31 changed files with 645 additions and 228 deletions
|
@ -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> {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>> {
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
44
src/settings/mailaddress/OwnMailAddressNameChanger.ts
Normal file
44
src/settings/mailaddress/OwnMailAddressNameChanger.ts
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
121
test/tests/api/worker/facades/UserManagementFacadeTest.ts
Normal file
121
test/tests/api/worker/facades/UserManagementFacadeTest.ts
Normal 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({})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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 () {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
|
|
|
@ -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[])
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue