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

@@ -3,6 +3,8 @@ Python Email server for sending emails directly to recipient ( no email Relay)
```bash
# Testing
.venv/bin/python app.py --web-only --debug
# Production:
python app.py --smtp-only & gunicorn -w 4 -b 0.0.0.0:5000 app:flask_app
```
## Plan:
- make full python MTA server with front end to allow sending any email

106
app.py
View File

@@ -20,9 +20,11 @@ import threading
import signal
import argparse
from datetime import datetime
from flask import Flask, render_template, redirect, url_for, jsonify
from zoneinfo import ZoneInfo
from flask import Flask, render_template, redirect, url_for, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import subprocess
# Add the project root to Python path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
@@ -49,23 +51,7 @@ class SMTPServerApp:
self.smtp_task = None
self.loop = None
self.shutdown_requested = False
# Setup signal handlers
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
def _signal_handler(self, signum, frame):
"""Handle shutdown signals gracefully"""
logger.info(f"Received signal {signum}, initiating shutdown...")
self.shutdown_requested = True
if self.loop and self.loop.is_running():
self.loop.call_soon_threadsafe(self._stop_smtp_server)
def _stop_smtp_server(self):
"""Stop the SMTP server"""
if self.smtp_task and not self.smtp_task.done():
self.smtp_task.cancel()
logger.info("SMTP server stopped")
self.shutdown_event = None
def _get_absolute_database_url(self):
"""Convert relative database URL to absolute path for Flask-SQLAlchemy"""
@@ -129,7 +115,7 @@ class SMTPServerApp:
"""Health check endpoint"""
return jsonify({
'status': 'healthy',
'timestamp': datetime.utcnow().isoformat(),
'timestamp': datetime.now(ZoneInfo('Europe/London')).isoformat(),
'services': {
'smtp_server': 'running' if self.smtp_task and not self.smtp_task.done() else 'stopped',
'web_frontend': 'running'
@@ -167,17 +153,23 @@ class SMTPServerApp:
@app.route('/api/server/restart', methods=['POST'])
def restart_server():
"""Restart the SMTP server (API endpoint)"""
"""Restart the SMTP server (API endpoint) via systemd."""
try:
if self.smtp_task and not self.smtp_task.done():
self._stop_smtp_server()
# Start SMTP server in a new task
if self.loop:
self.smtp_task = asyncio.create_task(start_server())
return jsonify({'status': 'success', 'message': 'SMTP server restarted'})
# Only allow from localhost for security
if request.remote_addr not in ('127.0.0.1', '::1'):
return jsonify({'status': 'error', 'message': 'Unauthorized'}), 403
# Restart the systemd service for SMTP (update service name as needed)
result = subprocess.run(
['systemctl', '--user', 'restart', 'pymta-smtp.service'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
if result.returncode == 0:
return jsonify({'status': 'success', 'message': 'SMTP server restart requested.'})
else:
return jsonify({'status': 'error', 'message': 'Event loop not available'}), 500
return jsonify({'status': 'error', 'message': f'Failed to restart: {result.stderr}'}), 500
except Exception as e:
logger.error(f"Error restarting server: {e}")
return jsonify({'status': 'error', 'message': str(e)}), 500
@@ -227,8 +219,6 @@ class SMTPServerApp:
# Add sample domains
sample_domains = [
'example.com',
'testdomain.org',
'mydomain.net'
]
for domain_name in sample_domains:
@@ -247,9 +237,7 @@ class SMTPServerApp:
# Add sample users
sample_users = [
('admin@example.com', 'example.com', 'admin123', True),
('user@example.com', 'example.com', 'user123', False),
('test@testdomain.org', 'testdomain.org', 'test123', False)
('admin@example.com', 'example.com', 'admin123', False),
]
for email, domain_name, password, can_send_as_domain in sample_users:
@@ -269,8 +257,6 @@ class SMTPServerApp:
# Add sample whitelisted IPs
sample_ips = [
('127.0.0.1', 'example.com'),
('192.168.1.0/24', 'example.com'),
('10.0.0.0/8', 'testdomain.org')
]
for ip, domain_name in sample_ips:
@@ -301,31 +287,49 @@ class SMTPServerApp:
"""Start the SMTP server in async context"""
try:
logger.info("Starting SMTP server...")
await start_server()
await start_server(self.shutdown_event)
except Exception as e:
logger.error(f"SMTP server error: {e}")
if not self.shutdown_requested:
raise
def run_smtp_server(self):
"""Run SMTP server in a separate thread"""
try:
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.shutdown_event = asyncio.Event()
# Only register signal handlers if in the main thread
if threading.current_thread() is threading.main_thread():
for sig in (signal.SIGINT, signal.SIGTERM):
try:
self.loop.add_signal_handler(sig, self.shutdown_event.set)
except NotImplementedError:
pass # Not available on Windows
self.smtp_task = self.loop.create_task(self.start_smtp_server())
self.loop.run_until_complete(self.smtp_task)
except asyncio.CancelledError:
logger.info("SMTP server task was cancelled")
except (asyncio.CancelledError, KeyboardInterrupt):
logger.info("SMTP server task was cancelled or interrupted")
except Exception as e:
if not self.shutdown_requested:
logger.error(f"SMTP server thread error: {e}")
finally:
if self.loop and self.loop.is_running():
self.loop.stop()
if self.loop:
self.loop.close()
if self.shutdown_requested:
os._exit(0)
def run(self, smtp_only=False, web_only=False, debug=False, host='127.0.0.1', port=5000):
"""Run the unified application"""
# If running under Gunicorn, do not start Flask dev server
if 'gunicorn' in os.environ.get('SERVER_SOFTWARE', '').lower():
logger.info("Running under Gunicorn. Flask app will be served by Gunicorn WSGI server.")
app = self.create_flask_app()
return app
if web_only:
# Run only Flask web frontend
logger.info("Starting web frontend only...")
@@ -369,8 +373,6 @@ class SMTPServerApp:
logger.info("Application interrupted by user")
finally:
self.shutdown_requested = True
if self.loop:
self.loop.call_soon_threadsafe(self._stop_smtp_server)
def main():
@@ -393,7 +395,7 @@ def main():
# Initialize sample data if requested
if args.init_data:
logger.info("Initializing sample data...")
app.init_sample_data()
# app.init_sample_data() # For testing uncomment, adds sample domain
logger.info("Sample data initialization complete")
return
@@ -417,14 +419,8 @@ Services:
• SMTP Server: {'Starting...' if not args.web_only else 'Disabled'}
• Web Frontend: {'Starting...' if not args.smtp_only else 'Disabled'}
Available web routes:
• / → Dashboard
• /email/domains → Domain management
• /email/users → User management
• /email/ips → IP whitelist management
• /email/dkim → DKIM management
• /email/settings → Server settings
• /email/logs → Server logs
Available app web test routes:
• /health → Health check
• /api/server/status → Server status API
@@ -451,15 +447,9 @@ Press Ctrl+C to stop the server
# For Flask CLI: expose a create_app() factory at module level
smtp_server_app_instance = SMTPServerApp()
flask_app = SMTPServerApp().create_flask_app()
def create_app():
"""Flask application factory for CLI and Flask-Migrate support.
Returns:
Flask: The Flask application instance.
"""
return smtp_server_app_instance.create_flask_app()
if __name__ == '__main__':
main()

View File

@@ -32,7 +32,7 @@ except RuntimeError:
# No running loop, set debug when we create one
pass
async def start_server():
async def start_server(shutdown_event=None):
"""Main server function."""
logger.debug("Starting SMTP Server with DKIM support...")
@@ -121,9 +121,13 @@ async def start_server():
logger.debug('Management available via web interface at: http://localhost:5000/email')
try:
await asyncio.Event().wait()
if shutdown_event is not None:
await shutdown_event.wait()
else:
await asyncio.Event().wait()
except KeyboardInterrupt:
logger.debug('Shutting down SMTP servers...')
finally:
controller_plain.stop()
controller_tls.stop()
logger.debug('SMTP servers stopped.')

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 %}

View File

@@ -1,32 +0,0 @@
[Unit]
Description=PyMTA Email Server
After=network.target
StartLimitIntervalSec=0
# check any errors when using this service:
# journalctl -u pymta-server.service -b -f
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/PyMTA-server
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/PyMTA-server/.venv/bin/python main.py
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
# Security settings
# Capabilities for low ports < 1024 following 2 lines:
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# if using port < 1024 comment out line bellow:
# NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/PyMTA-server
ProtectHome=true
[Install]
WantedBy=multi-user.target

View File

@@ -19,7 +19,8 @@ Flask-SQLAlchemy
Jinja2
Werkzeug
requests
Flask-Migrate>=4.0.0
Flask-Migrate
gunicorn
# Additional utilities
python-dotenv

234
script_install_service.sh Executable file
View File

@@ -0,0 +1,234 @@
#!/bin/bash
SMTP_SERVICE_NAME="pymta-smtp.service"
WEB_SERVICE_NAME="pymta-web.service"
APP_ROOT_FOLDER="" #/opt/PyMTA-server
# Set APP_ROOT_FOLDER to the directory where this script is located if not already set
if [[ -z "$APP_ROOT_FOLDER" ]]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_ROOT_FOLDER="$SCRIPT_DIR"
fi
SCRIPT_MODE="install"
TARGET_USER="$USER"
IS_SYSTEM=false
# Function to print usage
usage() {
echo "Usage:"
echo " $0 Install as current user"
echo " $0 -u username Install as system service for given user (requires sudo)"
echo " $0 -rm Remove user service for current user ( also works with 'remove')"
echo " $0 -rm username Remove system service for specified user (requires sudo)"
echo " $0 -rm system Remove system service"
exit 1
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-u)
TARGET_USER="$2"
IS_SYSTEM=true
shift 2
;;
-rm)
SCRIPT_MODE="remove"
if [[ "$2" ]]; then
if [[ "$2" == "system" ]]; then
IS_SYSTEM=true
TARGET_USER=""
else
IS_SYSTEM=true
TARGET_USER="$2"
fi
shift
fi
shift
;;
remove)
SCRIPT_MODE="remove"
shift
;;
*)
usage
;;
esac
done
write_user_services() {
mkdir -p "$HOME/.config/systemd/user"
cat > "$HOME/.config/systemd/user/$SMTP_SERVICE_NAME" <<EOF
[Unit]
Description=PyMTA SMTP Server
After=network.target
[Service]
Type=simple
WorkingDirectory=$APP_ROOT_FOLDER
Environment=PYTHONUNBUFFERED=1
ExecStart=$APP_ROOT_FOLDER/.venv/bin/python $APP_ROOT_FOLDER/app.py --smtp-only
Restart=always
RestartSec=5
TimeoutStopSec=4
StandardOutput=journal
StandardError=journal
# This needs to be uncommented if you want to bind to ports below 1024
#AmbientCapabilities=CAP_NET_BIND_SERVICE
#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# This may not be necessary uncommented if you are not binding to ports below 1024
#NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=$APP_ROOT_FOLDER
ProtectHome=true
[Install]
WantedBy=default.target
EOF
cat > "$HOME/.config/systemd/user/$WEB_SERVICE_NAME" <<EOF
[Unit]
Description=PyMTA Web Management (Flask/Gunicorn)
After=network.target
[Service]
Type=simple
WorkingDirectory=$APP_ROOT_FOLDER
Environment=PYTHONUNBUFFERED=1
ExecStart=$APP_ROOT_FOLDER/.venv/bin/gunicorn -w 4 -b 127.0.0.1:5000 app:flask_app
Restart=always
RestartSec=5
TimeoutStopSec=4
StandardOutput=journal
StandardError=journal
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=$APP_ROOT_FOLDER
ProtectHome=true
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable "$SMTP_SERVICE_NAME"
systemctl --user enable "$WEB_SERVICE_NAME"
systemctl --user start "$SMTP_SERVICE_NAME"
systemctl --user start "$WEB_SERVICE_NAME"
echo "Services installed for user $USER."
echo "To view logs, run:"
echo "journalctl --user -u $SMTP_SERVICE_NAME -b -f"
echo "journalctl --user -u $WEB_SERVICE_NAME -b -f"
echo "To view service status, run:"
echo "systemctl --user status $SMTP_SERVICE_NAME"
echo "systemctl --user status $WEB_SERVICE_NAME"
}
remove_user_services() {
systemctl --user stop "$SMTP_SERVICE_NAME" || true
systemctl --user stop "$WEB_SERVICE_NAME" || true
systemctl --user disable "$SMTP_SERVICE_NAME" || true
systemctl --user disable "$WEB_SERVICE_NAME" || true
rm -f "$HOME/.config/systemd/user/$SMTP_SERVICE_NAME"
rm -f "$HOME/.config/systemd/user/$WEB_SERVICE_NAME"
systemctl --user daemon-reload
echo "Removed services for user $USER."
}
write_system_services() {
SERVICE_DIR="/etc/systemd/system"
sudo tee "$SERVICE_DIR/$SMTP_SERVICE_NAME" > /dev/null <<EOF
[Unit]
Description=PyMTA SMTP Server (system)
After=network.target
[Service]
Type=simple
User=$TARGET_USER
WorkingDirectory=$APP_ROOT_FOLDER
Environment=PYTHONUNBUFFERED=1
ExecStart=$APP_ROOT_FOLDER/.venv/bin/python $APP_ROOT_FOLDER/app.py --smtp-only
Restart=always
RestartSec=5
TimeoutStopSec=4
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
#NoNewPrivileges=true
StandardOutput=journal
StandardError=journal
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=$APP_ROOT_FOLDER
ProtectHome=true
[Install]
WantedBy=multi-user.target
EOF
sudo tee "$SERVICE_DIR/$WEB_SERVICE_NAME" > /dev/null <<EOF
[Unit]
Description=PyMTA Web (system)
After=network.target
[Service]
Type=simple
User=$TARGET_USER
WorkingDirectory=$APP_ROOT_FOLDER
Environment=PYTHONUNBUFFERED=1
ExecStart=$APP_ROOT_FOLDER/.venv/bin/gunicorn -w 4 -b 127.0.0.1:5000 app:flask_app
Restart=always
RestartSec=5
TimeoutStopSec=4
StandardOutput=journal
StandardError=journal
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=$APP_ROOT_FOLDER
ProtectHome=true
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable "$SMTP_SERVICE_NAME"
sudo systemctl enable "$WEB_SERVICE_NAME"
sudo systemctl start "$SMTP_SERVICE_NAME"
sudo systemctl start "$WEB_SERVICE_NAME"
echo "Installed system services for user $TARGET_USER."
echo "To view logs, run:"
echo "journalctl -u $SMTP_SERVICE_NAME -b -f"
echo "journalctl -u $WEB_SERVICE_NAME -b -f"
echo "To view service status, run:"
echo "systemctl status $SMTP_SERVICE_NAME"
echo "systemctl status $WEB_SERVICE_NAME"
}
remove_system_services() {
sudo systemctl stop "$SMTP_SERVICE_NAME" || true
sudo systemctl stop "$WEB_SERVICE_NAME" || true
sudo systemctl disable "$SMTP_SERVICE_NAME" || true
sudo systemctl disable "$WEB_SERVICE_NAME" || true
sudo rm -f "/etc/systemd/system/$SMTP_SERVICE_NAME"
sudo rm -f "/etc/systemd/system/$WEB_SERVICE_NAME"
sudo systemctl daemon-reload
echo "Removed system services."
}
# Main logic
if [[ "$SCRIPT_MODE" == "remove" ]]; then
if $IS_SYSTEM; then
remove_system_services
else
remove_user_services
fi
else
if $IS_SYSTEM; then
write_system_services
else
write_user_services
fi
fi

297
script_nginx_setup.sh Normal file
View File

@@ -0,0 +1,297 @@
#!/bin/bash
# Configuration variables
DOMAIN="example.com" # Replace with your domain (e.g., example.com)
WEBSITE_URL="mail.example.com" # Replace with website URL for web interface
EMAIL="admin@example.com" # Replace with your email for Let's Encrypt
LETSENCRYPT_EXPORT_PATH="/opt/PyMTA-server/email_server/ssl_certs/" # Path to export .crt and .key files
APP_USERNAME="appuser" # Replace with the username of the user running the SMTP app
NGINX_CONF_DIR="/etc/nginx"
SITES_AVAILABLE="$NGINX_CONF_DIR/sites-available/$DOMAIN"
SITES_ENABLED="$NGINX_CONF_DIR/sites-enabled/$DOMAIN"
SITE_APP="$NGINX_CONF_DIR/sites-enabled/$WEBSITE_URL"
WEB_ROOT="/var/www/$DOMAIN/html"
ERROR_PAGE_DIR="$WEB_ROOT/errors"
CLOUDFLARE_DNS_PLUGIN="certbot-dns-cloudflare"
# https://medium.com/@life-is-short-so-enjoy-it/homelab-nginx-proxy-manager-setup-ssl-certificate-with-domain-name-in-cloudflare-dns-732af64ddc0b
CLOUDFLARE_API_TOKEN_HERE="" # <<<< Set up here your cloudflare API token
CLOUDFLARE_CREDENTIALS="/root/.cloudflare/credentials.ini"
VENV_DIR="/opt/certbot-venv"
# Exit on error
set -e
# List of common HTTP error codes with brief messages
# hides Nginx error pages for custom
declare -A ERROR_MESSAGES=(
[400]="Bad Request"
[401]="Unauthorized"
[403]="Forbidden"
[404]="Not Found"
[500]="Internal Server Error"
[502]="Bad Gateway"
[503]="Service Unavailable"
[504]="Gateway Timeout"
)
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root or with sudo" >&2
exit 1
fi
# Update system and install required packages
echo "Updating system and installing dependencies..."
apt update && apt upgrade -y
apt install -y nginx certbot python3 python3-pip python3-venv python3-dev
# Create and activate a virtual environment for certbot-dns-cloudflare
echo "Creating Python virtual environment for certbot-dns-cloudflare..."
mkdir -p "$VENV_DIR"
python3 -m venv "$VENV_DIR"
source "$VENV_DIR/bin/activate"
# Upgrade pip in the virtual environment
echo "Upgrading pip..."
pip3 install --upgrade pip
# Install Cloudflare DNS plugin for Certbot
echo "Installing certbot-dns-cloudflare..."
if ! pip3 install "$CLOUDFLARE_DNS_PLUGIN"; then
echo "Failed to install $CLOUDFLARE_DNS_PLUGIN. Please check your network or Python environment." >&2
deactivate
exit 1
fi
# Deactivate virtual environment
deactivate
# Create web root and error page directory
echo "Creating web root and error page directories..."
mkdir -p "$WEB_ROOT" "$ERROR_PAGE_DIR"
# Generate HTML files for each error code
for code in "${!ERROR_MESSAGES[@]}"; do
file="$ERROR_PAGE_DIR/$code.html"
cat > "$file" <<EOF
<!DOCTYPE html>
<html>
<head>
<title>Error $code</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
h1 { font-size: 48px; color: #c00; }
p { font-size: 20px; color: #666; }
a { text-decoration: none; color: #007acc; }
</style>
</head>
<body>
<h1>Error $code</h1>
<p>${ERROR_MESSAGES[$code]}</p>
<p><a href="/">Return to Home</a></p>
</body>
</html>
EOF
echo "Created: $file"
done
echo "All error pages generated in $ERROR_PAGE_DIR"
# Set permissions for error page
chown -R www-data:www-data "$WEB_ROOT"
chmod -R 755 "$WEB_ROOT"
# Create Cloudflare credentials file (ensure you replace with your actual API token)
echo "Creating Cloudflare credentials file..."
mkdir -p "$(dirname "$CLOUDFLARE_CREDENTIALS")"
cat > "$CLOUDFLARE_CREDENTIALS" << EOF
dns_cloudflare_api_token = $CLOUDFLARE_API_TOKEN_HERE
EOF
chmod 600 "$CLOUDFLARE_CREDENTIALS"
# Create NGINX configuration
echo "Creating NGINX configuration for $DOMAIN..."
mkdir -p "$NGINX_CONF_DIR/sites-available" "$NGINX_CONF_DIR/sites-enabled"
cat > "$SITES_AVAILABLE" << EOF
server {
listen 80;
server_name $DOMAIN *.$DOMAIN;
root $WEB_ROOT;
index index.html;
location / {
try_files \$uri \$uri/ /index.html;
}
error_page 404 $ERROR_PAGE;
location = $ERROR_PAGE {
root $WEB_ROOT;
internal;
}
}
EOF
# Enable the site
ln -sf "$SITES_AVAILABLE" "$SITES_ENABLED"
# Disable default site
rm -f "$NGINX_CONF_DIR/sites-enabled/default"
# Test NGINX configuration
echo "Testing NGINX configuration..."
nginx -t
# Reload NGINX to apply changes
echo "Reloading NGINX..."
systemctl reload nginx
# Obtain Let's Encrypt wildcard SSL certificate using Cloudflare DNS
echo "Obtaining Let's Encrypt wildcard SSL certificate..."
source "$VENV_DIR/bin/activate"
if ! certbot certonly \
--non-interactive \
--agree-tos \
--email "$EMAIL" \
--dns-cloudflare \
--dns-cloudflare-credentials "$CLOUDFLARE_CREDENTIALS" \
--domains "$DOMAIN,*.$DOMAIN"; then
echo "Failed to obtain Let's Encrypt certificate. Please check Cloudflare credentials and DNS settings." >&2
deactivate
exit 1
fi
deactivate
# Export .crt and .key files to LETSENCRYPT_EXPORT_PATH
echo "Exporting SSL certificate and key to $LETSENCRYPT_EXPORT_PATH..."
mkdir -p "$LETSENCRYPT_EXPORT_PATH"
cp "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" "$LETSENCRYPT_EXPORT_PATH/server.crt"
cp "/etc/letsencrypt/live/$DOMAIN/privkey.pem" "$LETSENCRYPT_EXPORT_PATH/server.key"
chown $APP_USERNAME:$APP_USERNAME "$LETSENCRYPT_EXPORT_PATH/server.crt" "$LETSENCRYPT_EXPORT_PATH/server.key"
chmod 600 "$LETSENCRYPT_EXPORT_PATH/server.crt" "$LETSENCRYPT_EXPORT_PATH/server.key"
# Update NGINX configuration to use SSL
echo "Updating NGINX configuration for SSL..."
cat > "$SITES_AVAILABLE" << EOF
# Hide NGINX server signature
server_tokens off;
server {
listen 80 default_server;
server_name _;
root $WEB_ROOT;
index errors/404.html; #index.html;
error_page 400 /errors/400.html;
error_page 401 /errors/401.html;
error_page 403 /errors/403.html;
error_page 404 /errors/404.html;
error_page 500 /errors/500.html;
error_page 502 /errors/502.html;
error_page 503 /errors/503.html;
error_page 504 /errors/504.html;
location /errors/ {
root $WEB_ROOT;
internal;
}
}
server {
listen 443 ssl default_server;
server_name _;
ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;
# SSL security settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
root $WEB_ROOT;
index errors/404.html; #index.html;
error_page 400 /errors/400.html;
error_page 401 /errors/401.html;
error_page 403 /errors/403.html;
error_page 404 /errors/404.html;
error_page 500 /errors/500.html;
error_page 502 /errors/502.html;
error_page 503 /errors/503.html;
error_page 504 /errors/504.html;
location /errors/ {
root $WEB_ROOT;
internal;
}
}
server {
listen 80;
server_name $WEBSITE_URL;
# Prevent redirect loop with Cloudflare
if ($http_x_forwarded_proto = "http") {
return 301 https://\$host\$request_uri;
}
}
server {
listen 443 ssl;
server_name $WEBSITE_URL;
ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;
# SSL security settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# Proxy settings for Flask app
location / {
proxy_pass http://127.0.0.1:5000; # Updated this, where runs your web interface
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
EOF
# Test NGINX configuration again
echo "Testing updated NGINX configuration..."
nginx -t
# Reload NGINX to apply SSL changes
echo "Reloading NGINX with SSL configuration..."
systemctl reload nginx
# Enable NGINX auto-start
echo "Enabling NGINX to start on boot..."
systemctl enable nginx
# Set up automatic certificate renewal
echo "Setting up Let's Encrypt renewal for wildcard certificate..."
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/certbot-venv/bin/certbot renew --quiet && \\
cp /etc/letsencrypt/live/\$DOMAIN/fullchain.pem \$LETSENCRYPT_EXPORT_PATH/server.crt && \\
cp /etc/letsencrypt/live/\$DOMAIN/privkey.pem \$LETSENCRYPT_EXPORT_PATH/server.key && \\
chown \$APP_USERNAME:\$APP_USERNAME "\$LETSENCRYPT_EXPORT_PATH/server.crt" "\$LETSENCRYPT_EXPORT_PATH/server.key" && \\
chmod 600 \$LETSENCRYPT_EXPORT_PATH/server.crt \$LETSENCRYPT_EXPORT_PATH/server.key") | crontab -
echo "NGINX setup complete! Your site is live at https://$DOMAIN"
echo "Wildcard certificate covers *.$DOMAIN"
echo "Custom 404 page is set at $ERROR_PAGE"
echo "SSL certificate and key exported to $LETSENCRYPT_EXPORT_PATH"
echo "Please ensure your Cloudflare API token is correctly set in $CLOUDFLARE_CREDENTIALS"

25
script_setup_py_environment.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
python3 --version
python3 -m venv "$SCRIPT_DIR/.venv" --copies # This will copy the Python binary so cap_net_bind_service will work
$SCRIPT_DIR/.venv/bin/pip install -r $SCRIPT_DIR/requirements.txt
echo "Need Sudo for allowing local .venv python to bind to port < 1024 (SMTP uses port 25)"
# Allow binding to port < 1024 (SMTP uses port 25) without use of sudo
for f in $SCRIPT_DIR/.venv/bin/python*; do if sudo setcap 'cap_net_bind_service=+ep' "$f"; then echo "Set CapNetBindService for $(basename "$f")"; fi; done
echo "*******************************************************************"
echo "To starth the app for testing just run in the virtual environment:"
echo "python app.py"
echo "*******************************************************************"
echo "For testing run SMTP server as:"
echo "python app.py --smtp-only --debug"
echo "For testing with web interface run:"
echo "python app.py --web-only --debug"
echo "*******************************************************************"
echo "Gunicorn must run web interface separately, from the SMTP server"
echo "Production Services will run the app as:"
echo "python app.py --smtp-only & gunicorn -w 4 -b 0.0.0.0:5000 app:flask_app"