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:
neo773
2026-07-02 19:06:57 +05:30
committed by GitHub
parent 4aaf171d63
commit 7cf8b58f1b
13 changed files with 174 additions and 95 deletions
@@ -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);
@@ -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,
);
}
@@ -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>;
}
@@ -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;
}
}
@@ -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;
};
@@ -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,
@@ -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,4 +1,4 @@
export type UnsubscribeUrls = {
httpsUrl: string;
webUrl: string;
mailtoUrl: string;
};
@@ -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' },
];
@@ -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>`;
@@ -1,2 +1,2 @@
export const buildUnsubscribeTextFooter = (httpsUrl: string): string =>
`\n\n--\nUnsubscribe: ${httpsUrl}`;
export const buildUnsubscribeTextFooter = (webUrl: string): string =>
`\n\n--\nUnsubscribe: ${webUrl}`;
@@ -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,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),
};
}
}