Add Email DNS upsert for Stalwart DKIM, MX, and SPF.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-01 22:00:04 +03:30
co-authored by Cursor
parent 6b8073f998
commit 6f1b13b6dc
7 changed files with 841 additions and 2 deletions
@@ -19,6 +19,7 @@ import { ListDomainsDto } from './dto/list-domains.dto';
import { ToggleSslDto } from './dto/toggle-ssl.dto';
import { UpdateDomainAdminDto } from './dto/update-domain-admin.dto';
import { UpdateParkedHostsDto } from './dto/update-parked-hosts.dto';
import { ApplyMailDnsDto } from './dto/apply-mail-dns.dto';
import { DomainAdminService } from './domain-admin.service';
@Controller('domains')
@@ -71,6 +72,16 @@ export class DomainAdminController {
return this.service.setParkedHosts(domainId, dto, user);
}
@Post(':domainId/mail-dns')
@UseGuards(JwtAuthGuard)
applyMailDns(
@Param('domainId') domainId: string,
@Body() dto: ApplyMailDnsDto,
@CurrentUser() user: AuthUser,
) {
return this.service.applyMailDns(domainId, dto, user);
}
@Patch(':domainId')
@UseGuards(JwtAuthGuard)
update(
+67 -1
View File
@@ -30,7 +30,14 @@ import { ListDomainsDto } from './dto/list-domains.dto';
import { ToggleSslDto } from './dto/toggle-ssl.dto';
import { UpdateDomainAdminDto } from './dto/update-domain-admin.dto';
import { UpdateParkedHostsDto } from './dto/update-parked-hosts.dto';
import { isValidParkedHost, parseParkedDnsMap, uniqueParkedAliases, uniqueParkedHosts } from './parked-host.util';
import { ApplyMailDnsDto } from './dto/apply-mail-dns.dto';
import { parseDkimPaste } from '../mail-dns/mail-dns';
import {
isValidParkedHost,
parseParkedDnsMap,
uniqueParkedAliases,
uniqueParkedHosts,
} from './parked-host.util';
import type { ParkedDnsProvider } from './parked-host.util';
type DomainRow = {
@@ -919,6 +926,65 @@ export class DomainAdminService {
};
}
async applyMailDns(domainIdRaw: string, dto: ApplyMailDnsDto, actor: AuthUser) {
await this.assertSuperAdmin(actor);
const domainId = BigInt(domainIdRaw);
const domain = await this.prisma.domain.findUnique({
where: { id: domainId },
select: { id: true, host: true },
});
if (!domain) throw new NotFoundException('Website not found');
let ed25519;
let rsa;
try {
ed25519 = parseDkimPaste(dto.ed25519Key, 'ed25519');
rsa = parseDkimPaste(dto.rsaKey, 'rsa');
} catch (err) {
throw new BadRequestException(err instanceof Error ? err.message : 'Invalid DKIM key');
}
if (ed25519.selector === rsa.selector) {
throw new BadRequestException('Ed25519 and RSA DKIM selectors must be different.');
}
const provider = dto.dnsProvider === 'cloudflare' ? 'cloudflare' : 'arvan';
const host = domain.host.trim().toLowerCase();
try {
const result =
provider === 'cloudflare'
? await this.cloudflareDns.ensureMailRecords(host, [ed25519, rsa])
: await this.arvanDns.ensureMailRecords(host, [ed25519, rsa]);
const parts = [
result.created.length ? `created ${result.created.join(', ')}` : '',
result.updated.length ? `updated ${result.updated.join(', ')}` : '',
result.removed.length ? `removed ${result.removed.join(', ')}` : '',
].filter(Boolean);
return {
host,
dnsProvider: provider,
created: result.created,
updated: result.updated,
skipped: result.skipped,
removed: result.removed,
message: parts.length
? `Mail DNS updated on ${provider === 'cloudflare' ? 'Cloudflare' : 'Arvan'} (${parts.join('; ')}).`
: `Mail DNS already up to date on ${provider === 'cloudflare' ? 'Cloudflare' : 'Arvan'}.`,
};
} catch (err) {
throw new ServiceUnavailableException(
this.httpErrorMessage(
err,
provider === 'cloudflare'
? 'Cloudflare mail DNS update failed'
: 'Arvan mail DNS update failed',
),
);
}
}
private async applyParkedDns(
host: string,
provider: ParkedDnsProvider,
@@ -0,0 +1,17 @@
import { IsIn, IsString, MaxLength, MinLength } from 'class-validator';
import { PARKED_DNS_PROVIDERS } from '../parked-host.util';
export class ApplyMailDnsDto {
@IsIn(PARKED_DNS_PROVIDERS)
dnsProvider!: (typeof PARKED_DNS_PROVIDERS)[number];
@IsString()
@MinLength(20)
@MaxLength(8000)
ed25519Key!: string;
@IsString()
@MinLength(20)
@MaxLength(8000)
rsaKey!: string;
}