From 95d851f9dd75d8ec865ca57559c1e6832722d9c8 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Fri, 18 Sep 2026 10:38:06 +0330 Subject: [PATCH] Accept value-only DKIM TXT pastes for Email DNS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow v=DKIM1… pastes without a zone name by taking an optional selector hint so Arvan/Cloudflare still get the right _domainkey record. --- src/domain-admin/domain-admin.service.ts | 4 ++-- src/domain-admin/dto/apply-mail-dns.dto.ts | 14 ++++++++++- src/mail-dns/mail-dns.ts | 28 ++++++++++++++++++---- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/domain-admin/domain-admin.service.ts b/src/domain-admin/domain-admin.service.ts index 8471f3a..ad20e83 100644 --- a/src/domain-admin/domain-admin.service.ts +++ b/src/domain-admin/domain-admin.service.ts @@ -939,8 +939,8 @@ export class DomainAdminService { let ed25519; let rsa; try { - ed25519 = parseDkimPaste(dto.ed25519Key, 'ed25519'); - rsa = parseDkimPaste(dto.rsaKey, 'rsa'); + ed25519 = parseDkimPaste(dto.ed25519Key, 'ed25519', dto.ed25519Selector); + rsa = parseDkimPaste(dto.rsaKey, 'rsa', dto.rsaSelector); } catch (err) { throw new BadRequestException(err instanceof Error ? err.message : 'Invalid DKIM key'); } diff --git a/src/domain-admin/dto/apply-mail-dns.dto.ts b/src/domain-admin/dto/apply-mail-dns.dto.ts index 56cdad1..5e04983 100644 --- a/src/domain-admin/dto/apply-mail-dns.dto.ts +++ b/src/domain-admin/dto/apply-mail-dns.dto.ts @@ -1,4 +1,4 @@ -import { IsIn, IsString, MaxLength, MinLength } from 'class-validator'; +import { IsIn, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; import { PARKED_DNS_PROVIDERS } from '../parked-host.util'; export class ApplyMailDnsDto { @@ -14,4 +14,16 @@ export class ApplyMailDnsDto { @MinLength(20) @MaxLength(8000) rsaKey!: string; + + /** Required when ed25519Key is a value-only `v=DKIM1; …` paste (no `…._domainkey`). */ + @IsOptional() + @IsString() + @MaxLength(253) + ed25519Selector?: string; + + /** Required when rsaKey is a value-only `v=DKIM1; …` paste (no `…._domainkey`). */ + @IsOptional() + @IsString() + @MaxLength(253) + rsaSelector?: string; } diff --git a/src/mail-dns/mail-dns.ts b/src/mail-dns/mail-dns.ts index 020080f..4dc345d 100644 --- a/src/mail-dns/mail-dns.ts +++ b/src/mail-dns/mail-dns.ts @@ -11,13 +11,29 @@ export type MailDkimKey = { text: string; }; +/** Bare selector, or `name._domainkey` / `name._domainkey.example.com.` */ +export function normalizeDkimSelector(raw: string | undefined | null): string | null { + if (raw == null) return null; + let input = raw.trim().replace(/^"+|"+$/g, '').trim(); + if (!input) return null; + + const hostMatch = input.match(/^([a-z0-9][a-z0-9-]*)\._domainkey(?:\.|$)/i); + if (hostMatch) return hostMatch[1].toLowerCase(); + + if (/^[a-z0-9][a-z0-9-]*$/i.test(input)) return input.toLowerCase(); + return null; +} + export function parseDkimPaste( raw: string, expectedKind: 'ed25519' | 'rsa', + selectorHint?: string, ): MailDkimKey { const input = raw.trim(); if (!input) { - throw new Error(`Paste the ${expectedKind} DKIM line from Stalwart.`); + throw new Error( + `Paste the ${expectedKind} DKIM TXT value (v=DKIM1; …) or the full Stalwart zone line.`, + ); } const quoted = [...input.matchAll(/"([^"]*)"/g)].map((match) => match[1]).join(''); @@ -29,7 +45,7 @@ export function parseDkimPaste( text = text.replace(/^"+|"+$/g, '').replace(/\s+/g, ' ').replace(/[.\s]+$/g, '').trim(); if (!/^v=DKIM1/i.test(text)) { throw new Error( - `Could not find a v=DKIM1 value in the ${expectedKind} field. Paste the Stalwart zone line.`, + `Could not find a v=DKIM1 value in the ${expectedKind} field. Paste the TXT value or Stalwart zone line.`, ); } @@ -45,14 +61,16 @@ export function parseDkimPaste( } const nameMatch = compact.match(/([a-z0-9][a-z0-9-]*)\._domainkey/i); - if (!nameMatch) { + const selector = + (nameMatch ? nameMatch[1].toLowerCase() : null) || normalizeDkimSelector(selectorHint); + if (!selector) { throw new Error( - 'Include the selector name (for example v1-ed25519-20260831._domainkey) from the Stalwart zone file.', + 'Could not find a DKIM selector. Enter the selector (for example v1-ed25519-20260901), or paste a zone line that includes …._domainkey.', ); } return { - selector: nameMatch[1].toLowerCase(), + selector, text, }; }