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:
neo773
2026-07-18 02:11:25 +05:30
committed by GitHub
parent 80ff1716e5
commit 5bedd5b8cc
23 changed files with 364 additions and 86 deletions
@@ -37,6 +37,10 @@ export class DnsManagerService {
}
}
isConfigured(): boolean {
return isDefined(this.cloudflareClient);
}
async registerHostname(customDomain: string, options?: DnsManagerOptions) {
dnsManagerValidator.isCloudflareInstanceDefined(this.cloudflareClient);
@@ -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 }));
}
}
@@ -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})`);
}
@@ -3,4 +3,5 @@ export type VerificationRecord = {
key: string;
value: string;
priority?: number;
status?: string;
};
@@ -13,4 +13,7 @@ export class VerificationRecordDTO {
@Field(() => Number, { nullable: true })
priority?: number;
@Field(() => String, { nullable: true })
status?: string;
}
@@ -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,
},
);
@@ -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(