<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    health_check
 */

/*EXTRA FUNCTIONS: dns_get_record|imap_.+|error_log*/

/**
 * Hook class.
 */
class Hook_health_check_email extends Hook_Health_Check
{
    protected $category_label = 'E-mail';

    /**
     * Standard hook run function to run this category of health checks.
     *
     * @param  ?array $sections_to_run Which check sections to run (null: all)
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     * @param  boolean $show_unusable_categories Whether to include categories that might not be accessible for some reason
     * @return array A pair: category label, list of results
     */
    public function run(?array $sections_to_run, int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null, bool $show_unusable_categories = false) : array
    {
        $this->process_checks_section('testEmailQueue', 'E-mail queue', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testEmailServerConfiguration', 'E-mail configuration', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testEmailPHPConfiguration', 'E-mail configuration in PHP', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testDKIMConfiguration', 'DKIM configuration', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testDMARCConfiguration', 'DMARC configuration', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testSPF', 'SPF', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testSMTPLogin', 'SMTP login', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testIMAPLogin', 'IMAP login', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testEmailOperation', 'E-mail operation (slow)', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testSMTPBlocklisting', 'SMTP blocklisting', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testEmailTemplates', 'E-mail templates', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testSpamStatus', 'Spam status', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testListUnsubscribe', 'List-Unsubscribe header', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);

        return [$this->category_label, $this->results];
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testEmailQueue(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        if (get_option('mail_queue_debug') == '1') {
            $this->log('Skipped; mail queue debugging is enabled.');
            return;
        }

        $this->assertTrue((get_option('mail_queue') == '1') || (get_option('smtp_sockets_use') == '0') || (strpos(get_option('smtp_sockets_host'), '.') === false) || (is_local_machine(get_option('smtp_sockets_host'))), 'If you using a remote SMTP server you should enable the Mail Queue');

        $sql = 'SELECT COUNT(*) FROM ' . get_table_prefix() . 'logged_mail_messages WHERE m_queued=1 AND m_date_and_time<' . strval(time() - 60 * 60 * 1);
        $count = $GLOBALS['SITE_DB']->query_value_if_there($sql);
        $this->assertTrue($count == 0, 'There are e-mails in the queue which have not been sent for over an hour');
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testEmailPHPConfiguration(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        $this->assertTrue(ini_get('mail.add_x_header') !== '1', 'The PHP mail.add_x_header is set which may get your e-mails flagged as spam');
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testDKIMConfiguration(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        $dkim_configured = (get_option('dkim_private_key') != '');
        $this->assertTrue($dkim_configured, 'You have not configured DKIM in the software. This is fine if your server signs outgoing mail, but otherwise you must configure this or your e-mails will get flagged as spam.');
        $domains = $this->get_mail_domains(true);
        foreach ($domains as $domain => $email) {
            if (php_function_allowed('dns_get_record')) {
                $found_record = false;
                $records = @dns_get_record($domain, DNS_TXT);
                foreach ($records as $record) {
                    if (strpos($record['host'], 'v=DKIM') !== false) {
                        $found_record = true;
                    }
                }
                $this->assertTrue($found_record, 'Could not find a DKIM DNS record; your e-mails may get flagged as spam. Check your DNS records and fix accordingly. You must have this record set in your DNS even if you set up DKIM in the software.');
            } else {
                $this->stateCheckSkipped('PHP [tt]dns_get_record[/tt] function not available');
            }
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testDMARCConfiguration(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        $domains = $this->get_mail_domains(true);
        foreach ($domains as $domain => $email) {
            if (php_function_allowed('dns_get_record')) {
                $found_record = false;
                $records = @dns_get_record($domain, DNS_TXT);
                foreach ($records as $record) {
                    if (strpos($record['host'], 'v=DMARC') !== false) {
                        $found_record = true;
                    }
                }
                $this->assertTrue($found_record, 'Could not find a DMARC DNS record');
            } else {
                $this->stateCheckSkipped('PHP [tt]dns_get_record[/tt] function not available');
            }
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testEmailServerConfiguration(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        if ((php_function_allowed('getmxrr')) && (php_function_allowed('checkdnsrr'))) {
            $domains = $this->get_mail_domains(true);

            foreach ($domains as $domain => $email) {
                $mail_hosts = [];
                $result = @getmxrr($domain, $mail_hosts);
                $this->assertTrue($result, 'No valid MX records were found for the [tt]' . $email . '[/tt] e-mail address. You might not receive incoming e-mail. Please check your DNS records and fix accordingly.');

                foreach (array_unique($mail_hosts) as $host) {
                    $has_dns = @checkdnsrr($host, 'A');
                    $this->assertTrue($has_dns, 'Every MX record must have a matching A record, but ' . $host . ' (from e-mail address [tt]' . $email . '[/tt]) does not have one. You might not receive incoming e-mail. Please fix accordingly in your DNS records.');

                    if (!$has_dns) {
                        continue;
                    }

                    $reverse_dns_host = null;
                    $ip_address = gethostbyname($host);
                    if ($ip_address != $host) {
                        $_reverse_dns_host = cms_gethostbyaddr($ip_address);
                        if ($_reverse_dns_host != $ip_address) {
                            $reverse_dns_host = $_reverse_dns_host;
                        }
                    }
                    $this->assertTrue($reverse_dns_host !== null, 'Missing reverse-DNS for [tt]' . $email . '[/tt] address (host=[tt]' . $host . '[/tt]). You might not receive incoming e-mail. Please check your DNS records and ensure the MX record and its matching A record point to the correct address, and that it can be resolved.');

                    if (php_function_allowed('fsockopen')) {
                        // See if SMTP running
                        $errno = 0;
                        $errstr = '';
                        $socket = @fsockopen($host, 25, $errno, $errstr, 4.0);
                        $can_connect = ($socket !== false);
                        $this->assertTrue($can_connect, 'Could not connect to SMTP server (port 25) for [tt]' . $email . '[/tt] address (host=[tt]' . $host . '[/tt]); possibly server network is firewalled on this port. You might not be able to send outgoing e-mails.');
                        if ($can_connect) {
                            fread($socket, 1024);
                            fwrite($socket, 'HELO ' . $domain . "\n");
                            $data = fread($socket, 1024);
                            fclose($socket);

                            $matches = [];
                            $has_helo = preg_match('#^250 ([^\s]*)#', $data, $matches) != 0;
                            $this->assertTrue($has_helo, 'Could not get HELO response from SMTP server for [tt]' . $email . '[/tt] address (host=[tt]' . $host . '[/tt]). You might not be able to send outgoing e-mails.');
                            if ($has_helo) {
                                $reported_host = $matches[1];

                                if (($reverse_dns_host !== null) && ($reported_host != $reverse_dns_host)) {
                                    $this->stateCheckManual('HELO response from SMTP server (' . $reported_host . ') not matching reverse DNS (' . $reverse_dns_host . ') for ' . $email . ' address; not ideal but we won\'t flag as unhealthy due to how common it is (and that your SMTP server should typically still work)');
                                }
                            }
                        }
                    } else {
                        $this->stateCheckSkipped('PHP [tt]fsockopen[/tt] function not available');
                    }
                }

                // What if mailbox missing? Or generally e-mail not received
                if ($manual_checks) {
                    require_code('mail');
                    dispatch_mail('Test', 'Test e-mail from Health Check', [$email]);
                    $this->stateCheckManual('An e-mail was sent to ' . $email . ', confirm it was received');
                }
            }
        } else {
            $this->stateCheckSkipped('PHP [tt]getmxrr[/tt]/[tt]checkdnsrr[/tt] function(s) not available');
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testSPF(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        if (!$manual_checks) {
            $this->stateCheckSkipped('Will not check automatically because we do not know if SMTP relaying would be in place');
            return;
        }

        if (get_option('smtp_sockets_use') == '0') {
            if (php_function_allowed('dns_get_record')) {
                $domains = get_server_names(false);
                $mail_domains = $this->get_mail_domains(true);

                foreach ($domains as $self_domain) {
                    $self_ip = cms_gethostbyname($self_domain);

                    foreach ($mail_domains as $domain => $email) {
                        $passed = $this->do_spf_check($domain, $self_domain, $self_ip);

                        $this->assertTrue($passed !== null, 'SPF record is not set for [tt]' . $domain . '[/tt], setting it will significantly reduce the chance of fraud and spam blockage');

                        if ($passed !== null) {
                            $this->assertTrue($passed, 'SPF record for [tt]' . $domain . '[/tt] does not allow the server to send (either blocked or neutral). Maybe your local SMTP is relaying via another server, but check that.');
                        }
                    }
                }
            } else {
                $this->stateCheckSkipped('PHP [tt]dns_get_record[/tt] function not available');
            }
        }
    }

    /**
     * Find whether SPF on a domain is working.
     *
     * @param  string $domain The domain name
     * @param  string $self_domain Domain of sending server to match against
     * @param  string $self_ip IP address of sending server to match against
     * @return ?boolean Whether the check matches (null: N/A)
     */
    protected function do_spf_check(string $domain, string $self_domain, string $self_ip) : ?bool
    {
        $records = @dns_get_record($domain, DNS_TXT);
        foreach ($records as $record) {
            if (!isset($record['txt'])) {
                continue;
            }
            $_record = $record['txt'];

            $matches = [];
            if (preg_match('#^v=spf1 (.*)#', $_record, $matches) != 0) {
                $passed = false;
                $blocked = false;
                $passed_wildcard = false;

                $parts = explode(' ', $matches[1]);
                foreach ($parts as $part) {
                    // Normalise to something more manageable
                    $prefix = substr($part, 0, 1);
                    if (in_array($prefix, ['+', '-', '#', '?'])) {
                        $part = substr($part, 1);
                    } else {
                        $prefix = '+';
                    }
                    if ($prefix == '~') {
                        $prefix = '-';
                    }

                    if ($this->spf_term_matches($part, $self_domain, $self_ip)) {
                        switch ($prefix) {
                            case '+':
                                $passed = true;
                                break;

                            case '-':
                                $blocked = true;
                                break;
                        }
                    }

                    if ($part == 'all') {
                        switch ($prefix) {
                            case '+':
                                $passed_wildcard = true;
                                break;

                            case '-':
                                // We ignore this, we consider neutrality and blocking all equivalently
                                break;
                        }
                    }
                }

                $this->assertTrue(!$passed_wildcard, 'SPF record for [tt]' . $domain . '[/tt] is wildcarded, so offers no real value');

                return ($passed || $passed_wildcard) && !$blocked;
            }
        }

        return null;
    }

    /**
     * Find whether an SPF term matches.
     *
     * @param  string $part The SPF term
     * @param  string $self_domain Domain of sending server to match against
     * @param  string $self_ip IP address of sending server to match against
     * @return boolean Whether the check matches
     */
    protected function spf_term_matches(string $part, string $self_domain, string $self_ip) : bool
    {
        if (substr($part, 0, 4) == 'ip4:') {
            $_part = substr($part, 4);

            if (strpos($_part, '/') === false) {
                return ($self_ip == $_part);
            } else {
                return ip_cidr_check($self_ip, $_part);
            }
        }

        if (($part == 'a:' . $self_domain) || ($part == 'a')) {
            return true;
        }

        if (($part == 'mx:' . $self_domain) || ($part == 'mx')) {
            return true;
        }
        if (($part == 'ptr:' . $self_domain) || ($part == 'ptr')) {
            return true;
        }

        if (substr($part, 0, 8) == 'include:') {
            $include = substr($part, 8);
            $ret = $this->do_spf_check($include, $self_domain, $self_ip);

            $this->assertTrue($ret !== null, 'SPF include [tt]' . $include . '[/tt] is broken');

            if ($ret === null) {
                $ret = false;
            }
            return $ret;
        }

        if (substr($part, 0, 9) == 'redirect:') {
            $redirect = substr($part, 9);
            $ret = $this->do_spf_check($redirect, $self_domain, $self_ip);

            $this->assertTrue($ret !== null, 'SPF redirect [tt]' . $redirect . '[/tt] is broken');

            if ($ret === null) {
                $ret = false;
            }
            return $ret;
        }

        return false;
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testSMTPLogin(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        if ((get_option('smtp_sockets_use') == '0') || (!php_function_allowed('fsockopen'))) {
            $this->stateCheckSkipped('SMTP mailer not enabled');

            return;
        }

        $host = get_option('smtp_sockets_host');
        $port = intval(get_option('smtp_sockets_port'));
        $username = get_option('smtp_sockets_username');
        $password = get_option('smtp_sockets_password');

        require_lang('mail');

        $error = null;

        if (php_function_allowed('fsockopen')) {
            $errno = 0;
            $errstr = '';

            $socket = @fsockopen($host, $port, $errno, $errstr, 30.0);
            if ($socket !== false) {
                $rcv = fread($socket, 1024);

                $domain = get_base_url_hostname();

                // Log in if necessary
                if ($username != '') {
                    fwrite($socket, 'EHLO ' . $domain . "\r\n");
                    $rcv = fread($socket, 1024);

                    fwrite($socket, "AUTH LOGIN\r\n");
                    $rcv = fread($socket, 1024);
                    if (substr($rcv, 0, 3) == '334') {
                        fwrite($socket, base64_encode($username) . "\r\n");
                        $rcv = fread($socket, 1024);
                        if ((substr($rcv, 0, 3) == '235') || (substr($rcv, 0, 3) == '334')) {
                            fwrite($socket, base64_encode($password) . "\r\n");
                            $rcv = fread($socket, 1024);
                            if (substr($rcv, 0, 3) == '235') {
                            } else {
                                $error = do_lang('MAIL_ERROR_CONNECT_PASSWORD') . ' (' . str_replace($password, '*', $rcv) . ')';
                            }
                        } else {
                            $error = do_lang('MAIL_ERROR_CONNECT_USERNAME') . ' (' . $rcv . ')';
                        }
                    } else {
                        $error = do_lang('MAIL_ERROR_CONNECT_AUTH') . ' (' . $rcv . ')';
                    }
                } else {
                    fwrite($socket, 'HELO ' . $domain . "\r\n");
                    $rcv = fread($socket, 1024);
                }
            }
        } else {
            $this->stateCheckSkipped('fsockopen is not available');

            return;
        }

        $this->assertTrue($error === null, 'SMTP login failed with ' . (($error === null) ? 'N/A' : $error));

        if ($error !== null) {
            if (running_script('cron_bridge')) {
                if (php_function_allowed('error_log')) {
                    @error_log(brand_name() . ' mailer: ERROR ' . $error); // We log this, as Health Check is not going to be able to send an e-mail
                }
            }
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testIMAPLogin(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        if (!php_function_allowed('imap_open')) {
            $this->stateCheckSkipped('IMAP functionality not available');
            return;
        }

        $type = get_option('mail_server_type');
        $host = get_option('mail_server_host');
        $port = intval(get_option('mail_server_port'));
        $folder = get_option('mail_folder');

        $username = get_option('mail_username');
        $password = get_option('mail_password');

        if (($host == '') || ($username == '') || ($password == '')) {
            $this->stateCheckSkipped('IMAP not fully configured');
            return;
        }

        require_code('mail');
        require_code('mail2');
        $server_spec = _imap_server_spec($host, $port, $type);

        $mbox = @imap_open($server_spec . $folder, $username, $password);
        $this->assertTrue($mbox !== false, 'Cannot connect to IMAP server');
        if ($mbox !== false) {
            imap_close($mbox);
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testEmailOperation(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        if (php_function_allowed('imap_open')) {
            require_code('mail');
            require_code('mail2');

            if ($use_test_data_for_pass === null) {
                $address = get_option('website_email');

                $type = get_option('mail_server_type');
                $host = get_option('mail_server_host');
                $port = intval(get_option('mail_server_port'));
                $folder = get_option('mail_folder');

                $username = get_option('mail_username');
                $password = get_option('mail_password');
            } else {
                $address = 'test@composr.app';

                $type = 'imaps';
                $host = 'ns100030.ip-147-135-1.us';
                $port = 993;
                $folder = 'INBOX';

                $username = 'test@composr.app';
                $password = '!qz09BwdsNJb1f9zn';
            }

            if (($address == '') || ($host == '') || ($username == '')) {
                $this->stateCheckSkipped('Test e-mail account not fully configured');
                return;
            }

            $uniq = uniqid('', true);
            $subject = brand_name() . ' Self-Test (' . $uniq . ')';
            dispatch_mail($subject, 'Test', [$address], null, '', '', ['bypass_queue' => true]);

            $wait_time = intval(get_option('hc_mail_wait_time'));

            $good = false;
            $time_started = time();
            $server_spec = _imap_server_spec($host, $port, $type);
            $i = 0;
            do {
                if (php_function_allowed('usleep')) {
                    usleep(3000000);
                }

                $mbox = @imap_open($server_spec . $folder, $username, $password, CL_EXPUNGE);
                $ok = ($mbox !== false);
                if ($i == 0) {
                    $this->assertTrue($ok, 'Could not connect to IMAP server, [tt]' . $host . '[/tt]');
                    if (!$ok) {
                        return;
                    }
                }
                if ($ok) {
                    $list = imap_search($mbox, 'FROM "' . get_site_name() . '"');
                    if ($list === false) {
                        $list = [];
                    }
                    foreach ($list as $l) {
                        $header = imap_headerinfo($mbox, $l);

                        $_subject = $header->subject;

                        if (strpos($_subject, $uniq) !== false) {
                            $good = true;
                        }

                        if (strpos($_subject, brand_name() . ' Self-Test') !== false) {
                            imap_delete($mbox, $l); // Auto-clean-up
                        }
                    }

                    imap_close($mbox);
                }

                $time_taken = time() - $time_started;

                $i++;
            } while ((!$good) && ($time_taken < $wait_time) && ($ok));

            $this->assertTrue($good, 'Did not receive test e-mail within ' . display_time_period($wait_time));
        } else {
            $this->stateCheckSkipped('PHP [tt]imap_open[/tt] function not available');
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testSMTPBlocklisting(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        require_code('antispam');

        if (php_function_allowed('getmxrr')) {
            $domains = $this->get_mail_domains(true);

            foreach ($domains as $domain => $email) {
                $mail_hosts = [];
                $result = @getmxrr($domain, $mail_hosts);
                $ok = ($result !== false);
                if (!$ok) {
                    $this->stateCheckSkipped('Could not look up MX of [tt]' . $domain . '[/tt]');
                    continue;
                }

                foreach ($mail_hosts as $host) {
                    $ip = cms_gethostbyname($host);
                    $rbls = [
                        '*.dnsbl.sorbs.net',
                        '*.bl.spamcop.net',
                    ];
                    foreach ($rbls as $rbl) {
                        $response = rbl_resolve($ip, $rbl, true);
                        $blocked = ($response === ['127', '0', '0', '2']);
                        $this->assertTrue(!$blocked, 'The [tt]' . $host . '[/tt] SMTP server is blocked by [tt]' . $rbl . '[/tt]');
                    }
                }
            }
        } else {
            $this->stateCheckSkipped('PHP [tt]getmxrr[/tt] function not available');
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testEmailTemplates(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        $tpl = do_template('MAIL', [
            '_GUID' => '9c6fdd592f5ba06ad079bc69860981d0',
            'CSS' => '',
            'LOGOURL' => '',
            'LANG' => get_site_default_lang(),
            'TITLE' => '',
            'CONTENT' => '',
        ], get_site_default_lang(), false, 'MAIL', '.tpl', 'templates', $GLOBALS['FORUM_DRIVER']->get_theme(''));
        $html_version = $tpl->evaluate();
        $html_version_stripped = html_entity_decode(strip_tags($html_version), ENT_QUOTES);
        $html_version_stripped = trim(cms_preg_replace_safe('#[\s-]+#', ' ', $html_version_stripped));

        $tpl = do_template('MAIL', [
            '_GUID' => '5767b9123a0871fb11b0d9bb60e5b98e',
            'CSS' => '',
            'LOGOURL' => '',
            'LANG' => get_site_default_lang(),
            'TITLE' => '',
            'CONTENT' => '',
        ], get_site_default_lang(), false, 'MAIL', '.txt', 'text', $GLOBALS['FORUM_DRIVER']->get_theme(''));
        $text_version = $tpl->evaluate();
        $text_version_stripped = trim(cms_preg_replace_safe('#[\s-]+#', ' ', $text_version));

        $this->assertTrue($html_version_stripped == $text_version_stripped, 'The HTML and text mail templates are not consistent, which can score you spam points');
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testSpamStatus(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        $this->stateCheckManual('Run a [url="spam check"]https://glockapps.com/spam-testing/[/url] on a test e-mail');
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testListUnsubscribe(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        $this->assertTrue(!empty(get_option('list_unsubscribe_target')), 'You do not have a List-Unsubscribe target configuration option set, which may increase your spam score and violate some local laws on user privacy.');
    }
}
