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:
Félix Malfait
2026-06-08 14:59:11 +02:00
committed by GitHub
parent b1b030c894
commit dfb3da1f8d
22 changed files with 724 additions and 608 deletions
@@ -1446,16 +1446,11 @@ type EmailingDomain {
createdAt: DateTime!
updatedAt: DateTime!
domain: String!
driver: EmailingDomainDriver!
status: EmailingDomainStatus!
verificationRecords: [VerificationRecord!]
verifiedAt: DateTime
}
enum EmailingDomainDriver {
AWS_SES
}
enum EmailingDomainStatus {
PENDING
VERIFIED
@@ -3226,7 +3221,7 @@ type Mutation {
resetPageLayoutToDefault(id: String!): PageLayout!
resetPageLayoutWidgetToDefault(id: String!): PageLayoutWidget!
resetPageLayoutTabToDefault(id: String!): PageLayoutTab!
createEmailingDomain(domain: String!, driver: EmailingDomainDriver!): EmailingDomain!
createEmailingDomain(domain: String!): EmailingDomain!
deleteEmailingDomain(id: String!): Boolean!
verifyEmailingDomain(id: String!): EmailingDomain!
sendEmailViaEmailingDomain(input: SendEmailViaDomainInput!): SendEmailViaDomainOutput!
@@ -1104,15 +1104,12 @@ export interface EmailingDomain {
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
domain: Scalars['String']
driver: EmailingDomainDriver
status: EmailingDomainStatus
verificationRecords?: VerificationRecord[]
verifiedAt?: Scalars['DateTime']
__typename: 'EmailingDomain'
}
export type EmailingDomainDriver = 'AWS_SES'
export type EmailingDomainStatus = 'PENDING' | 'VERIFIED' | 'FAILED' | 'TEMPORARY_FAILURE'
export interface SendEmailViaDomainOutput {
@@ -4064,7 +4061,6 @@ export interface EmailingDomainGenqlSelection{
createdAt?: boolean | number
updatedAt?: boolean | number
domain?: boolean | number
driver?: boolean | number
status?: boolean | number
verificationRecords?: VerificationRecordGenqlSelection
verifiedAt?: boolean | number
@@ -5838,7 +5834,7 @@ export interface MutationGenqlSelection{
resetPageLayoutToDefault?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String']} })
resetPageLayoutWidgetToDefault?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String']} })
resetPageLayoutTabToDefault?: (PageLayoutTabGenqlSelection & { __args: {id: Scalars['String']} })
createEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {domain: Scalars['String'], driver: EmailingDomainDriver} })
createEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {domain: Scalars['String']} })
deleteEmailingDomain?: { __args: {id: Scalars['String']} }
verifyEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {id: Scalars['String']} })
sendEmailViaEmailingDomain?: (SendEmailViaDomainOutputGenqlSelection & { __args: {input: SendEmailViaDomainInput} })
@@ -8732,10 +8728,6 @@ export const enumPageLayoutType = {
STANDALONE_PAGE: 'STANDALONE_PAGE' as const
}
export const enumEmailingDomainDriver = {
AWS_SES: 'AWS_SES' as const
}
export const enumEmailingDomainStatus = {
PENDING: 'PENDING' as const,
VERIFIED: 'VERIFIED' as const,
File diff suppressed because it is too large Load Diff
@@ -1464,7 +1464,6 @@ export type EmailingDomain = {
__typename?: 'EmailingDomain';
createdAt: Scalars['DateTime'];
domain: Scalars['String'];
driver: EmailingDomainDriver;
id: Scalars['UUID'];
status: EmailingDomainStatus;
updatedAt: Scalars['DateTime'];
@@ -1472,10 +1471,6 @@ export type EmailingDomain = {
verifiedAt?: Maybe<Scalars['DateTime']>;
};
export enum EmailingDomainDriver {
AWS_SES = 'AWS_SES'
}
export enum EmailingDomainStatus {
FAILED = 'FAILED',
PENDING = 'PENDING',
@@ -2695,7 +2690,6 @@ export type MutationCreateEmailGroupChannelArgs = {
export type MutationCreateEmailingDomainArgs = {
domain: Scalars['String'];
driver: EmailingDomainDriver;
};
@@ -7349,11 +7343,10 @@ export type FindManyPublicDomainsQuery = { __typename?: 'Query', findManyPublicD
export type CreateEmailingDomainMutationVariables = Exact<{
domain: Scalars['String'];
driver: EmailingDomainDriver;
}>;
export type CreateEmailingDomainMutation = { __typename?: 'Mutation', createEmailingDomain: { __typename?: 'EmailingDomain', id: string, domain: string, driver: EmailingDomainDriver, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string, verificationRecords?: Array<{ __typename?: 'VerificationRecord', type: string, key: string, value: string, priority?: number | null }> | null } };
export type CreateEmailingDomainMutation = { __typename?: 'Mutation', createEmailingDomain: { __typename?: 'EmailingDomain', id: string, domain: string, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string, verificationRecords?: Array<{ __typename?: 'VerificationRecord', type: string, key: string, value: string, priority?: number | null }> | null } };
export type DeleteEmailingDomainMutationVariables = Exact<{
id: Scalars['String'];
@@ -7367,12 +7360,12 @@ export type VerifyEmailingDomainMutationVariables = Exact<{
}>;
export type VerifyEmailingDomainMutation = { __typename?: 'Mutation', verifyEmailingDomain: { __typename?: 'EmailingDomain', id: string, domain: string, driver: EmailingDomainDriver, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string } };
export type VerifyEmailingDomainMutation = { __typename?: 'Mutation', verifyEmailingDomain: { __typename?: 'EmailingDomain', id: string, domain: string, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string } };
export type GetEmailingDomainsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetEmailingDomainsQuery = { __typename?: 'Query', getEmailingDomains: Array<{ __typename?: 'EmailingDomain', id: string, domain: string, driver: EmailingDomainDriver, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string, verificationRecords?: Array<{ __typename?: 'VerificationRecord', type: string, key: string, value: string, priority?: number | null }> | null }> };
export type GetEmailingDomainsQuery = { __typename?: 'Query', getEmailingDomains: Array<{ __typename?: 'EmailingDomain', id: string, domain: string, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string, verificationRecords?: Array<{ __typename?: 'VerificationRecord', type: string, key: string, value: string, priority?: number | null }> | null }> };
export type RefreshEnterpriseValidityTokenMutationVariables = Exact<{ [key: string]: never; }>;
@@ -8259,10 +8252,10 @@ export const CreatePublicDomainDocument = {"kind":"Document","definitions":[{"ki
export const DeletePublicDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeletePublicDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"domain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deletePublicDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"domain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"domain"}}}]}]}}]} as unknown as DocumentNode<DeletePublicDomainMutation, DeletePublicDomainMutationVariables>;
export const UpdatePublicDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdatePublicDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"domain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updatePublicDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"domain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"domain"}}},{"kind":"Argument","name":{"kind":"Name","value":"applicationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"isValidated"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode<UpdatePublicDomainMutation, UpdatePublicDomainMutationVariables>;
export const FindManyPublicDomainsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyPublicDomains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyPublicDomains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"isValidated"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode<FindManyPublicDomainsQuery, FindManyPublicDomainsQueryVariables>;
export const CreateEmailingDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateEmailingDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"domain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"driver"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EmailingDomainDriver"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createEmailingDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"domain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"domain"}}},{"kind":"Argument","name":{"kind":"Name","value":"driver"},"value":{"kind":"Variable","name":{"kind":"Name","value":"driver"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"driver"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"verifiedAt"}},{"kind":"Field","name":{"kind":"Name","value":"verificationRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<CreateEmailingDomainMutation, CreateEmailingDomainMutationVariables>;
export const CreateEmailingDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateEmailingDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"domain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createEmailingDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"domain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"domain"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"verifiedAt"}},{"kind":"Field","name":{"kind":"Name","value":"verificationRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<CreateEmailingDomainMutation, CreateEmailingDomainMutationVariables>;
export const DeleteEmailingDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteEmailingDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteEmailingDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode<DeleteEmailingDomainMutation, DeleteEmailingDomainMutationVariables>;
export const VerifyEmailingDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"VerifyEmailingDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"verifyEmailingDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"driver"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"verifiedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<VerifyEmailingDomainMutation, VerifyEmailingDomainMutationVariables>;
export const GetEmailingDomainsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetEmailingDomains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getEmailingDomains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"driver"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"verifiedAt"}},{"kind":"Field","name":{"kind":"Name","value":"verificationRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<GetEmailingDomainsQuery, GetEmailingDomainsQueryVariables>;
export const VerifyEmailingDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"VerifyEmailingDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"verifyEmailingDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"verifiedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<VerifyEmailingDomainMutation, VerifyEmailingDomainMutationVariables>;
export const GetEmailingDomainsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetEmailingDomains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getEmailingDomains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"verifiedAt"}},{"kind":"Field","name":{"kind":"Name","value":"verificationRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<GetEmailingDomainsQuery, GetEmailingDomainsQueryVariables>;
export const RefreshEnterpriseValidityTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RefreshEnterpriseValidityToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"refreshEnterpriseValidityToken"}}]}}]} as unknown as DocumentNode<RefreshEnterpriseValidityTokenMutation, RefreshEnterpriseValidityTokenMutationVariables>;
export const SetEnterpriseKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetEnterpriseKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"enterpriseKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setEnterpriseKey"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"enterpriseKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"enterpriseKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isValid"}},{"kind":"Field","name":{"kind":"Name","value":"licensee"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"subscriptionId"}}]}}]}}]} as unknown as DocumentNode<SetEnterpriseKeyMutation, SetEnterpriseKeyMutationVariables>;
export const EnterpriseCheckoutSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EnterpriseCheckoutSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"billingInterval"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enterpriseCheckoutSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"billingInterval"},"value":{"kind":"Variable","name":{"kind":"Name","value":"billingInterval"}}}]}]}}]} as unknown as DocumentNode<EnterpriseCheckoutSessionQuery, EnterpriseCheckoutSessionQueryVariables>;
@@ -1,14 +1,10 @@
import { gql } from '@apollo/client';
export const CREATE_EMAILING_DOMAIN = gql`
mutation CreateEmailingDomain(
$domain: String!
$driver: EmailingDomainDriver!
) {
createEmailingDomain(domain: $domain, driver: $driver) {
mutation CreateEmailingDomain($domain: String!) {
createEmailingDomain(domain: $domain) {
id
domain
driver
status
verifiedAt
verificationRecords {
@@ -5,7 +5,6 @@ export const VERIFY_EMAILING_DOMAIN = gql`
verifyEmailingDomain(id: $id) {
id
domain
driver
status
verifiedAt
createdAt
@@ -5,7 +5,6 @@ export const GET_ALL_EMAILING_DOMAINS = gql`
getEmailingDomains {
id
domain
driver
status
verifiedAt
verificationRecords {
@@ -11,10 +11,7 @@ import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { useMutation } from '@apollo/client/react';
import {
EmailingDomainDriver,
CreateEmailingDomainDocument,
} from '~/generated-metadata/graphql';
import { CreateEmailingDomainDocument } from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import {
settingsEmailingDomainFormSchema,
@@ -31,7 +28,6 @@ export const SettingsNewEmailingDomain = () => {
const [formValues, setFormValues] =
useState<SettingsEmailingDomainFormValues>({
driver: EmailingDomainDriver.AWS_SES,
domain: '',
});
@@ -54,7 +50,7 @@ export const SettingsNewEmailingDomain = () => {
const handleFieldChange = (
field: keyof SettingsEmailingDomainFormValues,
value: string | EmailingDomainDriver,
value: string,
) => {
setFormValues((prev) => ({
...prev,
@@ -82,7 +78,6 @@ export const SettingsNewEmailingDomain = () => {
await createEmailingDomain({
variables: {
domain: formValues.domain,
driver: formValues.driver,
},
onCompleted: (data) => {
enqueueSuccessSnackBar({
@@ -1,5 +1,4 @@
import { z } from 'zod';
import { EmailingDomainDriver } from '~/generated-metadata/graphql';
export const settingsEmailingDomainFormSchema = z.object({
domain: z
@@ -10,7 +9,6 @@ export const settingsEmailingDomainFormSchema = z.object({
'Invalid domain format. Please enter a valid domain name.',
)
.max(256, 'Domain must be less than 256 characters.'),
driver: z.nativeEnum(EmailingDomainDriver),
});
export type SettingsEmailingDomainFormValues = z.infer<
@@ -0,0 +1,30 @@
import { type QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { type FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
// The emailing domain driver is selected globally via the EMAILING_DOMAIN_DRIVER
// config variable; the per-row column was never read and is redundant.
@RegisteredInstanceCommand('2.11.0', 1780926908000)
export class DropEmailingDomainDriverColumnFastInstanceCommand implements FastInstanceCommand {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."emailingDomain" DROP COLUMN IF EXISTS "driver"`,
);
await queryRunner.query(
`DROP TYPE IF EXISTS "core"."emailingDomain_driver_enum"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "core"."emailingDomain_driver_enum" AS ENUM('AWS_SES')`,
);
await queryRunner.query(
`ALTER TABLE "core"."emailingDomain" ADD COLUMN IF NOT EXISTS "driver" "core"."emailingDomain_driver_enum" NOT NULL DEFAULT 'AWS_SES'`,
);
await queryRunner.query(
`ALTER TABLE "core"."emailingDomain" ALTER COLUMN "driver" DROP DEFAULT`,
);
}
}
@@ -59,6 +59,7 @@ import { EmailingDomainTenantStatusAndGlobalUniquenessFastInstanceCommand } from
import { AddLogicFunctionExecutionModeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-fast-1799000030000-add-logic-function-execution-mode';
import { EncryptNonSecretApplicationVariableSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1798400000000-encrypt-non-secret-application-variable';
import { MigrateAiModelPreferencesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1799000010000-migrate-ai-model-preferences';
import { DropEmailingDomainDriverColumnFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-11/2-11-instance-command-fast-1780926908000-drop-emailing-domain-driver-column';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -120,4 +121,5 @@ export const INSTANCE_COMMANDS = [
AddLogicFunctionExecutionModeFastInstanceCommand,
MigrateAiModelPreferencesSlowInstanceCommand,
EncryptNonSecretApplicationVariableSlowInstanceCommand,
DropEmailingDomainDriverColumnFastInstanceCommand,
];
@@ -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}`);
}
@@ -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,3 +1,4 @@
export enum EmailingDomainDriver {
AWS_SES = 'AWS_SES',
LOG = 'LOG',
}
@@ -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;
@@ -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),
@@ -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),
],
})
@@ -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,
);
@@ -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',
@@ -0,0 +1,46 @@
import { type QueryRunner } from 'typeorm';
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';
const tableName = 'emailingDomain';
const DEV_EMAILING_DOMAIN = 'dev.twenty.local';
type SeedEmailingDomainsArgs = {
queryRunner: QueryRunner;
schemaName: string;
workspaceId: string;
};
export const seedEmailingDomains = async ({
queryRunner,
schemaName,
workspaceId,
}: SeedEmailingDomainsArgs) => {
const domain = `${workspaceId.slice(0, 8)}.${DEV_EMAILING_DOMAIN}`;
await queryRunner.manager
.createQueryBuilder()
.insert()
.into(`${schemaName}.${tableName}`, [
'workspaceId',
'domain',
'status',
'verificationRecords',
'verifiedAt',
'tenantStatus',
])
.orIgnore()
.values([
{
workspaceId,
domain,
status: EmailingDomainStatus.VERIFIED,
verificationRecords: JSON.stringify([]),
verifiedAt: new Date(),
tenantStatus: EmailingDomainTenantStatus.ACTIVE,
},
])
.execute();
};
@@ -6,6 +6,7 @@ import { v4 } from 'uuid';
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-driver.type';
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
@@ -27,6 +28,7 @@ import {
import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service';
import { seedAgents } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-agents.util';
import { seedApiKeys } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-api-keys.util';
import { seedEmailingDomains } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-emailing-domains.util';
import { seedFeatureFlags } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util';
import { seedMetadataEntities } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-metadata-entities.util';
import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util';
@@ -319,6 +321,12 @@ export class DevSeederService {
await seedAgents({ queryRunner, schemaName, workspaceId });
await seedApiKeys({ queryRunner, schemaName, workspaceId });
if (
this.twentyConfigService.get('EMAILING_DOMAIN_DRIVER') ===
EmailingDomainDriver.LOG
) {
await seedEmailingDomains({ queryRunner, schemaName, workspaceId });
}
await seedFeatureFlags({ queryRunner, schemaName, workspaceId });
if (seedBilling) {