mirror of
https://github.com/tutao/tutanota.git
synced 2025-10-19 16:03:43 +00:00
217 lines
7 KiB
TypeScript
217 lines
7 KiB
TypeScript
import type { ContactModel } from "../../contactsFunctionality/ContactModel.js"
|
|
import type { LoginController } from "./LoginController.js"
|
|
import type { MailFacade } from "../worker/facades/lazy/MailFacade.js"
|
|
import type { EntityClient } from "../common/EntityClient.js"
|
|
import { getContactDisplayName } from "../../contactsFunctionality/ContactUtils.js"
|
|
import { PartialRecipient, Recipient, RecipientType } from "../common/recipients/Recipient.js"
|
|
import { BoundedExecutor, LazyLoaded } from "@tutao/tutanota-utils"
|
|
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,
|
|
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,
|
|
) {}
|
|
|
|
/**
|
|
* 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,
|
|
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())
|
|
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 === "")) {
|
|
this._name = getContactDisplayName(contact)
|
|
}
|
|
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) {
|
|
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
|
|
}
|
|
}
|
|
}
|