2025-06-07 10:48:03 +01:00
|
|
|
{% extends "base.html" %}
|
|
|
|
|
|
|
|
|
|
{% block title %}Server Settings - Email Server{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block 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;
|
|
|
|
|
}
|
2025-06-08 22:51:07 +01:00
|
|
|
.card-header {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
.card-header .bi-chevron-down {
|
|
|
|
|
transition: transform 0.2s;
|
|
|
|
|
}
|
|
|
|
|
.card-header.collapsed .bi-chevron-down {
|
|
|
|
|
transform: rotate(-90deg);
|
|
|
|
|
}
|
2025-06-07 10:48:03 +01:00
|
|
|
</style>
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block 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">
|
2025-06-08 11:13:43 +01:00
|
|
|
<button type="button" class="btn btn-outline-danger me-2" onclick="restartSmtpServer()" id="restart-smtp-btn">
|
|
|
|
|
<i class="bi bi-arrow-repeat me-2"></i>
|
|
|
|
|
Restart SMTP Server
|
|
|
|
|
</button>
|
2025-06-07 10:48:03 +01:00
|
|
|
<button type="button" class="btn btn-outline-warning" onclick="resetToDefaults()">
|
|
|
|
|
<i class="bi bi-arrow-clockwise me-2"></i>
|
|
|
|
|
Reset to Defaults
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" class="btn btn-outline-info" onclick="exportSettings()">
|
|
|
|
|
<i class="bi bi-download me-2"></i>
|
|
|
|
|
Export Config
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-06-08 22:51:07 +01:00
|
|
|
<form method="POST" action="{{ url_for('email.settings_update') }}" id="settingsForm">
|
|
|
|
|
<!-- Add CSRF token if enabled -->
|
|
|
|
|
{% if csrf_token %}
|
|
|
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
|
|
|
{% endif %}
|
2025-06-07 10:48:03 +01:00
|
|
|
<!-- Server Settings -->
|
|
|
|
|
<div class="card mb-4">
|
2025-06-08 22:51:07 +01:00
|
|
|
<div class="card-header" data-bs-toggle="collapse" data-bs-target="#serverSettings" aria-expanded="true">
|
|
|
|
|
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
|
|
|
|
<span><i class="bi bi-server me-2"></i>Server Configuration</span>
|
|
|
|
|
<i class="bi bi-chevron-down"></i>
|
2025-06-07 10:48:03 +01:00
|
|
|
</h5>
|
|
|
|
|
</div>
|
2025-06-08 22:51:07 +01:00
|
|
|
<div id="serverSettings" class="collapse show">
|
|
|
|
|
<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 SMTP unencrypted connections (standard: 25)</div>
|
|
|
|
|
<input type="number"
|
|
|
|
|
class="form-control"
|
|
|
|
|
name="Server.smtp_port"
|
|
|
|
|
value="{{ settings['Server']['smtp_port'] }}"
|
|
|
|
|
min="1" max="65535">
|
|
|
|
|
</div>
|
2025-06-07 10:48:03 +01:00
|
|
|
</div>
|
2025-06-08 22:51:07 +01:00
|
|
|
<div class="col-md-6">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">SMTP STARTTLS Port</label>
|
|
|
|
|
<div class="setting-description">Port for SMTP STARTTLS connections (standard: 587)</div>
|
|
|
|
|
<input type="number"
|
|
|
|
|
class="form-control"
|
|
|
|
|
name="Server.smtp_tls_port"
|
|
|
|
|
value="{{ settings['Server']['smtp_tls_port'] }}"
|
|
|
|
|
min="1" max="65535">
|
|
|
|
|
</div>
|
2025-06-07 10:48:03 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-08 22:51:07 +01:00
|
|
|
<div class="row">
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">Bind IP Address</label>
|
|
|
|
|
<div class="setting-description">IP address to bind SMTP server only to (0.0.0.0 for all interfaces)</div>
|
|
|
|
|
<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]?)$">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">Hostname</label>
|
|
|
|
|
<div class="setting-description">Server hostname for HELO/EHLO commands</div>
|
|
|
|
|
<input type="text"
|
|
|
|
|
class="form-control"
|
|
|
|
|
name="Server.hostname"
|
|
|
|
|
value="{{ settings['Server']['hostname'] }}">
|
|
|
|
|
</div>
|
2025-06-07 10:48:03 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-08 22:51:07 +01:00
|
|
|
<div class="row">
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">HELO Hostname</label>
|
|
|
|
|
<div class="setting-description">Override HELO hostname for SMTP identification</div>
|
|
|
|
|
<input type="text"
|
|
|
|
|
class="form-control"
|
|
|
|
|
name="Server.helo_hostname"
|
|
|
|
|
value="{{ settings['Server']['helo_hostname'] }}">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">Server Banner</label>
|
|
|
|
|
<div class="setting-description">Custom SMTP server banner (empty by default - hides SMTP version)</div>
|
|
|
|
|
<input type="text"
|
|
|
|
|
class="form-control"
|
|
|
|
|
name="Server.server_banner"
|
|
|
|
|
value="{{ settings['Server']['server_banner'] }}">
|
|
|
|
|
</div>
|
2025-06-07 10:48:03 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Database Settings -->
|
|
|
|
|
<div class="card mb-4">
|
2025-06-08 22:51:07 +01:00
|
|
|
<div class="card-header collapsed" data-bs-toggle="collapse" data-bs-target="#databaseSettings" aria-expanded="false">
|
|
|
|
|
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
|
|
|
|
<span><i class="bi bi-database me-2"></i>Database Configuration</span>
|
|
|
|
|
<i class="bi bi-chevron-down"></i>
|
2025-06-07 10:48:03 +01:00
|
|
|
</h5>
|
|
|
|
|
</div>
|
2025-06-08 22:51:07 +01:00
|
|
|
<div id="databaseSettings" class="collapse">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="setting-section">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">Database URL</label>
|
|
|
|
|
<div class="setting-description">Database connection string</div>
|
|
|
|
|
<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 class="mt-3">
|
|
|
|
|
<label class="form-label">Example Connection Strings:</label>
|
|
|
|
|
<div class="list-group">
|
|
|
|
|
<button type="button" class="list-group-item list-group-item-action" onclick="setDatabaseExample('sqlite')">
|
|
|
|
|
<strong>SQLite:</strong> sqlite:///email_server/server_data/smtp_server.db
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" class="list-group-item list-group-item-action" onclick="setDatabaseExample('mysql')">
|
|
|
|
|
<strong>MySQL:</strong> mysql://user:password@localhost:3306/dbname
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" class="list-group-item list-group-item-action" onclick="setDatabaseExample('postgresql')">
|
|
|
|
|
<strong>PostgreSQL:</strong> postgresql://user:password@localhost:5432/dbname
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" class="list-group-item list-group-item-action" onclick="setDatabaseExample('mssql')">
|
|
|
|
|
<strong>MSSQL:</strong> mssql+pyodbc://user:password@server:1433/dbname?driver=ODBC+Driver+17+for+SQL+Server
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-07 10:48:03 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Logging Settings -->
|
|
|
|
|
<div class="card mb-4">
|
2025-06-08 22:51:07 +01:00
|
|
|
<div class="card-header collapsed" data-bs-toggle="collapse" data-bs-target="#loggingSettings" aria-expanded="false">
|
|
|
|
|
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
|
|
|
|
<span><i class="bi bi-journal-text me-2"></i>Logging Configuration</span>
|
|
|
|
|
<i class="bi bi-chevron-down"></i>
|
2025-06-07 10:48:03 +01:00
|
|
|
</h5>
|
|
|
|
|
</div>
|
2025-06-08 22:51:07 +01:00
|
|
|
<div id="loggingSettings" class="collapse">
|
|
|
|
|
<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>
|
|
|
|
|
<div class="setting-description">Minimum log level to record</div>
|
|
|
|
|
<select class="form-select" name="Logging.log_level">
|
|
|
|
|
<option value="DEBUG" {{ 'selected' if settings['Logging']['log_level'] == 'DEBUG' else '' }}>DEBUG</option>
|
|
|
|
|
<option value="INFO" {{ 'selected' if settings['Logging']['log_level'] == 'INFO' else '' }}>INFO</option>
|
|
|
|
|
<option value="WARNING" {{ 'selected' if settings['Logging']['log_level'] == 'WARNING' else '' }}>WARNING</option>
|
|
|
|
|
<option value="ERROR" {{ 'selected' if settings['Logging']['log_level'] == 'ERROR' else '' }}>ERROR</option>
|
|
|
|
|
<option value="CRITICAL" {{ 'selected' if settings['Logging']['log_level'] == 'CRITICAL' else '' }}>CRITICAL</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
2025-06-07 10:48:03 +01:00
|
|
|
</div>
|
2025-06-08 22:51:07 +01:00
|
|
|
<div class="col-md-6">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">Hide aiosmtpd INFO Messages</label>
|
|
|
|
|
<div class="setting-description">Reduce verbose logging from aiosmtpd library</div>
|
|
|
|
|
<select class="form-select" name="Logging.hide_info_aiosmtpd">
|
|
|
|
|
<option value="true" {{ 'selected' if settings['Logging']['hide_info_aiosmtpd'] == 'true' else '' }}>Yes</option>
|
|
|
|
|
<option value="false" {{ 'selected' if settings['Logging']['hide_info_aiosmtpd'] == 'false' else '' }}>No</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
2025-06-07 10:48:03 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Relay Settings -->
|
|
|
|
|
<div class="card mb-4">
|
2025-06-08 22:51:07 +01:00
|
|
|
<div class="card-header collapsed" data-bs-toggle="collapse" data-bs-target="#relaySettings" aria-expanded="false">
|
|
|
|
|
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
|
|
|
|
<span><i class="bi bi-arrow-repeat me-2"></i>Email Relay Configuration</span>
|
|
|
|
|
<i class="bi bi-chevron-down"></i>
|
2025-06-07 10:48:03 +01:00
|
|
|
</h5>
|
|
|
|
|
</div>
|
2025-06-08 22:51:07 +01:00
|
|
|
<div id="relaySettings" class="collapse">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="setting-section">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">Relay Timeout (seconds)</label>
|
|
|
|
|
<div class="setting-description">Timeout for external SMTP connections when relaying emails</div>
|
|
|
|
|
<input type="number"
|
|
|
|
|
class="form-control"
|
|
|
|
|
name="Relay.relay_timeout"
|
|
|
|
|
value="{{ settings['Relay']['relay_timeout'] }}"
|
|
|
|
|
min="5" max="300">
|
|
|
|
|
</div>
|
2025-06-07 10:48:03 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- TLS Settings -->
|
|
|
|
|
<div class="card mb-4">
|
2025-06-08 22:51:07 +01:00
|
|
|
<div class="card-header collapsed" data-bs-toggle="collapse" data-bs-target="#tlsSettings" aria-expanded="false">
|
|
|
|
|
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
|
|
|
|
<span><i class="bi bi-shield-lock me-2"></i>TLS/SSL Configuration</span>
|
|
|
|
|
<i class="bi bi-chevron-down"></i>
|
2025-06-07 10:48:03 +01:00
|
|
|
</h5>
|
|
|
|
|
</div>
|
2025-06-08 22:51:07 +01:00
|
|
|
<div id="tlsSettings" class="collapse">
|
|
|
|
|
<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="setting-description">Path to SSL certificate file (.crt or .pem)</div>
|
|
|
|
|
<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>
|
2025-06-07 10:48:03 +01:00
|
|
|
</div>
|
2025-06-08 22:51:07 +01:00
|
|
|
<div class="col-md-6">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">TLS Private Key File</label>
|
|
|
|
|
<div class="setting-description">Path to SSL private key file (.key or .pem)</div>
|
|
|
|
|
<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>
|
2025-06-07 10:48:03 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- DKIM Settings -->
|
|
|
|
|
<div class="card mb-4">
|
2025-06-08 22:51:07 +01:00
|
|
|
<div class="card-header collapsed" data-bs-toggle="collapse" data-bs-target="#dkimSettings" aria-expanded="false">
|
|
|
|
|
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
|
|
|
|
<span><i class="bi bi-key me-2"></i>DKIM Configuration</span>
|
|
|
|
|
<i class="bi bi-chevron-down"></i>
|
2025-06-07 10:48:03 +01:00
|
|
|
</h5>
|
|
|
|
|
</div>
|
2025-06-08 22:51:07 +01:00
|
|
|
<div id="dkimSettings" class="collapse">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="setting-section">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">DKIM Key Size</label>
|
|
|
|
|
<div class="setting-description">RSA key size for new DKIM keys (larger = more secure, slower)</div>
|
|
|
|
|
<select class="form-select" name="DKIM.dkim_key_size">
|
|
|
|
|
<option value="1024" {{ 'selected' if settings['DKIM']['dkim_key_size'] == '1024' else '' }}>1024 bits</option>
|
|
|
|
|
<option value="2048" {{ 'selected' if settings['DKIM']['dkim_key_size'] == '2048' else '' }}>2048 bits (Recommended)</option>
|
|
|
|
|
<option value="4096" {{ 'selected' if settings['DKIM']['dkim_key_size'] == '4096' else '' }}>4096 bits</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">SPF Server IP</label>
|
|
|
|
|
<div class="setting-description">Public IP address of server for SPF records (used if auto-detection fails)</div>
|
|
|
|
|
<div class="input-group">
|
|
|
|
|
<input type="text"
|
|
|
|
|
class="form-control"
|
|
|
|
|
name="DKIM.spf_server_ip"
|
|
|
|
|
value="{{ settings['DKIM']['spf_server_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]?)$">
|
|
|
|
|
<button class="btn btn-danger" type="button" onclick="getPublicIP()">
|
|
|
|
|
<i class="bi bi-cloud-download me-1"></i>
|
|
|
|
|
Get Public IP
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-07 10:48:03 +01:00
|
|
|
</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">
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
<!-- Reset Confirmation Modal -->
|
|
|
|
|
<div class="modal fade" id="resetModal" tabindex="-1">
|
|
|
|
|
<div class="modal-dialog">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h5 class="modal-title">Reset Settings</h5>
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<p>Are you sure you want to reset all settings to their default values?</p>
|
|
|
|
|
<p class="text-warning"><strong>Warning:</strong> This action cannot be undone.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
|
|
|
<button type="button" class="btn btn-warning" onclick="confirmReset()">Reset Settings</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block extra_js %}
|
|
|
|
|
<script>
|
|
|
|
|
function resetToDefaults() {
|
|
|
|
|
new bootstrap.Modal(document.getElementById('resetModal')).show();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function confirmReset() {
|
|
|
|
|
// This would need to be implemented as a separate endpoint
|
|
|
|
|
// For now, just redirect to a reset URL
|
|
|
|
|
window.location.href = '{{ url_for("email.settings") }}?reset=true';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function exportSettings() {
|
|
|
|
|
// Create a downloadable config file
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const configText = generateConfigFile(settings);
|
|
|
|
|
downloadFile('settings.ini', configText);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generateConfigFile(settings) {
|
|
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function downloadFile(filename, content) {
|
|
|
|
|
const element = document.createElement('a');
|
|
|
|
|
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content));
|
|
|
|
|
element.setAttribute('download', filename);
|
|
|
|
|
element.style.display = 'none';
|
|
|
|
|
document.body.appendChild(element);
|
|
|
|
|
element.click();
|
|
|
|
|
document.body.removeChild(element);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Form validation
|
|
|
|
|
document.querySelector('form').addEventListener('submit', function(e) {
|
|
|
|
|
// Basic validation
|
2025-06-08 22:51:07 +01:00
|
|
|
const ports = ['Server.smtp_port', 'Server.smtp_tls_port'];
|
2025-06-07 10:48:03 +01:00
|
|
|
for (const portField of ports) {
|
|
|
|
|
const input = document.querySelector(`[name="${portField}"]`);
|
|
|
|
|
const port = parseInt(input.value);
|
|
|
|
|
if (port < 1 || port > 65535) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
alert(`Invalid port number: ${port}. Must be between 1 and 65535.`);
|
|
|
|
|
input.focus();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if ports are different
|
2025-06-08 22:51:07 +01:00
|
|
|
const smtpPort = document.querySelector('[name="Server.smtp_port"]').value;
|
|
|
|
|
const tlsPort = document.querySelector('[name="Server.smtp_tls_port"]').value;
|
2025-06-07 10:48:03 +01:00
|
|
|
if (smtpPort === tlsPort) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
alert('SMTP and TLS ports must be different.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-06-08 11:13:43 +01:00
|
|
|
|
|
|
|
|
function restartSmtpServer() {
|
|
|
|
|
showConfirmation("Are you sure you want to restart the SMTP server?", "Restart SMTP Server", "Restart", "btn-danger").then(confirmed => {
|
|
|
|
|
if (!confirmed) return;
|
|
|
|
|
const btn = document.getElementById('restart-smtp-btn');
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Restarting...';
|
|
|
|
|
fetch('/api/server/restart', {method: 'POST'})
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
.then(data => {
|
|
|
|
|
if (data.status === 'success') {
|
|
|
|
|
showToast(data.message || 'SMTP server restarted.', 'success');
|
|
|
|
|
} else {
|
|
|
|
|
showToast(data.message || 'Failed to restart SMTP server.', 'danger');
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => showToast('Failed to restart SMTP server.', 'danger'))
|
|
|
|
|
.finally(() => {
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
btn.innerHTML = '<i class="bi bi-arrow-repeat me-2"></i> Restart SMTP Server';
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-08 22:51:07 +01:00
|
|
|
|
|
|
|
|
// Handle certificate file uploads
|
|
|
|
|
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('{{ url_for("email.upload_cert") }}', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: formData
|
|
|
|
|
})
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
.then(data => {
|
|
|
|
|
if (data.status === 'success') {
|
|
|
|
|
document.querySelector('[name="TLS.tls_cert_file"]').value = data.filepath;
|
|
|
|
|
showToast('Certificate file uploaded successfully', 'success');
|
|
|
|
|
} else {
|
|
|
|
|
showToast(data.message || 'Failed to upload certificate file', 'danger');
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => showToast('Failed to upload certificate file', '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('{{ url_for("email.upload_key") }}', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: formData
|
|
|
|
|
})
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
.then(data => {
|
|
|
|
|
if (data.status === 'success') {
|
|
|
|
|
document.querySelector('[name="TLS.tls_key_file"]').value = data.filepath;
|
|
|
|
|
showToast('Key file uploaded successfully', 'success');
|
|
|
|
|
} else {
|
|
|
|
|
showToast(data.message || 'Failed to upload key file', 'danger');
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => showToast('Failed to upload key file', 'danger'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle database examples
|
|
|
|
|
function setDatabaseExample(type) {
|
|
|
|
|
const urlInput = document.getElementById('databaseUrl');
|
|
|
|
|
switch(type) {
|
|
|
|
|
case 'sqlite':
|
|
|
|
|
urlInput.value = 'sqlite:///email_server/server_data/smtp_server.db';
|
|
|
|
|
break;
|
|
|
|
|
case 'mysql':
|
|
|
|
|
urlInput.value = 'mysql://user:password@localhost:3306/dbname';
|
|
|
|
|
break;
|
|
|
|
|
case 'postgresql':
|
|
|
|
|
urlInput.value = 'postgresql://user:password@localhost:5432/dbname';
|
|
|
|
|
break;
|
|
|
|
|
case 'mssql':
|
|
|
|
|
urlInput.value = 'mssql+pyodbc://user:password@server:1433/dbname?driver=ODBC+Driver+17+for+SQL+Server';
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test database connection
|
|
|
|
|
function testDatabaseConnection() {
|
|
|
|
|
const url = document.getElementById('databaseUrl').value;
|
|
|
|
|
fetch('{{ url_for("email.test_database_connection_endpoint") }}', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'Accept': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ url: url })
|
|
|
|
|
})
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
.then(data => {
|
|
|
|
|
showToast(data.message || (data.status === 'success' ? 'Database connection successful!' : 'Failed to connect to database'),
|
|
|
|
|
data.status === 'success' ? 'success' : 'danger');
|
|
|
|
|
})
|
|
|
|
|
.catch(() => showToast('Failed to test database connection', 'danger'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get public IP
|
|
|
|
|
function getPublicIP() {
|
|
|
|
|
fetch('{{ url_for("email.get_server_ip") }}')
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
.then(data => {
|
|
|
|
|
if (data.ip) {
|
|
|
|
|
document.querySelector('[name="DKIM.spf_server_ip"]').value = data.ip;
|
|
|
|
|
showToast('Public IP fetched successfully', 'success');
|
|
|
|
|
} else {
|
|
|
|
|
showToast('Failed to fetch public IP', 'danger');
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => showToast('Failed to fetch public IP', 'danger'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle form submission
|
|
|
|
|
document.getElementById('settingsForm').addEventListener('submit', function(e) {
|
|
|
|
|
// Handle empty server banner
|
|
|
|
|
const serverBanner = document.querySelector('[name="Server.server_banner"]');
|
|
|
|
|
if (serverBanner && !serverBanner.value.trim()) {
|
|
|
|
|
serverBanner.value = '""';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log form data being submitted
|
|
|
|
|
const formData = new FormData(this);
|
|
|
|
|
console.log('Submitting settings with data:');
|
|
|
|
|
for (let [key, value] of formData.entries()) {
|
|
|
|
|
console.log(`${key}: ${value}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-06-07 10:48:03 +01:00
|
|
|
</script>
|
|
|
|
|
{% endblock %}
|