UBERF-11347: Fix gmail recipients (#9115)

* UBERF-11347: Fix gmail recipients

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11347: Fix gmail recipients

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11347: Formatting

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-11347: Fix js doc

Signed-off-by: Artem Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artyom Savchenko
2025-05-28 12:05:09 +07:00
committed by GitHub
parent 84ddf7e7c0
commit efa7eefe8a
6 changed files with 150 additions and 97 deletions
+7 -1
View File
@@ -26,6 +26,7 @@ import {
isWorkspaceLoginInfo,
AccountClient
} from '@hcengineering/account-client'
import { MailRecipient } from '@hcengineering/mail-common'
import { encode64 } from './base64'
import config from './config'
@@ -114,6 +115,11 @@ export class GmailClient {
this.account = this.user.userId
this.attachmentHandler = new AttachmentHandler(ctx, wsInfo, storageAdapter, this.gmail, this.client)
const keyValueClient = getKvsClient(this.integrationToken)
const recipient: MailRecipient = {
email: this.email,
socialId: this.socialId._id,
uuid: this.user.userId
}
this.messageManager = createMessageManager(
ctx,
this.client,
@@ -122,7 +128,7 @@ export class GmailClient {
this.attachmentHandler,
this.workspace,
this.integrationToken,
socialId
recipient
)
this.syncManager = new SyncManager(
ctx,
@@ -13,9 +13,10 @@
// limitations under the License.
//
import { MeasureContext, TxOperations, SocialId } from '@hcengineering/core'
import { MeasureContext, TxOperations } from '@hcengineering/core'
import { type KeyValueClient } from '@hcengineering/kvs-client'
import { AccountClient } from '@hcengineering/account-client'
import { type MailRecipient } from '@hcengineering/mail-common'
import config from '../config'
import { AttachmentHandler } from './attachments'
@@ -32,11 +33,11 @@ export function createMessageManager (
attachmentHandler: AttachmentHandler,
workspace: { getChannel: (email: string) => Channel | undefined },
token: string,
socialId: SocialId
recipient: MailRecipient
): IMessageManager {
if (config.Version === 'v2') {
return new MessageManagerV2(ctx, attachmentHandler, client, keyValueClient, accountClient, token, socialId._id)
return new MessageManagerV2(ctx, attachmentHandler, client, keyValueClient, accountClient, token, recipient)
} else {
return new MessageManagerV1(ctx, client, attachmentHandler, socialId._id, workspace)
return new MessageManagerV1(ctx, client, attachmentHandler, recipient.socialId, workspace)
}
}
@@ -16,13 +16,14 @@ import { type GaxiosResponse } from 'gaxios'
import { gmail_v1 } from 'googleapis'
import sanitizeHtml from 'sanitize-html'
import { type MeasureContext, PersonId, TxOperations } from '@hcengineering/core'
import { type MeasureContext, TxOperations } from '@hcengineering/core'
import {
createMessages,
parseEmailHeader,
parseNameFromEmailHeader,
EmailMessage,
getProducer
getProducer,
MailRecipient
} from '@hcengineering/mail-common'
import { type KeyValueClient } from '@hcengineering/kvs-client'
import { AccountClient, isWorkspaceLoginInfo, WorkspaceLoginInfo } from '@hcengineering/account-client'
@@ -41,7 +42,7 @@ export class MessageManagerV2 implements IMessageManager {
private readonly keyValueClient: KeyValueClient,
private readonly accountClient: AccountClient,
private readonly token: string,
private readonly personId: PersonId
private readonly recipient: MailRecipient
) {}
async saveMessage (message: GaxiosResponse<gmail_v1.Schema$Message>, me: string): Promise<void> {
@@ -67,7 +68,7 @@ export class MessageManagerV2 implements IMessageManager {
this.wsInfo,
res,
attachments,
[this.personId]
[this.recipient]
)
}
}
@@ -20,7 +20,7 @@ import { PersonSpace } from '@hcengineering/contact'
import { MeasureContext, PersonId, PersonUuid, Ref, TxOperations } from '@hcengineering/core'
import { KeyValueClient } from '@hcengineering/kvs-client'
import { Producer } from 'kafkajs'
import { Attachment, BaseConfig, EmailContact, EmailMessage } from '../types'
import { Attachment, BaseConfig, EmailContact, EmailMessage, MailRecipient } from '../types'
import { PersonCacheFactory } from '../person'
import { PersonSpacesCacheFactory } from '../personSpaces'
import { ChannelCacheFactory } from '../channel'
@@ -221,49 +221,9 @@ describe('createMessages', () => {
)
})
it('should use provided targetPersons when specified', async () => {
// Arrange
const customTargetPersons = ['custom-person-1', 'custom-person-2'] as PersonId[]
// Act
await createMessages(
mockConfig,
mockCtx,
mockTxClient,
mockKvsClient,
mockProducer,
mockToken,
mockWsInfo,
mockMessage,
mockAttachments,
customTargetPersons
)
// Assert
// Check that createDoc was called with the custom participants list
expect(mockTxClient.createDoc).toHaveBeenCalledWith(
expect.any(String),
expect.any(String),
expect.objectContaining({
members: customTargetPersons
}),
expect.any(String),
expect.any(Number),
expect.any(String)
)
// Also check that the channel was created with the custom participants
expect(mockChannelCache.getOrCreateChannel).toHaveBeenCalledWith(
mockSpace._id,
customTargetPersons,
mockToContact1.email,
expect.any(String)
)
})
it('should handle empty targetPersons array', async () => {
// Arrange
const emptyTargetPersons: PersonId[] = []
const emptyTargetPersons: MailRecipient[] = []
// Act
await createMessages(
@@ -285,7 +245,7 @@ describe('createMessages', () => {
expect.any(String),
expect.any(String),
expect.objectContaining({
members: [] // Empty array
members: ['from-person-id', 'to-person-id-1', 'to-person-id-2']
}),
expect.any(String),
expect.any(Number),
@@ -398,4 +358,111 @@ describe('createMessages', () => {
expect.any(String)
)
})
it('should call getPersonSpaces for both sender and recipients if target persons are not provided', async () => {
// Act
await createMessages(
mockConfig,
mockCtx,
mockTxClient,
mockKvsClient,
mockProducer,
mockToken,
mockWsInfo,
mockMessage,
mockAttachments
)
// Assert
// Based on the error message, the actual calls are with different parameters
// Update the assertions to match the actual implementation
expect(mockPersonSpacesCache.getPersonSpaces).toHaveBeenCalledWith('test-mail-id', 'from-uuid', 'from@example.com')
// Check that getPersonSpaces was called for each recipient
expect(mockPersonSpacesCache.getPersonSpaces).toHaveBeenCalledWith('test-mail-id', 'to-uuid-1', 'to1@example.com')
expect(mockPersonSpacesCache.getPersonSpaces).toHaveBeenCalledWith('test-mail-id', 'to-uuid-2', 'to2@example.com')
// Verify the total number of calls matches our expectations
// Called once for sender and once for each recipient
expect(mockPersonSpacesCache.getPersonSpaces).toHaveBeenCalledTimes(3)
})
it('should call getPersonSpaces only for specified targetPerson when provided', async () => {
const customTargetPersons: MailRecipient[] = [
{
socialId: 'to-person-id-1' as PersonId,
uuid: 'to-uuid-1' as PersonUuid,
email: 'test-mail@example.com'
}
]
// Act
await createMessages(
mockConfig,
mockCtx,
mockTxClient,
mockKvsClient,
mockProducer,
mockToken,
mockWsInfo,
mockMessage,
mockAttachments,
customTargetPersons
)
// Assert
expect(mockPersonSpacesCache.getPersonSpaces).toHaveBeenCalledWith(
'test-mail-id',
'to-uuid-1',
'test-mail@example.com'
)
// Verify the total number of calls matches the number of targetPersons
expect(mockPersonSpacesCache.getPersonSpaces).toHaveBeenCalledTimes(1)
})
it('should call getPersonSpaces only for specified targetPersons when provided', async () => {
const customTargetPersons: MailRecipient[] = [
{
socialId: 'to-person-id-1' as PersonId,
uuid: 'to-uuid-1' as PersonUuid,
email: 'to-person-email-1@example.com'
},
{
socialId: 'to-person-id-2' as PersonId,
uuid: 'to-uuid-2' as PersonUuid,
email: 'to-person-email-2@example.com'
}
]
// Act
await createMessages(
mockConfig,
mockCtx,
mockTxClient,
mockKvsClient,
mockProducer,
mockToken,
mockWsInfo,
mockMessage,
mockAttachments,
customTargetPersons
)
// Assert
expect(mockPersonSpacesCache.getPersonSpaces).toHaveBeenCalledWith(
'test-mail-id',
'to-uuid-1',
'to-person-email-1@example.com'
)
expect(mockPersonSpacesCache.getPersonSpaces).toHaveBeenCalledWith(
'test-mail-id',
'to-uuid-2',
'to-person-email-2@example.com'
)
// Verify the total number of calls matches the number of targetPersons
expect(mockPersonSpacesCache.getPersonSpaces).toHaveBeenCalledTimes(2)
})
})
+14 -45
View File
@@ -27,7 +27,6 @@ import {
type TxOperations,
Doc,
generateId,
PersonUuid,
RateLimiter,
Space
} from '@hcengineering/core'
@@ -39,7 +38,7 @@ import { MessageRequestEventType } from '@hcengineering/communication-sdk-types'
import { generateMessageId } from '@hcengineering/communication-shared'
import { BaseConfig, type Attachment } from './types'
import { EmailMessage } from './types'
import { EmailMessage, MailRecipient } from './types'
import { getMdContent } from './utils'
import { PersonCacheFactory } from './person'
import { PersonSpacesCacheFactory } from './personSpaces'
@@ -65,8 +64,8 @@ import { ThreadLookupService } from './thread'
* @param {WorkspaceLoginInfo} wsInfo - Workspace information including ID and URLs
* @param {EmailMessage} message - The email message to process
* @param {Attachment[]} attachments - Array of attachments for the message
* @param {PersonId[]} [targetPersons] - Optional list of specific persons who should receive the message.
* If not provided, all existing accounts from email addresses will be used.
* @param {MailRecipient[]} [recipients] - Optional list of specific persons who should receive the message.
* If not provided, all existing accounts from email addresses will be used.
* @returns {Promise<void>} A promise that resolves when all messages have been created
* @throws Will log errors but not throw exceptions for partial failures
*
@@ -82,7 +81,7 @@ export async function createMessages (
wsInfo: WorkspaceLoginInfo,
message: EmailMessage,
attachments: Attachment[],
targetPersons?: PersonId[]
recipients?: MailRecipient[]
): Promise<void> {
const { mailId, from, subject, replyTo } = message
const tos = [...(message.to ?? []), ...(message.copy ?? [])]
@@ -95,13 +94,13 @@ export async function createMessages (
const fromPerson = await personCache.ensurePerson(from)
const toPersons: { address: string, uuid: PersonUuid, socialId: PersonId }[] = []
const toPersons: MailRecipient[] = []
for (const to of tos) {
const toPerson = await personCache.ensurePerson(to)
if (toPerson === undefined) {
continue
}
toPersons.push({ address: to.email, ...toPerson })
toPersons.push({ email: to.email, ...toPerson })
}
if (toPersons.length === 0) {
ctx.error('Unable to create message without a proper TO', { mailId, from })
@@ -109,7 +108,7 @@ export async function createMessages (
}
const modifiedBy = fromPerson.socialId
const participants = targetPersons ?? [fromPerson.socialId, ...toPersons.map((p) => p.socialId)]
const participants = [fromPerson.socialId, ...toPersons.map((p) => p.socialId)]
const content = getMdContent(ctx, message)
const attachedBlobs: Attachment[] = []
@@ -141,42 +140,12 @@ export async function createMessages (
}
}
try {
const spaces = await personSpacesCache.getPersonSpaces(mailId, fromPerson.uuid, from.email)
if (spaces.length > 0) {
await saveMessageToSpaces(
config,
ctx,
txClient,
producer,
threadLookup,
wsInfo,
mailId,
spaces,
participants,
modifiedBy,
subject,
content,
attachedBlobs,
from.email,
fromPerson.socialId,
message.sendOn,
channelCache,
replyTo
)
}
} catch (error) {
ctx.error('Failed to save message to personal spaces', {
error,
mailId,
personUuid: fromPerson.uuid,
email: from
})
}
const allPersons = [{ ...fromPerson, email: from.email }, ...toPersons]
const messageRecipients = recipients != null && recipients.length > 0 ? recipients : allPersons
for (const to of toPersons) {
for (const person of messageRecipients) {
try {
const spaces = await personSpacesCache.getPersonSpaces(mailId, to.uuid, to.address)
const spaces = await personSpacesCache.getPersonSpaces(mailId, person.uuid, person.email)
if (spaces.length > 0) {
await saveMessageToSpaces(
config,
@@ -192,15 +161,15 @@ export async function createMessages (
subject,
content,
attachedBlobs,
to.address,
to.socialId,
person.email,
person.socialId,
message.sendOn,
channelCache,
replyTo
)
}
} catch (error) {
ctx.error('Failed to save message spaces', { error, mailId, personUuid: to.uuid, email: to.address })
ctx.error('Failed to save message spaces', { error, mailId, personUuid: person.uuid, email: person.email })
}
}
}
+9
View File
@@ -11,6 +11,9 @@
//
// See the License for the specific language governing permissions and
// limitations under the License.
import { PersonId, PersonUuid } from '@hcengineering/core'
//
export interface Attachment {
id: string
@@ -41,6 +44,12 @@ export interface EmailMessage {
sendOn: number
}
export interface MailRecipient {
email: string
uuid: PersonUuid
socialId: PersonId
}
export interface BaseConfig {
AccountsURL: string
KvsUrl: string