Compare commits

1 Commits
1.0.3 ... 1.0.4

Author SHA1 Message Date
d444bb5fcb Updated and fixes. v1.0.4 2026-01-16 11:13:32 +01:00
8 changed files with 824 additions and 29 deletions

View File

@@ -17,11 +17,17 @@ COPY static/ static/
# Create data directory
RUN mkdir -p /app/data
# Copy entrypoint script
COPY docker-entrypoint.sh .
RUN chmod +x docker-entrypoint.sh
# Expose port
EXPOSE 5172
# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV FLASK_DEBUG=False
ENV FLASK_ENV=production
# Run the application
CMD ["python", "app.py"]
# Run the application via entrypoint
ENTRYPOINT ["./docker-entrypoint.sh"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Rune Olsen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

136
app.py
View File

@@ -2,6 +2,8 @@ from flask import Flask, render_template, request, jsonify, redirect, url_for, s
from functools import wraps
import malias_wrapper as malias_w
import os
import argparse
import sys
app = Flask(__name__)
app.secret_key = os.urandom(24) # Secret key for session management
@@ -123,6 +125,60 @@ def delete_alias_route():
except Exception as e:
return jsonify({'status': 'error', 'message': f"Error deleting alias: {str(e)}"})
@app.route('/delete_aliases_bulk', methods=['POST'])
@login_required
def delete_aliases_bulk():
"""Delete multiple aliases at once"""
try:
data = request.json
aliases = data.get('aliases', [])
if not aliases or not isinstance(aliases, list):
return jsonify({'status': 'error', 'message': 'Aliases array is required'})
if len(aliases) == 0:
return jsonify({'status': 'error', 'message': 'No aliases provided'})
# Delete multiple aliases
result = malias_w.delete_multiple_aliases(aliases)
# Build response message
success_count = result['success_count']
failed_count = result['failed_count']
total_count = success_count + failed_count
if failed_count == 0:
message = f"{success_count} of {total_count} aliases deleted successfully"
return jsonify({
'status': 'success',
'message': message,
'details': result
})
elif success_count == 0:
# All failed
failed_list = ', '.join([f['alias'] for f in result['failed_aliases'][:3]])
if len(result['failed_aliases']) > 3:
failed_list += f" and {len(result['failed_aliases']) - 3} more"
message = f"Failed to delete aliases: {failed_list}"
return jsonify({
'status': 'error',
'message': message,
'details': result
})
else:
# Partial success
failed_list = ', '.join([f['alias'] for f in result['failed_aliases'][:3]])
if len(result['failed_aliases']) > 3:
failed_list += f" and {len(result['failed_aliases']) - 3} more"
message = f"{success_count} of {total_count} aliases deleted successfully. Failed: {failed_list}"
return jsonify({
'status': 'partial',
'message': message,
'details': result
})
except Exception as e:
return jsonify({'status': 'error', 'message': f"Error deleting aliases: {str(e)}"})
@app.route('/create_timed_alias', methods=['POST'])
@login_required
def create_timed_alias():
@@ -209,4 +265,82 @@ def change_password_route():
return jsonify({'status': 'error', 'message': str(e)})
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5172)
# Parse command-line arguments
parser = argparse.ArgumentParser(
description='Mailcow Alias Manager - Web interface for managing mail aliases',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
python3 app.py Run in normal mode on port 5172
python3 app.py --debug Run in debug mode with auto-reload
python3 app.py --port 8080 Run on custom port 8080
python3 app.py --debug --port 8080 Run in debug mode on port 8080
python3 app.py --host 127.0.0.1 Bind to localhost only
Environment Variables (Docker):
FLASK_DEBUG Set to 'True' for debug mode (default: False)
FLASK_PORT Port to run on (default: 5172)
FLASK_HOST Host to bind to (default: 0.0.0.0)
FLASK_ENV Set to 'production' or 'development'
'''
)
parser.add_argument(
'--debug',
action='store_true',
help='Enable debug mode with auto-reload and detailed error messages'
)
parser.add_argument(
'--port',
type=int,
default=None,
help='Port to run on (default: 5172 or FLASK_PORT env var)'
)
parser.add_argument(
'--host',
type=str,
default=None,
help='Host to bind to (default: 0.0.0.0 or FLASK_HOST env var)'
)
args = parser.parse_args()
# Priority: Command-line args > Environment variables > Defaults
# Debug mode
if args.debug:
debug_mode = True
else:
debug_mode = os.getenv('FLASK_DEBUG', 'False').lower() in ('true', '1', 'yes')
# Port
if args.port is not None:
port = args.port
else:
port = int(os.getenv('FLASK_PORT', '5172'))
# Host
if args.host is not None:
host = args.host
else:
host = os.getenv('FLASK_HOST', '0.0.0.0')
# Print startup info
print('=' * 60)
print(' Mailcow Alias Manager - Starting...')
print('=' * 60)
print(f' Mode: {"DEBUG (Development)" if debug_mode else "NORMAL (Production)"}')
print(f' Host: {host}')
print(f' Port: {port}')
print(f' URL: http://{host}:{port}')
print('=' * 60)
if debug_mode:
print(' ⚠️ WARNING: Debug mode is enabled!')
print(' - Auto-reload on code changes')
print(' - Detailed error messages shown')
print(' - NOT suitable for production use')
print('=' * 60)
print()
app.run(debug=debug_mode, host=host, port=port)

View File

@@ -1,3 +1,14 @@
# Mailcow Alias Manager - Docker Compose Configuration
#
# Production vs Development Mode:
# --------------------------------
# FLASK_ENV=production + FLASK_DEBUG=False → Uses Gunicorn (4 workers, production-ready)
# FLASK_ENV=development or FLASK_DEBUG=True → Uses Flask dev server (auto-reload, debug info)
#
# Recommended Settings:
# - Production: FLASK_ENV=production, FLASK_DEBUG=False (default)
# - Development: FLASK_ENV=development, FLASK_DEBUG=True
services:
mailcow-alias-manager:
image: gitlab.pm/rune/malias-web:latest
@@ -8,4 +19,17 @@ services:
- ./data:/app/data
restart: unless-stopped
environment:
- TZ=Europe/Oslo
- TZ=Europe/Oslo
# Flask Configuration
# Set to 'production' for Gunicorn (recommended) or 'development' for Flask dev server
- FLASK_ENV=production
# Debug mode: False for production (recommended), True for development/troubleshooting
- FLASK_DEBUG=False
# Port configuration (default: 5172)
- FLASK_PORT=5172
# Host binding (default: 0.0.0.0 for Docker)
- FLASK_HOST=0.0.0.0

29
docker-entrypoint.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
set -e
# Get environment variables with defaults
FLASK_DEBUG="${FLASK_DEBUG:-False}"
FLASK_ENV="${FLASK_ENV:-production}"
FLASK_PORT="${FLASK_PORT:-5172}"
FLASK_HOST="${FLASK_HOST:-0.0.0.0}"
echo "Starting Mailcow Alias Manager..."
echo "Environment: $FLASK_ENV"
echo "Debug mode: $FLASK_DEBUG"
echo "Port: $FLASK_PORT"
# Check if we should run in production mode
if [ "$FLASK_ENV" = "production" ] && [ "$FLASK_DEBUG" != "True" ] && [ "$FLASK_DEBUG" != "true" ] && [ "$FLASK_DEBUG" != "1" ]; then
echo "Starting with Gunicorn (production mode)..."
exec gunicorn --bind $FLASK_HOST:$FLASK_PORT \
--workers 4 \
--threads 2 \
--timeout 120 \
--access-logfile - \
--error-logfile - \
--log-level info \
"app:app"
else
echo "Starting with Flask development server (debug mode)..."
exec python app.py
fi

View File

@@ -107,12 +107,12 @@ def get_config():
return {'mailcow_server': '', 'mailcow_api_key': ''}
def search_aliases(query):
"""Search for aliases in local database"""
"""Search for aliases in local database (matches from start of alias address only)"""
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))
search_term = query + '%' # Match from start only
cursor.execute('SELECT alias, goto FROM aliases WHERE alias LIKE ?',
(search_term,))
results = cursor.fetchall()
conn.close()
return results
@@ -384,3 +384,37 @@ def change_password(old_password, new_password):
conn.close()
return True
def delete_multiple_aliases(alias_list):
"""
Delete multiple aliases in one operation
Args:
alias_list: List of alias email addresses to delete
Returns:
Dictionary with:
- success_count: Number of successfully deleted aliases
- failed_count: Number of failed deletions
- failed_aliases: List of dicts with 'alias' and 'error' for each failure
"""
success_count = 0
failed_count = 0
failed_aliases = []
for alias in alias_list:
try:
delete_alias(alias)
success_count += 1
except Exception as e:
failed_count += 1
failed_aliases.append({
'alias': alias,
'error': str(e)
})
return {
'success_count': success_count,
'failed_count': failed_count,
'failed_aliases': failed_aliases
}

View File

@@ -3,3 +3,4 @@ python-dotenv==1.0.0
httpx==0.27.0
rich==13.7.0
bcrypt==4.1.2
gunicorn==21.2.0

View File

@@ -440,6 +440,94 @@
.form-group input {
font-size: 16px;
}
/* Delete modal responsive styles */
.delete-results-table {
font-size: 14px;
}
.delete-results-table thead {
display: none;
}
.delete-results-table tbody {
display: block;
}
.delete-results-table tr {
display: block;
margin-bottom: 15px;
background-color: #252525;
border-radius: 5px;
padding: 50px 15px 15px 15px;
border: 1px solid #404040;
position: relative;
}
.delete-results-table td {
display: block;
text-align: left;
padding: 6px 0;
border: none;
}
/* Checkbox in top-left corner */
.delete-results-table td:first-child {
position: absolute;
top: 15px;
left: 15px;
padding: 0;
}
.delete-checkbox {
width: 24px;
height: 24px;
}
/* Trash icon in top-right corner */
.delete-results-table td:last-child {
position: absolute;
top: 15px;
right: 15px;
padding: 0;
}
.delete-icon {
font-size: 24px;
}
/* Alias content with labels */
.delete-results-table td:nth-child(2)::before {
content: "Alias: ";
font-weight: bold;
color: #0066cc;
}
.delete-results-table td:nth-child(3)::before {
content: "Goes to: ";
font-weight: bold;
color: #0066cc;
}
/* Bulk controls stack vertically */
.bulk-controls {
flex-direction: column;
}
.bulk-controls .btn {
width: 100%;
}
/* Delete confirm modal */
.delete-confirm-list {
max-height: 200px;
}
/* Results warning */
.results-warning {
font-size: 13px;
padding: 8px;
}
}
/* Tablet Styles */
@@ -456,6 +544,25 @@
margin-left: 0;
flex: 1 1 100%;
}
/* Delete table on tablets - keep table format but adjust sizing */
.delete-results-table {
font-size: 14px;
}
.delete-results-table th,
.delete-results-table td {
padding: 10px 8px;
}
.delete-checkbox {
width: 22px;
height: 22px;
}
.delete-icon {
font-size: 22px;
}
}
/* Small Mobile Devices */
@@ -472,6 +579,147 @@
.result-section h2 {
font-size: 16px;
}
/* Delete modal adjustments for very small screens */
.config-content {
padding: 15px 12px;
}
.delete-results-table tr {
padding: 45px 12px 12px 12px;
}
.delete-results-table td:first-child {
top: 12px;
left: 12px;
}
.delete-results-table td:last-child {
top: 12px;
right: 12px;
}
.delete-confirm-list {
max-height: 150px;
font-size: 14px;
}
}
/* Delete Aliases Modal Specific Styles */
.delete-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #0066cc;
margin: 0;
}
.delete-icon {
cursor: pointer;
font-size: 20px;
transition: transform 0.2s, filter 0.2s;
display: inline-block;
user-select: none;
}
.delete-icon:hover {
transform: scale(1.3);
filter: brightness(1.4);
}
.delete-icon:active {
transform: scale(1.1);
}
.delete-results-table {
width: 100%;
border-collapse: collapse;
}
.delete-results-table th {
background-color: #1a1a1a;
color: #ffffff;
padding: 12px;
text-align: left;
border-bottom: 2px solid #404040;
font-weight: 600;
}
.delete-results-table td {
padding: 12px;
border-bottom: 1px solid #404040;
color: #e0e0e0;
}
.delete-table-row {
transition: background-color 0.2s;
}
.delete-table-row:hover {
background-color: #252525;
}
.delete-table-row.selected {
background-color: #1a3a5a !important;
}
.delete-table-row.selected:hover {
background-color: #244b6b !important;
}
.results-warning {
color: #ff9800;
font-size: 14px;
margin-top: 10px;
padding: 10px;
background-color: rgba(255, 152, 0, 0.1);
border-radius: 5px;
border-left: 3px solid #ff9800;
}
.delete-confirm-list {
margin: 15px 0;
padding: 15px;
background-color: #1a1a1a;
border-radius: 5px;
max-height: 300px;
overflow-y: auto;
}
.delete-confirm-list ul {
list-style: none;
padding: 0;
margin: 0;
}
.delete-confirm-list li {
padding: 8px 0;
border-bottom: 1px solid #404040;
color: #e0e0e0;
}
.delete-confirm-list li:last-child {
border-bottom: none;
}
.delete-confirm-list li::before {
content: "• ";
color: #f44336;
font-weight: bold;
margin-right: 8px;
}
/* Column widths for delete table */
.delete-results-table th:first-child,
.delete-results-table td:first-child {
width: 50px;
text-align: center;
}
.delete-results-table th:last-child,
.delete-results-table td:last-child {
width: 60px;
text-align: center;
}
</style>
</head>
@@ -487,7 +735,7 @@
<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="openDeleteAliasesModal()">Delete Aliases</button>
<button class="btn" onclick="openTimedAlias()">Create Timed Alias</button>
<button class="btn btn-config" onclick="openConfig()">Configuration</button>
</div>
@@ -495,7 +743,7 @@
<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...">
<input type="text" class="search-input" id="searchInput" placeholder="Start typing alias address (e.g., power...)">
<button class="btn btn-primary" onclick="performSearch()">Search</button>
</div>
</div>
@@ -564,17 +812,53 @@
</div>
</div>
<!-- Delete Alias Modal -->
<!-- Delete Aliases Modal -->
<div class="config-modal" id="deleteAliasModal">
<div class="config-content">
<h2>Delete Alias</h2>
<div class="config-content" style="max-width: 800px;">
<h2>Delete Aliases</h2>
<!-- Search Section -->
<div class="form-group">
<label for="deleteAliasEmail">Alias Email to Delete</label>
<input type="text" id="deleteAliasEmail" placeholder="alias@example.com">
<label for="deleteSearchInput">Search for aliases to delete</label>
<div class="search-container">
<input type="text" class="search-input" id="deleteSearchInput" placeholder="Start typing alias address (e.g., power...)">
<button class="btn btn-primary" onclick="searchAliasesForDeletion()">Search</button>
</div>
</div>
<!-- Results Section (initially hidden) -->
<div id="deleteResultsSection" style="display: none; margin-top: 20px;">
<div id="deleteResultsInfo" style="margin-bottom: 10px; color: #4CAF50;"></div>
<div id="deleteResultsTable" style="max-height: 400px; overflow-y: auto;"></div>
<!-- Bulk Selection Controls -->
<div class="bulk-controls" style="display: flex; gap: 10px; margin: 15px 0; justify-content: center;">
<button class="btn" onclick="selectAllDeleteAliases()">Select All</button>
<button class="btn" onclick="deselectAllDeleteAliases()">Deselect All</button>
</div>
</div>
<!-- Action Buttons -->
<div class="modal-buttons">
<button class="btn" onclick="closeDeleteAlias()">Cancel</button>
<button class="btn btn-primary" onclick="deleteAlias()">Delete</button>
<button class="btn" onclick="closeDeleteAliasesModal()">Cancel</button>
<button class="btn btn-primary" id="deleteSelectedBtn" onclick="confirmDeleteSelected()" disabled>
Delete Selected (0)
</button>
</div>
</div>
</div>
<!-- Confirmation Dialog -->
<div class="config-modal" id="deleteConfirmModal">
<div class="config-content">
<h2 style="color: #f44336;">⚠️ Confirm Deletion</h2>
<div id="deleteConfirmContent"></div>
<p style="color: #f44336; margin-top: 15px; font-weight: bold;">This action cannot be undone.</p>
<div class="modal-buttons">
<button class="btn" onclick="closeDeleteConfirm()">Cancel</button>
<button class="btn" style="background-color: #d32f2f; border-color: #d32f2f;" onclick="executeDeleteSelected()">
Yes, Delete All
</button>
</div>
</div>
</div>
@@ -862,23 +1146,258 @@
}
}
// Delete Alias Modal Functions
function openDeleteAlias() {
// Delete Aliases Modal Functions
let selectedAliases = new Set();
let currentDeleteResults = [];
function openDeleteAliasesModal() {
document.getElementById('deleteAliasModal').classList.add('active');
document.getElementById('deleteSearchInput').value = '';
document.getElementById('deleteResultsSection').style.display = 'none';
selectedAliases.clear();
currentDeleteResults = [];
updateDeleteButtonText();
}
function closeDeleteAlias() {
function closeDeleteAliasesModal() {
document.getElementById('deleteAliasModal').classList.remove('active');
selectedAliases.clear();
currentDeleteResults = [];
}
async function deleteAlias() {
const alias = document.getElementById('deleteAliasEmail').value;
if (!alias) {
showResult('Alias email is required', false);
async function searchAliasesForDeletion() {
const query = document.getElementById('deleteSearchInput').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();
if (data.status === 'success') {
// Parse search results
const results = [];
const lines = data.message.split('\n');
for (const line of lines) {
if (line.includes('→')) {
const parts = line.split('→').map(p => p.trim());
if (parts.length === 2) {
results.push({
alias: parts[0],
goto: parts[1]
});
}
}
}
// Limit to 100 results
const limited = results.slice(0, 100);
const hasMore = results.length > 100;
currentDeleteResults = limited;
renderDeleteResults(limited, hasMore, results.length);
} else {
document.getElementById('deleteResultsSection').style.display = 'none';
showResult(data.message, false);
}
} catch (error) {
showResult(`Search error: ${error.message}`, false);
}
}
function renderDeleteResults(results, hasMore, totalCount) {
if (results.length === 0) {
document.getElementById('deleteResultsSection').style.display = 'none';
showResult('No aliases found matching your search', false);
return;
}
document.getElementById('deleteResultsSection').style.display = 'block';
// Info text
let infoText = `Found ${results.length} alias(es)`;
if (hasMore) {
infoText += ` (showing first 100 of ${totalCount})`;
}
document.getElementById('deleteResultsInfo').innerHTML = infoText;
// Warning for too many results
let warningHtml = '';
if (hasMore) {
warningHtml = `<div class="results-warning">⚠️ Showing first 100 of ${totalCount} results. Please refine your search for more specific results.</div>`;
}
// Build table
let html = warningHtml + `
<table class="delete-results-table">
<thead>
<tr>
<th>Select</th>
<th>Alias</th>
<th>Goes To</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
`;
for (let i = 0; i < results.length; i++) {
const result = results[i];
const isSelected = selectedAliases.has(result.alias);
html += `
<tr class="delete-table-row ${isSelected ? 'selected' : ''}" id="delete-row-${i}">
<td>
<input type="checkbox"
class="delete-checkbox"
id="checkbox-${i}"
${isSelected ? 'checked' : ''}
onchange="toggleAliasSelection('${escapeHtml(result.alias)}', ${i})">
</td>
<td>${escapeHtml(result.alias)}</td>
<td>${escapeHtml(result.goto)}</td>
<td>
<span class="delete-icon" onclick="deleteSingleAliasQuick('${escapeHtml(result.alias)}', ${i})">🗑️</span>
</td>
</tr>
`;
}
html += '</tbody></table>';
document.getElementById('deleteResultsTable').innerHTML = html;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function toggleAliasSelection(alias, rowIndex) {
const row = document.getElementById(`delete-row-${rowIndex}`);
const checkbox = document.getElementById(`checkbox-${rowIndex}`);
if (selectedAliases.has(alias)) {
selectedAliases.delete(alias);
row.classList.remove('selected');
} else {
selectedAliases.add(alias);
row.classList.add('selected');
}
updateDeleteButtonText();
}
function selectAllDeleteAliases() {
selectedAliases.clear();
for (const result of currentDeleteResults) {
selectedAliases.add(result.alias);
}
// Update UI
const checkboxes = document.querySelectorAll('.delete-checkbox');
const rows = document.querySelectorAll('.delete-table-row');
checkboxes.forEach(cb => cb.checked = true);
rows.forEach(row => row.classList.add('selected'));
updateDeleteButtonText();
}
function deselectAllDeleteAliases() {
selectedAliases.clear();
// Update UI
const checkboxes = document.querySelectorAll('.delete-checkbox');
const rows = document.querySelectorAll('.delete-table-row');
checkboxes.forEach(cb => cb.checked = false);
rows.forEach(row => row.classList.remove('selected'));
updateDeleteButtonText();
}
function updateDeleteButtonText() {
const btn = document.getElementById('deleteSelectedBtn');
const count = selectedAliases.size;
btn.textContent = `Delete Selected (${count})`;
btn.disabled = count === 0;
}
function confirmDeleteSelected() {
if (selectedAliases.size === 0) return;
const aliasArray = Array.from(selectedAliases);
let confirmHtml = `
<p>You are about to delete <strong>${aliasArray.length}</strong> alias(es):</p>
<div class="delete-confirm-list">
<ul>
`;
for (const alias of aliasArray) {
confirmHtml += `<li>${escapeHtml(alias)}</li>`;
}
confirmHtml += '</ul></div>';
document.getElementById('deleteConfirmContent').innerHTML = confirmHtml;
document.getElementById('deleteConfirmModal').classList.add('active');
}
function closeDeleteConfirm() {
document.getElementById('deleteConfirmModal').classList.remove('active');
}
async function executeDeleteSelected() {
const aliasArray = Array.from(selectedAliases);
if (aliasArray.length === 0) return;
closeDeleteConfirm();
try {
const response = await fetch('/delete_aliases_bulk', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ aliases: aliasArray })
});
if (checkAuth(response)) return;
const data = await response.json();
showResult(data.message, data.status === 'success' || data.status === 'partial');
// Refresh search results
if (data.status === 'success' || data.status === 'partial') {
// Clear selection
selectedAliases.clear();
updateDeleteButtonText();
// Re-run search to show updated results
await searchAliasesForDeletion();
}
} catch (error) {
showResult(`Error deleting aliases: ${error.message}`, false);
}
}
async function deleteSingleAliasQuick(alias, rowIndex) {
if (!confirm(`Delete ${alias}?`)) {
return;
}
try {
const response = await fetch('/delete_alias', {
method: 'POST',
@@ -888,17 +1407,41 @@
body: JSON.stringify({ alias: alias })
});
if (checkAuth(response)) return;
const data = await response.json();
showResult(data.message, data.status === 'success');
if (data.status === 'success') {
document.getElementById('deleteAliasEmail').value = '';
closeDeleteAlias();
// Remove from selected if it was selected
selectedAliases.delete(alias);
// Remove from current results
currentDeleteResults = currentDeleteResults.filter(r => r.alias !== alias);
// Re-render table
renderDeleteResults(currentDeleteResults, false, currentDeleteResults.length);
showResult(`Alias ${alias} deleted successfully`, true);
} else {
showResult(data.message, false);
}
} catch (error) {
showResult(`Error deleting alias: ${error.message}`, false);
}
}
// Allow search on Enter key in delete modal
document.addEventListener('DOMContentLoaded', function() {
const deleteSearchInput = document.getElementById('deleteSearchInput');
if (deleteSearchInput) {
deleteSearchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
searchAliasesForDeletion();
}
});
}
});
// Timed Alias Modal Functions
function openTimedAlias() {
document.getElementById('timedAliasModal').classList.add('active');
@@ -946,7 +1489,10 @@
if (e.target === this) closeCreateAlias();
});
document.getElementById('deleteAliasModal').addEventListener('click', function(e) {
if (e.target === this) closeDeleteAlias();
if (e.target === this) closeDeleteAliasesModal();
});
document.getElementById('deleteConfirmModal').addEventListener('click', function(e) {
if (e.target === this) closeDeleteConfirm();
});
document.getElementById('timedAliasModal').addEventListener('click', function(e) {
if (e.target === this) closeTimedAlias();