feat: recipient search
Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Recipient service
|
||||
*/
|
||||
|
||||
import { entityService } from '@PeopleManager/services'
|
||||
import { GroupObject, IndividualObject, OrganizationObject, type EntityObject } from '@PeopleManager/models'
|
||||
import type { MessageAddressInterface } from '@MailManager/types/message'
|
||||
|
||||
/** Minimum query length before a lookup is worthwhile. */
|
||||
const MIN_QUERY_LENGTH = 3
|
||||
|
||||
/** Default cap on the number of matches returned. */
|
||||
const DEFAULT_LIMIT = 10
|
||||
|
||||
/**
|
||||
* Derive a human-readable display name from a contact's properties.
|
||||
*/
|
||||
function displayName(properties: IndividualObject | OrganizationObject | GroupObject): string {
|
||||
if (properties.label) {
|
||||
return properties.label
|
||||
}
|
||||
|
||||
if (properties instanceof IndividualObject) {
|
||||
const parts = [properties.names?.given, properties.names?.family].filter(Boolean)
|
||||
if (parts.length > 0) {
|
||||
return parts.join(' ')
|
||||
}
|
||||
} else if (properties instanceof OrganizationObject) {
|
||||
if (properties.names?.full) {
|
||||
return properties.names.full
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a contact entity into one recipient per email address.
|
||||
* Groups (no addressable email) are skipped.
|
||||
*/
|
||||
function toRecipients(entity: EntityObject): MessageAddressInterface[] {
|
||||
const properties = entity.properties
|
||||
|
||||
if (properties instanceof GroupObject) {
|
||||
return []
|
||||
}
|
||||
|
||||
const emails = properties.emails
|
||||
if (!emails) {
|
||||
return []
|
||||
}
|
||||
|
||||
const label = displayName(properties)
|
||||
const recipients: MessageAddressInterface[] = []
|
||||
|
||||
for (const email of Object.values(emails)) {
|
||||
const address = email?.address?.trim()
|
||||
if (!address) {
|
||||
continue
|
||||
}
|
||||
recipients.push({ address, label: label || address })
|
||||
}
|
||||
|
||||
return recipients
|
||||
}
|
||||
|
||||
export const recipientService = {
|
||||
/**
|
||||
* Search the user's contacts for recipients matching a query.
|
||||
*
|
||||
* @param query - free-text fragment matched against contact names
|
||||
* @param limit - maximum number of recipients to return
|
||||
*
|
||||
* @returns De-duplicated recipients (one per email address)
|
||||
*/
|
||||
async search(query: string, limit: number = DEFAULT_LIMIT): Promise<MessageAddressInterface[]> {
|
||||
const trimmed = query.trim()
|
||||
if (trimmed.length < MIN_QUERY_LENGTH) {
|
||||
return []
|
||||
}
|
||||
|
||||
const recipients: MessageAddressInterface[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
try {
|
||||
// The "*" attribute defaults to LIKE, so a scalar string is all that is
|
||||
// needed; range caps the scan to the first `limit` matches.
|
||||
await entityService.listStream(
|
||||
{ filter: { '*': trimmed }, range: { type: 'tally', anchor: 'absolute', position: 0, tally: limit } },
|
||||
(entity: EntityObject) => {
|
||||
for (const recipient of toRecipients(entity)) {
|
||||
const key = recipient.address.toLowerCase()
|
||||
if (seen.has(key)) {
|
||||
continue
|
||||
}
|
||||
seen.add(key)
|
||||
recipients.push(recipient)
|
||||
}
|
||||
},
|
||||
)
|
||||
} catch (error: any) {
|
||||
console.error('[Mail][Recipient Service] - Search failed:', error)
|
||||
return []
|
||||
}
|
||||
|
||||
return recipients.slice(0, limit)
|
||||
},
|
||||
}
|
||||
|
||||
export default recipientService
|
||||
Reference in New Issue
Block a user