diff --git a/apps/meteor/app/api/server/lib/emailInbox.ts b/apps/meteor/app/api/server/lib/emailInbox.ts index 663459c7bc0..304d297261a 100644 --- a/apps/meteor/app/api/server/lib/emailInbox.ts +++ b/apps/meteor/app/api/server/lib/emailInbox.ts @@ -1,6 +1,8 @@ import type { IEmailInbox } from '@rocket.chat/core-typings'; import { EmailInbox, Users } from '@rocket.chat/models'; -import type { Filter, InsertOneResult, Sort, UpdateResult, WithId } from 'mongodb'; +import type { DeleteResult, Filter, InsertOneResult, Sort } from 'mongodb'; + +import { notifyOnEmailInboxChanged } from '../../../lib/server/lib/notifyListener'; export const findEmailInboxes = async ({ query = {}, @@ -34,33 +36,31 @@ export const findEmailInboxes = async ({ }; }; -export const findOneEmailInbox = async ({ _id }: { _id: string }): Promise => { - return EmailInbox.findOneById(_id); -}; export const insertOneEmailInbox = async ( userId: string, emailInboxParams: Pick, -): Promise>> => { +): Promise> => { const obj = { ...emailInboxParams, _createdAt: new Date(), _updatedAt: new Date(), _createdBy: await Users.findOneById(userId, { projection: { username: 1 } }), }; - return EmailInbox.insertOne(obj); + + const response = await EmailInbox.create(obj); + + if (response.insertedId) { + void notifyOnEmailInboxChanged({ _id: response.insertedId, ...obj }, 'inserted'); + } + + return response; }; export const updateEmailInbox = async ( emailInboxParams: Pick, -): Promise> | UpdateResult> => { +): Promise | null> => { const { _id, active, name, email, description, senderInfo, department, smtp, imap } = emailInboxParams; - const emailInbox = await findOneEmailInbox({ _id }); - - if (!emailInbox) { - throw new Error('error-invalid-email-inbox'); - } - const updateEmailInbox = { $set: { active, @@ -76,5 +76,29 @@ export const updateEmailInbox = async ( ...(department === 'All' && { $unset: { department: 1 as const } }), }; - return EmailInbox.updateOne({ _id }, updateEmailInbox); + const updatedResponse = await EmailInbox.updateById(_id, updateEmailInbox); + + if (!updatedResponse.value) { + throw new Error('error-invalid-email-inbox'); + } + + void notifyOnEmailInboxChanged( + { + ...updatedResponse.value, + ...(department === 'All' && { department: undefined }), + }, + 'updated', + ); + + return updatedResponse.value; +}; + +export const removeEmailInbox = async (emailInboxId: IEmailInbox['_id']): Promise => { + const removeResponse = await EmailInbox.removeById(emailInboxId); + + if (removeResponse.deletedCount) { + void notifyOnEmailInboxChanged({ _id: emailInboxId }, 'removed'); + } + + return removeResponse; }; diff --git a/apps/meteor/app/api/server/v1/email-inbox.ts b/apps/meteor/app/api/server/v1/email-inbox.ts index 5748565a0f7..89ede496b78 100644 --- a/apps/meteor/app/api/server/v1/email-inbox.ts +++ b/apps/meteor/app/api/server/v1/email-inbox.ts @@ -4,7 +4,7 @@ import { check, Match } from 'meteor/check'; import { sendTestEmailToInbox } from '../../../../server/features/EmailInbox/EmailInbox_Outgoing'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; -import { insertOneEmailInbox, findEmailInboxes, findOneEmailInbox, updateEmailInbox } from '../lib/emailInbox'; +import { insertOneEmailInbox, findEmailInboxes, updateEmailInbox, removeEmailInbox } from '../lib/emailInbox'; API.v1.addRoute( 'email-inbox.list', @@ -55,12 +55,23 @@ API.v1.addRoute( let _id: string; if (!emailInboxParams?._id) { - const emailInbox = await insertOneEmailInbox(this.userId, emailInboxParams); - _id = emailInbox.insertedId.toString(); + const { insertedId } = await insertOneEmailInbox(this.userId, emailInboxParams); + + if (!insertedId) { + return API.v1.failure('Failed to create email inbox'); + } + + _id = insertedId; } else { - _id = emailInboxParams._id; - await updateEmailInbox({ ...emailInboxParams, _id }); + const emailInbox = await updateEmailInbox({ ...emailInboxParams, _id: emailInboxParams._id }); + + if (!emailInbox?._id) { + return API.v1.failure('Failed to update email inbox'); + } + + _id = emailInbox._id; } + return API.v1.success({ _id }); }, }, @@ -79,7 +90,7 @@ API.v1.addRoute( if (!_id) { throw new Error('error-invalid-param'); } - const emailInbox = await findOneEmailInbox({ _id }); + const emailInbox = await EmailInbox.findOneById(_id); if (!emailInbox) { return API.v1.notFound(); @@ -97,11 +108,12 @@ API.v1.addRoute( throw new Error('error-invalid-param'); } - const emailInboxes = await EmailInbox.findOneById(_id); - if (!emailInboxes) { + const { deletedCount } = await removeEmailInbox(_id); + + if (!deletedCount) { return API.v1.notFound(); } - await EmailInbox.removeById(_id); + return API.v1.success({ _id }); }, }, @@ -120,7 +132,7 @@ API.v1.addRoute( // TODO: Chapter day backend - check if user has permission to view this email inbox instead of null values // TODO: Chapter day: Remove this endpoint and move search to GET /email-inbox - const emailInbox = await EmailInbox.findOne({ email }); + const emailInbox = await EmailInbox.findByEmail(email); return API.v1.success({ emailInbox }); }, @@ -140,7 +152,7 @@ API.v1.addRoute( if (!_id) { throw new Error('error-invalid-param'); } - const emailInbox = await findOneEmailInbox({ _id }); + const emailInbox = await EmailInbox.findOneById(_id); if (!emailInbox) { return API.v1.notFound(); diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts index df007342364..38b95404a13 100644 --- a/apps/meteor/app/lib/server/lib/notifyListener.ts +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -10,6 +10,7 @@ import type { IPbxEvent, LoginServiceConfiguration as LoginServiceConfigurationData, ILivechatPriority, + IEmailInbox, IIntegrationHistory, AtLeast, } from '@rocket.chat/core-typings'; @@ -266,6 +267,17 @@ export async function notifyOnIntegrationChangedByChannels( + data: Pick | T, // TODO: improve typing + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.emailInbox', { clientAction, id: data._id, data }); +} + export async function notifyOnIntegrationHistoryChanged( data: AtLeast, clientAction: ClientAction = 'updated', diff --git a/apps/meteor/server/database/watchCollections.ts b/apps/meteor/server/database/watchCollections.ts index 1f3628615eb..31691aab3b8 100644 --- a/apps/meteor/server/database/watchCollections.ts +++ b/apps/meteor/server/database/watchCollections.ts @@ -34,7 +34,6 @@ export function getWatchCollections(): string[] { LivechatInquiry.getCollectionName(), LivechatDepartmentAgents.getCollectionName(), InstanceStatus.getCollectionName(), - EmailInbox.getCollectionName(), Settings.getCollectionName(), Subscriptions.getCollectionName(), ]; @@ -49,6 +48,7 @@ export function getWatchCollections(): string[] { collections.push(Permissions.getCollectionName()); collections.push(LivechatPriority.getCollectionName()); collections.push(LoginServiceConfiguration.getCollectionName()); + collections.push(EmailInbox.getCollectionName()); collections.push(IntegrationHistory.getCollectionName()); } diff --git a/apps/meteor/server/email/IMAPInterceptor.ts b/apps/meteor/server/email/IMAPInterceptor.ts index 3371087d551..c599608cb18 100644 --- a/apps/meteor/server/email/IMAPInterceptor.ts +++ b/apps/meteor/server/email/IMAPInterceptor.ts @@ -6,6 +6,7 @@ import IMAP from 'imap'; import type { ParsedMail } from 'mailparser'; import { simpleParser } from 'mailparser'; +import { notifyOnEmailInboxChanged } from '../../app/lib/server/lib/notifyListener'; import { logger } from '../features/EmailInbox/logger'; type IMAPOptions = { @@ -221,9 +222,15 @@ export class IMAPInterceptor extends EventEmitter { async selfDisable(): Promise { logger.info(`Disabling inbox ${this.inboxId}`); + // Again, if there's 2 inboxes with the same email, this will prevent looping over the already disabled one // Active filter is just in case :) - await EmailInbox.findOneAndUpdate({ _id: this.inboxId, active: true }, { $set: { active: false } }); + const { value } = await EmailInbox.setDisabledById(this.inboxId); + + if (value) { + void notifyOnEmailInboxChanged(value, 'updated'); + } + logger.info(`IMAP inbox ${this.inboxId} automatically disabled`); } } diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox.ts b/apps/meteor/server/features/EmailInbox/EmailInbox.ts index 18b273c4301..f9c4422f88e 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox.ts @@ -18,9 +18,7 @@ export type Inbox = { export const inboxes = new Map(); export async function configureEmailInboxes(): Promise { - const emailInboxesCursor = EmailInbox.find({ - active: true, - }); + const emailInboxesCursor = EmailInbox.findActive(); logger.info('Clearing old email inbox registrations'); for (const { imap } of inboxes.values()) { diff --git a/apps/meteor/server/models/raw/EmailInbox.ts b/apps/meteor/server/models/raw/EmailInbox.ts index 138b7bc0296..edb17c8f861 100644 --- a/apps/meteor/server/models/raw/EmailInbox.ts +++ b/apps/meteor/server/models/raw/EmailInbox.ts @@ -1,6 +1,6 @@ import type { IEmailInbox, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { IEmailInboxModel } from '@rocket.chat/model-typings'; -import type { Collection, Db, IndexDescription } from 'mongodb'; +import type { Collection, Db, FindCursor, IndexDescription, InsertOneResult, ModifyResult, UpdateFilter } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -12,4 +12,27 @@ export class EmailInboxRaw extends BaseRaw implements IEmailInboxMo protected modelIndexes(): IndexDescription[] { return [{ key: { email: 1 }, unique: true }]; } + + async setDisabledById(id: IEmailInbox['_id']): Promise> { + return this.findOneAndUpdate({ _id: id, active: true }, { $set: { active: false } }, { returnDocument: 'after' }); + } + + async create(emailInbox: IEmailInbox): Promise> { + return this.insertOne(emailInbox); + } + + async updateById(id: IEmailInbox['_id'], data: UpdateFilter): Promise>> { + // findOneAndUpdate doesn't accept generics, so we had to type cast + return this.findOneAndUpdate({ _id: id }, data, { returnDocument: 'after', projection: { _id: 1 } }) as unknown as Promise< + ModifyResult> + >; + } + + findActive(): FindCursor { + return this.find({ active: true }); + } + + async findByEmail(email: IEmailInbox['email']): Promise { + return this.findOne({ email }); + } } diff --git a/packages/model-typings/src/models/IEmailInboxModel.ts b/packages/model-typings/src/models/IEmailInboxModel.ts index 1bb23584853..93145041af7 100644 --- a/packages/model-typings/src/models/IEmailInboxModel.ts +++ b/packages/model-typings/src/models/IEmailInboxModel.ts @@ -1,8 +1,12 @@ import type { IEmailInbox } from '@rocket.chat/core-typings'; +import type { FindCursor, InsertOneResult, ModifyResult, UpdateFilter } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface IEmailInboxModel extends IBaseModel { - // + setDisabledById(id: IEmailInbox['_id']): Promise>; + create(emailInbox: Omit): Promise>; + updateById(id: IEmailInbox['_id'], data: UpdateFilter): Promise>>; + findActive(): FindCursor; + findByEmail(email: IEmailInbox['email']): Promise; }