mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-06 10:26:23 +02:00
Bundle SnappyMail list-unsubscribe-header plugin; enable on install/upgrade
- Add install/snappymail/plugins/list-unsubscribe-header (upstream GitHub plugin) - Add plogical/snappymail_plugin_utilities.py to copy into snappymail + legacy rainloop data roots and merge enabled_list - Run after SnappyMail CyberPanel installer in install.py and upgrade.py - InstallMailBoxFoldersPlugin now merges plugins instead of replacing enabled_list; also installs list-unsubscribe Roundcube is not shipped by CyberPanel core; SnappyMail is the bundled webmail.
This commit is contained in:
@@ -4839,6 +4839,13 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h
|
||||
command = f'/usr/local/lsws/lsphp80/bin/php /usr/local/CyberCP/snappymail_cyberpanel.php'
|
||||
preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR)
|
||||
|
||||
try:
|
||||
from plogical.snappymail_plugin_utilities import install_and_enable_list_unsubscribe_header_plugin
|
||||
if install_and_enable_list_unsubscribe_header_plugin():
|
||||
logging.InstallLog.writeToFile("SnappyMail list-unsubscribe-header plugin installed and enabled", 0)
|
||||
except BaseException as plug_msg:
|
||||
logging.InstallLog.writeToFile("Warning: list-unsubscribe SnappyMail plugin: " + str(plug_msg), 0)
|
||||
|
||||
|
||||
except BaseException as msg:
|
||||
logging.InstallLog.writeToFile('[ERROR] ' + str(msg) + " [downoad_and_install_snappymail]")
|
||||
|
||||
83
install/snappymail/plugins/list-unsubscribe-header/index.php
Normal file
83
install/snappymail/plugins/list-unsubscribe-header/index.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Bundled with CyberPanel (see plogical/snappymail_plugin_utilities.py).
|
||||
* Upstream: https://github.com/master3395/snappymail-list-unsubscribe-header
|
||||
*
|
||||
* Adds List-Unsubscribe and List-Unsubscribe-Post on outbound mail (RFC 2369 / RFC 8058).
|
||||
*/
|
||||
class ListUnsubscribeHeaderPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||
{
|
||||
const
|
||||
NAME = 'List-Unsubscribe headers',
|
||||
AUTHOR = 'Master3395',
|
||||
URL = 'https://newstargeted.com/',
|
||||
VERSION = '1.1.0',
|
||||
RELEASE = '2026-04-11',
|
||||
REQUIRED = '2.0.0',
|
||||
DESCRIPTION = 'Adds List-Unsubscribe and List-Unsubscribe-Post for bulk/mailing-list best practices.';
|
||||
|
||||
public function Init() : void
|
||||
{
|
||||
$this->addHook('filter.send-message', 'FilterSendMessage');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function configMapping() : array
|
||||
{
|
||||
return array(
|
||||
\RainLoop\Plugins\Property::NewInstance('unsubscribe_https_url')
|
||||
->SetLabel('HTTPS unsubscribe URL')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::URL)
|
||||
->SetDescription('Must be https:// — used in List-Unsubscribe (RFC 8058 one-click target).')
|
||||
->SetDefaultValue('https://newstargeted.com/list-unsubscribe-endpoint.php'),
|
||||
\RainLoop\Plugins\Property::NewInstance('mailto_unsubscribe')
|
||||
->SetLabel('Unsubscribe mailto address')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING)
|
||||
->SetDescription('Email only (e.g. postmaster@example.com) or full mailto:user@example.com')
|
||||
->SetDefaultValue('postmaster@newstargeted.com'),
|
||||
\RainLoop\Plugins\Property::NewInstance('one_click_post')
|
||||
->SetLabel('Send List-Unsubscribe-Post (one-click)')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
|
||||
->SetDescription('RFC 8058 — disable if your HTTPS URL does not accept POST.')
|
||||
->SetDefaultValue(true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \MailSo\Mime\Message $oMessage
|
||||
*/
|
||||
public function FilterSendMessage(&$oMessage) : void
|
||||
{
|
||||
if ($oMessage instanceof \MailSo\Mime\Message) {
|
||||
$sHttps = \trim((string) $this->Config()->Get('plugin', 'unsubscribe_https_url', 'https://newstargeted.com/list-unsubscribe-endpoint.php'));
|
||||
$sMailRaw = \trim((string) $this->Config()->Get('plugin', 'mailto_unsubscribe', 'postmaster@newstargeted.com'));
|
||||
$bOneClick = (bool) $this->Config()->Get('plugin', 'one_click_post', true);
|
||||
|
||||
if ($sHttps === '' || !\preg_match('#^https://#i', $sHttps)) {
|
||||
$sHttps = 'https://newstargeted.com/list-unsubscribe-endpoint.php';
|
||||
}
|
||||
if ($sMailRaw === '') {
|
||||
$sMailto = 'mailto:postmaster@newstargeted.com';
|
||||
} elseif (\stripos($sMailRaw, 'mailto:') === 0) {
|
||||
$sMailto = $sMailRaw;
|
||||
} else {
|
||||
$sMailto = 'mailto:' . $sMailRaw;
|
||||
}
|
||||
if (!\preg_match('#^mailto:[^<>\s]+$#i', $sMailto)) {
|
||||
$sMailto = 'mailto:postmaster@newstargeted.com';
|
||||
}
|
||||
|
||||
$sListUnsub = '<' . $sHttps . '>, <' . $sMailto . '>';
|
||||
$oMessage->SetCustomHeader(
|
||||
\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE,
|
||||
$sListUnsub
|
||||
);
|
||||
if ($bOneClick) {
|
||||
$oMessage->SetCustomHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,26 +211,13 @@ class mailUtilities:
|
||||
command = f'chown lscpd:lscpd /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/plugins/mailbox-detect/index.php'
|
||||
ProcessUtilities.executioner(command)
|
||||
|
||||
### Enable plugins and enable mailbox creation plugin
|
||||
### Enable plugins: merge mailbox-detect + bundled list-unsubscribe (preserve other plugins)
|
||||
|
||||
labsDataLines = open(labsPath, 'r').readlines()
|
||||
PluginsActivator = 0
|
||||
WriteToFile = open(labsPath, 'w')
|
||||
from plogical.snappymail_plugin_utilities import merge_plugin_into_application_ini
|
||||
from plogical.snappymail_plugin_utilities import install_and_enable_list_unsubscribe_header_plugin
|
||||
|
||||
for lines in labsDataLines:
|
||||
if lines.find('[plugins]') > -1:
|
||||
PluginsActivator = 1
|
||||
WriteToFile.write(lines)
|
||||
elif PluginsActivator and lines.find('enable = ') > -1:
|
||||
WriteToFile.write(f'enable = On\n')
|
||||
elif PluginsActivator and lines.find('enabled_list = ') > -1:
|
||||
WriteToFile.write(f'enabled_list = "mailbox-detect"\n')
|
||||
elif PluginsActivator == 1 and lines.find('[defaults]') > -1:
|
||||
PluginsActivator = 0
|
||||
WriteToFile.write(lines)
|
||||
else:
|
||||
WriteToFile.write(lines)
|
||||
WriteToFile.close()
|
||||
merge_plugin_into_application_ini(labsPath, 'mailbox-detect')
|
||||
install_and_enable_list_unsubscribe_header_plugin()
|
||||
|
||||
## enable auto create in the enabled plugin
|
||||
PluginsFilePath = '/usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/configs/plugin-mailbox-detect.json'
|
||||
|
||||
136
plogical/snappymail_plugin_utilities.py
Normal file
136
plogical/snappymail_plugin_utilities.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Bundle and enable the SnappyMail list-unsubscribe-header plugin (CyberPanel).
|
||||
|
||||
Data may live under snappymail/ or legacy rainloop/ until migration completes.
|
||||
Upstream plugin: https://github.com/master3395/snappymail-list-unsubscribe-header
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
PLUGIN_ID_LIST_UNSUBSCRIBE = 'list-unsubscribe-header'
|
||||
BUNDLE_REL = 'install/snappymail/plugins/list-unsubscribe-header/index.php'
|
||||
CYBERCP_ROOT = '/usr/local/CyberCP'
|
||||
|
||||
DATA_ROOT_CANDIDATES = (
|
||||
'/usr/local/lscp/cyberpanel/snappymail/data',
|
||||
'/usr/local/lscp/cyberpanel/rainloop/data',
|
||||
)
|
||||
|
||||
|
||||
def bundled_list_unsubscribe_src():
|
||||
return os.path.join(CYBERCP_ROOT, BUNDLE_REL)
|
||||
|
||||
|
||||
def _merge_enabled_list_line(line, plugin_id):
|
||||
"""Merge plugin_id into SnappyMail enabled_list = \"...\" line."""
|
||||
m = re.match(r'^(\s*enabled_list\s*=\s*")([^"]*)("\s*)\r?$', line)
|
||||
if not m:
|
||||
return line
|
||||
inner = m.group(2)
|
||||
parts = [p.strip() for p in inner.split(',') if p.strip()]
|
||||
if plugin_id not in parts:
|
||||
parts.append(plugin_id)
|
||||
inner_new = ','.join(parts)
|
||||
return m.group(1) + inner_new + m.group(3) + ('\n' if line.endswith('\n') else '')
|
||||
|
||||
|
||||
def _force_plugins_enable_on_line(line):
|
||||
"""Under [plugins], ensure enable = On."""
|
||||
if not re.match(r'^\s*enable\s*=', line):
|
||||
return line
|
||||
if re.search(r'(?i)=\s*On\b', line):
|
||||
return line
|
||||
return re.sub(r'(?i)=\s*\S+', '= On', line, count=1)
|
||||
|
||||
|
||||
def merge_plugin_into_application_ini(application_ini_path, plugin_id):
|
||||
"""
|
||||
Merge plugin_id into [plugins] enabled_list; set enable = On when that key is present.
|
||||
"""
|
||||
if not os.path.isfile(application_ini_path):
|
||||
return False
|
||||
with open(application_ini_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
out = []
|
||||
in_plugins = False
|
||||
for line in lines:
|
||||
stripped = line.lstrip()
|
||||
if stripped.startswith('[plugins]'):
|
||||
in_plugins = True
|
||||
out.append(line)
|
||||
continue
|
||||
if in_plugins and stripped.startswith('[') and not stripped.startswith('[plugins]'):
|
||||
in_plugins = False
|
||||
if in_plugins and re.match(r'^\s*enabled_list\s*=', line):
|
||||
out.append(_merge_enabled_list_line(line, plugin_id))
|
||||
continue
|
||||
if in_plugins and re.match(r'^\s*enable\s*=', line):
|
||||
out.append(_force_plugins_enable_on_line(line))
|
||||
continue
|
||||
out.append(line)
|
||||
with open(application_ini_path, 'w') as f:
|
||||
f.writelines(out)
|
||||
return True
|
||||
|
||||
|
||||
def _chown_lscpd(path):
|
||||
try:
|
||||
subprocess.check_call(['chown', 'lscpd:lscpd', path], stderr=subprocess.DEVNULL)
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
pass
|
||||
|
||||
|
||||
def _copy_bundled_plugin_to_data_root(data_root):
|
||||
"""Copy bundled index.php into .../plugins/list-unsubscribe-header/."""
|
||||
src = bundled_list_unsubscribe_src()
|
||||
if not os.path.isfile(src):
|
||||
return False
|
||||
dest_dir = os.path.join(
|
||||
data_root, '_data_', '_default_', 'plugins', PLUGIN_ID_LIST_UNSUBSCRIBE
|
||||
)
|
||||
try:
|
||||
os.makedirs(dest_dir, mode=0o700, exist_ok=True)
|
||||
except OSError:
|
||||
return False
|
||||
dest_file = os.path.join(dest_dir, 'index.php')
|
||||
try:
|
||||
shutil.copy2(src, dest_file)
|
||||
except (OSError, IOError):
|
||||
return False
|
||||
try:
|
||||
os.chmod(dest_file, 0o644)
|
||||
except OSError:
|
||||
pass
|
||||
_chown_lscpd(dest_file)
|
||||
_chown_lscpd(dest_dir)
|
||||
return True
|
||||
|
||||
|
||||
def install_and_enable_list_unsubscribe_header_plugin():
|
||||
"""
|
||||
Copy bundled plugin into each existing SnappyMail data root and merge into enabled_list.
|
||||
Idempotent. Returns 1 if at least one data root was updated, else 0.
|
||||
"""
|
||||
if not os.path.isfile(bundled_list_unsubscribe_src()):
|
||||
return 0
|
||||
ok = 0
|
||||
for root in DATA_ROOT_CANDIDATES:
|
||||
default_path = os.path.join(root, '_data_', '_default_')
|
||||
if not os.path.isdir(default_path):
|
||||
continue
|
||||
if not _copy_bundled_plugin_to_data_root(root):
|
||||
continue
|
||||
app_ini = os.path.join(default_path, 'configs', 'application.ini')
|
||||
if os.path.isfile(app_ini):
|
||||
merge_plugin_into_application_ini(app_ini, PLUGIN_ID_LIST_UNSUBSCRIBE)
|
||||
try:
|
||||
os.chmod(app_ini, 0o600)
|
||||
except OSError:
|
||||
pass
|
||||
_chown_lscpd(app_ini)
|
||||
ok = 1
|
||||
return ok
|
||||
@@ -1587,6 +1587,13 @@ $cfg['Servers'][$i]['port'] = '3306';
|
||||
command = f'/usr/local/lsws/lsphp83/bin/php /usr/local/CyberCP/snappymail_cyberpanel.php'
|
||||
Upgrade.executioner_silent(command, 'verify certificate', 0)
|
||||
|
||||
try:
|
||||
from plogical.snappymail_plugin_utilities import install_and_enable_list_unsubscribe_header_plugin
|
||||
if install_and_enable_list_unsubscribe_header_plugin():
|
||||
Upgrade.stdOut("SnappyMail list-unsubscribe-header plugin installed and enabled", 0)
|
||||
except BaseException as plug_msg:
|
||||
Upgrade.stdOut("Warning: list-unsubscribe SnappyMail plugin: " + str(plug_msg), 0)
|
||||
|
||||
# labsPath = '/usr/local/lscp/cyberpanel/rainloop/data/_data_/_default_/configs/application.ini'
|
||||
|
||||
# labsData = """[labs]
|
||||
|
||||
Reference in New Issue
Block a user