diff --git a/.github/workflows/ci-utils.yaml b/.github/workflows/ci-utils.yaml
index 87f6a65681..c1625bc120 100644
--- a/.github/workflows/ci-utils.yaml
+++ b/.github/workflows/ci-utils.yaml
@@ -28,6 +28,6 @@ jobs:
- name: Utils / Install Dependencies
run: yarn
- name: Utils / Run Danger.js
- run: cd packages/twenty-utils && yarn danger ci --use-github-checks --failOnErrors
+ run: cd packages/twenty-utils && yarn nx danger:ci
env:
- DANGER_GITHUB_API_TOKEN: ${{ github.token }}
\ No newline at end of file
+ DANGER_GITHUB_API_TOKEN: ${{ github.token }}
diff --git a/package.json b/package.json
index d96b1ef6d2..5f356cb494 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,7 @@
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.merge": "^4.6.7",
"@types/mailparser": "^3.4.4",
+ "@types/nodemailer": "^6.4.14",
"add": "^2.0.6",
"afterframe": "^1.0.2",
"apollo-server-express": "^3.12.0",
@@ -103,6 +104,7 @@
"nest-commander": "^3.12.0",
"next": "14.0.4",
"next-mdx-remote": "^4.4.1",
+ "nodemailer": "^6.9.8",
"openapi-types": "^12.1.3",
"passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
diff --git a/packages/twenty-docs/docs/start/self-hosting/enviroment-variables.mdx b/packages/twenty-docs/docs/start/self-hosting/enviroment-variables.mdx
index 3a44a81e9a..926489ac93 100644
--- a/packages/twenty-docs/docs/start/self-hosting/enviroment-variables.mdx
+++ b/packages/twenty-docs/docs/start/self-hosting/enviroment-variables.mdx
@@ -6,6 +6,8 @@ sidebar_custom_props:
---
import OptionTable from '@site/src/theme/OptionTable'
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
## Frontend
@@ -56,6 +58,53 @@ import OptionTable from '@site/src/theme/OptionTable'
['FRONT_AUTH_CALLBACK_URL', 'http://localhost:3001/verify ', 'Callback used for Login page'],
]}>
+### Email
+
+
+
+#### Email SMTP Server configuration examples
+
+
+
+
+
+ You will need to provision an [App Password](https://support.google.com/accounts/answer/185833).
+ - EMAIL_SMTP_HOST=smtp.gmail.com
+ - EMAIL_SERVER_PORT=465
+ - EMAIL_SERVER_USER=gmail_email_address
+ - EMAIL_SERVER_PASSWORD='gmail_app_password'
+
+
+
+
+
+ Keep in mind that if you have 2FA enabled, you will need to provision an [App Password](https://support.microsoft.com/en-us/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9).
+ - EMAIL_SMTP_HOST=smtp.office365.com
+ - EMAIL_SERVER_PORT=587
+ - EMAIL_SERVER_USER=office365_email_address
+ - EMAIL_SERVER_PASSWORD='office365_password'
+
+
+
+
+
+ **smtp4dev** is a fake SMTP email server for development and testing.
+ - Run the smtp4dev image: `docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev`
+ - Access the smtp4dev ui here: [http://localhost:8090](http://localhost:8090)
+ - Set the following env variables:
+ - EMAIL_SERVER_HOST=localhost
+ - EMAIL_SERVER_PORT=2525
+
+
+
+
+
### Storage
@@ -96,6 +146,7 @@ import OptionTable from '@site/src/theme/OptionTable'
]}>
### Telemetry
+
;
+}
diff --git a/packages/twenty-server/src/integrations/email/drivers/logger.driver.ts b/packages/twenty-server/src/integrations/email/drivers/logger.driver.ts
new file mode 100644
index 0000000000..90ca586b91
--- /dev/null
+++ b/packages/twenty-server/src/integrations/email/drivers/logger.driver.ts
@@ -0,0 +1,20 @@
+import { Logger } from '@nestjs/common';
+
+import { SendMailOptions } from 'nodemailer';
+
+import { EmailDriver } from 'src/integrations/email/drivers/interfaces/email-driver.interface';
+
+export class LoggerDriver implements EmailDriver {
+ private readonly logger = new Logger(LoggerDriver.name);
+
+ async send(sendMailOptions: SendMailOptions): Promise {
+ const info =
+ `Sent email to: ${sendMailOptions.to}\n` +
+ `From: ${sendMailOptions.from}\n` +
+ `Subject: ${sendMailOptions.subject}\n` +
+ `Content Text: ${sendMailOptions.text}\n` +
+ `Content HTML: ${sendMailOptions.html}`;
+
+ this.logger.log(info);
+ }
+}
diff --git a/packages/twenty-server/src/integrations/email/drivers/smtp.driver.ts b/packages/twenty-server/src/integrations/email/drivers/smtp.driver.ts
new file mode 100644
index 0000000000..4cf1cbf0da
--- /dev/null
+++ b/packages/twenty-server/src/integrations/email/drivers/smtp.driver.ts
@@ -0,0 +1,16 @@
+import { createTransport, Transporter, SendMailOptions } from 'nodemailer';
+import SMTPConnection from 'nodemailer/lib/smtp-connection';
+
+import { EmailDriver } from 'src/integrations/email/drivers/interfaces/email-driver.interface';
+
+export class SmtpDriver implements EmailDriver {
+ private transport: Transporter;
+
+ constructor(options: SMTPConnection.Options) {
+ this.transport = createTransport(options);
+ }
+
+ async send(sendMailOptions: SendMailOptions): Promise {
+ await this.transport.sendMail(sendMailOptions);
+ }
+}
diff --git a/packages/twenty-server/src/integrations/email/email.constants.ts b/packages/twenty-server/src/integrations/email/email.constants.ts
new file mode 100644
index 0000000000..45649fcb22
--- /dev/null
+++ b/packages/twenty-server/src/integrations/email/email.constants.ts
@@ -0,0 +1 @@
+export const EMAIL_DRIVER = Symbol('EMAIL_DRIVER');
diff --git a/packages/twenty-server/src/integrations/email/email.module-factory.ts b/packages/twenty-server/src/integrations/email/email.module-factory.ts
new file mode 100644
index 0000000000..c7da0d8d8c
--- /dev/null
+++ b/packages/twenty-server/src/integrations/email/email.module-factory.ts
@@ -0,0 +1,40 @@
+import {
+ EmailDriver,
+ EmailModuleOptions,
+} from 'src/integrations/email/interfaces/email.interface';
+
+import { EnvironmentService } from 'src/integrations/environment/environment.service';
+
+export const emailModuleFactory = (
+ environmentService: EnvironmentService,
+): EmailModuleOptions => {
+ const driver = environmentService.getEmailDriver();
+
+ switch (driver) {
+ case EmailDriver.Logger: {
+ return;
+ }
+ case EmailDriver.Smtp: {
+ const host = environmentService.getEmailHost();
+ const port = environmentService.getEmailPort();
+ const user = environmentService.getEmailUser();
+ const pass = environmentService.getEmailPassword();
+
+ if (!(host && port)) {
+ throw new Error(
+ `${driver} email driver requires host: ${host} and port: ${port} to be defined, check your .env file`,
+ );
+ }
+
+ const auth = user && pass ? { user, pass } : undefined;
+
+ if (auth) {
+ return { host, port, auth };
+ }
+
+ return { host, port };
+ }
+ default:
+ throw new Error(`Invalid email driver (${driver}), check your .env file`);
+ }
+};
diff --git a/packages/twenty-server/src/integrations/email/email.module.ts b/packages/twenty-server/src/integrations/email/email.module.ts
new file mode 100644
index 0000000000..2e4c8d9a18
--- /dev/null
+++ b/packages/twenty-server/src/integrations/email/email.module.ts
@@ -0,0 +1,29 @@
+import { DynamicModule, Global } from '@nestjs/common';
+
+import { EmailModuleAsyncOptions } from 'src/integrations/email/interfaces/email.interface';
+
+import { EMAIL_DRIVER } from 'src/integrations/email/email.constants';
+import { LoggerDriver } from 'src/integrations/email/drivers/logger.driver';
+import { SmtpDriver } from 'src/integrations/email/drivers/smtp.driver';
+import { EmailService } from 'src/integrations/email/email.service';
+
+@Global()
+export class EmailModule {
+ static forRoot(options: EmailModuleAsyncOptions): DynamicModule {
+ const provider = {
+ provide: EMAIL_DRIVER,
+ useFactory: (...args: any[]) => {
+ const config = options.useFactory(...args);
+
+ return config ? new SmtpDriver(config) : new LoggerDriver();
+ },
+ inject: options.inject || [],
+ };
+
+ return {
+ module: EmailModule,
+ providers: [EmailService, provider],
+ exports: [EmailService],
+ };
+ }
+}
diff --git a/packages/twenty-server/src/integrations/email/email.service.ts b/packages/twenty-server/src/integrations/email/email.service.ts
new file mode 100644
index 0000000000..1c94c33b11
--- /dev/null
+++ b/packages/twenty-server/src/integrations/email/email.service.ts
@@ -0,0 +1,16 @@
+import { Inject, Injectable } from '@nestjs/common';
+
+import { SendMailOptions } from 'nodemailer';
+
+import { EmailDriver } from 'src/integrations/email/drivers/interfaces/email-driver.interface';
+
+import { EMAIL_DRIVER } from 'src/integrations/email/email.constants';
+
+@Injectable()
+export class EmailService implements EmailDriver {
+ constructor(@Inject(EMAIL_DRIVER) private driver: EmailDriver) {}
+
+ async send(sendMailOptions: SendMailOptions): Promise {
+ await this.driver.send(sendMailOptions);
+ }
+}
diff --git a/packages/twenty-server/src/integrations/email/interfaces/email.interface.ts b/packages/twenty-server/src/integrations/email/interfaces/email.interface.ts
new file mode 100644
index 0000000000..91d159262c
--- /dev/null
+++ b/packages/twenty-server/src/integrations/email/interfaces/email.interface.ts
@@ -0,0 +1,15 @@
+import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
+
+import SMTPConnection from 'nodemailer/lib/smtp-connection';
+
+export enum EmailDriver {
+ Logger = 'logger',
+ Smtp = 'smtp',
+}
+
+export type EmailModuleOptions = SMTPConnection.Options | undefined;
+
+export type EmailModuleAsyncOptions = {
+ useFactory: (...args: any[]) => EmailModuleOptions;
+} & Pick &
+ Pick;
diff --git a/packages/twenty-server/src/integrations/environment/environment.service.ts b/packages/twenty-server/src/integrations/environment/environment.service.ts
index 8f31defd82..7a6b1f318e 100644
--- a/packages/twenty-server/src/integrations/environment/environment.service.ts
+++ b/packages/twenty-server/src/integrations/environment/environment.service.ts
@@ -2,6 +2,8 @@
import { Injectable, LogLevel } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
+import { EmailDriver } from 'src/integrations/email/interfaces/email.interface';
+
import { LoggerDriverType } from 'src/integrations/logger/interfaces';
import { ExceptionHandlerDriver } from 'src/integrations/exception-handler/interfaces';
import { StorageDriverType } from 'src/integrations/file-storage/interfaces';
@@ -170,6 +172,28 @@ export class EnvironmentService {
);
}
+ getEmailDriver(): EmailDriver {
+ return (
+ this.configService.get('EMAIL_DRIVER') ?? EmailDriver.Logger
+ );
+ }
+
+ getEmailHost(): string | undefined {
+ return this.configService.get('EMAIL_SMTP_HOST');
+ }
+
+ getEmailPort(): number | undefined {
+ return this.configService.get('EMAIL_SMTP_PORT');
+ }
+
+ getEmailUser(): string | undefined {
+ return this.configService.get('EMAIL_SMTP_USER');
+ }
+
+ getEmailPassword(): string | undefined {
+ return this.configService.get('EMAIL_SMTP_PASSWORD');
+ }
+
getSupportDriver(): string {
return (
this.configService.get('SUPPORT_DRIVER') ?? SupportDriver.None
diff --git a/packages/twenty-server/src/integrations/integrations.module.ts b/packages/twenty-server/src/integrations/integrations.module.ts
index 5624bca094..51ac6f0895 100644
--- a/packages/twenty-server/src/integrations/integrations.module.ts
+++ b/packages/twenty-server/src/integrations/integrations.module.ts
@@ -6,6 +6,8 @@ import { exceptionHandlerModuleFactory } from 'src/integrations/exception-handle
import { fileStorageModuleFactory } from 'src/integrations/file-storage/file-storage.module-factory';
import { loggerModuleFactory } from 'src/integrations/logger/logger.module-factory';
import { messageQueueModuleFactory } from 'src/integrations/message-queue/message-queue.module-factory';
+import { emailModuleFactory } from 'src/integrations/email/email.module-factory';
+import { EmailModule } from 'src/integrations/email/email.module';
import { EnvironmentModule } from './environment/environment.module';
import { EnvironmentService } from './environment/environment.service';
@@ -32,6 +34,10 @@ import { MessageQueueModule } from './message-queue/message-queue.module';
useFactory: exceptionHandlerModuleFactory,
inject: [EnvironmentService, HttpAdapterHost],
}),
+ EmailModule.forRoot({
+ useFactory: emailModuleFactory,
+ inject: [EnvironmentService],
+ }),
],
exports: [],
providers: [],
diff --git a/packages/twenty-utils/package.json b/packages/twenty-utils/package.json
index 1345221fed..e7dce62e38 100644
--- a/packages/twenty-utils/package.json
+++ b/packages/twenty-utils/package.json
@@ -2,6 +2,8 @@
"name": "twenty-utils",
"private": true,
"scripts": {
+ "nx": "NX_DEFAULT_PROJECT=twenty-front node ../../node_modules/nx/bin/nx.js",
+ "danger:ci": "danger ci --use-github-checks --failOnErrors",
"release": "node release.js"
}
}
diff --git a/yarn.lock b/yarn.lock
index 4cd4327175..ae2b9428cf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -14422,6 +14422,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/nodemailer@npm:^6.4.14":
+ version: 6.4.14
+ resolution: "@types/nodemailer@npm:6.4.14"
+ dependencies:
+ "@types/node": "npm:*"
+ checksum: b5958843576cde76dc532aa7b726182fef8b466fa9fcaf1aa03f89f02e896bec4e28b593ffa1a289a46bd0b7fdf34da0640ab7ef8f0811948016f58f77e16307
+ languageName: node
+ linkType: hard
+
"@types/normalize-package-data@npm:^2.4.0":
version: 2.4.4
resolution: "@types/normalize-package-data@npm:2.4.4"
@@ -33184,7 +33193,7 @@ __metadata:
languageName: node
linkType: hard
-"nodemailer@npm:6.9.8":
+"nodemailer@npm:6.9.8, nodemailer@npm:^6.9.8":
version: 6.9.8
resolution: "nodemailer@npm:6.9.8"
checksum: 9332587975240ac648e1295b1df15e339fcace3f7fab8af0382e7f2dd10e48296344dfa698d58f1667f220f7fe13c779d55d39144c9cd9ed6f5f559714183c75
@@ -41568,6 +41577,7 @@ __metadata:
"@types/mailparser": "npm:^3.4.4"
"@types/ms": "npm:^0.7.31"
"@types/node": "npm:^20.10.6"
+ "@types/nodemailer": "npm:^6.4.14"
"@types/passport-google-oauth20": "npm:^2.0.11"
"@types/passport-jwt": "npm:^3.0.8"
"@types/react": "npm:^18.2.39"
@@ -41655,6 +41665,7 @@ __metadata:
nest-commander: "npm:^3.12.0"
next: "npm:14.0.4"
next-mdx-remote: "npm:^4.4.1"
+ nodemailer: "npm:^6.9.8"
nx: "npm:^17.2.8"
openapi-types: "npm:^12.1.3"
passport: "npm:^0.6.0"