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;
}
}