diff --git a/deploy-email-limits-fix.sh b/deploy-email-limits-fix.sh
new file mode 100755
index 000000000..51b0f9b1e
--- /dev/null
+++ b/deploy-email-limits-fix.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+# Deploy Email Limits fix to a CyberPanel installation.
+# Copies recommended mailServer files and optionally restarts lscpd.
+#
+# Usage (run from anywhere):
+# sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh
+# sudo bash deploy-email-limits-fix.sh [REPO_DIR] [CP_DIR]
+#
+# Or from repo root: cd /home/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh
+
+set -e
+
+log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
+err() { log "ERROR: $*" >&2; }
+
+# Resolve REPO_DIR: explicit arg, then script dir, then common locations
+if [[ -n "$1" && -d "$1/mailServer" ]]; then
+ REPO_DIR="$1"
+ shift
+elif [[ -d "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/mailServer" ]]; then
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+elif [[ -d "/home/cyberpanel-repo/mailServer" ]]; then
+ REPO_DIR="/home/cyberpanel-repo"
+elif [[ -d "./mailServer" ]]; then
+ REPO_DIR="$(pwd)"
+else
+ err "Repo not found. Use: sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh"
+ err "Or: cd /path/to/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh"
+ exit 1
+fi
+
+CP_DIR="${1:-/usr/local/CyberCP}"
+RESTART_LSCPD="${RESTART_LSCPD:-1}"
+
+if [[ ! -d "$CP_DIR" ]]; then
+ err "CyberPanel directory not found: $CP_DIR"
+ exit 1
+fi
+if [[ ! -d "$REPO_DIR/mailServer" ]]; then
+ err "Repo mailServer not found in: $REPO_DIR"
+ exit 1
+fi
+
+log "REPO_DIR=$REPO_DIR"
+log "CP_DIR=$CP_DIR"
+
+FILES=(
+ "mailServer/mailserverManager.py"
+ "mailServer/templates/mailServer/EmailLimits.html"
+ "mailServer/static/mailServer/mailServer.js"
+ "mailServer/static/mailServer/emailLimitsController.js"
+)
+
+for rel in "${FILES[@]}"; do
+ src="$REPO_DIR/$rel"
+ dst="$CP_DIR/$rel"
+ if [[ ! -f "$src" ]]; then
+ err "Source missing: $src"
+ exit 1
+ fi
+ mkdir -p "$(dirname "$dst")"
+ cp -f "$src" "$dst"
+ log "Copied: $rel"
+done
+
+if [[ "$RESTART_LSCPD" =~ ^(1|yes|true)$ ]]; then
+ if systemctl is-active --quiet lscpd 2>/dev/null; then
+ log "Restarting lscpd..."
+ systemctl restart lscpd || { err "lscpd restart failed"; exit 1; }
+ log "lscpd restarted."
+ else
+ log "lscpd not running or not a systemd service; skip restart."
+ fi
+else
+ log "Skipping restart (set RESTART_LSCPD=1 to restart lscpd)."
+fi
+
+log "Deploy complete. Hard-refresh /email/EmailLimits in the browser (Ctrl+Shift+R)."
diff --git a/mailServer/mailserverManager.py b/mailServer/mailserverManager.py
index 1fd984d07..758b26904 100644
--- a/mailServer/mailserverManager.py
+++ b/mailServer/mailserverManager.py
@@ -43,6 +43,27 @@ import bcrypt
import threading as multi
import argparse
+
+def _get_email_limits_controller_js():
+ """Return EmailLimitsNew controller JS: from file or hardcoded fallback so it always works."""
+ try:
+ from django.conf import settings
+ for base in (os.path.dirname(__file__), getattr(settings, 'BASE_DIR', None)):
+ if not base:
+ continue
+ for script_path in (
+ os.path.join(base, 'static', 'mailServer', 'emailLimitsController.js'),
+ os.path.join(base, 'mailServer', 'static', 'mailServer', 'emailLimitsController.js'),
+ ):
+ if os.path.isfile(script_path):
+ with open(script_path, 'r') as f:
+ return f.read()
+ except Exception:
+ pass
+ # Hardcoded fallback so page works even when static file is missing or path wrong
+ return r"""(function(){'use strict';var app=typeof window.app!=='undefined'?window.app:angular.module('CyberCP');if(!app)return;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",data={domain:$scope.emailDomain},config={headers:{'X-CSRFToken':getCookie('csrftoken')}};$http.post(url,data,config).then(function(r){if(r.data.fetchStatus===1){$scope.emails=JSON.parse(r.data.data);$scope.creationBox=true;$scope.emailDetails=false;$scope.forwardLoading=false;$scope.notifyBox=false;}else{$scope.creationBox=true;$scope.emailDetails=true;$scope.forwardLoading=false;$scope.forwardError=false;$scope.errorMessage=r.data.error_message;}},function(){$scope.creationBox=true;$scope.emailDetails=true;$scope.couldNotConnect=false;$scope.notifyBox=false;});};$scope.selectForwardingEmail=function(){$scope.creationBox=false;$scope.emailDetails=false;$scope.forwardLoading=true;$scope.notifyBox=true;var g=$scope.selectedEmail;if($scope.emails)for(var i=0;i<$scope.emails.length;i++)if($scope.emails[i].email===g){$scope.numberofEmails=$scope.emails[i].numberofEmails;$scope.duration=$scope.emails[i].duration;break;}};$scope.SaveChanges=function(){$scope.forwardLoading=true;var url="/email/SaveEmailLimitsNew",data={numberofEmails:$scope.numberofEmails,source:$scope.selectedEmail,duration:$scope.duration},config={headers:{'X-CSRFToken':getCookie('csrftoken')}};$http.post(url,data,config).then(function(r){if(r.data.status===1){$scope.forwardLoading=false;if(typeof PNotify!=='undefined')new PNotify({title:'Success!',text:'Changes applied.',type:'success'});$scope.showEmailDetails();}else{$scope.forwardError=false;$scope.notifyBox=false;if(typeof PNotify!=='undefined')new PNotify({title:'Error!',text:r.data.error_message||'Error',type:'error'});}},function(){$scope.creationBox=true;$scope.couldNotConnect=false;});};});})();"""
+
+
class MailServerManager(multi.Thread):
def __init__(self, request = None, function = None, extraArgs = None):
@@ -177,7 +198,9 @@ class MailServerManager(multi.Thread):
userID = self.request.session['userID']
currentACL = ACLManager.loadedACL(userID)
- if ACLManager.currentContextPermission(currentACL, 'deleteEmail') == 0:
+ # Allow fetch for List Emails (deleteEmail) or Email Limits (emailForwarding)
+ if (ACLManager.currentContextPermission(currentACL, 'deleteEmail') == 0 and
+ ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0):
return ACLManager.loadErrorJson('fetchStatus', 0)
data = json.loads(self.request.body)
@@ -1971,9 +1994,13 @@ protocol sieve {
except BaseException as msg:
template = 'mailServer/EmailLimits.html'
+ # Embed controller script inline so page works (no 404, no file path issues)
+ email_limits_controller_js = _get_email_limits_controller_js()
+ # Prevent in JS from closing the HTML script tag
+ email_limits_controller_js = email_limits_controller_js.replace('', '<\\/script>')
proc = httpProc(self.request, template,
- {'websiteList': websitesName, "status": 1}, 'emailForwarding')
+ {'websiteList': websitesName, "status": 1, 'email_limits_controller_js': email_limits_controller_js}, 'emailForwarding')
return proc.render()
def SaveEmailLimitsNew(self):
diff --git a/mailServer/static/mailServer/emailLimitsController.js b/mailServer/static/mailServer/emailLimitsController.js
new file mode 100644
index 000000000..21d504cc7
--- /dev/null
+++ b/mailServer/static/mailServer/emailLimitsController.js
@@ -0,0 +1,146 @@
+/**
+ * Email Limits page controller - ensures EmailLimitsNew is registered on CyberCP
+ * even if main mailServer.js loads late or fails. Load this script in the
+ * EmailLimits template so the page works reliably.
+ */
+(function () {
+ 'use strict';
+ var app = (typeof window.app !== 'undefined') ? window.app : angular.module('CyberCP');
+ if (!app) return;
+
+ 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 = true;
+ $scope.forwardError = true;
+ $scope.forwardSuccess = true;
+ $scope.couldNotConnect = true;
+ $scope.notifyBox = true;
+
+ var givenEmail = $scope.selectedEmail;
+ if ($scope.emails) {
+ for (var i = 0; i < $scope.emails.length; i++) {
+ if ($scope.emails[i].email === givenEmail) {
+ $scope.numberofEmails = $scope.emails[i].numberofEmails;
+ $scope.duration = $scope.emails[i].duration;
+ break;
+ }
+ }
+ }
+ };
+
+ $scope.SaveChanges = function () {
+ $scope.creationBox = false;
+ $scope.emailDetails = false;
+ $scope.forwardLoading = true;
+ $scope.forwardError = true;
+ $scope.forwardSuccess = true;
+ $scope.couldNotConnect = true;
+ $scope.notifyBox = true;
+
+ var url = "/email/SaveEmailLimitsNew";
+ var data = {
+ numberofEmails: $scope.numberofEmails,
+ source: $scope.selectedEmail,
+ duration: $scope.duration
+ };
+ var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
+
+ $http.post(url, data, config).then(SaveListInitialDatas, SaveCantLoadInitialDatas);
+
+ function SaveListInitialDatas(response) {
+ if (response.data.status === 1) {
+ $scope.creationBox = false;
+ $scope.emailDetails = false;
+ $scope.forwardLoading = false;
+ $scope.forwardError = true;
+ $scope.forwardSuccess = true;
+ $scope.couldNotConnect = true;
+ $scope.notifyBox = true;
+ if (typeof PNotify === 'function') {
+ new PNotify({ title: 'Success!', text: 'Changes applied.', type: 'success' });
+ }
+ $scope.showEmailDetails();
+ } else {
+ $scope.creationBox = false;
+ $scope.emailDetails = false;
+ $scope.forwardLoading = false;
+ $scope.forwardError = false;
+ $scope.forwardSuccess = true;
+ $scope.couldNotConnect = true;
+ $scope.notifyBox = false;
+ if (typeof PNotify === 'function') {
+ new PNotify({ title: 'Error!', text: response.data.error_message || 'Error', type: 'error' });
+ }
+ }
+ }
+
+ function SaveCantLoadInitialDatas() {
+ $scope.creationBox = true;
+ $scope.emailDetails = true;
+ $scope.forwardLoading = false;
+ $scope.forwardError = true;
+ $scope.forwardSuccess = true;
+ $scope.couldNotConnect = false;
+ $scope.notifyBox = false;
+ }
+ };
+ });
+})();
diff --git a/mailServer/static/mailServer/mailServer.js b/mailServer/static/mailServer/mailServer.js
index 6b6695052..a546a8cec 100644
--- a/mailServer/static/mailServer/mailServer.js
+++ b/mailServer/static/mailServer/mailServer.js
@@ -2,6 +2,14 @@
* Created by usman on 8/15/17.
*/
+// Ensure app is available (get existing CyberCP module so controllers register correctly)
+if (typeof app === 'undefined') {
+ if (typeof window !== 'undefined' && typeof window.app !== 'undefined') {
+ app = window.app;
+ } else {
+ app = angular.module('CyberCP');
+ }
+}
/* Java script code to create account */
app.controller('createEmailAccount', function ($scope, $http) {
@@ -1506,6 +1514,7 @@ app.controller('EmailLimitsNew', function ($scope, $http) {
// Given email to search for
var givenEmail = $scope.selectedEmail;
+ if ($scope.emails) {
for (var i = 0; i < $scope.emails.length; i++) {
if ($scope.emails[i].email === givenEmail) {
// Extract numberofEmails and duration
@@ -1515,14 +1524,11 @@ app.controller('EmailLimitsNew', function ($scope, $http) {
$scope.numberofEmails = numberofEmails;
$scope.duration = duration;
- // Use numberofEmails and duration as needed
- console.log("Number of emails:", numberofEmails);
- console.log("Duration:", duration);
-
// Break out of the loop since the email is found
break;
}
}
+ }
};
diff --git a/mailServer/templates/mailServer/EmailLimits.html b/mailServer/templates/mailServer/EmailLimits.html
index 6a7364f76..1c1961c7f 100644
--- a/mailServer/templates/mailServer/EmailLimits.html
+++ b/mailServer/templates/mailServer/EmailLimits.html
@@ -1,5 +1,6 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
+{% load static %}
{% block title %}{% trans "Email Limits - CyberPanel" %}{% endblock %}
{% block content %}
@@ -387,4 +388,13 @@
+{% endblock %}
+
+{% block footer_scripts %}
+ {# EmailLimitsNew controller: inline from view (preferred) or fallback to static file #}
+ {% if email_limits_controller_js %}
+
+ {% else %}
+
+ {% endif %}
{% endblock %}
\ No newline at end of file
diff --git a/static/mailServer/emailLimitsController.js b/static/mailServer/emailLimitsController.js
new file mode 100644
index 000000000..45f6bc77b
--- /dev/null
+++ b/static/mailServer/emailLimitsController.js
@@ -0,0 +1,146 @@
+/**
+ * Email Limits page controller - ensures EmailLimitsNew is registered on CyberCP
+ * even if main mailServer.js loads late or fails. Load this script in the
+ * EmailLimits template so the page works reliably.
+ */
+(function () {
+ 'use strict';
+ var app = (typeof window.app !== 'undefined') ? window.app : angular.module('CyberCP');
+ if (!app) return;
+
+ 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 = true;
+ $scope.forwardError = true;
+ $scope.forwardSuccess = true;
+ $scope.couldNotConnect = true;
+ $scope.notifyBox = true;
+
+ var givenEmail = $scope.selectedEmail;
+ if ($scope.emails) {
+ for (var i = 0; i < $scope.emails.length; i++) {
+ if ($scope.emails[i].email === givenEmail) {
+ $scope.numberofEmails = $scope.emails[i].numberofEmails;
+ $scope.duration = $scope.emails[i].duration;
+ break;
+ }
+ }
+ }
+ };
+
+ $scope.SaveChanges = function () {
+ $scope.creationBox = false;
+ $scope.emailDetails = false;
+ $scope.forwardLoading = true;
+ $scope.forwardError = true;
+ $scope.forwardSuccess = true;
+ $scope.couldNotConnect = true;
+ $scope.notifyBox = true;
+
+ var url = "/email/SaveEmailLimitsNew";
+ var data = {
+ numberofEmails: $scope.numberofEmails,
+ source: $scope.selectedEmail,
+ duration: $scope.duration
+ };
+ var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
+
+ $http.post(url, data, config).then(SaveListInitialDatas, SaveCantLoadInitialDatas);
+
+ function SaveListInitialDatas(response) {
+ if (response.data.status === 1) {
+ $scope.creationBox = false;
+ $scope.emailDetails = false;
+ $scope.forwardLoading = false;
+ $scope.forwardError = true;
+ $scope.forwardSuccess = true;
+ $scope.couldNotConnect = true;
+ $scope.notifyBox = true;
+ if (typeof new PNotify === 'function') {
+ new PNotify({ title: 'Success!', text: 'Changes applied.', type: 'success' });
+ }
+ $scope.showEmailDetails();
+ } else {
+ $scope.creationBox = false;
+ $scope.emailDetails = false;
+ $scope.forwardLoading = false;
+ $scope.forwardError = false;
+ $scope.forwardSuccess = true;
+ $scope.couldNotConnect = true;
+ $scope.notifyBox = false;
+ if (typeof new PNotify === 'function') {
+ new PNotify({ title: 'Error!', text: response.data.error_message || 'Error', type: 'error' });
+ }
+ }
+ }
+
+ function SaveCantLoadInitialDatas() {
+ $scope.creationBox = true;
+ $scope.emailDetails = true;
+ $scope.forwardLoading = false;
+ $scope.forwardError = true;
+ $scope.forwardSuccess = true;
+ $scope.couldNotConnect = false;
+ $scope.notifyBox = false;
+ }
+ };
+ });
+})();
diff --git a/static/mailServer/mailServer.js b/static/mailServer/mailServer.js
index 6b6695052..62be0aefe 100644
--- a/static/mailServer/mailServer.js
+++ b/static/mailServer/mailServer.js
@@ -2,6 +2,14 @@
* Created by usman on 8/15/17.
*/
+// Ensure app is available (get existing CyberCP module so controllers register correctly)
+if (typeof app === 'undefined') {
+ if (typeof window !== 'undefined' && typeof window.app !== 'undefined') {
+ app = window.app;
+ } else {
+ app = angular.module('CyberCP');
+ }
+}
/* Java script code to create account */
app.controller('createEmailAccount', function ($scope, $http) {
diff --git a/to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md b/to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md
new file mode 100644
index 000000000..779548dab
--- /dev/null
+++ b/to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md
@@ -0,0 +1,57 @@
+# Email Limits Fix – Deploy Checklist
+
+Use this after pulling the Email Limits fixes in this repo so that https://your-panel/email/EmailLimits works (controller registers, email list loads, configure section works).
+
+## Files that are part of the fix
+
+| File | Purpose |
+|------|--------|
+| `mailServer/mailserverManager.py` | Passes controller JS to template; allows getEmailsForDomain for emailForwarding |
+| `mailServer/templates/mailServer/EmailLimits.html` | Inline controller in footer_scripts (no static file dependency) |
+| `mailServer/static/mailServer/mailServer.js` | EmailLimitsNew controller + guard for `$scope.emails` |
+| `mailServer/static/mailServer/emailLimitsController.js` | Standalone controller + PNotify check fix |
+
+## Option A: Deploy script (recommended)
+
+**Run from anywhere** (use the full path to the script so the shell can find it):
+
+```bash
+sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh
+```
+
+Or from repo root:
+
+```bash
+cd /home/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh
+```
+
+- Script auto-detects repo at `/home/cyberpanel-repo` if run from another directory.
+- Default CyberPanel path: `/usr/local/CyberCP`.
+- Override: `sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh /path/to/repo /usr/local/CyberCP`.
+- Skip restart: `sudo RESTART_LSCPD=0 bash /home/cyberpanel-repo/deploy-email-limits-fix.sh`.
+
+## Option B: Manual copy + restart
+
+On the server, from the repo root (e.g. `/home/cyberpanel-repo`):
+
+```bash
+CP_DIR=/usr/local/CyberCP
+
+cp -f mailServer/mailserverManager.py "$CP_DIR/mailServer/"
+cp -f mailServer/templates/mailServer/EmailLimits.html "$CP_DIR/mailServer/templates/mailServer/"
+cp -f mailServer/static/mailServer/mailServer.js "$CP_DIR/mailServer/static/mailServer/"
+cp -f mailServer/static/mailServer/emailLimitsController.js "$CP_DIR/mailServer/static/mailServer/"
+
+sudo systemctl restart lscpd
+```
+
+## After deploy
+
+1. Hard refresh the Email Limits page: **Ctrl+Shift+R** (or Cmd+Shift+R).
+2. Open **Email Limits**, choose a **website**, then check that **email account** dropdown fills and **Configure Email Limits** appears and works.
+
+## If it still fails
+
+- Confirm the four files above are present under `$CP_DIR` and were updated (check timestamps).
+- Check panel/Python logs and browser console for `[$controller:ctrlreg]` or JS errors.
+- Ensure `lscpd` (or the process serving the panel) was restarted after copying.
diff --git a/to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md b/to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md
new file mode 100644
index 000000000..70fc18670
--- /dev/null
+++ b/to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md
@@ -0,0 +1,108 @@
+# Email Limits – Live Server Checklist (vs upstream v2.4.4)
+
+## Upstream v2.4.4 behaviour
+
+In [usmannasir/cyberpanel at v2.4.4](https://github.com/usmannasir/cyberpanel/tree/v2.4.4):
+
+- **Template**: `mailServer/templates/mailServer/EmailLimits.html` exists and uses `ng-controller="EmailLimitsNew"` and `{$ … $}` bindings.
+- **Routes**: `mailServer/urls.py` has `EmailLimits` and `SaveEmailLimitsNew`.
+- **Controller**: The **`EmailLimitsNew` controller is not present** in `static/mailServer/mailServer.js`. Upstream `mailServer.js` ends at “List Emails” and has no `EmailLimitsNew` block.
+
+So on a stock v2.4.4 install, the Email Limits page will show raw `{$ selectedEmail $}` and “Could not connect to server” because the Angular controller is never registered.
+
+---
+
+## How it is loaded in v2.4.4
+
+1. **Base template** (`baseTemplate/templates/baseTemplate/index.html`) loads one script bundle:
+ - `{% static 'mailServer/mailServer.js' %}?v={{ CP_VERSION }}`
+ (in the “Additional Scripts” block at the bottom of the body.)
+
+2. **Email Limits template** only provides content; it does **not** load any extra script in upstream. It expects `EmailLimitsNew` to come from `mailServer.js`, but that controller is missing in v2.4.4.
+
+3. **Backend**: `mailServer/views.py` → `EmailLimits`, `SaveEmailLimitsNew`; `mailServer/mailserverManager.py` → `EmailLimits()`, `SaveEmailLimitsNew()`.
+
+---
+
+## Files that must be on the live server
+
+Use the paths below relative to the CyberPanel app root (e.g. `/usr/local/CyberCP/` or your repo root). Django static files may be served from `STATIC_ROOT` after `collectstatic`; templates and Python files must be in the app directories.
+
+### 1. Python / URLs / views (same as upstream + your tweaks)
+
+| Path | Purpose |
+|------|--------|
+| `mailServer/urls.py` | Must include `EmailLimits` and `SaveEmailLimitsNew` routes. |
+| `mailServer/views.py` | Must define `EmailLimits` and `SaveEmailLimitsNew` and call manager. |
+| `mailServer/mailserverManager.py` | Must implement `EmailLimits()` and `SaveEmailLimitsNew()` and render `mailServer/EmailLimits.html` with `websiteList` and `status`. |
+
+### 2. Template (must load the controller script)
+
+| Path | Purpose |
+|------|--------|
+| `mailServer/templates/mailServer/EmailLimits.html` | Must extend `baseTemplate/index.html`, contain `ng-controller="EmailLimitsNew"`, and **include the script tag** that loads `emailLimitsController.js` at the top of `{% block content %}`. |
+
+### 3. Base template (unchanged from upstream for Email Limits)
+
+| Path | Purpose |
+|------|--------|
+| `baseTemplate/templates/baseTemplate/index.html` | Must load `{% static 'mailServer/mailServer.js' %}` in the same script block as other app JS (no `load_email_limits_controller` needed). |
+
+### 4. Static files (at least one of the two options)
+
+**Option A – Use main bundle (repo’s `mailServer.js` with controller)**
+
+| Path | Purpose |
+|------|--------|
+| `static/mailServer/mailServer.js` | Must define `app` (e.g. `window.app` or `angular.module('CyberCP')`) at the top and register `app.controller('EmailLimitsNew', ...)`. |
+| `mailServer/static/mailServer/mailServer.js` | Same as above if you use app static dirs. |
+
+**Option B – Use standalone controller (recommended so it works even if `mailServer.js` is old)**
+
+| Path | Purpose |
+|------|--------|
+| `static/mailServer/emailLimitsController.js` | Standalone script that registers `EmailLimitsNew` on the CyberCP module. |
+| `mailServer/static/mailServer/emailLimitsController.js` | Same file under the app’s `static` dir. |
+
+The Email Limits template in this repo loads `emailLimitsController.js` at the top of the content block, so the controller is registered on the Email Limits page even if the live server still has an older `mailServer.js` without `EmailLimitsNew`.
+
+---
+
+## Quick verification on the live server
+
+Run from the CyberPanel app root (e.g. `/usr/local/CyberCP/`):
+
+```bash
+# 1. Template must contain the controller script and ng-controller
+grep -l "emailLimitsController.js" mailServer/templates/mailServer/EmailLimits.html && \
+grep -l "EmailLimitsNew" mailServer/templates/mailServer/EmailLimits.html && \
+echo "Template OK" || echo "Template MISSING or WRONG"
+
+# 2. Standalone controller script must exist (at least one location)
+([ -f static/mailServer/emailLimitsController.js ] || [ -f mailServer/static/mailServer/emailLimitsController.js ]) && \
+echo "emailLimitsController.js OK" || echo "emailLimitsController.js MISSING"
+
+# 3. mailServer.js (if you rely on it for Email Limits) must define EmailLimitsNew
+grep -q "EmailLimitsNew" static/mailServer/mailServer.js 2>/dev/null || grep -q "EmailLimitsNew" mailServer/static/mailServer/mailServer.js 2>/dev/null && \
+echo "mailServer.js has EmailLimitsNew" || echo "mailServer.js has NO EmailLimitsNew (use emailLimitsController.js)"
+
+# 4. Routes
+grep -q "EmailLimits" mailServer/urls.py && echo "URLs OK" || echo "URLs MISSING"
+```
+
+After deploying, run:
+
+```bash
+python3 manage.py collectstatic --noinput
+# Restart your app server (e.g. LiteSpeed / Gunicorn)
+```
+
+Then hard-refresh the Email Limits page (Ctrl+Shift+R).
+
+---
+
+## Summary
+
+- **Upstream v2.4.4**: Email Limits template and routes exist; **controller is missing** from `mailServer.js`, so the page is broken by default.
+- **This repo**: Adds `EmailLimitsNew` in `mailServer.js` and a standalone `emailLimitsController.js`, and the Email Limits template loads `emailLimitsController.js` so the page works even with an old `mailServer.js`.
+- **Live server**: Ensure the template, URLs, views, manager, base template, and either the updated `mailServer.js` or `emailLimitsController.js` (or both) are present as in this checklist, then run `collectstatic` and restart the app.