This commit is contained in:
Mr Sleeps
2026-06-29 22:06:07 +01:00
parent 90f014361a
commit 8a5bdb2e2d
4 changed files with 626 additions and 0 deletions
@@ -0,0 +1,219 @@
{{-- resources/views/filament/pages/dmarc-records.blade.php --}}
<x-filament-panels::page>
<div class="space-y-6">
{{-- Header --}}
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
DMARC Record Configuration
</h2>
<p class="text-gray-600 dark:text-gray-400">
Configure Domain-based Message Authentication, Reporting, and Conformance (DMARC) settings.
This page shows how your current settings would appear as a DNS TXT record.
</p>
</div>
{{-- Current Settings --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
{{-- Policy Settings --}}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
Policy Settings
</h3>
</div>
<div class="p-6 space-y-4">
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Policy</span>
<span class="text-sm font-mono px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded">
{{ $this->settings['dmarc_policy'] ?? 'none' }}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Subdomain Policy</span>
<span class="text-sm font-mono px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded">
{{ $this->settings['dmarc_subdomain_policy'] ?? 'none' }}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">DKIM Alignment</span>
<span class="text-sm font-mono px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded">
{{ $this->settings['dmarc_adkim'] ?? 'relaxed' }}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">SPF Alignment</span>
<span class="text-sm font-mono px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded">
{{ $this->settings['dmarc_aspf'] ?? 'relaxed' }}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Percentage</span>
<span class="text-sm font-mono px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded">
{{ $this->settings['dmarc_percentage'] ?? 100 }}%
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Testing Mode</span>
<span class="text-sm font-mono px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded">
{{ isset($this->settings['dmarc_t']) && $this->settings['dmarc_t'] === 'y' ? 'Enabled' : 'Disabled' }}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Report Interval</span>
<span class="text-sm font-mono px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded">
{{ $this->settings['dmarc_report_interval'] ?? 86400 }} seconds
</span>
</div>
</div>
</div>
{{-- Reporting Settings --}}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
Reporting Settings
</h3>
</div>
<div class="p-6 space-y-4">
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 block mb-2">Aggregate Reports (RUA)</span>
<div class="flex flex-wrap gap-2">
@forelse($this->getRuaDestinations() as $destination)
<span class="text-sm font-mono px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded">
{{ $destination }}
</span>
@empty
<span class="text-sm text-gray-500 dark:text-gray-400">No destinations configured</span>
@endforelse
</div>
</div>
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 block mb-2">Forensic Reports (RUF)</span>
<div class="flex flex-wrap gap-2">
@forelse($this->getRufDestinations() as $destination)
<span class="text-sm font-mono px-2 py-1 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 rounded">
{{ $destination }}
</span>
@empty
<span class="text-sm text-gray-500 dark:text-gray-400">No destinations configured</span>
@endforelse
</div>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Reporting Options (FO)</span>
<span class="text-sm font-mono px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded">
{{ implode(', ', $this->settings['dmarc_reporting'] ?? ['all']) }}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">RUA Local Part</span>
<span class="text-sm font-mono px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded">
{{ $this->settings['dmarc_rua_localpart'] ?? 'dmarc' }}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">RUF Local Part</span>
<span class="text-sm font-mono px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded">
{{ $this->settings['dmarc_ruf_localpart'] ?? 'dmarc' }}
</span>
</div>
</div>
</div>
</div>
{{-- Advanced Settings --}}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
Advanced Settings
</h3>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Non-existent Subdomain Policy (NP)</span>
<span class="text-sm font-mono px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded">
{{ $this->settings['dmarc_np'] ?? 'Not set' }}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Public Suffix Domain Policy (PSD)</span>
<span class="text-sm font-mono px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded">
{{ $this->settings['dmarc_psd'] ?? 'Not set' }}
</span>
</div>
</div>
</div>
</div>
{{-- DMARC Record Preview --}}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm overflow-hidden border-2 border-blue-500 dark:border-blue-600">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-blue-50 dark:bg-blue-900/20">
<h3 class="text-lg font-medium text-blue-900 dark:text-blue-200 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
DMARC DNS Record Preview
</h3>
</div>
<div class="p-6">
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<div class="space-y-2">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-2">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Record Type:</span>
<span class="text-sm font-mono px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded">TXT</span>
</div>
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Name:</span>
<span class="text-sm font-mono px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded break-all">_dmarc.yourdomain.com</span>
</div>
<div class="flex flex-col gap-2">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Value:</span>
<div class="text-sm font-mono bg-gray-100 dark:bg-gray-700 p-4 rounded-lg border border-gray-200 dark:border-gray-600 overflow-x-auto">
<code class="text-gray-800 dark:text-gray-200 break-all">
{{ $this->getDmarcRecord($this->settings, 'yourdomain.com') }}
</code>
</div>
</div>
</div>
</div>
{{-- Explanation --}}
<div class="mt-6 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h4 class="font-medium text-blue-900 dark:text-blue-200 mb-2">Record Components:</h4>
<ul class="text-sm text-blue-800 dark:text-blue-300 space-y-1 list-disc list-inside">
<li><strong>v=DMARC1</strong> - Version identifier</li>
<li><strong>p=</strong> - Policy ({{ $this->settings['dmarc_policy'] ?? 'none' }})</li>
@if(!empty($this->settings['dmarc_rua'] ?? []))
<li><strong>rua=</strong> - Aggregate report destinations</li>
@endif
@if(!empty($this->settings['dmarc_ruf'] ?? []))
<li><strong>ruf=</strong> - Forensic report destinations</li>
@endif
@if(isset($this->settings['dmarc_percentage']) && $this->settings['dmarc_percentage'] != 100)
<li><strong>pct=</strong> - Percentage of messages to apply policy</li>
@endif
<li><strong>adkim=</strong> - DKIM alignment ({{ $this->settings['dmarc_adkim'] ?? 'relaxed' }})</li>
<li><strong>aspf=</strong> - SPF alignment ({{ $this->settings['dmarc_aspf'] ?? 'relaxed' }})</li>
</ul>
</div>
</div>
</div>
{{-- Action Buttons --}}
<div class="flex flex-wrap gap-3 justify-end">
<a href="{{ route('filament.admin.resources.settings.index') }}"
class="inline-flex items-center px-4 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 text-sm font-medium rounded-lg transition-colors">
Manage Settings
</a>
<button type="button"
onclick="navigator.clipboard.writeText('{{ $this->getDmarcRecord($this->settings, 'yourdomain.com') }}')"
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/>
</svg>
Copy DNS Record
</button>
</div>
</div>
</x-filament-panels::page>
@@ -0,0 +1,177 @@
<?php
namespace VEximweb\Plugin\DnsTools\Filament\Resources\Dmarc\Modals;
use Filament\Forms;
use VEximweb\Core\Data\Repositories\Interfaces\SettingRepositoryInterface;
use Filament\Forms\Components\CheckboxList;
use Filament\Schemas\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Hidden;
class GenerateDmarcForm
{
protected static ?string $domain = null;
/**
* Set the domain for the form.
*/
public static function setDomain(string $domain): void
{
static::$domain = $domain;
}
/**
* Get the current domain.
*/
protected static function getDomain(): string
{
return static::$domain ?? 'example.com';
}
/**
* The Filament form schema.
*/
public static function schema(string $domain): array
{
return [
Section::make('Policy')
->columns(2)
->schema([
Select::make('dmarc_policy')
->label('Policy')
->options([
'none' => 'None',
'quarantine' => 'Quarantine',
'reject' => 'Reject',
])
->required(),
Select::make('dmarc_subdomain_policy')
->label('Subdomain Policy')
->options([
'' => 'Inherit Policy',
'none' => 'None',
'quarantine' => 'Quarantine',
'reject' => 'Reject',
]),
Hidden::make('dmarc_np')
->default(''),
Hidden::make('dmarc_psd')
->default('n'),
Hidden::make('dmarc_t')
->default('n'),
Hidden::make('dmarc_percentage')
->default(100),
Hidden::make('dmarc_report_interval')
->default(86400),
]),
Section::make('Alignment')
->columns(2)
->schema([
Select::make('dmarc_adkim')
->label('DKIM Alignment')
->options([
'relaxed' => 'Relaxed',
'strict' => 'Strict',
]),
Select::make('dmarc_aspf')
->label('SPF Alignment')
->options([
'relaxed' => 'Relaxed',
'strict' => 'Strict',
]),
]),
Section::make('Reporting')
->schema([
CheckboxList::make('dmarc_reporting')
->label('Failure Reporting Options')
->columns(5)
->options([
'0' => 'No reports',
'1' => 'Summary only',
'd' => 'DKIM failures',
's' => 'SPF failures',
'all' => 'All failures',
]),
TextInput::make('dmarc_rua_localpart')
->label('Aggregate Reports Email')
->helperText('Email address for aggregate reports (auto-appended with @' . $domain . ')')
->default('dmarc-reports')
->prefix('📊')
->suffix('@' . $domain),
TextInput::make('dmarc_ruf_localpart')
->label('Forensic Reports Email')
->helperText('Email address for forensic reports (auto-appended with @' . $domain . ')')
->default('dmarc-forensic')
->prefix('🔍')
->suffix('@' . $domain),
]),
];
}
/**
* Populate the form from the repository.
*/
public static function values(SettingRepositoryInterface $settings): array
{
// Get settings for this domain (you might need to adjust this)
$rua_localpart = $settings->get('dmarc_rua_localpart', 'dmarc-reports');
$ruf_localpart = $settings->get('dmarc_ruf_localpart', 'dmarc-forensic');
return [
'dmarc_policy' => $settings->get('dmarc_policy'),
'dmarc_subdomain_policy' => $settings->get('dmarc_subdomain_policy'),
'dmarc_np' => $settings->get('dmarc_np', ''),
'dmarc_psd' => $settings->get('dmarc_psd', 'n'),
'dmarc_t' => $settings->get('dmarc_t', 'n'),
'dmarc_percentage' => $settings->get('dmarc_percentage', 100),
'dmarc_report_interval' => $settings->get('dmarc_report_interval', 86400),
'dmarc_adkim' => $settings->get('dmarc_adkim'),
'dmarc_aspf' => $settings->get('dmarc_aspf'),
'dmarc_reporting' => $settings->get('dmarc_reporting', []),
'dmarc_rua_localpart' => $rua_localpart,
'dmarc_ruf_localpart' => $ruf_localpart,
];
}
/**
* Save the form back to the repository.
*/
public static function save(
SettingRepositoryInterface $settings,
array $data,
string $domain,
): void {
foreach ([
'dmarc_policy',
'dmarc_subdomain_policy',
'dmarc_np',
'dmarc_psd',
'dmarc_t',
'dmarc_percentage',
'dmarc_report_interval',
'dmarc_adkim',
'dmarc_aspf',
'dmarc_rua_localpart',
'dmarc_ruf_localpart',
] as $key) {
$settings->set($key, $data[$key]);
}
$settings->set('dmarc_rua', [$data['dmarc_rua_localpart'] . '@' . $domain]);
$settings->set('dmarc_ruf', [$data['dmarc_ruf_localpart'] . '@' . $domain]);
$settings->set('dmarc_reporting', $data['dmarc_reporting'] ?? []);
}
}
@@ -0,0 +1,207 @@
<?php
namespace VEximweb\Plugin\DnsTools\Filament\Resources\Dmarc\Pages;
use App\Settings\DmarcSettings;
use Filament\Pages\Page;
use Illuminate\Support\Collection;
use VEximweb\Core\Data\Repositories\SettingRepository;
class DmarcRecords extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static string $view = 'filament.pages.dmarc-records';
protected static ?string $navigationGroup = 'Email';
protected static ?int $navigationSort = 10;
protected static ?string $title = 'DMARC Records';
protected SettingRepository $settingRepository;
public array $settings = [];
public function __construct()
{
$this->settingRepository = app(SettingRepository::class);
$this->settings = $this->getDmarcSettings();
}
public function getDmarcSettings(): array
{
$keys = [
'dmarc_policy',
'dmarc_subdomain_policy',
'dmarc_adkim',
'dmarc_aspf',
'dmarc_percentage',
'dmarc_report_interval',
'dmarc_t',
'dmarc_rua',
'dmarc_ruf',
'dmarc_reporting',
'dmarc_np',
'dmarc_psd',
'dmarc_rua_localpart',
'dmarc_ruf_localpart',
];
$settings = [];
foreach ($keys as $key) {
$value = $this->settingRepository->get($key);
$settings[$key] = $value;
}
return $settings;
}
public function getDefaultSettings(): array
{
return [
'dmarc_policy' => 'none',
'dmarc_subdomain_policy' => 'none',
'dmarc_adkim' => 'relaxed',
'dmarc_aspf' => 'relaxed',
'dmarc_percentage' => 100,
'dmarc_report_interval' => 86400,
'dmarc_t' => 'n',
'dmarc_rua' => [],
'dmarc_ruf' => [],
'dmarc_reporting' => ['all'],
'dmarc_np' => '',
'dmarc_psd' => '',
'dmarc_rua_localpart' => 'dmarc',
'dmarc_ruf_localpart' => 'dmarc',
];
}
public function getDmarcRecord(array $settings, string $domain): string
{
$parts = [];
// Required: v (version)
$parts[] = 'v=DMARC1';
// Required: p (policy)
$policy = $settings['dmarc_policy'] ?? 'none';
$parts[] = "p=$policy";
// pct (percentage)
$percentage = $settings['dmarc_percentage'] ?? 100;
if ($percentage !== 100) {
$parts[] = "pct=$percentage";
}
// adkim (DKIM alignment)
$adkim = $settings['dmarc_adkim'] ?? 'relaxed';
if ($adkim !== 'relaxed') {
$parts[] = "adkim=$adkim";
}
// aspf (SPF alignment)
$aspf = $settings['dmarc_aspf'] ?? 'relaxed';
if ($aspf !== 'relaxed') {
$parts[] = "aspf=$aspf";
}
// rua (aggregate reports)
$rua = $settings['dmarc_rua'] ?? [];
if (!empty($rua)) {
$ruaStr = implode(',', array_map(function ($email) {
return "mailto:$email";
}, $rua));
$parts[] = "rua=$ruaStr";
}
// ruf (forensic reports)
$ruf = $settings['dmarc_ruf'] ?? [];
if (!empty($ruf)) {
$rufStr = implode(',', array_map(function ($email) {
return "mailto:$email";
}, $ruf));
$parts[] = "ruf=$rufStr";
}
// fo (failure reporting options)
$reporting = $settings['dmarc_reporting'] ?? ['all'];
if (!empty($reporting)) {
$fo = implode('', array_map(function ($opt) {
return $this->getFoValue($opt);
}, $reporting));
if (!empty($fo)) {
$parts[] = "fo=$fo";
}
}
// np (non-existent subdomain policy)
$np = $settings['dmarc_np'] ?? '';
if (!empty($np)) {
$parts[] = "np=$np";
}
// sp (subdomain policy)
$sp = $settings['dmarc_subdomain_policy'] ?? '';
if (!empty($sp)) {
$parts[] = "sp=$sp";
}
// psd (public suffix domain policy)
$psd = $settings['dmarc_psd'] ?? '';
if (!empty($psd)) {
$parts[] = "psd=$psd";
}
// t (testing mode)
$testing = $settings['dmarc_t'] ?? 'n';
if ($testing === 'y') {
$parts[] = 't=y';
}
// ri (report interval)
$ri = $settings['dmarc_report_interval'] ?? 86400;
if ($ri !== 86400) {
$parts[] = "ri=$ri";
}
return implode('; ', $parts);
}
private function getFoValue(string $option): string
{
return match ($option) {
'all' => '1',
'any' => '1',
'dkim' => 'd',
'spf' => 's',
'both' => '1',
default => '1'
};
}
public function getRuaDestinations(): array
{
$rua = $this->settings['dmarc_rua'] ?? [];
$localpart = $this->settings['dmarc_rua_localpart'] ?? 'dmarc';
return array_map(function ($email) use ($localpart) {
return "mailto:$email";
}, $rua);
}
public function getRufDestinations(): array
{
$ruf = $this->settings['dmarc_ruf'] ?? [];
$localpart = $this->settings['dmarc_ruf_localpart'] ?? 'dmarc';
return array_map(function ($email) use ($localpart) {
return "mailto:$email";
}, $ruf);
}
public static function getNavigationBadge(): ?string
{
return 'DMARC';
}
}
@@ -20,6 +20,8 @@ use Filament\Support\Icons\Heroicon;
use VEximweb\Plugin\DnsTools\Models\DmarcCheck;
use Filament\Notifications\Notification;
use Filament\Actions\ActionGroup;
use VEximweb\Plugin\DnsTools\Filament\Resources\Dmarc\Modals\GenerateDmarcForm;
use VEximweb\Core\Data\Repositories\Interfaces\SettingRepositoryInterface;
class DomainsTable
{
@@ -209,6 +211,27 @@ class DomainsTable
EditAction::make(),
EditAction::make(),
EditAction::make(),
Action::make('dmarc')
->fillForm(fn (SettingRepositoryInterface $settings) =>
GenerateDmarcForm::values($settings)
)
->form(fn ($record) =>
GenerateDmarcForm::schema($record->domain)
)
->action(fn (array $data, SettingRepositoryInterface $settings, $record) =>
GenerateDmarcForm::save($settings, $data, $record->domain)
),
/*
Action::make('dmarc')
->fillForm(fn (SettingRepositoryInterface $settings) =>
GenerateDmarcForm::values($settings)
)
->form(GenerateDmarcForm::schema())
->action(fn (array $data, SettingRepositoryInterface $settings) =>
GenerateDmarcForm::save($settings, $data)
),
*/
Action::make('viewDmarc')
->label('DMARC Details')
->modalHeading('DMARC Record Details')