From cdceb8c196f4a0b18fd0bfbb567063661c0b5d41 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 22 Aug 2025 17:37:11 +0700 Subject: [PATCH] UBERF-13241: Support gmail space config (#9710) --- packages/integration-client/src/utils.ts | 4 + plugins/gmail-assets/lang/cs.json | 7 +- plugins/gmail-assets/lang/de.json | 7 +- plugins/gmail-assets/lang/en.json | 7 +- plugins/gmail-assets/lang/es.json | 7 +- plugins/gmail-assets/lang/fr.json | 7 +- plugins/gmail-assets/lang/it.json | 7 +- plugins/gmail-assets/lang/ja.json | 7 +- plugins/gmail-assets/lang/pt.json | 7 +- plugins/gmail-assets/lang/ru.json | 7 +- plugins/gmail-assets/lang/zh.json | 7 +- plugins/gmail-resources/package.json | 1 + plugins/gmail-resources/src/api.ts | 4 + .../src/components/Configure.svelte | 68 ++++----- .../src/components/ConfigureV2.svelte | 130 ++++++++++++++++++ .../src/components/IntegrationState.svelte | 9 +- .../src/components/icons/GmailColor.svelte | 5 + plugins/gmail-resources/src/plugin.ts | 7 +- plugins/gmail-resources/src/utils.ts | 6 + plugins/gmail/src/index.ts | 1 + .../src/__tests__/gmailClient.test.ts | 5 +- services/gmail/pod-gmail/src/gmail.ts | 61 ++++++-- .../gmail/pod-gmail/src/gmailController.ts | 16 +++ services/gmail/pod-gmail/src/integrations.ts | 7 +- services/gmail/pod-gmail/src/main.ts | 27 ++++ services/gmail/pod-gmail/src/types.ts | 1 + services/gmail/pod-gmail/src/utils.ts | 15 +- services/mail/mail-common/src/channel.ts | 11 +- services/mail/mail-common/src/message.ts | 18 +-- services/mail/mail-common/src/personSpaces.ts | 4 +- services/mail/mail-common/src/thread.ts | 13 +- services/mail/mail-common/src/types.ts | 6 +- 32 files changed, 405 insertions(+), 84 deletions(-) create mode 100644 plugins/gmail-resources/src/components/ConfigureV2.svelte diff --git a/packages/integration-client/src/utils.ts b/packages/integration-client/src/utils.ts index 61ea42d38d..62f93d9c93 100644 --- a/packages/integration-client/src/utils.ts +++ b/packages/integration-client/src/utils.ts @@ -30,3 +30,7 @@ export function isSameIntegrationEvent (event: IntegrationEventData, integration event.integration?.workspaceUuid === integration.workspaceUuid ) } + +export function getIntegrationConfig (integration: Integration): Record | undefined { + return integration?.data?.config +} diff --git a/plugins/gmail-assets/lang/cs.json b/plugins/gmail-assets/lang/cs.json index 45861eb4c7..7b9f139f28 100644 --- a/plugins/gmail-assets/lang/cs.json +++ b/plugins/gmail-assets/lang/cs.json @@ -40,6 +40,11 @@ "GooglePrivacy": "Použití a přenos informací získaných z Google API do jakékoli jiné aplikace bude v souladu s zásadami Google API Services User Data Policy, včetně požadavků na omezené použití.", "EmailNotificationsDescription": "Dostávejte osobní oznámení na e-mail.", "FailedToConnect": "Služba Gmail není k dispozici", - "TotalMessages": "Celkový počet zpráv:" + "TotalMessages": "Celkový počet zpráv:", + "Configure": "Konfigurovat integraci Gmail", + "GmailSpace": "Prostor pro Gmail zprávy", + "PersonSpaceInfo": "E-maily budou dostupné pouze vám", + "SharedSpaceInfo": "E-maily budou dostupné všem účastníkům prostoru", + "ConfigurationRequired": "Prosím, nakonfigurujte svou integraci Gmail, abyste ji mohli začít používat" } } \ No newline at end of file diff --git a/plugins/gmail-assets/lang/de.json b/plugins/gmail-assets/lang/de.json index ff9bd3047a..abc55b0667 100644 --- a/plugins/gmail-assets/lang/de.json +++ b/plugins/gmail-assets/lang/de.json @@ -40,6 +40,11 @@ "GooglePrivacy": "Die Nutzung und Übertragung von Informationen, die Huly von Google-APIs erhält, an andere Apps erfolgt gemäß der Google API Services User Data Policy, einschließlich der Anforderungen zur eingeschränkten Nutzung.", "EmailNotificationsDescription": "Erhalten Sie persönliche Benachrichtigungen per E-Mail.", "FailedToConnect": "Gmail-Dienst ist nicht verfügbar", - "TotalMessages": "Gesamtanzahl der Nachrichten:" + "TotalMessages": "Gesamtanzahl der Nachrichten:", + "Configure": "Gmail-Integration konfigurieren", + "GmailSpace": "Gmail-Nachrichtenbereich", + "PersonSpaceInfo": "E-Mails werden nur für Sie verfügbar sein", + "SharedSpaceInfo": "E-Mails werden für alle Bereichsteilnehmer verfügbar sein", + "ConfigurationRequired": "Bitte konfigurieren Sie Ihre Gmail-Integration, um sie nutzen zu können" } } \ No newline at end of file diff --git a/plugins/gmail-assets/lang/en.json b/plugins/gmail-assets/lang/en.json index 37382a2abd..044c477d94 100644 --- a/plugins/gmail-assets/lang/en.json +++ b/plugins/gmail-assets/lang/en.json @@ -40,6 +40,11 @@ "GooglePrivacy": "Huly’s use and transfer of information received from Google APIs to any other app will adhere to Google API Services User Data Policy, including the Limited Use requirements.", "EmailNotificationsDescription": "Receive personal notifications on email.", "FailedToConnect": "Gmail service is not available", - "TotalMessages": "Total messages:" + "TotalMessages": "Total messages:", + "Configure": "Configure Gmail integration", + "GmailSpace": "Space for message channel", + "PersonSpaceInfo": "Mail will be available to you", + "SharedSpaceInfo": "Mail will be available to all space participants", + "ConfigurationRequired": "Please configure your Gmail integration to start using it" } } \ No newline at end of file diff --git a/plugins/gmail-assets/lang/es.json b/plugins/gmail-assets/lang/es.json index a3c9a08330..68e60dd368 100644 --- a/plugins/gmail-assets/lang/es.json +++ b/plugins/gmail-assets/lang/es.json @@ -40,6 +40,11 @@ "GooglePrivacy": "Huly’s use and transfer of information received from Google APIs to any other app will adhere to Google API Services User Data Policy, including the Limited Use requirements.", "EmailNotificationsDescription": "Reciba notificaciones personales por correo electrónico.", "FailedToConnect": "El servicio de Gmail no está disponible", - "TotalMessages": "Total de mensajes:" + "TotalMessages": "Total de mensajes:", + "Configure": "Configurar integración de Gmail", + "GmailSpace": "Espacio para mensajes de Gmail", + "PersonSpaceInfo": "El correo estará disponible solo para usted", + "SharedSpaceInfo": "El correo estará disponible para todos los participantes del espacio", + "ConfigurationRequired": "Por favor, configure su integración de Gmail para comenzar a usarla" } } \ No newline at end of file diff --git a/plugins/gmail-assets/lang/fr.json b/plugins/gmail-assets/lang/fr.json index 27c38abb13..bcb86dd549 100644 --- a/plugins/gmail-assets/lang/fr.json +++ b/plugins/gmail-assets/lang/fr.json @@ -40,6 +40,11 @@ "GooglePrivacy": "L'utilisation et le transfert des informations reçues des API Google par Huly à toute autre application respecteront les règles d'utilisation des données utilisateur des services API Google, y compris les exigences d'utilisation limitée.", "EmailNotificationsDescription": "Recevez des notifications personnelles par e-mail.", "FailedToConnect": "Le service Gmail n'est pas disponible", - "TotalMessages": "Total des messages:" + "TotalMessages": "Total des messages:", + "Configure": "Configurer l'intégration Gmail", + "GmailSpace": "Espace pour les messages Gmail", + "PersonSpaceInfo": "Les e-mails seront disponibles uniquement pour vous", + "SharedSpaceInfo": "Les e-mails seront disponibles pour tous les participants de l'espace", + "ConfigurationRequired": "Veuillez configurer votre intégration Gmail pour commencer à l'utiliser" } } \ No newline at end of file diff --git a/plugins/gmail-assets/lang/it.json b/plugins/gmail-assets/lang/it.json index 26c80128ac..b536eb4c80 100644 --- a/plugins/gmail-assets/lang/it.json +++ b/plugins/gmail-assets/lang/it.json @@ -40,6 +40,11 @@ "GooglePrivacy": "L'uso e il trasferimento delle informazioni ricevute dalle API di Google a qualsiasi altra app di Huly adempirà alla Politica sui dati degli utenti dei servizi API di Google, inclusi i requisiti di utilizzo limitato.", "EmailNotificationsDescription": "Ricevi notifiche personali sulle email.", "FailedToConnect": "Il servizio Gmail non è disponibile", - "TotalMessages": "Totale messaggi:" + "TotalMessages": "Totale messaggi:", + "Configure": "Configura integrazione Gmail", + "GmailSpace": "Spazio per messaggi Gmail", + "PersonSpaceInfo": "Le email saranno disponibili solo per te", + "SharedSpaceInfo": "Le email saranno disponibili per tutti i partecipanti dello spazio", + "ConfigurationRequired": "Per favore, configura la tua integrazione Gmail per iniziare a usarla" } } diff --git a/plugins/gmail-assets/lang/ja.json b/plugins/gmail-assets/lang/ja.json index 1cc736b6f5..46b8a64c51 100644 --- a/plugins/gmail-assets/lang/ja.json +++ b/plugins/gmail-assets/lang/ja.json @@ -40,6 +40,11 @@ "GooglePrivacy": "Huly による Google API から受信した情報の他のアプリへの使用および転送は、Google API Services User Data Policy(限定的な使用の要件を含む)に準拠します。", "EmailNotificationsDescription": "メールで個人通知を受け取る", "FailedToConnect": "Gmail サービスは利用できません", - "TotalMessages": "メッセージの総数:" + "TotalMessages": "メッセージの総数:", + "Configure": "Gmail 統合を設定", + "GmailSpace": "Gmail メッセージスペース", + "PersonSpaceInfo": "メールはあなたのみに利用可能になります", + "SharedSpaceInfo": "メールはすべてのスペース参加者が利用可能になります", + "ConfigurationRequired": "Gmail 統合を設定する必要があります" } } diff --git a/plugins/gmail-assets/lang/pt.json b/plugins/gmail-assets/lang/pt.json index a9e599a0ef..43b123e84c 100644 --- a/plugins/gmail-assets/lang/pt.json +++ b/plugins/gmail-assets/lang/pt.json @@ -40,6 +40,11 @@ "GooglePrivacy": "Huly’s use and transfer of information received from Google APIs to any other app will adhere to Google API Services User Data Policy, including the Limited Use requirements.", "EmailNotificationsDescription": "Receba notificações pessoais por e-mail.", "FailedToConnect": "O serviço Gmail não está disponível", - "TotalMessages": "Total de mensagens:" + "TotalMessages": "Total de mensagens:", + "Configure": "Configurar integração do Gmail", + "GmailSpace": "Espaço para mensagens do Gmail", + "PersonSpaceInfo": "O email estará disponível apenas para você", + "SharedSpaceInfo": "O email estará disponível para todos os participantes do espaço", + "ConfigurationRequired": "Por favor, configure sua integração de Gmail para começar a usá-la" } } \ No newline at end of file diff --git a/plugins/gmail-assets/lang/ru.json b/plugins/gmail-assets/lang/ru.json index d97c5d1a52..69cf91a320 100644 --- a/plugins/gmail-assets/lang/ru.json +++ b/plugins/gmail-assets/lang/ru.json @@ -40,6 +40,11 @@ "GooglePrivacy": "Использование и передача информации, полученной Huly от Google API, будет соответствовать Политике использования данных пользователей Google API, включая требования ограниченного использования.", "EmailNotificationsDescription": "Получайте персональные уведомления на электронную почту.", "FailedToConnect": "Gmail сервис недоступен", - "TotalMessages": "Всего сообщений:" + "TotalMessages": "Всего сообщений:", + "Configure": "Настроить Gmail интеграцию", + "GmailSpace": "Пространство для канала сообщений", + "PersonSpaceInfo": "Почта будет доступна только вам", + "SharedSpaceInfo": "Почта будет доступна всем участникам пространства", + "ConfigurationRequired": "Пожалуйста, настройте интеграцию Gmail, чтобы начать ее использовать" } } \ No newline at end of file diff --git a/plugins/gmail-assets/lang/zh.json b/plugins/gmail-assets/lang/zh.json index ee28973941..12faf4b31c 100644 --- a/plugins/gmail-assets/lang/zh.json +++ b/plugins/gmail-assets/lang/zh.json @@ -40,6 +40,11 @@ "GooglePrivacy": "Huly 从 Google API 接收的信息的使用和传输将遵守 Google API 服务用户数据政策,包括有限使用要求。", "EmailNotificationsDescription": "透過電子郵件接收個人通知。", "FailedToConnect": "Gmail 服务不可用", - "TotalMessages": "总消息数:" + "TotalMessages": "总消息数:", + "Configure": "配置 Gmail 集成", + "GmailSpace": "Gmail 消息空间", + "PersonSpaceInfo": "邮件将仅对您可用", + "SharedSpaceInfo": "邮件将对所有空间参与者可用", + "ConfigurationRequired": "请配置您的 Gmail 集成以开始使用" } } diff --git a/plugins/gmail-resources/package.json b/plugins/gmail-resources/package.json index 467c96cd63..6141850fd1 100644 --- a/plugins/gmail-resources/package.json +++ b/plugins/gmail-resources/package.json @@ -47,6 +47,7 @@ "@hcengineering/text-html": "^0.6.0", "@hcengineering/text-editor": "^0.6.0", "@hcengineering/text-editor-resources": "^0.6.0", + "@hcengineering/card": "^0.6.0", "@hcengineering/contact": "^0.6.24", "@hcengineering/setting": "^0.6.17", "@hcengineering/chunter": "^0.6.20", diff --git a/plugins/gmail-resources/src/api.ts b/plugins/gmail-resources/src/api.ts index 2b197cd17d..1155dc88ad 100644 --- a/plugins/gmail-resources/src/api.ts +++ b/plugins/gmail-resources/src/api.ts @@ -49,6 +49,10 @@ export async function getState (socialId: string): Promise { + await request('POST', `/start-sync?socialId=${encodeURIComponent(socialId)}`) +} + export async function signout (): Promise { await request('GET', '/signout') } diff --git a/plugins/gmail-resources/src/components/Configure.svelte b/plugins/gmail-resources/src/components/Configure.svelte index 2fdc113357..4a01b7b776 100644 --- a/plugins/gmail-resources/src/components/Configure.svelte +++ b/plugins/gmail-resources/src/components/Configure.svelte @@ -24,8 +24,10 @@ import { isWorkspaceIntegration } from '@hcengineering/integration-client' import { Integration as AccountIntegration } from '@hcengineering/account-client' + import ConfigureV2 from './ConfigureV2.svelte' import { getIntegrationClient } from '../api' import gmail from '../plugin' + import { isNewGmailIntegration } from '../utils' export let integration: AccountIntegration let integrationSettings: Integration | undefined @@ -74,36 +76,40 @@ const dispatch = createEventDispatcher() - { - await apply() - dispatch('close') - }} - canSave={true} - fullSize - okLabel={presentation.string.Ok} - on:close={() => dispatch('close')} - on:changeContent -> -
- -
-
- - {#if shared} +{#if isNewGmailIntegration(integration)} + +{:else} + { + await apply() + dispatch('close') + }} + canSave={true} + fullSize + okLabel={presentation.string.Ok} + on:close={() => dispatch('close')} + on:changeContent + > +
+
-
- change(res)} - /> - {/if} -
-
-
+ + {#if shared} +
+
+ change(res)} + /> + {/if} +
+
+
+{/if} diff --git a/plugins/gmail-resources/src/components/ConfigureV2.svelte b/plugins/gmail-resources/src/components/ConfigureV2.svelte new file mode 100644 index 0000000000..1501cb436c --- /dev/null +++ b/plugins/gmail-resources/src/components/ConfigureV2.svelte @@ -0,0 +1,130 @@ + + + + { + await apply() + dispatch('close') + }} + canSave={true} + fullSize + okLabel={presentation.string.Ok} + on:close={() => dispatch('close')} + on:changeContent +> + +
+ + + +
+
+
+ {#if isLoading} +
+
+ +
+
+ {:else} +
+
+ {/if} +
+ +
+ {#if personSpace === selectedSpace} + + + + {:else} + + + + {/if} +
+
+
diff --git a/plugins/gmail-resources/src/components/IntegrationState.svelte b/plugins/gmail-resources/src/components/IntegrationState.svelte index f8e83f9021..add34bbff5 100644 --- a/plugins/gmail-resources/src/components/IntegrationState.svelte +++ b/plugins/gmail-resources/src/components/IntegrationState.svelte @@ -22,7 +22,7 @@ import gmail from '../plugin' import { getState } from '../api' - import { IntlString, OK, ERROR, Status } from '@hcengineering/platform' + import platform, { IntlString, OK, ERROR, Status, Severity } from '@hcengineering/platform' export let integration: Integration @@ -39,7 +39,7 @@ state = await getState(integration.socialId) isLoading = false subscribe() - status = OK + status = state?.status === 'inactive' ? new Status(Severity.WARNING, platform.status.OK, {}) : OK } catch (err) { status = ERROR error = err instanceof Error ? err.message : 'Failed to load gmail state' @@ -83,8 +83,11 @@ - {#if state?.totalMessages != null} + {#if state?.isConfigured === true && state?.totalMessages != null} {/if} + {#if state?.isConfigured !== true} + + {/if} diff --git a/plugins/gmail-resources/src/components/icons/GmailColor.svelte b/plugins/gmail-resources/src/components/icons/GmailColor.svelte index 6e14a59d2f..13a4b3ed10 100644 --- a/plugins/gmail-resources/src/components/icons/GmailColor.svelte +++ b/plugins/gmail-resources/src/components/icons/GmailColor.svelte @@ -14,10 +14,15 @@ // limitations under the License. --> + + diff --git a/plugins/gmail-resources/src/plugin.ts b/plugins/gmail-resources/src/plugin.ts index fc52d294e6..8ca6b9bac4 100644 --- a/plugins/gmail-resources/src/plugin.ts +++ b/plugins/gmail-resources/src/plugin.ts @@ -45,6 +45,11 @@ export default mergeIds(gmailId, gmail, { NewIncomingMessage: '' as IntlString, GooglePrivacy: '' as IntlString, FailedToConnect: '' as IntlString, - TotalMessages: '' as IntlString + TotalMessages: '' as IntlString, + Configure: '' as IntlString, + GmailSpace: '' as IntlString, + PersonSpaceInfo: '' as IntlString, + SharedSpaceInfo: '' as IntlString, + ConfigurationRequired: '' as IntlString } }) diff --git a/plugins/gmail-resources/src/utils.ts b/plugins/gmail-resources/src/utils.ts index ab7c42f20d..c496281d51 100644 --- a/plugins/gmail-resources/src/utils.ts +++ b/plugins/gmail-resources/src/utils.ts @@ -5,6 +5,8 @@ import { employeeBySocialKeyStore, getPersonByPersonId } from '@hcengineering/co import { buildSocialIdString, type PersonId, SocialIdType, type Client, type Doc, type Ref } from '@hcengineering/core' import { type Message, type SharedMessage } from '@hcengineering/gmail' import { getClient } from '@hcengineering/presentation' +import type { Integration } from '@hcengineering/account-client' + import gmail from './plugin' export function getTime (time: number): string { @@ -122,3 +124,7 @@ export async function MessageTitleProvider (client: Client, ref: Ref, d return object?.subject ?? '' } + +export function isNewGmailIntegration (integration: Integration): boolean { + return integration.data?.integrationVersion === 'v2' +} diff --git a/plugins/gmail/src/index.ts b/plugins/gmail/src/index.ts index 80bcd8e98f..513c775a22 100644 --- a/plugins/gmail/src/index.ts +++ b/plugins/gmail/src/index.ts @@ -90,6 +90,7 @@ export interface GmailSyncState { totalMessages: number | null | undefined lastSynchronization?: Date | null error?: string + isConfigured?: boolean } /** diff --git a/services/gmail/pod-gmail/src/__tests__/gmailClient.test.ts b/services/gmail/pod-gmail/src/__tests__/gmailClient.test.ts index 85f48b2a5b..ee1fff8320 100644 --- a/services/gmail/pod-gmail/src/__tests__/gmailClient.test.ts +++ b/services/gmail/pod-gmail/src/__tests__/gmailClient.test.ts @@ -123,7 +123,8 @@ jest.mock('../utils', () => ({ setValue: jest.fn().mockImplementation(() => Promise.resolve(undefined)), deleteKey: jest.fn().mockImplementation(() => Promise.resolve(undefined)), listKeys: jest.fn() - })) + })), + getSpaceId: jest.fn().mockReturnValue('test-space-id') })) // Mock gmail module @@ -376,7 +377,7 @@ describe('GmailClient', () => { // Fix unbound method reference by using the spy expect(jest.spyOn(SyncManager.prototype, 'sync')).toHaveBeenCalledWith( mockSocialId._id, - { noNotify: true }, + { noNotify: true, spaceId: 'test-space-id' }, 'test@example.com' ) }) diff --git a/services/gmail/pod-gmail/src/gmail.ts b/services/gmail/pod-gmail/src/gmail.ts index 64a23b1115..2192da3434 100644 --- a/services/gmail/pod-gmail/src/gmail.ts +++ b/services/gmail/pod-gmail/src/gmail.ts @@ -23,7 +23,7 @@ import core, { TxOperations, WorkspaceUuid } from '@hcengineering/core' -import gmail, { type NewMessage } from '@hcengineering/gmail' +import gmail, { gmailIntegrationKind, type NewMessage } from '@hcengineering/gmail' import { type StorageAdapter } from '@hcengineering/server-core' import setting from '@hcengineering/setting' import type { Credentials, OAuth2Client } from 'google-auth-library' @@ -49,7 +49,7 @@ import config from './config' import { GmailController } from './gmailController' import { RateLimiter } from './rateLimiter' import { type ProjectCredentials, type Token, type User, type SyncState, GmailMessageType } from './types' -import { addFooter, isToken, serviceToken, getKvsClient, createGmailSearchQuery } from './utils' +import { addFooter, isToken, serviceToken, getKvsClient, createGmailSearchQuery, getSpaceId } from './utils' import type { WorkspaceClient } from './workspaceClient' import { getOrCreateSocialId } from './accounts' import { createIntegrationIfNotExists, disableIntegration, removeIntegration } from './integrations' @@ -113,7 +113,7 @@ export class GmailClient { private readonly messageManager: IMessageManager private readonly syncManager: SyncManager private readonly integrationToken: string - private integration: Integration | undefined = undefined + private integration: Integration | null | undefined = undefined private syncStarted: boolean = false private channel: Card | undefined = undefined @@ -310,7 +310,19 @@ export class GmailClient { try { this.integration = await createIntegrationIfNotExists(this.socialId._id, this.user.workspace, this.email) } catch (err: any) { - this.ctx.error('Failed to create integration', { socialdId: this.socialId, workspace: this.workspace }) + this.ctx.error('Failed to create integration', { socialId: this.socialId, workspace: this.workspace }) + } + } + + async refreshIntegration (): Promise { + try { + this.integration = await this.accountClient.getIntegration({ + socialId: this.socialId._id, + workspaceUuid: this.user.workspace, + kind: gmailIntegrationKind + }) + } catch (err: any) { + this.ctx.error('Failed to refresh integration', { socialId: this.socialId, workspace: this.workspace }) } } @@ -367,6 +379,11 @@ export class GmailClient { if (personId !== this.socialId._id && !this.allSocialIds.has(personId)) { return } + + if (!this.isConfigured()) { + return + } + if (message.date !== undefined) { const messageDate = message.date instanceof Date ? message.date : new Date(message.date) if (messageDate < config.OutgoingSyncStartDate) { @@ -489,7 +506,7 @@ export class GmailClient { } async disableIntegration (byError: boolean = false): Promise { - if (this.integration !== undefined) { + if (this.integration != null) { if (byError) { await disableIntegration(this.integration) } else { @@ -508,10 +525,25 @@ export class GmailClient { return } + // Check for spaceId requirement in v2 configurations + if (!this.isConfigured()) { + this.ctx.info('Cannot start sync: spaceId is required for v2 configuration', { + workspaceUuid: this.user.workspace, + userId: this.user.userId, + email: this.email, + integrationVersion: this.integration?.data?.integrationVersion + }) + return + } + try { this.syncStarted = true this.ctx.info('Start sync', { workspaceUuid: this.user.workspace, userId: this.user.userId, email: this.email }) - await this.syncManager.sync(this.socialId._id, { noNotify: true }, this.email) + await this.syncManager.sync( + this.socialId._id, + { noNotify: true, spaceId: getSpaceId(this.integration) }, + this.email + ) await this.watch() // recall every 24 hours https://developers.google.com/gmail/api/guides/push if (this.watchTimer !== undefined) clearInterval(this.watchTimer) @@ -530,8 +562,15 @@ export class GmailClient { } async sync (options: SyncOptions): Promise { + if (!this.isConfigured()) { + return + } this.ctx.info('Sync', { workspaceUuid: this.user.workspace, userId: this.user.userId }) - await this.syncManager.sync(this.socialId._id, options, this.email) + const syncOptions = { + ...options, + spaceId: getSpaceId(this.integration) + } + await this.syncManager.sync(this.socialId._id, syncOptions, this.email) } async newChannel (value: string): Promise { @@ -689,6 +728,11 @@ export class GmailClient { } } + private isConfigured (): boolean { + if (this.integration?.data?.integrationVersion !== 'v2') return true + return getSpaceId(this.integration) !== undefined + } + async getStateSummary (): Promise { let totalMessages: number | null | undefined try { @@ -708,7 +752,8 @@ export class GmailClient { status: this.syncStarted ? 'active' : 'inactive', email: this.email, totalMessages, - syncInfo: syncStatus + syncInfo: syncStatus, + isConfigured: this.isConfigured() } } diff --git a/services/gmail/pod-gmail/src/gmailController.ts b/services/gmail/pod-gmail/src/gmailController.ts index dbe3145499..30457bdbdc 100644 --- a/services/gmail/pod-gmail/src/gmailController.ts +++ b/services/gmail/pod-gmail/src/gmailController.ts @@ -393,4 +393,20 @@ export class GmailController { } return await mailClient.getStateSummary() } + + async startSyncForClient (workspace: WorkspaceUuid, socialId: PersonId): Promise { + const workspaceClient = this.workspaces.get(workspace) + if (workspaceClient === undefined) { + this.ctx.info('Workspace client not found for startSync', { workspace, socialId }) + return + } + const mailClient = workspaceClient.getGmailClient(socialId) + if (mailClient === undefined) { + this.ctx.info('Gmail client not found for startSync', { workspace, socialId }) + return + } + this.ctx.info('Starting sync for specific client', { workspace, socialId }) + await mailClient.refreshIntegration() + void mailClient.startSync() + } } diff --git a/services/gmail/pod-gmail/src/integrations.ts b/services/gmail/pod-gmail/src/integrations.ts index 4a7ec3ee6f..b9d90c77a0 100644 --- a/services/gmail/pod-gmail/src/integrations.ts +++ b/services/gmail/pod-gmail/src/integrations.ts @@ -18,10 +18,11 @@ import { AccountUuid, MeasureContext, PersonId, TxOperations, WorkspaceUuid } fr import gmail, { gmailIntegrationKind } from '@hcengineering/gmail' import { getAccountClient } from '@hcengineering/server-client' import setting from '@hcengineering/setting' +import { IntegrationClient, IntegrationClientImpl } from '@hcengineering/integration-client' import { serviceToken } from './utils' import { getAccountSocialIds } from './accounts' -import { IntegrationClient, IntegrationClientImpl } from '@hcengineering/integration-client' +import config from './config' let integrationClient: IntegrationClient | undefined export function getIntegrationClient (): IntegrationClient { @@ -94,7 +95,9 @@ export async function createIntegrationIfNotExists ( const connection = await client.connect(socialId, { email }) - return await client.integrate(connection, workspace) + return await client.integrate(connection, workspace, { + integrationVersion: config.Version + }) } export async function disableIntegration (integration: Integration): Promise { diff --git a/services/gmail/pod-gmail/src/main.ts b/services/gmail/pod-gmail/src/main.ts index 59db5e66b2..8bff7a73de 100644 --- a/services/gmail/pod-gmail/src/main.ts +++ b/services/gmail/pod-gmail/src/main.ts @@ -199,6 +199,33 @@ export const main = async (): Promise => { res.status(500).send({ error: err.message }) } } + }, + { + endpoint: '/start-sync', + type: 'post', + handler: async (req, res) => { + try { + const token = extractToken(req.headers) + + if (token === undefined) { + res.status(401).send() + return + } + + const { workspace } = decodeToken(token) + const socialId = req.query.socialId as PersonId | undefined + ctx.info('Sync request received', { workspace, socialId }) + if (socialId == null || socialId === '') { + res.status(400).send({ error: 'Missing socialId param' }) + return + } + await gmailController.startSyncForClient(workspace, socialId) + res.send({ success: true }) + } catch (err: any) { + ctx.error('Failed to start sync for client', { message: err.message }) + res.status(500).send({ error: err.message }) + } + } } ] diff --git a/services/gmail/pod-gmail/src/types.ts b/services/gmail/pod-gmail/src/types.ts index b3872e202d..72f062fd78 100644 --- a/services/gmail/pod-gmail/src/types.ts +++ b/services/gmail/pod-gmail/src/types.ts @@ -85,6 +85,7 @@ export interface SyncState { totalMessages: number | null | undefined lastSynchronization?: Date | null error?: string + isConfigured?: boolean syncInfo?: { type: 'full' | 'partial' | null processedMessages: number diff --git a/services/gmail/pod-gmail/src/utils.ts b/services/gmail/pod-gmail/src/utils.ts index 4a53be065f..116d036235 100644 --- a/services/gmail/pod-gmail/src/utils.ts +++ b/services/gmail/pod-gmail/src/utils.ts @@ -14,12 +14,21 @@ // limitations under the License. // -import { type Data, type Doc, type DocumentUpdate, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core' +import { + type Data, + type Doc, + type DocumentUpdate, + Ref, + Space, + systemAccountUuid, + WorkspaceUuid +} from '@hcengineering/core' import { generateToken } from '@hcengineering/server-token' import { deepEqual } from 'fast-equals' import { type KeyValueClient, getClient as getKeyValueClient } from '@hcengineering/kvs-client' import { type Token, type User } from './types' import config from './config' +import { Integration } from '@hcengineering/account-client' export class DeferredPromise { public readonly promise: Promise @@ -107,3 +116,7 @@ export function createGmailSearchQuery (fromDate: Date, toDate: Date, fromEmail: return `after:${afterDate} before:${beforeDate} from:${fromEmail}` } + +export function getSpaceId (integration: Integration | null | undefined): Ref | undefined { + return integration?.data?.config?.spaceId +} diff --git a/services/mail/mail-common/src/channel.ts b/services/mail/mail-common/src/channel.ts index fe06e0fc1a..e79afdef08 100644 --- a/services/mail/mail-common/src/channel.ts +++ b/services/mail/mail-common/src/channel.ts @@ -13,11 +13,10 @@ // limitations under the License. // -import { MeasureContext, PersonId, Ref, TxOperations, WorkspaceUuid, generateId } from '@hcengineering/core' +import { MeasureContext, PersonId, Ref, Space, TxOperations, WorkspaceUuid, generateId } from '@hcengineering/core' import { type Card } from '@hcengineering/card' import chat from '@hcengineering/chat' import mail from '@hcengineering/mail' -import { PersonSpace } from '@hcengineering/contact' import { SyncMutex } from './mutex' import { MessageTimeShift, normalizeEmail } from './utils' @@ -40,7 +39,7 @@ export class ChannelCache { * Gets or creates a mail channel with caching */ async getOrCreateChannel ( - spaceId: Ref, + spaceId: Ref, participants: PersonId[], email: string, owner: PersonId @@ -61,7 +60,7 @@ export class ChannelCache { return channel } - clearCache (spaceId: Ref, email: string): void { + clearCache (spaceId: Ref, email: string): void { const normalizedEmail = normalizeEmail(email) this.cache.delete(`${spaceId}:${normalizedEmail}`) } @@ -75,7 +74,7 @@ export class ChannelCache { } private async fetchOrCreateChannel ( - space: Ref, + space: Ref, participants: PersonId[], email: string, personId: PersonId @@ -109,7 +108,7 @@ export class ChannelCache { } private async createNewChannel ( - space: Ref, + space: Ref, participants: PersonId[], email: string, personId: PersonId diff --git a/services/mail/mail-common/src/message.ts b/services/mail/mail-common/src/message.ts index 078f499829..7cb475b399 100644 --- a/services/mail/mail-common/src/message.ts +++ b/services/mail/mail-common/src/message.ts @@ -18,7 +18,6 @@ import { WorkspaceLoginInfo } from '@hcengineering/account-client' import { type Card } from '@hcengineering/card' import { MessageID, MessageType } from '@hcengineering/communication-types' import chat from '@hcengineering/chat' -import { PersonSpace } from '@hcengineering/contact' import core, { type Blob, type MeasureContext, @@ -28,7 +27,8 @@ import core, { type TxOperations, AccountUuid, generateId, - RateLimiter + RateLimiter, + Space } from '@hcengineering/core' import { type KeyValueClient } from '@hcengineering/kvs-client' @@ -152,7 +152,10 @@ export async function createMessages ( for (const person of messageRecipients) { try { - const spaces = await personSpacesCache.getPersonSpaces(mailId, person.uuid, person.email) + let spaces = options?.spaceId != null ? [options.spaceId] : undefined + if (spaces === undefined) { + spaces = (await personSpacesCache.getPersonSpaces(mailId, person.uuid, person.email)).map((s) => s._id) + } if (spaces.length > 0) { await saveMessageToSpaces( config, @@ -189,7 +192,7 @@ async function saveMessageToSpaces ( threadLookup: ThreadLookupService, wsInfo: WorkspaceLoginInfo, mailId: string, - spaces: PersonSpace[], + spaces: Ref[], participants: PersonId[], modifiedBy: PersonId, subject: string, @@ -203,8 +206,7 @@ async function saveMessageToSpaces ( ): Promise { const rateLimiter = new RateLimiter(10) const createdDate = mailMessage.sendOn ?? Date.now() - for (const space of spaces) { - const spaceId = space._id + for (const spaceId of spaces) { let isReply = false await rateLimiter.add(async () => { let threadId = await threadLookup.getThreadId(mailId, spaceId, recipient.email) @@ -221,7 +223,7 @@ async function saveMessageToSpaces ( if (threadId === undefined) { const newThreadId = await client.createDoc( chat.masterTag.Thread, - space._id, + spaceId, { title: subject, description: content, @@ -264,7 +266,7 @@ async function saveMessageToSpaces ( const messageId = await createMailMessage(producer, config, messageData, threadId, options) await createFiles(ctx, producer, config, attachments, messageData, threadId, messageId) - await threadLookup.setThreadId(mailId, space._id, threadId, recipient.email) + await threadLookup.setThreadId(mailId, spaceId, threadId, recipient.email) }) } await rateLimiter.waitProcessing() diff --git a/services/mail/mail-common/src/personSpaces.ts b/services/mail/mail-common/src/personSpaces.ts index a54c427809..b0ef27ded2 100644 --- a/services/mail/mail-common/src/personSpaces.ts +++ b/services/mail/mail-common/src/personSpaces.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import { MeasureContext, PersonUuid, TxOperations, WorkspaceUuid } from '@hcengineering/core' +import { MeasureContext, PersonUuid, Space, TxOperations, WorkspaceUuid } from '@hcengineering/core' import contact, { PersonSpace } from '@hcengineering/contact' /** @@ -28,7 +28,7 @@ export class PersonSpacesCache { private readonly workspace: WorkspaceUuid ) {} - async getPersonSpaces (mailId: string, personUuid: PersonUuid, email: string): Promise { + async getPersonSpaces (mailId: string, personUuid: PersonUuid, email: string): Promise { let spacesPromise = this.cache.get(personUuid) if (spacesPromise === undefined) { diff --git a/services/mail/mail-common/src/thread.ts b/services/mail/mail-common/src/thread.ts index 5749925135..f02577fea6 100644 --- a/services/mail/mail-common/src/thread.ts +++ b/services/mail/mail-common/src/thread.ts @@ -13,10 +13,9 @@ // limitations under the License. // -import { MeasureContext, Ref } from '@hcengineering/core' +import { MeasureContext, Ref, Space } from '@hcengineering/core' import { type KeyValueClient } from '@hcengineering/kvs-client' import { Card } from '@hcengineering/card' -import { PersonSpace } from '@hcengineering/contact' export interface ThreadInfo { threadId: Ref @@ -54,7 +53,7 @@ export class ThreadLookupService { ThreadLookupService.instances.delete(token) } - async getThreadId (mailId: string, spaceId: Ref, email: string): Promise | undefined> { + async getThreadId (mailId: string, spaceId: Ref, email: string): Promise | undefined> { try { if (mailId == null || spaceId == null) { this.ctx.warn('Invalid parameters for thread lookup', { mailId, spaceId }) @@ -80,7 +79,7 @@ export class ThreadLookupService { } } - async setThreadId (mailId: string, spaceId: Ref, threadId: Ref, email: string): Promise { + async setThreadId (mailId: string, spaceId: Ref, threadId: Ref, email: string): Promise { try { const key = this.getLookupKey(mailId, spaceId, email) @@ -102,7 +101,7 @@ export class ThreadLookupService { async getParentThreadId ( inReplyTo: string | undefined, - spaceId: Ref, + spaceId: Ref, email: string ): Promise | undefined> { if (inReplyTo === undefined) { @@ -112,7 +111,7 @@ export class ThreadLookupService { return await this.getThreadId(inReplyTo, spaceId, email) } - async deleteMapping (mailId: string, spaceId: Ref, email: string): Promise { + async deleteMapping (mailId: string, spaceId: Ref, email: string): Promise { try { const key = this.getLookupKey(mailId, spaceId, email) await this.keyValueClient.deleteKey(key) @@ -122,7 +121,7 @@ export class ThreadLookupService { } } - private getLookupKey (mailId: string, spaceId: Ref, email: string): string { + private getLookupKey (mailId: string, spaceId: Ref, email: string): string { return `mail-thread-lookup:${mailId}:${spaceId}:${email}` } } diff --git a/services/mail/mail-common/src/types.ts b/services/mail/mail-common/src/types.ts index d91a4023ae..2f319dfb8e 100644 --- a/services/mail/mail-common/src/types.ts +++ b/services/mail/mail-common/src/types.ts @@ -14,8 +14,7 @@ import { Card } from '@hcengineering/card' import { MessageExtra } from '@hcengineering/communication-types' -import { PersonSpace } from '@hcengineering/contact' -import { OperationDomain, PersonId, PersonUuid, Ref, WorkspaceUuid } from '@hcengineering/core' +import { OperationDomain, PersonId, PersonUuid, Ref, Space, WorkspaceUuid } from '@hcengineering/core' // export interface Attachment { @@ -71,7 +70,7 @@ export interface MessageData { created: Date modifiedBy: PersonId mailId: string - spaceId: Ref + spaceId: Ref workspace: WorkspaceUuid threadId: Ref recipient: MailRecipient @@ -81,6 +80,7 @@ export interface MessageData { export interface SyncOptions { noNotify?: boolean + spaceId?: Ref } export interface MailRecipients {