CyberPanel: fix webmail folder selection and UI assets

This commit is contained in:
master3395
2026-03-25 22:00:42 +01:00
parent b20422430f
commit e764828ac8
15 changed files with 1304 additions and 512 deletions

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

View File

@@ -270,7 +270,7 @@ p {
padding-left: 15px;
padding-right: 15px;
}
.table-responsive {
border: none;
margin-bottom: 20px;
@@ -284,19 +284,19 @@ p {
max-width: 100%;
margin-bottom: 20px;
}
/* Adjust sidebar for tablets */
#page-sidebar {
width: 100%;
position: static;
height: auto;
}
/* Make tables horizontally scrollable */
.table-responsive {
overflow-x: auto;
}
.table {
min-width: 600px;
}
@@ -307,17 +307,17 @@ p {
html {
font-size: 14px;
}
body {
font-size: 14px;
padding: 0;
}
.container, .container-fluid {
padding-left: 10px;
padding-right: 10px;
}
/* Stack all columns on mobile */
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-9, .col-sm-12,
.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-12 {
@@ -325,24 +325,24 @@ p {
max-width: 100%;
margin-bottom: 15px;
}
/* Adjust headings for mobile */
h1 {
font-size: 2rem !important; /* 32px */
}
h2 {
font-size: 1.75rem !important; /* 28px */
}
h3 {
font-size: 1.5rem !important; /* 24px */
}
h4 {
font-size: 1.25rem !important; /* 20px */
}
/* Button adjustments for mobile */
.btn {
font-size: 16px !important;
@@ -350,50 +350,50 @@ p {
width: 100%;
margin-bottom: 10px;
}
.btn-group .btn {
width: auto;
margin-bottom: 0;
}
/* Form adjustments for mobile */
.form-control, input, textarea, select {
font-size: 16px !important; /* Prevents zoom on iOS */
padding: 14px 16px !important;
width: 100%;
}
/* Table adjustments for mobile */
.table {
font-size: 14px !important;
}
.table th, .table td {
padding: 8px 6px !important;
font-size: 13px !important;
}
/* Hide less important columns on mobile */
.table .d-none-mobile {
display: none !important;
}
/* Modal adjustments for mobile */
.modal-dialog {
margin: 10px;
width: calc(100% - 20px);
}
.modal-content {
padding: 20px 15px;
}
/* Content box adjustments */
.content-box, .panel, .card {
padding: 15px;
margin-bottom: 15px;
}
/* Sidebar adjustments for mobile */
#page-sidebar {
position: fixed;
@@ -406,20 +406,20 @@ p {
background-color: #ffffff;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
}
#page-sidebar.show {
left: 0;
}
/* Main content adjustments when sidebar is open */
#main-content {
transition: margin-left 0.3s ease;
}
#main-content.sidebar-open {
margin-left: 280px;
}
/* Mobile menu toggle */
.mobile-menu-toggle {
display: block;
@@ -442,29 +442,29 @@ p {
html {
font-size: 14px;
}
.container, .container-fluid {
padding-left: 8px;
padding-right: 8px;
}
/* Even smaller buttons and forms for very small screens */
.btn {
font-size: 14px !important;
padding: 12px 16px !important;
}
.form-control, input, textarea, select {
font-size: 16px !important; /* Still 16px to prevent zoom */
padding: 12px 14px !important;
}
/* Compact table for very small screens */
.table th, .table td {
padding: 6px 4px !important;
font-size: 12px !important;
}
/* Hide even more columns on very small screens */
.table .d-none-mobile-sm {
display: none !important;
@@ -577,12 +577,12 @@ span, div, p, label, td, th {
color: #000000 !important;
background: #ffffff !important;
}
.table th, .table td {
font-size: 10pt !important;
color: #000000 !important;
}
.btn, .alert, .modal {
display: none !important;
}

View File

@@ -216,7 +216,7 @@ span, div, p, label, td, th, a, li {
.container-fluid {
color: var(--text-primary) !important;
}
/* Fix mobile menu text */
.mobile-menu .menu-item,
.mobile-menu .menu-item span {
@@ -233,7 +233,7 @@ span, div, p, label, td, th, a, li {
color: #000000 !important;
background: #ffffff !important;
}
.text-muted,
.text-secondary {
color: #000000 !important;
@@ -247,7 +247,7 @@ span, div, p, label, td, th, a, li {
--text-secondary: #000000;
--text-heading: #000000;
}
[data-theme="dark"] {
--text-primary: #ffffff;
--text-secondary: #ffffff;

File diff suppressed because it is too large Load Diff

View File

@@ -2032,10 +2032,18 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
var comodoInstalled = false;
var counterOWASP = 0;
var counterComodo = 0;
var updatingOWASPStatus = false;
var updatingComodoStatus = false;
$('#owaspInstalled').change(function () {
// Prevent triggering installation when status check updates the toggle
if (updatingOWASPStatus) {
counterOWASP = counterOWASP + 1; // Still increment counter
return;
}
owaspInstalled = $(this).prop('checked');
$scope.ruleFiles = true;
@@ -2052,6 +2060,12 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
$('#comodoInstalled').change(function () {
// Prevent triggering installation when status check updates the toggle
if (updatingComodoStatus) {
counterComodo = counterComodo + 1; // Still increment counter
return;
}
$scope.ruleFiles = true;
comodoInstalled = $(this).prop('checked');
@@ -2070,9 +2084,12 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
getOWASPAndComodoStatus(true);
function getOWASPAndComodoStatus(updateToggle) {
function getOWASPAndComodoStatus(updateToggle, showLoader) {
$scope.modsecLoading = false;
// Only show loader if explicitly requested (during installations)
if (showLoader === true) {
$scope.modsecLoading = false;
}
url = "/firewall/getOWASPAndComodoStatus";
@@ -2097,6 +2114,10 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
if (updateToggle === true) {
// Set flags to prevent change event from triggering installation
updatingOWASPStatus = true;
updatingComodoStatus = true;
if (response.data.owaspInstalled === 1) {
$('#owaspInstalled').prop('checked', true);
$scope.owaspDisable = false;
@@ -2115,6 +2136,7 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
$scope.comodoDisable = true;
comodoInstalled = false;
}
} else {
if (response.data.owaspInstalled === 1) {
@@ -2135,10 +2157,19 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
}
// Always reset flags after status check completes
$timeout(function() {
updatingOWASPStatus = false;
updatingComodoStatus = false;
}, 100);
}
function cantLoadInitialDatas(response) {
$scope.modsecLoading = true;
// Reset flags even on error
updatingOWASPStatus = false;
updatingComodoStatus = false;
}
}
@@ -2180,7 +2211,10 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
$scope.installationFailed = true;
$scope.installationSuccess = false;
getOWASPAndComodoStatus(false);
// Update toggle state after a short delay to reflect installation result
$timeout(function() {
getOWASPAndComodoStatus(true);
}, 500);
} else {
$scope.modsecLoading = true;
@@ -2193,6 +2227,11 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
$scope.installationSuccess = true;
$scope.errorMessage = response.data.error_message;
// Update toggle to reflect failed installation (will show OFF)
$timeout(function() {
getOWASPAndComodoStatus(true);
}, 500);
}
}

View File

@@ -118,8 +118,8 @@ class ServerStatusUtil(multi.Thread):
if ServerStatusUtil.executioner(command, statusFile) == 0:
return 0
if os.path.exists('/usr/local/CyberCP/lsws-6.0/'):
shutil.rmtree('/usr/local/CyberCP/lsws-6.0')
if os.path.exists('/usr/local/CyberCP/lsws-6.3.4/'):
shutil.rmtree('/usr/local/CyberCP/lsws-6.3.4')
if os.path.exists(f'/usr/local/CyberCP/lsws-{lsws_version}/'):
shutil.rmtree(f'/usr/local/CyberCP/lsws-{lsws_version}/')

File diff suppressed because it is too large Load Diff

View File

@@ -2713,4 +2713,3 @@ app.controller('manageImages', function ($scope, $http) {
})
}
});

View File

@@ -2032,10 +2032,18 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
var comodoInstalled = false;
var counterOWASP = 0;
var counterComodo = 0;
var updatingOWASPStatus = false;
var updatingComodoStatus = false;
$('#owaspInstalled').change(function () {
// Prevent triggering installation when status check updates the toggle
if (updatingOWASPStatus) {
counterOWASP = counterOWASP + 1; // Still increment counter
return;
}
owaspInstalled = $(this).prop('checked');
$scope.ruleFiles = true;
@@ -2052,6 +2060,12 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
$('#comodoInstalled').change(function () {
// Prevent triggering installation when status check updates the toggle
if (updatingComodoStatus) {
counterComodo = counterComodo + 1; // Still increment counter
return;
}
$scope.ruleFiles = true;
comodoInstalled = $(this).prop('checked');
@@ -2070,9 +2084,12 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
getOWASPAndComodoStatus(true);
function getOWASPAndComodoStatus(updateToggle) {
function getOWASPAndComodoStatus(updateToggle, showLoader) {
$scope.modsecLoading = false;
// Only show loader if explicitly requested (during installations)
if (showLoader === true) {
$scope.modsecLoading = false;
}
url = "/firewall/getOWASPAndComodoStatus";
@@ -2097,6 +2114,10 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
if (updateToggle === true) {
// Set flags to prevent change event from triggering installation
updatingOWASPStatus = true;
updatingComodoStatus = true;
if (response.data.owaspInstalled === 1) {
$('#owaspInstalled').prop('checked', true);
$scope.owaspDisable = false;
@@ -2115,6 +2136,7 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
$scope.comodoDisable = true;
comodoInstalled = false;
}
} else {
if (response.data.owaspInstalled === 1) {
@@ -2135,10 +2157,19 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
}
// Always reset flags after status check completes
$timeout(function() {
updatingOWASPStatus = false;
updatingComodoStatus = false;
}, 100);
}
function cantLoadInitialDatas(response) {
$scope.modsecLoading = true;
// Reset flags even on error
updatingOWASPStatus = false;
updatingComodoStatus = false;
}
}
@@ -2180,7 +2211,10 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
$scope.installationFailed = true;
$scope.installationSuccess = false;
getOWASPAndComodoStatus(false);
// Update toggle state after a short delay to reflect installation result
$timeout(function() {
getOWASPAndComodoStatus(true);
}, 500);
} else {
$scope.modsecLoading = true;
@@ -2193,6 +2227,11 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) {
$scope.installationSuccess = true;
$scope.errorMessage = response.data.error_message;
// Update toggle to reflect failed installation (will show OFF)
$timeout(function() {
getOWASPAndComodoStatus(true);
}, 500);
}
}

View File

@@ -10,6 +10,7 @@ class CyberPanelWebAuthn {
this.apiEndpoints = {
registrationStart: '/webauthn/registration/start/',
registrationComplete: '/webauthn/registration/complete/',
authenticationOptions: '/webauthn/authentication/options/',
authenticationStart: '/webauthn/authentication/start/',
authenticationComplete: '/webauthn/authentication/complete/',
credentialsList: '/webauthn/credentials/',
@@ -60,18 +61,10 @@ class CyberPanelWebAuthn {
addLoginButtons() {
const loginForm = document.querySelector('#loginForm');
if (!loginForm) return;
// Add WebAuthn login button
const webauthnButton = document.createElement('button');
webauthnButton.type = 'button';
webauthnButton.className = 'btn btn-primary btn-block';
webauthnButton.innerHTML = '<i class="fas fa-fingerprint"></i> Login with Passkey';
webauthnButton.onclick = () => this.startPasswordlessLogin();
// Insert after password field
const passwordField = loginForm.querySelector('input[type="password"]');
if (passwordField) {
passwordField.parentNode.insertBefore(webauthnButton, passwordField.parentNode.nextSibling);
const existingBtn = document.getElementById('webauthn-login-btn');
if (existingBtn && !existingBtn.dataset.bound) {
existingBtn.dataset.bound = '1';
existingBtn.onclick = () => this.startPasskeyFirstLogin();
}
}
@@ -80,6 +73,76 @@ class CyberPanelWebAuthn {
// Implementation depends on the specific UI structure
}
arrayBufferToBase64url(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
base64urlToArrayBuffer(str) {
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
const pad = (4 - (base64.length % 4)) % 4;
for (let i = 0; i < pad; i++) base64 += '=';
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes.buffer;
}
async startPasskeyFirstLogin() {
try {
this.showLoading('Signing in with passkey...');
const optsUrl = this.apiEndpoints.authenticationOptions + '?return=' + encodeURIComponent(window.location.pathname || '/');
const optsResponse = await fetch(optsUrl, { method: 'GET', credentials: 'same-origin' });
const optsData = await optsResponse.json();
if (!optsData.publicKey) {
throw new Error(optsData.error || 'Failed to get options');
}
const publicKey = optsData.publicKey;
publicKey.challenge = this.base64urlToArrayBuffer(publicKey.challenge);
if (publicKey.allowCredentials && publicKey.allowCredentials.length) {
publicKey.allowCredentials = publicKey.allowCredentials.map(function(c) {
return {
type: c.type || 'public-key',
id: typeof c.id === 'string' ? this.base64urlToArrayBuffer(c.id) : c.id,
transports: c.transports
};
}.bind(this));
}
const credential = await navigator.credentials.get({ publicKey });
if (!credential) throw new Error('No credential');
const credentialJson = {
id: credential.id,
rawId: this.arrayBufferToBase64url(credential.rawId),
type: credential.type,
response: {
clientDataJSON: this.arrayBufferToBase64url(credential.response.clientDataJSON),
authenticatorData: this.arrayBufferToBase64url(credential.response.authenticatorData),
signature: this.arrayBufferToBase64url(credential.response.signature),
userHandle: credential.response.userHandle ? this.arrayBufferToBase64url(credential.response.userHandle) : null
}
};
const authResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationComplete, {
credential: credentialJson
});
if (authResponse.success && authResponse.redirect) {
window.location.href = authResponse.redirect;
return;
}
throw new Error(authResponse.error || 'Verification failed');
} catch (error) {
if (error.name === 'NotAllowedError' || (error.message && (error.message.indexOf('cancel') !== -1 || error.message.indexOf('timed out') !== -1))) {
this.hideLoading();
return;
}
console.error('WebAuthn passkey-first error:', error);
this.showError(error.message || 'Passkey sign-in failed');
} finally {
this.hideLoading();
}
}
async startPasswordlessLogin() {
try {
const username = document.querySelector('input[name="username"]').value;
@@ -87,27 +150,13 @@ class CyberPanelWebAuthn {
this.showError('Please enter your username first');
return;
}
this.showLoading('Starting passkey authentication...');
// Get authentication challenge
const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationStart, {
username: username
});
const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationStart, { username: username });
if (!challengeResponse.success) {
throw new Error(challengeResponse.error || 'Failed to start authentication');
}
// Convert challenge to proper format
const challenge = this.convertChallenge(challengeResponse.challenge);
// Get credential
const credential = await navigator.credentials.get({
publicKey: challenge
});
// Complete authentication
const credential = await navigator.credentials.get({ publicKey: challenge });
const authResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationComplete, {
challenge_id: challengeResponse.challenge_id,
credential: {
@@ -117,19 +166,14 @@ class CyberPanelWebAuthn {
client_data_json: this.arrayBufferToBase64(credential.response.clientDataJSON),
authenticator_data: this.arrayBufferToBase64(credential.response.authenticatorData),
signature: this.arrayBufferToBase64(credential.response.signature),
user_handle: credential.response.userHandle ?
this.arrayBufferToBase64(credential.response.userHandle) : null
user_handle: credential.response.userHandle ? this.arrayBufferToBase64(credential.response.userHandle) : null
});
if (authResponse.success) {
this.showSuccess('Authentication successful! Redirecting...');
setTimeout(() => {
window.location.href = '/';
}, 1000);
setTimeout(() => { window.location.href = authResponse.redirect || '/'; }, 1000);
} else {
throw new Error(authResponse.error || 'Authentication failed');
}
} catch (error) {
console.error('WebAuthn authentication error:', error);
this.showError(error.message || 'Authentication failed');
@@ -138,9 +182,10 @@ class CyberPanelWebAuthn {
}
}
async registerPasskey(username, credentialName = '') {
async registerPasskey(username, credentialName = '', options = {}) {
const silent = options && options.silent === true;
try {
this.showLoading('Starting passkey registration...');
if (!silent) this.showLoading('Starting passkey registration...');
// Get registration challenge
const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.registrationStart, {
@@ -172,7 +217,7 @@ class CyberPanelWebAuthn {
});
if (regResponse.success) {
this.showSuccess('Passkey registered successfully!');
if (!silent) this.showSuccess('Passkey registered successfully!');
return regResponse;
} else {
throw new Error(regResponse.error || 'Registration failed');
@@ -180,10 +225,10 @@ class CyberPanelWebAuthn {
} catch (error) {
console.error('WebAuthn registration error:', error);
this.showError(error.message || 'Registration failed');
if (!silent) this.showError(error.message || 'Registration failed');
throw error;
} finally {
this.hideLoading();
if (!silent) this.hideLoading();
}
}
@@ -265,23 +310,25 @@ class CyberPanelWebAuthn {
}
convertChallenge(challenge) {
// Convert base64 challenge to ArrayBuffer
const challengeBytes = this.base64ToArrayBuffer(challenge.challenge);
const ch = challenge.challenge;
const challengeBytes = (typeof ch === 'string' && (ch.indexOf('-') !== -1 || ch.indexOf('_') !== -1))
? this.base64urlToArrayBuffer(ch) : this.base64ToArrayBuffer(ch);
const userId = challenge.user && challenge.user.id;
const userIdBuf = !userId ? undefined : (typeof userId === 'string' && (userId.indexOf('-') !== -1 || userId.indexOf('_') !== -1)
? this.base64urlToArrayBuffer(userId) : this.base64ToArrayBuffer(userId));
return {
...challenge,
challenge: challengeBytes,
user: {
...challenge.user,
id: this.base64ToArrayBuffer(challenge.user.id)
},
user: challenge.user ? { ...challenge.user, id: userIdBuf } : undefined,
excludeCredentials: challenge.excludeCredentials?.map(cred => ({
...cred,
id: this.base64ToArrayBuffer(cred.id)
id: typeof cred.id === 'string' && (cred.id.indexOf('-') !== -1 || cred.id.indexOf('_') !== -1)
? this.base64urlToArrayBuffer(cred.id) : this.base64ToArrayBuffer(cred.id)
})) || [],
allowCredentials: challenge.allowCredentials?.map(cred => ({
...cred,
id: this.base64ToArrayBuffer(cred.id)
id: typeof cred.id === 'string' && (cred.id.indexOf('-') !== -1 || cred.id.indexOf('_') !== -1)
? this.base64urlToArrayBuffer(cred.id) : this.base64ToArrayBuffer(cred.id)
})) || []
};
}
@@ -383,12 +430,17 @@ class CyberPanelWebAuthn {
}
}
// Initialize WebAuthn when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Initialize WebAuthn - run now if DOM ready, else on DOMContentLoaded (script often loads after DOM is ready)
function initCyberPanelWebAuthn() {
if (CyberPanelWebAuthn.isSupported()) {
window.cyberPanelWebAuthn = new CyberPanelWebAuthn();
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCyberPanelWebAuthn);
} else {
initCyberPanelWebAuthn();
}
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {

View File

@@ -1619,3 +1619,341 @@ app.controller('EmailLimitsNew', function ($scope, $http) {
});
/* 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 (response) {
$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 (response) {
$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 (response) {
$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;
// Fetch global settings on load
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 (response) {
$scope.loading = false;
});
$scope.saveGlobalSettings = function () {
$scope.loading = true;
$scope.globalNotifyBox = true;
var url = "/email/savePlusAddressingGlobal";
var data = {
enabled: $scope.globalEnabled,
delimiter: $scope.delimiter
};
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).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 (response) {
$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 url = "/email/savePlusAddressingDomain";
var data = {
domain: $scope.selectedDomain,
enabled: $scope.domainEnabled
};
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).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 (response) {
$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 (response) {
$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 url = "/email/createPatternRule";
var data = {
domain: $scope.selectedDomain,
pattern: $scope.pattern,
destination: $scope.destination,
pattern_type: $scope.patternType,
priority: $scope.priority
};
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).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 (response) {
$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 url = "/email/deletePatternRule";
var data = { ruleId: ruleId };
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.fetchRules();
} else {
$scope.errorBox = false;
$scope.notifyBox = false;
$scope.errorMessage = response.data.error_message;
}
}, function (response) {
$scope.loading = false;
$scope.couldNotConnect = false;
$scope.notifyBox = false;
});
};
});

View File

@@ -64,7 +64,15 @@ app.controller('createPackage', function ($scope, $http) {
dataBases: dataBases,
emails: emails,
allowedDomains: $scope.allowedDomains,
enforceDiskLimits: $scope.enforceDiskLimits
enforceDiskLimits: $scope.enforceDiskLimits,
// Resource Limits
memoryLimitMB: $scope.memoryLimitMB || 1024,
cpuCores: $scope.cpuCores || 1,
ioLimitMBPS: $scope.ioLimitMBPS || 10,
inodeLimit: $scope.inodeLimit || 400000,
maxConnections: $scope.maxConnections || 10,
procSoftLimit: $scope.procSoftLimit || 400,
procHardLimit: $scope.procHardLimit || 500
};
var config = {
@@ -236,6 +244,15 @@ app.controller('modifyPackages', function ($scope, $http) {
$scope.allowFullDomain = response.data.allowFullDomain === 1;
$scope.enforceDiskLimits = response.data.enforceDiskLimits === 1;
// Load resource limits
$scope.memoryLimitMB = response.data.memoryLimitMB || 1024;
$scope.cpuCores = response.data.cpuCores || 1;
$scope.ioLimitMBPS = response.data.ioLimitMBPS || 10;
$scope.inodeLimit = response.data.inodeLimit || 400000;
$scope.maxConnections = response.data.maxConnections || 10;
$scope.procSoftLimit = response.data.procSoftLimit || 400;
$scope.procHardLimit = response.data.procHardLimit || 500;
$scope.modifyButton = "Save Details";
$("#packageDetailsToBeModified").fadeIn();
@@ -283,6 +300,14 @@ app.controller('modifyPackages', function ($scope, $http) {
allowedDomains: $scope.allowedDomains,
allowFullDomain: $scope.allowFullDomain,
enforceDiskLimits: $scope.enforceDiskLimits,
// Resource Limits
memoryLimitMB: $scope.memoryLimitMB || 1024,
cpuCores: $scope.cpuCores || 1,
ioLimitMBPS: $scope.ioLimitMBPS || 10,
inodeLimit: $scope.inodeLimit || 400000,
maxConnections: $scope.maxConnections || 10,
procSoftLimit: $scope.procSoftLimit || 400,
procHardLimit: $scope.procHardLimit || 500
};
var config = {

View File

@@ -215,6 +215,58 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', function($
apiCall('/webmail/api/listFolders', {}, function(data) {
if (data.status === 1) {
$scope.folders = data.folders;
// Pick a sane default folder.
// Some Dovecot setups may not expose a real "INBOX" mailbox (messages live under "INBOX.*").
// The UI previously hardcoded currentFolder='INBOX', which caused "No messages" even when mail exists.
var chooseDefaultFolder = function(folders) {
if (!folders || folders.length === 0) return 'INBOX';
// 1) Prefer exact INBOX if it has messages; otherwise some servers store mail only under INBOX.*.
var inbox = null;
for (var i = 0; i < folders.length; i++) {
if (folders[i] && folders[i].name === 'INBOX') {
inbox = folders[i];
break;
}
}
if (inbox) {
var inboxUnread = parseInt((inbox.unread_count) ? inbox.unread_count : 0, 10);
var inboxTotal = parseInt((inbox.total_count) ? inbox.total_count : 0, 10);
if (inboxUnread > 0 || inboxTotal > 0) {
return inbox.name;
}
}
// 2) Prefer the folder with most unread messages.
var best = null;
var bestUnread = -1;
for (var j = 0; j < folders.length; j++) {
var u = parseInt((folders[j] && folders[j].unread_count) ? folders[j].unread_count : 0, 10);
if (u > bestUnread) {
bestUnread = u;
best = folders[j];
}
}
if (best && bestUnread > 0) {
return best.name;
}
// 3) Otherwise, pick the folder with most total messages.
best = null;
var bestTotal = -1;
for (var k = 0; k < folders.length; k++) {
var t = parseInt((folders[k] && folders[k].total_count) ? folders[k].total_count : 0, 10);
if (t > bestTotal) {
bestTotal = t;
best = folders[k];
}
}
return (best && best.name) ? best.name : (folders[0].name || 'INBOX');
};
$scope.currentFolder = chooseDefaultFolder($scope.folders);
$scope.currentPage = 1;
$scope.loadMessages();
} else {
notify(data.error_message || 'Failed to load folders.', 'error');