diff --git a/packages/twenty-emails/src/emails/clean-inactive-workspaces.email.tsx b/packages/twenty-emails/src/emails/clean-inactive-workspaces.email.tsx
deleted file mode 100644
index 0948a1b47..000000000
--- a/packages/twenty-emails/src/emails/clean-inactive-workspaces.email.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { BaseEmail } from 'src/components/BaseEmail';
-import { CallToAction } from 'src/components/CallToAction';
-import { HighlightedText } from 'src/components/HighlightedText';
-import { MainText } from 'src/components/MainText';
-import { Title } from 'src/components/Title';
-
-type CleanInactiveWorkspaceEmailProps = {
- daysLeft: number;
- userName: string;
- workspaceDisplayName: string;
-};
-
-export const CleanInactiveWorkspaceEmail = ({
- daysLeft,
- userName,
- workspaceDisplayName,
-}: CleanInactiveWorkspaceEmailProps) => {
- const dayOrDays = daysLeft > 1 ? 'days' : 'day';
- const remainingDays = daysLeft > 1 ? `${daysLeft} ` : '';
-
- const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello';
-
- return (
-
-
-
-
- {helloString},
-
-
- It appears that there has been a period of inactivity on your{' '}
- {workspaceDisplayName} workspace.
-
-
- Please note that the account is due for deactivation soon, and all
- associated data within this workspace will be deleted.
-
-
- No need for concern, though! Simply create or edit a record within the
- next {remainingDays}
- {dayOrDays} to retain access.
-
-
-
- );
-};
-
-export default CleanInactiveWorkspaceEmail;
diff --git a/packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx b/packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx
new file mode 100644
index 000000000..715768a71
--- /dev/null
+++ b/packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx
@@ -0,0 +1,41 @@
+import { BaseEmail } from 'src/components/BaseEmail';
+import { CallToAction } from 'src/components/CallToAction';
+import { MainText } from 'src/components/MainText';
+import { Title } from 'src/components/Title';
+
+type CleanSuspendedWorkspaceEmailProps = {
+ inactiveDaysBeforeDelete: number;
+ userName: string;
+ workspaceDisplayName: string | undefined;
+};
+
+export const CleanSuspendedWorkspaceEmail = ({
+ inactiveDaysBeforeDelete,
+ userName,
+ workspaceDisplayName,
+}: CleanSuspendedWorkspaceEmailProps) => {
+ const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello';
+
+ return (
+
+
+
+ {helloString},
+
+
+ Your workspace {workspaceDisplayName} has been deleted as your
+ subscription expired {inactiveDaysBeforeDelete} days ago.
+
+
+ All data in this workspace has been permanently deleted.
+
+
+ If you wish to use Twenty again, you can create a new workspace.
+
+
+
+ );
+};
diff --git a/packages/twenty-emails/src/emails/delete-inactive-workspaces.email.tsx b/packages/twenty-emails/src/emails/delete-inactive-workspaces.email.tsx
deleted file mode 100644
index c8ce348a3..000000000
--- a/packages/twenty-emails/src/emails/delete-inactive-workspaces.email.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { Column, Row, Section } from '@react-email/components';
-
-import { BaseEmail } from 'src/components/BaseEmail';
-import { MainText } from 'src/components/MainText';
-import { Title } from 'src/components/Title';
-
-type DeleteInactiveWorkspaceEmailData = {
- daysSinceInactive: number;
- workspaceId: string;
-};
-
-export const DeleteInactiveWorkspaceEmail = (
- workspacesToDelete: DeleteInactiveWorkspaceEmailData[],
-) => {
- const minDaysSinceInactive = Math.min(
- ...workspacesToDelete.map(
- (workspaceToDelete) => workspaceToDelete.daysSinceInactive,
- ),
- );
- return (
-
-
-
- List of workspaceIds inactive since at least{' '}
- {minDaysSinceInactive} days:
-
- {workspacesToDelete.map((workspaceToDelete) => {
- return (
-
- {workspaceToDelete.workspaceId}
-
- );
- })}
-
-
-
- );
-};
-
-export default DeleteInactiveWorkspaceEmail;
diff --git a/packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx b/packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx
new file mode 100644
index 000000000..10bf689ed
--- /dev/null
+++ b/packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx
@@ -0,0 +1,49 @@
+import { BaseEmail } from 'src/components/BaseEmail';
+import { CallToAction } from 'src/components/CallToAction';
+import { MainText } from 'src/components/MainText';
+import { Title } from 'src/components/Title';
+
+type WarnSuspendedWorkspaceEmailProps = {
+ daysSinceInactive: number;
+ inactiveDaysBeforeDelete: number;
+ userName: string;
+ workspaceDisplayName: string | undefined;
+};
+
+export const WarnSuspendedWorkspaceEmail = ({
+ daysSinceInactive,
+ inactiveDaysBeforeDelete,
+ userName,
+ workspaceDisplayName,
+}: WarnSuspendedWorkspaceEmailProps) => {
+ const daysLeft = inactiveDaysBeforeDelete - daysSinceInactive;
+ const dayOrDays = daysLeft > 1 ? 'days' : 'day';
+ const remainingDays = daysLeft > 0 ? `${daysLeft} ` : '';
+
+ const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello';
+
+ return (
+
+
+
+ {helloString},
+
+
+ It appears that your workspace {workspaceDisplayName} has been
+ suspended for {daysSinceInactive} days.
+
+
+ The workspace will be deactivated in {remainingDays} {dayOrDays}, and
+ all its data will be deleted.
+
+
+ If you wish to continue using Twenty, please update your subscription
+ within the next {remainingDays} {dayOrDays}.
+
+
+
+ );
+};
diff --git a/packages/twenty-emails/src/index.ts b/packages/twenty-emails/src/index.ts
index 5bb9cd0c9..9c25bea49 100644
--- a/packages/twenty-emails/src/index.ts
+++ b/packages/twenty-emails/src/index.ts
@@ -1,6 +1,6 @@
-export * from './emails/clean-inactive-workspaces.email';
-export * from './emails/delete-inactive-workspaces.email';
+export * from './emails/clean-suspended-workspace.email';
export * from './emails/password-reset-link.email';
export * from './emails/password-update-notify.email';
export * from './emails/send-email-verification-link.email';
export * from './emails/send-invite-link.email';
+export * from './emails/warn-suspended-workspace.email';
diff --git a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts
index 85375708e..df1afd094 100644
--- a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts
@@ -20,7 +20,6 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
-import { CleanInactiveWorkspaceJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job';
import { CleanSuspendedWorkspacesJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job';
import { CleanWorkspaceDeletionWarningUserVarsJob } from 'src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job';
import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module';
@@ -59,7 +58,6 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
FavoriteModule,
],
providers: [
- CleanInactiveWorkspaceJob,
CleanSuspendedWorkspacesJob,
EmailSenderJob,
DataSeedDemoWorkspaceJob,
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/clean-inactive-workspaces.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/clean-inactive-workspaces.command.ts
deleted file mode 100644
index 744f37bfb..000000000
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/clean-inactive-workspaces.command.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Command, CommandRunner, Option } from 'nest-commander';
-
-import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
-import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
-import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
-import { CleanInactiveWorkspaceJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job';
-
-export type CleanInactiveWorkspacesCommandOptions = {
- dryRun: boolean;
-};
-
-@Command({
- name: 'clean-inactive-workspaces',
- description: 'Clean inactive workspaces',
-})
-export class CleanInactiveWorkspacesCommand extends CommandRunner {
- constructor(
- @InjectMessageQueue(MessageQueue.cronQueue)
- private readonly messageQueueService: MessageQueueService,
- ) {
- super();
- }
-
- @Option({
- flags: '-d, --dry-run [dry run]',
- description: 'Dry run: Log cleaning actions without executing them.',
- required: false,
- })
- dryRun(value: string): boolean {
- return Boolean(value);
- }
-
- async run(
- _passedParam: string[],
- options: CleanInactiveWorkspacesCommandOptions,
- ): Promise {
- await this.messageQueueService.add(
- CleanInactiveWorkspaceJob.name,
- options,
- { retryLimit: 3 },
- );
- }
-}
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/start-clean-inactive-workspaces.cron.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/start-clean-inactive-workspaces.cron.command.ts
deleted file mode 100644
index d93cbf6b3..000000000
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/start-clean-inactive-workspaces.cron.command.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Command, CommandRunner } from 'nest-commander';
-
-import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
-import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
-import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
-import { cleanInactiveWorkspaceCronPattern } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.cron.pattern';
-import { CleanInactiveWorkspaceJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job';
-
-@Command({
- name: 'clean-inactive-workspaces:cron:start',
- description: 'Starts a cron job to clean inactive workspaces',
-})
-export class StartCleanInactiveWorkspacesCronCommand extends CommandRunner {
- constructor(
- @InjectMessageQueue(MessageQueue.cronQueue)
- private readonly messageQueueService: MessageQueueService,
- ) {
- super();
- }
-
- async run(): Promise {
- await this.messageQueueService.addCron(
- CleanInactiveWorkspaceJob.name,
- undefined,
- { retryLimit: 3, repeat: { pattern: cleanInactiveWorkspaceCronPattern } },
- );
- }
-}
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/stop-clean-inactive-workspaces.cron.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/stop-clean-inactive-workspaces.cron.command.ts
deleted file mode 100644
index fa0fd6668..000000000
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/stop-clean-inactive-workspaces.cron.command.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Command, CommandRunner } from 'nest-commander';
-
-import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
-import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
-import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
-import { cleanInactiveWorkspaceCronPattern } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.cron.pattern';
-import { CleanInactiveWorkspaceJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job';
-
-@Command({
- name: 'clean-inactive-workspaces:cron:stop',
- description: 'Stops the clean inactive workspaces cron job',
-})
-export class StopCleanInactiveWorkspacesCronCommand extends CommandRunner {
- constructor(
- @InjectMessageQueue(MessageQueue.cronQueue)
- private readonly messageQueueService: MessageQueueService,
- ) {
- super();
- }
-
- async run(): Promise {
- await this.messageQueueService.removeCron(
- CleanInactiveWorkspaceJob.name,
- cleanInactiveWorkspaceCronPattern,
- );
- }
-}
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.cron.pattern.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.cron.pattern.ts
deleted file mode 100644
index c1fdacec3..000000000
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.cron.pattern.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const cleanInactiveWorkspaceCronPattern = '0 22 * * *'; // Every day at 10pm
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job.ts
deleted file mode 100644
index ee883ec3a..000000000
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job.ts
+++ /dev/null
@@ -1,279 +0,0 @@
-import { Logger } from '@nestjs/common';
-import { InjectRepository } from '@nestjs/typeorm';
-
-import { render } from '@react-email/render';
-import {
- CleanInactiveWorkspaceEmail,
- DeleteInactiveWorkspaceEmail,
-} from 'twenty-emails';
-import { In, Repository } from 'typeorm';
-
-import { TypeORMService } from 'src/database/typeorm/typeorm.service';
-import { EmailService } from 'src/engine/core-modules/email/email.service';
-import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
-import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
-import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
-import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
-import { UserService } from 'src/engine/core-modules/user/services/user.service';
-import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
-import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
-import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
-import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
-import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
-import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
-import { CleanInactiveWorkspacesCommandOptions } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-inactive-workspaces.command';
-import { getDryRunLogHeader } from 'src/utils/get-dry-run-log-header';
-
-const MILLISECONDS_IN_ONE_DAY = 1000 * 3600 * 24;
-
-type WorkspaceToDeleteData = {
- workspaceId: string;
- daysSinceInactive: number;
-};
-
-@Processor(MessageQueue.cronQueue)
-export class CleanInactiveWorkspaceJob {
- private readonly logger = new Logger(CleanInactiveWorkspaceJob.name);
- private readonly inactiveDaysBeforeDelete;
- private readonly inactiveDaysBeforeEmail;
-
- constructor(
- @InjectRepository(Workspace, 'core')
- private readonly workspaceRepository: Repository,
- private readonly dataSourceService: DataSourceService,
- private readonly objectMetadataService: ObjectMetadataService,
- private readonly typeORMService: TypeORMService,
- private readonly userService: UserService,
- private readonly emailService: EmailService,
- private readonly environmentService: EnvironmentService,
- ) {
- this.inactiveDaysBeforeDelete = this.environmentService.get(
- 'WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION',
- );
- this.inactiveDaysBeforeEmail = this.environmentService.get(
- 'WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION',
- );
- }
-
- async getMostRecentUpdatedAt(
- dataSource: DataSourceEntity,
- objectsMetadata: ObjectMetadataEntity[],
- ): Promise {
- const tableNames = objectsMetadata
- .filter(
- (objectMetadata) =>
- objectMetadata.workspaceId === dataSource.workspaceId,
- )
- .map((objectMetadata) => computeObjectTargetTable(objectMetadata));
-
- const workspaceDataSource =
- await this.typeORMService.connectToDataSource(dataSource);
-
- let mostRecentUpdatedAtDate = new Date(0);
-
- for (const tableName of tableNames) {
- const mostRecentTableUpdatedAt = (
- await workspaceDataSource?.query(
- `SELECT MAX("updatedAt") FROM ${dataSource.schema}."${tableName}"`,
- )
- )?.[0]?.max;
-
- if (mostRecentTableUpdatedAt) {
- const mostRecentTableUpdatedAtDate = new Date(mostRecentTableUpdatedAt);
-
- if (
- !mostRecentUpdatedAtDate ||
- mostRecentTableUpdatedAtDate > mostRecentUpdatedAtDate
- ) {
- mostRecentUpdatedAtDate = mostRecentTableUpdatedAtDate;
- }
- }
- }
-
- return mostRecentUpdatedAtDate;
- }
-
- async warnWorkspaceUsers(
- dataSource: DataSourceEntity,
- daysSinceInactive: number,
- isDryRun: boolean,
- ) {
- const workspace = await this.workspaceRepository.findOne({
- where: { id: dataSource.workspaceId },
- });
-
- if (!workspace) {
- this.logger.error(
- `Workspace with id ${dataSource.workspaceId} not found in database`,
- );
-
- return;
- }
-
- const workspaceMembers =
- await this.userService.loadWorkspaceMembers(workspace);
-
- const workspaceDataSource =
- await this.typeORMService.connectToDataSource(dataSource);
-
- const displayName = (
- await workspaceDataSource?.query(
- `SELECT "displayName" FROM core.workspace WHERE id='${dataSource.workspaceId}'`,
- )
- )?.[0].displayName;
-
- this.logger.log(
- `${getDryRunLogHeader(isDryRun)}Sending workspace ${
- dataSource.workspaceId
- } inactive since ${daysSinceInactive} days emails to users ['${workspaceMembers
- .map((workspaceUser) => workspaceUser.userEmail)
- .join(', ')}']`,
- );
-
- if (isDryRun) {
- return;
- }
-
- workspaceMembers.forEach((workspaceMember) => {
- const emailData = {
- daysLeft: this.inactiveDaysBeforeDelete - daysSinceInactive,
- userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
- workspaceDisplayName: `${displayName}`,
- };
- const emailTemplate = CleanInactiveWorkspaceEmail(emailData);
- const html = render(emailTemplate, {
- pretty: true,
- });
- const text = render(emailTemplate, {
- plainText: true,
- });
-
- this.emailService.send({
- to: workspaceMember.userEmail,
- bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'),
- from: `${this.environmentService.get(
- 'EMAIL_FROM_NAME',
- )} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
- subject: 'Action Needed to Prevent Workspace Deletion',
- html,
- text,
- });
- });
- }
-
- chunkArray(array: any[], chunkSize = 6): any[][] {
- const chunkedArray: any[][] = [];
- let index = 0;
-
- while (index < array.length) {
- chunkedArray.push(array.slice(index, index + chunkSize));
- index += chunkSize;
- }
-
- return chunkedArray;
- }
-
- async sendDeleteWorkspaceEmail(
- workspacesToDelete: WorkspaceToDeleteData[],
- isDryRun: boolean,
- ): Promise {
- this.logger.log(
- `${getDryRunLogHeader(
- isDryRun,
- )}Sending email to delete workspaces "${workspacesToDelete
- .map((workspaceToDelete) => workspaceToDelete.workspaceId)
- .join('", "')}"`,
- );
-
- if (isDryRun || workspacesToDelete.length === 0) {
- return;
- }
-
- const emailTemplate = DeleteInactiveWorkspaceEmail(workspacesToDelete);
- const html = render(emailTemplate, {
- pretty: true,
- });
- const text = render(emailTemplate, {
- plainText: true,
- });
-
- await this.emailService.send({
- to: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'),
- from: `${this.environmentService.get(
- 'EMAIL_FROM_NAME',
- )} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
- subject: 'Action Needed to Delete Workspaces',
- html,
- text,
- });
- }
-
- @Process(CleanInactiveWorkspaceJob.name)
- async handle(data: CleanInactiveWorkspacesCommandOptions): Promise {
- const isDryRun = data.dryRun || false;
-
- const workspacesToDelete: WorkspaceToDeleteData[] = [];
-
- this.logger.log(`${getDryRunLogHeader(isDryRun)}Job running...`);
- if (!this.inactiveDaysBeforeDelete && !this.inactiveDaysBeforeEmail) {
- this.logger.log(
- `'WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION' and 'WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION' environment variables not set, please check this doc for more info: https://docs.twenty.com/start/self-hosting/environment-variables`,
- );
-
- return;
- }
-
- const dataSources =
- await this.dataSourceService.getManyDataSourceMetadata();
-
- const dataSourcesChunks = this.chunkArray(dataSources);
-
- this.logger.log(
- `${getDryRunLogHeader(isDryRun)}On ${
- dataSources.length
- } workspaces divided in ${dataSourcesChunks.length} chunks...`,
- );
-
- for (const dataSourcesChunk of dataSourcesChunks) {
- const objectsMetadata = await this.objectMetadataService.findMany({
- where: {
- dataSourceId: In(dataSourcesChunk.map((dataSource) => dataSource.id)),
- },
- });
-
- for (const dataSource of dataSourcesChunk) {
- this.logger.log(
- `${getDryRunLogHeader(isDryRun)}Cleaning Workspace ${
- dataSource.workspaceId
- }`,
- );
-
- const mostRecentUpdatedAt = await this.getMostRecentUpdatedAt(
- dataSource,
- objectsMetadata,
- );
- const daysSinceInactive = Math.floor(
- (new Date().getTime() - mostRecentUpdatedAt.getTime()) /
- MILLISECONDS_IN_ONE_DAY,
- );
-
- if (daysSinceInactive > this.inactiveDaysBeforeDelete) {
- workspacesToDelete.push({
- daysSinceInactive: daysSinceInactive,
- workspaceId: `${dataSource.workspaceId}`,
- });
- } else if (daysSinceInactive > this.inactiveDaysBeforeEmail) {
- await this.warnWorkspaceUsers(
- dataSource,
- daysSinceInactive,
- isDryRun,
- );
- }
- }
- }
-
- await this.sendDeleteWorkspaceEmail(workspacesToDelete, isDryRun);
-
- this.logger.log(`${getDryRunLogHeader(isDryRun)}job done!`);
- }
-}
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts
index f05f3a050..f5c756997 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts
@@ -1,11 +1,17 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
+import { render } from '@react-email/components';
import chunk from 'lodash.chunk';
+import {
+ CleanSuspendedWorkspaceEmail,
+ WarnSuspendedWorkspaceEmail,
+} from 'twenty-emails';
import { WorkspaceActivationStatus } from 'twenty-shared';
import { Repository } from 'typeorm';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
+import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
@@ -31,6 +37,7 @@ export class CleanSuspendedWorkspacesJob {
private readonly environmentService: EnvironmentService,
private readonly userService: UserService,
private readonly userVarsService: UserVarsService,
+ private readonly emailService: EmailService,
@InjectRepository(BillingSubscription, 'core')
private readonly billingSubscriptionRepository: Repository,
@InjectRepository(Workspace, 'core')
@@ -92,7 +99,38 @@ export class CleanSuspendedWorkspacesJob {
return false;
}
- async warnWorkspaceMembers(workspace: Workspace) {
+ async sendWarningEmail(
+ workspaceMember: WorkspaceMemberWorkspaceEntity,
+ workspaceDisplayName: string | undefined,
+ daysSinceInactive: number,
+ ) {
+ const emailData = {
+ daysSinceInactive,
+ inactiveDaysBeforeDelete: this.inactiveDaysBeforeDelete,
+ userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
+ workspaceDisplayName: `${workspaceDisplayName}`,
+ };
+ const emailTemplate = WarnSuspendedWorkspaceEmail(emailData);
+ const html = render(emailTemplate, {
+ pretty: true,
+ });
+ const text = render(emailTemplate, {
+ plainText: true,
+ });
+
+ this.emailService.send({
+ to: workspaceMember.userEmail,
+ bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'),
+ from: `${this.environmentService.get(
+ 'EMAIL_FROM_NAME',
+ )} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
+ subject: 'Action needed to prevent workspace deletion',
+ html,
+ text,
+ });
+ }
+
+ async warnWorkspaceMembers(workspace: Workspace, daysSinceInactive: number) {
const workspaceMembers =
await this.userService.loadWorkspaceMembers(workspace);
@@ -108,6 +146,14 @@ export class CleanSuspendedWorkspacesJob {
return;
} else {
+ this.logger.log(
+ `Sending ${workspace.id} ${
+ workspace.displayName
+ } suspended since ${daysSinceInactive} days emails to users ['${workspaceMembers
+ .map((workspaceUser) => workspaceUser.userEmail)
+ .join(', ')}']`,
+ );
+
const workspaceMembersChunks = chunk(workspaceMembers, 5);
for (const workspaceMembersChunk of workspaceMembersChunks) {
@@ -119,25 +165,61 @@ export class CleanSuspendedWorkspacesJob {
key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY,
value: true,
});
+
+ await this.sendWarningEmail(
+ workspaceMember,
+ workspace.displayName,
+ daysSinceInactive,
+ );
}),
);
}
- // TODO: issue #284
- // send email warning for deletion in (this.inactiveDaysBeforeDelete - this.inactiveDaysBeforeWarn) days (cci @twenty.com)
-
- this.logger.log(
- `Warning Workspace ${workspace.id} ${workspace.displayName}`,
- );
-
return;
}
}
+ async sendCleaningEmail(
+ workspaceMember: WorkspaceMemberWorkspaceEntity,
+ workspaceDisplayName: string | undefined,
+ ) {
+ const emailData = {
+ inactiveDaysBeforeDelete: this.inactiveDaysBeforeDelete,
+ userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
+ workspaceDisplayName: `${workspaceDisplayName}`,
+ };
+ const emailTemplate = CleanSuspendedWorkspaceEmail(emailData);
+ const html = render(emailTemplate, {
+ pretty: true,
+ });
+ const text = render(emailTemplate, {
+ plainText: true,
+ });
+
+ this.emailService.send({
+ to: workspaceMember.userEmail,
+ bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'),
+ from: `${this.environmentService.get(
+ 'EMAIL_FROM_NAME',
+ )} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
+ subject: 'Your workspace has been deleted',
+ html,
+ text,
+ });
+ }
+
async informWorkspaceMembersAndDeleteWorkspace(workspace: Workspace) {
const workspaceMembers =
await this.userService.loadWorkspaceMembers(workspace);
+ this.logger.log(
+ `Sending workspace ${workspace.id} ${
+ workspace.displayName
+ } deletion emails to users ['${workspaceMembers
+ .map((workspaceUser) => workspaceUser.userEmail)
+ .join(', ')}']`,
+ );
+
const workspaceMembersChunks = chunk(workspaceMembers, 5);
for (const workspaceMembersChunk of workspaceMembersChunks) {
@@ -148,12 +230,9 @@ export class CleanSuspendedWorkspacesJob {
workspaceId: workspace.id,
key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY,
});
- }),
- // TODO: issue #285
- // send email informing about deletion (cci @twenty.com)
- // remove clean-inactive-workspace.job.ts and .. files
- // add new env var in infra
+ await this.sendCleaningEmail(workspaceMember, workspace.displayName);
+ }),
);
}
@@ -197,7 +276,7 @@ export class CleanSuspendedWorkspacesJob {
workspaceInactivity > this.inactiveDaysBeforeWarn &&
workspaceInactivity <= this.inactiveDaysBeforeDelete
) {
- await this.warnWorkspaceMembers(workspace);
+ await this.warnWorkspaceMembers(workspace, workspaceInactivity);
return;
}
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts
index 34da9983e..fa18ef9ff 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts
@@ -4,11 +4,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
-import { CleanInactiveWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-inactive-workspaces.command';
import { CleanSuspendedWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.cron.command';
import { DeleteWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/delete-workspaces.command';
-import { StartCleanInactiveWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/start-clean-inactive-workspaces.cron.command';
-import { StopCleanInactiveWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/stop-clean-inactive-workspaces.cron.command';
@Module({
imports: [
@@ -16,12 +13,6 @@ import { StopCleanInactiveWorkspacesCronCommand } from 'src/engine/workspace-man
WorkspaceModule,
DataSourceModule,
],
- providers: [
- DeleteWorkspacesCommand,
- CleanInactiveWorkspacesCommand,
- StartCleanInactiveWorkspacesCronCommand,
- StopCleanInactiveWorkspacesCronCommand,
- CleanSuspendedWorkspacesCronCommand,
- ],
+ providers: [DeleteWorkspacesCommand, CleanSuspendedWorkspacesCronCommand],
})
export class WorkspaceCleanerModule {}