diff --git a/.changeset/chatty-camels-explain.md b/.changeset/chatty-camels-explain.md new file mode 100644 index 0000000000000..818dbaf10b29f --- /dev/null +++ b/.changeset/chatty-camels-explain.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/model-typings': patch +'@rocket.chat/models': patch +'@rocket.chat/meteor': patch +--- + +Fixes `/sendEmailAttachment` to support sending multiple file attachments in a single email diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts index 8ff7e4a0c5821..8d4a669762280 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts @@ -1,6 +1,7 @@ import { isIMessageInbox } from '@rocket.chat/core-typings'; -import type { IEmailInbox, IUser, IOmnichannelRoom, SlashCommandCallbackParams } from '@rocket.chat/core-typings'; +import type { IEmailInbox, IUser, IOmnichannelRoom, SlashCommandCallbackParams, IUpload } from '@rocket.chat/core-typings'; import { Messages, Uploads, LivechatRooms, Rooms, Users } from '@rocket.chat/models'; +import { isTruthy } from '@rocket.chat/tools'; import { Match } from 'meteor/check'; import type Mail from 'nodemailer/lib/mailer'; @@ -22,6 +23,18 @@ const getRocketCatUser = async (): Promise => Users.findOneById('r const language = settings.get('Language') || 'en'; const t = i18n.getFixedT(language); +async function buildMailAttachment(file: IUpload): Promise { + const buffer = await FileUpload.getBuffer(file); + if (!buffer) { + return; + } + return { + content: buffer, + contentType: file.type, + filename: file.name, + }; +} + // TODO: change these messages with room notifications const sendErrorReplyMessage = async (error: string, options: any) => { if (!options?.rid || !options?.msgId) { @@ -98,12 +111,16 @@ slashCommands.add({ } const message = await Messages.findOneById(params.trim()); - if (!message?.file) { + if (!message) { return; } - const room = await Rooms.findOneById(message.rid); + const fileRefs = (message.files || [message.file]).filter(isTruthy); + if (!fileRefs.length) { + return; + } + const room = await Rooms.findOneById(message.rid); if (!room?.email) { return; } @@ -118,37 +135,35 @@ slashCommands.add({ }); } - const file = await Uploads.findOneById(message.file._id); - - if (!file) { + const files = await Uploads.find({ _id: { $in: fileRefs.map((f) => f._id) } }).toArray(); + const emailAttachments = await Promise.all(files.map(buildMailAttachment)); + const validAttachments = emailAttachments.filter((a): a is Mail.Attachment => Boolean(a)); + if (validAttachments.length === 0) { return; } - const buffer = await FileUpload.getBuffer(file); - if (buffer) { - void sendEmail( - inbox, - { - to: room.email?.replyTo, - subject: room.email?.subject, - text: message?.attachments?.[0].description || '', - attachments: [ - { - content: buffer, - contentType: file.type, - filename: file.name, - }, - ], - inReplyTo: Array.isArray(room.email?.thread) ? room.email?.thread[0] : room.email?.thread, - references: ([] as string[]).concat(room.email?.thread || []), - }, - { - msgId: message._id, - sender: message.u.username, - rid: message.rid, - }, - ).then((info) => LivechatRooms.updateEmailThreadByRoomId(room._id, info.messageId)); - } + const emailText = + message?.attachments + ?.map((a) => a.description) + .filter(Boolean) + .join('\n\n') || ''; + + void sendEmail( + inbox, + { + to: room.email?.replyTo, + subject: room.email?.subject, + text: emailText, + attachments: validAttachments, + inReplyTo: Array.isArray(room.email?.thread) ? room.email?.thread[0] : room.email?.thread, + references: ([] as string[]).concat(room.email?.thread || []), + }, + { + msgId: message._id, + sender: message.u.username, + rid: message.rid, + }, + ).then((info) => LivechatRooms.updateEmailThreadByRoomId(room._id, info.messageId)); await Messages.updateOne( { _id: message._id }, diff --git a/packages/model-typings/src/models/IBaseUploadsModel.ts b/packages/model-typings/src/models/IBaseUploadsModel.ts index 332801dd50546..1161ab0fe86d9 100644 --- a/packages/model-typings/src/models/IBaseUploadsModel.ts +++ b/packages/model-typings/src/models/IBaseUploadsModel.ts @@ -10,6 +10,8 @@ export interface IBaseUploadsModel extends IBaseModel { confirmTemporaryFile(fileId: string, userId: string): Promise | undefined; + findByIds(_ids: string[], options?: FindOptions): FindCursor; + findOneByName(name: string, options?: { session?: ClientSession }): Promise; findOneByRoomId(rid: string): Promise; diff --git a/packages/models/src/models/BaseUploadModel.ts b/packages/models/src/models/BaseUploadModel.ts index 77614d162d36e..ba4165534c00d 100644 --- a/packages/models/src/models/BaseUploadModel.ts +++ b/packages/models/src/models/BaseUploadModel.ts @@ -92,6 +92,16 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.updateOne(filter, update); } + findByIds(_ids: string[], options?: FindOptions): FindCursor { + const query = { + _id: { + $in: _ids, + }, + }; + + return this.find(query, options); + } + async findOneByName(name: string, options?: { session?: ClientSession }): Promise { return this.findOne({ name }, { session: options?.session }); }