Added dns updates

This commit is contained in:
Mr Sleeps
2026-07-01 16:52:17 +01:00
parent 8a5bdb2e2d
commit 55925549e4
11 changed files with 433 additions and 714 deletions
@@ -0,0 +1,25 @@
<x-filament-panels::page>
<form wire:submit="generateDMARC">
{{ $this->form }}
</form>
<x-filament::modal id="dmarc-record-modal" width="2xl">
<x-slot name="heading">
Generated DMARC Record
</x-slot>
<div class="space-y-4">
<pre class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 text-sm overflow-x-auto whitespace-pre-wrap break-all">{{ $generatedRecord }}</pre>
<button
type="button"
x-on:click="navigator.clipboard.writeText(@js($generatedRecord))"
class="fi-btn"
>
Copy to clipboard
</button>
</div>
</x-filament::modal>
<div wire:poll.5s>
</div>
</x-filament-panels::page>
@@ -1,316 +0,0 @@
<?php
namespace VEximweb\Plugin\DnsTools\Filament\Resources\DmarcResource\Pages;
use Filament\Resources\Pages\CreateRecord;
use Filament\Forms\Form;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\Placeholder;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use VEximweb\Plugin\DnsTools\Filament\Resources\DmarcResource;
use VEximweb\Plugin\DnsTools\Models\DmarcCheck;
use VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService;
use VEximweb\Plugin\DnsTools\Dmarc\DmarcRecord;
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcPolicy;
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcAlignment;
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcReporting;
use VEximweb\Core\Data\Models\Domain;
class GenerateDmarc extends CreateRecord
{
protected static string $resource = DmarcResource::class;
public Domain $domain;
public ?DmarcCheck $existingDmarc = null;
public function mount($record = null): void
{
if ($record) {
$this->domain = Domain::find($record);
$this->existingDmarc = DmarcCheck::where('domain', $this->domain->domain)->first();
}
parent::mount();
if ($this->existingDmarc) {
$defaults = [
'policy' => $this->existingDmarc->policy ?? 'none',
'subdomain_policy' => $this->existingDmarc->subdomain_policy ?? null,
'adkim' => $this->existingDmarc->adkim ?? 'relaxed',
'aspf' => $this->existingDmarc->aspf ?? 'relaxed',
'reporting' => json_decode($this->existingDmarc->reporting ?? '["all"]', true),
'percentage' => $this->existingDmarc->percentage ?? 100,
't' => $this->existingDmarc->t ?? 'n',
];
if ($this->existingDmarc->rua) {
$rua = json_decode($this->existingDmarc->rua, true);
if (is_array($rua)) {
$defaults['rua'] = collect($rua)->map(function ($item) {
return ['email' => str_replace('mailto:', '', $item)];
})->toArray();
}
}
if ($this->existingDmarc->ruf) {
$ruf = json_decode($this->existingDmarc->ruf, true);
if (is_array($ruf)) {
$defaults['ruf'] = collect($ruf)->map(function ($item) {
return ['email' => str_replace('mailto:', '', $item)];
})->toArray();
}
}
$this->form->fill($defaults);
}
}
public function form(Form $form): Form
{
$service = app(DmarcCheckService::class);
$addresses = $service->getDefaultReportingAddresses($this->domain->domain ?? config('app.domain'));
return $form
->schema([
Select::make('policy')
->label('Policy')
->options([
'none' => 'None (Monitor only)',
'quarantine' => 'Quarantine (Mark as spam)',
'reject' => 'Reject (Block delivery)',
])
->required()
->default('none')
->live(),
Select::make('subdomain_policy')
->label('Subdomain Policy')
->options([
'' => 'Inherit from main policy',
'none' => 'None (Monitor only)',
'quarantine' => 'Quarantine (Mark as spam)',
'reject' => 'Reject (Block delivery)',
])
->nullable(),
Repeater::make('rua')
->label('Aggregate Reports (RUA)')
->schema([
TextInput::make('email')
->label('Email')
->email()
->prefix('mailto:')
->required(),
])
->default(function () use ($addresses) {
if ($this->existingDmarc && $this->existingDmarc->rua) {
$rua = json_decode($this->existingDmarc->rua, true);
if (is_array($rua)) {
return collect($rua)->map(function ($item) {
return ['email' => str_replace('mailto:', '', $item)];
})->toArray();
}
}
return [['email' => str_replace('mailto:', '', $addresses['rua'])]];
})
->addable()
->deletable()
->reorderable()
->columnSpanFull(),
Repeater::make('ruf')
->label('Forensic Reports (RUF)')
->schema([
TextInput::make('email')
->label('Email')
->email()
->prefix('mailto:')
->required(),
])
->default(function () use ($addresses) {
if ($this->existingDmarc && $this->existingDmarc->ruf) {
$ruf = json_decode($this->existingDmarc->ruf, true);
if (is_array($ruf)) {
return collect($ruf)->map(function ($item) {
return ['email' => str_replace('mailto:', '', $item)];
})->toArray();
}
}
return [['email' => str_replace('mailto:', '', $addresses['ruf'])]];
})
->addable()
->deletable()
->reorderable()
->columnSpanFull(),
Select::make('adkim')
->label('DKIM Alignment')
->options([
'relaxed' => 'Relaxed (subdomains allowed)',
'strict' => 'Strict (exact match)',
])
->default('relaxed'),
Select::make('aspf')
->label('SPF Alignment')
->options([
'relaxed' => 'Relaxed (subdomains allowed)',
'strict' => 'Strict (exact match)',
])
->default('relaxed'),
Select::make('reporting')
->label('Failure Reporting (FO)')
->multiple()
->options([
'all' => 'All failures',
'any' => 'Any failure',
'dkim' => 'DKIM failures only',
'spf' => 'SPF failures only',
])
->default(['all']),
Slider::make('percentage')
->label('Enforcement Percentage')
->min(0)
->max(100)
->default(100),
Select::make('t')
->label('Testing Mode')
->options([
'n' => 'Off (Apply policy)',
'y' => 'On (Don\'t apply policy)',
])
->default('n'),
Placeholder::make('generated_record_preview')
->label('Preview')
->content(function ($get) {
try {
$settings = $this->prepareSettings($get);
$dmarcRecord = new DmarcRecord(
policy: DmarcPolicy::tryFrom($settings['policy']),
subdomainPolicy: $settings['subdomain_policy'] ? DmarcPolicy::tryFrom($settings['subdomain_policy']) : null,
rua: $settings['rua'],
ruf: $settings['ruf'],
adkim: DmarcAlignment::tryFrom($settings['adkim']),
aspf: DmarcAlignment::tryFrom($settings['aspf']),
reporting: array_map(fn($r) => DmarcReporting::tryFrom($r), $settings['reporting']),
percentage: (int)$settings['percentage'],
t: $settings['t'],
);
if ($this->domain) {
return "Name: _dmarc.{$this->domain->domain}\n" .
"Type: TXT\n" .
"Value: v=DMARC1; " . $dmarcRecord->toDnsRecord();
}
return "Please select a domain to preview";
} catch (\Exception $e) {
return "Error generating preview: " . $e->getMessage();
}
})
->extraAttributes(['class' => 'font-mono text-sm bg-gray-50 dark:bg-gray-800 p-4 rounded'])
->columnSpanFull(),
]);
}
protected function prepareSettings(array $data): array
{
return [
'policy' => $data['policy'] ?? 'none',
'subdomain_policy' => $data['subdomain_policy'] ?? null,
'rua' => collect($data['rua'] ?? [])
->filter(fn($item) => !empty($item['email']))
->map(fn($item) => 'mailto:' . $item['email'])
->values()
->toArray(),
'ruf' => collect($data['ruf'] ?? [])
->filter(fn($item) => !empty($item['email']))
->map(fn($item) => 'mailto:' . $item['email'])
->values()
->toArray(),
'adkim' => $data['adkim'] ?? 'relaxed',
'aspf' => $data['aspf'] ?? 'relaxed',
'reporting' => $data['reporting'] ?? ['all'],
'percentage' => (int)($data['percentage'] ?? 100),
't' => $data['t'] ?? 'n',
];
}
protected function handleRecordCreation(array $data): \Illuminate\Database\Eloquent\Model
{
$settings = $this->prepareSettings($data);
$dmarcRecord = new DmarcRecord(
policy: DmarcPolicy::tryFrom($settings['policy']),
subdomainPolicy: $settings['subdomain_policy'] ? DmarcPolicy::tryFrom($settings['subdomain_policy']) : null,
rua: $settings['rua'],
ruf: $settings['ruf'],
adkim: DmarcAlignment::tryFrom($settings['adkim']),
aspf: DmarcAlignment::tryFrom($settings['aspf']),
reporting: array_map(fn($r) => DmarcReporting::tryFrom($r), $settings['reporting']),
percentage: (int)$settings['percentage'],
t: $settings['t'],
);
$dnsRecord = 'v=DMARC1; ' . $dmarcRecord->toDnsRecord();
return DmarcCheck::updateOrCreate(
['domain' => $this->domain->domain],
[
'domain_id' => $this->domain->domain_id,
'record' => $dnsRecord,
'policy' => $settings['policy'],
'subdomain_policy' => $settings['subdomain_policy'],
'adkim' => $settings['adkim'],
'aspf' => $settings['aspf'],
'rua' => json_encode($settings['rua']),
'ruf' => json_encode($settings['ruf']),
'reporting' => json_encode($settings['reporting']),
'percentage' => (int)$settings['percentage'],
't' => $settings['t'],
'valid' => true,
'error_message' => null,
'last_checked_at' => now(),
'next_check_at' => now()->addHours(24),
]
);
}
protected function getCreatedNotificationTitle(): ?string
{
return "DMARC record generated for {$this->domain->domain}";
}
protected function getRedirectUrl(): string
{
return DmarcResource::getUrl('view', ['record' => $this->domain->getKey()]);
}
protected function getHeaderActions(): array
{
return [
Action::make('back')
->label('Back to View')
->icon('heroicon-o-arrow-left')
->url(DmarcResource::getUrl('view', ['record' => $this->domain->getKey()])),
Action::make('back_to_list')
->label('Back to List')
->icon('heroicon-o-list-bullet')
->url(DmarcResource::getUrl('index')),
];
}
public function getTitle(): string
{
$action = $this->existingDmarc ? 'Update' : 'Generate';
return "{$action} DMARC Record";
}
}
-192
View File
@@ -1,192 +0,0 @@
<?php
namespace VEximweb\Plugin\DnsTools\Filament\Pages\Dmarc;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Table;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Notifications\Notification;
use VEximweb\Plugin\DnsTools\Filament\Resources\DmarcResource;
use VEximweb\Plugin\DnsTools\Models\DmarcCheck;
use VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService;
use VEximweb\Core\Data\Models\Domain;
use Illuminate\Database\Eloquent\Builder;
class ListDmarc extends ListRecords
{
protected static string $resource = DmarcResource::class;
protected function getTableQuery(): Builder
{
return Domain::where('enabled', true);
}
public function table(Table $table): Table
{
return $table
->query($this->getTableQuery())
->columns([
TextColumn::make('domain')
->label('Domain')
->searchable()
->sortable()
->size('lg')
->weight('bold'),
BadgeColumn::make('dmarc_status')
->label('DMARC Status')
->getStateUsing(function ($record) {
$dmarc = DmarcCheck::where('domain', $record->domain)->first();
if (!$dmarc) {
return 'Not Checked';
}
if (!$dmarc->valid) {
return 'No Record';
}
return $dmarc->getPolicyLabel();
})
->colors([
'success' => 'Monitor',
'warning' => 'Quarantine',
'danger' => 'Reject',
'gray' => ['Not Checked', 'No Record'],
]),
BadgeColumn::make('dmarc_policy')
->label('Policy')
->getStateUsing(function ($record) {
$dmarc = DmarcCheck::where('domain', $record->domain)->first();
return $dmarc?->policy ?? '-';
})
->colors([
'success' => 'none',
'warning' => 'quarantine',
'danger' => 'reject',
'gray' => '-',
]),
TextColumn::make('dmarc_last_checked')
->label('Last Checked')
->getStateUsing(function ($record) {
$dmarc = DmarcCheck::where('domain', $record->domain)->first();
return $dmarc?->last_checked_at;
})
->since()
->toggleable(),
TextColumn::make('exim_users_count')
->label('Accounts')
->counts('eximUsers')
->badge()
->color('info'),
])
->filters([
\Filament\Tables\Filters\SelectFilter::make('dmarc_status')
->label('DMARC Status')
->options([
'has_record' => 'Has DMARC Record',
'no_record' => 'No DMARC Record',
'not_checked' => 'Not Checked',
])
->query(function ($query, $data) {
if (empty($data['value'])) {
return $query;
}
return match($data['value']) {
'has_record' => $query->whereIn('domain',
DmarcCheck::where('valid', true)->pluck('domain')->toArray()
),
'no_record' => $query->whereIn('domain',
DmarcCheck::where('valid', false)->pluck('domain')->toArray()
),
'not_checked' => $query->whereNotIn('domain',
DmarcCheck::pluck('domain')->toArray()
),
default => $query,
};
}),
\Filament\Tables\Filters\SelectFilter::make('policy')
->label('Policy')
->options([
'none' => 'Monitor',
'quarantine' => 'Quarantine',
'reject' => 'Reject',
])
->query(function ($query, $data) {
if (empty($data['value'])) {
return $query;
}
$domains = DmarcCheck::where('policy', $data['value'])
->where('valid', true)
->pluck('domain')
->toArray();
return $query->whereIn('domain', $domains);
}),
])
->actions([
Action::make('view')
->label('View DMARC')
->icon('heroicon-o-eye')
->color('info')
->url(fn ($record): string => DmarcResource::getUrl('view', ['record' => $record])),
Action::make('generate')
->label('Generate Record')
->icon('heroicon-o-document-plus')
->color('success')
->url(fn ($record): string => DmarcResource::getUrl('generate', ['record' => $record])),
Action::make('check_dns')
->label('Check DNS')
->icon('heroicon-o-arrow-path')
->color('warning')
->action(function ($record) {
$service = app(DmarcCheckService::class);
$result = $service->checkDomain($record->domain, $record->domain_id);
if ($result && $result->valid) {
Notification::make()
->success()
->title('DMARC Record Found')
->body("Policy: {$result->getPolicyLabel()}")
->send();
} else {
Notification::make()
->warning()
->title('No DMARC Record Found')
->body($result?->error_message ?? 'No DMARC record found in DNS')
->send();
}
}),
])
->bulkActions([
BulkActionGroup::make([
Action::make('check_all_dmarc')
->label('Check DMARC for Selected')
->icon('heroicon-o-arrow-path')
->action(function ($records) {
$service = app(DmarcCheckService::class);
$valid = 0;
foreach ($records as $record) {
$result = $service->checkDomain($record->domain, $record->domain_id);
if ($result && $result->valid) {
$valid++;
}
}
Notification::make()
->success()
->title('DMARC Check Complete')
->body("Checked {$records->count()} domains, {$valid} have valid DMARC records")
->send();
}),
]),
]);
}
}
@@ -1,76 +0,0 @@
<?php
namespace VEximweb\Plugin\DnsTools\Filament\Pages\Dmarc;
use Filament\Resources\Pages\ViewRecord;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use VEximweb\Plugin\DnsTools\Filament\Resources\DmarcResource;
use VEximweb\Plugin\DnsTools\Models\DmarcCheck;
use VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService;
class ViewDmarc extends ViewRecord
{
protected static string $resource = DmarcResource::class;
public ?DmarcCheck $dmarc = null;
protected function mutateFormDataBeforeFill(array $data): array
{
// Get the DMARC record for this domain
$this->dmarc = DmarcCheck::where('domain', $this->record->domain)->first();
// If no cache or invalid, check now
if (!$this->dmarc || !$this->dmarc->valid) {
$service = app(DmarcCheckService::class);
$this->dmarc = $service->checkDomain($this->record->domain, $this->record->domain_id);
}
return $data;
}
protected function getHeaderActions(): array
{
return [
Action::make('check_dns')
->label('Check DNS Again')
->icon('heroicon-o-arrow-path')
->color('warning')
->action(function () {
$service = app(DmarcCheckService::class);
$this->dmarc = $service->checkDomain($this->record->domain, $this->record->domain_id);
Notification::make()
->success()
->title('DMARC Record Updated')
->body($this->dmarc?->valid ? 'Record found and updated' : 'No valid record found')
->send();
$this->refreshFormData();
}),
Action::make('generate')
->label('Generate New Record')
->icon('heroicon-o-document-plus')
->color('success')
->url(DmarcResource::getUrl('generate', ['record' => $this->record])),
Action::make('back')
->label('Back to List')
->icon('heroicon-o-arrow-left')
->url(DmarcResource::getUrl('index')),
];
}
protected function getViewData(): array
{
return [
'dmarc' => $this->dmarc,
];
}
public function getTitle(): string
{
return "DMARC Record for {$this->record->domain}";
}
}
@@ -1,27 +0,0 @@
<?php
namespace VEximweb\Plugin\DnsTools\Filament\Providers;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use VEximweb\Plugin\DnsTools\Filament\Resources\DmarcResource;
class DnsToolsPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->id('dns-tools')
->path('dns-tools')
->colors([
'primary' => Color::Blue,
])
->resources([
DmarcResource::class,
])
->navigationGroups([
'DNS Tools',
]);
}
}
@@ -1,46 +0,0 @@
<?php
namespace VEximweb\Plugin\DnsTools\Filament\Resources;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Notifications\Notification;
use VEximweb\Core\Data\Models\Domain;
use VEximweb\Plugin\DnsTools\Models\DmarcCheck;
use VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService;
use VEximweb\Plugin\DnsTools\Dmarc\DmarcRecord;
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcPolicy;
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcAlignment;
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcReporting;
use BackedEnum;
use VEximweb\Plugin\DnsTools\Filament\Pages\Dmarc\ListDmarc;
use VEximweb\Plugin\DnsTools\Filament\Pages\Dmarc\ViewDmarc;
use VEximweb\Plugin\DnsTools\Filament\Pages\Dmarc\GenerateDmarc;
class DmarcResource extends Resource
{
protected static ?string $model = Domain::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
protected static string|\UnitEnum|null $navigationGroup = 'DNS Tools';
protected static ?string $navigationLabel = 'DMARC Management';
protected static ?string $pluralLabel = 'DMARC Management';
protected static ?int $navigationSort = 1;
protected static ?string $slug = 'dnstools/dmarc';
public static function getModel(): string
{
return Domain::class;
}
public static function getPages(): array
{
return [
'index' => ListDmarc::route('/'),
'view' => ViewDmarc::route('/{record}/view'),
'generate' => GenerateDmarc::route('/{record}/generate'),
];
}
}
@@ -9,32 +9,75 @@ use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Hidden;
use VEximweb\Plugin\DnsTools\Models\SystemDomains as Domain;
class GenerateDmarcForm
{
protected static ?string $domain = null;
protected static ?Domain $domainRecord = null;
public static function extend(callable $components, callable $onSave): void
{
$existing = app()->bound('dmarcform.extenders')
? app('dmarcform.extenders')
: ['components' => [], 'hooks' => []];
$existing['components'][] = $components;
$existing['hooks'][] = $onSave;
app()->instance('dmarcform.extenders', $existing);
}
public static function runSaveHooks(mixed $record, array $data): void
{
if (! app()->bound('dmarcform.extenders')) {
return;
}
foreach (app('dmarcform.extenders')['hooks'] as $hook) {
$hook($record, $data);
}
}
/**
* Set the domain for the form.
* Set the domain record for the form.
*/
public static function setDomain(string $domain): void
public static function setDomainRecord(Domain $domain): void
{
static::$domain = $domain;
static::$domainRecord = $domain;
}
/**
* Get the current domain.
* Get the current domain record.
*/
protected static function getDomain(): string
protected static function getDomainRecord(): ?Domain
{
return static::$domain ?? 'example.com';
return static::$domainRecord;
}
/**
* The Filament form schema.
* The Filament form schema - now accepts Domain record instead of string
*/
public static function schema(string $domain): array
public static function schema(Domain $domain): array
{
// Set the domain record for extensions
static::setDomainRecord($domain);
$extra = [];
if (app()->bound('dmarcform.extenders')) {
foreach (app('dmarcform.extenders')['components'] as $extender) {
// Pass the domain record directly to the extender
if (is_callable($extender)) {
$result = $extender($domain);
if (is_array($result)) {
$extra = array_merge($extra, $result);
}
} else {
$extra = array_merge($extra, $extender());
}
}
}
return [
Section::make('Policy')
->columns(2)
@@ -104,29 +147,31 @@ class GenerateDmarcForm
'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_rua_localpart')
->label('Aggregate Reports Email')
->helperText('Email address for aggregate reports (auto-appended with @' . $domain->domain . ')')
->default('dmarc-reports')
->prefix('📊')
->suffix('@' . $domain->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),
TextInput::make('dmarc_ruf_localpart')
->label('Forensic Reports Email')
->helperText('Email address for forensic reports (auto-appended with @' . $domain->domain . ')')
->default('dmarc-forensic')
->prefix('🔍')
->suffix('@' . $domain->domain),
]),
...$extra,
];
}
/**
* Populate the form from the repository.
* Populate the form from the repository - now accepts domain record
*/
public static function values(SettingRepositoryInterface $settings): array
public static function values(SettingRepositoryInterface $settings, Domain $domain): array
{
// Get settings for this domain (you might need to adjust this)
// Get settings for this domain
$rua_localpart = $settings->get('dmarc_rua_localpart', 'dmarc-reports');
$ruf_localpart = $settings->get('dmarc_ruf_localpart', 'dmarc-forensic');
@@ -147,31 +192,31 @@ class GenerateDmarcForm
}
/**
* Save the form back to the repository.
* Save the form back to the repository - now accepts domain record
*/
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]);
}
public static function save(
SettingRepositoryInterface $settings,
array $data,
Domain $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'] ?? []);
}
$settings->set('dmarc_rua', [$data['dmarc_rua_localpart'] . '@' . $domain->domain]);
$settings->set('dmarc_ruf', [$data['dmarc_ruf_localpart'] . '@' . $domain->domain]);
$settings->set('dmarc_reporting', $data['dmarc_reporting'] ?? []);
}
}
@@ -0,0 +1,259 @@
<?php
namespace VEximweb\Plugin\DnsTools\Filament\Resources\Dmarc\Pages;
use Filament\Resources\Pages\Page;
use Filament\Forms\Concerns\InteractsWithForms;
use Illuminate\Support\Facades\Route;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Actions;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Hidden;
use Filament\Actions\Action;
use Filament\Schemas\Schema;
use BackedEnum;
use UnitEnum;
use Filament\Notifications\Notification;
use VEximweb\Core\Data\Repositories\Interfaces\SettingRepositoryInterface;
use VEximweb\Plugin\DnsTools\Models\SystemDomains as Domain;
use VEximweb\Plugin\DnsTools\Services\DmarcRecordService;
class GenerateDmarcPage extends Page
{
use InteractsWithForms;
protected string $view = 'dns-tools::filament.pages.generate-dmarc';
public ?array $data = [];
public Domain $domain;
public ?string $generatedRecord = null;
public static function getResource(): string
{
return \VEximweb\Plugin\DnsTools\Filament\Resources\DnsToolsResource::class;
}
public static function getNavigationIcon(): string|BackedEnum|null
{
return 'heroicon-o-document-text';
}
public static function getNavigationGroup(): string|UnitEnum|null
{
return 'Generate DMARC';
}
public function getTitle(): string
{
return 'Generate DMARC Record';
}
public function getListeners(): array
{
return [
'refreshNotifications' => 'refreshNotifications',
];
}
public function mount(Domain $domain): void
{
$this->domain = $domain;
$domainId = $this->domain->id;
// Load existing settings if any
$settings = app(SettingRepositoryInterface::class);
$this->form->fill($this->getValues($settings));
}
public function form(Schema $schema): Schema
{
return $schema
->schema($this->getFormSchema())
->statePath('data');
}
/**
* Get the form schema as an array of components
*/
protected function getFormSchema(): array
{
$extra = [];
if (app()->bound('dmarcform.extenders')) {
foreach (app('dmarcform.extenders')['components'] as $extender) {
if (is_callable($extender)) {
$result = $extender($this->domain);
if (is_array($result)) {
$extra = array_merge($extra, $result);
}
} else {
$extra = array_merge($extra, $extender());
}
}
}
return [
Section::make('Policy')
->columns(2)
->schema([
Hidden::make('domain_id')
->default($this->domain->id),
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('Forensic Report Triggers')
->helperText('"Any" and "All" are mutually exclusive; DKIM/SPF can be combined.')
->options([
'any' => 'Any mechanism fails (DKIM or SPF, most common) ',
'all' => 'All mechanisms fail (Not recommended)',
'dkim' => 'DKIM fails',
'spf' => 'SPF fails',
])
->rule(fn () => function (string $attribute, $value, \Closure $fail) {
if (in_array('all', $value ?? []) && in_array('any', $value ?? [])) {
$fail("'Any' and 'All' cannot both be selected.");
}
}),
TextInput::make('dmarc_rua_localpart')
->label('Aggregate Reports Email')
->helperText('Email address for aggregate reports (auto-appended with @' . $this->domain->domain . ')')
->default('dmarc-reports')
->suffix('@' . $this->domain->domain),
TextInput::make('dmarc_ruf_localpart')
->label('Forensic Reports Email')
->helperText('Email address for forensic reports (auto-appended with @' . $this->domain->domain . ')')
->default('dmarc-forensic')
->suffix('@' . $this->domain->domain),
]),
...$extra,
Actions::make([
Action::make('generate')
->label('Generate DMARC Record')
->submit('generateDMARC'),
]),
];
}
protected function getValues(SettingRepositoryInterface $settings): array
{
$rua_localpart = $settings->get('dmarc_rua_localpart', 'dmarc-reports');
$ruf_localpart = $settings->get('dmarc_ruf_localpart', 'dmarc-forensic');
return [
'domain_id' => $this->domain->domain_id,
'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,
];
}
public function generateDMARC(DmarcRecordService $dmarc): void
{
$data = $this->form->getState();
\Log::debug('Form data:'. json_encode($data));
$this->generatedRecord = $dmarc->generate($this->domain, $data);
$domainName = $this->domain->domain ?? $this->domain['domain'] ?? (string) $this->domain;
\Log::debug('DMARC Event data:', [
'zone' => $domainName,
'name' => '_dmarc',
'content' => $this->generatedRecord
]);
event(new \App\Events\DmarcKeyGenerated(
zone: $domainName,
name: '_dmarc',
type: 'TXT',
content: $this->generatedRecord,
ttl: 3600,
operation: 'create'
));
$this->dispatch('open-modal', id: 'dmarc-record-modal');
}
// Keep the static extend method for backward compatibility
public static function extend(callable $components, callable $onSave): void
{
$existing = app()->bound('dmarcform.extenders')
? app('dmarcform.extenders')
: ['components' => [], 'hooks' => []];
$existing['components'][] = $components;
$existing['hooks'][] = $onSave;
app()->instance('dmarcform.extenders', $existing);
}
}
+6 -7
View File
@@ -14,18 +14,19 @@ use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use VEximweb\Plugin\DnsTools\Filament\Resources\Dmarc\Pages\GenerateDmarcPage;
class DnsToolsResource extends Resource
{
protected static ?string $model = Domain::class;
protected static ?string $slug = 'dnstools/domains';
protected static ?string $slug = 'dnstools';
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
protected static string|BackedEnum|null $navigationIcon = Heroicon::WrenchScrewdriver;
protected static ?string $recordTitleAttribute = 'domain';
protected static string|\UnitEnum|null $navigationGroup = 'DNS Tools';
protected static string|\UnitEnum|null $navigationGroup = 'DNS';
protected static ?string $navigationLabel = 'DNS Tools';
@@ -81,10 +82,6 @@ class DnsToolsResource extends Resource
return $schema;
}
public static function form_old(Schema $schema): Schema
{
return DomainForm::configure($schema);
}
public static function table(Table $table): Table
{
@@ -168,6 +165,8 @@ class DnsToolsResource extends Resource
{
return [
'index' => ListDomains::route('/'),
//'dmarc' => GenerateDmarcPage::route('/dmarc'),
'generate' => GenerateDmarcPage::route('/{domain}/generate-dmarc'),
//'create' => CreateDomain::route('/create'),
//'edit' => EditDomain::route('/{record}/edit'),
];
+19 -1
View File
@@ -22,6 +22,8 @@ use Filament\Notifications\Notification;
use Filament\Actions\ActionGroup;
use VEximweb\Plugin\DnsTools\Filament\Resources\Dmarc\Modals\GenerateDmarcForm;
use VEximweb\Core\Data\Repositories\Interfaces\SettingRepositoryInterface;
use VEximweb\Plugin\DnsTools\Filament\Resources\DnsToolsResource;
class DomainsTable
{
@@ -208,9 +210,25 @@ class DomainsTable
//EditAction::make(),
ActionGroup::make([
Action::make('generateDmarc')
->label('Generate DMARC')
->icon('heroicon-o-document-text')
->url(fn ($record) => DnsToolsResource::getUrl('generate', ['domain' => $record])),
EditAction::make(),
EditAction::make(),
EditAction::make(),
Action::make('dmarc')
->fillForm(fn (SettingRepositoryInterface $settings, $record) =>
GenerateDmarcForm::values($settings, $record) // Pass the record
)
->form(fn ($record) =>
GenerateDmarcForm::schema($record) // Pass the full record, not just domain string
)
->action(fn (array $data, SettingRepositoryInterface $settings, $record) =>
GenerateDmarcForm::save($settings, $data, $record) // Pass the record
),
/*
Action::make('dmarc')
->fillForm(fn (SettingRepositoryInterface $settings) =>
GenerateDmarcForm::values($settings)
@@ -222,7 +240,7 @@ Action::make('dmarc')
GenerateDmarcForm::save($settings, $data, $record->domain)
),
/*
*/ /*
Action::make('dmarc')
->fillForm(fn (SettingRepositoryInterface $settings) =>
GenerateDmarcForm::values($settings)
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace VEximweb\Plugin\DnsTools\Services;
use CbowOfRivia\DmarcRecordBuilder\DmarcRecord;
use VEximweb\Plugin\DnsTools\Models\SystemDomains as Domain;
class DmarcRecordService
{
public function build(Domain $domain, array $data): DmarcRecord
{
return new DmarcRecord(
policy: $data['dmarc_policy'],
subdomain_policy: $data['dmarc_subdomain_policy'] ?: null,
rua: 'mailto:' . $data['dmarc_rua_localpart'] . '@' . $domain->domain,
ruf: 'mailto:' . $data['dmarc_ruf_localpart'] . '@' . $domain->domain,
adkim: $data['dmarc_adkim'],
aspf: $data['dmarc_aspf'],
reporting: $data['dmarc_reporting'] ?? [],
np: $data['dmarc_np'] ?: null,
psd: $data['dmarc_psd'],
t: $data['dmarc_t'],
);
}
public function generate(Domain $domain, array $data): string
{
return (string) $this->build($domain, $data);
}
}