Merge pull request #1628 from master3395/v2.5.5-dev

Implement Catch-All Email Configuration Management

https://github.com/usmannasir/cyberpanel/issues/1626
https://github.com/usmannasir/cyberpanel/issues/1627
This commit is contained in:
Master3395 2025-12-31 22:19:45 +01:00 committed by GitHub
commit 47dc442ace
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 3172 additions and 24 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1088,24 +1088,25 @@ class preFlightsChecks:
self.stdOut(f"Detected platform: {platform}", 1)
# Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 - v2.0.5 Static Build)
# Module Build Date: December 28, 2025 - v2.2.0 Brute Force with Progressive Throttle
BINARY_CONFIGS = {
'rhel8': {
'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel8-static',
'sha256': '6ce688a237615102cc1603ee1999b3cede0ff3482d31e1f65705e92396d34b3a',
'module_url': None, # RHEL 8 doesn't have module (use RHEL 9 if needed)
'module_sha256': None
'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols.so',
'module_sha256': '7c33d89c7fbcd3ed7b0422fee3f49b5e041713c2c2b7316a5774f6defa147572'
},
'rhel9': {
'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel9-static',
'sha256': '90468fb38767505185013024678d9144ae13100d2355097657f58719d98fbbc4',
'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_rhel.so',
'module_sha256': '127227db81bcbebf80b225fc747b69cfcd4ad2f01cea486aa02d5c9ba6c18109'
'sha256': '709093d99d5d3e789134c131893614968e17eefd9ade2200f811d9b076b2f02e',
'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols.so',
'module_sha256': 'ae65337e2d13babc0c675bb4264d469daffa2efb7627c9bf39ac59e42e3ebede'
},
'ubuntu': {
'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-ubuntu-static',
'sha256': '89aaf66474e78cb3c1666784e0e7a417550bd317e6ab148201bdc318d36710cb',
'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_ubuntu.so',
'module_sha256': 'e7734f1e6226c2a0a8e00c1f6534ea9f577df9081b046736a774b1c52c28e7e5'
'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols.so',
'module_sha256': '62978ede1f174dd2885e5227a3d9cc463d0c27acd77cfc23743d7309ee0c54ea'
}
}
@ -1471,7 +1472,9 @@ module cyberpanel_ols {
"""Change MySQL root password"""
try:
if self.remotemysql == 'OFF':
passwordCMD = "use mysql;DROP DATABASE IF EXISTS test;DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%%';GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY '%s';flush privileges;" % (self.mysql_Root_password)
# Use ALTER USER syntax (compatible with MariaDB 10.4+ and MySQL 5.7+)
# GRANT ... IDENTIFIED BY is deprecated in MariaDB 10.4+ and removed in 10.11+
passwordCMD = "use mysql;DROP DATABASE IF EXISTS test;DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%%';ALTER USER 'root'@'localhost' IDENTIFIED BY '%s';GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;flush privileges;" % (self.mysql_Root_password)
# Try socket authentication first (for fresh MariaDB installations)
socket_commands = ['sudo mysql', 'sudo mariadb', 'sudo /usr/bin/mysql', 'sudo /usr/bin/mariadb']

View File

@ -30,7 +30,7 @@ import _thread
try:
from dns.models import Domains as dnsDomains
from dns.models import Records as dnsRecords
from mailServer.models import Forwardings, Pipeprograms
from mailServer.models import Forwardings, Pipeprograms, CatchAllEmail
from plogical.acl import ACLManager
from plogical.dnsUtilities import DNS
from loginSystem.models import Administrator
@ -2030,6 +2030,151 @@ protocol sieve {
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def fetchCatchAllConfig(self):
try:
userID = self.request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0:
return ACLManager.loadErrorJson('fetchStatus', 0)
data = json.loads(self.request.body)
domain = data['domain']
admin = Administrator.objects.get(pk=userID)
if ACLManager.checkOwnership(domain, admin, currentACL) == 1:
pass
else:
return ACLManager.loadErrorJson()
try:
domainObj = Domains.objects.get(domain=domain)
catchAll = CatchAllEmail.objects.get(domain=domainObj)
data_ret = {
'status': 1,
'fetchStatus': 1,
'configured': 1,
'destination': catchAll.destination,
'enabled': catchAll.enabled
}
except CatchAllEmail.DoesNotExist:
data_ret = {
'status': 1,
'fetchStatus': 1,
'configured': 0
}
except Domains.DoesNotExist:
data_ret = {
'status': 0,
'fetchStatus': 0,
'error_message': 'Domain not found in email system'
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def saveCatchAllConfig(self):
try:
userID = self.request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0:
return ACLManager.loadErrorJson('saveStatus', 0)
data = json.loads(self.request.body)
domain = data['domain']
destination = data['destination']
enabled = data.get('enabled', True)
admin = Administrator.objects.get(pk=userID)
if ACLManager.checkOwnership(domain, admin, currentACL) == 1:
pass
else:
return ACLManager.loadErrorJson()
# Validate destination email
if '@' not in destination:
data_ret = {'status': 0, 'saveStatus': 0, 'error_message': 'Invalid destination email address'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
domainObj = Domains.objects.get(domain=domain)
# Create or update catch-all config
catchAll, created = CatchAllEmail.objects.update_or_create(
domain=domainObj,
defaults={'destination': destination, 'enabled': enabled}
)
# Also add/update entry in Forwardings table for Postfix
catchAllSource = '@' + domain
if enabled:
# Remove existing catch-all forwarding if any
Forwardings.objects.filter(source=catchAllSource).delete()
# Add new forwarding
forwarding = Forwardings(source=catchAllSource, destination=destination)
forwarding.save()
else:
# Remove catch-all forwarding when disabled
Forwardings.objects.filter(source=catchAllSource).delete()
data_ret = {
'status': 1,
'saveStatus': 1,
'message': 'Catch-all email configured successfully'
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'status': 0, 'saveStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def deleteCatchAllConfig(self):
try:
userID = self.request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0:
return ACLManager.loadErrorJson('deleteStatus', 0)
data = json.loads(self.request.body)
domain = data['domain']
admin = Administrator.objects.get(pk=userID)
if ACLManager.checkOwnership(domain, admin, currentACL) == 1:
pass
else:
return ACLManager.loadErrorJson()
domainObj = Domains.objects.get(domain=domain)
# Delete catch-all config
CatchAllEmail.objects.filter(domain=domainObj).delete()
# Remove from Forwardings table
catchAllSource = '@' + domain
Forwardings.objects.filter(source=catchAllSource).delete()
data_ret = {
'status': 1,
'deleteStatus': 1,
'message': 'Catch-all email removed successfully'
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'status': 0, 'deleteStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def refreshEmailDiskUsage(self):
"""Refresh disk usage for all email accounts in a domain"""
try:

View File

@ -49,3 +49,17 @@ class Transport(models.Model):
class Pipeprograms(models.Model):
source = models.CharField(max_length=80)
destination = models.TextField()
class Meta:
db_table = 'e_pipeprograms'
class CatchAllEmail(models.Model):
"""Stores catch-all email configuration per domain"""
domain = models.OneToOneField(Domains, on_delete=models.CASCADE, primary_key=True, db_column='domain_id')
destination = models.CharField(max_length=255)
enabled = models.BooleanField(default=True)
class Meta:
db_table = 'e_catchall'
managed = False

View File

@ -38,4 +38,9 @@ urlpatterns = [
### disk usage refresh
re_path(r'^refreshEmailDiskUsage$', views.refreshEmailDiskUsage, name='refreshEmailDiskUsage'),
## Catch-All Email
re_path(r'^fetchCatchAllConfig$', views.fetchCatchAllConfig, name='fetchCatchAllConfig'),
re_path(r'^saveCatchAllConfig$', views.saveCatchAllConfig, name='saveCatchAllConfig'),
re_path(r'^deleteCatchAllConfig$', views.deleteCatchAllConfig, name='deleteCatchAllConfig'),
]

View File

@ -262,6 +262,33 @@ def SaveEmailLimitsNew(request):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def fetchCatchAllConfig(request):
try:
msM = MailServerManager(request)
return msM.fetchCatchAllConfig()
except KeyError as msg:
data_ret = {'fetchStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def saveCatchAllConfig(request):
try:
msM = MailServerManager(request)
return msM.saveCatchAllConfig()
except KeyError as msg:
data_ret = {'saveStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def deleteCatchAllConfig(request):
try:
msM = MailServerManager(request)
return msM.deleteCatchAllConfig()
except KeyError as msg:
data_ret = {'deleteStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def refreshEmailDiskUsage(request):
try:
msM = MailServerManager(request)

View File

@ -291,24 +291,26 @@ extprocessor docker{port} {{
@staticmethod
def SetupN8NVhost(domain, port):
"""Setup n8n vhost with proper proxy configuration including Origin header"""
"""Setup n8n vhost with proper proxy configuration for OpenLiteSpeed"""
try:
vhost_path = f'/usr/local/lsws/conf/vhosts/{domain}/vhost.conf'
if not os.path.exists(vhost_path):
logging.writeToFile(f"Error: Vhost file not found at {vhost_path}")
return False
# Read existing vhost configuration
with open(vhost_path, 'r') as f:
content = f.read()
# Check if context already exists
if 'context / {' in content:
logging.writeToFile("Context already exists, skipping...")
return True
# Add proxy context with proper headers for n8n
# NOTE: Do NOT include "RequestHeader set Origin" - OpenLiteSpeed cannot override
# browser Origin headers, which is why NODE_ENV=development is required
proxy_context = f'''
# N8N Proxy Configuration
@ -322,7 +324,6 @@ context / {{
RequestHeader set X-Forwarded-For $ip
RequestHeader set X-Forwarded-Proto https
RequestHeader set X-Forwarded-Host "{domain}"
RequestHeader set Origin "{domain}, {domain}"
RequestHeader set Host "{domain}"
END_extraHeaders
}}
@ -1370,7 +1371,7 @@ services:
'DB_POSTGRESDB_PASSWORD': self.data['MySQLPassword'],
'N8N_HOST': '0.0.0.0',
'N8N_PORT': '5678',
'NODE_ENV': 'production',
'NODE_ENV': 'development', # Required for OpenLiteSpeed compatibility - OLS cannot override browser Origin headers which n8n v1.87.0+ validates in production mode
'N8N_EDITOR_BASE_URL': f"https://{self.data['finalURL']}",
'WEBHOOK_URL': f"https://{self.data['finalURL']}",
'WEBHOOK_TUNNEL_URL': f"https://{self.data['finalURL']}",

View File

@ -18,6 +18,102 @@ class modSec:
tempRulesFile = "/home/cyberpanel/tempModSecRules"
mirrorPath = "cyberpanel.net"
# Compatible ModSecurity binaries (built against custom OLS headers)
# These prevent ABI incompatibility crashes (Signal 11/SIGSEGV)
MODSEC_COMPATIBLE = {
'rhel8': {
'url': 'https://cyberpanel.net/mod_security-compatible-rhel8.so',
'sha256': 'bbbf003bdc7979b98f09b640dffe2cbbe5f855427f41319e4c121403c05837b2'
},
'rhel9': {
'url': 'https://cyberpanel.net/mod_security-compatible-rhel.so',
'sha256': '19deb2ffbaf1334cf4ce4d46d53f747a75b29e835bf5a01f91ebcc0c78e98629'
},
'ubuntu': {
'url': 'https://cyberpanel.net/mod_security-compatible-ubuntu.so',
'sha256': 'ed02c813136720bd4b9de5925f6e41bdc8392e494d7740d035479aaca6d1e0cd'
}
}
@staticmethod
def detectPlatform():
"""Detect OS platform for compatible binary selection"""
try:
# Check for Ubuntu/Debian
if os.path.exists('/etc/lsb-release'):
with open('/etc/lsb-release', 'r') as f:
content = f.read()
if 'Ubuntu' in content or 'ubuntu' in content:
return 'ubuntu'
# Check for Debian
if os.path.exists('/etc/debian_version'):
return 'ubuntu' # Use Ubuntu binary for Debian
# Check for RHEL-based distributions
if os.path.exists('/etc/os-release'):
with open('/etc/os-release', 'r') as f:
content = f.read().lower()
# Check for version 8.x
if 'version="8.' in content or 'version_id="8' in content:
return 'rhel8'
# Check for version 9.x
if 'version="9.' in content or 'version_id="9' in content:
return 'rhel9'
return 'rhel9' # Default to rhel9
except:
return 'rhel9'
@staticmethod
def downloadCompatibleModSec(platform):
"""Download and install compatible ModSecurity binary"""
try:
config = modSec.MODSEC_COMPATIBLE.get(platform)
if not config:
logging.CyberCPLogFileWriter.writeToFile(f"No compatible ModSecurity for platform {platform}")
return False
modsec_path = "/usr/local/lsws/modules/mod_security.so"
tmp_path = "/tmp/mod_security-compatible.so"
# Download compatible binary
command = f"wget -q {config['url']} -O {tmp_path}"
result = subprocess.call(shlex.split(command))
if result != 0:
logging.CyberCPLogFileWriter.writeToFile("Failed to download compatible ModSecurity")
return False
# Verify checksum
import hashlib
sha256_hash = hashlib.sha256()
with open(tmp_path, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
actual_sha256 = sha256_hash.hexdigest()
if actual_sha256 != config['sha256']:
logging.CyberCPLogFileWriter.writeToFile(f"ModSecurity checksum mismatch: expected {config['sha256']}, got {actual_sha256}")
os.remove(tmp_path)
return False
# Backup original if exists
if os.path.exists(modsec_path):
shutil.copy2(modsec_path, f"{modsec_path}.stock")
# Install compatible version
shutil.move(tmp_path, modsec_path)
os.chmod(modsec_path, 0o644)
logging.CyberCPLogFileWriter.writeToFile("Installed compatible ModSecurity binary")
return True
except BaseException as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [downloadCompatibleModSec]")
return False
@staticmethod
def installModSec():
try:
@ -45,6 +141,23 @@ class modSec:
writeToFile.writelines("ModSecurity Installed.[200]\n")
writeToFile.close()
# Check if custom OLS binary is installed - if so, replace with compatible ModSecurity
custom_ols_marker = "/usr/local/lsws/modules/cyberpanel_ols.so"
if os.path.exists(custom_ols_marker):
writeToFile = open(modSec.installLogPath, 'a')
writeToFile.writelines("Custom OLS detected, installing compatible ModSecurity...\n")
writeToFile.close()
platform = modSec.detectPlatform()
if modSec.downloadCompatibleModSec(platform):
writeToFile = open(modSec.installLogPath, 'a')
writeToFile.writelines("Compatible ModSecurity installed successfully.\n")
writeToFile.close()
else:
writeToFile = open(modSec.installLogPath, 'a')
writeToFile.writelines("WARNING: Could not install compatible ModSecurity. May experience crashes.\n")
writeToFile.close()
return 1
except BaseException as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + "[installModSec]")

View File

@ -938,25 +938,32 @@ class Upgrade:
platform = Upgrade.detectPlatform()
Upgrade.stdOut(f"Detected platform: {platform}", 0)
# Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 - v2.0.5 Static Build)
# Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 with PHPConfig + Header unset fix + Static Linking)
# Module Build Date: December 28, 2025 - v2.2.0 Brute Force with Progressive Throttle
BINARY_CONFIGS = {
'rhel8': {
'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel8-static',
'sha256': '6ce688a237615102cc1603ee1999b3cede0ff3482d31e1f65705e92396d34b3a',
'module_url': None, # RHEL 8 doesn't have module (use RHEL 9 if needed)
'module_sha256': None
'module_url': 'https://cyberpanel.net/binaries/rhel8/cyberpanel_ols.so',
'module_sha256': '7c33d89c7fbcd3ed7b0422fee3f49b5e041713c2c2b7316a5774f6defa147572',
'modsec_url': 'https://cyberpanel.net/mod_security-compatible-rhel8.so',
'modsec_sha256': 'bbbf003bdc7979b98f09b640dffe2cbbe5f855427f41319e4c121403c05837b2'
},
'rhel9': {
'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-rhel9-static',
'sha256': '90468fb38767505185013024678d9144ae13100d2355097657f58719d98fbbc4',
'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_rhel.so',
'module_sha256': '127227db81bcbebf80b225fc747b69cfcd4ad2f01cea486aa02d5c9ba6c18109'
'sha256': '709093d99d5d3e789134c131893614968e17eefd9ade2200f811d9b076b2f02e',
'module_url': 'https://cyberpanel.net/binaries/rhel9/cyberpanel_ols.so',
'module_sha256': 'ae65337e2d13babc0c675bb4264d469daffa2efb7627c9bf39ac59e42e3ebede',
'modsec_url': 'https://cyberpanel.net/mod_security-compatible-rhel.so',
'modsec_sha256': '19deb2ffbaf1334cf4ce4d46d53f747a75b29e835bf5a01f91ebcc0c78e98629'
},
'ubuntu': {
'url': 'https://cyberpanel.net/openlitespeed-phpconfig-x86_64-ubuntu-static',
'sha256': '89aaf66474e78cb3c1666784e0e7a417550bd317e6ab148201bdc318d36710cb',
'module_url': 'https://cyberpanel.net/cyberpanel_ols_x86_64_ubuntu.so',
'module_sha256': 'e7734f1e6226c2a0a8e00c1f6534ea9f577df9081b046736a774b1c52c28e7e5'
'module_url': 'https://cyberpanel.net/binaries/ubuntu/cyberpanel_ols.so',
'module_sha256': '62978ede1f174dd2885e5227a3d9cc463d0c27acd77cfc23743d7309ee0c54ea',
'modsec_url': 'https://cyberpanel.net/mod_security-compatible-ubuntu.so',
'modsec_sha256': 'ed02c813136720bd4b9de5925f6e41bdc8392e494d7740d035479aaca6d1e0cd'
}
}
@ -970,8 +977,11 @@ class Upgrade:
OLS_BINARY_SHA256 = config['sha256']
MODULE_URL = config['module_url']
MODULE_SHA256 = config['module_sha256']
MODSEC_URL = config.get('modsec_url')
MODSEC_SHA256 = config.get('modsec_sha256')
OLS_BINARY_PATH = "/usr/local/lsws/bin/openlitespeed"
MODULE_PATH = "/usr/local/lsws/modules/cyberpanel_ols.so"
MODSEC_PATH = "/usr/local/lsws/modules/mod_security.so"
# Create backup
from datetime import datetime
@ -983,12 +993,16 @@ class Upgrade:
if os.path.exists(OLS_BINARY_PATH):
shutil.copy2(OLS_BINARY_PATH, f"{backup_dir}/openlitespeed.backup")
Upgrade.stdOut(f"Backup created at: {backup_dir}", 0)
# Also backup existing ModSecurity if it exists
if os.path.exists(MODSEC_PATH):
shutil.copy2(MODSEC_PATH, f"{backup_dir}/mod_security.so.backup")
except Exception as e:
Upgrade.stdOut(f"WARNING: Could not create backup: {e}", 0)
# Download binaries to temp location
tmp_binary = "/tmp/openlitespeed-custom"
tmp_module = "/tmp/cyberpanel_ols.so"
tmp_modsec = "/tmp/mod_security.so"
Upgrade.stdOut("Downloading custom binaries...", 0)
@ -1026,6 +1040,18 @@ class Upgrade:
else:
Upgrade.stdOut("Note: No CyberPanel module for this platform", 0)
# Download compatible ModSecurity if existing ModSecurity is installed
# This prevents ABI incompatibility crashes (Signal 11/SIGSEGV)
modsec_downloaded = False
if os.path.exists(MODSEC_PATH) and MODSEC_URL and MODSEC_SHA256:
Upgrade.stdOut("Existing ModSecurity detected - downloading compatible version...", 0)
if Upgrade.downloadCustomBinary(MODSEC_URL, tmp_modsec, MODSEC_SHA256):
modsec_downloaded = True
else:
Upgrade.stdOut("WARNING: Failed to download compatible ModSecurity", 0)
Upgrade.stdOut("ModSecurity may crash due to ABI incompatibility", 0)
Upgrade.stdOut("Consider manually updating ModSecurity after upgrade", 0)
# Install OpenLiteSpeed binary
Upgrade.stdOut("Installing custom binaries...", 0)
@ -1068,6 +1094,16 @@ class Upgrade:
Upgrade.stdOut(f"ERROR: Failed to install module: {e}", 0)
return False
# Install compatible ModSecurity (if downloaded)
if modsec_downloaded:
try:
shutil.move(tmp_modsec, MODSEC_PATH)
os.chmod(MODSEC_PATH, 0o644)
Upgrade.stdOut("Installed compatible ModSecurity module", 0)
except Exception as e:
Upgrade.stdOut(f"WARNING: Failed to install ModSecurity: {e}", 0)
# Non-fatal, continue
# Verify installation
if os.path.exists(OLS_BINARY_PATH):
if not module_downloaded or os.path.exists(MODULE_PATH):
@ -2495,6 +2531,19 @@ CREATE TABLE `websiteFunctions_backupsv2` (`id` integer AUTO_INCREMENT NOT NULL
except:
pass
# Email Filtering Tables - Catch-All, Plus-Addressing, Pattern Forwarding
query = """CREATE TABLE IF NOT EXISTS `e_catchall` (
`domain_id` varchar(50) NOT NULL,
`destination` varchar(255) NOT NULL,
`enabled` tinyint(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`domain_id`),
CONSTRAINT `fk_catchall_domain` FOREIGN KEY (`domain_id`) REFERENCES `e_domains` (`domain`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"""
try:
cursor.execute(query)
except:
pass
try:
connection.close()
except: