<?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;
/*
* Signal MailServer.php to define its class without running any of
* its executable top-level code, so loading it for this test never
* boots the daemon or disturbs a running web server. Defined
* before the MailServer class is referenced below.
*/
if (!defined(
"seekquarry\\yioop\\executables\\MAIL_SERVER_TEST_LOAD")) {
define("seekquarry\\yioop\\executables\\MAIL_SERVER_TEST_LOAD",
true);
}
use seekquarry\yioop\executables\MailServer;
use seekquarry\yioop\library\UnitTest;
/**
* Tests for MailServer::deriveListenConfig, the pure function that
* turns the mail settings into the listener configuration array
* MailSite::listen consumes. The cases walk the decision matrix:
* TLS available or not, STARTTLS versus implicit TLS, each delivery
* posture, and loopback versus external binding. Because the
* function takes its inputs as arguments and returns an array, the
* cases are self-contained; no setUp data or teardown is required.
*
* @author Chris Pollett
*/
class MailServerConfigTest extends UnitTest
{
/**
* The conventional port settings used by most cases: plaintext
* SMTP 25 and IMAP 143, secure SMTPS 465 and IMAPS 993.
* @var array
*/
public $ports;
/**
* A SERVER_CONTEXT carrying an ssl block, standing in for a
* configured certificate so TLS is available.
* @var array
*/
public $ssl_context;
/**
* Sets up the shared port map and a TLS-bearing server
* context reused across cases.
*/
public function setUp()
{
$this->ports = ['smtp' => 25, 'submission' => 587,
'imap' => 143, 'smtps' => 465, 'imaps' => 993];
$this->ssl_context = ['ssl' =>
['local_cert' => '/x.crt', 'local_pk' => '/x.key']];
}
/**
* No teardown needed; the function under test is pure.
*/
public function tearDown()
{
}
/**
* Calls deriveListenConfig with this test's standard inputs,
* allowing each argument to be overridden.
* @param array $server_context SERVER_CONTEXT to pass
* @param string $posture delivery-security posture
* @param bool $use_starttls STARTTLS versus implicit TLS
* @param bool $external public versus loopback bind
* @param array $ports optional port-map override
* @return array the derived listen configuration
*/
public function derive($server_context, $posture, $use_starttls,
$external, $ports = null)
{
$ports = ($ports === null) ? $this->ports : $ports;
return MailServer::deriveListenConfig($ports,
$server_context, $posture, $use_starttls, $external,
'mail.example.com');
}
/**
* With no certificate the secure ports stay closed, STARTTLS
* cannot be offered, and SERVER_CONTEXT is not forwarded to
* listen.
*/
public function noCertificateClosesSecurePortsTestCase()
{
$config = $this->derive([], 'insecure', false, false);
$this->assertEqual(25, $config['SMTP_PORT'],
"plaintext SMTP binds on 25 without a cert");
$this->assertEqual(143, $config['IMAP_PORT'],
"plaintext IMAP binds on 143 without a cert");
$this->assertEqual(587, $config['SUBMISSION_PORT'],
"submission port binds without a cert");
$this->assertEqual(0, $config['SMTPS_PORT'],
"no implicit SMTPS socket without a cert");
$this->assertEqual(0, $config['IMAPS_PORT'],
"no implicit IMAPS socket without a cert");
$this->assertTrue(!isset($config['SERVER_CONTEXT']),
"SERVER_CONTEXT not forwarded when no cert");
}
/**
* The insecure posture allows plaintext AUTH even when a
* certificate is present.
*/
public function insecurePostureAllowsPlaintextAuthTestCase()
{
$config = $this->derive($this->ssl_context, 'insecure',
false, true);
$this->assertTrue($config['ALLOW_PLAINTEXT_AUTH'],
"insecure posture allows plaintext AUTH");
}
/**
* With a certificate, STARTTLS selected, and a secure posture,
* every listener opens together: the plaintext SMTP, submission,
* and IMAP ports (which upgrade with STARTTLS) plus the wrapped
* implicit-TLS SMTPS and IMAPS ports. Plaintext AUTH is refused
* and the context is forwarded. This replaces the earlier
* either/or in which STARTTLS moved the listeners onto the
* secure port numbers and left no implicit-TLS socket.
*/
public function starttlsOpensAllPortsTestCase()
{
$config = $this->derive($this->ssl_context, 'spam', true,
false);
$this->assertEqual(25, $config['SMTP_PORT'],
"STARTTLS: plaintext SMTP stays on 25");
$this->assertEqual(587, $config['SUBMISSION_PORT'],
"STARTTLS: submission listener opens on 587");
$this->assertEqual(143, $config['IMAP_PORT'],
"STARTTLS: plaintext IMAP stays on 143");
$this->assertEqual(465, $config['SMTPS_PORT'],
"STARTTLS: wrapped SMTPS still opens on 465");
$this->assertEqual(993, $config['IMAPS_PORT'],
"STARTTLS: wrapped IMAPS still opens on 993");
$this->assertTrue(!$config['ALLOW_PLAINTEXT_AUTH'],
"spam posture refuses plaintext AUTH");
$this->assertTrue(isset($config['SERVER_CONTEXT']),
"SERVER_CONTEXT forwarded when a cert is present");
}
/**
* With a certificate and implicit TLS, the plaintext listeners
* keep the plaintext-port settings (for inbound mail and
* opportunistic STARTTLS) and the secure ports open additional
* implicit-TLS sockets.
*/
public function implicitTlsOpensWrappedSecurePortsTestCase()
{
$config = $this->derive($this->ssl_context, 'require',
false, true);
$this->assertEqual(25, $config['SMTP_PORT'],
"implicit: plaintext SMTP stays on 25");
$this->assertEqual(143, $config['IMAP_PORT'],
"implicit: plaintext IMAP stays on 143");
$this->assertEqual(465, $config['SMTPS_PORT'],
"implicit: wrapped SMTPS opens on 465");
$this->assertEqual(993, $config['IMAPS_PORT'],
"implicit: wrapped IMAPS opens on 993");
$this->assertTrue(!$config['ALLOW_PLAINTEXT_AUTH'],
"require posture refuses plaintext AUTH");
}
/**
* A loopback bind with no certificate still allows plaintext
* AUTH even under a secure posture, so a developer rig that
* cannot offer TLS is not locked out of its own mailbox.
*/
public function loopbackNoCertAllowsPlaintextAuthTestCase()
{
$config = $this->derive([], 'spam', false, false);
$this->assertTrue($config['ALLOW_PLAINTEXT_AUTH'],
"loopback without a cert keeps plaintext AUTH usable");
$this->assertEqual('127.0.0.1', $config['BIND'],
"loopback bind is 127.0.0.1");
}
/**
* An external secure posture with no certificate refuses
* plaintext AUTH: the bind is public, so the loopback
* exception does not apply and credentials must not cross the
* network in the clear.
*/
public function externalNoCertRefusesPlaintextAuthTestCase()
{
$config = $this->derive([], 'require', false, true);
$this->assertTrue(!$config['ALLOW_PLAINTEXT_AUTH'],
"external secure posture refuses plaintext AUTH");
$this->assertEqual('0.0.0.0', $config['BIND'],
"external bind is 0.0.0.0");
}
/**
* STARTTLS requested but no certificate present falls back to
* the plaintext-port settings with no secure sockets, because
* STARTTLS cannot be offered without a cert.
*/
public function starttlsWithoutCertFallsBackTestCase()
{
$config = $this->derive([], 'insecure', true, false);
$this->assertEqual(25, $config['SMTP_PORT'],
"no cert: SMTP stays on the plaintext port");
$this->assertEqual(143, $config['IMAP_PORT'],
"no cert: IMAP stays on the plaintext port");
$this->assertEqual(0, $config['SMTPS_PORT'],
"no cert: no secure SMTP socket even with STARTTLS on");
$this->assertEqual(0, $config['IMAPS_PORT'],
"no cert: no secure IMAP socket even with STARTTLS on");
}
}