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
Python Email server for sending emails directly to recipient ( no email Relay)
```bash
# Testing
.venv/bin/python app.py --web-only --debug
```
## Plan:
- make full python MTA server with front end to allow sending any email
- 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)."""
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
def generate_dkim_keypair(self, domain_name, selector: str = None):
"""Generate DKIM key pair for a domain, optionally with a custom selector."""
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.
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()
try:
# Check if domain exists
@@ -45,11 +51,42 @@ class DKIMManager:
# Use provided selector or instance selector
use_selector = selector or self.selector
# Check if DKIM key with this selector already exists
existing_key = session.query(DKIMKey).filter_by(domain_id=domain.id, selector=use_selector, is_active=True).first()
if existing_key:
logger.debug(f"DKIM key already exists for domain {domain_name} and selector {use_selector}")
return True
# Ensure only one active DKIM key per domain - mark existing keys as replaced
existing_active_keys = session.query(DKIMKey).filter_by(domain_id=domain.id, is_active=True).all()
for existing_key in existing_active_keys:
existing_key.is_active = False
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
private_key = rsa.generate_private_key(
@@ -118,7 +155,7 @@ class DKIMManager:
def get_dkim_public_key_record(self, domain_name):
"""Get DKIM public key DNS record for a domain (active key only)."""
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_data = ''.join(public_key_lines[1:-1]) # Remove header/footer
return {

View File

@@ -176,6 +176,7 @@ class DKIMKey(Base):
public_key = Column(Text, nullable=False)
is_active = Column(Boolean, default=True)
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):
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...")
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...")
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
from .models import Session, Domain, User, WhitelistedIP, hash_password
@@ -118,8 +118,7 @@ async def start_server():
controller_tls.start()
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('Management commands:')
logger.debug(' python cli_tools.py --help')
logger.debug('Management available via web interface at: http://localhost:5000/email')
try:
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 re
from datetime import datetime
from datetime import datetime
from typing import Optional, Dict, List, Tuple
# Import email server modules
@@ -46,15 +47,32 @@ email_bp = Blueprint('email', __name__,
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()
response1 = requests.get('https://ifconfig.me/ip', timeout=3, verify=False)
ip = response1.text.strip()
if ip and ip != 'unknown':
return ip
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'
response = requests.get('https://httpbin.org/ip', timeout=3, verify=False)
ip = response.json()['origin'].split(',')[0].strip()
if ip and ip != '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:
"""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()
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)
# Add our server IP if it's not unknown
if public_ip and public_ip != 'unknown':
our_ip = f"ip4:{public_ip}"
if our_ip not in base_mechanisms:
base_mechanisms.append(our_ip)
# If no IP available, just use existing mechanisms
if not base_mechanisms and public_ip == 'unknown':
return existing_spf or 'v=spf1 ~all'
# Construct SPF record
spf_parts = ['v=spf1'] + base_mechanisms + ['~all']
@@ -712,14 +735,26 @@ 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 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
public_ip = get_public_ip()
# Prepare DKIM data with DNS information
dkim_data = []
for dkim_key, domain in dkim_keys:
# Prepare active DKIM data with DNS information
active_dkim_data = []
for dkim_key, domain in active_dkim_keys:
# Get DKIM DNS record
dkim_manager = DKIMManager()
dns_record = dkim_manager.get_dkim_public_key_record(domain.domain_name)
@@ -736,7 +771,7 @@ def dkim_list():
# Generate recommended SPF
recommended_spf = generate_spf_record(domain.domain_name, public_ip, existing_spf)
dkim_data.append({
active_dkim_data.append({
'dkim_key': dkim_key,
'domain': domain,
'dns_record': dns_record,
@@ -745,7 +780,20 @@ def dkim_list():
'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:
session.close()
@@ -756,21 +804,116 @@ def regenerate_dkim(domain_id: int):
try:
domain = session.query(Domain).get(domain_id)
if not domain:
if request.headers.get('Content-Type') == 'application/json':
return jsonify({'success': False, 'message': 'Domain not found'})
flash('Domain not found', 'error')
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()
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:
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()
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()
# 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')
else:
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')
return redirect(url_for('email.dkim_list'))
@@ -778,6 +921,8 @@ def regenerate_dkim(domain_id: int):
except Exception as e:
session.rollback()
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')
return redirect(url_for('email.dkim_list'))
finally:
@@ -844,16 +989,25 @@ def toggle_dkim(dkim_id: int):
if not dkim_key:
flash('DKIM key not found', 'error')
return redirect(url_for('email.dkim_list'))
domain = session.query(Domain).get(dkim_key.domain_id)
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
if dkim_key.is_active:
dkim_key.replaced_at = None
session.commit()
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')
return redirect(url_for('email.dkim_list'))
except Exception as e:
session.rollback()
logger.error(f"Error toggling DKIM status: {e}")
@@ -895,15 +1049,27 @@ 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'})
if not all([domain, selector]):
return jsonify({'success': False, 'message': 'Missing domain or selector parameters'})
dns_name = f"{selector}._domainkey.{domain}"
result = check_dns_record(dns_name, 'TXT', expected_value)
# Get the expected DKIM value from the DKIM manager
try:
dkim_manager = DKIMManager()
dns_record = dkim_manager.get_dkim_public_key_record(domain)
return jsonify(result)
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'])
def check_spf_dns():
@@ -1049,3 +1215,38 @@ def internal_error(error):
error_code=500,
error_message='Internal server error',
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>
</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>
<!-- Toast container for notifications -->
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1090;">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if 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="d-flex">
<div class="toast-body">
<i class="bi bi-{{ 'exclamation-triangle' if category == 'error' else 'check-circle' if category == 'success' else '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>
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<!-- Page content -->
<main>
@@ -180,6 +182,28 @@
</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 -->
<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);
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();
// Initialize toasts
document.addEventListener('DOMContentLoaded', function() {
const toastElements = document.querySelectorAll('.toast');
toastElements.forEach(function(toastElement) {
const toast = new bootstrap.Toast(toastElement);
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() {
const deleteButtons = document.querySelectorAll('[data-confirm]');
deleteButtons.forEach(function(button) {
button.addEventListener('click', function(e) {
if (!confirm(this.getAttribute('data-confirm'))) {
e.preventDefault();
button.addEventListener('click', async function(e) {
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
</h2>
<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()">
<i class="bi bi-arrow-clockwise me-2"></i>
Check All DNS
@@ -41,62 +45,97 @@
</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 %}
<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">
<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 }}')">
<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 }}">
<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>
<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>
Check DNS
</button>
<div class="btn-group btn-group-sm" role="group">
<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>
Edit
</a>
<form method="post" action="{{ url_for('email.toggle_dkim', dkim_id=item.dkim_key.id) }}" class="d-inline">
{% 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>
Disable
</button>
{% 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>
Enable
</button>
{% endif %}
</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"
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>
Remove
</button>
</form>
</div>
<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>
<button class="btn btn-outline-warning"
onclick="event.stopPropagation(); regenerateDKIM({{ item.domain.id }}, '{{ item.domain.domain_name }}')">
<i class="bi bi-arrow-clockwise me-1"></i>
Regenerate
</button>
</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>
</div>
</div>
@@ -109,7 +148,7 @@
<h6>
<i class="bi bi-key me-2"></i>
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>
<small class="text-muted">Not checked</small>
</span>
@@ -136,7 +175,7 @@
<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="dns-status" id="spf-status-{{ item.domain.domain_name.replace('.', '-') }}">
<span class="status-indicator status-warning"></span>
<small class="text-muted">Not checked</small>
</span>
@@ -198,6 +237,60 @@
</div>
{% 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 %}
<div class="card">
<div class="card-body text-center py-5">
@@ -314,21 +407,195 @@
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() {
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) {
const domainId = domainCard.id.split('-')[1];
// Extract domain name from the card header
const domainHeaderText = domainCard.querySelector('h5').textContent.trim();
const domainName = domainHeaderText.split('\n')[0].trim().replace(/^\s*\S+\s+/, ''); // Remove icon
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));
try {
const domainId = domainCard.id.split('-')[1];
// Extract domain name from the card header
const domainHeaderText = domainCard.querySelector('h5').textContent.trim();
const domainName = domainHeaderText.split('\n')[0].trim().replace(/^\s*\S+\s+/, ''); // Remove icon
const selectorElement = domainCard.querySelector('code');
if (selectorElement) {
const selector = selectorElement.textContent;
// 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) {
@@ -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
document.addEventListener('DOMContentLoaded', function() {
// Add click handlers for card headers
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(function(element) {
// Add click handlers for card headers - only for clickable areas
document.querySelectorAll('.card-header-clickable[data-bs-toggle="collapse"]').forEach(function(element) {
element.addEventListener('click', function() {
const targetId = this.getAttribute('data-bs-target');
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>
{% endblock %}

View File

@@ -1,129 +1,149 @@
{% extends "base.html" %}
{% block title %}Edit DKIM Key - Email Server{% endblock %}
{% block title %}Edit DKIM Selector{% 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>
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="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-key me-2"></i>
DKIM Key Configuration
</h5>
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h4 class="mb-0">
<i class="bi bi-pencil me-2"></i>Edit DKIM Selector
</h4>
<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 class="card-body">
<form method="post">
<form method="POST" class="needs-validation" novalidate>
<div class="mb-3">
<label for="domain_name" class="form-label">Domain</label>
<input type="text"
class="form-control"
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>
<label for="selector" class="form-label">
<i class="bi bi-key me-1"></i>DKIM Selector
</label>
<input type="text"
class="form-control"
id="selector"
name="selector"
value="{{ dkim_key.selector }}"
maxlength="50"
pattern="[a-zA-Z0-9_-]+"
placeholder="default"
pattern="^[a-zA-Z0-9_-]+$"
required>
<div class="invalid-feedback">
Please provide a valid selector (letters, numbers, hyphens, and underscores only).
</div>
<div class="form-text">
The DKIM selector (alphanumeric, hyphens, and underscores only).
This will be used in DNS record names like <code>[selector]._domainkey.{{ domain.domain_name }}</code>
<i class="bi bi-info-circle me-1"></i>
The selector is used in DNS records to identify this DKIM key (e.g., "selector._domainkey.{{ domain.domain_name }}")
</div>
</div>
<div class="mb-3">
<label class="form-label">Key Information</label>
<div class="row">
<div class="col-md-6">
<strong>Created:</strong><br>
<small class="text-muted">{{ dkim_key.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</div>
<div class="col-md-6">
<strong>Status:</strong><br>
{% if dkim_key.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-secondary">Inactive</span>
{% endif %}
<div class="row">
<div class="col-md-6">
<div class="alert alert-info">
<h6><i class="bi bi-info-circle me-1"></i>Current Information</h6>
<p class="mb-1">
<strong>Domain:</strong> {{ domain.domain_name }}
</p>
<p class="mb-1">
<strong>Current Selector:</strong> {{ dkim_key.selector }}
</p>
<p class="mb-1">
<strong>Status:</strong>
{% if dkim_key.is_active %}
<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 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 class="d-flex justify-content-between">
<a href="{{ url_for('email.dkim_list') }}" class="btn btn-secondary">
<i class="bi bi-x-circle me-2"></i>
Cancel
<i class="bi bi-x me-1"></i>Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-2"></i>
Update DKIM Selector
<i class="bi bi-save me-1"></i>Update Selector
</button>
</div>
</form>
</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>
{% endblock %}
{% block extra_js %}
<script>
// Update DNS preview when selector changes
document.getElementById('selector').addEventListener('input', function() {
const selector = this.value || 'default';
const domain = '{{ domain.domain_name }}';
document.getElementById('dns-preview').textContent = `${selector}._domainkey.${domain}`;
});
// Bootstrap validation
(function() {
'use strict';
window.addEventListener('load', function() {
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
document.querySelector('form').addEventListener('submit', function(e) {
const selector = document.getElementById('selector').value.trim();
// Selector validation
document.getElementById('selector').addEventListener('input', function(e) {
const value = e.target.value;
if (!selector) {
e.preventDefault();
alert('Selector is required');
return;
}
// Basic selector validation (alphanumeric, hyphens, underscores)
const selectorRegex = /^[a-zA-Z0-9_-]+$/;
if (!/^[a-zA-Z0-9_-]+$/.test(selector)) {
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;
}
});
if (value && !selectorRegex.test(value)) {
e.target.setCustomValidity('Selector must contain only letters, numbers, hyphens, and underscores');
} else {
e.target.setCustomValidity('');
}
});
</script>
{% endblock %}

View File

@@ -15,7 +15,7 @@
<h4 class="mb-0">
<i class="fas fa-edit me-2"></i>Edit Domain
</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
</a>
</div>
@@ -77,7 +77,7 @@
</div>
<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
</a>
<button type="submit" class="btn btn-primary">

View File

@@ -49,6 +49,8 @@ DEFAULTS = {
'; DKIM signing configuration': None,
'; RSA key size for DKIM keys (1024, 2048, 4096)': None,
'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"
<<com
python -m email_server.cli_tools add-domain $domain
python -m email_server.cli_tools add-user $sender $password $domain
python -m email_server.cli_tools add-ip 127.0.0.1 $domain
python -m email_server.cli_tools add-ip 10.100.111.1 $domain
python -m email_server.cli_tools generate-dkim $domain
# Setup domain and user via web interface first
# Visit http://localhost:5000/email to configure:
# - Add domain: $domain
# - Add user: $sender with password $password
# - 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
--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)
`python cli_tools.py init`
The SMTP Server now uses a web-based management interface instead of CLI tools.
# ========================================
# 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
## Starting the Application
```bash
sqlite3 smtp_server.db "SELECT * FROM domains;"
sqlite3 smtp_server.db "SELECT email, domain_id FROM users;"
sqlite3 smtp_server.db "SELECT ip_address, domain_id FROM whitelisted_ips;"
sqlite3 smtp_server.db "SELECT domain, selector, active FROM dkim_keys;"
# Start the unified application (SMTP + Web Interface)
python app.py
# 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
`sqlite3 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
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;"
```
# ========================================
# 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:
```bash
python -m email_server.cli_tools.py add-domain example.com
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
python -m email_server.cli_tools.py add-ip 10.100.111.1 example.com
python -m email_server.cli_tools.py generate-dkim example.com
# Manual Test Setup Instructions
## Setup via Web Interface
1. Start the application: `python app.py`
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