first commit
This commit is contained in:
28
.dockerignore
Normal file
28
.dockerignore
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Python cache
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Data directory (will be mounted as volume)
|
||||||
|
data/
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Other
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
config.ini
|
||||||
|
*.log
|
||||||
|
data/
|
||||||
|
.DS_Store
|
||||||
|
BUI*
|
||||||
|
build.sh
|
||||||
|
docker-compose.dist*
|
||||||
|
GITEA.md
|
||||||
136
DOCKER.md
Normal file
136
DOCKER.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# Docker Quick Start Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker installed on your system
|
||||||
|
- Docker Compose installed (usually comes with Docker Desktop)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Start the application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Build the Docker image (first time only)
|
||||||
|
- Start the container in detached mode
|
||||||
|
- Create the `data` directory for the database
|
||||||
|
- Expose port 5172
|
||||||
|
|
||||||
|
### 2. Access the application
|
||||||
|
|
||||||
|
Open your browser and navigate to:
|
||||||
|
- Local: `http://localhost:5172`
|
||||||
|
- Network: `http://YOUR_SERVER_IP:5172`
|
||||||
|
|
||||||
|
### 3. Login
|
||||||
|
|
||||||
|
Default credentials:
|
||||||
|
- Password: `admin123`
|
||||||
|
|
||||||
|
**Important**: Change this password immediately after first login!
|
||||||
|
|
||||||
|
### 4. Configure Mailcow
|
||||||
|
|
||||||
|
1. Click "Configuration"
|
||||||
|
2. Enter your Mailcow server (without https://)
|
||||||
|
3. Enter your Mailcow API key
|
||||||
|
4. Click "Save Mailcow Config"
|
||||||
|
5. Click "Change Password" and set a secure password
|
||||||
|
|
||||||
|
### 5. Sync aliases
|
||||||
|
|
||||||
|
Click "Sync Aliases" to import your existing aliases from Mailcow.
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
### View logs
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop the application
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart the application
|
||||||
|
```bash
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update after code changes
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove everything (including data)
|
||||||
|
```bash
|
||||||
|
docker-compose down -v
|
||||||
|
rm -rf data/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
The `./data` directory is mounted as a volume, containing:
|
||||||
|
- `malias2.db` - Your aliases database and configuration
|
||||||
|
- `malias2.log` - Application logs
|
||||||
|
|
||||||
|
This directory persists across container restarts and updates.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container won't start
|
||||||
|
Check logs:
|
||||||
|
```bash
|
||||||
|
docker-compose logs mailcow-alias-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port already in use
|
||||||
|
Edit `docker-compose.yml` and change the port mapping:
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "8080:5172" # Change 8080 to any available port
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can't connect to Mailcow server
|
||||||
|
Make sure your Mailcow server is accessible from the Docker container. If Mailcow is running on `localhost`, you may need to use `host.docker.internal` instead.
|
||||||
|
|
||||||
|
### Reset to defaults
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
rm -rf data/
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
To update to a new version:
|
||||||
|
|
||||||
|
1. Pull the latest code:
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Rebuild and restart:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Your data in the `./data` directory will be preserved.
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Change the default password immediately
|
||||||
|
- The application is designed for internal networks
|
||||||
|
- For external access, use a reverse proxy with HTTPS (nginx, Traefik, etc.)
|
||||||
|
- Consider using Docker secrets for sensitive data in production
|
||||||
|
|
||||||
|
## Network Access
|
||||||
|
|
||||||
|
To access from other devices on your network:
|
||||||
|
1. Find your server's IP address
|
||||||
|
2. Access via `http://YOUR_IP:5172`
|
||||||
|
3. Make sure your firewall allows connections on port 5172
|
||||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY app.py .
|
||||||
|
COPY malias_wrapper.py .
|
||||||
|
COPY malias.py .
|
||||||
|
COPY templates/ templates/
|
||||||
|
|
||||||
|
# Create data directory
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 5172
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["python", "app.py"]
|
||||||
297
README.md
Normal file
297
README.md
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
# Mailcow Alias Manager Web Interface
|
||||||
|
|
||||||
|
A Python-based web application for managing mail aliases on a Mailcow instance. Built with Flask and a dark theme interface for internal network use.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Dark-themed user interface
|
||||||
|
- Simple password authentication
|
||||||
|
- Manage Mailcow mail aliases through a web interface
|
||||||
|
- List all aliases on your Mailcow server (paginated view)
|
||||||
|
- Search for aliases in local database
|
||||||
|
- Create new permanent aliases
|
||||||
|
- Create time-limited aliases (1 year validity)
|
||||||
|
- Delete existing aliases
|
||||||
|
- Sync aliases from Mailcow server to local database
|
||||||
|
- View all mail domains
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### Option A: Docker (Recommended)
|
||||||
|
|
||||||
|
#### 1. Download the docker-compose.yml file
|
||||||
|
|
||||||
|
Download `docker-compose.yml` to your preferred directory.
|
||||||
|
|
||||||
|
#### 2. Start the application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at: `http://localhost:5172`
|
||||||
|
|
||||||
|
#### 3. View logs (optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Stop the application (when needed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: Manual Python Installation
|
||||||
|
|
||||||
|
#### 1. Create a virtual environment (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Run the application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at: `http://localhost:5172`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### 1. Configure Mailcow Connection
|
||||||
|
|
||||||
|
You can configure your Mailcow server connection either:
|
||||||
|
|
||||||
|
**Option A: Using the web interface (Recommended)**
|
||||||
|
- Click the "Configuration" button in the web interface
|
||||||
|
- Enter your Mailcow server (e.g., `mail.example.com` without https://)
|
||||||
|
- Enter your Mailcow API key
|
||||||
|
- Click Save
|
||||||
|
|
||||||
|
**Option B: Using malias.py directly**
|
||||||
|
```bash
|
||||||
|
python3 malias.py -s your.mailcow.server YOUR_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Initial Sync
|
||||||
|
|
||||||
|
After configuring the connection, sync your aliases from the Mailcow server:
|
||||||
|
- Click the "Sync Aliases" button in the web interface
|
||||||
|
- This will populate the local database with your existing aliases
|
||||||
|
|
||||||
|
### 3. First Login
|
||||||
|
|
||||||
|
- Navigate to the login page
|
||||||
|
- Enter the default password: `admin123`
|
||||||
|
- You'll be redirected to the main application
|
||||||
|
- **IMPORTANT**: Immediately go to Configuration and change your password!
|
||||||
|
|
||||||
|
### 4. Change Default Password
|
||||||
|
|
||||||
|
After first login:
|
||||||
|
1. Click the "Configuration" button
|
||||||
|
2. Scroll to "Change Password" section
|
||||||
|
3. Enter current password: `admin123`
|
||||||
|
4. Enter your new password (minimum 6 characters)
|
||||||
|
5. Confirm your new password
|
||||||
|
6. Click "Change Password"
|
||||||
|
|
||||||
|
The password is stored encrypted in the database using bcrypt hashing.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Available Functions
|
||||||
|
|
||||||
|
**List All Aliases**
|
||||||
|
- Shows all aliases in a paginated table (20 per page)
|
||||||
|
- Navigate through pages using Previous/Next buttons
|
||||||
|
|
||||||
|
**Sync Aliases**
|
||||||
|
- Synchronizes aliases from Mailcow server to local database
|
||||||
|
- Run this periodically to keep local database up to date
|
||||||
|
|
||||||
|
**Show Domains**
|
||||||
|
- Lists all mail domains configured on your Mailcow instance
|
||||||
|
|
||||||
|
**Create Alias**
|
||||||
|
- Opens a dialog to create a new permanent alias
|
||||||
|
- Enter the alias email (e.g., `alias@example.com`)
|
||||||
|
- Enter the destination email where mail should be forwarded
|
||||||
|
|
||||||
|
**Delete Alias**
|
||||||
|
- Opens a dialog to delete an existing alias
|
||||||
|
- Enter the alias email to remove
|
||||||
|
|
||||||
|
**Create Timed Alias**
|
||||||
|
- Creates a time-limited alias (valid for 1 year)
|
||||||
|
- Enter the destination email (where the mail goes)
|
||||||
|
- Enter the domain to create the alias on
|
||||||
|
- The system will generate a unique timed alias
|
||||||
|
|
||||||
|
**Search Aliases**
|
||||||
|
- Search for aliases in the local database
|
||||||
|
- Searches both alias addresses and destination addresses
|
||||||
|
- Results show matching aliases with their destinations
|
||||||
|
|
||||||
|
**Configuration**
|
||||||
|
- Configure your Mailcow server address and API key
|
||||||
|
- Server address should be without `https://` (e.g., `mail.example.com`)
|
||||||
|
- Change your application password for security
|
||||||
|
|
||||||
|
## Mailcow API Integration
|
||||||
|
|
||||||
|
This application integrates with your existing `malias.py` script, which provides:
|
||||||
|
- Connection to Mailcow API
|
||||||
|
- Local SQLite database for caching aliases
|
||||||
|
- CRUD operations for mail aliases
|
||||||
|
- Time-limited alias creation
|
||||||
|
- Search and sync functionality
|
||||||
|
|
||||||
|
The web interface provides a user-friendly way to access these functions without using the command line.
|
||||||
|
|
||||||
|
## Security Note
|
||||||
|
|
||||||
|
This application uses password authentication with bcrypt hashing and is designed for internal network use. Security features:
|
||||||
|
- Passwords are hashed using bcrypt before storage
|
||||||
|
- Session-based authentication
|
||||||
|
- Password change functionality
|
||||||
|
- Minimum password length enforcement (6 characters)
|
||||||
|
|
||||||
|
For production use on a public network, consider implementing:
|
||||||
|
- HTTPS/TLS encryption
|
||||||
|
- More robust authentication mechanisms (2FA, OAuth, etc.)
|
||||||
|
- Rate limiting for login attempts
|
||||||
|
- Session timeout configuration
|
||||||
|
- Account lockout after failed attempts
|
||||||
|
|
||||||
|
Do not expose this application to the public internet without additional security measures, particularly HTTPS.
|
||||||
|
|
||||||
|
## Docker Details
|
||||||
|
|
||||||
|
The application runs in Docker with persistent data storage.
|
||||||
|
|
||||||
|
### Data Persistence
|
||||||
|
|
||||||
|
Your data is stored in the `./data` directory on your host machine:
|
||||||
|
- `malias2.db` - Database containing aliases and configuration
|
||||||
|
- `malias2.log` - Application logs
|
||||||
|
|
||||||
|
This directory persists across container restarts and updates.
|
||||||
|
|
||||||
|
### Customization
|
||||||
|
|
||||||
|
You can customize the timezone by editing `docker-compose.yml`:
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Oslo # Change to your timezone
|
||||||
|
```
|
||||||
|
|
||||||
|
You can change the port by editing `docker-compose.yml`:
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "8080:5172" # Change 8080 to your preferred port
|
||||||
|
```
|
||||||
|
|
||||||
|
### Useful Docker Commands
|
||||||
|
|
||||||
|
**View logs:**
|
||||||
|
```bash
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restart:**
|
||||||
|
```bash
|
||||||
|
docker compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stop:**
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update to newer version:**
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables:
|
||||||
|
You can customize the timezone in `docker-compose.yml`:
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Oslo # Change to your timezone
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Docker Commands:
|
||||||
|
|
||||||
|
**Build and start (development):**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**View logs:**
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f mailcow-alias-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restart:**
|
||||||
|
```bash
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stop:**
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rebuild after code changes:**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- **README.md** - Main documentation (this file)
|
||||||
|
- **DOCKER.md** - Docker usage guide for end users
|
||||||
|
|
||||||
|
## What You Need
|
||||||
|
|
||||||
|
For Docker deployment:
|
||||||
|
- `docker-compose.yml` - The only file you need to download
|
||||||
|
- Docker and Docker Compose installed on your system
|
||||||
|
|
||||||
|
The application will automatically:
|
||||||
|
- Pull the Docker image
|
||||||
|
- Create the `data/` directory for your database
|
||||||
|
- Start on port 5172
|
||||||
|
|
||||||
|
For manual Python installation:
|
||||||
|
- All application files from the repository
|
||||||
|
- Python 3.11 or higher
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**"Error: No mailcow server or API key registered"**
|
||||||
|
- Make sure you've configured the Mailcow server and API key in the Configuration page
|
||||||
|
- The API key must have permissions to manage aliases on your Mailcow instance
|
||||||
|
|
||||||
|
**Search returns no results**
|
||||||
|
- Make sure you've run "Sync Aliases" first to populate the local database
|
||||||
|
- The search only searches the local database, not the live Mailcow server
|
||||||
|
|
||||||
|
**Connection errors**
|
||||||
|
- Verify your Mailcow server is accessible from the machine running this app
|
||||||
|
- Check that the API key is valid and has the correct permissions
|
||||||
|
- Review logs in `data/malias2.log`
|
||||||
212
app.py
Normal file
212
app.py
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
from flask import Flask, render_template, request, jsonify, redirect, url_for, session
|
||||||
|
from functools import wraps
|
||||||
|
import malias_wrapper as malias_w
|
||||||
|
import os
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.urandom(24) # Secret key for session management
|
||||||
|
|
||||||
|
# Initialize database on startup
|
||||||
|
malias_w.init_database()
|
||||||
|
|
||||||
|
def login_required(f):
|
||||||
|
"""Decorator to require login for routes"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
return jsonify({'status': 'error', 'message': 'Not authenticated', 'redirect': '/login'}), 401
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
"""Login page"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
password = request.json.get('password', '')
|
||||||
|
if malias_w.verify_password(password):
|
||||||
|
session['logged_in'] = True
|
||||||
|
return jsonify({'status': 'success', 'message': 'Login successful'})
|
||||||
|
else:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Invalid password'})
|
||||||
|
|
||||||
|
# Check if already logged in
|
||||||
|
if session.get('logged_in'):
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
"""Logout"""
|
||||||
|
session.pop('logged_in', None)
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
"""Main page - requires login"""
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/list_aliases', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def list_aliases():
|
||||||
|
"""List all aliases from Mailcow with pagination"""
|
||||||
|
try:
|
||||||
|
connection = malias_w.get_settings_from_db()
|
||||||
|
if not connection:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Please configure Mailcow server first'})
|
||||||
|
|
||||||
|
# Get page number from request, default to 1
|
||||||
|
page = request.json.get('page', 1) if request.json else 1
|
||||||
|
|
||||||
|
result = malias_w.get_all_aliases(page=page, per_page=20)
|
||||||
|
return jsonify({'status': 'success', 'data': result})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': f"Error: {str(e)}"})
|
||||||
|
|
||||||
|
@app.route('/sync_aliases', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def sync_aliases():
|
||||||
|
"""Sync aliases from server to local DB"""
|
||||||
|
try:
|
||||||
|
count = malias_w.update_aliases()
|
||||||
|
result = f"Aliases synchronized successfully! Added {count} new aliases to local DB."
|
||||||
|
return jsonify({'status': 'success', 'message': result})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': f"Error syncing: {str(e)}"})
|
||||||
|
|
||||||
|
@app.route('/get_domains', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def get_domains():
|
||||||
|
"""Get all mail domains"""
|
||||||
|
try:
|
||||||
|
domains = malias_w.get_domains()
|
||||||
|
domain_list = [d['domain_name'] for d in domains]
|
||||||
|
result = f"Domains: {', '.join(domain_list)}"
|
||||||
|
return jsonify({'status': 'success', 'message': result, 'domains': domain_list})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': f"Error: {str(e)}"})
|
||||||
|
|
||||||
|
@app.route('/create_alias', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def create_alias():
|
||||||
|
"""Create a new alias"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
alias = data.get('alias', '')
|
||||||
|
goto = data.get('goto', '')
|
||||||
|
|
||||||
|
if not alias or not goto:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Both alias and destination are required'})
|
||||||
|
|
||||||
|
malias_w.create_alias(alias, goto)
|
||||||
|
result = f"Alias {alias} created successfully for {goto}"
|
||||||
|
return jsonify({'status': 'success', 'message': result})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': f"Error creating alias: {str(e)}"})
|
||||||
|
|
||||||
|
@app.route('/delete_alias', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def delete_alias_route():
|
||||||
|
"""Delete an alias"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
alias = data.get('alias', '')
|
||||||
|
|
||||||
|
if not alias:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Alias is required'})
|
||||||
|
|
||||||
|
malias_w.delete_alias(alias)
|
||||||
|
result = f"Alias {alias} deleted successfully"
|
||||||
|
return jsonify({'status': 'success', 'message': result})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': f"Error deleting alias: {str(e)}"})
|
||||||
|
|
||||||
|
@app.route('/create_timed_alias', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def create_timed_alias():
|
||||||
|
"""Create a time-limited alias"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
username = data.get('username', '')
|
||||||
|
domain = data.get('domain', '')
|
||||||
|
|
||||||
|
if not username or not domain:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Both username and domain are required'})
|
||||||
|
|
||||||
|
malias_w.create_timed_alias(username, domain)
|
||||||
|
result = f"Timed alias created for {username} on domain {domain}"
|
||||||
|
return jsonify({'status': 'success', 'message': result})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': f"Error: {str(e)}"})
|
||||||
|
|
||||||
|
@app.route('/search', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def search():
|
||||||
|
"""Search for aliases"""
|
||||||
|
query = request.json.get('query', '')
|
||||||
|
|
||||||
|
if not query:
|
||||||
|
return jsonify({'status': 'error', 'message': 'No search query provided'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
results = malias_w.search_aliases(query)
|
||||||
|
|
||||||
|
if results:
|
||||||
|
result_list = [f"{alias} → {goto}" for alias, goto in results]
|
||||||
|
message = f"Found {len(results)} alias(es):\n" + "\n".join(result_list)
|
||||||
|
else:
|
||||||
|
message = f"No aliases found matching '{query}'"
|
||||||
|
|
||||||
|
return jsonify({'status': 'success', 'message': message, 'query': query})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': f"Search error: {str(e)}"})
|
||||||
|
|
||||||
|
@app.route('/config', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def config_page():
|
||||||
|
"""Configuration management for Mailcow connection"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
mailcow_server = request.json.get('mailcow_server', '')
|
||||||
|
mailcow_api_key = request.json.get('mailcow_api_key', '')
|
||||||
|
|
||||||
|
if mailcow_server and mailcow_api_key:
|
||||||
|
malias_w.set_connection_info(mailcow_server, mailcow_api_key)
|
||||||
|
return jsonify({'status': 'success', 'message': 'Mailcow configuration saved successfully'})
|
||||||
|
else:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Server and API key are required'})
|
||||||
|
|
||||||
|
# GET request - return current config
|
||||||
|
try:
|
||||||
|
current_config = malias_w.get_config()
|
||||||
|
return jsonify(current_config)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'mailcow_server': '', 'mailcow_api_key': ''})
|
||||||
|
|
||||||
|
@app.route('/change_password', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def change_password_route():
|
||||||
|
"""Change password"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
old_password = data.get('old_password', '')
|
||||||
|
new_password = data.get('new_password', '')
|
||||||
|
confirm_password = data.get('confirm_password', '')
|
||||||
|
|
||||||
|
if not old_password or not new_password or not confirm_password:
|
||||||
|
return jsonify({'status': 'error', 'message': 'All fields are required'})
|
||||||
|
|
||||||
|
if new_password != confirm_password:
|
||||||
|
return jsonify({'status': 'error', 'message': 'New passwords do not match'})
|
||||||
|
|
||||||
|
if len(new_password) < 6:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Password must be at least 6 characters'})
|
||||||
|
|
||||||
|
malias_w.change_password(old_password, new_password)
|
||||||
|
return jsonify({'status': 'success', 'message': 'Password changed successfully'})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)})
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=5172)
|
||||||
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
mailcow-alias-manager:
|
||||||
|
image: gitlab.pm/rune/malias-web:latest
|
||||||
|
container_name: mailcow-alias-manager
|
||||||
|
ports:
|
||||||
|
- "5172:5172"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Oslo
|
||||||
621
malias.py
Normal file
621
malias.py
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
#!/usr/bin/python3 -W ignore::DeprecationWarning
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
from sqlite3 import Error
|
||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from rich import print
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from string import ascii_letters, digits
|
||||||
|
from argparse import RawTextHelpFormatter
|
||||||
|
from operator import itemgetter
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
# Info pages for dev
|
||||||
|
# https://mailcow.docs.apiary.io/#reference/aliases/get-aliases/get-aliases
|
||||||
|
# https://demo.mailcow.email/api/#/Aliases
|
||||||
|
# HTTPx ref -> https://www.python-httpx.org/
|
||||||
|
|
||||||
|
homefilepath = Path.home()
|
||||||
|
filepath = homefilepath.joinpath('.config/malias2')
|
||||||
|
database = filepath.joinpath('malias2.db')
|
||||||
|
logfile = filepath.joinpath('malias2.log')
|
||||||
|
Path(filepath).mkdir(parents=True, exist_ok=True)
|
||||||
|
logging.basicConfig(filename=logfile,level=logging.INFO,format='%(message)s')
|
||||||
|
logging.getLogger("httpx").setLevel(logging.ERROR)
|
||||||
|
app_version = '2.0'
|
||||||
|
db_version = '2.0.0'
|
||||||
|
footer = 'malias version %s'%(app_version)
|
||||||
|
|
||||||
|
|
||||||
|
def unix_to_datetime(unix_timestamp):
|
||||||
|
return datetime.fromtimestamp(unix_timestamp)
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_release():
|
||||||
|
version = 'N/A'
|
||||||
|
req = httpx.get('https://gitlab.pm/api/v1/repos/rune/malias/releases/latest',
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
version = req.json()['tag_name']
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def release_check():
|
||||||
|
latest_release = get_latest_release()
|
||||||
|
if app_version != latest_release:
|
||||||
|
print('[b]New version available @ [i]https://iurl.no/malias[/b][/i]')
|
||||||
|
else:
|
||||||
|
print ('You have the the latest version. Version: %s' %(app_version))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def connect_database():
|
||||||
|
Path(filepath).mkdir(parents=True, exist_ok=True)
|
||||||
|
# noinspection PyShadowingNames
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(database)
|
||||||
|
except Error as e:
|
||||||
|
logging.error(time.strftime("%Y-%m-%d %H:%M") + ' - Error : ' + str(e))
|
||||||
|
print(e)
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('''CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
first_run INTEGER,
|
||||||
|
server TEXT,
|
||||||
|
apikey TEXT NOT NULL,
|
||||||
|
data_copy INTEGER
|
||||||
|
)''')
|
||||||
|
c.execute('''CREATE TABLE IF NOT EXISTS aliases
|
||||||
|
(id integer NOT NULL PRIMARY KEY,
|
||||||
|
alias text NOT NULL,
|
||||||
|
goto text NOT NULL,
|
||||||
|
created text NOT NULL)''')
|
||||||
|
c.execute('''CREATE TABLE IF NOT EXISTS timedaliases
|
||||||
|
(id integer NOT NULL PRIMARY KEY,
|
||||||
|
alias text NOT NULL,
|
||||||
|
goto text NOT NULL,
|
||||||
|
validity text NOT NULL)''')
|
||||||
|
c.execute('''CREATE TABLE IF NOT EXISTS dbversion
|
||||||
|
(version integer NOT NULL DEFAULT 0)''')
|
||||||
|
c.execute('''CREATE TABLE IF NOT EXISTS timedaliases
|
||||||
|
(id integer NOT NULL PRIMARY KEY,
|
||||||
|
alias text NOT NULL,
|
||||||
|
goto text NOT NULL,
|
||||||
|
validity text NOT NULL)''')
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
first_run(conn)
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def first_run(conn):
|
||||||
|
now = datetime.now().strftime("%m-%d-%Y %H:%M")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT count(*) FROM settings')
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
if count == 0:
|
||||||
|
logging.error(now + ' - First run!')
|
||||||
|
cursor.execute('INSERT INTO settings values(?,?,?,?,?)', (0, 1, 'dummy.server','DUMMY_KEY',0))
|
||||||
|
cursor.execute('INSERT INTO dbversion values(?)', (db_version,))
|
||||||
|
conn.commit()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_settings(kind):
|
||||||
|
now = datetime.now().strftime("%m-%d-%Y %H:%M")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT * FROM settings')
|
||||||
|
data = cursor.fetchall()
|
||||||
|
first_run_status = data[0][1] # pyright: ignore
|
||||||
|
server = data[0][2] # pyright: ignore
|
||||||
|
api_key = data[0][3] # pyright: ignore
|
||||||
|
copy_status = data[0][4] # pyright: ignore
|
||||||
|
if kind == 'connection':
|
||||||
|
if server == 'dummy.server' or api_key =='DUMMY_KEY':
|
||||||
|
print('Error: No mailcow server or API key registered. Please add with [b]malias -s [i]your.server APIKEY[/i][/b]')
|
||||||
|
exit(0)
|
||||||
|
else:
|
||||||
|
connection = {'key': api_key, 'server':server}
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/domain/0',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data=req.json()
|
||||||
|
code = str(req)
|
||||||
|
if code.find('200') != -1:
|
||||||
|
return connection # pyright: ignore
|
||||||
|
else:
|
||||||
|
logging.error(now + ' - Error : Server returned error: %s, ' %(data['msg']))
|
||||||
|
print('\n [b red]Error[/b red] : Server returned error [b]%s[/b]\n\n' %(data['msg']))
|
||||||
|
exit(0)
|
||||||
|
if kind == 'first_run_status':
|
||||||
|
return first_run_status
|
||||||
|
if kind == 'copy_status':
|
||||||
|
return copy_status
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_timed(username):
|
||||||
|
connection = get_settings('connection')
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/time_limited_aliases/%s' %username,
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
data.sort(key = itemgetter('validity'), reverse=True)
|
||||||
|
return(data[0])
|
||||||
|
|
||||||
|
|
||||||
|
def set_conection_info(server,apikey):
|
||||||
|
now = datetime.now().strftime("%m-%d-%Y %H:%M")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('UPDATE settings SET server = ?, apikey = ? WHERE id = 0',(server, apikey))
|
||||||
|
logging.info(now + ' - Info : Connectioninformations updated')
|
||||||
|
print('Your connection information has been updated.')
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def copy_data():
|
||||||
|
connection = get_settings('connection')
|
||||||
|
if get_settings('copy_status') == 0:
|
||||||
|
now = datetime.now().strftime("%m-%d-%Y %H:%M")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
if not data:
|
||||||
|
print('\n [b red]Error[/b red] : No aliases on server %s. Nothing to copy!\n\n' %(connection['server']))
|
||||||
|
exit(0)
|
||||||
|
i=0
|
||||||
|
for data in data:
|
||||||
|
cursor.execute('INSERT INTO aliases values(?,?,?,?)', (data['id'], data['address'],data['goto'],now))
|
||||||
|
i=i+1
|
||||||
|
cursor.execute('UPDATE settings SET data_copy = ? WHERE id = 0',(1,))
|
||||||
|
conn.commit()
|
||||||
|
logging.info(now + ' - Info : Imported %s new aliases from %s ' %(str(i),connection['server']))
|
||||||
|
print('\n[b]Info[/b] : %s aliases imported from the mailcow instance %s to local DB\n' %(i, connection['server']))
|
||||||
|
else:
|
||||||
|
print('\n[b]Info[/b] : aliases alreday imported from the mailcow instance %s to local DB\n\n[i]Updating with any missing aliases![/i]' %(connection['server']))
|
||||||
|
update_data()
|
||||||
|
|
||||||
|
|
||||||
|
def update_data():
|
||||||
|
connection = get_settings('connection')
|
||||||
|
if get_settings('copy_status') == 1:
|
||||||
|
now = datetime.now().strftime("%m-%d-%Y %H:%M")
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
i = 0
|
||||||
|
count_alias = 0
|
||||||
|
cursor = conn.cursor()
|
||||||
|
for data in data:
|
||||||
|
cursor.execute('SELECT count(*) FROM aliases where alias like ? and goto like ?', (data['address'],data['goto'],))
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
if count >= 1 :
|
||||||
|
i+=1
|
||||||
|
else:
|
||||||
|
cursor.execute('INSERT INTO aliases values(?,?,?,?)', (data['id'], data['address'],data['goto'],now))
|
||||||
|
count_alias+=1
|
||||||
|
i+=1
|
||||||
|
conn.commit()
|
||||||
|
if count_alias > 0:
|
||||||
|
logging.info(now + ' - Info : Local DB updated with %s new aliases from %s ' %(str(count_alias),connection['server']))
|
||||||
|
print('\n[b]Info[/b] : Local DB update with %s new aliases from %s \n' %(str(count_alias),connection['server']))
|
||||||
|
else:
|
||||||
|
print('\n[b]Info[/b] : No missing aliases from local DB \n')
|
||||||
|
|
||||||
|
|
||||||
|
def create(alias,to_address):
|
||||||
|
now = datetime.now().strftime("%m-%d-%Y %H:%M")
|
||||||
|
connection = get_settings('connection') # pyright: ignore
|
||||||
|
check = checklist(alias)
|
||||||
|
if check[0] == True:
|
||||||
|
logging.error(now + ' - Error : alias %s exists on the mailcow instance %s ' %(alias,connection['server']))
|
||||||
|
print('\n[b]Error[/b] : alias %s exists on the mailcow instance %s \n' %(alias,connection['server']))
|
||||||
|
exit(0)
|
||||||
|
elif check[1] == True:
|
||||||
|
logging.error(now + ' - Error : alias %s exists in local database.' %(alias))
|
||||||
|
print('\n[b]Error[/b] : alias %s exists in local database. \n' %(alias))
|
||||||
|
exit(0)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
new_data = {'address': alias,'goto': to_address,'active': "1"}
|
||||||
|
new_data = json.dumps(new_data)
|
||||||
|
req = httpx.post('https://'+connection['server']+'/api/v1/add/alias',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
},
|
||||||
|
data=new_data
|
||||||
|
)
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
print(f"Error while requesting {exc.request.url!r}.")
|
||||||
|
mail_id = alias_id(alias)
|
||||||
|
if mail_id == None:
|
||||||
|
logging.error(now + ' - Error : alias %s not created on the mailcow instance %s ' %(alias,connection['server']))
|
||||||
|
print('\n[b]Error[/b] : alias %s exists on the mailcow instance %s \n' %(alias,connection['server']))
|
||||||
|
else:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('INSERT INTO aliases values(?,?,?,?)', (mail_id, alias,to_address,now))
|
||||||
|
conn.commit()
|
||||||
|
logging.info(now + ' - Info : alias %s created for %s on the mailcow instance %s ' %(alias,to_address,connection['server']))
|
||||||
|
print('\n[b]Info[/b] : alias %s created for %s on the mailcow instance %s \n' %(alias,to_address,connection['server']))
|
||||||
|
|
||||||
|
|
||||||
|
def checklist(alias):
|
||||||
|
alias_e = None
|
||||||
|
alias_i = None
|
||||||
|
connection = get_settings('connection')
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
i = 0
|
||||||
|
for data in data:
|
||||||
|
if alias == data['address'] or alias in data['goto']:
|
||||||
|
alias_e = True
|
||||||
|
i=i+1
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT count(*) FROM aliases where alias == ? or goto == ?', (alias,alias,))
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
if count >= 1 :
|
||||||
|
alias_i = True
|
||||||
|
alias_exist = [alias_e,alias_i]
|
||||||
|
return alias_exist
|
||||||
|
|
||||||
|
|
||||||
|
def alias_id(alias):
|
||||||
|
connection = get_settings('connection')
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
i = 0
|
||||||
|
for data in data:
|
||||||
|
if data['address'] == alias:
|
||||||
|
return data['id']
|
||||||
|
i=i+1
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def number_of_aliases_in_db():
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT count(*) FROM aliases')
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def number_of_aliases_on_server():
|
||||||
|
connection = get_settings('connection')
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
|
||||||
|
def search(alias):
|
||||||
|
connection = get_settings('connection')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
search_term = '%'+alias+'%'
|
||||||
|
cursor.execute('SELECT alias,goto from aliases where alias like ? or goto like ?',(search_term,search_term,))
|
||||||
|
localdata = cursor.fetchall()
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
remotedata = req.json()
|
||||||
|
remote = []
|
||||||
|
i=0
|
||||||
|
for data in remotedata:
|
||||||
|
if alias in remotedata[i]['address'] or alias in remotedata[i]['goto']:
|
||||||
|
remote.append((remotedata[i]['address'], remotedata[i]['goto']))
|
||||||
|
i=i+1
|
||||||
|
finallist = localdata + list(set(remote) - set(localdata))
|
||||||
|
i = 0
|
||||||
|
print('\nAliases on %s containg search term [b]%s[/b]' %(connection['server'],alias))
|
||||||
|
print('=================================================================')
|
||||||
|
for data in finallist:
|
||||||
|
the_alias = finallist[i][0].ljust(20,' ')
|
||||||
|
print(the_alias + '\tgoes to\t\t' + finallist[i][1])
|
||||||
|
i=i+1
|
||||||
|
print('\n'+footer+'\n')
|
||||||
|
|
||||||
|
|
||||||
|
def get_mail_domains(info):
|
||||||
|
connection = get_settings('connection')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/domain/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
remoteData = req.json()
|
||||||
|
if info:
|
||||||
|
total_aliases = 0
|
||||||
|
i=0
|
||||||
|
print('\n[b]malias[/b] - All email domains on %s' %(connection['server']))
|
||||||
|
print('==================================================================')
|
||||||
|
for domains in remoteData:
|
||||||
|
cursor.execute('SELECT count(*) FROM aliases where alias like ? or goto like ?', ('%'+remoteData[i]['domain_name']+'%','%'+remoteData[i]['domain_name']+'%',))
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
total_aliases += count
|
||||||
|
print('%s \t\twith %s aliases' %(remoteData[i]['domain_name'],count))
|
||||||
|
i+=1
|
||||||
|
print('\n\nThere is a total of %s domains with %s aliases.\n%s' %(str(i),str(total_aliases),footer))
|
||||||
|
|
||||||
|
else:
|
||||||
|
return(remoteData)
|
||||||
|
|
||||||
|
|
||||||
|
def show_current_info():
|
||||||
|
connection = get_settings('connection')
|
||||||
|
latest_release = get_latest_release()
|
||||||
|
aliases_server = number_of_aliases_on_server()
|
||||||
|
alias_db = number_of_aliases_in_db()
|
||||||
|
mail_domains = get_mail_domains(False)
|
||||||
|
domain = ""
|
||||||
|
i=0
|
||||||
|
for domains in mail_domains:
|
||||||
|
if i!=0:
|
||||||
|
domain = domain + ', ' + str(mail_domains[i]['domain_name'])
|
||||||
|
else:
|
||||||
|
domain = domain + str(mail_domains[i]['domain_name'])
|
||||||
|
i+=1
|
||||||
|
print('\n[b]malias[/b] - Manage aliases on mailcow Instance.')
|
||||||
|
print('===================================================')
|
||||||
|
print('API key : [b]%s[/b]' % (connection['key']))
|
||||||
|
print('Mailcow Instance : [b]%s[/b]' % (connection['server']))
|
||||||
|
print('Active domains : [b]%s[/b]' % (domain))
|
||||||
|
print('Logfile : [b]%s[/b]' % (logfile))
|
||||||
|
print('Databse : [b]%s[b]' % (database))
|
||||||
|
print('Aliases on server : [b]%s[/b]' % (aliases_server))
|
||||||
|
print('Aliases in DB : [b]%s[/b]' % (alias_db))
|
||||||
|
print('')
|
||||||
|
if float(app_version) < float(latest_release):
|
||||||
|
print('App version : [b]%s[/b] a new version (%s) is available @ https://iurl.no/malias' % (app_version,latest_release))
|
||||||
|
else:
|
||||||
|
print('App version : [b]%s[/b]' % (app_version))
|
||||||
|
print('')
|
||||||
|
|
||||||
|
|
||||||
|
def delete_alias(alias):
|
||||||
|
status_e = None
|
||||||
|
status_i = None
|
||||||
|
now = datetime.now().strftime("%m-%d-%Y %H:%M")
|
||||||
|
connection = get_settings('connection')
|
||||||
|
check = checklist(alias)
|
||||||
|
if check[0] == None and check[1] == None:
|
||||||
|
print('\n[b]Error[/b] : The alias %s not found')
|
||||||
|
exit(0)
|
||||||
|
if check[0] or check[1] == True:
|
||||||
|
alias_server_id = alias_id(alias)
|
||||||
|
data = {'id': alias_server_id}
|
||||||
|
delete_data = json.dumps(data)
|
||||||
|
if check[0] == True and alias_server_id != None:
|
||||||
|
req = httpx.post('https://'+connection['server']+'/api/v1/delete/alias',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
},
|
||||||
|
data=delete_data
|
||||||
|
)
|
||||||
|
data=req.json()
|
||||||
|
code = str(req)
|
||||||
|
if code.find('200') != -1:
|
||||||
|
status_e = True
|
||||||
|
else:
|
||||||
|
status_e = None
|
||||||
|
if check[1] == True:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('DELETE from aliases where id = ?',(alias_server_id,))
|
||||||
|
conn.commit()
|
||||||
|
status_i = True
|
||||||
|
if status_e == True and status_i == True:
|
||||||
|
logging.info(now + ' - Info : alias %s deleted from the mailcow instance %s and Local DB' %(alias,connection['server']))
|
||||||
|
print('\n[b]Info[/b] : alias %s deleted from the mailcow instance %s and local DB' %(alias,connection['server']))
|
||||||
|
if status_e == True and status_i == None:
|
||||||
|
logging.info(now + ' - Info : alias %s deleted from the mailcow instance %s.' %(alias,connection['server']))
|
||||||
|
print('\n[b]Info[/b] : alias %s deleted from the mailcow instance %s.' %(alias,connection['server']))
|
||||||
|
if status_e == None and status_i == True:
|
||||||
|
logging.info(now + ' - Info : alias %s deleted from the local database.' %(alias))
|
||||||
|
print('\n[b]Info[/b] : alias %s deleted from the local database.' %(alias))
|
||||||
|
|
||||||
|
|
||||||
|
def create_timed(username,domain):
|
||||||
|
now = datetime.now().strftime("%m-%d-%Y %H:%M")
|
||||||
|
connection = get_settings('connection')
|
||||||
|
data = {'username': username,'domain': domain,'description': 'malias v'+app_version}
|
||||||
|
data_json = json.dumps(data)
|
||||||
|
req = httpx.post('https://'+connection['server']+'/api/v1/add/time_limited_alias',data=data_json,
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = json.loads(req.text)
|
||||||
|
if response[0]['type'] == 'danger' and response[0]['msg'] == 'domain_invalid':
|
||||||
|
logging.error(now + ' - Error : the domain %s does not exist.' %(domain))
|
||||||
|
print('[b][red]Error[/red][/b] : the domain %s does not exist.' %(domain))
|
||||||
|
exit(0)
|
||||||
|
if response[0]['type'] == 'danger' and response[0]['msg'] == 'access_denied':
|
||||||
|
logging.error(now + ' - Error : something went wrong. The server responded with access denied.')
|
||||||
|
print('[b][red]Error[/red][/b] : something went wrong. The server responded with [b]access denied[/b].')
|
||||||
|
exit(0)
|
||||||
|
alias = get_last_timed(username)
|
||||||
|
validity = unix_to_datetime(alias['validity'])
|
||||||
|
print('The timed alias %s was created. The alias is valid until %s UTC\n' %(alias['address'], validity))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('INSERT INTO timedaliases values(?,?,?,?)', (alias['validity'],alias['address'],username,validity))
|
||||||
|
conn.commit()
|
||||||
|
logging.info(now + ' - Info : timed alias %s created for %s and valid too %s UTC on the mailcow instance %s ' %(alias['address'],username,validity,connection['server']))
|
||||||
|
|
||||||
|
|
||||||
|
def check_local_db(alias_id):
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT count(*) FROM aliases where id = ?',(alias_id,))
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def list_alias():
|
||||||
|
now = datetime.now().strftime("%m-%d-%Y %H:%M")
|
||||||
|
connection = get_settings('connection')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
i = 0
|
||||||
|
l = 0
|
||||||
|
print('\n[b]malias[/b] - All aliases on %s ([b]*[/b] also in local db)' %(connection['server']))
|
||||||
|
print('==================================================================')
|
||||||
|
for search in data:
|
||||||
|
the_alias = data[i]['address'].ljust(20,' ')
|
||||||
|
the_goto = data[i]['goto'].ljust(20,' ')
|
||||||
|
cursor.execute('SELECT count(*) FROM aliases where alias like ? or goto like ?', (data[i]['address'],data[i]['address'],))
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
if count >= 1:
|
||||||
|
print(the_alias + '\tgoes to\t\t' + the_goto + '\t[b]*[/b]')
|
||||||
|
l=l+1
|
||||||
|
else:
|
||||||
|
print(the_alias + '\tgoes to\t\t' + the_goto)
|
||||||
|
i=i+1
|
||||||
|
print('\n\nTotal number of aliases %s on instance [b]%s[/b] and %s on [b]local DB[/b].' %(str(i),connection['server'],str(l)))
|
||||||
|
print('\n'+footer)
|
||||||
|
|
||||||
|
|
||||||
|
def export_data():
|
||||||
|
connection = get_settings('connection')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
with open("alias.json", "w") as outfile:
|
||||||
|
json.dump(data, outfile, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def list_timed_aliases(account):
|
||||||
|
connection = get_settings('connection')
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/time_limited_aliases/'+account,
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
i = 0
|
||||||
|
print('\n[b]malias[/b] - Timed aliases on %s for %s' %(connection['server'], account))
|
||||||
|
print('==========================================================================================================')
|
||||||
|
for data in data:
|
||||||
|
the_alias = data['address'].ljust(30,' ')
|
||||||
|
the_goto = data['goto'].ljust(20,' ')
|
||||||
|
the_validity = unix_to_datetime(data['validity'])
|
||||||
|
print(the_alias + '\tgoes to\t\t' + the_goto+'Valid to: '+str(the_validity))
|
||||||
|
i=i+1
|
||||||
|
#print('\n\nTotal number of timed aliases on instance [b]%s[/b] for account.' %(connection['server'],account))
|
||||||
|
print('\n'+footer)
|
||||||
|
|
||||||
|
|
||||||
|
# For Testing purposes
|
||||||
|
|
||||||
|
def list_all():
|
||||||
|
connection = get_settings('connection')
|
||||||
|
req = httpx.get('https://'+connection['server']+'/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
|
||||||
|
def updatedb():
|
||||||
|
# Function for updatimg DB when we have to
|
||||||
|
|
||||||
|
# 26.02.2025
|
||||||
|
# Placeholder for future updates and functions.
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
conn = connect_database()
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(prog='malias',
|
||||||
|
description='Malias is an application for adding, creating, and deleting aliases on a Mailcow instance. \n\nUse the issues section in the git repo for any problems or suggestions. https://gitlab.pm/rune/malias',
|
||||||
|
formatter_class=RawTextHelpFormatter,
|
||||||
|
epilog='Making mailcow easier...')
|
||||||
|
parser.add_argument('-c', '--copy', help='Copy alias data from mailcow server to local DB.\n\n',
|
||||||
|
required=False, action='store_true')
|
||||||
|
parser.add_argument('-s', '--set', help='Set connection information.\n\n',
|
||||||
|
nargs=2, metavar=('server', 'APIKey'), required=False, action="append")
|
||||||
|
parser.add_argument('-a', '--add', help='Add new alias.\n\n',
|
||||||
|
nargs=2, metavar=('alias@domain.com', 'to@domain.com'), required=False, action="append")
|
||||||
|
parser.add_argument('-f', '--find', help='Search for alias.\n\n',
|
||||||
|
nargs=1, metavar=('alias@domain.com'), required=False, action="append")
|
||||||
|
parser.add_argument('-d', '--delete', help='Delete alias.\n\n',
|
||||||
|
nargs=1, metavar=('alias@domain.com'), required=False, action="append")
|
||||||
|
parser.add_argument('-t', '--timed', help='Add new time limited alias for user on domain. \nThe user@domain.com is where you want the alias to be delivered to.\nThe domain.com is which domain to use when creating the timed-alias.\nOne year validity\n\n',
|
||||||
|
nargs=2, metavar=('user@domain.com', 'domain.com'), required=False, action="append")
|
||||||
|
parser.add_argument('-w', '--alias', help='List timed (temprary) aliases connected toone account.\n\n',
|
||||||
|
nargs=1, metavar=('alias@domain.com'), required=False, action="append")
|
||||||
|
parser.add_argument('-l', '--list', help='List all aliases on the Mailcow instance.\n\n',
|
||||||
|
required=False, action='store_true')
|
||||||
|
parser.add_argument('-o', '--domains', help='List all mail domains on the Mailcow instance.\n\n',
|
||||||
|
required=False, action='store_true')
|
||||||
|
parser.add_argument('-e', '--export', help='List all mail domains on the Mailcow instance.\n\n',
|
||||||
|
required=False, action='store_true')
|
||||||
|
parser.add_argument('-v', '--version', help='Show current version and information\n\n',
|
||||||
|
required=False, action='store_true')
|
||||||
|
|
||||||
|
|
||||||
|
args = vars(parser.parse_args())
|
||||||
|
|
||||||
|
if args['copy']:
|
||||||
|
copy_data()
|
||||||
|
elif args['set']:
|
||||||
|
set_conection_info(args['set'][0][0],args['set'][0][1])
|
||||||
|
elif args['add']:
|
||||||
|
create(args['add'][0][0],args['add'][0][1])
|
||||||
|
elif args['find']:
|
||||||
|
search(args['find'][0][0])
|
||||||
|
elif args['version']:
|
||||||
|
show_current_info()
|
||||||
|
elif args['delete']:
|
||||||
|
delete_alias(args['delete'][0][0])
|
||||||
|
elif args['timed']:
|
||||||
|
create_timed(args['timed'][0][0],args['timed'][0][1])
|
||||||
|
elif args['alias']:
|
||||||
|
list_timed_aliases(args['alias'][0][0])
|
||||||
|
elif args['list']:
|
||||||
|
list_alias()
|
||||||
|
elif args['domains']:
|
||||||
|
get_mail_domains(True)
|
||||||
|
elif args['export']:
|
||||||
|
export_data()
|
||||||
|
else:
|
||||||
|
print('\n\nEh, sorry! I need something more to help you! If you write [b]malias -h[/b] I\'ll show a help screen to get you going!!!\n\n\n')
|
||||||
|
|
||||||
386
malias_wrapper.py
Normal file
386
malias_wrapper.py
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
"""
|
||||||
|
Wrapper functions for malias to handle SQLite threading issues in Flask
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
import httpx
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
|
# Use project subfolder for database
|
||||||
|
project_dir = Path(__file__).parent
|
||||||
|
filepath = project_dir.joinpath('data')
|
||||||
|
database = filepath.joinpath('malias2.db')
|
||||||
|
logfile = filepath.joinpath('malias2.log')
|
||||||
|
|
||||||
|
def init_database():
|
||||||
|
"""Initialize database schema if it doesn't exist"""
|
||||||
|
Path(filepath).mkdir(parents=True, exist_ok=True)
|
||||||
|
conn = sqlite3.connect(database)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Create tables
|
||||||
|
cursor.execute('''CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
first_run INTEGER,
|
||||||
|
server TEXT,
|
||||||
|
apikey TEXT NOT NULL,
|
||||||
|
data_copy INTEGER
|
||||||
|
)''')
|
||||||
|
cursor.execute('''CREATE TABLE IF NOT EXISTS aliases
|
||||||
|
(id integer NOT NULL PRIMARY KEY,
|
||||||
|
alias text NOT NULL,
|
||||||
|
goto text NOT NULL,
|
||||||
|
created text NOT NULL)''')
|
||||||
|
cursor.execute('''CREATE TABLE IF NOT EXISTS timedaliases
|
||||||
|
(id integer NOT NULL PRIMARY KEY,
|
||||||
|
alias text NOT NULL,
|
||||||
|
goto text NOT NULL,
|
||||||
|
validity text NOT NULL)''')
|
||||||
|
cursor.execute('''CREATE TABLE IF NOT EXISTS dbversion
|
||||||
|
(version integer NOT NULL DEFAULT 0)''')
|
||||||
|
cursor.execute('''CREATE TABLE IF NOT EXISTS auth
|
||||||
|
(id integer NOT NULL PRIMARY KEY,
|
||||||
|
password_hash text NOT NULL)''')
|
||||||
|
|
||||||
|
# Check if settings exist, if not create default
|
||||||
|
cursor.execute('SELECT count(*) FROM settings')
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
if count == 0:
|
||||||
|
cursor.execute('INSERT INTO settings VALUES (?, ?, ?, ?, ?)',
|
||||||
|
(0, 1, 'dummy.server', 'DUMMY_KEY', 0))
|
||||||
|
|
||||||
|
# Check if auth table has password, if not create default
|
||||||
|
cursor.execute('SELECT count(*) FROM auth')
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
if count == 0:
|
||||||
|
# Default password is "admin123"
|
||||||
|
default_hash = bcrypt.hashpw("admin123".encode('utf-8'), bcrypt.gensalt())
|
||||||
|
cursor.execute('INSERT INTO auth VALUES (?, ?)', (0, default_hash.decode('utf-8')))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_db_connection():
|
||||||
|
"""Get a fresh database connection for the current thread"""
|
||||||
|
Path(filepath).mkdir(parents=True, exist_ok=True)
|
||||||
|
conn = sqlite3.connect(database)
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def get_settings_from_db():
|
||||||
|
"""Get Mailcow server and API key from database"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT server, apikey FROM settings WHERE id = 0')
|
||||||
|
data = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if data and data[0] != 'dummy.server' and data[1] != 'DUMMY_KEY':
|
||||||
|
return {'server': data[0], 'key': data[1]}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_connection_info(server, apikey):
|
||||||
|
"""Set Mailcow connection information"""
|
||||||
|
now = datetime.now().strftime("%m-%d-%Y %H:%M")
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('UPDATE settings SET server = ?, apikey = ? WHERE id = 0', (server, apikey))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_config():
|
||||||
|
"""Get current configuration"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT server, apikey FROM settings WHERE id = 0')
|
||||||
|
data = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if data:
|
||||||
|
return {
|
||||||
|
'mailcow_server': data[0] if data[0] != 'dummy.server' else '',
|
||||||
|
'mailcow_api_key': data[1] if data[1] != 'DUMMY_KEY' else ''
|
||||||
|
}
|
||||||
|
return {'mailcow_server': '', 'mailcow_api_key': ''}
|
||||||
|
|
||||||
|
def search_aliases(query):
|
||||||
|
"""Search for aliases in local database"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
search_term = '%' + query + '%'
|
||||||
|
cursor.execute('SELECT alias, goto FROM aliases WHERE alias LIKE ? OR goto LIKE ?',
|
||||||
|
(search_term, search_term))
|
||||||
|
results = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_number_of_aliases_on_server():
|
||||||
|
"""Get number of aliases from Mailcow server"""
|
||||||
|
connection = get_settings_from_db()
|
||||||
|
if not connection:
|
||||||
|
raise Exception("No Mailcow server configured")
|
||||||
|
|
||||||
|
req = httpx.get(f'https://{connection["server"]}/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def get_all_aliases(page=1, per_page=20):
|
||||||
|
"""Get all aliases with pagination from Mailcow server"""
|
||||||
|
connection = get_settings_from_db()
|
||||||
|
if not connection:
|
||||||
|
raise Exception("No Mailcow server configured")
|
||||||
|
|
||||||
|
req = httpx.get(f'https://{connection["server"]}/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
|
||||||
|
# Calculate pagination
|
||||||
|
total_aliases = len(data)
|
||||||
|
total_pages = (total_aliases + per_page - 1) // per_page # Ceiling division
|
||||||
|
start_idx = (page - 1) * per_page
|
||||||
|
end_idx = start_idx + per_page
|
||||||
|
|
||||||
|
# Get the page slice
|
||||||
|
page_data = data[start_idx:end_idx]
|
||||||
|
|
||||||
|
# Format aliases
|
||||||
|
aliases = []
|
||||||
|
for alias_data in page_data:
|
||||||
|
aliases.append({
|
||||||
|
'alias': alias_data['address'],
|
||||||
|
'goto': alias_data['goto'],
|
||||||
|
'active': alias_data.get('active', '1')
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'aliases': aliases,
|
||||||
|
'page': page,
|
||||||
|
'per_page': per_page,
|
||||||
|
'total': total_aliases,
|
||||||
|
'total_pages': total_pages,
|
||||||
|
'has_prev': page > 1,
|
||||||
|
'has_next': page < total_pages
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_domains():
|
||||||
|
"""Get all mail domains from Mailcow"""
|
||||||
|
connection = get_settings_from_db()
|
||||||
|
if not connection:
|
||||||
|
raise Exception("No Mailcow server configured")
|
||||||
|
|
||||||
|
req = httpx.get(f'https://{connection["server"]}/api/v1/get/domain/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return req.json()
|
||||||
|
|
||||||
|
def update_aliases():
|
||||||
|
"""Sync aliases from server to local DB"""
|
||||||
|
connection = get_settings_from_db()
|
||||||
|
if not connection:
|
||||||
|
raise Exception("No Mailcow server configured")
|
||||||
|
|
||||||
|
now = datetime.now().strftime("%m-%d-%Y %H:%M")
|
||||||
|
req = httpx.get(f'https://{connection["server"]}/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
count_alias = 0
|
||||||
|
for alias_data in data:
|
||||||
|
cursor.execute('SELECT count(*) FROM aliases WHERE alias LIKE ? AND goto LIKE ?',
|
||||||
|
(alias_data['address'], alias_data['goto']))
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
if count == 0:
|
||||||
|
cursor.execute('INSERT INTO aliases VALUES (?, ?, ?, ?)',
|
||||||
|
(alias_data['id'], alias_data['address'], alias_data['goto'], now))
|
||||||
|
count_alias += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return count_alias
|
||||||
|
|
||||||
|
def create_alias(alias, goto):
|
||||||
|
"""Create a new alias"""
|
||||||
|
connection = get_settings_from_db()
|
||||||
|
if not connection:
|
||||||
|
raise Exception("No Mailcow server configured")
|
||||||
|
|
||||||
|
now = datetime.now().strftime("%m-%d-%Y %H:%M")
|
||||||
|
|
||||||
|
# Check if alias exists
|
||||||
|
if check_alias_exists(alias):
|
||||||
|
raise Exception(f"Alias {alias} already exists")
|
||||||
|
|
||||||
|
# Create on server
|
||||||
|
new_data = {'address': alias, 'goto': goto, 'active': "1"}
|
||||||
|
new_data_json = json.dumps(new_data)
|
||||||
|
|
||||||
|
req = httpx.post(f'https://{connection["server"]}/api/v1/add/alias',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
},
|
||||||
|
data=new_data_json
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the new alias ID
|
||||||
|
alias_id_val = get_alias_id(alias)
|
||||||
|
|
||||||
|
if alias_id_val:
|
||||||
|
# Add to local DB
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('INSERT INTO aliases VALUES (?, ?, ?, ?)',
|
||||||
|
(alias_id_val, alias, goto, now))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to create alias on server")
|
||||||
|
|
||||||
|
def delete_alias(alias):
|
||||||
|
"""Delete an alias"""
|
||||||
|
connection = get_settings_from_db()
|
||||||
|
if not connection:
|
||||||
|
raise Exception("No Mailcow server configured")
|
||||||
|
|
||||||
|
alias_id_val = get_alias_id(alias)
|
||||||
|
if not alias_id_val:
|
||||||
|
raise Exception(f"Alias {alias} not found")
|
||||||
|
|
||||||
|
# Delete from server
|
||||||
|
delete_data = json.dumps({'id': alias_id_val})
|
||||||
|
req = httpx.post(f'https://{connection["server"]}/api/v1/delete/alias',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
},
|
||||||
|
data=delete_data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete from local DB
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('DELETE FROM aliases WHERE id = ?', (alias_id_val,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_timed_alias(username, domain):
|
||||||
|
"""Create a time-limited alias"""
|
||||||
|
connection = get_settings_from_db()
|
||||||
|
if not connection:
|
||||||
|
raise Exception("No Mailcow server configured")
|
||||||
|
|
||||||
|
data = {'username': username, 'domain': domain, 'description': 'malias web interface'}
|
||||||
|
data_json = json.dumps(data)
|
||||||
|
|
||||||
|
req = httpx.post(f'https://{connection["server"]}/api/v1/add/time_limited_alias',
|
||||||
|
data=data_json,
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = json.loads(req.text)
|
||||||
|
|
||||||
|
if response and len(response) > 0:
|
||||||
|
if response[0].get('type') == 'danger':
|
||||||
|
raise Exception(response[0].get('msg', 'Unknown error'))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_alias_exists(alias):
|
||||||
|
"""Check if alias exists on server or in local DB"""
|
||||||
|
connection = get_settings_from_db()
|
||||||
|
if not connection:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check server
|
||||||
|
req = httpx.get(f'https://{connection["server"]}/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
if alias == item['address'] or alias in item['goto']:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check local DB
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT count(*) FROM aliases WHERE alias = ? OR goto = ?', (alias, alias))
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return count >= 1
|
||||||
|
|
||||||
|
def get_alias_id(alias):
|
||||||
|
"""Get alias ID from server"""
|
||||||
|
connection = get_settings_from_db()
|
||||||
|
if not connection:
|
||||||
|
return None
|
||||||
|
|
||||||
|
req = httpx.get(f'https://{connection["server"]}/api/v1/get/alias/all',
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
'X-API-Key': connection['key']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = req.json()
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
if item['address'] == alias:
|
||||||
|
return item['id']
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Password management functions
|
||||||
|
def verify_password(password):
|
||||||
|
"""Verify password against stored hash"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT password_hash FROM auth WHERE id = 0')
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return False
|
||||||
|
|
||||||
|
stored_hash = result[0].encode('utf-8')
|
||||||
|
return bcrypt.checkpw(password.encode('utf-8'), stored_hash)
|
||||||
|
|
||||||
|
def change_password(old_password, new_password):
|
||||||
|
"""Change password - requires old password for verification"""
|
||||||
|
# Verify old password
|
||||||
|
if not verify_password(old_password):
|
||||||
|
raise Exception("Current password is incorrect")
|
||||||
|
|
||||||
|
# Hash new password
|
||||||
|
new_hash = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
|
||||||
|
|
||||||
|
# Update database
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('UPDATE auth SET password_hash = ? WHERE id = 0', (new_hash.decode('utf-8'),))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return True
|
||||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Flask==3.0.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
httpx==0.27.0
|
||||||
|
rich==13.7.0
|
||||||
|
bcrypt==4.1.2
|
||||||
750
templates/index.html
Normal file
750
templates/index.html
Normal file
@@ -0,0 +1,750 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Internal Web App</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
border-color: #505050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
background-color: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #0066cc;
|
||||||
|
border-color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #0052a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-config {
|
||||||
|
background-color: #cc6600;
|
||||||
|
border-color: #cc6600;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-config:hover {
|
||||||
|
background-color: #a35200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout {
|
||||||
|
background-color: #8b0000;
|
||||||
|
border-color: #8b0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout:hover {
|
||||||
|
background-color: #6b0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-bar h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-section {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section h2 {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
color: #b0b0b0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alias-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alias-table th {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 2px solid #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alias-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #404040;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alias-table tr:hover {
|
||||||
|
background-color: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination button:hover:not(:disabled) {
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-info {
|
||||||
|
color: #b0b0b0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal.active {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-content {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-content h2 {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header-bar">
|
||||||
|
<h1>Mailcow Alias Manager</h1>
|
||||||
|
<a href="/logout" class="btn btn-logout">Logout</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn" onclick="executeAction('list_aliases')">List All Aliases</button>
|
||||||
|
<button class="btn" onclick="executeAction('sync_aliases')">Sync Aliases</button>
|
||||||
|
<button class="btn" onclick="executeAction('get_domains')">Show Domains</button>
|
||||||
|
<button class="btn" onclick="openCreateAlias()">Create Alias</button>
|
||||||
|
<button class="btn" onclick="openDeleteAlias()">Delete Alias</button>
|
||||||
|
<button class="btn" onclick="openTimedAlias()">Create Timed Alias</button>
|
||||||
|
<button class="btn btn-config" onclick="openConfig()">Configuration</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-section">
|
||||||
|
<h2 style="margin-bottom: 15px; color: #ffffff;">Search Aliases</h2>
|
||||||
|
<div class="search-container">
|
||||||
|
<input type="text" class="search-input" id="searchInput" placeholder="Search for alias or destination email...">
|
||||||
|
<button class="btn btn-primary" onclick="performSearch()">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-section">
|
||||||
|
<h2>Results</h2>
|
||||||
|
<div class="result-content" id="resultContent">
|
||||||
|
Ready to execute actions or perform searches...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuration Modal -->
|
||||||
|
<div class="config-modal" id="configModal">
|
||||||
|
<div class="config-content">
|
||||||
|
<h2>Configuration</h2>
|
||||||
|
|
||||||
|
<h3 style="color: #ffffff; margin-top: 20px; margin-bottom: 10px; font-size: 16px;">Mailcow Settings</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mailcowServer">Mailcow Server (without https://)</label>
|
||||||
|
<input type="text" id="mailcowServer" placeholder="mail.example.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mailcowApiKey">Mailcow API Key</label>
|
||||||
|
<input type="text" id="mailcowApiKey" placeholder="Enter API key">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="color: #ffffff; margin-top: 30px; margin-bottom: 10px; font-size: 16px;">Change Password</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oldPassword">Current Password</label>
|
||||||
|
<input type="password" id="oldPassword" placeholder="Enter current password">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="newPassword">New Password</label>
|
||||||
|
<input type="password" id="newPassword" placeholder="Enter new password">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirmPassword">Confirm New Password</label>
|
||||||
|
<input type="password" id="confirmPassword" placeholder="Confirm new password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button class="btn" onclick="closeConfig()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="changePassword()">Change Password</button>
|
||||||
|
<button class="btn btn-primary" onclick="saveConfig()">Save Mailcow Config</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Alias Modal -->
|
||||||
|
<div class="config-modal" id="createAliasModal">
|
||||||
|
<div class="config-content">
|
||||||
|
<h2>Create New Alias</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="newAlias">Alias Email</label>
|
||||||
|
<input type="text" id="newAlias" placeholder="alias@example.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="aliasGoto">Destination Email</label>
|
||||||
|
<input type="text" id="aliasGoto" placeholder="destination@example.com">
|
||||||
|
</div>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button class="btn" onclick="closeCreateAlias()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="createAlias()">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Alias Modal -->
|
||||||
|
<div class="config-modal" id="deleteAliasModal">
|
||||||
|
<div class="config-content">
|
||||||
|
<h2>Delete Alias</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="deleteAliasEmail">Alias Email to Delete</label>
|
||||||
|
<input type="text" id="deleteAliasEmail" placeholder="alias@example.com">
|
||||||
|
</div>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button class="btn" onclick="closeDeleteAlias()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="deleteAlias()">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timed Alias Modal -->
|
||||||
|
<div class="config-modal" id="timedAliasModal">
|
||||||
|
<div class="config-content">
|
||||||
|
<h2>Create Timed Alias</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="timedUsername">Destination Email (user@domain.com)</label>
|
||||||
|
<input type="text" id="timedUsername" placeholder="user@example.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="timedDomain">Domain for New Alias</label>
|
||||||
|
<input type="text" id="timedDomain" placeholder="example.com">
|
||||||
|
</div>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button class="btn" onclick="closeTimedAlias()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="createTimedAlias()">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentPage = 1;
|
||||||
|
|
||||||
|
function showResult(message, isSuccess = true) {
|
||||||
|
const resultContent = document.getElementById('resultContent');
|
||||||
|
resultContent.innerHTML = `<span class="${isSuccess ? 'success' : 'error'}">${message}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAliasTable(data) {
|
||||||
|
const resultContent = document.getElementById('resultContent');
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div style="color: #4CAF50; margin-bottom: 15px;">
|
||||||
|
Showing ${data.aliases.length} of ${data.total} aliases (Page ${data.page} of ${data.total_pages})
|
||||||
|
</div>
|
||||||
|
<table class="alias-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Alias</th>
|
||||||
|
<th>Goes To</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
for (const alias of data.aliases) {
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>${alias.alias}</td>
|
||||||
|
<td>${alias.goto}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="pagination">
|
||||||
|
<button onclick="loadAliases(${data.page - 1})" ${!data.has_prev ? 'disabled' : ''}>Previous</button>
|
||||||
|
<span class="page-info">Page ${data.page} of ${data.total_pages}</span>
|
||||||
|
<button onclick="loadAliases(${data.page + 1})" ${!data.has_next ? 'disabled' : ''}>Next</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
resultContent.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAuth(response) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAliases(page = 1) {
|
||||||
|
currentPage = page;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/list_aliases', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ page: page })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkAuth(response)) return;
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.status === 'success') {
|
||||||
|
showAliasTable(result.data);
|
||||||
|
} else {
|
||||||
|
showResult(result.message, false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showResult(`Error loading aliases: ${error.message}`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeAction(actionName) {
|
||||||
|
if (actionName === 'list_aliases') {
|
||||||
|
loadAliases(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/${actionName}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkAuth(response)) return;
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
showResult(data.message, data.status === 'success');
|
||||||
|
} catch (error) {
|
||||||
|
showResult(`Error executing ${actionName}: ${error.message}`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performSearch() {
|
||||||
|
const query = document.getElementById('searchInput').value;
|
||||||
|
|
||||||
|
if (!query.trim()) {
|
||||||
|
showResult('Please enter a search query', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/search', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query: query })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkAuth(response)) return;
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
showResult(data.message, data.status === 'success');
|
||||||
|
} catch (error) {
|
||||||
|
showResult(`Search error: ${error.message}`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow search on Enter key
|
||||||
|
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function openConfig() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/config');
|
||||||
|
const config = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('mailcowServer').value = config.mailcow_server || '';
|
||||||
|
document.getElementById('mailcowApiKey').value = config.mailcow_api_key || '';
|
||||||
|
|
||||||
|
// Clear password fields
|
||||||
|
document.getElementById('oldPassword').value = '';
|
||||||
|
document.getElementById('newPassword').value = '';
|
||||||
|
document.getElementById('confirmPassword').value = '';
|
||||||
|
|
||||||
|
document.getElementById('configModal').classList.add('active');
|
||||||
|
} catch (error) {
|
||||||
|
showResult(`Error loading config: ${error.message}`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeConfig() {
|
||||||
|
document.getElementById('configModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
const mailcowServer = document.getElementById('mailcowServer').value;
|
||||||
|
const mailcowApiKey = document.getElementById('mailcowApiKey').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
mailcow_server: mailcowServer,
|
||||||
|
mailcow_api_key: mailcowApiKey
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkAuth(response)) return;
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
showResult(data.message, data.status === 'success');
|
||||||
|
if (data.status === 'success') {
|
||||||
|
closeConfig();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showResult(`Error saving config: ${error.message}`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changePassword() {
|
||||||
|
const oldPassword = document.getElementById('oldPassword').value;
|
||||||
|
const newPassword = document.getElementById('newPassword').value;
|
||||||
|
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||||
|
|
||||||
|
if (!oldPassword || !newPassword || !confirmPassword) {
|
||||||
|
showResult('All password fields are required', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/change_password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
old_password: oldPassword,
|
||||||
|
new_password: newPassword,
|
||||||
|
confirm_password: confirmPassword
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkAuth(response)) return;
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
showResult(data.message, data.status === 'success');
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Clear password fields on success
|
||||||
|
document.getElementById('oldPassword').value = '';
|
||||||
|
document.getElementById('newPassword').value = '';
|
||||||
|
document.getElementById('confirmPassword').value = '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showResult(`Error changing password: ${error.message}`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Alias Modal Functions
|
||||||
|
function openCreateAlias() {
|
||||||
|
document.getElementById('createAliasModal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCreateAlias() {
|
||||||
|
document.getElementById('createAliasModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAlias() {
|
||||||
|
const alias = document.getElementById('newAlias').value;
|
||||||
|
const goto = document.getElementById('aliasGoto').value;
|
||||||
|
|
||||||
|
if (!alias || !goto) {
|
||||||
|
showResult('Both alias and destination are required', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/create_alias', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ alias: alias, goto: goto })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
showResult(data.message, data.status === 'success');
|
||||||
|
if (data.status === 'success') {
|
||||||
|
document.getElementById('newAlias').value = '';
|
||||||
|
document.getElementById('aliasGoto').value = '';
|
||||||
|
closeCreateAlias();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showResult(`Error creating alias: ${error.message}`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete Alias Modal Functions
|
||||||
|
function openDeleteAlias() {
|
||||||
|
document.getElementById('deleteAliasModal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteAlias() {
|
||||||
|
document.getElementById('deleteAliasModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAlias() {
|
||||||
|
const alias = document.getElementById('deleteAliasEmail').value;
|
||||||
|
|
||||||
|
if (!alias) {
|
||||||
|
showResult('Alias email is required', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/delete_alias', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ alias: alias })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
showResult(data.message, data.status === 'success');
|
||||||
|
if (data.status === 'success') {
|
||||||
|
document.getElementById('deleteAliasEmail').value = '';
|
||||||
|
closeDeleteAlias();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showResult(`Error deleting alias: ${error.message}`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timed Alias Modal Functions
|
||||||
|
function openTimedAlias() {
|
||||||
|
document.getElementById('timedAliasModal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTimedAlias() {
|
||||||
|
document.getElementById('timedAliasModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTimedAlias() {
|
||||||
|
const username = document.getElementById('timedUsername').value;
|
||||||
|
const domain = document.getElementById('timedDomain').value;
|
||||||
|
|
||||||
|
if (!username || !domain) {
|
||||||
|
showResult('Both username and domain are required', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/create_timed_alias', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username: username, domain: domain })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
showResult(data.message, data.status === 'success');
|
||||||
|
if (data.status === 'success') {
|
||||||
|
document.getElementById('timedUsername').value = '';
|
||||||
|
document.getElementById('timedDomain').value = '';
|
||||||
|
closeTimedAlias();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showResult(`Error creating timed alias: ${error.message}`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modals when clicking outside
|
||||||
|
document.getElementById('configModal').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) closeConfig();
|
||||||
|
});
|
||||||
|
document.getElementById('createAliasModal').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) closeCreateAlias();
|
||||||
|
});
|
||||||
|
document.getElementById('deleteAliasModal').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) closeDeleteAlias();
|
||||||
|
});
|
||||||
|
document.getElementById('timedAliasModal').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) closeTimedAlias();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
159
templates/login.html
Normal file
159
templates/login.html
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - Mailcow Alias Manager</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #b0b0b0;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #0066cc;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #0052a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
background-color: #004080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #f44336;
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<h1>Mailcow Alias Manager</h1>
|
||||||
|
<p class="subtitle">Please enter your password to continue</p>
|
||||||
|
|
||||||
|
<form id="loginForm" onsubmit="handleLogin(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" placeholder="Enter password" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn">Login</button>
|
||||||
|
|
||||||
|
<div id="errorMessage" class="error-message"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function handleLogin(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const errorMessage = document.getElementById('errorMessage');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ password: password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
errorMessage.textContent = data.message;
|
||||||
|
errorMessage.classList.add('show');
|
||||||
|
document.getElementById('password').value = '';
|
||||||
|
document.getElementById('password').focus();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.textContent = 'Login error: ' + error.message;
|
||||||
|
errorMessage.classList.add('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user