<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\tests;
use seekquarry\yioop\library\mail\MailSiteFactory;
use seekquarry\yioop\library\UnitTest;
/**
* Tests for the MTA-STS policy helpers on MailSiteFactory:
* mtaStsMode (posture to policy mode), mtaStsPolicy (policy file
* body for one domain), and mtaStsDomainForHost (request host to
* policy domain). All three are static pure functions of their
* arguments, so the cases are self-contained; no setUp data or
* teardown is required.
*
* @author Chris Pollett
*/
class MailStsPolicyTest extends UnitTest
{
/**
* No set-up state needed; the helpers are static pure
* functions of their arguments.
*/
public function setUp()
{
}
/**
* No teardown needed either.
*/
public function tearDown()
{
}
/**
* Each delivery posture maps to the expected policy mode: the
* insecure posture publishes nothing, spam publishes a testing
* policy, and require publishes an enforce policy.
*/
public function modeMappingTestCase()
{
$this->assertEqual('', MailSiteFactory::mtaStsMode(
'insecure'), "insecure posture publishes no policy");
$this->assertEqual('testing', MailSiteFactory::mtaStsMode(
'spam'), "spam posture publishes a testing policy");
$this->assertEqual('enforce', MailSiteFactory::mtaStsMode(
'require'), "require posture publishes enforce");
}
/**
* The policy body carries the RFC 8461 fields: the STSv1
* version, the mode, both a wildcard and an apex mx line for
* the domain, and a max_age, with CRLF line endings.
*/
public function policyBodyTestCase()
{
$policy = MailSiteFactory::mtaStsPolicy('testing',
'yioop.com');
$this->assertTrue(strpos($policy, "version: STSv1") === 0,
"policy starts with the STSv1 version line");
$this->assertTrue(strpos($policy, "mode: testing") !== false,
"policy carries the mode");
$this->assertTrue(strpos($policy, "mx: *.yioop.com") !== false,
"policy authorizes subdomain MX hosts");
$this->assertTrue(strpos($policy, "mx: yioop.com") !== false,
"policy authorizes an apex MX host");
$this->assertTrue(strpos($policy, "max_age: ") !== false,
"policy carries a max_age");
$this->assertTrue(strpos($policy, "\r\n") !== false,
"policy uses CRLF line endings");
}
/**
* An empty mode yields an empty policy body, so the route can
* treat it as nothing to serve.
*/
public function emptyModeYieldsNoPolicyTestCase()
{
$this->assertEqual('', MailSiteFactory::mtaStsPolicy('',
'yioop.com'), "no mode means no policy body");
}
/**
* The dedicated mta-sts.<domain> host resolves to that
* configured domain, matched case-insensitively, while a host
* for an unconfigured domain resolves to nothing.
*/
public function hostResolvesConfiguredDomainTestCase()
{
$domains = ['yioop.com', 'example.org'];
$this->assertEqual('yioop.com',
MailSiteFactory::mtaStsDomainForHost(
'mta-sts.yioop.com', $domains),
"mta-sts host resolves to its domain");
$this->assertEqual('example.org',
MailSiteFactory::mtaStsDomainForHost(
'MTA-STS.Example.Org', $domains),
"host match is case-insensitive");
$this->assertEqual('',
MailSiteFactory::mtaStsDomainForHost(
'mta-sts.notmine.com', $domains),
"unconfigured domain resolves to nothing");
}
/**
* A bare configured domain (without the mta-sts prefix) does
* not resolve, so the apex site is not mistaken for the policy
* host.
*/
public function bareDomainDoesNotResolveTestCase()
{
$domains = ['yioop.com'];
$this->assertEqual('',
MailSiteFactory::mtaStsDomainForHost('yioop.com',
$domains), "bare domain is not the policy host");
}
/**
* Loopback hosts resolve to the first configured domain as a
* testing convenience, and a port suffix on the host is
* ignored.
*/
public function loopbackTestPathTestCase()
{
$domains = ['yioop.com', 'example.org'];
$this->assertEqual('yioop.com',
MailSiteFactory::mtaStsDomainForHost('localhost',
$domains), "localhost resolves to the first domain");
$this->assertEqual('yioop.com',
MailSiteFactory::mtaStsDomainForHost('127.0.0.1:8080',
$domains), "a port suffix is ignored");
}
/**
* The suggested SPF and DMARC record strings carry their
* version tags, SPF authorizes the domain's MX hosts with a
* soft-fail default, and DMARC starts in report-only mode with
* the report address woven into the rua tag when given.
*/
public function spfAndDmarcRecordsTestCase()
{
$spf = MailSiteFactory::spfTxtRecord();
$this->assertTrue(strpos($spf, 'v=spf1') === 0,
"SPF record starts with the version tag");
$this->assertTrue(strpos($spf, 'mx') !== false,
"SPF authorizes the MX hosts");
$this->assertTrue(strpos($spf, '~all') !== false,
"SPF soft-fails other senders by default");
$dmarc = MailSiteFactory::dmarcTxtRecord(
'postmaster@yioop.com');
$this->assertTrue(strpos($dmarc, 'v=DMARC1') === 0,
"DMARC record starts with the version tag");
$this->assertTrue(strpos($dmarc, 'p=none') !== false,
"DMARC starts in report-only policy");
$this->assertTrue(
strpos($dmarc, 'rua=mailto:postmaster@yioop.com')
!== false, "DMARC carries the aggregate-report address");
$bare = MailSiteFactory::dmarcTxtRecord('');
$this->assertTrue(strpos($bare, 'rua=') === false,
"DMARC omits rua when no report address is given");
}
/**
* The _mta-sts discovery record carries the STSv1 version and
* the given id when a policy is published, and is empty when
* the posture publishes no policy.
*/
public function mtaStsTxtRecordTestCase()
{
$record = MailSiteFactory::mtaStsTxtRecord('testing', 42);
$this->assertTrue(strpos($record, 'v=STSv1') === 0,
"discovery record starts with the version tag");
$this->assertTrue(strpos($record, 'id=42') !== false,
"discovery record carries the policy id");
$this->assertEqual('',
MailSiteFactory::mtaStsTxtRecord('', 42),
"no discovery record when no policy is published");
}
/**
* The assembled suggestions include SPF and DMARC for every
* domain, add the MTA-STS records only under a policy-
* publishing posture, and skip them under the insecure
* posture.
*/
public function suggestedRecordsAssemblyTestCase()
{
$secure = MailSiteFactory::suggestedDnsRecords(
['yioop.com'], 'spam', 100, 'postmaster@yioop.com');
$hosts = array_column($secure['yioop.com'], 'host');
$this->assertTrue(in_array('yioop.com', $hosts),
"SPF record present at the apex host");
$this->assertTrue(in_array('_dmarc.yioop.com', $hosts),
"DMARC record present at _dmarc host");
$this->assertTrue(in_array('_mta-sts.yioop.com', $hosts),
"MTA-STS discovery record present under spam posture");
$this->assertTrue(in_array('mta-sts.yioop.com', $hosts),
"MTA-STS policy host record present under spam");
$insecure = MailSiteFactory::suggestedDnsRecords(
['yioop.com'], 'insecure', 100, '');
$hosts = array_column($insecure['yioop.com'], 'host');
$this->assertTrue(!in_array('_mta-sts.yioop.com', $hosts),
"no MTA-STS record under the insecure posture");
$this->assertTrue(in_array('yioop.com', $hosts),
"SPF still suggested under the insecure posture");
}
/**
* When a DKIM selector and public-key record are supplied, the
* suggestions include the <selector>._domainkey.<domain> TXT
* record carrying that value; when no record is supplied (no
* key generated) the DKIM row is omitted.
*/
public function dkimRecordInSuggestionsTestCase()
{
$with = MailSiteFactory::suggestedDnsRecords(['yioop.com'],
'spam', 100, '', 'yioop20260602',
'v=DKIM1; k=rsa; p=ABCDEF');
$dkim = null;
foreach ($with['yioop.com'] as $row) {
if ($row['host'] === 'yioop20260602._domainkey.yioop.com') {
$dkim = $row;
}
}
$this->assertTrue($dkim !== null,
"DKIM record present at the selector host");
$this->assertEqual('v=DKIM1; k=rsa; p=ABCDEF',
$dkim['value'], "DKIM record carries the key value");
$without = MailSiteFactory::suggestedDnsRecords(
['yioop.com'], 'spam', 100, '', 'yioop20260602', '');
$hosts = array_column($without['yioop.com'], 'host');
$this->assertTrue(
!in_array('yioop20260602._domainkey.yioop.com', $hosts),
"no DKIM row when no key record is supplied");
}
/**
* A www. or mta-sts. host of a domain already in the list is
* not given its own record set, since it is not an independent
* mail domain; a bare www./mta-sts. host whose apex is not
* listed still gets one.
*/
public function subdomainOfListedApexSkippedTestCase()
{
$records = MailSiteFactory::suggestedDnsRecords(
['pollett.org', 'mta-sts.pollett.org',
'www.pollett.org'], 'spam', 100, '');
$this->assertTrue(isset($records['pollett.org']),
"apex mail domain keeps its record set");
$this->assertTrue(!isset($records['mta-sts.pollett.org']),
"mta-sts host of a listed apex is skipped");
$this->assertTrue(!isset($records['www.pollett.org']),
"www host of a listed apex is skipped");
$alone = MailSiteFactory::suggestedDnsRecords(
['www.example.com'], 'spam', 100, '');
$this->assertTrue(isset($alone['www.example.com']),
"www host with no listed apex still gets a set");
}
}