From 55eb570884935f987122a84845834b44cc65eeff Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 13 Feb 2026 17:32:10 +0700 Subject: [PATCH] Fix email notifications for export (#10506) Signed-off-by: Artem Savchenko --- common/config/rush/pnpm-lock.yaml | 6 + models/export/src/index.ts | 7 +- server-plugins/gmail-resources/src/index.ts | 109 +++++++++++++++--- .../notification-resources/src/index.ts | 13 ++- server/server-pipeline/package.json | 2 + .../src/internationalization.ts | 3 + .../export/pod-export/src/notifications.ts | 1 + 7 files changed, 124 insertions(+), 17 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 92d7848f7b..31464f8849 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -36578,6 +36578,12 @@ importers: '@hcengineering/drive-assets': specifier: workspace:^0.7.0 version: link:../../plugins/drive-assets + '@hcengineering/export': + specifier: workspace:^0.7.0 + version: link:../../plugins/export + '@hcengineering/export-assets': + specifier: workspace:^0.7.0 + version: link:../../plugins/export-assets '@hcengineering/github': specifier: workspace:^0.7.0 version: link:../../services/github/github diff --git a/models/export/src/index.ts b/models/export/src/index.ts index 05cb3384b1..14fc130562 100644 --- a/models/export/src/index.ts +++ b/models/export/src/index.ts @@ -89,7 +89,12 @@ export function createModel (builder: Builder): void { group: exportPlugin.ids.ImportNotificationGroup, txClasses: [], objectClass: exportPlugin.class.ExportResultRecord, - defaultEnabled: true + defaultEnabled: true, + templates: { + textTemplate: '{body}', + htmlTemplate: '

{body}

{link}

', + subjectTemplate: '{title}' + } }, exportPlugin.ids.ImportedDocumentsNotification ) diff --git a/server-plugins/gmail-resources/src/index.ts b/server-plugins/gmail-resources/src/index.ts index cc62d3db4d..d8fbbf0c4c 100644 --- a/server-plugins/gmail-resources/src/index.ts +++ b/server-plugins/gmail-resources/src/index.ts @@ -44,7 +44,10 @@ import serverNotification from '@hcengineering/server-notification' import { AvailableProvidersCache, AvailableProvidersCacheKey, - getContentByTemplate + getContentByTemplate, + getNotificationProviderControl, + getReceiversInfo, + getAllowedProviders } from '@hcengineering/server-notification-resources' import { getMetadata } from '@hcengineering/platform' import activity, { ActivityMessage } from '@hcengineering/activity' @@ -156,7 +159,7 @@ async function notifyByEmail ( senderSocialId: PersonId, email: string, data: InboxNotification, - message: ActivityMessage + message?: ActivityMessage ): Promise { let senderName = sender !== undefined ? formatName(sender.name, control.branding?.lastNameFirst) : '' if (senderName === '' && senderSocialId === core.account.System) { @@ -166,6 +169,12 @@ async function notifyByEmail ( if (content !== undefined) { await sendEmailNotification(control.ctx, content.text, content.html, content.subject, email) + } else { + control.ctx.info('notifyByEmail: getContentByTemplate returned undefined, email not sent', { + notificationId: data._id, + type, + docClass: doc._class + }) } } @@ -197,7 +206,10 @@ async function processEmailNotifications (control: TriggerControl, notifications const docId = notifications[0].objectId const docClass = notifications[0].objectClass const doc = (await control.findAll(control.ctx, docClass, { _id: docId }))[0] - if (doc === undefined) return + if (doc === undefined) { + control.ctx.info('processEmailNotifications: doc not found', { docId, docClass }) + return + } const messages = await getNotificationMessages(notifications, control) const { hierarchy } = control @@ -205,7 +217,10 @@ async function processEmailNotifications (control: TriggerControl, notifications for (const n of notifications) { const type = (n.types ?? [])[0] - if (type === undefined) continue + if (type === undefined) { + control.ctx.info('processEmailNotifications: skipping notification without type', { notificationId: n._id }) + continue + } let message: ActivityMessage | undefined if (hierarchy.isDerived(n._class, notification.class.ActivityInboxNotification)) { const activityNotification = n as ActivityInboxNotification @@ -215,11 +230,22 @@ async function processEmailNotifications (control: TriggerControl, notifications if (hierarchy.isDerived(mentionNotification.mentionedInClass, activity.class.ActivityMessage)) { message = messages.find((m) => m._id === mentionNotification.mentionedIn) } + } else if (hierarchy.isDerived(n._class, notification.class.CommonInboxNotification)) { + message = undefined } - if (message === undefined) continue + if (message === undefined && !hierarchy.isDerived(n._class, notification.class.CommonInboxNotification)) { + control.ctx.info('processEmailNotifications: skipping - no ActivityMessage and not CommonInboxNotification', { + notificationId: n._id, + notificationClass: n._class + }) + continue + } const employee = await getEmployeeByAcc(control, n.user) - if (employee === undefined) continue + if (employee === undefined) { + control.ctx.info('processEmailNotifications: no employee for user', { notificationId: n._id, user: n.user }) + continue + } const emailQuery = { attachedTo: employee._id, type: { $in: [SocialIdType.EMAIL, SocialIdType.GOOGLE] }, @@ -247,30 +273,85 @@ async function processEmailNotifications (control: TriggerControl, notifications continue } - const senderSocialId = message.createdBy ?? message.modifiedBy + const senderSocialId = message !== undefined ? (message.createdBy ?? message.modifiedBy) : core.account.System const sender = senders.get(senderSocialId) ?? (await getPerson(control, senderSocialId)) if (sender != null) { senders.set(senderSocialId, sender) } + control.ctx.info('processEmailNotifications: sending email', { + notificationId: n._id, + type, + notificationClass: n._class + }) await notifyByEmail(control, type, doc, sender, senderSocialId, emails[0].value, n, message) } } +function hasEmailProvider (n: InboxNotification, availableProviders: AvailableProvidersCache): boolean { + const providers = availableProviders.get(n._id) ?? availableProviders.get(n.objectId as Ref) + return providers?.find((p) => p === gmail.providers.EmailNotificationProvider) !== undefined +} + async function NotificationsHandler (txes: TxCreateDoc[], control: TriggerControl): Promise { + control.ctx.info('NotificationsHandler: received InboxNotification txes', { + count: txes.length, + workspace: control.workspace?.url, + objectClasses: [...new Set(txes.map((tx) => tx.objectClass))] + }) + const availableProviders: AvailableProvidersCache = control.contextCache.get(AvailableProvidersCacheKey) ?? new Map() - const all: InboxNotification[] = txes - .map((tx) => TxProcessor.createDoc2Doc(tx)) - .filter( - (it) => availableProviders.get(it._id)?.find((p) => p === gmail.providers.EmailNotificationProvider) !== undefined - ) + const all: InboxNotification[] = txes.map((tx) => TxProcessor.createDoc2Doc(tx)) - if (all.length === 0) { + const notificationsWithEmail = all.filter((it) => hasEmailProvider(it, availableProviders)) + + control.ctx.info('NotificationsHandler: processing inbox notifications', { + total: all.length, + withEmailFromCache: notificationsWithEmail.length, + notificationClasses: [...new Set(all.map((n) => n._class))], + notificationTypes: [...new Set(all.flatMap((n) => n.types ?? []))] + }) + + if (notificationsWithEmail.length < all.length) { + const notificationControl = await getNotificationProviderControl(control.ctx, control) + const receivers = await getReceiversInfo(control.ctx, [...new Set(all.map((n) => n.user))], control) + const receiverByAccount = new Map(receivers.map((r) => [r.account, r])) + for (const n of all) { + if (hasEmailProvider(n, availableProviders)) continue + const type = (n.types ?? [])[0] + if (type === undefined) { + control.ctx.info('NotificationsHandler: skipping notification without type', { notificationId: n._id }) + continue + } + const notificationType = control.modelDb.getObject(type) + const receiver = receiverByAccount.get(n.user) + if (receiver === undefined) { + control.ctx.info('NotificationsHandler: no receiver info for user', { + notificationId: n._id, + type, + reason: 'user not in getReceiversInfo result' + }) + continue + } + const allowedProviders = getAllowedProviders(control, receiver.socialIds, notificationType, notificationControl) + if (allowedProviders.includes(gmail.providers.EmailNotificationProvider)) { + notificationsWithEmail.push(n) + } else { + control.ctx.info('NotificationsHandler: email provider not enabled for notification type', { + notificationId: n._id, + type + }) + } + } + } + + if (notificationsWithEmail.length === 0) { + control.ctx.info('NotificationsHandler: no notifications with email provider, skipping') return [] } - const notificationsByDocId = groupByArray(all, (n) => n.objectId) + const notificationsByDocId = groupByArray(notificationsWithEmail, (n) => n.objectId) await Promise.all( Array.from(notificationsByDocId.entries()).map(([docId, notifications]) => diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index f79922f42d..4a29f17781 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -183,13 +183,22 @@ export async function getContentByTemplate ( const notificationType = control.modelDb.getObject(type) if (notificationType.templates === undefined) return - const textPart = await getTextPart(doc, control) - if (textPart === undefined) return const params: Record = notificationData !== undefined ? await getTranslatedNotificationContent(notificationData, notificationData._class, control) : {} + let textPart = await getTextPart(doc, control) + if (textPart === undefined) { + if ( + notificationData !== undefined && + control.hierarchy.isDerived(notificationData._class, notification.class.CommonInboxNotification) + ) { + textPart = params.title ?? params.body ?? '' + } + if (textPart === undefined || textPart === '') return + } + if ( notificationData !== undefined && control.hierarchy.isDerived(notificationData._class, notification.class.MentionInboxNotification) diff --git a/server/server-pipeline/package.json b/server/server-pipeline/package.json index 879cc60a3f..574db66844 100644 --- a/server/server-pipeline/package.json +++ b/server/server-pipeline/package.json @@ -127,6 +127,8 @@ "@hcengineering/hr-assets": "workspace:^0.7.0", "@hcengineering/request-assets": "workspace:^0.7.0", "@hcengineering/document-assets": "workspace:^0.7.0", + "@hcengineering/export": "workspace:^0.7.0", + "@hcengineering/export-assets": "workspace:^0.7.0", "@hcengineering/controlled-documents-assets": "workspace:^0.7.0", "@hcengineering/products-assets": "workspace:^0.7.0", "@hcengineering/training-assets": "workspace:^0.7.0", diff --git a/server/server-pipeline/src/internationalization.ts b/server/server-pipeline/src/internationalization.ts index eb44651f5c..e24082a7c4 100644 --- a/server/server-pipeline/src/internationalization.ts +++ b/server/server-pipeline/src/internationalization.ts @@ -8,6 +8,7 @@ import { chunterId } from '@hcengineering/chunter' import { contactId } from '@hcengineering/contact' import { documentsId } from '@hcengineering/controlled-documents' import { documentId } from '@hcengineering/document' +import { exportId } from '@hcengineering/export' import { driveId } from '@hcengineering/drive' import { githubId } from '@hcengineering/github' import { gmailId } from '@hcengineering/gmail' @@ -50,6 +51,7 @@ import chunterEn from '@hcengineering/chunter-assets/lang/en.json' import contactEn from '@hcengineering/contact-assets/lang/en.json' import documentsEn from '@hcengineering/controlled-documents-assets/lang/en.json' import documentEn from '@hcengineering/document-assets/lang/en.json' +import exportEn from '@hcengineering/export-assets/lang/en.json' import driveEn from '@hcengineering/drive-assets/lang/en.json' import githubEn from '@hcengineering/github-assets/lang/en.json' import gmailEn from '@hcengineering/gmail-assets/lang/en.json' @@ -109,6 +111,7 @@ export function registerStringLoaders (): void { addStringsLoader(preferenceId, async (lang: string) => preferenceEn) addStringsLoader(hrId, async (lang: string) => hrEn) addStringsLoader(documentId, async (lang: string) => documentEn) + addStringsLoader(exportId, async (lang: string) => exportEn) addStringsLoader(requestId, async (lang: string) => requestEn) addStringsLoader(loveId, async (lang: string) => loveEn) addStringsLoader(driveId, async (lang: string) => driveEn) diff --git a/services/export/pod-export/src/notifications.ts b/services/export/pod-export/src/notifications.ts index be49c23d64..ff378253b6 100644 --- a/services/export/pod-export/src/notifications.ts +++ b/services/export/pod-export/src/notifications.ts @@ -85,6 +85,7 @@ export async function sendExportCompletionNotification ( objectId: resultId, objectClass: exportPlugin.class.ExportResultRecord, icon: exportPlugin.icon.Export, + header: exportPlugin.string.ImportCompleted, message: exportPlugin.string.ImportToWorkspaceNotificationMessage, props: { count,