scripts for setup, nginx and service, fixing issue with server shutdown when both are running

This commit is contained in:
nahakubuilde
2025-06-08 11:13:43 +01:00
parent 89ab6b218e
commit a7e41ad231
18 changed files with 808 additions and 358 deletions

View File

@@ -33,7 +33,7 @@ from email_server.models import (
hash_password, create_tables, get_user_by_email, get_domain_by_name, get_whitelisted_ip
)
from email_server.dkim_manager import DKIMManager
from email_server.settings_loader import load_settings, generate_settings_ini, SETTINGS_PATH
from email_server.settings_loader import load_settings, SETTINGS_PATH
from email_server.tool_box import get_logger
logger = get_logger()
@@ -42,7 +42,7 @@ logger = get_logger()
email_bp = Blueprint('email', __name__,
template_folder='templates',
static_folder='static',
url_prefix='/email')
url_prefix='/pymta-manager')
def get_public_ip() -> str:
"""Get the public IP address of the server."""
@@ -102,29 +102,33 @@ def check_dns_record(domain: str, record_type: str, expected_value: str = None)
return {'success': False, 'message': f'DNS lookup error: {str(e)}'}
def generate_spf_record(domain: str, public_ip: str, existing_spf: str = None) -> str:
"""Generate SPF record including the current server IP."""
base_mechanisms = []
"""Generate or update SPF record to include the current server IP."""
if not public_ip or public_ip == 'unknown':
return f'"{existing_spf or "v=spf1 ~all"}"'
our_ip = f"ip4:{public_ip}"
if existing_spf:
# Parse existing SPF record
spf_clean = existing_spf.replace('"', '').strip()
if spf_clean.startswith('v=spf1'):
parts = spf_clean.split()
base_mechanisms = [part for part in parts[1:] if not part.startswith('ip4:') and part != 'all' and part != '-all' and part != '~all']
# Add our server IP if it's not unknown
if public_ip and public_ip != 'unknown':
our_ip = f"ip4:{public_ip}"
if our_ip not in base_mechanisms:
base_mechanisms.append(our_ip)
# If no IP available, just use existing mechanisms
if not base_mechanisms and public_ip == 'unknown':
return existing_spf or 'v=spf1 ~all'
# Construct SPF record
spf_parts = ['v=spf1'] + base_mechanisms + ['~all']
return ' '.join(spf_parts)
if not spf_clean.startswith('v=spf1'):
spf_clean = f"v=spf1 {spf_clean}"
parts = spf_clean.split()
if our_ip in parts:
return f'Current SPF records includes already server ip {public_ip}'
# Find position of the final all mechanism (if present)
all_mechanism_index = next((i for i, part in enumerate(parts) if part in ['-all', '~all', '?all', 'all']), None)
if all_mechanism_index is not None:
new_parts = parts[:all_mechanism_index] + [our_ip] + parts[all_mechanism_index:]
else:
new_parts = parts + [our_ip, '~all']
return f'"{" ".join(new_parts)}"'
else:
# No existing SPF, create a new one
return f'"v=spf1 {our_ip} ~all"'
# Dashboard and Main Routes
@email_bp.route('/')

View File

@@ -4,6 +4,30 @@
{% block content %}
<div class="container-fluid">
<!-- Current IP Detection -->
<div class="row">
<div class="col-md-8 mx-auto">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-geo-alt me-2"></i>
Your Current IP
</h6>
</div>
<div class="card-body text-center">
<div class="fw-bold font-monospace fs-5 mb-2" id="current-ip">
<span class="spinner-border spinner-border-sm me-2"></span>
Detecting...
</div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="useCurrentIP()">
<i class="bi bi-arrow-up me-1"></i>
Use This IP
</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8 mx-auto">
<div class="card">
@@ -73,113 +97,7 @@
</div>
</div>
<!-- Current IP Detection and Available Domains -->
<div class="row mt-4">
<div class="col-md-4 mx-auto">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-geo-alt me-2"></i>
Your Current IP
</h6>
</div>
<div class="card-body text-center">
<div class="fw-bold font-monospace fs-5 mb-2" id="current-ip">
<span class="spinner-border spinner-border-sm me-2"></span>
Detecting...
</div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="useCurrentIP()">
<i class="bi bi-arrow-up me-1"></i>
Use This IP
</button>
</div>
</div>
</div>
{% if domains %}
<div class="col-md-4 mx-auto">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-server me-2"></i>
Available Domains
</h6>
</div>
<div class="card-body">
{% for domain in domains %}
<div class="mb-2">
<div class="fw-bold">{{ domain.domain_name }}</div>
<small class="text-muted">
Created: {{ domain.created_at.strftime('%Y-%m-%d') }}
</small>
</div>
{% if not loop.last %}<hr class="my-2">{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endif %}
</div>
<!-- Example Use Cases -->
<div class="row mt-4">
<div class="col-md-8 mx-auto">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-lightbulb me-2"></i>
Common Use Cases
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-primary">
<i class="bi bi-server me-1"></i>
Application Servers
</h6>
<p class="text-muted small">
Web applications that need to send transactional emails
(password resets, notifications, etc.)
</p>
</div>
<div class="col-md-6">
<h6 class="text-success">
<i class="bi bi-clock me-1"></i>
Scheduled Tasks
</h6>
<p class="text-muted small">
Cron jobs or scheduled scripts that send automated
reports or alerts
</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h6 class="text-warning">
<i class="bi bi-monitor me-1"></i>
Monitoring Systems
</h6>
<p class="text-muted small">
Monitoring tools that send alerts and status updates
to administrators
</p>
</div>
<div class="col-md-6">
<h6 class="text-info">
<i class="bi bi-cloud me-1"></i>
Cloud Services
</h6>
<p class="text-muted small">
Cloud-based applications or services that need to
send emails on behalf of your domain
</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
@@ -187,15 +105,22 @@
// Detect current IP address
async function detectCurrentIP() {
try {
const response = await fetch('https://api.ipify.org?format=json');
const response = await fetch('https://ifconfig.me/all.json');
const data = await response.json();
document.getElementById('current-ip').innerHTML =
`<span class="text-primary">${data.ip}</span>`;
} catch (error) {
document.getElementById('current-ip').innerHTML =
`<span class="text-primary">${data.ip_addr}</span>`;
} catch (er) {
try {
const response = await fetch('https://httpbin.org/ip');
const data = await response.json();
document.getElementById('current-ip').innerHTML =
`<span class="text-primary">${data.origin}</span>`;
} catch (error) {
document.getElementById('current-ip').innerHTML =
'<span class="text-muted">Unable to detect</span>';
}
}
}
function useCurrentIP() {
const currentIPElement = document.getElementById('current-ip');

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Add User - Email Server{% endblock %}
{% block title %}Add Sender - Email Server{% endblock %}
{% block content %}
<div class="container-fluid">
@@ -10,7 +10,7 @@
<div class="card-header">
<h4 class="mb-0">
<i class="bi bi-person-plus me-2"></i>
Add New User
Add New Sender
</h4>
</div>
<div class="card-body">
@@ -50,7 +50,7 @@
{% endfor %}
</select>
<div class="form-text">
The domain this user belongs to
The domain this sender belongs to
</div>
</div>
@@ -61,11 +61,11 @@
id="can_send_as_domain"
name="can_send_as_domain">
<label class="form-check-label" for="can_send_as_domain">
<strong>Domain Administrator</strong>
<strong>Domain Sender</strong>
</label>
<div class="form-text">
If checked, user can send emails as any address in their domain.
Otherwise, user can only send as their own email address.
If checked, sender can send emails as any address in their domain.
Otherwise, sender can only send as their own email address.
</div>
</div>
</div>
@@ -76,19 +76,19 @@
Permission Levels
</h6>
<ul class="mb-0">
<li><strong>Regular User:</strong> Can only send emails from their own email address</li>
<li><strong>Domain Admin:</strong> Can send emails from any address in their domain (e.g., noreply@domain.com, support@domain.com)</li>
<li><strong>Regular Sender:</strong> Can only send emails from their own email address</li>
<li><strong>Domain Sender:</strong> Can send emails from any address in their domain (e.g., noreply@domain.com, support@domain.com)</li>
</ul>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('email.users_list') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left me-2"></i>
Back to Users
Back to Senders
</a>
<button type="submit" class="btn btn-success">
<i class="bi bi-person-plus me-2"></i>
Add User
Add Sender
</button>
</div>
</form>
@@ -98,35 +98,6 @@
</div>
</div>
<!-- Domain information sidebar -->
<div class="row mt-4">
{% if domains %}
<div class="col-md-6 mx-auto">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-info-circle me-2"></i>
Available Domains
</h6>
</div>
<div class="card-body">
<div class="row">
{% for domain in domains %}
<div class="col-md-6 mb-2">
<div class="border rounded p-2">
<div class="fw-bold">{{ domain.domain_name }}</div>
<small class="text-muted">
Created: {{ domain.created_at.strftime('%Y-%m-%d') }}
</small>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}

View File

@@ -190,12 +190,12 @@
{% if item.existing_spf %}
<div class="mb-2">
<strong>Current SPF:</strong>
<div class="dns-record text-info">{{ item.existing_spf }}</div>
<div class="dns-record">{{ item.existing_spf }}</div>
</div>
{% endif %}
<div class="mb-2">
<strong>Recommended SPF:</strong>
<div class="dns-record text-success">{{ item.recommended_spf }}</div>
<div class="dns-record">{{ item.recommended_spf }}</div>
</div>
<button class="btn btn-outline-secondary btn-sm" onclick="copyToClipboard('{{ item.recommended_spf }}')">
<i class="bi bi-clipboard me-1"></i>

View File

@@ -57,7 +57,7 @@
name="can_send_as_domain"
{% if user.can_send_as_domain %}checked{% endif %}>
<label class="form-check-label" for="can_send_as_domain">
<strong>Can send as domain</strong>
<strong>Can send as any email from domain</strong>
</label>
<div class="form-text">
Allow this user to send emails using any address within their domain

View File

@@ -200,10 +200,10 @@
// Detect current IP address
async function detectCurrentIP() {
try {
const response = await fetch('https://api.ipify.org?format=json');
const response = await fetch('https://ifconfig.me/all.json');
const data = await response.json();
document.getElementById('current-ip').innerHTML =
`<span class="text-primary">${data.ip}</span>`;
`<span class="text-primary">${data.ip_addr}</span>`;
} catch (error) {
document.getElementById('current-ip').innerHTML =
'<span class="text-muted">Unable to detect</span>';

View File

@@ -34,7 +34,7 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="bi bi-journal-text me-2"></i>
Server Logs
Emails Log
</h2>
<div class="btn-group">
<a href="{{ url_for('email.logs', type='all') }}"

View File

@@ -25,6 +25,10 @@
Server Settings
</h2>
<div class="btn-group">
<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>
<button type="button" class="btn btn-outline-warning" onclick="resetToDefaults()">
<i class="bi bi-arrow-clockwise me-2"></i>
Reset to Defaults
@@ -351,5 +355,28 @@
return;
}
});
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';
});
});
}
</script>
{% endblock %}

View File

@@ -26,7 +26,7 @@
<li class="nav-item mb-2">
<h6 class="text-muted text-uppercase small mb-2 mt-3">
<i class="bi bi-globe me-1"></i>
Domain Management
Email Server Management
</h6>
</li>
@@ -38,20 +38,12 @@
<span class="badge bg-secondary ms-auto">{{ domain_count if domain_count is defined else '' }}</span>
</a>
</li>
<!-- Authentication Section -->
<li class="nav-item mb-2">
<h6 class="text-muted text-uppercase small mb-2 mt-3">
<i class="bi bi-shield-lock me-1"></i>
Authentication
</h6>
</li>
<li class="nav-item mb-1">
<a href="{{ url_for('email.users_list') }}"
class="nav-link text-white {{ 'active' if request.endpoint in ['email.users_list', 'email.add_user'] else '' }}">
<i class="bi bi-people me-2"></i>
Users
Allowed Senders
<span class="badge bg-secondary ms-auto">{{ user_count if user_count is defined else '' }}</span>
</a>
</li>
@@ -65,14 +57,6 @@
</a>
</li>
<!-- DKIM Section -->
<li class="nav-item mb-2">
<h6 class="text-muted text-uppercase small mb-2 mt-3">
<i class="bi bi-key me-1"></i>
Email Security
</h6>
</li>
<li class="nav-item mb-1">
<a href="{{ url_for('email.dkim_list') }}"
class="nav-link text-white {{ 'active' if request.endpoint == 'email.dkim_list' else '' }}">
@@ -81,7 +65,24 @@
<span class="badge bg-secondary ms-auto">{{ dkim_count if dkim_count is defined else '' }}</span>
</a>
</li>
<li class="nav-item mb-1">
<a href="{{ url_for('email.logs') }}"
class="nav-link text-white {{ 'active' if request.endpoint == 'email.logs' else '' }}">
<i class="bi bi-journal-text me-2"></i>
Emails Log
</a>
</li>
<!-- Authentication Section -->
<li class="nav-item mb-2">
<h6 class="text-muted text-uppercase small mb-2 mt-3">
<i class="bi bi-shield-lock me-1"></i>
Authentication
</h6>
</li>
<!-- Configuration Section -->
<li class="nav-item mb-2">
<h6 class="text-muted text-uppercase small mb-2 mt-3">

View File

@@ -1,25 +1,78 @@
{% extends "base.html" %}
{% block title %}Users - Email Server Management{% endblock %}
{% block page_title %}User Management{% endblock %}
{% block title %}Senders - Email Server Management{% endblock %}
{% block page_title %}Sender Management{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="bi bi-people me-2"></i>
Users
Senders
</h2>
<a href="{{ url_for('email.add_user') }}" class="btn btn-primary">
<i class="bi bi-person-plus me-2"></i>
Add User
Add Sender
</a>
</div>
{% if users %}
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-info-circle me-2"></i>
Sender Statistics
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Active Senders:</strong> {{ users|selectattr('0.is_active')|list|length }}
</li>
<li class="mb-2">
<i class="bi bi-star text-warning me-2"></i>
<strong>Domain Sender:</strong> {{ users|selectattr('0.can_send_as_domain')|list|length }}
</li>
<li>
<i class="bi bi-person text-info me-2"></i>
<strong>Regular Senders:</strong> {{ users|rejectattr('0.can_send_as_domain')|list|length }}
</li>
</ul>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-lightbulb me-2"></i>
Permission Levels
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2">
<span class="badge bg-warning me-2" style="color: black;">Domain Sender</span>
Can send as any email address in their domain
</li>
<li>
<span class="badge bg-info me-2" style="color: black;">Regular Sender</span>
Can only send as their own email address
</li>
</ul>
</div>
</div>
</div>
</div>
{% endif %}
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-list-ul me-2"></i>
All Users
All Senders
</h5>
</div>
<div class="card-body p-0">
@@ -47,16 +100,16 @@
</td>
<td>
{% if user.can_send_as_domain %}
<span class="badge bg-warning">
<span class="badge bg-warning" style="color: black;">
<i class="bi bi-star me-1"></i>
Domain Admin
Domain Sender
</span>
<br>
<small class="text-muted">Can send as *@{{ domain.domain_name }}</small>
{% else %}
<span class="badge bg-info">
<span class="badge bg-info" style="color: black;">
<i class="bi bi-person me-1"></i>
User
Regular Sender
</span>
<br>
<small class="text-muted">Can only send as {{ user.email }}</small>
@@ -85,7 +138,7 @@
<!-- Edit Button -->
<a href="{{ url_for('email.edit_user', user_id=user.id) }}"
class="btn btn-outline-primary btn-sm"
title="Edit User">
title="Edit Sender">
<i class="bi bi-pencil"></i>
</a>
@@ -94,7 +147,7 @@
<form method="post" action="{{ url_for('email.delete_user', user_id=user.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-warning btn-sm"
title="Disable User"
title="Disable Sender"
onclick="return confirm('Disable user {{ user.email }}?')">
<i class="bi bi-pause-circle"></i>
</button>
@@ -103,7 +156,7 @@
<form method="post" action="{{ url_for('email.enable_user', user_id=user.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-success btn-sm"
title="Enable User"
title="Enable Sender"
onclick="return confirm('Enable user {{ user.email }}?')">
<i class="bi bi-play-circle"></i>
</button>
@@ -114,7 +167,7 @@
<form method="post" action="{{ url_for('email.remove_user', user_id=user.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-danger btn-sm"
title="Permanently Remove User"
title="Permanently Remove Sender"
onclick="return confirm('Permanently remove user {{ user.email }}? This cannot be undone!')">
<i class="bi bi-trash"></i>
</button>
@@ -129,67 +182,15 @@
{% else %}
<div class="text-center py-5">
<i class="bi bi-people text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No users configured</h4>
<p class="text-muted">Add users to enable username/password authentication</p>
<h4 class="text-muted mt-3">No senders configured</h4>
<p class="text-muted">Add sender to enable username/password authentication</p>
<a href="{{ url_for('email.add_user') }}" class="btn btn-primary">
<i class="bi bi-person-plus me-2"></i>
Add Your First User
Add Your First Sender
</a>
</div>
{% endif %}
</div>
</div>
{% if users %}
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-info-circle me-2"></i>
User Statistics
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Active users:</strong> {{ users|selectattr('0.is_active')|list|length }}
</li>
<li class="mb-2">
<i class="bi bi-star text-warning me-2"></i>
<strong>Domain admins:</strong> {{ users|selectattr('0.can_send_as_domain')|list|length }}
</li>
<li>
<i class="bi bi-person text-info me-2"></i>
<strong>Regular users:</strong> {{ users|rejectattr('0.can_send_as_domain')|list|length }}
</li>
</ul>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-lightbulb me-2"></i>
Permission Levels
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2">
<span class="badge bg-warning me-2">Domain Admin</span>
Can send as any email address in their domain
</li>
<li>
<span class="badge bg-info me-2">Regular User</span>
Can only send as their own email address
</li>
</ul>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}