From c4f9953d7d1aa501f387545104d8dfea2ee91c24 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Wed, 13 Aug 2025 22:13:24 +0700 Subject: [PATCH] Do not notify about new messages in initial Gmail sync (#9673) Signed-off-by: Artem Savchenko Signed-off-by: Artyom Savchenko Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../pod-gmail/src/__tests__/sync.test.ts | 4 +- services/gmail/pod-gmail/src/message/sync.ts | 37 ++++++++++++++----- .../gmail/pod-gmail/src/message/syncState.ts | 18 +++++++++ services/gmail/pod-gmail/src/message/types.ts | 3 +- .../gmail/pod-gmail/src/message/v1/message.ts | 7 +++- .../gmail/pod-gmail/src/message/v2/message.ts | 12 ++++-- 6 files changed, 65 insertions(+), 16 deletions(-) diff --git a/services/gmail/pod-gmail/src/__tests__/sync.test.ts b/services/gmail/pod-gmail/src/__tests__/sync.test.ts index c1ad072530..a4c75532d0 100644 --- a/services/gmail/pod-gmail/src/__tests__/sync.test.ts +++ b/services/gmail/pod-gmail/src/__tests__/sync.test.ts @@ -133,7 +133,7 @@ describe('SyncManager', () => { await syncManager.sync(userId, { noNotify: true }, userEmail) // Assert - expect(spyFullSync).toHaveBeenCalledWith(userId, userEmail) + expect(spyFullSync).toHaveBeenCalledWith(userId, userEmail, undefined, { noNotify: true }) }) it('should perform part sync when history exists', async () => { @@ -197,7 +197,7 @@ describe('SyncManager', () => { // Assert expect(spyGetHistory).toHaveBeenCalledWith(userId) expect(mockCtx.info).toHaveBeenCalledWith('No stored history ID found, performing full sync instead', { userId }) - expect(spyFullSync).toHaveBeenCalledWith(userId, userEmail) + expect(spyFullSync).toHaveBeenCalledWith(userId, userEmail, undefined, undefined) }) it('should throw error when userEmail is undefined', async () => { diff --git a/services/gmail/pod-gmail/src/message/sync.ts b/services/gmail/pod-gmail/src/message/sync.ts index ede3e7cc8a..78540a2fd1 100644 --- a/services/gmail/pod-gmail/src/message/sync.ts +++ b/services/gmail/pod-gmail/src/message/sync.ts @@ -71,6 +71,8 @@ export class SyncManager { this.processedMessages = 0 this.syncStartTime = new Date() + const syncOptions = await this.getSyncOptions(userId, options) + try { let pageToken: string | undefined let histories: GaxiosResponse @@ -91,7 +93,7 @@ export class SyncManager { error: err.message, historyId }) - void this.syncNewMessages(userId, userEmail) + void this.syncNewMessages(userId, userEmail, syncOptions) return } const nextPageToken = histories.data.nextPageToken @@ -106,7 +108,7 @@ export class SyncManager { } try { const res = await this.getMessage(message.message.id) - await this.messageManager.saveMessage(res, userEmail) + await this.messageManager.saveMessage(res, userEmail, syncOptions) this.processedMessages++ } catch (err) { this.ctx.error('Part sync message error', { @@ -143,6 +145,7 @@ export class SyncManager { }) } } + await this.stateManager.setLastSyncDate(userId, new Date()) return } else { pageToken = nextPageToken @@ -160,7 +163,7 @@ export class SyncManager { } } - async fullSync (userId: PersonId, userEmail?: string, q?: string): Promise { + async fullSync (userId: PersonId, userEmail?: string, q?: string, options?: SyncOptions): Promise { this.ctx.info('Start full sync', { workspaceUuid: this.workspace, userId, userEmail }) if (userEmail === undefined) { throw new Error('Cannot sync without user email') @@ -171,6 +174,8 @@ export class SyncManager { this.processedMessages = 0 this.syncStartTime = new Date() + const syncOptions = await this.getSyncOptions(userId, options) + try { // Get saved page token to continue from let pageToken: string | undefined = (await this.stateManager.getPageToken(userId)) ?? undefined @@ -205,7 +210,7 @@ export class SyncManager { try { const message = await this.getMessage(id) const historyId = message.data.historyId - await this.messageManager.saveMessage(message, userEmail) + await this.messageManager.saveMessage(message, userEmail, syncOptions) this.processedMessages++ if (historyId != null && q === undefined) { @@ -233,6 +238,7 @@ export class SyncManager { if (currentHistoryId != null) { await this.stateManager.setHistoryId(userId, currentHistoryId) } + await this.stateManager.setLastSyncDate(userId, new Date()) this.ctx.info('Full sync finished', { workspaceUuid: this.workspace, userId, userEmail }) } catch (err) { if (this.isClosing) return @@ -245,6 +251,19 @@ export class SyncManager { } } + private async getSyncOptions (userId: PersonId, syncOptions?: SyncOptions): Promise { + try { + const isUpdateAfterFullSync = (await this.stateManager.getLastSyncDate(userId)) != null + return { + ...(syncOptions ?? {}), + noNotify: isUpdateAfterFullSync ? syncOptions?.noNotify ?? false : true + } + } catch (err) { + this.ctx.error('Error getting last sync date', { workspace: this.workspace, userId, err }) + return syncOptions + } + } + private async getMessage (id: string): Promise> { await this.rateLimiter.take(5) return await this.gmail.messages.get({ @@ -254,7 +273,7 @@ export class SyncManager { }) } - async syncNewMessages (userId: PersonId, userEmail?: string): Promise { + async syncNewMessages (userId: PersonId, userEmail?: string, options?: SyncOptions): Promise { this.ctx.info('Start sync new messages', { workspaceUuid: this.workspace, userId, userEmail }) if (userEmail === undefined) { throw new Error('Cannot sync without user email') @@ -266,7 +285,7 @@ export class SyncManager { if (storedHistoryId == null) { this.ctx.info('No stored history ID found, performing full sync instead', { userId }) - await this.fullSync(userId, userEmail) + await this.fullSync(userId, userEmail, undefined, options) return } @@ -312,7 +331,7 @@ export class SyncManager { // If message has a history ID, check if it's newer than stored if (messageHistoryId != null) { if (BigInt(messageHistoryId) > BigInt(storedHistoryId)) { - await this.messageManager.saveMessage(message, userEmail) + await this.messageManager.saveMessage(message, userEmail, options) totalProcessedMessages++ // Track the maximum history ID found @@ -331,7 +350,7 @@ export class SyncManager { } } else { // Message without history ID, save it anyway - await this.messageManager.saveMessage(message, userEmail) + await this.messageManager.saveMessage(message, userEmail, options) totalProcessedMessages++ } } catch (err: any) { @@ -401,7 +420,7 @@ export class SyncManager { await this.partSync(userId, userEmail, history.historyId, options) } else { this.ctx.info('Start full sync', { workspaceUuid: this.workspace, userId }) - await this.fullSync(userId, userEmail) + await this.fullSync(userId, userEmail, undefined, options) } } catch (err) { if (this.isClosing) return diff --git a/services/gmail/pod-gmail/src/message/syncState.ts b/services/gmail/pod-gmail/src/message/syncState.ts index efbb6d26bc..85ab174e82 100644 --- a/services/gmail/pod-gmail/src/message/syncState.ts +++ b/services/gmail/pod-gmail/src/message/syncState.ts @@ -58,6 +58,17 @@ export class SyncStateManager { await this.keyValueClient.setValue(pageTokenKey, pageToken) } + async getLastSyncDate (userId: PersonId): Promise { + const lastSyncKey = this.getLastSyncKey(userId) + const lastSync = await this.keyValueClient.getValue(lastSyncKey) + return lastSync != null ? new Date(lastSync) : null + } + + async setLastSyncDate (userId: PersonId, date: Date): Promise { + const lastSyncKey = this.getLastSyncKey(userId) + await this.keyValueClient.setValue(lastSyncKey, date) + } + private getHistoryKey (userId: PersonId): string { if (this.version === IntegrationVersion.V2) { return `history-v2:${this.workspace}:${userId}` @@ -71,4 +82,11 @@ export class SyncStateManager { } return `page-token:${this.workspace}:${userId}` } + + private getLastSyncKey (userId: PersonId): string { + if (this.version === IntegrationVersion.V2) { + return `last-sync-date-v2:${this.workspace}:${userId}` + } + return `last-sync-date:${this.workspace}:${userId}` + } } diff --git a/services/gmail/pod-gmail/src/message/types.ts b/services/gmail/pod-gmail/src/message/types.ts index 0b0dc46150..fdcb118a9b 100644 --- a/services/gmail/pod-gmail/src/message/types.ts +++ b/services/gmail/pod-gmail/src/message/types.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // +import { SyncOptions } from '@hcengineering/mail-common' import { type GaxiosResponse } from 'gaxios' import { gmail_v1 } from 'googleapis' @@ -22,5 +23,5 @@ export interface History { } export interface IMessageManager { - saveMessage: (message: GaxiosResponse, me: string) => Promise + saveMessage: (message: GaxiosResponse, me: string, options?: SyncOptions) => Promise } diff --git a/services/gmail/pod-gmail/src/message/v1/message.ts b/services/gmail/pod-gmail/src/message/v1/message.ts index ff9be48cf1..7e39b19cb2 100644 --- a/services/gmail/pod-gmail/src/message/v1/message.ts +++ b/services/gmail/pod-gmail/src/message/v1/message.ts @@ -37,6 +37,7 @@ import { type Channel } from '../../types' import { AttachmentHandler } from '../attachments' import { decode64 } from '../../base64' import { diffAttributes } from '../../utils' +import { SyncOptions } from '@hcengineering/mail-common' const EMAIL_REGEX = /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/ @@ -65,7 +66,11 @@ export class MessageManagerV1 implements IMessageManager { return result } - async saveMessage (message: GaxiosResponse, me: string): Promise { + async saveMessage ( + message: GaxiosResponse, + me: string, + options?: SyncOptions + ): Promise { const res = convertMessage(message, me) const channels = this.findChannels(res) if (channels.length === 0) return diff --git a/services/gmail/pod-gmail/src/message/v2/message.ts b/services/gmail/pod-gmail/src/message/v2/message.ts index ae73073ef1..e1c1e3b193 100644 --- a/services/gmail/pod-gmail/src/message/v2/message.ts +++ b/services/gmail/pod-gmail/src/message/v2/message.ts @@ -26,7 +26,8 @@ import { MailRecipient, getMessageExtra, HulyMailHeader, - HulyMessageIdHeader + HulyMessageIdHeader, + SyncOptions } from '@hcengineering/mail-common' import { type KeyValueClient } from '@hcengineering/kvs-client' import { AccountClient, isWorkspaceLoginInfo, WorkspaceLoginInfo } from '@hcengineering/account-client' @@ -49,7 +50,11 @@ export class MessageManagerV2 implements IMessageManager { private readonly recipient: MailRecipient ) {} - async saveMessage (message: GaxiosResponse, me: string): Promise { + async saveMessage ( + message: GaxiosResponse, + me: string, + options?: SyncOptions + ): Promise { if (isHulyMessage(message.data.payload)) { this.ctx.info('Skipping Huly message', { mailId: message.data.id, me }) return @@ -76,7 +81,8 @@ export class MessageManagerV2 implements IMessageManager { this.wsInfo, res, attachments, - [this.recipient] + [this.recipient], + options ) } }