DKIM key management front end - ok

This commit is contained in:
nahakubuilde
2025-06-07 14:43:00 +01:00
parent ce0f7e0ac9
commit ed3d28d34e
17 changed files with 1030 additions and 949 deletions

View File

@@ -1,6 +1,9 @@
# PyMTA-server # PyMTA-server
Python Email server for sending emails directly to recipient ( no email Relay) Python Email server for sending emails directly to recipient ( no email Relay)
```bash
# Testing
.venv/bin/python app.py --web-only --debug
```
## Plan: ## Plan:
- make full python MTA server with front end to allow sending any email - make full python MTA server with front end to allow sending any email
- include DKIM - include DKIM

View File

@@ -0,0 +1,28 @@
"""add_replaced_at_to_dkim_keys
Revision ID: 7f200580bbd3
Revises: d02f993649e8
Create Date: 2025-06-07 12:48:07.930008
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '7f200580bbd3'
down_revision: Union[str, None] = 'd02f993649e8'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Add replaced_at field to DKIM keys table."""
op.add_column('esrv_dkim_keys', sa.Column('replaced_at', sa.DateTime(), nullable=True))
def downgrade() -> None:
"""Remove replaced_at field from DKIM keys table."""
op.drop_column('esrv_dkim_keys', 'replaced_at')

View File

@@ -1,257 +0,0 @@
"""
Command-line tools for managing the SMTP server.
"""
import argparse
from email_server.models import Session, Domain, User, WhitelistedIP, hash_password, create_tables, CustomHeader
from email_server.dkim_manager import DKIMManager
from email_server.tool_box import get_logger
logger = get_logger()
def add_domain(domain_name):
"""Add a new domain to the database."""
session = Session()
try:
existing = session.query(Domain).filter_by(domain_name=domain_name).first()
if existing:
print(f"Domain {domain_name} already exists")
return False
domain = Domain(domain_name=domain_name)
session.add(domain)
session.commit()
print(f"Added domain: {domain_name}")
return True
except Exception as e:
session.rollback()
print(f"Error adding domain: {e}")
return False
finally:
session.close()
def add_user(email, password, domain_name):
"""Add a new user to the database."""
session = Session()
try:
domain = session.query(Domain).filter_by(domain_name=domain_name).first()
if not domain:
print(f"Domain {domain_name} not found")
return False
existing = session.query(User).filter_by(email=email).first()
if existing:
print(f"User {email} already exists")
return False
user = User(
email=email,
password_hash=hash_password(password),
domain_id=domain.id
)
session.add(user)
session.commit()
print(f"Added user: {email}")
return True
except Exception as e:
session.rollback()
print(f"Error adding user: {e}")
return False
finally:
session.close()
def add_whitelisted_ip(ip_address, domain_name):
"""Add an IP to the whitelist for a domain."""
session = Session()
try:
domain = session.query(Domain).filter_by(domain_name=domain_name).first()
if not domain:
print(f"Domain {domain_name} not found")
return False
existing = session.query(WhitelistedIP).filter_by(ip_address=ip_address).first()
if existing:
print(f"IP {ip_address} already whitelisted")
return False
whitelist = WhitelistedIP(
ip_address=ip_address,
domain_id=domain.id
)
session.add(whitelist)
session.commit()
print(f"Added whitelisted IP: {ip_address} for domain {domain_name}")
return True
except Exception as e:
session.rollback()
print(f"Error adding whitelisted IP: {e}")
return False
finally:
session.close()
def generate_dkim_key(domain_name):
"""Generate DKIM key for a domain."""
dkim_manager = DKIMManager()
if (dkim_manager.generate_dkim_keypair(domain_name)):
print(f"Generated DKIM key for domain: {domain_name}")
# Show DNS record
dns_record = dkim_manager.get_dkim_public_key_record(domain_name)
if dns_record:
print("\nAdd this DNS TXT record:")
print(f"Name: {dns_record['name']}")
print(f"Value: {dns_record['value']}")
return True
else:
print(f"Failed to generate DKIM key for domain: {domain_name}")
return False
def list_dkim_keys():
"""List all DKIM keys."""
dkim_manager = DKIMManager()
keys = dkim_manager.list_dkim_keys()
if not keys:
print("No DKIM keys found")
return
print("DKIM Keys:")
print("-" * 60)
for key in keys:
status = "ACTIVE" if key['active'] else "INACTIVE"
print(f"Domain: {key['domain']}")
print(f"Selector: {key['selector']}")
print(f"Status: {status}")
print(f"Created: {key['created_at']}")
print("-" * 60)
def show_dns_records():
"""Show DNS records for all domains."""
dkim_manager = DKIMManager()
session = Session()
try:
domains = session.query(Domain).all()
if not domains:
print("No domains found")
return
print("DNS Records for DKIM:")
print("=" * 80)
for domain in domains:
dns_record = dkim_manager.get_dkim_public_key_record(domain.domain_name)
if dns_record:
print(f"\nDomain: {domain.domain_name}")
print(f"Record Name: {dns_record['name']}")
print(f"Record Type: {dns_record['type']}")
print(f"Record Value: {dns_record['value']}")
print("-" * 80)
finally:
session.close()
def add_custom_header(domain_name: str, header_name: str, header_value: str, is_active: bool = True) -> bool:
"""Add a custom header for a domain.
Args:
domain_name (str): The domain name.
header_name (str): The header name.
header_value (str): The header value.
is_active (bool): Whether the header is active.
Returns:
bool: True if added, False otherwise.
"""
session = Session()
try:
domain = session.query(Domain).filter_by(domain_name=domain_name).first()
if not domain:
print(f"Domain {domain_name} not found")
return False
custom_header = CustomHeader(
domain_id=domain.id,
header_name=header_name,
header_value=header_value,
is_active=is_active
)
session.add(custom_header)
session.commit()
print(f"Added custom header '{header_name}: {header_value}' for domain {domain_name}")
return True
except Exception as e:
session.rollback()
print(f"Error adding custom header: {e}")
return False
finally:
session.close()
def main():
"""Main CLI function."""
parser = argparse.ArgumentParser(description="SMTP Server Management Tool")
subparsers = parser.add_subparsers(dest='command', help='Available commands')
# Initialize command
init_parser = subparsers.add_parser('init', help='Initialize database')
# Domain commands
domain_parser = subparsers.add_parser('add-domain', help='Add a domain')
domain_parser.add_argument('domain', help='Domain name')
domain_parser.add_argument('--no-auth', action='store_true', help='Domain does not require authentication')
# User commands
user_parser = subparsers.add_parser('add-user', help='Add a user')
user_parser.add_argument('email', help='User email')
user_parser.add_argument('password', help='User password')
user_parser.add_argument('domain', help='Domain name')
# IP whitelist commands
ip_parser = subparsers.add_parser('add-ip', help='Add whitelisted IP')
ip_parser.add_argument('ip', help='IP address')
ip_parser.add_argument('domain', help='Domain name')
# DKIM commands
dkim_parser = subparsers.add_parser('generate-dkim', help='Generate DKIM key for domain')
dkim_parser.add_argument('domain', help='Domain name')
list_dkim_parser = subparsers.add_parser('list-dkim', help='List DKIM keys')
dns_parser = subparsers.add_parser('show-dns', help='Show DNS records for DKIM')
custom_header_parser = subparsers.add_parser('add-custom-header', help='Add a custom header for a domain')
custom_header_parser.add_argument('domain', help='Domain name')
custom_header_parser.add_argument('header_name', help='Header name')
custom_header_parser.add_argument('header_value', help='Header value')
custom_header_parser.add_argument('--inactive', action='store_true', help='Add as inactive (disabled)')
args = parser.parse_args()
if not args.command:
parser.print_help()
return
if args.command == 'init':
create_tables()
print("Database tables created successfully")
elif args.command == 'add-domain':
add_domain(args.domain)
elif args.command == 'add-user':
add_user(args.email, args.password, args.domain)
elif args.command == 'add-ip':
add_whitelisted_ip(args.ip, args.domain)
elif args.command == 'generate-dkim':
generate_dkim_key(args.domain)
elif args.command == 'list-dkim':
list_dkim_keys()
elif args.command == 'show-dns':
show_dns_records()
elif args.command == 'add-custom-header':
add_custom_header(args.domain, args.header_name, args.header_value, not args.inactive)
if __name__ == '__main__':
main()

View File

@@ -32,8 +32,14 @@ class DKIMManager:
"""Generate a random DKIM selector name (8-12 chars).""" """Generate a random DKIM selector name (8-12 chars)."""
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
def generate_dkim_keypair(self, domain_name, selector: str = None): def generate_dkim_keypair(self, domain_name, selector: str = None, force_new_key: bool = False):
"""Generate DKIM key pair for a domain, optionally with a custom selector.""" """Generate DKIM key pair for a domain, optionally with a custom selector.
Args:
domain_name: The domain to generate the key for
selector: Custom selector name. If None, generates random one
force_new_key: If True, always create a new key even if selector exists
"""
session = Session() session = Session()
try: try:
# Check if domain exists # Check if domain exists
@@ -45,11 +51,42 @@ class DKIMManager:
# Use provided selector or instance selector # Use provided selector or instance selector
use_selector = selector or self.selector use_selector = selector or self.selector
# Check if DKIM key with this selector already exists # Ensure only one active DKIM key per domain - mark existing keys as replaced
existing_key = session.query(DKIMKey).filter_by(domain_id=domain.id, selector=use_selector, is_active=True).first() existing_active_keys = session.query(DKIMKey).filter_by(domain_id=domain.id, is_active=True).all()
if existing_key: for existing_key in existing_active_keys:
logger.debug(f"DKIM key already exists for domain {domain_name} and selector {use_selector}") existing_key.is_active = False
return True existing_key.replaced_at = datetime.now()
logger.debug(f"Marked DKIM key as replaced for domain {domain_name} selector {existing_key.selector}")
# Check if we're reusing an existing selector - if so, reactivate instead of creating new
# Skip this check if force_new_key is True (for regeneration)
if not force_new_key:
existing_key_with_selector = session.query(DKIMKey).filter_by(
domain_id=domain.id,
selector=use_selector
).first()
if existing_key_with_selector and not existing_key_with_selector.is_active:
# Before re-activating, ensure no other DKIM is active for this domain
other_active_keys = session.query(DKIMKey).filter(
DKIMKey.domain_id == domain.id,
DKIMKey.is_active == True,
DKIMKey.id != existing_key_with_selector.id
).all()
for key in other_active_keys:
key.is_active = False
key.replaced_at = datetime.now()
logger.debug(f"Deactivated other active DKIM key for domain {domain_name} selector {key.selector}")
# Reactivate existing key with same selector, clear replaced_at timestamp
existing_key_with_selector.is_active = True
existing_key_with_selector.replaced_at = None
session.commit()
logger.debug(f"Reactivated existing DKIM key for domain {domain_name} selector {use_selector}")
return True
elif existing_key_with_selector and existing_key_with_selector.is_active:
# Key is already active (shouldn't happen due to deactivation above, but just in case)
logger.debug(f"DKIM key already active for domain {domain_name} and selector {use_selector}")
return True
# Generate RSA key pair # Generate RSA key pair
private_key = rsa.generate_private_key( private_key = rsa.generate_private_key(
@@ -118,7 +155,7 @@ class DKIMManager:
def get_dkim_public_key_record(self, domain_name): def get_dkim_public_key_record(self, domain_name):
"""Get DKIM public key DNS record for a domain (active key only).""" """Get DKIM public key DNS record for a domain (active key only)."""
dkim_key = self.get_active_dkim_key(domain_name) dkim_key = self.get_active_dkim_key(domain_name)
if dkim_key: if (dkim_key):
public_key_lines = dkim_key.public_key.strip().split('\n') public_key_lines = dkim_key.public_key.strip().split('\n')
public_key_data = ''.join(public_key_lines[1:-1]) # Remove header/footer public_key_data = ''.join(public_key_lines[1:-1]) # Remove header/footer
return { return {

View File

@@ -176,6 +176,7 @@ class DKIMKey(Base):
public_key = Column(Text, nullable=False) public_key = Column(Text, nullable=False)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime, default=func.now())
replaced_at = Column(DateTime, nullable=True) # When this key was replaced by a new one
def __repr__(self): def __repr__(self):
return f"<DKIMKey(id={self.id}, domain_id={self.domain_id}, selector='{self.selector}', active={self.is_active})>" return f"<DKIMKey(id={self.id}, domain_id={self.domain_id}, selector='{self.selector}', active={self.is_active})>"

View File

@@ -40,10 +40,10 @@ async def start_server():
logger.debug("Initializing database...") logger.debug("Initializing database...")
create_tables() create_tables()
# Initialize DKIM manager and generate keys for domains without them # Initialize DKIM manager (do not auto-generate keys for all domains)
logger.debug("Initializing DKIM manager...") logger.debug("Initializing DKIM manager...")
dkim_manager = DKIMManager() dkim_manager = DKIMManager()
dkim_manager.initialize_default_keys() # dkim_manager.initialize_default_keys() # Removed: do not auto-generate DKIM keys for all domains
# Add test data if needed # Add test data if needed
from .models import Session, Domain, User, WhitelistedIP, hash_password from .models import Session, Domain, User, WhitelistedIP, hash_password
@@ -118,8 +118,7 @@ async def start_server():
controller_tls.start() controller_tls.start()
logger.debug(f' - Plain SMTP (IP whitelist): {BIND_IP}:{SMTP_PORT}') logger.debug(f' - Plain SMTP (IP whitelist): {BIND_IP}:{SMTP_PORT}')
logger.debug(f' - STARTTLS SMTP (auth required): {BIND_IP}:{SMTP_TLS_PORT}') logger.debug(f' - STARTTLS SMTP (auth required): {BIND_IP}:{SMTP_TLS_PORT}')
logger.debug('Management commands:') logger.debug('Management available via web interface at: http://localhost:5000/email')
logger.debug(' python cli_tools.py --help')
try: try:
await asyncio.Event().wait() await asyncio.Event().wait()

View File

@@ -1,329 +0,0 @@
# 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.

View File

@@ -1,7 +0,0 @@
Flask
Flask-SQLAlchemy
Jinja2
Werkzeug
dnspython
cryptography
requests

View File

@@ -20,6 +20,7 @@ import requests
import dns.resolver import dns.resolver
import re import re
from datetime import datetime from datetime import datetime
from datetime import datetime
from typing import Optional, Dict, List, Tuple from typing import Optional, Dict, List, Tuple
# Import email server modules # Import email server modules
@@ -46,15 +47,32 @@ email_bp = Blueprint('email', __name__,
def get_public_ip() -> str: def get_public_ip() -> str:
"""Get the public IP address of the server.""" """Get the public IP address of the server."""
try: try:
response = requests.get('https://api.ipify.org', timeout=5) response1 = requests.get('https://ifconfig.me/ip', timeout=3, verify=False)
return response.text.strip()
ip = response1.text.strip()
if ip and ip != 'unknown':
return ip
except Exception: except Exception:
try: try:
# Fallback method # Fallback method
response = requests.get('https://httpbin.org/ip', timeout=5) response = requests.get('https://httpbin.org/ip', timeout=3, verify=False)
return response.json()['origin'].split(',')[0].strip() ip = response.json()['origin'].split(',')[0].strip()
except Exception: if ip and ip != 'unknown':
return 'unknown' return ip
except Exception as e:
pass
# Use fallback from settings.ini if available
try:
settings = load_settings()
fallback_ip = settings.get('DKIM', 'SPF_SERVER_IP', fallback=None)
if fallback_ip and fallback_ip.strip() and fallback_ip != '""':
# Check if it's a valid IPv4 address (basic check)
parts = fallback_ip.split('.')
if len(parts) == 4 and all(part.isdigit() and 0 <= int(part) <= 255 for part in parts):
return fallback_ip.strip()
except Exception as e:
return {'success': False, 'message': f'DNS lookup error, If it continues, consider setting up public IP in settings - SPF_SERVER_IP. Details: {str(e)}'}
def check_dns_record(domain: str, record_type: str, expected_value: str = None) -> Dict: def check_dns_record(domain: str, record_type: str, expected_value: str = None) -> Dict:
"""Check DNS record for a domain.""" """Check DNS record for a domain."""
@@ -94,10 +112,15 @@ def generate_spf_record(domain: str, public_ip: str, existing_spf: str = None) -
parts = spf_clean.split() 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'] 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 # Add our server IP if it's not unknown
our_ip = f"ip4:{public_ip}" if public_ip and public_ip != 'unknown':
if our_ip not in base_mechanisms: our_ip = f"ip4:{public_ip}"
base_mechanisms.append(our_ip) if our_ip not in base_mechanisms:
base_mechanisms.append(our_ip)
# If no IP available, just use existing mechanisms
if not base_mechanisms and public_ip == 'unknown':
return existing_spf or 'v=spf1 ~all'
# Construct SPF record # Construct SPF record
spf_parts = ['v=spf1'] + base_mechanisms + ['~all'] spf_parts = ['v=spf1'] + base_mechanisms + ['~all']
@@ -712,14 +735,26 @@ def dkim_list():
"""List all DKIM keys and DNS records.""" """List all DKIM keys and DNS records."""
session = Session() session = Session()
try: try:
dkim_keys = session.query(DKIMKey, Domain).join(Domain, DKIMKey.domain_id == Domain.id).order_by(Domain.domain_name).all() # Get active DKIM keys
active_dkim_keys = session.query(DKIMKey, Domain).join(
Domain, DKIMKey.domain_id == Domain.id
).filter(DKIMKey.is_active == True).order_by(Domain.domain_name).all()
# Get old/inactive DKIM keys (prioritize replaced keys over disabled ones)
old_dkim_keys = session.query(DKIMKey, Domain).join(
Domain, DKIMKey.domain_id == Domain.id
).filter(DKIMKey.is_active == False).order_by(
Domain.domain_name,
DKIMKey.replaced_at.desc().nullslast(), # Replaced keys first, then disabled ones
DKIMKey.created_at.desc()
).all()
# Get public IP for SPF records # Get public IP for SPF records
public_ip = get_public_ip() public_ip = get_public_ip()
# Prepare DKIM data with DNS information # Prepare active DKIM data with DNS information
dkim_data = [] active_dkim_data = []
for dkim_key, domain in dkim_keys: for dkim_key, domain in active_dkim_keys:
# Get DKIM DNS record # Get DKIM DNS record
dkim_manager = DKIMManager() dkim_manager = DKIMManager()
dns_record = dkim_manager.get_dkim_public_key_record(domain.domain_name) dns_record = dkim_manager.get_dkim_public_key_record(domain.domain_name)
@@ -736,7 +771,7 @@ def dkim_list():
# Generate recommended SPF # Generate recommended SPF
recommended_spf = generate_spf_record(domain.domain_name, public_ip, existing_spf) recommended_spf = generate_spf_record(domain.domain_name, public_ip, existing_spf)
dkim_data.append({ active_dkim_data.append({
'dkim_key': dkim_key, 'dkim_key': dkim_key,
'domain': domain, 'domain': domain,
'dns_record': dns_record, 'dns_record': dns_record,
@@ -745,7 +780,20 @@ def dkim_list():
'public_ip': public_ip 'public_ip': public_ip
}) })
return render_template('dkim.html', dkim_data=dkim_data) # Prepare old DKIM data with status information
old_dkim_data = []
for dkim_key, domain in old_dkim_keys:
old_dkim_data.append({
'dkim_key': dkim_key,
'domain': domain,
'public_ip': public_ip,
'is_replaced': dkim_key.replaced_at is not None,
'status_text': 'Replaced' if dkim_key.replaced_at else 'Disabled'
})
return render_template('dkim.html',
dkim_data=active_dkim_data,
old_dkim_data=old_dkim_data)
finally: finally:
session.close() session.close()
@@ -756,21 +804,116 @@ def regenerate_dkim(domain_id: int):
try: try:
domain = session.query(Domain).get(domain_id) domain = session.query(Domain).get(domain_id)
if not domain: if not domain:
if request.headers.get('Content-Type') == 'application/json':
return jsonify({'success': False, 'message': 'Domain not found'})
flash('Domain not found', 'error') flash('Domain not found', 'error')
return redirect(url_for('email.dkim_list')) return redirect(url_for('email.dkim_list'))
# Deactivate existing keys # Get the current active DKIM key's selector to preserve it
existing_keys = session.query(DKIMKey).filter_by(domain_id=domain_id, is_active=True).all() existing_keys = session.query(DKIMKey).filter_by(domain_id=domain_id, is_active=True).all()
current_selector = None
if existing_keys:
# Use the selector from the first active key (there should typically be only one)
current_selector = existing_keys[0].selector
# Mark existing keys as replaced
for key in existing_keys: for key in existing_keys:
key.is_active = False key.is_active = False
key.replaced_at = datetime.now() # Mark when this key was replaced
# Generate new DKIM key # Generate new DKIM key preserving the existing selector
dkim_manager = DKIMManager() dkim_manager = DKIMManager()
if dkim_manager.generate_dkim_keypair(domain.domain_name): if dkim_manager.generate_dkim_keypair(domain.domain_name, selector=current_selector, force_new_key=True):
session.commit() session.commit()
# Get the new key data for AJAX response
new_key = session.query(DKIMKey).filter_by(
domain_id=domain_id, is_active=True
).order_by(DKIMKey.created_at.desc()).first()
if not new_key:
session.rollback()
if request.headers.get('Content-Type') == 'application/json' or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': False, 'message': f'Failed to create new DKIM key for {domain.domain_name}'})
flash(f'Failed to create new DKIM key for {domain.domain_name}', 'error')
return redirect(url_for('email.dkim_list'))
if request.headers.get('Content-Type') == 'application/json' or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
# Get updated DNS record for the new key
dns_record = dkim_manager.get_dkim_public_key_record(domain.domain_name)
public_ip = get_public_ip()
# 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
recommended_spf = generate_spf_record(domain.domain_name, public_ip, existing_spf)
# Get replaced keys for the Old DKIM section update
old_keys = session.query(DKIMKey, Domain).join(
Domain, DKIMKey.domain_id == Domain.id
).filter(
DKIMKey.domain_id == domain_id,
DKIMKey.is_active == False
).order_by(DKIMKey.created_at.desc()).all()
old_dkim_data = []
for old_key, old_domain in old_keys:
status_text = "Replaced" if old_key.replaced_at else "Disabled"
old_dkim_data.append({
'dkim_key': {
'id': old_key.id,
'selector': old_key.selector,
'created_at': old_key.created_at.strftime('%Y-%m-%d %H:%M'),
'replaced_at': old_key.replaced_at.strftime('%Y-%m-%d %H:%M') if old_key.replaced_at else None,
'is_active': old_key.is_active
},
'domain': {
'id': old_domain.id,
'domain_name': old_domain.domain_name
},
'status_text': status_text,
'public_ip': public_ip
})
# Additional null check for new_key before accessing its attributes
if not new_key:
logger.error(f"new_key is None after generation for domain {domain.domain_name}")
return jsonify({'success': False, 'message': f'Failed to retrieve new DKIM key for {domain.domain_name}'})
return jsonify({
'success': True,
'message': f'DKIM key regenerated for {domain.domain_name}',
'new_key': {
'id': new_key.id,
'selector': new_key.selector,
'created_at': new_key.created_at.strftime('%Y-%m-%d %H:%M'),
'is_active': new_key.is_active
},
'dns_record': {
'name': dns_record['name'] if dns_record else '',
'value': dns_record['value'] if dns_record else ''
},
'existing_spf': existing_spf,
'recommended_spf': recommended_spf,
'public_ip': public_ip,
'domain': {
'id': domain.id,
'domain_name': domain.domain_name
},
'old_dkim_data': old_dkim_data
})
flash(f'DKIM key regenerated for {domain.domain_name}', 'success') flash(f'DKIM key regenerated for {domain.domain_name}', 'success')
else: else:
session.rollback() session.rollback()
if request.headers.get('Content-Type') == 'application/json' or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': False, 'message': f'Failed to regenerate DKIM key for {domain.domain_name}'})
flash(f'Failed to regenerate DKIM key for {domain.domain_name}', 'error') flash(f'Failed to regenerate DKIM key for {domain.domain_name}', 'error')
return redirect(url_for('email.dkim_list')) return redirect(url_for('email.dkim_list'))
@@ -778,6 +921,8 @@ def regenerate_dkim(domain_id: int):
except Exception as e: except Exception as e:
session.rollback() session.rollback()
logger.error(f"Error regenerating DKIM: {e}") logger.error(f"Error regenerating DKIM: {e}")
if request.headers.get('Content-Type') == 'application/json' or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': False, 'message': f'Error regenerating DKIM: {str(e)}'})
flash(f'Error regenerating DKIM: {str(e)}', 'error') flash(f'Error regenerating DKIM: {str(e)}', 'error')
return redirect(url_for('email.dkim_list')) return redirect(url_for('email.dkim_list'))
finally: finally:
@@ -844,16 +989,25 @@ def toggle_dkim(dkim_id: int):
if not dkim_key: if not dkim_key:
flash('DKIM key not found', 'error') flash('DKIM key not found', 'error')
return redirect(url_for('email.dkim_list')) return redirect(url_for('email.dkim_list'))
domain = session.query(Domain).get(dkim_key.domain_id) domain = session.query(Domain).get(dkim_key.domain_id)
old_status = dkim_key.is_active old_status = dkim_key.is_active
if not old_status:
# About to activate this key, so deactivate any other active DKIM for this domain
other_active_keys = session.query(DKIMKey).filter(
DKIMKey.domain_id == dkim_key.domain_id,
DKIMKey.is_active == True,
DKIMKey.id != dkim_id
).all()
for key in other_active_keys:
key.is_active = False
key.replaced_at = datetime.now()
dkim_key.is_active = not old_status dkim_key.is_active = not old_status
if dkim_key.is_active:
dkim_key.replaced_at = None
session.commit() session.commit()
status_text = "enabled" if dkim_key.is_active else "disabled" status_text = "enabled" if dkim_key.is_active else "disabled"
flash(f'DKIM key for {domain.domain_name} (selector: {dkim_key.selector}) has been {status_text}', 'success') flash(f'DKIM key for {domain.domain_name} (selector: {dkim_key.selector}) has been {status_text}', 'success')
return redirect(url_for('email.dkim_list')) return redirect(url_for('email.dkim_list'))
except Exception as e: except Exception as e:
session.rollback() session.rollback()
logger.error(f"Error toggling DKIM status: {e}") logger.error(f"Error toggling DKIM status: {e}")
@@ -895,15 +1049,27 @@ def check_dkim_dns():
"""Check DKIM DNS record via AJAX.""" """Check DKIM DNS record via AJAX."""
domain = request.form.get('domain') domain = request.form.get('domain')
selector = request.form.get('selector') selector = request.form.get('selector')
expected_value = request.form.get('expected_value')
if not all([domain, selector, expected_value]): if not all([domain, selector]):
return jsonify({'success': False, 'message': 'Missing parameters'}) return jsonify({'success': False, 'message': 'Missing domain or selector parameters'})
dns_name = f"{selector}._domainkey.{domain}" # Get the expected DKIM value from the DKIM manager
result = check_dns_record(dns_name, 'TXT', expected_value) try:
dkim_manager = DKIMManager()
return jsonify(result) dns_record = dkim_manager.get_dkim_public_key_record(domain)
if not dns_record or not dns_record.get('value'):
return jsonify({'success': False, 'message': 'No DKIM key found for domain'})
expected_value = dns_record['value']
dns_name = f"{selector}._domainkey.{domain}"
result = check_dns_record(dns_name, 'TXT', expected_value)
return jsonify(result)
except Exception as e:
logger.error(f"Error checking DKIM DNS: {e}")
return jsonify({'success': False, 'message': f'Error checking DKIM DNS: {str(e)}'})
@email_bp.route('/dkim/check_spf', methods=['POST']) @email_bp.route('/dkim/check_spf', methods=['POST'])
def check_spf_dns(): def check_spf_dns():
@@ -1049,3 +1215,38 @@ def internal_error(error):
error_code=500, error_code=500,
error_message='Internal server error', error_message='Internal server error',
current_time=datetime.now()), 500 current_time=datetime.now()), 500
@email_bp.route('/dkim/create', methods=['POST'], endpoint='create_dkim')
def create_dkim():
"""Create a new DKIM key for a domain, optionally with a custom selector."""
from flask import request, jsonify
data = request.get_json() if request.is_json else request.form
domain_name = data.get('domain')
selector = data.get('selector', None)
session = Session()
try:
if not domain_name:
return jsonify({'success': False, 'message': 'Domain is required.'}), 400
domain = session.query(Domain).filter_by(domain_name=domain_name).first()
if not domain:
return jsonify({'success': False, 'message': 'Domain not found.'}), 404
# Deactivate any existing active DKIM key for this domain
active_keys = session.query(DKIMKey).filter_by(domain_id=domain.id, is_active=True).all()
for key in active_keys:
key.is_active = False
key.replaced_at = datetime.now()
# Create new DKIM key
dkim_manager = DKIMManager()
created = dkim_manager.generate_dkim_keypair(domain_name, selector=selector, force_new_key=True)
if created:
session.commit()
return jsonify({'success': True, 'message': f'DKIM key created for {domain_name}.'})
else:
session.rollback()
return jsonify({'success': False, 'message': f'Failed to create DKIM key for {domain_name}.'}), 500
except Exception as e:
session.rollback()
logger.error(f"Error creating DKIM: {e}")
return jsonify({'success': False, 'message': f'Error creating DKIM: {str(e)}'}), 500
finally:
session.close()

View File

@@ -156,22 +156,24 @@
</div> </div>
</nav> </nav>
<!-- Flash messages --> <!-- Toast container for notifications -->
{% with messages = get_flashed_messages(with_categories=true) %} <div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1090;">
{% if messages %} {% with messages = get_flashed_messages(with_categories=true) %}
<div class="row"> {% if messages %}
<div class="col-12"> {% for category, message in messages %}
{% for category, message in messages %} <div class="toast align-items-center text-bg-{{ 'danger' if category == 'error' else category }} border-0" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert"> <div class="d-flex">
<i class="bi bi-{{ 'exclamation-triangle' if category == 'error' else 'check-circle' if category == 'success' else 'info-circle' }} me-2"></i> <div class="toast-body">
{{ message }} <i class="bi bi-{{ 'exclamation-triangle' if category == 'error' else 'check-circle' if category == 'success' else 'info-circle' }} me-2"></i>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button> {{ message }}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div> </div>
{% endfor %} </div>
</div> {% endfor %}
</div> {% endif %}
{% endif %} {% endwith %}
{% endwith %} </div>
<!-- Page content --> <!-- Page content -->
<main> <main>
@@ -180,6 +182,28 @@
</div> </div>
</div> </div>
<!-- Custom Confirmation Modal -->
<div class="modal fade" id="confirmationModal" tabindex="-1" aria-labelledby="confirmationModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="confirmationModalLabel">
<i class="bi bi-question-circle me-2"></i>
Confirm Action
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="confirmationModalBody">
Are you sure you want to proceed?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirmationModalConfirm">Confirm</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS --> <!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
@@ -197,22 +221,113 @@
setInterval(updateTime, 1000); setInterval(updateTime, 1000);
updateTime(); // Initial call updateTime(); // Initial call
// Auto-dismiss alerts after 5 seconds // Initialize toasts
setTimeout(function() { document.addEventListener('DOMContentLoaded', function() {
const alerts = document.querySelectorAll('.alert:not(.alert-permanent)'); const toastElements = document.querySelectorAll('.toast');
alerts.forEach(function(alert) { toastElements.forEach(function(toastElement) {
const bootstrapAlert = new bootstrap.Alert(alert); const toast = new bootstrap.Toast(toastElement);
bootstrapAlert.close(); toast.show();
}); });
}, 5000); });
// Confirmation dialogs for delete actions // Function to show dynamic toasts
function showToast(message, type = 'info') {
const toastContainer = document.querySelector('.toast-container');
const toastId = 'toast-' + Date.now();
const iconMap = {
'danger': 'exclamation-triangle',
'success': 'check-circle',
'warning': 'exclamation-triangle',
'info': 'info-circle'
};
const toastHtml = `
<div id="${toastId}" class="toast align-items-center text-bg-${type} border-0" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="d-flex">
<div class="toast-body">
<i class="bi bi-${iconMap[type] || 'info-circle'} me-2"></i>
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const newToast = new bootstrap.Toast(document.getElementById(toastId));
newToast.show();
// Remove toast element after it's hidden
document.getElementById(toastId).addEventListener('hidden.bs.toast', function() {
this.remove();
});
}
// Custom confirmation dialog to replace browser alerts
function showConfirmation(message, title = 'Confirm Action', confirmButtonText = 'Confirm', confirmButtonClass = 'btn-primary') {
return new Promise((resolve) => {
const modal = document.getElementById('confirmationModal');
const modalTitle = document.getElementById('confirmationModalLabel');
const modalBody = document.getElementById('confirmationModalBody');
const confirmButton = document.getElementById('confirmationModalConfirm');
// Set content
modalTitle.innerHTML = `<i class="bi bi-question-circle me-2"></i>${title}`;
modalBody.textContent = message;
confirmButton.textContent = confirmButtonText;
// Reset button classes and add new one
confirmButton.className = `btn ${confirmButtonClass}`;
// Set up event handlers
const handleConfirm = () => {
resolve(true);
bootstrap.Modal.getInstance(modal).hide();
cleanup();
};
const handleCancel = () => {
resolve(false);
cleanup();
};
const cleanup = () => {
confirmButton.removeEventListener('click', handleConfirm);
modal.removeEventListener('hidden.bs.modal', handleCancel);
};
confirmButton.addEventListener('click', handleConfirm);
modal.addEventListener('hidden.bs.modal', handleCancel, { once: true });
// Show modal
new bootstrap.Modal(modal).show();
});
}
// Confirmation dialogs for delete actions with data-confirm attribute
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const deleteButtons = document.querySelectorAll('[data-confirm]'); const deleteButtons = document.querySelectorAll('[data-confirm]');
deleteButtons.forEach(function(button) { deleteButtons.forEach(function(button) {
button.addEventListener('click', function(e) { button.addEventListener('click', async function(e) {
if (!confirm(this.getAttribute('data-confirm'))) { e.preventDefault();
e.preventDefault();
const confirmMessage = this.getAttribute('data-confirm');
const confirmed = await showConfirmation(
confirmMessage,
'Confirm Action',
'Confirm',
'btn-danger'
);
if (confirmed) {
// If it's a form button, submit the form
const form = this.closest('form');
if (form) {
form.submit();
} else if (this.href) {
// If it's a link, navigate to the URL
window.location.href = this.href;
}
} }
}); });
}); });

View File

@@ -34,6 +34,10 @@
DKIM Key Management DKIM Key Management
</h2> </h2>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-outline-primary me-2" data-bs-toggle="modal" data-bs-target="#createDKIMModal">
<i class="bi bi-plus-circle me-2"></i>
Create DKIM
</button>
<button class="btn btn-outline-info" onclick="checkAllDNS()"> <button class="btn btn-outline-info" onclick="checkAllDNS()">
<i class="bi bi-arrow-clockwise me-2"></i> <i class="bi bi-arrow-clockwise me-2"></i>
Check All DNS Check All DNS
@@ -41,62 +45,97 @@
</div> </div>
</div> </div>
<!-- Create DKIM Modal -->
<div class="modal fade" id="createDKIMModal" tabindex="-1" aria-labelledby="createDKIMModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form id="createDKIMForm">
<div class="modal-header">
<h5 class="modal-title" id="createDKIMModalLabel">Create New DKIM Key</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="dkimDomain" class="form-label">Domain</label>
<select class="form-select" id="dkimDomain" name="domain" required>
<option value="" disabled selected>Select domain</option>
{% for item in dkim_data %}
<option value="{{ item.domain.domain_name }}">{{ item.domain.domain_name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="dkimSelector" class="form-label">Selector (optional)</label>
<input type="text" class="form-control" id="dkimSelector" name="selector" maxlength="32" placeholder="Leave blank for random selector">
</div>
<div id="createDKIMError" class="alert alert-danger d-none"></div>
<div id="createDKIMSuccess" class="alert alert-success d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</form>
</div>
</div>
</div>
{% for item in dkim_data %} {% for item in dkim_data %}
<div class="card mb-4" id="domain-{{ item.domain.id }}"> <div class="card mb-4" id="domain-{{ item.domain.id }}">
<div class="card-header" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapse-{{ item.domain.id }}" aria-expanded="false" aria-controls="collapse-{{ item.domain.id }}"> <div class="card-header">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0"> <div class="flex-grow-1 card-header-clickable" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapse-{{ item.domain.id }}" aria-expanded="false" aria-controls="collapse-{{ item.domain.id }}">
<i class="bi bi-server me-2"></i> <h5 class="mb-0">
{{ item.domain.domain_name }} <i class="bi bi-server me-2"></i>
{% if item.dkim_key.is_active %} {{ item.domain.domain_name }}
<span class="badge bg-success ms-2">Active</span> {% if item.dkim_key.is_active %}
{% else %} <span class="badge bg-success ms-2">Active</span>
<span class="badge bg-secondary ms-2">Inactive</span> {% else %}
{% endif %} <span class="badge bg-secondary ms-2">Inactive</span>
</h5> {% endif %}
<div class="btn-group btn-group-sm"> </h5>
<button class="btn btn-outline-primary" onclick="checkDomainDNS('{{ item.domain.domain_name }}', '{{ item.dkim_key.selector }}')"> </div>
<div class="btn-group btn-group-sm me-2">
<button class="btn btn-outline-primary" onclick="event.stopPropagation(); checkDomainDNS('{{ item.domain.domain_name }}', '{{ item.dkim_key.selector }}')">
<i class="bi bi-search me-1"></i> <i class="bi bi-search me-1"></i>
Check DNS Check DNS
</button> </button>
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
<a href="{{ url_for('email.edit_dkim', dkim_id=item.dkim_key.id) }}" <a href="{{ url_for('email.edit_dkim', dkim_id=item.dkim_key.id) }}"
class="btn btn-outline-info"> class="btn btn-outline-info"
onclick="event.stopPropagation();">
<i class="bi bi-pencil me-1"></i> <i class="bi bi-pencil me-1"></i>
Edit Edit
</a> </a>
<form method="post" action="{{ url_for('email.toggle_dkim', dkim_id=item.dkim_key.id) }}" class="d-inline"> <form method="post" action="{{ url_for('email.toggle_dkim', dkim_id=item.dkim_key.id) }}" class="d-inline">
{% if item.dkim_key.is_active %} {% if item.dkim_key.is_active %}
<button type="submit" class="btn btn-outline-warning"> <button type="submit" class="btn btn-outline-warning" onclick="event.stopPropagation();">
<i class="bi bi-pause-circle me-1"></i> <i class="bi bi-pause-circle me-1"></i>
Disable Disable
</button> </button>
{% else %} {% else %}
<button type="submit" class="btn btn-outline-success"> <button type="submit" class="btn btn-outline-success" onclick="event.stopPropagation();">
<i class="bi bi-play-circle me-1"></i> <i class="bi bi-play-circle me-1"></i>
Enable Enable
</button> </button>
{% endif %} {% endif %}
</form> </form>
<form method="post" action="{{ url_for('email.remove_dkim', dkim_id=item.dkim_key.id) }}" class="d-inline"> <form method="post" action="{{ url_for('email.remove_dkim', dkim_id=item.dkim_key.id) }}" class="d-inline" onsubmit="return handleFormSubmit(event, 'Are you sure you want to permanently remove the DKIM key for {{ item.domain.domain_name }}? This action cannot be undone and you will lose the ability to sign emails until you regenerate a new key.')">
<button type="submit" <button type="submit"
class="btn btn-outline-danger" class="btn btn-outline-danger"
onclick="return confirm('Are you sure you want to permanently remove the DKIM key for {{ item.domain.domain_name }}? This action cannot be undone and you will lose the ability to sign emails until you regenerate a new key.')"> onclick="event.stopPropagation();">
<i class="bi bi-trash me-1"></i> <i class="bi bi-trash me-1"></i>
Remove Remove
</button> </button>
</form> </form>
</div> </div>
<form method="post" action="{{ url_for('email.regenerate_dkim', domain_id=item.domain.id) }}" class="d-inline"> <button class="btn btn-outline-warning"
<button type="submit" onclick="event.stopPropagation(); regenerateDKIM({{ item.domain.id }}, '{{ item.domain.domain_name }}')">
class="btn btn-outline-warning" <i class="bi bi-arrow-clockwise me-1"></i>
onclick="return confirm('Regenerate DKIM key for {{ item.domain.domain_name }}? This will require updating DNS records.')"> Regenerate
<i class="bi bi-arrow-clockwise me-1"></i> </button>
Regenerate
</button>
</form>
</div> </div>
<div class="ms-auto"> <div class="card-header-clickable" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapse-{{ item.domain.id }}" aria-expanded="false" aria-controls="collapse-{{ item.domain.id }}">
<i class="bi bi-chevron-down" id="chevron-{{ item.domain.id }}"></i> <i class="bi bi-chevron-down" id="chevron-{{ item.domain.id }}"></i>
</div> </div>
</div> </div>
@@ -109,7 +148,7 @@
<h6> <h6>
<i class="bi bi-key me-2"></i> <i class="bi bi-key me-2"></i>
DKIM DNS Record DKIM DNS Record
<span class="dns-status" id="dkim-status-{{ item.domain.id }}"> <span class="dns-status" id="dkim-status-{{ item.domain.domain_name.replace('.', '-') }}">
<span class="status-indicator status-warning"></span> <span class="status-indicator status-warning"></span>
<small class="text-muted">Not checked</small> <small class="text-muted">Not checked</small>
</span> </span>
@@ -136,7 +175,7 @@
<h6> <h6>
<i class="bi bi-shield-lock me-2"></i> <i class="bi bi-shield-lock me-2"></i>
SPF DNS Record SPF DNS Record
<span class="dns-status" id="spf-status-{{ item.domain.id }}"> <span class="dns-status" id="spf-status-{{ item.domain.domain_name.replace('.', '-') }}">
<span class="status-indicator status-warning"></span> <span class="status-indicator status-warning"></span>
<small class="text-muted">Not checked</small> <small class="text-muted">Not checked</small>
</span> </span>
@@ -198,6 +237,60 @@
</div> </div>
{% endfor %} {% endfor %}
<!-- Old DKIM Keys Section -->
{% if old_dkim_data %}
<div class="card mb-4">
<div class="card-header">
<h4 class="mb-0">
<i class="bi bi-archive me-2"></i>
Old DKIM Keys
<span class="badge bg-secondary ms-2">{{ old_dkim_data|length }}</span>
</h4>
</div>
<div class="card-body">
<p class="text-muted mb-3">These keys have been replaced or disabled. They are kept for reference and can be permanently removed.</p>
{% for item in old_dkim_data %}
<div class="card mb-3 border-secondary">
<div class="card-header bg-dark">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">
<i class="bi bi-server me-2"></i>
{{ item.domain.domain_name }}
<span class="badge bg-secondary ms-2">{{ item.status_text }}</span>
</h6>
<small class="text-muted">
Selector: <code>{{ item.dkim_key.selector }}</code> |
Created: {{ item.dkim_key.created_at.strftime('%Y-%m-%d %H:%M') }}
{% if item.dkim_key.replaced_at %}
| Replaced: {{ item.dkim_key.replaced_at.strftime('%Y-%m-%d %H:%M') }}
{% endif %}
</small>
</div>
<div class="btn-group btn-group-sm">
<form method="post" action="{{ url_for('email.toggle_dkim', dkim_id=item.dkim_key.id) }}" class="d-inline">
<button type="submit" class="btn btn-outline-success btn-sm">
<i class="bi bi-play-circle me-1"></i>
Reactivate
</button>
</form>
<form method="post" action="{{ url_for('email.remove_dkim', dkim_id=item.dkim_key.id) }}" class="d-inline" onsubmit="return handleFormSubmit(event, 'Are you sure you want to permanently remove this old DKIM key? This action cannot be undone.')">
<button type="submit"
class="btn btn-outline-danger btn-sm">
<i class="bi bi-trash me-1"></i>
Remove
</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if not dkim_data %} {% if not dkim_data %}
<div class="card"> <div class="card">
<div class="card-body text-center py-5"> <div class="card-body text-center py-5">
@@ -314,21 +407,195 @@
new bootstrap.Modal(document.getElementById('dnsResultModal')).show(); new bootstrap.Modal(document.getElementById('dnsResultModal')).show();
} }
function showAllDNSResults(results) {
let tableRows = '';
results.forEach(result => {
const dkimIcon = result.dkim.success ? '<i class="bi bi-check-circle-fill text-success"></i>' : '<i class="bi bi-x-circle-fill text-danger"></i>';
const spfIcon = result.spf.success ? '<i class="bi bi-check-circle-fill text-success"></i>' : '<i class="bi bi-x-circle-fill text-danger"></i>';
tableRows += `
<tr>
<td><strong>${result.domain}</strong></td>
<td class="text-center">
${dkimIcon}
<small class="d-block">${result.dkim.success ? 'Configured' : 'Not Found'}</small>
</td>
<td class="text-center">
${spfIcon}
<small class="d-block">${result.spf.success ? 'Found' : 'Not Found'}</small>
</td>
<td>
<small class="text-muted">
DKIM: ${result.dkim.message}<br>
SPF: ${result.spf.message}
</small>
</td>
</tr>
`;
});
const resultsHtml = `
<h6>DNS Check Results for All Domains</h6>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Domain</th>
<th class="text-center">DKIM Status</th>
<th class="text-center">SPF Status</th>
<th>Details</th>
</tr>
</thead>
<tbody>
${tableRows}
</tbody>
</table>
</div>
<div class="mt-3">
<div class="alert alert-info">
<small>
<i class="bi bi-info-circle me-1"></i>
<strong>DKIM:</strong> Verifies email signatures for authenticity<br>
<i class="bi bi-info-circle me-1"></i>
<strong>SPF:</strong> Authorizes servers that can send email for your domain
</small>
</div>
</div>
`;
document.getElementById('dnsResults').innerHTML = resultsHtml;
new bootstrap.Modal(document.getElementById('dnsResultModal')).show();
}
async function checkAllDNS() { async function checkAllDNS() {
const domains = document.querySelectorAll('[id^="domain-"]'); const domains = document.querySelectorAll('[id^="domain-"]');
const results = [];
// Show a progress indicator
showToast('Checking DNS records for all domains...', 'info');
for (const domainCard of domains) { for (const domainCard of domains) {
const domainId = domainCard.id.split('-')[1]; try {
// Extract domain name from the card header const domainId = domainCard.id.split('-')[1];
const domainHeaderText = domainCard.querySelector('h5').textContent.trim(); // Extract domain name from the card header
const domainName = domainHeaderText.split('\n')[0].trim().replace(/^\s*\S+\s+/, ''); // Remove icon const domainHeaderText = domainCard.querySelector('h5').textContent.trim();
const selectorElement = domainCard.querySelector('code'); const domainName = domainHeaderText.split('\n')[0].trim().replace(/^\s*\S+\s+/, ''); // Remove icon
if (selectorElement) { const selectorElement = domainCard.querySelector('code');
const selector = selectorElement.textContent;
await checkDomainDNS(domainName, selector); if (selectorElement) {
// Small delay between checks to avoid overwhelming the DNS server const selector = selectorElement.textContent;
await new Promise(resolve => setTimeout(resolve, 500));
// 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(domainName)}&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(domainName)}`
});
const spfResult = await spfResponse.json();
results.push({
domain: domainName,
dkim: dkimResult,
spf: spfResult
});
// Update individual status indicators
const dkimStatus = document.getElementById(`dkim-status-${domainName.replace('.', '-')}`);
const spfStatus = document.getElementById(`spf-status-${domainName.replace('.', '-')}`);
if (dkimStatus) {
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>';
}
}
if (spfStatus) {
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>';
}
}
// Small delay between checks to avoid overwhelming the DNS server
await new Promise(resolve => setTimeout(resolve, 300));
}
} catch (error) {
console.error('Error checking DNS for domain:', error);
} }
} }
// Show combined results in modal
showAllDNSResults(results);
}
// AJAX DKIM regeneration function
// Simple DKIM regeneration function with page reload
async function regenerateDKIM(domainId, domainName) {
const confirmed = await showConfirmation(
`Regenerate DKIM key for ${domainName}? This will require updating DNS records.`,
'Regenerate DKIM Key',
'Regenerate',
'btn-warning'
);
if (!confirmed) {
return;
}
const button = event.target.closest('button');
const originalContent = button.innerHTML;
// Show loading state
button.innerHTML = '<i class="bi bi-arrow-clockwise spinner-border spinner-border-sm me-1"></i>Regenerating...';
button.disabled = true;
try {
const response = await fetch(`{{ url_for('email.regenerate_dkim', domain_id=0) }}`.replace('0', domainId), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({})
});
const result = await response.json();
if (result.success) {
showToast('DKIM key regenerated successfully! Reloading page...', 'success');
// Reload the page after a short delay to show the success message
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showToast(result.message || 'Error regenerating DKIM key', 'danger');
// Restore button on error
button.innerHTML = originalContent;
button.disabled = false;
}
} catch (error) {
console.error('DKIM regeneration error:', error);
showToast('Error regenerating DKIM key', 'danger');
// Restore button on error
button.innerHTML = originalContent;
button.disabled = false;
}
} }
function copyToClipboard(text) { function copyToClipboard(text) {
@@ -350,10 +617,29 @@
}); });
} }
// Handle form submissions with custom confirmation dialogs
async function handleFormSubmit(event, message) {
event.preventDefault(); // Prevent default form submission
const confirmed = await showConfirmation(
message,
'Confirm Action',
'Confirm',
'btn-danger'
);
if (confirmed) {
// Submit the form if confirmed
event.target.submit();
}
return false; // Always return false to prevent default submission
}
// Handle collapsible cards // Handle collapsible cards
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Add click handlers for card headers // Add click handlers for card headers - only for clickable areas
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(function(element) { document.querySelectorAll('.card-header-clickable[data-bs-toggle="collapse"]').forEach(function(element) {
element.addEventListener('click', function() { element.addEventListener('click', function() {
const targetId = this.getAttribute('data-bs-target'); const targetId = this.getAttribute('data-bs-target');
const chevronId = targetId.replace('#collapse-', '#chevron-'); const chevronId = targetId.replace('#collapse-', '#chevron-');
@@ -373,5 +659,42 @@
}); });
}); });
}); });
document.getElementById('createDKIMForm').addEventListener('submit', async function(event) {
event.preventDefault();
const domain = document.getElementById('dkimDomain').value;
const selector = document.getElementById('dkimSelector').value.trim();
const errorDiv = document.getElementById('createDKIMError');
const successDiv = document.getElementById('createDKIMSuccess');
errorDiv.classList.add('d-none');
successDiv.classList.add('d-none');
if (!domain) {
errorDiv.textContent = 'Please select a domain.';
errorDiv.classList.remove('d-none');
return;
}
try {
const response = await fetch("{{ url_for('email.create_dkim') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ domain, selector })
});
const result = await response.json();
if (result.success) {
successDiv.textContent = result.message || 'DKIM key created.';
successDiv.classList.remove('d-none');
setTimeout(() => { window.location.reload(); }, 1200);
} else {
errorDiv.textContent = result.message || 'Failed to create DKIM key.';
errorDiv.classList.remove('d-none');
}
} catch (err) {
errorDiv.textContent = 'Error creating DKIM key.';
errorDiv.classList.remove('d-none');
}
});
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,129 +1,149 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Edit DKIM Key - Email Server{% endblock %} {% block title %}Edit DKIM Selector{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="row">
<h2> <div class="col-md-8 offset-md-2">
<i class="bi bi-shield-check me-2"></i>
Edit DKIM Key for {{ domain.domain_name }}
</h2>
<a href="{{ url_for('email.dkim_list') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>
Back to DKIM Keys
</a>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"> <h4 class="mb-0">
<i class="bi bi-key me-2"></i> <i class="bi bi-pencil me-2"></i>Edit DKIM Selector
DKIM Key Configuration </h4>
</h5> <a href="{{ url_for('email.dkim_list') }}" class="btn btn-light btn-sm">
<i class="bi bi-arrow-left me-1"></i>Back to DKIM Keys
</a>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post"> <form method="POST" class="needs-validation" novalidate>
<div class="mb-3"> <div class="mb-3">
<label for="domain_name" class="form-label">Domain</label> <label for="selector" class="form-label">
<input type="text" <i class="bi bi-key me-1"></i>DKIM Selector
class="form-control" </label>
id="domain_name"
value="{{ domain.domain_name }}"
readonly>
<div class="form-text">The domain this DKIM key belongs to (read-only)</div>
</div>
<div class="mb-3">
<label for="selector" class="form-label">DKIM Selector</label>
<input type="text" <input type="text"
class="form-control" class="form-control"
id="selector" id="selector"
name="selector" name="selector"
value="{{ dkim_key.selector }}" value="{{ dkim_key.selector }}"
maxlength="50" placeholder="default"
pattern="[a-zA-Z0-9_-]+" pattern="^[a-zA-Z0-9_-]+$"
required> required>
<div class="invalid-feedback">
Please provide a valid selector (letters, numbers, hyphens, and underscores only).
</div>
<div class="form-text"> <div class="form-text">
The DKIM selector (alphanumeric, hyphens, and underscores only). <i class="bi bi-info-circle me-1"></i>
This will be used in DNS record names like <code>[selector]._domainkey.{{ domain.domain_name }}</code> The selector is used in DNS records to identify this DKIM key (e.g., "selector._domainkey.{{ domain.domain_name }}")
</div> </div>
</div> </div>
<div class="mb-3"> <div class="row">
<label class="form-label">Key Information</label> <div class="col-md-6">
<div class="row"> <div class="alert alert-info">
<div class="col-md-6"> <h6><i class="bi bi-info-circle me-1"></i>Current Information</h6>
<strong>Created:</strong><br> <p class="mb-1">
<small class="text-muted">{{ dkim_key.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small> <strong>Domain:</strong> {{ domain.domain_name }}
</div> </p>
<div class="col-md-6"> <p class="mb-1">
<strong>Status:</strong><br> <strong>Current Selector:</strong> {{ dkim_key.selector }}
{% if dkim_key.is_active %} </p>
<span class="badge bg-success">Active</span> <p class="mb-1">
{% else %} <strong>Status:</strong>
<span class="badge bg-secondary">Inactive</span> {% if dkim_key.is_active %}
{% endif %} <span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-danger">Inactive</span>
{% endif %}
</p>
<p class="mb-0">
<strong>Created:</strong> {{ dkim_key.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
</p>
</div>
</div>
<div class="col-md-6">
<div class="alert alert-warning">
<h6><i class="bi bi-exclamation-triangle me-1"></i>Important Note</h6>
<p class="mb-0">
Changing the selector will require updating your DNS records.
Make sure to update the DNS record name from
<code>{{ dkim_key.selector }}._domainkey.{{ domain.domain_name }}</code>
to match the new selector name.
</p>
</div> </div>
</div> </div>
</div>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Important:</strong> Changing the selector will require updating your DNS records.
The new DNS record name will be <code id="dns-preview">{{ dkim_key.selector }}._domainkey.{{ domain.domain_name }}</code>
</div> </div>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<a href="{{ url_for('email.dkim_list') }}" class="btn btn-secondary"> <a href="{{ url_for('email.dkim_list') }}" class="btn btn-secondary">
<i class="bi bi-x-circle me-2"></i> <i class="bi bi-x me-1"></i>Cancel
Cancel
</a> </a>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-2"></i> <i class="bi bi-save me-1"></i>Update Selector
Update DKIM Selector
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- DNS Record Information Card -->
<div class="card mt-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="bi bi-dns me-2"></i>DNS Record Information
</h5>
</div>
<div class="card-body">
<div class="alert alert-light">
<h6>Current DNS Record</h6>
<p class="mb-2">
<strong>Name:</strong> <code>{{ dkim_key.selector }}._domainkey.{{ domain.domain_name }}</code>
</p>
<p class="mb-0">
<strong>Type:</strong> TXT
</p>
</div>
<p class="text-muted">
<i class="bi bi-lightbulb me-1"></i>
<strong>Tip:</strong> After changing the selector, you'll need to update your DNS provider
to use the new record name. The DNS record value will remain the same.
</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
{% block extra_js %}
<script> <script>
// Update DNS preview when selector changes // Bootstrap validation
document.getElementById('selector').addEventListener('input', function() { (function() {
const selector = this.value || 'default'; 'use strict';
const domain = '{{ domain.domain_name }}'; window.addEventListener('load', function() {
document.getElementById('dns-preview').textContent = `${selector}._domainkey.${domain}`; var forms = document.getElementsByClassName('needs-validation');
}); var validation = Array.prototype.filter.call(forms, function(form) {
form.addEventListener('submit', function(event) {
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
}, false);
})();
// Form validation // Selector validation
document.querySelector('form').addEventListener('submit', function(e) { document.getElementById('selector').addEventListener('input', function(e) {
const selector = document.getElementById('selector').value.trim(); const value = e.target.value;
if (!selector) { // Basic selector validation (alphanumeric, hyphens, underscores)
e.preventDefault(); const selectorRegex = /^[a-zA-Z0-9_-]+$/;
alert('Selector is required');
return; if (value && !selectorRegex.test(value)) {
} e.target.setCustomValidity('Selector must contain only letters, numbers, hyphens, and underscores');
} else {
if (!/^[a-zA-Z0-9_-]+$/.test(selector)) { e.target.setCustomValidity('');
e.preventDefault(); }
alert('Selector must contain only letters, numbers, hyphens, and underscores'); });
return;
}
if (selector.length > 50) {
e.preventDefault();
alert('Selector must be 50 characters or less');
return;
}
});
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -15,7 +15,7 @@
<h4 class="mb-0"> <h4 class="mb-0">
<i class="fas fa-edit me-2"></i>Edit Domain <i class="fas fa-edit me-2"></i>Edit Domain
</h4> </h4>
<a href="{{ url_for('email.domains') }}" class="btn btn-light btn-sm"> <a href="{{ url_for('email.domains_list') }}" class="btn btn-light btn-sm">
<i class="fas fa-arrow-left me-1"></i>Back to Domains <i class="fas fa-arrow-left me-1"></i>Back to Domains
</a> </a>
</div> </div>
@@ -77,7 +77,7 @@
</div> </div>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<a href="{{ url_for('email.domains') }}" class="btn btn-secondary"> <a href="{{ url_for('email.domains_list') }}" class="btn btn-secondary">
<i class="fas fa-times me-1"></i>Cancel <i class="fas fa-times me-1"></i>Cancel
</a> </a>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">

View File

@@ -49,6 +49,8 @@ DEFAULTS = {
'; DKIM signing configuration': None, '; DKIM signing configuration': None,
'; RSA key size for DKIM keys (1024, 2048, 4096)': None, '; RSA key size for DKIM keys (1024, 2048, 4096)': None,
'DKIM_KEY_SIZE': '2048', 'DKIM_KEY_SIZE': '2048',
'; Provide Public IP address of server, used for SPF in case detection fails': None,
'SPF_SERVER_IP': '192.168.1.1',
}, },
} }

View File

@@ -10,11 +10,12 @@ cc_recipient="targetcc@example.com"
bcc_recipient="targetbcc@example.com" bcc_recipient="targetbcc@example.com"
<<com <<com
python -m email_server.cli_tools add-domain $domain # Setup domain and user via web interface first
python -m email_server.cli_tools add-user $sender $password $domain # Visit http://localhost:5000/email to configure:
python -m email_server.cli_tools add-ip 127.0.0.1 $domain # - Add domain: $domain
python -m email_server.cli_tools add-ip 10.100.111.1 $domain # - Add user: $sender with password $password
python -m email_server.cli_tools generate-dkim $domain # - Add IP whitelist: 127.0.0.1 and 10.100.111.1
# - Generate DKIM key for domain
# options to add CC and BCC recipients for swaks # options to add CC and BCC recipients for swaks
--cc $cc_recipient --cc $cc_recipient

View File

@@ -1,154 +1,90 @@
# ======================================== # ========================================
# SMTP Server Management with cli_tools.py # SMTP Server Management via Web Interface
# ======================================== # ========================================
# 1. Initialize the database (run this first) The SMTP Server now uses a web-based management interface instead of CLI tools.
`python cli_tools.py init`
# ======================================== ## Starting the Application
# DOMAIN MANAGEMENT
# ========================================
# Add domains that require authentication (default)
```python
python cli_tools.py add-domain example.com
python cli_tools.py add-domain mycompany.org
python cli_tools.py add-domain testdomain.net
```
# Add domain that doesn't require authentication (open relay for this domain)
`python cli_tools.py add-domain public.com --no-auth`
# ========================================
# USER MANAGEMENT (for authentication)
# ========================================
# Add users for authentication
```python
python cli_tools.py add-user test@example.com testpass123 example.com
python cli_tools.py add-user admin@example.com adminpass456 example.com
python cli_tools.py add-user john@mycompany.org johnpass789 mycompany.org
python cli_tools.py add-user support@mycompany.org supportpass321 mycompany.org
```
# Add more test users
```
python cli_tools.py add-user demo@testdomain.net demopass111 testdomain.net
python cli_tools.py add-user sales@example.com salespass222 example.com
```
# ========================================
# IP WHITELIST MANAGEMENT (for IP-based auth)
# ========================================
# Add IP addresses that can send without username/password
```python
python cli_tools.py add-ip 127.0.0.1 example.com # Localhost
python cli_tools.py add-ip 192.168.1.100 example.com # Local network
python cli_tools.py add-ip 10.0.0.50 mycompany.org # Internal server
python cli_tools.py add-ip 203.0.113.10 example.com # External trusted IP
```
# ========================================
# DKIM KEY MANAGEMENT
# ========================================
# Generate DKIM keys for domains (for email signing)
```python
python cli_tools.py generate-dkim example.com
python cli_tools.py generate-dkim mycompany.org
python cli_tools.py generate-dkim testdomain.net
```
# List all DKIM keys
`python cli_tools.py list-dkim`
# Show DNS records that need to be added to your DNS provider
`python cli_tools.py show-dns`
# ========================================
# COMPLETE SETUP EXAMPLE
# ========================================
# Complete setup for a new domain:
```python
python cli_tools.py add-domain newdomain.com
python cli_tools.py add-user info@newdomain.com password123 newdomain.com
python cli_tools.py add-user noreply@newdomain.com noreplypass456 newdomain.com
python cli_tools.py add-ip 192.168.1.200 newdomain.com
python cli_tools.py generate-dkim newdomain.com
```
# ========================================
# VERIFICATION COMMANDS
# ========================================
# Check what's in the database
```bash ```bash
sqlite3 smtp_server.db "SELECT * FROM domains;" # Start the unified application (SMTP + Web Interface)
sqlite3 smtp_server.db "SELECT email, domain_id FROM users;" python app.py
sqlite3 smtp_server.db "SELECT ip_address, domain_id FROM whitelisted_ips;"
sqlite3 smtp_server.db "SELECT domain, selector, active FROM dkim_keys;" # Start only the web interface (for management)
python app.py --web-only
# Start only the SMTP server
python app.py --smtp-only
``` ```
## Web Interface Access
- URL: http://localhost:5000/email
- Available management features:
- Domain management
- User authentication management
- IP whitelist management
- DKIM key management with DNS validation
- Email logs and monitoring
- Server settings configuration
## Management Tasks via Web Interface
### Domain Management
1. Navigate to http://localhost:5000/email/domains
2. Click "Add Domain" to add new domains
3. Configure authentication requirements per domain
4. Enable/disable domains as needed
### User Management
1. Navigate to http://localhost:5000/email/users
2. Add users for email authentication
3. Associate users with specific domains
4. Enable/disable user accounts
### IP Whitelist Management
1. Navigate to http://localhost:5000/email/ips
2. Add IP addresses for authentication-free sending
3. Associate IPs with specific domains
4. Manage IP access permissions
### DKIM Key Management
1. Navigate to http://localhost:5000/email/dkim
2. Generate DKIM keys automatically when adding domains
3. View DNS records that need to be configured
4. Check DNS propagation status
5. Regenerate keys if needed
## Example Setup Workflow
### Development Setup
1. Start the application: `python app.py --debug`
2. Open browser to: http://localhost:5000/email
3. Add domain: localhost.dev
4. Add user: dev@localhost.dev with password devpass123
5. Add IP: 127.0.0.1 for localhost.dev
6. Generate and configure DKIM key
### Production Setup
1. Start the application: `python app.py`
2. Open browser to: http://localhost:5000/email
3. Add your company domain
4. Add notification/alert users with strong passwords
5. Add your application server IPs to whitelist
6. Generate DKIM keys and update DNS records
## Database Direct Access (if needed)
```bash
# Check domains
sqlite3 email_server/server_data/smtp_server.db "SELECT * FROM domains;"
# Check users
sqlite3 email_server/server_data/smtp_server.db "SELECT email, domain_id FROM users;"
# Check IP whitelist
sqlite3 email_server/server_data/smtp_server.db "SELECT ip_address, domain_id FROM whitelisted_ips;"
# Check DKIM keys
sqlite3 email_server/server_data/smtp_server.db "SELECT domain, selector, active FROM dkim_keys;"
# Check email logs # Check email logs
`sqlite3 smtp_server.db "SELECT message_id, mail_from, rcpt_tos, status, created_at FROM email_logs ORDER BY created_at DESC LIMIT 10;"` sqlite3 email_server/server_data/smtp_server.db "SELECT message_id, mail_from, rcpt_tos, status, created_at FROM email_logs ORDER BY created_at DESC LIMIT 10;"
```
# ========================================
# HELP AND INFORMATION
# ========================================
# Show all available commands
`python cli_tools.py --help`
# Show help for specific commands
```python
python cli_tools.py add-domain --help
python cli_tools.py add-user --help
python cli_tools.py add-ip --help
python cli_tools.py generate-dkim --help
```
# ========================================
# PRACTICAL EXAMPLES
# ========================================
# Example 1: Setup for development
```python
python cli_tools.py init
python cli_tools.py add-domain localhost.dev
python cli_tools.py add-user dev@localhost.dev devpass123 localhost.dev
python cli_tools.py add-ip 127.0.0.1 localhost.dev
python cli_tools.py generate-dkim localhost.dev
```
# Example 2: Setup for production company
```python
python cli_tools.py add-domain company.com
python cli_tools.py add-user notifications@company.com notifypass123 company.com
python cli_tools.py add-user alerts@company.com alertpass456 company.com
python cli_tools.py add-ip 10.0.1.100 company.com # Application server
python cli_tools.py add-ip 10.0.1.101 company.com # Backup server
python cli_tools.py generate-dkim company.com
```
# Example 3: Setup for testing with external domain
```python
python cli_tools.py add-domain example.org
python cli_tools.py add-user test@example.org testpass789 example.org
python cli_tools.py generate-dkim example.org
python cli_tools.py show-dns # Get DNS records to add
```
# ========================================
# TROUBLESHOOTING COMMANDS
# ========================================
# If you need to check if everything is set up correctly:
```python
python cli_tools.py list-dkim # Verify DKIM keys exist
sqlite3 smtp_server.db "SELECT COUNT(*) FROM domains;" # Count domains
sqlite3 smtp_server.db "SELECT COUNT(*) FROM users;" # Count users
sqlite3 smtp_server.db "SELECT COUNT(*) FROM whitelisted_ips;" # Count IPs
```
# Check recent email activity
`sqlite3 smtp_server.db "SELECT mail_from, rcpt_tos, status, created_at FROM email_logs WHERE created_at > datetime('now', '-1 hour');"`

View File

@@ -1,10 +1,18 @@
## setup domain and account for sending email: ## setup domain and account for sending email:
```bash ```bash
python -m email_server.cli_tools.py add-domain example.com # Manual Test Setup Instructions
python -m email_server.cli_tools.py add-user test@example.com testpass123 example.com
python -m email_server.cli_tools.py add-ip 127.0.0.1 example.com ## Setup via Web Interface
python -m email_server.cli_tools.py add-ip 10.100.111.1 example.com 1. Start the application: `python app.py`
python -m email_server.cli_tools.py generate-dkim example.com 2. Open web browser to: http://localhost:5000/email
3. Use the web interface to:
- Add domain: example.com
- Add user: test@example.com with password testpass123
- Add IP whitelist: 127.0.0.1 and 10.100.111.1 for domain example.com
- Generate DKIM key for example.com
## Alternative Setup via Python Script
Create a setup script if needed for automated testing.
``` ```
## Check db logs ## Check db logs