HEX
Server: Apache
System: Linux hz.vslconceptsdomains.com 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64
User: dkfounda (3233)
PHP: 8.1.34
Disabled: exec,passthru,shell_exec,system
Upload Files
File: //usr/local/mailchannels/lib/EximConfigurer.php
<?php

namespace MailChannels;

define("MC_DKIM_SETTINGS",'dkim_domain = \$\{perl\{get_dkim_domain\}\}\ndkim_selector = default\ndkim_canon = relaxed\ndkim_private_key = "\/var\/cpanel\/domain_keys\/private\/\$\{dkim_domain\}"');
define("MC_DKIM_SETTINGS_OLD",'dkim_domain = \$sender_address_domain\ndkim_selector = default\ndkim_canon = relaxed\ndkim_private_key = "\/var\/cpanel\/domain_keys\/private\/\$\{dkim_domain\}"');

class EximConfigurer {
    private $confFile;
    private $confLocalFile;
    private $routerConf;
    private $postMailCountConf;
    private $transportConf;
    private $authConf;
    private $mailmanHeadersConf;

    /**
     * EximConfigurer constructor.
     * @param $conf
     * @param $confLocal
     * @throws FileNotFoundException
     * @throws FileNotWritableException
     * @throws \Exception
     */
    public function __construct($confFile, $confLocalFile) {
        $config = App::getConfig();

        if (!file_exists($confFile)) {
            throw new FileNotFoundException("exim conf file '$confFile' doesn't exist");
        } else if (!file_exists($confLocalFile)) {
            if (!copy(App::appRoot() . $config::EXIM_CONF_LOCAL_TEMPLATE, $confLocalFile)) {
                throw new FileNotWritableException("couldn't create exim.conf.local file at $confLocalFile");
            }
        } else if (!is_writeable($confLocalFile)) {
            throw new FileNotWritableException("this process doesn't have write permissions for file '$confLocalFile'");
        }

        $this->confFile = $confFile;
        $this->confLocalFile = $confLocalFile;

        $this->routerConf = App::appRoot() . $config::EXIM_ROUTE_CONF;
        $this->postMailCountConf = App::appRoot() . $config::EXIM_POST_MAIL_COUNT_CONF;
        $this->transportConf = App::appRoot() . $config::EXIM_TRANSPORT_CONF;
        $this->authConf = App::appRoot() . $config::EXIM_AUTH_CONF;
        $this->mailmanHeadersConf = App::appRoot() . $config::EXIM_MAILMAN_HEADERS_CONF;
    }

    /**
     * @return bool
     * @throws EximCMDException
     */
    private function localHasMCAuth() {
        return $this->fileContains('#MAILCHANNELS_AUTHENTICATORS_START', $this->confLocalFile);
    }

    /**
     * Check if both the exim conf and exim local conf files have the #MAILCHANNELS_AUTHENTICATORS_START tag, signifying
     * the auth is correctly setup.
     *
     * NOTE: both the exim conf and exim conf local files are checked to protect against discrepancies where one file has
     * it and the other doesn't.
     *
     * @return bool
     * @throws EximCMDException
     */
    public function hasMCAuth() {
        return $this->fileContains('#MAILCHANNELS_AUTHENTICATORS_START', $this->confFile)
            && $this->fileContains('#MAILCHANNELS_AUTHENTICATORS_START', $this->confLocalFile);
    }

    /**
     * @throws EximCMDException
     */
    public function addMCAuth() {
        if ($this->localHasMCAuth()) {
            $this->removeMCAuth();
        }

        $this->runCMD('sed -i', ["/@AUTH@/r $this->authConf", $this->confLocalFile]);

        if (!$this->localHasMCAuth()) {
            App::getLogger()->error("failed to add AUTH section to '$this->confLocalFile'");
        }
    }

    /**
     * @throws EximCMDException
     */
    public function removeMCAuth() {
        if (!$this->localHasMCAuth()) {
            return;
        }

        $this->runCMD('sed -i', ['/#MAILCHANNELS_AUTHENTICATORS_START/,/#MAILCHANNELS_AUTHENTICATORS_STOP/d', $this->confLocalFile]);

        if ($this->localHasMCAuth()) {
            App::getLogger()->error("failed to remove MailChannels AUTH section in '$this->confLocalFile'");
        }
    }

    /**
     * @return bool
     * @throws EximCMDException
     */
    private function localHasMCTransport() {
        return $this->fileContains('#MAILCHANNELS_TRANSPORTSTART', $this->confLocalFile);
    }

    public function hasLineLength() {
        return $this->fileContains('message_linelength_limit = ', $this->transportConf);
    }

    /**
     * @return bool
     * @throws EximCMDException
     */
    public function hasMCTransport() {
        return $this->fileContains('#MAILCHANNELS_TRANSPORTSTART', $this->confFile) && $this->localHasMCTransport();
    }

    public function isDkimSet() {
        return $this->fileContainsInSection('dkim_domain = ', $this->transportConf, 'mailchannels_smtp:', 'mailchannels_forwarded_smtp:');
    }

    /**
     * @param int $linelength_limit
     * @throws EximCMDException
     * @throws FileNotFoundException
     */
    public function addMCTransport($linelength_limit) {
        if (!file_exists($this->transportConf)) {
            throw new FileNotFoundException("couldn't find the transport conf at $this->transportConf");
        }

        if ($this->localHasMCTransport()) {
            $this->removeMCTransport();
        }

        $message_linelength_limit = "message_linelength_limit = $linelength_limit";
        $this->runCMD('sed -i', ["s/#MESSAGE_LINELENGTH_LIMIT_UNSET/$message_linelength_limit/", $this->transportConf]);

        if (!$this->hasLineLength()) {
            App::getLogger()->error("failed to add message_linelength_limit to '$this->transportConf'");
        }

        $this->runCMD('sed -i', ["/@TRANSPORTSTART@/r $this->transportConf", $this->confLocalFile]);

        if (!$this->localHasMCTransport()) {
            App::getLogger()->error("failed to add TRANSPORTSTART section to '$this->confLocalFile'");
        }
    }

    public function toggleDkim($enableDkim){
        if (!file_exists($this->transportConf)) {
            throw new FileNotFoundException("couldn't find the transport conf at $this->transportConf");
        }
        
        if ($this->localHasMCTransport()) {
            $this->removeMCTransport();
        }

        if ($enableDkim){
            try {
                $eximVersion = $this->runCMD('exim -bV | grep version | awk \'{ print $3 }\'');
                if ($eximVersion[0] >= 4.94) {
                    $this->runCMD('perl -i -pe', ["BEGIN{undef $/;} s/#DKIM_UNSET/".MC_DKIM_SETTINGS."/g", $this->transportConf]);
                }else{
                    $this->runCMD('perl -i -pe', ["BEGIN{undef $/;} s/#DKIM_UNSET/".MC_DKIM_SETTINGS_OLD."/g", $this->transportConf]);
                }
            } catch (EximCMDException $e) {
                App::getLogger()->info("Failed to replace DKIM_UNSET with DKIM settings");
            }
        } else {
            try {
                $this->runCMD('perl -i -0777 -pe', ["s/".MC_DKIM_SETTINGS."/#DKIM_UNSET/g", $this->transportConf]);
            } catch (EximCMDException $e) {
                App::getLogger()->info("Failed to replace DKIM settings with DKIM_UNSET");
            }
        }
        $this->runCMD('sed -i', ["/@TRANSPORTSTART@/r $this->transportConf", $this->confLocalFile]);
    }

    /**
     * @throws EximCMDException
     */
    public function removeMCTransport() {
        if (!$this->localHasMCTransport()) {
            return;
        }

        if ($this->isDkimSet()){
            $eximVersion = $this->runCMD('exim -bV | grep version | awk \'{ print $3 }\'');
            if ($eximVersion[0] >= 4.94) {
                $this->runCMD('perl -i -0777 -pe', ["s/".MC_DKIM_SETTINGS."/#DKIM_UNSET/g", $this->transportConf]);
            }else{
                $this->runCMD('perl -i -0777 -pe', ["s/".MC_DKIM_SETTINGS_OLD."/#DKIM_UNSET/g", $this->transportConf]);
            }
            if ($this->isDkimSet()) {
                App::getLogger()->error("failed to remove DKIM settings section from '$this->transportConf'");
            }
        }

        $this->runCMD('sed -i', ["s/message_linelength_limit = .*/#MESSAGE_LINELENGTH_LIMIT_UNSET/", $this->transportConf]);
        if ($this->hasLineLength()) {
            App::getLogger()->error("failed to remove MESSAGE_LINELENGTH_LIMIT section in '$this->transportConf'");
        }

        $this->runCMD('sed -i', ['/#MAILCHANNELS_TRANSPORTSTART/,/#MAILCHANNELS_TRANSPORTSTOP/d', $this->confLocalFile]);

        if ($this->localHasMCTransport()) {
            App::getLogger()->error("failed to remove MailChannels TRANSPORT section in '$this->confLocalFile'");
        }
    }

    /**
     * @return bool
     * @throws EximCMDException
     */
    private function localHasMCRouter() {
        return $this->fileContains('#MAILCHANNELS_ROUTERSTART', $this->confLocalFile);
    }

    /**
     * @return bool
     * @throws EximCMDException
     */
    public function hasMCRouter() {
        return $this->fileContains('#MAILCHANNELS_ROUTERSTART', $this->confFile) && $this->localHasMCRouter();
    }

    /**
     * @throws EximCMDException
     * @throws FileNotFoundException
     */
    public function addMCRouter() {
        if (!file_exists($this->routerConf)) {
            throw new FileNotFoundException("couldn't find the transport conf at $this->routerConf");
        }

        if ($this->localHasMCRouter()) {
            $this->removeMCRouter();
        }

        $this->runCMD('sed -i', ["/@ROUTERSTART@/r $this->routerConf", $this->confLocalFile]);

        if (!$this->localHasMCRouter()) {
            App::getLogger()->error("failed to add MailChannels ROUTERSTART section in '$this->confLocalFile'");
        }
    }

    /**
     * @throws EximCMDException
     */
    public function removeMCRouter() {
        if (!$this->localHasMCRouter()) {
            return;
        }

        $this->runCMD('sed -i', ['/#MAILCHANNELS_ROUTERSTART/,/#MAILCHANNELS_ROUTERSTOP/d', $this->confLocalFile]);

        if ($this->localHasMCRouter()) {
            App::getLogger()->error("failed to remove MailChannels ROUTERSTART section in '$this->confLocalFile'");
        }
    }

    /**
     * @return bool
     * @throws EximCMDException
     */
    private function localHasMCPostMailCount() {
        return $this->fileContains('#MAILCHANNELS_POSTMAILCOUNTSTART', $this->confLocalFile);
    }

    /**
     * @return bool
     * @throws EximCMDException
     */
    public function hasMCPostMailCount() {
        return $this->fileContains('#MAILCHANNELS_POSTMAILCOUNTSTART', $this->confFile) && $this->localHasMCPostMailCount();
    }

    /**
     * @throws EximCMDException
     * @throws FileNotFoundException
     */
    public function addMCPostMailCount() {
        if (!file_exists($this->postMailCountConf)) {
            throw new FileNotFoundException("couldn't find the post mail count conf at $this->postMailCountConf");
        }

        if ($this->localHasMCPostMailCount()) {
            $this->removeMCPostMailCount();
        }

        $this->runCMD('sed -i', ["/@POSTMAILCOUNT@/r $this->postMailCountConf", $this->confLocalFile]);

        if (!$this->localHasMCPostMailCount()) {
            App::getLogger()->error("failed to add MailChannels POSTMAILCOUNT section in '$this->confLocalFile'");
        }
    }

    /**
     * @throws EximCMDException
     */
    public function removeMCPostMailCount() {
        if (!$this->localHasMCPostMailCount()) {
            return;
        }

        $this->runCMD('sed -i', ['/#MAILCHANNELS_POSTMAILCOUNTSTART/,/#MAILCHANNELS_POSTMAILCOUNTSTOP/d', $this->confLocalFile]);

        if ($this->localHasMCPostMailCount()) {
            App::getLogger()->error("failed to remove MailChannels POSTMAILCOUNT section in '$this->confLocalFile'");
        }
    }

        /**
     * @return bool
     * @throws EximCMDException
     */
    public function hasMailManHeaders() {
        return $this->fileContains('#MC_MAILMAN_HEADER_START', $this->confFile);
    }

    /**
     * @param int $linelength_limit
     * @throws EximCMDException
     * @throws FileNotFoundException
     */
    public function addMailManHeaders() {
        App::getLogger()->info("Adding mail man headers to $this->confFile");
        if (!file_exists($this->confFile)) {
            throw new FileNotFoundException("couldn't find the mailman headers conf at $this->confFile");
        }

        if ($this->hasMailManHeaders()) {
            App::getLogger()->info("mail man headers already present in $this->confFile");
            return;
        }

        $this->runCMD('sed -i', ["/mailman_virtual_transport:/r $this->mailmanHeadersConf", $this->confFile]);

        if (!$this->hasMailManHeaders()) {
            App::getLogger()->error("failed to add MailMan Headers section to '$this->confFile'");
        }
    }

    /**
     * @throws EximCMDException
     */
    public function removeMailManHeaders() {
        App::getLogger()->info("removing mailman headers from $this->confFile");

        $this->runCMD('sed -i', ['/#MC_MAILMAN_HEADER_START/,/#MC_MAILMAN_HEADER_STOP/d', $this->confFile]);

        if ($this->hasMailManHeaders()) {
            App::getLogger()->error("failed to remove MailMan Headers section in '$this->confFile'");
        }
    }

    /**
     * @throws EximCMDException
     */
    public function rebuildExim() {
        $config = App::getConfig();

        $this->runCMD($config::EXIM_BUILD_SCRIPT);
        if ($config::RESTART_EXIM_ON_CONF_CHANGE) {
            $this->runCMD('service ' . $config::EXIM_SERVICE_NAME . ' restart');
        }
    }

    /**
     * @throws EximCMDException
     */
    public function restartExim() {
        $config = App::getConfig();

        if ($config::RESTART_EXIM_ON_CONF_CHANGE) {
            App::getLogger()->info("restarting exim");
            $this->runCMD('service ' . $config::EXIM_SERVICE_NAME . ' restart');
        }
    }

    /**
     * @return bool
     * @throws EximCMDException
     */
    public function isConfigured() {
        return $this->hasMCAuth() && $this->hasMCPostMailCount() && $this->hasMCTransport();
    }

    /**
     * @param $txt
     * @param $file
     * @return bool
     */
    private function fileContains($txt, $file) {
        try {
            $this->runCMD('grep -q', [$txt, $file]);
        } catch (EximCMDException $e) {
            return false;
        }
        return true;
    }

    /**
     * @param $txt text to search for
     * @param $file file to search in
     * @param $after line to start searching after
     * @param $before line to stop searching before
     * @return bool
     */
    private function fileContainsInSection($txt, $file, $after, $before) {
        try {
            $this->runCMD("sed -n '/^$after/,/^$before/p' $file | grep -q '$txt'");
        } catch (EximCMDException $e) {
            return false;
        }

        return true;
    }

    /**
     * @param $cmd
     * @param null $args
     * @return mixed
     * @throws EximCMDException
     */
    private function runCMD($cmd, $args = null) {
        if ($args != null) {
            if (!is_array($args)) {
                $args = [$args];
            }

            array_walk($args, function(&$value, $key) {
                $value = escapeshellarg($value);
            });

            $cmd .= " " . implode(' ', $args);
        }

        exec($cmd, $output, $return);

        if ($return != 0) {
            throw new EximCMDException("command $cmd return with code $return");
        }

        return $output;
    }
}