Mailbox fixes (#8406)

Signed-off-by: Nikolay Chunosov <Chunosov.N@gmail.com>
This commit is contained in:
Chunosov 2025-04-01 11:41:24 +07:00 committed by GitHub
parent b104585c92
commit a226f9aa0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 243 additions and 63 deletions

View File

@ -12,8 +12,8 @@
"webpack": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) VERSION=$(node ../common/scripts/show_tag.js) NODE_ENV=development webpack --stats-error-details --progress -w", "webpack": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) VERSION=$(node ../common/scripts/show_tag.js) NODE_ENV=development webpack --stats-error-details --progress -w",
"devp": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) VERSION=$(node ../common/scripts/show_tag.js) NODE_ENV=production CLIENT_TYPE=dev webpack --progress -w", "devp": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) VERSION=$(node ../common/scripts/show_tag.js) NODE_ENV=production CLIENT_TYPE=dev webpack --progress -w",
"dev": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) VERSION=$(node ../common/scripts/show_tag.js) NODE_ENV=development webpack --progress -w", "dev": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) VERSION=$(node ../common/scripts/show_tag.js) NODE_ENV=development webpack --progress -w",
"start": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) VERSION=$(node ../common/scripts/show_tag.js) NODE_ENV=production electron .", "start": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) VERSION=$(node ../common/scripts/show_tag.js) NODE_ENV=production electron --no-sandbox .",
"start-dev": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) VERSION=$(node ../common/scripts/show_tag.js) NODE_ENV=development electron .", "start-dev": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) VERSION=$(node ../common/scripts/show_tag.js) NODE_ENV=development electron --no-sandbox .",
"format": "format", "format": "format",
"bump": "bump-package-version" "bump": "bump-package-version"
}, },

View File

@ -89,6 +89,7 @@ import '@hcengineering/lead-assets'
import '@hcengineering/login-assets' import '@hcengineering/login-assets'
import '@hcengineering/love-assets' import '@hcengineering/love-assets'
import '@hcengineering/notification-assets' import '@hcengineering/notification-assets'
import '@hcengineering/my-space-assets'
import '@hcengineering/preference-assets' import '@hcengineering/preference-assets'
import '@hcengineering/print-assets' import '@hcengineering/print-assets'
import '@hcengineering/process-assets' import '@hcengineering/process-assets'
@ -361,6 +362,7 @@ export async function configurePlatform (): Promise<void> {
addLocation(mySpaceId, () => import(/* webpackChunkName: "card" */ '@hcengineering/my-space-resources')) addLocation(mySpaceId, () => import(/* webpackChunkName: "card" */ '@hcengineering/my-space-resources'))
addLocation(chatId, () => import(/* webpackChunkName: "chat" */ '@hcengineering/chat-resources')) addLocation(chatId, () => import(/* webpackChunkName: "chat" */ '@hcengineering/chat-resources'))
addLocation(inboxId, () => import(/* webpackChunkName: "inbox" */ '@hcengineering/inbox-resources')) addLocation(inboxId, () => import(/* webpackChunkName: "inbox" */ '@hcengineering/inbox-resources'))
addLocation(mailId, () => import(/* webpackChunkName: "card" */ '@hcengineering/mail-resources'))
addLocation(processId, () => import(/* webpackChunkName: "process" */ '@hcengineering/process-resources')) addLocation(processId, () => import(/* webpackChunkName: "process" */ '@hcengineering/process-resources'))
setMetadata(client.metadata.FilterModel, 'ui') setMetadata(client.metadata.FilterModel, 'ui')

View File

@ -124,6 +124,7 @@ import '@hcengineering/view-assets'
import '@hcengineering/workbench-assets' import '@hcengineering/workbench-assets'
import '@hcengineering/chat-assets' import '@hcengineering/chat-assets'
import '@hcengineering/inbox-assets' import '@hcengineering/inbox-assets'
import '@hcengineering/mail-assets'
import '@hcengineering/github-assets' import '@hcengineering/github-assets'
import { coreId } from '@hcengineering/core' import { coreId } from '@hcengineering/core'

View File

@ -93,7 +93,7 @@ export interface AccountClient {
findPersonBySocialId: (socialId: PersonId, requireAccount?: boolean) => Promise<PersonUuid | undefined> findPersonBySocialId: (socialId: PersonId, requireAccount?: boolean) => Promise<PersonUuid | undefined>
findSocialIdBySocialKey: (socialKey: string) => Promise<PersonId | undefined> findSocialIdBySocialKey: (socialKey: string) => Promise<PersonId | undefined>
getMailboxOptions: () => Promise<MailboxOptions> getMailboxOptions: () => Promise<MailboxOptions>
createMailbox: (name: string, domain: string) => Promise<void> createMailbox: (name: string, domain: string) => Promise<{ mailbox: string, socialId: PersonId }>
getMailboxes: () => Promise<MailboxInfo[]> getMailboxes: () => Promise<MailboxInfo[]>
deleteMailbox: (mailbox: string) => Promise<void> deleteMailbox: (mailbox: string) => Promise<void>
@ -653,13 +653,13 @@ class AccountClientImpl implements AccountClient {
return await this.rpc(request) return await this.rpc(request)
} }
async createMailbox (name: string, domain: string): Promise<void> { async createMailbox (name: string, domain: string): Promise<{ mailbox: string, socialId: PersonId }> {
const request = { const request = {
method: 'createMailbox' as const, method: 'createMailbox' as const,
params: { name, domain } params: { name, domain }
} }
await this.rpc(request) return await this.rpc(request)
} }
async getMailboxes (): Promise<MailboxInfo[]> { async getMailboxes (): Promise<MailboxInfo[]> {

View File

@ -14,12 +14,14 @@
--> -->
<script lang="ts"> <script lang="ts">
import { MailboxOptions } from '@hcengineering/account-client' import { MailboxOptions } from '@hcengineering/account-client'
import presentation from '@hcengineering/presentation' import presentation, { getClient } from '@hcengineering/presentation'
import { Dropdown, ListItem, Modal, ModernEditbox, Spinner, themeStore } from '@hcengineering/ui' import { Dropdown, ListItem, Modal, ModernEditbox, Spinner, themeStore } from '@hcengineering/ui'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { getAccountClient } from '../utils' import { getAccountClient } from '../utils'
import { IntlString, translateCB } from '@hcengineering/platform' import { IntlString, translateCB } from '@hcengineering/platform'
import contact, { getCurrentEmployee, SocialIdentity } from '@hcengineering/contact'
import { buildSocialIdString, Ref, SocialIdType } from '@hcengineering/core'
export let mailboxOptions: MailboxOptions export let mailboxOptions: MailboxOptions
@ -39,10 +41,42 @@
return n.length >= mailboxOptions.minNameLength && n.length <= mailboxOptions.maxNameLength return n.length >= mailboxOptions.minNameLength && n.length <= mailboxOptions.maxNameLength
} }
async function createMailbox (): Promise<void> {
const { mailbox, socialId } = await getAccountClient().createMailbox(name, (domain ?? domains[0])._id)
console.log('Mailbox created', mailbox, socialId)
const currentUser = getCurrentEmployee()
const client = getClient()
await client.addCollection(
contact.class.SocialIdentity,
contact.space.Contacts,
currentUser,
contact.class.Person,
'socialIds',
{
key: buildSocialIdString({ type: SocialIdType.EMAIL, value: mailbox }),
type: SocialIdType.EMAIL,
value: mailbox,
verifiedOn: Date.now()
},
socialId as any as Ref<SocialIdentity>
)
await client.addCollection(
contact.class.Channel,
contact.space.Contacts,
currentUser,
contact.class.Person,
'channels',
{
provider: contact.channelProvider.Email,
value: mailbox
}
)
}
async function save (): Promise<void> { async function save (): Promise<void> {
loading = true loading = true
try { try {
await getAccountClient().createMailbox(name, (domain ?? domains[0])._id) await createMailbox()
loading = false loading = false
dispatch('close', true) dispatch('close', true)
} catch (err: any) { } catch (err: any) {

View File

@ -25,29 +25,19 @@
} from '@hcengineering/ui' } from '@hcengineering/ui'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
import { MailboxInfo } from '@hcengineering/account-client' import { MailboxInfo } from '@hcengineering/account-client'
import { MessageBox } from '@hcengineering/presentation' import { getClient, MessageBox } from '@hcengineering/presentation'
import { getAccountClient } from '../utils' import { getAccountClient } from '../utils'
import contact, { getCurrentEmployee } from '@hcengineering/contact'
import { buildSocialIdString, SocialIdType } from '@hcengineering/core'
export let mailbox: MailboxInfo export let mailbox: MailboxInfo
export let mailboxIdx: number export let mailboxIdx: number
export let loadingRequested: () => void
export let reloadRequested: () => void export let reloadRequested: () => void
let opened = false let opened = false
function getMenuItems (mailbox: MailboxInfo): (DropdownIntlItem & { action: () => void })[] { function deleteMailboxAction (): void {
return [
{
id: 'delete',
icon: IconDelete,
label: setting.string.DeleteMailbox,
action: () => {
deleteMailbox(mailbox)
}
}
]
}
function deleteMailbox (mailbox: MailboxInfo): void {
showPopup( showPopup(
MessageBox, MessageBox,
{ {
@ -56,24 +46,71 @@
dangerous: true, dangerous: true,
okLabel: setting.string.Delete, okLabel: setting.string.Delete,
action: async () => { action: async () => {
getAccountClient() loadingRequested()
.deleteMailbox(mailbox.mailbox) try {
.then(() => { await deleteMailbox()
reloadRequested() } catch (err) {
})
.catch((err: any) => {
console.error('Failed to delete mailbox', err) console.error('Failed to delete mailbox', err)
}) }
reloadRequested()
} }
}, },
undefined undefined
) )
} }
const openMailboxMenu = (mailbox: MailboxInfo, ev: MouseEvent): void => { async function deleteMailbox (): Promise<void> {
await getAccountClient().deleteMailbox(mailbox.mailbox)
const client = getClient()
const currentUser = getCurrentEmployee()
const socialIds = await client.findAll(contact.class.SocialIdentity, {
attachedTo: currentUser,
type: SocialIdType.EMAIL,
value: mailbox.mailbox
})
for (const socialId of socialIds) {
const value = `${socialId.value}#${socialId._id}`
await client.updateCollection(
socialId._class,
socialId.space,
socialId._id,
socialId.attachedTo,
socialId.attachedToClass,
socialId.collection,
{
value,
key: buildSocialIdString({ type: SocialIdType.EMAIL, value })
}
)
}
const channels = await client.findAll(contact.class.Channel, {
attachedTo: currentUser,
provider: contact.channelProvider.Email,
value: mailbox.mailbox
})
for (const channel of channels) {
await client.removeCollection(
channel._class,
channel.space,
channel._id,
channel.attachedTo,
channel.attachedToClass,
channel.collection
)
}
}
const openMailboxMenu = (ev: MouseEvent): void => {
if (!opened) { if (!opened) {
opened = true opened = true
const items = getMenuItems(mailbox) const items: (DropdownIntlItem & { action: () => void })[] = [
{
id: 'delete',
icon: IconDelete,
label: setting.string.DeleteMailbox,
action: deleteMailboxAction
}
]
showPopup(ModernPopup, { items }, eventToHTMLElement(ev), (result) => { showPopup(ModernPopup, { items }, eventToHTMLElement(ev), (result) => {
items.find((it) => it.id === result)?.action() items.find((it) => it.id === result)?.action()
opened = false opened = false
@ -94,9 +131,7 @@
pressed={opened} pressed={opened}
inheritColor inheritColor
hasMenu hasMenu
on:click={(ev) => { on:click={openMailboxMenu}
openMailboxMenu(mailbox, ev)
}}
/> />
</div> </div>
</div> </div>

View File

@ -119,7 +119,14 @@
{:else} {:else}
<Scroller> <Scroller>
{#each mailboxes as mailbox, i} {#each mailboxes as mailbox, i}
<MailboxItem {mailbox} mailboxIdx={i} reloadRequested={loadMailboxes} /> <MailboxItem
{mailbox}
mailboxIdx={i}
reloadRequested={loadMailboxes}
loadingRequested={() => {
boxesLoading = true
}}
/>
{/each} {/each}
</Scroller> </Scroller>
{/if} {/if}

View File

@ -2032,7 +2032,7 @@ async function createMailbox (
name: string name: string
domain: string domain: string
} }
): Promise<void> { ): Promise<{ mailbox: string, socialId: PersonId }> {
const { account } = decodeTokenVerbose(ctx, token) const { account } = decodeTokenVerbose(ctx, token)
const { name, domain } = params const { name, domain } = params
const normalizedName = cleanEmail(name) const normalizedName = cleanEmail(name)
@ -2060,7 +2060,14 @@ async function createMailbox (
await db.mailbox.insertOne({ accountUuid: account, mailbox }) await db.mailbox.insertOne({ accountUuid: account, mailbox })
await db.mailboxSecret.insertOne({ mailbox, secret: generatePassword() }) await db.mailboxSecret.insertOne({ mailbox, secret: generatePassword() })
await db.socialId.insertOne({ personUuid: account, type: SocialIdType.EMAIL, value: mailbox }) const socialId: PersonId = await db.socialId.insertOne({
personUuid: account,
type: SocialIdType.EMAIL,
value: mailbox,
verifiedOn: Date.now()
})
ctx.info('Mailbox created', { mailbox, account, socialId })
return { mailbox, socialId }
} }
async function getMailboxes ( async function getMailboxes (
@ -2081,15 +2088,33 @@ async function deleteMailbox (
params: { mailbox: string } params: { mailbox: string }
): Promise<void> { ): Promise<void> {
const { account } = decodeTokenVerbose(ctx, token) const { account } = decodeTokenVerbose(ctx, token)
const { mailbox } = params const mailbox = cleanEmail(params.mailbox)
if (!isEmail(mailbox)) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.MailboxError, { reason: 'invalid-name' }))
}
const mb = await db.mailbox.findOne({ mailbox, accountUuid: account }) const mb = await db.mailbox.findOne({ mailbox, accountUuid: account })
if (mb == null) { if (mb == null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.MailboxError, { reason: 'mailbox-not-found' })) throw new PlatformError(new Status(Severity.ERROR, platform.status.MailboxError, { reason: 'mailbox-not-found' }))
} }
await db.mailbox.deleteMany({ mailbox })
await db.mailboxSecret.deleteMany({ mailbox }) await db.mailboxSecret.deleteMany({ mailbox })
await db.mailbox.deleteMany({ mailbox })
await releaseSocialId(db, account, SocialIdType.EMAIL, mailbox)
ctx.info('Mailbox deleted', { mailbox, account })
}
async function releaseSocialId (
db: AccountDB,
personUuid: PersonUuid,
type: SocialIdType,
value: string
): Promise<void> {
const socialIds = await db.socialId.find({ personUuid, type, value })
for (const socialId of socialIds) {
await db.socialId.updateOne({ _id: socialId._id }, { value: `${socialId.value}#${socialId._id}` })
}
} }
export type AccountMethods = export type AccountMethods =

View File

@ -22,6 +22,7 @@ interface Config {
accountsUrl: string accountsUrl: string
workspaceUrl: string workspaceUrl: string
ignoredAddresses: string[] ignoredAddresses: string[]
hookToken?: string
} }
const config: Config = { const config: Config = {
@ -34,7 +35,8 @@ const config: Config = {
} }
throw Error('WORKSPACE_URL env var is not set') throw Error('WORKSPACE_URL env var is not set')
})(), })(),
ignoredAddresses: process.env.IGNORED_ADDRESSES?.split(',') ?? [] ignoredAddresses: process.env.IGNORED_ADDRESSES?.split(',') ?? [],
hookToken: process.env.HOOK_TOKEN
} }
export default config export default config

View File

@ -36,6 +36,13 @@ interface MtaMessage {
export async function handleMtaHook (req: Request, res: Response): Promise<void> { export async function handleMtaHook (req: Request, res: Response): Promise<void> {
try { try {
if (config.hookToken !== undefined) {
const token = req.headers['x-hook-token']
if (token !== config.hookToken) {
throw new Error('Invalid hook token')
}
}
const mta: MtaMessage = req.body const mta: MtaMessage = req.body
const from = { address: mta.envelope.from.address, name: '' } const from = { address: mta.envelope.from.address, name: '' }
@ -119,5 +126,31 @@ async function getContent (mta: MtaMessage): Promise<string> {
function extractContactName (fromHeader: string): string { function extractContactName (fromHeader: string): string {
// Match name part that appears before an email in angle brackets // Match name part that appears before an email in angle brackets
const nameMatch = fromHeader.match(/^\s*"?([^"<]+?)"?\s*<.+?>/) const nameMatch = fromHeader.match(/^\s*"?([^"<]+?)"?\s*<.+?>/)
return nameMatch?.[1].trim() ?? '' const name = nameMatch?.[1].trim() ?? ''
if (name.length > 0) {
return decodeMimeWord(name)
}
return ''
}
function decodeMimeWord (text: string): string {
return text.replace(/=\?([^?]+)\?([BQ])\?([^?]+)\?=/gi, (match, charset, encoding, content) => {
try {
if (encoding.toUpperCase() === 'B') {
// Base64 encoding
const buffer = Buffer.from(content, 'base64')
return buffer.toString(charset as BufferEncoding)
} else if (encoding.toUpperCase() === 'Q') {
// Quoted-printable encoding
const decoded = content
.replace(/_/g, ' ')
.replace(/=([0-9A-F]{2})/gi, (_: any, hex: string) => String.fromCharCode(parseInt(hex, 16)))
return Buffer.from(decoded).toString(charset as BufferEncoding)
}
return match
} catch (error) {
console.warn('Failed to decode encoded word:', match, error)
return match
}
})
} }

View File

@ -49,7 +49,7 @@ async function main (): Promise<void> {
const server = app.listen(config.port, () => { const server = app.listen(config.port, () => {
console.log(`server started on port ${config.port}`) console.log(`server started on port ${config.port}`)
console.log({ ...config, secret: '(stripped)' }) console.log({ ...config, secret: '(stripped)', hookToken: '(stripped)' })
}) })
const shutdown = (): void => { const shutdown = (): void => {

View File

@ -29,7 +29,7 @@ import chunter from '@hcengineering/chunter'
import contact, { PersonSpace } from '@hcengineering/contact' import contact, { PersonSpace } from '@hcengineering/contact'
import mail from '@hcengineering/mail' import mail from '@hcengineering/mail'
import config from './config' import config from './config'
import { ensureGlobalPerson } from './person' import { ensureGlobalPerson, ensureLocalPerson } from './person'
function generateToken (): string { function generateToken (): string {
return encode( return encode(
@ -61,6 +61,21 @@ export async function createMessages (
console.error(`[${mailId}] Unable to create message without a proper FROM`) console.error(`[${mailId}] Unable to create message without a proper FROM`)
return return
} }
try {
await ensureLocalPerson(
client,
mailId,
fromPerson.uuid,
fromPerson.socialId,
from.address,
fromPerson.firstName,
fromPerson.lastName
)
} catch (err) {
console.error(`[${mailId}] Failed to ensure local FROM person`, err)
console.error(`[${mailId}] Unable to create message without a proper FROM`)
return
}
const toPersons: { address: string, uuid: PersonUuid, socialId: PersonId }[] = [] const toPersons: { address: string, uuid: PersonUuid, socialId: PersonId }[] = []
for (const to of tos) { for (const to of tos) {
@ -68,6 +83,20 @@ export async function createMessages (
if (toPerson === undefined) { if (toPerson === undefined) {
continue continue
} }
try {
await ensureLocalPerson(
client,
mailId,
toPerson.uuid,
toPerson.socialId,
to.address,
toPerson.firstName,
toPerson.lastName
)
} catch (err) {
console.error(`[${mailId}] Failed to ensure local TO person, skip`, err)
continue
}
toPersons.push({ address: to.address, ...toPerson }) toPersons.push({ address: to.address, ...toPerson })
} }
if (toPersons.length === 0) { if (toPersons.length === 0) {

View File

@ -12,26 +12,34 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import { buildSocialIdString, generateId, PersonId, PersonUuid, SocialIdType, TxOperations } from '@hcengineering/core' import {
import contact, { AvatarType, combineName } from '@hcengineering/contact' buildSocialIdString,
generateId,
PersonId,
PersonUuid,
Ref,
SocialIdType,
TxOperations
} from '@hcengineering/core'
import contact, { AvatarType, combineName, SocialIdentity } from '@hcengineering/contact'
import { AccountClient } from '@hcengineering/account-client' import { AccountClient } from '@hcengineering/account-client'
export async function ensureGlobalPerson ( export async function ensureGlobalPerson (
client: AccountClient, client: AccountClient,
mailId: string, mailId: string,
contact: { address: string, name: string } contact: { address: string, name: string }
): Promise<{ socialId: PersonId, uuid: PersonUuid } | undefined> { ): Promise<{ socialId: PersonId, uuid: PersonUuid, firstName: string, lastName: string } | undefined> {
const [firstName, lastName] = contact.name.split(' ')
const socialKey = buildSocialIdString({ type: SocialIdType.EMAIL, value: contact.address }) const socialKey = buildSocialIdString({ type: SocialIdType.EMAIL, value: contact.address })
const socialId = await client.findSocialIdBySocialKey(socialKey) const socialId = await client.findSocialIdBySocialKey(socialKey)
const uuid = await client.findPersonBySocialKey(socialKey) const uuid = await client.findPersonBySocialKey(socialKey)
if (socialId !== undefined && uuid !== undefined) { if (socialId !== undefined && uuid !== undefined) {
console.log(`[${mailId}] Found global person for ${contact.address}: ${uuid}`) return { socialId, uuid, firstName, lastName }
return { socialId, uuid }
} }
const [firstName, lastName] = contact.name.split(' ')
try { try {
const globalPerson = await client.ensurePerson(SocialIdType.EMAIL, contact.address, firstName, lastName) const globalPerson = await client.ensurePerson(SocialIdType.EMAIL, contact.address, firstName, lastName)
return globalPerson console.log(`[${mailId}] Created global person for ${contact.address}: ${globalPerson.uuid}`)
return { ...globalPerson, firstName, lastName }
} catch (err) { } catch (err) {
console.error(`[${mailId}] Failed to create global person for ${contact.address}`, err) console.error(`[${mailId}] Failed to create global person for ${contact.address}`, err)
} }
@ -40,21 +48,14 @@ export async function ensureGlobalPerson (
export async function ensureLocalPerson ( export async function ensureLocalPerson (
client: TxOperations, client: TxOperations,
mailId: string,
personUuid: PersonUuid, personUuid: PersonUuid,
socialId: PersonId, personId: PersonId,
email: string, email: string,
firstName: string, firstName: string,
lastName: string lastName: string
): Promise<void> { ): Promise<void> {
let person = await client.findOne( let person = await client.findOne(contact.class.Person, { personUuid })
contact.class.Person,
{
personUuid
},
{
projection: { name: 1 }
}
)
if (person === undefined) { if (person === undefined) {
const newPersonId = await client.createDoc( const newPersonId = await client.createDoc(
contact.class.Person, contact.class.Person,
@ -69,7 +70,16 @@ export async function ensureLocalPerson (
person = await client.findOne(contact.class.Person, { _id: newPersonId }) person = await client.findOne(contact.class.Person, { _id: newPersonId })
if (person === undefined) { if (person === undefined) {
throw new Error(`Failed to create local person for ${personUuid}`) throw new Error(`Failed to create local person for ${personUuid}`)
} else {
console.log(`[${mailId}] Created local person for ${personUuid}: ${person._id}`)
} }
}
const socialId = await client.findOne(contact.class.SocialIdentity, {
attachedTo: person._id,
type: SocialIdType.EMAIL,
value: email
})
if (socialId === undefined) {
await client.addCollection( await client.addCollection(
contact.class.SocialIdentity, contact.class.SocialIdentity,
contact.space.Contacts, contact.space.Contacts,
@ -77,12 +87,13 @@ export async function ensureLocalPerson (
contact.class.Person, contact.class.Person,
'socialIds', 'socialIds',
{ {
key: socialId, key: buildSocialIdString({ type: SocialIdType.EMAIL, value: email }),
type: SocialIdType.EMAIL, type: SocialIdType.EMAIL,
value: email value: email
}, },
generateId() personId as any as Ref<SocialIdentity>
) )
console.log(`[${mailId}] Created local socialId for ${personUuid}: ${email}`)
} }
const channel = await client.findOne(contact.class.Channel, { const channel = await client.findOne(contact.class.Channel, {
attachedTo: person._id, attachedTo: person._id,
@ -103,5 +114,6 @@ export async function ensureLocalPerson (
}, },
generateId() generateId()
) )
console.log(`[${mailId}] Created channel for ${personUuid}: ${email}`)
} }
} }