mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2025-12-15 12:59:42 +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>
|
||||
</a>
|
||||
{% 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 %}
|
||||
<a href="{% url 'changeEmailAccountPassword' %}" class="menu-item">
|
||||
<span>Change Password</span>
|
||||
|
||||
@@ -30,13 +30,14 @@ import _thread
|
||||
try:
|
||||
from dns.models import Domains as dnsDomains
|
||||
from dns.models import Records as dnsRecords
|
||||
from mailServer.models import Forwardings, Pipeprograms
|
||||
from mailServer.models import Forwardings, Pipeprograms, CatchAllEmail, EmailServerSettings, PlusAddressingOverride, PatternForwarding
|
||||
from plogical.acl import ACLManager
|
||||
from plogical.dnsUtilities import DNS
|
||||
from loginSystem.models import Administrator
|
||||
from websiteFunctions.models import Websites
|
||||
except:
|
||||
pass
|
||||
import re
|
||||
import os
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
import bcrypt
|
||||
@@ -2001,6 +2002,559 @@ protocol sieve {
|
||||
json_data = json.dumps(data_ret)
|
||||
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():
|
||||
|
||||
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):
|
||||
source = models.CharField(max_length=80)
|
||||
destination = models.TextField()
|
||||
|
||||
class Meta:
|
||||
db_table = 'e_pipeprograms'
|
||||
|
||||
|
||||
class CatchAllEmail(models.Model):
|
||||
"""Stores catch-all email configuration per domain"""
|
||||
domain = models.OneToOneField(Domains, on_delete=models.CASCADE, primary_key=True)
|
||||
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 */
|
||||
|
||||
/* 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
|
||||
re_path(r'^EmailLimits$', views.EmailLimits, name='EmailLimits'),
|
||||
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)
|
||||
|
||||
|
||||
## 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