Do not notify about new messages in initial Gmail sync (#9673)

Signed-off-by: Artem Savchenko <armisav@gmail.com>
Signed-off-by: Artyom Savchenko <armisav@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Artyom Savchenko
2025-08-13 22:13:24 +07:00
committed by GitHub
co-authored by Copilot
parent d40eb99774
commit c4f9953d7d
6 changed files with 65 additions and 16 deletions
@@ -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 () => {
+28 -9
View File
@@ -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<gmail_v1.Schema$ListHistoryResponse>
@@ -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<void> {
async fullSync (userId: PersonId, userEmail?: string, q?: string, options?: SyncOptions): Promise<void> {
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<SyncOptions | undefined> {
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<GaxiosResponse<gmail_v1.Schema$Message>> {
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<void> {
async syncNewMessages (userId: PersonId, userEmail?: string, options?: SyncOptions): Promise<void> {
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
@@ -58,6 +58,17 @@ export class SyncStateManager {
await this.keyValueClient.setValue(pageTokenKey, pageToken)
}
async getLastSyncDate (userId: PersonId): Promise<Date | null> {
const lastSyncKey = this.getLastSyncKey(userId)
const lastSync = await this.keyValueClient.getValue<Date>(lastSyncKey)
return lastSync != null ? new Date(lastSync) : null
}
async setLastSyncDate (userId: PersonId, date: Date): Promise<void> {
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}`
}
}
@@ -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<gmail_v1.Schema$Message>, me: string) => Promise<void>
saveMessage: (message: GaxiosResponse<gmail_v1.Schema$Message>, me: string, options?: SyncOptions) => Promise<void>
}
@@ -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<gmail_v1.Schema$Message>, me: string): Promise<void> {
async saveMessage (
message: GaxiosResponse<gmail_v1.Schema$Message>,
me: string,
options?: SyncOptions
): Promise<void> {
const res = convertMessage(message, me)
const channels = this.findChannels(res)
if (channels.length === 0) return
@@ -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<gmail_v1.Schema$Message>, me: string): Promise<void> {
async saveMessage (
message: GaxiosResponse<gmail_v1.Schema$Message>,
me: string,
options?: SyncOptions
): Promise<void> {
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
)
}
}