tutanota/src/common/api/main/RecipientsModel.ts

218 lines
7 KiB
TypeScript
Raw Normal View History

import type { ContactModel } from "../../contactsFunctionality/ContactModel.js"
2022-12-27 15:37:40 +01:00
import type { LoginController } from "./LoginController.js"
import type { MailFacade } from "../worker/facades/lazy/MailFacade.js"
2022-12-27 15:37:40 +01:00
import type { EntityClient } from "../common/EntityClient.js"
import { getContactDisplayName } from "../../contactsFunctionality/ContactUtils.js"
2022-12-27 15:37:40 +01:00
import { PartialRecipient, Recipient, RecipientType } from "../common/recipients/Recipient.js"
import { BoundedExecutor, LazyLoaded } from "@tutao/tutanota-utils"
2022-12-27 15:37:40 +01:00
import { Contact, ContactTypeRef } from "../entities/tutanota/TypeRefs"
import { cleanMailAddress } from "../common/utils/CommonCalendarUtils.js"
import { createNewContact, isTutaMailAddress } from "../../mailFunctionality/SharedMailUtils.js"
/**
* A recipient that can be resolved to obtain contact and recipient type
* It is defined as an interface, because it should only be created using RecipientsModel.resolve
* rather than directly constructing one
*/
export interface ResolvableRecipient extends Recipient {
/** get the resolved value of the recipient, when it's ready */
resolved(): Promise<Recipient>
/** check if resolution is complete */
isResolved(): boolean
/** provide a handler to run when resolution is done, handy for chaining */
whenResolved(onResolved: (resolvedRecipient: Recipient) => void): this
/** update the contact. will override whatever contact gets resolved */
setContact(contact: Contact): void
/** update the name. will override whatever the name has resolved to */
setName(name: string): void
}
export enum ResolveMode {
Lazy,
2022-12-27 15:37:40 +01:00
Eager,
}
export class RecipientsModel {
private executor = new BoundedExecutor(5)
constructor(
private readonly contactModel: ContactModel,
private readonly loginController: LoginController,
private readonly mailFacade: MailFacade,
private readonly entityClient: EntityClient,
2022-12-27 15:37:40 +01:00
) {}
/**
* Start resolving a recipient
* If resolveLazily === true, Then resolution will not be initiated (i.e. no server calls will be made) until the first call to `resolved`
*/
resolve(recipient: PartialRecipient, resolveMode: ResolveMode): ResolvableRecipient {
return new ResolvableRecipientImpl(
recipient,
this.contactModel,
this.loginController,
(mailAddress) => this.executor.run(this.resolveRecipientType(mailAddress)),
this.entityClient,
resolveMode,
)
}
private readonly resolveRecipientType = (mailAddress: string) => async () => {
const keyData = await this.mailFacade.getRecipientKeyData(mailAddress)
return keyData == null ? RecipientType.EXTERNAL : RecipientType.INTERNAL
}
}
class ResolvableRecipientImpl implements ResolvableRecipient {
private _address: string
private _name: string | null
private readonly lazyType: LazyLoaded<RecipientType>
private readonly lazyContact: LazyLoaded<Contact | null>
private readonly initialType: RecipientType = RecipientType.UNKNOWN
private readonly initialContact: Contact | null = null
private overrideContact: Contact | null = null
get address(): string {
return this._address
}
get name(): string {
return this._name ?? ""
}
get type(): RecipientType {
return this.lazyType.getSync() ?? this.initialType
}
get contact(): Contact | null {
return this.lazyContact.getSync() ?? this.initialContact
}
constructor(
arg: PartialRecipient,
private readonly contactModel: ContactModel,
private readonly loginController: LoginController,
private readonly typeResolver: (mailAddress: string) => Promise<RecipientType>,
private readonly entityClient: EntityClient,
2022-12-27 15:37:40 +01:00
resolveMode: ResolveMode,
) {
if (isTutaMailAddress(arg.address) || arg.type === RecipientType.INTERNAL) {
this.initialType = RecipientType.INTERNAL
this._address = cleanMailAddress(arg.address)
} else if (arg.type) {
this.initialType = arg.type
this._address = arg.address
} else {
this._address = arg.address
}
this._name = arg.name ?? null
if (!(arg.contact instanceof Array)) {
this.initialContact = arg.contact ?? null
}
this.lazyType = new LazyLoaded(() => this.resolveType())
2022-12-27 15:37:40 +01:00
this.lazyContact = new LazyLoaded(async () => {
const contact = await this.resolveContact(arg.contact)
// sometimes we create resolvable contact and then dissect it into parts and resolve it again in which case we will default to an empty name
// (see the getter) but we actually want the name from contact.
if (contact != null && (this._name == null || this._name === "")) {
2022-12-27 15:37:40 +01:00
this._name = getContactDisplayName(contact)
}
2022-12-27 15:37:40 +01:00
return contact
})
if (resolveMode === ResolveMode.Eager) {
this.lazyType.load()
this.lazyContact.load()
}
}
setName(newName: string) {
this._name = newName
}
setContact(newContact: Contact) {
this.overrideContact = newContact
this.lazyContact.reload()
}
async resolved(): Promise<Recipient> {
await Promise.all([this.lazyType.getAsync(), this.lazyContact.getAsync()])
return {
address: this.address,
name: this.name,
type: this.type,
contact: this.contact,
}
}
isResolved(): boolean {
// We are only resolved when both type and contact are non-null and finished
return this.lazyType.isLoaded() && this.lazyContact.isLoaded()
}
whenResolved(handler: (resolvedRecipient: Recipient) => void): this {
this.resolved().then(handler)
return this
}
/**
* Determine whether recipient is INTERNAL or EXTERNAL based on the existence of key data (external recipients don't have any)
*/
private async resolveType(): Promise<RecipientType> {
if (this.initialType === RecipientType.UNKNOWN) {
const cleanedAddress = cleanMailAddress(this.address)
const recipientType = await this.typeResolver(cleanedAddress)
if (recipientType === RecipientType.INTERNAL) {
// we know this is one of ours, so it's safe to clean it up
this._address = cleanedAddress
}
return recipientType
} else {
return this.initialType
}
}
/**
* Resolve the recipients contact.
* If {@param contact} is an Id, the contact will be loaded directly
* Otherwise, the contact will be searched for in the ContactModel
*/
private async resolveContact(contact: Contact | IdTuple | None): Promise<Contact | null> {
try {
if (this.overrideContact) {
return this.overrideContact
} else if ((await this.contactModel.getContactListId()) == null) {
Rewrite calendar editor and calendar popup make updating calendar events delete the old uidIndex correctly when updating calendar events, we sometimes had events in the delete call that do not have the hashedUid set, causing us to be unable to delete the uid index entry. when re-creating the event, that leads to a db.exists error. make sure the event popup always has the current version of the event. now that edit operations are possible from the popup, we either need to close the popup after it calls the model factory to make sure it's only called once, or make sure the model factory always uses the last version of the event. we opted for the first option here to make sure repeated changes to the attendance are actually sent as a response. make contact resolution failure more controlled for external users previously, responding to an event from an external mailbox would try to resolve a contact for the response mail, fail, try to create a contact and fail again on an opaque assertNotNull. show partial editability banner when creating new event in shared calendar make it possible to add alarms to invites in private calendars don't make saving/sending invites and cancellations depend on user choice for own events updateExistingEvent() should call sendNotifications/saveEvent even when there are no update worthy changes or the user did not tick the sendUpdates checkbox because invites/cancellations must be sent in any case and sending updates has a separate check for sendUpdates also make the sendUpdates button on the popup use the same logic as the shortcut when clicked (ask confirmation) Co-authored-by: nig <nig@tutao.de>
2023-04-25 16:54:46 +02:00
console.log("can't resolve contacts for users with no contact list id")
return null
} else if (contact instanceof Array) {
return await this.entityClient.load(ContactTypeRef, contact)
} else if (contact == null) {
const foundContact = await this.contactModel.searchForContact(this.address)
if (foundContact) {
return foundContact
} else {
// we don't want to create a mixed-case contact if the address is an internal one.
// after lazyType is loaded, if it resolves to RecipientType.INTERNAL, we have the
// cleaned address in this.address.
await this.lazyType
return createNewContact(this.loginController.getUserController().user, this.address, this.name)
}
} else {
return contact
}
} catch (e) {
console.log("error resolving contact", e)
return null
}
}
}