diff --git a/emailDelivery/migrations/0001_initial.py b/emailDelivery/migrations/0001_initial.py new file mode 100644 index 000000000..825493e3d --- /dev/null +++ b/emailDelivery/migrations/0001_initial.py @@ -0,0 +1,128 @@ +# CyberMail Email Delivery — initial schema. +# Database DDL uses int(11) FK to legacy loginSystem_administrator.id (MariaDB rejects bigint FK -> int PK). + +from django.db import migrations, models +import django.db.models.deletion + + +CREATE_CYBERMAIL_TABLES = ''' +CREATE TABLE `cybermail_accounts` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `platform_account_id` int(11) DEFAULT NULL, + `api_key` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, + `plan_name` varchar(100) NOT NULL, + `plan_slug` varchar(50) NOT NULL, + `emails_per_month` int(11) NOT NULL, + `is_connected` tinyint(1) NOT NULL, + `relay_enabled` tinyint(1) NOT NULL, + `smtp_credential_id` int(11) DEFAULT NULL, + `smtp_username` varchar(255) NOT NULL, + `smtp_host` varchar(255) NOT NULL, + `smtp_port` int(11) NOT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `admin_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `cybermail_accounts_admin_id_uniq` (`admin_id`), + CONSTRAINT `cybermail_accounts_admin_id_fk` FOREIGN KEY (`admin_id`) + REFERENCES `loginSystem_administrator` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +CREATE TABLE `cybermail_domains` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain` varchar(255) NOT NULL, + `platform_domain_id` int(11) DEFAULT NULL, + `status` varchar(50) NOT NULL, + `spf_verified` tinyint(1) NOT NULL, + `dkim_verified` tinyint(1) NOT NULL, + `dmarc_verified` tinyint(1) NOT NULL, + `dns_configured` tinyint(1) NOT NULL, + `created_at` datetime(6) NOT NULL, + `account_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + KEY `cybermail_domains_account_id_fk` (`account_id`), + CONSTRAINT `cybermail_domains_account_id_fk` FOREIGN KEY (`account_id`) + REFERENCES `cybermail_accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +''' + +DROP_CYBERMAIL_TABLES = ''' +DROP TABLE IF EXISTS `cybermail_domains`; +DROP TABLE IF EXISTS `cybermail_accounts`; +''' + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('loginSystem', '0001_initial'), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.RunSQL(CREATE_CYBERMAIL_TABLES, DROP_CYBERMAIL_TABLES), + ], + state_operations=[ + migrations.CreateModel( + name='CyberMailAccount', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('platform_account_id', models.IntegerField(null=True)), + ('api_key', models.CharField(blank=True, max_length=255)), + ('email', models.CharField(max_length=255)), + ('plan_name', models.CharField(default='Free', max_length=100)), + ('plan_slug', models.CharField(default='free', max_length=50)), + ('emails_per_month', models.IntegerField(default=15000)), + ('is_connected', models.BooleanField(default=False)), + ('relay_enabled', models.BooleanField(default=False)), + ('smtp_credential_id', models.IntegerField(null=True)), + ('smtp_username', models.CharField(blank=True, max_length=255)), + ('smtp_host', models.CharField(default='mail.cyberpersons.com', max_length=255)), + ('smtp_port', models.IntegerField(default=587)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ( + 'admin', + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name='cybermail_account', + to='loginSystem.administrator', + ), + ), + ], + options={ + 'db_table': 'cybermail_accounts', + }, + ), + migrations.CreateModel( + name='CyberMailDomain', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('domain', models.CharField(max_length=255)), + ('platform_domain_id', models.IntegerField(null=True)), + ('status', models.CharField(default='pending', max_length=50)), + ('spf_verified', models.BooleanField(default=False)), + ('dkim_verified', models.BooleanField(default=False)), + ('dmarc_verified', models.BooleanField(default=False)), + ('dns_configured', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ( + 'account', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='domains', + to='emailDelivery.cybermailaccount', + ), + ), + ], + options={ + 'db_table': 'cybermail_domains', + }, + ), + ], + ), + ] diff --git a/emailDelivery/migrations/__init__.py b/emailDelivery/migrations/__init__.py index e69de29bb..e9c0b14d4 100644 --- a/emailDelivery/migrations/__init__.py +++ b/emailDelivery/migrations/__init__.py @@ -0,0 +1 @@ +# CyberMail Email Delivery migrations diff --git a/emailDelivery/models.py b/emailDelivery/models.py index b1a6cdd5d..e4fc453b7 100644 --- a/emailDelivery/models.py +++ b/emailDelivery/models.py @@ -3,6 +3,8 @@ from loginSystem.models import Administrator class CyberMailAccount(models.Model): + # int(11) PK/FK to match legacy loginSystem_administrator.id (not DEFAULT_AUTO_FIELD BigAutoField) + id = models.AutoField(primary_key=True) admin = models.OneToOneField(Administrator, on_delete=models.CASCADE, related_name='cybermail_account') platform_account_id = models.IntegerField(null=True) api_key = models.CharField(max_length=255, blank=True) @@ -27,6 +29,7 @@ class CyberMailAccount(models.Model): class CyberMailDomain(models.Model): + id = models.AutoField(primary_key=True) account = models.ForeignKey(CyberMailAccount, on_delete=models.CASCADE, related_name='domains') domain = models.CharField(max_length=255) platform_domain_id = models.IntegerField(null=True) diff --git a/emailPremium/views.py b/emailPremium/views.py index 63caefcb8..8a3be354c 100644 --- a/emailPremium/views.py +++ b/emailPremium/views.py @@ -3,7 +3,7 @@ import os import time from django.shortcuts import redirect -from django.http import HttpResponse +from django.http import HttpResponse, JsonResponse from loginSystem.models import Administrator from mailServer.models import Domains, EUsers @@ -1233,32 +1233,28 @@ def installStatusMailScanner(request): ###Rspamd def Rspamd(request): - url = "https://platform.cyberpersons.com/CyberpanelAdOns/Adonpermission" - data = { - "name": "email-debugger", - "IP": ACLManager.GetServerIP() - } + """Rspamd UI — allow for any logged-in admin (do not require cloud addon permission).""" + try: + userID = request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + if currentACL['admin'] != 1: + return ACLManager.loadError() + except KeyError: + return redirect(loadLoginPage) - import requests - response = requests.post(url, data=json.dumps(data)) - Status = response.json()['status'] + checkIfRspamdInstalled = 0 - if (Status == 1) or ProcessUtilities.decideServer() == ProcessUtilities.ent: - checkIfRspamdInstalled = 0 + ipFile = "/etc/cyberpanel/machineIP" + f = open(ipFile) + ipData = f.read() + ipAddress = ipData.split('\n', 1)[0] - ipFile = "/etc/cyberpanel/machineIP" - f = open(ipFile) - ipData = f.read() - ipAddress = ipData.split('\n', 1)[0] + if mailUtilities.checkIfRspamdInstalled() == 1: + checkIfRspamdInstalled = 1 - if mailUtilities.checkIfRspamdInstalled() == 1: - checkIfRspamdInstalled = 1 - - proc = httpProc(request, 'emailPremium/Rspamd.html', - {'checkIfRspamdInstalled': checkIfRspamdInstalled, 'ipAddress': ipAddress}, 'admin') - return proc.render() - else: - return redirect("https://cyberpanel.net/cyberpanel-addons") + proc = httpProc(request, 'emailPremium/Rspamd.html', + {'checkIfRspamdInstalled': checkIfRspamdInstalled, 'ipAddress': ipAddress}, 'admin') + return proc.render() def installRspamd(request): try: @@ -1268,35 +1264,20 @@ def installRspamd(request): if currentACL['admin'] == 1: pass else: - return ACLManager.loadErrorJson() + return JsonResponse({'status': 0, 'error_message': 'Admin access required.'}) - url = "https://platform.cyberpersons.com/CyberpanelAdOns/Adonpermission" - data = { - "name": "email-debugger", - "IP": ACLManager.GetServerIP() - } - - import requests - response = requests.post(url, data=json.dumps(data)) - Status = response.json()['status'] - - if (Status == 1) or ProcessUtilities.decideServer() == ProcessUtilities.ent: - try: - - execPath = "/usr/local/CyberCP/bin/python " + virtualHostUtilities.cyberPanel + "/plogical/mailUtilities.py" - execPath = execPath + " installRspamd" - ProcessUtilities.popenExecutioner(execPath) - - final_json = json.dumps({'status': 1, 'error_message': "None"}) - return HttpResponse(final_json) - except BaseException as msg: - final_dic = {'status': 0, 'error_message': str(msg)} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + try: + execPath = "/usr/local/CyberCP/bin/python " + virtualHostUtilities.cyberPanel + "/plogical/mailUtilities.py" + execPath = execPath + " installRspamd" + ProcessUtilities.popenExecutioner(execPath) + return JsonResponse({'status': 1, 'error_message': 'None'}) + except BaseException as msg: + return JsonResponse({'status': 0, 'error_message': str(msg)}) except KeyError: - final_dic = {'status': 0, 'error_message': "Not Logged In, please refresh the page or login again."} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return JsonResponse({ + 'status': 0, + 'error_message': 'Not Logged In, please refresh the page or login again.', + }) def installStatusRspamd(request): try: @@ -1310,8 +1291,15 @@ def installStatusRspamd(request): try: if request.method == 'POST': - command = "sudo cat " + mailUtilities.RspamdInstallLogPath - installStatus = ProcessUtilities.outputExecutioner(command) + if not os.path.isfile(mailUtilities.RspamdInstallLogPath): + installStatus = ( + 'Waiting for installation to start... ' + '(Progress file: /var/log/cyberpanel/rspamd-install.log — if this persists, ' + 'click Install again or run as root: tail -f /var/log/cyberpanel/rspamd-install.log)\n' + ) + else: + command = "sudo cat " + mailUtilities.RspamdInstallLogPath + installStatus = ProcessUtilities.outputExecutioner(command) if installStatus.find("[200]") > -1: @@ -1365,19 +1353,8 @@ def fetchRspamdSettings(request): else: return ACLManager.loadErrorJson('fetchStatus', 0) - url = "https://platform.cyberpersons.com/CyberpanelAdOns/Adonpermission" - data = { - "name": "email-debugger", - "IP": ACLManager.GetServerIP() - } - - import requests - response = requests.post(url, data=json.dumps(data)) - Status = response.json()['status'] - - if (Status == 1) or ProcessUtilities.decideServer() == ProcessUtilities.ent: - try: - if request.method == 'POST': + try: + if request.method == 'POST': enabled = True action = '' @@ -1577,10 +1554,10 @@ def fetchRspamdSettings(request): final_json = json.dumps(final_dic) return HttpResponse(final_json) - except BaseException as msg: - final_dic = {'fetchStatus': 0, 'error_message': str(msg)} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + except BaseException as msg: + final_dic = {'fetchStatus': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) except KeyError: return redirect(loadLoginPage) @@ -1747,21 +1724,17 @@ def unistallRspamd(request): ProcessUtilities.popenExecutioner(execPath) - final_json = json.dumps({'status': 1, 'error_message': "None"}) - return HttpResponse(final_json) + return JsonResponse({'status': 1, 'error_message': 'None'}) except BaseException as msg: - final_dic = {'status': 0, 'error_message': str(msg)} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return JsonResponse({'status': 0, 'error_message': str(msg)}) except KeyError: - final_dic = {'status': 0, 'error_message': "Not Logged In, please refresh the page or login again."} - - final_json = json.dumps(final_dic) - - return HttpResponse(final_json) + return JsonResponse({ + 'status': 0, + 'error_message': 'Not Logged In, please refresh the page or login again.', + }) def uninstallStatusRspamd(request): try: @@ -1775,8 +1748,14 @@ def uninstallStatusRspamd(request): try: if request.method == 'POST': - command = "sudo cat " + mailUtilities.RspamdUnInstallLogPath - installStatus = ProcessUtilities.outputExecutioner(command) + if not os.path.isfile(mailUtilities.RspamdUnInstallLogPath): + installStatus = ( + 'Waiting for uninstall to start... ' + '(Progress file: /var/log/cyberpanel/rspamd-uninstall.log)\n' + ) + else: + command = "sudo cat " + mailUtilities.RspamdUnInstallLogPath + installStatus = ProcessUtilities.outputExecutioner(command) if installStatus.find("[200]") > -1: diff --git a/mailServer/mailserverManager.py b/mailServer/mailserverManager.py index 24252a4c2..a44106142 100644 --- a/mailServer/mailserverManager.py +++ b/mailServer/mailserverManager.py @@ -12,6 +12,7 @@ sys.path.append('/usr/local/CyberCP') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings") django.setup() from django.http import HttpResponse +from django.db import connection try: from .models import Domains,EUsers from loginSystem.views import loadLoginPage @@ -45,6 +46,57 @@ import threading as multi import argparse + + +def _ensure_email_filter_tables(): + """Create catch-all/plus/pattern email feature tables if missing.""" + create_statements = [ + """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`), + KEY `idx_e_catchall_domain_id` (`domain_id`) +) ENGINE=InnoDB""", + """CREATE TABLE IF NOT EXISTS `e_server_settings` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `plus_addressing_enabled` tinyint(1) NOT NULL DEFAULT 0, + `plus_addressing_delimiter` varchar(1) NOT NULL DEFAULT '+', + PRIMARY KEY (`id`) +) ENGINE=InnoDB""", + """CREATE TABLE IF NOT EXISTS `e_plus_override` ( + `domain_id` varchar(50) NOT NULL, + `enabled` tinyint(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`domain_id`), + KEY `idx_e_plus_override_domain_id` (`domain_id`) +) ENGINE=InnoDB""", + """CREATE TABLE IF NOT EXISTS `e_pattern_forwarding` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` varchar(50) NOT NULL, + `pattern` varchar(255) NOT NULL, + `destination` varchar(255) NOT NULL, + `pattern_type` varchar(20) NOT NULL DEFAULT 'wildcard', + `priority` int(11) NOT NULL DEFAULT 100, + `enabled` tinyint(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + KEY `idx_e_pattern_forwarding_domain_id` (`domain_id`) +) ENGINE=InnoDB""" + ] + + try: + with connection.cursor() as cursor: + for query in create_statements: + cursor.execute(query) + cursor.execute( + """INSERT INTO `e_server_settings` (`id`, `plus_addressing_enabled`, `plus_addressing_delimiter`) + SELECT 1, 0, '+' + WHERE NOT EXISTS (SELECT 1 FROM `e_server_settings` WHERE `id` = 1)""" + ) + return True, '' + except BaseException as msg: + logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [_ensure_email_filter_tables]') + return False, 'Email feature tables are missing and could not be created automatically. Please run CyberPanel upgrade or apply DB migration.' + def _get_email_limits_controller_js(): """Return EmailLimitsNew controller JS: from file or hardcoded fallback so it always works.""" try: @@ -2084,6 +2136,12 @@ protocol sieve { if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0: return ACLManager.loadErrorJson('fetchStatus', 0) + ok, schemaErr = _ensure_email_filter_tables() + if not ok: + data_ret = {'status': 0, 'fetchStatus': 0, 'error_message': schemaErr} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + data = json.loads(self.request.body) domain = data['domain'] @@ -2132,6 +2190,12 @@ protocol sieve { if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0: return ACLManager.loadErrorJson('saveStatus', 0) + ok, schemaErr = _ensure_email_filter_tables() + if not ok: + data_ret = {'status': 0, 'saveStatus': 0, 'error_message': schemaErr} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + data = json.loads(self.request.body) domain = data['domain'] destination = data['destination'] @@ -2190,6 +2254,12 @@ protocol sieve { if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0: return ACLManager.loadErrorJson('deleteStatus', 0) + ok, schemaErr = _ensure_email_filter_tables() + if not ok: + data_ret = {'status': 0, 'deleteStatus': 0, 'error_message': schemaErr} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + data = json.loads(self.request.body) domain = data['domain'] diff --git a/plogical/mailUtilities.py b/plogical/mailUtilities.py index ecf334642..ec5f821ef 100644 --- a/plogical/mailUtilities.py +++ b/plogical/mailUtilities.py @@ -1,12 +1,13 @@ import json -import os,sys +import os, sys import time +# CyberPanel Django app "dns" must win over PyPI "dns" (dnspython). append() leaves site-packages first. +_cybercp_root = '/usr/local/CyberCP' +if _cybercp_root not in sys.path: + sys.path.insert(0, _cybercp_root) + from django.http import HttpResponse - - - -sys.path.append('/usr/local/CyberCP') import django os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings") try: @@ -37,8 +38,10 @@ class mailUtilities: installLogPath = "/home/cyberpanel/openDKIMInstallLog" spamassassinInstallLogPath = "/home/cyberpanel/spamassassinInstallLogPath" - RspamdInstallLogPath = "/home/cyberpanel/RspamdInstallLogPath" - RspamdUnInstallLogPath = "/home/cyberpanel/RspamdUnInstallLogPath" + # Use /var/log (not /home/cyberpanel mode 0700) so lscpd workers can always write install progress. + rspamdLogDir = "/var/log/cyberpanel" + RspamdInstallLogPath = "/var/log/cyberpanel/rspamd-install.log" + RspamdUnInstallLogPath = "/var/log/cyberpanel/rspamd-uninstall.log" cyberPanelHome = "/home/cyberpanel" mailScannerInstallLogPath = "/home/cyberpanel/mailScannerInstallLogPath" RSpamdLogPath = '/var/log/rspamd/rspamd.log' @@ -822,14 +825,27 @@ return custom_keywords @staticmethod def installRspamd(install, rspamd): + # Progress file before ServiceManager import (import can fail; panel polls this path). + os.makedirs(mailUtilities.rspamdLogDir, mode=0o755, exist_ok=True) + if not os.path.isdir(mailUtilities.cyberPanelHome): + os.makedirs(mailUtilities.cyberPanelHome, mode=0o750, exist_ok=True) + with open(mailUtilities.RspamdInstallLogPath, 'w') as lf: + lf.write('Starting Rspamd installation (preparing Redis and packages)...\n') + lf.flush() + from manageServices.serviceManager import ServiceManager try: - if os.path.exists(mailUtilities.RspamdInstallLogPath): - os.remove(mailUtilities.RspamdInstallLogPath) - - ####Frist install redis + with open(mailUtilities.RspamdInstallLogPath, 'a') as lf: + lf.write( + 'Running InstallRedis() — this often takes 5–20 minutes (system packages). ' + 'Progress from that step is not streamed here; the log continues after Redis finishes.\n' + ) + lf.flush() ServiceManager.InstallRedis() + with open(mailUtilities.RspamdInstallLogPath, 'a') as lf: + lf.write('Redis step finished. Adding Rspamd repository and installing packages...\n') + lf.flush() if ProcessUtilities.decideDistro() == ProcessUtilities.centos: @@ -862,16 +878,18 @@ return custom_keywords command = 'rpm --import https://rspamd.com/rpm-stable/gpg.key' ProcessUtilities.normalExecutioner(command, True) - command = 'yum update' + command = 'dnf update -y' ProcessUtilities.normalExecutioner(command, True) - command = 'sudo yum install rspamd clamav clamd clamav-update -y' + command = 'sudo dnf install -y rspamd clamav clamd clamav-update' else: command = 'DEBIAN_FRONTEND=noninteractive apt-get install rspamd clamav clamav-daemon -y' - with open(mailUtilities.RspamdInstallLogPath, 'w') as f: - res = subprocess.call(command, stdout=f, shell=True) + with open(mailUtilities.RspamdInstallLogPath, 'a') as f: + f.write('\n--- Package install ---\n') + f.flush() + res = subprocess.call(command, stdout=f, stderr=f, shell=True) ###### makefile @@ -1106,10 +1124,11 @@ LogFile /var/log/clamav/clamav.log return 1 except BaseException as msg: - writeToFile = open(mailUtilities.RspamdInstallLogPath, 'a') - writeToFile.writelines("Can not be installed.[404]\n") - writeToFile.close() - logging.CyberCPLogFileWriter.writeToFile(str(msg) + "[installRspamd]") + err = str(msg) + with open(mailUtilities.RspamdInstallLogPath, 'a') as writeToFile: + writeToFile.write('Install failed: %s\n' % err.replace('\r', ' ').replace('\n', ' ')[:2000]) + writeToFile.write('Can not be installed.[404]\n') + logging.CyberCPLogFileWriter.writeToFile(err + "[installRspamd]") @staticmethod def uninstallRspamd(install, rspamd): @@ -1119,6 +1138,16 @@ LogFile /var/log/clamav/clamav.log if os.path.exists(mailUtilities.RspamdUnInstallLogPath): os.remove(mailUtilities.RspamdUnInstallLogPath) + try: + os.makedirs(mailUtilities.rspamdLogDir, mode=0o755, exist_ok=True) + if not os.path.isdir(mailUtilities.cyberPanelHome): + os.makedirs(mailUtilities.cyberPanelHome, mode=0o750, exist_ok=True) + with open(mailUtilities.RspamdUnInstallLogPath, 'w') as lf: + lf.write('Starting Rspamd removal...\n') + lf.flush() + except BaseException as log_err: + logging.CyberCPLogFileWriter.writeToFile( + str(log_err) + ' [uninstallRspamd init log]') if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8: command = 'sudo yum remove rspamd clamav clamav-daemon -y' @@ -1129,7 +1158,7 @@ LogFile /var/log/clamav/clamav.log - with open(mailUtilities.RspamdUnInstallLogPath, 'w') as f: + with open(mailUtilities.RspamdUnInstallLogPath, 'a') as f: res = subprocess.call(cmd, stdout=f) if res == 1: writeToFile = open(mailUtilities.RspamdUnInstallLogPath, 'a') diff --git a/plogical/upgrade.py b/plogical/upgrade.py index c702a55d4..30a9fdcb3 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -2878,8 +2878,8 @@ CREATE TABLE `websiteFunctions_backupsv2` (`id` integer AUTO_INCREMENT 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""" + KEY `idx_e_catchall_domain_id` (`domain_id`) +) ENGINE=InnoDB""" try: cursor.execute(query) except: @@ -2890,7 +2890,7 @@ CREATE TABLE `websiteFunctions_backupsv2` (`id` integer AUTO_INCREMENT NOT NULL `plus_addressing_enabled` tinyint(1) NOT NULL DEFAULT 0, `plus_addressing_delimiter` varchar(1) NOT NULL DEFAULT '+', PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""" +) ENGINE=InnoDB""" try: cursor.execute(query) except: @@ -2900,8 +2900,8 @@ CREATE TABLE `websiteFunctions_backupsv2` (`id` integer AUTO_INCREMENT NOT NULL `domain_id` varchar(50) NOT NULL, `enabled` tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (`domain_id`), - CONSTRAINT `fk_plus_override_domain` FOREIGN KEY (`domain_id`) REFERENCES `e_domains` (`domain`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""" + KEY `idx_e_plus_override_domain_id` (`domain_id`) +) ENGINE=InnoDB""" try: cursor.execute(query) except: @@ -2916,14 +2916,21 @@ CREATE TABLE `websiteFunctions_backupsv2` (`id` integer AUTO_INCREMENT NOT NULL `priority` int(11) NOT NULL DEFAULT 100, `enabled` tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (`id`), - KEY `fk_pattern_domain` (`domain_id`), - CONSTRAINT `fk_pattern_domain` FOREIGN KEY (`domain_id`) REFERENCES `e_domains` (`domain`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""" + KEY `idx_e_pattern_forwarding_domain_id` (`domain_id`) +) ENGINE=InnoDB""" try: cursor.execute(query) except: pass + # Seed singleton row for global email settings if missing. + try: + cursor.execute("""INSERT INTO `e_server_settings` (`id`, `plus_addressing_enabled`, `plus_addressing_delimiter`) +SELECT 1, 0, '+' +WHERE NOT EXISTS (SELECT 1 FROM `e_server_settings` WHERE `id` = 1)""") + except: + pass + try: connection.close() except: diff --git a/public/static/mailServer/mailServer.js b/public/static/mailServer/mailServer.js index f46bb4d4a..e7b6fa72a 100644 --- a/public/static/mailServer/mailServer.js +++ b/public/static/mailServer/mailServer.js @@ -1345,3 +1345,488 @@ app.controller('listEmails', function ($scope, $http) { /* Java script code for List Emails Ends here */ + +/* Java script code for EmailLimitsNew */ +app.controller('EmailLimitsNew', function ($scope, $http) { + + $scope.creationBox = true; + $scope.emailDetails = true; + $scope.forwardLoading = false; + $scope.forwardError = true; + $scope.forwardSuccess = true; + $scope.couldNotConnect = true; + $scope.notifyBox = true; + + $scope.showEmailDetails = function () { + + $scope.creationBox = true; + $scope.emailDetails = true; + $scope.forwardLoading = true; + $scope.forwardError = true; + $scope.forwardSuccess = true; + $scope.couldNotConnect = true; + $scope.notifyBox = true; + + var url = "/email/getEmailsForDomain"; + + var data = { + domain: $scope.emailDomain + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + + function ListInitialDatas(response) { + + if (response.data.fetchStatus === 1) { + + $scope.emails = JSON.parse(response.data.data); + + $scope.creationBox = true; + $scope.emailDetails = false; + $scope.forwardLoading = false; + $scope.forwardError = true; + $scope.forwardSuccess = true; + $scope.couldNotConnect = true; + $scope.notifyBox = false; + + } else { + $scope.creationBox = true; + $scope.emailDetails = true; + $scope.forwardLoading = false; + $scope.forwardError = false; + $scope.forwardSuccess = true; + $scope.couldNotConnect = true; + $scope.notifyBox = false; + + $scope.errorMessage = response.data.error_message; + } + } + + function cantLoadInitialDatas() { + $scope.creationBox = true; + $scope.emailDetails = true; + $scope.forwardLoading = false; + $scope.forwardError = true; + $scope.forwardSuccess = true; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + } + }; + + $scope.selectForwardingEmail = function () { + $scope.creationBox = false; + $scope.emailDetails = false; + $scope.forwardLoading = false; + $scope.forwardError = true; + $scope.forwardSuccess = true; + $scope.couldNotConnect = true; + $scope.notifyBox = true; + }; + + $scope.fetchCurrentLimits = function () { + var url = "/email/fetchCurrentLimits"; + var data = { + emailAddress: $scope.emailAddress + }; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function (response) { + if (response.data.fetchStatus === 1) { + $scope.currentEmailLimit = response.data.emailLimit || 3072; + $scope.currentEmailAllowed = response.data.allowedPCT || 100; + $scope.newEmailLimit = $scope.currentEmailLimit; + $scope.newEmailAllowed = $scope.currentEmailAllowed; + } else { + new PNotify({ + title: 'Error!', + text: response.data.error_message || 'Could not fetch current limits.', + type: 'error' + }); + } + }, function () { + new PNotify({ + title: 'Error!', + text: 'Could not connect to server.', + type: 'error' + }); + }); + }; + + $scope.saveEmailLimits = function () { + var url = "/email/saveEmailLimits"; + var data = { + emailAddress: $scope.emailAddress, + emailLimit: $scope.newEmailLimit, + allowedPCT: $scope.newEmailAllowed + }; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function (response) { + if (response.data.saveStatus === 1) { + new PNotify({ + title: 'Success!', + text: response.data.message || 'Limits saved successfully.', + type: 'success' + }); + $scope.showEmailDetails(); + } else { + new PNotify({ + title: 'Error!', + text: response.data.error_message || 'Failed to save limits.', + type: 'error' + }); + } + }, function () { + new PNotify({ + title: 'Error!', + text: 'Could not connect to server.', + type: 'error' + }); + }); + }; +}); +/* Java script for EmailLimitsNew */ + +/* Catch-All Email Controller */ +app.controller('catchAllEmail', function ($scope, $http) { + $scope.configBox = true; + $scope.loading = false; + $scope.errorBox = true; + $scope.successBox = true; + $scope.couldNotConnect = true; + $scope.notifyBox = true; + $scope.currentConfigured = false; + $scope.enabled = true; + + $scope.fetchConfig = function () { + if (!$scope.selectedDomain) { + $scope.configBox = true; + return; + } + $scope.loading = true; + $scope.configBox = true; + $scope.notifyBox = true; + + var url = "/email/fetchCatchAllConfig"; + var data = { domain: $scope.selectedDomain }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, data, config).then(function (response) { + $scope.loading = false; + if (response.data.fetchStatus === 1) { + $scope.configBox = false; + if (response.data.configured === 1) { + $scope.currentConfigured = true; + $scope.currentDestination = response.data.destination; + $scope.currentEnabled = response.data.enabled; + $scope.destination = response.data.destination; + $scope.enabled = response.data.enabled; + } else { + $scope.currentConfigured = false; + $scope.destination = ''; + $scope.enabled = true; + } + } else { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = response.data.error_message; + } + }, function () { + $scope.loading = false; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + }); + }; + + $scope.saveConfig = function () { + if (!$scope.destination) { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = 'Please enter a destination email address'; + return; + } + + $scope.loading = true; + $scope.notifyBox = true; + + var url = "/email/saveCatchAllConfig"; + var data = { + domain: $scope.selectedDomain, + destination: $scope.destination, + enabled: $scope.enabled + }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, data, config).then(function (response) { + $scope.loading = false; + if (response.data.saveStatus === 1) { + $scope.successBox = false; + $scope.notifyBox = false; + $scope.successMessage = response.data.message; + $scope.currentConfigured = true; + $scope.currentDestination = $scope.destination; + $scope.currentEnabled = $scope.enabled; + } else { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = response.data.error_message; + } + }, function () { + $scope.loading = false; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + }); + }; + + $scope.deleteConfig = function () { + if (!confirm('Are you sure you want to remove the catch-all configuration?')) { + return; + } + + $scope.loading = true; + $scope.notifyBox = true; + + var url = "/email/deleteCatchAllConfig"; + var data = { domain: $scope.selectedDomain }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, data, config).then(function (response) { + $scope.loading = false; + if (response.data.deleteStatus === 1) { + $scope.successBox = false; + $scope.notifyBox = false; + $scope.successMessage = response.data.message; + $scope.currentConfigured = false; + $scope.destination = ''; + $scope.enabled = true; + } else { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = response.data.error_message; + } + }, function () { + $scope.loading = false; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + }); + }; +}); + +/* Plus-Addressing Controller */ +app.controller('plusAddressing', function ($scope, $http) { + $scope.loading = true; + $scope.globalEnabled = false; + $scope.delimiter = '+'; + $scope.domainEnabled = true; + $scope.globalNotifyBox = true; + $scope.globalErrorBox = true; + $scope.globalSuccessBox = true; + $scope.domainNotifyBox = true; + $scope.domainErrorBox = true; + $scope.domainSuccessBox = true; + + var url = "/email/fetchPlusAddressingConfig"; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, {}, config).then(function (response) { + $scope.loading = false; + if (response.data.fetchStatus === 1) { + $scope.globalEnabled = response.data.globalEnabled; + $scope.delimiter = response.data.delimiter || '+'; + } + }, function () { + $scope.loading = false; + }); + + $scope.saveGlobalSettings = function () { + $scope.loading = true; + $scope.globalNotifyBox = true; + + var saveUrl = "/email/savePlusAddressingGlobal"; + var data = { + enabled: $scope.globalEnabled, + delimiter: $scope.delimiter + }; + var saveConfig = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(saveUrl, data, saveConfig).then(function (response) { + $scope.loading = false; + if (response.data.saveStatus === 1) { + $scope.globalSuccessBox = false; + $scope.globalNotifyBox = false; + $scope.globalSuccessMessage = response.data.message; + } else { + $scope.globalErrorBox = false; + $scope.globalNotifyBox = false; + $scope.globalErrorMessage = response.data.error_message; + } + }, function () { + $scope.loading = false; + $scope.globalErrorBox = false; + $scope.globalNotifyBox = false; + $scope.globalErrorMessage = 'Could not connect to server'; + }); + }; + + $scope.saveDomainSettings = function () { + if (!$scope.selectedDomain) { + return; + } + + $scope.domainNotifyBox = true; + + var saveDomainUrl = "/email/savePlusAddressingDomain"; + var data = { + domain: $scope.selectedDomain, + enabled: $scope.domainEnabled + }; + var saveDomainConfig = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(saveDomainUrl, data, saveDomainConfig).then(function (response) { + if (response.data.saveStatus === 1) { + $scope.domainSuccessBox = false; + $scope.domainNotifyBox = false; + $scope.domainSuccessMessage = response.data.message; + } else { + $scope.domainErrorBox = false; + $scope.domainNotifyBox = false; + $scope.domainErrorMessage = response.data.error_message; + } + }, function () { + $scope.domainErrorBox = false; + $scope.domainNotifyBox = false; + $scope.domainErrorMessage = 'Could not connect to server'; + }); + }; +}); + +/* Pattern Forwarding Controller */ +app.controller('patternForwarding', function ($scope, $http) { + $scope.configBox = true; + $scope.loading = false; + $scope.errorBox = true; + $scope.successBox = true; + $scope.couldNotConnect = true; + $scope.notifyBox = true; + $scope.rules = []; + $scope.patternType = 'wildcard'; + $scope.priority = 100; + + $scope.fetchRules = function () { + if (!$scope.selectedDomain) { + $scope.configBox = true; + return; + } + + $scope.loading = true; + $scope.configBox = true; + $scope.notifyBox = true; + + var url = "/email/fetchPatternRules"; + var data = { domain: $scope.selectedDomain }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(url, data, config).then(function (response) { + $scope.loading = false; + if (response.data.fetchStatus === 1) { + $scope.configBox = false; + $scope.rules = response.data.rules; + } else { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = response.data.error_message; + } + }, function () { + $scope.loading = false; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + }); + }; + + $scope.createRule = function () { + if (!$scope.pattern || !$scope.destination) { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = 'Please enter both pattern and destination'; + return; + } + + $scope.loading = true; + $scope.notifyBox = true; + + var createUrl = "/email/createPatternRule"; + var data = { + domain: $scope.selectedDomain, + pattern: $scope.pattern, + destination: $scope.destination, + pattern_type: $scope.patternType, + priority: $scope.priority + }; + var createConfig = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(createUrl, data, createConfig).then(function (response) { + $scope.loading = false; + if (response.data.createStatus === 1) { + $scope.successBox = false; + $scope.notifyBox = false; + $scope.successMessage = response.data.message; + $scope.pattern = ''; + $scope.destination = ''; + $scope.fetchRules(); + } else { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = response.data.error_message; + } + }, function () { + $scope.loading = false; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + }); + }; + + $scope.deleteRule = function (ruleId) { + if (!confirm('Are you sure you want to delete this forwarding rule?')) { + return; + } + + $scope.loading = true; + $scope.notifyBox = true; + + var deleteUrl = "/email/deletePatternRule"; + var data = { ruleId: ruleId }; + var deleteConfig = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + + $http.post(deleteUrl, data, deleteConfig).then(function (response) { + $scope.loading = false; + if (response.data.deleteStatus === 1) { + $scope.successBox = false; + $scope.notifyBox = false; + $scope.successMessage = response.data.message; + $scope.fetchRules(); + } else { + $scope.errorBox = false; + $scope.notifyBox = false; + $scope.errorMessage = response.data.error_message; + } + }, function () { + $scope.loading = false; + $scope.couldNotConnect = false; + $scope.notifyBox = false; + }); + }; +});