feat(email-group): communications UX, per-record DNS status (#23002)
- Rename Communications label to singular, remove docs-home banner - Provision unsubscribe Cloudflare records at domain creation and surface per-record status badges; skip Cloudflare when not configured - Move sending-domain status into the section header and only show the records table when a record is unverified - Reply to the original recipients when replying to your own message - Fix DNS records table column/badge alignment; emit synthetic records in the log driver for local testing <img width="1496" height="849" alt="Screenshot 2026-07-17 at 7 47 24 PM" src="https://github.com/user-attachments/assets/a5a59adb-2df4-4154-98b2-acf87a8008da" />
This commit is contained in:
+4
@@ -37,6 +37,10 @@ export class DnsManagerService {
|
||||
}
|
||||
}
|
||||
|
||||
isConfigured(): boolean {
|
||||
return isDefined(this.cloudflareClient);
|
||||
}
|
||||
|
||||
async registerHostname(customDomain: string, options?: DnsManagerOptions) {
|
||||
dnsManagerValidator.isCloudflareInstanceDefined(this.cloudflareClient);
|
||||
|
||||
|
||||
+23
-7
@@ -60,7 +60,7 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
|
||||
|
||||
const tenantName = this.buildTenantName(input.workspaceId);
|
||||
|
||||
const { isVerified, verificationRecords } =
|
||||
const { isVerified, status, verificationRecords } =
|
||||
await this.createOrUpdateEmailIdentity(input.domain, tenantName);
|
||||
|
||||
if (isVerified) {
|
||||
@@ -68,10 +68,8 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
|
||||
}
|
||||
|
||||
return {
|
||||
status: isVerified
|
||||
? EmailingDomainStatus.VERIFIED
|
||||
: EmailingDomainStatus.PENDING,
|
||||
verificationRecords,
|
||||
status,
|
||||
verificationRecords: this.withRecordStatus(verificationRecords, status),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to verify domain ${input.domain}: ${error}`);
|
||||
@@ -101,7 +99,7 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
|
||||
|
||||
return {
|
||||
status,
|
||||
verificationRecords,
|
||||
verificationRecords: this.withRecordStatus(verificationRecords, status),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundException) {
|
||||
@@ -252,6 +250,7 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
|
||||
tenantName: string,
|
||||
): Promise<{
|
||||
isVerified: boolean;
|
||||
status: EmailingDomainStatus;
|
||||
verificationRecords: VerificationRecordDTO[];
|
||||
}> {
|
||||
const sesClient = this.awsSesClientProvider.getSESClient();
|
||||
@@ -263,6 +262,7 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
|
||||
const existingIdentity = await sesClient.send(getIdentityCommand);
|
||||
|
||||
const isVerified = existingIdentity.VerifiedForSendingStatus === true;
|
||||
const status = this.determineVerificationStatus(existingIdentity);
|
||||
const verificationRecords = this.buildVerificationRecords(
|
||||
domain,
|
||||
existingIdentity.DkimAttributes?.Tokens || [],
|
||||
@@ -270,7 +270,7 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
|
||||
|
||||
await this.associateResourceWithTenant(domain, tenantName);
|
||||
|
||||
return { isVerified, verificationRecords };
|
||||
return { isVerified, status, verificationRecords };
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundException) {
|
||||
return await this.createNewEmailIdentity(domain, tenantName);
|
||||
@@ -284,6 +284,7 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
|
||||
tenantName: string,
|
||||
): Promise<{
|
||||
isVerified: boolean;
|
||||
status: EmailingDomainStatus;
|
||||
verificationRecords: VerificationRecordDTO[];
|
||||
}> {
|
||||
const sesClient = this.awsSesClientProvider.getSESClient();
|
||||
@@ -305,6 +306,7 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
|
||||
|
||||
return {
|
||||
isVerified: false,
|
||||
status: EmailingDomainStatus.PENDING,
|
||||
verificationRecords,
|
||||
};
|
||||
}
|
||||
@@ -383,4 +385,18 @@ export class AwsSesDriver implements EmailingDomainDriverInterface {
|
||||
|
||||
return EmailingDomainStatus.PENDING;
|
||||
}
|
||||
|
||||
private withRecordStatus(
|
||||
records: VerificationRecordDTO[],
|
||||
status: EmailingDomainStatus,
|
||||
): VerificationRecordDTO[] {
|
||||
const recordStatus =
|
||||
status === EmailingDomainStatus.VERIFIED
|
||||
? 'success'
|
||||
: status === EmailingDomainStatus.FAILED
|
||||
? 'error'
|
||||
: 'pending';
|
||||
|
||||
return records.map((record) => ({ ...record, status: recordStatus }));
|
||||
}
|
||||
}
|
||||
|
||||
+39
-2
@@ -46,7 +46,7 @@ export class LogEmailingDomainDriver implements EmailingDomainDriverInterface {
|
||||
|
||||
return {
|
||||
status: EmailingDomainStatus.VERIFIED,
|
||||
verificationRecords: [],
|
||||
verificationRecords: this.buildSyntheticVerificationRecords(input.domain),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,10 +57,47 @@ export class LogEmailingDomainDriver implements EmailingDomainDriverInterface {
|
||||
|
||||
return {
|
||||
status: EmailingDomainStatus.VERIFIED,
|
||||
verificationRecords: [],
|
||||
verificationRecords: this.buildSyntheticVerificationRecords(input.domain),
|
||||
};
|
||||
}
|
||||
|
||||
private buildSyntheticVerificationRecords(
|
||||
domain: string,
|
||||
): EmailingDomainVerificationResult['verificationRecords'] {
|
||||
return [
|
||||
{
|
||||
type: 'CNAME',
|
||||
key: `synthetic1._domainkey.${domain}`,
|
||||
value: `synthetic1.dkim.amazonses.example`,
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
type: 'CNAME',
|
||||
key: `synthetic2._domainkey.${domain}`,
|
||||
value: `synthetic2.dkim.amazonses.example`,
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
type: 'CNAME',
|
||||
key: `synthetic3._domainkey.${domain}`,
|
||||
value: `synthetic3.dkim.amazonses.example`,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
type: 'CNAME',
|
||||
key: `${UNSUBSCRIBE_HOSTNAME_PREFIX}.${domain}`,
|
||||
value: `app.localhost`,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
type: 'CNAME',
|
||||
key: `_acme-challenge.${UNSUBSCRIBE_HOSTNAME_PREFIX}.${domain}`,
|
||||
value: `${domain}.dcv.cloudflare.example`,
|
||||
status: 'error',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async registerDomain(input: EmailingDomainResourceInput): Promise<void> {
|
||||
this.logger.log(`[log-driver] registerDomain(${input.domain})`);
|
||||
}
|
||||
|
||||
+1
@@ -3,4 +3,5 @@ export type VerificationRecord = {
|
||||
key: string;
|
||||
value: string;
|
||||
priority?: number;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
+3
@@ -13,4 +13,7 @@ export class VerificationRecordDTO {
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
priority?: number;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
status?: string;
|
||||
}
|
||||
|
||||
+4
-10
@@ -80,15 +80,9 @@ export class EmailingDomainService {
|
||||
},
|
||||
);
|
||||
|
||||
if (isVerifiedOnCreation) {
|
||||
await this.unsubscribeHostnameService.sync(
|
||||
workspaceId,
|
||||
emailingDomain.id,
|
||||
{
|
||||
provision: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
await this.unsubscribeHostnameService.sync(workspaceId, emailingDomain.id, {
|
||||
provision: true,
|
||||
});
|
||||
|
||||
return this.unsubscribeHostnameService.withDnsRecords(
|
||||
await this.emailingDomainRepository.findOneOrFail(workspaceId, {
|
||||
@@ -227,7 +221,7 @@ export class EmailingDomainService {
|
||||
workspace.id,
|
||||
emailingDomain.id,
|
||||
{
|
||||
provision: verificationResult.status === EmailingDomainStatus.VERIFIED,
|
||||
provision: true,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+9
-1
@@ -90,7 +90,10 @@ export class UnsubscribeHostnameService {
|
||||
}
|
||||
|
||||
async deprovision(emailingDomain: EmailingDomainEntity): Promise<void> {
|
||||
if (!isNonEmptyString(emailingDomain.unsubscribeHostname)) {
|
||||
if (
|
||||
!this.dnsManagerService.isConfigured() ||
|
||||
!isNonEmptyString(emailingDomain.unsubscribeHostname)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,6 +107,10 @@ export class UnsubscribeHostnameService {
|
||||
emailingDomainId: string,
|
||||
{ provision }: { provision: boolean },
|
||||
): Promise<void> {
|
||||
if (!this.dnsManagerService.isConfigured()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const emailingDomain = await this.emailingDomainRepository.findOneOrFail(
|
||||
workspaceId,
|
||||
@@ -165,6 +172,7 @@ export class UnsubscribeHostnameService {
|
||||
type: 'CNAME' as const,
|
||||
key: record.key,
|
||||
value: record.value,
|
||||
status: record.status,
|
||||
}));
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
|
||||
Reference in New Issue
Block a user