instance(DnsResolver::class, $resolver); return app(MailDomainChecker::class); } test('a usable mx means deliverable, without claiming the mailbox exists', function () { /** @var TestCase $this */ $check = fakeDns([ 'firma.example' => ['MX' => [['target' => 'mx.firma.example', 'pri' => 10]]], 'mx.firma.example' => ['A' => [['ip' => '203.0.113.5']]], ])->check('firma.example'); expect($check->state)->toBe(EmailDomainState::Deliverable) ->and($check->state->isDeliverable())->toBeTrue() ->and($check->state->isBroken())->toBeFalse() ->and($check->expires_at)->not->toBeNull(); }); test('no mx but an a record is the implicit mx fallback of rfc 5321', function () { /** @var TestCase $this */ $check = fakeDns(['firma.example' => ['A' => [['ip' => '203.0.113.5']]]])->check('firma.example'); expect($check->state)->toBe(EmailDomainState::ImplicitMx) ->and($check->state->isDeliverable())->toBeTrue(); }); test('a null mx says no mail, even with an a record present', function () { /** @var TestCase $this */ // RFC 7505: `MX 0 .` — kein Zurückfallen auf die Webserver-IP. $check = fakeDns([ 'firma.example' => ['MX' => [['target' => '.', 'pri' => 0]], 'A' => [['ip' => '203.0.113.5']]], ])->check('firma.example'); expect($check->state)->toBe(EmailDomainState::NullMx) ->and($check->state->isBroken())->toBeTrue(); }); test('a domain without any records is missing, one with a zone but no mail path is not', function () { /** @var TestCase $this */ $missing = fakeDns(['weg.example' => []])->check('weg.example'); $noPath = fakeDns(['nurweb.example' => ['NS' => [['target' => 'ns1.example']]]])->check('nurweb.example'); expect($missing->state)->toBe(EmailDomainState::DomainMissing) ->and($noPath->state)->toBe(EmailDomainState::NoMailPath); }); test('one usable mx target is enough even when a sibling is broken', function () { /** @var TestCase $this */ $check = fakeDns([ 'firma.example' => ['MX' => [['target' => 'kaputt.example'], ['target' => 'mx2.example']]], 'kaputt.example' => [], 'mx2.example' => ['AAAA' => [['ipv6' => '2001:db8::1']]], ])->check('firma.example'); expect($check->state)->toBe(EmailDomainState::Deliverable); }); test('mx targets that all fail to resolve are a broken configuration', function () { /** @var TestCase $this */ $check = fakeDns([ 'firma.example' => ['MX' => [['target' => 'kaputt.example']]], 'kaputt.example' => [], ])->check('firma.example'); expect($check->state)->toBe(EmailDomainState::BrokenMx) ->and($check->state->isBroken())->toBeTrue(); }); test('a resolver failure is no finding and is retried with growing distance', function () { /** @var TestCase $this */ // Timeout/SERVFAIL darf nie als „Domain kaputt" gelten. $checker = fakeDns(['firma.example' => ['MX' => null]]); $first = $checker->check('firma.example'); $firstExpiry = $first->expires_at; expect($first->state)->toBe(EmailDomainState::Unresolved) ->and($first->state->isBroken())->toBeFalse() ->and($first->attempts)->toBe(1); $second = $checker->check('firma.example', force: true); expect($second->attempts)->toBe(2) ->and($second->expires_at->greaterThan($firstExpiry))->toBeTrue(); }); test('a temporary mx target failure does not become a broken configuration', function () { /** @var TestCase $this */ $check = fakeDns([ 'firma.example' => ['MX' => [['target' => 'mx.example']]], 'mx.example' => ['A' => null, 'AAAA' => null], ])->check('firma.example'); expect($check->state)->toBe(EmailDomainState::Unresolved); }); test('a fresh result is reused and a correction re-checks the new domain', function () { /** @var TestCase $this */ $resolver = new FakeDnsResolver(['firma.example' => []]); app()->instance(DnsResolver::class, $resolver); $checker = app(MailDomainChecker::class); expect($checker->check('firma.example')->state)->toBe(EmailDomainState::DomainMissing); // Domain repariert — der gespeicherte Befund gilt aber noch. $resolver->set('firma.example', 'A', [['ip' => '203.0.113.9']]); expect($checker->check('firma.example')->state)->toBe(EmailDomainState::DomainMissing) ->and($checker->check('firma.example', force: true)->state)->toBe(EmailDomainState::ImplicitMx) ->and(EmailDomainCheck::query()->where('domain', 'firma.example')->first()->first_broken_at)->toBeNull(); }); test('saving a company queues a check for its mail domain, unchanged addresses do not', function () { /** @var TestCase $this */ Queue::fake(); $company = Company::factory()->create(['email' => 'presse@firma.example']); Queue::assertPushed(CheckEmailDomain::class, fn ($job) => $job->domain === 'firma.example'); $company->update(['name' => 'Neuer Name']); Queue::assertPushed(CheckEmailDomain::class, 1); $company->update(['email' => 'presse@andere.example']); Queue::assertPushed(CheckEmailDomain::class, fn ($job) => $job->domain === 'andere.example'); }); test('the domain finding never suppresses an address', function () { /** @var TestCase $this */ fakeDns(['firma.example' => []])->check('firma.example'); expect(app(EmailSuppressionService::class)->isSuppressed('presse@firma.example'))->toBeFalse(); });