Files
mailgoserver/internal/webui/templates/settings.html
T

353 lines
22 KiB
HTML

{{define "title"}}Server Settings - Email Server{{end}}
{{define "extra_css"}}
<style>
.setting-section { border-left: 4px solid var(--bs-primary); padding-left: 1rem; margin-bottom: 2rem; }
.setting-description { font-size: 0.875rem; color: var(--bs-secondary); margin-bottom: 0.5rem; }
</style>
{{end}}
{{define "content"}}
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-sliders me-2"></i>Server Settings</h2>
<div class="btn-group">
<button type="button" class="btn btn-outline-info" onclick="exportSettings()"><i class="bi bi-download me-2"></i>Export Config</button>
</div>
</div>
<form method="POST" action="/pymta-manager/settings_update" id="settingsForm">
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-server me-2"></i>Server Configuration</h5></div>
<div class="card-body">
<div class="setting-section">
<div class="row">
<div class="col-md-6"><div class="mb-3"><label class="form-label">SMTP Port</label>
<div class="setting-description">Port for plain/IP-whitelisted SMTP connections</div>
<input type="number" class="form-control" name="Server.smtp_port" value="{{.settings.Server.smtp_port}}" min="1" max="65535">
</div></div>
<div class="col-md-6"><div class="mb-3"><label class="form-label">SMTP TLS Port</label>
<div class="setting-description">Port for direct-TLS authenticated SMTP connections</div>
<input type="number" class="form-control" name="Server.smtp_tls_port" value="{{.settings.Server.smtp_tls_port}}" min="1" max="65535">
</div></div>
</div>
<div class="row">
<div class="col-md-6"><div class="mb-3"><label class="form-label">Admin UI HTTP Port</label>
<div class="setting-description">Plain HTTP port for this admin web interface</div>
<input type="number" class="form-control" name="Server.web_http_port" value="{{.settings.Server.web_http_port}}" min="1" max="65535">
</div></div>
<div class="col-md-6"><div class="mb-3"><label class="form-label">Admin UI HTTPS Port</label>
<div class="setting-description">Self-signed by default, or the Let's Encrypt cert once enabled</div>
<input type="number" class="form-control" name="Server.web_https_port" value="{{.settings.Server.web_https_port}}" min="1" max="65535">
</div></div>
</div>
<div class="row">
<div class="col-md-6"><div class="mb-3"><label class="form-label">Bind IP Address</label>
<input type="text" class="form-control" name="Server.bind_ip" value="{{.settings.Server.bind_ip}}">
</div></div>
<div class="col-md-6"><div class="mb-3"><label class="form-label">Server Timezone</label>
<select class="form-select" name="Server.time_zone">
{{$currentTZ := .settings.Server.time_zone}}
{{range .timezones}}<option value="{{.}}" {{if eq . $currentTZ}}selected{{end}}>{{.}}</option>{{end}}
</select>
</div></div>
</div>
<div class="row">
<div class="col-md-6"><div class="mb-3"><label class="form-label">Hostname</label>
<input type="text" class="form-control" name="Server.hostname" value="{{.settings.Server.hostname}}">
</div></div>
<div class="col-md-6"><div class="mb-3"><label class="form-label">HELO Hostname</label>
<input type="text" class="form-control" name="Server.helo_hostname" 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>
<div class="setting-description">Custom SMTP banner (empty by default)</div>
<input type="text" class="form-control" name="Server.server_banner" value="{{.settings.Server.server_banner}}">
</div></div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-database me-2"></i>Database Configuration</h5></div>
<div class="card-body">
<div class="setting-section">
<div class="mb-3">
<label class="form-label">Database URL</label>
<div class="input-group mb-2">
<input type="text" class="form-control font-monospace" name="Database.database_url" id="databaseUrl" value="{{.settings.Database.database_url}}">
<button class="btn btn-primary" type="button" onclick="testDatabaseConnection()"><i class="bi bi-check-circle me-1"></i>Test Connection</button>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-journal-text me-2"></i>Logging Configuration</h5></div>
<div class="card-body">
<div class="setting-section">
<div class="row">
<div class="col-md-6"><div class="mb-3"><label class="form-label">Log Level</label>
<select class="form-select" name="Logging.log_level">
{{$lvl := .settings.Logging.log_level}}
{{range (list "DEBUG" "INFO" "WARNING" "ERROR" "CRITICAL")}}<option value="{{.}}" {{if eq . $lvl}}selected{{end}}>{{.}}</option>{{end}}
</select>
</div></div>
<div class="col-md-6"><div class="mb-3"><label class="form-label">Hide aiosmtpd-equivalent INFO Messages</label>
<select class="form-select" name="Logging.hide_info_aiosmtpd">
<option value="true" {{if eq .settings.Logging.hide_info_aiosmtpd "true"}}selected{{end}}>Yes</option>
<option value="false" {{if eq .settings.Logging.hide_info_aiosmtpd "false"}}selected{{end}}>No</option>
</select>
</div></div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-arrow-repeat me-2"></i>Email Relay Configuration</h5></div>
<div class="card-body">
<div class="setting-section">
<div class="mb-3"><label class="form-label">Relay Timeout (seconds)</label>
<input type="number" class="form-control" name="Relay.relay_timeout" value="{{.settings.Relay.relay_timeout}}" min="5" max="300">
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-inbox me-2"></i>IMAP Configuration</h5></div>
<div class="card-body">
<div class="setting-section">
<div class="row">
<div class="col-md-6"><div class="mb-3"><label class="form-label">IMAP Port</label>
<div class="setting-description">Plain IMAP port (no STARTTLS offered)</div>
<input type="number" class="form-control" name="IMAP.imap_port" value="{{.settings.IMAP.imap_port}}" min="1" max="65535">
</div></div>
<div class="col-md-6"><div class="mb-3"><label class="form-label">IMAP TLS Port</label>
<div class="setting-description">Implicit-TLS IMAP port (IMAPS)</div>
<input type="number" class="form-control" name="IMAP.imap_tls_port" value="{{.settings.IMAP.imap_tls_port}}" min="1" max="65535">
</div></div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-shield-exclamation me-2"></i>Rspamd (Spam Scoring)</h5></div>
<div class="card-body">
<div class="setting-section">
<div class="setting-description">Optional; the built-in heuristic spam score always runs regardless of this setting.</div>
<div class="row">
<div class="col-md-4"><div class="mb-3"><label class="form-label">Enabled</label>
<select class="form-select" name="Rspamd.enabled">
<option value="true" {{if eq .settings.Rspamd.enabled "true"}}selected{{end}}>Yes</option>
<option value="false" {{if eq .settings.Rspamd.enabled "false"}}selected{{end}}>No</option>
</select>
</div></div>
<div class="col-md-4"><div class="mb-3"><label class="form-label">Rspamd URL</label>
<input type="text" class="form-control" name="Rspamd.url" value="{{.settings.Rspamd.url}}" placeholder="http://127.0.0.1:11333">
</div></div>
<div class="col-md-4"><div class="mb-3"><label class="form-label">Reject Score</label>
<input type="number" class="form-control" name="Rspamd.reject_score" value="{{.settings.Rspamd.reject_score}}" min="1">
</div></div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-shield-check me-2"></i>Two-Factor Authentication (MFA)</h5></div>
<div class="card-body">
<div class="setting-section">
<div class="row">
<div class="col-md-6"><div class="mb-3"><label class="form-label">Enforce MFA for Admins</label>
<div class="setting-description">Require every admin account (global or domain-scoped) to set up TOTP/passkey MFA before using the dashboard</div>
<select class="form-select" name="Auth.enforce_admin_mfa">
<option value="true" {{if eq .settings.Auth.enforce_admin_mfa "true"}}selected{{end}}>Yes</option>
<option value="false" {{if eq .settings.Auth.enforce_admin_mfa "false"}}selected{{end}}>No</option>
</select>
</div></div>
<div class="col-md-6"><div class="mb-3"><label class="form-label">Enforce MFA for Mailboxes</label>
<div class="setting-description">Require MFA before a mailbox owner can create new app passwords in the self-service portal — overridable per-domain or per-mailbox</div>
<select class="form-select" name="Auth.enforce_mailbox_mfa">
<option value="true" {{if eq .settings.Auth.enforce_mailbox_mfa "true"}}selected{{end}}>Yes</option>
<option value="false" {{if eq .settings.Auth.enforce_mailbox_mfa "false"}}selected{{end}}>No</option>
</select>
</div></div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>TLS/SSL Configuration</h5></div>
<div class="card-body">
<div class="setting-section">
<div class="row">
<div class="col-md-6"><div class="mb-3"><label class="form-label">TLS Certificate File</label>
<div class="input-group">
<input type="text" class="form-control font-monospace" name="TLS.tls_cert_file" value="{{.settings.TLS.tls_cert_file}}">
<input type="file" class="d-none" id="certFileUpload" accept=".crt,.pem">
<button class="btn btn-outline-secondary" type="button" onclick="document.getElementById('certFileUpload').click()"><i class="bi bi-upload"></i></button>
</div>
</div></div>
<div class="col-md-6"><div class="mb-3"><label class="form-label">TLS Private Key File</label>
<div class="input-group">
<input type="text" class="form-control font-monospace" name="TLS.tls_key_file" value="{{.settings.TLS.tls_key_file}}">
<input type="file" class="d-none" id="keyFileUpload" accept=".key,.pem">
<button class="btn btn-outline-secondary" type="button" onclick="document.getElementById('keyFileUpload').click()"><i class="bi bi-upload"></i></button>
</div>
</div></div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-key me-2"></i>DKIM Configuration</h5></div>
<div class="card-body">
<div class="setting-section">
<div class="mb-3"><label class="form-label">DKIM Key Size</label>
<select class="form-select" name="DKIM.dkim_key_size">
{{$ks := .settings.DKIM.dkim_key_size}}
<option value="1024" {{if eq $ks "1024"}}selected{{end}}>1024 bits</option>
<option value="2048" {{if eq $ks "2048"}}selected{{end}}>2048 bits (Recommended)</option>
<option value="4096" {{if eq $ks "4096"}}selected{{end}}>4096 bits</option>
</select>
</div>
<div class="mb-3"><label class="form-label">SPF Server IP</label>
<div class="input-group">
<input type="text" class="form-control" name="DKIM.spf_server_ip" value="{{.settings.DKIM.spf_server_ip}}">
<button class="btn btn-danger" type="button" onclick="getPublicIP()"><i class="bi bi-cloud-download me-1"></i>Get Public IP</button>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-paperclip me-2"></i>Attachments Configuration</h5></div>
<div class="card-body">
<div class="setting-section">
<div class="mb-3"><label class="form-label">Attachments Storage Path</label>
<input type="text" class="form-control" name="Attachments.attachments_path" value="{{.settings.Attachments.attachments_path}}" placeholder="server_data/attachments">
<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>
</div>
</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="alert alert-warning d-flex align-items-center mb-0"><i class="bi bi-exclamation-triangle me-2"></i><small>Server restart required after changing settings</small></div>
<button type="submit" class="btn btn-primary btn-lg"><i class="bi bi-save me-2"></i>Save Settings</button>
</div>
</form>
</div>
{{end}}
{{define "extra_js"}}
<script>
function exportSettings() {
const settings = {};
const formData = new FormData(document.querySelector('form'));
for (let [key, value] of formData.entries()) {
const [section, setting] = key.split('.');
if (!settings[section]) { settings[section] = {}; }
settings[section][setting] = value;
}
let config = '';
for (const [section, values] of Object.entries(settings)) {
config += `[${section}]\n`;
for (const [key, value] of Object.entries(values)) { config += `${key} = ${value}\n`; }
config += '\n';
}
const element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(config));
element.setAttribute('download', 'settings.ini');
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
document.querySelector('form').addEventListener('submit', function(e) {
const ports = ['Server.smtp_port', 'Server.smtp_tls_port', 'Server.web_http_port', 'Server.web_https_port', 'IMAP.imap_port', 'IMAP.imap_tls_port'];
const seen = {};
for (const portField of ports) {
const input = document.querySelector(`[name="${portField}"]`);
const port = parseInt(input.value);
if (port < 1 || port > 65535) { e.preventDefault(); showToast(`Invalid port number: ${port}.`, 'danger'); input.focus(); return; }
if (seen[port]) { e.preventDefault(); showToast(`Port ${port} is used more than once — each port must be different.`, 'danger'); input.focus(); return; }
seen[port] = true;
}
const serverBanner = document.querySelector('[name="Server.server_banner"]');
if (serverBanner && !serverBanner.value.trim()) { serverBanner.value = '""'; }
const attachmentsPath = document.querySelector('input[name="Attachments.attachments_path"]');
if (!attachmentsPath.value.trim()) { e.preventDefault(); showToast('Please specify a valid attachments storage path', 'danger'); attachmentsPath.focus(); }
});
document.getElementById('certFileUpload').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('cert_file', file);
fetch('/pymta-manager/api/settings/upload_cert', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.status === 'success') { document.querySelector('[name="TLS.tls_cert_file"]').value = data.filepath; showToast('Certificate uploaded', 'success'); }
else { showToast(data.message || 'Failed to upload certificate', 'danger'); }
}).catch(() => showToast('Failed to upload certificate', 'danger'));
});
document.getElementById('keyFileUpload').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('key_file', file);
fetch('/pymta-manager/api/settings/upload_key', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.status === 'success') { document.querySelector('[name="TLS.tls_key_file"]').value = data.filepath; showToast('Key uploaded', 'success'); }
else { showToast(data.message || 'Failed to upload key', 'danger'); }
}).catch(() => showToast('Failed to upload key', 'danger'));
});
function testDatabaseConnection() {
const url = document.getElementById('databaseUrl').value;
fetch('/pymta-manager/api/settings/test_database', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({url}) })
.then(r => r.json())
.then(data => showToast(data.message || (data.status === 'success' ? 'Connection successful!' : 'Failed to connect'), data.status === 'success' ? 'success' : 'danger'))
.catch(() => showToast('Failed to test database connection', 'danger'));
}
function getPublicIP() {
fetch('/pymta-manager/api/settings/get_public_ip')
.then(r => r.json())
.then(data => {
if (data.ip) { document.querySelector('[name="DKIM.spf_server_ip"]').value = data.ip; showToast('Public IP fetched', 'success'); }
else { showToast('Failed to fetch public IP', 'danger'); }
}).catch(() => showToast('Failed to fetch public IP', 'danger'));
}
function validateAttachmentsPath() {
const path = document.querySelector('input[name="Attachments.attachments_path"]').value;
const feedback = document.getElementById('attachments-path-feedback');
if (!feedback) return;
fetch('/pymta-manager/test_attachments_path', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: `path=${encodeURIComponent(path)}` })
.then(r => r.json())
.then(data => {
feedback.innerHTML = data.message + (data.success ? `<br><small class="text-muted">Absolute path: ${data.absolute_path}</small>` : '');
feedback.className = data.success ? 'text-success mt-2' : 'text-danger mt-2';
}).catch(error => { feedback.innerHTML = `Error validating path: ${error}`; feedback.className = 'text-danger mt-2'; });
}
document.querySelector('input[name="Attachments.attachments_path"]')?.addEventListener('change', validateAttachmentsPath);
</script>
{{end}}