DKIM key management front end - ok
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
@@ -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,10 +51,41 @@ 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}")
|
||||
# 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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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})>"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
@@ -1,7 +0,0 @@
|
||||
Flask
|
||||
Flask-SQLAlchemy
|
||||
Jinja2
|
||||
Werkzeug
|
||||
dnspython
|
||||
cryptography
|
||||
requests
|
||||
@@ -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,11 +112,16 @@ 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
|
||||
# Add our server IP if it's not unknown
|
||||
if public_ip and public_ip != 'unknown':
|
||||
our_ip = f"ip4:{public_ip}"
|
||||
if our_ip not in base_mechanisms:
|
||||
base_mechanisms.append(our_ip)
|
||||
|
||||
# If no IP available, just use existing mechanisms
|
||||
if not base_mechanisms and public_ip == 'unknown':
|
||||
return existing_spf or 'v=spf1 ~all'
|
||||
|
||||
# Construct SPF record
|
||||
spf_parts = ['v=spf1'] + base_mechanisms + ['~all']
|
||||
return ' '.join(spf_parts)
|
||||
@@ -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'})
|
||||
|
||||
# Get the expected DKIM value from the DKIM manager
|
||||
try:
|
||||
dkim_manager = DKIMManager()
|
||||
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'])
|
||||
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()
|
||||
|
||||
@@ -156,22 +156,24 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Flash messages -->
|
||||
<!-- 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 %}
|
||||
<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">
|
||||
<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 }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</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>
|
||||
{% 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'))) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,10 +45,46 @@
|
||||
</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">
|
||||
<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 }}
|
||||
@@ -54,49 +94,48 @@
|
||||
<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>
|
||||
<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.')">
|
||||
<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>
|
||||
</form>
|
||||
</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) {
|
||||
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;
|
||||
await checkDomainDNS(domainName, selector);
|
||||
// Small delay between checks to avoid overwhelming the DNS server
|
||||
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) {
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<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="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-key me-2"></i>
|
||||
DKIM Key Configuration
|
||||
</h5>
|
||||
</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>
|
||||
<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-secondary">Inactive</span>
|
||||
<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>
|
||||
|
||||
<div class="col-md-6">
|
||||
<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>
|
||||
<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="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 (value && !selectorRegex.test(value)) {
|
||||
e.target.setCustomValidity('Selector must contain only letters, numbers, hyphens, and underscores');
|
||||
} else {
|
||||
e.target.setCustomValidity('');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');"`
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user