Compare commits

...

2 Commits

Author SHA1 Message Date
Mr Sleeps 1ef8f39c3b Added .gitignore 2026-07-01 21:59:49 +01:00
Mr Sleeps e36b39d7c0 Updated composer required 2026-07-01 21:59:10 +01:00
8 changed files with 13564 additions and 3 deletions
+34
View File
@@ -0,0 +1,34 @@
*.log
.DS_Store
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.codex
/.cursor/
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/fonts-manifest.dev.json
/public/hot
/public/storage
/storage/*.key
/storage/pail
/storage/framework/views/
/vendor
_ide_helper.php
Homestead.json
Homestead.yaml
Thumbs.db
bootstrap/cache/
composer.dev
OLDBACKUPS
composer.local.json
build/
test_curl
+2 -1
View File
@@ -20,7 +20,8 @@
"require": {
"php": "^8.4",
"filament/filament": "^5.0",
"cbowofrivia/dmarc-record-builder": "^4.0"
"cbowofrivia/dmarc-record-builder": "^4.0",
"mlocati/spf-lib": "^3.3"
},
"autoload": {
"psr-4": {
Generated
+12667
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
<x-filament-panels::page>
<form wire:submit="generateSpf">
{{ $this->form }}
</form>
<x-filament::modal id="dmarc-record-modal" width="2xl">
<x-slot name="heading">
Generated SPF 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>
+3 -1
View File
@@ -15,6 +15,7 @@ use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use VEximweb\Plugin\DnsTools\Filament\Resources\Dmarc\Pages\GenerateDmarcPage;
use VEximweb\Plugin\DnsTools\Filament\Resources\Spf\Pages\GenerateSpfPage;
class DnsToolsResource extends Resource
{
@@ -166,7 +167,8 @@ class DnsToolsResource extends Resource
return [
'index' => ListDomains::route('/'),
//'dmarc' => GenerateDmarcPage::route('/dmarc'),
'generate' => GenerateDmarcPage::route('/{domain}/generate-dmarc'),
'generateDmarc' => GenerateDmarcPage::route('/{domain}/generate-dmarc'),
'generateSpf' => GenerateSpfPage::route('/{domain}/generate-spf'),
//'create' => CreateDomain::route('/create'),
//'edit' => EditDomain::route('/{record}/edit'),
];
@@ -0,0 +1,346 @@
<?php
namespace VEximweb\Plugin\DnsTools\Filament\Resources\Spf\Pages;
use Filament\Resources\Pages\Page;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Actions;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\Repeater;
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\SpfRecordService;
class GenerateSpfPage extends Page
{
use InteractsWithForms;
protected string $view = 'dns-tools::filament.pages.generate-spf';
public ?array $data = [];
public Domain $domain;
public ?string $generatedRecord = null;
public ?int $lookupCount = 0;
public ?string $dnsName = '';
public ?array $validationIssues = [];
public static function getResource(): string
{
return \VEximweb\Plugin\DnsTools\Filament\Resources\DnsToolsResource::class;
}
public static function getNavigationIcon(): string|BackedEnum|null
{
return 'heroicon-o-shield-check';
}
public static function getNavigationGroup(): string|UnitEnum|null
{
return 'Generate SPF';
}
public function getTitle(): string
{
return 'Generate SPF Record';
}
public function getListeners(): array
{
return [
'refreshNotifications' => 'refreshNotifications',
];
}
public function mount(Domain $domain): void
{
$this->domain = $domain;
// Get domain name
$domainName = $domain->domain;
// Load existing settings if any
$settings = app(SettingRepositoryInterface::class);
$formData = $this->getValues($settings);
// Explicitly set the domain fields
$formData['domain'] = $domainName;
$formData['domain_id'] = $domain->domain_id;
// Fill the form
$this->form->fill($formData);
// Set DNS name
$this->dnsName = $domainName;
// Initialize validation issues
$this->validationIssues = [];
}
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('spfform.extenders')) {
foreach (app('spfform.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('Domain Information')
->columns(2)
->schema([
Hidden::make('domain_id')
->default($this->domain->domain_id ?? null),
TextInput::make('domain')
->label('Domain')
->default($this->domain->domain ?? '')
->disabled()
->dehydrated(false)
->helperText('The domain for which this SPF record is being generated'),
]),
Section::make('Policy & Fail Settings')
->columns(2)
->schema([
Select::make('spf_fail_policy')
->label('Fail Policy')
->helperText('How receiving servers should handle SPF failures')
->options([
'-all' => 'Hard Fail (Recommended) - Reject emails that fail SPF',
'~all' => 'Soft Fail - Accept but mark as suspicious',
'?all' => 'Neutral - Do nothing',
'+all' => 'Permit All (Not recommended - breaks SPF)',
])
->default('-all')
->required(),
Select::make('ttl')
->label('TTL (Time to Live)')
->options([
'300' => '300 (5 minutes - testing)',
'3600' => '3600 (1 hour - standard)',
'86400' => '86400 (24 hours - stable)',
])
->default('3600')
->required(),
]),
Section::make('Sending Services (Include Mechanisms)')
->schema([
Repeater::make('spf_includes')
->label('Email Sending Services')
->helperText('Add the include mechanisms for your email providers (e.g., Google Workspace, SendGrid, Mailchimp)')
->schema([
TextInput::make('service')
->label('Service Include')
->placeholder('e.g., spf.google.com')
->helperText('Enter the domain after "include:" (e.g., "spf.google.com" not "include:spf.google.com")')
->required(),
])
->columns(1)
->defaultItems(0)
->reorderable()
->addActionLabel('Add Email Service'),
]),
Section::make('IP Addresses')
->columns(2)
->schema([
Repeater::make('spf_ipv4')
->label('IPv4 Addresses')
->helperText('Add dedicated IPv4 addresses or CIDR blocks')
->schema([
TextInput::make('ipv4')
->label('IPv4 Address')
->placeholder('e.g., 192.0.2.0 or 192.0.2.0/24')
->required()
->rule('ipv4'),
])
->columns(1)
->defaultItems(0)
->reorderable()
->addActionLabel('Add IPv4 Address'),
Repeater::make('spf_ipv6')
->label('IPv6 Addresses')
->helperText('Add dedicated IPv6 addresses or CIDR blocks')
->schema([
TextInput::make('ipv6')
->label('IPv6 Address')
->placeholder('e.g., 2001:db8:: or 2001:db8::/32')
->required()
->rule('ipv6'),
])
->columns(1)
->defaultItems(0)
->reorderable()
->addActionLabel('Add IPv6 Address'),
]),
Section::make('Mail Server Settings')
->columns(2)
->schema([
Toggle::make('spf_use_mx')
->label('Use MX Records')
->helperText('Allow all mail servers listed in your domain\'s MX records to send mail')
->default(false),
TextInput::make('spf_mx_domain')
->label('Custom MX Domain')
->placeholder('Optional: mail.otherdomain.com')
->helperText('Use MX records from another domain (e.g., "mx:mail.otherdomain.com")')
->visible(fn ($get) => $get('spf_use_mx') === true),
Toggle::make('spf_use_a')
->label('Use A Record')
->helperText('Allow the IP found in your domain\'s A record to send mail')
->default(false),
TextInput::make('spf_a_domain')
->label('Custom A Domain')
->placeholder('Optional: mail.otherdomain.com')
->helperText('Use A record from another domain (e.g., "a:mail.otherdomain.com")')
->visible(fn ($get) => $get('spf_use_a') === true),
]),
Section::make('Advanced Settings')
->collapsible()
->collapsed()
->schema([
TextInput::make('spf_redirect')
->label('Redirect')
->placeholder('e.g., _spf.facebook.com')
->helperText('Points the SPF query to another domain\'s SPF record entirely'),
TextInput::make('spf_exp')
->label('Exp (Explanation)')
->placeholder('e.g., %{i} is not authorized')
->helperText('Sets an explanation text to show to receivers when an email fails (rarely used)'),
Select::make('spf_ptr')
->label('PTR Mechanism')
->options([
'none' => 'Do not use PTR (Recommended)',
'ptr' => 'Use PTR (Not recommended - deprecated)',
])
->default('none')
->helperText('PTR is deprecated and causes massive DNS load. Use only if absolutely necessary.'),
TextInput::make('spf_exists')
->label('Exists')
->placeholder('e.g., exists.%{i}.example.com')
->helperText('Complex DNS lookup query (only for power users)'),
]),
...$extra,
Actions::make([
Action::make('generate')
->label('Generate SPF Record')
->submit('generateSpf'),
]),
];
}
protected function getValues(SettingRepositoryInterface $settings): array
{
return [
'domain_id' => $this->domain->domain_id ?? null,
'domain' => $this->domain->domain ?? '',
'spf_fail_policy' => $settings->get('spf_fail_policy', '-all'),
'ttl' => $settings->get('spf_ttl', '3600'),
'spf_includes' => $settings->get('spf_includes', []),
'spf_ipv4' => $settings->get('spf_ipv4', []),
'spf_ipv6' => $settings->get('spf_ipv6', []),
'spf_use_mx' => $settings->get('spf_use_mx', false),
'spf_mx_domain' => $settings->get('spf_mx_domain', ''),
'spf_use_a' => $settings->get('spf_use_a', false),
'spf_a_domain' => $settings->get('spf_a_domain', ''),
'spf_redirect' => $settings->get('spf_redirect', ''),
'spf_exp' => $settings->get('spf_exp', ''),
'spf_ptr' => $settings->get('spf_ptr', 'none'),
'spf_exists' => $settings->get('spf_exists', ''),
];
}
public function generateSpf(SpfRecordService $spf): void
{
$data = $this->form->getState();
\Log::debug('SPF Form data:' . json_encode($data));
// Generate the SPF record
$result = $spf->generate($this->domain, $data);
$this->generatedRecord = $result['record'];
$this->lookupCount = $result['lookups'] ?? 0;
$this->dnsName = $this->domain->domain ?? '';
$this->validationIssues = $result['issues'] ?? [];
// Log the generated record
\Log::debug('SPF Event data:', [
'zone' => $this->dnsName,
'name' => '_dmarc',
'content' => $this->generatedRecord
]);
// Dispatch event
event(new \App\Events\SpfKeyGenerated(
zone: $this->dnsName,
name: '_dmarc',
type: 'TXT',
content: $this->generatedRecord,
ttl: (int) ($data['ttl'] ?? 3600),
operation: 'create'
));
// Show the modal with the generated record
$this->dispatch('open-modal', id: 'spf-record-modal');
}
// Keep the static extend method for backward compatibility
public static function extend(callable $components, callable $onSave): void
{
$existing = app()->bound('spfform.extenders')
? app('spfform.extenders')
: ['components' => [], 'hooks' => []];
$existing['components'][] = $components;
$existing['hooks'][] = $onSave;
app()->instance('spfform.extenders', $existing);
}
}
@@ -213,7 +213,11 @@ class DomainsTable
Action::make('generateDmarc')
->label('Generate DMARC')
->icon('heroicon-o-document-text')
->url(fn ($record) => DnsToolsResource::getUrl('generate', ['domain' => $record])),
->url(fn ($record) => DnsToolsResource::getUrl('generateDmarc', ['domain' => $record])),
Action::make('generateSpf')
->label('Generate SPF')
->icon('heroicon-o-document-text')
->url(fn ($record) => DnsToolsResource::getUrl('generateSpf', ['domain' => $record])),
EditAction::make(),
EditAction::make(),
EditAction::make(),
+482
View File
@@ -0,0 +1,482 @@
<?php
namespace VEximweb\Plugin\DnsTools\Services;
use SPFLib\Record;
use SPFLib\Term\Mechanism;
use SPFLib\Term\Modifier;
use SPFLib\SemanticValidator;
use SPFLib\Exception\InvalidTermException;
use VEximweb\Plugin\DnsTools\Models\SystemDomains as Domain;
use Illuminate\Support\Facades\Log;
class SpfRecordService
{
/**
* Generate an SPF record from form data
*
* @param Domain $domain
* @param array $data
* @return array
*/
public function generate(Domain $domain, array $data): array
{
try {
// Initialize the SPF record with the domain
$record = new Record($domain->domain);
// Add the version mechanism (v=spf1)
$record->addTerm(new Mechanism\VersionMechanism());
// 1. Add Include mechanisms (sending services)
if (!empty($data['spf_includes'])) {
foreach ($data['spf_includes'] as $include) {
if (!empty($include['service'])) {
$record->addTerm(
new Mechanism\IncludeMechanism(
Mechanism::QUALIFIER_PASS,
$include['service']
)
);
}
}
}
// 2. Add IPv4 addresses
if (!empty($data['spf_ipv4'])) {
foreach ($data['spf_ipv4'] as $ipv4) {
if (!empty($ipv4['ipv4'])) {
$record->addTerm(
new Mechanism\Ip4Mechanism(
Mechanism::QUALIFIER_PASS,
$ipv4['ipv4']
)
);
}
}
}
// 3. Add IPv6 addresses
if (!empty($data['spf_ipv6'])) {
foreach ($data['spf_ipv6'] as $ipv6) {
if (!empty($ipv6['ipv6'])) {
$record->addTerm(
new Mechanism\Ip6Mechanism(
Mechanism::QUALIFIER_PASS,
$ipv6['ipv6']
)
);
}
}
}
// 4. Add MX mechanism
if (!empty($data['spf_use_mx'])) {
if (!empty($data['spf_mx_domain'])) {
// Custom MX domain
$record->addTerm(
new Mechanism\MxMechanism(
Mechanism::QUALIFIER_PASS,
$data['spf_mx_domain']
)
);
} else {
// Use current domain's MX records
$record->addTerm(
new Mechanism\MxMechanism(
Mechanism::QUALIFIER_PASS
)
);
}
}
// 5. Add A mechanism
if (!empty($data['spf_use_a'])) {
if (!empty($data['spf_a_domain'])) {
// Custom A domain
$record->addTerm(
new Mechanism\AMechanism(
Mechanism::QUALIFIER_PASS,
$data['spf_a_domain']
)
);
} else {
// Use current domain's A record
$record->addTerm(
new Mechanism\AMechanism(
Mechanism::QUALIFIER_PASS
)
);
}
}
// 6. Add Advanced mechanisms
// PTR (deprecated - use with caution)
if (!empty($data['spf_ptr']) && $data['spf_ptr'] === 'ptr') {
$record->addTerm(
new Mechanism\PtrMechanism(
Mechanism::QUALIFIER_PASS
)
);
}
// Exists mechanism
if (!empty($data['spf_exists'])) {
$record->addTerm(
new Mechanism\ExistsMechanism(
Mechanism::QUALIFIER_PASS,
$data['spf_exists']
)
);
}
// 7. Add Redirect modifier (must be added before the ALL mechanism)
if (!empty($data['spf_redirect'])) {
try {
$record->addTerm(
new Modifier\RedirectModifier($data['spf_redirect'])
);
} catch (InvalidTermException $e) {
Log::warning('Invalid redirect modifier: ' . $e->getMessage());
}
}
// 8. Add Exp modifier
if (!empty($data['spf_exp'])) {
try {
$record->addTerm(
new Modifier\ExpModifier($data['spf_exp'])
);
} catch (InvalidTermException $e) {
Log::warning('Invalid exp modifier: ' . $e->getMessage());
}
}
// 9. Add the fail policy (ALL mechanism - MUST be last)
$qualifier = $this->getQualifier($data['spf_fail_policy'] ?? '-all');
$record->addTerm(new Mechanism\AllMechanism($qualifier));
// Convert the record to string
$spfString = (string) $record;
// Validate the record and get issues
$issues = $this->validateRecord($record);
$lookupCount = $this->countLookups($record);
// Log any issues
if (!empty($issues)) {
Log::warning('SPF record validation issues:', [
'domain' => $domain->domain,
'issues' => $issues
]);
}
return [
'record' => $spfString,
'lookups' => $lookupCount,
'issues' => $issues,
'is_valid' => empty($issues),
];
} catch (\Exception $e) {
Log::error('Failed to generate SPF record: ' . $e->getMessage(), [
'domain' => $domain->domain,
'trace' => $e->getTraceAsString()
]);
return [
'record' => 'v=spf1 -all', // Fallback to a safe default
'lookups' => 0,
'issues' => ['Error: ' . $e->getMessage()],
'is_valid' => false,
];
}
}
/**
* Get the qualifier for the ALL mechanism
*
* @param string $policy
* @return int
*/
protected function getQualifier(string $policy): int
{
return match($policy) {
'-all' => Mechanism::QUALIFIER_FAIL,
'~all' => Mechanism::QUALIFIER_SOFTFAIL,
'?all' => Mechanism::QUALIFIER_NEUTRAL,
'+all' => Mechanism::QUALIFIER_PASS,
default => Mechanism::QUALIFIER_FAIL,
};
}
/**
* Validate the SPF record and return any issues
*
* @param Record $record
* @return array
*/
protected function validateRecord(Record $record): array
{
try {
$validator = new SemanticValidator();
$issues = $validator->validate($record);
$formattedIssues = [];
foreach ($issues as $issue) {
$formattedIssues[] = [
'level' => $issue->getLevel(), // 'info', 'warning', 'error'
'message' => $issue->getMessage(),
'term' => $issue->getTerm() ? (string) $issue->getTerm() : null,
];
}
return $formattedIssues;
} catch (\Exception $e) {
return [
[
'level' => 'error',
'message' => 'Validation failed: ' . $e->getMessage(),
'term' => null,
]
];
}
}
/**
* Count the number of DNS lookups in the SPF record
*
* @param Record $record
* @return int
*/
protected function countLookups(Record $record): int
{
$lookupCount = 0;
$terms = $record->getTerms();
foreach ($terms as $term) {
// Mechanisms that cause DNS lookups
if ($term instanceof Mechanism\IncludeMechanism ||
$term instanceof Mechanism\MxMechanism ||
$term instanceof Mechanism\AMechanism ||
$term instanceof Mechanism\PtrMechanism ||
$term instanceof Mechanism\ExistsMechanism) {
$lookupCount++;
}
// Redirect modifier also causes a lookup
if ($term instanceof Modifier\RedirectModifier) {
$lookupCount++;
}
}
return $lookupCount;
}
/**
* Test if an IP address is allowed by the SPF record
*
* @param string $ip
* @param string $domain
* @param array $data
* @return bool
*/
public function testIp(string $ip, string $domain, array $data): bool
{
try {
// Generate the record first
$result = $this->generate(new Domain(['domain' => $domain]), $data);
// Parse the record
$record = Record::fromString($result['record']);
// Check if the IP is allowed
// Note: This is a simplified check. In production, you'd use a proper SPF checker
// that performs actual DNS lookups. The mlocati/spf-lib doesn't have a built-in
// checker, so you might want to use a separate package for this.
return true; // Placeholder
} catch (\Exception $e) {
Log::error('Failed to test SPF record: ' . $e->getMessage());
return false;
}
}
/**
* Get a human-readable description of the SPF record
*
* @param string $recordString
* @return array
*/
public function describe(string $recordString): array
{
try {
$record = Record::fromString($recordString);
$terms = $record->getTerms();
$description = [
'version' => 'v=spf1',
'mechanisms' => [],
'modifiers' => [],
'summary' => '',
];
foreach ($terms as $term) {
if ($term instanceof Mechanism\VersionMechanism) {
continue;
}
$termString = (string) $term;
if ($term instanceof Mechanism\AllMechanism) {
$description['mechanisms'][] = [
'type' => 'all',
'value' => $termString,
'description' => $this->getMechanismDescription($term),
];
} elseif ($term instanceof Mechanism\IncludeMechanism) {
$description['mechanisms'][] = [
'type' => 'include',
'value' => $termString,
'domain' => $term->getDomain(),
'description' => 'Include SPF records from ' . $term->getDomain(),
];
} elseif ($term instanceof Mechanism\Ip4Mechanism) {
$description['mechanisms'][] = [
'type' => 'ip4',
'value' => $termString,
'ip' => $term->getIp(),
'description' => 'Allow IP ' . $term->getIp(),
];
} elseif ($term instanceof Mechanism\Ip6Mechanism) {
$description['mechanisms'][] = [
'type' => 'ip6',
'value' => $termString,
'ip' => $term->getIp(),
'description' => 'Allow IP ' . $term->getIp(),
];
} elseif ($term instanceof Mechanism\MxMechanism) {
$description['mechanisms'][] = [
'type' => 'mx',
'value' => $termString,
'domain' => $term->getDomain() ?? 'current domain',
'description' => 'Allow MX servers for ' . ($term->getDomain() ?? 'this domain'),
];
} elseif ($term instanceof Mechanism\AMechanism) {
$description['mechanisms'][] = [
'type' => 'a',
'value' => $termString,
'domain' => $term->getDomain() ?? 'current domain',
'description' => 'Allow A record for ' . ($term->getDomain() ?? 'this domain'),
];
} elseif ($term instanceof Modifier\RedirectModifier) {
$description['modifiers'][] = [
'type' => 'redirect',
'value' => $termString,
'domain' => $term->getDomain(),
'description' => 'Redirect to ' . $term->getDomain(),
];
} elseif ($term instanceof Modifier\ExpModifier) {
$description['modifiers'][] = [
'type' => 'exp',
'value' => $termString,
'domain' => $term->getDomain(),
'description' => 'Explanation: ' . $term->getDomain(),
];
} else {
$description['mechanisms'][] = [
'type' => 'unknown',
'value' => $termString,
'description' => 'Unknown mechanism',
];
}
}
// Generate a summary
$parts = [];
foreach ($description['mechanisms'] as $mech) {
if ($mech['type'] !== 'all') {
$parts[] = $mech['value'];
}
}
$description['summary'] = implode(' ', $parts);
return $description;
} catch (\Exception $e) {
return [
'error' => 'Failed to parse SPF record: ' . $e->getMessage(),
];
}
}
/**
* Get a description for a mechanism
*
* @param mixed $mechanism
* @return string
*/
protected function getMechanismDescription($mechanism): string
{
if ($mechanism instanceof Mechanism\AllMechanism) {
$qualifier = $mechanism->getQualifier();
return match($qualifier) {
Mechanism::QUALIFIER_FAIL => 'Reject all other senders (Hard Fail)',
Mechanism::QUALIFIER_SOFTFAIL => 'Accept but mark as suspicious (Soft Fail)',
Mechanism::QUALIFIER_NEUTRAL => 'Do nothing (Neutral)',
Mechanism::QUALIFIER_PASS => 'Allow all senders (DANGEROUS)',
default => 'Unknown qualifier',
};
}
return 'SPF mechanism';
}
/**
* Flatten a complex SPF record to its simplest form
* (Useful for reducing lookups)
*
* @param string $recordString
* @return array
*/
public function flattenRecord(string $recordString): array
{
try {
$record = Record::fromString($recordString);
$terms = $record->getTerms();
$ip4List = [];
$ip6List = [];
$includes = [];
$otherMechanisms = [];
$allQualifier = null;
foreach ($terms as $term) {
if ($term instanceof Mechanism\Ip4Mechanism) {
$ip4List[] = $term->getIp();
} elseif ($term instanceof Mechanism\Ip6Mechanism) {
$ip6List[] = $term->getIp();
} elseif ($term instanceof Mechanism\IncludeMechanism) {
$includes[] = $term->getDomain();
} elseif ($term instanceof Mechanism\AllMechanism) {
$allQualifier = $term->getQualifier();
} else {
$otherMechanisms[] = (string) $term;
}
}
return [
'ip4' => $ip4List,
'ip6' => $ip6List,
'includes' => $includes,
'other' => $otherMechanisms,
'all_qualifier' => $allQualifier,
'lookup_reduction' => count($includes) > 0 ? 'Consider flattening includes' : 'Already optimal',
];
} catch (\Exception $e) {
return [
'error' => 'Failed to flatten SPF record: ' . $e->getMessage(),
];
}
}
}