246 lines
8.0 KiB
TypeScript
246 lines
8.0 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Modal } from './Modal'
|
|
import { DnsProviderSwitch } from './DnsProviderSwitch'
|
|
import { ApiError } from '../lib/api'
|
|
import { applyMailDns } from '../services/domainService'
|
|
import type { DomainListItem, ParkedDnsProvider } from '../types/domain'
|
|
import { useToast } from '../context/ToastContext'
|
|
import tableStyles from '../pages/BusinessesPage.module.css'
|
|
import parkedStyles from './ParkedDomainsModal.module.css'
|
|
import styles from './EmailDnsModal.module.css'
|
|
|
|
interface EmailDnsModalProps {
|
|
domain: DomainListItem | null
|
|
onClose: () => void
|
|
}
|
|
|
|
const DEFAULT_ED25519_SELECTOR = 'v1-ed25519-20260901'
|
|
const DEFAULT_RSA_SELECTOR = 'v1-rsa-20260901'
|
|
|
|
function pasteIncludesDomainkey(paste: string) {
|
|
return /[a-z0-9][a-z0-9-]*\._domainkey/i.test(paste)
|
|
}
|
|
|
|
/** Prefer typed selector; otherwise default when the paste has no …._domainkey. */
|
|
function resolveSelector(paste: string, selector: string, fallback: string) {
|
|
const typed = selector.trim()
|
|
if (typed) return typed
|
|
if (pasteIncludesDomainkey(paste)) return ''
|
|
return fallback
|
|
}
|
|
|
|
export function EmailDnsModal({ domain, onClose }: EmailDnsModalProps) {
|
|
const { showToast } = useToast()
|
|
const [dnsProvider, setDnsProvider] = useState<ParkedDnsProvider>('arvan')
|
|
const [ed25519Selector, setEd25519Selector] = useState(DEFAULT_ED25519_SELECTOR)
|
|
const [ed25519Key, setEd25519Key] = useState('')
|
|
const [rsaSelector, setRsaSelector] = useState(DEFAULT_RSA_SELECTOR)
|
|
const [rsaKey, setRsaKey] = useState('')
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
useEffect(() => {
|
|
if (!domain) return
|
|
setDnsProvider('arvan')
|
|
setEd25519Selector(DEFAULT_ED25519_SELECTOR)
|
|
setEd25519Key('')
|
|
setRsaSelector(DEFAULT_RSA_SELECTOR)
|
|
setRsaKey('')
|
|
setError('')
|
|
setSubmitting(false)
|
|
}, [domain])
|
|
|
|
if (!domain) return null
|
|
const target = domain
|
|
const canSubmit = Boolean(rsaKey.trim())
|
|
|
|
async function submit() {
|
|
if (!rsaKey.trim()) {
|
|
const message = 'Paste the RSA DKIM TXT value from Stalwart.'
|
|
setError(message)
|
|
showToast(message, 'error')
|
|
return
|
|
}
|
|
|
|
const ed25519Paste = ed25519Key.trim()
|
|
const rsaPaste = rsaKey.trim()
|
|
const resolvedRsaSelector = resolveSelector(
|
|
rsaPaste,
|
|
rsaSelector,
|
|
DEFAULT_RSA_SELECTOR,
|
|
)
|
|
const resolvedEd25519Selector = ed25519Paste
|
|
? resolveSelector(
|
|
ed25519Paste,
|
|
ed25519Selector,
|
|
DEFAULT_ED25519_SELECTOR,
|
|
)
|
|
: ''
|
|
|
|
// Show resolved defaults in the fields if they were cleared.
|
|
if (!rsaSelector.trim() && resolvedRsaSelector) {
|
|
setRsaSelector(resolvedRsaSelector)
|
|
}
|
|
if (ed25519Paste && !ed25519Selector.trim() && resolvedEd25519Selector) {
|
|
setEd25519Selector(resolvedEd25519Selector)
|
|
}
|
|
|
|
setSubmitting(true)
|
|
setError('')
|
|
try {
|
|
const result = await applyMailDns(target.id, {
|
|
dnsProvider,
|
|
rsaKey: rsaPaste,
|
|
...(resolvedRsaSelector ? { rsaSelector: resolvedRsaSelector } : {}),
|
|
...(ed25519Paste ? { ed25519Key: ed25519Paste } : {}),
|
|
...(ed25519Paste && resolvedEd25519Selector
|
|
? { ed25519Selector: resolvedEd25519Selector }
|
|
: {}),
|
|
})
|
|
showToast(result.message, 'success')
|
|
onClose()
|
|
} catch (err) {
|
|
const message =
|
|
err instanceof ApiError ? err.message : 'Unable to update mail DNS.'
|
|
setError(message)
|
|
showToast(message, 'error')
|
|
} finally {
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
open
|
|
wide
|
|
title={`Email DNS · ${domain.host}`}
|
|
onClose={() => {
|
|
if (!submitting) onClose()
|
|
}}
|
|
>
|
|
<p className={parkedStyles.hint}>
|
|
Paste the RSA DKIM <strong>TXT value</strong> (<code>v=DKIM1; …</code>) from
|
|
Stalwart, or the full zone line. Ed25519 is optional — leave it empty to
|
|
skip. Selectors default to <code>{DEFAULT_RSA_SELECTOR}</code> /{' '}
|
|
<code>{DEFAULT_ED25519_SELECTOR}</code> when empty. We upsert common mail
|
|
records on {dnsProvider === 'cloudflare' ? 'Cloudflare' : 'Arvan'}{' '}
|
|
(DNS-only / not proxied): <strong>mail</strong> A → 185.214.101.41, MX →
|
|
mail.meshkee.com (priority 10), SPF <code>v=spf1 mx -all</code>, DKIM
|
|
(RSA required, Ed25519 if provided), autoconfig / autodiscover →
|
|
mail.meshkee.com. Extra MX, <strong>dkim._domainkey</strong>, and{' '}
|
|
<strong>_dmarc</strong> are removed.
|
|
</p>
|
|
{error ? (
|
|
<p className={tableStyles.alertError} role="alert">
|
|
{error}
|
|
</p>
|
|
) : null}
|
|
{submitting ? (
|
|
<p className={tableStyles.helperText} role="status">
|
|
Updating {dnsProvider === 'cloudflare' ? 'Cloudflare' : 'Arvan'} mail DNS…
|
|
</p>
|
|
) : null}
|
|
|
|
<div className={styles.providerRow}>
|
|
<span className={styles.providerLabel}>DNS host</span>
|
|
<DnsProviderSwitch
|
|
value={dnsProvider}
|
|
onChange={setDnsProvider}
|
|
disabled={submitting}
|
|
/>
|
|
</div>
|
|
|
|
<div className={tableStyles.field}>
|
|
<label htmlFor="mail-dkim-ed25519-selector">
|
|
Ed25519 selector <span className={tableStyles.helperText}>(optional)</span>
|
|
</label>
|
|
<input
|
|
id="mail-dkim-ed25519-selector"
|
|
className={styles.selectorInput}
|
|
type="text"
|
|
value={ed25519Selector}
|
|
onChange={(e) => setEd25519Selector(e.target.value)}
|
|
placeholder={DEFAULT_ED25519_SELECTOR}
|
|
disabled={submitting}
|
|
spellCheck={false}
|
|
dir="ltr"
|
|
autoComplete="off"
|
|
/>
|
|
<p className={tableStyles.helperText}>
|
|
Auto-filled when empty. Change it if Stalwart uses a different selector.
|
|
</p>
|
|
</div>
|
|
<div className={tableStyles.field}>
|
|
<label htmlFor="mail-dkim-ed25519">
|
|
Ed25519 domain key{' '}
|
|
<span className={tableStyles.helperText}>(optional)</span>
|
|
</label>
|
|
<textarea
|
|
id="mail-dkim-ed25519"
|
|
className={styles.textarea}
|
|
value={ed25519Key}
|
|
onChange={(e) => setEd25519Key(e.target.value)}
|
|
placeholder="Leave empty to skip — or paste v=DKIM1; k=ed25519; …"
|
|
disabled={submitting}
|
|
spellCheck={false}
|
|
dir="ltr"
|
|
rows={4}
|
|
/>
|
|
</div>
|
|
<div style={{ height: 12 }} />
|
|
<div className={tableStyles.field}>
|
|
<label htmlFor="mail-dkim-rsa-selector">RSA selector</label>
|
|
<input
|
|
id="mail-dkim-rsa-selector"
|
|
className={styles.selectorInput}
|
|
type="text"
|
|
value={rsaSelector}
|
|
onChange={(e) => setRsaSelector(e.target.value)}
|
|
placeholder={DEFAULT_RSA_SELECTOR}
|
|
disabled={submitting}
|
|
spellCheck={false}
|
|
dir="ltr"
|
|
autoComplete="off"
|
|
/>
|
|
<p className={tableStyles.helperText}>
|
|
Auto-filled when empty. Change it if Stalwart uses a different selector.
|
|
</p>
|
|
</div>
|
|
<div className={tableStyles.field}>
|
|
<label htmlFor="mail-dkim-rsa">RSA domain key</label>
|
|
<textarea
|
|
id="mail-dkim-rsa"
|
|
className={styles.textarea}
|
|
value={rsaKey}
|
|
onChange={(e) => setRsaKey(e.target.value)}
|
|
placeholder='v=DKIM1; k=rsa; h=sha256; p=…'
|
|
disabled={submitting}
|
|
spellCheck={false}
|
|
dir="ltr"
|
|
rows={6}
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ height: 12 }} />
|
|
<div className={tableStyles.actionsRow}>
|
|
<button
|
|
type="button"
|
|
className={`${tableStyles.btn} ${tableStyles.btnGhost}`}
|
|
onClick={onClose}
|
|
disabled={submitting}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`${tableStyles.btn} ${tableStyles.btnPrimary}`}
|
|
onClick={() => void submit()}
|
|
disabled={submitting || !canSubmit}
|
|
>
|
|
Update DNS
|
|
</button>
|
|
</div>
|
|
</Modal>
|
|
)
|
|
}
|