Fix email notifications (#10376)

* Fix email notifications

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Add warning

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Clean up

Signed-off-by: Artem Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artyom Savchenko
2026-01-08 21:09:52 +07:00
committed by GitHub
parent a7599bb3d9
commit cdbc1959af
4 changed files with 49 additions and 380 deletions
+25 -136
View File
@@ -13,9 +13,8 @@
// limitations under the License.
//
/* eslint-disable @typescript-eslint/no-unused-vars */
import contact, { Channel, Employee, formatName, Person } from '@hcengineering/contact'
import contact, { Channel, formatName, Person, SocialIdentity } from '@hcengineering/contact'
import core, {
AccountUuid,
PersonId,
Class,
concatLink,
@@ -30,7 +29,8 @@ import core, {
TxCreateDoc,
TxProcessor,
groupByArray,
SocialIdType
SocialIdType,
Domain
} from '@hcengineering/core'
import gmail, { Message } from '@hcengineering/gmail'
import { TriggerControl } from '@hcengineering/server-core'
@@ -122,18 +122,10 @@ export async function sendEmailNotification (
try {
const mailURL = getMetadata(serverNotification.metadata.MailUrl)
if (mailURL === undefined || mailURL === '') {
ctx.error('sendEmailNotification: Email service URL not configured')
ctx.error('Please provide email service url to enable email notifications.')
return
}
const mailAuth: string | undefined = getMetadata(serverNotification.metadata.MailAuthToken)
ctx.info('sendEmailNotification: Sending email', {
receiver,
subject,
mailURL,
hasAuth: mailAuth != null
})
const response = await fetch(concatLink(mailURL, '/send'), {
method: 'post',
keepalive: true,
@@ -149,24 +141,10 @@ export async function sendEmailNotification (
})
})
if (!response.ok) {
ctx.error('sendEmailNotification: Failed to send email', {
receiver,
subject,
status: response.status,
statusText: response.statusText
})
} else {
ctx.info('sendEmailNotification: Email sent successfully', {
receiver,
subject
})
ctx.error(`Failed to send email notification: ${response.statusText}`)
}
} catch (err) {
ctx.error('sendEmailNotification: Exception while sending email', {
err,
receiver,
subject
})
ctx.error('Could not send email notification', { err, receiver })
}
}
@@ -180,17 +158,6 @@ async function notifyByEmail (
data: InboxNotification,
message: ActivityMessage
): Promise<void> {
control.ctx.info('notifyByEmail: Preparing email notification', {
notificationId: data._id,
notificationUser: data.user,
type,
docId: doc._id,
docClass: doc._class,
email,
messageId: message._id,
senderSocialId
})
let senderName = sender !== undefined ? formatName(sender.name, control.branding?.lastNameFirst) : ''
if (senderName === '' && senderSocialId === core.account.System) {
senderName = 'System'
@@ -199,14 +166,6 @@ async function notifyByEmail (
if (content !== undefined) {
await sendEmailNotification(control.ctx, content.text, content.html, content.subject, email)
} else {
control.ctx.warn('notifyByEmail: No content generated for email', {
notificationId: data._id,
notificationUser: data.user,
type,
docId: doc._id,
email
})
}
}
@@ -234,50 +193,19 @@ async function getNotificationMessages (
}
async function processEmailNotifications (control: TriggerControl, notifications: InboxNotification[]): Promise<void> {
if (notifications.length === 0) {
control.ctx.info('processEmailNotifications: No notifications to process')
return
}
if (notifications.length === 0) return
const docId = notifications[0].objectId
const docClass = notifications[0].objectClass
control.ctx.info('processEmailNotifications: Starting email notification processing', {
docId,
docClass,
notificationCount: notifications.length
})
const doc = (await control.findAll(control.ctx, docClass, { _id: docId }))[0]
if (doc === undefined) {
control.ctx.warn('processEmailNotifications: Document not found', {
docId,
docClass
})
return
}
if (doc === undefined) return
const messages = await getNotificationMessages(notifications, control)
const { hierarchy } = control
control.ctx.info('processEmailNotifications: Retrieved activity messages', {
docId,
messageCount: messages.length
})
const senders = new Map<PersonId, Person>()
const skipped: Array<{
notificationId: Ref<InboxNotification>
user: AccountUuid
reason: string
employeeId?: Ref<Employee>
}> = []
let sentCount = 0
for (const n of notifications) {
const type = (n.types ?? [])[0]
if (type === undefined) {
skipped.push({ notificationId: n._id, user: n.user, reason: 'no_notification_type' })
continue
}
if (type === undefined) continue
let message: ActivityMessage | undefined
if (hierarchy.isDerived(n._class, notification.class.ActivityInboxNotification)) {
const activityNotification = n as ActivityInboxNotification
@@ -289,28 +217,28 @@ async function processEmailNotifications (control: TriggerControl, notifications
}
}
if (message === undefined) {
skipped.push({ notificationId: n._id, user: n.user, reason: 'activity_message_not_found' })
continue
}
if (message === undefined) continue
const employee = await getEmployeeByAcc(control, n.user)
if (employee === undefined) {
skipped.push({ notificationId: n._id, user: n.user, reason: 'employee_not_found' })
continue
}
const emails = await control.findAll(control.ctx, contact.class.SocialIdentity, {
if (employee === undefined) continue
const emailQuery = {
attachedTo: employee._id,
type: { $in: [SocialIdType.EMAIL, SocialIdType.GOOGLE] },
verifiedOn: { $gt: 0 },
isDeleted: { $ne: true }
})
}
let emails: SocialIdentity[] = []
try {
// Use rawFindAll to avoid filters from permission middleware
emails = await control.lowLevel.rawFindAll<SocialIdentity>('channel' as Domain, emailQuery, { limit: 10 })
} catch (err) {
// Fallback to regular findAll if lowLevel fails
control.ctx.warn('processEmailNotifications: Raw find all failed', { employeeId: employee._id, err })
const emailsResult = await control.findAll(control.ctx, contact.class.SocialIdentity, emailQuery)
emails = emailsResult as SocialIdentity[]
}
if (emails.length === 0) {
skipped.push({
notificationId: n._id,
user: n.user,
reason: 'no_verified_email_found',
employeeId: employee._id
})
control.ctx.warn('processEmailNotifications: No verified email found for employee', {
notificationId: n._id,
user: n.user,
@@ -319,15 +247,6 @@ async function processEmailNotifications (control: TriggerControl, notifications
continue
}
control.ctx.info('processEmailNotifications: Found verified email for user', {
notificationId: n._id,
user: n.user,
employeeId: employee._id,
email: emails[0].value,
emailType: emails[0].type,
totalEmails: emails.length
})
const senderSocialId = message.createdBy ?? message.modifiedBy
const sender = senders.get(senderSocialId) ?? (await getPerson(control, senderSocialId))
if (sender != null) {
@@ -335,23 +254,10 @@ async function processEmailNotifications (control: TriggerControl, notifications
}
await notifyByEmail(control, type, doc, sender, senderSocialId, emails[0].value, n, message)
sentCount++
}
control.ctx.info('processEmailNotifications: Completed email notification processing', {
docId,
totalNotifications: notifications.length,
sentCount,
skippedCount: skipped.length,
skipped
})
}
async function NotificationsHandler (txes: TxCreateDoc<InboxNotification>[], control: TriggerControl): Promise<Tx[]> {
control.ctx.info('NotificationsHandler: Processing email notifications', {
totalTxes: txes.length
})
const availableProviders: AvailableProvidersCache = control.contextCache.get(AvailableProvidersCacheKey) ?? new Map()
const all: InboxNotification[] = txes
@@ -360,35 +266,18 @@ async function NotificationsHandler (txes: TxCreateDoc<InboxNotification>[], con
(it) => availableProviders.get(it._id)?.find((p) => p === gmail.providers.EmailNotificationProvider) !== undefined
)
const filteredOut = txes.length - all.length
if (filteredOut > 0) {
control.ctx.info('NotificationsHandler: Filtered out notifications without email provider', {
filteredOut,
totalTxes: txes.length,
remaining: all.length
})
}
if (all.length === 0) {
control.ctx.info('NotificationsHandler: No notifications with email provider found')
return []
}
const notificationsByDocId = groupByArray(all, (n) => n.objectId)
control.ctx.info('NotificationsHandler: Grouped notifications by document', {
totalNotifications: all.length,
documentCount: notificationsByDocId.size
})
await Promise.all(
Array.from(notificationsByDocId.entries()).map(([docId, notifications]) =>
processEmailNotifications(control, notifications)
)
)
control.ctx.info('NotificationsHandler: Completed processing all email notifications')
return []
}