feat(emailing-domain): add LOG driver for local development (#21286)
## What Adds a `LOG` driver to the emailing-domain feature, selected via a new `EMAILING_DOMAIN_DRIVER` config variable (defaults to `AWS_SES`, so production behavior is unchanged). The LOG driver: - resolves domains to `VERIFIED` instantly (no DNS / SES setup) - logs each `sendEmail` and returns a synthetic `messageId` instead of calling SES It also dev-seeds a pre-verified domain per workspace (`<workspaceId>.dev.twenty.local`) so the feature works out of the box. ## Why The emailing-domain feature currently ships only the AWS SES driver, so the verify → send flow can't be exercised locally (or in CI) without real AWS credentials. This unblocks local development and review of anything built on emailing domains. ## Usage ``` EMAILING_DOMAIN_DRIVER=LOG ``` The seeded `*.dev.twenty.local` domain is already verified; sends are logged (`[log-driver] sendEmail ...`). --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
+18
-9
@@ -8,6 +8,7 @@ import { AwsSesRegisterDomainService } from 'src/engine/core-modules/emailing-do
|
||||
import { AwsSesDriver } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-driver.service';
|
||||
import { AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service';
|
||||
import { AwsSesSendEmailService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-send-email.service';
|
||||
import { LogEmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/log/services/log-emailing-domain-driver.service';
|
||||
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-driver.type';
|
||||
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
|
||||
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||
@@ -23,26 +24,31 @@ export class EmailingDomainDriverFactory extends DriverFactoryBase<EmailingDomai
|
||||
private readonly awsSesHandleErrorService: AwsSesHandleErrorService,
|
||||
private readonly awsSesRegisterDomainService: AwsSesRegisterDomainService,
|
||||
private readonly awsSesSendEmailService: AwsSesSendEmailService,
|
||||
private readonly logEmailingDomainDriver: LogEmailingDomainDriver,
|
||||
) {
|
||||
super(twentyConfigService, configGroupHashService);
|
||||
}
|
||||
|
||||
protected buildConfigKey(): string {
|
||||
const driver = EmailingDomainDriver.AWS_SES;
|
||||
const driver = this.twentyConfigService.get('EMAILING_DOMAIN_DRIVER');
|
||||
|
||||
if (driver === EmailingDomainDriver.AWS_SES) {
|
||||
const awsConfigHash = this.configGroupHashService.computeHash(
|
||||
ConfigVariablesGroup.AWS_SES_SETTINGS,
|
||||
);
|
||||
switch (driver) {
|
||||
case EmailingDomainDriver.AWS_SES: {
|
||||
const awsConfigHash = this.configGroupHashService.computeHash(
|
||||
ConfigVariablesGroup.AWS_SES_SETTINGS,
|
||||
);
|
||||
|
||||
return `aws-ses|${awsConfigHash}`;
|
||||
return `aws-ses|${awsConfigHash}`;
|
||||
}
|
||||
case EmailingDomainDriver.LOG:
|
||||
return 'log';
|
||||
default:
|
||||
throw new Error(`Unsupported emailing domain driver: ${driver}`);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported emailing domain driver: ${driver}`);
|
||||
}
|
||||
|
||||
protected createDriver(): EmailingDomainDriverInterface {
|
||||
const driver = EmailingDomainDriver.AWS_SES;
|
||||
const driver = this.twentyConfigService.get('EMAILING_DOMAIN_DRIVER');
|
||||
|
||||
switch (driver) {
|
||||
case EmailingDomainDriver.AWS_SES: {
|
||||
@@ -76,6 +82,9 @@ export class EmailingDomainDriverFactory extends DriverFactoryBase<EmailingDomai
|
||||
);
|
||||
}
|
||||
|
||||
case EmailingDomainDriver.LOG:
|
||||
return this.logEmailingDomainDriver;
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid emailing domain driver: ${driver}`);
|
||||
}
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
type EmailingDomainDriverInterface,
|
||||
type EmailingDomainResourceInput,
|
||||
type EmailingDomainVerificationResult,
|
||||
} from 'src/engine/core-modules/emailing-domain/drivers/interfaces/emailing-domain-driver.interface';
|
||||
import { EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-status.type';
|
||||
import {
|
||||
type EmailingDomainSendEmailInput,
|
||||
type EmailingDomainSendEmailResult,
|
||||
} from 'src/engine/core-modules/emailing-domain/drivers/types/send-email';
|
||||
|
||||
@Injectable()
|
||||
export class LogEmailingDomainDriver implements EmailingDomainDriverInterface {
|
||||
private readonly logger = new Logger(LogEmailingDomainDriver.name);
|
||||
|
||||
async provisionWorkspace(workspaceId: string): Promise<void> {
|
||||
this.logger.log(`[log-driver] provisionWorkspace(${workspaceId})`);
|
||||
}
|
||||
|
||||
async deprovisionWorkspace(workspaceId: string): Promise<void> {
|
||||
this.logger.log(`[log-driver] deprovisionWorkspace(${workspaceId})`);
|
||||
}
|
||||
|
||||
async verifyDomain(
|
||||
input: EmailingDomainResourceInput,
|
||||
): Promise<EmailingDomainVerificationResult> {
|
||||
this.logger.log(
|
||||
`[log-driver] verifyDomain(${input.domain}) → VERIFIED (instant)`,
|
||||
);
|
||||
|
||||
return {
|
||||
status: EmailingDomainStatus.VERIFIED,
|
||||
verificationRecords: [],
|
||||
};
|
||||
}
|
||||
|
||||
async getDomainStatus(
|
||||
input: EmailingDomainResourceInput,
|
||||
): Promise<EmailingDomainVerificationResult> {
|
||||
this.logger.log(`[log-driver] getDomainStatus(${input.domain}) → VERIFIED`);
|
||||
|
||||
return {
|
||||
status: EmailingDomainStatus.VERIFIED,
|
||||
verificationRecords: [],
|
||||
};
|
||||
}
|
||||
|
||||
async registerDomain(input: EmailingDomainResourceInput): Promise<void> {
|
||||
this.logger.log(`[log-driver] registerDomain(${input.domain})`);
|
||||
}
|
||||
|
||||
async cleanupDomain(input: EmailingDomainResourceInput): Promise<void> {
|
||||
this.logger.log(`[log-driver] cleanupDomain(${input.domain})`);
|
||||
}
|
||||
|
||||
async sendEmail(
|
||||
input: EmailingDomainSendEmailInput,
|
||||
): Promise<EmailingDomainSendEmailResult> {
|
||||
const messageId = `log-${v4()}`;
|
||||
|
||||
this.logger.log(
|
||||
`[log-driver] sendEmail from=${input.from} to=${input.to.join(',')} subject="${input.subject}" → fake messageId=${messageId}`,
|
||||
);
|
||||
|
||||
return { messageId };
|
||||
}
|
||||
}
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
export enum EmailingDomainDriver {
|
||||
AWS_SES = 'AWS_SES',
|
||||
LOG = 'LOG',
|
||||
}
|
||||
|
||||
-8
@@ -3,14 +3,9 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-driver.type';
|
||||
import { EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-status.type';
|
||||
import { VerificationRecordDTO } from 'src/engine/core-modules/emailing-domain/dtos/verification-record.dto';
|
||||
|
||||
registerEnumType(EmailingDomainDriver, {
|
||||
name: 'EmailingDomainDriver',
|
||||
});
|
||||
|
||||
registerEnumType(EmailingDomainStatus, {
|
||||
name: 'EmailingDomainStatus',
|
||||
});
|
||||
@@ -29,9 +24,6 @@ export class EmailingDomainDTO {
|
||||
@Field(() => String)
|
||||
domain: string;
|
||||
|
||||
@Field(() => EmailingDomainDriver)
|
||||
driver: EmailingDomainDriver;
|
||||
|
||||
@Field(() => EmailingDomainStatus)
|
||||
status: EmailingDomainStatus;
|
||||
|
||||
|
||||
-8
@@ -9,7 +9,6 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-driver.type';
|
||||
import { EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-status.type';
|
||||
import { EmailingDomainTenantStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-tenant-status.type';
|
||||
import { VerificationRecord } from 'src/engine/core-modules/emailing-domain/drivers/types/verifications-record';
|
||||
@@ -31,13 +30,6 @@ export class EmailingDomainEntity extends WorkspaceRelatedEntity {
|
||||
@Column({ type: 'varchar', nullable: false })
|
||||
domain: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: Object.values(EmailingDomainDriver),
|
||||
nullable: false,
|
||||
})
|
||||
driver: EmailingDomainDriver;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: Object.values(EmailingDomainStatus),
|
||||
|
||||
+2
@@ -8,6 +8,7 @@ import { AwsSesRegisterDomainService } from 'src/engine/core-modules/emailing-do
|
||||
import { AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service';
|
||||
import { AwsSesSendEmailService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-send-email.service';
|
||||
import { EmailingDomainDriverFactory } from 'src/engine/core-modules/emailing-domain/drivers/emailing-domain-driver.factory';
|
||||
import { LogEmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/log/services/log-emailing-domain-driver.service';
|
||||
import { EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
|
||||
import { EmailingDomainResolver } from 'src/engine/core-modules/emailing-domain/emailing-domain.resolver';
|
||||
import { EmailingDomainWorkspaceCleanupJob } from 'src/engine/core-modules/emailing-domain/jobs/emailing-domain-workspace-cleanup.job';
|
||||
@@ -34,6 +35,7 @@ import { provideWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspac
|
||||
AwsSesHandleErrorService,
|
||||
AwsSesRegisterDomainService,
|
||||
AwsSesSendEmailService,
|
||||
LogEmailingDomainDriver,
|
||||
provideWorkspaceScopedRepository(EmailingDomainEntity),
|
||||
],
|
||||
})
|
||||
|
||||
-3
@@ -5,7 +5,6 @@ import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-driver.type';
|
||||
import { EmailingDomainDTO } from 'src/engine/core-modules/emailing-domain/dtos/emailing-domain.dto';
|
||||
import { SendEmailViaDomainOutputDTO } from 'src/engine/core-modules/emailing-domain/dtos/send-email-via-domain-output.dto';
|
||||
import { SendEmailViaDomainInput } from 'src/engine/core-modules/emailing-domain/dtos/send-email-via-domain.input';
|
||||
@@ -34,13 +33,11 @@ export class EmailingDomainResolver {
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_EMAIL_GROUP_ENABLED)
|
||||
async createEmailingDomain(
|
||||
@Args('domain') domain: string,
|
||||
@Args('driver') driver: EmailingDomainDriver,
|
||||
@AuthWorkspace() currentWorkspace: WorkspaceEntity,
|
||||
): Promise<EmailingDomainDTO> {
|
||||
const emailingDomain =
|
||||
await this.emailingDomainService.createEmailingDomain(
|
||||
domain,
|
||||
driver,
|
||||
currentWorkspace,
|
||||
);
|
||||
|
||||
|
||||
-3
@@ -5,7 +5,6 @@ import {
|
||||
EmailingDomainDriverExceptionCode,
|
||||
} from 'src/engine/core-modules/emailing-domain/drivers/exceptions/emailing-domain-driver.exception';
|
||||
import { EmailingDomainDriverFactory } from 'src/engine/core-modules/emailing-domain/drivers/emailing-domain-driver.factory';
|
||||
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-driver.type';
|
||||
import { EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-status.type';
|
||||
import { EmailingDomainTenantStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-tenant-status.type';
|
||||
import {
|
||||
@@ -28,7 +27,6 @@ export class EmailingDomainService {
|
||||
|
||||
async createEmailingDomain(
|
||||
domain: string,
|
||||
driverType: EmailingDomainDriver,
|
||||
workspace: WorkspaceEntity,
|
||||
): Promise<EmailingDomainEntity> {
|
||||
const existingEmailingDomain = await this.emailingDomainRepository.findOne(
|
||||
@@ -65,7 +63,6 @@ export class EmailingDomainService {
|
||||
|
||||
return this.emailingDomainRepository.save(workspace.id, {
|
||||
domain,
|
||||
driver: driverType,
|
||||
status: verificationResult.status,
|
||||
verificationRecords: verificationResult.verificationRecords,
|
||||
verifiedAt: isVerifiedOnCreation ? new Date() : null,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/
|
||||
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
||||
import { CodeInterpreterDriverType } from 'src/engine/core-modules/code-interpreter/code-interpreter.interface';
|
||||
import { EmailDriver } from 'src/engine/core-modules/email/enums/email-driver.enum';
|
||||
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-driver.type';
|
||||
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
import { StorageDriverType } from 'src/engine/core-modules/file-storage/interfaces';
|
||||
import { LoggerDriverType } from 'src/engine/core-modules/logger/interfaces';
|
||||
@@ -1697,6 +1698,16 @@ export class ConfigVariables {
|
||||
@IsOptional()
|
||||
MINTLIFY_SUBDOMAIN: string;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.AWS_SES_SETTINGS,
|
||||
description:
|
||||
'Driver used for the emailing domain feature — AWS_SES for production, LOG for local development (no AWS credentials needed)',
|
||||
type: ConfigVariableType.ENUM,
|
||||
options: Object.values(EmailingDomainDriver),
|
||||
})
|
||||
@CastToUpperSnakeCase()
|
||||
EMAILING_DOMAIN_DRIVER: EmailingDomainDriver = EmailingDomainDriver.AWS_SES;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.AWS_SES_SETTINGS,
|
||||
description: 'AWS region',
|
||||
|
||||
Reference in New Issue
Block a user