From 737d26aea04862ea6a10e3bf5138b745e3df95f3 Mon Sep 17 00:00:00 2001 From: Gustavo Reis Bauer Date: Tue, 27 Aug 2024 10:14:15 -0300 Subject: [PATCH 1/3] feat: SMTP OTP bridge (#790) Co-authored-by: Douglas Gubert Co-authored-by: Rafael Tapia --- .../lib/accessors/modify/ModifyCreator.ts | 21 +++++++++++++ src/definition/accessors/IEmailCreator.ts | 10 +++++++ src/definition/accessors/IModifyCreator.ts | 6 ++++ src/definition/email/IEmail.ts | 9 ++++++ src/definition/email/index.ts | 1 + src/server/accessors/EmailCreator.ts | 11 +++++++ src/server/accessors/ModifyCreator.ts | 9 ++++++ src/server/bridges/AppBridges.ts | 4 +++ src/server/bridges/EmailBridge.ts | 30 +++++++++++++++++++ src/server/bridges/index.ts | 2 ++ src/server/permissions/AppPermissions.ts | 3 ++ tests/test-data/bridges/appBridges.ts | 9 ++++++ tests/test-data/bridges/emailBridge.ts | 8 +++++ 13 files changed, 123 insertions(+) create mode 100644 src/definition/accessors/IEmailCreator.ts create mode 100644 src/definition/email/IEmail.ts create mode 100644 src/server/accessors/EmailCreator.ts create mode 100644 src/server/bridges/EmailBridge.ts create mode 100644 tests/test-data/bridges/emailBridge.ts diff --git a/deno-runtime/lib/accessors/modify/ModifyCreator.ts b/deno-runtime/lib/accessors/modify/ModifyCreator.ts index c08b027ed..06797551a 100644 --- a/deno-runtime/lib/accessors/modify/ModifyCreator.ts +++ b/deno-runtime/lib/accessors/modify/ModifyCreator.ts @@ -1,5 +1,6 @@ import type { IModifyCreator } from '@rocket.chat/apps-engine/definition/accessors/IModifyCreator.ts'; import type { IUploadCreator } from '@rocket.chat/apps-engine/definition/accessors/IUploadCreator.ts'; +import type { IEmailCreator } from '@rocket.chat/apps-engine/definition/accessors/IEmailCreator.ts'; import type { ILivechatCreator } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator.ts'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; @@ -85,6 +86,26 @@ export class ModifyCreator implements IModifyCreator { ) as IUploadCreator; } + getEmailCreator(): IEmailCreator { + return new Proxy( + { __kind: 'getEmailCreator' }, + { + get: (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getCreator:getEmailCreator:${prop}`, + params + }) + .then((response) => response.result) + .catch((err) => { + throw new Error(err.error); + }), + } + ) + } + getBlockBuilder() { return new BlockBuilder(); } diff --git a/src/definition/accessors/IEmailCreator.ts b/src/definition/accessors/IEmailCreator.ts new file mode 100644 index 000000000..d5d051bc2 --- /dev/null +++ b/src/definition/accessors/IEmailCreator.ts @@ -0,0 +1,10 @@ +import type { IEmail } from '../email'; + +export interface IEmailCreator { + /** + * Sends an email through Rocket.Chat + * + * @param email the email data + */ + send(email: IEmail): Promise; +} diff --git a/src/definition/accessors/IModifyCreator.ts b/src/definition/accessors/IModifyCreator.ts index a92528334..45de797bd 100644 --- a/src/definition/accessors/IModifyCreator.ts +++ b/src/definition/accessors/IModifyCreator.ts @@ -12,6 +12,7 @@ import type { IRoomBuilder } from './IRoomBuilder'; import type { IUploadCreator } from './IUploadCreator'; import type { IUserBuilder } from './IUserBuilder'; import type { IVideoConferenceBuilder } from './IVideoConferenceBuilder'; +import type { IEmailCreator } from './IEmailCreator'; export interface IModifyCreator { /** @@ -25,6 +26,11 @@ export interface IModifyCreator { */ getUploadCreator(): IUploadCreator; + /** + * Gets the creator object responsible for email sending + */ + getEmailCreator(): IEmailCreator; + /** * @deprecated please prefer the rocket.chat/ui-kit components * diff --git a/src/definition/email/IEmail.ts b/src/definition/email/IEmail.ts new file mode 100644 index 000000000..ca81b23e5 --- /dev/null +++ b/src/definition/email/IEmail.ts @@ -0,0 +1,9 @@ +export interface IEmail { + to: string | string[]; + from: string; + replyTo?: string; + subject: string; + html?: string; + text?: string; + headers?: string; +} diff --git a/src/definition/email/index.ts b/src/definition/email/index.ts index 8b1319367..6074ebaec 100644 --- a/src/definition/email/index.ts +++ b/src/definition/email/index.ts @@ -1,3 +1,4 @@ export * from './IEmailDescriptor'; export * from './IPreEmailSent'; export * from './IPreEmailSentContext'; +export * from './IEmail'; diff --git a/src/server/accessors/EmailCreator.ts b/src/server/accessors/EmailCreator.ts new file mode 100644 index 000000000..6eda8e057 --- /dev/null +++ b/src/server/accessors/EmailCreator.ts @@ -0,0 +1,11 @@ +import type { AppBridges } from '../bridges'; +import type { IEmailCreator } from '../../definition/accessors/IEmailCreator'; +import type { IEmail } from '../../definition/email'; + +export class EmailCreator implements IEmailCreator { + constructor(private readonly bridges: AppBridges, private readonly appId: string) {} + + public async send(email: IEmail): Promise { + return this.bridges.getEmailBridge().doSendEmail(email, this.appId); + } +} diff --git a/src/server/accessors/ModifyCreator.ts b/src/server/accessors/ModifyCreator.ts index 04e6c3cbd..6e8fb324e 100644 --- a/src/server/accessors/ModifyCreator.ts +++ b/src/server/accessors/ModifyCreator.ts @@ -28,15 +28,20 @@ import { RoomBuilder } from './RoomBuilder'; import { UploadCreator } from './UploadCreator'; import { UserBuilder } from './UserBuilder'; import { VideoConferenceBuilder } from './VideoConferenceBuilder'; +import { EmailCreator } from './EmailCreator'; +import type { IEmailCreator } from '../../definition/accessors/IEmailCreator'; export class ModifyCreator implements IModifyCreator { private livechatCreator: LivechatCreator; private uploadCreator: UploadCreator; + private emailCreator: EmailCreator; + constructor(private readonly bridges: AppBridges, private readonly appId: string) { this.livechatCreator = new LivechatCreator(bridges, appId); this.uploadCreator = new UploadCreator(bridges, appId); + this.emailCreator = new EmailCreator(bridges, appId); } public getLivechatCreator(): ILivechatCreator { @@ -47,6 +52,10 @@ export class ModifyCreator implements IModifyCreator { return this.uploadCreator; } + public getEmailCreator(): IEmailCreator { + return this.emailCreator; + } + /** * @deprecated please prefer the rocket.chat/ui-kit components */ diff --git a/src/server/bridges/AppBridges.ts b/src/server/bridges/AppBridges.ts index 9cb647eb8..74f8d90fa 100644 --- a/src/server/bridges/AppBridges.ts +++ b/src/server/bridges/AppBridges.ts @@ -17,6 +17,7 @@ import type { RoleBridge } from './RoleBridge'; import type { RoomBridge } from './RoomBridge'; import type { SchedulerBridge } from './SchedulerBridge'; import type { ServerSettingBridge } from './ServerSettingBridge'; +import type { EmailBridge } from './EmailBridge'; import type { ThreadBridge } from './ThreadBridge'; import type { UiInteractionBridge } from './UiInteractionBridge'; import type { UploadBridge } from './UploadBridge'; @@ -37,6 +38,7 @@ export type Bridge = | RoomBridge | IInternalBridge | ServerSettingBridge + | EmailBridge | UploadBridge | UserBridge | UiInteractionBridge @@ -77,6 +79,8 @@ export abstract class AppBridges { public abstract getUploadBridge(): UploadBridge; + public abstract getEmailBridge(): EmailBridge; + public abstract getUserBridge(): UserBridge; public abstract getUiInteractionBridge(): UiInteractionBridge; diff --git a/src/server/bridges/EmailBridge.ts b/src/server/bridges/EmailBridge.ts new file mode 100644 index 000000000..c8a5b22e9 --- /dev/null +++ b/src/server/bridges/EmailBridge.ts @@ -0,0 +1,30 @@ +import type { IEmail } from '../../definition/email'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; +import { BaseBridge } from './BaseBridge'; + +export abstract class EmailBridge extends BaseBridge { + public async doSendEmail(email: IEmail, appId: string): Promise { + if (this.hasWritePermission(appId)) { + return this.sendEmail(email, appId); + } + } + + protected abstract sendEmail(email: IEmail, appId: string): Promise; + + private hasWritePermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.email.send)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.email.send], + }), + ); + + return false; + } +} diff --git a/src/server/bridges/index.ts b/src/server/bridges/index.ts index 5ecfcd96d..f5550e96f 100644 --- a/src/server/bridges/index.ts +++ b/src/server/bridges/index.ts @@ -4,6 +4,7 @@ import { AppBridges } from './AppBridges'; import { AppDetailChangesBridge } from './AppDetailChangesBridge'; import { CloudWorkspaceBridge } from './CloudWorkspaceBridge'; import { CommandBridge } from './CommandBridge'; +import { EmailBridge } from './EmailBridge'; import { EnvironmentalVariableBridge } from './EnvironmentalVariableBridge'; import { HttpBridge, IHttpBridgeRequestInfo } from './HttpBridge'; import { IInternalBridge } from './IInternalBridge'; @@ -40,6 +41,7 @@ export { ServerSettingBridge, UserBridge, UploadBridge, + EmailBridge, UiInteractionBridge, SchedulerBridge, AppBridges, diff --git a/src/server/permissions/AppPermissions.ts b/src/server/permissions/AppPermissions.ts index d9a1ecf6f..a0cc643c7 100644 --- a/src/server/permissions/AppPermissions.ts +++ b/src/server/permissions/AppPermissions.ts @@ -25,6 +25,9 @@ export const AppPermissions = { read: { name: 'upload.read' }, write: { name: 'upload.write' }, }, + email: { + send: { name: 'email.send' }, + }, ui: { interaction: { name: 'ui.interact' }, registerButtons: { name: 'ui.registerButtons' }, diff --git a/tests/test-data/bridges/appBridges.ts b/tests/test-data/bridges/appBridges.ts index 4636aa0fd..cf6a15133 100644 --- a/tests/test-data/bridges/appBridges.ts +++ b/tests/test-data/bridges/appBridges.ts @@ -22,6 +22,7 @@ import { AppBridges } from '../../../src/server/bridges'; import type { CloudWorkspaceBridge } from '../../../src/server/bridges/CloudWorkspaceBridge'; import type { IInternalFederationBridge } from '../../../src/server/bridges/IInternalFederationBridge'; import type { OAuthAppsBridge } from '../../../src/server/bridges/OAuthAppsBridge'; +import type { EmailBridge } from '../../../src/server/bridges/EmailBridge'; import type { ThreadBridge } from '../../../src/server/bridges/ThreadBridge'; import { TestsActivationBridge } from './activationBridge'; import { TestsApiBridge } from './apiBridge'; @@ -41,6 +42,7 @@ import { TestsRoleBridge } from './roleBridge'; import { TestsRoomBridge } from './roomBridge'; import { TestSchedulerBridge } from './schedulerBridge'; import { TestsServerSettingBridge } from './serverSettingBridge'; +import { TestsEmailBridge } from './emailBridge'; import { TestsThreadBridge } from './threadBridge'; import { TestsUiIntegrationBridge } from './uiIntegrationBridge'; import { TestUploadBridge } from './uploadBridge'; @@ -80,6 +82,8 @@ export class TestsAppBridges extends AppBridges { private readonly uploadBridge: TestUploadBridge; + private readonly emailBridge: EmailBridge; + private readonly uiIntegrationBridge: TestsUiIntegrationBridge; private readonly schedulerBridge: TestSchedulerBridge; @@ -119,6 +123,7 @@ export class TestsAppBridges extends AppBridges { this.oauthBridge = new TestOAuthAppsBridge(); this.internalFederationBridge = new TestsInternalFederationBridge(); this.threadBridge = new TestsThreadBridge(); + this.emailBridge = new TestsEmailBridge(); } public getCommandBridge(): TestsCommandBridge { @@ -189,6 +194,10 @@ export class TestsAppBridges extends AppBridges { return this.livechatBridge; } + public getEmailBridge(): EmailBridge { + return this.emailBridge; + } + public getUploadBridge(): UploadBridge { return this.uploadBridge; } diff --git a/tests/test-data/bridges/emailBridge.ts b/tests/test-data/bridges/emailBridge.ts new file mode 100644 index 000000000..4a86fc3a3 --- /dev/null +++ b/tests/test-data/bridges/emailBridge.ts @@ -0,0 +1,8 @@ +import type { IEmail } from '../../../src/definition/email'; +import { EmailBridge } from '../../../src/server/bridges/EmailBridge'; + +export class TestsEmailBridge extends EmailBridge { + protected sendEmail(email: IEmail, appId: string): Promise { + throw new Error('Method not implemented.'); + } +} From b951927241e5d0f0fee0535964bbe05d83343c3c Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Mon, 9 Sep 2024 14:42:44 -0300 Subject: [PATCH 2/3] Update actions/upload-artifact and actions/download-artifact --- .github/workflows/build_and_test.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index d6278d8f2..1fcf05a77 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -65,7 +65,7 @@ jobs: run: | tar czf /tmp/workspace.tar.gz . - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: workspace path: /tmp/workspace.tar.gz @@ -80,7 +80,7 @@ jobs: with: node-version: '14.19.3' - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 with: name: workspace path: /tmp @@ -102,7 +102,7 @@ jobs: with: node-version: '14.19.3' - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 with: name: workspace path: /tmp @@ -124,7 +124,7 @@ jobs: with: node-version: '14.19.3' - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 with: name: workspace path: /tmp @@ -140,7 +140,7 @@ jobs: run: | tar czf /tmp/workspace.tar.gz . - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: workspace path: /tmp/workspace.tar.gz @@ -156,7 +156,7 @@ jobs: with: node-version: '14.19.3' - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 with: name: workspace path: /tmp From 57236cb19b14f0980b5f84dc051be0910d43fd05 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Mon, 9 Sep 2024 15:14:46 -0300 Subject: [PATCH 3/3] Fix upload-artifact usage breaking change --- .github/workflows/build_and_test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 1fcf05a77..9f65042d3 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -144,6 +144,7 @@ jobs: with: name: workspace path: /tmp/workspace.tar.gz + overwrite: true publish: runs-on: ubuntu-latest