working on front end
This commit is contained in:
417
README_unified.md
Normal file
417
README_unified.md
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
# SMTP Server with Web Management Frontend
|
||||||
|
|
||||||
|
A comprehensive SMTP server with an integrated Flask-based web management interface. This application provides both a fully functional SMTP server and a modern web interface for managing domains, users, DKIM keys, and server settings.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### SMTP Server
|
||||||
|
- **Full SMTP Server**: Send and receive emails with authentication
|
||||||
|
- **DKIM Support**: Automatic DKIM key generation and email signing
|
||||||
|
- **TLS/SSL Encryption**: Secure email transmission
|
||||||
|
- **Domain Management**: Support for multiple domains
|
||||||
|
- **User Authentication**: Per-user and per-domain authentication
|
||||||
|
- **IP Whitelisting**: Allow specific IPs to send without authentication
|
||||||
|
- **Email Relay**: Forward emails to external servers
|
||||||
|
- **Comprehensive Logging**: Track all email and authentication activities
|
||||||
|
|
||||||
|
### Web Management Interface
|
||||||
|
- **Modern Dark UI**: Bootstrap-based responsive interface
|
||||||
|
- **Domain Management**: Add, configure, and manage email domains
|
||||||
|
- **User Management**: Create and manage email users with permissions
|
||||||
|
- **DKIM Management**: Generate keys, view DNS records, verify setup
|
||||||
|
- **IP Whitelist Management**: Configure IP-based access controls
|
||||||
|
- **Server Settings**: Web-based configuration management
|
||||||
|
- **Real-time Logs**: View email and authentication logs with filtering
|
||||||
|
- **DNS Verification**: Built-in DNS record checking for DKIM and SPF
|
||||||
|
- **Health Monitoring**: Server status and performance metrics
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Python 3.9 or higher
|
||||||
|
- Linux/macOS (Windows with WSL)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone or navigate to the project directory:**
|
||||||
|
```bash
|
||||||
|
cd /home/nahaku/Documents/Projects/SMTP_Server
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run the setup script:**
|
||||||
|
```bash
|
||||||
|
chmod +x email_frontend/setup.sh
|
||||||
|
./email_frontend/setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Initialize sample data (optional):**
|
||||||
|
```bash
|
||||||
|
.venv/bin/python app.py --init-data
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Start the unified application:**
|
||||||
|
```bash
|
||||||
|
.venv/bin/python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will start both the SMTP server and web interface:
|
||||||
|
- **Web Interface**: http://127.0.0.1:5000
|
||||||
|
- **SMTP Server**: Port 25 (plain), Port 587 (TLS)
|
||||||
|
|
||||||
|
## Usage Modes
|
||||||
|
|
||||||
|
### Unified Mode (Default)
|
||||||
|
Run both SMTP server and web interface together:
|
||||||
|
```bash
|
||||||
|
.venv/bin/python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### SMTP Server Only
|
||||||
|
Run only the SMTP server without web interface:
|
||||||
|
```bash
|
||||||
|
.venv/bin/python app.py --smtp-only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Frontend Only
|
||||||
|
Run only the web management interface:
|
||||||
|
```bash
|
||||||
|
.venv/bin/python app.py --web-only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
Enable debug mode with auto-reload:
|
||||||
|
```bash
|
||||||
|
.venv/bin/python app.py --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Host and Port
|
||||||
|
Specify custom web server settings:
|
||||||
|
```bash
|
||||||
|
.venv/bin/python app.py --host 0.0.0.0 --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Main Configuration File: `settings.ini`
|
||||||
|
|
||||||
|
The server uses a comprehensive configuration file with the following sections:
|
||||||
|
|
||||||
|
#### Server Settings
|
||||||
|
```ini
|
||||||
|
[Server]
|
||||||
|
SMTP_PORT = 25
|
||||||
|
SMTP_TLS_PORT = 587
|
||||||
|
hostname = your-domain.com
|
||||||
|
BIND_IP = 0.0.0.0
|
||||||
|
helo_hostname = your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Database Settings
|
||||||
|
```ini
|
||||||
|
[Database]
|
||||||
|
database_path = email_server/server_data/smtp_server.db
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TLS/SSL Settings
|
||||||
|
```ini
|
||||||
|
[TLS]
|
||||||
|
enable_tls = true
|
||||||
|
cert_file = email_server/ssl_certs/server.crt
|
||||||
|
key_file = email_server/ssl_certs/server.key
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DKIM Settings
|
||||||
|
```ini
|
||||||
|
[DKIM]
|
||||||
|
enable_dkim = true
|
||||||
|
default_selector = default
|
||||||
|
key_size = 2048
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Interface Configuration
|
||||||
|
|
||||||
|
The web interface can be configured through the settings page or by editing `settings.ini`. Changes require a server restart to take effect.
|
||||||
|
|
||||||
|
## Web Interface Features
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
- Server status overview
|
||||||
|
- Domain, user, and DKIM key counts
|
||||||
|
- Recent email and authentication activity
|
||||||
|
- Quick access to all management functions
|
||||||
|
|
||||||
|
### Domain Management
|
||||||
|
- Add and remove email domains
|
||||||
|
- Enable/disable domains
|
||||||
|
- Automatic DKIM key generation
|
||||||
|
- Domain-specific settings
|
||||||
|
|
||||||
|
### User Management
|
||||||
|
- Create email users with passwords
|
||||||
|
- Assign users to domains
|
||||||
|
- Set user permissions (regular user vs domain admin)
|
||||||
|
- Manage user access levels
|
||||||
|
|
||||||
|
### DKIM Management
|
||||||
|
- View and generate DKIM keys
|
||||||
|
- DNS record display with copy-to-clipboard
|
||||||
|
- Real-time DNS verification
|
||||||
|
- SPF record recommendations
|
||||||
|
- Selector management
|
||||||
|
|
||||||
|
### IP Whitelist Management
|
||||||
|
- Add IP addresses or ranges
|
||||||
|
- Domain-specific whitelisting
|
||||||
|
- Current IP detection
|
||||||
|
- Use case documentation
|
||||||
|
|
||||||
|
### Server Settings
|
||||||
|
- Web-based configuration editor
|
||||||
|
- All settings sections accessible
|
||||||
|
- Form validation and help text
|
||||||
|
- Export/import configuration
|
||||||
|
|
||||||
|
### Logs and Monitoring
|
||||||
|
- Real-time email logs
|
||||||
|
- Authentication logs
|
||||||
|
- Filtering and pagination
|
||||||
|
- Auto-refresh functionality
|
||||||
|
- Error details and troubleshooting
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
The web interface provides REST API endpoints for integration:
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
```
|
||||||
|
GET /health
|
||||||
|
```
|
||||||
|
Returns server health status and basic information.
|
||||||
|
|
||||||
|
### Server Status
|
||||||
|
```
|
||||||
|
GET /api/server/status
|
||||||
|
```
|
||||||
|
Returns detailed server status including SMTP server state, database counts, and configuration.
|
||||||
|
|
||||||
|
### Server Restart
|
||||||
|
```
|
||||||
|
POST /api/server/restart
|
||||||
|
```
|
||||||
|
Restarts the SMTP server component.
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
The application uses SQLite with the following main tables:
|
||||||
|
- **domains**: Email domain configuration
|
||||||
|
- **users**: User accounts and authentication
|
||||||
|
- **whitelisted_ips**: IP-based access control
|
||||||
|
- **dkim_keys**: DKIM signing keys
|
||||||
|
- **email_logs**: Email transaction records
|
||||||
|
- **auth_logs**: Authentication attempts
|
||||||
|
- **custom_headers**: Custom email headers
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- User-based authentication for email sending
|
||||||
|
- Domain-specific user management
|
||||||
|
- IP-based whitelisting
|
||||||
|
- Secure password hashing
|
||||||
|
|
||||||
|
### Email Security
|
||||||
|
- DKIM signing for email authentication
|
||||||
|
- SPF record support and verification
|
||||||
|
- TLS encryption for secure transmission
|
||||||
|
- DNS record validation
|
||||||
|
|
||||||
|
### Web Interface Security
|
||||||
|
- Session management
|
||||||
|
- CSRF protection
|
||||||
|
- Input validation and sanitization
|
||||||
|
- Secure configuration handling
|
||||||
|
|
||||||
|
## DNS Configuration
|
||||||
|
|
||||||
|
### Required DNS Records
|
||||||
|
|
||||||
|
For each domain, configure the following DNS records:
|
||||||
|
|
||||||
|
#### DKIM Record
|
||||||
|
```
|
||||||
|
default._domainkey.yourdomain.com. IN TXT "v=DKIM1; k=rsa; p=YOUR_PUBLIC_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SPF Record
|
||||||
|
```
|
||||||
|
yourdomain.com. IN TXT "v=spf1 ip4:YOUR_SERVER_IP ~all"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MX Record
|
||||||
|
```
|
||||||
|
yourdomain.com. IN MX 10 your-server.com.
|
||||||
|
```
|
||||||
|
|
||||||
|
The web interface provides the exact DNS records needed and can verify their configuration.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Port Permission Issues
|
||||||
|
If you get permission denied errors on ports 25 or 587:
|
||||||
|
```bash
|
||||||
|
sudo setcap 'cap_net_bind_service=+ep' .venv/bin/python
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Database Issues
|
||||||
|
Reset the database:
|
||||||
|
```bash
|
||||||
|
rm email_server/server_data/smtp_server.db
|
||||||
|
.venv/bin/python app.py --init-data
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SSL Certificate Issues
|
||||||
|
Generate new self-signed certificates:
|
||||||
|
```bash
|
||||||
|
.venv/bin/python -c "from email_server.tls_utils import generate_self_signed_cert; generate_self_signed_cert()"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DNS Verification Fails
|
||||||
|
- Ensure DNS records are properly configured
|
||||||
|
- Wait for DNS propagation (up to 24 hours)
|
||||||
|
- Check with online DNS checkers
|
||||||
|
- Verify your domain's nameservers
|
||||||
|
|
||||||
|
### Log Files
|
||||||
|
|
||||||
|
Check logs for detailed error information:
|
||||||
|
- **Application logs**: Console output or systemd logs
|
||||||
|
- **Email logs**: Available in web interface
|
||||||
|
- **Authentication logs**: Available in web interface
|
||||||
|
|
||||||
|
### Web Interface Issues
|
||||||
|
|
||||||
|
If the web interface is not accessible:
|
||||||
|
1. Check that Flask is running on the correct host/port
|
||||||
|
2. Verify firewall settings
|
||||||
|
3. Check browser console for JavaScript errors
|
||||||
|
4. Review Flask application logs
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
├── app.py # Unified application entry point
|
||||||
|
├── main.py # SMTP server only entry point
|
||||||
|
├── settings.ini # Configuration file
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
├── email_server/ # SMTP server implementation
|
||||||
|
│ ├── models.py # Database models
|
||||||
|
│ ├── smtp_handler.py # SMTP protocol handling
|
||||||
|
│ ├── dkim_manager.py # DKIM key management
|
||||||
|
│ ├── settings_loader.py # Configuration loader
|
||||||
|
│ └── ...
|
||||||
|
├── email_frontend/ # Web management interface
|
||||||
|
│ ├── blueprint.py # Flask Blueprint
|
||||||
|
│ ├── templates/ # HTML templates
|
||||||
|
│ ├── static/ # CSS/JS assets
|
||||||
|
│ └── ...
|
||||||
|
└── tests/ # Test files
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Features
|
||||||
|
|
||||||
|
To add new features to the web interface:
|
||||||
|
|
||||||
|
1. **Add routes** to `email_frontend/blueprint.py`
|
||||||
|
2. **Create templates** in `email_frontend/templates/`
|
||||||
|
3. **Add static assets** in `email_frontend/static/`
|
||||||
|
4. **Update navigation** in `sidebar_email.html`
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Run tests manually:
|
||||||
|
```bash
|
||||||
|
cd tests
|
||||||
|
./custom_test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Send test emails:
|
||||||
|
```bash
|
||||||
|
.venv/bin/python tests/send_email.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Systemd Service
|
||||||
|
|
||||||
|
Create a systemd service for production deployment:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=SMTP Server with Web Management
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=smtp-user
|
||||||
|
WorkingDirectory=/path/to/SMTP_Server
|
||||||
|
ExecStart=/path/to/SMTP_Server/.venv/bin/python app.py --host 0.0.0.0
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reverse Proxy
|
||||||
|
|
||||||
|
For production web interface, use nginx or Apache as a reverse proxy:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:5000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
|
||||||
|
1. **Change default secrets** in configuration
|
||||||
|
2. **Use proper SSL certificates** for web interface
|
||||||
|
3. **Configure firewall** to restrict access
|
||||||
|
4. **Regular backups** of database and configuration
|
||||||
|
5. **Monitor logs** for suspicious activity
|
||||||
|
6. **Keep dependencies updated**
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and questions:
|
||||||
|
1. Check the troubleshooting section above
|
||||||
|
2. Review log files for error details
|
||||||
|
3. Verify DNS and network configuration
|
||||||
|
4. Test with sample data using `--init-data`
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License. See the LICENSE file for details.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please:
|
||||||
|
1. Follow the existing code style
|
||||||
|
2. Add tests for new features
|
||||||
|
3. Update documentation
|
||||||
|
4. Submit pull requests for review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: This application is designed for educational and development purposes. For production use, ensure proper security configuration, monitoring, and maintenance.
|
||||||
427
app.py
Normal file
427
app.py
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Unified SMTP Server with Web Management Frontend
|
||||||
|
|
||||||
|
This application runs both the SMTP server and the Flask web frontend
|
||||||
|
in a single integrated application.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python app.py # Run with default settings
|
||||||
|
python app.py --smtp-only # Run SMTP server only
|
||||||
|
python app.py --web-only # Run web frontend only
|
||||||
|
python app.py --debug # Enable debug mode
|
||||||
|
python app.py --init-data # Initialize sample data and exit
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
import signal
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import Flask, render_template, redirect, url_for, jsonify
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
# Add the project root to Python path
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
# Import SMTP server components
|
||||||
|
from email_server.server_runner import start_server
|
||||||
|
from email_server.models import create_tables, Session, Domain, User, WhitelistedIP, DKIMKey, hash_password
|
||||||
|
from email_server.settings_loader import load_settings
|
||||||
|
from email_server.tool_box import get_logger
|
||||||
|
from email_server.dkim_manager import DKIMManager
|
||||||
|
|
||||||
|
# Import Flask frontend
|
||||||
|
from email_frontend.blueprint import email_bp
|
||||||
|
|
||||||
|
logger = get_logger()
|
||||||
|
|
||||||
|
class SMTPServerApp:
|
||||||
|
"""Unified SMTP Server and Web Frontend Application"""
|
||||||
|
|
||||||
|
def __init__(self, config_file='settings.ini'):
|
||||||
|
self.config_file = config_file
|
||||||
|
self.settings = load_settings()
|
||||||
|
self.flask_app = None
|
||||||
|
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")
|
||||||
|
|
||||||
|
def create_flask_app(self):
|
||||||
|
"""Create and configure the Flask application"""
|
||||||
|
app = Flask(__name__,
|
||||||
|
static_folder='email_frontend/static',
|
||||||
|
template_folder='email_frontend/templates')
|
||||||
|
|
||||||
|
# Flask configuration
|
||||||
|
app.config.update({
|
||||||
|
'SECRET_KEY': self.settings.get('Flask', 'secret_key', fallback='change-this-secret-key-in-production'),
|
||||||
|
'SQLALCHEMY_DATABASE_URI': f"sqlite:///{self.settings.get('Database', 'database_path', fallback='email_server/server_data/smtp_server.db')}",
|
||||||
|
'SQLALCHEMY_TRACK_MODIFICATIONS': False,
|
||||||
|
'TEMPLATES_AUTO_RELOAD': True,
|
||||||
|
'SEND_FILE_MAX_AGE_DEFAULT': 0 # Disable caching for development
|
||||||
|
})
|
||||||
|
|
||||||
|
# Initialize database
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
# Create database tables if they don't exist
|
||||||
|
with app.app_context():
|
||||||
|
create_tables()
|
||||||
|
|
||||||
|
# Register the email management blueprint
|
||||||
|
app.register_blueprint(email_bp)
|
||||||
|
|
||||||
|
# Main application routes
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
"""Redirect root to email dashboard"""
|
||||||
|
return redirect(url_for('email.dashboard'))
|
||||||
|
|
||||||
|
@app.route('/health')
|
||||||
|
def health_check():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
return jsonify({
|
||||||
|
'status': 'healthy',
|
||||||
|
'timestamp': datetime.utcnow().isoformat(),
|
||||||
|
'services': {
|
||||||
|
'smtp_server': 'running' if self.smtp_task and not self.smtp_task.done() else 'stopped',
|
||||||
|
'web_frontend': 'running'
|
||||||
|
},
|
||||||
|
'version': '1.0.0'
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/api/server/status')
|
||||||
|
def server_status():
|
||||||
|
"""Get detailed server status"""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
status = {
|
||||||
|
'smtp_server': {
|
||||||
|
'running': self.smtp_task and not self.smtp_task.done(),
|
||||||
|
'port': int(self.settings.get('Server', 'SMTP_PORT', fallback=25)),
|
||||||
|
'tls_port': int(self.settings.get('Server', 'SMTP_TLS_PORT', fallback=587)),
|
||||||
|
'hostname': self.settings.get('Server', 'hostname', fallback='localhost')
|
||||||
|
},
|
||||||
|
'database': {
|
||||||
|
'domains': session.query(Domain).filter_by(is_active=True).count(),
|
||||||
|
'users': session.query(User).filter_by(is_active=True).count(),
|
||||||
|
'dkim_keys': session.query(DKIMKey).filter_by(is_active=True).count(),
|
||||||
|
'whitelisted_ips': session.query(WhitelistedIP).filter_by(is_active=True).count()
|
||||||
|
},
|
||||||
|
'settings': {
|
||||||
|
'relay_enabled': self.settings.getboolean('Relay', 'enable_relay', fallback=False),
|
||||||
|
'tls_enabled': self.settings.getboolean('TLS', 'enable_tls', fallback=True),
|
||||||
|
'dkim_enabled': self.settings.getboolean('DKIM', 'enable_dkim', fallback=True)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonify(status)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@app.route('/api/server/restart', methods=['POST'])
|
||||||
|
def restart_server():
|
||||||
|
"""Restart the SMTP server (API endpoint)"""
|
||||||
|
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'})
|
||||||
|
else:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Event loop not available'}), 500
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error restarting server: {e}")
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
# Error handlers
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found_error(error):
|
||||||
|
"""Handle 404 errors"""
|
||||||
|
return render_template('error.html',
|
||||||
|
error_code=404,
|
||||||
|
error_message="Page not found",
|
||||||
|
error_details="The requested page could not be found."), 404
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def internal_error(error):
|
||||||
|
"""Handle 500 errors"""
|
||||||
|
logger.error(f"Internal error: {error}")
|
||||||
|
return render_template('error.html',
|
||||||
|
error_code=500,
|
||||||
|
error_message="Internal server error",
|
||||||
|
error_details=str(error)), 500
|
||||||
|
|
||||||
|
# Context processors for templates
|
||||||
|
@app.context_processor
|
||||||
|
def utility_processor():
|
||||||
|
"""Add utility functions to template context"""
|
||||||
|
return {
|
||||||
|
'moment': datetime,
|
||||||
|
'len': len,
|
||||||
|
'enumerate': enumerate,
|
||||||
|
'zip': zip,
|
||||||
|
'str': str,
|
||||||
|
'int': int,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.flask_app = app
|
||||||
|
return app
|
||||||
|
|
||||||
|
def init_sample_data(self):
|
||||||
|
"""Initialize the database with sample data for testing"""
|
||||||
|
try:
|
||||||
|
# Initialize database
|
||||||
|
create_tables()
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Add sample domains
|
||||||
|
sample_domains = [
|
||||||
|
'example.com',
|
||||||
|
'testdomain.org',
|
||||||
|
'mydomain.net'
|
||||||
|
]
|
||||||
|
|
||||||
|
for domain_name in sample_domains:
|
||||||
|
existing = session.query(Domain).filter_by(domain_name=domain_name).first()
|
||||||
|
if not existing:
|
||||||
|
domain = Domain(domain_name=domain_name)
|
||||||
|
session.add(domain)
|
||||||
|
logger.info(f"Added sample domain: {domain_name}")
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Generate DKIM keys for new domains
|
||||||
|
dkim_manager = DKIMManager()
|
||||||
|
for domain_name in sample_domains:
|
||||||
|
dkim_manager.generate_dkim_keypair(domain_name)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
]
|
||||||
|
|
||||||
|
for email, domain_name, password, can_send_as_domain in sample_users:
|
||||||
|
existing = session.query(User).filter_by(email=email).first()
|
||||||
|
if not existing:
|
||||||
|
domain = session.query(Domain).filter_by(domain_name=domain_name).first()
|
||||||
|
if domain:
|
||||||
|
user = User(
|
||||||
|
email=email,
|
||||||
|
password_hash=hash_password(password),
|
||||||
|
domain_id=domain.id,
|
||||||
|
can_send_as_domain=can_send_as_domain
|
||||||
|
)
|
||||||
|
session.add(user)
|
||||||
|
logger.info(f"Added sample user: {email}")
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
domain = session.query(Domain).filter_by(domain_name=domain_name).first()
|
||||||
|
if domain:
|
||||||
|
existing = session.query(WhitelistedIP).filter_by(
|
||||||
|
ip_address=ip, domain_id=domain.id
|
||||||
|
).first()
|
||||||
|
if not existing:
|
||||||
|
whitelist = WhitelistedIP(
|
||||||
|
ip_address=ip,
|
||||||
|
domain_id=domain.id
|
||||||
|
)
|
||||||
|
session.add(whitelist)
|
||||||
|
logger.info(f"Added sample whitelisted IP: {ip} for {domain_name}")
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
logger.info("Sample data initialized successfully!")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error initializing sample data: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def start_smtp_server(self):
|
||||||
|
"""Start the SMTP server in async context"""
|
||||||
|
try:
|
||||||
|
logger.info("Starting SMTP server...")
|
||||||
|
await start_server()
|
||||||
|
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.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 Exception as e:
|
||||||
|
if not self.shutdown_requested:
|
||||||
|
logger.error(f"SMTP server thread error: {e}")
|
||||||
|
finally:
|
||||||
|
if self.loop:
|
||||||
|
self.loop.close()
|
||||||
|
|
||||||
|
def run(self, smtp_only=False, web_only=False, debug=False, host='127.0.0.1', port=5000):
|
||||||
|
"""Run the unified application"""
|
||||||
|
if web_only:
|
||||||
|
# Run only Flask web frontend
|
||||||
|
logger.info("Starting web frontend only...")
|
||||||
|
app = self.create_flask_app()
|
||||||
|
try:
|
||||||
|
logger.info(f"Web frontend starting at http://{host}:{port}")
|
||||||
|
app.run(host=host, port=port, debug=debug, threaded=True, use_reloader=False)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Web server interrupted by user")
|
||||||
|
return
|
||||||
|
|
||||||
|
if smtp_only:
|
||||||
|
# Run only SMTP server
|
||||||
|
logger.info("Starting SMTP server only...")
|
||||||
|
try:
|
||||||
|
asyncio.run(self.start_smtp_server())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("SMTP server interrupted by user")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Run both SMTP server and web frontend
|
||||||
|
logger.info("Starting unified SMTP server with web management frontend...")
|
||||||
|
|
||||||
|
# Start SMTP server in a separate thread
|
||||||
|
smtp_thread = threading.Thread(target=self.run_smtp_server, daemon=True)
|
||||||
|
smtp_thread.start()
|
||||||
|
|
||||||
|
# Give SMTP server time to start
|
||||||
|
import time
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Start Flask web frontend in main thread
|
||||||
|
try:
|
||||||
|
app = self.create_flask_app()
|
||||||
|
logger.info(f"Web frontend starting at http://{host}:{port}")
|
||||||
|
logger.info("SMTP server running in background")
|
||||||
|
|
||||||
|
app.run(host=host, port=port, debug=debug, threaded=True, use_reloader=False)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
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():
|
||||||
|
"""Main function"""
|
||||||
|
parser = argparse.ArgumentParser(description='Unified SMTP Server with Web Management')
|
||||||
|
parser.add_argument('--smtp-only', action='store_true', help='Run SMTP server only')
|
||||||
|
parser.add_argument('--web-only', action='store_true', help='Run web frontend only')
|
||||||
|
parser.add_argument('--host', default='127.0.0.1', help='Web server host (default: 127.0.0.1)')
|
||||||
|
parser.add_argument('--port', type=int, default=5000, help='Web server port (default: 5000)')
|
||||||
|
parser.add_argument('--debug', action='store_true', help='Enable debug mode')
|
||||||
|
parser.add_argument('--init-data', action='store_true', help='Initialize sample data and exit')
|
||||||
|
parser.add_argument('--config', default='settings.ini', help='Configuration file path')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Create application instance
|
||||||
|
try:
|
||||||
|
app = SMTPServerApp(args.config)
|
||||||
|
|
||||||
|
# Initialize sample data if requested
|
||||||
|
if args.init_data:
|
||||||
|
logger.info("Initializing sample data...")
|
||||||
|
app.init_sample_data()
|
||||||
|
logger.info("Sample data initialization complete")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Print startup information
|
||||||
|
settings = load_settings()
|
||||||
|
smtp_port = settings.get('Server', 'SMTP_PORT', fallback='25')
|
||||||
|
smtp_tls_port = settings.get('Server', 'SMTP_TLS_PORT', fallback='587')
|
||||||
|
|
||||||
|
print(f"""
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ SMTP Server with Web Management ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
• Configuration file: {args.config}
|
||||||
|
• SMTP Server ports: {smtp_port} (plain), {smtp_tls_port} (TLS)
|
||||||
|
• Web Interface: http://{args.host}:{args.port}
|
||||||
|
• Debug mode: {'ON' if args.debug else 'OFF'}
|
||||||
|
|
||||||
|
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
|
||||||
|
• /health → Health check
|
||||||
|
• /api/server/status → Server status API
|
||||||
|
|
||||||
|
To initialize sample data:
|
||||||
|
python app.py --init-data
|
||||||
|
|
||||||
|
Press Ctrl+C to stop the server
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
app.run(
|
||||||
|
smtp_only=args.smtp_only,
|
||||||
|
web_only=args.web_only,
|
||||||
|
debug=args.debug,
|
||||||
|
host=args.host,
|
||||||
|
port=args.port
|
||||||
|
)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Application shutdown requested")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Application error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
329
email_frontend/README.md
Normal file
329
email_frontend/README.md
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
# SMTP Server Management Frontend
|
||||||
|
|
||||||
|
A comprehensive Flask-based web interface for managing SMTP server operations, including domain management, user authentication, DKIM configuration, IP whitelisting, and email monitoring.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 🏠 Dashboard
|
||||||
|
- Server statistics and health monitoring
|
||||||
|
- Recent activity overview
|
||||||
|
- Quick access to all management sections
|
||||||
|
- Real-time server status indicators
|
||||||
|
|
||||||
|
### 🌐 Domain Management
|
||||||
|
- Add, edit, and remove domains
|
||||||
|
- Domain status monitoring
|
||||||
|
- Bulk domain operations
|
||||||
|
- Domain-specific statistics
|
||||||
|
|
||||||
|
### 👥 User Management
|
||||||
|
- Create and manage email users
|
||||||
|
- Password management
|
||||||
|
- Domain-based user organization
|
||||||
|
- User permission levels (regular user vs domain admin)
|
||||||
|
- Email validation and verification
|
||||||
|
|
||||||
|
### 🔒 IP Whitelist Management
|
||||||
|
- Add/remove whitelisted IP addresses
|
||||||
|
- Support for single IPs and CIDR notation
|
||||||
|
- Current IP detection
|
||||||
|
- Domain-specific IP restrictions
|
||||||
|
- Security notes and best practices
|
||||||
|
|
||||||
|
### 🔐 DKIM Management
|
||||||
|
- Generate and manage DKIM keys
|
||||||
|
- DNS record verification
|
||||||
|
- SPF record management
|
||||||
|
- Real-time DNS checking
|
||||||
|
- Copy-to-clipboard functionality for DNS records
|
||||||
|
|
||||||
|
### ⚙️ Server Settings
|
||||||
|
- Configure all server parameters via web interface
|
||||||
|
- Real-time settings.ini file updates
|
||||||
|
- Settings validation and error checking
|
||||||
|
- Export/import configuration
|
||||||
|
- Sections: Server, Database, Logging, Relay, TLS, DKIM
|
||||||
|
|
||||||
|
### 📊 Logs & Monitoring
|
||||||
|
- Email logs with detailed filtering
|
||||||
|
- Authentication logs
|
||||||
|
- Error tracking and debugging
|
||||||
|
- Real-time log updates
|
||||||
|
- Export log data
|
||||||
|
- Advanced search and filtering
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Python 3.9 or higher
|
||||||
|
- Flask 2.3+
|
||||||
|
- Access to the SMTP server database
|
||||||
|
- Web browser with JavaScript enabled
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. **Clone or navigate to the SMTP server directory:**
|
||||||
|
```bash
|
||||||
|
cd /path/to/SMTP_Server
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install frontend dependencies:**
|
||||||
|
```bash
|
||||||
|
# Create virtual environment if it doesn't exist
|
||||||
|
python3 -m venv .venv
|
||||||
|
|
||||||
|
# Activate virtual environment
|
||||||
|
source .venv/bin/activate # Linux/macOS
|
||||||
|
# or
|
||||||
|
.venv\Scripts\activate # Windows
|
||||||
|
|
||||||
|
# Install frontend requirements
|
||||||
|
.venv/bin/pip install -r email_frontend/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Initialize sample data (optional):**
|
||||||
|
```bash
|
||||||
|
.venv/bin/python email_frontend/example_app.py --init-data
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run the example application:**
|
||||||
|
```bash
|
||||||
|
.venv/bin/python email_frontend/example_app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Access the web interface:**
|
||||||
|
Open your browser and navigate to `http://127.0.0.1:5000`
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
### Using as a Flask Blueprint
|
||||||
|
|
||||||
|
The frontend is designed as a Flask Blueprint that can be integrated into existing Flask applications:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from flask import Flask
|
||||||
|
from email_frontend.blueprint import email_bp
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['SECRET_KEY'] = 'your-secret-key'
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///smtp_server.db'
|
||||||
|
|
||||||
|
# Register the blueprint
|
||||||
|
app.register_blueprint(email_bp, url_prefix='/email')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blueprint Routes
|
||||||
|
|
||||||
|
The blueprint provides the following routes under the `/email` prefix:
|
||||||
|
|
||||||
|
- `/` or `/dashboard` - Main dashboard
|
||||||
|
- `/domains` - Domain management
|
||||||
|
- `/domains/add` - Add new domain
|
||||||
|
- `/users` - User management
|
||||||
|
- `/users/add` - Add new user
|
||||||
|
- `/ips` - IP whitelist management
|
||||||
|
- `/ips/add` - Add whitelisted IP
|
||||||
|
- `/dkim` - DKIM management
|
||||||
|
- `/settings` - Server configuration
|
||||||
|
- `/logs` - Email and authentication logs
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
The frontend requires access to your SMTP server's database and configuration files:
|
||||||
|
|
||||||
|
1. **Database Access:** Ensure the Flask app can connect to your SMTP server database
|
||||||
|
2. **Settings File:** The frontend reads from `settings.ini` in the project root
|
||||||
|
3. **Static Files:** CSS and JavaScript files are served from `email_frontend/static/`
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Templates
|
||||||
|
|
||||||
|
All templates extend `base.html` and use the dark Bootstrap theme. Key templates:
|
||||||
|
|
||||||
|
- `base.html` - Base layout with navigation
|
||||||
|
- `sidebar_email.html` - Navigation sidebar
|
||||||
|
- `email/dashboard.html` - Main dashboard
|
||||||
|
- `email/*.html` - Feature-specific pages
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
|
||||||
|
Custom CSS is located in `static/css/smtp-management.css` and includes:
|
||||||
|
|
||||||
|
- Dark theme enhancements
|
||||||
|
- Custom form styling
|
||||||
|
- DNS record display formatting
|
||||||
|
- Log entry styling
|
||||||
|
- Responsive design tweaks
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
Interactive features are implemented in `static/js/smtp-management.js`:
|
||||||
|
|
||||||
|
- Form validation
|
||||||
|
- AJAX requests for DNS checking
|
||||||
|
- Copy-to-clipboard functionality
|
||||||
|
- Auto-refresh for logs
|
||||||
|
- Real-time IP detection
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
The frontend provides several AJAX endpoints for enhanced functionality:
|
||||||
|
|
||||||
|
### DNS Verification
|
||||||
|
```
|
||||||
|
POST /email/check-dns
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"domain": "example.com",
|
||||||
|
"record_type": "TXT",
|
||||||
|
"expected_value": "v=DKIM1; k=rsa; p=..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Settings Updates
|
||||||
|
```
|
||||||
|
POST /email/settings
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
|
||||||
|
section=server&key=smtp_port&value=587
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Filtering
|
||||||
|
```
|
||||||
|
GET /email/logs?filter=email&page=1&per_page=50
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- Implement proper authentication before deploying to production
|
||||||
|
- Use strong session keys
|
||||||
|
- Consider implementing role-based access control
|
||||||
|
|
||||||
|
### Network Security
|
||||||
|
- Run behind a reverse proxy (nginx/Apache) in production
|
||||||
|
- Use HTTPS for all connections
|
||||||
|
- Implement rate limiting
|
||||||
|
- Restrict access to management interface
|
||||||
|
|
||||||
|
### Data Protection
|
||||||
|
- Sanitize all user inputs
|
||||||
|
- Use parameterized queries
|
||||||
|
- Implement CSRF protection
|
||||||
|
- Regular security updates
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
email_frontend/
|
||||||
|
├── __init__.py # Package initialization
|
||||||
|
├── blueprint.py # Main Flask Blueprint
|
||||||
|
├── example_app.py # Example Flask application
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
├── static/ # Static assets
|
||||||
|
│ ├── css/
|
||||||
|
│ │ └── smtp-management.css
|
||||||
|
│ └── js/
|
||||||
|
│ └── smtp-management.js
|
||||||
|
└── templates/ # Jinja2 templates
|
||||||
|
├── base.html # Base template
|
||||||
|
├── sidebar_email.html # Navigation sidebar
|
||||||
|
└── email/ # Feature templates
|
||||||
|
├── dashboard.html
|
||||||
|
├── domains.html
|
||||||
|
├── users.html
|
||||||
|
├── ips.html
|
||||||
|
├── dkim.html
|
||||||
|
├── settings.html
|
||||||
|
├── logs.html
|
||||||
|
└── error.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding New Features
|
||||||
|
|
||||||
|
1. **Add routes to `blueprint.py`**
|
||||||
|
2. **Create corresponding templates in `templates/email/`**
|
||||||
|
3. **Update navigation in `sidebar_email.html`**
|
||||||
|
4. **Add custom styling to `smtp-management.css`**
|
||||||
|
5. **Implement JavaScript interactions in `smtp-management.js`**
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Run the example application with debug mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python email_frontend/example_app.py --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Initialize test data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python email_frontend/example_app.py --init-data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Database Connection Errors:**
|
||||||
|
- Verify database file exists and is accessible
|
||||||
|
- Check file permissions
|
||||||
|
- Ensure SQLAlchemy is properly configured
|
||||||
|
|
||||||
|
**Template Not Found Errors:**
|
||||||
|
- Verify templates are in the correct directory structure
|
||||||
|
- Check template inheritance and block names
|
||||||
|
- Ensure blueprint is registered with correct static folder
|
||||||
|
|
||||||
|
**Static Files Not Loading:**
|
||||||
|
- Check Flask static file configuration
|
||||||
|
- Verify CSS/JS files exist in static directories
|
||||||
|
- Clear browser cache
|
||||||
|
|
||||||
|
**DNS Verification Not Working:**
|
||||||
|
- Ensure `dnspython` is installed
|
||||||
|
- Check network connectivity
|
||||||
|
- Verify DNS server accessibility
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable debug mode for detailed error information:
|
||||||
|
|
||||||
|
```python
|
||||||
|
app.run(debug=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via command line:
|
||||||
|
```bash
|
||||||
|
.venv/bin/python email_frontend/example_app.py --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Test thoroughly
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is part of the SMTP Server suite. Please refer to the main project license.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and questions:
|
||||||
|
1. Check the troubleshooting section
|
||||||
|
2. Review the example application
|
||||||
|
3. Create an issue in the project repository
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note:** This frontend is designed specifically for the SMTP Server project and requires the associated database models and configuration files to function properly.
|
||||||
1
email_frontend/__init__.py
Normal file
1
email_frontend/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Email frontend module
|
||||||
646
email_frontend/blueprint.py
Normal file
646
email_frontend/blueprint.py
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
"""
|
||||||
|
Flask Blueprint for Email Server Management Frontend
|
||||||
|
|
||||||
|
This module provides a comprehensive web interface for managing the SMTP server:
|
||||||
|
- Domain management
|
||||||
|
- User authentication and authorization
|
||||||
|
- DKIM key management with DNS record verification
|
||||||
|
- Server settings configuration
|
||||||
|
- Email logs and monitoring
|
||||||
|
|
||||||
|
Security features:
|
||||||
|
- Authentication management per domain
|
||||||
|
- IP whitelisting capabilities
|
||||||
|
- SPF and DKIM DNS validation
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||||
|
import socket
|
||||||
|
import requests
|
||||||
|
import dns.resolver
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Dict, List, Tuple
|
||||||
|
|
||||||
|
# Import email server modules
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from email_server.models import (
|
||||||
|
Session, Domain, User, WhitelistedIP, DKIMKey, CustomHeader, EmailLog, AuthLog,
|
||||||
|
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.tool_box import get_logger
|
||||||
|
|
||||||
|
logger = get_logger()
|
||||||
|
|
||||||
|
# Create Blueprint
|
||||||
|
email_bp = Blueprint('email', __name__,
|
||||||
|
template_folder='templates',
|
||||||
|
static_folder='static',
|
||||||
|
url_prefix='/email')
|
||||||
|
|
||||||
|
def get_public_ip() -> str:
|
||||||
|
"""Get the public IP address of the server."""
|
||||||
|
try:
|
||||||
|
response = requests.get('https://api.ipify.org', timeout=5)
|
||||||
|
return response.text.strip()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
# Fallback method
|
||||||
|
response = requests.get('https://httpbin.org/ip', timeout=5)
|
||||||
|
return response.json()['origin'].split(',')[0].strip()
|
||||||
|
except Exception:
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
def check_dns_record(domain: str, record_type: str, expected_value: str = None) -> Dict:
|
||||||
|
"""Check DNS record for a domain."""
|
||||||
|
try:
|
||||||
|
answers = dns.resolver.resolve(domain, record_type)
|
||||||
|
records = [str(answer) for answer in answers]
|
||||||
|
|
||||||
|
if expected_value:
|
||||||
|
found = any(expected_value in record for record in records)
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'found': found,
|
||||||
|
'records': records,
|
||||||
|
'message': f"Record {'found' if found else 'not found'}"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'records': records,
|
||||||
|
'message': f"Found {len(records)} {record_type} record(s)"
|
||||||
|
}
|
||||||
|
except dns.resolver.NXDOMAIN:
|
||||||
|
return {'success': False, 'message': 'Domain not found'}
|
||||||
|
except dns.resolver.NoAnswer:
|
||||||
|
return {'success': False, 'message': f'No {record_type} records found'}
|
||||||
|
except Exception as e:
|
||||||
|
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 = []
|
||||||
|
|
||||||
|
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
|
||||||
|
our_ip = f"ip4:{public_ip}"
|
||||||
|
if our_ip not in base_mechanisms:
|
||||||
|
base_mechanisms.append(our_ip)
|
||||||
|
|
||||||
|
# Construct SPF record
|
||||||
|
spf_parts = ['v=spf1'] + base_mechanisms + ['~all']
|
||||||
|
return ' '.join(spf_parts)
|
||||||
|
|
||||||
|
@email_bp.route('/')
|
||||||
|
def dashboard():
|
||||||
|
"""Main dashboard showing overview of the email server."""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
# Get counts
|
||||||
|
domain_count = session.query(Domain).filter_by(is_active=True).count()
|
||||||
|
user_count = session.query(User).filter_by(is_active=True).count()
|
||||||
|
dkim_count = session.query(DKIMKey).filter_by(is_active=True).count()
|
||||||
|
|
||||||
|
# Get recent email logs
|
||||||
|
recent_emails = session.query(EmailLog).order_by(EmailLog.created_at.desc()).limit(10).all()
|
||||||
|
|
||||||
|
# Get recent auth logs
|
||||||
|
recent_auths = session.query(AuthLog).order_by(AuthLog.created_at.desc()).limit(10).all()
|
||||||
|
|
||||||
|
return render_template('dashboard.html',
|
||||||
|
domain_count=domain_count,
|
||||||
|
user_count=user_count,
|
||||||
|
dkim_count=dkim_count,
|
||||||
|
recent_emails=recent_emails,
|
||||||
|
recent_auths=recent_auths)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@email_bp.route('/domains')
|
||||||
|
def domains_list():
|
||||||
|
"""List all domains."""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
domains = session.query(Domain).order_by(Domain.domain_name).all()
|
||||||
|
return render_template('domains.html', domains=domains)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@email_bp.route('/domains/add', methods=['GET', 'POST'])
|
||||||
|
def add_domain():
|
||||||
|
"""Add new domain."""
|
||||||
|
if request.method == 'POST':
|
||||||
|
domain_name = request.form.get('domain_name', '').strip().lower()
|
||||||
|
|
||||||
|
if not domain_name:
|
||||||
|
flash('Domain name is required', 'error')
|
||||||
|
return redirect(url_for('email.add_domain'))
|
||||||
|
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
# Check if domain already exists
|
||||||
|
existing = session.query(Domain).filter_by(domain_name=domain_name).first()
|
||||||
|
if existing:
|
||||||
|
flash(f'Domain {domain_name} already exists', 'error')
|
||||||
|
return redirect(url_for('email.domains_list'))
|
||||||
|
|
||||||
|
# Create domain
|
||||||
|
domain = Domain(domain_name=domain_name)
|
||||||
|
session.add(domain)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Generate DKIM key for the domain
|
||||||
|
dkim_manager = DKIMManager()
|
||||||
|
dkim_manager.generate_dkim_keypair(domain_name)
|
||||||
|
|
||||||
|
flash(f'Domain {domain_name} added successfully with DKIM key', 'success')
|
||||||
|
return redirect(url_for('email.domains_list'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
logger.error(f"Error adding domain: {e}")
|
||||||
|
flash(f'Error adding domain: {str(e)}', 'error')
|
||||||
|
return redirect(url_for('email.add_domain'))
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
return render_template('add_domain.html')
|
||||||
|
|
||||||
|
@email_bp.route('/domains/<int:domain_id>/delete', methods=['POST'])
|
||||||
|
def delete_domain(domain_id: int):
|
||||||
|
"""Delete domain."""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
domain = session.query(Domain).get(domain_id)
|
||||||
|
if not domain:
|
||||||
|
flash('Domain not found', 'error')
|
||||||
|
return redirect(url_for('email.domains_list'))
|
||||||
|
|
||||||
|
domain_name = domain.domain_name
|
||||||
|
domain.is_active = False
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
flash(f'Domain {domain_name} deactivated', 'success')
|
||||||
|
return redirect(url_for('email.domains_list'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
logger.error(f"Error deleting domain: {e}")
|
||||||
|
flash(f'Error deleting domain: {str(e)}', 'error')
|
||||||
|
return redirect(url_for('email.domains_list'))
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@email_bp.route('/users')
|
||||||
|
def users_list():
|
||||||
|
"""List all users."""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
users = session.query(User, Domain).join(Domain, User.domain_id == Domain.id).order_by(User.email).all()
|
||||||
|
return render_template('users.html', users=users)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@email_bp.route('/users/add', methods=['GET', 'POST'])
|
||||||
|
def add_user():
|
||||||
|
"""Add new user."""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
domains = session.query(Domain).filter_by(is_active=True).order_by(Domain.domain_name).all()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
email = request.form.get('email', '').strip().lower()
|
||||||
|
password = request.form.get('password', '').strip()
|
||||||
|
domain_id = request.form.get('domain_id', type=int)
|
||||||
|
can_send_as_domain = request.form.get('can_send_as_domain') == 'on'
|
||||||
|
|
||||||
|
if not all([email, password, domain_id]):
|
||||||
|
flash('All fields are required', 'error')
|
||||||
|
return redirect(url_for('email.add_user'))
|
||||||
|
|
||||||
|
# Validate email format
|
||||||
|
if '@' not in email:
|
||||||
|
flash('Invalid email format', 'error')
|
||||||
|
return redirect(url_for('email.add_user'))
|
||||||
|
|
||||||
|
# Check if user already exists
|
||||||
|
existing = session.query(User).filter_by(email=email).first()
|
||||||
|
if existing:
|
||||||
|
flash(f'User {email} already exists', 'error')
|
||||||
|
return redirect(url_for('email.users_list'))
|
||||||
|
|
||||||
|
# Create user
|
||||||
|
user = User(
|
||||||
|
email=email,
|
||||||
|
password_hash=hash_password(password),
|
||||||
|
domain_id=domain_id,
|
||||||
|
can_send_as_domain=can_send_as_domain
|
||||||
|
)
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
flash(f'User {email} added successfully', 'success')
|
||||||
|
return redirect(url_for('email.users_list'))
|
||||||
|
|
||||||
|
return render_template('add_user.html', domains=domains)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
logger.error(f"Error adding user: {e}")
|
||||||
|
flash(f'Error adding user: {str(e)}', 'error')
|
||||||
|
return redirect(url_for('email.add_user'))
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@email_bp.route('/users/<int:user_id>/delete', methods=['POST'])
|
||||||
|
def delete_user(user_id: int):
|
||||||
|
"""Delete user."""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
user = session.query(User).get(user_id)
|
||||||
|
if not user:
|
||||||
|
flash('User not found', 'error')
|
||||||
|
return redirect(url_for('email.users_list'))
|
||||||
|
|
||||||
|
user_email = user.email
|
||||||
|
user.is_active = False
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
flash(f'User {user_email} deactivated', 'success')
|
||||||
|
return redirect(url_for('email.users_list'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
logger.error(f"Error deleting user: {e}")
|
||||||
|
flash(f'Error deleting user: {str(e)}', 'error')
|
||||||
|
return redirect(url_for('email.users_list'))
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@email_bp.route('/ips')
|
||||||
|
def ips_list():
|
||||||
|
"""List all whitelisted IPs."""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
ips = session.query(WhitelistedIP, Domain).join(Domain, WhitelistedIP.domain_id == Domain.id).order_by(WhitelistedIP.ip_address).all()
|
||||||
|
return render_template('ips.html', ips=ips)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@email_bp.route('/ips/add', methods=['GET', 'POST'])
|
||||||
|
def add_ip():
|
||||||
|
"""Add new whitelisted IP."""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
domains = session.query(Domain).filter_by(is_active=True).order_by(Domain.domain_name).all()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
ip_address = request.form.get('ip_address', '').strip()
|
||||||
|
domain_id = request.form.get('domain_id', type=int)
|
||||||
|
|
||||||
|
if not all([ip_address, domain_id]):
|
||||||
|
flash('All fields are required', 'error')
|
||||||
|
return redirect(url_for('email.add_ip'))
|
||||||
|
|
||||||
|
# Basic IP validation
|
||||||
|
try:
|
||||||
|
socket.inet_aton(ip_address)
|
||||||
|
except socket.error:
|
||||||
|
flash('Invalid IP address format', 'error')
|
||||||
|
return redirect(url_for('email.add_ip'))
|
||||||
|
|
||||||
|
# Check if IP already exists for this domain
|
||||||
|
existing = session.query(WhitelistedIP).filter_by(ip_address=ip_address, domain_id=domain_id).first()
|
||||||
|
if existing:
|
||||||
|
flash(f'IP {ip_address} already whitelisted for this domain', 'error')
|
||||||
|
return redirect(url_for('email.ips_list'))
|
||||||
|
|
||||||
|
# Create whitelisted IP
|
||||||
|
whitelist = WhitelistedIP(
|
||||||
|
ip_address=ip_address,
|
||||||
|
domain_id=domain_id
|
||||||
|
)
|
||||||
|
session.add(whitelist)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
flash(f'IP {ip_address} added to whitelist', 'success')
|
||||||
|
return redirect(url_for('email.ips_list'))
|
||||||
|
|
||||||
|
return render_template('add_ip.html', domains=domains)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
logger.error(f"Error adding IP: {e}")
|
||||||
|
flash(f'Error adding IP: {str(e)}', 'error')
|
||||||
|
return redirect(url_for('email.add_ip'))
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@email_bp.route('/ips/<int:ip_id>/delete', methods=['POST'])
|
||||||
|
def delete_ip(ip_id: int):
|
||||||
|
"""Delete whitelisted IP."""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
ip_record = session.query(WhitelistedIP).get(ip_id)
|
||||||
|
if not ip_record:
|
||||||
|
flash('IP record not found', 'error')
|
||||||
|
return redirect(url_for('email.ips_list'))
|
||||||
|
|
||||||
|
ip_address = ip_record.ip_address
|
||||||
|
ip_record.is_active = False
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
flash(f'IP {ip_address} removed from whitelist', 'success')
|
||||||
|
return redirect(url_for('email.ips_list'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
logger.error(f"Error deleting IP: {e}")
|
||||||
|
flash(f'Error deleting IP: {str(e)}', 'error')
|
||||||
|
return redirect(url_for('email.ips_list'))
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@email_bp.route('/dkim')
|
||||||
|
def dkim_list():
|
||||||
|
"""List all DKIM keys and DNS records."""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
dkim_keys = session.query(DKIMKey, Domain).join(Domain, DKIMKey.domain_id == Domain.id).order_by(Domain.domain_name).all()
|
||||||
|
|
||||||
|
# Get public IP for SPF records
|
||||||
|
public_ip = get_public_ip()
|
||||||
|
|
||||||
|
# Prepare DKIM data with DNS information
|
||||||
|
dkim_data = []
|
||||||
|
for dkim_key, domain in dkim_keys:
|
||||||
|
# Get DKIM DNS record
|
||||||
|
dkim_manager = DKIMManager()
|
||||||
|
dns_record = dkim_manager.get_dkim_public_key_record(domain.domain_name)
|
||||||
|
|
||||||
|
# Check existing SPF record
|
||||||
|
spf_check = check_dns_record(domain.domain_name, 'TXT')
|
||||||
|
existing_spf = None
|
||||||
|
if spf_check['success']:
|
||||||
|
for record in spf_check['records']:
|
||||||
|
if 'v=spf1' in record:
|
||||||
|
existing_spf = record
|
||||||
|
break
|
||||||
|
|
||||||
|
# Generate recommended SPF
|
||||||
|
recommended_spf = generate_spf_record(domain.domain_name, public_ip, existing_spf)
|
||||||
|
|
||||||
|
dkim_data.append({
|
||||||
|
'dkim_key': dkim_key,
|
||||||
|
'domain': domain,
|
||||||
|
'dns_record': dns_record,
|
||||||
|
'existing_spf': existing_spf,
|
||||||
|
'recommended_spf': recommended_spf,
|
||||||
|
'public_ip': public_ip
|
||||||
|
})
|
||||||
|
|
||||||
|
return render_template('dkim.html', dkim_data=dkim_data)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@email_bp.route('/dkim/<int:domain_id>/regenerate', methods=['POST'])
|
||||||
|
def regenerate_dkim(domain_id: int):
|
||||||
|
"""Regenerate DKIM key for domain."""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
domain = session.query(Domain).get(domain_id)
|
||||||
|
if not domain:
|
||||||
|
flash('Domain not found', 'error')
|
||||||
|
return redirect(url_for('email.dkim_list'))
|
||||||
|
|
||||||
|
# Deactivate existing keys
|
||||||
|
existing_keys = session.query(DKIMKey).filter_by(domain_id=domain_id, is_active=True).all()
|
||||||
|
for key in existing_keys:
|
||||||
|
key.is_active = False
|
||||||
|
|
||||||
|
# Generate new DKIM key
|
||||||
|
dkim_manager = DKIMManager()
|
||||||
|
if dkim_manager.generate_dkim_keypair(domain.domain_name):
|
||||||
|
session.commit()
|
||||||
|
flash(f'DKIM key regenerated for {domain.domain_name}', 'success')
|
||||||
|
else:
|
||||||
|
session.rollback()
|
||||||
|
flash(f'Failed to regenerate DKIM key for {domain.domain_name}', 'error')
|
||||||
|
|
||||||
|
return redirect(url_for('email.dkim_list'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
logger.error(f"Error regenerating DKIM: {e}")
|
||||||
|
flash(f'Error regenerating DKIM: {str(e)}', 'error')
|
||||||
|
return redirect(url_for('email.dkim_list'))
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@email_bp.route('/dkim/<int:dkim_id>/update_selector', methods=['POST'])
|
||||||
|
def update_dkim_selector(dkim_id: int):
|
||||||
|
"""Update DKIM selector name."""
|
||||||
|
new_selector = request.form.get('selector', '').strip()
|
||||||
|
|
||||||
|
if not new_selector:
|
||||||
|
flash('Selector name is required', 'error')
|
||||||
|
return redirect(url_for('email.dkim_list'))
|
||||||
|
|
||||||
|
# Validate selector (alphanumeric only)
|
||||||
|
if not re.match(r'^[a-zA-Z0-9]+$', new_selector):
|
||||||
|
flash('Selector must contain only letters and numbers', 'error')
|
||||||
|
return redirect(url_for('email.dkim_list'))
|
||||||
|
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
dkim_key = session.query(DKIMKey).get(dkim_id)
|
||||||
|
if not dkim_key:
|
||||||
|
flash('DKIM key not found', 'error')
|
||||||
|
return redirect(url_for('email.dkim_list'))
|
||||||
|
|
||||||
|
old_selector = dkim_key.selector
|
||||||
|
dkim_key.selector = new_selector
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
flash(f'DKIM selector updated from {old_selector} to {new_selector}', 'success')
|
||||||
|
return redirect(url_for('email.dkim_list'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
logger.error(f"Error updating DKIM selector: {e}")
|
||||||
|
flash(f'Error updating DKIM selector: {str(e)}', 'error')
|
||||||
|
return redirect(url_for('email.dkim_list'))
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@email_bp.route('/dkim/check_dns', methods=['POST'])
|
||||||
|
def check_dkim_dns():
|
||||||
|
"""Check DKIM DNS record via AJAX."""
|
||||||
|
domain = request.form.get('domain')
|
||||||
|
selector = request.form.get('selector')
|
||||||
|
expected_value = request.form.get('expected_value')
|
||||||
|
|
||||||
|
if not all([domain, selector, expected_value]):
|
||||||
|
return jsonify({'success': False, 'message': 'Missing parameters'})
|
||||||
|
|
||||||
|
dns_name = f"{selector}._domainkey.{domain}"
|
||||||
|
result = check_dns_record(dns_name, 'TXT', expected_value)
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
@email_bp.route('/dkim/check_spf', methods=['POST'])
|
||||||
|
def check_spf_dns():
|
||||||
|
"""Check SPF DNS record via AJAX."""
|
||||||
|
domain = request.form.get('domain')
|
||||||
|
|
||||||
|
if not domain:
|
||||||
|
return jsonify({'success': False, 'message': 'Domain is required'})
|
||||||
|
|
||||||
|
result = check_dns_record(domain, 'TXT')
|
||||||
|
|
||||||
|
# Look for SPF record
|
||||||
|
spf_record = None
|
||||||
|
if result['success']:
|
||||||
|
for record in result['records']:
|
||||||
|
if 'v=spf1' in record:
|
||||||
|
spf_record = record
|
||||||
|
break
|
||||||
|
|
||||||
|
if spf_record:
|
||||||
|
result['spf_record'] = spf_record
|
||||||
|
result['message'] = 'SPF record found'
|
||||||
|
else:
|
||||||
|
result['success'] = False
|
||||||
|
result['message'] = 'No SPF record found'
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
@email_bp.route('/settings')
|
||||||
|
def settings():
|
||||||
|
"""Display and edit server settings."""
|
||||||
|
settings = load_settings()
|
||||||
|
return render_template('settings.html', settings=settings)
|
||||||
|
|
||||||
|
@email_bp.route('/settings/update', methods=['POST'])
|
||||||
|
def update_settings():
|
||||||
|
"""Update server settings."""
|
||||||
|
try:
|
||||||
|
# Load current settings
|
||||||
|
config = load_settings()
|
||||||
|
|
||||||
|
# Update settings from form
|
||||||
|
for section_name in config.sections():
|
||||||
|
for key in config[section_name]:
|
||||||
|
if not key.startswith(';'): # Skip comment lines
|
||||||
|
form_key = f"{section_name}.{key}"
|
||||||
|
if form_key in request.form:
|
||||||
|
config.set(section_name, key, request.form[form_key])
|
||||||
|
|
||||||
|
# Save settings
|
||||||
|
with open(SETTINGS_PATH, 'w') as f:
|
||||||
|
config.write(f)
|
||||||
|
|
||||||
|
flash('Settings updated successfully. Restart the server to apply changes.', 'success')
|
||||||
|
return redirect(url_for('email.settings'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating settings: {e}")
|
||||||
|
flash(f'Error updating settings: {str(e)}', 'error')
|
||||||
|
return redirect(url_for('email.settings'))
|
||||||
|
|
||||||
|
@email_bp.route('/logs')
|
||||||
|
def logs():
|
||||||
|
"""Display email and authentication logs."""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
# Get filter parameters
|
||||||
|
filter_type = request.args.get('type', 'all')
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
per_page = 50
|
||||||
|
|
||||||
|
if filter_type == 'emails':
|
||||||
|
# Email logs only
|
||||||
|
total_query = session.query(EmailLog)
|
||||||
|
logs_query = session.query(EmailLog).order_by(EmailLog.created_at.desc())
|
||||||
|
elif filter_type == 'auth':
|
||||||
|
# Auth logs only
|
||||||
|
total_query = session.query(AuthLog)
|
||||||
|
logs_query = session.query(AuthLog).order_by(AuthLog.created_at.desc())
|
||||||
|
else:
|
||||||
|
# Combined view (default)
|
||||||
|
email_logs = session.query(EmailLog).order_by(EmailLog.created_at.desc()).limit(per_page//2).all()
|
||||||
|
auth_logs = session.query(AuthLog).order_by(AuthLog.created_at.desc()).limit(per_page//2).all()
|
||||||
|
|
||||||
|
# Convert to unified format
|
||||||
|
combined_logs = []
|
||||||
|
for log in email_logs:
|
||||||
|
combined_logs.append({
|
||||||
|
'type': 'email',
|
||||||
|
'timestamp': log.created_at,
|
||||||
|
'data': log
|
||||||
|
})
|
||||||
|
for log in auth_logs:
|
||||||
|
combined_logs.append({
|
||||||
|
'type': 'auth',
|
||||||
|
'timestamp': log.created_at,
|
||||||
|
'data': log
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by timestamp
|
||||||
|
combined_logs.sort(key=lambda x: x['timestamp'], reverse=True)
|
||||||
|
|
||||||
|
return render_template('logs.html',
|
||||||
|
logs=combined_logs[:per_page],
|
||||||
|
filter_type=filter_type,
|
||||||
|
page=page,
|
||||||
|
has_next=len(combined_logs) > per_page,
|
||||||
|
has_prev=page > 1)
|
||||||
|
|
||||||
|
# Pagination for single type logs
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
total = total_query.count()
|
||||||
|
logs = logs_query.offset(offset).limit(per_page).all()
|
||||||
|
|
||||||
|
has_next = offset + per_page < total
|
||||||
|
has_prev = page > 1
|
||||||
|
|
||||||
|
return render_template('logs.html',
|
||||||
|
logs=logs,
|
||||||
|
filter_type=filter_type,
|
||||||
|
page=page,
|
||||||
|
has_next=has_next,
|
||||||
|
has_prev=has_prev)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
# Error handlers
|
||||||
|
@email_bp.errorhandler(404)
|
||||||
|
def not_found(error):
|
||||||
|
"""Handle 404 errors."""
|
||||||
|
from datetime import datetime
|
||||||
|
return render_template('error.html',
|
||||||
|
error_code=404,
|
||||||
|
error_message='Page not found',
|
||||||
|
current_time=datetime.now()), 404
|
||||||
|
|
||||||
|
@email_bp.errorhandler(500)
|
||||||
|
def internal_error(error):
|
||||||
|
"""Handle 500 errors."""
|
||||||
|
from datetime import datetime
|
||||||
|
logger.error(f"Internal error: {error}")
|
||||||
|
return render_template('error.html',
|
||||||
|
error_code=500,
|
||||||
|
error_message='Internal server error',
|
||||||
|
current_time=datetime.now()), 500
|
||||||
209
email_frontend/example_app.py
Normal file
209
email_frontend/example_app.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Example Flask Application demonstrating SMTP Management Frontend
|
||||||
|
This example shows how to integrate the email_frontend Blueprint
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
# Add the project root to Python path
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
# Import the SMTP server models and utilities
|
||||||
|
try:
|
||||||
|
from database import Database, Domain, User, WhitelistedIP, DKIMKey, EmailLog, AuthLog
|
||||||
|
from email_frontend.blueprint import email_bp
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Error importing modules: {e}")
|
||||||
|
print("Make sure you're running this from the SMTP_Server directory")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def create_app(config_file='settings.ini'):
|
||||||
|
"""Create and configure the Flask application."""
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Basic Flask configuration
|
||||||
|
app.config['SECRET_KEY'] = 'your-secret-key-change-this-in-production'
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///smtp_server.db'
|
||||||
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
|
||||||
|
# Initialize database
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
# Create database tables if they don't exist
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Register the email management blueprint
|
||||||
|
app.register_blueprint(email_bp, url_prefix='/email')
|
||||||
|
|
||||||
|
# Main application routes
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
"""Main application dashboard."""
|
||||||
|
return redirect(url_for('email.dashboard'))
|
||||||
|
|
||||||
|
@app.route('/health')
|
||||||
|
def health_check():
|
||||||
|
"""Simple health check endpoint."""
|
||||||
|
return jsonify({
|
||||||
|
'status': 'healthy',
|
||||||
|
'timestamp': datetime.utcnow().isoformat(),
|
||||||
|
'service': 'SMTP Management Frontend'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Error handlers
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found_error(error):
|
||||||
|
"""Handle 404 errors."""
|
||||||
|
return render_template('error.html',
|
||||||
|
error_code=404,
|
||||||
|
error_message="Page not found",
|
||||||
|
error_details="The requested page could not be found."), 404
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def internal_error(error):
|
||||||
|
"""Handle 500 errors."""
|
||||||
|
return render_template('error.html',
|
||||||
|
error_code=500,
|
||||||
|
error_message="Internal server error",
|
||||||
|
error_details=str(error)), 500
|
||||||
|
|
||||||
|
@app.errorhandler(403)
|
||||||
|
def forbidden_error(error):
|
||||||
|
"""Handle 403 errors."""
|
||||||
|
return render_template('error.html',
|
||||||
|
error_code=403,
|
||||||
|
error_message="Access forbidden",
|
||||||
|
error_details="You don't have permission to access this resource."), 403
|
||||||
|
|
||||||
|
# Context processors for templates
|
||||||
|
@app.context_processor
|
||||||
|
def utility_processor():
|
||||||
|
"""Add utility functions to template context."""
|
||||||
|
return {
|
||||||
|
'moment': datetime,
|
||||||
|
'len': len,
|
||||||
|
'enumerate': enumerate,
|
||||||
|
'zip': zip,
|
||||||
|
'str': str,
|
||||||
|
'int': int,
|
||||||
|
}
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
def init_sample_data():
|
||||||
|
"""Initialize the database with sample data for testing."""
|
||||||
|
try:
|
||||||
|
# Initialize database connection
|
||||||
|
db = Database('settings.ini')
|
||||||
|
|
||||||
|
# Add sample domains
|
||||||
|
sample_domains = [
|
||||||
|
'example.com',
|
||||||
|
'testdomain.org',
|
||||||
|
'mydomain.net'
|
||||||
|
]
|
||||||
|
|
||||||
|
for domain_name in sample_domains:
|
||||||
|
if not db.get_domain(domain_name):
|
||||||
|
domain = Domain(domain_name)
|
||||||
|
db.add_domain(domain)
|
||||||
|
print(f"Added sample domain: {domain_name}")
|
||||||
|
|
||||||
|
# Add sample users
|
||||||
|
sample_users = [
|
||||||
|
('admin@example.com', 'example.com', 'admin123'),
|
||||||
|
('user@example.com', 'example.com', 'user123'),
|
||||||
|
('test@testdomain.org', 'testdomain.org', 'test123')
|
||||||
|
]
|
||||||
|
|
||||||
|
for email, domain, password in sample_users:
|
||||||
|
if not db.get_user(email):
|
||||||
|
user = User(email, domain, password)
|
||||||
|
db.add_user(user)
|
||||||
|
print(f"Added sample user: {email}")
|
||||||
|
|
||||||
|
# Add sample whitelisted IPs
|
||||||
|
sample_ips = [
|
||||||
|
('127.0.0.1', 'example.com', 'localhost'),
|
||||||
|
('192.168.1.0/24', 'example.com', 'local network'),
|
||||||
|
('10.0.0.0/8', 'testdomain.org', 'private network')
|
||||||
|
]
|
||||||
|
|
||||||
|
for ip, domain, description in sample_ips:
|
||||||
|
if not db.get_whitelisted_ip(ip, domain):
|
||||||
|
whitelisted_ip = WhitelistedIP(ip, domain, description)
|
||||||
|
db.add_whitelisted_ip(whitelisted_ip)
|
||||||
|
print(f"Added sample whitelisted IP: {ip} for {domain}")
|
||||||
|
|
||||||
|
print("Sample data initialized successfully!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error initializing sample data: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function to run the example application."""
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='SMTP Management Frontend Example')
|
||||||
|
parser.add_argument('--host', default='127.0.0.1', help='Host to bind to')
|
||||||
|
parser.add_argument('--port', type=int, default=5000, help='Port to bind to')
|
||||||
|
parser.add_argument('--debug', action='store_true', help='Enable debug mode')
|
||||||
|
parser.add_argument('--init-data', action='store_true', help='Initialize sample data')
|
||||||
|
parser.add_argument('--config', default='settings.ini', help='Configuration file path')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Initialize sample data if requested
|
||||||
|
if args.init_data:
|
||||||
|
print("Initializing sample data...")
|
||||||
|
init_sample_data()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create Flask application
|
||||||
|
app = create_app(args.config)
|
||||||
|
|
||||||
|
print(f"""
|
||||||
|
SMTP Management Frontend Example
|
||||||
|
================================
|
||||||
|
|
||||||
|
Starting server on http://{args.host}:{args.port}
|
||||||
|
|
||||||
|
Available routes:
|
||||||
|
- / -> Dashboard (redirects to /email/dashboard)
|
||||||
|
- /email/dashboard -> Main dashboard
|
||||||
|
- /email/domains -> Domain management
|
||||||
|
- /email/users -> User management
|
||||||
|
- /email/ips -> IP whitelist management
|
||||||
|
- /email/dkim -> DKIM management
|
||||||
|
- /email/settings -> Server settings
|
||||||
|
- /email/logs -> Email and authentication logs
|
||||||
|
- /health -> Health check endpoint
|
||||||
|
|
||||||
|
Debug mode: {'ON' if args.debug else 'OFF'}
|
||||||
|
|
||||||
|
To initialize sample data, run:
|
||||||
|
python example_app.py --init-data
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Run the Flask application
|
||||||
|
try:
|
||||||
|
app.run(
|
||||||
|
host=args.host,
|
||||||
|
port=args.port,
|
||||||
|
debug=args.debug,
|
||||||
|
threaded=True
|
||||||
|
)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nShutting down gracefully...")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error starting server: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
7
email_frontend/requirements.txt
Normal file
7
email_frontend/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Flask
|
||||||
|
Flask-SQLAlchemy
|
||||||
|
Jinja2
|
||||||
|
Werkzeug
|
||||||
|
dnspython
|
||||||
|
cryptography
|
||||||
|
requests
|
||||||
260
email_frontend/static/css/smtp-management.css
Normal file
260
email_frontend/static/css/smtp-management.css
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
/* Custom CSS for SMTP Management Frontend */
|
||||||
|
|
||||||
|
/* Enhanced dark theme tweaks */
|
||||||
|
.card {
|
||||||
|
border: 1px solid #404040;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
border-bottom: 1px solid #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark {
|
||||||
|
--bs-table-bg: #2d3748;
|
||||||
|
--bs-table-striped-bg: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status badges */
|
||||||
|
.status-active {
|
||||||
|
background-color: #10b981 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inactive {
|
||||||
|
background-color: #ef4444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background-color: #f59e0b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom form styling */
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy button styling */
|
||||||
|
.copy-btn {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn::after {
|
||||||
|
content: "Copied!";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: #10b981;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn.copied::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DNS record styling */
|
||||||
|
.dns-record {
|
||||||
|
background-color: #1f2937;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dns-record-header {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dns-record-value {
|
||||||
|
color: #e5e7eb;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Log entry styling */
|
||||||
|
.log-entry {
|
||||||
|
border-left: 4px solid #374151;
|
||||||
|
padding-left: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.log-error {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.log-warning {
|
||||||
|
border-left-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.log-info {
|
||||||
|
border-left-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.log-success {
|
||||||
|
border-left-color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Statistics cards */
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
|
||||||
|
border: 1px solid #4b5563;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading states */
|
||||||
|
.loading {
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border-sm {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive table wrapper */
|
||||||
|
.table-responsive {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert styling */
|
||||||
|
.alert {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: rgba(16, 185, 129, 0.1);
|
||||||
|
color: #10b981;
|
||||||
|
border-left: 4px solid #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #ef4444;
|
||||||
|
border-left: 4px solid #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background-color: rgba(245, 158, 11, 0.1);
|
||||||
|
color: #f59e0b;
|
||||||
|
border-left: 4px solid #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background-color: rgba(59, 130, 246, 0.1);
|
||||||
|
color: #3b82f6;
|
||||||
|
border-left: 4px solid #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
.custom-scrollbar {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #4b5563 #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #4b5563;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation classes */
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-in {
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateX(-20px); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip styling */
|
||||||
|
.tooltip {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-inner {
|
||||||
|
background-color: #1f2937;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.bs-tooltip-top .tooltip-arrow::before {
|
||||||
|
border-top-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.bs-tooltip-bottom .tooltip-arrow::before {
|
||||||
|
border-bottom-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsiveness */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stat-number {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dns-record {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
285
email_frontend/static/js/smtp-management.js
Normal file
285
email_frontend/static/js/smtp-management.js
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
/* Custom JavaScript for SMTP Management Frontend */
|
||||||
|
|
||||||
|
// Global utilities
|
||||||
|
const SMTPManagement = {
|
||||||
|
// Copy text to clipboard
|
||||||
|
copyToClipboard: function(text, button) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
this.showCopySuccess(button);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
this.showCopyError(button);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show copy success feedback
|
||||||
|
showCopySuccess: function(button) {
|
||||||
|
const originalText = button.innerHTML;
|
||||||
|
button.innerHTML = '<i class="fas fa-check me-1"></i>Copied!';
|
||||||
|
button.classList.remove('btn-outline-light');
|
||||||
|
button.classList.add('btn-success');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = originalText;
|
||||||
|
button.classList.remove('btn-success');
|
||||||
|
button.classList.add('btn-outline-light');
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show copy error feedback
|
||||||
|
showCopyError: function(button) {
|
||||||
|
const originalText = button.innerHTML;
|
||||||
|
button.innerHTML = '<i class="fas fa-times me-1"></i>Failed!';
|
||||||
|
button.classList.remove('btn-outline-light');
|
||||||
|
button.classList.add('btn-danger');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = originalText;
|
||||||
|
button.classList.remove('btn-danger');
|
||||||
|
button.classList.add('btn-outline-light');
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Format timestamps
|
||||||
|
formatTimestamp: function(timestamp) {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return date.toLocaleString();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Validate email address
|
||||||
|
validateEmail: function(email) {
|
||||||
|
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return re.test(email);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Validate IP address
|
||||||
|
validateIP: function(ip) {
|
||||||
|
const ipv4Regex = /^(?:(?: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]?)$/;
|
||||||
|
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
||||||
|
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
showLoading: function(element) {
|
||||||
|
element.classList.add('loading');
|
||||||
|
const spinner = element.querySelector('.spinner-border');
|
||||||
|
if (spinner) {
|
||||||
|
spinner.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Hide loading state
|
||||||
|
hideLoading: function(element) {
|
||||||
|
element.classList.remove('loading');
|
||||||
|
const spinner = element.querySelector('.spinner-border');
|
||||||
|
if (spinner) {
|
||||||
|
spinner.style.display = 'none';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show toast notification
|
||||||
|
showToast: function(message, type = 'info') {
|
||||||
|
const toastContainer = document.getElementById('toast-container') || this.createToastContainer();
|
||||||
|
const toast = this.createToast(message, type);
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Auto-remove after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create toast container
|
||||||
|
createToastContainer: function() {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.id = 'toast-container';
|
||||||
|
container.className = 'position-fixed top-0 end-0 p-3';
|
||||||
|
container.style.zIndex = '1056';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
return container;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create toast element
|
||||||
|
createToast: function(message, type) {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast align-items-center text-white bg-${type} border-0`;
|
||||||
|
toast.setAttribute('role', 'alert');
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body">${message}</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Initialize Bootstrap toast
|
||||||
|
const bsToast = new bootstrap.Toast(toast);
|
||||||
|
bsToast.show();
|
||||||
|
|
||||||
|
return toast;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Auto-refresh functionality
|
||||||
|
autoRefresh: function(url, interval = 30000) {
|
||||||
|
setInterval(() => {
|
||||||
|
fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
const newContent = doc.querySelector('#refresh-content');
|
||||||
|
const currentContent = document.querySelector('#refresh-content');
|
||||||
|
|
||||||
|
if (newContent && currentContent) {
|
||||||
|
currentContent.innerHTML = newContent.innerHTML;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Auto-refresh failed:', error);
|
||||||
|
});
|
||||||
|
}, interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DNS verification functionality
|
||||||
|
const DNSVerification = {
|
||||||
|
// Check DNS record
|
||||||
|
checkDNSRecord: function(domain, recordType, expectedValue) {
|
||||||
|
return fetch('/email/check-dns', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
domain: domain,
|
||||||
|
record_type: recordType,
|
||||||
|
expected_value: expectedValue
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json());
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update DNS status indicator
|
||||||
|
updateDNSStatus: function(element, status, message) {
|
||||||
|
const statusIcon = element.querySelector('.dns-status-icon');
|
||||||
|
const statusText = element.querySelector('.dns-status-text');
|
||||||
|
|
||||||
|
if (statusIcon && statusText) {
|
||||||
|
statusIcon.className = `dns-status-icon fas ${status === 'valid' ? 'fa-check-circle text-success' : 'fa-times-circle text-danger'}`;
|
||||||
|
statusText.textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
const FormValidation = {
|
||||||
|
// Real-time email validation
|
||||||
|
validateEmailField: function(input) {
|
||||||
|
const isValid = SMTPManagement.validateEmail(input.value);
|
||||||
|
this.updateFieldStatus(input, isValid, 'Please enter a valid email address');
|
||||||
|
return isValid;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Real-time IP validation
|
||||||
|
validateIPField: function(input) {
|
||||||
|
const isValid = SMTPManagement.validateIP(input.value);
|
||||||
|
this.updateFieldStatus(input, isValid, 'Please enter a valid IP address');
|
||||||
|
return isValid;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update field validation status
|
||||||
|
updateFieldStatus: function(input, isValid, errorMessage) {
|
||||||
|
const feedback = input.parentNode.querySelector('.invalid-feedback');
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
input.classList.remove('is-invalid');
|
||||||
|
input.classList.add('is-valid');
|
||||||
|
if (feedback) feedback.textContent = '';
|
||||||
|
} else {
|
||||||
|
input.classList.remove('is-valid');
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
if (feedback) feedback.textContent = errorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize on DOM load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize tooltips
|
||||||
|
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
|
tooltipTriggerList.map(function(tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize form validation
|
||||||
|
const emailInputs = document.querySelectorAll('input[type="email"]');
|
||||||
|
emailInputs.forEach(input => {
|
||||||
|
input.addEventListener('blur', () => FormValidation.validateEmailField(input));
|
||||||
|
});
|
||||||
|
|
||||||
|
const ipInputs = document.querySelectorAll('input[data-validate="ip"]');
|
||||||
|
ipInputs.forEach(input => {
|
||||||
|
input.addEventListener('blur', () => FormValidation.validateIPField(input));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize auto-refresh for logs page
|
||||||
|
if (document.querySelector('#logs-page')) {
|
||||||
|
SMTPManagement.autoRefresh(window.location.href, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize current IP detection
|
||||||
|
const currentIPSpan = document.querySelector('#current-ip');
|
||||||
|
if (currentIPSpan) {
|
||||||
|
fetch('https://api.ipify.org?format=json')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
currentIPSpan.textContent = data.ip;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
currentIPSpan.textContent = 'Unable to detect';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize copy buttons
|
||||||
|
const copyButtons = document.querySelectorAll('.copy-btn');
|
||||||
|
copyButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const textToCopy = this.getAttribute('data-copy') || this.nextElementSibling.textContent;
|
||||||
|
SMTPManagement.copyToClipboard(textToCopy, this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize DNS check buttons
|
||||||
|
const dnsCheckButtons = document.querySelectorAll('.dns-check-btn');
|
||||||
|
dnsCheckButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const domain = this.getAttribute('data-domain');
|
||||||
|
const recordType = this.getAttribute('data-record-type');
|
||||||
|
const expectedValue = this.getAttribute('data-expected-value');
|
||||||
|
const statusElement = this.closest('.dns-record').querySelector('.dns-status');
|
||||||
|
|
||||||
|
SMTPManagement.showLoading(this);
|
||||||
|
|
||||||
|
DNSVerification.checkDNSRecord(domain, recordType, expectedValue)
|
||||||
|
.then(result => {
|
||||||
|
DNSVerification.updateDNSStatus(statusElement, result.status, result.message);
|
||||||
|
SMTPManagement.hideLoading(this);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('DNS check failed:', error);
|
||||||
|
DNSVerification.updateDNSStatus(statusElement, 'error', 'DNS check failed');
|
||||||
|
SMTPManagement.hideLoading(this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export for use in other scripts
|
||||||
|
window.SMTPManagement = SMTPManagement;
|
||||||
|
window.DNSVerification = DNSVerification;
|
||||||
|
window.FormValidation = FormValidation;
|
||||||
112
email_frontend/templates/add_domain.html
Normal file
112
email_frontend/templates/add_domain.html
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Add Domain - Email Server Management{% endblock %}
|
||||||
|
{% block page_title %}Add New Domain{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add New Domain
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="domain_name" class="form-label">
|
||||||
|
<i class="bi bi-globe me-1"></i>
|
||||||
|
Domain Name
|
||||||
|
</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="domain_name"
|
||||||
|
name="domain_name"
|
||||||
|
placeholder="example.com"
|
||||||
|
required
|
||||||
|
pattern="^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.?[a-zA-Z]{2,}$">
|
||||||
|
<div class="form-text">
|
||||||
|
Enter the domain name that will be used for sending emails (e.g., example.com)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
What happens next?
|
||||||
|
</h6>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Domain will be added to the system</li>
|
||||||
|
<li>DKIM key pair will be automatically generated</li>
|
||||||
|
<li>You'll need to configure DNS records</li>
|
||||||
|
<li>Add users or whitelist IPs for authentication</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ url_for('email.domains_list') }}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-arrow-left me-2"></i>
|
||||||
|
Back to Domains
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add Domain
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bi bi-question-circle me-2"></i>
|
||||||
|
Domain Requirements
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-success">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
|
Valid Examples
|
||||||
|
</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><code>example.com</code></li>
|
||||||
|
<li><code>mail.example.com</code></li>
|
||||||
|
<li><code>my-domain.org</code></li>
|
||||||
|
<li><code>company.co.uk</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-danger">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>
|
||||||
|
Invalid Examples
|
||||||
|
</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><code>http://example.com</code></li>
|
||||||
|
<li><code>example</code></li>
|
||||||
|
<li><code>.example.com</code></li>
|
||||||
|
<li><code>example..com</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
document.getElementById('domain_name').addEventListener('input', function(e) {
|
||||||
|
// Convert to lowercase and remove protocol if present
|
||||||
|
let value = e.target.value.toLowerCase();
|
||||||
|
value = value.replace(/^https?:\/\//, '');
|
||||||
|
value = value.replace(/\/$/, '');
|
||||||
|
e.target.value = value;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
228
email_frontend/templates/add_ip.html
Normal file
228
email_frontend/templates/add_ip.html
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Add IP Address - Email Server{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 mx-auto">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<i class="bi bi-shield-plus me-2"></i>
|
||||||
|
Add IP Address to Whitelist
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ip_address" class="form-label">IP Address</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control font-monospace"
|
||||||
|
id="ip_address"
|
||||||
|
name="ip_address"
|
||||||
|
required
|
||||||
|
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]?)$"
|
||||||
|
placeholder="192.168.1.100"
|
||||||
|
value="{{ request.args.get('ip', '') }}">
|
||||||
|
<div class="form-text">
|
||||||
|
IPv4 address that will be allowed to send emails without authentication
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="domain_id" class="form-label">Authorized Domain</label>
|
||||||
|
<select class="form-select" id="domain_id" name="domain_id" required>
|
||||||
|
<option value="">Select a domain...</option>
|
||||||
|
{% for domain in domains %}
|
||||||
|
<option value="{{ domain.id }}">{{ domain.domain_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
This IP will only be able to send emails for the selected domain
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
|
Security Note
|
||||||
|
</h6>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Only whitelist trusted IP addresses</li>
|
||||||
|
<li>This IP can send emails without username/password authentication</li>
|
||||||
|
<li>The IP is restricted to the selected domain only</li>
|
||||||
|
<li>Use static IP addresses for reliable access</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ url_for('email.ips_list') }}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-arrow-left me-2"></i>
|
||||||
|
Back to IP List
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="bi bi-shield-plus me-2"></i>
|
||||||
|
Add to Whitelist
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 %}
|
||||||
|
<script>
|
||||||
|
// Detect current IP address
|
||||||
|
async function detectCurrentIP() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.ipify.org?format=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-muted">Unable to detect</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useCurrentIP() {
|
||||||
|
const currentIPElement = document.getElementById('current-ip');
|
||||||
|
const ip = currentIPElement.textContent.trim();
|
||||||
|
|
||||||
|
if (ip && ip !== 'Detecting...' && ip !== 'Unable to detect') {
|
||||||
|
document.getElementById('ip_address').value = ip;
|
||||||
|
// Focus on domain selection
|
||||||
|
document.getElementById('domain_id').focus();
|
||||||
|
} else {
|
||||||
|
alert('Unable to detect current IP address');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP address validation
|
||||||
|
document.getElementById('ip_address').addEventListener('input', function(e) {
|
||||||
|
const ip = e.target.value;
|
||||||
|
const ipPattern = /^(?:(?: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]?)$/;
|
||||||
|
|
||||||
|
if (ip && !ipPattern.test(ip)) {
|
||||||
|
e.target.setCustomValidity('Please enter a valid IPv4 address');
|
||||||
|
} else {
|
||||||
|
e.target.setCustomValidity('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-detect IP on page load
|
||||||
|
detectCurrentIP();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
176
email_frontend/templates/add_user.html
Normal file
176
email_frontend/templates/add_user.html
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Add User - Email Server{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 mx-auto">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>
|
||||||
|
Add New User
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email Address</label>
|
||||||
|
<input type="email"
|
||||||
|
class="form-control"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
placeholder="user@example.com">
|
||||||
|
<div class="form-text">
|
||||||
|
The email address for authentication and sending
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
minlength="6">
|
||||||
|
<div class="form-text">
|
||||||
|
Minimum 6 characters
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="domain_id" class="form-label">Domain</label>
|
||||||
|
<select class="form-select" id="domain_id" name="domain_id" required>
|
||||||
|
<option value="">Select a domain...</option>
|
||||||
|
{% for domain in domains %}
|
||||||
|
<option value="{{ domain.id }}">{{ domain.domain_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
The domain this user belongs to
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="can_send_as_domain"
|
||||||
|
name="can_send_as_domain">
|
||||||
|
<label class="form-check-label" for="can_send_as_domain">
|
||||||
|
<strong>Domain Administrator</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.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
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>
|
||||||
|
</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
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>
|
||||||
|
Add User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 %}
|
||||||
|
<script>
|
||||||
|
// Auto-fill domain based on email input
|
||||||
|
document.getElementById('email').addEventListener('input', function(e) {
|
||||||
|
const email = e.target.value;
|
||||||
|
const atIndex = email.indexOf('@');
|
||||||
|
|
||||||
|
if (atIndex > -1) {
|
||||||
|
const domain = email.substring(atIndex + 1).toLowerCase();
|
||||||
|
const domainSelect = document.getElementById('domain_id');
|
||||||
|
|
||||||
|
// Try to find matching domain in select options
|
||||||
|
for (let option of domainSelect.options) {
|
||||||
|
if (option.text.toLowerCase() === domain) {
|
||||||
|
domainSelect.value = option.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show/hide domain admin explanation
|
||||||
|
document.getElementById('can_send_as_domain').addEventListener('change', function(e) {
|
||||||
|
const isChecked = e.target.checked;
|
||||||
|
const domainSelect = document.getElementById('domain_id');
|
||||||
|
const selectedDomain = domainSelect.options[domainSelect.selectedIndex]?.text || 'domain.com';
|
||||||
|
|
||||||
|
// Update help text dynamically
|
||||||
|
const helpText = e.target.closest('.form-check').querySelector('.form-text');
|
||||||
|
if (isChecked) {
|
||||||
|
helpText.innerHTML = `User can send as any address in ${selectedDomain} (e.g., noreply@${selectedDomain}, support@${selectedDomain})`;
|
||||||
|
} else {
|
||||||
|
helpText.innerHTML = 'User can only send as their own email address.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update help text when domain changes
|
||||||
|
document.getElementById('domain_id').addEventListener('change', function(e) {
|
||||||
|
const checkbox = document.getElementById('can_send_as_domain');
|
||||||
|
if (checkbox.checked) {
|
||||||
|
checkbox.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
227
email_frontend/templates/base.html
Normal file
227
email_frontend/templates/base.html
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Email Server Management{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- Bootstrap Icons -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--sidebar-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: var(--sidebar-width);
|
||||||
|
padding: 20px;
|
||||||
|
transition: margin-left 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark {
|
||||||
|
--bs-table-bg: #2d2d2d;
|
||||||
|
--bs-table-border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-light:hover {
|
||||||
|
background-color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: #0f5132;
|
||||||
|
border-color: #146c43;
|
||||||
|
color: #75b798;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background-color: #58151c;
|
||||||
|
border-color: #842029;
|
||||||
|
color: #ea868f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background-color: #664d03;
|
||||||
|
border-color: #997404;
|
||||||
|
color: #ffda6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background-color: #055160;
|
||||||
|
border-color: #087990;
|
||||||
|
color: #6edff6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: #0d6efd;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select:focus {
|
||||||
|
border-color: #0d6efd;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #adb5bd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-success {
|
||||||
|
border-color: #198754 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-danger {
|
||||||
|
border-color: #dc3545 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-success {
|
||||||
|
color: #75b798 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #ea868f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-warning {
|
||||||
|
color: #ffda6a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #495057;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #6c757d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Custom SMTP Management CSS -->
|
||||||
|
<link href="{{ url_for('email.static', filename='css/smtp-management.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main-container">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
{% include 'sidebar_email.html' %}
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<div class="content-area">
|
||||||
|
<!-- Top navbar -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<span class="navbar-brand mb-0 h1">
|
||||||
|
<i class="bi bi-envelope-fill me-2"></i>
|
||||||
|
{% block page_title %}Email Server Management{% endblock %}
|
||||||
|
</span>
|
||||||
|
<div class="navbar-nav ms-auto">
|
||||||
|
<span class="navbar-text">
|
||||||
|
<i class="bi bi-clock-fill me-1"></i>
|
||||||
|
<span id="current-time"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Flash messages -->
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-{{ 'exclamation-triangle' if category == 'error' else 'check-circle' if category == 'success' else 'info-circle' }} me-2"></i>
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<!-- Page content -->
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS -->
|
||||||
|
<script>
|
||||||
|
// Update current time
|
||||||
|
function updateTime() {
|
||||||
|
const now = new Date();
|
||||||
|
const timeString = now.toLocaleTimeString();
|
||||||
|
const dateString = now.toLocaleDateString();
|
||||||
|
document.getElementById('current-time').textContent = `${dateString} ${timeString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update time every second
|
||||||
|
setInterval(updateTime, 1000);
|
||||||
|
updateTime(); // Initial call
|
||||||
|
|
||||||
|
// Auto-dismiss alerts after 5 seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
const alerts = document.querySelectorAll('.alert:not(.alert-permanent)');
|
||||||
|
alerts.forEach(function(alert) {
|
||||||
|
const bootstrapAlert = new bootstrap.Alert(alert);
|
||||||
|
bootstrapAlert.close();
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Confirmation dialogs for delete actions
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const deleteButtons = document.querySelectorAll('[data-confirm]');
|
||||||
|
deleteButtons.forEach(function(button) {
|
||||||
|
button.addEventListener('click', function(e) {
|
||||||
|
if (!confirm(this.getAttribute('data-confirm'))) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Custom SMTP Management JavaScript -->
|
||||||
|
<script src="{{ url_for('email.static', filename='js/smtp-management.js') }}"></script>
|
||||||
|
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
285
email_frontend/templates/dashboard.html
Normal file
285
email_frontend/templates/dashboard.html
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard - Email Server Management{% endblock %}
|
||||||
|
{% block page_title %}Dashboard{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<!-- Statistics Cards -->
|
||||||
|
<div class="col-lg-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-primary">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h5 class="card-title text-primary mb-1">
|
||||||
|
<i class="bi bi-globe me-2"></i>
|
||||||
|
Domains
|
||||||
|
</h5>
|
||||||
|
<h3 class="mb-0">{{ domain_count }}</h3>
|
||||||
|
<small class="text-muted">Active domains</small>
|
||||||
|
</div>
|
||||||
|
<div class="fs-2 text-primary opacity-50">
|
||||||
|
<i class="bi bi-globe"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-success">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h5 class="card-title text-success mb-1">
|
||||||
|
<i class="bi bi-people me-2"></i>
|
||||||
|
Users
|
||||||
|
</h5>
|
||||||
|
<h3 class="mb-0">{{ user_count }}</h3>
|
||||||
|
<small class="text-muted">Authenticated users</small>
|
||||||
|
</div>
|
||||||
|
<div class="fs-2 text-success opacity-50">
|
||||||
|
<i class="bi bi-people"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-warning">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h5 class="card-title text-warning mb-1">
|
||||||
|
<i class="bi bi-shield-check me-2"></i>
|
||||||
|
DKIM Keys
|
||||||
|
</h5>
|
||||||
|
<h3 class="mb-0">{{ dkim_count }}</h3>
|
||||||
|
<small class="text-muted">Active DKIM keys</small>
|
||||||
|
</div>
|
||||||
|
<div class="fs-2 text-warning opacity-50">
|
||||||
|
<i class="bi bi-shield-check"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-info">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h5 class="card-title text-info mb-1">
|
||||||
|
<i class="bi bi-activity me-2"></i>
|
||||||
|
Status
|
||||||
|
</h5>
|
||||||
|
<h6 class="text-success mb-0">
|
||||||
|
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>
|
||||||
|
Online
|
||||||
|
</h6>
|
||||||
|
<small class="text-muted">Server running</small>
|
||||||
|
</div>
|
||||||
|
<div class="fs-2 text-info opacity-50">
|
||||||
|
<i class="bi bi-activity"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Recent Email Activity -->
|
||||||
|
<div class="col-lg-8 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-envelope me-2"></i>
|
||||||
|
Recent Email Activity
|
||||||
|
</h5>
|
||||||
|
<a href="{{ url_for('email.logs', type='emails') }}" class="btn btn-outline-light btn-sm">
|
||||||
|
View All
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if recent_emails %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>From</th>
|
||||||
|
<th>To</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>DKIM</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for email in recent_emails %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ email.created_at.strftime('%H:%M:%S') }}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-truncate d-inline-block" style="max-width: 150px;" title="{{ email.mail_from }}">
|
||||||
|
{{ email.mail_from }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-truncate d-inline-block" style="max-width: 150px;" title="{{ email.to_address }}">
|
||||||
|
{{ email.to_address }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if email.status == 'relayed' %}
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
|
Sent
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>
|
||||||
|
Failed
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if email.dkim_signed %}
|
||||||
|
<span class="text-success">
|
||||||
|
<i class="bi bi-shield-check" title="DKIM Signed"></i>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">
|
||||||
|
<i class="bi bi-shield-x" title="Not DKIM Signed"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-envelope text-muted fs-1"></i>
|
||||||
|
<p class="text-muted mt-2">No email activity yet</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Authentication Activity -->
|
||||||
|
<div class="col-lg-4 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-shield-lock me-2"></i>
|
||||||
|
Recent Auth Activity
|
||||||
|
</h5>
|
||||||
|
<a href="{{ url_for('email.logs', type='auth') }}" class="btn btn-outline-light btn-sm">
|
||||||
|
View All
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if recent_auths %}
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{% for auth in recent_auths %}
|
||||||
|
<div class="list-group-item list-group-item-dark d-flex justify-content-between align-items-start">
|
||||||
|
<div class="ms-2 me-auto">
|
||||||
|
<div class="fw-bold">
|
||||||
|
{% if auth.success %}
|
||||||
|
<i class="bi bi-check-circle text-success me-1"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-x-circle text-danger me-1"></i>
|
||||||
|
{% endif %}
|
||||||
|
{{ auth.auth_type|title }}
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ auth.identifier }}
|
||||||
|
</small>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ auth.created_at.strftime('%H:%M:%S') }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ auth.ip_address }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-shield-lock text-muted fs-1"></i>
|
||||||
|
<p class="text-muted mt-2">No authentication activity yet</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-lightning me-2"></i>
|
||||||
|
Quick Actions
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{{ url_for('email.add_domain') }}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add Domain
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{{ url_for('email.add_user') }}" class="btn btn-outline-success">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>
|
||||||
|
Add User
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{{ url_for('email.add_ip') }}" class="btn btn-outline-warning">
|
||||||
|
<i class="bi bi-shield-plus me-2"></i>
|
||||||
|
Whitelist IP
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{{ url_for('email.settings') }}" class="btn btn-outline-info">
|
||||||
|
<i class="bi bi-gear me-2"></i>
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// Auto-refresh dashboard every 30 seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
location.reload();
|
||||||
|
}, 30000);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
318
email_frontend/templates/dkim.html
Normal file
318
email_frontend/templates/dkim.html
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}DKIM Keys - Email Server{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.dns-record {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background-color: var(--bs-gray-100);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.status-indicator {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.status-success { background-color: #28a745; }
|
||||||
|
.status-warning { background-color: #ffc107; }
|
||||||
|
.status-danger { background-color: #dc3545; }
|
||||||
|
</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-shield-check me-2"></i>
|
||||||
|
DKIM Key Management
|
||||||
|
</h2>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-outline-info" onclick="checkAllDNS()">
|
||||||
|
<i class="bi bi-arrow-clockwise me-2"></i>
|
||||||
|
Check All DNS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for item in dkim_data %}
|
||||||
|
<div class="card mb-4" id="domain-{{ item.domain.id }}">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-server me-2"></i>
|
||||||
|
{{ item.domain.domain_name }}
|
||||||
|
{% if item.dkim_key.is_active %}
|
||||||
|
<span class="badge bg-success ms-2">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary ms-2">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button class="btn btn-outline-primary" onclick="checkDomainDNS('{{ item.domain.domain_name }}', '{{ item.dkim_key.selector }}')">
|
||||||
|
<i class="bi bi-search me-1"></i>
|
||||||
|
Check DNS
|
||||||
|
</button>
|
||||||
|
<form method="post" action="{{ url_for('email.regenerate_dkim', domain_id=item.domain.id) }}" class="d-inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-outline-warning"
|
||||||
|
onclick="return confirm('Regenerate DKIM key for {{ item.domain.domain_name }}? This will require updating DNS records.')">
|
||||||
|
<i class="bi bi-arrow-clockwise me-1"></i>
|
||||||
|
Regenerate
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<!-- DKIM DNS Record -->
|
||||||
|
<div class="col-lg-6 mb-3">
|
||||||
|
<h6>
|
||||||
|
<i class="bi bi-key me-2"></i>
|
||||||
|
DKIM DNS Record
|
||||||
|
<span class="dns-status" id="dkim-status-{{ item.domain.id }}">
|
||||||
|
<span class="status-indicator status-warning"></span>
|
||||||
|
<small class="text-muted">Not checked</small>
|
||||||
|
</span>
|
||||||
|
</h6>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Name:</strong>
|
||||||
|
<div class="dns-record">{{ item.dns_record.name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Type:</strong> TXT
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Value:</strong>
|
||||||
|
<div class="dns-record">{{ item.dns_record.value }}</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="copyToClipboard('{{ item.dns_record.value }}')">
|
||||||
|
<i class="bi bi-clipboard me-1"></i>
|
||||||
|
Copy Value
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SPF DNS Record -->
|
||||||
|
<div class="col-lg-6 mb-3">
|
||||||
|
<h6>
|
||||||
|
<i class="bi bi-shield-lock me-2"></i>
|
||||||
|
SPF DNS Record
|
||||||
|
<span class="dns-status" id="spf-status-{{ item.domain.id }}">
|
||||||
|
<span class="status-indicator status-warning"></span>
|
||||||
|
<small class="text-muted">Not checked</small>
|
||||||
|
</span>
|
||||||
|
</h6>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Name:</strong>
|
||||||
|
<div class="dns-record">{{ item.domain.domain_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Type:</strong> TXT
|
||||||
|
</div>
|
||||||
|
{% if item.existing_spf %}
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Current SPF:</strong>
|
||||||
|
<div class="dns-record text-info">{{ 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>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="copyToClipboard('{{ item.recommended_spf }}')">
|
||||||
|
<i class="bi bi-clipboard me-1"></i>
|
||||||
|
Copy SPF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Key Information -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h6><i class="bi bi-info-circle me-2"></i>Key Information</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Selector:</strong><br>
|
||||||
|
<code>{{ item.dkim_key.selector }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Created:</strong><br>
|
||||||
|
{{ item.dkim_key.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Server IP:</strong><br>
|
||||||
|
<code>{{ item.public_ip }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Status:</strong><br>
|
||||||
|
{% if item.dkim_key.is_active %}
|
||||||
|
<span class="text-success">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-secondary">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if not dkim_data %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<i class="bi bi-shield-x text-muted" style="font-size: 4rem;"></i>
|
||||||
|
<h4 class="text-muted mt-3">No DKIM Keys Found</h4>
|
||||||
|
<p class="text-muted">Add domains first to automatically generate DKIM keys</p>
|
||||||
|
<a href="{{ url_for('email.add_domain') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add Domain
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DNS Check Results Modal -->
|
||||||
|
<div class="modal fade" id="dnsResultModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">DNS Check Results</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="dnsResults">
|
||||||
|
<!-- Results will be populated here -->
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
async function checkDomainDNS(domain, selector) {
|
||||||
|
const dkimStatus = document.getElementById(`dkim-status-${domain.replace('.', '-')}`);
|
||||||
|
const spfStatus = document.getElementById(`spf-status-${domain.replace('.', '-')}`);
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
dkimStatus.innerHTML = '<span class="status-indicator status-warning"></span><small class="text-muted">Checking...</small>';
|
||||||
|
spfStatus.innerHTML = '<span class="status-indicator status-warning"></span><small class="text-muted">Checking...</small>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check DKIM DNS
|
||||||
|
const dkimResponse = await fetch('{{ url_for("email.check_dkim_dns") }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: `domain=${encodeURIComponent(domain)}&selector=${encodeURIComponent(selector)}`
|
||||||
|
});
|
||||||
|
const dkimResult = await dkimResponse.json();
|
||||||
|
|
||||||
|
// Check SPF DNS
|
||||||
|
const spfResponse = await fetch('{{ url_for("email.check_spf_dns") }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: `domain=${encodeURIComponent(domain)}`
|
||||||
|
});
|
||||||
|
const spfResult = await spfResponse.json();
|
||||||
|
|
||||||
|
// Update DKIM status
|
||||||
|
if (dkimResult.success) {
|
||||||
|
dkimStatus.innerHTML = '<span class="status-indicator status-success"></span><small class="text-success">✓ Configured</small>';
|
||||||
|
} else {
|
||||||
|
dkimStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">✗ Not found</small>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update SPF status
|
||||||
|
if (spfResult.success) {
|
||||||
|
spfStatus.innerHTML = '<span class="status-indicator status-success"></span><small class="text-success">✓ Found</small>';
|
||||||
|
} else {
|
||||||
|
spfStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">✗ Not found</small>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show detailed results in modal
|
||||||
|
showDNSResults(domain, dkimResult, spfResult);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DNS check error:', error);
|
||||||
|
dkimStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">Error</small>';
|
||||||
|
spfStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">Error</small>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDNSResults(domain, dkimResult, spfResult) {
|
||||||
|
const resultsHtml = `
|
||||||
|
<h6>DNS Check Results for ${domain}</h6>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6 class="text-primary">DKIM Record</h6>
|
||||||
|
<div class="alert ${dkimResult.success ? 'alert-success' : 'alert-danger'}">
|
||||||
|
<strong>Status:</strong> ${dkimResult.success ? 'Found' : 'Not Found'}<br>
|
||||||
|
<strong>Message:</strong> ${dkimResult.message}
|
||||||
|
${dkimResult.records ? `<br><strong>Records:</strong> ${dkimResult.records.join(', ')}` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6 class="text-primary">SPF Record</h6>
|
||||||
|
<div class="alert ${spfResult.success ? 'alert-success' : 'alert-danger'}">
|
||||||
|
<strong>Status:</strong> ${spfResult.success ? 'Found' : 'Not Found'}<br>
|
||||||
|
<strong>Message:</strong> ${spfResult.message}
|
||||||
|
${spfResult.spf_record ? `<br><strong>Current SPF:</strong> <code>${spfResult.spf_record}</code>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('dnsResults').innerHTML = resultsHtml;
|
||||||
|
new bootstrap.Modal(document.getElementById('dnsResultModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAllDNS() {
|
||||||
|
const domains = document.querySelectorAll('[id^="domain-"]');
|
||||||
|
for (const domainCard of domains) {
|
||||||
|
const domainId = domainCard.id.split('-')[1];
|
||||||
|
// Extract domain name and selector from the card
|
||||||
|
const domainName = domainCard.querySelector('h5').textContent.trim().split('\n')[0].trim();
|
||||||
|
const selectorElement = domainCard.querySelector('code');
|
||||||
|
if (selectorElement) {
|
||||||
|
const selector = selectorElement.textContent;
|
||||||
|
await checkDomainDNS(domainName, selector);
|
||||||
|
// Small delay between checks to avoid overwhelming the DNS server
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
// Show temporary success message
|
||||||
|
const button = event.target.closest('button');
|
||||||
|
const originalText = button.innerHTML;
|
||||||
|
button.innerHTML = '<i class="bi bi-check me-1"></i>Copied!';
|
||||||
|
button.classList.add('btn-success');
|
||||||
|
button.classList.remove('btn-outline-secondary');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = originalText;
|
||||||
|
button.classList.remove('btn-success');
|
||||||
|
button.classList.add('btn-outline-secondary');
|
||||||
|
}, 2000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
174
email_frontend/templates/domains.html
Normal file
174
email_frontend/templates/domains.html
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Domains - Email Server Management{% endblock %}
|
||||||
|
{% block page_title %}Domain Management{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2>
|
||||||
|
<i class="bi bi-globe me-2"></i>
|
||||||
|
Domains
|
||||||
|
</h2>
|
||||||
|
<a href="{{ url_for('email.add_domain') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add Domain
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-list-ul me-2"></i>
|
||||||
|
All Domains
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if domains %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Domain Name</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Users</th>
|
||||||
|
<th>DKIM</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for domain in domains %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold">{{ domain.domain_name }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if domain.is_active %}
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>
|
||||||
|
Inactive
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ domain.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info">
|
||||||
|
{{ domain.users|length if domain.users else 0 }} users
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% set has_dkim = domain.dkim_keys and domain.dkim_keys|selectattr('is_active')|list %}
|
||||||
|
{% if has_dkim %}
|
||||||
|
<span class="text-success">
|
||||||
|
<i class="bi bi-shield-check" title="DKIM Configured"></i>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-warning">
|
||||||
|
<i class="bi bi-shield-exclamation" title="No DKIM Key"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{{ url_for('email.dkim_list') }}#domain-{{ domain.id }}"
|
||||||
|
class="btn btn-outline-info"
|
||||||
|
title="Manage DKIM">
|
||||||
|
<i class="bi bi-key"></i>
|
||||||
|
</a>
|
||||||
|
{% if domain.is_active %}
|
||||||
|
<form method="post" action="{{ url_for('email.delete_domain', domain_id=domain.id) }}" class="d-inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
data-confirm="Are you sure you want to deactivate domain '{{ domain.domain_name }}'?"
|
||||||
|
title="Deactivate Domain">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-globe text-muted" style="font-size: 4rem;"></i>
|
||||||
|
<h4 class="text-muted mt-3">No domains configured</h4>
|
||||||
|
<p class="text-muted">Get started by adding your first domain</p>
|
||||||
|
<a href="{{ url_for('email.add_domain') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add Your First Domain
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if domains %}
|
||||||
|
<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>
|
||||||
|
Domain Information
|
||||||
|
</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 domains:</strong> {{ domains|selectattr('is_active')|list|length }}
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-shield-check text-warning me-2"></i>
|
||||||
|
<strong>DKIM configured:</strong> {{ domains|selectattr('dkim_keys')|list|length }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bi bi-people text-info me-2"></i>
|
||||||
|
<strong>Total users:</strong> {{ domains|sum(attribute='users')|length if domains[0].users is defined else 'N/A' }}
|
||||||
|
</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>
|
||||||
|
Quick Tips
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-arrow-right text-primary me-2"></i>
|
||||||
|
DKIM keys are automatically generated for new domains
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-arrow-right text-primary me-2"></i>
|
||||||
|
Configure DNS records after adding domains
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bi bi-arrow-right text-primary me-2"></i>
|
||||||
|
Add users or whitelist IPs for authentication
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
112
email_frontend/templates/email/add_domain.html
Normal file
112
email_frontend/templates/email/add_domain.html
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Add Domain - Email Server Management{% endblock %}
|
||||||
|
{% block page_title %}Add New Domain{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add New Domain
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="domain_name" class="form-label">
|
||||||
|
<i class="bi bi-globe me-1"></i>
|
||||||
|
Domain Name
|
||||||
|
</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="domain_name"
|
||||||
|
name="domain_name"
|
||||||
|
placeholder="example.com"
|
||||||
|
required
|
||||||
|
pattern="^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.?[a-zA-Z]{2,}$">
|
||||||
|
<div class="form-text">
|
||||||
|
Enter the domain name that will be used for sending emails (e.g., example.com)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
What happens next?
|
||||||
|
</h6>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Domain will be added to the system</li>
|
||||||
|
<li>DKIM key pair will be automatically generated</li>
|
||||||
|
<li>You'll need to configure DNS records</li>
|
||||||
|
<li>Add users or whitelist IPs for authentication</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ url_for('email.domains_list') }}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-arrow-left me-2"></i>
|
||||||
|
Back to Domains
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add Domain
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bi bi-question-circle me-2"></i>
|
||||||
|
Domain Requirements
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-success">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
|
Valid Examples
|
||||||
|
</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><code>example.com</code></li>
|
||||||
|
<li><code>mail.example.com</code></li>
|
||||||
|
<li><code>my-domain.org</code></li>
|
||||||
|
<li><code>company.co.uk</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-danger">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>
|
||||||
|
Invalid Examples
|
||||||
|
</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><code>http://example.com</code></li>
|
||||||
|
<li><code>example</code></li>
|
||||||
|
<li><code>.example.com</code></li>
|
||||||
|
<li><code>example..com</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
document.getElementById('domain_name').addEventListener('input', function(e) {
|
||||||
|
// Convert to lowercase and remove protocol if present
|
||||||
|
let value = e.target.value.toLowerCase();
|
||||||
|
value = value.replace(/^https?:\/\//, '');
|
||||||
|
value = value.replace(/\/$/, '');
|
||||||
|
e.target.value = value;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
228
email_frontend/templates/email/add_ip.html
Normal file
228
email_frontend/templates/email/add_ip.html
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Add IP Address - Email Server{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 mx-auto">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<i class="bi bi-shield-plus me-2"></i>
|
||||||
|
Add IP Address to Whitelist
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ip_address" class="form-label">IP Address</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control font-monospace"
|
||||||
|
id="ip_address"
|
||||||
|
name="ip_address"
|
||||||
|
required
|
||||||
|
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]?)$"
|
||||||
|
placeholder="192.168.1.100"
|
||||||
|
value="{{ request.args.get('ip', '') }}">
|
||||||
|
<div class="form-text">
|
||||||
|
IPv4 address that will be allowed to send emails without authentication
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="domain_id" class="form-label">Authorized Domain</label>
|
||||||
|
<select class="form-select" id="domain_id" name="domain_id" required>
|
||||||
|
<option value="">Select a domain...</option>
|
||||||
|
{% for domain in domains %}
|
||||||
|
<option value="{{ domain.id }}">{{ domain.domain_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
This IP will only be able to send emails for the selected domain
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
|
Security Note
|
||||||
|
</h6>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Only whitelist trusted IP addresses</li>
|
||||||
|
<li>This IP can send emails without username/password authentication</li>
|
||||||
|
<li>The IP is restricted to the selected domain only</li>
|
||||||
|
<li>Use static IP addresses for reliable access</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ url_for('email.ips_list') }}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-arrow-left me-2"></i>
|
||||||
|
Back to IP List
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="bi bi-shield-plus me-2"></i>
|
||||||
|
Add to Whitelist
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 %}
|
||||||
|
<script>
|
||||||
|
// Detect current IP address
|
||||||
|
async function detectCurrentIP() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.ipify.org?format=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-muted">Unable to detect</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useCurrentIP() {
|
||||||
|
const currentIPElement = document.getElementById('current-ip');
|
||||||
|
const ip = currentIPElement.textContent.trim();
|
||||||
|
|
||||||
|
if (ip && ip !== 'Detecting...' && ip !== 'Unable to detect') {
|
||||||
|
document.getElementById('ip_address').value = ip;
|
||||||
|
// Focus on domain selection
|
||||||
|
document.getElementById('domain_id').focus();
|
||||||
|
} else {
|
||||||
|
alert('Unable to detect current IP address');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP address validation
|
||||||
|
document.getElementById('ip_address').addEventListener('input', function(e) {
|
||||||
|
const ip = e.target.value;
|
||||||
|
const ipPattern = /^(?:(?: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]?)$/;
|
||||||
|
|
||||||
|
if (ip && !ipPattern.test(ip)) {
|
||||||
|
e.target.setCustomValidity('Please enter a valid IPv4 address');
|
||||||
|
} else {
|
||||||
|
e.target.setCustomValidity('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-detect IP on page load
|
||||||
|
detectCurrentIP();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
176
email_frontend/templates/email/add_user.html
Normal file
176
email_frontend/templates/email/add_user.html
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Add User - Email Server{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 mx-auto">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>
|
||||||
|
Add New User
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email Address</label>
|
||||||
|
<input type="email"
|
||||||
|
class="form-control"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
placeholder="user@example.com">
|
||||||
|
<div class="form-text">
|
||||||
|
The email address for authentication and sending
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
minlength="6">
|
||||||
|
<div class="form-text">
|
||||||
|
Minimum 6 characters
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="domain_id" class="form-label">Domain</label>
|
||||||
|
<select class="form-select" id="domain_id" name="domain_id" required>
|
||||||
|
<option value="">Select a domain...</option>
|
||||||
|
{% for domain in domains %}
|
||||||
|
<option value="{{ domain.id }}">{{ domain.domain_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
The domain this user belongs to
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="can_send_as_domain"
|
||||||
|
name="can_send_as_domain">
|
||||||
|
<label class="form-check-label" for="can_send_as_domain">
|
||||||
|
<strong>Domain Administrator</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.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
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>
|
||||||
|
</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
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>
|
||||||
|
Add User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 %}
|
||||||
|
<script>
|
||||||
|
// Auto-fill domain based on email input
|
||||||
|
document.getElementById('email').addEventListener('input', function(e) {
|
||||||
|
const email = e.target.value;
|
||||||
|
const atIndex = email.indexOf('@');
|
||||||
|
|
||||||
|
if (atIndex > -1) {
|
||||||
|
const domain = email.substring(atIndex + 1).toLowerCase();
|
||||||
|
const domainSelect = document.getElementById('domain_id');
|
||||||
|
|
||||||
|
// Try to find matching domain in select options
|
||||||
|
for (let option of domainSelect.options) {
|
||||||
|
if (option.text.toLowerCase() === domain) {
|
||||||
|
domainSelect.value = option.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show/hide domain admin explanation
|
||||||
|
document.getElementById('can_send_as_domain').addEventListener('change', function(e) {
|
||||||
|
const isChecked = e.target.checked;
|
||||||
|
const domainSelect = document.getElementById('domain_id');
|
||||||
|
const selectedDomain = domainSelect.options[domainSelect.selectedIndex]?.text || 'domain.com';
|
||||||
|
|
||||||
|
// Update help text dynamically
|
||||||
|
const helpText = e.target.closest('.form-check').querySelector('.form-text');
|
||||||
|
if (isChecked) {
|
||||||
|
helpText.innerHTML = `User can send as any address in ${selectedDomain} (e.g., noreply@${selectedDomain}, support@${selectedDomain})`;
|
||||||
|
} else {
|
||||||
|
helpText.innerHTML = 'User can only send as their own email address.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update help text when domain changes
|
||||||
|
document.getElementById('domain_id').addEventListener('change', function(e) {
|
||||||
|
const checkbox = document.getElementById('can_send_as_domain');
|
||||||
|
if (checkbox.checked) {
|
||||||
|
checkbox.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
285
email_frontend/templates/email/dashboard.html
Normal file
285
email_frontend/templates/email/dashboard.html
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard - Email Server Management{% endblock %}
|
||||||
|
{% block page_title %}Dashboard{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<!-- Statistics Cards -->
|
||||||
|
<div class="col-lg-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-primary">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h5 class="card-title text-primary mb-1">
|
||||||
|
<i class="bi bi-globe me-2"></i>
|
||||||
|
Domains
|
||||||
|
</h5>
|
||||||
|
<h3 class="mb-0">{{ domain_count }}</h3>
|
||||||
|
<small class="text-muted">Active domains</small>
|
||||||
|
</div>
|
||||||
|
<div class="fs-2 text-primary opacity-50">
|
||||||
|
<i class="bi bi-globe"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-success">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h5 class="card-title text-success mb-1">
|
||||||
|
<i class="bi bi-people me-2"></i>
|
||||||
|
Users
|
||||||
|
</h5>
|
||||||
|
<h3 class="mb-0">{{ user_count }}</h3>
|
||||||
|
<small class="text-muted">Authenticated users</small>
|
||||||
|
</div>
|
||||||
|
<div class="fs-2 text-success opacity-50">
|
||||||
|
<i class="bi bi-people"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-warning">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h5 class="card-title text-warning mb-1">
|
||||||
|
<i class="bi bi-shield-check me-2"></i>
|
||||||
|
DKIM Keys
|
||||||
|
</h5>
|
||||||
|
<h3 class="mb-0">{{ dkim_count }}</h3>
|
||||||
|
<small class="text-muted">Active DKIM keys</small>
|
||||||
|
</div>
|
||||||
|
<div class="fs-2 text-warning opacity-50">
|
||||||
|
<i class="bi bi-shield-check"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-info">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h5 class="card-title text-info mb-1">
|
||||||
|
<i class="bi bi-activity me-2"></i>
|
||||||
|
Status
|
||||||
|
</h5>
|
||||||
|
<h6 class="text-success mb-0">
|
||||||
|
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>
|
||||||
|
Online
|
||||||
|
</h6>
|
||||||
|
<small class="text-muted">Server running</small>
|
||||||
|
</div>
|
||||||
|
<div class="fs-2 text-info opacity-50">
|
||||||
|
<i class="bi bi-activity"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Recent Email Activity -->
|
||||||
|
<div class="col-lg-8 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-envelope me-2"></i>
|
||||||
|
Recent Email Activity
|
||||||
|
</h5>
|
||||||
|
<a href="{{ url_for('email.logs', type='emails') }}" class="btn btn-outline-light btn-sm">
|
||||||
|
View All
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if recent_emails %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>From</th>
|
||||||
|
<th>To</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>DKIM</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for email in recent_emails %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ email.created_at.strftime('%H:%M:%S') }}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-truncate d-inline-block" style="max-width: 150px;" title="{{ email.mail_from }}">
|
||||||
|
{{ email.mail_from }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-truncate d-inline-block" style="max-width: 150px;" title="{{ email.to_address }}">
|
||||||
|
{{ email.to_address }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if email.status == 'relayed' %}
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
|
Sent
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>
|
||||||
|
Failed
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if email.dkim_signed %}
|
||||||
|
<span class="text-success">
|
||||||
|
<i class="bi bi-shield-check" title="DKIM Signed"></i>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">
|
||||||
|
<i class="bi bi-shield-x" title="Not DKIM Signed"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-envelope text-muted fs-1"></i>
|
||||||
|
<p class="text-muted mt-2">No email activity yet</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Authentication Activity -->
|
||||||
|
<div class="col-lg-4 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-shield-lock me-2"></i>
|
||||||
|
Recent Auth Activity
|
||||||
|
</h5>
|
||||||
|
<a href="{{ url_for('email.logs', type='auth') }}" class="btn btn-outline-light btn-sm">
|
||||||
|
View All
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if recent_auths %}
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{% for auth in recent_auths %}
|
||||||
|
<div class="list-group-item list-group-item-dark d-flex justify-content-between align-items-start">
|
||||||
|
<div class="ms-2 me-auto">
|
||||||
|
<div class="fw-bold">
|
||||||
|
{% if auth.success %}
|
||||||
|
<i class="bi bi-check-circle text-success me-1"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-x-circle text-danger me-1"></i>
|
||||||
|
{% endif %}
|
||||||
|
{{ auth.auth_type|title }}
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ auth.identifier }}
|
||||||
|
</small>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ auth.timestamp.strftime('%H:%M:%S') }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ auth.ip_address }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-shield-lock text-muted fs-1"></i>
|
||||||
|
<p class="text-muted mt-2">No authentication activity yet</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-lightning me-2"></i>
|
||||||
|
Quick Actions
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{{ url_for('email.add_domain') }}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add Domain
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{{ url_for('email.add_user') }}" class="btn btn-outline-success">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>
|
||||||
|
Add User
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{{ url_for('email.add_ip') }}" class="btn btn-outline-warning">
|
||||||
|
<i class="bi bi-shield-plus me-2"></i>
|
||||||
|
Whitelist IP
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{{ url_for('email.settings') }}" class="btn btn-outline-info">
|
||||||
|
<i class="bi bi-gear me-2"></i>
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// Auto-refresh dashboard every 30 seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
location.reload();
|
||||||
|
}, 30000);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
318
email_frontend/templates/email/dkim.html
Normal file
318
email_frontend/templates/email/dkim.html
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}DKIM Keys - Email Server{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.dns-record {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background-color: var(--bs-gray-100);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.status-indicator {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.status-success { background-color: #28a745; }
|
||||||
|
.status-warning { background-color: #ffc107; }
|
||||||
|
.status-danger { background-color: #dc3545; }
|
||||||
|
</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-shield-check me-2"></i>
|
||||||
|
DKIM Key Management
|
||||||
|
</h2>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-outline-info" onclick="checkAllDNS()">
|
||||||
|
<i class="bi bi-arrow-clockwise me-2"></i>
|
||||||
|
Check All DNS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for item in dkim_data %}
|
||||||
|
<div class="card mb-4" id="domain-{{ item.domain.id }}">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-server me-2"></i>
|
||||||
|
{{ item.domain.domain_name }}
|
||||||
|
{% if item.dkim_key.is_active %}
|
||||||
|
<span class="badge bg-success ms-2">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary ms-2">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button class="btn btn-outline-primary" onclick="checkDomainDNS('{{ item.domain.domain_name }}', '{{ item.dkim_key.selector }}')">
|
||||||
|
<i class="bi bi-search me-1"></i>
|
||||||
|
Check DNS
|
||||||
|
</button>
|
||||||
|
<form method="post" action="{{ url_for('email.regenerate_dkim', domain_id=item.domain.id) }}" class="d-inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-outline-warning"
|
||||||
|
onclick="return confirm('Regenerate DKIM key for {{ item.domain.domain_name }}? This will require updating DNS records.')">
|
||||||
|
<i class="bi bi-arrow-clockwise me-1"></i>
|
||||||
|
Regenerate
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<!-- DKIM DNS Record -->
|
||||||
|
<div class="col-lg-6 mb-3">
|
||||||
|
<h6>
|
||||||
|
<i class="bi bi-key me-2"></i>
|
||||||
|
DKIM DNS Record
|
||||||
|
<span class="dns-status" id="dkim-status-{{ item.domain.id }}">
|
||||||
|
<span class="status-indicator status-warning"></span>
|
||||||
|
<small class="text-muted">Not checked</small>
|
||||||
|
</span>
|
||||||
|
</h6>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Name:</strong>
|
||||||
|
<div class="dns-record">{{ item.dns_record.name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Type:</strong> TXT
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Value:</strong>
|
||||||
|
<div class="dns-record">{{ item.dns_record.value }}</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="copyToClipboard('{{ item.dns_record.value }}')">
|
||||||
|
<i class="bi bi-clipboard me-1"></i>
|
||||||
|
Copy Value
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SPF DNS Record -->
|
||||||
|
<div class="col-lg-6 mb-3">
|
||||||
|
<h6>
|
||||||
|
<i class="bi bi-shield-lock me-2"></i>
|
||||||
|
SPF DNS Record
|
||||||
|
<span class="dns-status" id="spf-status-{{ item.domain.id }}">
|
||||||
|
<span class="status-indicator status-warning"></span>
|
||||||
|
<small class="text-muted">Not checked</small>
|
||||||
|
</span>
|
||||||
|
</h6>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Name:</strong>
|
||||||
|
<div class="dns-record">{{ item.domain.domain_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Type:</strong> TXT
|
||||||
|
</div>
|
||||||
|
{% if item.existing_spf %}
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Current SPF:</strong>
|
||||||
|
<div class="dns-record text-info">{{ 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>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="copyToClipboard('{{ item.recommended_spf }}')">
|
||||||
|
<i class="bi bi-clipboard me-1"></i>
|
||||||
|
Copy SPF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Key Information -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h6><i class="bi bi-info-circle me-2"></i>Key Information</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Selector:</strong><br>
|
||||||
|
<code>{{ item.dkim_key.selector }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Created:</strong><br>
|
||||||
|
{{ item.dkim_key.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Server IP:</strong><br>
|
||||||
|
<code>{{ item.public_ip }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Status:</strong><br>
|
||||||
|
{% if item.dkim_key.is_active %}
|
||||||
|
<span class="text-success">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-secondary">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if not dkim_data %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<i class="bi bi-shield-x text-muted" style="font-size: 4rem;"></i>
|
||||||
|
<h4 class="text-muted mt-3">No DKIM Keys Found</h4>
|
||||||
|
<p class="text-muted">Add domains first to automatically generate DKIM keys</p>
|
||||||
|
<a href="{{ url_for('email.add_domain') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add Domain
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DNS Check Results Modal -->
|
||||||
|
<div class="modal fade" id="dnsResultModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">DNS Check Results</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="dnsResults">
|
||||||
|
<!-- Results will be populated here -->
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
async function checkDomainDNS(domain, selector) {
|
||||||
|
const dkimStatus = document.getElementById(`dkim-status-${domain.replace('.', '-')}`);
|
||||||
|
const spfStatus = document.getElementById(`spf-status-${domain.replace('.', '-')}`);
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
dkimStatus.innerHTML = '<span class="status-indicator status-warning"></span><small class="text-muted">Checking...</small>';
|
||||||
|
spfStatus.innerHTML = '<span class="status-indicator status-warning"></span><small class="text-muted">Checking...</small>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check DKIM DNS
|
||||||
|
const dkimResponse = await fetch('{{ url_for("email.check_dkim_dns") }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: `domain=${encodeURIComponent(domain)}&selector=${encodeURIComponent(selector)}`
|
||||||
|
});
|
||||||
|
const dkimResult = await dkimResponse.json();
|
||||||
|
|
||||||
|
// Check SPF DNS
|
||||||
|
const spfResponse = await fetch('{{ url_for("email.check_spf_dns") }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: `domain=${encodeURIComponent(domain)}`
|
||||||
|
});
|
||||||
|
const spfResult = await spfResponse.json();
|
||||||
|
|
||||||
|
// Update DKIM status
|
||||||
|
if (dkimResult.success) {
|
||||||
|
dkimStatus.innerHTML = '<span class="status-indicator status-success"></span><small class="text-success">✓ Configured</small>';
|
||||||
|
} else {
|
||||||
|
dkimStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">✗ Not found</small>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update SPF status
|
||||||
|
if (spfResult.success) {
|
||||||
|
spfStatus.innerHTML = '<span class="status-indicator status-success"></span><small class="text-success">✓ Found</small>';
|
||||||
|
} else {
|
||||||
|
spfStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">✗ Not found</small>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show detailed results in modal
|
||||||
|
showDNSResults(domain, dkimResult, spfResult);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DNS check error:', error);
|
||||||
|
dkimStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">Error</small>';
|
||||||
|
spfStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">Error</small>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDNSResults(domain, dkimResult, spfResult) {
|
||||||
|
const resultsHtml = `
|
||||||
|
<h6>DNS Check Results for ${domain}</h6>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6 class="text-primary">DKIM Record</h6>
|
||||||
|
<div class="alert ${dkimResult.success ? 'alert-success' : 'alert-danger'}">
|
||||||
|
<strong>Status:</strong> ${dkimResult.success ? 'Found' : 'Not Found'}<br>
|
||||||
|
<strong>Message:</strong> ${dkimResult.message}
|
||||||
|
${dkimResult.records ? `<br><strong>Records:</strong> ${dkimResult.records.join(', ')}` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6 class="text-primary">SPF Record</h6>
|
||||||
|
<div class="alert ${spfResult.success ? 'alert-success' : 'alert-danger'}">
|
||||||
|
<strong>Status:</strong> ${spfResult.success ? 'Found' : 'Not Found'}<br>
|
||||||
|
<strong>Message:</strong> ${spfResult.message}
|
||||||
|
${spfResult.spf_record ? `<br><strong>Current SPF:</strong> <code>${spfResult.spf_record}</code>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('dnsResults').innerHTML = resultsHtml;
|
||||||
|
new bootstrap.Modal(document.getElementById('dnsResultModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAllDNS() {
|
||||||
|
const domains = document.querySelectorAll('[id^="domain-"]');
|
||||||
|
for (const domainCard of domains) {
|
||||||
|
const domainId = domainCard.id.split('-')[1];
|
||||||
|
// Extract domain name and selector from the card
|
||||||
|
const domainName = domainCard.querySelector('h5').textContent.trim().split('\n')[0].trim();
|
||||||
|
const selectorElement = domainCard.querySelector('code');
|
||||||
|
if (selectorElement) {
|
||||||
|
const selector = selectorElement.textContent;
|
||||||
|
await checkDomainDNS(domainName, selector);
|
||||||
|
// Small delay between checks to avoid overwhelming the DNS server
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
// Show temporary success message
|
||||||
|
const button = event.target.closest('button');
|
||||||
|
const originalText = button.innerHTML;
|
||||||
|
button.innerHTML = '<i class="bi bi-check me-1"></i>Copied!';
|
||||||
|
button.classList.add('btn-success');
|
||||||
|
button.classList.remove('btn-outline-secondary');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = originalText;
|
||||||
|
button.classList.remove('btn-success');
|
||||||
|
button.classList.add('btn-outline-secondary');
|
||||||
|
}, 2000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
174
email_frontend/templates/email/domains.html
Normal file
174
email_frontend/templates/email/domains.html
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Domains - Email Server Management{% endblock %}
|
||||||
|
{% block page_title %}Domain Management{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2>
|
||||||
|
<i class="bi bi-globe me-2"></i>
|
||||||
|
Domains
|
||||||
|
</h2>
|
||||||
|
<a href="{{ url_for('email.add_domain') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add Domain
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-list-ul me-2"></i>
|
||||||
|
All Domains
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if domains %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Domain Name</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Users</th>
|
||||||
|
<th>DKIM</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for domain in domains %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold">{{ domain.domain_name }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if domain.is_active %}
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>
|
||||||
|
Inactive
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ domain.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info">
|
||||||
|
{{ domain.users|length if domain.users else 0 }} users
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% set has_dkim = domain.dkim_keys and domain.dkim_keys|selectattr('is_active')|list %}
|
||||||
|
{% if has_dkim %}
|
||||||
|
<span class="text-success">
|
||||||
|
<i class="bi bi-shield-check" title="DKIM Configured"></i>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-warning">
|
||||||
|
<i class="bi bi-shield-exclamation" title="No DKIM Key"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{{ url_for('email.dkim_list') }}#domain-{{ domain.id }}"
|
||||||
|
class="btn btn-outline-info"
|
||||||
|
title="Manage DKIM">
|
||||||
|
<i class="bi bi-key"></i>
|
||||||
|
</a>
|
||||||
|
{% if domain.is_active %}
|
||||||
|
<form method="post" action="{{ url_for('email.delete_domain', domain_id=domain.id) }}" class="d-inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
data-confirm="Are you sure you want to deactivate domain '{{ domain.domain_name }}'?"
|
||||||
|
title="Deactivate Domain">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-globe text-muted" style="font-size: 4rem;"></i>
|
||||||
|
<h4 class="text-muted mt-3">No domains configured</h4>
|
||||||
|
<p class="text-muted">Get started by adding your first domain</p>
|
||||||
|
<a href="{{ url_for('email.add_domain') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add Your First Domain
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if domains %}
|
||||||
|
<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>
|
||||||
|
Domain Information
|
||||||
|
</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 domains:</strong> {{ domains|selectattr('is_active')|list|length }}
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-shield-check text-warning me-2"></i>
|
||||||
|
<strong>DKIM configured:</strong> {{ domains|selectattr('dkim_keys')|list|length }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bi bi-people text-info me-2"></i>
|
||||||
|
<strong>Total users:</strong> {{ domains|sum(attribute='users')|length if domains[0].users is defined else 'N/A' }}
|
||||||
|
</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>
|
||||||
|
Quick Tips
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-arrow-right text-primary me-2"></i>
|
||||||
|
DKIM keys are automatically generated for new domains
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-arrow-right text-primary me-2"></i>
|
||||||
|
Configure DNS records after adding domains
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bi bi-arrow-right text-primary me-2"></i>
|
||||||
|
Add users or whitelist IPs for authentication
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
173
email_frontend/templates/email/error.html
Normal file
173
email_frontend/templates/email/error.html
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Error - SMTP Management{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
<h5 class="mb-0">Error Occurred</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if error_code %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-3"><strong>Error Code:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<span class="badge bg-danger fs-6">{{ error_code }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if error_message %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-3"><strong>Message:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<div class="alert alert-danger mb-0">
|
||||||
|
{{ error_message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if error_details %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-3"><strong>Details:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<div class="bg-dark text-light p-3 rounded">
|
||||||
|
<pre class="mb-0"><code>{{ error_details }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-3"><strong>Timestamp:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<span class="text-muted">{{ moment().format('YYYY-MM-DD HH:mm:ss') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-3"><strong>Request URL:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<code>{{ request.url if request else 'Unknown' }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('email_management.dashboard') }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-home me-1"></i>
|
||||||
|
Return to Dashboard
|
||||||
|
</a>
|
||||||
|
<button onclick="history.back()" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>
|
||||||
|
Go Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-outline-light" onclick="copyErrorDetails()">
|
||||||
|
<i class="fas fa-copy me-1"></i>
|
||||||
|
Copy Error Details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Common Error Solutions -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-lightbulb me-2"></i>
|
||||||
|
Common Solutions
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Database Issues:</h6>
|
||||||
|
<ul class="text-muted small">
|
||||||
|
<li>Check database connection settings</li>
|
||||||
|
<li>Verify database tables exist</li>
|
||||||
|
<li>Check database permissions</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Configuration Issues:</h6>
|
||||||
|
<ul class="text-muted small">
|
||||||
|
<li>Verify settings.ini file exists</li>
|
||||||
|
<li>Check file permissions</li>
|
||||||
|
<li>Validate configuration values</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Network Issues:</h6>
|
||||||
|
<ul class="text-muted small">
|
||||||
|
<li>Check firewall settings</li>
|
||||||
|
<li>Verify DNS resolution</li>
|
||||||
|
<li>Test network connectivity</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Permission Issues:</h6>
|
||||||
|
<ul class="text-muted small">
|
||||||
|
<li>Check file system permissions</li>
|
||||||
|
<li>Verify user authentication</li>
|
||||||
|
<li>Review access controls</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyErrorDetails() {
|
||||||
|
const errorDetails = {
|
||||||
|
code: '{{ error_code or "Unknown" }}',
|
||||||
|
message: '{{ error_message or "No message" }}',
|
||||||
|
details: `{{ error_details or "No details" }}`,
|
||||||
|
timestamp: '{{ moment().format("YYYY-MM-DD HH:mm:ss") }}',
|
||||||
|
url: '{{ request.url if request else "Unknown" }}'
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorText = `Error Report:
|
||||||
|
Code: ${errorDetails.code}
|
||||||
|
Message: ${errorDetails.message}
|
||||||
|
Details: ${errorDetails.details}
|
||||||
|
Time: ${errorDetails.timestamp}
|
||||||
|
URL: ${errorDetails.url}`;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(errorText).then(() => {
|
||||||
|
// Show success message
|
||||||
|
const btn = event.target.closest('button');
|
||||||
|
const originalText = btn.innerHTML;
|
||||||
|
btn.innerHTML = '<i class="fas fa-check me-1"></i>Copied!';
|
||||||
|
btn.classList.remove('btn-outline-light');
|
||||||
|
btn.classList.add('btn-success');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
btn.classList.remove('btn-success');
|
||||||
|
btn.classList.add('btn-outline-light');
|
||||||
|
}, 2000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
alert('Failed to copy error details to clipboard');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
203
email_frontend/templates/email/ips.html
Normal file
203
email_frontend/templates/email/ips.html
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Whitelisted IPs - Email Server{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2>
|
||||||
|
<i class="bi bi-router me-2"></i>
|
||||||
|
Whitelisted IP Addresses
|
||||||
|
</h2>
|
||||||
|
<a href="{{ url_for('email.add_ip') }}" class="btn btn-success">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add IP Address
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-list me-2"></i>
|
||||||
|
Whitelisted IP Addresses
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if ips %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>IP Address</th>
|
||||||
|
<th>Domain</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Added</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for ip, domain in ips %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold font-monospace">{{ ip.ip_address }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary">{{ domain.domain_name }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if ip.is_active %}
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>
|
||||||
|
Inactive
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ ip.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if ip.is_active %}
|
||||||
|
<form method="post" action="{{ url_for('email.delete_ip', ip_id=ip.id) }}" class="d-inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-outline-danger btn-sm"
|
||||||
|
onclick="return confirm('Remove {{ ip.ip_address }} from whitelist?')">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">
|
||||||
|
<i class="bi bi-dash"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-router text-muted" style="font-size: 4rem;"></i>
|
||||||
|
<h4 class="text-muted mt-3">No IP Addresses Whitelisted</h4>
|
||||||
|
<p class="text-muted">Add IP addresses to allow authentication without username/password</p>
|
||||||
|
<a href="{{ url_for('email.add_ip') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add First IP Address
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<!-- Information Panel -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
IP Whitelist Information
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if ips %}
|
||||||
|
<ul class="list-unstyled mb-3">
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-check-circle text-success me-2"></i>
|
||||||
|
<strong>Active IPs:</strong> {{ ips|selectattr('0.is_active')|list|length }}
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-server text-info me-2"></i>
|
||||||
|
<strong>Domains covered:</strong> {{ ips|map(attribute='1.domain_name')|unique|list|length }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bi bi-calendar text-muted me-2"></i>
|
||||||
|
<strong>Latest addition:</strong>
|
||||||
|
{% set latest = ips|map(attribute='0')|max(attribute='created_at') %}
|
||||||
|
{{ latest.strftime('%Y-%m-%d') if latest else 'N/A' }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-shield-check me-2"></i>
|
||||||
|
How IP Whitelisting Works
|
||||||
|
</h6>
|
||||||
|
<ul class="mb-0 small">
|
||||||
|
<li>Whitelisted IPs can send emails without username/password authentication</li>
|
||||||
|
<li>Each IP is associated with a specific domain</li>
|
||||||
|
<li>IP can only send emails for its authorized domain</li>
|
||||||
|
<li>Useful for server-to-server email sending</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current IP Detection -->
|
||||||
|
<div class="card mt-3">
|
||||||
|
<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">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="fw-bold font-monospace fs-5" id="current-ip">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
Detecting...
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-primary btn-sm mt-2" onclick="addCurrentIP()">
|
||||||
|
<i class="bi bi-plus-circle me-1"></i>
|
||||||
|
Add This IP
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// Detect current IP address
|
||||||
|
async function detectCurrentIP() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.ipify.org?format=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-muted">Unable to detect</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCurrentIP() {
|
||||||
|
const currentIPElement = document.getElementById('current-ip');
|
||||||
|
const ip = currentIPElement.textContent.trim();
|
||||||
|
|
||||||
|
if (ip && ip !== 'Detecting...' && ip !== 'Unable to detect') {
|
||||||
|
const url = new URL('{{ url_for("email.add_ip") }}', window.location.origin);
|
||||||
|
url.searchParams.set('ip', ip);
|
||||||
|
window.location.href = url.toString();
|
||||||
|
} else {
|
||||||
|
alert('Unable to detect current IP address');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect IP on page load
|
||||||
|
detectCurrentIP();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
322
email_frontend/templates/email/logs.html
Normal file
322
email_frontend/templates/email/logs.html
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Logs - Email Server{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.log-entry {
|
||||||
|
border-left: 4px solid var(--bs-border-color);
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
.log-email { border-left-color: #0d6efd; }
|
||||||
|
.log-auth { border-left-color: #198754; }
|
||||||
|
.log-error { border-left-color: #dc3545; }
|
||||||
|
.log-success { border-left-color: #198754; }
|
||||||
|
.log-failed { border-left-color: #dc3545; }
|
||||||
|
|
||||||
|
.log-content {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
background-color: var(--bs-gray-100);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</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-journal-text me-2"></i>
|
||||||
|
Server Logs
|
||||||
|
</h2>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{ url_for('email.logs', type='all') }}"
|
||||||
|
class="btn {{ 'btn-primary' if filter_type == 'all' else 'btn-outline-primary' }}">
|
||||||
|
<i class="bi bi-list-ul me-1"></i>
|
||||||
|
All Logs
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('email.logs', type='emails') }}"
|
||||||
|
class="btn {{ 'btn-primary' if filter_type == 'emails' else 'btn-outline-primary' }}">
|
||||||
|
<i class="bi bi-envelope me-1"></i>
|
||||||
|
Email Logs
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('email.logs', type='auth') }}"
|
||||||
|
class="btn {{ 'btn-primary' if filter_type == 'auth' else 'btn-outline-primary' }}">
|
||||||
|
<i class="bi bi-shield-lock me-1"></i>
|
||||||
|
Auth Logs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
{% if filter_type == 'emails' %}
|
||||||
|
<i class="bi bi-envelope me-2"></i>
|
||||||
|
Email Activity
|
||||||
|
{% elif filter_type == 'auth' %}
|
||||||
|
<i class="bi bi-shield-lock me-2"></i>
|
||||||
|
Authentication Activity
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-list-ul me-2"></i>
|
||||||
|
Recent Activity
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="refreshLogs()">
|
||||||
|
<i class="bi bi-arrow-clockwise me-1"></i>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if logs %}
|
||||||
|
{% if filter_type == 'all' %}
|
||||||
|
<!-- Combined logs view -->
|
||||||
|
{% for log_entry in logs %}
|
||||||
|
{% if log_entry.type == 'email' %}
|
||||||
|
{% set log = log_entry.data %}
|
||||||
|
<div class="log-entry log-email log-{{ 'success' if log.status == 'relayed' else 'failed' }}">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-primary me-2">EMAIL</span>
|
||||||
|
<strong>{{ log.mail_from }}</strong> → {{ log.rcpt_tos }}
|
||||||
|
{% if log.dkim_signed %}
|
||||||
|
<span class="badge bg-success ms-2">
|
||||||
|
<i class="bi bi-shield-check me-1"></i>
|
||||||
|
DKIM
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Status:</strong>
|
||||||
|
{% if log.status == 'relayed' %}
|
||||||
|
<span class="text-success">Sent Successfully</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger">Failed</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Message ID:</strong> <code>{{ log.message_id }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if log.subject %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Subject:</strong> {{ log.subject }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% set log = log_entry.data %}
|
||||||
|
<div class="log-entry log-auth log-{{ 'success' if log.success else 'failed' }}">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-success me-2">AUTH</span>
|
||||||
|
<strong>{{ log.identifier }}</strong>
|
||||||
|
<span class="badge {{ 'bg-success' if log.success else 'bg-danger' }} ms-2">
|
||||||
|
{{ 'Success' if log.success else 'Failed' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Type:</strong> {{ log.auth_type.upper() }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>IP:</strong> <code>{{ log.ip_address or 'N/A' }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if log.message %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Message:</strong> {{ log.message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% elif filter_type == 'emails' %}
|
||||||
|
<!-- Email logs only -->
|
||||||
|
{% for log in logs %}
|
||||||
|
<div class="log-entry log-email log-{{ 'success' if log.status == 'relayed' else 'failed' }}">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<strong>{{ log.mail_from }}</strong> → {{ log.rcpt_tos }}
|
||||||
|
{% if log.dkim_signed %}
|
||||||
|
<span class="badge bg-success ms-2">
|
||||||
|
<i class="bi bi-shield-check me-1"></i>
|
||||||
|
DKIM
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Status:</strong>
|
||||||
|
{% if log.status == 'relayed' %}
|
||||||
|
<span class="text-success">Sent</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger">Failed</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Peer:</strong> <code>{{ log.peer }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Message ID:</strong> <code>{{ log.message_id }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if log.subject %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Subject:</strong> {{ log.subject }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if log.content and log.content|length > 50 %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#content-{{ log.id }}">
|
||||||
|
<i class="bi bi-eye me-1"></i>
|
||||||
|
View Content
|
||||||
|
</button>
|
||||||
|
<div class="collapse mt-2" id="content-{{ log.id }}">
|
||||||
|
<div class="log-content">{{ log.content }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<!-- Auth logs only -->
|
||||||
|
{% for log in logs %}
|
||||||
|
<div class="log-entry log-auth log-{{ 'success' if log.success else 'failed' }}">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<strong>{{ log.identifier }}</strong>
|
||||||
|
<span class="badge {{ 'bg-success' if log.success else 'bg-danger' }} ms-2">
|
||||||
|
{{ 'Success' if log.success else 'Failed' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<strong>Type:</strong> {{ log.auth_type.upper() }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<strong>IP:</strong> <code>{{ log.ip_address or 'N/A' }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<strong>Result:</strong>
|
||||||
|
{% if log.success %}
|
||||||
|
<span class="text-success">Authenticated</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger">Rejected</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if log.message %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Details:</strong> {{ log.message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
{% if has_prev or has_next %}
|
||||||
|
<nav aria-label="Log pagination" class="mt-4">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{% if has_prev %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{{ url_for('email.logs', type=filter_type, page=page-1) }}">
|
||||||
|
<i class="bi bi-chevron-left"></i>
|
||||||
|
Previous
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">Page {{ page }}</span>
|
||||||
|
</li>
|
||||||
|
{% if has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{{ url_for('email.logs', type=filter_type, page=page+1) }}">
|
||||||
|
Next
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-journal-text text-muted" style="font-size: 4rem;"></i>
|
||||||
|
<h4 class="text-muted mt-3">No Logs Found</h4>
|
||||||
|
<p class="text-muted">
|
||||||
|
{% if filter_type == 'emails' %}
|
||||||
|
No email activity has been logged yet.
|
||||||
|
{% elif filter_type == 'auth' %}
|
||||||
|
No authentication attempts have been logged yet.
|
||||||
|
{% else %}
|
||||||
|
No activity has been logged yet.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
function refreshLogs() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
setInterval(function() {
|
||||||
|
// Only auto-refresh if the user is viewing the page
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
const button = document.querySelector('[onclick="refreshLogs()"]');
|
||||||
|
if (button) {
|
||||||
|
// Add visual indicator that refresh is happening
|
||||||
|
const originalText = button.innerHTML;
|
||||||
|
button.innerHTML = '<i class="bi bi-arrow-clockwise me-1 spin"></i>Refreshing...';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
// Add CSS for spinning icon
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
355
email_frontend/templates/email/settings.html
Normal file
355
email_frontend/templates/email/settings.html
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
{% 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;
|
||||||
|
}
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('email.update_settings') }}">
|
||||||
|
<!-- Server Settings -->
|
||||||
|
<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 SMTP connections (standard: 25, 587)</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 SMTP over TLS connections (standard: 465)</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">Bind IP Address</label>
|
||||||
|
<div class="setting-description">IP address to bind the server 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Database Settings -->
|
||||||
|
<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="setting-description">SQLite database file path or connection string</div>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control font-monospace"
|
||||||
|
name="Database.DATABASE_URL"
|
||||||
|
value="{{ settings['Database']['DATABASE_URL'] }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logging Settings -->
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Relay Settings -->
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TLS Settings -->
|
||||||
|
<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="setting-description">Path to SSL certificate file (.crt or .pem)</div>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control font-monospace"
|
||||||
|
name="TLS.TLS_CERT_FILE"
|
||||||
|
value="{{ settings['TLS']['TLS_CERT_FILE'] }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control font-monospace"
|
||||||
|
name="TLS.TLS_KEY_FILE"
|
||||||
|
value="{{ settings['TLS']['TLS_KEY_FILE'] }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DKIM Settings -->
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</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
|
||||||
|
const ports = ['Server.SMTP_PORT', 'Server.SMTP_TLS_PORT'];
|
||||||
|
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
|
||||||
|
const smtpPort = document.querySelector('[name="Server.SMTP_PORT"]').value;
|
||||||
|
const tlsPort = document.querySelector('[name="Server.SMTP_TLS_PORT"]').value;
|
||||||
|
if (smtpPort === tlsPort) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('SMTP and TLS ports must be different.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
170
email_frontend/templates/email/users.html
Normal file
170
email_frontend/templates/email/users.html
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Users - Email Server Management{% endblock %}
|
||||||
|
{% block page_title %}User 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
|
||||||
|
</h2>
|
||||||
|
<a href="{{ url_for('email.add_user') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>
|
||||||
|
Add User
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-list-ul me-2"></i>
|
||||||
|
All Users
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if users %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Domain</th>
|
||||||
|
<th>Permissions</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user, domain in users %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold">{{ user.email }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary">{{ domain.domain_name }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if user.can_send_as_domain %}
|
||||||
|
<span class="badge bg-warning">
|
||||||
|
<i class="bi bi-star me-1"></i>
|
||||||
|
Domain Admin
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">Can send as *@{{ domain.domain_name }}</small>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-info">
|
||||||
|
<i class="bi bi-person me-1"></i>
|
||||||
|
User
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">Can only send as {{ user.email }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if user.is_active %}
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>
|
||||||
|
Inactive
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if user.is_active %}
|
||||||
|
<form method="post" action="{{ url_for('email.delete_user', user_id=user.id) }}" class="d-inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-outline-danger btn-sm"
|
||||||
|
data-confirm="Are you sure you want to deactivate user '{{ user.email }}'?"
|
||||||
|
title="Deactivate User">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">
|
||||||
|
<i class="bi bi-dash-circle"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% 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>
|
||||||
|
<a href="{{ url_for('email.add_user') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>
|
||||||
|
Add Your First User
|
||||||
|
</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 %}
|
||||||
173
email_frontend/templates/error.html
Normal file
173
email_frontend/templates/error.html
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Error - SMTP Management{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
<h5 class="mb-0">Error Occurred</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if error_code %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-3"><strong>Error Code:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<span class="badge bg-danger fs-6">{{ error_code }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if error_message %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-3"><strong>Message:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<div class="alert alert-danger mb-0">
|
||||||
|
{{ error_message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if error_details %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-3"><strong>Details:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<div class="bg-dark text-light p-3 rounded">
|
||||||
|
<pre class="mb-0"><code>{{ error_details }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-3"><strong>Timestamp:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<span class="text-muted">{{ current_time.strftime('%Y-%m-%d %H:%M:%S') if current_time else 'Unknown' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-3"><strong>Request URL:</strong></div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<code>{{ request.url if request else 'Unknown' }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('email_management.dashboard') }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-home me-1"></i>
|
||||||
|
Return to Dashboard
|
||||||
|
</a>
|
||||||
|
<button onclick="history.back()" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>
|
||||||
|
Go Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-outline-light" onclick="copyErrorDetails()">
|
||||||
|
<i class="fas fa-copy me-1"></i>
|
||||||
|
Copy Error Details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Common Error Solutions -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-lightbulb me-2"></i>
|
||||||
|
Common Solutions
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Database Issues:</h6>
|
||||||
|
<ul class="text-muted small">
|
||||||
|
<li>Check database connection settings</li>
|
||||||
|
<li>Verify database tables exist</li>
|
||||||
|
<li>Check database permissions</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Configuration Issues:</h6>
|
||||||
|
<ul class="text-muted small">
|
||||||
|
<li>Verify settings.ini file exists</li>
|
||||||
|
<li>Check file permissions</li>
|
||||||
|
<li>Validate configuration values</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Network Issues:</h6>
|
||||||
|
<ul class="text-muted small">
|
||||||
|
<li>Check firewall settings</li>
|
||||||
|
<li>Verify DNS resolution</li>
|
||||||
|
<li>Test network connectivity</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Permission Issues:</h6>
|
||||||
|
<ul class="text-muted small">
|
||||||
|
<li>Check file system permissions</li>
|
||||||
|
<li>Verify user authentication</li>
|
||||||
|
<li>Review access controls</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyErrorDetails() {
|
||||||
|
const errorDetails = {
|
||||||
|
code: '{{ error_code or "Unknown" }}',
|
||||||
|
message: '{{ error_message or "No message" }}',
|
||||||
|
details: `{{ error_details or "No details" }}`,
|
||||||
|
timestamp: '{{ current_time.strftime("%Y-%m-%d %H:%M:%S") if current_time else "Unknown" }}',
|
||||||
|
url: '{{ request.url if request else "Unknown" }}'
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorText = `Error Report:
|
||||||
|
Code: ${errorDetails.code}
|
||||||
|
Message: ${errorDetails.message}
|
||||||
|
Details: ${errorDetails.details}
|
||||||
|
Time: ${errorDetails.timestamp}
|
||||||
|
URL: ${errorDetails.url}`;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(errorText).then(() => {
|
||||||
|
// Show success message
|
||||||
|
const btn = event.target.closest('button');
|
||||||
|
const originalText = btn.innerHTML;
|
||||||
|
btn.innerHTML = '<i class="fas fa-check me-1"></i>Copied!';
|
||||||
|
btn.classList.remove('btn-outline-light');
|
||||||
|
btn.classList.add('btn-success');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
btn.classList.remove('btn-success');
|
||||||
|
btn.classList.add('btn-outline-light');
|
||||||
|
}, 2000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
alert('Failed to copy error details to clipboard');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
203
email_frontend/templates/ips.html
Normal file
203
email_frontend/templates/ips.html
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Whitelisted IPs - Email Server{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2>
|
||||||
|
<i class="bi bi-router me-2"></i>
|
||||||
|
Whitelisted IP Addresses
|
||||||
|
</h2>
|
||||||
|
<a href="{{ url_for('email.add_ip') }}" class="btn btn-success">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add IP Address
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-list me-2"></i>
|
||||||
|
Whitelisted IP Addresses
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if ips %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>IP Address</th>
|
||||||
|
<th>Domain</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Added</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for ip, domain in ips %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold font-monospace">{{ ip.ip_address }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary">{{ domain.domain_name }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if ip.is_active %}
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>
|
||||||
|
Inactive
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ ip.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if ip.is_active %}
|
||||||
|
<form method="post" action="{{ url_for('email.delete_ip', ip_id=ip.id) }}" class="d-inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-outline-danger btn-sm"
|
||||||
|
onclick="return confirm('Remove {{ ip.ip_address }} from whitelist?')">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">
|
||||||
|
<i class="bi bi-dash"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-router text-muted" style="font-size: 4rem;"></i>
|
||||||
|
<h4 class="text-muted mt-3">No IP Addresses Whitelisted</h4>
|
||||||
|
<p class="text-muted">Add IP addresses to allow authentication without username/password</p>
|
||||||
|
<a href="{{ url_for('email.add_ip') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
Add First IP Address
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<!-- Information Panel -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
IP Whitelist Information
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if ips %}
|
||||||
|
<ul class="list-unstyled mb-3">
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-check-circle text-success me-2"></i>
|
||||||
|
<strong>Active IPs:</strong> {{ ips|selectattr('0.is_active')|list|length }}
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-server text-info me-2"></i>
|
||||||
|
<strong>Domains covered:</strong> {{ ips|map(attribute='1.domain_name')|unique|list|length }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bi bi-calendar text-muted me-2"></i>
|
||||||
|
<strong>Latest addition:</strong>
|
||||||
|
{% set latest = ips|map(attribute='0')|max(attribute='created_at') %}
|
||||||
|
{{ latest.strftime('%Y-%m-%d') if latest else 'N/A' }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-shield-check me-2"></i>
|
||||||
|
How IP Whitelisting Works
|
||||||
|
</h6>
|
||||||
|
<ul class="mb-0 small">
|
||||||
|
<li>Whitelisted IPs can send emails without username/password authentication</li>
|
||||||
|
<li>Each IP is associated with a specific domain</li>
|
||||||
|
<li>IP can only send emails for its authorized domain</li>
|
||||||
|
<li>Useful for server-to-server email sending</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current IP Detection -->
|
||||||
|
<div class="card mt-3">
|
||||||
|
<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">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="fw-bold font-monospace fs-5" id="current-ip">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
Detecting...
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-primary btn-sm mt-2" onclick="addCurrentIP()">
|
||||||
|
<i class="bi bi-plus-circle me-1"></i>
|
||||||
|
Add This IP
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// Detect current IP address
|
||||||
|
async function detectCurrentIP() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.ipify.org?format=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-muted">Unable to detect</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCurrentIP() {
|
||||||
|
const currentIPElement = document.getElementById('current-ip');
|
||||||
|
const ip = currentIPElement.textContent.trim();
|
||||||
|
|
||||||
|
if (ip && ip !== 'Detecting...' && ip !== 'Unable to detect') {
|
||||||
|
const url = new URL('{{ url_for("email.add_ip") }}', window.location.origin);
|
||||||
|
url.searchParams.set('ip', ip);
|
||||||
|
window.location.href = url.toString();
|
||||||
|
} else {
|
||||||
|
alert('Unable to detect current IP address');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect IP on page load
|
||||||
|
detectCurrentIP();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
322
email_frontend/templates/logs.html
Normal file
322
email_frontend/templates/logs.html
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Logs - Email Server{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.log-entry {
|
||||||
|
border-left: 4px solid var(--bs-border-color);
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
.log-email { border-left-color: #0d6efd; }
|
||||||
|
.log-auth { border-left-color: #198754; }
|
||||||
|
.log-error { border-left-color: #dc3545; }
|
||||||
|
.log-success { border-left-color: #198754; }
|
||||||
|
.log-failed { border-left-color: #dc3545; }
|
||||||
|
|
||||||
|
.log-content {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
background-color: var(--bs-gray-100);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</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-journal-text me-2"></i>
|
||||||
|
Server Logs
|
||||||
|
</h2>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{ url_for('email.logs', type='all') }}"
|
||||||
|
class="btn {{ 'btn-primary' if filter_type == 'all' else 'btn-outline-primary' }}">
|
||||||
|
<i class="bi bi-list-ul me-1"></i>
|
||||||
|
All Logs
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('email.logs', type='emails') }}"
|
||||||
|
class="btn {{ 'btn-primary' if filter_type == 'emails' else 'btn-outline-primary' }}">
|
||||||
|
<i class="bi bi-envelope me-1"></i>
|
||||||
|
Email Logs
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('email.logs', type='auth') }}"
|
||||||
|
class="btn {{ 'btn-primary' if filter_type == 'auth' else 'btn-outline-primary' }}">
|
||||||
|
<i class="bi bi-shield-lock me-1"></i>
|
||||||
|
Auth Logs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
{% if filter_type == 'emails' %}
|
||||||
|
<i class="bi bi-envelope me-2"></i>
|
||||||
|
Email Activity
|
||||||
|
{% elif filter_type == 'auth' %}
|
||||||
|
<i class="bi bi-shield-lock me-2"></i>
|
||||||
|
Authentication Activity
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-list-ul me-2"></i>
|
||||||
|
Recent Activity
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="refreshLogs()">
|
||||||
|
<i class="bi bi-arrow-clockwise me-1"></i>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if logs %}
|
||||||
|
{% if filter_type == 'all' %}
|
||||||
|
<!-- Combined logs view -->
|
||||||
|
{% for log_entry in logs %}
|
||||||
|
{% if log_entry.type == 'email' %}
|
||||||
|
{% set log = log_entry.data %}
|
||||||
|
<div class="log-entry log-email log-{{ 'success' if log.status == 'relayed' else 'failed' }}">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-primary me-2">EMAIL</span>
|
||||||
|
<strong>{{ log.mail_from }}</strong> → {{ log.rcpt_tos }}
|
||||||
|
{% if log.dkim_signed %}
|
||||||
|
<span class="badge bg-success ms-2">
|
||||||
|
<i class="bi bi-shield-check me-1"></i>
|
||||||
|
DKIM
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Status:</strong>
|
||||||
|
{% if log.status == 'relayed' %}
|
||||||
|
<span class="text-success">Sent Successfully</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger">Failed</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Message ID:</strong> <code>{{ log.message_id }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if log.subject %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Subject:</strong> {{ log.subject }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% set log = log_entry.data %}
|
||||||
|
<div class="log-entry log-auth log-{{ 'success' if log.success else 'failed' }}">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-success me-2">AUTH</span>
|
||||||
|
<strong>{{ log.identifier }}</strong>
|
||||||
|
<span class="badge {{ 'bg-success' if log.success else 'bg-danger' }} ms-2">
|
||||||
|
{{ 'Success' if log.success else 'Failed' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Type:</strong> {{ log.auth_type.upper() }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>IP:</strong> <code>{{ log.ip_address or 'N/A' }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if log.message %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Message:</strong> {{ log.message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% elif filter_type == 'emails' %}
|
||||||
|
<!-- Email logs only -->
|
||||||
|
{% for log in logs %}
|
||||||
|
<div class="log-entry log-email log-{{ 'success' if log.status == 'relayed' else 'failed' }}">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<strong>{{ log.mail_from }}</strong> → {{ log.rcpt_tos }}
|
||||||
|
{% if log.dkim_signed %}
|
||||||
|
<span class="badge bg-success ms-2">
|
||||||
|
<i class="bi bi-shield-check me-1"></i>
|
||||||
|
DKIM
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Status:</strong>
|
||||||
|
{% if log.status == 'relayed' %}
|
||||||
|
<span class="text-success">Sent</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger">Failed</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<strong>Peer:</strong> <code>{{ log.peer }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Message ID:</strong> <code>{{ log.message_id }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if log.subject %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Subject:</strong> {{ log.subject }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if log.content and log.content|length > 50 %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#content-{{ log.id }}">
|
||||||
|
<i class="bi bi-eye me-1"></i>
|
||||||
|
View Content
|
||||||
|
</button>
|
||||||
|
<div class="collapse mt-2" id="content-{{ log.id }}">
|
||||||
|
<div class="log-content">{{ log.content }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<!-- Auth logs only -->
|
||||||
|
{% for log in logs %}
|
||||||
|
<div class="log-entry log-auth log-{{ 'success' if log.success else 'failed' }}">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<strong>{{ log.identifier }}</strong>
|
||||||
|
<span class="badge {{ 'bg-success' if log.success else 'bg-danger' }} ms-2">
|
||||||
|
{{ 'Success' if log.success else 'Failed' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<strong>Type:</strong> {{ log.auth_type.upper() }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<strong>IP:</strong> <code>{{ log.ip_address or 'N/A' }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<strong>Result:</strong>
|
||||||
|
{% if log.success %}
|
||||||
|
<span class="text-success">Authenticated</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger">Rejected</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if log.message %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Details:</strong> {{ log.message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
{% if has_prev or has_next %}
|
||||||
|
<nav aria-label="Log pagination" class="mt-4">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{% if has_prev %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{{ url_for('email.logs', type=filter_type, page=page-1) }}">
|
||||||
|
<i class="bi bi-chevron-left"></i>
|
||||||
|
Previous
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">Page {{ page }}</span>
|
||||||
|
</li>
|
||||||
|
{% if has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{{ url_for('email.logs', type=filter_type, page=page+1) }}">
|
||||||
|
Next
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-journal-text text-muted" style="font-size: 4rem;"></i>
|
||||||
|
<h4 class="text-muted mt-3">No Logs Found</h4>
|
||||||
|
<p class="text-muted">
|
||||||
|
{% if filter_type == 'emails' %}
|
||||||
|
No email activity has been logged yet.
|
||||||
|
{% elif filter_type == 'auth' %}
|
||||||
|
No authentication attempts have been logged yet.
|
||||||
|
{% else %}
|
||||||
|
No activity has been logged yet.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
function refreshLogs() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
setInterval(function() {
|
||||||
|
// Only auto-refresh if the user is viewing the page
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
const button = document.querySelector('[onclick="refreshLogs()"]');
|
||||||
|
if (button) {
|
||||||
|
// Add visual indicator that refresh is happening
|
||||||
|
const originalText = button.innerHTML;
|
||||||
|
button.innerHTML = '<i class="bi bi-arrow-clockwise me-1 spin"></i>Refreshing...';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
// Add CSS for spinning icon
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
355
email_frontend/templates/settings.html
Normal file
355
email_frontend/templates/settings.html
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
{% 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;
|
||||||
|
}
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('email.update_settings') }}">
|
||||||
|
<!-- Server Settings -->
|
||||||
|
<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 SMTP connections (standard: 25, 587)</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 SMTP over TLS connections (standard: 465)</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">Bind IP Address</label>
|
||||||
|
<div class="setting-description">IP address to bind the server 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Database Settings -->
|
||||||
|
<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="setting-description">SQLite database file path or connection string</div>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control font-monospace"
|
||||||
|
name="Database.DATABASE_URL"
|
||||||
|
value="{{ settings['Database']['DATABASE_URL'] }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logging Settings -->
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Relay Settings -->
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TLS Settings -->
|
||||||
|
<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="setting-description">Path to SSL certificate file (.crt or .pem)</div>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control font-monospace"
|
||||||
|
name="TLS.TLS_CERT_FILE"
|
||||||
|
value="{{ settings['TLS']['TLS_CERT_FILE'] }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control font-monospace"
|
||||||
|
name="TLS.TLS_KEY_FILE"
|
||||||
|
value="{{ settings['TLS']['TLS_KEY_FILE'] }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DKIM Settings -->
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</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
|
||||||
|
const ports = ['Server.SMTP_PORT', 'Server.SMTP_TLS_PORT'];
|
||||||
|
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
|
||||||
|
const smtpPort = document.querySelector('[name="Server.SMTP_PORT"]').value;
|
||||||
|
const tlsPort = document.querySelector('[name="Server.SMTP_TLS_PORT"]').value;
|
||||||
|
if (smtpPort === tlsPort) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('SMTP and TLS ports must be different.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
187
email_frontend/templates/sidebar_email.html
Normal file
187
email_frontend/templates/sidebar_email.html
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<!-- Sidebar Navigation -->
|
||||||
|
<nav class="sidebar bg-dark border-end border-secondary position-fixed h-100" style="width: var(--sidebar-width); z-index: 1000;">
|
||||||
|
<div class="d-flex flex-column h-100">
|
||||||
|
<!-- Sidebar header -->
|
||||||
|
<div class="p-3 border-bottom border-secondary">
|
||||||
|
<h5 class="text-white mb-0">
|
||||||
|
<i class="bi bi-server me-2"></i>
|
||||||
|
SMTP Server
|
||||||
|
</h5>
|
||||||
|
<small class="text-muted">Management Console</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation menu -->
|
||||||
|
<div class="flex-grow-1 overflow-auto">
|
||||||
|
<ul class="nav nav-pills flex-column p-3">
|
||||||
|
<!-- Dashboard -->
|
||||||
|
<li class="nav-item mb-2">
|
||||||
|
<a href="{{ url_for('email.dashboard') }}"
|
||||||
|
class="nav-link text-white {{ 'active' if request.endpoint == 'email.dashboard' else '' }}">
|
||||||
|
<i class="bi bi-speedometer2 me-2"></i>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Domains Section -->
|
||||||
|
<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
|
||||||
|
</h6>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item mb-1">
|
||||||
|
<a href="{{ url_for('email.domains_list') }}"
|
||||||
|
class="nav-link text-white {{ 'active' if request.endpoint in ['email.domains_list', 'email.add_domain'] else '' }}">
|
||||||
|
<i class="bi bi-list-ul me-2"></i>
|
||||||
|
Domains
|
||||||
|
<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
|
||||||
|
<span class="badge bg-secondary ms-auto">{{ user_count if user_count is defined else '' }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item mb-1">
|
||||||
|
<a href="{{ url_for('email.ips_list') }}"
|
||||||
|
class="nav-link text-white {{ 'active' if request.endpoint in ['email.ips_list', 'email.add_ip'] else '' }}">
|
||||||
|
<i class="bi bi-router me-2"></i>
|
||||||
|
Whitelisted IPs
|
||||||
|
<span class="badge bg-secondary ms-auto">{{ ip_count if ip_count is defined else '' }}</span>
|
||||||
|
</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 '' }}">
|
||||||
|
<i class="bi bi-shield-check me-2"></i>
|
||||||
|
DKIM Keys
|
||||||
|
<span class="badge bg-secondary ms-auto">{{ dkim_count if dkim_count is defined else '' }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Configuration Section -->
|
||||||
|
<li class="nav-item mb-2">
|
||||||
|
<h6 class="text-muted text-uppercase small mb-2 mt-3">
|
||||||
|
<i class="bi bi-gear me-1"></i>
|
||||||
|
Configuration
|
||||||
|
</h6>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item mb-1">
|
||||||
|
<a href="{{ url_for('email.settings') }}"
|
||||||
|
class="nav-link text-white {{ 'active' if request.endpoint == 'email.settings' else '' }}">
|
||||||
|
<i class="bi bi-sliders me-2"></i>
|
||||||
|
Server Settings
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Monitoring Section -->
|
||||||
|
<li class="nav-item mb-2">
|
||||||
|
<h6 class="text-muted text-uppercase small mb-2 mt-3">
|
||||||
|
<i class="bi bi-activity me-1"></i>
|
||||||
|
Monitoring
|
||||||
|
</h6>
|
||||||
|
</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>
|
||||||
|
Logs & Activity
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar footer -->
|
||||||
|
<div class="p-3 border-top border-secondary">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<small class="text-muted d-block">Server Status</small>
|
||||||
|
<small class="text-success">
|
||||||
|
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>
|
||||||
|
Online
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" title="Refresh Status">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.sidebar .nav-link {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link.active {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link.active:hover {
|
||||||
|
background-color: #0b5ed7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar h6 {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
margin-bottom: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.show {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
170
email_frontend/templates/users.html
Normal file
170
email_frontend/templates/users.html
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Users - Email Server Management{% endblock %}
|
||||||
|
{% block page_title %}User 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
|
||||||
|
</h2>
|
||||||
|
<a href="{{ url_for('email.add_user') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>
|
||||||
|
Add User
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-list-ul me-2"></i>
|
||||||
|
All Users
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if users %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Domain</th>
|
||||||
|
<th>Permissions</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user, domain in users %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold">{{ user.email }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary">{{ domain.domain_name }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if user.can_send_as_domain %}
|
||||||
|
<span class="badge bg-warning">
|
||||||
|
<i class="bi bi-star me-1"></i>
|
||||||
|
Domain Admin
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">Can send as *@{{ domain.domain_name }}</small>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-info">
|
||||||
|
<i class="bi bi-person me-1"></i>
|
||||||
|
User
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">Can only send as {{ user.email }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if user.is_active %}
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>
|
||||||
|
Inactive
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if user.is_active %}
|
||||||
|
<form method="post" action="{{ url_for('email.delete_user', user_id=user.id) }}" class="d-inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-outline-danger btn-sm"
|
||||||
|
data-confirm="Are you sure you want to deactivate user '{{ user.email }}'?"
|
||||||
|
title="Deactivate User">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">
|
||||||
|
<i class="bi bi-dash-circle"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% 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>
|
||||||
|
<a href="{{ url_for('email.add_user') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>
|
||||||
|
Add Your First User
|
||||||
|
</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 %}
|
||||||
@@ -1,12 +1,25 @@
|
|||||||
# SMTP MTA server
|
# SMTP Server with Web Management Frontend
|
||||||
# then you can allow run ports < 1024 with
|
# Unified requirements for both SMTP server and Flask web interface
|
||||||
# create env with `python -m venv .venv --copies` (This will copy the Python binary)
|
#
|
||||||
# for f in /opt/PyMTA-server/.venv/bin/python*; do sudo setcap 'cap_net_bind_service=+ep' "$f"; done
|
# For port binding < 1024, create env with `python -m venv .venv --copies` (This will copy the Python binary)
|
||||||
|
# Then run: for f in /path/to/.venv/bin/python*; do sudo setcap 'cap_net_bind_service=+ep' "$f"; done
|
||||||
|
|
||||||
|
# Core SMTP Server Dependencies
|
||||||
aiosmtpd
|
aiosmtpd
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
pyOpenSSL
|
pyOpenSSL
|
||||||
bcrypt
|
bcrypt
|
||||||
dnspython
|
dnspython
|
||||||
dkimpy
|
dkimpy
|
||||||
cryptography
|
cryptography
|
||||||
|
|
||||||
|
# Web Frontend Dependencies
|
||||||
|
Flask
|
||||||
|
Flask-SQLAlchemy
|
||||||
|
Jinja2
|
||||||
|
Werkzeug
|
||||||
|
requests
|
||||||
|
waitress
|
||||||
|
|
||||||
|
# Additional utilities
|
||||||
|
alembic
|
||||||
Reference in New Issue
Block a user