feat(emailing): local unsubscribe URL + full-content log driver (#22412)
In LOG mode, emit a working http://unsubscribe.<subdomain>.localhost/emailing/unsubscribe link (from SERVER_URL + workspace subdomain) instead of empty content, and log the full text/html body + List-Unsubscribe header so the flow is inspectable locally. buildUnsubscribeUrls now takes a full base URL (field renamed httpsUrl -> webUrl). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22412?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+34
-3
@@ -14,14 +14,23 @@ import {
|
||||
PutEmailIdentityDkimAttributesCommand,
|
||||
} from '@aws-sdk/client-sesv2';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { type AwsSesDriverConfig } from 'src/engine/core-modules/emailing-domain/drivers/interfaces/driver-config.interface';
|
||||
import {
|
||||
type EmailingDomainDriverInterface,
|
||||
type EmailingDomainResourceInput,
|
||||
type EmailingDomainVerificationResult,
|
||||
} from 'src/engine/core-modules/emailing-domain/drivers/interfaces/emailing-domain-driver.interface';
|
||||
import { type EmailingDomainSendEmailInput } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-input.type';
|
||||
import {
|
||||
EmailingDomainDriverException,
|
||||
EmailingDomainDriverExceptionCode,
|
||||
} from 'src/engine/core-modules/emailing-domain/drivers/exceptions/emailing-domain-driver.exception';
|
||||
import { type EmailingDomainSendEmailRequest } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-input.type';
|
||||
import { type EmailingDomainSendEmailResult } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-result.type';
|
||||
import { UnsubscribeHostnameStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/unsubscribe-hostname-status.type';
|
||||
import { type EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
|
||||
import { type UnsubscribeContentService } from 'src/engine/core-modules/emailing-domain/services/unsubscribe-content.service';
|
||||
|
||||
import { AWS_SES_RESOURCE_NAME_PREFIX } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/constants/aws-ses-resource-name-prefix.constant';
|
||||
import { type AwsSesClientProvider } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/providers/aws-ses-client.provider';
|
||||
@@ -40,6 +49,7 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
|
||||
private readonly awsSesHandleErrorService: AwsSesHandleErrorService,
|
||||
private readonly awsSesRegisterDomainService: AwsSesRegisterDomainService,
|
||||
private readonly awsSesSendEmailService: AwsSesSendEmailService,
|
||||
private readonly unsubscribeContentService: UnsubscribeContentService,
|
||||
) {}
|
||||
|
||||
async verifyDomain(
|
||||
@@ -127,14 +137,35 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
|
||||
}
|
||||
|
||||
async sendEmail(
|
||||
input: EmailingDomainSendEmailInput,
|
||||
input: EmailingDomainSendEmailRequest,
|
||||
): Promise<EmailingDomainSendEmailResult> {
|
||||
return this.awsSesSendEmailService.sendEmail(input, {
|
||||
const unsubscribeBaseUrl = this.getUnsubscribeBaseUrl(input.emailingDomain);
|
||||
const emailToSend = this.unsubscribeContentService.addTo(
|
||||
input,
|
||||
unsubscribeBaseUrl,
|
||||
);
|
||||
|
||||
return this.awsSesSendEmailService.sendEmail(emailToSend, {
|
||||
tenantName: this.buildTenantName(input.workspaceId),
|
||||
configurationSetName: this.buildConfigurationSetName(input.workspaceId),
|
||||
});
|
||||
}
|
||||
|
||||
private getUnsubscribeBaseUrl(emailingDomain: EmailingDomainEntity): string {
|
||||
if (
|
||||
emailingDomain.unsubscribeHostnameStatus !==
|
||||
UnsubscribeHostnameStatus.ACTIVE ||
|
||||
!isNonEmptyString(emailingDomain.unsubscribeHostname)
|
||||
) {
|
||||
throw new EmailingDomainDriverException(
|
||||
`Cannot send email for ${emailingDomain.domain}: unsubscribe domain is not active (status: ${emailingDomain.unsubscribeHostnameStatus})`,
|
||||
EmailingDomainDriverExceptionCode.UNSUBSCRIBE_NOT_READY,
|
||||
);
|
||||
}
|
||||
|
||||
return `https://${emailingDomain.unsubscribeHostname}`;
|
||||
}
|
||||
|
||||
async cleanupDomain(input: EmailingDomainResourceInput): Promise<void> {
|
||||
const sesClient = this.awsSesClientProvider.getSESClient();
|
||||
const tenantName = this.buildTenantName(input.workspaceId);
|
||||
|
||||
+3
@@ -10,6 +10,7 @@ import { AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domai
|
||||
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 { UnsubscribeContentService } from 'src/engine/core-modules/emailing-domain/services/unsubscribe-content.service';
|
||||
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';
|
||||
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
|
||||
@@ -25,6 +26,7 @@ export class EmailingDomainDriverFactory extends DriverFactoryBase<EmailingDomai
|
||||
private readonly awsSesRegisterDomainService: AwsSesRegisterDomainService,
|
||||
private readonly awsSesSendEmailService: AwsSesSendEmailService,
|
||||
private readonly logEmailingDomainDriver: LogEmailingDomainDriver,
|
||||
private readonly unsubscribeContentService: UnsubscribeContentService,
|
||||
) {
|
||||
super(twentyConfigService, configGroupHashService);
|
||||
}
|
||||
@@ -79,6 +81,7 @@ export class EmailingDomainDriverFactory extends DriverFactoryBase<EmailingDomai
|
||||
this.awsSesHandleErrorService,
|
||||
this.awsSesRegisterDomainService,
|
||||
this.awsSesSendEmailService,
|
||||
this.unsubscribeContentService,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { type EmailingDomainSendEmailInput } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-input.type';
|
||||
import { type EmailingDomainSendEmailRequest } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-input.type';
|
||||
import { type EmailingDomainSendEmailResult } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-result.type';
|
||||
import { type EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-status.type';
|
||||
import { type VerificationRecord } from 'src/engine/core-modules/emailing-domain/drivers/types/verifications-record';
|
||||
@@ -25,6 +25,6 @@ export interface EmailingDomainDriverInterface {
|
||||
registerDomain(input: EmailingDomainResourceInput): Promise<void>;
|
||||
cleanupDomain(input: EmailingDomainResourceInput): Promise<void>;
|
||||
sendEmail(
|
||||
input: EmailingDomainSendEmailInput,
|
||||
input: EmailingDomainSendEmailRequest,
|
||||
): Promise<EmailingDomainSendEmailResult>;
|
||||
}
|
||||
|
||||
+58
-6
@@ -1,20 +1,34 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { UNSUBSCRIBE_HOSTNAME_PREFIX } from 'src/engine/core-modules/emailing-domain/constants/unsubscribe-hostname-prefix.constant';
|
||||
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 } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-input.type';
|
||||
import { type EmailingDomainSendEmailRequest } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-input.type';
|
||||
import { type EmailingDomainSendEmailResult } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-result.type';
|
||||
import { UnsubscribeContentService } from 'src/engine/core-modules/emailing-domain/services/unsubscribe-content.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Injectable()
|
||||
export class LogEmailingDomainDriver implements EmailingDomainDriverInterface {
|
||||
private readonly logger = new Logger(LogEmailingDomainDriver.name);
|
||||
|
||||
constructor(
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly unsubscribeContentService: UnsubscribeContentService,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
) {}
|
||||
|
||||
async provisionWorkspace(workspaceId: string): Promise<void> {
|
||||
this.logger.log(`[log-driver] provisionWorkspace(${workspaceId})`);
|
||||
}
|
||||
@@ -56,21 +70,59 @@ export class LogEmailingDomainDriver implements EmailingDomainDriverInterface {
|
||||
}
|
||||
|
||||
async sendEmail(
|
||||
input: EmailingDomainSendEmailInput,
|
||||
input: EmailingDomainSendEmailRequest,
|
||||
): Promise<EmailingDomainSendEmailResult> {
|
||||
const unsubscribeBaseUrl = await this.getUnsubscribeBaseUrl(
|
||||
input.workspaceId,
|
||||
);
|
||||
const emailToSend = this.unsubscribeContentService.addTo(
|
||||
input,
|
||||
unsubscribeBaseUrl,
|
||||
);
|
||||
|
||||
const messageId = `log-${v4()}`;
|
||||
|
||||
const listUnsubscribe = emailToSend.headers?.find(
|
||||
(header) => header.name === 'List-Unsubscribe',
|
||||
)?.value;
|
||||
|
||||
this.logger.log(
|
||||
`[log-driver] sendEmail from=${input.from} to=${input.to.join(',')} subject="${input.subject}" → fake messageId=${messageId}`,
|
||||
`[log-driver] sendEmail → fake messageId=${messageId}\n` +
|
||||
`From: ${emailToSend.from}\n` +
|
||||
`To: ${emailToSend.to.join(',')}\n` +
|
||||
`Subject: ${emailToSend.subject}\n` +
|
||||
`List-Unsubscribe: ${listUnsubscribe ?? '(none)'}\n` +
|
||||
`Content Text: ${emailToSend.text}\n` +
|
||||
`Content HTML: ${emailToSend.html ?? '(none)'}`,
|
||||
);
|
||||
|
||||
return {
|
||||
messageId,
|
||||
deliveredRecipients: {
|
||||
to: input.to,
|
||||
cc: input.cc ?? [],
|
||||
bcc: input.bcc ?? [],
|
||||
to: emailToSend.to,
|
||||
cc: emailToSend.cc ?? [],
|
||||
bcc: emailToSend.bcc ?? [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getUnsubscribeBaseUrl(
|
||||
workspaceId: string,
|
||||
): Promise<string | null> {
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
|
||||
if (!isNonEmptyString(workspace?.subdomain)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseUrl = new URL(this.twentyConfigService.get('SERVER_URL'));
|
||||
|
||||
baseUrl.hostname = this.twentyConfigService.get('IS_MULTIWORKSPACE_ENABLED')
|
||||
? `${UNSUBSCRIBE_HOSTNAME_PREFIX}.${workspace.subdomain}.${baseUrl.hostname}`
|
||||
: `${UNSUBSCRIBE_HOSTNAME_PREFIX}.${baseUrl.hostname}`;
|
||||
|
||||
return baseUrl.origin;
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -1,6 +1,11 @@
|
||||
import { type EmailingDomainEmailContent } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-email-content.type';
|
||||
import { type EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
|
||||
|
||||
export type EmailingDomainSendEmailInput = EmailingDomainEmailContent & {
|
||||
workspaceId: string;
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export type EmailingDomainSendEmailRequest = EmailingDomainSendEmailInput & {
|
||||
emailingDomain: EmailingDomainEntity;
|
||||
};
|
||||
|
||||
+5
@@ -1,4 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
@@ -16,16 +17,19 @@ import { EmailingDomainResolver } from 'src/engine/core-modules/emailing-domain/
|
||||
import { EmailingDomainWorkspaceCleanupJob } from 'src/engine/core-modules/emailing-domain/jobs/emailing-domain-workspace-cleanup.job';
|
||||
import { EmailingDomainTenantStatusService } from 'src/engine/core-modules/emailing-domain/services/emailing-domain-tenant-status.service';
|
||||
import { EmailingDomainService } from 'src/engine/core-modules/emailing-domain/services/emailing-domain.service';
|
||||
import { UnsubscribeContentService } from 'src/engine/core-modules/emailing-domain/services/unsubscribe-content.service';
|
||||
import { UnsubscribeHostnameService } from 'src/engine/core-modules/emailing-domain/services/unsubscribe-hostname.service';
|
||||
import { UnsubscribeTokenService } from 'src/engine/core-modules/emailing-domain/services/unsubscribe-token.service';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { provideWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/provide-workspace-scoped-repository';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeORMModule,
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
NestjsQueryTypeOrmModule.forFeature([EmailingDomainEntity]),
|
||||
FeatureFlagModule,
|
||||
PermissionsModule,
|
||||
@@ -42,6 +46,7 @@ import { provideWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspac
|
||||
EmailingDomainService,
|
||||
EmailingDomainTenantStatusService,
|
||||
UnsubscribeTokenService,
|
||||
UnsubscribeContentService,
|
||||
UnsubscribeHostnameService,
|
||||
EmailingDomainResolver,
|
||||
EmailingDomainDriverFactory,
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { type EmailingDomainSendEmailInput } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-input.type';
|
||||
import { UnsubscribeTokenService } from 'src/engine/core-modules/emailing-domain/services/unsubscribe-token.service';
|
||||
import { buildUnsubscribeHeaders } from 'src/engine/core-modules/emailing-domain/utils/build-unsubscribe-headers.util';
|
||||
import { buildUnsubscribeHtmlFooter } from 'src/engine/core-modules/emailing-domain/utils/build-unsubscribe-html-footer.util';
|
||||
import { buildUnsubscribeTextFooter } from 'src/engine/core-modules/emailing-domain/utils/build-unsubscribe-text-footer.util';
|
||||
import { buildUnsubscribeUrls } from 'src/engine/core-modules/emailing-domain/utils/build-unsubscribe-urls.util';
|
||||
|
||||
@Injectable()
|
||||
export class UnsubscribeContentService {
|
||||
constructor(
|
||||
private readonly unsubscribeTokenService: UnsubscribeTokenService,
|
||||
) {}
|
||||
|
||||
addTo(
|
||||
email: EmailingDomainSendEmailInput,
|
||||
unsubscribeBaseUrl: string | null,
|
||||
): EmailingDomainSendEmailInput {
|
||||
if (!isNonEmptyString(unsubscribeBaseUrl)) {
|
||||
return email;
|
||||
}
|
||||
|
||||
const token = this.unsubscribeTokenService.sign({
|
||||
workspaceId: email.workspaceId,
|
||||
emailAddress: email.to[0],
|
||||
...(isNonEmptyString(email.unsubscribeTopicId)
|
||||
? { unsubscribeTopicId: email.unsubscribeTopicId }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const unsubscribeUrls = buildUnsubscribeUrls({
|
||||
unsubscribeBaseUrl,
|
||||
domain: email.domain,
|
||||
token,
|
||||
});
|
||||
|
||||
return {
|
||||
...email,
|
||||
text: `${email.text}${buildUnsubscribeTextFooter(unsubscribeUrls.webUrl)}`,
|
||||
html: isNonEmptyString(email.html)
|
||||
? `${email.html}${buildUnsubscribeHtmlFooter(unsubscribeUrls.webUrl)}`
|
||||
: email.html,
|
||||
headers: [
|
||||
...(email.headers ?? []),
|
||||
...buildUnsubscribeHeaders(unsubscribeUrls),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export type UnsubscribeUrls = {
|
||||
httpsUrl: string;
|
||||
webUrl: string;
|
||||
mailtoUrl: string;
|
||||
};
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@ import { type EmailingDomainHeader } from 'src/engine/core-modules/emailing-doma
|
||||
import { type UnsubscribeUrls } from 'src/engine/core-modules/emailing-domain/types/unsubscribe-urls.type';
|
||||
|
||||
export const buildUnsubscribeHeaders = ({
|
||||
httpsUrl,
|
||||
webUrl,
|
||||
mailtoUrl,
|
||||
}: UnsubscribeUrls): EmailingDomainHeader[] => [
|
||||
{ name: 'List-Unsubscribe', value: `<${httpsUrl}>, <${mailtoUrl}>` },
|
||||
{ name: 'List-Unsubscribe', value: `<${webUrl}>, <${mailtoUrl}>` },
|
||||
{ name: 'List-Unsubscribe-Post', value: 'List-Unsubscribe=One-Click' },
|
||||
];
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
export const buildUnsubscribeHtmlFooter = (httpsUrl: string): string =>
|
||||
`<hr style="margin-top:24px;border:none;border-top:1px solid #eee" /><p style="font-size:12px;color:#888">Don't want these emails? <a href="${httpsUrl}">Unsubscribe</a>.</p>`;
|
||||
export const buildUnsubscribeHtmlFooter = (webUrl: string): string =>
|
||||
`<hr style="margin-top:24px;border:none;border-top:1px solid #eee" /><p style="font-size:12px;color:#888">Don't want these emails? <a href="${webUrl}">Unsubscribe</a>.</p>`;
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
export const buildUnsubscribeTextFooter = (httpsUrl: string): string =>
|
||||
`\n\n--\nUnsubscribe: ${httpsUrl}`;
|
||||
export const buildUnsubscribeTextFooter = (webUrl: string): string =>
|
||||
`\n\n--\nUnsubscribe: ${webUrl}`;
|
||||
|
||||
+3
-3
@@ -2,16 +2,16 @@ import { UNSUBSCRIBE_MAILBOX_LOCAL_PART } from 'src/engine/core-modules/emailing
|
||||
import { type UnsubscribeUrls } from 'src/engine/core-modules/emailing-domain/types/unsubscribe-urls.type';
|
||||
|
||||
type BuildUnsubscribeUrlsArgs = {
|
||||
unsubscribeHostname: string;
|
||||
unsubscribeBaseUrl: string;
|
||||
domain: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export const buildUnsubscribeUrls = ({
|
||||
unsubscribeHostname,
|
||||
unsubscribeBaseUrl,
|
||||
domain,
|
||||
token,
|
||||
}: BuildUnsubscribeUrlsArgs): UnsubscribeUrls => ({
|
||||
httpsUrl: `https://${unsubscribeHostname}/emailing/unsubscribe?t=${token}`,
|
||||
webUrl: `${unsubscribeBaseUrl}/emailing/unsubscribe?t=${token}`,
|
||||
mailtoUrl: `mailto:${UNSUBSCRIBE_MAILBOX_LOCAL_PART}@${domain}?subject=${token}`,
|
||||
});
|
||||
|
||||
+5
-74
@@ -5,7 +5,6 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { MessageChannelType } from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { EMPTY_UNSUBSCRIBE_CONTENT } from 'src/engine/core-modules/emailing-domain/constants/empty-unsubscribe-content.constant';
|
||||
import {
|
||||
EmailingDomainDriverException,
|
||||
EmailingDomainDriverExceptionCode,
|
||||
@@ -14,21 +13,12 @@ import { EmailingDomainDriverFactory } from 'src/engine/core-modules/emailing-do
|
||||
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 { type EmailingDomainEmailContent } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-email-content.type';
|
||||
import { type EmailingDomainSendEmailInput } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-input.type';
|
||||
import { type EmailingDomainSendEmailRequest } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-input.type';
|
||||
import { type EmailingDomainSendEmailResult } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-result.type';
|
||||
import { UnsubscribeHostnameStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/unsubscribe-hostname-status.type';
|
||||
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-driver.type';
|
||||
import { EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
|
||||
import { MessageSuppressionService } from 'src/modules/emailing/services/message-suppression.service';
|
||||
import { UnsubscribeTokenService } from 'src/engine/core-modules/emailing-domain/services/unsubscribe-token.service';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type DeliverableRecipients } from 'src/engine/core-modules/emailing-domain/types/deliverable-recipients.type';
|
||||
import { type UnsubscribeContent } from 'src/engine/core-modules/emailing-domain/types/unsubscribe-content.type';
|
||||
import { buildUnsubscribeHeaders } from 'src/engine/core-modules/emailing-domain/utils/build-unsubscribe-headers.util';
|
||||
import { buildUnsubscribeHtmlFooter } from 'src/engine/core-modules/emailing-domain/utils/build-unsubscribe-html-footer.util';
|
||||
import { buildUnsubscribeTextFooter } from 'src/engine/core-modules/emailing-domain/utils/build-unsubscribe-text-footer.util';
|
||||
import { buildUnsubscribeUrls } from 'src/engine/core-modules/emailing-domain/utils/build-unsubscribe-urls.util';
|
||||
import { getDomainFromEmail } from 'src/utils/get-domain-from-email';
|
||||
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
|
||||
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
|
||||
@@ -41,8 +31,6 @@ export class EmailingDomainSenderService {
|
||||
private readonly emailingDomainRepository: WorkspaceScopedRepository<EmailingDomainEntity>,
|
||||
private readonly emailingDomainDriverFactory: EmailingDomainDriverFactory,
|
||||
private readonly messageSuppressionService: MessageSuppressionService,
|
||||
private readonly unsubscribeTokenService: UnsubscribeTokenService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
|
||||
) {}
|
||||
@@ -65,31 +53,18 @@ export class EmailingDomainSenderService {
|
||||
emailContent,
|
||||
);
|
||||
|
||||
const unsubscribe = this.buildUnsubscribeContent(
|
||||
workspaceId,
|
||||
emailingDomain,
|
||||
recipients.to[0],
|
||||
emailContent.unsubscribeTopicId,
|
||||
);
|
||||
|
||||
const replyTo = await this.resolveReplyTo(workspaceId, emailContent);
|
||||
|
||||
const emailToSend = {
|
||||
const emailToSend: EmailingDomainSendEmailRequest = {
|
||||
...emailContent,
|
||||
workspaceId,
|
||||
domain: emailingDomain.domain,
|
||||
from: emailContent.from,
|
||||
emailingDomain,
|
||||
replyTo,
|
||||
to: recipients.to,
|
||||
cc: recipients.cc,
|
||||
bcc: recipients.bcc,
|
||||
subject: emailContent.subject,
|
||||
text: `${emailContent.text}${unsubscribe.textFooter}`,
|
||||
html: isNonEmptyString(emailContent.html)
|
||||
? `${emailContent.html}${unsubscribe.htmlFooter}`
|
||||
: emailContent.html,
|
||||
attachments: emailContent.attachments,
|
||||
headers: [...(emailContent.headers ?? []), ...unsubscribe.headers],
|
||||
} as EmailingDomainSendEmailInput;
|
||||
};
|
||||
|
||||
return this.emailingDomainDriverFactory
|
||||
.getCurrentDriver()
|
||||
@@ -230,48 +205,4 @@ export class EmailingDomainSenderService {
|
||||
unsubscribeTopicId,
|
||||
);
|
||||
}
|
||||
|
||||
private buildUnsubscribeContent(
|
||||
workspaceId: string,
|
||||
emailingDomain: EmailingDomainEntity,
|
||||
primaryRecipient: string,
|
||||
unsubscribeTopicId: string | undefined,
|
||||
): UnsubscribeContent {
|
||||
const isDemoMode =
|
||||
this.twentyConfigService.get('EMAILING_DOMAIN_DRIVER') ===
|
||||
EmailingDomainDriver.LOG;
|
||||
|
||||
if (isDemoMode) {
|
||||
return EMPTY_UNSUBSCRIBE_CONTENT;
|
||||
}
|
||||
|
||||
if (
|
||||
emailingDomain.unsubscribeHostnameStatus !==
|
||||
UnsubscribeHostnameStatus.ACTIVE ||
|
||||
!isNonEmptyString(emailingDomain.unsubscribeHostname)
|
||||
) {
|
||||
throw new EmailingDomainDriverException(
|
||||
`Cannot send email for ${emailingDomain.domain}: unsubscribe domain is not active (status: ${emailingDomain.unsubscribeHostnameStatus})`,
|
||||
EmailingDomainDriverExceptionCode.UNSUBSCRIBE_NOT_READY,
|
||||
);
|
||||
}
|
||||
|
||||
const token = this.unsubscribeTokenService.sign({
|
||||
workspaceId,
|
||||
emailAddress: primaryRecipient,
|
||||
...(isNonEmptyString(unsubscribeTopicId) ? { unsubscribeTopicId } : {}),
|
||||
});
|
||||
|
||||
const unsubscribeUrls = buildUnsubscribeUrls({
|
||||
unsubscribeHostname: emailingDomain.unsubscribeHostname,
|
||||
domain: emailingDomain.domain,
|
||||
token,
|
||||
});
|
||||
|
||||
return {
|
||||
headers: buildUnsubscribeHeaders(unsubscribeUrls),
|
||||
textFooter: buildUnsubscribeTextFooter(unsubscribeUrls.httpsUrl),
|
||||
htmlFooter: buildUnsubscribeHtmlFooter(unsubscribeUrls.httpsUrl),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user