1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531:
<?php
declare(strict_types=1);
use Opcenter\Filesystem;
use Opcenter\Mail\Services\Majordomo;
use Opcenter\Mail\Services\Postfix;
class Majordomo_Module extends Module_Skeleton implements \Opcenter\Contracts\Hookable
{
const DEPENDENCY_MAP = [
'mail'
];
const MAJORDOMO_SETUID = 'nobody';
public function __construct()
{
parent::__construct();
if (!($this->permission_level & PRIVILEGE_SITE) || !$this->enabled()) {
$this->exportedFunctions = [
'*' => PRIVILEGE_NONE,
'enabled' => PRIVILEGE_SITE
];
return;
}
$this->exportedFunctions = array(
'*' => PRIVILEGE_SITE,
'list_mailing_lists_backend' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC,
'create_mailing_list_backend' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC,
'delete_mailing_list_backend' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC,
'get_mailing_list_users_backend' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC,
'get_domain_from_list_name_backend' => PRIVILEGE_SITE | PRIVILEGE_SERVER_EXEC
);
include_once(INCLUDE_PATH . '/lib/configuration_driver.php');
include(INCLUDE_PATH . '/lib/modules/majordomo/config_skeleton.php');
$this->majordomo_skeleton = $__majordomo_skeleton;
$this->majordomo_preamble = $__majordomo_preamble;
}
public function enabled(): bool {
if (!platform_is('7.5')) {
return true;
}
return $this->email_get_provider() === 'builtin' &&
$this->getConfig('mlist', 'enabled') &&
$this->getConfig('mlist', 'provider', 'majordomo') === 'majordomo';
}
public function get_mailing_list_users($list)
{
if (!IS_CLI) {
return $this->query('majordomo_get_mailing_list_users', $list);
}
if (!preg_match(Regex::MAILING_LIST_NAME, $list)) {
return error('Invalid list ' . $list);
}
if (!file_exists($this->domain_fs_path() . Majordomo::MAILING_LIST_HOME . '/lists/' . $list)) {
return error('Invalid list name ' . $list);
}
return file_get_contents($this->domain_fs_path() . Majordomo::MAILING_LIST_HOME . '/lists/' . $list);
}
public function create_mailing_list($list, $password, $email = null, $domain = null)
{
$max = $this->getServiceValue('mlist', 'max');
if ($max !== null) {
$count = \count($this->list_mailing_lists());
if ($count >= $max) {
return error("Mailing list limit `%d' reached", $count);
}
}
$list = strtolower(trim($list));
if (!$domain) {
$domain = $this->getServiceValue('siteinfo', 'domain');
}
if (!$email) {
$email = trim($this->getServiceValue('siteinfo', 'email'));
}
$email = strtolower($email);
if (!preg_match(Regex::MAILING_LIST_NAME, $list)) {
return error('Invalid list name');
}
if ($this->mailing_list_exists($list)) {
return error('Mailing list already exists');
}
if (!$password) {
return error('Invalid argument: missing password');
}
if (!in_array($domain, $this->email_list_virtual_transports())) {
return error('Domain not configured to handle mail');
}
if (!preg_match(Regex::EMAIL, $email)) {
return error('Invalid owner e-mail address');
}
$status = $this->query('majordomo_create_mailing_list_backend',
$list,
$password,
$email,
$domain
);
if ($status instanceof Exception) {
return $status;
}
$this->email_add_alias($list, $domain, $list . '+' . $domain);
$this->email_add_alias($list . '-approval', $domain, $email);
$this->email_add_alias($list . '-owner', $domain, $email);
$this->email_add_alias('owner-' . $list, $domain, $list . '-owner');
$this->email_add_alias($list . '-request', $domain, $list . '-request+' . $domain);
if (!$this->email_address_exists('majordomo-owner', $domain)) {
$this->email_add_alias('majordomo-owner', $domain, $email);
}
if (!$this->email_address_exists('majordomo', $domain)) {
$this->email_add_alias('majordomo', $domain, 'majordomo+' . $domain);
}
return true;
}
public function set_mailing_list_users($list, $members)
{
if (!IS_CLI) {
if (!preg_match(Regex::MAILING_LIST_NAME, $list)) {
return error("`%s': invalid list name", $list);
}
if (is_array($members)) {
$members = join("\n", $members);
}
return $this->query('majordomo_set_mailing_list_users', $list, $members);
}
if (!file_exists($this->domain_fs_path() . Majordomo::MAILING_LIST_HOME . '/lists/' . $list)) {
return error("list `%s' does not exist", $list);
}
$members = trim($members) . "\n";
$listHome = Majordomo::bindTo($this->domain_fs_path())->getListHome();
file_put_contents("${listHome}/lists/${list}", $members);
return Filesystem::chogp("${listHome}/lists/${list}", self::MAJORDOMO_SETUID, $this->group_id, 0644);
}
public function create_mailing_list_backend($list, $password, $email, $domain)
{
$prefix = $this->domain_fs_path();
if (file_exists($prefix . Majordomo::MAILING_LIST_HOME . '/lists/' . $list)) {
return error("list `%s' already exists", $list);
}
$ret = \Util_Process_Safe::exec('/usr/sbin/postalias -q majordomo+%s %s', $domain, Postfix::getAliasesPath(), [0, 1]);
if ($ret['return'] === 1) {
$aliasPath = Postfix::getAliasesPath();
file_put_contents(
$aliasPath,
trim(file_get_contents($aliasPath)) . "\n" .
'majordomo+' . $domain . ': "| env HOME=/usr/lib/majordomo MAJORDOMO_CF=' .
$prefix . '/etc/majordomo-' . $domain . '.cf /usr/lib/majordomo/majordomo"'
);
$this->email_remove_alias('majordomo', $domain);
$this->email_add_alias('majordomo', $domain, 'majordomo+' . $domain);
$proc = new Util_Account_Editor($this->getAuthContext()->getAccount(), $this->getAuthContext());
if (version_compare(platform_version(), '7.5', '>=')) {
$svc = 'mlist';
} else {
$svc = 'majordomo';
}
$proc->setConfig($svc, 'enabled', 1);
$proc->edit();
}
if (!file_exists($prefix . '/etc/majordomo.cf')) {
(new \Opcenter\Provisioning\ConfigurationWriter('majordomo.majordomo-cf', \Opcenter\SiteConfiguration::shallow($this->getAuthContext())))->
write($prefix . '/etc/majordomo.cf');
Filesystem::chogp($prefix . '/etc/majordomo.cf', 0, 0);
}
file_put_contents($prefix . '/etc/majordomo-' . $domain . '.cf',
preg_replace('/^\s*\$whereami.+$/m', '$whereami = "' . $domain . '";',
file_get_contents($prefix . '/etc/majordomo.cf')));
Filesystem::chogp($prefix . "/etc/majordomo-${domain}.cf", 0, 0);
if (!file_exists($listHome = Majordomo::bindTo($prefix)->getListHome())) {
Filesystem::mkdir($listHome, self::MAJORDOMO_SETUID, $this->group_id);
\Util_Process_Safe::exec('setfacl -d -m user:%d:7 %s', $this->user_id, $listHome);
}
foreach (array('archives', 'digest', 'lists', 'OLDLOGS', 'tmp') as $dir) {
if (!file_exists("${listHome}/${dir}")) {
mkdir("${listHome}/${dir}");
}
Filesystem::chogp("${listHome}/${dir}", self::MAJORDOMO_SETUID, $this->group_id, 02771) &&
\Util_Process_Safe::exec('setfacl -m user:postfix:7 -d -m user:%s:7 %s/%s', self::MAJORDOMO_SETUID, $listHome, $dir);
}
$aliasPath = Postfix::getAliasesPath();
file_put_contents(
$aliasPath,
trim(file_get_contents($aliasPath)) . "\n" .
$list . '+' . $domain . ': "| env HOME=/usr/lib/majordomo /usr/lib/majordomo/wrapper resend -C ' . $prefix . '/etc/majordomo-' . $domain . '.cf -l ' . $list . ' -h ' . $domain . ' ' . $list . '-outgoing+' . $domain . '"' . "\n" .
$list . '-outgoing+' . $domain . ': :include:' . $listHome . '/lists/' . $list . "\n" .
$list . '-request+' . $domain . ': "| env HOME=/usr/lib/majordomo MAJORDOMO_CF=' . $prefix . '/etc/majordomo-' . $domain . '.cf /usr/lib/majordomo/request-answer ' . $list . ' -h ' . $domain . '"' . "\n"
);
Util_Process::exec('/usr/sbin/postalias -w %s', $aliasPath);
foreach (array($list, $list . '.config', $list . '.intro', $list . '.info') as $file) {
Filesystem::touch("${listHome}/lists/${file}", self::MAJORDOMO_SETUID, $this->group_id);
}
file_put_contents("${listHome}/lists/${list}", $email . "\n");
file_put_contents("${listHome}/lists/${list}.config",
$this->change_configuration_options(array(
'admin_passwd' => $password,
'resend_host' => $domain,
'restrict_post' => $list,
'sender' => 'owner-' . $list
)));
chmod($listHome, 0755);
chmod("${listHome}/lists", 02751);
chmod("${listHome}/lists/${list}", 0644);
Util_Process_Safe::exec('setfacl -d -m user:%s:7 -m user:postfix:7 %s/*',
self::MAJORDOMO_SETUID,
$listHome
);
Util_Process_Safe::exec('setfacl -m user:%d:7 %s/lists/%s*',
$this->user_id,
$listHome,
$list
);
Util_Process_Safe::exec('setfacl -R -m user:%s:7 -m user:postfix:7 %s',
self::MAJORDOMO_SETUID,
$listHome
);
return true;
}
public function change_configuration_options(array $options)
{
$configuration = $this->majordomo_skeleton;
foreach ($options as $option => $value) {
if (!isset($configuration[$option])) {
continue;
}
if ($configuration[$option]['type'] == enum) {
$configuration[$option]['value'] = in_array($value, $configuration[$option]['values']) ?
$value :
(isset($configuration[$option]['default']) ? $configuration[$option]['default'] : '');
} else {
$configuration[$option]['value'] = $value;
}
}
return $this->generate_configuration($configuration);
}
public function generate_configuration(array $config)
{
$configuration = $this->majordomo_preamble;
foreach ($config as $opt_name => $opt_params) {
$configuration .= wordwrap('# ' . $opt_params['help'], 72, "\n# ") . "\n" . $opt_name;
if ($opt_params['type'] == text) {
$configuration .= " << END \n" . (isset($opt_params['value']) ? $opt_params['value'] : (isset($opt_params['default']) ? $opt_params['default'] : '')) . "\n" . 'END';
} else {
$configuration .= ' = ';
if ($opt_params['type'] == bool) {
$configuration .= (isset($opt_params['value']) ? ($opt_params['value'] ? 'yes' : 'no') : ((isset($opt_params['default']) && $opt_params['default']) ? 'yes' : 'no'));
} else {
$configuration .= (isset($opt_params['value']) ? $opt_params['value'] : (isset($opt_params['default']) ? $opt_params['default'] : ''));
}
}
$configuration .= "\n\n";
}
return $configuration;
}
public function load_configuration_options($list)
{
return $this->_parse_configuration($this->file_get_file_contents(Majordomo::MAILING_LIST_HOME . '/lists/' . $list . '.config'));
}
private function _parse_configuration($text)
{
if (!preg_match_all(Regex::MAJORDOMO_CONFIG_ENTRY, $text, $matches, PREG_SET_ORDER)) {
return false;
}
$base = $this->majordomo_skeleton;
foreach ($matches as $match => $value) {
if (isset($base[$value[1]])) {
$base[$value[1]]['value'] = trim(($base[$value[1]]['type'] == text) ? str_replace('END', '',
$value[2]) : $value[2]);
}
}
return array_merge($base, array_intersect_key($base, $base));
}
public function save_configuration_options($list, $data)
{
return $this->file_put_file_contents(Majordomo::MAILING_LIST_HOME . "/lists/${list}.config", $data, true) &&
$this->file_chmod(Majordomo::MAILING_LIST_HOME . "/lists/${list}.config", 644);
}
public function _delete()
{
if (!$this->enabled()) {
return true;
}
foreach ($this->list_mailing_lists() as $list) {
$this->delete_mailing_list($list);
}
}
public function list_mailing_lists()
{
if (!IS_CLI) {
return $this->query('majordomo_list_mailing_lists');
}
$entries = array();
$listHome = Majordomo::bindTo($this->domain_fs_path())->getListHome();
if (!file_exists("${listHome}/lists")) {
return $entries;
}
$dh = dir("${listHome}/lists");
while (false !== ($entry = $dh->read())) {
if (false !== strpos($entry, '.')) {
continue;
}
$entries[] = $entry;
}
$dh->close();
return $entries;
}
public function delete_mailing_list($list)
{
if (!IS_CLI) {
return $this->query('majordomo_delete_mailing_list', $list);
}
$list = trim(strtolower($list));
if (!preg_match(Regex::MAILING_LIST_NAME, $list)) {
return error('Invalid list name');
} else {
if (!$this->mailing_list_exists($list)) {
return error("mailing list `%s' does not exist", $list);
}
}
$domain = $this->get_domain_from_list_name($list);
$listHome = Majordomo::bindTo($this->domain_fs_path())->getListHome() . '/lists';
foreach (array($list, $list . '.config', $list . '.intro', $list . '.info') as $file) {
$path = "${listHome}/${file}";
if (file_exists($path)) {
unlink($path);
}
}
$moreLists = $this->mailing_lists_exist();
if (!$moreLists) {
$this->email_remove_alias('majordomo', $domain);
}
$lines = [];
foreach (explode("\n", file_get_contents(Postfix::getAliasesPath())) as $line) {
if (preg_match('!' . $list . '(?:-outgoing|-request)?\+' . $domain . ':!', $line)) {
continue;
} else {
if (!$moreLists && preg_match('!majordomo\+' . $domain . ':!', $line)) {
continue;
}
}
$lines[] = $line;
}
$this->email_remove_alias($list, $domain);
$this->email_remove_alias($list . '-approval', $domain);
$this->email_remove_alias($list . '-owner', $domain);
$this->email_remove_alias('owner-' . $list, $domain);
$this->email_remove_alias($list . '-request', $domain);
file_put_contents(Postfix::getAliasesPath(), join("\n", $lines));
Util_Process::exec('postalias -r %s', Postfix::getAliasesPath());
return true;
}
public function mailing_list_exists($list)
{
if (!IS_CLI) {
return $this->query('majordomo_mailing_list_exists', $list);
}
return file_exists(Majordomo::bindTo($this->domain_fs_path())->getListHome() . "/lists/${list}.config");
}
public function get_domain_from_list_name($list)
{
if (!IS_CLI) {
return $this->query('majordomo_get_domain_from_list_name', $list);
}
if (!preg_match(Regex::MAILING_LIST_NAME, $list)) {
return error('Invalid list ' . $list);
}
$listHome = Majordomo::bindTo($this->domain_fs_path())->getListHome();
if (!file_exists("${listHome}/lists/${list}")) {
return error($list . ' does not exist');
}
$file = "${listHome}/lists/${list}.config";
if (!file_exists($file)) {
return null;
} else if (preg_match('/^\s*resend_host\s*=[ \t]*(\S+)/m', file_get_contents($file), $domain)) {
$domain = $domain[1];
} else {
$domain = $this->getServiceValue('siteinfo', 'domain');
}
return $domain;
}
public function mailing_lists_exist()
{
if (!IS_CLI) {
return $this->query('majordomo_mailing_lists_exist');
}
$listHome = Majordomo::bindTo($this->domain_fs_path())->getListHome();
if (!file_exists("${listHome}/lists")) {
return false;
}
$glob = glob("${listHome}/lists");
return sizeof($glob) > 1;
}
public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool
{
return true;
}
public function _create()
{
}
public function _edit()
{
}
public function _create_user(string $user)
{
}
public function _delete_user(string $user)
{
}
public function _edit_user(string $userold, string $usernew, array $oldpwd)
{
}
}