From 845ec007bf6f226bcdc9d66370de52898c6f53fe Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 16 May 2025 18:42:56 +0700 Subject: [PATCH] UBERF-10631: Fix attachments in old gmail integration (#8971) Signed-off-by: Artem Savchenko --- .../src/__tests__/attachments.test.ts | 15 +++- .../src/__tests__/gmailClient.test.ts | 9 ++- services/gmail/pod-gmail/src/gmail.ts | 29 +++++-- .../pod-gmail/src/message/attachments.ts | 76 +++++++++++++------ 4 files changed, 95 insertions(+), 34 deletions(-) diff --git a/services/gmail/pod-gmail/src/__tests__/attachments.test.ts b/services/gmail/pod-gmail/src/__tests__/attachments.test.ts index 8ea2f663a7..ce158d2620 100644 --- a/services/gmail/pod-gmail/src/__tests__/attachments.test.ts +++ b/services/gmail/pod-gmail/src/__tests__/attachments.test.ts @@ -15,6 +15,7 @@ import { AttachmentHandler } from '../message/attachments' import type { Attachment as AttachedFile } from '@hcengineering/mail-common' import attachment, { Attachment } from '@hcengineering/attachment' import { decode64, encode64 } from '../base64' +import { WorkspaceLoginInfo } from '@hcengineering/account-client' jest.mock('../config') @@ -41,7 +42,11 @@ describe('AttachmentHandler', () => { warn: jest.fn(), end: jest.fn() } - const mockWorkspaceId = 'test-workspace' as WorkspaceUuid + const mockWorkspaceLoginInfo: WorkspaceLoginInfo = { + endpoint: 'wss://test-endpoint.com', + workspace: 'test-workspace' as WorkspaceUuid, + token: 'test-token' + } as any const mockStorageAdapter = { put: jest.fn(), read: jest.fn() @@ -77,7 +82,13 @@ describe('AttachmentHandler', () => { let attachmentHandler: AttachmentHandler beforeEach(() => { - attachmentHandler = new AttachmentHandler(mockCtx, mockWorkspaceId, mockStorageAdapter, mockGmail, mockClient) + attachmentHandler = new AttachmentHandler( + mockCtx, + mockWorkspaceLoginInfo, + mockStorageAdapter, + mockGmail, + mockClient + ) }) describe('addAttachement', () => { diff --git a/services/gmail/pod-gmail/src/__tests__/gmailClient.test.ts b/services/gmail/pod-gmail/src/__tests__/gmailClient.test.ts index 0f46d3a0c6..7687c1107b 100644 --- a/services/gmail/pod-gmail/src/__tests__/gmailClient.test.ts +++ b/services/gmail/pod-gmail/src/__tests__/gmailClient.test.ts @@ -125,7 +125,14 @@ jest.mock('../config', () => ({ })) jest.mock('@hcengineering/account-client', () => ({ - getClient: jest.fn() + getClient: jest.fn().mockImplementation(() => ({ + getLoginInfoByToken: jest.fn().mockResolvedValue({ + endpoint: 'wss://test-endpoint.com', + workspace: 'mockWorkspaceId', + token: 'test-token' + }) + })), + isWorkspaceLoginInfo: jest.fn().mockImplementation(() => true) })) describe('GmailClient', () => { diff --git a/services/gmail/pod-gmail/src/gmail.ts b/services/gmail/pod-gmail/src/gmail.ts index beaba018ab..afbfad079f 100644 --- a/services/gmail/pod-gmail/src/gmail.ts +++ b/services/gmail/pod-gmail/src/gmail.ts @@ -19,7 +19,13 @@ import { type StorageAdapter } from '@hcengineering/server-core' import setting from '@hcengineering/setting' import type { Credentials, OAuth2Client } from 'google-auth-library' import { gmail_v1, google } from 'googleapis' -import { getClient as getAccountClient, Integration } from '@hcengineering/account-client' +import { + getClient as getAccountClient, + Integration, + WorkspaceLoginInfo, + isWorkspaceLoginInfo, + AccountClient +} from '@hcengineering/account-client' import { encode64 } from './base64' import config from './config' @@ -94,19 +100,19 @@ export class GmailClient { private readonly gmail: gmail_v1.Resource$Users, private readonly user: User, client: Client, - workspaceId: WorkspaceUuid, + accountClient: AccountClient, + wsInfo: WorkspaceLoginInfo, storageAdapter: StorageAdapter, private readonly workspace: WorkspaceClient, email: string, private socialId: SocialId ) { this.email = email - this.integrationToken = serviceToken(workspaceId) - this.tokenStorage = new TokenStorage(this.ctx, workspaceId, this.integrationToken) + this.integrationToken = serviceToken(wsInfo.workspace) + this.tokenStorage = new TokenStorage(this.ctx, wsInfo.workspace, this.integrationToken) this.client = new TxOperations(client, this.socialId._id) this.account = this.user.userId - this.attachmentHandler = new AttachmentHandler(ctx, workspaceId, storageAdapter, this.gmail, this.client) - const accountClient = getAccountClient(config.AccountsURL, this.integrationToken) + this.attachmentHandler = new AttachmentHandler(ctx, wsInfo, storageAdapter, this.gmail, this.client) const keyValueClient = getKvsClient(this.integrationToken) this.messageManager = createMessageManager( ctx, @@ -164,13 +170,22 @@ export class GmailClient { } user.socialId = socialId + const integrationToken = serviceToken(workspaceId) + const accountClient = getAccountClient(config.AccountsURL, integrationToken) + const workspaceInfo = await accountClient.getLoginInfoByToken() + if (!isWorkspaceLoginInfo(workspaceInfo)) { + ctx.error('Unable to get workspace info', { workspaceId, email }) + throw new Error('Unable to get workspace info') + } + const gmailClient = new GmailClient( ctx, oAuth2Client, googleClient, user, client, - workspaceId, + accountClient, + workspaceInfo, storageAdapter, workspace, email, diff --git a/services/gmail/pod-gmail/src/message/attachments.ts b/services/gmail/pod-gmail/src/message/attachments.ts index eff5f5f7df..d7e910115d 100644 --- a/services/gmail/pod-gmail/src/message/attachments.ts +++ b/services/gmail/pod-gmail/src/message/attachments.ts @@ -14,18 +14,19 @@ // import { randomUUID } from 'crypto' import attachment, { Attachment } from '@hcengineering/attachment' -import { AttachedData, Blob, MeasureContext, Ref, TxOperations, WorkspaceUuid } from '@hcengineering/core' +import { Blob, MeasureContext, Ref, TxOperations } from '@hcengineering/core' import { StorageAdapter } from '@hcengineering/server-core' import { type Attachment as AttachedFile } from '@hcengineering/mail-common' import { gmail_v1 } from 'googleapis' -import { v4 as uuid } from 'uuid' +import { WorkspaceLoginInfo } from '@hcengineering/account-client' + import { encode64 } from '../base64' import { addFooter } from '../utils' export class AttachmentHandler { constructor ( private readonly ctx: MeasureContext, - private readonly workspaceId: WorkspaceUuid, + private readonly wsInfo: WorkspaceLoginInfo, private readonly storageAdapter: StorageAdapter, private readonly gmail: gmail_v1.Resource$Users, private readonly client: TxOperations @@ -36,22 +37,32 @@ export class AttachmentHandler { if (currentAttachemtns.findIndex((p) => p.name === file.name && p.lastModified === file.lastModified) !== -1) { return } - const id = uuid() - const data: AttachedData = { - name: file.name, - file: id as Ref, - type: file.data.toString('base64') ?? 'undefined', - size: file.size ?? file.data.length, - lastModified: file.lastModified ?? Date.now() - } - await this.storageAdapter.put(this.ctx, this.workspaceId as any, id, file.data, data.type, data.size) // TODO: FIXME + const fileId = file.id ?? randomUUID() + await this.storageAdapter.put( + this.ctx, + { + uuid: this.wsInfo.workspace, + url: this.wsInfo.workspaceUrl, + dataId: this.wsInfo.workspaceDataId + }, + fileId, + file.data, + file.contentType, + file.size + ) await this.client.addCollection( attachment.class.Attachment, message.space, message._id, message._class, 'attachments', - data + { + name: file.name, + file: fileId as Ref, + type: file.data.toString('base64') ?? 'undefined', + size: file.size ?? file.data.length, + lastModified: file.lastModified ?? Date.now() + } ) } catch (err: any) { this.ctx.error('Add attachment error', { error: err.message }) @@ -153,16 +164,33 @@ export class AttachmentHandler { } private async makeAttachmentPart (attachment: Attachment): Promise { - const buffer = await this.storageAdapter.read(this.ctx, this.workspaceId as any, attachment.file) // TODO: FIXME - const data = Buffer.concat(buffer.map((b) => new Uint8Array(b))).toString('base64') - const res: string[] = [] - res.push('--mail\n') - res.push(`Content-Type: ${attachment.type}\n`) - res.push('MIME-Version: 1.0\n') - res.push('Content-Transfer-Encoding: base64\n') - res.push(`Content-Disposition: attachment; filename="${attachment.name}"\n\n`) - res.push(data) - res.push('\n\n') - return res + try { + const buffer = await this.storageAdapter.read( + this.ctx, + { + uuid: this.wsInfo.workspace, + url: this.wsInfo.workspaceUrl, + dataId: this.wsInfo.workspaceDataId + }, + attachment.file + ) + const data = Buffer.concat(buffer.map((b) => new Uint8Array(b))).toString('base64') + const res: string[] = [] + res.push('--mail\n') + res.push(`Content-Type: ${attachment.type}\n`) + res.push('MIME-Version: 1.0\n') + res.push('Content-Transfer-Encoding: base64\n') + res.push(`Content-Disposition: attachment; filename="${attachment.name}"\n\n`) + res.push(data) + res.push('\n\n') + return res + } catch (err: any) { + this.ctx.error('Failed to make attachment part', { + error: err.message, + file: attachment.file, + name: attachment.name + }) + return [] + } } }