mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2025-12-16 05:19:43 +01:00
Add advanced email filtering features: catch-all, plus-addressing, and pattern forwarding
Features: - Catch-All Email: Forward unmatched emails for a domain to a single address - Plus-Addressing: Enable user+tag@domain.com delivery with configurable delimiter - Pattern Forwarding: Wildcard and regex-based email forwarding rules Implementation: - New database models: CatchAllEmail, EmailServerSettings, PlusAddressingOverride, PatternForwarding - New UI pages with AngularJS controllers - Backend methods in mailserverManager.py with ACL permission checks - Auto-generates /etc/postfix/virtual_regexp for pattern rules - Menu items added under Email section
This commit is contained in:
@@ -1573,6 +1573,21 @@
|
|||||||
<span>Email Forwarding</span>
|
<span>Email Forwarding</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if admin or emailForwarding %}
|
||||||
|
<a href="{% url 'catchAllEmail' %}" class="menu-item">
|
||||||
|
<span>Catch-All Email</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if admin or emailForwarding %}
|
||||||
|
<a href="{% url 'patternForwarding' %}" class="menu-item">
|
||||||
|
<span>Pattern Forwarding</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if admin %}
|
||||||
|
<a href="{% url 'plusAddressingSettings' %}" class="menu-item">
|
||||||
|
<span>Plus-Addressing</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if admin or changeEmailPassword %}
|
{% if admin or changeEmailPassword %}
|
||||||
<a href="{% url 'changeEmailAccountPassword' %}" class="menu-item">
|
<a href="{% url 'changeEmailAccountPassword' %}" class="menu-item">
|
||||||
<span>Change Password</span>
|
<span>Change Password</span>
|
||||||
|
|||||||
@@ -30,13 +30,14 @@ import _thread
|
|||||||
try:
|
try:
|
||||||
from dns.models import Domains as dnsDomains
|
from dns.models import Domains as dnsDomains
|
||||||
from dns.models import Records as dnsRecords
|
from dns.models import Records as dnsRecords
|
||||||
from mailServer.models import Forwardings, Pipeprograms
|
from mailServer.models import Forwardings, Pipeprograms, CatchAllEmail, EmailServerSettings, PlusAddressingOverride, PatternForwarding
|
||||||
from plogical.acl import ACLManager
|
from plogical.acl import ACLManager
|
||||||
from plogical.dnsUtilities import DNS
|
from plogical.dnsUtilities import DNS
|
||||||
from loginSystem.models import Administrator
|
from loginSystem.models import Administrator
|
||||||
from websiteFunctions.models import Websites
|
from websiteFunctions.models import Websites
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
import re
|
||||||
import os
|
import os
|
||||||
from plogical.processUtilities import ProcessUtilities
|
from plogical.processUtilities import ProcessUtilities
|
||||||
import bcrypt
|
import bcrypt
|
||||||
@@ -2001,6 +2002,559 @@ protocol sieve {
|
|||||||
json_data = json.dumps(data_ret)
|
json_data = json.dumps(data_ret)
|
||||||
return HttpResponse(json_data)
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
## Catch-All Email Methods
|
||||||
|
|
||||||
|
def catchAllEmail(self):
|
||||||
|
userID = self.request.session['userID']
|
||||||
|
currentACL = ACLManager.loadedACL(userID)
|
||||||
|
|
||||||
|
if not os.path.exists('/home/cyberpanel/postfix'):
|
||||||
|
proc = httpProc(self.request, 'mailServer/catchAllEmail.html',
|
||||||
|
{"status": 0}, 'emailForwarding')
|
||||||
|
return proc.render()
|
||||||
|
|
||||||
|
websitesName = ACLManager.findAllSites(currentACL, userID)
|
||||||
|
websitesName = websitesName + ACLManager.findChildDomains(websitesName)
|
||||||
|
|
||||||
|
proc = httpProc(self.request, 'mailServer/catchAllEmail.html',
|
||||||
|
{'websiteList': websitesName, "status": 1}, 'emailForwarding')
|
||||||
|
return proc.render()
|
||||||
|
|
||||||
|
def fetchCatchAllConfig(self):
|
||||||
|
try:
|
||||||
|
userID = self.request.session['userID']
|
||||||
|
currentACL = ACLManager.loadedACL(userID)
|
||||||
|
|
||||||
|
if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0:
|
||||||
|
return ACLManager.loadErrorJson('fetchStatus', 0)
|
||||||
|
|
||||||
|
data = json.loads(self.request.body)
|
||||||
|
domain = data['domain']
|
||||||
|
|
||||||
|
admin = Administrator.objects.get(pk=userID)
|
||||||
|
if ACLManager.checkOwnership(domain, admin, currentACL) == 1:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return ACLManager.loadErrorJson()
|
||||||
|
|
||||||
|
try:
|
||||||
|
domainObj = Domains.objects.get(domain=domain)
|
||||||
|
catchAll = CatchAllEmail.objects.get(domain=domainObj)
|
||||||
|
data_ret = {
|
||||||
|
'status': 1,
|
||||||
|
'fetchStatus': 1,
|
||||||
|
'configured': 1,
|
||||||
|
'destination': catchAll.destination,
|
||||||
|
'enabled': catchAll.enabled
|
||||||
|
}
|
||||||
|
except CatchAllEmail.DoesNotExist:
|
||||||
|
data_ret = {
|
||||||
|
'status': 1,
|
||||||
|
'fetchStatus': 1,
|
||||||
|
'configured': 0
|
||||||
|
}
|
||||||
|
except Domains.DoesNotExist:
|
||||||
|
data_ret = {
|
||||||
|
'status': 0,
|
||||||
|
'fetchStatus': 0,
|
||||||
|
'error_message': 'Domain not found in email system'
|
||||||
|
}
|
||||||
|
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
except BaseException as msg:
|
||||||
|
data_ret = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def saveCatchAllConfig(self):
|
||||||
|
try:
|
||||||
|
userID = self.request.session['userID']
|
||||||
|
currentACL = ACLManager.loadedACL(userID)
|
||||||
|
|
||||||
|
if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0:
|
||||||
|
return ACLManager.loadErrorJson('saveStatus', 0)
|
||||||
|
|
||||||
|
data = json.loads(self.request.body)
|
||||||
|
domain = data['domain']
|
||||||
|
destination = data['destination']
|
||||||
|
enabled = data.get('enabled', True)
|
||||||
|
|
||||||
|
admin = Administrator.objects.get(pk=userID)
|
||||||
|
if ACLManager.checkOwnership(domain, admin, currentACL) == 1:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return ACLManager.loadErrorJson()
|
||||||
|
|
||||||
|
# Validate destination email
|
||||||
|
if '@' not in destination:
|
||||||
|
data_ret = {'status': 0, 'saveStatus': 0, 'error_message': 'Invalid destination email address'}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
domainObj = Domains.objects.get(domain=domain)
|
||||||
|
|
||||||
|
# Create or update catch-all config
|
||||||
|
catchAll, created = CatchAllEmail.objects.update_or_create(
|
||||||
|
domain=domainObj,
|
||||||
|
defaults={'destination': destination, 'enabled': enabled}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Also add/update entry in Forwardings table for Postfix
|
||||||
|
catchAllSource = '@' + domain
|
||||||
|
if enabled:
|
||||||
|
# Remove existing catch-all forwarding if any
|
||||||
|
Forwardings.objects.filter(source=catchAllSource).delete()
|
||||||
|
# Add new forwarding
|
||||||
|
forwarding = Forwardings(source=catchAllSource, destination=destination)
|
||||||
|
forwarding.save()
|
||||||
|
else:
|
||||||
|
# Remove catch-all forwarding when disabled
|
||||||
|
Forwardings.objects.filter(source=catchAllSource).delete()
|
||||||
|
|
||||||
|
data_ret = {
|
||||||
|
'status': 1,
|
||||||
|
'saveStatus': 1,
|
||||||
|
'message': 'Catch-all email configured successfully'
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
except BaseException as msg:
|
||||||
|
data_ret = {'status': 0, 'saveStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def deleteCatchAllConfig(self):
|
||||||
|
try:
|
||||||
|
userID = self.request.session['userID']
|
||||||
|
currentACL = ACLManager.loadedACL(userID)
|
||||||
|
|
||||||
|
if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0:
|
||||||
|
return ACLManager.loadErrorJson('deleteStatus', 0)
|
||||||
|
|
||||||
|
data = json.loads(self.request.body)
|
||||||
|
domain = data['domain']
|
||||||
|
|
||||||
|
admin = Administrator.objects.get(pk=userID)
|
||||||
|
if ACLManager.checkOwnership(domain, admin, currentACL) == 1:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return ACLManager.loadErrorJson()
|
||||||
|
|
||||||
|
domainObj = Domains.objects.get(domain=domain)
|
||||||
|
|
||||||
|
# Delete catch-all config
|
||||||
|
CatchAllEmail.objects.filter(domain=domainObj).delete()
|
||||||
|
|
||||||
|
# Remove from Forwardings table
|
||||||
|
catchAllSource = '@' + domain
|
||||||
|
Forwardings.objects.filter(source=catchAllSource).delete()
|
||||||
|
|
||||||
|
data_ret = {
|
||||||
|
'status': 1,
|
||||||
|
'deleteStatus': 1,
|
||||||
|
'message': 'Catch-all email removed successfully'
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
except BaseException as msg:
|
||||||
|
data_ret = {'status': 0, 'deleteStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
## Plus-Addressing Methods
|
||||||
|
|
||||||
|
def plusAddressingSettings(self):
|
||||||
|
userID = self.request.session['userID']
|
||||||
|
currentACL = ACLManager.loadedACL(userID)
|
||||||
|
|
||||||
|
if not os.path.exists('/home/cyberpanel/postfix'):
|
||||||
|
proc = httpProc(self.request, 'mailServer/plusAddressingSettings.html',
|
||||||
|
{"status": 0}, 'admin')
|
||||||
|
return proc.render()
|
||||||
|
|
||||||
|
websitesName = ACLManager.findAllSites(currentACL, userID)
|
||||||
|
websitesName = websitesName + ACLManager.findChildDomains(websitesName)
|
||||||
|
|
||||||
|
proc = httpProc(self.request, 'mailServer/plusAddressingSettings.html',
|
||||||
|
{'websiteList': websitesName, "status": 1, 'admin': currentACL['admin']}, 'admin')
|
||||||
|
return proc.render()
|
||||||
|
|
||||||
|
def fetchPlusAddressingConfig(self):
|
||||||
|
try:
|
||||||
|
userID = self.request.session['userID']
|
||||||
|
currentACL = ACLManager.loadedACL(userID)
|
||||||
|
|
||||||
|
# Get global settings
|
||||||
|
settings = EmailServerSettings.get_settings()
|
||||||
|
|
||||||
|
# Check if plus-addressing is enabled in Postfix
|
||||||
|
postfixEnabled = False
|
||||||
|
try:
|
||||||
|
mainCfPath = '/etc/postfix/main.cf'
|
||||||
|
if os.path.exists(mainCfPath):
|
||||||
|
with open(mainCfPath, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
if 'recipient_delimiter' in content:
|
||||||
|
postfixEnabled = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
data_ret = {
|
||||||
|
'status': 1,
|
||||||
|
'fetchStatus': 1,
|
||||||
|
'globalEnabled': settings.plus_addressing_enabled,
|
||||||
|
'delimiter': settings.plus_addressing_delimiter,
|
||||||
|
'postfixEnabled': postfixEnabled
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
except BaseException as msg:
|
||||||
|
data_ret = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def savePlusAddressingGlobal(self):
|
||||||
|
try:
|
||||||
|
userID = self.request.session['userID']
|
||||||
|
currentACL = ACLManager.loadedACL(userID)
|
||||||
|
|
||||||
|
# Admin only
|
||||||
|
if currentACL['admin'] != 1:
|
||||||
|
return ACLManager.loadErrorJson('saveStatus', 0)
|
||||||
|
|
||||||
|
data = json.loads(self.request.body)
|
||||||
|
enabled = data['enabled']
|
||||||
|
delimiter = data.get('delimiter', '+')
|
||||||
|
|
||||||
|
# Update database settings
|
||||||
|
settings = EmailServerSettings.get_settings()
|
||||||
|
settings.plus_addressing_enabled = enabled
|
||||||
|
settings.plus_addressing_delimiter = delimiter
|
||||||
|
settings.save()
|
||||||
|
|
||||||
|
# Update Postfix configuration
|
||||||
|
mainCfPath = '/etc/postfix/main.cf'
|
||||||
|
if os.path.exists(mainCfPath):
|
||||||
|
with open(mainCfPath, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Remove existing recipient_delimiter line
|
||||||
|
lines = content.split('\n')
|
||||||
|
newLines = [line for line in lines if not line.strip().startswith('recipient_delimiter')]
|
||||||
|
content = '\n'.join(newLines)
|
||||||
|
|
||||||
|
if enabled:
|
||||||
|
# Add recipient_delimiter setting
|
||||||
|
content = content.rstrip() + f'\nrecipient_delimiter = {delimiter}\n'
|
||||||
|
|
||||||
|
with open(mainCfPath, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
# Reload Postfix
|
||||||
|
ProcessUtilities.executioner('postfix reload')
|
||||||
|
|
||||||
|
data_ret = {
|
||||||
|
'status': 1,
|
||||||
|
'saveStatus': 1,
|
||||||
|
'message': 'Plus-addressing settings saved successfully'
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
except BaseException as msg:
|
||||||
|
data_ret = {'status': 0, 'saveStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def savePlusAddressingDomain(self):
|
||||||
|
try:
|
||||||
|
userID = self.request.session['userID']
|
||||||
|
currentACL = ACLManager.loadedACL(userID)
|
||||||
|
|
||||||
|
if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0:
|
||||||
|
return ACLManager.loadErrorJson('saveStatus', 0)
|
||||||
|
|
||||||
|
data = json.loads(self.request.body)
|
||||||
|
domain = data['domain']
|
||||||
|
enabled = data['enabled']
|
||||||
|
|
||||||
|
admin = Administrator.objects.get(pk=userID)
|
||||||
|
if ACLManager.checkOwnership(domain, admin, currentACL) == 1:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return ACLManager.loadErrorJson()
|
||||||
|
|
||||||
|
domainObj = Domains.objects.get(domain=domain)
|
||||||
|
|
||||||
|
# Create or update per-domain override
|
||||||
|
override, created = PlusAddressingOverride.objects.update_or_create(
|
||||||
|
domain=domainObj,
|
||||||
|
defaults={'enabled': enabled}
|
||||||
|
)
|
||||||
|
|
||||||
|
data_ret = {
|
||||||
|
'status': 1,
|
||||||
|
'saveStatus': 1,
|
||||||
|
'message': f'Plus-addressing {"enabled" if enabled else "disabled"} for {domain}'
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
except BaseException as msg:
|
||||||
|
data_ret = {'status': 0, 'saveStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
## Pattern Forwarding Methods
|
||||||
|
|
||||||
|
def patternForwarding(self):
|
||||||
|
userID = self.request.session['userID']
|
||||||
|
currentACL = ACLManager.loadedACL(userID)
|
||||||
|
|
||||||
|
if not os.path.exists('/home/cyberpanel/postfix'):
|
||||||
|
proc = httpProc(self.request, 'mailServer/patternForwarding.html',
|
||||||
|
{"status": 0}, 'emailForwarding')
|
||||||
|
return proc.render()
|
||||||
|
|
||||||
|
websitesName = ACLManager.findAllSites(currentACL, userID)
|
||||||
|
websitesName = websitesName + ACLManager.findChildDomains(websitesName)
|
||||||
|
|
||||||
|
proc = httpProc(self.request, 'mailServer/patternForwarding.html',
|
||||||
|
{'websiteList': websitesName, "status": 1}, 'emailForwarding')
|
||||||
|
return proc.render()
|
||||||
|
|
||||||
|
def fetchPatternRules(self):
|
||||||
|
try:
|
||||||
|
userID = self.request.session['userID']
|
||||||
|
currentACL = ACLManager.loadedACL(userID)
|
||||||
|
|
||||||
|
if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0:
|
||||||
|
return ACLManager.loadErrorJson('fetchStatus', 0)
|
||||||
|
|
||||||
|
data = json.loads(self.request.body)
|
||||||
|
domain = data['domain']
|
||||||
|
|
||||||
|
admin = Administrator.objects.get(pk=userID)
|
||||||
|
if ACLManager.checkOwnership(domain, admin, currentACL) == 1:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return ACLManager.loadErrorJson()
|
||||||
|
|
||||||
|
domainObj = Domains.objects.get(domain=domain)
|
||||||
|
rules = PatternForwarding.objects.filter(domain=domainObj).order_by('priority')
|
||||||
|
|
||||||
|
rulesData = []
|
||||||
|
for rule in rules:
|
||||||
|
rulesData.append({
|
||||||
|
'id': rule.id,
|
||||||
|
'pattern': rule.pattern,
|
||||||
|
'destination': rule.destination,
|
||||||
|
'pattern_type': rule.pattern_type,
|
||||||
|
'priority': rule.priority,
|
||||||
|
'enabled': rule.enabled
|
||||||
|
})
|
||||||
|
|
||||||
|
data_ret = {
|
||||||
|
'status': 1,
|
||||||
|
'fetchStatus': 1,
|
||||||
|
'rules': rulesData
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
except BaseException as msg:
|
||||||
|
data_ret = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def createPatternRule(self):
|
||||||
|
try:
|
||||||
|
userID = self.request.session['userID']
|
||||||
|
currentACL = ACLManager.loadedACL(userID)
|
||||||
|
|
||||||
|
if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0:
|
||||||
|
return ACLManager.loadErrorJson('createStatus', 0)
|
||||||
|
|
||||||
|
data = json.loads(self.request.body)
|
||||||
|
domain = data['domain']
|
||||||
|
pattern = data['pattern']
|
||||||
|
destination = data['destination']
|
||||||
|
pattern_type = data.get('pattern_type', 'wildcard')
|
||||||
|
priority = data.get('priority', 100)
|
||||||
|
|
||||||
|
admin = Administrator.objects.get(pk=userID)
|
||||||
|
if ACLManager.checkOwnership(domain, admin, currentACL) == 1:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return ACLManager.loadErrorJson()
|
||||||
|
|
||||||
|
# Validate destination email
|
||||||
|
if '@' not in destination:
|
||||||
|
data_ret = {'status': 0, 'createStatus': 0, 'error_message': 'Invalid destination email address'}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
# Validate pattern
|
||||||
|
if pattern_type == 'regex':
|
||||||
|
# Validate regex pattern
|
||||||
|
valid, msg = self._validateRegexPattern(pattern)
|
||||||
|
if not valid:
|
||||||
|
data_ret = {'status': 0, 'createStatus': 0, 'error_message': f'Invalid regex pattern: {msg}'}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
else:
|
||||||
|
# Validate wildcard pattern
|
||||||
|
if not pattern or len(pattern) > 200:
|
||||||
|
data_ret = {'status': 0, 'createStatus': 0, 'error_message': 'Invalid wildcard pattern'}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
domainObj = Domains.objects.get(domain=domain)
|
||||||
|
|
||||||
|
# Create pattern rule
|
||||||
|
rule = PatternForwarding(
|
||||||
|
domain=domainObj,
|
||||||
|
pattern=pattern,
|
||||||
|
destination=destination,
|
||||||
|
pattern_type=pattern_type,
|
||||||
|
priority=priority,
|
||||||
|
enabled=True
|
||||||
|
)
|
||||||
|
rule.save()
|
||||||
|
|
||||||
|
# Regenerate virtual_regexp file
|
||||||
|
self._regenerateVirtualRegexp()
|
||||||
|
|
||||||
|
data_ret = {
|
||||||
|
'status': 1,
|
||||||
|
'createStatus': 1,
|
||||||
|
'message': 'Pattern forwarding rule created successfully'
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
except BaseException as msg:
|
||||||
|
data_ret = {'status': 0, 'createStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def deletePatternRule(self):
|
||||||
|
try:
|
||||||
|
userID = self.request.session['userID']
|
||||||
|
currentACL = ACLManager.loadedACL(userID)
|
||||||
|
|
||||||
|
if ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0:
|
||||||
|
return ACLManager.loadErrorJson('deleteStatus', 0)
|
||||||
|
|
||||||
|
data = json.loads(self.request.body)
|
||||||
|
ruleId = data['ruleId']
|
||||||
|
|
||||||
|
# Get the rule and verify ownership
|
||||||
|
rule = PatternForwarding.objects.get(id=ruleId)
|
||||||
|
domain = rule.domain.domain
|
||||||
|
|
||||||
|
admin = Administrator.objects.get(pk=userID)
|
||||||
|
if ACLManager.checkOwnership(domain, admin, currentACL) == 1:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return ACLManager.loadErrorJson()
|
||||||
|
|
||||||
|
# Delete the rule
|
||||||
|
rule.delete()
|
||||||
|
|
||||||
|
# Regenerate virtual_regexp file
|
||||||
|
self._regenerateVirtualRegexp()
|
||||||
|
|
||||||
|
data_ret = {
|
||||||
|
'status': 1,
|
||||||
|
'deleteStatus': 1,
|
||||||
|
'message': 'Pattern forwarding rule deleted successfully'
|
||||||
|
}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
except BaseException as msg:
|
||||||
|
data_ret = {'status': 0, 'deleteStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def _validateRegexPattern(self, pattern):
|
||||||
|
"""Validate regex pattern for security and syntax"""
|
||||||
|
if len(pattern) > 200:
|
||||||
|
return False, "Pattern too long"
|
||||||
|
|
||||||
|
# Dangerous patterns that could cause ReDoS or security issues
|
||||||
|
dangerous = ['\\1', '\\2', '\\3', '(?P', '(?=', '(?!', '(?<', '(?:']
|
||||||
|
for d in dangerous:
|
||||||
|
if d in pattern:
|
||||||
|
return False, f"Disallowed construct: {d}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
re.compile(pattern)
|
||||||
|
return True, "Valid"
|
||||||
|
except re.error as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def _wildcardToRegex(self, pattern, domain):
|
||||||
|
"""Convert wildcard pattern to Postfix regexp format"""
|
||||||
|
# Escape special regex characters except * and ?
|
||||||
|
escaped = re.escape(pattern.replace('*', '__STAR__').replace('?', '__QUESTION__'))
|
||||||
|
# Replace placeholders with regex equivalents
|
||||||
|
regex = escaped.replace('__STAR__', '.*').replace('__QUESTION__', '.')
|
||||||
|
# Return full Postfix regexp format
|
||||||
|
return f'/^{regex}@{re.escape(domain)}$/'
|
||||||
|
|
||||||
|
def _regenerateVirtualRegexp(self):
|
||||||
|
"""Regenerate /etc/postfix/virtual_regexp from database"""
|
||||||
|
try:
|
||||||
|
rules = PatternForwarding.objects.filter(enabled=True).order_by('priority')
|
||||||
|
|
||||||
|
content = "# Auto-generated by CyberPanel - DO NOT EDIT MANUALLY\n"
|
||||||
|
for rule in rules:
|
||||||
|
if rule.pattern_type == 'wildcard':
|
||||||
|
pattern = self._wildcardToRegex(rule.pattern, rule.domain.domain)
|
||||||
|
else:
|
||||||
|
pattern = f'/^{rule.pattern}@{re.escape(rule.domain.domain)}$/'
|
||||||
|
content += f"{pattern} {rule.destination}\n"
|
||||||
|
|
||||||
|
# Write the file
|
||||||
|
regexpPath = '/etc/postfix/virtual_regexp'
|
||||||
|
with open(regexpPath, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
# Set permissions
|
||||||
|
os.chmod(regexpPath, 0o640)
|
||||||
|
ProcessUtilities.executioner('chown root:postfix /etc/postfix/virtual_regexp')
|
||||||
|
|
||||||
|
# Update main.cf to include regexp file if not already present
|
||||||
|
mainCfPath = '/etc/postfix/main.cf'
|
||||||
|
if os.path.exists(mainCfPath):
|
||||||
|
with open(mainCfPath, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
if 'virtual_regexp' not in content:
|
||||||
|
# Add regexp file to virtual_alias_maps
|
||||||
|
if 'virtual_alias_maps' in content:
|
||||||
|
content = content.replace(
|
||||||
|
'virtual_alias_maps =',
|
||||||
|
'virtual_alias_maps = regexp:/etc/postfix/virtual_regexp,'
|
||||||
|
)
|
||||||
|
with open(mainCfPath, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
# Reload Postfix
|
||||||
|
ProcessUtilities.executioner('postfix reload')
|
||||||
|
return True
|
||||||
|
except BaseException as msg:
|
||||||
|
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [_regenerateVirtualRegexp]')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='CyberPanel')
|
parser = argparse.ArgumentParser(description='CyberPanel')
|
||||||
|
|||||||
80
mailServer/migrations/0001_email_filtering_features.py
Normal file
80
mailServer/migrations/0001_email_filtering_features.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Generated migration for email filtering features
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CatchAllEmail',
|
||||||
|
fields=[
|
||||||
|
('domain', models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to='mailServer.Domains'
|
||||||
|
)),
|
||||||
|
('destination', models.CharField(max_length=255)),
|
||||||
|
('enabled', models.BooleanField(default=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'e_catchall',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EmailServerSettings',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('plus_addressing_enabled', models.BooleanField(default=False)),
|
||||||
|
('plus_addressing_delimiter', models.CharField(default='+', max_length=1)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'e_server_settings',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PlusAddressingOverride',
|
||||||
|
fields=[
|
||||||
|
('domain', models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to='mailServer.Domains'
|
||||||
|
)),
|
||||||
|
('enabled', models.BooleanField(default=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'e_plus_override',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PatternForwarding',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('pattern', models.CharField(max_length=255)),
|
||||||
|
('destination', models.CharField(max_length=255)),
|
||||||
|
('pattern_type', models.CharField(
|
||||||
|
choices=[('wildcard', 'Wildcard'), ('regex', 'Regular Expression')],
|
||||||
|
default='wildcard',
|
||||||
|
max_length=20
|
||||||
|
)),
|
||||||
|
('priority', models.IntegerField(default=100)),
|
||||||
|
('enabled', models.BooleanField(default=True)),
|
||||||
|
('domain', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to='mailServer.Domains'
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'e_pattern_forwarding',
|
||||||
|
'ordering': ['priority'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -49,3 +49,58 @@ class Transport(models.Model):
|
|||||||
class Pipeprograms(models.Model):
|
class Pipeprograms(models.Model):
|
||||||
source = models.CharField(max_length=80)
|
source = models.CharField(max_length=80)
|
||||||
destination = models.TextField()
|
destination = models.TextField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'e_pipeprograms'
|
||||||
|
|
||||||
|
|
||||||
|
class CatchAllEmail(models.Model):
|
||||||
|
"""Stores catch-all email configuration per domain"""
|
||||||
|
domain = models.OneToOneField(Domains, on_delete=models.CASCADE, primary_key=True)
|
||||||
|
destination = models.CharField(max_length=255)
|
||||||
|
enabled = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'e_catchall'
|
||||||
|
|
||||||
|
|
||||||
|
class EmailServerSettings(models.Model):
|
||||||
|
"""Global email server settings (singleton)"""
|
||||||
|
plus_addressing_enabled = models.BooleanField(default=False)
|
||||||
|
plus_addressing_delimiter = models.CharField(max_length=1, default='+')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'e_server_settings'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_settings(cls):
|
||||||
|
settings, _ = cls.objects.get_or_create(pk=1)
|
||||||
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
class PlusAddressingOverride(models.Model):
|
||||||
|
"""Per-domain plus-addressing override"""
|
||||||
|
domain = models.OneToOneField(Domains, on_delete=models.CASCADE, primary_key=True)
|
||||||
|
enabled = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'e_plus_override'
|
||||||
|
|
||||||
|
|
||||||
|
class PatternForwarding(models.Model):
|
||||||
|
"""Stores wildcard/regex forwarding rules"""
|
||||||
|
PATTERN_TYPES = [
|
||||||
|
('wildcard', 'Wildcard'),
|
||||||
|
('regex', 'Regular Expression'),
|
||||||
|
]
|
||||||
|
|
||||||
|
domain = models.ForeignKey(Domains, on_delete=models.CASCADE)
|
||||||
|
pattern = models.CharField(max_length=255)
|
||||||
|
destination = models.CharField(max_length=255)
|
||||||
|
pattern_type = models.CharField(max_length=20, choices=PATTERN_TYPES, default='wildcard')
|
||||||
|
priority = models.IntegerField(default=100)
|
||||||
|
enabled = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'e_pattern_forwarding'
|
||||||
|
ordering = ['priority']
|
||||||
@@ -1556,3 +1556,341 @@ app.controller('EmailLimitsNew', function ($scope, $http) {
|
|||||||
|
|
||||||
});
|
});
|
||||||
/* Java script for EmailLimitsNew */
|
/* 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;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|||||||
468
mailServer/templates/mailServer/catchAllEmail.html
Normal file
468
mailServer/templates/mailServer/catchAllEmail.html
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
{% extends "baseTemplate/index.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Catch-All Email - CyberPanel" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modern-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
animation: fadeInDown 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: var(--text-secondary, #64748b);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-card {
|
||||||
|
background: var(--bg-secondary, white);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 1px 3px var(--shadow-light, rgba(0,0,0,0.05)), 0 10px 40px var(--shadow-color, rgba(0,0,0,0.08));
|
||||||
|
border: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: fadeInUp 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: linear-gradient(135deg, var(--bg-hover, #f8f9ff) 0%, var(--bg-gradient, #f0f1ff) 100%);
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border-bottom: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: var(--bg-secondary, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color, #5b5fcf);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-focus, rgba(91, 95, 207, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent-color, #5b5fcf);
|
||||||
|
color: var(--text-inverse, white);
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--accent-dark, #4547a9);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px var(--accent-shadow-hover, rgba(91, 95, 207, 0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--bg-secondary, #fff);
|
||||||
|
color: var(--danger-color, #ef4444);
|
||||||
|
border: 1px solid var(--danger-bg-light, #fee2e2);
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: var(--danger-color, #ef4444);
|
||||||
|
color: var(--text-inverse, white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
animation: slideInRight 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: var(--success-bg, #d1fae5);
|
||||||
|
color: var(--success-text, #065f46);
|
||||||
|
border: 1px solid var(--success-border, #a7f3d0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background: var(--danger-bg-light, #fee2e2);
|
||||||
|
color: var(--danger-text, #991b1b);
|
||||||
|
border: 1px solid var(--danger-border, #fecaca);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: var(--info-bg, #dbeafe);
|
||||||
|
color: var(--info-text, #1e40af);
|
||||||
|
border: 1px solid var(--info-border, #bfdbfe);
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-notice {
|
||||||
|
background: var(--warning-bg, #fef3c7);
|
||||||
|
border: 1px solid var(--warning-border, #fde68a);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-notice h3 {
|
||||||
|
color: var(--warning-text, #92400e);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--border-color, #e8e9ff);
|
||||||
|
border-top-color: var(--accent-color, #5b5fcf);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-box {
|
||||||
|
background: var(--bg-hover, #f8f9ff);
|
||||||
|
border: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-box h3 {
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-config {
|
||||||
|
background: var(--bg-secondary, white);
|
||||||
|
border: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-config h4 {
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
border-bottom: 1px solid var(--border-light, #f3f4f6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-label {
|
||||||
|
color: var(--text-secondary, #64748b);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value {
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-enabled {
|
||||||
|
background: var(--success-bg, #d1fae5);
|
||||||
|
color: var(--success-text, #065f46);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-disabled {
|
||||||
|
background: var(--danger-bg-light, #fee2e2);
|
||||||
|
color: var(--danger-text, #991b1b);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: 0.4s;
|
||||||
|
border-radius: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
transition: 0.4s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .toggle-slider {
|
||||||
|
background-color: var(--accent-color, #5b5fcf);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .toggle-slider:before {
|
||||||
|
transform: translateX(24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
color: var(--warning-color, #ffa000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="modern-container" ng-controller="catchAllEmail">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">
|
||||||
|
<i class="fas fa-inbox"></i>
|
||||||
|
{% trans "Catch-All Email" %}
|
||||||
|
</h1>
|
||||||
|
<p class="page-subtitle">{% trans "Forward all unmatched emails for a domain to a single address" %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<i class="fas fa-filter"></i>
|
||||||
|
{% trans "Catch-All Configuration" %}
|
||||||
|
<span ng-show="loading" class="loading-spinner"></span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if not status %}
|
||||||
|
<div class="disabled-notice">
|
||||||
|
<i class="fas fa-exclamation-triangle fa-3x mb-3 warning-icon"></i>
|
||||||
|
<h3>{% trans "Postfix is disabled" %}</h3>
|
||||||
|
<p class="mb-3">{% trans "You need to enable Postfix to configure catch-all email" %}</p>
|
||||||
|
<a href="{% url 'managePostfix' %}" class="btn-primary">
|
||||||
|
<i class="fas fa-power-off"></i>
|
||||||
|
{% trans "Enable Postfix Now" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<form action="/" method="post">
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{% trans "Select Domain" %}</label>
|
||||||
|
<select ng-change="fetchConfig()" ng-model="selectedDomain" class="form-control">
|
||||||
|
<option value="">{% trans "Choose a domain..." %}</option>
|
||||||
|
{% for items in websiteList %}
|
||||||
|
<option value="{{ items }}">{{ items }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-hide="configBox" class="config-box">
|
||||||
|
<h3><i class="fas fa-cog"></i> {% trans "Configure Catch-All" %}</h3>
|
||||||
|
|
||||||
|
<div ng-show="currentConfigured" class="current-config">
|
||||||
|
<h4><i class="fas fa-info-circle"></i> {% trans "Current Configuration" %}</h4>
|
||||||
|
<div class="config-item">
|
||||||
|
<span class="config-label">{% trans "Status" %}</span>
|
||||||
|
<span class="status-badge" ng-class="currentEnabled ? 'status-enabled' : 'status-disabled'">
|
||||||
|
{$ currentEnabled ? 'Enabled' : 'Disabled' $}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="config-item">
|
||||||
|
<span class="config-label">{% trans "Destination" %}</span>
|
||||||
|
<span class="config-value">{$ currentDestination $}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{% trans "Destination Email" %}</label>
|
||||||
|
<input type="email" class="form-control" ng-model="destination"
|
||||||
|
placeholder="{% trans 'catchall@example.com' %}" required>
|
||||||
|
<small class="text-muted">{% trans "All unmatched emails will be forwarded to this address" %}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{% trans "Enable Catch-All" %}</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" ng-model="enabled" ng-init="enabled=true">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<button type="button" ng-click="saveConfig()" class="btn-primary">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
{% trans "Save Configuration" %}
|
||||||
|
</button>
|
||||||
|
<button type="button" ng-show="currentConfigured" ng-click="deleteConfig()" class="btn-danger ml-2">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
{% trans "Remove Catch-All" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alert Messages -->
|
||||||
|
<div ng-hide="notifyBox" class="form-section mt-3">
|
||||||
|
<div ng-hide="errorBox" class="alert alert-danger">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
{$ errorMessage $}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-hide="successBox" class="alert alert-success">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
{$ successMessage $}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-hide="couldNotConnect" class="alert alert-danger">
|
||||||
|
<i class="fas fa-times-circle"></i>
|
||||||
|
{% trans "Could not connect to server. Please refresh this page." %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
465
mailServer/templates/mailServer/patternForwarding.html
Normal file
465
mailServer/templates/mailServer/patternForwarding.html
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
{% extends "baseTemplate/index.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Pattern Forwarding - CyberPanel" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modern-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
animation: fadeInDown 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: var(--text-secondary, #64748b);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-card {
|
||||||
|
background: var(--bg-secondary, white);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 1px 3px var(--shadow-light, rgba(0,0,0,0.05)), 0 10px 40px var(--shadow-color, rgba(0,0,0,0.08));
|
||||||
|
border: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: fadeInUp 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: linear-gradient(135deg, var(--bg-hover, #f8f9ff) 0%, var(--bg-gradient, #f0f1ff) 100%);
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border-bottom: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: var(--bg-secondary, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color, #5b5fcf);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-focus, rgba(91, 95, 207, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent-color, #5b5fcf);
|
||||||
|
color: var(--text-inverse, white);
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--accent-dark, #4547a9);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--bg-secondary, #fff);
|
||||||
|
color: var(--danger-color, #ef4444);
|
||||||
|
border: 1px solid var(--danger-bg-light, #fee2e2);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: var(--danger-color, #ef4444);
|
||||||
|
color: var(--text-inverse, white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: var(--success-bg, #d1fae5);
|
||||||
|
color: var(--success-text, #065f46);
|
||||||
|
border: 1px solid var(--success-border, #a7f3d0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background: var(--danger-bg-light, #fee2e2);
|
||||||
|
color: var(--danger-text, #991b1b);
|
||||||
|
border: 1px solid var(--danger-border, #fecaca);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: var(--info-bg, #dbeafe);
|
||||||
|
color: var(--info-text, #1e40af);
|
||||||
|
border: 1px solid var(--info-border, #bfdbfe);
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-notice {
|
||||||
|
background: var(--warning-bg, #fef3c7);
|
||||||
|
border: 1px solid var(--warning-border, #fde68a);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--border-color, #e8e9ff);
|
||||||
|
border-top-color: var(--accent-color, #5b5fcf);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-box {
|
||||||
|
background: var(--bg-hover, #f8f9ff);
|
||||||
|
border: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-box h3 {
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-table {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--bg-secondary, white);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-table thead {
|
||||||
|
background: var(--bg-hover, #f8f9ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-table th {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
border-bottom: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-table td {
|
||||||
|
padding: 1rem;
|
||||||
|
color: var(--text-secondary, #64748b);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-bottom: 1px solid var(--border-light, #f3f4f6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-table tbody tr:hover {
|
||||||
|
background: var(--bg-hover, #f8f9ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-type-badge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-wildcard {
|
||||||
|
background: var(--accent-bg, #e0e7ff);
|
||||||
|
color: var(--accent-color, #5b5fcf);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-regex {
|
||||||
|
background: var(--warning-bg, #fef3c7);
|
||||||
|
color: var(--warning-text, #92400e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
background: var(--info-bg, #dbeafe);
|
||||||
|
border: 1px solid var(--info-border, #bfdbfe);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text h4 {
|
||||||
|
color: var(--info-text, #1e40af);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text ul {
|
||||||
|
color: var(--info-text, #1e40af);
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
color: var(--warning-color, #ffa000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from { opacity: 0; transform: translateY(-20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="modern-container" ng-controller="patternForwarding">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">
|
||||||
|
<i class="fas fa-asterisk"></i>
|
||||||
|
{% trans "Pattern Forwarding" %}
|
||||||
|
</h1>
|
||||||
|
<p class="page-subtitle">{% trans "Create wildcard and regex-based email forwarding rules" %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<i class="fas fa-code-branch"></i>
|
||||||
|
{% trans "Pattern Forwarding Rules" %}
|
||||||
|
<span ng-show="loading" class="loading-spinner"></span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if not status %}
|
||||||
|
<div class="disabled-notice">
|
||||||
|
<i class="fas fa-exclamation-triangle fa-3x mb-3 warning-icon"></i>
|
||||||
|
<h3>{% trans "Postfix is disabled" %}</h3>
|
||||||
|
<p class="mb-3">{% trans "You need to enable Postfix to configure pattern forwarding" %}</p>
|
||||||
|
<a href="{% url 'managePostfix' %}" class="btn-primary">
|
||||||
|
<i class="fas fa-power-off"></i>
|
||||||
|
{% trans "Enable Postfix Now" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<form action="/" method="post">
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{% trans "Select Domain" %}</label>
|
||||||
|
<select ng-change="fetchRules()" ng-model="selectedDomain" class="form-control">
|
||||||
|
<option value="">{% trans "Choose a domain..." %}</option>
|
||||||
|
{% for items in websiteList %}
|
||||||
|
<option value="{{ items }}">{{ items }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-hide="configBox" class="config-box">
|
||||||
|
<h3><i class="fas fa-plus-circle"></i> {% trans "Create New Rule" %}</h3>
|
||||||
|
|
||||||
|
<div class="help-text" ng-show="patternType === 'wildcard'">
|
||||||
|
<h4><i class="fas fa-lightbulb"></i> {% trans "Wildcard Pattern Examples" %}</h4>
|
||||||
|
<ul>
|
||||||
|
<li><code>user_*</code> - {% trans "Matches user_anything (e.g., user_sales, user_123)" %}</li>
|
||||||
|
<li><code>support-?</code> - {% trans "Matches support- followed by any single character" %}</li>
|
||||||
|
<li><code>team*</code> - {% trans "Matches anything starting with team" %}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="help-text" ng-show="patternType === 'regex'">
|
||||||
|
<h4><i class="fas fa-exclamation-triangle"></i> {% trans "Regex Pattern (Advanced)" %}</h4>
|
||||||
|
<ul>
|
||||||
|
<li><code>user_[0-9]+</code> - {% trans "Matches user_ followed by digits" %}</li>
|
||||||
|
<li><code>support-(sales|billing)</code> - {% trans "Matches support-sales or support-billing" %}</li>
|
||||||
|
<li>{% trans "Note: Pattern is matched against the local part only (before @)" %}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{% trans "Pattern Type" %}</label>
|
||||||
|
<select class="form-control" ng-model="patternType" ng-init="patternType='wildcard'">
|
||||||
|
<option value="wildcard">{% trans "Wildcard" %}</option>
|
||||||
|
<option value="regex">{% trans "Regex (Advanced)" %}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{% trans "Pattern" %}</label>
|
||||||
|
<input type="text" class="form-control" ng-model="pattern"
|
||||||
|
placeholder="{% trans 'e.g., user_*' %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{% trans "Destination Email" %}</label>
|
||||||
|
<input type="email" class="form-control" ng-model="destination"
|
||||||
|
placeholder="{% trans 'forward@example.com' %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{% trans "Priority" %}</label>
|
||||||
|
<input type="number" class="form-control" ng-model="priority"
|
||||||
|
ng-init="priority=100" min="1" max="999">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label"> </label>
|
||||||
|
<button type="button" ng-click="createRule()" class="btn-primary" style="width:100%">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="rules-table" ng-show="rules.length > 0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:10%">{% trans "Priority" %}</th>
|
||||||
|
<th style="width:15%">{% trans "Type" %}</th>
|
||||||
|
<th style="width:25%">{% trans "Pattern" %}</th>
|
||||||
|
<th style="width:35%">{% trans "Destination" %}</th>
|
||||||
|
<th style="width:15%">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="rule in rules track by $index">
|
||||||
|
<td><strong ng-bind="rule.priority"></strong></td>
|
||||||
|
<td>
|
||||||
|
<span class="pattern-type-badge" ng-class="rule.pattern_type === 'wildcard' ? 'type-wildcard' : 'type-regex'">
|
||||||
|
<i ng-class="rule.pattern_type === 'wildcard' ? 'fas fa-asterisk' : 'fas fa-code'"></i>
|
||||||
|
{$ rule.pattern_type $}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><code ng-bind="rule.pattern"></code>@{$ selectedDomain $}</td>
|
||||||
|
<td ng-bind="rule.destination"></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" ng-click="deleteRule(rule.id)" class="btn-danger">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div ng-hide="rules.length > 0" class="alert alert-info mt-3">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
{% trans "No pattern forwarding rules configured for this domain yet." %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alert Messages -->
|
||||||
|
<div ng-hide="notifyBox" class="form-section mt-3">
|
||||||
|
<div ng-hide="errorBox" class="alert alert-danger">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
{$ errorMessage $}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-hide="successBox" class="alert alert-success">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
{$ successMessage $}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-hide="couldNotConnect" class="alert alert-danger">
|
||||||
|
<i class="fas fa-times-circle"></i>
|
||||||
|
{% trans "Could not connect to server. Please refresh this page." %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
406
mailServer/templates/mailServer/plusAddressingSettings.html
Normal file
406
mailServer/templates/mailServer/plusAddressingSettings.html
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
{% extends "baseTemplate/index.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Plus-Addressing Settings - CyberPanel" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modern-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
animation: fadeInDown 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: var(--text-secondary, #64748b);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-card {
|
||||||
|
background: var(--bg-secondary, white);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 1px 3px var(--shadow-light, rgba(0,0,0,0.05)), 0 10px 40px var(--shadow-color, rgba(0,0,0,0.08));
|
||||||
|
border: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: fadeInUp 0.5s ease-out;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: linear-gradient(135deg, var(--bg-hover, #f8f9ff) 0%, var(--bg-gradient, #f0f1ff) 100%);
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border-bottom: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--border-color, #e8e9ff);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: var(--bg-secondary, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color, #5b5fcf);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-focus, rgba(91, 95, 207, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent-color, #5b5fcf);
|
||||||
|
color: var(--text-inverse, white);
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--accent-dark, #4547a9);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: var(--success-bg, #d1fae5);
|
||||||
|
color: var(--success-text, #065f46);
|
||||||
|
border: 1px solid var(--success-border, #a7f3d0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background: var(--danger-bg-light, #fee2e2);
|
||||||
|
color: var(--danger-text, #991b1b);
|
||||||
|
border: 1px solid var(--danger-border, #fecaca);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: var(--info-bg, #dbeafe);
|
||||||
|
color: var(--info-text, #1e40af);
|
||||||
|
border: 1px solid var(--info-border, #bfdbfe);
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-notice {
|
||||||
|
background: var(--warning-bg, #fef3c7);
|
||||||
|
border: 1px solid var(--warning-border, #fde68a);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--border-color, #e8e9ff);
|
||||||
|
border-top-color: var(--accent-color, #5b5fcf);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: 0.4s;
|
||||||
|
border-radius: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
transition: 0.4s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .toggle-slider {
|
||||||
|
background-color: var(--accent-color, #5b5fcf);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .toggle-slider:before {
|
||||||
|
transform: translateX(24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: var(--info-bg, #dbeafe);
|
||||||
|
border: 1px solid var(--info-border, #bfdbfe);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box h4 {
|
||||||
|
color: var(--info-text, #1e40af);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box p {
|
||||||
|
color: var(--info-text, #1e40af);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-enabled {
|
||||||
|
background: var(--success-bg, #d1fae5);
|
||||||
|
color: var(--success-text, #065f46);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-disabled {
|
||||||
|
background: var(--danger-bg-light, #fee2e2);
|
||||||
|
color: var(--danger-text, #991b1b);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
color: var(--warning-color, #ffa000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from { opacity: 0; transform: translateY(-20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="modern-container" ng-controller="plusAddressing" ng-init="isAdmin={{ admin|yesno:'true,false' }}">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">
|
||||||
|
<i class="fas fa-plus-circle"></i>
|
||||||
|
{% trans "Plus-Addressing" %}
|
||||||
|
</h1>
|
||||||
|
<p class="page-subtitle">{% trans "Enable email sub-addressing (user+tag@domain.com)" %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not status %}
|
||||||
|
<div class="main-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="disabled-notice">
|
||||||
|
<i class="fas fa-exclamation-triangle fa-3x mb-3 warning-icon"></i>
|
||||||
|
<h3>{% trans "Postfix is disabled" %}</h3>
|
||||||
|
<p class="mb-3">{% trans "You need to enable Postfix to configure plus-addressing" %}</p>
|
||||||
|
<a href="{% url 'managePostfix' %}" class="btn-primary">
|
||||||
|
<i class="fas fa-power-off"></i>
|
||||||
|
{% trans "Enable Postfix Now" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<!-- Global Settings (Admin Only) -->
|
||||||
|
<div class="main-card" ng-show="isAdmin">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<i class="fas fa-globe"></i>
|
||||||
|
{% trans "Global Settings" %}
|
||||||
|
<span ng-show="loading" class="loading-spinner"></span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="info-box">
|
||||||
|
<h4><i class="fas fa-info-circle"></i> {% trans "What is Plus-Addressing?" %}</h4>
|
||||||
|
<p>{% trans "Plus-addressing allows users to receive email at user+anything@domain.com which will be delivered to user@domain.com. This is useful for filtering and tracking email sources." %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{% trans "Enable Plus-Addressing (Server-wide)" %}</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" ng-model="globalEnabled">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<span class="ml-2 status-badge" ng-class="globalEnabled ? 'status-enabled' : 'status-disabled'">
|
||||||
|
{$ globalEnabled ? 'Enabled' : 'Disabled' $}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{% trans "Delimiter Character" %}</label>
|
||||||
|
<select class="form-control" ng-model="delimiter" style="width: auto;">
|
||||||
|
<option value="+">+ (Plus)</option>
|
||||||
|
<option value="-">- (Hyphen)</option>
|
||||||
|
<option value="_">_ (Underscore)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" ng-click="saveGlobalSettings()" class="btn-primary">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
{% trans "Save Global Settings" %}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Alert Messages -->
|
||||||
|
<div ng-hide="globalNotifyBox" class="mt-3">
|
||||||
|
<div ng-hide="globalErrorBox" class="alert alert-danger">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
{$ globalErrorMessage $}
|
||||||
|
</div>
|
||||||
|
<div ng-hide="globalSuccessBox" class="alert alert-success">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
{$ globalSuccessMessage $}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Per-Domain Override -->
|
||||||
|
<div class="main-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<i class="fas fa-sitemap"></i>
|
||||||
|
{% trans "Per-Domain Settings" %}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info mb-3">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
{% trans "Per-domain settings allow you to track which domains should use plus-addressing. Note: Actual filtering is server-wide in Postfix." %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{% trans "Select Domain" %}</label>
|
||||||
|
<select ng-model="selectedDomain" class="form-control">
|
||||||
|
<option value="">{% trans "Choose a domain..." %}</option>
|
||||||
|
{% for items in websiteList %}
|
||||||
|
<option value="{{ items }}">{{ items }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6" ng-show="selectedDomain">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{% trans "Enable for this domain" %}</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" ng-model="domainEnabled">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" ng-show="selectedDomain" ng-click="saveDomainSettings()" class="btn-primary">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
{% trans "Save Domain Settings" %}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Alert Messages -->
|
||||||
|
<div ng-hide="domainNotifyBox" class="mt-3">
|
||||||
|
<div ng-hide="domainErrorBox" class="alert alert-danger">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
{$ domainErrorMessage $}
|
||||||
|
</div>
|
||||||
|
<div ng-hide="domainSuccessBox" class="alert alert-success">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
{$ domainSuccessMessage $}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -35,4 +35,22 @@ urlpatterns = [
|
|||||||
### email limits
|
### email limits
|
||||||
re_path(r'^EmailLimits$', views.EmailLimits, name='EmailLimits'),
|
re_path(r'^EmailLimits$', views.EmailLimits, name='EmailLimits'),
|
||||||
re_path(r'^SaveEmailLimitsNew$', views.SaveEmailLimitsNew, name='SaveEmailLimitsNew'),
|
re_path(r'^SaveEmailLimitsNew$', views.SaveEmailLimitsNew, name='SaveEmailLimitsNew'),
|
||||||
|
|
||||||
|
## Catch-All Email
|
||||||
|
re_path(r'^catchAllEmail$', views.catchAllEmail, name='catchAllEmail'),
|
||||||
|
re_path(r'^fetchCatchAllConfig$', views.fetchCatchAllConfig, name='fetchCatchAllConfig'),
|
||||||
|
re_path(r'^saveCatchAllConfig$', views.saveCatchAllConfig, name='saveCatchAllConfig'),
|
||||||
|
re_path(r'^deleteCatchAllConfig$', views.deleteCatchAllConfig, name='deleteCatchAllConfig'),
|
||||||
|
|
||||||
|
## Plus-Addressing
|
||||||
|
re_path(r'^plusAddressingSettings$', views.plusAddressingSettings, name='plusAddressingSettings'),
|
||||||
|
re_path(r'^fetchPlusAddressingConfig$', views.fetchPlusAddressingConfig, name='fetchPlusAddressingConfig'),
|
||||||
|
re_path(r'^savePlusAddressingGlobal$', views.savePlusAddressingGlobal, name='savePlusAddressingGlobal'),
|
||||||
|
re_path(r'^savePlusAddressingDomain$', views.savePlusAddressingDomain, name='savePlusAddressingDomain'),
|
||||||
|
|
||||||
|
## Pattern Forwarding
|
||||||
|
re_path(r'^patternForwarding$', views.patternForwarding, name='patternForwarding'),
|
||||||
|
re_path(r'^fetchPatternRules$', views.fetchPatternRules, name='fetchPatternRules'),
|
||||||
|
re_path(r'^createPatternRule$', views.createPatternRule, name='createPatternRule'),
|
||||||
|
re_path(r'^deletePatternRule$', views.deletePatternRule, name='deletePatternRule'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -263,4 +263,113 @@ def SaveEmailLimitsNew(request):
|
|||||||
return HttpResponse(json_data)
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
|
||||||
|
## Catch-All Email
|
||||||
|
|
||||||
|
def catchAllEmail(request):
|
||||||
|
try:
|
||||||
|
msM = MailServerManager(request)
|
||||||
|
return msM.catchAllEmail()
|
||||||
|
except KeyError:
|
||||||
|
return redirect(loadLoginPage)
|
||||||
|
|
||||||
|
def fetchCatchAllConfig(request):
|
||||||
|
try:
|
||||||
|
msM = MailServerManager(request)
|
||||||
|
return msM.fetchCatchAllConfig()
|
||||||
|
except KeyError as msg:
|
||||||
|
data_ret = {'fetchStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def saveCatchAllConfig(request):
|
||||||
|
try:
|
||||||
|
msM = MailServerManager(request)
|
||||||
|
return msM.saveCatchAllConfig()
|
||||||
|
except KeyError as msg:
|
||||||
|
data_ret = {'saveStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def deleteCatchAllConfig(request):
|
||||||
|
try:
|
||||||
|
msM = MailServerManager(request)
|
||||||
|
return msM.deleteCatchAllConfig()
|
||||||
|
except KeyError as msg:
|
||||||
|
data_ret = {'deleteStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
|
||||||
|
## Plus-Addressing
|
||||||
|
|
||||||
|
def plusAddressingSettings(request):
|
||||||
|
try:
|
||||||
|
msM = MailServerManager(request)
|
||||||
|
return msM.plusAddressingSettings()
|
||||||
|
except KeyError:
|
||||||
|
return redirect(loadLoginPage)
|
||||||
|
|
||||||
|
def fetchPlusAddressingConfig(request):
|
||||||
|
try:
|
||||||
|
msM = MailServerManager(request)
|
||||||
|
return msM.fetchPlusAddressingConfig()
|
||||||
|
except KeyError as msg:
|
||||||
|
data_ret = {'fetchStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def savePlusAddressingGlobal(request):
|
||||||
|
try:
|
||||||
|
msM = MailServerManager(request)
|
||||||
|
return msM.savePlusAddressingGlobal()
|
||||||
|
except KeyError as msg:
|
||||||
|
data_ret = {'saveStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def savePlusAddressingDomain(request):
|
||||||
|
try:
|
||||||
|
msM = MailServerManager(request)
|
||||||
|
return msM.savePlusAddressingDomain()
|
||||||
|
except KeyError as msg:
|
||||||
|
data_ret = {'saveStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
|
||||||
|
## Pattern Forwarding
|
||||||
|
|
||||||
|
def patternForwarding(request):
|
||||||
|
try:
|
||||||
|
msM = MailServerManager(request)
|
||||||
|
return msM.patternForwarding()
|
||||||
|
except KeyError:
|
||||||
|
return redirect(loadLoginPage)
|
||||||
|
|
||||||
|
def fetchPatternRules(request):
|
||||||
|
try:
|
||||||
|
msM = MailServerManager(request)
|
||||||
|
return msM.fetchPatternRules()
|
||||||
|
except KeyError as msg:
|
||||||
|
data_ret = {'fetchStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def createPatternRule(request):
|
||||||
|
try:
|
||||||
|
msM = MailServerManager(request)
|
||||||
|
return msM.createPatternRule()
|
||||||
|
except KeyError as msg:
|
||||||
|
data_ret = {'createStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def deletePatternRule(request):
|
||||||
|
try:
|
||||||
|
msM = MailServerManager(request)
|
||||||
|
return msM.deletePatternRule()
|
||||||
|
except KeyError as msg:
|
||||||
|
data_ret = {'deleteStatus': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user