email log

This commit is contained in:
2025-06-02 09:30:42 +02:00
parent 38e24a690a
commit 5a9b6be79d
4 changed files with 406 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session
from flask_login import current_user, login_required from flask_login import current_user, login_required
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate, Mail
from routes.auth import require_password_change from routes.auth import require_password_change
import os import os
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@@ -11,6 +11,8 @@ import sys
import time import time
from forms import CompanySettingsForm from forms import CompanySettingsForm
from utils import log_event, create_notification, get_unread_count from utils import log_event, create_notification, get_unread_count
from io import StringIO
import csv
# Set up logging to show in console # Set up logging to show in console
logging.basicConfig( logging.basicConfig(
@@ -1119,4 +1121,166 @@ def init_routes(main_bp):
}) })
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@main_bp.route('/settings/mails')
@login_required
def mails():
if not current_user.is_admin:
flash('Only administrators can access mail logs.', 'error')
return redirect(url_for('main.dashboard'))
# Get filter parameters
status = request.args.get('status')
date_range = request.args.get('date_range', '7d')
user_id = request.args.get('user_id')
page = request.args.get('page', 1, type=int)
per_page = 10
# Calculate date range
end_date = datetime.utcnow()
if date_range == '24h':
start_date = end_date - timedelta(days=1)
elif date_range == '7d':
start_date = end_date - timedelta(days=7)
elif date_range == '30d':
start_date = end_date - timedelta(days=30)
else:
start_date = None
# Build query
query = Mail.query
if status:
query = query.filter_by(status=status)
if start_date:
query = query.filter(Mail.created_at >= start_date)
if user_id:
query = query.filter(Mail.recipient == User.query.get(user_id).email)
# Get total count for pagination
total_mails = query.count()
total_pages = (total_mails + per_page - 1) // per_page
# Get paginated mails
mails = query.order_by(Mail.created_at.desc()).paginate(page=page, per_page=per_page)
# Get all users for filter dropdown
users = User.query.order_by(User.username).all()
# Check if this is an AJAX request
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return render_template('settings/tabs/mails.html',
mails=mails.items,
total_pages=total_pages,
current_page=page,
status=status,
date_range=date_range,
user_id=user_id,
users=users,
csrf_token=session.get('csrf_token'))
# For full page requests, render the full settings page
site_settings = SiteSettings.get_settings()
return render_template('settings/settings.html',
primary_color=site_settings.primary_color,
secondary_color=site_settings.secondary_color,
active_tab='mails',
site_settings=site_settings,
mails=mails.items,
total_pages=total_pages,
current_page=page,
users=users,
csrf_token=session.get('csrf_token'))
@main_bp.route('/settings/mails/<int:mail_id>')
@login_required
def get_mail_details(mail_id):
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
mail = Mail.query.get_or_404(mail_id)
return jsonify({
'id': mail.id,
'recipient': mail.recipient,
'subject': mail.subject,
'body': mail.body,
'status': mail.status,
'created_at': mail.created_at.isoformat(),
'sent_at': mail.sent_at.isoformat() if mail.sent_at else None,
'template': {
'id': mail.template.id,
'name': mail.template.name
} if mail.template else None
})
@main_bp.route('/settings/mails/download')
@login_required
def download_mails():
if not current_user.is_admin:
flash('Only administrators can download mail logs.', 'error')
return redirect(url_for('main.dashboard'))
# Get filter parameters
status = request.args.get('status')
date_range = request.args.get('date_range', '7d')
user_id = request.args.get('user_id')
# Calculate date range
end_date = datetime.utcnow()
if date_range == '24h':
start_date = end_date - timedelta(days=1)
elif date_range == '7d':
start_date = end_date - timedelta(days=7)
elif date_range == '30d':
start_date = end_date - timedelta(days=30)
else:
start_date = None
# Build query
query = Mail.query
if status:
query = query.filter_by(status=status)
if start_date:
query = query.filter(Mail.created_at >= start_date)
if user_id:
query = query.filter(Mail.recipient == User.query.get(user_id).email)
# Get all mails
mails = query.order_by(Mail.created_at.desc()).all()
# Create CSV
output = StringIO()
writer = csv.writer(output)
# Write header
writer.writerow([
'Created At',
'Recipient',
'Subject',
'Status',
'Template',
'Sent At'
])
# Write data
for mail in mails:
writer.writerow([
mail.created_at.strftime('%Y-%m-%d %H:%M:%S'),
mail.recipient,
mail.subject,
mail.status,
mail.template.name if mail.template else '-',
mail.sent_at.strftime('%Y-%m-%d %H:%M:%S') if mail.sent_at else '-'
])
output.seek(0)
return Response(
output,
mimetype='text/csv',
headers={
'Content-Disposition': f'attachment; filename=mail_log_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}.csv'
}
)

View File

@@ -6,6 +6,7 @@
{% from "settings/tabs/debugging.html" import debugging_tab %} {% from "settings/tabs/debugging.html" import debugging_tab %}
{% from "settings/tabs/events.html" import events_tab %} {% from "settings/tabs/events.html" import events_tab %}
{% from "settings/tabs/email_templates.html" import email_templates_tab %} {% from "settings/tabs/email_templates.html" import email_templates_tab %}
{% from "settings/tabs/mails.html" import mails_tab %}
{% from "settings/components/reset_colors_modal.html" import reset_colors_modal %} {% from "settings/components/reset_colors_modal.html" import reset_colors_modal %}
{% block title %}Settings - DocuPulse{% endblock %} {% block title %}Settings - DocuPulse{% endblock %}
@@ -43,6 +44,11 @@
<i class="fas fa-envelope me-2"></i>Email Templates <i class="fas fa-envelope me-2"></i>Email Templates
</button> </button>
</li> </li>
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'mails' %}active{% endif %}" id="mails-tab" data-bs-toggle="tab" data-bs-target="#mails" type="button" role="tab" aria-controls="mails" aria-selected="{{ 'true' if active_tab == 'mails' else 'false' }}">
<i class="fas fa-paper-plane me-2"></i>Mail Log
</button>
</li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'security' %}active{% endif %}" id="security-tab" data-bs-toggle="tab" data-bs-target="#security" type="button" role="tab" aria-controls="security" aria-selected="{{ 'true' if active_tab == 'security' else 'false' }}"> <button class="nav-link {% if active_tab == 'security' %}active{% endif %}" id="security-tab" data-bs-toggle="tab" data-bs-target="#security" type="button" role="tab" aria-controls="security" aria-selected="{{ 'true' if active_tab == 'security' else 'false' }}">
<i class="fas fa-shield-alt me-2"></i>Security <i class="fas fa-shield-alt me-2"></i>Security
@@ -77,6 +83,11 @@
{{ email_templates_tab(email_templates) }} {{ email_templates_tab(email_templates) }}
</div> </div>
<!-- Mails Tab -->
<div class="tab-pane fade {% if active_tab == 'mails' %}show active{% endif %}" id="mails" role="tabpanel" aria-labelledby="mails-tab">
{{ mails_tab(mails, csrf_token, users, total_pages, current_page) }}
</div>
<!-- Security Tab --> <!-- Security Tab -->
<div class="tab-pane fade {% if active_tab == 'security' %}show active{% endif %}" id="security" role="tabpanel" aria-labelledby="security-tab"> <div class="tab-pane fade {% if active_tab == 'security' %}show active{% endif %}" id="security" role="tabpanel" aria-labelledby="security-tab">
{{ security_tab() }} {{ security_tab() }}

View File

@@ -0,0 +1,229 @@
{% macro mails_tab(mails, csrf_token, users, total_pages, current_page) %}
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">Mail Log</h5>
<div class="d-flex gap-2">
<select class="form-select form-select-sm" id="statusFilter" onchange="updateFilters()">
<option value="">All Statuses</option>
<option value="pending">Pending</option>
<option value="sent">Sent</option>
<option value="failed">Failed</option>
</select>
<select class="form-select form-select-sm" id="dateRangeFilter" onchange="updateFilters()">
<option value="24h">Last 24 Hours</option>
<option value="7d" selected>Last 7 Days</option>
<option value="30d">Last 30 Days</option>
<option value="all">All Time</option>
</select>
<select class="form-select form-select-sm" id="userFilter" onchange="updateFilters()">
<option value="">All Recipients</option>
{% for user in users %}
<option value="{{ user.id }}">{{ user.username }} {{ user.last_name }}</option>
{% endfor %}
</select>
<button class="btn btn-secondary btn-sm" onclick="clearFilters()">
<i class="fas fa-times me-1"></i>Clear Filters
</button>
<button class="btn btn-primary btn-sm" onclick="downloadMailLog()">
<i class="fas fa-download me-1"></i>Download CSV
</button>
</div>
</div>
<!-- Mail Table -->
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Created At</th>
<th>Recipient</th>
<th>Subject</th>
<th>Status</th>
<th>Template</th>
<th>Sent At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for mail in mails %}
<tr>
<td>{{ mail.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>{{ mail.recipient }}</td>
<td>{{ mail.subject }}</td>
<td>
<span class="badge {% if mail.status == 'pending' %}bg-warning{% elif mail.status == 'sent' %}bg-success{% else %}bg-danger{% endif %}">
{{ mail.status }}
</span>
</td>
<td>{{ mail.template.name if mail.template else '-' }}</td>
<td>{{ mail.sent_at.strftime('%Y-%m-%d %H:%M:%S') if mail.sent_at else '-' }}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="viewMailDetails({{ mail.id }})">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if total_pages > 1 %}
<div class="d-flex justify-content-between align-items-center mt-4">
<div>
<button class="btn btn-outline-secondary" onclick="changePage({{ current_page - 1 }})" {% if current_page == 1 %}disabled{% endif %}>
<i class="fas fa-chevron-left me-2"></i>Previous
</button>
<span class="mx-3">Page {{ current_page }} of {{ total_pages }}</span>
<button class="btn btn-outline-secondary" onclick="changePage({{ current_page + 1 }})" {% if current_page == total_pages %}disabled{% endif %}>
Next<i class="fas fa-chevron-right ms-2"></i>
</button>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Mail Details Modal -->
<div class="modal fade" id="mailDetailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Mail Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="fw-bold">Subject:</label>
<p id="modalSubject"></p>
</div>
<div class="mb-3">
<label class="fw-bold">Recipient:</label>
<p id="modalRecipient"></p>
</div>
<div class="mb-3">
<label class="fw-bold">Status:</label>
<p id="modalStatus"></p>
</div>
<div class="mb-3">
<label class="fw-bold">Template:</label>
<p id="modalTemplate"></p>
</div>
<div class="mb-3">
<label class="fw-bold">Created At:</label>
<p id="modalCreatedAt"></p>
</div>
<div class="mb-3">
<label class="fw-bold">Sent At:</label>
<p id="modalSentAt"></p>
</div>
<div class="mb-3">
<label class="fw-bold">Body:</label>
<div id="modalBody" class="border p-3 bg-light"></div>
</div>
</div>
</div>
</div>
</div>
<script>
function updateFilters() {
const status = document.getElementById('statusFilter').value;
const dateRange = document.getElementById('dateRangeFilter').value;
const userId = document.getElementById('userFilter').value;
fetch(`/settings/mails?status=${status}&date_range=${dateRange}&user_id=${userId}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newTable = doc.querySelector('.table-responsive');
const newPagination = doc.querySelector('.d-flex.justify-content-between.align-items-center.mt-4');
if (newTable) {
document.querySelector('.table-responsive').innerHTML = newTable.innerHTML;
}
if (newPagination) {
const existingPagination = document.querySelector('.d-flex.justify-content-between.align-items-center.mt-4');
if (existingPagination) {
existingPagination.innerHTML = newPagination.innerHTML;
} else {
document.querySelector('.card-body').appendChild(newPagination);
}
}
});
}
function clearFilters() {
document.getElementById('statusFilter').value = '';
document.getElementById('dateRangeFilter').value = '7d';
document.getElementById('userFilter').value = '';
updateFilters();
}
function changePage(page) {
const status = document.getElementById('statusFilter').value;
const dateRange = document.getElementById('dateRangeFilter').value;
const userId = document.getElementById('userFilter').value;
fetch(`/settings/mails?status=${status}&date_range=${dateRange}&user_id=${userId}&page=${page}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newTable = doc.querySelector('.table-responsive');
const newPagination = doc.querySelector('.d-flex.justify-content-between.align-items-center.mt-4');
if (newTable) {
document.querySelector('.table-responsive').innerHTML = newTable.innerHTML;
}
if (newPagination) {
const existingPagination = document.querySelector('.d-flex.justify-content-between.align-items-center.mt-4');
if (existingPagination) {
existingPagination.innerHTML = newPagination.innerHTML;
} else {
document.querySelector('.card-body').appendChild(newPagination);
}
}
});
}
function viewMailDetails(mailId) {
fetch(`/settings/mails/${mailId}`)
.then(response => response.json())
.then(mail => {
document.getElementById('modalSubject').textContent = mail.subject;
document.getElementById('modalRecipient').textContent = mail.recipient;
document.getElementById('modalStatus').innerHTML = `
<span class="badge ${mail.status === 'pending' ? 'bg-warning' : mail.status === 'sent' ? 'bg-success' : 'bg-danger'}">
${mail.status}
</span>
`;
document.getElementById('modalTemplate').textContent = mail.template ? mail.template.name : '-';
document.getElementById('modalCreatedAt').textContent = new Date(mail.created_at).toLocaleString();
document.getElementById('modalSentAt').textContent = mail.sent_at ? new Date(mail.sent_at).toLocaleString() : '-';
document.getElementById('modalBody').innerHTML = mail.body;
new bootstrap.Modal(document.getElementById('mailDetailsModal')).show();
});
}
function downloadMailLog() {
const status = document.getElementById('statusFilter').value;
const dateRange = document.getElementById('dateRangeFilter').value;
const userId = document.getElementById('userFilter').value;
window.location.href = `/settings/mails/download?status=${status}&date_range=${dateRange}&user_id=${userId}`;
}
</script>
{% endmacro %}