Fixing To,CC,BCC and email headers applying, add date folder for storing attachments, add new settings management to web interface - timezone, attachment storage
This commit is contained in:
@@ -12,6 +12,7 @@ This module provides server settings management functionality including:
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
import zoneinfo
|
||||
from flask import render_template, request, redirect, url_for, flash, jsonify
|
||||
from werkzeug.utils import secure_filename
|
||||
from email_server.settings_loader import load_settings, SETTINGS_PATH
|
||||
@@ -29,11 +30,21 @@ ALLOWED_EXTENSIONS = {'crt', 'key', 'pem'}
|
||||
def allowed_file(filename):
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
def get_template_context():
|
||||
"""Get template context with CSRF token and common data."""
|
||||
context = {
|
||||
'settings': load_settings(),
|
||||
'timezones': get_available_timezones(),
|
||||
}
|
||||
# Only add CSRF token if it exists and is enabled
|
||||
if hasattr(request, 'csrf_token'):
|
||||
context['csrf_token_value'] = request.csrf_token
|
||||
return context
|
||||
|
||||
@email_bp.route('/settings')
|
||||
def settings():
|
||||
"""Display and edit server settings."""
|
||||
settings = load_settings()
|
||||
return render_template('settings.html', settings=settings)
|
||||
return render_template('settings.html', **get_template_context())
|
||||
|
||||
@email_bp.route('/settings_update', methods=['POST'])
|
||||
def settings_update():
|
||||
@@ -195,3 +206,40 @@ def get_server_ip():
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting public IP: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
@email_bp.route('/test_attachments_path', methods=['POST'])
|
||||
def test_attachments_path():
|
||||
"""Test if the attachments path is writable."""
|
||||
path = request.form.get('path')
|
||||
if not path:
|
||||
return jsonify({'success': False, 'message': 'No path provided'})
|
||||
|
||||
# Convert to absolute path if relative
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.abspath(os.path.join(os.path.dirname(SETTINGS_PATH), path))
|
||||
|
||||
try:
|
||||
# Create path if it doesn't exist
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
# Try to create a test file
|
||||
test_file = os.path.join(path, '.write_test')
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('test')
|
||||
os.remove(test_file)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Attachments path is valid and writable',
|
||||
'absolute_path': path
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing attachments path: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'Error: {str(e)}',
|
||||
'absolute_path': path
|
||||
})
|
||||
|
||||
def get_available_timezones():
|
||||
"""Get a list of all available timezones sorted alphabetically."""
|
||||
return sorted(zoneinfo.available_timezones())
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>Recipients</th>
|
||||
<th>Status</th>
|
||||
<th>DKIM</th>
|
||||
</tr>
|
||||
@@ -153,9 +153,46 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-truncate d-inline-block" style="max-width: 150px;" title="{{ email.rcpt_tos }}">
|
||||
{{ email.rcpt_tos }}
|
||||
</span>
|
||||
<div style="max-width: 200px; font-size: 0.85rem;">
|
||||
<div class="recipients-list">
|
||||
{% if email.to_address %}
|
||||
{% for rcpt in email.to_address.split(',') %}
|
||||
{% if rcpt.strip() %}
|
||||
<div class="text-truncate">
|
||||
<span class="text-info fw-bold" style="font-size: 0.75rem;">To:</span>
|
||||
<span title="{{ rcpt.strip() }}">{{ rcpt.strip() }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if email.cc_addresses %}
|
||||
{% for rcpt in email.cc_addresses.split(',') %}
|
||||
{% if rcpt.strip() %}
|
||||
<div class="text-truncate">
|
||||
<span class="text-warning fw-bold" style="font-size: 0.75rem;">CC:</span>
|
||||
<span title="{{ rcpt.strip() }}">{{ rcpt.strip() }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if email.bcc_addresses %}
|
||||
{% for rcpt in email.bcc_addresses.split(',') %}
|
||||
{% if rcpt.strip() %}
|
||||
<div class="text-truncate">
|
||||
<span class="text-secondary fw-bold" style="font-size: 0.75rem;">BCC:</span>
|
||||
<span title="{{ rcpt.strip() }}">{{ rcpt.strip() }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if not email.to_address and not email.cc_addresses and not email.bcc_addresses %}
|
||||
<div class="text-muted">No recipients</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% set delivered = recipient_logs_map[email.id]|selectattr('status', 'equalto', 'success')|list %}
|
||||
@@ -167,7 +204,6 @@
|
||||
{% else %}
|
||||
{% set overall_status = 'failed' %}
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if overall_status == 'relayed' %}
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
@@ -334,6 +370,15 @@
|
||||
box-shadow: 0 0 0 2px #0d6efd33;
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
.recipients-list {
|
||||
line-height: 1.2;
|
||||
}
|
||||
.recipients-list div {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.recipients-list div:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -97,10 +97,22 @@
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="Server.bind_ip"
|
||||
value="{{ settings['Server']['bind_ip'] }}"
|
||||
pattern="^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$">
|
||||
value="{{ settings['Server']['bind_ip'] }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Server Timezone</label>
|
||||
<div class="setting-description">Timezone for server operations and logging</div>
|
||||
<select class="form-select" name="Server.time_zone">
|
||||
{% for tz in timezones %}
|
||||
<option value="{{ tz }}" {% if tz == settings['Server']['time_zone'] %}selected{% endif %}>{{ tz }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Hostname</label>
|
||||
@@ -111,8 +123,6 @@
|
||||
value="{{ settings['Server']['hostname'] }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">HELO Hostname</label>
|
||||
@@ -123,6 +133,8 @@
|
||||
value="{{ settings['Server']['helo_hostname'] }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Server Banner</label>
|
||||
@@ -353,6 +365,40 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attachments Settings -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header" data-bs-toggle="collapse" data-bs-target="#attachmentsSettings" aria-expanded="true">
|
||||
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-paperclip me-2"></i>Attachments Configuration</span>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</h5>
|
||||
</div>
|
||||
<div id="attachmentsSettings" class="collapse show">
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Attachments Storage Path</label>
|
||||
<div class="setting-description">Path where email attachments will be stored (relative to SMTP server root)</div>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="Attachments.attachments_path"
|
||||
value="{{ settings['Attachments']['attachments_path'] }}"
|
||||
placeholder="email_server/server_data/attachments">
|
||||
</div> <div class="setting-description text-warning">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
Make sure the path exists and is writable by the server process
|
||||
</div>
|
||||
<div id="attachments-path-feedback" class="mt-2"></div>
|
||||
<div id="attachments-path-feedback" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="alert alert-warning d-flex align-items-center mb-0">
|
||||
@@ -602,5 +648,60 @@
|
||||
console.log(`${key}: ${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Populate timezone select options
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const timeZoneSelect = document.getElementById('timeZoneSelect');
|
||||
fetch('/api/timezones')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
data.timezones.forEach(tz => {
|
||||
const option = document.createElement('option');
|
||||
option.value = tz;
|
||||
option.textContent = tz;
|
||||
timeZoneSelect.appendChild(option);
|
||||
});
|
||||
})
|
||||
.catch(err => console.error('Failed to load timezones:', err));
|
||||
});
|
||||
|
||||
function validateAttachmentsPath() {
|
||||
const path = document.querySelector('input[name="Attachments.attachments_path"]').value;
|
||||
const feedback = document.getElementById('attachments-path-feedback');
|
||||
if (!feedback) return;
|
||||
|
||||
fetch('{{ url_for("email.test_attachments_path") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRFToken': '{{ csrf_token_value|default("") }}'
|
||||
},
|
||||
body: `path=${encodeURIComponent(path)}`
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
feedback.innerHTML = data.message;
|
||||
feedback.className = data.success ? 'text-success mt-2' : 'text-danger mt-2';
|
||||
if (data.success) {
|
||||
feedback.innerHTML += `<br><small class="text-muted">Absolute path: ${data.absolute_path}</small>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
feedback.innerHTML = `Error validating path: ${error}`;
|
||||
feedback.className = 'text-danger mt-2';
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener to attachments path input
|
||||
document.querySelector('input[name="Attachments.attachments_path"]')?.addEventListener('change', validateAttachmentsPath);
|
||||
|
||||
document.getElementById('settingsForm')?.addEventListener('submit', function(e) {
|
||||
const attachmentsPath = document.querySelector('input[name="Attachments.attachments_path"]');
|
||||
if (!attachmentsPath.value.trim()) {
|
||||
e.preventDefault();
|
||||
alert('Please specify a valid attachments storage path');
|
||||
attachmentsPath.focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
Server Settings
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{#
|
||||
<!-- Monitoring Section -->
|
||||
<li class="nav-item mb-2">
|
||||
<h6 class="text-muted text-uppercase small mb-2 mt-3">
|
||||
@@ -114,6 +114,7 @@
|
||||
Logs & Activity
|
||||
</a>
|
||||
</li>
|
||||
#}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user